diff --git a/api/api_test.go b/api/api_test.go deleted file mode 100644 index 8bf69e0de..000000000 --- a/api/api_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "fmt" - "net" - "net/http" - "testing" -) - -// testHTTPServer creates a test HTTP server that handles requests until -// the listener returned is closed. -func testHTTPServer(t *testing.T, handler http.Handler) (*Config, net.Listener) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("err: %s", err) - } - - server := &http.Server{Handler: handler} - go server.Serve(ln) - - config := DefaultConfig() - config.Address = fmt.Sprintf("http://%s", ln.Addr()) - - return config, ln -} diff --git a/api/auth/approle/approle_test.go b/api/auth/approle/approle_test.go deleted file mode 100644 index cdfb4e285..000000000 --- a/api/auth/approle/approle_test.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package approle - -import ( - "context" - "encoding/json" - "fmt" - "net" - "net/http" - "os" - "strings" - "testing" - - "github.com/hashicorp/vault/api" -) - -// testHTTPServer creates a test HTTP server that handles requests until -// the listener returned is closed. -func testHTTPServer( - t *testing.T, handler http.Handler, -) (*api.Config, net.Listener) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("err: %s", err) - } - - server := &http.Server{Handler: handler} - go server.Serve(ln) - - config := api.DefaultConfig() - config.Address = fmt.Sprintf("http://%s", ln.Addr()) - - return config, ln -} - -func init() { - os.Setenv("VAULT_TOKEN", "") -} - -func TestLogin(t *testing.T) { - secretIDEnvVar := "APPROLE_SECRET_ID" - allowedRoleID := "my-role-id" - allowedSecretID := "my-secret-id" - - content := []byte(allowedSecretID) - tmpfile, err := os.CreateTemp("", "file-containing-secret-id") - if err != nil { - t.Fatalf("error creating temp file: %v", err) - } - defer os.Remove(tmpfile.Name()) // clean up - err = os.Setenv(secretIDEnvVar, allowedSecretID) - if err != nil { - t.Fatalf("error writing secret ID to env var: %v", err) - } - - if _, err := tmpfile.Write(content); err != nil { - t.Fatalf("error writing to temp file: %v", err) - } - if err := tmpfile.Close(); err != nil { - t.Fatalf("error closing temp file: %v", err) - } - - // a response to return if the correct values were passed to login - authSecret := &api.Secret{ - Auth: &api.SecretAuth{ - ClientToken: "a-client-token", - }, - } - - authBytes, err := json.Marshal(authSecret) - if err != nil { - t.Fatalf("error marshaling json: %v", err) - } - - handler := func(w http.ResponseWriter, req *http.Request) { - payload := make(map[string]interface{}) - err := json.NewDecoder(req.Body).Decode(&payload) - if err != nil { - t.Fatalf("error decoding json: %v", err) - } - if payload["role_id"] == allowedRoleID && payload["secret_id"] == allowedSecretID { - w.Write(authBytes) - } - } - - config, ln := testHTTPServer(t, http.HandlerFunc(handler)) - defer ln.Close() - - config.Address = strings.ReplaceAll(config.Address, "127.0.0.1", "localhost") - client, err := api.NewClient(config) - if err != nil { - t.Fatalf("error initializing Vault client: %v", err) - } - - authFromFile, err := NewAppRoleAuth(allowedRoleID, &SecretID{FromFile: tmpfile.Name()}) - if err != nil { - t.Fatalf("error initializing AppRoleAuth with secret ID file: %v", err) - } - - loginRespFromFile, err := client.Auth().Login(context.TODO(), authFromFile) - if err != nil { - t.Fatalf("error logging in with secret ID from file: %v", err) - } - if loginRespFromFile.Auth == nil || loginRespFromFile.Auth.ClientToken == "" { - t.Fatalf("no authentication info returned by login") - } - - authFromEnv, err := NewAppRoleAuth(allowedRoleID, &SecretID{FromEnv: secretIDEnvVar}) - if err != nil { - t.Fatalf("error initializing AppRoleAuth with secret ID env var: %v", err) - } - - loginRespFromEnv, err := client.Auth().Login(context.TODO(), authFromEnv) - if err != nil { - t.Fatalf("error logging in with secret ID from env var: %v", err) - } - if loginRespFromEnv.Auth == nil || loginRespFromEnv.Auth.ClientToken == "" { - t.Fatalf("no authentication info returned by login with secret ID from env var") - } - - authFromStr, err := NewAppRoleAuth(allowedRoleID, &SecretID{FromString: allowedSecretID}) - if err != nil { - t.Fatalf("error initializing AppRoleAuth with secret ID string: %v", err) - } - - loginRespFromStr, err := client.Auth().Login(context.TODO(), authFromStr) - if err != nil { - t.Fatalf("error logging in with string: %v", err) - } - if loginRespFromStr.Auth == nil || loginRespFromStr.Auth.ClientToken == "" { - t.Fatalf("no authentication info returned by login with secret ID from string") - } -} diff --git a/api/auth/ldap/ldap_test.go b/api/auth/ldap/ldap_test.go deleted file mode 100644 index abdccb035..000000000 --- a/api/auth/ldap/ldap_test.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package ldap - -import ( - "context" - "encoding/json" - "fmt" - "net" - "net/http" - "os" - "strings" - "testing" - - "github.com/hashicorp/vault/api" -) - -// testHTTPServer creates a test HTTP server that handles requests until -// the listener returned is closed. -func testHTTPServer( - t *testing.T, handler http.Handler, -) (*api.Config, net.Listener) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("err: %s", err) - } - - server := &http.Server{Handler: handler} - go server.Serve(ln) - - config := api.DefaultConfig() - config.Address = fmt.Sprintf("http://%s", ln.Addr()) - - return config, ln -} - -func init() { - os.Setenv("VAULT_TOKEN", "") -} - -func TestLogin(t *testing.T) { - passwordEnvVar := "LDAP_PASSWORD" - allowedPassword := "6hrtL!*bro!ywbQbvDwW" - - content := []byte(allowedPassword) - tmpfile, err := os.CreateTemp("./", "file-containing-password") - if err != nil { - t.Fatalf("error creating temp file: %v", err) - } - defer os.Remove(tmpfile.Name()) // clean up - err = os.Setenv(passwordEnvVar, allowedPassword) - if err != nil { - t.Fatalf("error writing password to env var: %v", err) - } - - if _, err := tmpfile.Write(content); err != nil { - t.Fatalf("error writing to temp file: %v", err) - } - if err := tmpfile.Close(); err != nil { - t.Fatalf("error closing temp file: %v", err) - } - - // a response to return if the correct values were passed to login - authSecret := &api.Secret{ - Auth: &api.SecretAuth{ - ClientToken: "a-client-token", - }, - } - - authBytes, err := json.Marshal(authSecret) - if err != nil { - t.Fatalf("error marshaling json: %v", err) - } - - handler := func(w http.ResponseWriter, req *http.Request) { - payload := make(map[string]interface{}) - err := json.NewDecoder(req.Body).Decode(&payload) - if err != nil { - t.Fatalf("error decoding json: %v", err) - } - if payload["password"] == allowedPassword { - w.Write(authBytes) - } - } - - config, ln := testHTTPServer(t, http.HandlerFunc(handler)) - defer ln.Close() - - config.Address = strings.ReplaceAll(config.Address, "127.0.0.1", "localhost") - client, err := api.NewClient(config) - if err != nil { - t.Fatalf("error initializing Vault client: %v", err) - } - - // Password fromFile test - authFromFile, err := NewLDAPAuth("my-ldap-username", &Password{FromFile: tmpfile.Name()}) - if err != nil { - t.Fatalf("error initializing LDAPAuth with password file: %v", err) - } - - loginRespFromFile, err := client.Auth().Login(context.TODO(), authFromFile) - if err != nil { - t.Fatalf("error logging in with password from file: %v", err) - } - - if loginRespFromFile.Auth == nil || loginRespFromFile.Auth.ClientToken == "" { - t.Fatalf("no authentication info returned by login") - } - - // Password fromEnv Test - authFromEnv, err := NewLDAPAuth("my-ldap-username", &Password{FromEnv: passwordEnvVar}) - if err != nil { - t.Fatalf("error initializing LDAPAuth with password env var: %v", err) - } - - loginRespFromEnv, err := client.Auth().Login(context.TODO(), authFromEnv) - if err != nil { - t.Fatalf("error logging in with password from env var: %v", err) - } - - if loginRespFromEnv.Auth == nil || loginRespFromEnv.Auth.ClientToken == "" { - t.Fatalf("no authentication info returned by login with password from env var") - } - - // Password fromStr test - authFromStr, err := NewLDAPAuth("my-ldap-username", &Password{FromString: allowedPassword}) - if err != nil { - t.Fatalf("error initializing LDAPAuth with password string: %v", err) - } - - loginRespFromStr, err := client.Auth().Login(context.TODO(), authFromStr) - if err != nil { - t.Fatalf("error logging in with string: %v", err) - } - - if loginRespFromStr.Auth == nil || loginRespFromStr.Auth.ClientToken == "" { - t.Fatalf("no authentication info returned by login with password from string") - } - - // Empty User Test - _, err = NewLDAPAuth("", &Password{FromString: allowedPassword}) - if err.Error() != "no user name provided for login" { - t.Fatalf("Auth object created for empty username: %v", err) - } - - // Empty Password Test - _, err = NewLDAPAuth("my-ldap-username", nil) - if err.Error() != "no password provided for login" { - t.Fatalf("Auth object created when passing a nil Password struct: %v", err) - } - - // Auth with Custom MountPath - ldapMount := WithMountPath("customMount") - _, err = NewLDAPAuth("my-ldap-username", &Password{FromString: allowedPassword}, ldapMount) - if err != nil { - t.Fatalf("error initializing LDAPAuth with custom mountpath: %v", err) - } -} diff --git a/api/auth/userpass/userpass_test.go b/api/auth/userpass/userpass_test.go deleted file mode 100644 index 4fe68d8d4..000000000 --- a/api/auth/userpass/userpass_test.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package userpass - -import ( - "context" - "encoding/json" - "fmt" - "net" - "net/http" - "os" - "strings" - "testing" - - "github.com/hashicorp/vault/api" -) - -// testHTTPServer creates a test HTTP server that handles requests until -// the listener returned is closed. -func testHTTPServer( - t *testing.T, handler http.Handler, -) (*api.Config, net.Listener) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("err: %s", err) - } - - server := &http.Server{Handler: handler} - go server.Serve(ln) - - config := api.DefaultConfig() - config.Address = fmt.Sprintf("http://%s", ln.Addr()) - - return config, ln -} - -func init() { - os.Setenv("VAULT_TOKEN", "") -} - -func TestLogin(t *testing.T) { - passwordEnvVar := "USERPASS_PASSWORD" - allowedPassword := "my-password" - - content := []byte(allowedPassword) - tmpfile, err := os.CreateTemp("", "file-containing-password") - if err != nil { - t.Fatalf("error creating temp file: %v", err) - } - defer os.Remove(tmpfile.Name()) // clean up - err = os.Setenv(passwordEnvVar, allowedPassword) - if err != nil { - t.Fatalf("error writing password to env var: %v", err) - } - - if _, err := tmpfile.Write(content); err != nil { - t.Fatalf("error writing to temp file: %v", err) - } - if err := tmpfile.Close(); err != nil { - t.Fatalf("error closing temp file: %v", err) - } - - // a response to return if the correct values were passed to login - authSecret := &api.Secret{ - Auth: &api.SecretAuth{ - ClientToken: "a-client-token", - }, - } - - authBytes, err := json.Marshal(authSecret) - if err != nil { - t.Fatalf("error marshaling json: %v", err) - } - - handler := func(w http.ResponseWriter, req *http.Request) { - payload := make(map[string]interface{}) - err := json.NewDecoder(req.Body).Decode(&payload) - if err != nil { - t.Fatalf("error decoding json: %v", err) - } - if payload["password"] == allowedPassword { - w.Write(authBytes) - } - } - - config, ln := testHTTPServer(t, http.HandlerFunc(handler)) - defer ln.Close() - - config.Address = strings.ReplaceAll(config.Address, "127.0.0.1", "localhost") - client, err := api.NewClient(config) - if err != nil { - t.Fatalf("error initializing Vault client: %v", err) - } - - authFromFile, err := NewUserpassAuth("my-role-id", &Password{FromFile: tmpfile.Name()}) - if err != nil { - t.Fatalf("error initializing AppRoleAuth with password file: %v", err) - } - - loginRespFromFile, err := client.Auth().Login(context.TODO(), authFromFile) - if err != nil { - t.Fatalf("error logging in with password from file: %v", err) - } - if loginRespFromFile.Auth == nil || loginRespFromFile.Auth.ClientToken == "" { - t.Fatalf("no authentication info returned by login") - } - - authFromEnv, err := NewUserpassAuth("my-role-id", &Password{FromEnv: passwordEnvVar}) - if err != nil { - t.Fatalf("error initializing AppRoleAuth with password env var: %v", err) - } - - loginRespFromEnv, err := client.Auth().Login(context.TODO(), authFromEnv) - if err != nil { - t.Fatalf("error logging in with password from env var: %v", err) - } - if loginRespFromEnv.Auth == nil || loginRespFromEnv.Auth.ClientToken == "" { - t.Fatalf("no authentication info returned by login with password from env var") - } - - authFromStr, err := NewUserpassAuth("my-role-id", &Password{FromString: allowedPassword}) - if err != nil { - t.Fatalf("error initializing AppRoleAuth with password string: %v", err) - } - - loginRespFromStr, err := client.Auth().Login(context.TODO(), authFromStr) - if err != nil { - t.Fatalf("error logging in with string: %v", err) - } - if loginRespFromStr.Auth == nil || loginRespFromStr.Auth.ClientToken == "" { - t.Fatalf("no authentication info returned by login with password from string") - } -} diff --git a/api/auth_test.go b/api/auth_test.go deleted file mode 100644 index ca69630cc..000000000 --- a/api/auth_test.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "context" - "testing" -) - -type mockAuthMethod struct { - mockedSecret *Secret - mockedError error -} - -func (m *mockAuthMethod) Login(_ context.Context, _ *Client) (*Secret, error) { - return m.mockedSecret, m.mockedError -} - -func TestAuth_Login(t *testing.T) { - a := &Auth{ - c: &Client{}, - } - - m := mockAuthMethod{ - mockedSecret: &Secret{ - Auth: &SecretAuth{ - ClientToken: "a-client-token", - }, - }, - mockedError: nil, - } - - t.Run("Login should set token on success", func(t *testing.T) { - if a.c.Token() != "" { - t.Errorf("client token was %v expected to be unset", a.c.Token()) - } - - _, err := a.Login(context.Background(), &m) - if err != nil { - t.Errorf("Login() error = %v", err) - return - } - - if a.c.Token() != m.mockedSecret.Auth.ClientToken { - t.Errorf("client token was %v expected %v", a.c.Token(), m.mockedSecret.Auth.ClientToken) - return - } - }) -} - -func TestAuth_MFALoginSinglePhase(t *testing.T) { - t.Run("MFALogin() should succeed if credentials are passed in", func(t *testing.T) { - a := &Auth{ - c: &Client{}, - } - - m := mockAuthMethod{ - mockedSecret: &Secret{ - Auth: &SecretAuth{ - ClientToken: "a-client-token", - }, - }, - mockedError: nil, - } - - _, err := a.MFALogin(context.Background(), &m, "testMethod:testPasscode") - if err != nil { - t.Errorf("MFALogin() error %v", err) - return - } - if a.c.Token() != m.mockedSecret.Auth.ClientToken { - t.Errorf("client token was %v expected %v", a.c.Token(), m.mockedSecret.Auth.ClientToken) - return - } - }) -} - -func TestAuth_MFALoginTwoPhase(t *testing.T) { - tests := []struct { - name string - a *Auth - m *mockAuthMethod - creds *string - wantErr bool - }{ - { - name: "return MFARequirements", - a: &Auth{ - c: &Client{}, - }, - m: &mockAuthMethod{ - mockedSecret: &Secret{ - Auth: &SecretAuth{ - MFARequirement: &MFARequirement{ - MFARequestID: "a-req-id", - MFAConstraints: nil, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "error if no MFARequirements", - a: &Auth{ - c: &Client{}, - }, - m: &mockAuthMethod{ - mockedSecret: &Secret{ - Auth: &SecretAuth{}, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - secret, err := tt.a.MFALogin(context.Background(), tt.m) - if (err != nil) != tt.wantErr { - t.Errorf("MFALogin() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if secret.Auth.MFARequirement != tt.m.mockedSecret.Auth.MFARequirement { - t.Errorf("MFALogin() returned %v, expected %v", secret.Auth.MFARequirement, tt.m.mockedSecret.Auth.MFARequirement) - return - } - }) - } -} diff --git a/api/client_test.go b/api/client_test.go deleted file mode 100644 index 88a75d037..000000000 --- a/api/client_test.go +++ /dev/null @@ -1,1438 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "bytes" - "context" - "crypto/x509" - "encoding/base64" - "fmt" - "io" - "net/http" - "net/url" - "os" - "reflect" - "sort" - "strings" - "sync" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/hashicorp/go-hclog" -) - -func init() { - // Ensure our special envvars are not present - os.Setenv("VAULT_ADDR", "") - os.Setenv("VAULT_TOKEN", "") -} - -func TestDefaultConfig_envvar(t *testing.T) { - os.Setenv("VAULT_ADDR", "https://vault.mycompany.com") - defer os.Setenv("VAULT_ADDR", "") - - config := DefaultConfig() - if config.Address != "https://vault.mycompany.com" { - t.Fatalf("bad: %s", config.Address) - } - - os.Setenv("VAULT_TOKEN", "testing") - defer os.Setenv("VAULT_TOKEN", "") - - client, err := NewClient(config) - if err != nil { - t.Fatalf("err: %s", err) - } - - if token := client.Token(); token != "testing" { - t.Fatalf("bad: %s", token) - } -} - -func TestClientDefaultHttpClient(t *testing.T) { - _, err := NewClient(&Config{ - HttpClient: http.DefaultClient, - }) - if err != nil { - t.Fatal(err) - } -} - -func TestClientNilConfig(t *testing.T) { - client, err := NewClient(nil) - if err != nil { - t.Fatal(err) - } - if client == nil { - t.Fatal("expected a non-nil client") - } -} - -func TestClientDefaultHttpClient_unixSocket(t *testing.T) { - os.Setenv("VAULT_AGENT_ADDR", "unix:///var/run/vault.sock") - defer os.Setenv("VAULT_AGENT_ADDR", "") - - client, err := NewClient(nil) - if err != nil { - t.Fatal(err) - } - if client == nil { - t.Fatal("expected a non-nil client") - } - if client.addr.Scheme != "http" { - t.Fatalf("bad: %s", client.addr.Scheme) - } - if client.addr.Host != "localhost" { - t.Fatalf("bad: %s", client.addr.Host) - } -} - -func TestClientSetAddress(t *testing.T) { - client, err := NewClient(nil) - if err != nil { - t.Fatal(err) - } - // Start with TCP address using HTTP - if err := client.SetAddress("http://172.168.2.1:8300"); err != nil { - t.Fatal(err) - } - if client.addr.Host != "172.168.2.1:8300" { - t.Fatalf("bad: expected: '172.168.2.1:8300' actual: %q", client.addr.Host) - } - // Test switching to Unix Socket address from TCP address - if err := client.SetAddress("unix:///var/run/vault.sock"); err != nil { - t.Fatal(err) - } - if client.addr.Scheme != "http" { - t.Fatalf("bad: expected: 'http' actual: %q", client.addr.Scheme) - } - if client.addr.Host != "localhost" { - t.Fatalf("bad: expected: 'localhost' actual: %q", client.addr.Host) - } - if client.addr.Path != "" { - t.Fatalf("bad: expected '' actual: %q", client.addr.Path) - } - if client.config.HttpClient.Transport.(*http.Transport).DialContext == nil { - t.Fatal("bad: expected DialContext to not be nil") - } - // Test switching to TCP address from Unix Socket address - if err := client.SetAddress("http://172.168.2.1:8300"); err != nil { - t.Fatal(err) - } - if client.addr.Host != "172.168.2.1:8300" { - t.Fatalf("bad: expected: '172.168.2.1:8300' actual: %q", client.addr.Host) - } - if client.addr.Scheme != "http" { - t.Fatalf("bad: expected: 'http' actual: %q", client.addr.Scheme) - } -} - -func TestClientToken(t *testing.T) { - tokenValue := "foo" - handler := func(w http.ResponseWriter, req *http.Request) {} - - config, ln := testHTTPServer(t, http.HandlerFunc(handler)) - defer ln.Close() - - client, err := NewClient(config) - if err != nil { - t.Fatalf("err: %s", err) - } - - client.SetToken(tokenValue) - - // Verify the token is set - if v := client.Token(); v != tokenValue { - t.Fatalf("bad: %s", v) - } - - client.ClearToken() - - if v := client.Token(); v != "" { - t.Fatalf("bad: %s", v) - } -} - -func TestClientHostHeader(t *testing.T) { - handler := func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte(req.Host)) - } - config, ln := testHTTPServer(t, http.HandlerFunc(handler)) - defer ln.Close() - - config.Address = strings.ReplaceAll(config.Address, "127.0.0.1", "localhost") - client, err := NewClient(config) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Set the token manually - client.SetToken("foo") - - resp, err := client.RawRequest(client.NewRequest(http.MethodPut, "/")) - if err != nil { - t.Fatal(err) - } - - // Copy the response - var buf bytes.Buffer - io.Copy(&buf, resp.Body) - - // Verify we got the response from the primary - if buf.String() != strings.ReplaceAll(config.Address, "http://", "") { - t.Fatalf("Bad address: %s", buf.String()) - } -} - -func TestClientBadToken(t *testing.T) { - handler := func(w http.ResponseWriter, req *http.Request) {} - - config, ln := testHTTPServer(t, http.HandlerFunc(handler)) - defer ln.Close() - - client, err := NewClient(config) - if err != nil { - t.Fatalf("err: %s", err) - } - - client.SetToken("foo") - _, err = client.RawRequest(client.NewRequest(http.MethodPut, "/")) - if err != nil { - t.Fatal(err) - } - - client.SetToken("foo\u007f") - _, err = client.RawRequest(client.NewRequest(http.MethodPut, "/")) - if err == nil || !strings.Contains(err.Error(), "printable") { - t.Fatalf("expected error due to bad token") - } -} - -func TestClientDisableRedirects(t *testing.T) { - tests := map[string]struct { - statusCode int - expectedNumReqs int - disableRedirects bool - }{ - "Disabled redirects: Moved permanently": {statusCode: 301, expectedNumReqs: 1, disableRedirects: true}, - "Disabled redirects: Found": {statusCode: 302, expectedNumReqs: 1, disableRedirects: true}, - "Disabled redirects: Temporary Redirect": {statusCode: 307, expectedNumReqs: 1, disableRedirects: true}, - "Enable redirects: Moved permanently": {statusCode: 301, expectedNumReqs: 2, disableRedirects: false}, - } - - for name, tc := range tests { - test := tc - name := name - t.Run(name, func(t *testing.T) { - t.Parallel() - numReqs := 0 - var config *Config - - respFunc := func(w http.ResponseWriter, req *http.Request) { - // Track how many requests the server has handled - numReqs++ - // Send back the relevant status code and generate a location - w.Header().Set("Location", fmt.Sprintf(config.Address+"/reqs/%v", numReqs)) - w.WriteHeader(test.statusCode) - } - - config, ln := testHTTPServer(t, http.HandlerFunc(respFunc)) - config.DisableRedirects = test.disableRedirects - defer ln.Close() - - client, err := NewClient(config) - if err != nil { - t.Fatalf("%s: error %v", name, err) - } - - req := client.NewRequest("GET", "/") - resp, err := client.rawRequestWithContext(context.Background(), req) - if err != nil { - t.Fatalf("%s: error %v", name, err) - } - - if numReqs != test.expectedNumReqs { - t.Fatalf("%s: expected %v request(s) but got %v", name, test.expectedNumReqs, numReqs) - } - - if resp.StatusCode != test.statusCode { - t.Fatalf("%s: expected status code %v got %v", name, test.statusCode, resp.StatusCode) - } - - location, err := resp.Location() - if err != nil { - t.Fatalf("%s error %v", name, err) - } - if req.URL.String() == location.String() { - t.Fatalf("%s: expected request URL %v to be different from redirect URL %v", name, req.URL, resp.Request.URL) - } - }) - } -} - -func TestClientRedirect(t *testing.T) { - primary := func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte("test")) - } - config, ln := testHTTPServer(t, http.HandlerFunc(primary)) - defer ln.Close() - - standby := func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Location", config.Address) - w.WriteHeader(307) - } - config2, ln2 := testHTTPServer(t, http.HandlerFunc(standby)) - defer ln2.Close() - - client, err := NewClient(config2) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Set the token manually - client.SetToken("foo") - - // Do a raw "/" request - resp, err := client.RawRequest(client.NewRequest(http.MethodPut, "/")) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Copy the response - var buf bytes.Buffer - io.Copy(&buf, resp.Body) - - // Verify we got the response from the primary - if buf.String() != "test" { - t.Fatalf("Bad: %s", buf.String()) - } -} - -func TestDefaulRetryPolicy(t *testing.T) { - cases := map[string]struct { - resp *http.Response - err error - expect bool - expectErr error - }{ - "retry on error": { - err: fmt.Errorf("error"), - expect: true, - }, - "don't retry connection failures": { - err: &url.Error{ - Err: x509.UnknownAuthorityError{}, - }, - }, - "don't retry on 200": { - resp: &http.Response{ - StatusCode: http.StatusOK, - }, - }, - "don't retry on 4xx": { - resp: &http.Response{ - StatusCode: http.StatusBadRequest, - }, - }, - "don't retry on 501": { - resp: &http.Response{ - StatusCode: http.StatusNotImplemented, - }, - }, - "retry on 500": { - resp: &http.Response{ - StatusCode: http.StatusInternalServerError, - }, - expect: true, - }, - "retry on 5xx": { - resp: &http.Response{ - StatusCode: http.StatusGatewayTimeout, - }, - expect: true, - }, - } - - for name, test := range cases { - t.Run(name, func(t *testing.T) { - retry, err := DefaultRetryPolicy(context.Background(), test.resp, test.err) - if retry != test.expect { - t.Fatalf("expected to retry request: '%t', but actual result was: '%t'", test.expect, retry) - } - if err != test.expectErr { - t.Fatalf("expected error from retry policy: %q, but actual result was: %q", err, test.expectErr) - } - }) - } -} - -func TestClientEnvSettings(t *testing.T) { - cwd, _ := os.Getwd() - - caCertBytes, err := os.ReadFile(cwd + "/test-fixtures/keys/cert.pem") - if err != nil { - t.Fatalf("error reading %q cert file: %v", cwd+"/test-fixtures/keys/cert.pem", err) - } - - oldCACert := os.Getenv(EnvVaultCACert) - oldCACertBytes := os.Getenv(EnvVaultCACertBytes) - oldCAPath := os.Getenv(EnvVaultCAPath) - oldClientCert := os.Getenv(EnvVaultClientCert) - oldClientKey := os.Getenv(EnvVaultClientKey) - oldSkipVerify := os.Getenv(EnvVaultSkipVerify) - oldMaxRetries := os.Getenv(EnvVaultMaxRetries) - oldDisableRedirects := os.Getenv(EnvVaultDisableRedirects) - - os.Setenv(EnvVaultCACert, cwd+"/test-fixtures/keys/cert.pem") - os.Setenv(EnvVaultCACertBytes, string(caCertBytes)) - os.Setenv(EnvVaultCAPath, cwd+"/test-fixtures/keys") - os.Setenv(EnvVaultClientCert, cwd+"/test-fixtures/keys/cert.pem") - os.Setenv(EnvVaultClientKey, cwd+"/test-fixtures/keys/key.pem") - os.Setenv(EnvVaultSkipVerify, "true") - os.Setenv(EnvVaultMaxRetries, "5") - os.Setenv(EnvVaultDisableRedirects, "true") - - defer func() { - os.Setenv(EnvVaultCACert, oldCACert) - os.Setenv(EnvVaultCACertBytes, oldCACertBytes) - os.Setenv(EnvVaultCAPath, oldCAPath) - os.Setenv(EnvVaultClientCert, oldClientCert) - os.Setenv(EnvVaultClientKey, oldClientKey) - os.Setenv(EnvVaultSkipVerify, oldSkipVerify) - os.Setenv(EnvVaultMaxRetries, oldMaxRetries) - os.Setenv(EnvVaultDisableRedirects, oldDisableRedirects) - }() - - config := DefaultConfig() - if err := config.ReadEnvironment(); err != nil { - t.Fatalf("error reading environment: %v", err) - } - - tlsConfig := config.HttpClient.Transport.(*http.Transport).TLSClientConfig - if len(tlsConfig.RootCAs.Subjects()) == 0 { - t.Fatalf("bad: expected a cert pool with at least one subject") - } - if tlsConfig.GetClientCertificate == nil { - t.Fatalf("bad: expected client tls config to have a certificate getter") - } - if tlsConfig.InsecureSkipVerify != true { - t.Fatalf("bad: %v", tlsConfig.InsecureSkipVerify) - } - if config.DisableRedirects != true { - t.Fatalf("bad: expected disable redirects to be true: %v", config.DisableRedirects) - } -} - -func TestClientDeprecatedEnvSettings(t *testing.T) { - oldInsecure := os.Getenv(EnvVaultInsecure) - os.Setenv(EnvVaultInsecure, "true") - defer os.Setenv(EnvVaultInsecure, oldInsecure) - - config := DefaultConfig() - if err := config.ReadEnvironment(); err != nil { - t.Fatalf("error reading environment: %v", err) - } - - tlsConfig := config.HttpClient.Transport.(*http.Transport).TLSClientConfig - if tlsConfig.InsecureSkipVerify != true { - t.Fatalf("bad: %v", tlsConfig.InsecureSkipVerify) - } -} - -func TestClientEnvNamespace(t *testing.T) { - var seenNamespace string - handler := func(w http.ResponseWriter, req *http.Request) { - seenNamespace = req.Header.Get(NamespaceHeaderName) - } - config, ln := testHTTPServer(t, http.HandlerFunc(handler)) - defer ln.Close() - - oldVaultNamespace := os.Getenv(EnvVaultNamespace) - defer os.Setenv(EnvVaultNamespace, oldVaultNamespace) - os.Setenv(EnvVaultNamespace, "test") - - client, err := NewClient(config) - if err != nil { - t.Fatalf("err: %s", err) - } - - _, err = client.RawRequest(client.NewRequest(http.MethodGet, "/")) - if err != nil { - t.Fatalf("err: %s", err) - } - - if seenNamespace != "test" { - t.Fatalf("Bad: %s", seenNamespace) - } -} - -func TestParsingRateAndBurst(t *testing.T) { - var ( - correctFormat = "400:400" - observedRate, observedBurst, err = parseRateLimit(correctFormat) - expectedRate, expectedBurst = float64(400), 400 - ) - if err != nil { - t.Error(err) - } - if expectedRate != observedRate { - t.Errorf("Expected rate %v but found %v", expectedRate, observedRate) - } - if expectedBurst != observedBurst { - t.Errorf("Expected burst %v but found %v", expectedBurst, observedBurst) - } -} - -func TestParsingRateOnly(t *testing.T) { - var ( - correctFormat = "400" - observedRate, observedBurst, err = parseRateLimit(correctFormat) - expectedRate, expectedBurst = float64(400), 400 - ) - if err != nil { - t.Error(err) - } - if expectedRate != observedRate { - t.Errorf("Expected rate %v but found %v", expectedRate, observedRate) - } - if expectedBurst != observedBurst { - t.Errorf("Expected burst %v but found %v", expectedBurst, observedBurst) - } -} - -func TestParsingErrorCase(t *testing.T) { - incorrectFormat := "foobar" - _, _, err := parseRateLimit(incorrectFormat) - if err == nil { - t.Error("Expected error, found no error") - } -} - -func TestClientTimeoutSetting(t *testing.T) { - oldClientTimeout := os.Getenv(EnvVaultClientTimeout) - os.Setenv(EnvVaultClientTimeout, "10") - defer os.Setenv(EnvVaultClientTimeout, oldClientTimeout) - config := DefaultConfig() - config.ReadEnvironment() - _, err := NewClient(config) - if err != nil { - t.Fatal(err) - } -} - -type roundTripperFunc func(*http.Request) (*http.Response, error) - -func (rt roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { - return rt(r) -} - -func TestClientNonTransportRoundTripper(t *testing.T) { - client := &http.Client{ - Transport: roundTripperFunc(http.DefaultTransport.RoundTrip), - } - - _, err := NewClient(&Config{ - HttpClient: client, - }) - if err != nil { - t.Fatal(err) - } -} - -func TestClientNonTransportRoundTripperUnixAddress(t *testing.T) { - client := &http.Client{ - Transport: roundTripperFunc(http.DefaultTransport.RoundTrip), - } - - _, err := NewClient(&Config{ - HttpClient: client, - Address: "unix:///var/run/vault.sock", - }) - if err == nil { - t.Fatal("bad: expected error got nil") - } -} - -func TestClone(t *testing.T) { - type fields struct{} - tests := []struct { - name string - config *Config - headers *http.Header - token string - }{ - { - name: "default", - config: DefaultConfig(), - }, - { - name: "cloneHeaders", - config: &Config{ - CloneHeaders: true, - }, - headers: &http.Header{ - "X-foo": []string{"bar"}, - "X-baz": []string{"qux"}, - }, - }, - { - name: "preventStaleReads", - config: &Config{ - ReadYourWrites: true, - }, - }, - { - name: "cloneToken", - config: &Config{ - CloneToken: true, - }, - token: "cloneToken", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parent, err := NewClient(tt.config) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Set all of the things that we provide setter methods for, which modify config values - err = parent.SetAddress("http://example.com:8080") - if err != nil { - t.Fatalf("SetAddress failed: %v", err) - } - - clientTimeout := time.Until(time.Now().AddDate(0, 0, 1)) - parent.SetClientTimeout(clientTimeout) - - checkRetry := func(ctx context.Context, resp *http.Response, err error) (bool, error) { - return true, nil - } - parent.SetCheckRetry(checkRetry) - - parent.SetLogger(hclog.NewNullLogger()) - - parent.SetLimiter(5.0, 10) - parent.SetMaxRetries(5) - parent.SetOutputCurlString(true) - parent.SetOutputPolicy(true) - parent.SetSRVLookup(true) - - if tt.headers != nil { - parent.SetHeaders(*tt.headers) - } - - if tt.token != "" { - parent.SetToken(tt.token) - } - - clone, err := parent.Clone() - if err != nil { - t.Fatalf("Clone failed: %v", err) - } - - if parent.Address() != clone.Address() { - t.Fatalf("addresses don't match: %v vs %v", parent.Address(), clone.Address()) - } - if parent.ClientTimeout() != clone.ClientTimeout() { - t.Fatalf("timeouts don't match: %v vs %v", parent.ClientTimeout(), clone.ClientTimeout()) - } - if parent.CheckRetry() != nil && clone.CheckRetry() == nil { - t.Fatal("checkRetry functions don't match. clone is nil.") - } - if (parent.Limiter() != nil && clone.Limiter() == nil) || (parent.Limiter() == nil && clone.Limiter() != nil) { - t.Fatalf("limiters don't match: %v vs %v", parent.Limiter(), clone.Limiter()) - } - if parent.Limiter().Limit() != clone.Limiter().Limit() { - t.Fatalf("limiter limits don't match: %v vs %v", parent.Limiter().Limit(), clone.Limiter().Limit()) - } - if parent.Limiter().Burst() != clone.Limiter().Burst() { - t.Fatalf("limiter bursts don't match: %v vs %v", parent.Limiter().Burst(), clone.Limiter().Burst()) - } - if parent.MaxRetries() != clone.MaxRetries() { - t.Fatalf("maxRetries don't match: %v vs %v", parent.MaxRetries(), clone.MaxRetries()) - } - if parent.OutputCurlString() == clone.OutputCurlString() { - t.Fatalf("outputCurlString was copied over when it shouldn't have been: %v and %v", parent.OutputCurlString(), clone.OutputCurlString()) - } - if parent.SRVLookup() != clone.SRVLookup() { - t.Fatalf("SRVLookup doesn't match: %v vs %v", parent.SRVLookup(), clone.SRVLookup()) - } - if tt.config.CloneHeaders { - if !reflect.DeepEqual(parent.Headers(), clone.Headers()) { - t.Fatalf("Headers() don't match: %v vs %v", parent.Headers(), clone.Headers()) - } - if parent.config.CloneHeaders != clone.config.CloneHeaders { - t.Fatalf("config.CloneHeaders doesn't match: %v vs %v", parent.config.CloneHeaders, clone.config.CloneHeaders) - } - if tt.headers != nil { - if !reflect.DeepEqual(*tt.headers, clone.Headers()) { - t.Fatalf("expected headers %v, actual %v", *tt.headers, clone.Headers()) - } - } - } - if tt.config.ReadYourWrites && parent.replicationStateStore == nil { - t.Fatalf("replicationStateStore is nil") - } - if tt.config.CloneToken { - if tt.token == "" { - t.Fatalf("test requires a non-empty token") - } - if parent.config.CloneToken != clone.config.CloneToken { - t.Fatalf("config.CloneToken doesn't match: %v vs %v", parent.config.CloneToken, clone.config.CloneToken) - } - if parent.token != clone.token { - t.Fatalf("tokens do not match: %v vs %v", parent.token, clone.token) - } - } else { - // assumes `VAULT_TOKEN` is unset or has an empty value. - expected := "" - if clone.token != expected { - t.Fatalf("expected clone's token %q, actual %q", expected, clone.token) - } - } - if !reflect.DeepEqual(parent.replicationStateStore, clone.replicationStateStore) { - t.Fatalf("expected replicationStateStore %v, actual %v", parent.replicationStateStore, - clone.replicationStateStore) - } - }) - } -} - -func TestSetHeadersRaceSafe(t *testing.T) { - client, err1 := NewClient(nil) - if err1 != nil { - t.Fatalf("NewClient failed: %v", err1) - } - - start := make(chan interface{}) - done := make(chan interface{}) - - testPairs := map[string]string{ - "soda": "rootbeer", - "veggie": "carrots", - "fruit": "apples", - "color": "red", - "protein": "egg", - } - - for key, value := range testPairs { - tmpKey := key - tmpValue := value - go func() { - <-start - // This test fails if here, you replace client.AddHeader(tmpKey, tmpValue) with: - // headerCopy := client.Header() - // headerCopy.AddHeader(tmpKey, tmpValue) - // client.SetHeader(headerCopy) - client.AddHeader(tmpKey, tmpValue) - done <- true - }() - } - - // Start everyone at once. - close(start) - - // Wait until everyone is done. - for i := 0; i < len(testPairs); i++ { - <-done - } - - // Check that all the test pairs are in the resulting - // headers. - resultingHeaders := client.Headers() - for key, value := range testPairs { - if resultingHeaders.Get(key) != value { - t.Fatal("expected " + value + " for " + key) - } - } -} - -func TestMergeReplicationStates(t *testing.T) { - type testCase struct { - name string - old []string - new string - expected []string - } - - testCases := []testCase{ - { - name: "empty-old", - old: nil, - new: "v1:cid:1:0:", - expected: []string{"v1:cid:1:0:"}, - }, - { - name: "old-smaller", - old: []string{"v1:cid:1:0:"}, - new: "v1:cid:2:0:", - expected: []string{"v1:cid:2:0:"}, - }, - { - name: "old-bigger", - old: []string{"v1:cid:2:0:"}, - new: "v1:cid:1:0:", - expected: []string{"v1:cid:2:0:"}, - }, - { - name: "mixed-single", - old: []string{"v1:cid:1:0:"}, - new: "v1:cid:0:1:", - expected: []string{"v1:cid:0:1:", "v1:cid:1:0:"}, - }, - { - name: "mixed-single-alt", - old: []string{"v1:cid:0:1:"}, - new: "v1:cid:1:0:", - expected: []string{"v1:cid:0:1:", "v1:cid:1:0:"}, - }, - { - name: "mixed-double", - old: []string{"v1:cid:0:1:", "v1:cid:1:0:"}, - new: "v1:cid:2:0:", - expected: []string{"v1:cid:0:1:", "v1:cid:2:0:"}, - }, - { - name: "newer-both", - old: []string{"v1:cid:0:1:", "v1:cid:1:0:"}, - new: "v1:cid:2:1:", - expected: []string{"v1:cid:2:1:"}, - }, - } - - b64enc := func(ss []string) []string { - var ret []string - for _, s := range ss { - ret = append(ret, base64.StdEncoding.EncodeToString([]byte(s))) - } - return ret - } - b64dec := func(ss []string) []string { - var ret []string - for _, s := range ss { - d, err := base64.StdEncoding.DecodeString(s) - if err != nil { - t.Fatal(err) - } - ret = append(ret, string(d)) - } - return ret - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - out := b64dec(MergeReplicationStates(b64enc(tc.old), base64.StdEncoding.EncodeToString([]byte(tc.new)))) - if diff := deep.Equal(out, tc.expected); len(diff) != 0 { - t.Errorf("got=%v, expected=%v, diff=%v", out, tc.expected, diff) - } - }) - } -} - -func TestReplicationStateStore_recordState(t *testing.T) { - b64enc := func(s string) string { - return base64.StdEncoding.EncodeToString([]byte(s)) - } - - tests := []struct { - name string - expected []string - resp []*Response - }{ - { - name: "single", - resp: []*Response{ - { - Response: &http.Response{ - Header: map[string][]string{ - HeaderIndex: { - b64enc("v1:cid:1:0:"), - }, - }, - }, - }, - }, - expected: []string{ - b64enc("v1:cid:1:0:"), - }, - }, - { - name: "empty", - resp: []*Response{ - { - Response: &http.Response{ - Header: map[string][]string{}, - }, - }, - }, - expected: nil, - }, - { - name: "multiple", - resp: []*Response{ - { - Response: &http.Response{ - Header: map[string][]string{ - HeaderIndex: { - b64enc("v1:cid:0:1:"), - }, - }, - }, - }, - { - Response: &http.Response{ - Header: map[string][]string{ - HeaderIndex: { - b64enc("v1:cid:1:0:"), - }, - }, - }, - }, - }, - expected: []string{ - b64enc("v1:cid:0:1:"), - b64enc("v1:cid:1:0:"), - }, - }, - { - name: "duplicates", - resp: []*Response{ - { - Response: &http.Response{ - Header: map[string][]string{ - HeaderIndex: { - b64enc("v1:cid:1:0:"), - }, - }, - }, - }, - { - Response: &http.Response{ - Header: map[string][]string{ - HeaderIndex: { - b64enc("v1:cid:1:0:"), - }, - }, - }, - }, - }, - expected: []string{ - b64enc("v1:cid:1:0:"), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - w := &replicationStateStore{} - - var wg sync.WaitGroup - for _, r := range tt.resp { - wg.Add(1) - go func(r *Response) { - defer wg.Done() - w.recordState(r) - }(r) - } - wg.Wait() - - if !reflect.DeepEqual(tt.expected, w.store) { - t.Errorf("recordState(): expected states %v, actual %v", tt.expected, w.store) - } - }) - } -} - -func TestReplicationStateStore_requireState(t *testing.T) { - tests := []struct { - name string - states []string - req []*Request - expected []string - }{ - { - name: "empty", - states: []string{}, - req: []*Request{ - { - Headers: make(http.Header), - }, - }, - expected: nil, - }, - { - name: "basic", - states: []string{ - "v1:cid:0:1:", - "v1:cid:1:0:", - }, - req: []*Request{ - { - Headers: make(http.Header), - }, - }, - expected: []string{ - "v1:cid:0:1:", - "v1:cid:1:0:", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - store := &replicationStateStore{ - store: tt.states, - } - - var wg sync.WaitGroup - for _, r := range tt.req { - wg.Add(1) - go func(r *Request) { - defer wg.Done() - store.requireState(r) - }(r) - } - - wg.Wait() - - var actual []string - for _, r := range tt.req { - if values := r.Headers.Values(HeaderIndex); len(values) > 0 { - actual = append(actual, values...) - } - } - sort.Strings(actual) - if !reflect.DeepEqual(tt.expected, actual) { - t.Errorf("requireState(): expected states %v, actual %v", tt.expected, actual) - } - }) - } -} - -func TestClient_ReadYourWrites(t *testing.T) { - b64enc := func(s string) string { - return base64.StdEncoding.EncodeToString([]byte(s)) - } - - handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.Header().Set(HeaderIndex, strings.TrimLeft(req.URL.Path, "/")) - }) - - tests := []struct { - name string - handler http.Handler - wantStates []string - values [][]string - clone bool - }{ - { - name: "multiple_duplicates", - clone: false, - handler: handler, - wantStates: []string{ - b64enc("v1:cid:0:4:"), - }, - values: [][]string{ - { - b64enc("v1:cid:0:4:"), - b64enc("v1:cid:0:2:"), - }, - { - b64enc("v1:cid:0:4:"), - b64enc("v1:cid:0:2:"), - }, - }, - }, - { - name: "basic_clone", - clone: true, - handler: handler, - wantStates: []string{ - b64enc("v1:cid:0:4:"), - }, - values: [][]string{ - { - b64enc("v1:cid:0:4:"), - }, - { - b64enc("v1:cid:0:3:"), - }, - }, - }, - { - name: "multiple_clone", - clone: true, - handler: handler, - wantStates: []string{ - b64enc("v1:cid:0:4:"), - }, - values: [][]string{ - { - b64enc("v1:cid:0:4:"), - b64enc("v1:cid:0:2:"), - }, - { - b64enc("v1:cid:0:3:"), - b64enc("v1:cid:0:1:"), - }, - }, - }, - { - name: "multiple_duplicates_clone", - clone: true, - handler: handler, - wantStates: []string{ - b64enc("v1:cid:0:4:"), - }, - values: [][]string{ - { - b64enc("v1:cid:0:4:"), - b64enc("v1:cid:0:2:"), - }, - { - b64enc("v1:cid:0:4:"), - b64enc("v1:cid:0:2:"), - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testRequest := func(client *Client, val string) { - req := client.NewRequest(http.MethodGet, "/"+val) - req.Headers.Set(HeaderIndex, val) - resp, err := client.RawRequestWithContext(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // validate that the server provided a valid header value in its response - actual := resp.Header.Get(HeaderIndex) - if actual != val { - t.Errorf("expected header value %v, actual %v", val, actual) - } - } - - config, ln := testHTTPServer(t, handler) - defer ln.Close() - - config.ReadYourWrites = true - config.Address = fmt.Sprintf("http://%s", ln.Addr()) - parent, err := NewClient(config) - if err != nil { - t.Fatal(err) - } - - var wg sync.WaitGroup - for i := 0; i < len(tt.values); i++ { - var c *Client - if tt.clone { - c, err = parent.Clone() - if err != nil { - t.Fatal(err) - } - } else { - c = parent - } - - for _, val := range tt.values[i] { - wg.Add(1) - go func(val string) { - defer wg.Done() - testRequest(c, val) - }(val) - } - } - - wg.Wait() - - if !reflect.DeepEqual(tt.wantStates, parent.replicationStateStore.states()) { - t.Errorf("expected states %v, actual %v", tt.wantStates, parent.replicationStateStore.states()) - } - }) - } -} - -func TestClient_SetReadYourWrites(t *testing.T) { - tests := []struct { - name string - config *Config - calls []bool - }{ - { - name: "false", - config: &Config{}, - calls: []bool{false}, - }, - { - name: "true", - config: &Config{}, - calls: []bool{true}, - }, - { - name: "multi-false", - config: &Config{}, - calls: []bool{false, false}, - }, - { - name: "multi-true", - config: &Config{}, - calls: []bool{true, true}, - }, - { - name: "multi-mix", - config: &Config{}, - calls: []bool{false, true, false, true}, - }, - } - - assertSetReadYourRights := func(t *testing.T, c *Client, v bool, s *replicationStateStore) { - t.Helper() - c.SetReadYourWrites(v) - if c.config.ReadYourWrites != v { - t.Fatalf("expected config.ReadYourWrites %#v, actual %#v", v, c.config.ReadYourWrites) - } - if !reflect.DeepEqual(s, c.replicationStateStore) { - t.Fatalf("expected replicationStateStore %#v, actual %#v", s, c.replicationStateStore) - } - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Client{ - config: tt.config, - } - for i, v := range tt.calls { - var expectStateStore *replicationStateStore - if v { - if c.replicationStateStore == nil { - c.replicationStateStore = &replicationStateStore{ - store: []string{}, - } - } - c.replicationStateStore.store = append(c.replicationStateStore.store, - fmt.Sprintf("%s-%d", tt.name, i)) - expectStateStore = c.replicationStateStore - } - assertSetReadYourRights(t, c, v, expectStateStore) - } - }) - } -} - -func TestClient_SetCloneToken(t *testing.T) { - tests := []struct { - name string - calls []bool - }{ - { - name: "false", - calls: []bool{false}, - }, - { - name: "true", - calls: []bool{true}, - }, - { - name: "multi", - calls: []bool{true, false, true}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Client{ - config: &Config{}, - } - - var expected bool - for _, v := range tt.calls { - actual := c.CloneToken() - if expected != actual { - t.Fatalf("expected %v, actual %v", expected, actual) - } - - expected = v - c.SetCloneToken(expected) - actual = c.CloneToken() - if actual != expected { - t.Fatalf("SetCloneToken(): expected %v, actual %v", expected, actual) - } - } - }) - } -} - -func TestClientWithNamespace(t *testing.T) { - var ns string - handler := func(w http.ResponseWriter, req *http.Request) { - ns = req.Header.Get(NamespaceHeaderName) - } - config, ln := testHTTPServer(t, http.HandlerFunc(handler)) - defer ln.Close() - - // set up a client with a namespace - client, err := NewClient(config) - if err != nil { - t.Fatalf("err: %s", err) - } - ogNS := "test" - client.SetNamespace(ogNS) - _, err = client.rawRequestWithContext( - context.Background(), - client.NewRequest(http.MethodGet, "/")) - if err != nil { - t.Fatalf("err: %s", err) - } - if ns != ogNS { - t.Fatalf("Expected namespace: %q, got %q", ogNS, ns) - } - - // make a call with a temporary namespace - newNS := "new-namespace" - _, err = client.WithNamespace(newNS).rawRequestWithContext( - context.Background(), - client.NewRequest(http.MethodGet, "/")) - if err != nil { - t.Fatalf("err: %s", err) - } - if ns != newNS { - t.Fatalf("Expected new namespace: %q, got %q", newNS, ns) - } - // ensure client has not been modified - _, err = client.rawRequestWithContext( - context.Background(), - client.NewRequest(http.MethodGet, "/")) - if err != nil { - t.Fatalf("err: %s", err) - } - if ns != ogNS { - t.Fatalf("Expected original namespace: %q, got %q", ogNS, ns) - } - - // make call with empty ns - _, err = client.WithNamespace("").rawRequestWithContext( - context.Background(), - client.NewRequest(http.MethodGet, "/")) - if err != nil { - t.Fatalf("err: %s", err) - } - if ns != "" { - t.Fatalf("Expected no namespace, got %q", ns) - } - - // ensure client has not been modified - if client.Namespace() != ogNS { - t.Fatalf("Expected original namespace: %q, got %q", ogNS, client.Namespace()) - } -} - -func TestVaultProxy(t *testing.T) { - const NoProxy string = "NO_PROXY" - - tests := map[string]struct { - name string - vaultHttpProxy string - vaultProxyAddr string - noProxy string - requestUrl string - expectedResolvedProxyUrl string - }{ - "VAULT_HTTP_PROXY used when NO_PROXY env var doesn't include request host": { - vaultHttpProxy: "https://hashicorp.com", - vaultProxyAddr: "", - noProxy: "terraform.io", - requestUrl: "https://vaultproject.io", - }, - "VAULT_HTTP_PROXY used when NO_PROXY env var includes request host": { - vaultHttpProxy: "https://hashicorp.com", - vaultProxyAddr: "", - noProxy: "terraform.io,vaultproject.io", - requestUrl: "https://vaultproject.io", - }, - "VAULT_PROXY_ADDR used when NO_PROXY env var doesn't include request host": { - vaultHttpProxy: "", - vaultProxyAddr: "https://hashicorp.com", - noProxy: "terraform.io", - requestUrl: "https://vaultproject.io", - }, - "VAULT_PROXY_ADDR used when NO_PROXY env var includes request host": { - vaultHttpProxy: "", - vaultProxyAddr: "https://hashicorp.com", - noProxy: "terraform.io,vaultproject.io", - requestUrl: "https://vaultproject.io", - }, - "VAULT_PROXY_ADDR used when VAULT_HTTP_PROXY env var also supplied": { - vaultHttpProxy: "https://hashicorp.com", - vaultProxyAddr: "https://terraform.io", - noProxy: "", - requestUrl: "https://vaultproject.io", - expectedResolvedProxyUrl: "https://terraform.io", - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - if tc.vaultHttpProxy != "" { - oldVaultHttpProxy := os.Getenv(EnvHTTPProxy) - os.Setenv(EnvHTTPProxy, tc.vaultHttpProxy) - defer os.Setenv(EnvHTTPProxy, oldVaultHttpProxy) - } - - if tc.vaultProxyAddr != "" { - oldVaultProxyAddr := os.Getenv(EnvVaultProxyAddr) - os.Setenv(EnvVaultProxyAddr, tc.vaultProxyAddr) - defer os.Setenv(EnvVaultProxyAddr, oldVaultProxyAddr) - } - - if tc.noProxy != "" { - oldNoProxy := os.Getenv(NoProxy) - os.Setenv(NoProxy, tc.noProxy) - defer os.Setenv(NoProxy, oldNoProxy) - } - - c := DefaultConfig() - if c.Error != nil { - t.Fatalf("Expected no error reading config, found error %v", c.Error) - } - - r, _ := http.NewRequest("GET", tc.requestUrl, nil) - proxyUrl, err := c.HttpClient.Transport.(*http.Transport).Proxy(r) - if err != nil { - t.Fatalf("Expected no error resolving proxy, found error %v", err) - } - if proxyUrl == nil || proxyUrl.String() == "" { - t.Fatalf("Expected proxy to be resolved but no proxy returned") - } - if tc.expectedResolvedProxyUrl != "" && proxyUrl.String() != tc.expectedResolvedProxyUrl { - t.Fatalf("Expected resolved proxy URL to be %v but was %v", tc.expectedResolvedProxyUrl, proxyUrl.String()) - } - }) - } -} - -func TestParseAddressWithUnixSocket(t *testing.T) { - address := "unix:///var/run/vault.sock" - config := DefaultConfig() - - u, err := config.ParseAddress(address) - if err != nil { - t.Fatal("Error not expected") - } - if u.Scheme != "http" { - t.Fatal("Scheme not changed to http") - } - if u.Host != "localhost" { - t.Fatal("Host not changed to socket name") - } - if u.Path != "" { - t.Fatal("Path expected to be blank") - } - if config.HttpClient.Transport.(*http.Transport).DialContext == nil { - t.Fatal("DialContext function not set in config.HttpClient.Transport") - } -} diff --git a/api/kv_test.go b/api/kv_test.go deleted file mode 100644 index 36d769fea..000000000 --- a/api/kv_test.go +++ /dev/null @@ -1,390 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "reflect" - "testing" - "time" -) - -func TestExtractVersionMetadata(t *testing.T) { - t.Parallel() - - inputCreatedTimeStr := "2022-05-06T23:02:04.865025Z" - inputDeletionTimeStr := "2022-06-17T01:15:03.279013Z" - expectedCreatedTimeParsed, err := time.Parse(time.RFC3339, inputCreatedTimeStr) - if err != nil { - t.Fatalf("unable to parse expected created time: %v", err) - } - expectedDeletionTimeParsed, err := time.Parse(time.RFC3339, inputDeletionTimeStr) - if err != nil { - t.Fatalf("unable to parse expected created time: %v", err) - } - - testCases := []struct { - name string - input *Secret - expected *KVVersionMetadata - }{ - { - name: "a secret", - input: &Secret{ - Data: map[string]interface{}{ - "data": map[string]interface{}{ - "password": "Hashi123", - }, - "metadata": map[string]interface{}{ - "version": 10, - "created_time": inputCreatedTimeStr, - "deletion_time": "", - "destroyed": false, - "custom_metadata": nil, - }, - }, - }, - expected: &KVVersionMetadata{ - Version: 10, - CreatedTime: expectedCreatedTimeParsed, - DeletionTime: time.Time{}, - Destroyed: false, - }, - }, - { - name: "a secret that has been deleted", - input: &Secret{ - Data: map[string]interface{}{ - "data": map[string]interface{}{ - "password": "Hashi123", - }, - "metadata": map[string]interface{}{ - "version": 10, - "created_time": inputCreatedTimeStr, - "deletion_time": inputDeletionTimeStr, - "destroyed": false, - "custom_metadata": nil, - }, - }, - }, - expected: &KVVersionMetadata{ - Version: 10, - CreatedTime: expectedCreatedTimeParsed, - DeletionTime: expectedDeletionTimeParsed, - Destroyed: false, - }, - }, - { - name: "a response from a Write operation", - input: &Secret{ - Data: map[string]interface{}{ - "version": 10, - "created_time": inputCreatedTimeStr, - "deletion_time": "", - "destroyed": false, - "custom_metadata": nil, - }, - }, - expected: &KVVersionMetadata{ - Version: 10, - CreatedTime: expectedCreatedTimeParsed, - DeletionTime: time.Time{}, - Destroyed: false, - }, - }, - } - - for _, tc := range testCases { - versionMetadata, err := extractVersionMetadata(tc.input) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(versionMetadata, tc.expected) { - t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, versionMetadata, tc.expected) - } - } -} - -func TestExtractDataAndVersionMetadata(t *testing.T) { - t.Parallel() - - inputCreatedTimeStr := "2022-05-06T23:02:04.865025Z" - inputDeletionTimeStr := "2022-06-17T01:15:03.279013Z" - expectedCreatedTimeParsed, err := time.Parse(time.RFC3339, inputCreatedTimeStr) - if err != nil { - t.Fatalf("unable to parse expected created time: %v", err) - } - expectedDeletionTimeParsed, err := time.Parse(time.RFC3339, inputDeletionTimeStr) - if err != nil { - t.Fatalf("unable to parse expected created time: %v", err) - } - - readResp := &Secret{ - Data: map[string]interface{}{ - "data": map[string]interface{}{ - "password": "Hashi123", - }, - "metadata": map[string]interface{}{ - "version": 10, - "created_time": inputCreatedTimeStr, - "deletion_time": "", - "destroyed": false, - "custom_metadata": nil, - }, - }, - } - - readRespDeleted := &Secret{ - Data: map[string]interface{}{ - "data": nil, - "metadata": map[string]interface{}{ - "version": 10, - "created_time": inputCreatedTimeStr, - "deletion_time": inputDeletionTimeStr, - "destroyed": false, - "custom_metadata": nil, - }, - }, - } - - testCases := []struct { - name string - input *Secret - expected *KVSecret - }{ - { - name: "a response from a Read operation", - input: readResp, - expected: &KVSecret{ - Data: map[string]interface{}{ - "password": "Hashi123", - }, - VersionMetadata: &KVVersionMetadata{ - Version: 10, - CreatedTime: expectedCreatedTimeParsed, - DeletionTime: time.Time{}, - Destroyed: false, - }, - // it's tempting to test some Secrets with custom_metadata but - // we can't in this test because it isn't until we call the - // extractCustomMetadata function that the custom metadata - // gets added onto the struct. See TestExtractCustomMetadata. - CustomMetadata: nil, - Raw: readResp, - }, - }, - { - name: "a secret that has been deleted and thus has nil data", - input: readRespDeleted, - expected: &KVSecret{ - Data: nil, - VersionMetadata: &KVVersionMetadata{ - Version: 10, - CreatedTime: expectedCreatedTimeParsed, - DeletionTime: expectedDeletionTimeParsed, - Destroyed: false, - }, - CustomMetadata: nil, - Raw: readRespDeleted, - }, - }, - } - - for _, tc := range testCases { - dvm, err := extractDataAndVersionMetadata(tc.input) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(dvm, tc.expected) { - t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, dvm, tc.expected) - } - } -} - -func TestExtractFullMetadata(t *testing.T) { - inputCreatedTimeStr := "2022-05-20T00:51:49.419794Z" - expectedCreatedTimeParsed, err := time.Parse(time.RFC3339, inputCreatedTimeStr) - if err != nil { - t.Fatalf("unable to parse expected created time: %v", err) - } - - inputUpdatedTimeStr := "2022-05-20T20:23:43.284488Z" - expectedUpdatedTimeParsed, err := time.Parse(time.RFC3339, inputUpdatedTimeStr) - if err != nil { - t.Fatalf("unable to parse expected updated time: %v", err) - } - - inputDeletedTimeStr := "2022-05-21T00:05:49.521697Z" - expectedDeletedTimeParsed, err := time.Parse(time.RFC3339, inputDeletedTimeStr) - if err != nil { - t.Fatalf("unable to parse expected deletion time: %v", err) - } - - metadataResp := &Secret{ - Data: map[string]interface{}{ - "cas_required": true, - "created_time": inputCreatedTimeStr, - "current_version": 2, - "custom_metadata": map[string]interface{}{ - "org": "eng", - }, - "delete_version_after": "200s", - "max_versions": 3, - "oldest_version": 1, - "updated_time": inputUpdatedTimeStr, - "versions": map[string]interface{}{ - "2": map[string]interface{}{ - "created_time": inputUpdatedTimeStr, - "deletion_time": "", - "destroyed": false, - }, - "1": map[string]interface{}{ - "created_time": inputCreatedTimeStr, - "deletion_time": inputDeletedTimeStr, - "destroyed": false, - }, - }, - }, - } - - testCases := []struct { - name string - input *Secret - expected *KVMetadata - }{ - { - name: "a metadata response", - input: metadataResp, - expected: &KVMetadata{ - CASRequired: true, - CreatedTime: expectedCreatedTimeParsed, - CurrentVersion: 2, - CustomMetadata: map[string]interface{}{ - "org": "eng", - }, - DeleteVersionAfter: time.Duration(200 * time.Second), - MaxVersions: 3, - OldestVersion: 1, - UpdatedTime: expectedUpdatedTimeParsed, - Versions: map[string]KVVersionMetadata{ - "2": { - Version: 2, - CreatedTime: expectedUpdatedTimeParsed, - DeletionTime: time.Time{}, - }, - "1": { - Version: 1, - CreatedTime: expectedCreatedTimeParsed, - DeletionTime: expectedDeletedTimeParsed, - }, - }, - }, - }, - } - - for _, tc := range testCases { - md, err := extractFullMetadata(tc.input) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(md, tc.expected) { - t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, md, tc.expected) - } - } -} - -func TestExtractCustomMetadata(t *testing.T) { - testCases := []struct { - name string - inputAPIResp *Secret - expected map[string]interface{} - }{ - { - name: "a read response with some custom metadata", - inputAPIResp: &Secret{ - Data: map[string]interface{}{ - "metadata": map[string]interface{}{ - "custom_metadata": map[string]interface{}{"org": "eng"}, - }, - }, - }, - expected: map[string]interface{}{"org": "eng"}, - }, - { - name: "a write response with some (pre-existing) custom metadata", - inputAPIResp: &Secret{ - Data: map[string]interface{}{ - "custom_metadata": map[string]interface{}{"org": "eng"}, - }, - }, - expected: map[string]interface{}{"org": "eng"}, - }, - { - name: "a read response with no custom metadata from a pre-1.9 Vault server", - inputAPIResp: &Secret{ - Data: map[string]interface{}{ - "metadata": map[string]interface{}{}, - }, - }, - expected: map[string]interface{}(nil), - }, - { - name: "a write response with no custom metadata from a pre-1.9 Vault server", - inputAPIResp: &Secret{ - Data: map[string]interface{}{}, - }, - expected: map[string]interface{}(nil), - }, - { - name: "a read response with no custom metadata from a post-1.9 Vault server", - inputAPIResp: &Secret{ - Data: map[string]interface{}{ - "metadata": map[string]interface{}{ - "custom_metadata": nil, - }, - }, - }, - expected: map[string]interface{}(nil), - }, - { - name: "a write response with no custom metadata from a post-1.9 Vault server", - inputAPIResp: &Secret{ - Data: map[string]interface{}{ - "custom_metadata": nil, - }, - }, - expected: map[string]interface{}(nil), - }, - { - name: "a read response where custom metadata was deleted", - inputAPIResp: &Secret{ - Data: map[string]interface{}{ - "metadata": map[string]interface{}{ - "custom_metadata": map[string]interface{}{}, - }, - }, - }, - expected: map[string]interface{}{}, - }, - { - name: "a write response where custom metadata was deleted", - inputAPIResp: &Secret{ - Data: map[string]interface{}{ - "custom_metadata": map[string]interface{}{}, - }, - }, - expected: map[string]interface{}{}, - }, - } - - for _, tc := range testCases { - cm := extractCustomMetadata(tc.inputAPIResp) - - if !reflect.DeepEqual(cm, tc.expected) { - t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, cm, tc.expected) - } - } -} diff --git a/api/output_policy_test.go b/api/output_policy_test.go deleted file mode 100644 index 2092e2ba2..000000000 --- a/api/output_policy_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "net/http" - "net/url" - "testing" -) - -func TestBuildSamplePolicy(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - req *OutputPolicyError - expected string - err error - }{ - { - "happy path", - &OutputPolicyError{ - method: http.MethodGet, - path: "/something", - }, - formatOutputPolicy("/something", []string{"read"}), - nil, - }, - { // test included to clear up some confusion around the sanitize comment - "demonstrate that this function does not format fully", - &OutputPolicyError{ - method: http.MethodGet, - path: "http://vault.test/v1/something", - }, - formatOutputPolicy("http://vault.test/v1/something", []string{"read"}), - nil, - }, - { // test that list is properly returned - "list over read returned", - &OutputPolicyError{ - method: http.MethodGet, - path: "/something", - params: url.Values{ - "list": []string{"true"}, - }, - }, - formatOutputPolicy("/something", []string{"list"}), - nil, - }, - { - "valid protected path", - &OutputPolicyError{ - method: http.MethodGet, - path: "/sys/config/ui/headers/", - }, - formatOutputPolicy("/sys/config/ui/headers/", []string{"read", "sudo"}), - nil, - }, - { // ensure that a formatted path that trims the trailing slash as the code does still works for recognizing a sudo path - "valid protected path no trailing /", - &OutputPolicyError{ - method: http.MethodGet, - path: "/sys/config/ui/headers", - }, - formatOutputPolicy("/sys/config/ui/headers", []string{"read", "sudo"}), - nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result, err := tc.req.buildSamplePolicy() - if tc.err != err { - t.Fatalf("expected for the error to be %v instead got %v\n", tc.err, err) - } - - if tc.expected != result { - t.Fatalf("expected for the policy string to be %v instead got %v\n", tc.expected, result) - } - }) - } -} diff --git a/api/plugin_helpers_test.go b/api/plugin_helpers_test.go deleted file mode 100644 index 2e97d44cb..000000000 --- a/api/plugin_helpers_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import "testing" - -func TestIsSudoPath(t *testing.T) { - t.Parallel() - - testCases := []struct { - path string - expected bool - }{ - // Testing: Not a real endpoint - { - "/not/in/sudo/paths/list", - false, - }, - // Testing: sys/raw/{path} - { - "/sys/raw/single-node-path", - true, - }, - { - "/sys/raw/multiple/nodes/path", - true, - }, - { - "/sys/raw/WEIRD(but_still_valid!)p4Th?🗿笑", - true, - }, - // Testing: sys/auth/{path}/tune - { - "/sys/auth/path/in/middle/tune", - true, - }, - // Testing: sys/plugins/catalog/{type} and sys/plugins/catalog/{name} (regexes overlap) - { - "/sys/plugins/catalog/some-type", - true, - }, - // Testing: Not a real endpoint - { - "/sys/plugins/catalog/some/type/or/name/with/slashes", - false, - }, - // Testing: sys/plugins/catalog/{type}/{name} - { - "/sys/plugins/catalog/some-type/some-name", - true, - }, - // Testing: Not a real endpoint - { - "/sys/plugins/catalog/some-type/some/name/with/slashes", - false, - }, - // Testing: auth/token/accessors (an example of a sudo path that only accepts list operations) - // It is matched as sudo without the trailing slash... - { - "/auth/token/accessors", - true, - }, - // ...and also with it. - // (Although at the time of writing, the only caller of IsSudoPath always removes trailing slashes.) - { - "/auth/token/accessors/", - true, - }, - } - - for _, tc := range testCases { - result := IsSudoPath(tc.path) - if result != tc.expected { - t.Fatalf("expected api.IsSudoPath to return %v for path %s but it returned %v", tc.expected, tc.path, result) - } - } -} diff --git a/api/renewer_test.go b/api/renewer_test.go deleted file mode 100644 index 7ba16e66e..000000000 --- a/api/renewer_test.go +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "errors" - "fmt" - "math/rand" - "reflect" - "testing" - "testing/quick" - "time" - - "github.com/go-test/deep" -) - -func TestRenewer_NewRenewer(t *testing.T) { - t.Parallel() - - client, err := NewClient(DefaultConfig()) - if err != nil { - t.Fatal(err) - } - - cases := []struct { - name string - i *RenewerInput - e *Renewer - err bool - }{ - { - name: "nil", - i: nil, - e: nil, - err: true, - }, - { - name: "missing_secret", - i: &RenewerInput{ - Secret: nil, - }, - e: nil, - err: true, - }, - { - name: "default_grace", - i: &RenewerInput{ - Secret: &Secret{}, - }, - e: &Renewer{ - secret: &Secret{}, - }, - err: false, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - v, err := client.NewRenewer(tc.i) - if (err != nil) != tc.err { - t.Fatal(err) - } - - if v == nil { - return - } - - // Zero-out channels because reflect - v.client = nil - v.random = nil - v.doneCh = nil - v.renewCh = nil - v.stopCh = nil - - if diff := deep.Equal(tc.e, v); diff != nil { - t.Error(diff) - } - }) - } -} - -func TestLifetimeWatcher(t *testing.T) { - t.Parallel() - - client, err := NewClient(DefaultConfig()) - if err != nil { - t.Fatal(err) - } - - // Note that doRenewWithOptions starts its loop with an initial renewal. - // This has a big impact on the particulars of the following cases. - - renewedSecret := &Secret{} - var caseOneErrorCount int - var caseManyErrorsCount int - cases := []struct { - maxTestTime time.Duration - name string - leaseDurationSeconds int - incrementSeconds int - renew renewFunc - expectError error - expectRenewal bool - }{ - { - maxTestTime: time.Second, - name: "no_error", - leaseDurationSeconds: 60, - incrementSeconds: 60, - renew: func(_ string, _ int) (*Secret, error) { - return renewedSecret, nil - }, - expectError: nil, - expectRenewal: true, - }, - { - maxTestTime: time.Second, - name: "short_increment_duration", - leaseDurationSeconds: 60, - incrementSeconds: 10, - renew: func(_ string, _ int) (*Secret, error) { - return renewedSecret, nil - }, - expectError: nil, - expectRenewal: true, - }, - { - maxTestTime: 5 * time.Second, - name: "one_error", - leaseDurationSeconds: 15, - incrementSeconds: 15, - renew: func(_ string, _ int) (*Secret, error) { - if caseOneErrorCount == 0 { - caseOneErrorCount++ - return nil, fmt.Errorf("renew failure") - } - return renewedSecret, nil - }, - expectError: nil, - expectRenewal: true, - }, - { - maxTestTime: 15 * time.Second, - name: "many_errors", - leaseDurationSeconds: 15, - incrementSeconds: 15, - renew: func(_ string, _ int) (*Secret, error) { - if caseManyErrorsCount == 3 { - return renewedSecret, nil - } - caseManyErrorsCount++ - return nil, fmt.Errorf("renew failure") - }, - expectError: nil, - expectRenewal: true, - }, - { - maxTestTime: 15 * time.Second, - name: "only_errors", - leaseDurationSeconds: 15, - incrementSeconds: 15, - renew: func(_ string, _ int) (*Secret, error) { - return nil, fmt.Errorf("renew failure") - }, - expectError: nil, - expectRenewal: false, - }, - { - maxTestTime: 15 * time.Second, - name: "negative_lease_duration", - leaseDurationSeconds: -15, - incrementSeconds: 15, - renew: func(_ string, _ int) (*Secret, error) { - return renewedSecret, nil - }, - expectError: nil, - expectRenewal: true, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - v, err := client.NewLifetimeWatcher(&LifetimeWatcherInput{ - Secret: &Secret{ - LeaseDuration: tc.leaseDurationSeconds, - }, - Increment: tc.incrementSeconds, - }) - if err != nil { - t.Fatal(err) - } - - doneCh := make(chan error, 1) - go func() { - doneCh <- v.doRenewWithOptions(false, false, - tc.leaseDurationSeconds, "myleaseID", tc.renew, time.Second) - }() - defer v.Stop() - - receivedRenewal := false - receivedDone := false - ChannelLoop: - for { - select { - case <-time.After(tc.maxTestTime): - t.Fatalf("renewal didn't happen") - case r := <-v.RenewCh(): - if !tc.expectRenewal { - t.Fatal("expected no renewals") - } - if r.Secret != renewedSecret { - t.Fatalf("expected secret %v, got %v", renewedSecret, r.Secret) - } - receivedRenewal = true - if !receivedDone { - continue ChannelLoop - } - break ChannelLoop - case err := <-doneCh: - receivedDone = true - if tc.expectError != nil && !errors.Is(err, tc.expectError) { - t.Fatalf("expected error %q, got: %v", tc.expectError, err) - } - if tc.expectError == nil && err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if tc.expectRenewal && !receivedRenewal { - // We might have received the stop before the renew call on the channel. - continue ChannelLoop - } - break ChannelLoop - } - } - - if tc.expectRenewal && !receivedRenewal { - t.Fatalf("expected at least one renewal, got none.") - } - }) - } -} - -// TestCalcSleepPeriod uses property based testing to evaluate the calculateSleepDuration -// function of LifeTimeWatchers, but also incidentally tests "calculateGrace". -// This is on account of "calculateSleepDuration" performing the "calculateGrace" -// function in particular instances. -// Both of these functions support the vital functionality of the LifeTimeWatcher -// and therefore should be tested rigorously. -func TestCalcSleepPeriod(t *testing.T) { - c := quick.Config{ - MaxCount: 10000, - Values: func(values []reflect.Value, r *rand.Rand) { - leaseDuration := r.Int63() - priorDuration := r.Int63n(leaseDuration) - remainingLeaseDuration := r.Int63n(priorDuration) - increment := r.Int63n(remainingLeaseDuration) - - values[0] = reflect.ValueOf(r) - values[1] = reflect.ValueOf(time.Duration(leaseDuration)) - values[2] = reflect.ValueOf(time.Duration(priorDuration)) - values[3] = reflect.ValueOf(time.Duration(remainingLeaseDuration)) - values[4] = reflect.ValueOf(time.Duration(increment)) - }, - } - - // tests that "calculateSleepDuration" will always return a value less than - // the remaining lease duration given a random leaseDuration, priorDuration, remainingLeaseDuration, and increment. - // Inputs are generated so that: - // leaseDuration > priorDuration > remainingLeaseDuration - // and remainingLeaseDuration > increment - if err := quick.Check(func(r *rand.Rand, leaseDuration, priorDuration, remainingLeaseDuration, increment time.Duration) bool { - lw := LifetimeWatcher{ - grace: 0, - increment: int(increment.Seconds()), - random: r, - } - - lw.calculateGrace(remainingLeaseDuration, increment) - - // ensure that we sleep for less than the remaining lease. - return lw.calculateSleepDuration(remainingLeaseDuration, priorDuration) < remainingLeaseDuration - }, &c); err != nil { - t.Error(err) - } -} diff --git a/api/request_test.go b/api/request_test.go deleted file mode 100644 index ac21b8019..000000000 --- a/api/request_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "strings" - "testing" -) - -func TestRequestSetJSONBody(t *testing.T) { - var r Request - raw := map[string]interface{}{"foo": "bar"} - if err := r.SetJSONBody(raw); err != nil { - t.Fatalf("err: %s", err) - } - - expected := `{"foo":"bar"}` - actual := strings.TrimSpace(string(r.BodyBytes)) - if actual != expected { - t.Fatalf("bad: %s", actual) - } -} - -func TestRequestResetJSONBody(t *testing.T) { - var r Request - raw := map[string]interface{}{"foo": "bar"} - if err := r.SetJSONBody(raw); err != nil { - t.Fatalf("err: %s", err) - } - - if err := r.ResetJSONBody(); err != nil { - t.Fatalf("err: %s", err) - } - - buf := make([]byte, len(r.BodyBytes)) - copy(buf, r.BodyBytes) - - expected := `{"foo":"bar"}` - actual := strings.TrimSpace(string(buf)) - if actual != expected { - t.Fatalf("bad: actual %s, expected %s", actual, expected) - } -} diff --git a/api/secret_test.go b/api/secret_test.go deleted file mode 100644 index 9fa20e1a9..000000000 --- a/api/secret_test.go +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "testing" -) - -func TestTokenPolicies(t *testing.T) { - var s *Secret - - // Verify some of the short-circuit paths in the function - if policies, err := s.TokenPolicies(); policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } - - s = &Secret{} - - if policies, err := s.TokenPolicies(); policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } - - s.Auth = &SecretAuth{} - - if policies, err := s.TokenPolicies(); policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } - - s.Auth.Policies = []string{} - - if policies, err := s.TokenPolicies(); policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } - - s.Auth.Policies = []string{"test"} - - if policies, err := s.TokenPolicies(); policies == nil { - t.Error("policies was nil") - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } - - s.Auth = nil - s.Data = make(map[string]interface{}) - - if policies, err := s.TokenPolicies(); policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } - - // Verify that s.Data["policies"] are properly processed - { - policyList := make([]string, 0) - s.Data["policies"] = policyList - - if policies, err := s.TokenPolicies(); len(policies) != len(policyList) { - t.Errorf("expecting policies length %d, got %d", len(policyList), len(policies)) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } else if s.Auth == nil { - t.Error("Auth field is still nil") - } - - policyList = append(policyList, "policy1", "policy2") - s.Data["policies"] = policyList - - if policies, err := s.TokenPolicies(); len(policyList) != 2 { - t.Errorf("expecting policies length %d, got %d", len(policyList), len(policies)) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } else if s.Auth == nil { - t.Error("Auth field is still nil") - } - } - - // Do it again but with an interface{} slice - { - s.Auth = nil - policyList := make([]interface{}, 0) - s.Data["policies"] = policyList - - if policies, err := s.TokenPolicies(); len(policies) != len(policyList) { - t.Errorf("expecting policies length %d, got %d", len(policyList), len(policies)) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } else if s.Auth == nil { - t.Error("Auth field is still nil") - } - - policyItems := make([]interface{}, 2) - policyItems[0] = "policy1" - policyItems[1] = "policy2" - - policyList = append(policyList, policyItems...) - s.Data["policies"] = policyList - - if policies, err := s.TokenPolicies(); len(policies) != 2 { - t.Errorf("expecting policies length %d, got %d", len(policyList), len(policies)) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } else if s.Auth == nil { - t.Error("Auth field is still nil") - } - - s.Auth = nil - s.Data["policies"] = 7.0 - - if policies, err := s.TokenPolicies(); err == nil { - t.Error("err was nil") - } else if policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } - - s.Auth = nil - s.Data["policies"] = []int{2, 3, 5, 8, 13} - - if policies, err := s.TokenPolicies(); err == nil { - t.Error("err was nil") - } else if policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } - } - - s.Auth = nil - s.Data["policies"] = nil - - if policies, err := s.TokenPolicies(); err != nil { - t.Errorf("err was not nil, got %v", err) - } else if policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } - - // Verify that logic that merges s.Data["policies"] and s.Data["identity_policies"] works - { - policyList := []string{"policy1", "policy2", "policy3"} - s.Data["policies"] = policyList[:1] - s.Data["identity_policies"] = "not_a_slice" - s.Auth = nil - - if policies, err := s.TokenPolicies(); err == nil { - t.Error("err was nil") - } else if policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } - - s.Data["identity_policies"] = policyList[1:] - - if policies, err := s.TokenPolicies(); len(policyList) != len(policies) { - t.Errorf("expecting policies length %d, got %d", len(policyList), len(policies)) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } else if s.Auth == nil { - t.Error("Auth field is still nil") - } - } - - // Do it again but with an interface{} slice - { - policyList := []interface{}{"policy1", "policy2", "policy3"} - s.Data["policies"] = policyList[:1] - s.Data["identity_policies"] = "not_a_slice" - s.Auth = nil - - if policies, err := s.TokenPolicies(); err == nil { - t.Error("err was nil") - } else if policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } - - s.Data["identity_policies"] = policyList[1:] - - if policies, err := s.TokenPolicies(); len(policyList) != len(policies) { - t.Errorf("expecting policies length %d, got %d", len(policyList), len(policies)) - } else if err != nil { - t.Errorf("err was not nil, got %v", err) - } else if s.Auth == nil { - t.Error("Auth field is still nil") - } - - s.Auth = nil - s.Data["identity_policies"] = []int{2, 3, 5, 8, 13} - - if policies, err := s.TokenPolicies(); err == nil { - t.Error("err was nil") - } else if policies != nil { - t.Errorf("policies was not nil, got %v", policies) - } - } - - s.Auth = nil - s.Data["policies"] = []string{"policy1"} - s.Data["identity_policies"] = nil - - if policies, err := s.TokenPolicies(); err != nil { - t.Errorf("err was not nil, got %v", err) - } else if len(policies) != 1 { - t.Errorf("expecting policies length %d, got %d", 1, len(policies)) - } else if s.Auth == nil { - t.Error("Auth field is still nil") - } -} diff --git a/api/ssh_agent_test.go b/api/ssh_agent_test.go deleted file mode 100644 index 38117e42a..000000000 --- a/api/ssh_agent_test.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "fmt" - "net/http" - "strings" - "testing" -) - -func TestSSH_CreateTLSClient(t *testing.T) { - // load the default configuration - config, err := LoadSSHHelperConfig("./test-fixtures/agent_config.hcl") - if err != nil { - panic(fmt.Sprintf("error loading agent's config file: %s", err)) - } - - client, err := config.NewClient() - if err != nil { - panic(fmt.Sprintf("error creating the client: %s", err)) - } - - // Provide a certificate and enforce setting of transport - config.CACert = "./test-fixtures/vault.crt" - - client, err = config.NewClient() - if err != nil { - panic(fmt.Sprintf("error creating the client: %s", err)) - } - if client.config.HttpClient.Transport == nil { - panic(fmt.Sprintf("error creating client with TLS transport")) - } -} - -func TestSSH_CreateTLSClient_tlsServerName(t *testing.T) { - // Ensure that the HTTP client is associated with the configured TLS server name. - tlsServerName := "tls.server.name" - - config, err := ParseSSHHelperConfig(fmt.Sprintf(` -vault_addr = "1.2.3.4" -tls_server_name = "%s" -`, tlsServerName)) - if err != nil { - panic(fmt.Sprintf("error loading config: %s", err)) - } - - client, err := config.NewClient() - if err != nil { - panic(fmt.Sprintf("error creating the client: %s", err)) - } - - actualTLSServerName := client.config.HttpClient.Transport.(*http.Transport).TLSClientConfig.ServerName - if actualTLSServerName != tlsServerName { - panic(fmt.Sprintf("incorrect TLS server name. expected: %s actual: %s", tlsServerName, actualTLSServerName)) - } -} - -func TestParseSSHHelperConfig(t *testing.T) { - config, err := ParseSSHHelperConfig(` - vault_addr = "1.2.3.4" -`) - if err != nil { - t.Fatal(err) - } - - if config.SSHMountPoint != SSHHelperDefaultMountPoint { - t.Errorf("expected %q to be %q", config.SSHMountPoint, SSHHelperDefaultMountPoint) - } -} - -func TestParseSSHHelperConfig_missingVaultAddr(t *testing.T) { - _, err := ParseSSHHelperConfig("") - if err == nil { - t.Fatal("expected error") - } - - if !strings.Contains(err.Error(), `missing config "vault_addr"`) { - t.Errorf("bad error: %s", err) - } -} - -func TestParseSSHHelperConfig_badKeys(t *testing.T) { - _, err := ParseSSHHelperConfig(` -vault_addr = "1.2.3.4" -nope = "bad" -`) - if err == nil { - t.Fatal("expected error") - } - - if !strings.Contains(err.Error(), `ssh_helper: invalid key "nope" on line 3`) { - t.Errorf("bad error: %s", err) - } -} - -func TestParseSSHHelperConfig_tlsServerName(t *testing.T) { - tlsServerName := "tls.server.name" - - config, err := ParseSSHHelperConfig(fmt.Sprintf(` -vault_addr = "1.2.3.4" -tls_server_name = "%s" -`, tlsServerName)) - if err != nil { - t.Fatal(err) - } - - if config.TLSServerName != tlsServerName { - t.Errorf("incorrect TLS server name. expected: %s actual: %s", tlsServerName, config.TLSServerName) - } -} diff --git a/api/sys_mounts_test.go b/api/sys_mounts_test.go deleted file mode 100644 index a810c6268..000000000 --- a/api/sys_mounts_test.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestListMounts(t *testing.T) { - mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultMountsHandler)) - defer mockVaultServer.Close() - - cfg := DefaultConfig() - cfg.Address = mockVaultServer.URL - client, err := NewClient(cfg) - if err != nil { - t.Fatal(err) - } - - resp, err := client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - expectedMounts := map[string]struct { - Type string - Version string - }{ - "cubbyhole/": {Type: "cubbyhole", Version: "v1.0.0"}, - "identity/": {Type: "identity", Version: ""}, - "secret/": {Type: "kv", Version: ""}, - "sys/": {Type: "system", Version: ""}, - } - - for path, mount := range resp { - expected, ok := expectedMounts[path] - if !ok { - t.Errorf("Unexpected mount: %s: %+v", path, mount) - continue - } - if expected.Type != mount.Type || expected.Version != mount.PluginVersion { - t.Errorf("Mount did not match: %s -> expected %+v but got %+v", path, expected, mount) - } - } - - for path, expected := range expectedMounts { - mount, ok := resp[path] - if !ok { - t.Errorf("Expected mount not found mount: %s: %+v", path, expected) - continue - } - if expected.Type != mount.Type || expected.Version != mount.PluginVersion { - t.Errorf("Mount did not match: %s -> expected %+v but got %+v", path, expected, mount) - } - } -} - -func mockVaultMountsHandler(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(listMountsResponse)) -} - -const listMountsResponse = `{ - "request_id": "3cd881e9-ea50-2e06-90b2-5641667485fa", - "lease_id": "", - "lease_duration": 0, - "renewable": false, - "data": { - "cubbyhole/": { - "accessor": "cubbyhole_2e3fc28d", - "config": { - "default_lease_ttl": 0, - "force_no_cache": false, - "max_lease_ttl": 0 - }, - "description": "per-token private secret storage", - "external_entropy_access": false, - "local": true, - "options": null, - "plugin_version": "v1.0.0", - "running_sha256": "", - "running_plugin_version": "", - "seal_wrap": false, - "type": "cubbyhole", - "uuid": "575063dc-5ef8-4487-c842-22c494c19a6f" - }, - "identity/": { - "accessor": "identity_6e01c327", - "config": { - "default_lease_ttl": 0, - "force_no_cache": false, - "max_lease_ttl": 0, - "passthrough_request_headers": [ - "Authorization" - ] - }, - "description": "identity store", - "external_entropy_access": false, - "local": false, - "options": null, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": "", - "seal_wrap": false, - "type": "identity", - "uuid": "187d7eba-3471-554b-c2d9-1479612c8046" - }, - "secret/": { - "accessor": "kv_3e2f282f", - "config": { - "default_lease_ttl": 0, - "force_no_cache": false, - "max_lease_ttl": 0 - }, - "description": "key/value secret storage", - "external_entropy_access": false, - "local": false, - "options": { - "version": "2" - }, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": "", - "seal_wrap": false, - "type": "kv", - "uuid": "13375e0f-876e-7e96-0a3e-076f37b6b69d" - }, - "sys/": { - "accessor": "system_93503264", - "config": { - "default_lease_ttl": 0, - "force_no_cache": false, - "max_lease_ttl": 0, - "passthrough_request_headers": [ - "Accept" - ] - }, - "description": "system endpoints used for control, policy and debugging", - "external_entropy_access": false, - "local": false, - "options": null, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": "", - "seal_wrap": true, - "type": "system", - "uuid": "1373242d-cc4d-c023-410b-7f336e7ba0a8" - } - } -}` diff --git a/api/sys_plugins_test.go b/api/sys_plugins_test.go deleted file mode 100644 index 367318147..000000000 --- a/api/sys_plugins_test.go +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package api - -import ( - "context" - "net/http" - "net/http/httptest" - "reflect" - "testing" - - "github.com/hashicorp/go-secure-stdlib/strutil" -) - -func TestRegisterPlugin(t *testing.T) { - mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandlerRegister)) - defer mockVaultServer.Close() - - cfg := DefaultConfig() - cfg.Address = mockVaultServer.URL - client, err := NewClient(cfg) - if err != nil { - t.Fatal(err) - } - - err = client.Sys().RegisterPluginWithContext(context.Background(), &RegisterPluginInput{ - Version: "v1.0.0", - }) - if err != nil { - t.Fatal(err) - } -} - -func TestListPlugins(t *testing.T) { - mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandlerList)) - defer mockVaultServer.Close() - - cfg := DefaultConfig() - cfg.Address = mockVaultServer.URL - client, err := NewClient(cfg) - if err != nil { - t.Fatal(err) - } - - for name, tc := range map[string]struct { - input ListPluginsInput - expectedPlugins map[PluginType][]string - }{ - "no type specified": { - input: ListPluginsInput{}, - expectedPlugins: map[PluginType][]string{ - PluginTypeCredential: {"alicloud"}, - PluginTypeDatabase: {"cassandra-database-plugin"}, - PluginTypeSecrets: {"ad", "alicloud"}, - }, - }, - "only auth plugins": { - input: ListPluginsInput{Type: PluginTypeCredential}, - expectedPlugins: map[PluginType][]string{ - PluginTypeCredential: {"alicloud"}, - }, - }, - "only database plugins": { - input: ListPluginsInput{Type: PluginTypeDatabase}, - expectedPlugins: map[PluginType][]string{ - PluginTypeDatabase: {"cassandra-database-plugin"}, - }, - }, - "only secret plugins": { - input: ListPluginsInput{Type: PluginTypeSecrets}, - expectedPlugins: map[PluginType][]string{ - PluginTypeSecrets: {"ad", "alicloud"}, - }, - }, - } { - t.Run(name, func(t *testing.T) { - resp, err := client.Sys().ListPluginsWithContext(context.Background(), &tc.input) - if err != nil { - t.Fatal(err) - } - - for pluginType, expected := range tc.expectedPlugins { - actualPlugins := resp.PluginsByType[pluginType] - if len(expected) != len(actualPlugins) { - t.Fatal("Wrong number of plugins", expected, actualPlugins) - } - for i := range actualPlugins { - if expected[i] != actualPlugins[i] { - t.Fatalf("Expected %q but got %q", expected[i], actualPlugins[i]) - } - } - - for _, expectedPlugin := range expected { - found := false - for _, plugin := range resp.Details { - if plugin.Type == pluginType.String() && plugin.Name == expectedPlugin { - found = true - break - } - } - if !found { - t.Errorf("Expected to find %s plugin %s but not found in details: %#v", pluginType.String(), expectedPlugin, resp.Details) - } - } - } - - for _, actual := range resp.Details { - pluginType, err := ParsePluginType(actual.Type) - if err != nil { - t.Fatal(err) - } - if !strutil.StrListContains(tc.expectedPlugins[pluginType], actual.Name) { - t.Errorf("Did not expect to find %s in details", actual.Name) - } - } - }) - } -} - -func TestGetPlugin(t *testing.T) { - for name, tc := range map[string]struct { - version string - body string - expected GetPluginResponse - }{ - "builtin": { - body: getResponse, - expected: GetPluginResponse{ - Args: nil, - Builtin: true, - Command: "", - Name: "azure", - SHA256: "", - DeprecationStatus: "supported", - Version: "v0.14.0+builtin", - }, - }, - "external": { - version: "v1.0.0", - body: getResponseExternal, - expected: GetPluginResponse{ - Args: []string{}, - Builtin: false, - Command: "azure-plugin", - Name: "azure", - SHA256: "8ba442dba253803685b05e35ad29dcdebc48dec16774614aa7a4ebe53c1e90e1", - DeprecationStatus: "", - Version: "v1.0.0", - }, - }, - "old server": { - body: getResponseOldServerVersion, - expected: GetPluginResponse{ - Args: nil, - Builtin: true, - Command: "", - Name: "azure", - SHA256: "", - DeprecationStatus: "", - Version: "", - }, - }, - } { - t.Run(name, func(t *testing.T) { - mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandlerInfo(tc.body))) - defer mockVaultServer.Close() - - cfg := DefaultConfig() - cfg.Address = mockVaultServer.URL - client, err := NewClient(cfg) - if err != nil { - t.Fatal(err) - } - - input := GetPluginInput{ - Name: "azure", - Type: PluginTypeSecrets, - } - if tc.version != "" { - input.Version = tc.version - } - - info, err := client.Sys().GetPluginWithContext(context.Background(), &input) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(tc.expected, *info) { - t.Errorf("expected: %#v\ngot: %#v", tc.expected, info) - } - }) - } -} - -func mockVaultHandlerInfo(body string) func(w http.ResponseWriter, _ *http.Request) { - return func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(body)) - } -} - -const getResponse = `{ - "request_id": "e93d3f93-8e4f-8443-a803-f1c97c495241", - "lease_id": "", - "renewable": false, - "lease_duration": 0, - "data": { - "args": null, - "builtin": true, - "command": "", - "deprecation_status": "supported", - "name": "azure", - "sha256": "", - "version": "v0.14.0+builtin" - }, - "wrap_info": null, - "warnings": null, - "auth": null -}` - -const getResponseExternal = `{ - "request_id": "e93d3f93-8e4f-8443-a803-f1c97c495241", - "lease_id": "", - "renewable": false, - "lease_duration": 0, - "data": { - "args": [], - "builtin": false, - "command": "azure-plugin", - "name": "azure", - "sha256": "8ba442dba253803685b05e35ad29dcdebc48dec16774614aa7a4ebe53c1e90e1", - "version": "v1.0.0" - }, - "wrap_info": null, - "warnings": null, - "auth": null -}` - -const getResponseOldServerVersion = `{ - "request_id": "e93d3f93-8e4f-8443-a803-f1c97c495241", - "lease_id": "", - "renewable": false, - "lease_duration": 0, - "data": { - "args": null, - "builtin": true, - "command": "", - "name": "azure", - "sha256": "" - }, - "wrap_info": null, - "warnings": null, - "auth": null -}` - -func mockVaultHandlerList(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(listUntypedResponse)) -} - -const listUntypedResponse = `{ - "request_id": "82601a91-cd7a-718f-feca-f573449cc1bb", - "lease_id": "", - "renewable": false, - "lease_duration": 0, - "data": { - "auth": [ - "alicloud" - ], - "database": [ - "cassandra-database-plugin" - ], - "secret": [ - "ad", - "alicloud" - ], - "some_other_unexpected_key": [ - { - "objectKey": "objectValue" - }, - { - "arbitraryData": 7 - } - ], - "detailed": [ - { - "type": "auth", - "name": "alicloud", - "version": "v0.13.0+builtin", - "builtin": true, - "deprecation_status": "supported" - }, - { - "type": "database", - "name": "cassandra-database-plugin", - "version": "v1.13.0+builtin.vault", - "builtin": true, - "deprecation_status": "supported" - }, - { - "type": "secret", - "name": "ad", - "version": "v0.14.0+builtin", - "builtin": true, - "deprecation_status": "supported" - }, - { - "type": "secret", - "name": "alicloud", - "version": "v0.13.0+builtin", - "builtin": true, - "deprecation_status": "supported" - } - ] - }, - "wrap_info": null, - "warnings": null, - "auth": null -}` - -func mockVaultHandlerRegister(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(registerResponse)) -} - -const registerResponse = `{}` diff --git a/api/test-fixtures/agent_config.hcl b/api/test-fixtures/agent_config.hcl deleted file mode 100644 index 38d802605..000000000 --- a/api/test-fixtures/agent_config.hcl +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - -vault_addr="http://127.0.0.1:8200" -ssh_mount_point="ssh" diff --git a/api/test-fixtures/keys/cert.pem b/api/test-fixtures/keys/cert.pem deleted file mode 100644 index 67ef67dd8..000000000 --- a/api/test-fixtures/keys/cert.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIUf+jhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw -MTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS -TRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGn -SgMld6ZWRhNheZhA6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmi -YYMiIWplidMmMO5NTRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5 -donyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVG -B+5+AAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABo4H1 -MIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm++e -HpyM3p708bgZJuRYEdX1o+UwHwYDVR0jBBgwFoAUncSzT/6HMexyuiU9/7EgHu+o -k5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x -OjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A -AAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br -aS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy+SgMIrwfs -X1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4 -aYqNKFWrRaBRAaaYZ/O1ApRTOrXqRx9Eqr0H1BXLsoAq+mWassL8sf6siae+CpwA -KqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU+idkuqfV2h1BQKgSEhFDABjFdTCN -QDAHsEHsi2M4/jRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj -xqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc/J9DIQM+Xmk= ------END CERTIFICATE----- \ No newline at end of file diff --git a/api/test-fixtures/keys/key.pem b/api/test-fixtures/keys/key.pem deleted file mode 100644 index add982002..000000000 --- a/api/test-fixtures/keys/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftetT8pu -HflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNheZhA -6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMmMO5N -TRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJGuUd -y54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuHC3N2 -DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABAoIBAHR7fFV0eAGaopsX -9OD0TUGlsephBXb43g0GYHfJ/1Ew18w9oaxszJEqkl+PB4W3xZ3yG3e8ZomxDOhF -RreF2WgG5xOfhDogMwu6NodbArfgnAvoC6JnW3qha8HMP4F500RFVyCRcd6A3Frd -rFtaZn/UyCsBAN8/zkwPeYHayo7xX6d9kzgRl9HluEX5PXI5+3uiBDUiM085gkLI -5Cmadh9fMdjfhDXI4x2JYmILpp/9Nlc/krB15s5n1MPNtn3yL0TI0tWp0WlwDCV7 -oUm1SfIM0F1fXGFyFDcqwoIr6JCQgXk6XtTg31YhH1xgUIclUVdtHqmAwAbLdIhQ -GAiHn2kCgYEAwD4pZ8HfpiOG/EHNoWsMATc/5yC7O8F9WbvcHZQIymLY4v/7HKZb -VyOR6UQ5/O2cztSGIuKSF6+OK1C34lOyCuTSOTFrjlgEYtLIXjdGLfFdtOO8GRQR -akVXdwuzNAjTBaH5eXbG+NKcjmCvZL48dQVlfDTVulzFGbcsVTHIMQUCgYEA7IQI -FVsKnY3KqpyGqXq92LMcsT3XgW6X1BIIV+YhJ5AFUFkFrjrbXs94/8XyLfi0xBQy -efK+8g5sMs7koF8LyZEcAXWZJQduaKB71hoLlRaU4VQkL/dl2B6VFmAII/CsRCYh -r9RmDN2PF/mp98Ih9dpC1VqcCDRGoTYsd7jLalMCgYAMgH5k1wDaZxkSMp1S0AlZ -0uP+/evvOOgT+9mWutfPgZolOQx1koQCKLgGeX9j6Xf3I28NubpSfAI84uTyfQrp -FnRtb79U5Hh0jMynA+U2e6niZ6UF5H41cQj9Hu+qhKBkj2IP+h96cwfnYnZFkPGR -kqZE65KyqfHPeFATwkcImQKBgCdrfhlpGiTWXCABhKQ8s+WpPLAB2ahV8XJEKyXT -UlVQuMIChGLcpnFv7P/cUxf8asx/fUY8Aj0/0CLLvulHziQjTmKj4gl86pb/oIQ3 -xRRtNhU0O+/OsSfLORgIm3K6C0w0esregL/GMbJSR1TnA1gBr7/1oSnw5JC8Ab9W -injHAoGAJT1MGAiQrhlt9GCGe6Ajw4omdbY0wS9NXefnFhf7EwL0es52ezZ28zpU -2LXqSFbtann5CHgpSLxiMYPDIf+er4xgg9Bz34tz1if1rDfP2Qrxdrpr4jDnrGT3 -gYC2qCpvVD9RRUMKFfnJTfl5gMQdBW/LINkHtJ82snAeLl3gjQ4= ------END RSA PRIVATE KEY----- diff --git a/api/test-fixtures/keys/pkioutput b/api/test-fixtures/keys/pkioutput deleted file mode 100644 index 526ff0316..000000000 --- a/api/test-fixtures/keys/pkioutput +++ /dev/null @@ -1,74 +0,0 @@ -Key Value -lease_id pki/issue/example-dot-com/d8214077-9976-8c68-9c07-6610da30aea4 -lease_duration 279359999 -lease_renewable false -certificate -----BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIUf+jhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw -MTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS -TRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGn -SgMld6ZWRhNheZhA6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmi -YYMiIWplidMmMO5NTRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5 -donyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVG -B+5+AAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABo4H1 -MIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm++e -HpyM3p708bgZJuRYEdX1o+UwHwYDVR0jBBgwFoAUncSzT/6HMexyuiU9/7EgHu+o -k5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x -OjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A -AAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br -aS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy+SgMIrwfs -X1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4 -aYqNKFWrRaBRAaaYZ/O1ApRTOrXqRx9Eqr0H1BXLsoAq+mWassL8sf6siae+CpwA -KqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU+idkuqfV2h1BQKgSEhFDABjFdTCN -QDAHsEHsi2M4/jRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj -xqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc/J9DIQM+Xmk= ------END CERTIFICATE----- -issuing_ca -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -private_key -----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftetT8pu -HflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNheZhA -6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMmMO5N -TRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJGuUd -y54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuHC3N2 -DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABAoIBAHR7fFV0eAGaopsX -9OD0TUGlsephBXb43g0GYHfJ/1Ew18w9oaxszJEqkl+PB4W3xZ3yG3e8ZomxDOhF -RreF2WgG5xOfhDogMwu6NodbArfgnAvoC6JnW3qha8HMP4F500RFVyCRcd6A3Frd -rFtaZn/UyCsBAN8/zkwPeYHayo7xX6d9kzgRl9HluEX5PXI5+3uiBDUiM085gkLI -5Cmadh9fMdjfhDXI4x2JYmILpp/9Nlc/krB15s5n1MPNtn3yL0TI0tWp0WlwDCV7 -oUm1SfIM0F1fXGFyFDcqwoIr6JCQgXk6XtTg31YhH1xgUIclUVdtHqmAwAbLdIhQ -GAiHn2kCgYEAwD4pZ8HfpiOG/EHNoWsMATc/5yC7O8F9WbvcHZQIymLY4v/7HKZb -VyOR6UQ5/O2cztSGIuKSF6+OK1C34lOyCuTSOTFrjlgEYtLIXjdGLfFdtOO8GRQR -akVXdwuzNAjTBaH5eXbG+NKcjmCvZL48dQVlfDTVulzFGbcsVTHIMQUCgYEA7IQI -FVsKnY3KqpyGqXq92LMcsT3XgW6X1BIIV+YhJ5AFUFkFrjrbXs94/8XyLfi0xBQy -efK+8g5sMs7koF8LyZEcAXWZJQduaKB71hoLlRaU4VQkL/dl2B6VFmAII/CsRCYh -r9RmDN2PF/mp98Ih9dpC1VqcCDRGoTYsd7jLalMCgYAMgH5k1wDaZxkSMp1S0AlZ -0uP+/evvOOgT+9mWutfPgZolOQx1koQCKLgGeX9j6Xf3I28NubpSfAI84uTyfQrp -FnRtb79U5Hh0jMynA+U2e6niZ6UF5H41cQj9Hu+qhKBkj2IP+h96cwfnYnZFkPGR -kqZE65KyqfHPeFATwkcImQKBgCdrfhlpGiTWXCABhKQ8s+WpPLAB2ahV8XJEKyXT -UlVQuMIChGLcpnFv7P/cUxf8asx/fUY8Aj0/0CLLvulHziQjTmKj4gl86pb/oIQ3 -xRRtNhU0O+/OsSfLORgIm3K6C0w0esregL/GMbJSR1TnA1gBr7/1oSnw5JC8Ab9W -injHAoGAJT1MGAiQrhlt9GCGe6Ajw4omdbY0wS9NXefnFhf7EwL0es52ezZ28zpU -2LXqSFbtann5CHgpSLxiMYPDIf+er4xgg9Bz34tz1if1rDfP2Qrxdrpr4jDnrGT3 -gYC2qCpvVD9RRUMKFfnJTfl5gMQdBW/LINkHtJ82snAeLl3gjQ4= ------END RSA PRIVATE KEY----- -private_key_type rsa diff --git a/api/test-fixtures/root/pkioutput b/api/test-fixtures/root/pkioutput deleted file mode 100644 index 312ae18de..000000000 --- a/api/test-fixtures/root/pkioutput +++ /dev/null @@ -1,74 +0,0 @@ -Key Value -lease_id pki/root/generate/exported/7bf99d76-dd3e-2c5b-04ce-5253062ad586 -lease_duration 315359999 -lease_renewable false -certificate -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -expiration 1.772072879e+09 -issuing_ca -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -private_key -----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p -t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 -BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w -/2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv -0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi -18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb -ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn -8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f -nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 -2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t -grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc -bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 -0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN -ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf -lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 -lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj -AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG -ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib -thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU -4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb -iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO -tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y -LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc -4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX -OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= ------END RSA PRIVATE KEY----- -private_key_type rsa -serial_number 6f:98:9d:f8:67:1a:31:e3:27:60:1b:f7:32:f7:53:19:68:a0:c8:9d diff --git a/api/test-fixtures/root/root.crl b/api/test-fixtures/root/root.crl deleted file mode 100644 index a80c9e411..000000000 --- a/api/test-fixtures/root/root.crl +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN X509 CRL----- -MIIBrjCBlzANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbRcN -MTYwMjI5MDIyOTE3WhcNMjUwMTA1MTAyOTE3WjArMCkCFG+YnfhnGjHjJ2Ab9zL3 -UxlooMidFxExNjAyMjgyMTI5MTctMDUwMKAjMCEwHwYDVR0jBBgwFoAUncSzT/6H -MexyuiU9/7EgHu+ok5swDQYJKoZIhvcNAQELBQADggEBAG9YDXpNe4LJroKZmVCn -HqMhW8eyzyaPak2nPPGCVUnc6vt8rlBYQU+xlBizD6xatZQDMPgrT8sBl9W3ysXk -RUlliHsT/SHddMz5dAZsBPRMJ7pYWLTx8jI4w2WRfbSyI4bY/6qTRNkEBUv+Fk8J -xvwB89+EM0ENcVMhv9ghsUA8h7kOg673HKwRstLDAzxS/uLmEzFjj8SV2m5DbV2Y -UUCKRSV20/kxJMIC9x2KikZhwOSyv1UE1otD+RQvbfAoZPUDmvp2FR/E0NGjBBOg -1TtCPRrl63cjqU3s8KQ4uah9Vj+Cwcu9n/yIKKtNQq4NKHvagv8GlUsoJ4BdAxCw -IA0= ------END X509 CRL----- diff --git a/api/test-fixtures/root/rootcacert.pem b/api/test-fixtures/root/rootcacert.pem deleted file mode 100644 index dcb307a14..000000000 --- a/api/test-fixtures/root/rootcacert.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- diff --git a/api/test-fixtures/root/rootcakey.pem b/api/test-fixtures/root/rootcakey.pem deleted file mode 100644 index e950da5ba..000000000 --- a/api/test-fixtures/root/rootcakey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p -t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 -BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w -/2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv -0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi -18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb -ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn -8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f -nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 -2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t -grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc -bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 -0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN -ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf -lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 -lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj -AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG -ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib -thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU -4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb -iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO -tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y -LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc -4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX -OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= ------END RSA PRIVATE KEY----- diff --git a/api/test-fixtures/vault.crt b/api/test-fixtures/vault.crt deleted file mode 100644 index 3e34cf17a..000000000 --- a/api/test-fixtures/vault.crt +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEEjCCAvqgAwIBAgIJAM7PFmA6Y+KeMA0GCSqGSIb3DQEBCwUAMIGWMQswCQYD -VQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxFDASBgNVBAcMC1N0b255IEJyb29r -MRIwEAYDVQQKDAlIYXNoaUNvcnAxDjAMBgNVBAsMBVZhdWx0MRUwEwYDVQQDDAxW -aXNoYWwgTmF5YWsxIzAhBgkqhkiG9w0BCQEWFHZpc2hhbEBoYXNoaWNvcnAuY29t -MB4XDTE1MDgwNzE5MTk1OFoXDTE1MDkwNjE5MTk1OFowgZYxCzAJBgNVBAYTAlVT -MREwDwYDVQQIDAhOZXcgWW9yazEUMBIGA1UEBwwLU3RvbnkgQnJvb2sxEjAQBgNV -BAoMCUhhc2hpQ29ycDEOMAwGA1UECwwFVmF1bHQxFTATBgNVBAMMDFZpc2hhbCBO -YXlhazEjMCEGCSqGSIb3DQEJARYUdmlzaGFsQGhhc2hpY29ycC5jb20wggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcGlPKIrsq5sDJAUB7mtLjnjbcfR0b -dX1sDHUaTdT+2YBq0JvtoLZOmKw1iVwsMBhaLeXwnKP/O/n67sE8zvZPsuU3REw1 -NTjPof8IbepkENWNxR68KoSB2Vn5r4KiO3ux+KbkXssrZB62+k9khj0e7qIiwyZP -y5+RQPOL2ESmX5DznX+90vH4mzAEF654PbXFI/qOBZcWvWZJ37i+lHkeyCqcB+sm -5o5+zd1ua8jVlN0eLjyqa7FDvIuXPAFEX+r5DVQgIvS2++YaFRqTFCIxRXdDQXdw -1xDMCuG1w4PGVWf3TtlpHeGSIU07DdrCgXsvIRYfW++aZ2pvXwJYCr8hAgMBAAGj -YTBfMA8GA1UdEQQIMAaHBKwYFugwHQYDVR0OBBYEFPl+AkButpRfbblZE9Jb3xNj -AyhkMB8GA1UdIwQYMBaAFPl+AkButpRfbblZE9Jb3xNjAyhkMAwGA1UdEwQFMAMB -Af8wDQYJKoZIhvcNAQELBQADggEBADdIyyBJ3BVghW1shhxYsqQgg/gj2TagpO1P -ulGNzS0aCfB4tzMD4MGWm7cTlL6QW9W6r9OuWKCd1ADherIX9j0gtVWgIMtWGx+i -NbHrYin1xHr4rkB7/f6veCiJ3CtzBC9P/rEI6keyfOn1BfQBsOxfo3oGe/HDlSzD -lpu0GlQECjTXD7dd4jrD0T/wdRQI0BmxcYjn9cZLgoJHtLHZwaS16TGVmKs4iRAW -V9Aw5hLK4jJ59IID830/ly+Ndfc//QGgdE5PM44OrvVFO3Q8+zs7pwr1ql7uQWew -MSuDfbL7EcEGajD/o085sj2u4xVUfkVBW+3TQvs4/pHYOxlhPjI= ------END CERTIFICATE----- diff --git a/audit/format_json_test.go b/audit/format_json_test.go deleted file mode 100644 index 7d6410db6..000000000 --- a/audit/format_json_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package audit - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/helper/salt" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestFormatJSON_formatRequest(t *testing.T) { - salter, err := salt.NewSalt(context.Background(), nil, nil) - if err != nil { - t.Fatal(err) - } - saltFunc := func(context.Context) (*salt.Salt, error) { - return salter, nil - } - - expectedResultStr := fmt.Sprintf(testFormatJSONReqBasicStrFmt, salter.GetIdentifiedHMAC("foo")) - - issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00") - cases := map[string]struct { - Auth *logical.Auth - Req *logical.Request - Err error - Prefix string - ExpectedStr string - }{ - "auth, request": { - &logical.Auth{ - ClientToken: "foo", - Accessor: "bar", - DisplayName: "testtoken", - EntityID: "foobarentity", - NoDefaultPolicy: true, - Policies: []string{"root"}, - TokenType: logical.TokenTypeService, - LeaseOptions: logical.LeaseOptions{ - TTL: time.Hour * 4, - IssueTime: issueTime, - }, - }, - &logical.Request{ - Operation: logical.UpdateOperation, - Path: "/foo", - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - WrapInfo: &logical.RequestWrapInfo{ - TTL: 60 * time.Second, - }, - Headers: map[string][]string{ - "foo": {"bar"}, - }, - }, - errors.New("this is an error"), - "", - expectedResultStr, - }, - "auth, request with prefix": { - &logical.Auth{ - ClientToken: "foo", - Accessor: "bar", - EntityID: "foobarentity", - DisplayName: "testtoken", - NoDefaultPolicy: true, - Policies: []string{"root"}, - TokenType: logical.TokenTypeService, - LeaseOptions: logical.LeaseOptions{ - TTL: time.Hour * 4, - IssueTime: issueTime, - }, - }, - &logical.Request{ - Operation: logical.UpdateOperation, - Path: "/foo", - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - WrapInfo: &logical.RequestWrapInfo{ - TTL: 60 * time.Second, - }, - Headers: map[string][]string{ - "foo": {"bar"}, - }, - }, - errors.New("this is an error"), - "@cee: ", - expectedResultStr, - }, - } - - for name, tc := range cases { - var buf bytes.Buffer - formatter := AuditFormatter{ - AuditFormatWriter: &JSONFormatWriter{ - Prefix: tc.Prefix, - SaltFunc: saltFunc, - }, - } - config := FormatterConfig{ - HMACAccessor: false, - } - in := &logical.LogInput{ - Auth: tc.Auth, - Request: tc.Req, - OuterErr: tc.Err, - } - if err := formatter.FormatRequest(namespace.RootContext(nil), &buf, config, in); err != nil { - t.Fatalf("bad: %s\nerr: %s", name, err) - } - - if !strings.HasPrefix(buf.String(), tc.Prefix) { - t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, expectedResultStr, tc.Prefix) - } - - expectedjson := new(AuditRequestEntry) - - if err := jsonutil.DecodeJSON([]byte(expectedResultStr), &expectedjson); err != nil { - t.Fatalf("bad json: %s", err) - } - expectedjson.Request.Namespace = &AuditNamespace{ID: "root"} - - actualjson := new(AuditRequestEntry) - if err := jsonutil.DecodeJSON([]byte(buf.String())[len(tc.Prefix):], &actualjson); err != nil { - t.Fatalf("bad json: %s", err) - } - - expectedjson.Time = actualjson.Time - - expectedBytes, err := json.Marshal(expectedjson) - if err != nil { - t.Fatalf("unable to marshal json: %s", err) - } - - if !strings.HasSuffix(strings.TrimSpace(buf.String()), string(expectedBytes)) { - t.Fatalf( - "bad: %s\nResult:\n\n%q\n\nExpected:\n\n%q", - name, buf.String(), string(expectedBytes)) - } - } -} - -const testFormatJSONReqBasicStrFmt = `{"time":"2015-08-05T13:45:46Z","type":"request","auth":{"client_token":"%s","accessor":"bar","display_name":"testtoken","policies":["root"],"no_default_policy":true,"metadata":null,"entity_id":"foobarentity","token_type":"service", "token_ttl": 14400, "token_issue_time": "2020-05-28T13:40:18-05:00"},"request":{"operation":"update","path":"/foo","data":null,"wrap_ttl":60,"remote_address":"127.0.0.1","headers":{"foo":["bar"]}},"error":"this is an error"} -` diff --git a/audit/format_jsonx_test.go b/audit/format_jsonx_test.go deleted file mode 100644 index ed72c56f5..000000000 --- a/audit/format_jsonx_test.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package audit - -import ( - "bytes" - "context" - "errors" - "fmt" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/sdk/helper/salt" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestFormatJSONx_formatRequest(t *testing.T) { - salter, err := salt.NewSalt(context.Background(), nil, nil) - if err != nil { - t.Fatal(err) - } - saltFunc := func(context.Context) (*salt.Salt, error) { - return salter, nil - } - - fooSalted := salter.GetIdentifiedHMAC("foo") - issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00") - - cases := map[string]struct { - Auth *logical.Auth - Req *logical.Request - Err error - Prefix string - Result string - ExpectedStr string - }{ - "auth, request": { - &logical.Auth{ - ClientToken: "foo", - Accessor: "bar", - DisplayName: "testtoken", - EntityID: "foobarentity", - NoDefaultPolicy: true, - Policies: []string{"root"}, - TokenType: logical.TokenTypeService, - LeaseOptions: logical.LeaseOptions{ - TTL: time.Hour * 4, - IssueTime: issueTime, - }, - }, - &logical.Request{ - ID: "request", - ClientToken: "foo", - ClientTokenAccessor: "bar", - Operation: logical.UpdateOperation, - Path: "/foo", - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - WrapInfo: &logical.RequestWrapInfo{ - TTL: 60 * time.Second, - }, - Headers: map[string][]string{ - "foo": {"bar"}, - }, - PolicyOverride: true, - }, - errors.New("this is an error"), - "", - "", - fmt.Sprintf(`bar%stesttokenfoobarentitytrueroot2020-05-28T13:40:18-05:0014400servicethis is an error%sbarbarrequestrootupdate/footrue127.0.0.160request`, - fooSalted, fooSalted), - }, - "auth, request with prefix": { - &logical.Auth{ - ClientToken: "foo", - Accessor: "bar", - DisplayName: "testtoken", - NoDefaultPolicy: true, - EntityID: "foobarentity", - Policies: []string{"root"}, - TokenType: logical.TokenTypeService, - LeaseOptions: logical.LeaseOptions{ - TTL: time.Hour * 4, - IssueTime: issueTime, - }, - }, - &logical.Request{ - ID: "request", - ClientToken: "foo", - ClientTokenAccessor: "bar", - Operation: logical.UpdateOperation, - Path: "/foo", - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - WrapInfo: &logical.RequestWrapInfo{ - TTL: 60 * time.Second, - }, - Headers: map[string][]string{ - "foo": {"bar"}, - }, - PolicyOverride: true, - }, - errors.New("this is an error"), - "", - "@cee: ", - fmt.Sprintf(`bar%stesttokenfoobarentitytrueroot2020-05-28T13:40:18-05:0014400servicethis is an error%sbarbarrequestrootupdate/footrue127.0.0.160request`, - fooSalted, fooSalted), - }, - } - - for name, tc := range cases { - var buf bytes.Buffer - formatter := AuditFormatter{ - AuditFormatWriter: &JSONxFormatWriter{ - Prefix: tc.Prefix, - SaltFunc: saltFunc, - }, - } - config := FormatterConfig{ - OmitTime: true, - HMACAccessor: false, - } - in := &logical.LogInput{ - Auth: tc.Auth, - Request: tc.Req, - OuterErr: tc.Err, - } - if err := formatter.FormatRequest(namespace.RootContext(nil), &buf, config, in); err != nil { - t.Fatalf("bad: %s\nerr: %s", name, err) - } - - if !strings.HasPrefix(buf.String(), tc.Prefix) { - t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, tc.Result, tc.Prefix) - } - - if !strings.HasSuffix(strings.TrimSpace(buf.String()), string(tc.ExpectedStr)) { - t.Fatalf( - "bad: %s\nResult:\n\n%q\n\nExpected:\n\n%q", - name, strings.TrimSpace(buf.String()), string(tc.ExpectedStr)) - } - } -} diff --git a/audit/format_test.go b/audit/format_test.go deleted file mode 100644 index 3c0924c36..000000000 --- a/audit/format_test.go +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package audit - -import ( - "context" - "io" - "testing" - - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/sdk/helper/salt" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/copystructure" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type testingFormatWriter struct { - salt *salt.Salt - lastRequest *AuditRequestEntry - lastResponse *AuditResponseEntry -} - -func (fw *testingFormatWriter) WriteRequest(_ io.Writer, entry *AuditRequestEntry) error { - fw.lastRequest = entry - return nil -} - -func (fw *testingFormatWriter) WriteResponse(_ io.Writer, entry *AuditResponseEntry) error { - fw.lastResponse = entry - return nil -} - -func (fw *testingFormatWriter) Salt(ctx context.Context) (*salt.Salt, error) { - if fw.salt != nil { - return fw.salt, nil - } - var err error - fw.salt, err = salt.NewSalt(ctx, nil, nil) - if err != nil { - return nil, err - } - return fw.salt, nil -} - -// hashExpectedValueForComparison replicates enough of the audit HMAC process on a piece of expected data in a test, -// so that we can use assert.Equal to compare the expected and output values. -func (fw *testingFormatWriter) hashExpectedValueForComparison(input map[string]interface{}) map[string]interface{} { - // Copy input before modifying, since we may re-use the same data in another test - copied, err := copystructure.Copy(input) - if err != nil { - panic(err) - } - copiedAsMap := copied.(map[string]interface{}) - - salter, err := fw.Salt(context.Background()) - if err != nil { - panic(err) - } - - err = hashMap(salter.GetIdentifiedHMAC, copiedAsMap, nil) - if err != nil { - panic(err) - } - - return copiedAsMap -} - -func TestFormatRequestErrors(t *testing.T) { - config := FormatterConfig{} - formatter := AuditFormatter{ - AuditFormatWriter: &testingFormatWriter{}, - } - - if err := formatter.FormatRequest(context.Background(), io.Discard, config, &logical.LogInput{}); err == nil { - t.Fatal("expected error due to nil request") - } - - in := &logical.LogInput{ - Request: &logical.Request{}, - } - if err := formatter.FormatRequest(context.Background(), nil, config, in); err == nil { - t.Fatal("expected error due to nil writer") - } -} - -func TestFormatResponseErrors(t *testing.T) { - config := FormatterConfig{} - formatter := AuditFormatter{ - AuditFormatWriter: &testingFormatWriter{}, - } - - if err := formatter.FormatResponse(context.Background(), io.Discard, config, &logical.LogInput{}); err == nil { - t.Fatal("expected error due to nil request") - } - - in := &logical.LogInput{ - Request: &logical.Request{}, - } - if err := formatter.FormatResponse(context.Background(), nil, config, in); err == nil { - t.Fatal("expected error due to nil writer") - } -} - -func TestElideListResponses(t *testing.T) { - tfw := testingFormatWriter{} - formatter := AuditFormatter{&tfw} - ctx := namespace.RootContext(context.Background()) - - type test struct { - name string - inputData map[string]interface{} - expectedData map[string]interface{} - } - - tests := []test{ - { - "nil data", - nil, - nil, - }, - { - "Normal list (keys only)", - map[string]interface{}{ - "keys": []string{"foo", "bar", "baz"}, - }, - map[string]interface{}{ - "keys": 3, - }, - }, - { - "Enhanced list (has key_info)", - map[string]interface{}{ - "keys": []string{"foo", "bar", "baz", "quux"}, - "key_info": map[string]interface{}{ - "foo": "alpha", - "bar": "beta", - "baz": "gamma", - "quux": "delta", - }, - }, - map[string]interface{}{ - "keys": 4, - "key_info": 4, - }, - }, - { - "Unconventional other values in a list response are not touched", - map[string]interface{}{ - "keys": []string{"foo", "bar"}, - "something_else": "baz", - }, - map[string]interface{}{ - "keys": 2, - "something_else": "baz", - }, - }, - { - "Conventional values in a list response are not elided if their data types are unconventional", - map[string]interface{}{ - "keys": map[string]interface{}{ - "You wouldn't expect keys to be a map": nil, - }, - "key_info": []string{ - "You wouldn't expect key_info to be a slice", - }, - }, - map[string]interface{}{ - "keys": map[string]interface{}{ - "You wouldn't expect keys to be a map": nil, - }, - "key_info": []string{ - "You wouldn't expect key_info to be a slice", - }, - }, - }, - } - oneInterestingTestCase := tests[2] - - formatResponse := func( - t *testing.T, - config FormatterConfig, - operation logical.Operation, - inputData map[string]interface{}, - ) { - err := formatter.FormatResponse(ctx, io.Discard, config, &logical.LogInput{ - Request: &logical.Request{Operation: operation}, - Response: &logical.Response{Data: inputData}, - }) - require.Nil(t, err) - } - - t.Run("Default case", func(t *testing.T) { - config := FormatterConfig{ElideListResponses: true} - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - formatResponse(t, config, logical.ListOperation, tc.inputData) - assert.Equal(t, tfw.hashExpectedValueForComparison(tc.expectedData), tfw.lastResponse.Response.Data) - }) - } - }) - - t.Run("When Operation is not list, eliding does not happen", func(t *testing.T) { - config := FormatterConfig{ElideListResponses: true} - tc := oneInterestingTestCase - formatResponse(t, config, logical.ReadOperation, tc.inputData) - assert.Equal(t, tfw.hashExpectedValueForComparison(tc.inputData), tfw.lastResponse.Response.Data) - }) - - t.Run("When ElideListResponses is false, eliding does not happen", func(t *testing.T) { - config := FormatterConfig{ElideListResponses: false} - tc := oneInterestingTestCase - formatResponse(t, config, logical.ListOperation, tc.inputData) - assert.Equal(t, tfw.hashExpectedValueForComparison(tc.inputData), tfw.lastResponse.Response.Data) - }) - - t.Run("When Raw is true, eliding still happens", func(t *testing.T) { - config := FormatterConfig{ElideListResponses: true, Raw: true} - tc := oneInterestingTestCase - formatResponse(t, config, logical.ListOperation, tc.inputData) - assert.Equal(t, tc.expectedData, tfw.lastResponse.Response.Data) - }) -} diff --git a/audit/hashstructure_test.go b/audit/hashstructure_test.go deleted file mode 100644 index 0a1aef558..000000000 --- a/audit/hashstructure_test.go +++ /dev/null @@ -1,400 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package audit - -import ( - "context" - "crypto/sha256" - "encoding/json" - "fmt" - "reflect" - "testing" - "time" - - "github.com/go-test/deep" - - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/hashicorp/vault/sdk/helper/salt" - "github.com/hashicorp/vault/sdk/helper/wrapping" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/copystructure" -) - -func TestCopy_auth(t *testing.T) { - // Make a non-pointer one so that it can't be modified directly - expected := logical.Auth{ - LeaseOptions: logical.LeaseOptions{ - TTL: 1 * time.Hour, - }, - - ClientToken: "foo", - } - auth := expected - - // Copy it - dup, err := copystructure.Copy(&auth) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Check equality - auth2 := dup.(*logical.Auth) - if !reflect.DeepEqual(*auth2, expected) { - t.Fatalf("bad:\n\n%#v\n\n%#v", *auth2, expected) - } -} - -func TestCopy_request(t *testing.T) { - // Make a non-pointer one so that it can't be modified directly - expected := logical.Request{ - Data: map[string]interface{}{ - "foo": "bar", - }, - WrapInfo: &logical.RequestWrapInfo{ - TTL: 60 * time.Second, - }, - } - arg := expected - - // Copy it - dup, err := copystructure.Copy(&arg) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Check equality - arg2 := dup.(*logical.Request) - if !reflect.DeepEqual(*arg2, expected) { - t.Fatalf("bad:\n\n%#v\n\n%#v", *arg2, expected) - } -} - -func TestCopy_response(t *testing.T) { - // Make a non-pointer one so that it can't be modified directly - expected := logical.Response{ - Data: map[string]interface{}{ - "foo": "bar", - }, - WrapInfo: &wrapping.ResponseWrapInfo{ - TTL: 60, - Token: "foo", - CreationTime: time.Now(), - WrappedAccessor: "abcd1234", - }, - } - arg := expected - - // Copy it - dup, err := copystructure.Copy(&arg) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Check equality - arg2 := dup.(*logical.Response) - if !reflect.DeepEqual(*arg2, expected) { - t.Fatalf("bad:\n\n%#v\n\n%#v", *arg2, expected) - } -} - -func TestHashString(t *testing.T) { - inmemStorage := &logical.InmemStorage{} - inmemStorage.Put(context.Background(), &logical.StorageEntry{ - Key: "salt", - Value: []byte("foo"), - }) - localSalt, err := salt.NewSalt(context.Background(), inmemStorage, &salt.Config{ - HMAC: sha256.New, - HMACType: "hmac-sha256", - }) - if err != nil { - t.Fatalf("Error instantiating salt: %s", err) - } - out := HashString(localSalt, "foo") - if out != "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a" { - t.Fatalf("err: HashString output did not match expected") - } -} - -func TestHashAuth(t *testing.T) { - cases := []struct { - Input *logical.Auth - Output *logical.Auth - HMACAccessor bool - }{ - { - &logical.Auth{ClientToken: "foo"}, - &logical.Auth{ClientToken: "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a"}, - false, - }, - { - &logical.Auth{ - LeaseOptions: logical.LeaseOptions{ - TTL: 1 * time.Hour, - }, - - ClientToken: "foo", - }, - &logical.Auth{ - LeaseOptions: logical.LeaseOptions{ - TTL: 1 * time.Hour, - }, - - ClientToken: "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a", - }, - false, - }, - } - - inmemStorage := &logical.InmemStorage{} - inmemStorage.Put(context.Background(), &logical.StorageEntry{ - Key: "salt", - Value: []byte("foo"), - }) - localSalt, err := salt.NewSalt(context.Background(), inmemStorage, &salt.Config{ - HMAC: sha256.New, - HMACType: "hmac-sha256", - }) - if err != nil { - t.Fatalf("Error instantiating salt: %s", err) - } - for _, tc := range cases { - input := fmt.Sprintf("%#v", tc.Input) - out, err := HashAuth(localSalt, tc.Input, tc.HMACAccessor) - if err != nil { - t.Fatalf("err: %s\n\n%s", err, input) - } - if !reflect.DeepEqual(out, tc.Output) { - t.Fatalf("bad:\nInput:\n%s\nOutput:\n%#v\nExpected output:\n%#v", input, out, tc.Output) - } - } -} - -type testOptMarshaler struct { - S string - I int -} - -func (o *testOptMarshaler) MarshalJSONWithOptions(options *logical.MarshalOptions) ([]byte, error) { - return json.Marshal(&testOptMarshaler{S: options.ValueHasher(o.S), I: o.I}) -} - -var _ logical.OptMarshaler = &testOptMarshaler{} - -func TestHashRequest(t *testing.T) { - cases := []struct { - Input *logical.Request - Output *logical.Request - NonHMACDataKeys []string - HMACAccessor bool - }{ - { - &logical.Request{ - Data: map[string]interface{}{ - "foo": "bar", - "baz": "foobar", - "private_key_type": certutil.PrivateKeyType("rsa"), - "om": &testOptMarshaler{S: "bar", I: 1}, - }, - }, - &logical.Request{ - Data: map[string]interface{}{ - "foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", - "baz": "foobar", - "private_key_type": "hmac-sha256:995230dca56fffd310ff591aa404aab52b2abb41703c787cfa829eceb4595bf1", - "om": json.RawMessage(`{"S":"hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317","I":1}`), - }, - }, - []string{"baz"}, - false, - }, - } - - inmemStorage := &logical.InmemStorage{} - inmemStorage.Put(context.Background(), &logical.StorageEntry{ - Key: "salt", - Value: []byte("foo"), - }) - localSalt, err := salt.NewSalt(context.Background(), inmemStorage, &salt.Config{ - HMAC: sha256.New, - HMACType: "hmac-sha256", - }) - if err != nil { - t.Fatalf("Error instantiating salt: %s", err) - } - for _, tc := range cases { - input := fmt.Sprintf("%#v", tc.Input) - out, err := HashRequest(localSalt, tc.Input, tc.HMACAccessor, tc.NonHMACDataKeys) - if err != nil { - t.Fatalf("err: %s\n\n%s", err, input) - } - if diff := deep.Equal(out, tc.Output); len(diff) > 0 { - t.Fatalf("bad:\nInput:\n%s\nDiff:\n%#v", input, diff) - } - } -} - -func TestHashResponse(t *testing.T) { - now := time.Now() - - cases := []struct { - Input *logical.Response - Output *logical.Response - NonHMACDataKeys []string - HMACAccessor bool - }{ - { - &logical.Response{ - Data: map[string]interface{}{ - "foo": "bar", - "baz": "foobar", - // Responses can contain time values, so test that with - // a known fixed value. - "bar": now, - "om": &testOptMarshaler{S: "bar", I: 1}, - }, - WrapInfo: &wrapping.ResponseWrapInfo{ - TTL: 60, - Token: "bar", - Accessor: "flimflam", - CreationTime: now, - WrappedAccessor: "bar", - }, - }, - &logical.Response{ - Data: map[string]interface{}{ - "foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", - "baz": "foobar", - "bar": now.Format(time.RFC3339Nano), - "om": json.RawMessage(`{"S":"hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317","I":1}`), - }, - WrapInfo: &wrapping.ResponseWrapInfo{ - TTL: 60, - Token: "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", - Accessor: "hmac-sha256:7c9c6fe666d0af73b3ebcfbfabe6885015558213208e6635ba104047b22f6390", - CreationTime: now, - WrappedAccessor: "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", - }, - }, - []string{"baz"}, - true, - }, - } - - inmemStorage := &logical.InmemStorage{} - inmemStorage.Put(context.Background(), &logical.StorageEntry{ - Key: "salt", - Value: []byte("foo"), - }) - localSalt, err := salt.NewSalt(context.Background(), inmemStorage, &salt.Config{ - HMAC: sha256.New, - HMACType: "hmac-sha256", - }) - if err != nil { - t.Fatalf("Error instantiating salt: %s", err) - } - for _, tc := range cases { - input := fmt.Sprintf("%#v", tc.Input) - out, err := HashResponse(localSalt, tc.Input, tc.HMACAccessor, tc.NonHMACDataKeys, false) - if err != nil { - t.Fatalf("err: %s\n\n%s", err, input) - } - if diff := deep.Equal(out, tc.Output); len(diff) > 0 { - t.Fatalf("bad:\nInput:\n%s\nDiff:\n%#v", input, diff) - } - } -} - -func TestHashWalker(t *testing.T) { - replaceText := "foo" - - cases := []struct { - Input map[string]interface{} - Output map[string]interface{} - }{ - { - map[string]interface{}{ - "hello": "foo", - }, - map[string]interface{}{ - "hello": replaceText, - }, - }, - - { - map[string]interface{}{ - "hello": []interface{}{"world"}, - }, - map[string]interface{}{ - "hello": []interface{}{replaceText}, - }, - }, - } - - for _, tc := range cases { - err := HashStructure(tc.Input, func(string) string { - return replaceText - }, nil) - if err != nil { - t.Fatalf("err: %s\n\n%#v", err, tc.Input) - } - if !reflect.DeepEqual(tc.Input, tc.Output) { - t.Fatalf("bad:\n\n%#v\n\n%#v", tc.Input, tc.Output) - } - } -} - -func TestHashWalker_TimeStructs(t *testing.T) { - replaceText := "bar" - - now := time.Now() - cases := []struct { - Input map[string]interface{} - Output map[string]interface{} - }{ - // Should not touch map keys of type time.Time. - { - map[string]interface{}{ - "hello": map[time.Time]struct{}{ - now: {}, - }, - }, - map[string]interface{}{ - "hello": map[time.Time]struct{}{ - now: {}, - }, - }, - }, - // Should handle map values of type time.Time. - { - map[string]interface{}{ - "hello": now, - }, - map[string]interface{}{ - "hello": now.Format(time.RFC3339Nano), - }, - }, - // Should handle slice values of type time.Time. - { - map[string]interface{}{ - "hello": []interface{}{"foo", now, "foo2"}, - }, - map[string]interface{}{ - "hello": []interface{}{"foobar", now.Format(time.RFC3339Nano), "foo2bar"}, - }, - }, - } - - for _, tc := range cases { - err := HashStructure(tc.Input, func(s string) string { - return s + replaceText - }, nil) - if err != nil { - t.Fatalf("err: %v\n\n%#v", err, tc.Input) - } - if !reflect.DeepEqual(tc.Input, tc.Output) { - t.Fatalf("bad:\n\n%#v\n\n%#v", tc.Input, tc.Output) - } - } -} diff --git a/builtin/audit/file/backend_test.go b/builtin/audit/file/backend_test.go deleted file mode 100644 index 010a6bf8b..000000000 --- a/builtin/audit/file/backend_test.go +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package file - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "strconv" - "testing" - "time" - - "github.com/hashicorp/vault/audit" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/sdk/helper/salt" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestAuditFile_fileModeNew(t *testing.T) { - modeStr := "0777" - mode, err := strconv.ParseUint(modeStr, 8, 32) - if err != nil { - t.Fatal(err) - } - - path, err := ioutil.TempDir("", "vault-test_audit_file-file_mode_new") - if err != nil { - t.Fatal(err) - } - - defer os.RemoveAll(path) - - file := filepath.Join(path, "auditTest.txt") - - config := map[string]string{ - "path": file, - "mode": modeStr, - } - - _, err = Factory(context.Background(), &audit.BackendConfig{ - SaltConfig: &salt.Config{}, - SaltView: &logical.InmemStorage{}, - Config: config, - }) - if err != nil { - t.Fatal(err) - } - - info, err := os.Stat(file) - if err != nil { - t.Fatalf("Cannot retrieve file mode from `Stat`") - } - if info.Mode() != os.FileMode(mode) { - t.Fatalf("File mode does not match.") - } -} - -func TestAuditFile_fileModeExisting(t *testing.T) { - f, err := ioutil.TempFile("", "test") - if err != nil { - t.Fatalf("Failure to create test file.") - } - defer os.Remove(f.Name()) - - err = os.Chmod(f.Name(), 0o777) - if err != nil { - t.Fatalf("Failure to chmod temp file for testing.") - } - - err = f.Close() - if err != nil { - t.Fatalf("Failure to close temp file for test.") - } - - config := map[string]string{ - "path": f.Name(), - } - - _, err = Factory(context.Background(), &audit.BackendConfig{ - Config: config, - SaltConfig: &salt.Config{}, - SaltView: &logical.InmemStorage{}, - }) - if err != nil { - t.Fatal(err) - } - - info, err := os.Stat(f.Name()) - if err != nil { - t.Fatalf("cannot retrieve file mode from `Stat`") - } - if info.Mode() != os.FileMode(0o600) { - t.Fatalf("File mode does not match.") - } -} - -func TestAuditFile_fileMode0000(t *testing.T) { - f, err := ioutil.TempFile("", "test") - if err != nil { - t.Fatalf("Failure to create test file. The error is %v", err) - } - defer os.Remove(f.Name()) - - err = os.Chmod(f.Name(), 0o777) - if err != nil { - t.Fatalf("Failure to chmod temp file for testing. The error is %v", err) - } - - err = f.Close() - if err != nil { - t.Fatalf("Failure to close temp file for test. The error is %v", err) - } - - config := map[string]string{ - "path": f.Name(), - "mode": "0000", - } - - _, err = Factory(context.Background(), &audit.BackendConfig{ - Config: config, - SaltConfig: &salt.Config{}, - SaltView: &logical.InmemStorage{}, - }) - if err != nil { - t.Fatal(err) - } - - info, err := os.Stat(f.Name()) - if err != nil { - t.Fatalf("cannot retrieve file mode from `Stat`. The error is %v", err) - } - if info.Mode() != os.FileMode(0o777) { - t.Fatalf("File mode does not match.") - } -} - -func BenchmarkAuditFile_request(b *testing.B) { - config := map[string]string{ - "path": "/dev/null", - } - sink, err := Factory(context.Background(), &audit.BackendConfig{ - Config: config, - SaltConfig: &salt.Config{}, - SaltView: &logical.InmemStorage{}, - }) - if err != nil { - b.Fatal(err) - } - - in := &logical.LogInput{ - Auth: &logical.Auth{ - ClientToken: "foo", - Accessor: "bar", - EntityID: "foobarentity", - DisplayName: "testtoken", - NoDefaultPolicy: true, - Policies: []string{"root"}, - TokenType: logical.TokenTypeService, - }, - Request: &logical.Request{ - Operation: logical.UpdateOperation, - Path: "/foo", - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - WrapInfo: &logical.RequestWrapInfo{ - TTL: 60 * time.Second, - }, - Headers: map[string][]string{ - "foo": {"bar"}, - }, - }, - } - - ctx := namespace.RootContext(nil) - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - if err := sink.LogRequest(ctx, in); err != nil { - panic(err) - } - } - }) -} diff --git a/builtin/credential/approle/backend_test.go b/builtin/credential/approle/backend_test.go deleted file mode 100644 index a3dc68be0..000000000 --- a/builtin/credential/approle/backend_test.go +++ /dev/null @@ -1,430 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package approle - -import ( - "context" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/hashicorp/vault/sdk/logical" -) - -func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) { - t.Helper() - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - - b, err := Backend(config) - if err != nil { - t.Fatal(err) - } - if b == nil { - t.Fatalf("failed to create backend") - } - err = b.Backend.Setup(context.Background(), config) - if err != nil { - t.Fatal(err) - } - return b, config.StorageView -} - -func TestAppRole_RoleServiceToBatchNumUses(t *testing.T) { - b, s := createBackendWithStorage(t) - - requestFunc := func(operation logical.Operation, data map[string]interface{}) { - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/testrole", - Operation: operation, - Storage: s, - Data: data, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %#v\nresp: %#v", err, resp) - } - } - - data := map[string]interface{}{ - "bind_secret_id": true, - "secret_id_num_uses": 0, - "secret_id_ttl": "10m", - "token_policies": "policy", - "token_ttl": "5m", - "token_max_ttl": "10m", - "token_num_uses": 2, - "token_type": "default", - } - requestFunc(logical.CreateOperation, data) - - data["token_num_uses"] = 0 - data["token_type"] = "batch" - requestFunc(logical.UpdateOperation, data) - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/testrole/role-id", - Operation: logical.ReadOperation, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - roleID := resp.Data["role_id"] - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/testrole/secret-id", - Operation: logical.UpdateOperation, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - secretID := resp.Data["secret_id"] - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - require.NotNil(t, resp.Auth) -} - -func TestAppRole_RoleNameCaseSensitivity(t *testing.T) { - testFunc := func(t *testing.T, roleName string) { - var resp *logical.Response - var err error - b, s := createBackendWithStorage(t) - - // Create the role - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + roleName, - Operation: logical.CreateOperation, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) - } - - // Get the role-id - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + roleName + "/role-id", - Operation: logical.ReadOperation, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - roleID := resp.Data["role_id"] - - // Create a secret-id - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + roleName + "/secret-id", - Operation: logical.UpdateOperation, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - secretID := resp.Data["secret_id"] - secretIDAccessor := resp.Data["secret_id_accessor"] - - // Ensure login works - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - if resp.Auth == nil { - t.Fatalf("failed to perform login") - } - - // Destroy secret ID accessor - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + roleName + "/secret-id-accessor/destroy", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "secret_id_accessor": secretIDAccessor, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - - // Login again using the accessor's corresponding secret ID should fail - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil && err != logical.ErrInvalidCredentials { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected error due to invalid secret ID") - } - - // Generate another secret ID - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + roleName + "/secret-id", - Operation: logical.UpdateOperation, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - secretID = resp.Data["secret_id"] - secretIDAccessor = resp.Data["secret_id_accessor"] - - // Ensure login works - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - if resp.Auth == nil { - t.Fatalf("failed to perform login") - } - - // Destroy the secret ID - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + roleName + "/secret-id/destroy", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "secret_id": secretID, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - - // Login again using the same secret ID should fail - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil && err != logical.ErrInvalidCredentials { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected error due to invalid secret ID") - } - - // Generate another secret ID - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + roleName + "/secret-id", - Operation: logical.UpdateOperation, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - secretID = resp.Data["secret_id"] - secretIDAccessor = resp.Data["secret_id_accessor"] - - // Ensure login works - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - if resp.Auth == nil { - t.Fatalf("failed to perform login") - } - - // Destroy the secret ID using lower cased role name - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + strings.ToLower(roleName) + "/secret-id/destroy", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "secret_id": secretID, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - - // Login again using the same secret ID should fail - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil && err != logical.ErrInvalidCredentials { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected error due to invalid secret ID") - } - - // Generate another secret ID - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + roleName + "/secret-id", - Operation: logical.UpdateOperation, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - secretID = resp.Data["secret_id"] - secretIDAccessor = resp.Data["secret_id_accessor"] - - // Ensure login works - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - if resp.Auth == nil { - t.Fatalf("failed to perform login") - } - - // Destroy the secret ID using upper cased role name - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + strings.ToUpper(roleName) + "/secret-id/destroy", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "secret_id": secretID, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - - // Login again using the same secret ID should fail - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil && err != logical.ErrInvalidCredentials { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected error due to invalid secret ID") - } - - // Generate another secret ID - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/" + roleName + "/secret-id", - Operation: logical.UpdateOperation, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - secretID = resp.Data["secret_id"] - secretIDAccessor = resp.Data["secret_id_accessor"] - - // Ensure login works - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - if resp.Auth == nil { - t.Fatalf("failed to perform login") - } - - // Destroy the secret ID using mixed case name - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/saMpleRolEnaMe/secret-id/destroy", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "secret_id": secretID, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - - // Login again using the same secret ID should fail - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - }) - if err != nil && err != logical.ErrInvalidCredentials { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected error due to invalid secret ID") - } - } - - // Lower case role name - testFunc(t, "samplerolename") - // Upper case role name - testFunc(t, "SAMPLEROLENAME") - // Mixed case role name - testFunc(t, "SampleRoleName") -} diff --git a/builtin/credential/approle/path_login_test.go b/builtin/credential/approle/path_login_test.go deleted file mode 100644 index 7dd8c7f0f..000000000 --- a/builtin/credential/approle/path_login_test.go +++ /dev/null @@ -1,360 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package approle - -import ( - "context" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestAppRole_BoundCIDRLogin(t *testing.T) { - var resp *logical.Response - var err error - b, s := createBackendWithStorage(t) - - // Create a role with secret ID binding disabled and only bound cidr list - // enabled - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole", - Operation: logical.CreateOperation, - Data: map[string]interface{}{ - "bind_secret_id": false, - "bound_cidr_list": []string{"127.0.0.1/8"}, - "token_bound_cidrs": []string{"10.0.0.0/8"}, - }, - Storage: s, - }) - - // Read the role ID - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole/role-id", - Operation: logical.ReadOperation, - Storage: s, - }) - - roleID := resp.Data["role_id"] - - // Fill in the connection information and login with just the role ID - resp = b.requestNoErr(t, &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - }, - Storage: s, - Connection: &logical.Connection{RemoteAddr: "127.0.0.1"}, - }) - - if resp.Auth == nil { - t.Fatal("expected login to succeed") - } - if len(resp.Auth.BoundCIDRs) != 1 { - t.Fatal("bad token bound cidrs") - } - if resp.Auth.BoundCIDRs[0].String() != "10.0.0.0/8" { - t.Fatalf("bad: %s", resp.Auth.BoundCIDRs[0].String()) - } - - // Override with a secret-id value, verify it doesn't pass - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "bind_secret_id": true, - }, - Storage: s, - }) - - roleSecretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/testrole/secret-id", - Storage: s, - Data: map[string]interface{}{ - "token_bound_cidrs": []string{"11.0.0.0/24"}, - }, - } - resp, err = b.HandleRequest(context.Background(), roleSecretIDReq) - if err == nil { - t.Fatal("expected error due to mismatching subnet relationship") - } - - roleSecretIDReq.Data["token_bound_cidrs"] = "10.0.0.0/24" - resp = b.requestNoErr(t, roleSecretIDReq) - - secretID := resp.Data["secret_id"] - - resp = b.requestNoErr(t, &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - Storage: s, - Connection: &logical.Connection{RemoteAddr: "127.0.0.1"}, - }) - - if resp.Auth == nil { - t.Fatal("expected login to succeed") - } - if len(resp.Auth.BoundCIDRs) != 1 { - t.Fatal("bad token bound cidrs") - } - if resp.Auth.BoundCIDRs[0].String() != "10.0.0.0/24" { - t.Fatalf("bad: %s", resp.Auth.BoundCIDRs[0].String()) - } -} - -func TestAppRole_RoleLogin(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - createRole(t, b, storage, "role1", "a,b,c") - roleRoleIDReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "role/role1/role-id", - Storage: storage, - } - resp = b.requestNoErr(t, roleRoleIDReq) - - roleID := resp.Data["role_id"] - - roleSecretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/role1/secret-id", - Storage: storage, - } - resp = b.requestNoErr(t, roleSecretIDReq) - - secretID := resp.Data["secret_id"] - - loginData := map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - } - loginReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Storage: storage, - Data: loginData, - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - } - loginResp, err := b.HandleRequest(context.Background(), loginReq) - if err != nil || (loginResp != nil && loginResp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, loginResp) - } - - if loginResp.Auth == nil { - t.Fatalf("expected a non-nil auth object in the response") - } - - if loginResp.Auth.Metadata == nil { - t.Fatalf("expected a non-nil metadata object in the response") - } - - if val := loginResp.Auth.Metadata["role_name"]; val != "role1" { - t.Fatalf("expected metadata.role_name to equal 'role1', got: %v", val) - } - - if loginResp.Auth.Alias.Metadata == nil { - t.Fatalf("expected a non-nil alias metadata object in the response") - } - - if val := loginResp.Auth.Alias.Metadata["role_name"]; val != "role1" { - t.Fatalf("expected metadata.alias.role_name to equal 'role1', got: %v", val) - } - - // Test renewal - renewReq := generateRenewRequest(storage, loginResp.Auth) - - resp, err = b.HandleRequest(context.Background(), renewReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Auth.TTL != 400*time.Second { - t.Fatalf("expected period value from response to be 400s, got: %s", resp.Auth.TTL) - } - - /// - // Test renewal with period - /// - - // Create role - period := 600 * time.Second - roleData := map[string]interface{}{ - "policies": "a,b,c", - "period": period.String(), - } - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/" + "role-period", - Storage: storage, - Data: roleData, - } - resp = b.requestNoErr(t, roleReq) - - roleRoleIDReq = &logical.Request{ - Operation: logical.ReadOperation, - Path: "role/role-period/role-id", - Storage: storage, - } - resp = b.requestNoErr(t, roleRoleIDReq) - - roleID = resp.Data["role_id"] - - roleSecretIDReq = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/role-period/secret-id", - Storage: storage, - } - resp = b.requestNoErr(t, roleSecretIDReq) - - secretID = resp.Data["secret_id"] - - loginData["role_id"] = roleID - loginData["secret_id"] = secretID - - loginResp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil || (loginResp != nil && loginResp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, loginResp) - } - - if loginResp.Auth == nil { - t.Fatalf("expected a non-nil auth object in the response") - } - - renewReq = generateRenewRequest(storage, loginResp.Auth) - - resp, err = b.HandleRequest(context.Background(), renewReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Auth.Period != period { - t.Fatalf("expected period value of %d in the response, got: %s", period, resp.Auth.Period) - } - - // Test input validation with secret_id that exceeds max length - loginData["secret_id"] = strings.Repeat("a", maxHmacInputLength+1) - - loginReq = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Storage: storage, - Data: loginData, - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - } - - loginResp, err = b.HandleRequest(context.Background(), loginReq) - - expectedErr := "failed to create HMAC of secret_id" - if loginResp != nil || err == nil || !strings.Contains(err.Error(), expectedErr) { - t.Fatalf("expected login test to fail with error %q, resp: %#v, err: %v", expectedErr, loginResp, err) - } -} - -func generateRenewRequest(s logical.Storage, auth *logical.Auth) *logical.Request { - renewReq := &logical.Request{ - Operation: logical.RenewOperation, - Storage: s, - Auth: &logical.Auth{}, - } - renewReq.Auth.InternalData = auth.InternalData - renewReq.Auth.Metadata = auth.Metadata - renewReq.Auth.LeaseOptions = auth.LeaseOptions - renewReq.Auth.Policies = auth.Policies - renewReq.Auth.Period = auth.Period - - return renewReq -} - -func TestAppRole_RoleResolve(t *testing.T) { - b, storage := createBackendWithStorage(t) - - role := "role1" - createRole(t, b, storage, role, "a,b,c") - roleRoleIDReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "role/role1/role-id", - Storage: storage, - } - resp := b.requestNoErr(t, roleRoleIDReq) - - roleID := resp.Data["role_id"] - - roleSecretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/role1/secret-id", - Storage: storage, - } - resp = b.requestNoErr(t, roleSecretIDReq) - - secretID := resp.Data["secret_id"] - - loginData := map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - } - loginReq := &logical.Request{ - Operation: logical.ResolveRoleOperation, - Path: "login", - Storage: storage, - Data: loginData, - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - } - - resp = b.requestNoErr(t, loginReq) - - if resp.Data["role"] != role { - t.Fatalf("Role was not as expected. Expected %s, received %s", role, resp.Data["role"]) - } -} - -func TestAppRole_RoleDoesNotExist(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - roleID := "roleDoesNotExist" - - loginData := map[string]interface{}{ - "role_id": roleID, - "secret_id": "secret", - } - loginReq := &logical.Request{ - Operation: logical.ResolveRoleOperation, - Path: "login", - Storage: storage, - Data: loginData, - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - } - - resp, err = b.HandleRequest(context.Background(), loginReq) - if resp == nil && !resp.IsError() { - t.Fatalf("Response was not an error: err:%v resp:%#v", err, resp) - } - - errString, ok := resp.Data["error"].(string) - if !ok { - t.Fatal("Error not part of response.") - } - - if !strings.Contains(errString, "invalid role or secret ID") { - t.Fatalf("Error was not due to invalid role ID. Error: %s", errString) - } -} diff --git a/builtin/credential/approle/path_role_test.go b/builtin/credential/approle/path_role_test.go deleted file mode 100644 index e0eba31f7..000000000 --- a/builtin/credential/approle/path_role_test.go +++ /dev/null @@ -1,2134 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package approle - -import ( - "context" - "encoding/json" - "fmt" - "reflect" - "strings" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/hashicorp/go-sockaddr" - "github.com/hashicorp/vault/sdk/helper/policyutil" - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - "github.com/hashicorp/vault/sdk/helper/tokenutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/mapstructure" -) - -func (b *backend) requestNoErr(t *testing.T, req *logical.Request) *logical.Response { - t.Helper() - resp, err := b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route(req.Path), req.Operation), resp, true) - return resp -} - -func TestAppRole_LocalSecretIDsRead(t *testing.T) { - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "local_secret_ids": true, - "bind_secret_id": true, - } - - b.requestNoErr(t, &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/testrole", - Storage: storage, - Data: roleData, - }) - - resp := b.requestNoErr(t, &logical.Request{ - Operation: logical.ReadOperation, - Storage: storage, - Path: "role/testrole/local-secret-ids", - }) - - if !resp.Data["local_secret_ids"].(bool) { - t.Fatalf("expected local_secret_ids to be returned") - } -} - -func TestAppRole_LocalNonLocalSecretIDs(t *testing.T) { - b, storage := createBackendWithStorage(t) - - // Create a role with local_secret_ids set - resp := b.requestNoErr(t, &logical.Request{ - Path: "role/testrole1", - Operation: logical.CreateOperation, - Storage: storage, - Data: map[string]interface{}{ - "policies": []string{"default", "role1policy"}, - "bind_secret_id": true, - "local_secret_ids": true, - }, - }) - - // Create another role without setting local_secret_ids - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole2", - Operation: logical.CreateOperation, - Storage: storage, - Data: map[string]interface{}{ - "policies": []string{"default", "role1policy"}, - "bind_secret_id": true, - }, - }) - - count := 10 - // Create secret IDs on testrole1 - for i := 0; i < count; i++ { - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole1/secret-id", - Operation: logical.UpdateOperation, - Storage: storage, - }) - } - - // Check the number of secret IDs generated - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole1/secret-id", - Operation: logical.ListOperation, - Storage: storage, - }) - - if len(resp.Data["keys"].([]string)) != count { - t.Fatalf("failed to list secret IDs") - } - - // Create secret IDs on testrole1 - for i := 0; i < count; i++ { - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole2/secret-id", - Operation: logical.UpdateOperation, - Storage: storage, - }) - } - - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole2/secret-id", - Operation: logical.ListOperation, - Storage: storage, - }) - - if len(resp.Data["keys"].([]string)) != count { - t.Fatalf("failed to list secret IDs") - } -} - -func TestAppRole_UpgradeSecretIDPrefix(t *testing.T) { - var resp *logical.Response - var err error - - b, storage := createBackendWithStorage(t) - - // Create a role entry directly in storage without SecretIDPrefix - err = b.setRoleEntry(context.Background(), storage, "testrole", &roleStorageEntry{ - RoleID: "testroleid", - HMACKey: "testhmackey", - Policies: []string{"default"}, - BindSecretID: true, - BoundCIDRListOld: "127.0.0.1/18,192.178.1.2/24", - }, "") - if err != nil { - t.Fatal(err) - } - - // Reading the role entry should upgrade it to contain SecretIDPrefix - role, err := b.roleEntry(context.Background(), storage, "testrole") - if err != nil { - t.Fatal(err) - } - if role.SecretIDPrefix == "" { - t.Fatalf("expected SecretIDPrefix to be set") - } - - // Ensure that the API response contains local_secret_ids - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole", - Operation: logical.ReadOperation, - Storage: storage, - }) - - _, ok := resp.Data["local_secret_ids"] - if !ok { - t.Fatalf("expected local_secret_ids to be present in the response") - } -} - -func TestAppRole_LocalSecretIDImmutability(t *testing.T) { - var resp *logical.Response - var err error - - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "policies": []string{"default"}, - "bind_secret_id": true, - "bound_cidr_list": []string{"127.0.0.1/18", "192.178.1.2/24"}, - "local_secret_ids": true, - } - - // Create a role with local_secret_ids set - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole", - Operation: logical.CreateOperation, - Storage: storage, - Data: roleData, - }) - - // Attempt to modify local_secret_ids should fail - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "role/testrole", - Operation: logical.UpdateOperation, - Storage: storage, - Data: roleData, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected an error since local_secret_ids can't be overwritten") - } -} - -func TestAppRole_UpgradeBoundCIDRList(t *testing.T) { - var resp *logical.Response - var err error - - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "policies": []string{"default"}, - "bind_secret_id": true, - "bound_cidr_list": []string{"127.0.0.1/18", "192.178.1.2/24"}, - } - - // Create a role with bound_cidr_list set - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole", - Operation: logical.CreateOperation, - Storage: storage, - Data: roleData, - }) - - // Read the role and check that the bound_cidr_list is set properly - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole", - Operation: logical.ReadOperation, - Storage: storage, - }) - - expected := []string{"127.0.0.1/18", "192.178.1.2/24"} - actual := resp.Data["secret_id_bound_cidrs"].([]string) - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("bad: secret_id_bound_cidrs; expected: %#v\nactual: %#v\n", expected, actual) - } - - // Modify the storage entry of the role to hold the old style string typed bound_cidr_list - role := &roleStorageEntry{ - RoleID: "testroleid", - HMACKey: "testhmackey", - Policies: []string{"default"}, - BindSecretID: true, - BoundCIDRListOld: "127.0.0.1/18,192.178.1.2/24", - SecretIDPrefix: secretIDPrefix, - } - err = b.setRoleEntry(context.Background(), storage, "testrole", role, "") - if err != nil { - t.Fatal(err) - } - - // Read the role. The upgrade code should have migrated the old type to the new type - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole", - Operation: logical.ReadOperation, - Storage: storage, - }) - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("bad: bound_cidr_list; expected: %#v\nactual: %#v\n", expected, actual) - } - - // Create a secret-id by supplying a subset of the role's CIDR blocks with the new type - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole/secret-id", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "cidr_list": []string{"127.0.0.1/24"}, - }, - }) - - if resp.Data["secret_id"].(string) == "" { - t.Fatalf("failed to generate secret-id") - } - - // Check that the backwards compatibility for the string type is not broken - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrole/secret-id", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "cidr_list": "127.0.0.1/24", - }, - }) - - if resp.Data["secret_id"].(string) == "" { - t.Fatalf("failed to generate secret-id") - } -} - -func TestAppRole_RoleNameLowerCasing(t *testing.T) { - var resp *logical.Response - var err error - var roleID, secretID string - - b, storage := createBackendWithStorage(t) - - // Save a role with out LowerCaseRoleName set - role := &roleStorageEntry{ - RoleID: "testroleid", - HMACKey: "testhmackey", - Policies: []string{"default"}, - BindSecretID: true, - SecretIDPrefix: secretIDPrefix, - } - err = b.setRoleEntry(context.Background(), storage, "testRoleName", role, "") - if err != nil { - t.Fatal(err) - } - - secretIDReq := &logical.Request{ - Path: "role/testRoleName/secret-id", - Operation: logical.UpdateOperation, - Storage: storage, - } - resp = b.requestNoErr(t, secretIDReq) - - secretID = resp.Data["secret_id"].(string) - roleID = "testroleid" - - // Regular login flow. This should succeed. - resp = b.requestNoErr(t, &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - }) - - // Lower case the role name when generating the secret id - secretIDReq.Path = "role/testrolename/secret-id" - resp = b.requestNoErr(t, secretIDReq) - - secretID = resp.Data["secret_id"].(string) - - // Login should fail - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - }) - if err != nil && err != logical.ErrInvalidCredentials { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected an error") - } - - // Delete the role and create it again. This time don't directly persist - // it, but route the request to the creation handler so that it sets the - // LowerCaseRoleName to true. - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testRoleName", - Operation: logical.DeleteOperation, - Storage: storage, - }) - - roleReq := &logical.Request{ - Path: "role/testRoleName", - Operation: logical.CreateOperation, - Storage: storage, - Data: map[string]interface{}{ - "bind_secret_id": true, - }, - } - resp = b.requestNoErr(t, roleReq) - - // Create secret id with lower cased role name - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrolename/secret-id", - Operation: logical.UpdateOperation, - Storage: storage, - }) - - secretID = resp.Data["secret_id"].(string) - - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrolename/role-id", - Operation: logical.ReadOperation, - Storage: storage, - }) - - roleID = resp.Data["role_id"].(string) - - // Login should pass - resp = b.requestNoErr(t, &logical.Request{ - Path: "login", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "role_id": roleID, - "secret_id": secretID, - }, - }) - - // Lookup of secret ID should work in case-insensitive manner - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrolename/secret-id/lookup", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "secret_id": secretID, - }, - }) - - if resp == nil { - t.Fatalf("failed to lookup secret IDs") - } - - // Listing of secret IDs should work in case-insensitive manner - resp = b.requestNoErr(t, &logical.Request{ - Path: "role/testrolename/secret-id", - Operation: logical.ListOperation, - Storage: storage, - }) - - if len(resp.Data["keys"].([]string)) != 1 { - t.Fatalf("failed to list secret IDs") - } -} - -func TestAppRole_RoleReadSetIndex(t *testing.T) { - var resp *logical.Response - var err error - - b, storage := createBackendWithStorage(t) - - roleReq := &logical.Request{ - Path: "role/testrole", - Operation: logical.CreateOperation, - Storage: storage, - Data: map[string]interface{}{ - "bind_secret_id": true, - }, - } - - // Create a role - resp = b.requestNoErr(t, roleReq) - - roleIDReq := &logical.Request{ - Path: "role/testrole/role-id", - Operation: logical.ReadOperation, - Storage: storage, - } - - // Get the role ID - resp = b.requestNoErr(t, roleIDReq) - - roleID := resp.Data["role_id"].(string) - - // Delete the role ID index - err = b.roleIDEntryDelete(context.Background(), storage, roleID) - if err != nil { - t.Fatal(err) - } - - // Read the role again. This should add the index and return a warning - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - // Check if the warning is being returned - if !strings.Contains(resp.Warnings[0], "Role identifier was missing an index back to role name.") { - t.Fatalf("bad: expected a warning in the response") - } - - roleIDIndex, err := b.roleIDEntry(context.Background(), storage, roleID) - if err != nil { - t.Fatal(err) - } - - // Check if the index has been successfully created - if roleIDIndex == nil || roleIDIndex.Name != "testrole" { - t.Fatalf("bad: expected role to have an index") - } - - roleReq.Operation = logical.UpdateOperation - roleReq.Data = map[string]interface{}{ - "bind_secret_id": true, - "policies": "default", - } - - // Check if updating and reading of roles work and that there are no lock - // contentions dangling due to previous operation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) -} - -func TestAppRole_CIDRSubset(t *testing.T) { - var resp *logical.Response - var err error - - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "role_id": "role-id-123", - "policies": "a,b", - "bound_cidr_list": "127.0.0.1/24", - } - - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/testrole1", - Storage: storage, - Data: roleData, - } - - resp = b.requestNoErr(t, roleReq) - - secretIDData := map[string]interface{}{ - "cidr_list": "127.0.0.1/16", - } - secretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/testrole1/secret-id", - Data: secretIDData, - } - - resp, err = b.HandleRequest(context.Background(), secretIDReq) - if resp != nil { - t.Fatalf("resp:%#v", resp) - } - if err == nil { - t.Fatal("expected an error") - } - - roleData["bound_cidr_list"] = "192.168.27.29/16,172.245.30.40/24,10.20.30.40/30" - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - secretIDData["cidr_list"] = "192.168.27.29/20,172.245.30.40/25,10.20.30.40/32" - resp = b.requestNoErr(t, secretIDReq) -} - -func TestAppRole_TokenBoundCIDRSubset32Mask(t *testing.T) { - var resp *logical.Response - var err error - - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "role_id": "role-id-123", - "policies": "a,b", - "token_bound_cidrs": "127.0.0.1/32", - } - - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/testrole1", - Storage: storage, - Data: roleData, - } - - resp = b.requestNoErr(t, roleReq) - - secretIDData := map[string]interface{}{ - "token_bound_cidrs": "127.0.0.1/32", - } - secretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/testrole1/secret-id", - Data: secretIDData, - } - - resp = b.requestNoErr(t, secretIDReq) - - secretIDData = map[string]interface{}{ - "token_bound_cidrs": "127.0.0.1/24", - } - secretIDReq = &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/testrole1/secret-id", - Data: secretIDData, - } - - resp, err = b.HandleRequest(context.Background(), secretIDReq) - if resp != nil { - t.Fatalf("resp:%#v", resp) - } - - if err == nil { - t.Fatal("expected an error") - } -} - -func TestAppRole_RoleConstraints(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "role_id": "role-id-123", - "policies": "a,b", - } - - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/testrole1", - Storage: storage, - Data: roleData, - } - - // Set bind_secret_id, which is enabled by default - resp = b.requestNoErr(t, roleReq) - - // Set bound_cidr_list alone by explicitly disabling bind_secret_id - roleReq.Operation = logical.UpdateOperation - roleData["bind_secret_id"] = false - roleData["bound_cidr_list"] = "0.0.0.0/0" - resp = b.requestNoErr(t, roleReq) - - // Remove both constraints - roleReq.Operation = logical.UpdateOperation - roleData["bound_cidr_list"] = "" - roleData["bind_secret_id"] = false - resp, err = b.HandleRequest(context.Background(), roleReq) - if resp != nil && resp.IsError() { - t.Fatalf("err:%v, resp:%#v", err, resp) - } - if err == nil { - t.Fatalf("expected an error") - } -} - -func TestAppRole_RoleIDUpdate(t *testing.T) { - var resp *logical.Response - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "role_id": "role-id-123", - "policies": "a,b", - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - } - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/testrole1", - Storage: storage, - Data: roleData, - } - resp = b.requestNoErr(t, roleReq) - - roleIDUpdateReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/testrole1/role-id", - Storage: storage, - Data: map[string]interface{}{ - "role_id": "customroleid", - }, - } - resp = b.requestNoErr(t, roleIDUpdateReq) - - secretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/testrole1/secret-id", - } - resp = b.requestNoErr(t, secretIDReq) - - secretID := resp.Data["secret_id"].(string) - - loginData := map[string]interface{}{ - "role_id": "customroleid", - "secret_id": secretID, - } - loginReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Storage: storage, - Data: loginData, - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - } - resp = b.requestNoErr(t, loginReq) - - if resp.Auth == nil { - t.Fatalf("expected a non-nil auth object in the response") - } -} - -func TestAppRole_RoleIDUniqueness(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "role_id": "role-id-123", - "policies": "a,b", - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - } - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/testrole1", - Storage: storage, - Data: roleData, - } - - resp = b.requestNoErr(t, roleReq) - - roleReq.Path = "role/testrole2" - resp, err = b.HandleRequest(context.Background(), roleReq) - if err == nil && !(resp != nil && resp.IsError()) { - t.Fatalf("expected an error: got resp:%#v", resp) - } - - roleData["role_id"] = "role-id-456" - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.UpdateOperation - roleData["role_id"] = "role-id-123" - resp, err = b.HandleRequest(context.Background(), roleReq) - if err == nil && !(resp != nil && resp.IsError()) { - t.Fatalf("expected an error: got resp:%#v", resp) - } - - roleReq.Path = "role/testrole1" - roleData["role_id"] = "role-id-456" - resp, err = b.HandleRequest(context.Background(), roleReq) - if err == nil && !(resp != nil && resp.IsError()) { - t.Fatalf("expected an error: got resp:%#v", resp) - } - - roleIDData := map[string]interface{}{ - "role_id": "role-id-456", - } - roleIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/testrole1/role-id", - Storage: storage, - Data: roleIDData, - } - resp, err = b.HandleRequest(context.Background(), roleIDReq) - if err == nil && !(resp != nil && resp.IsError()) { - t.Fatalf("expected an error: got resp:%#v", resp) - } - - roleIDData["role_id"] = "role-id-123" - roleIDReq.Path = "role/testrole2/role-id" - resp, err = b.HandleRequest(context.Background(), roleIDReq) - if err == nil && !(resp != nil && resp.IsError()) { - t.Fatalf("expected an error: got resp:%#v", resp) - } - - roleIDData["role_id"] = "role-id-2000" - resp = b.requestNoErr(t, roleIDReq) - - roleIDData["role_id"] = "role-id-1000" - roleIDReq.Path = "role/testrole1/role-id" - resp = b.requestNoErr(t, roleIDReq) -} - -func TestAppRole_RoleDeleteSecretID(t *testing.T) { - var resp *logical.Response - b, storage := createBackendWithStorage(t) - - createRole(t, b, storage, "role1", "a,b") - secretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1/secret-id", - } - // Create 3 secrets on the role - resp = b.requestNoErr(t, secretIDReq) - resp = b.requestNoErr(t, secretIDReq) - resp = b.requestNoErr(t, secretIDReq) - - listReq := &logical.Request{ - Operation: logical.ListOperation, - Storage: storage, - Path: "role/role1/secret-id", - } - resp = b.requestNoErr(t, listReq) - - secretIDAccessors := resp.Data["keys"].([]string) - if len(secretIDAccessors) != 3 { - t.Fatalf("bad: len of secretIDAccessors: expected:3 actual:%d", len(secretIDAccessors)) - } - - roleReq := &logical.Request{ - Operation: logical.DeleteOperation, - Storage: storage, - Path: "role/role1", - } - resp = b.requestNoErr(t, roleReq) - - resp, err := b.HandleRequest(context.Background(), listReq) - if err != nil || resp == nil || (resp != nil && !resp.IsError()) { - t.Fatalf("expected an error. err:%v resp:%#v", err, resp) - } -} - -func TestAppRole_RoleSecretIDReadDelete(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - createRole(t, b, storage, "role1", "a,b") - secretIDCreateReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1/secret-id", - } - resp = b.requestNoErr(t, secretIDCreateReq) - - secretID := resp.Data["secret_id"].(string) - if secretID == "" { - t.Fatal("expected non empty secret ID") - } - - secretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1/secret-id/lookup", - Data: map[string]interface{}{ - "secret_id": secretID, - }, - } - resp = b.requestNoErr(t, secretIDReq) - - if resp.Data == nil { - t.Fatal(err) - } - - deleteSecretIDReq := &logical.Request{ - Operation: logical.DeleteOperation, - Storage: storage, - Path: "role/role1/secret-id/destroy", - Data: map[string]interface{}{ - "secret_id": secretID, - }, - } - resp = b.requestNoErr(t, deleteSecretIDReq) - resp, err = b.HandleRequest(context.Background(), secretIDReq) - if resp != nil && resp.IsError() { - t.Fatalf("error response:%#v", resp) - } - if err != nil { - t.Fatal(err) - } -} - -func TestAppRole_RoleSecretIDAccessorReadDelete(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - createRole(t, b, storage, "role1", "a,b") - secretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1/secret-id", - } - resp = b.requestNoErr(t, secretIDReq) - - listReq := &logical.Request{ - Operation: logical.ListOperation, - Storage: storage, - Path: "role/role1/secret-id", - } - resp = b.requestNoErr(t, listReq) - - hmacSecretID := resp.Data["keys"].([]string)[0] - - hmacReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1/secret-id-accessor/lookup", - Data: map[string]interface{}{ - "secret_id_accessor": hmacSecretID, - }, - } - resp = b.requestNoErr(t, hmacReq) - - if resp.Data == nil { - t.Fatal(err) - } - - hmacReq.Path = "role/role1/secret-id-accessor/destroy" - resp = b.requestNoErr(t, hmacReq) - - hmacReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), hmacReq) - if resp != nil && resp.IsError() { - t.Fatalf("err:%v resp:%#v", err, resp) - } - if err == nil { - t.Fatalf("expected an error") - } -} - -func TestAppRoleSecretIDLookup(t *testing.T) { - b, storage := createBackendWithStorage(t) - createRole(t, b, storage, "role1", "a,b") - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1/secret-id-accessor/lookup", - Data: map[string]interface{}{ - "secret_id_accessor": "invalid", - }, - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - expected := &logical.Response{ - Data: map[string]interface{}{ - "http_content_type": "application/json", - "http_raw_body": `{"request_id":"","lease_id":"","renewable":false,"lease_duration":0,"data":{"error":"failed to find accessor entry for secret_id_accessor: \"invalid\""},"wrap_info":null,"warnings":null,"auth":null}`, - "http_status_code": 404, - }, - } - if !reflect.DeepEqual(resp, expected) { - t.Fatalf("resp:%#v expected:%#v", resp, expected) - } -} - -func TestAppRoleRoleListSecretID(t *testing.T) { - var resp *logical.Response - b, storage := createBackendWithStorage(t) - - createRole(t, b, storage, "role1", "a,b") - - secretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1/secret-id", - } - // Create 5 'secret_id's - resp = b.requestNoErr(t, secretIDReq) - resp = b.requestNoErr(t, secretIDReq) - resp = b.requestNoErr(t, secretIDReq) - resp = b.requestNoErr(t, secretIDReq) - resp = b.requestNoErr(t, secretIDReq) - - listReq := &logical.Request{ - Operation: logical.ListOperation, - Storage: storage, - Path: "role/role1/secret-id/", - } - resp = b.requestNoErr(t, listReq) - - secrets := resp.Data["keys"].([]string) - if len(secrets) != 5 { - t.Fatalf("bad: len of secrets: expected:5 actual:%d", len(secrets)) - } -} - -func TestAppRole_RoleList(t *testing.T) { - var resp *logical.Response - b, storage := createBackendWithStorage(t) - - createRole(t, b, storage, "role1", "a,b") - createRole(t, b, storage, "role2", "c,d") - createRole(t, b, storage, "role3", "e,f") - createRole(t, b, storage, "role4", "g,h") - createRole(t, b, storage, "role5", "i,j") - - listReq := &logical.Request{ - Operation: logical.ListOperation, - Path: "role", - Storage: storage, - } - resp = b.requestNoErr(t, listReq) - - actual := resp.Data["keys"].([]string) - expected := []string{"role1", "role2", "role3", "role4", "role5"} - if !policyutil.EquivalentPolicies(actual, expected) { - t.Fatalf("bad: listed roles: expected:%s\nactual:%s", expected, actual) - } -} - -func TestAppRole_RoleSecretIDWithoutFields(t *testing.T) { - var resp *logical.Response - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "policies": "p,q,r,s", - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - } - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/role1", - Storage: storage, - Data: roleData, - } - - resp = b.requestNoErr(t, roleReq) - - roleSecretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/role1/secret-id", - Storage: storage, - } - resp = b.requestNoErr(t, roleSecretIDReq) - - if resp.Data["secret_id"].(string) == "" { - t.Fatalf("failed to generate secret_id") - } - if resp.Data["secret_id_ttl"].(int64) != int64(roleData["secret_id_ttl"].(int)) { - t.Fatalf("secret_id_ttl has not defaulted to the role's secret id ttl") - } - if resp.Data["secret_id_num_uses"].(int) != roleData["secret_id_num_uses"].(int) { - t.Fatalf("secret_id_num_uses has not defaulted to the role's secret id num_uses") - } - - roleSecretIDReq.Path = "role/role1/custom-secret-id" - roleCustomSecretIDData := map[string]interface{}{ - "secret_id": "abcd123", - } - roleSecretIDReq.Data = roleCustomSecretIDData - resp = b.requestNoErr(t, roleSecretIDReq) - - if resp.Data["secret_id"] != "abcd123" { - t.Fatalf("failed to set specific secret_id to role") - } - if resp.Data["secret_id_ttl"].(int64) != int64(roleData["secret_id_ttl"].(int)) { - t.Fatalf("secret_id_ttl has not defaulted to the role's secret id ttl") - } - if resp.Data["secret_id_num_uses"].(int) != roleData["secret_id_num_uses"].(int) { - t.Fatalf("secret_id_num_uses has not defaulted to the role's secret id num_uses") - } -} - -func TestAppRole_RoleSecretIDWithValidFields(t *testing.T) { - type testCase struct { - name string - payload map[string]interface{} - } - - var resp *logical.Response - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "policies": "p,q,r,s", - "secret_id_num_uses": 0, - "secret_id_ttl": 0, - "token_ttl": 400, - "token_max_ttl": 500, - } - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/role1", - Storage: storage, - Data: roleData, - } - - resp = b.requestNoErr(t, roleReq) - - testCases := []testCase{ - { - name: "finite num_uses ttl", - payload: map[string]interface{}{"secret_id": "finite", "ttl": 5, "num_uses": 5}, - }, - { - name: "infinite num_uses and ttl", - payload: map[string]interface{}{"secret_id": "infinite", "ttl": 0, "num_uses": 0}, - }, - { - name: "finite num_uses and infinite ttl", - payload: map[string]interface{}{"secret_id": "mixed1", "ttl": 0, "num_uses": 5}, - }, - { - name: "infinite num_uses and finite ttl", - payload: map[string]interface{}{"secret_id": "mixed2", "ttl": 5, "num_uses": 0}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - roleSecretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/role1/secret-id", - Storage: storage, - } - roleCustomSecretIDData := tc.payload - roleSecretIDReq.Data = roleCustomSecretIDData - - resp = b.requestNoErr(t, roleSecretIDReq) - - if resp.Data["secret_id"].(string) == "" { - t.Fatalf("failed to generate secret_id") - } - if resp.Data["secret_id_ttl"].(int64) != int64(tc.payload["ttl"].(int)) { - t.Fatalf("secret_id_ttl has not been set by the 'ttl' field") - } - if resp.Data["secret_id_num_uses"].(int) != tc.payload["num_uses"].(int) { - t.Fatalf("secret_id_num_uses has not been set by the 'num_uses' field") - } - - roleSecretIDReq.Path = "role/role1/custom-secret-id" - roleSecretIDReq.Data = roleCustomSecretIDData - resp = b.requestNoErr(t, roleSecretIDReq) - - if resp.Data["secret_id"] != tc.payload["secret_id"] { - t.Fatalf("failed to set specific secret_id to role") - } - if resp.Data["secret_id_ttl"].(int64) != int64(tc.payload["ttl"].(int)) { - t.Fatalf("secret_id_ttl has not been set by the 'ttl' field") - } - if resp.Data["secret_id_num_uses"].(int) != tc.payload["num_uses"].(int) { - t.Fatalf("secret_id_num_uses has not been set by the 'num_uses' field") - } - }) - } -} - -func TestAppRole_ErrorsRoleSecretIDWithInvalidFields(t *testing.T) { - type testCase struct { - name string - payload map[string]interface{} - expected string - } - - type roleTestCase struct { - name string - options map[string]interface{} - cases []testCase - } - - infiniteTestCases := []testCase{ - { - name: "infinite ttl", - payload: map[string]interface{}{"secret_id": "abcd123", "num_uses": 1, "ttl": 0}, - expected: "ttl cannot be longer than the role's secret_id_ttl", - }, - { - name: "infinite num_uses", - payload: map[string]interface{}{"secret_id": "abcd123", "num_uses": 0, "ttl": 1}, - expected: "num_uses cannot be higher than the role's secret_id_num_uses", - }, - } - - negativeTestCases := []testCase{ - { - name: "negative num_uses", - payload: map[string]interface{}{"secret_id": "abcd123", "num_uses": -1, "ttl": 0}, - expected: "num_uses cannot be negative", - }, - } - - roleTestCases := []roleTestCase{ - { - name: "infinite role secret id ttl", - options: map[string]interface{}{ - "secret_id_num_uses": 1, - "secret_id_ttl": 0, - }, - cases: []testCase{ - { - name: "higher num_uses", - payload: map[string]interface{}{"secret_id": "abcd123", "ttl": 0, "num_uses": 2}, - expected: "num_uses cannot be higher than the role's secret_id_num_uses", - }, - }, - }, - { - name: "infinite role num_uses", - options: map[string]interface{}{ - "secret_id_num_uses": 0, - "secret_id_ttl": 1, - }, - cases: []testCase{ - { - name: "longer ttl", - payload: map[string]interface{}{"secret_id": "abcd123", "ttl": 2, "num_uses": 0}, - expected: "ttl cannot be longer than the role's secret_id_ttl", - }, - }, - }, - { - name: "finite role ttl and num_uses", - options: map[string]interface{}{ - "secret_id_num_uses": 2, - "secret_id_ttl": 2, - }, - cases: infiniteTestCases, - }, - { - name: "mixed role ttl and num_uses", - options: map[string]interface{}{ - "secret_id_num_uses": 400, - "secret_id_ttl": 500, - }, - cases: negativeTestCases, - }, - } - - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - for i, rc := range roleTestCases { - roleData := map[string]interface{}{ - "policies": "p,q,r,s", - "token_ttl": 400, - "token_max_ttl": 500, - } - roleData["secret_id_num_uses"] = rc.options["secret_id_num_uses"] - roleData["secret_id_ttl"] = rc.options["secret_id_ttl"] - - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: fmt.Sprintf("role/role%d", i), - Storage: storage, - Data: roleData, - } - - resp = b.requestNoErr(t, roleReq) - - for _, tc := range rc.cases { - t.Run(fmt.Sprintf("%s/%s", rc.name, tc.name), func(t *testing.T) { - roleSecretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("role/role%d/secret-id", i), - Storage: storage, - } - roleSecretIDReq.Data = tc.payload - resp, err = b.HandleRequest(context.Background(), roleSecretIDReq) - if err != nil || (resp != nil && !resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - if resp.Data["error"].(string) != tc.expected { - t.Fatalf("expected: %q, got: %q", tc.expected, resp.Data["error"].(string)) - } - - roleSecretIDReq.Path = fmt.Sprintf("role/role%d/custom-secret-id", i) - resp, err = b.HandleRequest(context.Background(), roleSecretIDReq) - if err != nil || (resp != nil && !resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - if resp.Data["error"].(string) != tc.expected { - t.Fatalf("expected: %q, got: %q", tc.expected, resp.Data["error"].(string)) - } - }) - } - } -} - -func TestAppRole_RoleCRUD(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "policies": "p,q,r,s", - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - "token_num_uses": 600, - "secret_id_bound_cidrs": "127.0.0.1/32,127.0.0.1/16", - } - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/role1", - Storage: storage, - Data: roleData, - } - - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - expected := map[string]interface{}{ - "bind_secret_id": true, - "policies": []string{"p", "q", "r", "s"}, - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - "token_num_uses": 600, - "secret_id_bound_cidrs": []string{"127.0.0.1/32", "127.0.0.1/16"}, - "token_bound_cidrs": []string{}, - "token_type": "default", - } - - var expectedStruct roleStorageEntry - err = mapstructure.Decode(expected, &expectedStruct) - if err != nil { - t.Fatal(err) - } - - var actualStruct roleStorageEntry - err = mapstructure.Decode(resp.Data, &actualStruct) - if err != nil { - t.Fatal(err) - } - - expectedStruct.RoleID = actualStruct.RoleID - if diff := deep.Equal(expectedStruct, actualStruct); diff != nil { - t.Fatal(diff) - } - - roleData = map[string]interface{}{ - "role_id": "test_role_id", - "policies": "a,b,c,d", - "secret_id_num_uses": 100, - "secret_id_ttl": 3000, - "token_ttl": 4000, - "token_max_ttl": 5000, - } - roleReq.Data = roleData - roleReq.Operation = logical.UpdateOperation - - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - expected = map[string]interface{}{ - "policies": []string{"a", "b", "c", "d"}, - "secret_id_num_uses": 100, - "secret_id_ttl": 3000, - "token_ttl": 4000, - "token_max_ttl": 5000, - } - err = mapstructure.Decode(expected, &expectedStruct) - if err != nil { - t.Fatal(err) - } - - err = mapstructure.Decode(resp.Data, &actualStruct) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(expectedStruct, actualStruct) { - t.Fatalf("bad:\nexpected:%#v\nactual:%#v\n", expectedStruct, actualStruct) - } - - // RU for role_id field - roleReq.Path = "role/role1/role-id" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["role_id"].(string) != "test_role_id" { - t.Fatalf("bad: role_id: expected:test_role_id actual:%s\n", resp.Data["role_id"].(string)) - } - - roleReq.Data = map[string]interface{}{"role_id": "custom_role_id"} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["role_id"].(string) != "custom_role_id" { - t.Fatalf("bad: role_id: expected:custom_role_id actual:%s\n", resp.Data["role_id"].(string)) - } - - // RUD for bind_secret_id field - roleReq.Path = "role/role1/bind-secret-id" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Data = map[string]interface{}{"bind_secret_id": false} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["bind_secret_id"].(bool) { - t.Fatalf("bad: bind_secret_id: expected:false actual:%t\n", resp.Data["bind_secret_id"].(bool)) - } - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if !resp.Data["bind_secret_id"].(bool) { - t.Fatalf("expected the default value of 'true' to be set") - } - - // RUD for policies field - roleReq.Path = "role/role1/policies" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Data = map[string]interface{}{"policies": "a1,b1,c1,d1"} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if !reflect.DeepEqual(resp.Data["policies"].([]string), []string{"a1", "b1", "c1", "d1"}) { - t.Fatalf("bad: policies: actual:%s\n", resp.Data["policies"].([]string)) - } - if !reflect.DeepEqual(resp.Data["token_policies"].([]string), []string{"a1", "b1", "c1", "d1"}) { - t.Fatalf("bad: policies: actual:%s\n", resp.Data["policies"].([]string)) - } - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - expectedPolicies := []string{} - actualPolicies := resp.Data["token_policies"].([]string) - if !policyutil.EquivalentPolicies(expectedPolicies, actualPolicies) { - t.Fatalf("bad: token_policies: expected:%s actual:%s", expectedPolicies, actualPolicies) - } - - // RUD for secret-id-num-uses field - roleReq.Path = "role/role1/secret-id-num-uses" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Data = map[string]interface{}{"secret_id_num_uses": 200} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["secret_id_num_uses"].(int) != 200 { - t.Fatalf("bad: secret_id_num_uses: expected:200 actual:%d\n", resp.Data["secret_id_num_uses"].(int)) - } - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["secret_id_num_uses"].(int) != 0 { - t.Fatalf("expected value to be reset") - } - - // RUD for secret_id_ttl field - roleReq.Path = "role/role1/secret-id-ttl" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Data = map[string]interface{}{"secret_id_ttl": 3001} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["secret_id_ttl"].(time.Duration) != 3001 { - t.Fatalf("bad: secret_id_ttl: expected:3001 actual:%d\n", resp.Data["secret_id_ttl"].(time.Duration)) - } - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["secret_id_ttl"].(time.Duration) != 0 { - t.Fatalf("expected value to be reset") - } - - // RUD for secret-id-num-uses field - roleReq.Path = "role/role1/token-num-uses" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_num_uses"].(int) != 600 { - t.Fatalf("bad: token_num_uses: expected:600 actual:%d\n", resp.Data["token_num_uses"].(int)) - } - - roleReq.Data = map[string]interface{}{"token_num_uses": 60} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_num_uses"].(int) != 60 { - t.Fatalf("bad: token_num_uses: expected:60 actual:%d\n", resp.Data["token_num_uses"].(int)) - } - - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_num_uses"].(int) != 0 { - t.Fatalf("expected value to be reset") - } - - // RUD for 'period' field - roleReq.Path = "role/role1/period" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Data = map[string]interface{}{"period": 9001} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["period"].(time.Duration) != 9001 { - t.Fatalf("bad: period: expected:9001 actual:%d\n", resp.Data["9001"].(time.Duration)) - } - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_period"].(time.Duration) != 0 { - t.Fatalf("expected value to be reset") - } - - // RUD for token_ttl field - roleReq.Path = "role/role1/token-ttl" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Data = map[string]interface{}{"token_ttl": 4001} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_ttl"].(time.Duration) != 4001 { - t.Fatalf("bad: token_ttl: expected:4001 actual:%d\n", resp.Data["token_ttl"].(time.Duration)) - } - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_ttl"].(time.Duration) != 0 { - t.Fatalf("expected value to be reset") - } - - // RUD for token_max_ttl field - roleReq.Path = "role/role1/token-max-ttl" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Data = map[string]interface{}{"token_max_ttl": 5001} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_max_ttl"].(time.Duration) != 5001 { - t.Fatalf("bad: token_max_ttl: expected:5001 actual:%d\n", resp.Data["token_max_ttl"].(time.Duration)) - } - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_max_ttl"].(time.Duration) != 0 { - t.Fatalf("expected value to be reset") - } - - // Delete test for role - roleReq.Path = "role/role1" - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp != nil { - t.Fatalf("expected a nil response") - } -} - -func TestAppRole_RoleWithTokenBoundCIDRsCRUD(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "policies": "p,q,r,s", - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - "token_num_uses": 600, - "secret_id_bound_cidrs": "127.0.0.1/32,127.0.0.1/16", - "token_bound_cidrs": "127.0.0.1/32,127.0.0.1/16", - } - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/role1", - Storage: storage, - Data: roleData, - } - - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - expected := map[string]interface{}{ - "bind_secret_id": true, - "policies": []string{"p", "q", "r", "s"}, - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - "token_num_uses": 600, - "token_bound_cidrs": []string{"127.0.0.1/32", "127.0.0.1/16"}, - "secret_id_bound_cidrs": []string{"127.0.0.1/32", "127.0.0.1/16"}, - "token_type": "default", - } - - var expectedStruct roleStorageEntry - err = mapstructure.Decode(expected, &expectedStruct) - if err != nil { - t.Fatal(err) - } - - var actualStruct roleStorageEntry - err = mapstructure.Decode(resp.Data, &actualStruct) - if err != nil { - t.Fatal(err) - } - - expectedStruct.RoleID = actualStruct.RoleID - if !reflect.DeepEqual(expectedStruct, actualStruct) { - t.Fatalf("bad:\nexpected:%#v\nactual:%#v\n", expectedStruct, actualStruct) - } - - roleData = map[string]interface{}{ - "role_id": "test_role_id", - "policies": "a,b,c,d", - "secret_id_num_uses": 100, - "secret_id_ttl": 3000, - "token_ttl": 4000, - "token_max_ttl": 5000, - } - roleReq.Data = roleData - roleReq.Operation = logical.UpdateOperation - - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - expected = map[string]interface{}{ - "policies": []string{"a", "b", "c", "d"}, - "secret_id_num_uses": 100, - "secret_id_ttl": 3000, - "token_ttl": 4000, - "token_max_ttl": 5000, - } - err = mapstructure.Decode(expected, &expectedStruct) - if err != nil { - t.Fatal(err) - } - - err = mapstructure.Decode(resp.Data, &actualStruct) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(expectedStruct, actualStruct) { - t.Fatalf("bad:\nexpected:%#v\nactual:%#v\n", expectedStruct, actualStruct) - } - - // RUD for secret-id-bound-cidrs field - roleReq.Path = "role/role1/secret-id-bound-cidrs" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["secret_id_bound_cidrs"].([]string)[0] != "127.0.0.1/32" || - resp.Data["secret_id_bound_cidrs"].([]string)[1] != "127.0.0.1/16" { - t.Fatalf("bad: secret_id_bound_cidrs: expected:127.0.0.1/32,127.0.0.1/16 actual:%d\n", resp.Data["secret_id_bound_cidrs"].(int)) - } - - roleReq.Data = map[string]interface{}{"secret_id_bound_cidrs": []string{"127.0.0.1/20"}} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["secret_id_bound_cidrs"].([]string)[0] != "127.0.0.1/20" { - t.Fatalf("bad: secret_id_bound_cidrs: expected:127.0.0.1/20 actual:%s\n", resp.Data["secret_id_bound_cidrs"].([]string)[0]) - } - - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if len(resp.Data["secret_id_bound_cidrs"].([]string)) != 0 { - t.Fatalf("expected value to be reset") - } - - // RUD for token-bound-cidrs field - roleReq.Path = "role/role1/token-bound-cidrs" - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0].String() != "127.0.0.1" || - resp.Data["token_bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[1].String() != "127.0.0.1/16" { - m, err := json.Marshal(resp.Data["token_bound_cidrs"].([]*sockaddr.SockAddrMarshaler)) - if err != nil { - t.Fatal(err) - } - t.Fatalf("bad: token_bound_cidrs: expected:127.0.0.1/32,127.0.0.1/16 actual:%s\n", string(m)) - } - - roleReq.Data = map[string]interface{}{"token_bound_cidrs": []string{"127.0.0.1/20"}} - roleReq.Operation = logical.UpdateOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if resp.Data["token_bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0].String() != "127.0.0.1/20" { - t.Fatalf("bad: token_bound_cidrs: expected:127.0.0.1/20 actual:%s\n", resp.Data["token_bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0]) - } - - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - if len(resp.Data["token_bound_cidrs"].([]*sockaddr.SockAddrMarshaler)) != 0 { - t.Fatalf("expected value to be reset") - } - - // Delete test for role - roleReq.Path = "role/role1" - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - if resp != nil { - t.Fatalf("expected a nil response") - } -} - -func TestAppRole_RoleWithTokenTypeCRUD(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "policies": "p,q,r,s", - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - "token_num_uses": 600, - "token_type": "default-service", - } - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/role1", - Storage: storage, - Data: roleData, - } - - resp = b.requestNoErr(t, roleReq) - - if 0 == len(resp.Warnings) { - t.Fatalf("bad:\nexpected warning in resp:%#v\n", resp.Warnings) - } - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - expected := map[string]interface{}{ - "bind_secret_id": true, - "policies": []string{"p", "q", "r", "s"}, - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - "token_num_uses": 600, - "token_type": "service", - } - - var expectedStruct roleStorageEntry - err = mapstructure.Decode(expected, &expectedStruct) - if err != nil { - t.Fatal(err) - } - - var actualStruct roleStorageEntry - err = mapstructure.Decode(resp.Data, &actualStruct) - if err != nil { - t.Fatal(err) - } - - expectedStruct.RoleID = actualStruct.RoleID - if !reflect.DeepEqual(expectedStruct, actualStruct) { - t.Fatalf("bad:\nexpected:%#v\nactual:%#v\n", expectedStruct, actualStruct) - } - - roleData = map[string]interface{}{ - "role_id": "test_role_id", - "policies": "a,b,c,d", - "secret_id_num_uses": 100, - "secret_id_ttl": 3000, - "token_ttl": 4000, - "token_max_ttl": 5000, - "token_type": "default-service", - } - roleReq.Data = roleData - roleReq.Operation = logical.UpdateOperation - - resp = b.requestNoErr(t, roleReq) - - if 0 == len(resp.Warnings) { - t.Fatalf("bad:\nexpected a warning in resp:%#v\n", resp.Warnings) - } - - roleReq.Operation = logical.ReadOperation - resp = b.requestNoErr(t, roleReq) - - expected = map[string]interface{}{ - "policies": []string{"a", "b", "c", "d"}, - "secret_id_num_uses": 100, - "secret_id_ttl": 3000, - "token_ttl": 4000, - "token_max_ttl": 5000, - "token_type": "service", - } - err = mapstructure.Decode(expected, &expectedStruct) - if err != nil { - t.Fatal(err) - } - - err = mapstructure.Decode(resp.Data, &actualStruct) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(expectedStruct, actualStruct) { - t.Fatalf("bad:\nexpected:%#v\nactual:%#v\n", expectedStruct, actualStruct) - } - - // Delete test for role - roleReq.Path = "role/role1" - roleReq.Operation = logical.DeleteOperation - resp = b.requestNoErr(t, roleReq) - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - if resp != nil { - t.Fatalf("expected a nil response") - } -} - -func createRole(t *testing.T, b *backend, s logical.Storage, roleName, policies string) { - roleData := map[string]interface{}{ - "policies": policies, - "secret_id_num_uses": 10, - "secret_id_ttl": 300, - "token_ttl": 400, - "token_max_ttl": 500, - } - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/" + roleName, - Storage: s, - Data: roleData, - } - _ = b.requestNoErr(t, roleReq) -} - -// TestAppRole_TokenutilUpgrade ensures that when we read values out that are -// values with upgrade logic we see the correct struct entries populated -func TestAppRole_TokenutilUpgrade(t *testing.T) { - tests := []struct { - name string - storageValMissing bool - storageVal string - expectedTokenType logical.TokenType - }{ - { - "token_type_missing", - true, - "", - logical.TokenTypeDefault, - }, - { - "token_type_empty", - false, - "", - logical.TokenTypeDefault, - }, - { - "token_type_service", - false, - "service", - logical.TokenTypeService, - }, - } - - s := &logical.InmemStorage{} - - config := logical.TestBackendConfig() - config.StorageView = s - - ctx := context.Background() - - b, err := Backend(config) - if err != nil { - t.Fatal(err) - } - if b == nil { - t.Fatalf("failed to create backend") - } - if err := b.Setup(ctx, config); err != nil { - t.Fatal(err) - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Construct the storage entry object based on our test case. - tokenTypeKV := "" - if !tt.storageValMissing { - tokenTypeKV = fmt.Sprintf(`, "token_type": "%s"`, tt.storageVal) - } - entryVal := fmt.Sprintf(`{"policies": ["foo"], "period": 300000000000, "token_bound_cidrs": ["127.0.0.1", "10.10.10.10/24"]%s}`, tokenTypeKV) - - // Hand craft JSON because there is overlap between fields - if err := s.Put(ctx, &logical.StorageEntry{ - Key: "role/" + tt.name, - Value: []byte(entryVal), - }); err != nil { - t.Fatal(err) - } - - resEntry, err := b.roleEntry(ctx, s, tt.name) - if err != nil { - t.Fatal(err) - } - - exp := &roleStorageEntry{ - SecretIDPrefix: "secret_id/", - Policies: []string{"foo"}, - Period: 300 * time.Second, - TokenParams: tokenutil.TokenParams{ - TokenPolicies: []string{"foo"}, - TokenPeriod: 300 * time.Second, - TokenBoundCIDRs: []*sockaddr.SockAddrMarshaler{ - {SockAddr: sockaddr.MustIPAddr("127.0.0.1")}, - {SockAddr: sockaddr.MustIPAddr("10.10.10.10/24")}, - }, - TokenType: tt.expectedTokenType, - }, - } - if diff := deep.Equal(resEntry, exp); diff != nil { - t.Fatal(diff) - } - }) - } -} - -func TestAppRole_SecretID_WithTTL(t *testing.T) { - tests := []struct { - name string - roleName string - ttl int64 - sysTTLCap bool - }{ - { - "zero ttl", - "role-zero-ttl", - 0, - false, - }, - { - "custom ttl", - "role-custom-ttl", - 60, - false, - }, - { - "system ttl capped", - "role-sys-ttl-cap", - 700000000, - true, - }, - } - - b, storage := createBackendWithStorage(t) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create role - roleData := map[string]interface{}{ - "policies": "default", - "secret_id_ttl": tt.ttl, - } - - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/" + tt.roleName, - Storage: storage, - Data: roleData, - } - resp := b.requestNoErr(t, roleReq) - - // Generate secret ID - secretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/" + tt.roleName + "/secret-id", - Storage: storage, - } - resp = b.requestNoErr(t, secretIDReq) - - // Extract the "ttl" value from the response data if it exists - ttlRaw, okTTL := resp.Data["secret_id_ttl"] - if !okTTL { - t.Fatalf("expected TTL value in response") - } - - var ( - respTTL int64 - ok bool - ) - respTTL, ok = ttlRaw.(int64) - if !ok { - t.Fatalf("expected ttl to be an integer, got: %T", ttlRaw) - } - - // Verify secret ID response for different cases - switch { - case tt.sysTTLCap: - if respTTL != int64(b.System().MaxLeaseTTL().Seconds()) { - t.Fatalf("expected TTL value to be system's max lease TTL, got: %d", respTTL) - } - default: - if respTTL != tt.ttl { - t.Fatalf("expected TTL value to be %d, got: %d", tt.ttl, respTTL) - } - } - }) - } -} - -// TestAppRole_RoleSecretIDAccessorCrossDelete tests deleting a secret id via -// secret id accessor belonging to a different role -func TestAppRole_RoleSecretIDAccessorCrossDelete(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - // Create First Role - createRole(t, b, storage, "role1", "a,b") - _ = b.requestNoErr(t, &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1/secret-id", - }) - - // Create Second Role - createRole(t, b, storage, "role2", "a,b") - _ = b.requestNoErr(t, &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role2/secret-id", - }) - - // Get role2 secretID Accessor - resp = b.requestNoErr(t, &logical.Request{ - Operation: logical.ListOperation, - Storage: storage, - Path: "role/role2/secret-id", - }) - - // Read back role2 secretID Accessor information - hmacSecretID := resp.Data["keys"].([]string)[0] - _ = b.requestNoErr(t, &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role2/secret-id-accessor/lookup", - Data: map[string]interface{}{ - "secret_id_accessor": hmacSecretID, - }, - }) - - // Attempt to destroy role2 secretID accessor using role1 path - _, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1/secret-id-accessor/destroy", - Data: map[string]interface{}{ - "secret_id_accessor": hmacSecretID, - }, - }) - - if err == nil { - t.Fatalf("expected error") - } -} diff --git a/builtin/credential/approle/path_tidy_user_id_test.go b/builtin/credential/approle/path_tidy_user_id_test.go deleted file mode 100644 index 4b932cd11..000000000 --- a/builtin/credential/approle/path_tidy_user_id_test.go +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package approle - -import ( - "context" - "fmt" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestAppRole_TidyDanglingAccessors_Normal(t *testing.T) { - b, storage := createBackendWithStorage(t) - - // Create a role - createRole(t, b, storage, "role1", "a,b,c") - - // Create a secret-id - roleSecretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/role1/secret-id", - Storage: storage, - } - _ = b.requestNoErr(t, roleSecretIDReq) - - accessorHashes, err := storage.List(context.Background(), "accessor/") - if err != nil { - t.Fatal(err) - } - if len(accessorHashes) != 1 { - t.Fatalf("bad: len(accessorHashes); expect 1, got %d", len(accessorHashes)) - } - - entry1, err := logical.StorageEntryJSON( - "accessor/invalid1", - &secretIDAccessorStorageEntry{ - SecretIDHMAC: "samplesecretidhmac", - }, - ) - if err != nil { - t.Fatal(err) - } - - if err := storage.Put(context.Background(), entry1); err != nil { - t.Fatal(err) - } - - entry2, err := logical.StorageEntryJSON( - "accessor/invalid2", - &secretIDAccessorStorageEntry{ - SecretIDHMAC: "samplesecretidhmac2", - }, - ) - if err != nil { - t.Fatal(err) - } - if err := storage.Put(context.Background(), entry2); err != nil { - t.Fatal(err) - } - - accessorHashes, err = storage.List(context.Background(), "accessor/") - if err != nil { - t.Fatal(err) - } - if len(accessorHashes) != 3 { - t.Fatalf("bad: len(accessorHashes); expect 3, got %d", len(accessorHashes)) - } - - secret, err := b.tidySecretID(context.Background(), &logical.Request{ - Storage: storage, - }) - if err != nil { - t.Fatal(err) - } - schema.ValidateResponse( - t, - schema.GetResponseSchema(t, pathTidySecretID(b), logical.UpdateOperation), - secret, - true, - ) - - // It runs async so we give it a bit of time to run - time.Sleep(10 * time.Second) - - accessorHashes, err = storage.List(context.Background(), "accessor/") - if err != nil { - t.Fatal(err) - } - if len(accessorHashes) != 1 { - t.Fatalf("bad: len(accessorHashes); expect 1, got %d", len(accessorHashes)) - } -} - -func TestAppRole_TidyDanglingAccessors_RaceTest(t *testing.T) { - b, storage := createBackendWithStorage(t) - - // Create a role - createRole(t, b, storage, "role1", "a,b,c") - - // Create an initial entry - roleSecretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/role1/secret-id", - Storage: storage, - } - _ = b.requestNoErr(t, roleSecretIDReq) - - count := 1 - - wg := &sync.WaitGroup{} - start := time.Now() - for time.Now().Sub(start) < 10*time.Second { - if time.Now().Sub(start) > 100*time.Millisecond && atomic.LoadUint32(b.tidySecretIDCASGuard) == 0 { - secret, err := b.tidySecretID(context.Background(), &logical.Request{ - Storage: storage, - }) - if err != nil { - t.Fatal(err) - } - schema.ValidateResponse( - t, - schema.GetResponseSchema(t, pathTidySecretID(b), logical.UpdateOperation), - secret, - true, - ) - } - wg.Add(1) - go func() { - defer wg.Done() - roleSecretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/role1/secret-id", - Storage: storage, - } - _ = b.requestNoErr(t, roleSecretIDReq) - }() - - entry, err := logical.StorageEntryJSON( - fmt.Sprintf("accessor/invalid%d", count), - &secretIDAccessorStorageEntry{ - SecretIDHMAC: "samplesecretidhmac", - }, - ) - if err != nil { - t.Fatal(err) - } - - if err := storage.Put(context.Background(), entry); err != nil { - t.Fatal(err) - } - - count++ - time.Sleep(100 * time.Microsecond) - } - - logger := b.Logger().Named(t.Name()) - logger.Info("wrote entries", "count", count) - - wg.Wait() - // Let tidy finish - for atomic.LoadUint32(b.tidySecretIDCASGuard) != 0 { - time.Sleep(100 * time.Millisecond) - } - - logger.Info("running tidy again") - - // Run tidy again - secret, err := b.tidySecretID(context.Background(), &logical.Request{ - Storage: storage, - }) - if err != nil || len(secret.Warnings) > 0 { - t.Fatal(err, secret.Warnings) - } - schema.ValidateResponse( - t, - schema.GetResponseSchema(t, pathTidySecretID(b), logical.UpdateOperation), - secret, - true, - ) - - // Wait for tidy to start - for atomic.LoadUint32(b.tidySecretIDCASGuard) == 0 { - time.Sleep(100 * time.Millisecond) - } - - // Let tidy finish - for atomic.LoadUint32(b.tidySecretIDCASGuard) != 0 { - time.Sleep(100 * time.Millisecond) - } - - accessorHashes, err := storage.List(context.Background(), "accessor/") - if err != nil { - t.Fatal(err) - } - if len(accessorHashes) != count { - t.Fatalf("bad: len(accessorHashes); expect %d, got %d", count, len(accessorHashes)) - } - - roleHMACs, err := storage.List(context.Background(), secretIDPrefix) - if err != nil { - t.Fatal(err) - } - secretIDs, err := storage.List(context.Background(), fmt.Sprintf("%s%s", secretIDPrefix, roleHMACs[0])) - if err != nil { - t.Fatal(err) - } - if len(secretIDs) != count { - t.Fatalf("bad: len(secretIDs); expect %d, got %d", count, len(secretIDs)) - } -} diff --git a/builtin/credential/approle/validation_test.go b/builtin/credential/approle/validation_test.go deleted file mode 100644 index d3386aa35..000000000 --- a/builtin/credential/approle/validation_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package approle - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestAppRole_SecretIDNumUsesUpgrade(t *testing.T) { - var resp *logical.Response - var err error - - b, storage := createBackendWithStorage(t) - - roleData := map[string]interface{}{ - "secret_id_num_uses": 10, - } - - roleReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "role/role1", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - secretIDReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/role1/secret-id", - Storage: storage, - } - - resp, err = b.HandleRequest(context.Background(), secretIDReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - secretIDReq.Operation = logical.UpdateOperation - secretIDReq.Path = "role/role1/secret-id/lookup" - secretIDReq.Data = map[string]interface{}{ - "secret_id": resp.Data["secret_id"].(string), - } - resp, err = b.HandleRequest(context.Background(), secretIDReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Check if the response contains the value set for secret_id_num_uses - if resp.Data["secret_id_num_uses"] != 10 { - t.Fatal("invalid secret_id_num_uses") - } -} diff --git a/builtin/credential/cert/backend_test.go b/builtin/credential/cert/backend_test.go deleted file mode 100644 index 828344c0e..000000000 --- a/builtin/credential/cert/backend_test.go +++ /dev/null @@ -1,2941 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cert - -import ( - "context" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "io" - "io/ioutil" - "math/big" - mathrand "math/rand" - "net" - "net/http" - "net/http/httptest" - "net/url" - "os" - "path/filepath" - "reflect" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/hashicorp/go-cleanhttp" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-rootcerts" - "github.com/hashicorp/go-sockaddr" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/logical/pki" - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/hashicorp/vault/sdk/helper/tokenutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/mapstructure" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/ocsp" - "golang.org/x/net/http2" -) - -const ( - serverCertPath = "test-fixtures/cacert.pem" - serverKeyPath = "test-fixtures/cakey.pem" - serverCAPath = serverCertPath - - testRootCACertPath1 = "test-fixtures/testcacert1.pem" - testRootCAKeyPath1 = "test-fixtures/testcakey1.pem" - testCertPath1 = "test-fixtures/testissuedcert4.pem" - testKeyPath1 = "test-fixtures/testissuedkey4.pem" - testIssuedCertCRL = "test-fixtures/issuedcertcrl" - - testRootCACertPath2 = "test-fixtures/testcacert2.pem" - testRootCAKeyPath2 = "test-fixtures/testcakey2.pem" - testRootCertCRL = "test-fixtures/cacert2crl" -) - -func generateTestCertAndConnState(t *testing.T, template *x509.Certificate) (string, tls.ConnectionState, error) { - t.Helper() - tempDir, err := ioutil.TempDir("", "vault-cert-auth-test-") - if err != nil { - t.Fatal(err) - } - t.Logf("test %s, temp dir %s", t.Name(), tempDir) - caCertTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - DNSNames: []string{"localhost"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, - KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - BasicConstraintsValid: true, - IsCA: true, - } - caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - caBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, caKey.Public(), caKey) - if err != nil { - t.Fatal(err) - } - caCert, err := x509.ParseCertificate(caBytes) - if err != nil { - t.Fatal(err) - } - caCertPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - } - err = ioutil.WriteFile(filepath.Join(tempDir, "ca_cert.pem"), pem.EncodeToMemory(caCertPEMBlock), 0o755) - if err != nil { - t.Fatal(err) - } - marshaledCAKey, err := x509.MarshalECPrivateKey(caKey) - if err != nil { - t.Fatal(err) - } - caKeyPEMBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: marshaledCAKey, - } - err = ioutil.WriteFile(filepath.Join(tempDir, "ca_key.pem"), pem.EncodeToMemory(caKeyPEMBlock), 0o755) - if err != nil { - t.Fatal(err) - } - - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - certBytes, err := x509.CreateCertificate(rand.Reader, template, caCert, key.Public(), caKey) - if err != nil { - t.Fatal(err) - } - certPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: certBytes, - } - err = ioutil.WriteFile(filepath.Join(tempDir, "cert.pem"), pem.EncodeToMemory(certPEMBlock), 0o755) - if err != nil { - t.Fatal(err) - } - marshaledKey, err := x509.MarshalECPrivateKey(key) - if err != nil { - t.Fatal(err) - } - keyPEMBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: marshaledKey, - } - err = ioutil.WriteFile(filepath.Join(tempDir, "key.pem"), pem.EncodeToMemory(keyPEMBlock), 0o755) - if err != nil { - t.Fatal(err) - } - connInfo, err := testConnState(filepath.Join(tempDir, "cert.pem"), filepath.Join(tempDir, "key.pem"), filepath.Join(tempDir, "ca_cert.pem")) - return tempDir, connInfo, err -} - -// Unlike testConnState, this method does not use the same 'tls.Config' objects for -// both dialing and listening. Instead, it runs the server without specifying its CA. -// But the client, presents the CA cert of the server to trust the server. -// The client can present a cert and key which is completely independent of server's CA. -// The connection state returned will contain the certificate presented by the client. -func connectionState(serverCAPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath string) (tls.ConnectionState, error) { - serverKeyPair, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath) - if err != nil { - return tls.ConnectionState{}, err - } - // Prepare the listener configuration with server's key pair - listenConf := &tls.Config{ - Certificates: []tls.Certificate{serverKeyPair}, - ClientAuth: tls.RequestClientCert, - } - - clientKeyPair, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath) - if err != nil { - return tls.ConnectionState{}, err - } - // Load the CA cert required by the client to authenticate the server. - rootConfig := &rootcerts.Config{ - CAFile: serverCAPath, - } - serverCAs, err := rootcerts.LoadCACerts(rootConfig) - if err != nil { - return tls.ConnectionState{}, err - } - // Prepare the dial configuration that the client uses to establish the connection. - dialConf := &tls.Config{ - Certificates: []tls.Certificate{clientKeyPair}, - RootCAs: serverCAs, - } - - // Start the server. - list, err := tls.Listen("tcp", "127.0.0.1:0", listenConf) - if err != nil { - return tls.ConnectionState{}, err - } - defer list.Close() - - // Accept connections. - serverErrors := make(chan error, 1) - connState := make(chan tls.ConnectionState) - go func() { - defer close(connState) - serverConn, err := list.Accept() - if err != nil { - serverErrors <- err - close(serverErrors) - return - } - defer serverConn.Close() - - // Read the ping - buf := make([]byte, 4) - _, err = serverConn.Read(buf) - if (err != nil) && (err != io.EOF) { - serverErrors <- err - close(serverErrors) - return - } - close(serverErrors) - connState <- serverConn.(*tls.Conn).ConnectionState() - }() - - // Establish a connection from the client side and write a few bytes. - clientErrors := make(chan error, 1) - go func() { - addr := list.Addr().String() - conn, err := tls.Dial("tcp", addr, dialConf) - if err != nil { - clientErrors <- err - close(clientErrors) - return - } - defer conn.Close() - - // Write ping - _, err = conn.Write([]byte("ping")) - if err != nil { - clientErrors <- err - } - close(clientErrors) - }() - - for err = range clientErrors { - if err != nil { - return tls.ConnectionState{}, fmt.Errorf("error in client goroutine:%v", err) - } - } - - for err = range serverErrors { - if err != nil { - return tls.ConnectionState{}, fmt.Errorf("error in server goroutine:%v", err) - } - } - // Grab the current state - return <-connState, nil -} - -func TestBackend_PermittedDNSDomainsIntermediateCA(t *testing.T) { - // Enable PKI secret engine and Cert auth method - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: log.NewNullLogger(), - CredentialBackends: map[string]logical.Factory{ - "cert": Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "pki": pki.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - client := cores[0].Client - - var err error - - // Mount /pki as a root CA - err = client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "32h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Set the cluster's certificate as the root CA in /pki - pemBundleRootCA := string(cluster.CACertPEM) + string(cluster.CAKeyPEM) - _, err = client.Logical().Write("pki/config/ca", map[string]interface{}{ - "pem_bundle": pemBundleRootCA, - }) - if err != nil { - t.Fatal(err) - } - - // Mount /pki2 to operate as an intermediate CA - err = client.Sys().Mount("pki2", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "32h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Create a CSR for the intermediate CA - secret, err := client.Logical().Write("pki2/intermediate/generate/internal", nil) - if err != nil { - t.Fatal(err) - } - intermediateCSR := secret.Data["csr"].(string) - - // Sign the intermediate CSR using /pki - secret, err = client.Logical().Write("pki/root/sign-intermediate", map[string]interface{}{ - "permitted_dns_domains": ".myvault.com", - "csr": intermediateCSR, - }) - if err != nil { - t.Fatal(err) - } - intermediateCertPEM := secret.Data["certificate"].(string) - - // Configure the intermediate cert as the CA in /pki2 - _, err = client.Logical().Write("pki2/intermediate/set-signed", map[string]interface{}{ - "certificate": intermediateCertPEM, - }) - if err != nil { - t.Fatal(err) - } - - // Create a role on the intermediate CA mount - _, err = client.Logical().Write("pki2/roles/myvault-dot-com", map[string]interface{}{ - "allowed_domains": "myvault.com", - "allow_subdomains": "true", - "max_ttl": "5m", - }) - if err != nil { - t.Fatal(err) - } - - // Issue a leaf cert using the intermediate CA - secret, err = client.Logical().Write("pki2/issue/myvault-dot-com", map[string]interface{}{ - "common_name": "cert.myvault.com", - "format": "pem", - "ip_sans": "127.0.0.1", - }) - if err != nil { - t.Fatal(err) - } - leafCertPEM := secret.Data["certificate"].(string) - leafCertKeyPEM := secret.Data["private_key"].(string) - - // Enable the cert auth method - err = client.Sys().EnableAuthWithOptions("cert", &api.EnableAuthOptions{ - Type: "cert", - }) - if err != nil { - t.Fatal(err) - } - - // Set the intermediate CA cert as a trusted certificate in the backend - _, err = client.Logical().Write("auth/cert/certs/myvault-dot-com", map[string]interface{}{ - "display_name": "myvault.com", - "policies": "default", - "certificate": intermediateCertPEM, - }) - if err != nil { - t.Fatal(err) - } - - // Create temporary files for CA cert, client cert and client cert key. - // This is used to configure TLS in the api client. - caCertFile, err := ioutil.TempFile("", "caCert") - if err != nil { - t.Fatal(err) - } - defer os.Remove(caCertFile.Name()) - if _, err := caCertFile.Write([]byte(cluster.CACertPEM)); err != nil { - t.Fatal(err) - } - if err := caCertFile.Close(); err != nil { - t.Fatal(err) - } - - leafCertFile, err := ioutil.TempFile("", "leafCert") - if err != nil { - t.Fatal(err) - } - defer os.Remove(leafCertFile.Name()) - if _, err := leafCertFile.Write([]byte(leafCertPEM)); err != nil { - t.Fatal(err) - } - if err := leafCertFile.Close(); err != nil { - t.Fatal(err) - } - - leafCertKeyFile, err := ioutil.TempFile("", "leafCertKey") - if err != nil { - t.Fatal(err) - } - defer os.Remove(leafCertKeyFile.Name()) - if _, err := leafCertKeyFile.Write([]byte(leafCertKeyPEM)); err != nil { - t.Fatal(err) - } - if err := leafCertKeyFile.Close(); err != nil { - t.Fatal(err) - } - - // This function is a copy-pasta from the NewTestCluster, with the - // modification to reconfigure the TLS on the api client with the leaf - // certificate generated above. - getAPIClient := func(port int, tlsConfig *tls.Config) *api.Client { - transport := cleanhttp.DefaultPooledTransport() - transport.TLSClientConfig = tlsConfig.Clone() - if err := http2.ConfigureTransport(transport); err != nil { - t.Fatal(err) - } - client := &http.Client{ - Transport: transport, - CheckRedirect: func(*http.Request, []*http.Request) error { - // This can of course be overridden per-test by using its own client - return fmt.Errorf("redirects not allowed in these tests") - }, - } - config := api.DefaultConfig() - if config.Error != nil { - t.Fatal(config.Error) - } - config.Address = fmt.Sprintf("https://127.0.0.1:%d", port) - config.HttpClient = client - - // Set the above issued certificates as the client certificates - config.ConfigureTLS(&api.TLSConfig{ - CACert: caCertFile.Name(), - ClientCert: leafCertFile.Name(), - ClientKey: leafCertKeyFile.Name(), - }) - - apiClient, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - return apiClient - } - - // Create a new api client with the desired TLS configuration - newClient := getAPIClient(cores[0].Listeners[0].Address.Port, cores[0].TLSConfig()) - - secret, err = newClient.Logical().Write("auth/cert/login", map[string]interface{}{ - "name": "myvault-dot-com", - }) - if err != nil { - t.Fatal(err) - } - if secret.Auth == nil || secret.Auth.ClientToken == "" { - t.Fatalf("expected a successful authentication") - } - - // testing pathLoginRenew for cert auth - oldAccessor := secret.Auth.Accessor - newClient.SetToken(client.Token()) - secret, err = newClient.Logical().Write("auth/token/renew-accessor", map[string]interface{}{ - "accessor": secret.Auth.Accessor, - "increment": 3600, - }) - if err != nil { - t.Fatal(err) - } - - if secret.Auth == nil || secret.Auth.ClientToken != "" || secret.Auth.LeaseDuration != 3600 || secret.Auth.Accessor != oldAccessor { - t.Fatalf("unexpected accessor renewal") - } -} - -func TestBackend_MetadataBasedACLPolicy(t *testing.T) { - // Start cluster with cert auth method enabled - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: log.NewNullLogger(), - CredentialBackends: map[string]logical.Factory{ - "cert": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - client := cores[0].Client - - var err error - - // Enable the cert auth method - err = client.Sys().EnableAuthWithOptions("cert", &api.EnableAuthOptions{ - Type: "cert", - }) - if err != nil { - t.Fatal(err) - } - - // Enable metadata in aliases - _, err = client.Logical().Write("auth/cert/config", map[string]interface{}{ - "enable_identity_alias_metadata": true, - }) - if err != nil { - t.Fatal(err) - } - - // Retrieve its accessor id - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - - var accessor string - - for _, auth := range auths { - if auth.Type == "cert" { - accessor = auth.Accessor - } - } - - if accessor == "" { - t.Fatal("failed to find cert auth accessor") - } - - // Write ACL policy - err = client.Sys().PutPolicy("metadata-based", fmt.Sprintf(` -path "kv/cn/{{identity.entity.aliases.%s.metadata.common_name}}" { - capabilities = ["read"] -} -path "kv/ext/{{identity.entity.aliases.%s.metadata.2-1-1-1}}" { - capabilities = ["read"] -} -`, accessor, accessor)) - if err != nil { - t.Fatalf("err: %v", err) - } - - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - - // Set the trusted certificate in the backend - _, err = client.Logical().Write("auth/cert/certs/test", map[string]interface{}{ - "display_name": "test", - "policies": "metadata-based", - "certificate": string(ca), - "allowed_metadata_extensions": "2.1.1.1,1.2.3.45", - }) - if err != nil { - t.Fatal(err) - } - - // This function is a copy-paste from the NewTestCluster, with the - // modification to reconfigure the TLS on the api client with a - // specific client certificate. - getAPIClient := func(port int, tlsConfig *tls.Config) *api.Client { - transport := cleanhttp.DefaultPooledTransport() - transport.TLSClientConfig = tlsConfig.Clone() - if err := http2.ConfigureTransport(transport); err != nil { - t.Fatal(err) - } - client := &http.Client{ - Transport: transport, - CheckRedirect: func(*http.Request, []*http.Request) error { - // This can of course be overridden per-test by using its own client - return fmt.Errorf("redirects not allowed in these tests") - }, - } - config := api.DefaultConfig() - if config.Error != nil { - t.Fatal(config.Error) - } - config.Address = fmt.Sprintf("https://127.0.0.1:%d", port) - config.HttpClient = client - - // Set the client certificates - config.ConfigureTLS(&api.TLSConfig{ - CACertBytes: cluster.CACertPEM, - ClientCert: "test-fixtures/root/rootcawextcert.pem", - ClientKey: "test-fixtures/root/rootcawextkey.pem", - }) - - apiClient, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - return apiClient - } - - // Create a new api client with the desired TLS configuration - newClient := getAPIClient(cores[0].Listeners[0].Address.Port, cores[0].TLSConfig()) - - var secret *api.Secret - - secret, err = newClient.Logical().Write("auth/cert/login", map[string]interface{}{ - "name": "test", - }) - if err != nil { - t.Fatal(err) - } - if secret.Auth == nil || secret.Auth.ClientToken == "" { - t.Fatalf("expected a successful authentication") - } - - // Check paths guarded by ACL policy - newClient.SetToken(secret.Auth.ClientToken) - - _, err = newClient.Logical().Read("kv/cn/example.com") - if err != nil { - t.Fatal(err) - } - - _, err = newClient.Logical().Read("kv/cn/not.example.com") - if err == nil { - t.Fatal("expected access denied") - } - - _, err = newClient.Logical().Read("kv/ext/A UTF8String Extension") - if err != nil { - t.Fatal(err) - } - - _, err = newClient.Logical().Read("kv/ext/bar") - if err == nil { - t.Fatal("expected access denied") - } -} - -func TestBackend_NonCAExpiry(t *testing.T) { - var resp *logical.Response - var err error - - // Create a self-signed certificate and issue a leaf certificate using the - // CA cert - template := &x509.Certificate{ - SerialNumber: big.NewInt(1234), - Subject: pkix.Name{ - CommonName: "localhost", - Organization: []string{"hashicorp"}, - OrganizationalUnit: []string{"vault"}, - }, - BasicConstraintsValid: true, - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(50 * time.Second), - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, - KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), - } - - // Set IP SAN - parsedIP := net.ParseIP("127.0.0.1") - if parsedIP == nil { - t.Fatalf("failed to create parsed IP") - } - template.IPAddresses = []net.IP{parsedIP} - - // Private key for CA cert - caPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatal(err) - } - - // Marshalling to be able to create PEM file - caPrivateKeyBytes := x509.MarshalPKCS1PrivateKey(caPrivateKey) - - caPublicKey := &caPrivateKey.PublicKey - - template.IsCA = true - - caCertBytes, err := x509.CreateCertificate(rand.Reader, template, template, caPublicKey, caPrivateKey) - if err != nil { - t.Fatal(err) - } - - caCert, err := x509.ParseCertificate(caCertBytes) - if err != nil { - t.Fatal(err) - } - - parsedCaBundle := &certutil.ParsedCertBundle{ - Certificate: caCert, - CertificateBytes: caCertBytes, - PrivateKeyBytes: caPrivateKeyBytes, - PrivateKeyType: certutil.RSAPrivateKey, - } - - caCertBundle, err := parsedCaBundle.ToCertBundle() - if err != nil { - t.Fatal(err) - } - - caCertFile, err := ioutil.TempFile("", "caCert") - if err != nil { - t.Fatal(err) - } - - defer os.Remove(caCertFile.Name()) - - if _, err := caCertFile.Write([]byte(caCertBundle.Certificate)); err != nil { - t.Fatal(err) - } - if err := caCertFile.Close(); err != nil { - t.Fatal(err) - } - - caKeyFile, err := ioutil.TempFile("", "caKey") - if err != nil { - t.Fatal(err) - } - - defer os.Remove(caKeyFile.Name()) - - if _, err := caKeyFile.Write([]byte(caCertBundle.PrivateKey)); err != nil { - t.Fatal(err) - } - if err := caKeyFile.Close(); err != nil { - t.Fatal(err) - } - - // Prepare template for non-CA cert - - template.IsCA = false - template.SerialNumber = big.NewInt(5678) - - template.KeyUsage = x509.KeyUsage(x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign) - issuedPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatal(err) - } - - issuedPrivateKeyBytes := x509.MarshalPKCS1PrivateKey(issuedPrivateKey) - - issuedPublicKey := &issuedPrivateKey.PublicKey - - // Keep a short certificate lifetime so logins can be tested both when - // cert is valid and when it gets expired - template.NotBefore = time.Now().Add(-2 * time.Second) - template.NotAfter = time.Now().Add(3 * time.Second) - - issuedCertBytes, err := x509.CreateCertificate(rand.Reader, template, caCert, issuedPublicKey, caPrivateKey) - if err != nil { - t.Fatal(err) - } - - issuedCert, err := x509.ParseCertificate(issuedCertBytes) - if err != nil { - t.Fatal(err) - } - - parsedIssuedBundle := &certutil.ParsedCertBundle{ - Certificate: issuedCert, - CertificateBytes: issuedCertBytes, - PrivateKeyBytes: issuedPrivateKeyBytes, - PrivateKeyType: certutil.RSAPrivateKey, - } - - issuedCertBundle, err := parsedIssuedBundle.ToCertBundle() - if err != nil { - t.Fatal(err) - } - - issuedCertFile, err := ioutil.TempFile("", "issuedCert") - if err != nil { - t.Fatal(err) - } - - defer os.Remove(issuedCertFile.Name()) - - if _, err := issuedCertFile.Write([]byte(issuedCertBundle.Certificate)); err != nil { - t.Fatal(err) - } - if err := issuedCertFile.Close(); err != nil { - t.Fatal(err) - } - - issuedKeyFile, err := ioutil.TempFile("", "issuedKey") - if err != nil { - t.Fatal(err) - } - - defer os.Remove(issuedKeyFile.Name()) - - if _, err := issuedKeyFile.Write([]byte(issuedCertBundle.PrivateKey)); err != nil { - t.Fatal(err) - } - if err := issuedKeyFile.Close(); err != nil { - t.Fatal(err) - } - - config := logical.TestBackendConfig() - storage := &logical.InmemStorage{} - config.StorageView = storage - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Register the Non-CA certificate of the client key pair - certData := map[string]interface{}{ - "certificate": issuedCertBundle.Certificate, - "policies": "abc", - "display_name": "cert1", - "ttl": 10000, - } - certReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "certs/cert1", - Storage: storage, - Data: certData, - } - - resp, err = b.HandleRequest(context.Background(), certReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Create connection state using the certificates generated - connState, err := connectionState(caCertFile.Name(), caCertFile.Name(), caKeyFile.Name(), issuedCertFile.Name(), issuedKeyFile.Name()) - if err != nil { - t.Fatalf("error testing connection state:%v", err) - } - - loginReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "login", - Connection: &logical.Connection{ - ConnState: &connState, - }, - } - - // Login when the certificate is still valid. Login should succeed. - resp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Wait until the certificate expires - time.Sleep(5 * time.Second) - - // Login attempt after certificate expiry should fail - resp, err = b.HandleRequest(context.Background(), loginReq) - if err == nil { - t.Fatalf("expected error due to expired certificate") - } -} - -func TestBackend_RegisteredNonCA_CRL(t *testing.T) { - config := logical.TestBackendConfig() - storage := &logical.InmemStorage{} - config.StorageView = storage - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - nonCACert, err := ioutil.ReadFile(testCertPath1) - if err != nil { - t.Fatal(err) - } - - // Register the Non-CA certificate of the client key pair - certData := map[string]interface{}{ - "certificate": nonCACert, - "policies": "abc", - "display_name": "cert1", - "ttl": 10000, - } - certReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "certs/cert1", - Storage: storage, - Data: certData, - } - - resp, err := b.HandleRequest(context.Background(), certReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Connection state is presenting the client Non-CA cert and its key. - // This is exactly what is registered at the backend. - connState, err := connectionState(serverCAPath, serverCertPath, serverKeyPath, testCertPath1, testKeyPath1) - if err != nil { - t.Fatalf("error testing connection state:%v", err) - } - loginReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "login", - Connection: &logical.Connection{ - ConnState: &connState, - }, - } - // Login should succeed. - resp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Register a CRL containing the issued client certificate used above. - issuedCRL, err := ioutil.ReadFile(testIssuedCertCRL) - if err != nil { - t.Fatal(err) - } - crlData := map[string]interface{}{ - "crl": issuedCRL, - } - crlReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "crls/issuedcrl", - Data: crlData, - } - resp, err = b.HandleRequest(context.Background(), crlReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Ensure the CRL shows up on a list. - listReq := &logical.Request{ - Operation: logical.ListOperation, - Storage: storage, - Path: "crls", - Data: map[string]interface{}{}, - } - resp, err = b.HandleRequest(context.Background(), listReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - if len(resp.Data) != 1 || len(resp.Data["keys"].([]string)) != 1 || resp.Data["keys"].([]string)[0] != "issuedcrl" { - t.Fatalf("bad listing: resp:%v", resp) - } - - // Attempt login with the same connection state but with the CRL registered - resp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected failure due to revoked certificate") - } -} - -func TestBackend_CRLs(t *testing.T) { - config := logical.TestBackendConfig() - storage := &logical.InmemStorage{} - config.StorageView = storage - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - clientCA1, err := ioutil.ReadFile(testRootCACertPath1) - if err != nil { - t.Fatal(err) - } - // Register the CA certificate of the client key pair - certData := map[string]interface{}{ - "certificate": clientCA1, - "policies": "abc", - "display_name": "cert1", - "ttl": 10000, - } - - certReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "certs/cert1", - Storage: storage, - Data: certData, - } - - resp, err := b.HandleRequest(context.Background(), certReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Connection state is presenting the client CA cert and its key. - // This is exactly what is registered at the backend. - connState, err := connectionState(serverCAPath, serverCertPath, serverKeyPath, testRootCACertPath1, testRootCAKeyPath1) - if err != nil { - t.Fatalf("error testing connection state:%v", err) - } - loginReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "login", - Connection: &logical.Connection{ - ConnState: &connState, - }, - } - resp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Now, without changing the registered client CA cert, present from - // the client side, a cert issued using the registered CA. - connState, err = connectionState(serverCAPath, serverCertPath, serverKeyPath, testCertPath1, testKeyPath1) - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - loginReq.Connection.ConnState = &connState - - // Attempt login with the updated connection - resp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Register a CRL containing the issued client certificate used above. - issuedCRL, err := ioutil.ReadFile(testIssuedCertCRL) - if err != nil { - t.Fatal(err) - } - crlData := map[string]interface{}{ - "crl": issuedCRL, - } - - crlReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "crls/issuedcrl", - Data: crlData, - } - resp, err = b.HandleRequest(context.Background(), crlReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Attempt login with the revoked certificate. - resp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected failure due to revoked certificate") - } - - // Register a different client CA certificate. - clientCA2, err := ioutil.ReadFile(testRootCACertPath2) - if err != nil { - t.Fatal(err) - } - certData["certificate"] = clientCA2 - resp, err = b.HandleRequest(context.Background(), certReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Test login using a different client CA cert pair. - connState, err = connectionState(serverCAPath, serverCertPath, serverKeyPath, testRootCACertPath2, testRootCAKeyPath2) - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - loginReq.Connection.ConnState = &connState - - // Attempt login with the updated connection - resp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Register a CRL containing the root CA certificate used above. - rootCRL, err := ioutil.ReadFile(testRootCertCRL) - if err != nil { - t.Fatal(err) - } - crlData["crl"] = rootCRL - resp, err = b.HandleRequest(context.Background(), crlReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Attempt login with the same connection state but with the CRL registered - resp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected failure due to revoked certificate") - } -} - -func testFactory(t *testing.T) logical.Backend { - storage := &logical.InmemStorage{} - b, err := Factory(context.Background(), &logical.BackendConfig{ - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: 1000 * time.Second, - MaxLeaseTTLVal: 1800 * time.Second, - }, - StorageView: storage, - }) - if err != nil { - t.Fatalf("error: %s", err) - } - if err := b.Initialize(context.Background(), &logical.InitializationRequest{ - Storage: storage, - }); err != nil { - t.Fatalf("error: %s", err) - } - return b -} - -// Test the certificates being registered to the backend -func TestBackend_CertWrites(t *testing.T) { - // CA cert - ca1, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - // Non CA Cert - ca2, err := ioutil.ReadFile("test-fixtures/keys/cert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - // Non CA cert without TLS web client authentication - ca3, err := ioutil.ReadFile("test-fixtures/noclientauthcert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - - tc := logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "aaa", ca1, "foo", allowed{}, false), - testAccStepCert(t, "bbb", ca2, "foo", allowed{}, false), - testAccStepCert(t, "ccc", ca3, "foo", allowed{}, true), - }, - } - tc.Steps = append(tc.Steps, testAccStepListCerts(t, []string{"aaa", "bbb"})...) - logicaltest.Test(t, tc) -} - -// Test a client trusted by a CA -func TestBackend_basic_CA(t *testing.T) { - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{}, false), - testAccStepLogin(t, connState), - testAccStepCertLease(t, "web", ca, "foo"), - testAccStepCertTTL(t, "web", ca, "foo"), - testAccStepLogin(t, connState), - testAccStepCertMaxTTL(t, "web", ca, "foo"), - testAccStepLogin(t, connState), - testAccStepCertNoLease(t, "web", ca, "foo"), - testAccStepLoginDefaultLease(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "*.example.com"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "*.invalid.com"}, false), - testAccStepLoginInvalid(t, connState), - }, - }) -} - -// Test CRL behavior -func TestBackend_Basic_CRLs(t *testing.T) { - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - crl, err := ioutil.ReadFile("test-fixtures/root/root.crl") - if err != nil { - t.Fatalf("err: %v", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCertNoLease(t, "web", ca, "foo"), - testAccStepLoginDefaultLease(t, connState), - testAccStepAddCRL(t, crl, connState), - testAccStepReadCRL(t, connState), - testAccStepLoginInvalid(t, connState), - testAccStepDeleteCRL(t, connState), - testAccStepLoginDefaultLease(t, connState), - }, - }) -} - -// Test a self-signed client (root CA) that is trusted -func TestBackend_basic_singleCert(t *testing.T) { - connState, err := testConnState("test-fixtures/root/rootcacert.pem", - "test-fixtures/root/rootcakey.pem", "test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.4:invalid"}, false), - testAccStepLoginInvalid(t, connState), - }, - }) -} - -func TestBackend_common_name_singleCert(t *testing.T) { - connState, err := testConnState("test-fixtures/root/rootcacert.pem", - "test-fixtures/root/rootcakey.pem", "test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{common_names: "example.com"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{common_names: "invalid"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.4:invalid"}, false), - testAccStepLoginInvalid(t, connState), - }, - }) -} - -// Test a self-signed client with custom ext (root CA) that is trusted -func TestBackend_ext_singleCert(t *testing.T) { - connState, err := testConnState( - "test-fixtures/root/rootcawextcert.pem", - "test-fixtures/root/rootcawextkey.pem", - "test-fixtures/root/rootcacert.pem", - ) - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:A UTF8String Extension"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.45:*"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:The Wrong Value"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:,2.1.1.2:*"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:A UTF8String Extension"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "1.2.3.45:*"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:The Wrong Value"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:A UTF8String Extension"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "1.2.3.45:*"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:The Wrong Value"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepReadConfig(t, config{EnableIdentityAliasMetadata: false}, connState), - testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false), - testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}, false), - testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "1.2.3.45"}, false), - testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}, false), - testAccStepSetConfig(t, config{EnableIdentityAliasMetadata: true}, connState), - testAccStepReadConfig(t, config{EnableIdentityAliasMetadata: true}, connState), - testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false), - testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}, true), - testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "1.2.3.45"}, false), - testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}, true), - }, - }) -} - -// Test a self-signed client with URI alt names (root CA) that is trusted -func TestBackend_dns_singleCert(t *testing.T) { - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "example.com", - }, - DNSNames: []string{"example.com"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - - tempDir, connState, err := generateTestCertAndConnState(t, certTemplate) - if tempDir != "" { - defer os.RemoveAll(tempDir) - } - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem")) - if err != nil { - t.Fatalf("err: %v", err) - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{dns: "*ample.com"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{dns: "notincert.com"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{dns: "abc"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{dns: "*.example.com"}, false), - testAccStepLoginInvalid(t, connState), - }, - }) -} - -// Test a self-signed client with URI alt names (root CA) that is trusted -func TestBackend_email_singleCert(t *testing.T) { - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "example.com", - }, - EmailAddresses: []string{"valid@example.com"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - - tempDir, connState, err := generateTestCertAndConnState(t, certTemplate) - if tempDir != "" { - defer os.RemoveAll(tempDir) - } - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem")) - if err != nil { - t.Fatalf("err: %v", err) - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{emails: "valid@example.com"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{emails: "*@example.com"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{emails: "invalid@notincert.com"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{emails: "abc"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{emails: "*.example.com"}, false), - testAccStepLoginInvalid(t, connState), - }, - }) -} - -// Test a self-signed client with OU (root CA) that is trusted -func TestBackend_organizationalUnit_singleCert(t *testing.T) { - connState, err := testConnState( - "test-fixtures/root/rootcawoucert.pem", - "test-fixtures/root/rootcawoukey.pem", - "test-fixtures/root/rootcawoucert.pem", - ) - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcawoucert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "engineering"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "eng*"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "engineering,finance"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "foo"}, false), - testAccStepLoginInvalid(t, connState), - }, - }) -} - -// Test a self-signed client with URI alt names (root CA) that is trusted -func TestBackend_uri_singleCert(t *testing.T) { - u, err := url.Parse("spiffe://example.com/host") - if err != nil { - t.Fatal(err) - } - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "example.com", - }, - DNSNames: []string{"example.com"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, - URIs: []*url.URL{u}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - - tempDir, connState, err := generateTestCertAndConnState(t, certTemplate) - if tempDir != "" { - defer os.RemoveAll(tempDir) - } - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem")) - if err != nil { - t.Fatalf("err: %v", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{uris: "spiffe://example.com/*"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{uris: "spiffe://example.com/host"}, false), - testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{uris: "spiffe://example.com/invalid"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{uris: "abc"}, false), - testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{uris: "http://www.google.com"}, false), - testAccStepLoginInvalid(t, connState), - }, - }) -} - -// Test against a collection of matching and non-matching rules -func TestBackend_mixed_constraints(t *testing.T) { - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "1unconstrained", ca, "foo", allowed{}, false), - testAccStepCert(t, "2matching", ca, "foo", allowed{names: "*.example.com,whatever"}, false), - testAccStepCert(t, "3invalid", ca, "foo", allowed{names: "invalid"}, false), - testAccStepLogin(t, connState), - // Assumes CertEntries are processed in alphabetical order (due to store.List), so we only match 2matching if 1unconstrained doesn't match - testAccStepLoginWithName(t, connState, "2matching"), - testAccStepLoginWithNameInvalid(t, connState, "3invalid"), - }, - }) -} - -// Test an untrusted client -func TestBackend_untrusted(t *testing.T) { - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepLoginInvalid(t, connState), - }, - }) -} - -func TestBackend_validCIDR(t *testing.T) { - config := logical.TestBackendConfig() - storage := &logical.InmemStorage{} - config.StorageView = storage - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - - name := "web" - boundCIDRs := []string{"127.0.0.1", "128.252.0.0/16"} - - addCertReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "certs/" + name, - Data: map[string]interface{}{ - "certificate": string(ca), - "policies": "foo", - "display_name": name, - "allowed_names": "", - "required_extensions": "", - "lease": 1000, - "bound_cidrs": boundCIDRs, - }, - Storage: storage, - Connection: &logical.Connection{ConnState: &connState}, - } - - _, err = b.HandleRequest(context.Background(), addCertReq) - if err != nil { - t.Fatal(err) - } - - readCertReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "certs/" + name, - Storage: storage, - Connection: &logical.Connection{ConnState: &connState}, - } - - readResult, err := b.HandleRequest(context.Background(), readCertReq) - if err != nil { - t.Fatal(err) - } - cidrsResult := readResult.Data["bound_cidrs"].([]*sockaddr.SockAddrMarshaler) - - if cidrsResult[0].String() != boundCIDRs[0] || - cidrsResult[1].String() != boundCIDRs[1] { - t.Fatalf("bound_cidrs couldn't be set correctly, EXPECTED: %v, ACTUAL: %v", boundCIDRs, cidrsResult) - } - - loginReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Unauthenticated: true, - Data: map[string]interface{}{ - "name": name, - }, - Storage: storage, - Connection: &logical.Connection{ConnState: &connState}, - } - - // override the remote address with an IPV4 that is authorized - loginReq.Connection.RemoteAddr = "127.0.0.1/32" - - _, err = b.HandleRequest(context.Background(), loginReq) - if err != nil { - t.Fatal(err.Error()) - } -} - -func TestBackend_invalidCIDR(t *testing.T) { - config := logical.TestBackendConfig() - storage := &logical.InmemStorage{} - config.StorageView = storage - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - - name := "web" - - addCertReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "certs/" + name, - Data: map[string]interface{}{ - "certificate": string(ca), - "policies": "foo", - "display_name": name, - "allowed_names": "", - "required_extensions": "", - "lease": 1000, - "bound_cidrs": []string{"127.0.0.1/32", "128.252.0.0/16"}, - }, - Storage: storage, - Connection: &logical.Connection{ConnState: &connState}, - } - - _, err = b.HandleRequest(context.Background(), addCertReq) - if err != nil { - t.Fatal(err) - } - - loginReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Unauthenticated: true, - Data: map[string]interface{}{ - "name": name, - }, - Storage: storage, - Connection: &logical.Connection{ConnState: &connState}, - } - - // override the remote address with an IPV4 that isn't authorized - loginReq.Connection.RemoteAddr = "127.0.0.1/8" - - _, err = b.HandleRequest(context.Background(), loginReq) - if err == nil { - t.Fatal("expected \"ERROR: permission denied\"") - } -} - -func testAccStepAddCRL(t *testing.T, crl []byte, connState tls.ConnectionState) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "crls/test", - ConnState: &connState, - Data: map[string]interface{}{ - "crl": crl, - }, - } -} - -func testAccStepReadCRL(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "crls/test", - ConnState: &connState, - Check: func(resp *logical.Response) error { - crlInfo := CRLInfo{} - err := mapstructure.Decode(resp.Data, &crlInfo) - if err != nil { - t.Fatalf("err: %v", err) - } - if len(crlInfo.Serials) != 1 { - t.Fatalf("bad: expected CRL with length 1, got %d", len(crlInfo.Serials)) - } - if _, ok := crlInfo.Serials["637101449987587619778072672905061040630001617053"]; !ok { - t.Fatalf("bad: expected serial number not found in CRL") - } - return nil - }, - } -} - -func testAccStepDeleteCRL(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "crls/test", - ConnState: &connState, - } -} - -func testAccStepSetConfig(t *testing.T, conf config, connState tls.ConnectionState) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config", - ConnState: &connState, - Data: map[string]interface{}{ - "enable_identity_alias_metadata": conf.EnableIdentityAliasMetadata, - }, - } -} - -func testAccStepReadConfig(t *testing.T, conf config, connState tls.ConnectionState) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "config", - ConnState: &connState, - Check: func(resp *logical.Response) error { - value, ok := resp.Data["enable_identity_alias_metadata"] - if !ok { - t.Fatalf("enable_identity_alias_metadata not found in response") - } - - b, ok := value.(bool) - if !ok { - t.Fatalf("bad: expected enable_identity_alias_metadata to be a bool") - } - - if b != conf.EnableIdentityAliasMetadata { - t.Fatalf("bad: expected enable_identity_alias_metadata to be %t, got %t", conf.EnableIdentityAliasMetadata, b) - } - - return nil - }, - } -} - -func testAccStepLogin(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { - return testAccStepLoginWithName(t, connState, "") -} - -func testAccStepLoginWithName(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login", - Unauthenticated: true, - ConnState: &connState, - Check: func(resp *logical.Response) error { - if resp.Auth.TTL != 1000*time.Second { - t.Fatalf("bad lease length: %#v", resp.Auth) - } - - if certName != "" && resp.Auth.DisplayName != ("mnt-"+certName) { - t.Fatalf("matched the wrong cert: %#v", resp.Auth.DisplayName) - } - - fn := logicaltest.TestCheckAuth([]string{"default", "foo"}) - return fn(resp) - }, - Data: map[string]interface{}{ - "name": certName, - }, - } -} - -func testAccStepLoginDefaultLease(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login", - Unauthenticated: true, - ConnState: &connState, - Check: func(resp *logical.Response) error { - if resp.Auth.TTL != 1000*time.Second { - t.Fatalf("bad lease length: %#v", resp.Auth) - } - - fn := logicaltest.TestCheckAuth([]string{"default", "foo"}) - return fn(resp) - }, - } -} - -func testAccStepLoginWithMetadata(t *testing.T, connState tls.ConnectionState, certName string, metadata map[string]string, expectAliasMetadata bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login", - Unauthenticated: true, - ConnState: &connState, - Check: func(resp *logical.Response) error { - // Check for fixed metadata too - metadata["cert_name"] = certName - metadata["common_name"] = connState.PeerCertificates[0].Subject.CommonName - metadata["serial_number"] = connState.PeerCertificates[0].SerialNumber.String() - metadata["subject_key_id"] = certutil.GetHexFormatted(connState.PeerCertificates[0].SubjectKeyId, ":") - metadata["authority_key_id"] = certutil.GetHexFormatted(connState.PeerCertificates[0].AuthorityKeyId, ":") - - for key, expected := range metadata { - value, ok := resp.Auth.Metadata[key] - if !ok { - t.Fatalf("missing metadata key: %s", key) - } - - if value != expected { - t.Fatalf("expected metadata key %s to equal %s, but got: %s", key, expected, value) - } - - if expectAliasMetadata { - value, ok = resp.Auth.Alias.Metadata[key] - if !ok { - t.Fatalf("missing alias metadata key: %s", key) - } - - if value != expected { - t.Fatalf("expected metadata key %s to equal %s, but got: %s", key, expected, value) - } - } else { - if len(resp.Auth.Alias.Metadata) > 0 { - t.Fatal("found alias metadata keys, but should not have any") - } - } - } - - fn := logicaltest.TestCheckAuth([]string{"default", "foo"}) - return fn(resp) - }, - Data: map[string]interface{}{ - "metadata": metadata, - }, - } -} - -func testAccStepLoginInvalid(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { - return testAccStepLoginWithNameInvalid(t, connState, "") -} - -func testAccStepLoginWithNameInvalid(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login", - Unauthenticated: true, - ConnState: &connState, - Check: func(resp *logical.Response) error { - if resp.Auth != nil { - return fmt.Errorf("should not be authorized: %#v", resp) - } - return nil - }, - Data: map[string]interface{}{ - "name": certName, - }, - ErrorOk: true, - } -} - -func testAccStepListCerts( - t *testing.T, certs []string, -) []logicaltest.TestStep { - return []logicaltest.TestStep{ - { - Operation: logical.ListOperation, - Path: "certs", - Check: func(resp *logical.Response) error { - if resp == nil { - return fmt.Errorf("nil response") - } - if resp.Data == nil { - return fmt.Errorf("nil data") - } - if resp.Data["keys"] == interface{}(nil) { - return fmt.Errorf("nil keys") - } - keys := resp.Data["keys"].([]string) - if !reflect.DeepEqual(keys, certs) { - return fmt.Errorf("mismatch: keys is %#v, certs is %#v", keys, certs) - } - return nil - }, - }, { - Operation: logical.ListOperation, - Path: "certs/", - Check: func(resp *logical.Response) error { - if resp == nil { - return fmt.Errorf("nil response") - } - if resp.Data == nil { - return fmt.Errorf("nil data") - } - if resp.Data["keys"] == interface{}(nil) { - return fmt.Errorf("nil keys") - } - keys := resp.Data["keys"].([]string) - if !reflect.DeepEqual(keys, certs) { - return fmt.Errorf("mismatch: keys is %#v, certs is %#v", keys, certs) - } - - return nil - }, - }, - } -} - -type allowed struct { - names string // allowed names in the certificate, looks at common, name, dns, email [depricated] - common_names string // allowed common names in the certificate - dns string // allowed dns names in the SAN extension of the certificate - emails string // allowed email names in SAN extension of the certificate - uris string // allowed uris in SAN extension of the certificate - organizational_units string // allowed OUs in the certificate - ext string // required extensions in the certificate - metadata_ext string // allowed metadata extensions to add to identity alias -} - -func testAccStepCert(t *testing.T, name string, cert []byte, policies string, testData allowed, expectError bool) logicaltest.TestStep { - return testAccStepCertWithExtraParams(t, name, cert, policies, testData, expectError, nil) -} - -func testAccStepCertWithExtraParams(t *testing.T, name string, cert []byte, policies string, testData allowed, expectError bool, extraParams map[string]interface{}) logicaltest.TestStep { - data := map[string]interface{}{ - "certificate": string(cert), - "policies": policies, - "display_name": name, - "allowed_names": testData.names, - "allowed_common_names": testData.common_names, - "allowed_dns_sans": testData.dns, - "allowed_email_sans": testData.emails, - "allowed_uri_sans": testData.uris, - "allowed_organizational_units": testData.organizational_units, - "required_extensions": testData.ext, - "allowed_metadata_extensions": testData.metadata_ext, - "lease": 1000, - } - for k, v := range extraParams { - data[k] = v - } - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "certs/" + name, - ErrorOk: expectError, - Data: data, - Check: func(resp *logical.Response) error { - if resp == nil && expectError { - return fmt.Errorf("expected error but received nil") - } - return nil - }, - } -} - -func testAccStepReadCertPolicy(t *testing.T, name string, expectError bool, expected map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "certs/" + name, - ErrorOk: expectError, - Data: nil, - Check: func(resp *logical.Response) error { - if (resp == nil || len(resp.Data) == 0) && expectError { - return fmt.Errorf("expected error but received nil") - } - for key, expectedValue := range expected { - actualValue := resp.Data[key] - if expectedValue != actualValue { - return fmt.Errorf("Expected to get [%v]=[%v] but read [%v]=[%v] from server for certs/%v: %v", key, expectedValue, key, actualValue, name, resp) - } - } - return nil - }, - } -} - -func testAccStepCertLease( - t *testing.T, name string, cert []byte, policies string, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "certs/" + name, - Data: map[string]interface{}{ - "certificate": string(cert), - "policies": policies, - "display_name": name, - "lease": 1000, - }, - } -} - -func testAccStepCertTTL( - t *testing.T, name string, cert []byte, policies string, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "certs/" + name, - Data: map[string]interface{}{ - "certificate": string(cert), - "policies": policies, - "display_name": name, - "ttl": "1000s", - }, - } -} - -func testAccStepCertMaxTTL( - t *testing.T, name string, cert []byte, policies string, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "certs/" + name, - Data: map[string]interface{}{ - "certificate": string(cert), - "policies": policies, - "display_name": name, - "ttl": "1000s", - "max_ttl": "1200s", - }, - } -} - -func testAccStepCertNoLease( - t *testing.T, name string, cert []byte, policies string, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "certs/" + name, - Data: map[string]interface{}{ - "certificate": string(cert), - "policies": policies, - "display_name": name, - }, - } -} - -func testConnState(certPath, keyPath, rootCertPath string) (tls.ConnectionState, error) { - cert, err := tls.LoadX509KeyPair(certPath, keyPath) - if err != nil { - return tls.ConnectionState{}, err - } - rootConfig := &rootcerts.Config{ - CAFile: rootCertPath, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) - if err != nil { - return tls.ConnectionState{}, err - } - - return testConnStateWithCert(cert, rootCAs) -} - -func testConnStateWithCert(cert tls.Certificate, rootCAs *x509.CertPool) (tls.ConnectionState, error) { - listenConf := &tls.Config{ - Certificates: []tls.Certificate{cert}, - ClientAuth: tls.RequestClientCert, - InsecureSkipVerify: false, - RootCAs: rootCAs, - } - dialConf := listenConf.Clone() - // start a server - list, err := tls.Listen("tcp", "127.0.0.1:0", listenConf) - if err != nil { - return tls.ConnectionState{}, err - } - defer list.Close() - - // Accept connections. - serverErrors := make(chan error, 1) - connState := make(chan tls.ConnectionState) - go func() { - defer close(connState) - serverConn, err := list.Accept() - serverErrors <- err - if err != nil { - close(serverErrors) - return - } - defer serverConn.Close() - - // Read the ping - buf := make([]byte, 4) - _, err = serverConn.Read(buf) - if (err != nil) && (err != io.EOF) { - serverErrors <- err - close(serverErrors) - return - } else { - // EOF is a reasonable error condition, so swallow it. - serverErrors <- nil - } - close(serverErrors) - connState <- serverConn.(*tls.Conn).ConnectionState() - }() - - // Establish a connection from the client side and write a few bytes. - clientErrors := make(chan error, 1) - go func() { - addr := list.Addr().String() - conn, err := tls.Dial("tcp", addr, dialConf) - clientErrors <- err - if err != nil { - close(clientErrors) - return - } - defer conn.Close() - - // Write ping - _, err = conn.Write([]byte("ping")) - clientErrors <- err - close(clientErrors) - }() - - for err = range clientErrors { - if err != nil { - return tls.ConnectionState{}, fmt.Errorf("error in client goroutine:%v", err) - } - } - - for err = range serverErrors { - if err != nil { - return tls.ConnectionState{}, fmt.Errorf("error in server goroutine:%v", err) - } - } - // Grab the current state - return <-connState, nil -} - -func Test_Renew(t *testing.T) { - storage := &logical.InmemStorage{} - - lb, err := Factory(context.Background(), &logical.BackendConfig{ - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: 300 * time.Second, - MaxLeaseTTLVal: 1800 * time.Second, - }, - StorageView: storage, - }) - if err != nil { - t.Fatalf("error: %s", err) - } - - b := lb.(*backend) - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatal(err) - } - - req := &logical.Request{ - Connection: &logical.Connection{ - ConnState: &connState, - }, - Storage: storage, - Auth: &logical.Auth{}, - } - - fd := &framework.FieldData{ - Raw: map[string]interface{}{ - "name": "test", - "certificate": ca, - "policies": "foo,bar", - }, - Schema: pathCerts(b).Fields, - } - - resp, err := b.pathCertWrite(context.Background(), req, fd) - if err != nil { - t.Fatal(err) - } - - empty_login_fd := &framework.FieldData{ - Raw: map[string]interface{}{}, - Schema: pathLogin(b).Fields, - } - resp, err = b.pathLogin(context.Background(), req, empty_login_fd) - if err != nil { - t.Fatal(err) - } - if resp.IsError() { - t.Fatalf("got error: %#v", *resp) - } - req.Auth.InternalData = resp.Auth.InternalData - req.Auth.Metadata = resp.Auth.Metadata - req.Auth.LeaseOptions = resp.Auth.LeaseOptions - req.Auth.Policies = resp.Auth.Policies - req.Auth.TokenPolicies = req.Auth.Policies - req.Auth.Period = resp.Auth.Period - - // Normal renewal - resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - if resp.IsError() { - t.Fatalf("got error: %#v", *resp) - } - - // Change the policies -- this should fail - fd.Raw["policies"] = "zip,zap" - resp, err = b.pathCertWrite(context.Background(), req, fd) - if err != nil { - t.Fatal(err) - } - - resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd) - if err == nil { - t.Fatal("expected error") - } - - // Put the policies back, this should be okay - fd.Raw["policies"] = "bar,foo" - resp, err = b.pathCertWrite(context.Background(), req, fd) - if err != nil { - t.Fatal(err) - } - - resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - if resp.IsError() { - t.Fatalf("got error: %#v", *resp) - } - - // Add period value to cert entry - period := 350 * time.Second - fd.Raw["period"] = period.String() - resp, err = b.pathCertWrite(context.Background(), req, fd) - if err != nil { - t.Fatal(err) - } - - resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - if resp.IsError() { - t.Fatalf("got error: %#v", *resp) - } - - if resp.Auth.Period != period { - t.Fatalf("expected a period value of %s in the response, got: %s", period, resp.Auth.Period) - } - - // Delete CA, make sure we can't renew - resp, err = b.pathCertDelete(context.Background(), req, fd) - if err != nil { - t.Fatal(err) - } - - resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - if !resp.IsError() { - t.Fatal("expected error") - } -} - -func TestBackend_CertUpgrade(t *testing.T) { - s := &logical.InmemStorage{} - - config := logical.TestBackendConfig() - config.StorageView = s - - ctx := context.Background() - - b := Backend() - if b == nil { - t.Fatalf("failed to create backend") - } - if err := b.Setup(ctx, config); err != nil { - t.Fatal(err) - } - - foo := &CertEntry{ - Policies: []string{"foo"}, - Period: time.Second, - TTL: time.Second, - MaxTTL: time.Second, - BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}}, - } - - entry, err := logical.StorageEntryJSON("cert/foo", foo) - if err != nil { - t.Fatal(err) - } - err = s.Put(ctx, entry) - if err != nil { - t.Fatal(err) - } - - certEntry, err := b.Cert(ctx, s, "foo") - if err != nil { - t.Fatal(err) - } - - exp := &CertEntry{ - Policies: []string{"foo"}, - Period: time.Second, - TTL: time.Second, - MaxTTL: time.Second, - BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}}, - TokenParams: tokenutil.TokenParams{ - TokenPolicies: []string{"foo"}, - TokenPeriod: time.Second, - TokenTTL: time.Second, - TokenMaxTTL: time.Second, - TokenBoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}}, - }, - } - if diff := deep.Equal(certEntry, exp); diff != nil { - t.Fatal(diff) - } -} - -// TestOCSPFailOpenWithBadIssuer validates we fail all different types of cert auth -// login scenarios if we encounter an OCSP verification error -func TestOCSPFailOpenWithBadIssuer(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") - - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) - connState, err := testConnStateWithCert(leafTLS, rootCAs) - require.NoError(t, err, "error testing connection state: %v", err) - - badCa, badCaKey := createCa(t) - - // Setup an OCSP handler - ocspHandler := func(ca *x509.Certificate, caKey crypto.Signer) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - now := time.Now() - ocspRes := ocsp.Response{ - SerialNumber: leafTLS.Leaf.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: ocsp.Good, - } - response, err := ocsp.CreateResponse(ca, ca, ocspRes, caKey) - if err != nil { - t.Fatalf("failed generating OCSP response: %v", err) - } - _, _ = w.Write(response) - }) - } - goodTs := httptest.NewServer(ocspHandler(caTLS.Leaf, caTLS.PrivateKey.(crypto.Signer))) - badTs := httptest.NewServer(ocspHandler(badCa, badCaKey)) - defer goodTs.Close() - defer badTs.Close() - - steps := []logicaltest.TestStep{ - // step 1/2: This should fail as we get a response from a bad root, even with ocsp_fail_open is set to true - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{badTs.URL}, - "ocsp_query_all_servers": false, - "ocsp_fail_open": true, - }), - testAccStepLoginInvalid(t, connState), - // step 3/4: This should fail as we query all the servers which will get a response with an invalid signature - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{goodTs.URL, badTs.URL}, - "ocsp_query_all_servers": true, - "ocsp_fail_open": true, - }), - testAccStepLoginInvalid(t, connState), - // step 5/6: This should fail as we will query the OCSP server with the bad root key first. - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{badTs.URL, goodTs.URL}, - "ocsp_query_all_servers": false, - "ocsp_fail_open": true, - }), - testAccStepLoginInvalid(t, connState), - // step 7/8: This should pass as we will only query the first server with the valid root signature - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{goodTs.URL, badTs.URL}, - "ocsp_query_all_servers": false, - "ocsp_fail_open": true, - }), - testAccStepLogin(t, connState), - } - - // Setup a new factory everytime to avoid OCSP caching from influencing the test - for i := 0; i < len(steps); i += 2 { - setup := i - execute := i + 1 - t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) { - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{steps[setup], steps[execute]}, - }) - }) - } -} - -// TestOCSPWithMixedValidResponses validates the expected behavior of multiple OCSP servers configured, -// with and without ocsp_query_all_servers enabled or disabled. -func TestOCSPWithMixedValidResponses(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") - - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) - connState, err := testConnStateWithCert(leafTLS, rootCAs) - require.NoError(t, err, "error testing connection state: %v", err) - - // Setup an OCSP handler - ocspHandler := func(status int) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - now := time.Now() - ocspRes := ocsp.Response{ - SerialNumber: leafTLS.Leaf.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: status, - } - response, err := ocsp.CreateResponse(caTLS.Leaf, caTLS.Leaf, ocspRes, caTLS.PrivateKey.(crypto.Signer)) - if err != nil { - t.Fatalf("failed generating OCSP response: %v", err) - } - _, _ = w.Write(response) - }) - } - goodTs := httptest.NewServer(ocspHandler(ocsp.Good)) - revokeTs := httptest.NewServer(ocspHandler(ocsp.Revoked)) - defer goodTs.Close() - defer revokeTs.Close() - - steps := []logicaltest.TestStep{ - // step 1/2: This should pass as we will query the first server and get a valid good response, not testing - // the second configured server - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{goodTs.URL, revokeTs.URL}, - "ocsp_query_all_servers": false, - }), - testAccStepLogin(t, connState), - // step 3/4: This should fail as we will query the revoking OCSP server first and get a revoke response - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{revokeTs.URL, goodTs.URL}, - "ocsp_query_all_servers": false, - }), - testAccStepLoginInvalid(t, connState), - // step 5/6: This should fail as we will query all the OCSP servers and prefer the revoke response - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", - allowed{names: "cert.example.com"}, false, map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{goodTs.URL, revokeTs.URL}, - "ocsp_query_all_servers": true, - }), - testAccStepLoginInvalid(t, connState), - } - - // Setup a new factory everytime to avoid OCSP caching from influencing the test - for i := 0; i < len(steps); i += 2 { - setup := i - execute := i + 1 - t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) { - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{steps[setup], steps[execute]}, - }) - }) - } -} - -// TestOCSPFailOpenWithGoodResponse validates the expected behavior with multiple OCSP servers configured -// one that returns a Good response the other is not available, along with the ocsp_fail_open in multiple modes -func TestOCSPFailOpenWithGoodResponse(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") - - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) - connState, err := testConnStateWithCert(leafTLS, rootCAs) - require.NoError(t, err, "error testing connection state: %v", err) - - // Setup an OCSP handler - ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - now := time.Now() - ocspRes := ocsp.Response{ - SerialNumber: leafTLS.Leaf.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: ocsp.Good, - } - response, err := ocsp.CreateResponse(caTLS.Leaf, caTLS.Leaf, ocspRes, caTLS.PrivateKey.(crypto.Signer)) - if err != nil { - t.Fatalf("failed generating OCSP response: %v", err) - } - _, _ = w.Write(response) - }) - ts := httptest.NewServer(ocspHandler) - defer ts.Close() - - steps := []logicaltest.TestStep{ - // Step 1/2 With no proper responses from any OCSP server and fail_open to true, we should pass validation - // as fail_open is true - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{"http://127.0.0.1:30000", "http://127.0.0.1:30001"}, - "ocsp_fail_open": true, - "ocsp_query_all_servers": false, - "ocsp_max_retries": 0, - }), - testAccStepLogin(t, connState), - // Step 3/4 With no proper responses from any OCSP server and fail_open to false we should fail validation - // as fail_open is false - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", - allowed{names: "cert.example.com"}, false, map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{"http://127.0.0.1:30000", "http://127.0.0.1:30001"}, - "ocsp_fail_open": false, - "ocsp_query_all_servers": false, - "ocsp_max_retries": 0, - }), - testAccStepLoginInvalid(t, connState), - // Step 5/6 With a single positive response, query all servers set to false and fail open true, pass validation - // as query all servers is false - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": true, - "ocsp_query_all_servers": false, - "ocsp_max_retries": 0, - }), - testAccStepLogin(t, connState), - // Step 7/8 With a single positive response, query all servers set to false and fail open false, pass validation - // as query all servers is false - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", - allowed{names: "cert.example.com"}, false, map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": false, - "ocsp_query_all_servers": false, - "ocsp_max_retries": 0, - }), - testAccStepLogin(t, connState), - // Step 9/10 With a single positive response, query all servers set to true and fail open true, pass validation - // as fail open is true - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": true, - "ocsp_query_all_servers": true, - "ocsp_max_retries": 0, - }), - testAccStepLogin(t, connState), - // Step 11/12 With a single positive response, query all servers set to true and fail open false, fail validation - // as not all servers agree - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", - allowed{names: "cert.example.com"}, false, map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": false, - "ocsp_query_all_servers": true, - "ocsp_max_retries": 0, - }), - testAccStepLoginInvalid(t, connState), - } - - // Setup a new factory everytime to avoid OCSP caching from influencing the test - for i := 0; i < len(steps); i += 2 { - setup := i - execute := i + 1 - t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) { - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{steps[setup], steps[execute]}, - }) - }) - } -} - -// TestOCSPFailOpenWithRevokeResponse validates the expected behavior with multiple OCSP servers configured -// one that returns a Revoke response the other is not available, along with the ocsp_fail_open in multiple modes -func TestOCSPFailOpenWithRevokeResponse(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") - - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) - connState, err := testConnStateWithCert(leafTLS, rootCAs) - require.NoError(t, err, "error testing connection state: %v", err) - - // Setup an OCSP handler - ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - now := time.Now() - ocspRes := ocsp.Response{ - SerialNumber: leafTLS.Leaf.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: ocsp.Revoked, - } - response, err := ocsp.CreateResponse(caTLS.Leaf, caTLS.Leaf, ocspRes, caTLS.PrivateKey.(crypto.Signer)) - if err != nil { - t.Fatalf("failed generating OCSP response: %v", err) - } - _, _ = w.Write(response) - }) - ts := httptest.NewServer(ocspHandler) - defer ts.Close() - - // With no OCSP servers available, make sure that we behave as we expect - steps := []logicaltest.TestStep{ - // Step 1/2 With a single revoke response, query all servers set to false and fail open true, fail validation - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": true, - "ocsp_query_all_servers": false, - "ocsp_max_retries": 0, - }), - testAccStepLoginInvalid(t, connState), - // Step 3/4 With a single revoke response, query all servers set to false and fail open false, fail validation - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", - allowed{names: "cert.example.com"}, false, map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": false, - "ocsp_query_all_servers": false, - "ocsp_max_retries": 0, - }), - testAccStepLoginInvalid(t, connState), - // Step 5/6 With a single revoke response, query all servers set to true and fail open false, fail validation - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", - allowed{names: "cert.example.com"}, false, map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": false, - "ocsp_query_all_servers": true, - "ocsp_max_retries": 0, - }), - testAccStepLoginInvalid(t, connState), - // Step 7/8 With a single revoke response, query all servers set to true and fail open true, fail validation - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": true, - "ocsp_query_all_servers": true, - "ocsp_max_retries": 0, - }), - testAccStepLoginInvalid(t, connState), - } - - // Setup a new factory everytime to avoid OCSP caching from influencing the test - for i := 0; i < len(steps); i += 2 { - setup := i - execute := i + 1 - t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) { - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{steps[setup], steps[execute]}, - }) - }) - } -} - -// TestOCSPFailOpenWithUnknownResponse validates the expected behavior with multiple OCSP servers configured -// one that returns an Unknown response the other is not available, along with the ocsp_fail_open in multiple modes -func TestOCSPFailOpenWithUnknownResponse(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") - - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) - connState, err := testConnStateWithCert(leafTLS, rootCAs) - require.NoError(t, err, "error testing connection state: %v", err) - - // Setup an OCSP handler - ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - now := time.Now() - ocspRes := ocsp.Response{ - SerialNumber: leafTLS.Leaf.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: ocsp.Unknown, - } - response, err := ocsp.CreateResponse(caTLS.Leaf, caTLS.Leaf, ocspRes, caTLS.PrivateKey.(crypto.Signer)) - if err != nil { - t.Fatalf("failed generating OCSP response: %v", err) - } - _, _ = w.Write(response) - }) - ts := httptest.NewServer(ocspHandler) - defer ts.Close() - - // With no OCSP servers available, make sure that we behave as we expect - steps := []logicaltest.TestStep{ - // Step 1/2 With a single unknown response, query all servers set to false and fail open true, pass validation - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": true, - "ocsp_query_all_servers": false, - "ocsp_max_retries": 0, - }), - testAccStepLogin(t, connState), - // Step 3/4 With a single unknown response, query all servers set to false and fail open false, fail validation - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", - allowed{names: "cert.example.com"}, false, map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": false, - "ocsp_query_all_servers": false, - "ocsp_max_retries": 0, - }), - testAccStepLoginInvalid(t, connState), - // Step 5/6 With a single unknown response, query all servers set to true and fail open true, fail validation - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false, - map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": true, - "ocsp_query_all_servers": true, - "ocsp_max_retries": 0, - }), - testAccStepLogin(t, connState), - // Step 7/8 With a single unknown response, query all servers set to true and fail open false, fail validation - testAccStepCertWithExtraParams(t, "web", pemCa, "foo", - allowed{names: "cert.example.com"}, false, map[string]interface{}{ - "ocsp_enabled": true, - "ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"}, - "ocsp_fail_open": false, - "ocsp_query_all_servers": true, - "ocsp_max_retries": 0, - }), - testAccStepLoginInvalid(t, connState), - } - - // Setup a new factory everytime to avoid OCSP caching from influencing the test - for i := 0; i < len(steps); i += 2 { - setup := i - execute := i + 1 - t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) { - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{steps[setup], steps[execute]}, - }) - }) - } -} - -// TestOcspMaxRetriesUpdate verifies that the ocsp_max_retries field is properly initialized -// with our default value of 4, legacy roles have it initialized automatically to 4 and we -// can properly store and retrieve updates to the field. -func TestOcspMaxRetriesUpdate(t *testing.T) { - storage := &logical.InmemStorage{} - ctx := context.Background() - - lb, err := Factory(context.Background(), &logical.BackendConfig{ - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: 300 * time.Second, - MaxLeaseTTLVal: 1800 * time.Second, - }, - StorageView: storage, - }) - require.NoError(t, err, "failed creating backend") - - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - - data := map[string]interface{}{ - "certificate": string(pemCa), - "display_name": "test", - } - - // Test initial creation of role sets ocsp_max_retries to a default of 4 - _, err = lb.HandleRequest(ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "certs/test", - Data: data, - Storage: storage, - }) - require.NoError(t, err, "failed initial role creation request") - - resp, err := lb.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "certs/test", - Storage: storage, - }) - require.NoError(t, err, "failed reading role request") - require.NotNil(t, resp) - require.Equal(t, 4, resp.Data["ocsp_max_retries"], "ocsp config didn't match expectations") - - // Test we can update the field and read it back - data["ocsp_max_retries"] = 1 - _, err = lb.HandleRequest(ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "certs/test", - Data: data, - Storage: storage, - }) - require.NoError(t, err, "failed updating role request") - - resp, err = lb.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "certs/test", - Storage: storage, - }) - require.NoError(t, err, "failed reading role request") - require.NotNil(t, resp) - require.Equal(t, 1, resp.Data["ocsp_max_retries"], "ocsp config didn't match expectations on update") - - // Verify existing storage entries get updated with a value of 4 - entry := &logical.StorageEntry{ - Key: "cert/legacy", - Value: []byte(`{"token_bound_cidrs":null,"token_explicit_max_ttl":0,"token_max_ttl":0, - "token_no_default_policy":false,"token_num_uses":0,"token_period":0, - "token_policies":null,"token_type":0,"token_ttl":0,"Name":"test", - "Certificate":"-----BEGIN CERTIFICATE-----\nMIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL\nBQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw\nMjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7\nQ7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0\nz2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x\nAHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb\n6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH\nSWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G\nA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx\n7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc\nBgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA\nwHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2\nU946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa\ncNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N\nScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ\nt2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk\nzehNe5dFTjFpylg1o6b8Ow==\n-----END CERTIFICATE-----\n", - "DisplayName":"test","Policies":null,"TTL":0,"MaxTTL":0,"Period":0, - "AllowedNames":null,"AllowedCommonNames":null,"AllowedDNSSANs":null, - "AllowedEmailSANs":null,"AllowedURISANs":null,"AllowedOrganizationalUnits":null, - "RequiredExtensions":null,"AllowedMetadataExtensions":null,"BoundCIDRs":null, - "OcspCaCertificates":"","OcspEnabled":false,"OcspServersOverride":null, - "OcspFailOpen":false,"OcspQueryAllServers":false}`), - } - err = storage.Put(ctx, entry) - require.NoError(t, err, "failed putting legacy storage entry") - - resp, err = lb.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "certs/legacy", - Storage: storage, - }) - require.NoError(t, err, "failed reading role request") - require.NotNil(t, resp) - require.Equal(t, 4, resp.Data["ocsp_max_retries"], "ocsp config didn't match expectations on legacy entry") -} - -func loadCerts(t *testing.T, certFile, certKey string) tls.Certificate { - caTLS, err := tls.LoadX509KeyPair(certFile, certKey) - require.NoError(t, err, "failed reading ca/key files") - - caTLS.Leaf, err = x509.ParseCertificate(caTLS.Certificate[0]) - require.NoError(t, err, "failed parsing certificate from file %s", certFile) - - return caTLS -} - -func createCa(t *testing.T) (*x509.Certificate, *ecdsa.PrivateKey) { - rootCaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated root key for CA") - - // Validate we reject CSRs that contain CN that aren't in the original order - cr := &x509.Certificate{ - Subject: pkix.Name{CommonName: "Root Cert"}, - SerialNumber: big.NewInt(1), - IsCA: true, - BasicConstraintsValid: true, - SignatureAlgorithm: x509.ECDSAWithSHA256, - NotBefore: time.Now().Add(-1 * time.Second), - NotAfter: time.Now().AddDate(1, 0, 0), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning}, - } - rootCaBytes, err := x509.CreateCertificate(rand.Reader, cr, cr, &rootCaKey.PublicKey, rootCaKey) - require.NoError(t, err, "failed generating root ca") - - rootCa, err := x509.ParseCertificate(rootCaBytes) - require.NoError(t, err, "failed parsing root ca") - - return rootCa, rootCaKey -} diff --git a/builtin/credential/cert/path_crls_test.go b/builtin/credential/cert/path_crls_test.go deleted file mode 100644 index fff11d588..000000000 --- a/builtin/credential/cert/path_crls_test.go +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cert - -import ( - "context" - "crypto/rand" - "crypto/x509" - "crypto/x509/pkix" - "fmt" - "io/ioutil" - "math/big" - "net/http" - "net/http/httptest" - "net/url" - "sync" - "testing" - "time" - - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" -) - -func TestCRLFetch(t *testing.T) { - storage := &logical.InmemStorage{} - - lb, err := Factory(context.Background(), &logical.BackendConfig{ - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: 300 * time.Second, - MaxLeaseTTLVal: 1800 * time.Second, - }, - StorageView: storage, - }) - - require.NoError(t, err) - b := lb.(*backend) - closeChan := make(chan bool) - go func() { - t := time.NewTicker(50 * time.Millisecond) - for { - select { - case <-t.C: - b.PeriodicFunc(context.Background(), &logical.Request{Storage: storage}) - case <-closeChan: - break - } - } - }() - defer close(closeChan) - - if err != nil { - t.Fatalf("error: %s", err) - } - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") - require.NoError(t, err) - caPEM, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - require.NoError(t, err) - caKeyPEM, err := ioutil.ReadFile("test-fixtures/keys/key.pem") - require.NoError(t, err) - certPEM, err := ioutil.ReadFile("test-fixtures/keys/cert.pem") - - caBundle, err := certutil.ParsePEMBundle(string(caPEM)) - require.NoError(t, err) - bundle, err := certutil.ParsePEMBundle(string(certPEM) + "\n" + string(caKeyPEM)) - require.NoError(t, err) - // Entry with one cert first - - revocationListTemplate := &x509.RevocationList{ - RevokedCertificates: []pkix.RevokedCertificate{ - { - SerialNumber: big.NewInt(1), - RevocationTime: time.Now(), - }, - }, - Number: big.NewInt(1), - ThisUpdate: time.Now(), - NextUpdate: time.Now().Add(50 * time.Millisecond), - SignatureAlgorithm: x509.SHA1WithRSA, - } - - var crlBytesLock sync.Mutex - crlBytes, err := x509.CreateRevocationList(rand.Reader, revocationListTemplate, caBundle.Certificate, bundle.PrivateKey) - require.NoError(t, err) - - var serverURL *url.URL - crlServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Host == serverURL.Host { - crlBytesLock.Lock() - w.Write(crlBytes) - crlBytesLock.Unlock() - } else { - w.WriteHeader(http.StatusNotFound) - } - })) - serverURL, _ = url.Parse(crlServer.URL) - - req := &logical.Request{ - Connection: &logical.Connection{ - ConnState: &connState, - }, - Storage: storage, - Auth: &logical.Auth{}, - } - - fd := &framework.FieldData{ - Raw: map[string]interface{}{ - "name": "test", - "certificate": string(caPEM), - "policies": "foo,bar", - }, - Schema: pathCerts(b).Fields, - } - - resp, err := b.pathCertWrite(context.Background(), req, fd) - if err != nil { - t.Fatal(err) - } - - empty_login_fd := &framework.FieldData{ - Raw: map[string]interface{}{}, - Schema: pathLogin(b).Fields, - } - resp, err = b.pathLogin(context.Background(), req, empty_login_fd) - if err != nil { - t.Fatal(err) - } - if resp.IsError() { - t.Fatalf("got error: %#v", *resp) - } - - // Set a bad CRL - fd = &framework.FieldData{ - Raw: map[string]interface{}{ - "name": "testcrl", - "url": "http://wrongserver.com", - }, - Schema: pathCRLs(b).Fields, - } - resp, err = b.pathCRLWrite(context.Background(), req, fd) - if err == nil { - t.Fatal(err) - } - if resp.IsError() { - t.Fatalf("got error: %#v", *resp) - } - - // Set good CRL - fd = &framework.FieldData{ - Raw: map[string]interface{}{ - "name": "testcrl", - "url": crlServer.URL, - }, - Schema: pathCRLs(b).Fields, - } - resp, err = b.pathCRLWrite(context.Background(), req, fd) - if err != nil { - t.Fatal(err) - } - if resp.IsError() { - t.Fatalf("got error: %#v", *resp) - } - - b.crlUpdateMutex.Lock() - if len(b.crls["testcrl"].Serials) != 1 { - t.Fatalf("wrong number of certs in CRL got %d, expected 1", len(b.crls["testcrl"].Serials)) - } - b.crlUpdateMutex.Unlock() - - // Add a cert to the CRL, then wait to see if it gets automatically picked up - revocationListTemplate.RevokedCertificates = []pkix.RevokedCertificate{ - { - SerialNumber: big.NewInt(1), - RevocationTime: revocationListTemplate.RevokedCertificates[0].RevocationTime, - }, - { - SerialNumber: big.NewInt(2), - RevocationTime: time.Now(), - }, - } - revocationListTemplate.ThisUpdate = time.Now() - revocationListTemplate.NextUpdate = time.Now().Add(1 * time.Minute) - revocationListTemplate.Number = big.NewInt(2) - - crlBytesLock.Lock() - crlBytes, err = x509.CreateRevocationList(rand.Reader, revocationListTemplate, caBundle.Certificate, bundle.PrivateKey) - crlBytesLock.Unlock() - require.NoError(t, err) - - // Give ourselves a little extra room on slower CI systems to ensure we - // can fetch the new CRL. - corehelpers.RetryUntil(t, 2*time.Second, func() error { - b.crlUpdateMutex.Lock() - defer b.crlUpdateMutex.Unlock() - - serialCount := len(b.crls["testcrl"].Serials) - if serialCount != 2 { - return fmt.Errorf("CRL refresh did not occur serial count %d", serialCount) - } - return nil - }) -} diff --git a/builtin/credential/cert/path_login_test.go b/builtin/credential/cert/path_login_test.go deleted file mode 100644 index fcc7b37c5..000000000 --- a/builtin/credential/cert/path_login_test.go +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cert - -import ( - "context" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "fmt" - "io/ioutil" - "math/big" - mathrand "math/rand" - "net" - "net/http" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/helper/certutil" - - "golang.org/x/crypto/ocsp" - - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - - "github.com/hashicorp/vault/sdk/logical" -) - -var ocspPort int - -var source InMemorySource - -type testLogger struct{} - -func (t *testLogger) Log(args ...any) { - fmt.Printf("%v", args) -} - -func TestMain(m *testing.M) { - source = make(InMemorySource) - - listener, err := net.Listen("tcp", ":0") - if err != nil { - return - } - - ocspPort = listener.Addr().(*net.TCPAddr).Port - srv := &http.Server{ - Addr: "localhost:0", - Handler: NewResponder(&testLogger{}, source, nil), - } - go func() { - srv.Serve(listener) - }() - defer srv.Shutdown(context.Background()) - m.Run() -} - -func TestCert_RoleResolve(t *testing.T) { - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "example.com", - }, - DNSNames: []string{"example.com"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - - tempDir, connState, err := generateTestCertAndConnState(t, certTemplate) - if tempDir != "" { - defer os.RemoveAll(tempDir) - } - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem")) - if err != nil { - t.Fatalf("err: %v", err) - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false), - testAccStepLoginWithName(t, connState, "web"), - testAccStepResolveRoleWithName(t, connState, "web"), - // Test with caching disabled - testAccStepSetRoleCacheSize(t, -1), - testAccStepLoginWithName(t, connState, "web"), - testAccStepResolveRoleWithName(t, connState, "web"), - }, - }) -} - -func testAccStepResolveRoleWithName(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ResolveRoleOperation, - Path: "login", - Unauthenticated: true, - ConnState: &connState, - Check: func(resp *logical.Response) error { - if resp.Data["role"] != certName { - t.Fatalf("Role was not as expected. Expected %s, received %s", certName, resp.Data["role"]) - } - return nil - }, - Data: map[string]interface{}{ - "name": certName, - }, - } -} - -func TestCert_RoleResolveWithoutProvidingCertName(t *testing.T) { - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "example.com", - }, - DNSNames: []string{"example.com"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - - tempDir, connState, err := generateTestCertAndConnState(t, certTemplate) - if tempDir != "" { - defer os.RemoveAll(tempDir) - } - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem")) - if err != nil { - t.Fatalf("err: %v", err) - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false), - testAccStepLoginWithName(t, connState, "web"), - testAccStepResolveRoleWithEmptyDataMap(t, connState, "web"), - testAccStepSetRoleCacheSize(t, -1), - testAccStepLoginWithName(t, connState, "web"), - testAccStepResolveRoleWithEmptyDataMap(t, connState, "web"), - }, - }) -} - -func testAccStepSetRoleCacheSize(t *testing.T, size int) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config", - Data: map[string]interface{}{ - "role_cache_size": size, - }, - } -} - -func testAccStepResolveRoleWithEmptyDataMap(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ResolveRoleOperation, - Path: "login", - Unauthenticated: true, - ConnState: &connState, - Check: func(resp *logical.Response) error { - if resp.Data["role"] != certName { - t.Fatalf("Role was not as expected. Expected %s, received %s", certName, resp.Data["role"]) - } - return nil - }, - Data: map[string]interface{}{}, - } -} - -func testAccStepResolveRoleExpectRoleResolutionToFail(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ResolveRoleOperation, - Path: "login", - Unauthenticated: true, - ConnState: &connState, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp == nil && !resp.IsError() { - t.Fatalf("Response was not an error: resp:%#v", resp) - } - - errString, ok := resp.Data["error"].(string) - if !ok { - t.Fatal("Error not part of response.") - } - - if !strings.Contains(errString, "invalid certificate") { - t.Fatalf("Error was not due to invalid role name. Error: %s", errString) - } - return nil - }, - Data: map[string]interface{}{ - "name": certName, - }, - } -} - -func testAccStepResolveRoleOCSPFail(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ResolveRoleOperation, - Path: "login", - Unauthenticated: true, - ConnState: &connState, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp == nil || !resp.IsError() { - t.Fatalf("Response was not an error: resp:%#v", resp) - } - - errString, ok := resp.Data["error"].(string) - if !ok { - t.Fatal("Error not part of response.") - } - - if !strings.Contains(errString, "no chain matching") { - t.Fatalf("Error was not due to OCSP failure. Error: %s", errString) - } - return nil - }, - Data: map[string]interface{}{ - "name": certName, - }, - } -} - -func TestCert_RoleResolve_RoleDoesNotExist(t *testing.T) { - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "example.com", - }, - DNSNames: []string{"example.com"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - - tempDir, connState, err := generateTestCertAndConnState(t, certTemplate) - if tempDir != "" { - defer os.RemoveAll(tempDir) - } - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem")) - if err != nil { - t.Fatalf("err: %v", err) - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: testFactory(t), - Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false), - testAccStepLoginWithName(t, connState, "web"), - testAccStepResolveRoleExpectRoleResolutionToFail(t, connState, "notweb"), - }, - }) -} - -func TestCert_RoleResolveOCSP(t *testing.T) { - cases := []struct { - name string - failOpen bool - certStatus int - errExpected bool - }{ - {"failFalseGoodCert", false, ocsp.Good, false}, - {"failFalseRevokedCert", false, ocsp.Revoked, true}, - {"failFalseUnknownCert", false, ocsp.Unknown, true}, - {"failTrueGoodCert", true, ocsp.Good, false}, - {"failTrueRevokedCert", true, ocsp.Revoked, true}, - {"failTrueUnknownCert", true, ocsp.Unknown, false}, - } - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "example.com", - }, - DNSNames: []string{"example.com"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - OCSPServer: []string{fmt.Sprintf("http://localhost:%d", ocspPort)}, - } - tempDir, connState, err := generateTestCertAndConnState(t, certTemplate) - if tempDir != "" { - defer os.RemoveAll(tempDir) - } - if err != nil { - t.Fatalf("error testing connection state: %v", err) - } - ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem")) - if err != nil { - t.Fatalf("err: %v", err) - } - - issuer := parsePEM(ca) - pkf, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_key.pem")) - if err != nil { - t.Fatalf("err: %v", err) - } - pk, err := certutil.ParsePEMBundle(string(pkf)) - if err != nil { - t.Fatalf("err: %v", err) - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - resp, err := ocsp.CreateResponse(issuer[0], issuer[0], ocsp.Response{ - Status: c.certStatus, - SerialNumber: certTemplate.SerialNumber, - ProducedAt: time.Now(), - ThisUpdate: time.Now(), - NextUpdate: time.Now().Add(time.Hour), - }, pk.PrivateKey) - if err != nil { - t.Fatal(err) - } - source[certTemplate.SerialNumber.String()] = resp - - b := testFactory(t) - b.(*backend).ocspClient.ClearCache() - var resolveStep logicaltest.TestStep - var loginStep logicaltest.TestStep - if c.errExpected { - loginStep = testAccStepLoginWithNameInvalid(t, connState, "web") - resolveStep = testAccStepResolveRoleOCSPFail(t, connState, "web") - } else { - loginStep = testAccStepLoginWithName(t, connState, "web") - resolveStep = testAccStepResolveRoleWithName(t, connState, "web") - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCertWithExtraParams(t, "web", ca, "foo", allowed{dns: "example.com"}, false, - map[string]interface{}{"ocsp_enabled": true, "ocsp_fail_open": c.failOpen}), - testAccStepReadCertPolicy(t, "web", false, map[string]interface{}{"ocsp_enabled": true, "ocsp_fail_open": c.failOpen}), - loginStep, - resolveStep, - }, - }) - }) - } -} - -func serialFromBigInt(serial *big.Int) string { - return strings.TrimSpace(certutil.GetHexFormatted(serial.Bytes(), ":")) -} diff --git a/builtin/credential/cert/test-fixtures/cacert.pem b/builtin/credential/cert/test-fixtures/cacert.pem deleted file mode 100644 index 9d9a3859e..000000000 --- a/builtin/credential/cert/test-fixtures/cacert.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPjCCAiagAwIBAgIUXiEDuecwua9+j1XHLnconxQ/JBcwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wIBcNMTYwNTAyMTYwMzU4WhgPMjA2 -NjA0MjAxNjA0MjhaMBYxFDASBgNVBAMTC215dmF1bHQuY29tMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwWPjnTqnkc6acah+wWLmdTK0oCrf2687XVhx -VP3IN897TYzkaBQ2Dn1UM2VEL71sE3OZSVm0UWs5n7UqRuDp6mvkvrT2q5zgh/bV -zg9ZL1AI5H7dY2Rsor95I849ymFpXZooMgNtIQLxIeleBwzTnVSkFl8RqKM7NkjZ -wvBafQEjSsYk9050Bu0GMLgFJYRo1LozJLbwIs5ykG5F5PWTMfRvLCgLBzixPb75 -unIJ29nL0yB7zzUdkM8CG1EX8NkjGLEnpRnPa7+RMf8bd10v84cr0JFCUQmoabks -sqVyA825/1we2r5Y8blyXZVIr2lcPyGocLDxz1qT1MqxrNQIywIDAQABo4GBMH8w -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBTo2I+W -3Wb2MBe3OWuj5qCbafavMB8GA1UdIwQYMBaAFBTo2I+W3Wb2MBe3OWuj5qCbafav -MBwGA1UdEQQVMBOCC215dmF1bHQuY29thwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB -AQAyjJzDMzf28yMgiu//2R6LD3+zuLHlfX8+p5JB7WDBT7CgSm89gzMRtD2DvqZQ -6iLbZv/x7Td8bdLsOKf3LDCkZyOygJ0Sr9+6YZdc9heWO8tsO/SbcLhj9/vK8YyV -5fJo+vECW8I5zQLeTKfPqJtTU0zFspv0WYCB96Hsbhd1hTfHmVgjBoxi0YuduAa8 -3EHuYPfTYkO3M4QJCoQ+3S6LXSTDqppd1KGAy7QhRU6shd29EpSVxhgqZ+CIOpZu -3RgPOgPqfqcOD/v/SRPqhRf+P5O5Dc/N4ZXTZtfJbaY0qE+smpeQUskVQ2TrSqha -UYpNk7+toZW3Gioo0lBD3gH2 ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/cacert2crl b/builtin/credential/cert/test-fixtures/cacert2crl deleted file mode 100644 index 82db7a3f6..000000000 --- a/builtin/credential/cert/test-fixtures/cacert2crl +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN X509 CRL----- -MIIBrjCBlzANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtteXZhdWx0LmNvbRcN -MTYwNTAyMTYxNDMzWhcNMTYwNTA1MTYxNDMzWjArMCkCFCXxxcbS0ATpI2PYrx8d -ACLEQ3B9FxExNjA1MDIxMjE0MzMtMDQwMKAjMCEwHwYDVR0jBBgwFoAUwsRNYCw4 -U2won66rMKEJm8inFfgwDQYJKoZIhvcNAQELBQADggEBAD/VvoRK4eaEDzG7Z95b -fHL5ubJGkyvkp8ruNu+rfQp8NLgFVvY6a93Hz7WLOhACkKIWJ63+/4vCfDi5uU0B -HW2FICHdlSQ+6DdGJ6MrgujALlyT+69iF+fPiJ/M1j/N7Am8XPYYcfNdSK6CHtfg -gHNB7E+ubBA7lIw7ucIkoiJjXrSWSXTs9/GzLUImiXJAKQ+JzPYryIsGKXKAwgHh -HB56BnJ2vOs7+6UxQ6fjKTMxYdNgoZ34MhkkxNNhylrEndO6XUvUvC1f/1p1wlzy -xTq2MrMfJHJyu08rkrD+kwMPH2uoVwKyDhXdRBP0QrvQwOsvNEhW8LTKwLWkK17b -fEI= ------END X509 CRL----- diff --git a/builtin/credential/cert/test-fixtures/cakey.pem b/builtin/credential/cert/test-fixtures/cakey.pem deleted file mode 100644 index ecba4754c..000000000 --- a/builtin/credential/cert/test-fixtures/cakey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAwWPjnTqnkc6acah+wWLmdTK0oCrf2687XVhxVP3IN897TYzk -aBQ2Dn1UM2VEL71sE3OZSVm0UWs5n7UqRuDp6mvkvrT2q5zgh/bVzg9ZL1AI5H7d -Y2Rsor95I849ymFpXZooMgNtIQLxIeleBwzTnVSkFl8RqKM7NkjZwvBafQEjSsYk -9050Bu0GMLgFJYRo1LozJLbwIs5ykG5F5PWTMfRvLCgLBzixPb75unIJ29nL0yB7 -zzUdkM8CG1EX8NkjGLEnpRnPa7+RMf8bd10v84cr0JFCUQmoabkssqVyA825/1we -2r5Y8blyXZVIr2lcPyGocLDxz1qT1MqxrNQIywIDAQABAoIBAD1pBd9ov8t6Surq -sY2hZUM0Hc16r+ln5LcInbx6djjaxvHiWql+OYgyXimP764lPYuTuspjFPKB1SOU -+N7XDxCkwFeayXXHdDlYtZ4gm5Z9mMVOT+j++8xWdxZaqJ56fmX9zOPM2LuR3paB -L52Xgh9EwHJmMApYAzaCvbu8bU+iHeNTW80xabxQrp9VCu/A1BXUX06jK4T+wmjZ -kDA82uQp3dCOF1tv/10HgwqkJj6/1jjM0XUzUZR6iV85S6jrA7wD7gDDeqNO8YHN -08YMRgTKk4pbA7AqoC5xbL3gbSjsjyw48KRq0FkdkjsgV0PJZRMUU9fv9puDa23K -WRPa8LECgYEAyeth5bVH8FXnVXIAAFU6W0WdgCK3VakhjItLw0eoxshuTwbVq64w -CNOB8y1pfP83WiJjX3qRG43NDW07X69J57YKtCCb6KICVUPmecgYZPkmegD1HBQZ -5+Aak+5pIUQuycQ0t65yHGu4Jsju05gEFgdzydFjNANgiPxRzZxzAkkCgYEA9S+y -ZR063oCQDg/GhMLCx19nCJyU44Figh1YCD6kTrsSTECuRpQ5B1F9a+LeZT2wnYxv -+qMvvV+lfVY73f5WZ567u2jSDIsCH34p4g7sE25lKwo+Lhik6EtOehJFs2ZUemaT -Ym7EjqWlC1whrG7P4MnTGzPOVNAGAxsGPtT58nMCgYAs/R8A2VU//UPfy9ioOlUY -RPiEtjd3BIoPEHI+/lZihAHf5bvx1oupS8bmcbXRPeQNVyAhA+QU6ZFIbpAOD7Y9 -xFe6LpHOUVqHuOs/MxAMX17tTA1QxkHHYi1JzJLr8I8kMW01h86w+mc7bQWZa4Nt -jReFXfvmeOInY2CumS8e0QKBgC23ow/vj1aFqla04lNG7YK3a0LTz39MVM3mItAG -viRgBV1qghRu9uNCcpx3RPijtBbsZMTbQL+S4gyo06jlD79qfZ7IQMJN+SteHvkj -xykoYHzSAB4gQj9+KzffyFdXMVFRZxHnjYb7o/amSzEXyHMlrtNXqZVu5HAXzeZR -V/m5AoGAAStS43Q7qSJSMfMBITKMdKlqCObnifD77WeR2WHGrpkq26300ggsDpMS -UTmnAAo77lSMmDsdoNn2XZmdeTu1CPoQnoZSE5CqPd5GeHA/hhegVCdeYxSXZJoH -Lhiac+AhCEog/MS1GmVsjynD7eDGVFcsJ6SWuam7doKfrpPqPnE= ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/generate.txt b/builtin/credential/cert/test-fixtures/generate.txt deleted file mode 100644 index 5b888ee77..000000000 --- a/builtin/credential/cert/test-fixtures/generate.txt +++ /dev/null @@ -1,67 +0,0 @@ -vault mount pki -vault mount-tune -max-lease-ttl=438000h pki -vault write pki/root/generate/exported common_name=myvault.com ttl=438000h ip_sans=127.0.0.1 -vi cacert.pem -vi cakey.pem - -vaultcert.hcl -backend "inmem" { -} -disable_mlock = true -default_lease_ttl = "700h" -max_lease_ttl = "768h" -listener "tcp" { - address = "127.0.0.1:8200" - tls_cert_file = "./cacert.pem" - tls_key_file = "./cakey.pem" -} -======================================== -vault mount pki -vault mount-tune -max-lease-ttl=438000h pki -vault write pki/root/generate/exported common_name=myvault.com ttl=438000h max_ttl=438000h ip_sans=127.0.0.1 -vi testcacert1.pem -vi testcakey1.pem -vi testcaserial1 - -vault write pki/config/urls issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl" -vault write pki/roles/myvault-dot-com allowed_domains=myvault.com allow_subdomains=true ttl=437999h max_ttl=438000h allow_ip_sans=true - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedserial1 - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedcert2.pem -vi testissuedkey2.pem -vi testissuedserial2 - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedserial3 - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedcert4.pem -vi testissuedkey4.pem -vi testissuedserial4 - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedserial5 - -vault write pki/revoke serial_number=$(cat testissuedserial2) -vault write pki/revoke serial_number=$(cat testissuedserial4) -curl -XGET "http://127.0.0.1:8200/v1/pki/crl/pem" -H "x-vault-token:123" > issuedcertcrl -openssl crl -in issuedcertcrl -noout -text - -======================================== -export VAULT_ADDR='http://127.0.0.1:8200' -vault mount pki -vault mount-tune -max-lease-ttl=438000h pki -vault write pki/root/generate/exported common_name=myvault.com ttl=438000h ip_sans=127.0.0.1 -vi testcacert2.pem -vi testcakey2.pem -vi testcaserial2 -vi testcacert2leaseid - -vault write pki/config/urls issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl" -vault revoke $(cat testcacert2leaseid) - -curl -XGET "http://127.0.0.1:8200/v1/pki/crl/pem" -H "x-vault-token:123" > cacert2crl -openssl crl -in cacert2crl -noout -text diff --git a/builtin/credential/cert/test-fixtures/issuedcertcrl b/builtin/credential/cert/test-fixtures/issuedcertcrl deleted file mode 100644 index 45e9a98ec..000000000 --- a/builtin/credential/cert/test-fixtures/issuedcertcrl +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN X509 CRL----- -MIIB2TCBwjANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtteXZhdWx0LmNvbRcN -MTYwNTAyMTYxMTA4WhcNMTYwNTA1MTYxMTA4WjBWMCkCFAS6oenLRllQ1MRYcSV+ -5ukv2563FxExNjA1MDIxMjExMDgtMDQwMDApAhQaQdPJfbIwE3q4nyYp60lVnZaE -5hcRMTYwNTAyMTIxMTA1LTA0MDCgIzAhMB8GA1UdIwQYMBaAFOuKvPiUG06iHkRX -AOeMiUdBfHFyMA0GCSqGSIb3DQEBCwUAA4IBAQBD2jkeOAmkDdYkAXbmjLGdHaQI -WMS/M+wtFnHVIDVQEmUmj/KPsrkshTZv2UgCHIxBha6y+kXUMQFMg6FwriDTB170 -WyJVDVhGg2WjiQjnzrzEI+iOmcpx60sPPXE63J/Zxo4QS5M62RTXRq3909HQTFI5 -f3xf0pog8mOrv5uQxO1SACP6YFtdDE2dGOVwoIPuNMTY5vijnj8I9dAw8VrbdoBX -m/Ky56kT+BpmVWHKwQd1nEcP/RHSKbZwwJzJG0BoGM8cvzjITtBmpEF+OZcea81x -p9XJkpfFeiVIgzxks3zTeuQjLF8u+MDcdGt0ztHEbkswjxuk1cCovZe2GFr4 ------END X509 CRL----- diff --git a/builtin/credential/cert/test-fixtures/keys/cert.pem b/builtin/credential/cert/test-fixtures/keys/cert.pem deleted file mode 100644 index 942d26698..000000000 --- a/builtin/credential/cert/test-fixtures/keys/cert.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIUf+jhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw -MTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS -TRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGn -SgMld6ZWRhNheZhA6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmi -YYMiIWplidMmMO5NTRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5 -donyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVG -B+5+AAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABo4H1 -MIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm++e -HpyM3p708bgZJuRYEdX1o+UwHwYDVR0jBBgwFoAUncSzT/6HMexyuiU9/7EgHu+o -k5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x -OjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A -AAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br -aS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy+SgMIrwfs -X1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4 -aYqNKFWrRaBRAaaYZ/O1ApRTOrXqRx9Eqr0H1BXLsoAq+mWassL8sf6siae+CpwA -KqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU+idkuqfV2h1BQKgSEhFDABjFdTCN -QDAHsEHsi2M4/jRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj -xqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc/J9DIQM+Xmk= ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/keys/key.pem b/builtin/credential/cert/test-fixtures/keys/key.pem deleted file mode 100644 index add982002..000000000 --- a/builtin/credential/cert/test-fixtures/keys/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftetT8pu -HflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNheZhA -6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMmMO5N -TRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJGuUd -y54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuHC3N2 -DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABAoIBAHR7fFV0eAGaopsX -9OD0TUGlsephBXb43g0GYHfJ/1Ew18w9oaxszJEqkl+PB4W3xZ3yG3e8ZomxDOhF -RreF2WgG5xOfhDogMwu6NodbArfgnAvoC6JnW3qha8HMP4F500RFVyCRcd6A3Frd -rFtaZn/UyCsBAN8/zkwPeYHayo7xX6d9kzgRl9HluEX5PXI5+3uiBDUiM085gkLI -5Cmadh9fMdjfhDXI4x2JYmILpp/9Nlc/krB15s5n1MPNtn3yL0TI0tWp0WlwDCV7 -oUm1SfIM0F1fXGFyFDcqwoIr6JCQgXk6XtTg31YhH1xgUIclUVdtHqmAwAbLdIhQ -GAiHn2kCgYEAwD4pZ8HfpiOG/EHNoWsMATc/5yC7O8F9WbvcHZQIymLY4v/7HKZb -VyOR6UQ5/O2cztSGIuKSF6+OK1C34lOyCuTSOTFrjlgEYtLIXjdGLfFdtOO8GRQR -akVXdwuzNAjTBaH5eXbG+NKcjmCvZL48dQVlfDTVulzFGbcsVTHIMQUCgYEA7IQI -FVsKnY3KqpyGqXq92LMcsT3XgW6X1BIIV+YhJ5AFUFkFrjrbXs94/8XyLfi0xBQy -efK+8g5sMs7koF8LyZEcAXWZJQduaKB71hoLlRaU4VQkL/dl2B6VFmAII/CsRCYh -r9RmDN2PF/mp98Ih9dpC1VqcCDRGoTYsd7jLalMCgYAMgH5k1wDaZxkSMp1S0AlZ -0uP+/evvOOgT+9mWutfPgZolOQx1koQCKLgGeX9j6Xf3I28NubpSfAI84uTyfQrp -FnRtb79U5Hh0jMynA+U2e6niZ6UF5H41cQj9Hu+qhKBkj2IP+h96cwfnYnZFkPGR -kqZE65KyqfHPeFATwkcImQKBgCdrfhlpGiTWXCABhKQ8s+WpPLAB2ahV8XJEKyXT -UlVQuMIChGLcpnFv7P/cUxf8asx/fUY8Aj0/0CLLvulHziQjTmKj4gl86pb/oIQ3 -xRRtNhU0O+/OsSfLORgIm3K6C0w0esregL/GMbJSR1TnA1gBr7/1oSnw5JC8Ab9W -injHAoGAJT1MGAiQrhlt9GCGe6Ajw4omdbY0wS9NXefnFhf7EwL0es52ezZ28zpU -2LXqSFbtann5CHgpSLxiMYPDIf+er4xgg9Bz34tz1if1rDfP2Qrxdrpr4jDnrGT3 -gYC2qCpvVD9RRUMKFfnJTfl5gMQdBW/LINkHtJ82snAeLl3gjQ4= ------END RSA PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/keys/pkioutput b/builtin/credential/cert/test-fixtures/keys/pkioutput deleted file mode 100644 index 526ff0316..000000000 --- a/builtin/credential/cert/test-fixtures/keys/pkioutput +++ /dev/null @@ -1,74 +0,0 @@ -Key Value -lease_id pki/issue/example-dot-com/d8214077-9976-8c68-9c07-6610da30aea4 -lease_duration 279359999 -lease_renewable false -certificate -----BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIUf+jhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw -MTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS -TRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGn -SgMld6ZWRhNheZhA6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmi -YYMiIWplidMmMO5NTRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5 -donyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVG -B+5+AAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABo4H1 -MIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm++e -HpyM3p708bgZJuRYEdX1o+UwHwYDVR0jBBgwFoAUncSzT/6HMexyuiU9/7EgHu+o -k5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x -OjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A -AAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br -aS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy+SgMIrwfs -X1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4 -aYqNKFWrRaBRAaaYZ/O1ApRTOrXqRx9Eqr0H1BXLsoAq+mWassL8sf6siae+CpwA -KqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU+idkuqfV2h1BQKgSEhFDABjFdTCN -QDAHsEHsi2M4/jRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj -xqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc/J9DIQM+Xmk= ------END CERTIFICATE----- -issuing_ca -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -private_key -----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftetT8pu -HflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNheZhA -6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMmMO5N -TRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJGuUd -y54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuHC3N2 -DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABAoIBAHR7fFV0eAGaopsX -9OD0TUGlsephBXb43g0GYHfJ/1Ew18w9oaxszJEqkl+PB4W3xZ3yG3e8ZomxDOhF -RreF2WgG5xOfhDogMwu6NodbArfgnAvoC6JnW3qha8HMP4F500RFVyCRcd6A3Frd -rFtaZn/UyCsBAN8/zkwPeYHayo7xX6d9kzgRl9HluEX5PXI5+3uiBDUiM085gkLI -5Cmadh9fMdjfhDXI4x2JYmILpp/9Nlc/krB15s5n1MPNtn3yL0TI0tWp0WlwDCV7 -oUm1SfIM0F1fXGFyFDcqwoIr6JCQgXk6XtTg31YhH1xgUIclUVdtHqmAwAbLdIhQ -GAiHn2kCgYEAwD4pZ8HfpiOG/EHNoWsMATc/5yC7O8F9WbvcHZQIymLY4v/7HKZb -VyOR6UQ5/O2cztSGIuKSF6+OK1C34lOyCuTSOTFrjlgEYtLIXjdGLfFdtOO8GRQR -akVXdwuzNAjTBaH5eXbG+NKcjmCvZL48dQVlfDTVulzFGbcsVTHIMQUCgYEA7IQI -FVsKnY3KqpyGqXq92LMcsT3XgW6X1BIIV+YhJ5AFUFkFrjrbXs94/8XyLfi0xBQy -efK+8g5sMs7koF8LyZEcAXWZJQduaKB71hoLlRaU4VQkL/dl2B6VFmAII/CsRCYh -r9RmDN2PF/mp98Ih9dpC1VqcCDRGoTYsd7jLalMCgYAMgH5k1wDaZxkSMp1S0AlZ -0uP+/evvOOgT+9mWutfPgZolOQx1koQCKLgGeX9j6Xf3I28NubpSfAI84uTyfQrp -FnRtb79U5Hh0jMynA+U2e6niZ6UF5H41cQj9Hu+qhKBkj2IP+h96cwfnYnZFkPGR -kqZE65KyqfHPeFATwkcImQKBgCdrfhlpGiTWXCABhKQ8s+WpPLAB2ahV8XJEKyXT -UlVQuMIChGLcpnFv7P/cUxf8asx/fUY8Aj0/0CLLvulHziQjTmKj4gl86pb/oIQ3 -xRRtNhU0O+/OsSfLORgIm3K6C0w0esregL/GMbJSR1TnA1gBr7/1oSnw5JC8Ab9W -injHAoGAJT1MGAiQrhlt9GCGe6Ajw4omdbY0wS9NXefnFhf7EwL0es52ezZ28zpU -2LXqSFbtann5CHgpSLxiMYPDIf+er4xgg9Bz34tz1if1rDfP2Qrxdrpr4jDnrGT3 -gYC2qCpvVD9RRUMKFfnJTfl5gMQdBW/LINkHtJ82snAeLl3gjQ4= ------END RSA PRIVATE KEY----- -private_key_type rsa diff --git a/builtin/credential/cert/test-fixtures/noclientauthcert.pem b/builtin/credential/cert/test-fixtures/noclientauthcert.pem deleted file mode 100644 index 3948f2203..000000000 --- a/builtin/credential/cert/test-fixtures/noclientauthcert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDGTCCAgGgAwIBAgIBBDANBgkqhkiG9w0BAQUFADBxMQowCAYDVQQDFAEqMQsw -CQYDVQQIEwJHQTELMAkGA1UEBhMCVVMxJTAjBgkqhkiG9w0BCQEWFnZpc2hhbG5h -eWFrdkBnbWFpbC5jb20xEjAQBgNVBAoTCUhhc2hpQ29ycDEOMAwGA1UECxMFVmF1 -bHQwHhcNMTYwMjI5MjE0NjE2WhcNMjEwMjI3MjE0NjE2WjBxMQowCAYDVQQDFAEq -MQswCQYDVQQIEwJHQTELMAkGA1UEBhMCVVMxJTAjBgkqhkiG9w0BCQEWFnZpc2hh -bG5heWFrdkBnbWFpbC5jb20xEjAQBgNVBAoTCUhhc2hpQ29ycDEOMAwGA1UECxMF -VmF1bHQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMfRkLfIGHt1r2jjnV0N -LqRCu3oB+J1dqpM03vQt3qzIiqtuQuIA2ba7TJm2HwU3W3+rtfFcS+hkBR/LZM+u -cBPB+9b9+7i08vHjgy2P3QH/Ebxa8j1v7JtRMT2qyxWK8NlT/+wZSH82Cr812aS/ -zNT56FbBo2UAtzpqeC4eiv6NAgMBAAGjQDA+MAkGA1UdEwQCMAAwCwYDVR0PBAQD -AgXgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZI -hvcNAQEFBQADggEBAG2mUwsZ6+R8qqyNjzMk7mgpsRZv9TEl6c1IiQdyjaCOPaYH -vtZpLX20um36cxrLuOUtZLllG/VJEhRZW5mXWxuOk4QunWMBXQioCDJG1ktcZAcQ -QqYv9Dzy2G9lZHjLztEac37T75RXW7OEeQREgwP11c8sQYiS9jf+7ITYL7nXjoKq -gEuH0h86BOH2O/BxgMelt9O0YCkvkLLHnE27xuNelRRZcBLSuE1GxdUi32MDJ+ff -25GUNM0zzOEaJAFE/USUBEdQqN1gvJidNXkAiMtIK7T8omQZONRaD2ZnSW8y2krh -eUg+rKis9RinqFlahLPfI5BlyQsNMEnsD07Q85E= ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/pkioutput b/builtin/credential/cert/test-fixtures/root/pkioutput deleted file mode 100644 index 312ae18de..000000000 --- a/builtin/credential/cert/test-fixtures/root/pkioutput +++ /dev/null @@ -1,74 +0,0 @@ -Key Value -lease_id pki/root/generate/exported/7bf99d76-dd3e-2c5b-04ce-5253062ad586 -lease_duration 315359999 -lease_renewable false -certificate -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -expiration 1.772072879e+09 -issuing_ca -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -private_key -----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p -t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 -BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w -/2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv -0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi -18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb -ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn -8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f -nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 -2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t -grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc -bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 -0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN -ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf -lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 -lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj -AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG -ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib -thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU -4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb -iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO -tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y -LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc -4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX -OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= ------END RSA PRIVATE KEY----- -private_key_type rsa -serial_number 6f:98:9d:f8:67:1a:31:e3:27:60:1b:f7:32:f7:53:19:68:a0:c8:9d diff --git a/builtin/credential/cert/test-fixtures/root/root.crl b/builtin/credential/cert/test-fixtures/root/root.crl deleted file mode 100644 index a80c9e411..000000000 --- a/builtin/credential/cert/test-fixtures/root/root.crl +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN X509 CRL----- -MIIBrjCBlzANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbRcN -MTYwMjI5MDIyOTE3WhcNMjUwMTA1MTAyOTE3WjArMCkCFG+YnfhnGjHjJ2Ab9zL3 -UxlooMidFxExNjAyMjgyMTI5MTctMDUwMKAjMCEwHwYDVR0jBBgwFoAUncSzT/6H -MexyuiU9/7EgHu+ok5swDQYJKoZIhvcNAQELBQADggEBAG9YDXpNe4LJroKZmVCn -HqMhW8eyzyaPak2nPPGCVUnc6vt8rlBYQU+xlBizD6xatZQDMPgrT8sBl9W3ysXk -RUlliHsT/SHddMz5dAZsBPRMJ7pYWLTx8jI4w2WRfbSyI4bY/6qTRNkEBUv+Fk8J -xvwB89+EM0ENcVMhv9ghsUA8h7kOg673HKwRstLDAzxS/uLmEzFjj8SV2m5DbV2Y -UUCKRSV20/kxJMIC9x2KikZhwOSyv1UE1otD+RQvbfAoZPUDmvp2FR/E0NGjBBOg -1TtCPRrl63cjqU3s8KQ4uah9Vj+Cwcu9n/yIKKtNQq4NKHvagv8GlUsoJ4BdAxCw -IA0= ------END X509 CRL----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcacert.pem b/builtin/credential/cert/test-fixtures/root/rootcacert.pem deleted file mode 100644 index dcb307a14..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcacert.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcacert.srl b/builtin/credential/cert/test-fixtures/root/rootcacert.srl deleted file mode 100644 index 1c85d6318..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcacert.srl +++ /dev/null @@ -1 +0,0 @@ -92223EAFBBEE17AF diff --git a/builtin/credential/cert/test-fixtures/root/rootcakey.pem b/builtin/credential/cert/test-fixtures/root/rootcakey.pem deleted file mode 100644 index e950da5ba..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcakey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p -t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 -BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w -/2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv -0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi -18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb -ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn -8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f -nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 -2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t -grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc -bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 -0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN -ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf -lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 -lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj -AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG -ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib -thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU -4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb -iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO -tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y -LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc -4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX -OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= ------END RSA PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawext.cnf b/builtin/credential/cert/test-fixtures/root/rootcawext.cnf deleted file mode 100644 index 77e8258e1..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawext.cnf +++ /dev/null @@ -1,16 +0,0 @@ -[ req ] -default_bits = 2048 -encrypt_key = no -prompt = no -default_md = sha256 -req_extensions = req_v3 -distinguished_name = dn - -[ dn ] -CN = example.com - -[ req_v3 ] -2.1.1.1=ASN1:UTF8String:A UTF8String Extension -2.1.1.2=ASN1:UTF8:A UTF8 Extension -2.1.1.3=ASN1:IA5:An IA5 Extension -2.1.1.4=ASN1:VISIBLE:A Visible Extension diff --git a/builtin/credential/cert/test-fixtures/root/rootcawext.csr b/builtin/credential/cert/test-fixtures/root/rootcawext.csr deleted file mode 100644 index 55e22eede..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawext.csr +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIDAzCCAesCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQDM2PrLyK/wVQIcnK362ZylDrIVMjFQzps/0AxM -ke+8MNPMArBlSAhnZus6qb0nN0nJrDLkHQgYqnSvK9N7VUv/xFblEcOLBlciLhyN -Wkm92+q/M/xOvUVmnYkN3XgTI5QNxF7ZWDFHmwCNV27RraQZou0hG7yvyoILLMQE -3MnMCNM1nZ9JIuBMcRsZLGqQ1XNaQljboRVIUjimzkcfYyTruhLosTIbwForp78J -MzHHqVjtLJXPqUnRMS7KhGMj1f2mIswQzCv6F2PWEzNBbP4Gb67znKikKDs0RgyL -RyfizFNFJSC58XntK8jwHK1D8W3UepFf4K8xNFnhPoKWtWfJAgMBAAGggacwgaQG -CSqGSIb3DQEJDjGBljCBkzAcBgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATAf -BgNRAQEEGAwWQSBVVEY4U3RyaW5nIEV4dGVuc2lvbjAZBgNRAQIEEgwQQSBVVEY4 -IEV4dGVuc2lvbjAZBgNRAQMEEhYQQW4gSUE1IEV4dGVuc2lvbjAcBgNRAQQEFRoT -QSBWaXNpYmxlIEV4dGVuc2lvbjANBgkqhkiG9w0BAQsFAAOCAQEAtYjewBcqAXxk -tDY0lpZid6ZvfngdDlDZX0vrs3zNppKNe5Sl+jsoDOexqTA7HQA/y1ru117sAEeB -yiqMeZ7oPk8b3w+BZUpab7p2qPMhZypKl93y/jGXGscc3jRbUBnym9S91PSq6wUd -f2aigSqFc9+ywFVdx5PnnZUfcrUQ2a+AweYEkGOzXX2Ga+Ige8grDMCzRgCoP5cW -kM5ghwZp5wYIBGrKBU9iDcBlmnNhYaGWf+dD00JtVDPNn2bJnCsJHIO0nklZgnrS -fli8VQ1nYPkONdkiRYLt6//6at1iNDoDgsVCChtlVkLpxFIKcDFUHlffZsc1kMFI -HTX579k8hA== ------END CERTIFICATE REQUEST----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawextcert.pem b/builtin/credential/cert/test-fixtures/root/rootcawextcert.pem deleted file mode 100644 index 2c8591735..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawextcert.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDRjCCAi6gAwIBAgIJAJIiPq+77hejMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV -BAMTC2V4YW1wbGUuY29tMB4XDTE3MTEyOTE5MTgwM1oXDTI3MTEyNzE5MTgwM1ow -FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDM2PrLyK/wVQIcnK362ZylDrIVMjFQzps/0AxMke+8MNPMArBlSAhn -Zus6qb0nN0nJrDLkHQgYqnSvK9N7VUv/xFblEcOLBlciLhyNWkm92+q/M/xOvUVm -nYkN3XgTI5QNxF7ZWDFHmwCNV27RraQZou0hG7yvyoILLMQE3MnMCNM1nZ9JIuBM -cRsZLGqQ1XNaQljboRVIUjimzkcfYyTruhLosTIbwForp78JMzHHqVjtLJXPqUnR -MS7KhGMj1f2mIswQzCv6F2PWEzNBbP4Gb67znKikKDs0RgyLRyfizFNFJSC58Xnt -K8jwHK1D8W3UepFf4K8xNFnhPoKWtWfJAgMBAAGjgZYwgZMwHAYDVR0RBBUwE4IL -ZXhhbXBsZS5jb22HBH8AAAEwHwYDUQEBBBgMFkEgVVRGOFN0cmluZyBFeHRlbnNp -b24wGQYDUQECBBIMEEEgVVRGOCBFeHRlbnNpb24wGQYDUQEDBBIWEEFuIElBNSBF -eHRlbnNpb24wHAYDUQEEBBUaE0EgVmlzaWJsZSBFeHRlbnNpb24wDQYJKoZIhvcN -AQELBQADggEBAGU/iA6saupEaGn/veVNCknFGDL7pst5D6eX/y9atXlBOdJe7ZJJ -XQRkeHJldA0khVpzH7Ryfi+/25WDuNz+XTZqmb4ppeV8g9amtqBwxziQ9UUwYrza -eDBqdXBaYp/iHUEHoceX4F44xuo80BIqwF0lD9TFNUFoILnF26ajhKX0xkGaiKTH -6SbjBfHoQVMzOHokVRWregmgNycV+MAI9Ne9XkIZvdOYeNlcS9drZeJI3szkiaxB -WWaWaAr5UU2Z0yUCZnAIDMRcIiUbSEjIDz504sSuCzTctMOxWZu0r/0UrXRzwZZi -HAaKm3MUmBh733ChP4rTB58nr5DEr5rJ9P8= ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawextkey.pem b/builtin/credential/cert/test-fixtures/root/rootcawextkey.pem deleted file mode 100644 index 3f8d8ebed..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawextkey.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDM2PrLyK/wVQIc -nK362ZylDrIVMjFQzps/0AxMke+8MNPMArBlSAhnZus6qb0nN0nJrDLkHQgYqnSv -K9N7VUv/xFblEcOLBlciLhyNWkm92+q/M/xOvUVmnYkN3XgTI5QNxF7ZWDFHmwCN -V27RraQZou0hG7yvyoILLMQE3MnMCNM1nZ9JIuBMcRsZLGqQ1XNaQljboRVIUjim -zkcfYyTruhLosTIbwForp78JMzHHqVjtLJXPqUnRMS7KhGMj1f2mIswQzCv6F2PW -EzNBbP4Gb67znKikKDs0RgyLRyfizFNFJSC58XntK8jwHK1D8W3UepFf4K8xNFnh -PoKWtWfJAgMBAAECggEAW7hLkzMok9N8PpNo0wjcuor58cOnkSbxHIFrAF3XmcvD -CXWqxa6bFLFgYcPejdCTmVkg8EKPfXvVAxn8dxyaCss+nRJ3G6ibGxLKdgAXRItT -cIk2T4svp+KhmzOur+MeR4vFbEuwxP8CIEclt3yoHVJ2Gnzw30UtNRO2MPcq48/C -ZODGeBqUif1EGjDAvlqu5kl/pcDBJ3ctIZdVUMYYW4R9JtzKsmwhX7CRCBm8k5hG -2uzn8AKwpuVtfWcnX59UUmHGJ8mjETuNLARRAwWBWhl8f7wckmi+PKERJGEM2QE5 -/Voy0p22zmQ3waS8LgiI7YHCAEFqjVWNziVGdR36gQKBgQDxkpfkEsfa5PieIaaF -iQOO0rrjEJ9MBOQqmTDeclmDPNkM9qvCF/dqpJfOtliYFxd7JJ3OR2wKrBb5vGHt -qIB51Rnm9aDTM4OUEhnhvbPlERD0W+yWYXWRvqyHz0GYwEFGQ83h95GC/qfTosqy -LEzYLDafiPeNP+DG/HYRljAxUwKBgQDZFOWHEcZkSFPLNZiksHqs90OR2zIFxZcx -SrbkjqXjRjehWEAwgpvQ/quSBxrE2E8xXgVm90G1JpWzxjUfKKQRM6solQeEpnwY -kCy2Ozij/TtbLNRlU65UQ+nMto8KTSIyJbxxdOZxYdtJAJQp1FJO1a1WC11z4+zh -lnLV1O5S8wKBgQCDf/QU4DBQtNGtas315Oa96XJ4RkUgoYz+r1NN09tsOERC7UgE -KP2y3JQSn2pMqE1M6FrKvlBO4uzC10xLja0aJOmrssvwDBu1D8FtA9IYgJjFHAEG -v1i7lJrgdu7TUtx1flVli1l3gF4lM3m5UaonBrJZV7rB9iLKzwUKf8IOJwKBgFt/ -QktPA6brEV56Za8sr1hOFA3bLNdf9B0Tl8j4ExWbWAFKeCu6MUDCxsAS/IZxgdeW -AILovqpC7CBM78EFWTni5EaDohqYLYAQ7LeWeIYuSyFf4Nogjj74LQha/iliX4Jx -g17y3dp2W34Gn2yOEG8oAxpcSfR54jMnPZnBWP5fAoGBAMNAd3oa/xq9A5v719ik -naD7PdrjBdhnPk4egzMDv54y6pCFlvFbEiBduBWTmiVa7dSzhYtmEbri2WrgARlu -vkfTnVH9E8Hnm4HTbNn+ebxrofq1AOAvdApSoslsOP1NT9J6zB89RzChJyzjbIQR -Gevrutb4uO9qpB1jDVoMmGde ------END PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawou.cnf b/builtin/credential/cert/test-fixtures/root/rootcawou.cnf deleted file mode 100644 index be11c33a1..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawou.cnf +++ /dev/null @@ -1,18 +0,0 @@ -[ req ] -default_bits = 2048 -encrypt_key = no -prompt = no -default_md = sha256 -distinguished_name = dn -req_extensions = req_v3 - -[ req_v3 ] -subjectAltName = @alt_names - -[ dn ] -CN = example.com -OU = engineering - -[ alt_names ] -IP.1 = 127.0.0.1 -email = valid@example.com diff --git a/builtin/credential/cert/test-fixtures/root/rootcawou.csr b/builtin/credential/cert/test-fixtures/root/rootcawou.csr deleted file mode 100644 index d72579b76..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawou.csr +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICpjCCAY4CAQAwLDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xFDASBgNVBAsMC2Vu -Z2luZWVyaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsI4ZJWfQ -mI/3qfacas7O260Iii06oTP4GoQ5QpAYvcfWKKnkXagd0fBl+hfpnrK6ojYY71Jt -cMstVdff2Wc5D3bnQ8Hikb1TMhdAAtZDUW4QbeWAXJ4mkDq1ARRcbTvK121bmDQp -1efepohe0mDxNCruGSHpqfayC6LOkk7XZ73VAOcPPV5OOpY8el7quUdfvElxn0vH -KBVlFRBBW2fbY5EAHDMkmBjWr0ofpwb+vhSuQlOZgsbd20mjDwSYIbywG0tAEOoj -pLI0pOQV5msdfbqmKYE6ZmUeL/Q/pZjYh5uxFUZ4aMD/STDaeq7GdYQYcm17WL+N -ceal9+gKceJSiQIDAQABoDUwMwYJKoZIhvcNAQkOMSYwJDAiBgNVHREEGzAZhwR/ -AAABgRF2YWxpZEBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAf1tnXgX1 -/1p2MAxHhcil5/lsOMgHWU5dRL6KjK2cepuBpfzlCbxFtvnsj9WHx46f9Q/xbqy+ -1A2TJIBUWxK+Eji//WJxbDsi7fmV5VQlpG7+sEa7yin3KobfMd84nDIYP8wLF1Fq -HhRf7ZjIDh3zTgBosvIIjGEyABrouGYm4Nl409I09MftGXK/5TLJkgm6sxcJCAHG -BMm8IFaI0VN5QFIHKvJ/1oQLpLV+gvtR6jAM/99LXc0SXmFn0Jcy/mE/hxJXJigW -dDOblgjliJo0rWwHK4gfsgpMbHjJiG70g0XHtTpBW+i/NyuPnc8RYzBIJv+4sks+ -hWSmn6/IL46qTg== ------END CERTIFICATE REQUEST----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawoucert.pem b/builtin/credential/cert/test-fixtures/root/rootcawoucert.pem deleted file mode 100644 index fe0f22754..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawoucert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDATCCAemgAwIBAgIJAMAMmdiZi5G/MA0GCSqGSIb3DQEBCwUAMCwxFDASBgNV -BAMMC2V4YW1wbGUuY29tMRQwEgYDVQQLDAtlbmdpbmVlcmluZzAeFw0xODA5MDEx -NDM0NTVaFw0yODA4MjkxNDM0NTVaMCwxFDASBgNVBAMMC2V4YW1wbGUuY29tMRQw -EgYDVQQLDAtlbmdpbmVlcmluZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBALCOGSVn0JiP96n2nGrOztutCIotOqEz+BqEOUKQGL3H1iip5F2oHdHwZfoX -6Z6yuqI2GO9SbXDLLVXX39lnOQ9250PB4pG9UzIXQALWQ1FuEG3lgFyeJpA6tQEU -XG07ytdtW5g0KdXn3qaIXtJg8TQq7hkh6an2sguizpJO12e91QDnDz1eTjqWPHpe -6rlHX7xJcZ9LxygVZRUQQVtn22ORABwzJJgY1q9KH6cG/r4UrkJTmYLG3dtJow8E -mCG8sBtLQBDqI6SyNKTkFeZrHX26pimBOmZlHi/0P6WY2IebsRVGeGjA/0kw2nqu -xnWEGHJte1i/jXHmpffoCnHiUokCAwEAAaMmMCQwIgYDVR0RBBswGYcEfwAAAYER -dmFsaWRAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAHATSjW20P7+6en0 -Oq/n/R/i+aCgzcxIWSgf3dhOyxGfBW6svSg8ZtBQFEZZHqIRSXZX89zz25+mvwqi -kGRJKKzD/KDd2v9C5+H3DSuu9CqClVtpjF2XLvRHnuclBIrwvyijRcqa2GCTA9YZ -sOfVVGQYobDbtRCgTwWkEpU9RrZWWoD8HAYMkxFc1Cs/vJconeAaQDPEIZx9wnAN -4r/F5143rn5dyhbYehz1/gykL3K0v7s4U5NhaSACE2AiQ+63vhAEd5xt9WPKAAGY -zEyK4b/qPO88mxLr3A/rdzzt1UYAwT38kXA7aV82AH1J8EaCr7tLnXzyLXiEsI4E -BOrHBgU= ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawoukey.pem b/builtin/credential/cert/test-fixtures/root/rootcawoukey.pem deleted file mode 100644 index 166317294..000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawoukey.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwjhklZ9CYj/ep -9pxqzs7brQiKLTqhM/gahDlCkBi9x9YoqeRdqB3R8GX6F+mesrqiNhjvUm1wyy1V -19/ZZzkPdudDweKRvVMyF0AC1kNRbhBt5YBcniaQOrUBFFxtO8rXbVuYNCnV596m -iF7SYPE0Ku4ZIemp9rILos6STtdnvdUA5w89Xk46ljx6Xuq5R1+8SXGfS8coFWUV -EEFbZ9tjkQAcMySYGNavSh+nBv6+FK5CU5mCxt3bSaMPBJghvLAbS0AQ6iOksjSk -5BXmax19uqYpgTpmZR4v9D+lmNiHm7EVRnhowP9JMNp6rsZ1hBhybXtYv41x5qX3 -6Apx4lKJAgMBAAECggEAF1Jd7fv9qPlzfKcP2GgDGS+NLjt1QDAOOOp4aduA+Si5 -mFuAyAJaFg5MWjHocUcosh61Qn+/5yNflLRUZHJnLizFtcSZuiipIbfCg91rvQjt -8KZdQ168t1aZ7E+VOfSpAbX3YG6bjB754UOoSt/1XK/DDdzV8dadhD34TYlOmOxZ -MMnIRERqa+IBSn90TONWPyY3ELSpaiCkz1YZpp6g9RnTACZKLwzBMSunNO5qbEfH -TWlk5o14DZ3zRu5gLT5wy3SGfzm2M+qi8afQq1MT2I6opXj4KU3c64agjNUBYTq7 -S2YWmw6yrqPzxcg0hOz9H6djCx2oen/UxM2z4uoE1QKBgQDlHIFQcVTWEmxhy5yp -uV7Ya5ubx6rW4FnCgh5lJ+wWuSa5TkMuBr30peJn0G6y0I0J1El4o3iwLD/jxwHb -BIJTB1z5fBo3K7lhpZLuRFSWe9Mcd/Aj2pFcy5TqaIV9x8bgVAMVOoZAq9muiEog -zIWVWrVF6FDuFgRMRegNDej6pwKBgQDFRpNQMscPpH+x6xeS0E8boZKnHyuJUZQZ -kfEmnHQuTYmmHS4kXSnJhjODa53YddknTrPrHOvddDDYAaulyyYitPemubYQzBog -MyIgaeFSw/eHrcr/8g4QTohRFcI71xnKRmHvQZb8UflFJkqsqil6WZ6FJiC+STcn -Qdnhol9fTwKBgQCZtGDw1cdjgqKhjVcB6nG94ZtYjECJvaOaQW8g0AKsT/SxttaN -B0ri2XMl0IijgBROttO/knQCRP1r03PkOocwKq1uVprDzpqk7s6++KqC9nlwDOrX -Muf4iD/UbuC3vJIop1QWJtgwhNoaJCcPEAbCZ0Nbrfq1b6Hchb2jHGTj2wKBgHJo -DpDJEeaBeMi+1SoAgpA8sKcZDY+SbvgxShAhVcNwli5u586Q9OX5XTCPHbhmB+yi -2Pa2DBefBaCPv3LkEJa6KpFXTD4Lj+8ymE0B+nmcSpY19O9f+kX8tVOI8d7wTPWg -wbUWbbCg/ZXbshzWhj19cdA4H28bWM/8gZY4K2VDAoGBAMYsNhKdu9ON/7vaLijh -kai2tQLObYqDV6OAzdYm1gopmTTLcxQ6jP6aQlyw1ie51ms/hFozmNkGkaQGD8pp -751Lv3prQz/lDaZeQfKANNN1tpz/QqUOu2di9secMmodxXkwcLzcEKjWPDTuPhcO -VODU1hC5oj8yGFInoDLL2B0K ------END PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/testcacert1.pem b/builtin/credential/cert/test-fixtures/testcacert1.pem deleted file mode 100644 index ab8bf9e93..000000000 --- a/builtin/credential/cert/test-fixtures/testcacert1.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPjCCAiagAwIBAgIUfIKsF2VPT7sdFcKOHJH2Ii6K4MwwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wIBcNMTYwNTAyMTYwNTQyWhgPMjA2 -NjA0MjAxNjA2MTJaMBYxFDASBgNVBAMTC215dmF1bHQuY29tMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuOimEXawD2qBoLCFP3Skq5zi1XzzcMAJlfdS -xz9hfymuJb+cN8rB91HOdU9wQCwVKnkUtGWxUnMp0tT0uAZj5NzhNfyinf0JGAbP -67HDzVZhGBHlHTjPX0638yaiUx90cTnucX0N20SgCYct29dMSgcPl+W78D3Jw3xE -JsHQPYS9ASe2eONxG09F/qNw7w/RO5/6WYoV2EmdarMMxq52pPe2chtNMQdSyOUb -cCcIZyk4QVFZ1ZLl6jTnUPb+JoCx1uMxXvMek4NF/5IL0Wr9dw2gKXKVKoHDr6SY -WrCONRw61A5Zwx1V+kn73YX3USRlkufQv/ih6/xThYDAXDC9cwIDAQABo4GBMH8w -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOuKvPiU -G06iHkRXAOeMiUdBfHFyMB8GA1UdIwQYMBaAFOuKvPiUG06iHkRXAOeMiUdBfHFy -MBwGA1UdEQQVMBOCC215dmF1bHQuY29thwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB -AQBcN/UdAMzc7UjRdnIpZvO+5keBGhL/vjltnGM1dMWYHa60Y5oh7UIXF+P1RdNW -n7g80lOyvkSR15/r1rDkqOK8/4oruXU31EcwGhDOC4hU6yMUy4ltV/nBoodHBXNh -MfKiXeOstH1vdI6G0P6W93Bcww6RyV1KH6sT2dbETCw+iq2VN9CrruGIWzd67UT/ -spe/kYttr3UYVV3O9kqgffVVgVXg/JoRZ3J7Hy2UEXfh9UtWNanDlRuXaZgE9s/d -CpA30CHpNXvKeyNeW2ktv+2nAbSpvNW+e6MecBCTBIoDSkgU8ShbrzmDKVwNN66Q -5gn6KxUPBKHEtNzs5DgGM7nq ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testcacert2.pem b/builtin/credential/cert/test-fixtures/testcacert2.pem deleted file mode 100644 index a8fe6c480..000000000 --- a/builtin/credential/cert/test-fixtures/testcacert2.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPjCCAiagAwIBAgIUJfHFxtLQBOkjY9ivHx0AIsRDcH0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wIBcNMTYwNTAyMTYxMjI5WhgPMjA2 -NjA0MjAxNjEyNTlaMBYxFDASBgNVBAMTC215dmF1bHQuY29tMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqj8ANjAGrg5BgUb3owGwUHlMYDxljMdwroA/ -Bv76ESjomj1zCyVtoJxlDZ8m9VcKQldk5ashFNuY+Ms9FrJ1YsePvsfStNe37C26 -2uldDToh5rm7K8uwp/bQiErwM9QZMCVYCPEH8QgETPg9qWnikDFLMqcLBNbIiXVL -alxEYgA1Qt6+ayMvoS35288hFdZj6a0pCF0+zMHORZxloPhkXWnZLp5lWBiunSJG -0kVz56TjF+oY0L74iW4y3x2805biisGvFqgpZJW8/hLw/kDthNylNTzEqBktsctQ -BXpSMcwG3woJ0uZ8cH/HA/m0VDeIA77UisXnlLiQDpdB7U7QPwIDAQABo4GBMH8w -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMLETWAs -OFNsKJ+uqzChCZvIpxX4MB8GA1UdIwQYMBaAFMLETWAsOFNsKJ+uqzChCZvIpxX4 -MBwGA1UdEQQVMBOCC215dmF1bHQuY29thwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB -AQCRlFb6bZDrq3NkoZF9evls7cT41V3XCdykMA4K9YRgDroZ5psanSvYEnSrk9cU -Y7sVYW7b8qSRWkLZrHCAwc2V0/i5F5j4q9yVnWaTZ+kOVCFYCI8yUS7ixRQdTLNN -os/r9dcRSzzTEqoQThAzn571yRcbJHzTjda3gCJ5F4utYUBU2F9WK+ukW9nqfepa -ju5vEEGDuL2+RyApzL0nGzMUkCdBcK82QBksTlElPnbICbJZWUUMTZWPaZ7WGDDa -Pj+pWMXiDQmzIuzgXUCNtQL6lEv4tQwGYRHjjPmhgJP4sr6Cyrj4G0iljrqM+z/3 -gLyJOlNU8c5x02/C1nFDDa14 ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testcakey1.pem b/builtin/credential/cert/test-fixtures/testcakey1.pem deleted file mode 100644 index 05211bad1..000000000 --- a/builtin/credential/cert/test-fixtures/testcakey1.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAuOimEXawD2qBoLCFP3Skq5zi1XzzcMAJlfdSxz9hfymuJb+c -N8rB91HOdU9wQCwVKnkUtGWxUnMp0tT0uAZj5NzhNfyinf0JGAbP67HDzVZhGBHl -HTjPX0638yaiUx90cTnucX0N20SgCYct29dMSgcPl+W78D3Jw3xEJsHQPYS9ASe2 -eONxG09F/qNw7w/RO5/6WYoV2EmdarMMxq52pPe2chtNMQdSyOUbcCcIZyk4QVFZ -1ZLl6jTnUPb+JoCx1uMxXvMek4NF/5IL0Wr9dw2gKXKVKoHDr6SYWrCONRw61A5Z -wx1V+kn73YX3USRlkufQv/ih6/xThYDAXDC9cwIDAQABAoIBAG3bCo7ljMQb6tel -CAUjL5Ilqz5a9ebOsONABRYLOclq4ePbatxawdJF7/sSLwZxKkIJnZtvr2Hkubxg -eOO8KC0YbVS9u39Rjc2QfobxHfsojpbWSuCJl+pvwinbkiUAUxXR7S/PtCPJKat/ -fGdYCiMQ/tqnynh4vR4+/d5o12c0KuuQ22/MdEf3GOadUamRXS1ET9iJWqla1pJW -TmzrlkGAEnR5PPO2RMxbnZCYmj3dArxWAnB57W+bWYla0DstkDKtwg2j2ikNZpXB -nkZJJpxR76IYD1GxfwftqAKxujKcyfqB0dIKCJ0UmfOkauNWjexroNLwaAOC3Nud -XIxppAECgYEA1wJ9EH6A6CrSjdzUocF9LtQy1LCDHbdiQFHxM5/zZqIxraJZ8Gzh -Q0d8JeOjwPdG4zL9pHcWS7+x64Wmfn0+Qfh6/47Vy3v90PIL0AeZYshrVZyJ/s6X -YkgFK80KEuWtacqIZ1K2UJyCw81u/ynIl2doRsIbgkbNeN0opjmqVTMCgYEA3CkW -2fETWK1LvmgKFjG1TjOotVRIOUfy4iN0kznPm6DK2PgTF5DX5RfktlmA8i8WPmB7 -YFOEdAWHf+RtoM/URa7EAGZncCWe6uggAcWqznTS619BJ63OmncpSWov5Byg90gJ -48qIMY4wDjE85ypz1bmBc2Iph974dtWeDtB7dsECgYAyKZh4EquMfwEkq9LH8lZ8 -aHF7gbr1YeWAUB3QB49H8KtacTg+iYh8o97pEBUSXh6hvzHB/y6qeYzPAB16AUpX -Jdu8Z9ylXsY2y2HKJRu6GjxAewcO9bAH8/mQ4INrKT6uIdx1Dq0OXZV8jR9KVLtB -55RCfeLhIBesDR0Auw9sVQKBgB0xTZhkgP43LF35Ca1btgDClNJGdLUztx8JOIH1 -HnQyY/NVIaL0T8xO2MLdJ131pGts+68QI/YGbaslrOuv4yPCQrcS3RBfzKy1Ttkt -TrLFhtoy7T7HqyeMOWtEq0kCCs3/PWB5EIoRoomfOcYlOOrUCDg2ge9EP4nyVVz9 -hAGBAoGBAJXw/ufevxpBJJMSyULmVWYr34GwLC1OhSE6AVVt9JkIYnc5L4xBKTHP -QNKKJLmFmMsEqfxHUNWmpiHkm2E0p37Zehui3kywo+A4ybHPTua70ZWQfZhKxLUr -PvJa8JmwiCM7kO8zjOv+edY1mMWrbjAZH1YUbfcTHmST7S8vp0F3 ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testcakey2.pem b/builtin/credential/cert/test-fixtures/testcakey2.pem deleted file mode 100644 index c2e3763e2..000000000 --- a/builtin/credential/cert/test-fixtures/testcakey2.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAqj8ANjAGrg5BgUb3owGwUHlMYDxljMdwroA/Bv76ESjomj1z -CyVtoJxlDZ8m9VcKQldk5ashFNuY+Ms9FrJ1YsePvsfStNe37C262uldDToh5rm7 -K8uwp/bQiErwM9QZMCVYCPEH8QgETPg9qWnikDFLMqcLBNbIiXVLalxEYgA1Qt6+ -ayMvoS35288hFdZj6a0pCF0+zMHORZxloPhkXWnZLp5lWBiunSJG0kVz56TjF+oY -0L74iW4y3x2805biisGvFqgpZJW8/hLw/kDthNylNTzEqBktsctQBXpSMcwG3woJ -0uZ8cH/HA/m0VDeIA77UisXnlLiQDpdB7U7QPwIDAQABAoIBADivQ2XHdeHsUzk1 -JOz8efVBfgGo+nL2UPl5MAMnUKH4CgKZJT3311mb2TXA4RrdQUg3ixvBcAFe4L8u -BIgTIWyjX6Q5KloWXWHhFA8hll76FSGag8ygRJCYaHSI5xOKslxKgtZvUqKZdb0f -BoDrBYnXL9+MqOmSjjDegh7G2+n49n774Z2VVR47TZTBB5LCWDWj4AtEcalgwlvw -d5yL/GU/RfCkXCjCeie1pInp3eCMUI9jlvbe/vyaoFq2RiaJw1LSlJLXZBMYzaij -XkgMtRsr5bf0Tg2z3SPiaa9QZogfVLqHWAt6RHZf9Keidtiho+Ad6/dzJu+jKDys -Z6cthOECgYEAxMUCIYKO74BtPRN2r7KxbSjHzFsasxbfwkSg4Qefd4UoZJX2ShlL -cClnef3WdkKxtShJhqEPaKTYTrfgM+iz/a9+3lAFnS4EZawSf3YgXXslVTory0Da -yPQZKxX6XsupaLl4s13ehw/D0qfdxWVYaiFad3ePEE4ytmSkMMHLHo8CgYEA3X4a -jMWVbVv1W1lj+LFcg7AhU7lHgla+p7NI4gHw9V783noafnW7/8pNF80kshYo4u0g -aJRwaU/Inr5uw14eAyEjB4X7N8AE5wGmcxxS2uluGG6r3oyQSJBqktGnLwyTfcfC -XrfsGJza2BRGF4Mn8SFb7WtCl3f1qu0hTF+mC1ECgYB4oA1eXZsiV6if+H6Z1wHN -2WIidPc5MpyZi1jUmse3jXnlr8j8Q+VrLPayYlpGxTwLwlbQoYvAqs2v9CkNqWot -6pfr0UKfyMYJTiNI4DGXHRcV2ENgprF436tOLnr+AfwopwrHapQwWAnD6gSaLja1 -WR0Mf87EQCv2hFvjR+otIQKBgQCLyvJQ1MeZzQdPT1zkcnSUfM6b+/1hCwSr7WDb -nCQLiZcJh4E/PWmZaII9unEloQzPJKBmwQEtxng1kLVxwu4oRXrJXcuPhTbS4dy/ -HCpDFj8xVnBNNuQ9mEBbR80/ya0xHqnThDuT0TPiWvFeF55W9xoA/8h4tvKrnZx9 -ioTO8QKBgCMqRa5pHb+vCniTWUTz9JZRnRsdq7fRSsJHngMe5gOR4HylyAmmqKrd -kEXfkdu9TH2jxSWcZbHUPVwKfOUqQUZMz0pml0DIs1kedUDFanTZ8Rgg5SGUHBW0 -5bNCq64tKMmw6GiicaAGqd04OPo85WD9h8mPhM1Jdv/UmTV+HFAr ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testissuedcert4.pem b/builtin/credential/cert/test-fixtures/testissuedcert4.pem deleted file mode 100644 index 5bffd6777..000000000 --- a/builtin/credential/cert/test-fixtures/testissuedcert4.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIUBLqh6ctGWVDUxFhxJX7m6S/bnrcwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wIBcNMTYwNTAyMTYwOTI2WhgPMjA2 -NjA0MjAxNTA5NTZaMBsxGTAXBgNVBAMTEGNlcnQubXl2YXVsdC5jb20wggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY3gPB29kkdbu0mPO6J0efagQhSiXB -9OyDuLf5sMk6CVDWVWal5hISkyBmw/lXgF7qC2XFKivpJOrcGQd5Ep9otBqyJLzI -b0IWdXuPIrVnXDwcdWr86ybX2iC42zKWfbXgjzGijeAVpl0UJLKBj+fk5q6NvkRL -5FUL6TRV7Krn9mrmnrV9J5IqV15pTd9W2aVJ6IqWvIPCACtZKulqWn4707uy2X2W -1Stq/5qnp1pDshiGk1VPyxCwQ6yw3iEcgecbYo3vQfhWcv7Q8LpSIM9ZYpXu6OmF -+czqRZS9gERl+wipmmrN1MdYVrTuQem21C/PNZ4jo4XUk1SFx6JrcA+lAgMBAAGj -gfUwgfIwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSe -Cl9WV3BjGCwmS/KrDSLRjfwyqjAfBgNVHSMEGDAWgBTrirz4lBtOoh5EVwDnjIlH -QXxxcjA7BggrBgEFBQcBAQQvMC0wKwYIKwYBBQUHMAKGH2h0dHA6Ly8xMjcuMC4w -LjE6ODIwMC92MS9wa2kvY2EwIQYDVR0RBBowGIIQY2VydC5teXZhdWx0LmNvbYcE -fwAAATAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vMTI3LjAuMC4xOjgyMDAvdjEv -cGtpL2NybDANBgkqhkiG9w0BAQsFAAOCAQEAWGholPN8buDYwKbUiDavbzjsxUIX -lU4MxEqOHw7CD3qIYIauPboLvB9EldBQwhgOOy607Yvdg3rtyYwyBFwPhHo/hK3Z -6mn4hc6TF2V+AUdHBvGzp2dbYLeo8noVoWbQ/lBulggwlIHNNF6+a3kALqsqk1Ch -f/hzsjFnDhAlNcYFgG8TgfE2lE/FckvejPqBffo7Q3I+wVAw0buqiz5QL81NOT+D -Y2S9LLKLRaCsWo9wRU1Az4Rhd7vK5SEMh16jJ82GyEODWPvuxOTI1MnzfnbWyLYe -TTp6YBjGMVf1I6NEcWNur7U17uIOiQjMZ9krNvoMJ1A/cxCoZ98QHgcIPg== ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testissuedkey4.pem b/builtin/credential/cert/test-fixtures/testissuedkey4.pem deleted file mode 100644 index 58e7f8df7..000000000 --- a/builtin/credential/cert/test-fixtures/testissuedkey4.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA2N4DwdvZJHW7tJjzuidHn2oEIUolwfTsg7i3+bDJOglQ1lVm -peYSEpMgZsP5V4Be6gtlxSor6STq3BkHeRKfaLQasiS8yG9CFnV7jyK1Z1w8HHVq -/Osm19oguNsyln214I8xoo3gFaZdFCSygY/n5Oaujb5ES+RVC+k0Veyq5/Zq5p61 -fSeSKldeaU3fVtmlSeiKlryDwgArWSrpalp+O9O7stl9ltUrav+ap6daQ7IYhpNV -T8sQsEOssN4hHIHnG2KN70H4VnL+0PC6UiDPWWKV7ujphfnM6kWUvYBEZfsIqZpq -zdTHWFa07kHpttQvzzWeI6OF1JNUhceia3APpQIDAQABAoIBAQCH3vEzr+3nreug -RoPNCXcSJXXY9X+aeT0FeeGqClzIg7Wl03OwVOjVwl/2gqnhbIgK0oE8eiNwurR6 -mSPZcxV0oAJpwiKU4T/imlCDaReGXn86xUX2l82KRxthNdQH/VLKEmzij0jpx4Vh -bWx5SBPdkbmjDKX1dmTiRYWIn/KjyNPvNvmtwdi8Qluhf4eJcNEUr2BtblnGOmfL -FdSu+brPJozpoQ1QdDnbAQRgqnh7Shl0tT85whQi0uquqIj1gEOGVjmBvDDnL3GV -WOENTKqsmIIoEzdZrql1pfmYTk7WNaD92bfpN128j8BF7RmAV4/DphH0pvK05y9m -tmRhyHGxAoGBAOV2BBocsm6xup575VqmFN+EnIOiTn+haOvfdnVsyQHnth63fOQx -PNtMpTPR1OMKGpJ13e2bV0IgcYRsRkScVkUtoa/17VIgqZXffnJJ0A/HT67uKBq3 -8o7RrtyK5N20otw0lZHyqOPhyCdpSsurDhNON1kPVJVYY4N1RiIxfut/AoGBAPHz -HfsJ5ZkyELE9N/r4fce04lprxWH+mQGK0/PfjS9caXPhj/r5ZkVMvzWesF3mmnY8 -goE5S35TuTvV1+6rKGizwlCFAQlyXJiFpOryNWpLwCmDDSzLcm+sToAlML3tMgWU -jM3dWHx3C93c3ft4rSWJaUYI9JbHsMzDW6Yh+GbbAoGBANIbKwxh5Hx5XwEJP2yu -kIROYCYkMy6otHLujgBdmPyWl+suZjxoXWoMl2SIqR8vPD+Jj6mmyNJy9J6lqf3f -DRuQ+fEuBZ1i7QWfvJ+XuN0JyovJ5Iz6jC58D1pAD+p2IX3y5FXcVQs8zVJRFjzB -p0TEJOf2oqORaKWRd6ONoMKvAoGALKu6aVMWdQZtVov6/fdLIcgf0pn7Q3CCR2qe -X3Ry2L+zKJYIw0mwvDLDSt8VqQCenB3n6nvtmFFU7ds5lvM67rnhsoQcAOaAehiS -rl4xxoJd5Ewx7odRhZTGmZpEOYzFo4odxRSM9c30/u18fqV1Mm0AZtHYds4/sk6P -aUj0V+kCgYBMpGrJk8RSez5g0XZ35HfpI4ENoWbiwB59FIpWsLl2LADEh29eC455 -t9Muq7MprBVBHQo11TMLLFxDIjkuMho/gcKgpYXCt0LfiNm8EZehvLJUXH+3WqUx -we6ywrbFCs6LaxaOCtTiLsN+GbZCatITL0UJaeBmTAbiw0KQjUuZPQ== ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test_responder.go b/builtin/credential/cert/test_responder.go deleted file mode 100644 index 2052736d3..000000000 --- a/builtin/credential/cert/test_responder.go +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -// Package ocsp implements an OCSP responder based on a generic storage backend. -// It provides a couple of sample implementations. -// Because OCSP responders handle high query volumes, we have to be careful -// about how much logging we do. Error-level logs are reserved for problems -// internal to the server, that can be fixed by an administrator. Any type of -// incorrect input from a user should be logged and Info or below. For things -// that are logged on every request, Debug is the appropriate level. -// -// From https://github.com/cloudflare/cfssl/blob/master/ocsp/responder.go - -package cert - -import ( - "crypto" - "crypto/sha256" - "encoding/base64" - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "time" - - "golang.org/x/crypto/ocsp" -) - -var ( - malformedRequestErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x01} - internalErrorErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x02} - tryLaterErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x03} - sigRequredErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x05} - unauthorizedErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x06} - - // ErrNotFound indicates the request OCSP response was not found. It is used to - // indicate that the responder should reply with unauthorizedErrorResponse. - ErrNotFound = errors.New("Request OCSP Response not found") -) - -// Source represents the logical source of OCSP responses, i.e., -// the logic that actually chooses a response based on a request. In -// order to create an actual responder, wrap one of these in a Responder -// object and pass it to http.Handle. By default the Responder will set -// the headers Cache-Control to "max-age=(response.NextUpdate-now), public, no-transform, must-revalidate", -// Last-Modified to response.ThisUpdate, Expires to response.NextUpdate, -// ETag to the SHA256 hash of the response, and Content-Type to -// application/ocsp-response. If you want to override these headers, -// or set extra headers, your source should return a http.Header -// with the headers you wish to set. If you don'log want to set any -// extra headers you may return nil instead. -type Source interface { - Response(*ocsp.Request) ([]byte, http.Header, error) -} - -// An InMemorySource is a map from serialNumber -> der(response) -type InMemorySource map[string][]byte - -// Response looks up an OCSP response to provide for a given request. -// InMemorySource looks up a response purely based on serial number, -// without regard to what issuer the request is asking for. -func (src InMemorySource) Response(request *ocsp.Request) ([]byte, http.Header, error) { - response, present := src[request.SerialNumber.String()] - if !present { - return nil, nil, ErrNotFound - } - return response, nil, nil -} - -// Stats is a basic interface that allows users to record information -// about returned responses -type Stats interface { - ResponseStatus(ocsp.ResponseStatus) -} - -type logger interface { - Log(args ...any) -} - -// A Responder object provides the HTTP logic to expose a -// Source of OCSP responses. -type Responder struct { - log logger - Source Source - stats Stats -} - -// NewResponder instantiates a Responder with the give Source. -func NewResponder(t logger, source Source, stats Stats) *Responder { - return &Responder{ - Source: source, - stats: stats, - log: t, - } -} - -func overrideHeaders(response http.ResponseWriter, headers http.Header) { - for k, v := range headers { - if len(v) == 1 { - response.Header().Set(k, v[0]) - } else if len(v) > 1 { - response.Header().Del(k) - for _, e := range v { - response.Header().Add(k, e) - } - } - } -} - -// hashToString contains mappings for the only hash functions -// x/crypto/ocsp supports -var hashToString = map[crypto.Hash]string{ - crypto.SHA1: "SHA1", - crypto.SHA256: "SHA256", - crypto.SHA384: "SHA384", - crypto.SHA512: "SHA512", -} - -// A Responder can process both GET and POST requests. The mapping -// from an OCSP request to an OCSP response is done by the Source; -// the Responder simply decodes the request, and passes back whatever -// response is provided by the source. -// Note: The caller must use http.StripPrefix to strip any path components -// (including '/') on GET requests. -// Do not use this responder in conjunction with http.NewServeMux, because the -// default handler will try to canonicalize path components by changing any -// strings of repeated '/' into a single '/', which will break the base64 -// encoding. -func (rs *Responder) ServeHTTP(response http.ResponseWriter, request *http.Request) { - // By default we set a 'max-age=0, no-cache' Cache-Control header, this - // is only returned to the client if a valid authorized OCSP response - // is not found or an error is returned. If a response if found the header - // will be altered to contain the proper max-age and modifiers. - response.Header().Add("Cache-Control", "max-age=0, no-cache") - // Read response from request - var requestBody []byte - var err error - switch request.Method { - case "GET": - base64Request, err := url.QueryUnescape(request.URL.Path) - if err != nil { - rs.log.Log("Error decoding URL:", request.URL.Path) - response.WriteHeader(http.StatusBadRequest) - return - } - // url.QueryUnescape not only unescapes %2B escaping, but it additionally - // turns the resulting '+' into a space, which makes base64 decoding fail. - // So we go back afterwards and turn ' ' back into '+'. This means we - // accept some malformed input that includes ' ' or %20, but that's fine. - base64RequestBytes := []byte(base64Request) - for i := range base64RequestBytes { - if base64RequestBytes[i] == ' ' { - base64RequestBytes[i] = '+' - } - } - // In certain situations a UA may construct a request that has a double - // slash between the host name and the base64 request body due to naively - // constructing the request URL. In that case strip the leading slash - // so that we can still decode the request. - if len(base64RequestBytes) > 0 && base64RequestBytes[0] == '/' { - base64RequestBytes = base64RequestBytes[1:] - } - requestBody, err = base64.StdEncoding.DecodeString(string(base64RequestBytes)) - if err != nil { - rs.log.Log("Error decoding base64 from URL", string(base64RequestBytes)) - response.WriteHeader(http.StatusBadRequest) - return - } - case "POST": - requestBody, err = ioutil.ReadAll(request.Body) - if err != nil { - rs.log.Log("Problem reading body of POST", err) - response.WriteHeader(http.StatusBadRequest) - return - } - default: - response.WriteHeader(http.StatusMethodNotAllowed) - return - } - b64Body := base64.StdEncoding.EncodeToString(requestBody) - rs.log.Log("Received OCSP request", b64Body) - - // All responses after this point will be OCSP. - // We could check for the content type of the request, but that - // seems unnecessariliy restrictive. - response.Header().Add("Content-Type", "application/ocsp-response") - - // Parse response as an OCSP request - // XXX: This fails if the request contains the nonce extension. - // We don'log intend to support nonces anyway, but maybe we - // should return unauthorizedRequest instead of malformed. - ocspRequest, err := ocsp.ParseRequest(requestBody) - if err != nil { - rs.log.Log("Error decoding request body", b64Body) - response.WriteHeader(http.StatusBadRequest) - response.Write(malformedRequestErrorResponse) - if rs.stats != nil { - rs.stats.ResponseStatus(ocsp.Malformed) - } - return - } - - // Look up OCSP response from source - ocspResponse, headers, err := rs.Source.Response(ocspRequest) - if err != nil { - if err == ErrNotFound { - rs.log.Log("No response found for request: serial %x, request body %s", - ocspRequest.SerialNumber, b64Body) - response.Write(unauthorizedErrorResponse) - if rs.stats != nil { - rs.stats.ResponseStatus(ocsp.Unauthorized) - } - return - } - rs.log.Log("Error retrieving response for request: serial %x, request body %s, error", - ocspRequest.SerialNumber, b64Body, err) - response.WriteHeader(http.StatusInternalServerError) - response.Write(internalErrorErrorResponse) - if rs.stats != nil { - rs.stats.ResponseStatus(ocsp.InternalError) - } - return - } - - parsedResponse, err := ocsp.ParseResponse(ocspResponse, nil) - if err != nil { - rs.log.Log("Error parsing response for serial %x", - ocspRequest.SerialNumber, err) - response.Write(internalErrorErrorResponse) - if rs.stats != nil { - rs.stats.ResponseStatus(ocsp.InternalError) - } - return - } - - // Write OCSP response to response - response.Header().Add("Last-Modified", parsedResponse.ThisUpdate.Format(time.RFC1123)) - response.Header().Add("Expires", parsedResponse.NextUpdate.Format(time.RFC1123)) - now := time.Now() - maxAge := 0 - if now.Before(parsedResponse.NextUpdate) { - maxAge = int(parsedResponse.NextUpdate.Sub(now) / time.Second) - } else { - // TODO(#530): we want max-age=0 but this is technically an authorized OCSP response - // (despite being stale) and 5019 forbids attaching no-cache - maxAge = 0 - } - response.Header().Set( - "Cache-Control", - fmt.Sprintf( - "max-age=%d, public, no-transform, must-revalidate", - maxAge, - ), - ) - responseHash := sha256.Sum256(ocspResponse) - response.Header().Add("ETag", fmt.Sprintf("\"%X\"", responseHash)) - - if headers != nil { - overrideHeaders(response, headers) - } - - // RFC 7232 says that a 304 response must contain the above - // headers if they would also be sent for a 200 for the same - // request, so we have to wait until here to do this - if etag := request.Header.Get("If-None-Match"); etag != "" { - if etag == fmt.Sprintf("\"%X\"", responseHash) { - response.WriteHeader(http.StatusNotModified) - return - } - } - response.WriteHeader(http.StatusOK) - response.Write(ocspResponse) - if rs.stats != nil { - rs.stats.ResponseStatus(ocsp.Success) - } -} - -/* -Copyright (c) 2014 CloudFlare Inc. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -Redistributions of source code must retain the above copyright notice, -this list of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ diff --git a/builtin/credential/ldap/backend_test.go b/builtin/credential/ldap/backend_test.go deleted file mode 100644 index 0e4a0d1a3..000000000 --- a/builtin/credential/ldap/backend_test.go +++ /dev/null @@ -1,1277 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package ldap - -import ( - "context" - "fmt" - "reflect" - "sort" - "testing" - "time" - - goldap "github.com/go-ldap/ldap/v3" - "github.com/go-test/deep" - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-secure-stdlib/strutil" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/helper/testhelpers/ldap" - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - "github.com/hashicorp/vault/sdk/helper/ldaputil" - "github.com/hashicorp/vault/sdk/helper/policyutil" - "github.com/hashicorp/vault/sdk/helper/tokenutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/mapstructure" -) - -func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - - b := Backend() - if b == nil { - t.Fatalf("failed to create backend") - } - - err := b.Backend.Setup(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - return b, config.StorageView -} - -func TestLdapAuthBackend_Listing(t *testing.T) { - b, storage := createBackendWithStorage(t) - - // Create group "testgroup" - resp, err := b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Path: "groups/testgroup", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "policies": []string{"default"}, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - - // Create group "nested/testgroup" - resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Path: "groups/nested/testgroup", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "policies": []string{"default"}, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - - // Create user "testuser" - resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Path: "users/testuser", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "policies": []string{"default"}, - "groups": "testgroup,nested/testgroup", - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - - // Create user "nested/testuser" - resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Path: "users/nested/testuser", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "policies": []string{"default"}, - "groups": "testgroup,nested/testgroup", - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - - // List users - resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Path: "users/", - Operation: logical.ListOperation, - Storage: storage, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - expected := []string{"testuser", "nested/testuser"} - if !reflect.DeepEqual(expected, resp.Data["keys"].([]string)) { - t.Fatalf("bad: listed users; expected: %#v actual: %#v", expected, resp.Data["keys"].([]string)) - } - - // List groups - resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Path: "groups/", - Operation: logical.ListOperation, - Storage: storage, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) - } - expected = []string{"testgroup", "nested/testgroup"} - if !reflect.DeepEqual(expected, resp.Data["keys"].([]string)) { - t.Fatalf("bad: listed groups; expected: %#v actual: %#v", expected, resp.Data["keys"].([]string)) - } -} - -func TestLdapAuthBackend_CaseSensitivity(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - ctx := context.Background() - - testVals := func(caseSensitive bool) { - // Clear storage - userList, err := storage.List(ctx, "user/") - if err != nil { - t.Fatal(err) - } - for _, user := range userList { - err = storage.Delete(ctx, "user/"+user) - if err != nil { - t.Fatal(err) - } - } - groupList, err := storage.List(ctx, "group/") - if err != nil { - t.Fatal(err) - } - for _, group := range groupList { - err = storage.Delete(ctx, "group/"+group) - if err != nil { - t.Fatal(err) - } - } - - configReq := &logical.Request{ - Path: "config", - Operation: logical.ReadOperation, - Storage: storage, - } - resp, err = b.HandleRequest(ctx, configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - if resp == nil { - t.Fatal("nil response") - } - if resp.Data["case_sensitive_names"].(bool) != caseSensitive { - t.Fatalf("expected case sensitivity %t, got %t", caseSensitive, resp.Data["case_sensitive_names"].(bool)) - } - - groupReq := &logical.Request{ - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "policies": "grouppolicy", - }, - Path: "groups/EngineerS", - Storage: storage, - } - resp, err = b.HandleRequest(ctx, groupReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - keys, err := storage.List(ctx, "group/") - if err != nil { - t.Fatal(err) - } - switch caseSensitive { - case true: - if keys[0] != "EngineerS" { - t.Fatalf("bad: %s", keys[0]) - } - default: - if keys[0] != "engineers" { - t.Fatalf("bad: %s", keys[0]) - } - } - - userReq := &logical.Request{ - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "groups": "EngineerS", - "policies": "userpolicy", - }, - Path: "users/hermeS conRad", - Storage: storage, - } - resp, err = b.HandleRequest(ctx, userReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - keys, err = storage.List(ctx, "user/") - if err != nil { - t.Fatal(err) - } - switch caseSensitive { - case true: - if keys[0] != "hermeS conRad" { - t.Fatalf("bad: %s", keys[0]) - } - default: - if keys[0] != "hermes conrad" { - t.Fatalf("bad: %s", keys[0]) - } - } - - if caseSensitive { - // The online test server is actually case sensitive so we need to - // write again so it works - userReq = &logical.Request{ - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "groups": "EngineerS", - "policies": "userpolicy", - }, - Path: "users/Hermes Conrad", - Storage: storage, - Connection: &logical.Connection{}, - } - resp, err = b.HandleRequest(ctx, userReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - } - - loginReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login/Hermes Conrad", - Data: map[string]interface{}{ - "password": "hermes", - }, - Storage: storage, - Connection: &logical.Connection{}, - } - resp, err = b.HandleRequest(ctx, loginReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - expected := []string{"grouppolicy", "userpolicy"} - if !reflect.DeepEqual(expected, resp.Auth.Policies) { - t.Fatalf("bad: policies: expected: %q, actual: %q", expected, resp.Auth.Policies) - } - } - - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config", - Data: map[string]interface{}{ - "url": cfg.Url, - "userattr": cfg.UserAttr, - "userdn": cfg.UserDN, - "groupdn": cfg.GroupDN, - "groupattr": cfg.GroupAttr, - "binddn": cfg.BindDN, - "bindpass": cfg.BindPassword, - }, - Storage: storage, - } - resp, err = b.HandleRequest(ctx, configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - testVals(false) - - // Check that if the value is nil, on read it is case sensitive - configEntry, err := b.Config(ctx, configReq) - if err != nil { - t.Fatal(err) - } - configEntry.CaseSensitiveNames = nil - entry, err := logical.StorageEntryJSON("config", configEntry) - if err != nil { - t.Fatal(err) - } - err = configReq.Storage.Put(ctx, entry) - if err != nil { - t.Fatal(err) - } - - testVals(true) -} - -func TestLdapAuthBackend_UserPolicies(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config", - Data: map[string]interface{}{ - "url": cfg.Url, - "userattr": cfg.UserAttr, - "userdn": cfg.UserDN, - "groupdn": cfg.GroupDN, - "groupattr": cfg.GroupAttr, - "binddn": cfg.BindDN, - "bindpassword": cfg.BindPassword, - }, - Storage: storage, - } - resp, err = b.HandleRequest(context.Background(), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - groupReq := &logical.Request{ - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "policies": "grouppolicy", - }, - Path: "groups/engineers", - Storage: storage, - Connection: &logical.Connection{}, - } - resp, err = b.HandleRequest(context.Background(), groupReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - userReq := &logical.Request{ - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "groups": "engineers", - "policies": "userpolicy", - }, - Path: "users/hermes conrad", - Storage: storage, - Connection: &logical.Connection{}, - } - - resp, err = b.HandleRequest(context.Background(), userReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - loginReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login/hermes conrad", - Data: map[string]interface{}{ - "password": "hermes", - }, - Storage: storage, - Connection: &logical.Connection{}, - } - - resp, err = b.HandleRequest(context.Background(), loginReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - expected := []string{"grouppolicy", "userpolicy"} - if !reflect.DeepEqual(expected, resp.Auth.Policies) { - t.Fatalf("bad: policies: expected: %q, actual: %q", expected, resp.Auth.Policies) - } -} - -/* -* Acceptance test for LDAP Auth Method -* -* The tests here rely on a docker LDAP server: -* [https://github.com/rroemhild/docker-test-openldap] -* -* ...as well as existence of a person object, `cn=Hermes Conrad,dc=example,dc=com`, -* which is a member of a group, `cn=admin_staff,ou=people,dc=example,dc=com` -* - - Querying the server from the command line: - - $ docker run --privileged -d -p 389:389 --name ldap --rm rroemhild/test-openldap - - $ ldapsearch -x -H ldap://localhost -b dc=planetexpress,dc=com -s sub uid=hermes - - $ ldapsearch -x -H ldap://localhost -b dc=planetexpress,dc=com -s sub \ - 'member=cn=Hermes Conrad,ou=people,dc=planetexpress,dc=com' -*/ -func factory(t *testing.T) logical.Backend { - defaultLeaseTTLVal := time.Hour * 24 - maxLeaseTTLVal := time.Hour * 24 * 32 - b, err := Factory(context.Background(), &logical.BackendConfig{ - Logger: hclog.New(&hclog.LoggerOptions{ - Name: "FactoryLogger", - Level: hclog.Debug, - }), - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: defaultLeaseTTLVal, - MaxLeaseTTLVal: maxLeaseTTLVal, - }, - }) - if err != nil { - t.Fatalf("Unable to create backend: %s", err) - } - return b -} - -func TestBackend_basic(t *testing.T) { - b := factory(t) - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t, cfg), - // Map Admin_staff group (from LDAP server) with foo policy - testAccStepGroup(t, "admin_staff", "foo"), - - // Map engineers group (local) with bar policy - testAccStepGroup(t, "engineers", "bar"), - - // Map hermes conrad user with local engineers group - testAccStepUser(t, "hermes conrad", "engineers"), - - // Authenticate - testAccStepLogin(t, "hermes conrad", "hermes"), - - // Verify both groups mappings can be listed back - testAccStepGroupList(t, []string{"engineers", "admin_staff"}), - - // Verify user mapping can be listed back - testAccStepUserList(t, []string{"hermes conrad"}), - }, - }) -} - -func TestBackend_basic_noPolicies(t *testing.T) { - b := factory(t) - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t, cfg), - // Create LDAP user - testAccStepUser(t, "hermes conrad", ""), - // Authenticate - testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"), - testAccStepUserList(t, []string{"hermes conrad"}), - }, - }) -} - -func TestBackend_basic_group_noPolicies(t *testing.T) { - b := factory(t) - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t, cfg), - // Create engineers group with no policies - testAccStepGroup(t, "engineers", ""), - // Map hermes conrad user with local engineers group - testAccStepUser(t, "hermes conrad", "engineers"), - // Authenticate - testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"), - // Verify group mapping can be listed back - testAccStepGroupList(t, []string{"engineers"}), - }, - }) -} - -func TestBackend_basic_authbind(t *testing.T) { - b := factory(t) - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrlWithAuthBind(t, cfg), - testAccStepGroup(t, "admin_staff", "foo"), - testAccStepGroup(t, "engineers", "bar"), - testAccStepUser(t, "hermes conrad", "engineers"), - testAccStepLogin(t, "hermes conrad", "hermes"), - }, - }) -} - -func TestBackend_basic_authbind_userfilter(t *testing.T) { - b := factory(t) - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - - // userattr not used in the userfilter should result in a warning in the response - cfg.UserFilter = "((mail={{.Username}}))" - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrlWarningCheck(t, cfg, logical.UpdateOperation, []string{userFilterWarning}), - testAccStepConfigUrlWarningCheck(t, cfg, logical.ReadOperation, []string{userFilterWarning}), - }, - }) - - // If both upndomain and userfilter is set, ensure that a warning is still - // returned if userattr is not considered - cfg.UPNDomain = "planetexpress.com" - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrlWarningCheck(t, cfg, logical.UpdateOperation, []string{userFilterWarning}), - testAccStepConfigUrlWarningCheck(t, cfg, logical.ReadOperation, []string{userFilterWarning}), - }, - }) - - cfg.UPNDomain = "" - - // Add a liberal user filter, allowing to log in with either cn or email - cfg.UserFilter = "(|({{.UserAttr}}={{.Username}})(mail={{.Username}}))" - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t, cfg), - // Create engineers group with no policies - testAccStepGroup(t, "engineers", ""), - // Map hermes conrad user with local engineers group - testAccStepUser(t, "hermes conrad", "engineers"), - // Authenticate with cn attribute - testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"), - // Authenticate with mail attribute - testAccStepLoginNoAttachedPolicies(t, "hermes@planetexpress.com", "hermes"), - }, - }) - - // A filter giving the same DN makes the entity_id the same - entity_id := "" - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t, cfg), - // Create engineers group with no policies - testAccStepGroup(t, "engineers", ""), - // Map hermes conrad user with local engineers group - testAccStepUser(t, "hermes conrad", "engineers"), - // Authenticate with cn attribute - testAccStepLoginReturnsSameEntity(t, "hermes conrad", "hermes", &entity_id), - // Authenticate with mail attribute - testAccStepLoginReturnsSameEntity(t, "hermes@planetexpress.com", "hermes", &entity_id), - }, - }) - - // Missing entity alias attribute means access denied - cfg.UserAttr = "inexistent" - cfg.UserFilter = "(|({{.UserAttr}}={{.Username}})(mail={{.Username}}))" - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t, cfg), - // Authenticate with mail attribute will find DN but missing attribute means access denied - testAccStepLoginFailure(t, "hermes@planetexpress.com", "hermes"), - }, - }) - cfg.UserAttr = "cn" - - // UPNDomain has precedence over userfilter, for backward compatibility - cfg.UPNDomain = "planetexpress.com" - - addUPNAttributeToLDAPSchemaAndUser(t, cfg, "cn=Hubert J. Farnsworth,ou=people,dc=planetexpress,dc=com", "professor@planetexpress.com") - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrlWithAuthBind(t, cfg), - testAccStepLoginNoAttachedPolicies(t, "professor", "professor"), - }, - }) - - cfg.UPNDomain = "" - - // Add a strict user filter, rejecting login of bureaucrats - cfg.UserFilter = "(&({{.UserAttr}}={{.Username}})(!(employeeType=Bureaucrat)))" - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t, cfg), - // Authenticate with cn attribute - testAccStepLoginFailure(t, "hermes conrad", "hermes"), - }, - }) - - // Login fails when multiple user match search filter (using an incorrect filter on purporse) - cfg.UserFilter = "(objectClass=*)" - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - // testAccStepConfigUrl(t, cfg), - testAccStepConfigUrlWithAuthBind(t, cfg), - // Authenticate with cn attribute - testAccStepLoginFailure(t, "hermes conrad", "hermes"), - }, - }) - - // If UserAttr returns multiple attributes that can be used as alias then - // we return an error... - cfg.UserAttr = "employeeType" - cfg.UserFilter = "(cn={{.Username}})" - cfg.UsernameAsAlias = false - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t, cfg), - testAccStepLoginFailure(t, "hermes conrad", "hermes"), - }, - }) - - // ...unless username_as_alias has been set in which case we don't care - // about the alias returned by the LDAP server and always use the username - cfg.UsernameAsAlias = true - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t, cfg), - testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"), - }, - }) -} - -func TestBackend_basic_authbind_metadata_name(t *testing.T) { - b := factory(t) - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - - cfg.UserAttr = "cn" - cfg.UPNDomain = "planetexpress.com" - - addUPNAttributeToLDAPSchemaAndUser(t, cfg, "cn=Hubert J. Farnsworth,ou=people,dc=planetexpress,dc=com", "professor@planetexpress.com") - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrlWithAuthBind(t, cfg), - testAccStepLoginAliasMetadataName(t, "professor", "professor"), - }, - }) -} - -func addUPNAttributeToLDAPSchemaAndUser(t *testing.T, cfg *ldaputil.ConfigEntry, testUserDN string, testUserUPN string) { - // Setup connection - client := &ldaputil.Client{ - Logger: hclog.New(&hclog.LoggerOptions{ - Name: "LDAPAuthTest", - Level: hclog.Debug, - }), - LDAP: ldaputil.NewLDAP(), - } - conn, err := client.DialLDAP(cfg) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - if err := conn.Bind("cn=admin,cn=config", cfg.BindPassword); err != nil { - t.Fatal(err) - } - - // Add userPrincipalName attribute type - userPrincipleNameTypeReq := goldap.NewModifyRequest("cn={0}core,cn=schema,cn=config", nil) - userPrincipleNameTypeReq.Add("olcAttributetypes", []string{"( 2.25.247072656268950430024439664556757516066 NAME ( 'userPrincipalName' ) SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 EQUALITY caseIgnoreMatch SINGLE-VALUE )"}) - if err := conn.Modify(userPrincipleNameTypeReq); err != nil { - t.Fatal(err) - } - - // Add new object class - userPrincipleNameObjClassReq := goldap.NewModifyRequest("cn={0}core,cn=schema,cn=config", nil) - userPrincipleNameObjClassReq.Add("olcObjectClasses", []string{"( 1.2.840.113556.6.2.6 NAME 'PrincipalNameClass' AUXILIARY MAY ( userPrincipalName ) )"}) - if err := conn.Modify(userPrincipleNameObjClassReq); err != nil { - t.Fatal(err) - } - - // Re-authenticate with the binddn user - if err := conn.Bind(cfg.BindDN, cfg.BindPassword); err != nil { - t.Fatal(err) - } - - // Modify professor user and add userPrincipalName attribute - modifyUserReq := goldap.NewModifyRequest(testUserDN, nil) - modifyUserReq.Add("objectClass", []string{"PrincipalNameClass"}) - modifyUserReq.Add("userPrincipalName", []string{testUserUPN}) - if err := conn.Modify(modifyUserReq); err != nil { - t.Fatal(err) - } -} - -func TestBackend_basic_discover(t *testing.T) { - b := factory(t) - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrlWithDiscover(t, cfg), - testAccStepGroup(t, "admin_staff", "foo"), - testAccStepGroup(t, "engineers", "bar"), - testAccStepUser(t, "hermes conrad", "engineers"), - testAccStepLogin(t, "hermes conrad", "hermes"), - }, - }) -} - -func TestBackend_basic_nogroupdn(t *testing.T) { - b := factory(t) - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfigUrlNoGroupDN(t, cfg), - testAccStepGroup(t, "admin_staff", "foo"), - testAccStepGroup(t, "engineers", "bar"), - testAccStepUser(t, "hermes conrad", "engineers"), - testAccStepLoginNoGroupDN(t, "hermes conrad", "hermes"), - }, - }) -} - -func TestBackend_groupCrud(t *testing.T) { - b := factory(t) - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepGroup(t, "g1", "foo"), - testAccStepReadGroup(t, "g1", "foo"), - testAccStepDeleteGroup(t, "g1"), - testAccStepReadGroup(t, "g1", ""), - }, - }) -} - -/* - * Test backend configuration defaults are successfully read. - */ -func TestBackend_configDefaultsAfterUpdate(t *testing.T) { - b := factory(t) - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - { - Operation: logical.UpdateOperation, - Path: "config", - Data: map[string]interface{}{}, - }, - { - Operation: logical.ReadOperation, - Path: "config", - Check: func(resp *logical.Response) error { - if resp == nil { - return fmt.Errorf("bad: %#v", resp) - } - - // Test well-known defaults - cfg := resp.Data - defaultGroupFilter := "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))" - if cfg["groupfilter"] != defaultGroupFilter { - t.Errorf("Default mismatch: groupfilter. Expected: %q, received :%q", defaultGroupFilter, cfg["groupfilter"]) - } - - defaultGroupAttr := "cn" - if cfg["groupattr"] != defaultGroupAttr { - t.Errorf("Default mismatch: groupattr. Expected: %q, received :%q", defaultGroupAttr, cfg["groupattr"]) - } - - defaultUserAttr := "cn" - if cfg["userattr"] != defaultUserAttr { - t.Errorf("Default mismatch: userattr. Expected: %q, received :%q", defaultUserAttr, cfg["userattr"]) - } - - defaultUserFilter := "({{.UserAttr}}={{.Username}})" - if cfg["userfilter"] != defaultUserFilter { - t.Errorf("Default mismatch: userfilter. Expected: %q, received :%q", defaultUserFilter, cfg["userfilter"]) - } - - defaultDenyNullBind := true - if cfg["deny_null_bind"] != defaultDenyNullBind { - t.Errorf("Default mismatch: deny_null_bind. Expected: '%t', received :%q", defaultDenyNullBind, cfg["deny_null_bind"]) - } - - return nil - }, - }, - }, - }) -} - -func testAccStepConfigUrl(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config", - Data: map[string]interface{}{ - "url": cfg.Url, - "userattr": cfg.UserAttr, - "userdn": cfg.UserDN, - "userfilter": cfg.UserFilter, - "groupdn": cfg.GroupDN, - "groupattr": cfg.GroupAttr, - "binddn": cfg.BindDN, - "bindpass": cfg.BindPassword, - "case_sensitive_names": true, - "token_policies": "abc,xyz", - "request_timeout": cfg.RequestTimeout, - "connection_timeout": cfg.ConnectionTimeout, - "username_as_alias": cfg.UsernameAsAlias, - }, - } -} - -func testAccStepConfigUrlWithAuthBind(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config", - Data: map[string]interface{}{ - // In this test we also exercise multiple URL support - "url": "foobar://ldap.example.com," + cfg.Url, - "userattr": cfg.UserAttr, - "userdn": cfg.UserDN, - "groupdn": cfg.GroupDN, - "groupattr": cfg.GroupAttr, - "binddn": cfg.BindDN, - "bindpass": cfg.BindPassword, - "upndomain": cfg.UPNDomain, - "case_sensitive_names": true, - "token_policies": "abc,xyz", - "request_timeout": cfg.RequestTimeout, - "connection_timeout": cfg.ConnectionTimeout, - }, - } -} - -func testAccStepConfigUrlWithDiscover(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config", - Data: map[string]interface{}{ - "url": cfg.Url, - "userattr": cfg.UserAttr, - "userdn": cfg.UserDN, - "groupdn": cfg.GroupDN, - "groupattr": cfg.GroupAttr, - "binddn": cfg.BindDN, - "bindpass": cfg.BindPassword, - "discoverdn": true, - "case_sensitive_names": true, - "token_policies": "abc,xyz", - "request_timeout": cfg.RequestTimeout, - "connection_timeout": cfg.ConnectionTimeout, - }, - } -} - -func testAccStepConfigUrlNoGroupDN(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config", - Data: map[string]interface{}{ - "url": cfg.Url, - "userattr": cfg.UserAttr, - "userdn": cfg.UserDN, - "binddn": cfg.BindDN, - "bindpass": cfg.BindPassword, - "discoverdn": true, - "case_sensitive_names": true, - "request_timeout": cfg.RequestTimeout, - "connection_timeout": cfg.ConnectionTimeout, - }, - } -} - -func testAccStepConfigUrlWarningCheck(t *testing.T, cfg *ldaputil.ConfigEntry, operation logical.Operation, warnings []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: operation, - Path: "config", - Data: map[string]interface{}{ - "url": cfg.Url, - "userattr": cfg.UserAttr, - "userdn": cfg.UserDN, - "userfilter": cfg.UserFilter, - "groupdn": cfg.GroupDN, - "groupattr": cfg.GroupAttr, - "binddn": cfg.BindDN, - "bindpass": cfg.BindPassword, - "case_sensitive_names": true, - "token_policies": "abc,xyz", - "request_timeout": cfg.RequestTimeout, - "connection_timeout": cfg.ConnectionTimeout, - }, - Check: func(response *logical.Response) error { - if len(response.Warnings) == 0 { - return fmt.Errorf("expected warnings, got none") - } - - if !strutil.StrListSubset(response.Warnings, warnings) { - return fmt.Errorf("expected response to contain the following warnings:\n%s\ngot:\n%s", warnings, response.Warnings) - } - return nil - }, - } -} - -func testAccStepGroup(t *testing.T, group string, policies string) logicaltest.TestStep { - t.Logf("[testAccStepGroup] - Registering group %s, policy %s", group, policies) - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "groups/" + group, - Data: map[string]interface{}{ - "policies": policies, - }, - } -} - -func testAccStepReadGroup(t *testing.T, group string, policies string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "groups/" + group, - Check: func(resp *logical.Response) error { - if resp == nil { - if policies == "" { - return nil - } - return fmt.Errorf("bad: %#v", resp) - } - - var d struct { - Policies []string `mapstructure:"policies"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - if !reflect.DeepEqual(d.Policies, policyutil.ParsePolicies(policies)) { - return fmt.Errorf("bad: %#v", resp) - } - - return nil - }, - } -} - -func testAccStepDeleteGroup(t *testing.T, group string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "groups/" + group, - } -} - -func TestBackend_userCrud(t *testing.T) { - b := Backend() - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepUser(t, "g1", "bar"), - testAccStepReadUser(t, "g1", "bar"), - testAccStepDeleteUser(t, "g1"), - testAccStepReadUser(t, "g1", ""), - }, - }) -} - -func testAccStepUser(t *testing.T, user string, groups string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "users/" + user, - Data: map[string]interface{}{ - "groups": groups, - }, - } -} - -func testAccStepReadUser(t *testing.T, user string, groups string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "users/" + user, - Check: func(resp *logical.Response) error { - if resp == nil { - if groups == "" { - return nil - } - return fmt.Errorf("bad: %#v", resp) - } - - var d struct { - Groups string `mapstructure:"groups"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - if d.Groups != groups { - return fmt.Errorf("bad: %#v", resp) - } - - return nil - }, - } -} - -func testAccStepDeleteUser(t *testing.T, user string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "users/" + user, - } -} - -func testAccStepLogin(t *testing.T, user string, pass string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: map[string]interface{}{ - "password": pass, - }, - Unauthenticated: true, - - // Verifies user hermes conrad maps to groups via local group (engineers) as well as remote group (Scientists) - Check: logicaltest.TestCheckAuth([]string{"abc", "bar", "default", "foo", "xyz"}), - } -} - -func testAccStepLoginReturnsSameEntity(t *testing.T, user string, pass string, entity_id *string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: map[string]interface{}{ - "password": pass, - }, - Unauthenticated: true, - - // Verifies user hermes conrad maps to groups via local group (engineers) as well as remote group (Scientists) - Check: logicaltest.TestCheckAuthEntityId(entity_id), - } -} - -func testAccStepLoginNoAttachedPolicies(t *testing.T, user string, pass string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: map[string]interface{}{ - "password": pass, - }, - Unauthenticated: true, - - // Verifies user hermes conrad maps to groups via local group (engineers) as well as remote group (Scientists) - Check: logicaltest.TestCheckAuth([]string{"abc", "default", "xyz"}), - } -} - -func testAccStepLoginAliasMetadataName(t *testing.T, user string, pass string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: map[string]interface{}{ - "password": pass, - }, - Unauthenticated: true, - - Check: logicaltest.TestCheckAuthEntityAliasMetadataName("name", user), - } -} - -func testAccStepLoginFailure(t *testing.T, user string, pass string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: map[string]interface{}{ - "password": pass, - }, - Unauthenticated: true, - - ErrorOk: true, - } -} - -func testAccStepLoginNoGroupDN(t *testing.T, user string, pass string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: map[string]interface{}{ - "password": pass, - }, - Unauthenticated: true, - - // Verifies a search without defined GroupDN returns a warning rather than failing - Check: func(resp *logical.Response) error { - if len(resp.Warnings) != 1 { - return fmt.Errorf("expected a warning due to no group dn, got: %#v", resp.Warnings) - } - - return logicaltest.TestCheckAuth([]string{"bar", "default"})(resp) - }, - } -} - -func testAccStepGroupList(t *testing.T, groups []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "groups", - Check: func(resp *logical.Response) error { - if resp.IsError() { - return fmt.Errorf("got error response: %#v", *resp) - } - - expected := make([]string, len(groups)) - copy(expected, groups) - sort.Strings(expected) - - sortedResponse := make([]string, len(resp.Data["keys"].([]string))) - copy(sortedResponse, resp.Data["keys"].([]string)) - sort.Strings(sortedResponse) - - if !reflect.DeepEqual(expected, sortedResponse) { - return fmt.Errorf("expected:\n%#v\ngot:\n%#v\n", expected, sortedResponse) - } - return nil - }, - } -} - -func testAccStepUserList(t *testing.T, users []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "users", - Check: func(resp *logical.Response) error { - if resp.IsError() { - return fmt.Errorf("got error response: %#v", *resp) - } - - expected := make([]string, len(users)) - copy(expected, users) - sort.Strings(expected) - - sortedResponse := make([]string, len(resp.Data["keys"].([]string))) - copy(sortedResponse, resp.Data["keys"].([]string)) - sort.Strings(sortedResponse) - - if !reflect.DeepEqual(expected, sortedResponse) { - return fmt.Errorf("expected:\n%#v\ngot:\n%#v\n", expected, sortedResponse) - } - return nil - }, - } -} - -func TestLdapAuthBackend_ConfigUpgrade(t *testing.T) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - ctx := context.Background() - - cleanup, cfg := ldap.PrepareTestContainer(t, "latest") - defer cleanup() - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config", - Data: map[string]interface{}{ - "url": cfg.Url, - "userattr": cfg.UserAttr, - "userdn": cfg.UserDN, - "userfilter": cfg.UserFilter, - "groupdn": cfg.GroupDN, - "groupattr": cfg.GroupAttr, - "binddn": cfg.BindDN, - "bindpass": cfg.BindPassword, - "token_period": "5m", - "token_explicit_max_ttl": "24h", - "request_timeout": cfg.RequestTimeout, - "max_page_size": cfg.MaximumPageSize, - "connection_timeout": cfg.ConnectionTimeout, - }, - Storage: storage, - Connection: &logical.Connection{}, - } - resp, err = b.HandleRequest(ctx, configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - fd, err := b.getConfigFieldData() - if err != nil { - t.Fatal(err) - } - defParams, err := ldaputil.NewConfigEntry(nil, fd) - if err != nil { - t.Fatal(err) - } - falseBool := new(bool) - *falseBool = false - - exp := &ldapConfigEntry{ - TokenParams: tokenutil.TokenParams{ - TokenPeriod: 5 * time.Minute, - TokenExplicitMaxTTL: 24 * time.Hour, - }, - ConfigEntry: &ldaputil.ConfigEntry{ - Url: cfg.Url, - UserAttr: cfg.UserAttr, - UserFilter: cfg.UserFilter, - UserDN: cfg.UserDN, - GroupDN: cfg.GroupDN, - GroupAttr: cfg.GroupAttr, - BindDN: cfg.BindDN, - BindPassword: cfg.BindPassword, - GroupFilter: defParams.GroupFilter, - DenyNullBind: defParams.DenyNullBind, - TLSMinVersion: defParams.TLSMinVersion, - TLSMaxVersion: defParams.TLSMaxVersion, - CaseSensitiveNames: falseBool, - UsePre111GroupCNBehavior: new(bool), - RequestTimeout: cfg.RequestTimeout, - ConnectionTimeout: cfg.ConnectionTimeout, - UsernameAsAlias: false, - DerefAliases: "never", - MaximumPageSize: 1000, - }, - } - - configEntry, err := b.Config(ctx, configReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(exp, configEntry); diff != nil { - t.Fatal(diff) - } - - // Store just the config entry portion, for upgrade testing - entry, err := logical.StorageEntryJSON("config", configEntry.ConfigEntry) - if err != nil { - t.Fatal(err) - } - err = configReq.Storage.Put(ctx, entry) - if err != nil { - t.Fatal(err) - } - - configEntry, err = b.Config(ctx, configReq) - if err != nil { - t.Fatal(err) - } - // We won't have token params anymore so nil those out - exp.TokenParams = tokenutil.TokenParams{} - if diff := deep.Equal(exp, configEntry); diff != nil { - t.Fatal(diff) - } -} diff --git a/builtin/credential/radius/backend_test.go b/builtin/credential/radius/backend_test.go deleted file mode 100644 index c00e2dc26..000000000 --- a/builtin/credential/radius/backend_test.go +++ /dev/null @@ -1,361 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package radius - -import ( - "context" - "fmt" - "os" - "reflect" - "strconv" - "strings" - "testing" - "time" - - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/hashicorp/vault/sdk/logical" -) - -const ( - testSysTTL = time.Hour * 10 - testSysMaxTTL = time.Hour * 20 - - envRadiusRadiusHost = "RADIUS_HOST" - envRadiusPort = "RADIUS_PORT" - envRadiusSecret = "RADIUS_SECRET" - envRadiusUsername = "RADIUS_USERNAME" - envRadiusUserPass = "RADIUS_USERPASS" -) - -func prepareRadiusTestContainer(t *testing.T) (func(), string, int) { - if os.Getenv(envRadiusRadiusHost) != "" { - port, _ := strconv.Atoi(os.Getenv(envRadiusPort)) - return func() {}, os.Getenv(envRadiusRadiusHost), port - } - - // Now allow any client to connect to this radiusd instance by writing our - // own clients.conf file. - // - // This is necessary because we lack control over the container's network - // IPs. We might be running in Circle CI (with variable IPs per new - // network) or in Podman (which uses an entirely different set of default - // ranges than Docker). - // - // See also: https://freeradius.org/radiusd/man/clients.conf.html - ctx := context.Background() - clientsConfig := ` -client 0.0.0.0/1 { - ipaddr = 0.0.0.0/1 - secret = testing123 - shortname = all-clients-first -} - -client 128.0.0.0/1 { - ipaddr = 128.0.0.0/1 - secret = testing123 - shortname = all-clients-second -} -` - - containerfile := ` -FROM docker.mirror.hashicorp.services/jumanjiman/radiusd:latest - -COPY clients.conf /etc/raddb/clients.conf -` - - bCtx := docker.NewBuildContext() - bCtx["clients.conf"] = docker.PathContentsFromBytes([]byte(clientsConfig)) - - imageName := "vault_radiusd_any_client" - imageTag := "latest" - - runner, err := docker.NewServiceRunner(docker.RunOptions{ - ImageRepo: imageName, - ImageTag: imageTag, - ContainerName: "radiusd", - Cmd: []string{"-f", "-l", "stdout", "-X"}, - Ports: []string{"1812/udp"}, - LogConsumer: func(s string) { - if t.Failed() { - t.Logf("container logs: %s", s) - } - }, - }) - if err != nil { - t.Fatalf("Could not provision docker service runner: %s", err) - } - - output, err := runner.BuildImage(ctx, containerfile, bCtx, - docker.BuildRemove(true), docker.BuildForceRemove(true), - docker.BuildPullParent(true), - docker.BuildTags([]string{imageName + ":" + imageTag})) - if err != nil { - t.Fatalf("Could not build new image: %v", err) - } - - t.Logf("Image build output: %v", string(output)) - - svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - time.Sleep(2 * time.Second) - return docker.NewServiceHostPort(host, port), nil - }) - if err != nil { - t.Fatalf("Could not start docker radiusd: %s", err) - } - - pieces := strings.Split(svc.Config.Address(), ":") - port, _ := strconv.Atoi(pieces[1]) - return svc.Cleanup, pieces[0], port -} - -func TestBackend_Config(t *testing.T) { - b, err := Factory(context.Background(), &logical.BackendConfig{ - Logger: nil, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: testSysTTL, - MaxLeaseTTLVal: testSysMaxTTL, - }, - }) - if err != nil { - t.Fatalf("Unable to create backend: %s", err) - } - - configDataBasic := map[string]interface{}{ - "host": "test.radius.hostname.com", - "secret": "test-secret", - } - - configDataMissingRequired := map[string]interface{}{ - "host": "test.radius.hostname.com", - } - - configDataEmptyPort := map[string]interface{}{ - "host": "test.radius.hostname.com", - "port": "", - "secret": "test-secret", - } - - configDataInvalidPort := map[string]interface{}{ - "host": "test.radius.hostname.com", - "port": "notnumeric", - "secret": "test-secret", - } - - configDataInvalidBool := map[string]interface{}{ - "host": "test.radius.hostname.com", - "secret": "test-secret", - "unregistered_user_policies": "test", - } - - logicaltest.Test(t, logicaltest.TestCase{ - AcceptanceTest: false, - // PreCheck: func() { testAccPreCheck(t) }, - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testConfigWrite(t, configDataBasic, false), - testConfigWrite(t, configDataMissingRequired, true), - testConfigWrite(t, configDataEmptyPort, true), - testConfigWrite(t, configDataInvalidPort, true), - testConfigWrite(t, configDataInvalidBool, true), - }, - }) -} - -func TestBackend_users(t *testing.T) { - b, err := Factory(context.Background(), &logical.BackendConfig{ - Logger: nil, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: testSysTTL, - MaxLeaseTTLVal: testSysMaxTTL, - }, - }) - if err != nil { - t.Fatalf("Unable to create backend: %s", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testStepUpdateUser(t, "web", "foo"), - testStepUpdateUser(t, "web2", "foo"), - testStepUpdateUser(t, "web3", "foo"), - testStepUserList(t, []string{"web", "web2", "web3"}), - }, - }) -} - -func TestBackend_acceptance(t *testing.T) { - b, err := Factory(context.Background(), &logical.BackendConfig{ - Logger: nil, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: testSysTTL, - MaxLeaseTTLVal: testSysMaxTTL, - }, - }) - if err != nil { - t.Fatalf("Unable to create backend: %s", err) - } - - cleanup, host, port := prepareRadiusTestContainer(t) - defer cleanup() - - // These defaults are specific to the jumanjiman/radiusd docker image - username := os.Getenv(envRadiusUsername) - if username == "" { - username = "test" - } - - password := os.Getenv(envRadiusUserPass) - if password == "" { - password = "test" - } - - secret := os.Getenv(envRadiusSecret) - if len(secret) == 0 { - secret = "testing123" - } - - configDataAcceptanceAllowUnreg := map[string]interface{}{ - "host": host, - "port": strconv.Itoa(port), - "secret": secret, - "unregistered_user_policies": "policy1,policy2", - } - if configDataAcceptanceAllowUnreg["port"] == "" { - configDataAcceptanceAllowUnreg["port"] = "1812" - } - - configDataAcceptanceNoAllowUnreg := map[string]interface{}{ - "host": host, - "port": strconv.Itoa(port), - "secret": secret, - "unregistered_user_policies": "", - } - if configDataAcceptanceNoAllowUnreg["port"] == "" { - configDataAcceptanceNoAllowUnreg["port"] = "1812" - } - - dataRealpassword := map[string]interface{}{ - "password": password, - } - - dataWrongpassword := map[string]interface{}{ - "password": "wrongpassword", - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - PreCheck: testAccPreCheck(t, host, port), - Steps: []logicaltest.TestStep{ - // Login with valid but unknown user will fail because unregistered_user_policies is empty - testConfigWrite(t, configDataAcceptanceNoAllowUnreg, false), - testAccUserLogin(t, username, dataRealpassword, true), - // Once the user is registered auth will succeed - testStepUpdateUser(t, username, ""), - testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default"}, false), - - testStepUpdateUser(t, username, "foopolicy"), - testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "foopolicy"}, false), - testAccStepDeleteUser(t, username), - - // When unregistered_user_policies is specified, an unknown user will be granted access and granted the listed policies - testConfigWrite(t, configDataAcceptanceAllowUnreg, false), - testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "policy1", "policy2"}, false), - - // More tests - testAccUserLogin(t, "nonexistinguser", dataRealpassword, true), - testAccUserLogin(t, username, dataWrongpassword, true), - testStepUpdateUser(t, username, "foopolicy"), - testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "foopolicy"}, false), - testStepUpdateUser(t, username, "foopolicy, secondpolicy"), - testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "foopolicy", "secondpolicy"}, false), - testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "foopolicy", "secondpolicy", "thirdpolicy"}, true), - }, - }) -} - -func testAccPreCheck(t *testing.T, host string, port int) func() { - return func() { - if host == "" { - t.Fatal("Host must be set for acceptance tests") - } - - if port == 0 { - t.Fatal("Port must be non-zero for acceptance tests") - } - } -} - -func testConfigWrite(t *testing.T, d map[string]interface{}, expectError bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config", - Data: d, - ErrorOk: expectError, - } -} - -func testAccStepDeleteUser(t *testing.T, n string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "users/" + n, - } -} - -func testStepUserList(t *testing.T, users []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "users", - Check: func(resp *logical.Response) error { - if resp.IsError() { - return fmt.Errorf("got error response: %#v", *resp) - } - - if !reflect.DeepEqual(users, resp.Data["keys"].([]string)) { - return fmt.Errorf("expected:\n%#v\ngot:\n%#v\n", users, resp.Data["keys"]) - } - return nil - }, - } -} - -func testStepUpdateUser( - t *testing.T, name string, policies string, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "users/" + name, - Data: map[string]interface{}{ - "policies": policies, - }, - } -} - -func testAccUserLogin(t *testing.T, user string, data map[string]interface{}, expectError bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: data, - ErrorOk: expectError, - Unauthenticated: true, - } -} - -func testAccUserLoginPolicy(t *testing.T, user string, data map[string]interface{}, policies []string, expectError bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: data, - ErrorOk: expectError, - Unauthenticated: true, - // Check: logicaltest.TestCheckAuth(policies), - Check: func(resp *logical.Response) error { - res := logicaltest.TestCheckAuth(policies)(resp) - if res != nil && expectError { - return nil - } - return res - }, - } -} diff --git a/builtin/credential/userpass/backend_test.go b/builtin/credential/userpass/backend_test.go deleted file mode 100644 index 7280667c8..000000000 --- a/builtin/credential/userpass/backend_test.go +++ /dev/null @@ -1,419 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package userpass - -import ( - "context" - "crypto/tls" - "fmt" - "reflect" - "testing" - "time" - - "github.com/go-test/deep" - sockaddr "github.com/hashicorp/go-sockaddr" - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - "github.com/hashicorp/vault/sdk/helper/policyutil" - "github.com/hashicorp/vault/sdk/helper/tokenutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/mapstructure" -) - -const ( - testSysTTL = time.Hour * 10 - testSysMaxTTL = time.Hour * 20 -) - -func TestBackend_CRUD(t *testing.T) { - var resp *logical.Response - var err error - - storage := &logical.InmemStorage{} - - config := logical.TestBackendConfig() - config.StorageView = storage - - ctx := context.Background() - - b, err := Factory(ctx, config) - if err != nil { - t.Fatal(err) - } - if b == nil { - t.Fatalf("failed to create backend") - } - - localhostSockAddr, err := sockaddr.NewSockAddr("127.0.0.1") - if err != nil { - t.Fatal(err) - } - - // Use new token_ forms - resp, err = b.HandleRequest(ctx, &logical.Request{ - Path: "users/testuser", - Operation: logical.CreateOperation, - Storage: storage, - Data: map[string]interface{}{ - "password": "testpassword", - "token_ttl": 5, - "token_max_ttl": 10, - "token_policies": []string{"foo"}, - "token_bound_cidrs": []string{"127.0.0.1"}, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err) - } - - resp, err = b.HandleRequest(ctx, &logical.Request{ - Path: "users/testuser", - Operation: logical.ReadOperation, - Storage: storage, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err) - } - if resp.Data["token_ttl"].(int64) != 5 && resp.Data["token_max_ttl"].(int64) != 10 { - t.Fatalf("bad: token_ttl and token_max_ttl are not set correctly") - } - if diff := deep.Equal(resp.Data["token_policies"], []string{"foo"}); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["token_bound_cidrs"], []*sockaddr.SockAddrMarshaler{{localhostSockAddr}}); diff != nil { - t.Fatal(diff) - } - - localhostSockAddr, err = sockaddr.NewSockAddr("127.0.1.1") - if err != nil { - t.Fatal(err) - } - - // Use the old forms and verify that they zero out the new ones and then - // the new ones read with the expected value - resp, err = b.HandleRequest(ctx, &logical.Request{ - Path: "users/testuser", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "ttl": "5m", - "max_ttl": "10m", - "policies": []string{"bar"}, - "bound_cidrs": []string{"127.0.1.1"}, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err) - } - - resp, err = b.HandleRequest(ctx, &logical.Request{ - Path: "users/testuser", - Operation: logical.ReadOperation, - Storage: storage, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err) - } - if resp.Data["ttl"].(int64) != 300 && resp.Data["max_ttl"].(int64) != 600 { - t.Fatalf("bad: ttl and max_ttl are not set correctly") - } - if resp.Data["token_ttl"].(int64) != 300 && resp.Data["token_max_ttl"].(int64) != 600 { - t.Fatalf("bad: token_ttl and token_max_ttl are not set correctly") - } - if diff := deep.Equal(resp.Data["policies"], []string{"bar"}); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["token_policies"], []string{"bar"}); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["bound_cidrs"], []*sockaddr.SockAddrMarshaler{{localhostSockAddr}}); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["token_bound_cidrs"], []*sockaddr.SockAddrMarshaler{{localhostSockAddr}}); diff != nil { - t.Fatal(diff) - } -} - -func TestBackend_basic(t *testing.T) { - b, err := Factory(context.Background(), &logical.BackendConfig{ - Logger: nil, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: testSysTTL, - MaxLeaseTTLVal: testSysMaxTTL, - }, - }) - if err != nil { - t.Fatalf("Unable to create backend: %s", err) - } - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepUser(t, "web", "password", "foo"), - testAccStepUser(t, "web2", "password", "foo"), - testAccStepUser(t, "web3", "password", "foo"), - testAccStepList(t, []string{"web", "web2", "web3"}), - testAccStepLogin(t, "web", "password", []string{"default", "foo"}), - }, - }) -} - -func TestBackend_userCrud(t *testing.T) { - b, err := Factory(context.Background(), &logical.BackendConfig{ - Logger: nil, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: testSysTTL, - MaxLeaseTTLVal: testSysMaxTTL, - }, - }) - if err != nil { - t.Fatalf("Unable to create backend: %s", err) - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepUser(t, "web", "password", "foo"), - testAccStepReadUser(t, "web", "foo"), - testAccStepDeleteUser(t, "web"), - testAccStepReadUser(t, "web", ""), - }, - }) -} - -func TestBackend_userCreateOperation(t *testing.T) { - b, err := Factory(context.Background(), &logical.BackendConfig{ - Logger: nil, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: testSysTTL, - MaxLeaseTTLVal: testSysMaxTTL, - }, - }) - if err != nil { - t.Fatalf("Unable to create backend: %s", err) - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testUserCreateOperation(t, "web", "password", "foo"), - testAccStepLogin(t, "web", "password", []string{"default", "foo"}), - }, - }) -} - -func TestBackend_passwordUpdate(t *testing.T) { - b, err := Factory(context.Background(), &logical.BackendConfig{ - Logger: nil, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: testSysTTL, - MaxLeaseTTLVal: testSysMaxTTL, - }, - }) - if err != nil { - t.Fatalf("Unable to create backend: %s", err) - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepUser(t, "web", "password", "foo"), - testAccStepReadUser(t, "web", "foo"), - testAccStepLogin(t, "web", "password", []string{"default", "foo"}), - testUpdatePassword(t, "web", "newpassword"), - testAccStepLogin(t, "web", "newpassword", []string{"default", "foo"}), - }, - }) -} - -func TestBackend_policiesUpdate(t *testing.T) { - b, err := Factory(context.Background(), &logical.BackendConfig{ - Logger: nil, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: testSysTTL, - MaxLeaseTTLVal: testSysMaxTTL, - }, - }) - if err != nil { - t.Fatalf("Unable to create backend: %s", err) - } - - logicaltest.Test(t, logicaltest.TestCase{ - CredentialBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepUser(t, "web", "password", "foo"), - testAccStepReadUser(t, "web", "foo"), - testAccStepLogin(t, "web", "password", []string{"default", "foo"}), - testUpdatePolicies(t, "web", "foo,bar"), - testAccStepReadUser(t, "web", "bar,foo"), - testAccStepLogin(t, "web", "password", []string{"bar", "default", "foo"}), - }, - }) -} - -func testUpdatePassword(t *testing.T, user, password string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "users/" + user + "/password", - Data: map[string]interface{}{ - "password": password, - }, - } -} - -func testUpdatePolicies(t *testing.T, user, policies string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "users/" + user + "/policies", - Data: map[string]interface{}{ - "policies": policies, - }, - } -} - -func testAccStepList(t *testing.T, users []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "users", - Check: func(resp *logical.Response) error { - if resp.IsError() { - return fmt.Errorf("got error response: %#v", *resp) - } - - exp := []string{"web", "web2", "web3"} - if !reflect.DeepEqual(exp, resp.Data["keys"].([]string)) { - return fmt.Errorf("expected:\n%#v\ngot:\n%#v\n", exp, resp.Data["keys"]) - } - return nil - }, - } -} - -func testAccStepLogin(t *testing.T, user string, pass string, policies []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: map[string]interface{}{ - "password": pass, - }, - Unauthenticated: true, - - Check: logicaltest.TestCheckAuth(policies), - ConnState: &tls.ConnectionState{}, - } -} - -func testUserCreateOperation( - t *testing.T, name string, password string, policies string, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.CreateOperation, - Path: "users/" + name, - Data: map[string]interface{}{ - "password": password, - "policies": policies, - }, - } -} - -func testAccStepUser( - t *testing.T, name string, password string, policies string, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "users/" + name, - Data: map[string]interface{}{ - "password": password, - "policies": policies, - }, - } -} - -func testAccStepDeleteUser(t *testing.T, n string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "users/" + n, - } -} - -func testAccStepReadUser(t *testing.T, name string, policies string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "users/" + name, - Check: func(resp *logical.Response) error { - if resp == nil { - if policies == "" { - return nil - } - - return fmt.Errorf("bad: %#v", resp) - } - - var d struct { - Policies []string `mapstructure:"policies"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - if !reflect.DeepEqual(d.Policies, policyutil.ParsePolicies(policies)) { - return fmt.Errorf("bad: %#v", resp) - } - - return nil - }, - } -} - -func TestBackend_UserUpgrade(t *testing.T) { - s := &logical.InmemStorage{} - - config := logical.TestBackendConfig() - config.StorageView = s - - ctx := context.Background() - - b := Backend() - if b == nil { - t.Fatalf("failed to create backend") - } - if err := b.Setup(ctx, config); err != nil { - t.Fatal(err) - } - - foo := &UserEntry{ - Policies: []string{"foo"}, - TTL: time.Second, - MaxTTL: time.Second, - BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}}, - } - - entry, err := logical.StorageEntryJSON("user/foo", foo) - if err != nil { - t.Fatal(err) - } - err = s.Put(ctx, entry) - if err != nil { - t.Fatal(err) - } - - userEntry, err := b.user(ctx, s, "foo") - if err != nil { - t.Fatal(err) - } - - exp := &UserEntry{ - Policies: []string{"foo"}, - TTL: time.Second, - MaxTTL: time.Second, - BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}}, - TokenParams: tokenutil.TokenParams{ - TokenPolicies: []string{"foo"}, - TokenTTL: time.Second, - TokenMaxTTL: time.Second, - TokenBoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}}, - }, - } - if diff := deep.Equal(userEntry, exp); diff != nil { - t.Fatal(diff) - } -} diff --git a/builtin/credential/userpass/stepwise_test.go b/builtin/credential/userpass/stepwise_test.go deleted file mode 100644 index 241d7707b..000000000 --- a/builtin/credential/userpass/stepwise_test.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package userpass - -import ( - "fmt" - "reflect" - "testing" - - stepwise "github.com/hashicorp/vault-testing-stepwise" - dockerEnvironment "github.com/hashicorp/vault-testing-stepwise/environments/docker" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/helper/policyutil" - "github.com/mitchellh/mapstructure" -) - -func TestAccBackend_stepwise_UserCrud(t *testing.T) { - customPluginName := "my-userpass" - envOptions := &stepwise.MountOptions{ - RegistryName: customPluginName, - PluginType: api.PluginTypeCredential, - PluginName: "userpass", - MountPathPrefix: customPluginName, - } - stepwise.Run(t, stepwise.Case{ - Environment: dockerEnvironment.NewEnvironment(customPluginName, envOptions), - Steps: []stepwise.Step{ - testAccStepwiseUser(t, "web", "password", "foo"), - testAccStepwiseReadUser(t, "web", "foo"), - testAccStepwiseDeleteUser(t, "web"), - testAccStepwiseReadUser(t, "web", ""), - }, - }) -} - -func testAccStepwiseUser( - t *testing.T, name string, password string, policies string, -) stepwise.Step { - return stepwise.Step{ - Operation: stepwise.UpdateOperation, - Path: "users/" + name, - Data: map[string]interface{}{ - "password": password, - "policies": policies, - }, - } -} - -func testAccStepwiseDeleteUser(t *testing.T, name string) stepwise.Step { - return stepwise.Step{ - Operation: stepwise.DeleteOperation, - Path: "users/" + name, - } -} - -func testAccStepwiseReadUser(t *testing.T, name string, policies string) stepwise.Step { - return stepwise.Step{ - Operation: stepwise.ReadOperation, - Path: "users/" + name, - Assert: func(resp *api.Secret, err error) error { - if resp == nil { - if policies == "" { - return nil - } - - return fmt.Errorf("unexpected nil response") - } - - var d struct { - Policies []string `mapstructure:"policies"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - expectedPolicies := policyutil.ParsePolicies(policies) - if !reflect.DeepEqual(d.Policies, expectedPolicies) { - return fmt.Errorf("Actual policies: %#v\nExpected policies: %#v", d.Policies, expectedPolicies) - } - - return nil - }, - } -} diff --git a/builtin/logical/database/backend_test.go b/builtin/logical/database/backend_test.go deleted file mode 100644 index 13eeec71e..000000000 --- a/builtin/logical/database/backend_test.go +++ /dev/null @@ -1,1573 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package database - -import ( - "context" - "database/sql" - "fmt" - "log" - "net/url" - "os" - "reflect" - "strings" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/helper/builtinplugins" - "github.com/hashicorp/vault/helper/namespace" - postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/plugins/database/postgresql" - v4 "github.com/hashicorp/vault/sdk/database/dbplugin" - v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/hashicorp/vault/sdk/database/helper/dbutil" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/pluginutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - _ "github.com/jackc/pgx/v4" - "github.com/mitchellh/mapstructure" -) - -func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "database": Factory, - }, - BuiltinRegistry: builtinplugins.Registry, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - - os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) - - sys := vault.TestDynamicSystemView(cores[0].Core, nil) - vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_Postgres", []string{}, "") - vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_PostgresMultiplexed", []string{}, "") - - return cluster, sys -} - -func TestBackend_PluginMain_Postgres(t *testing.T) { - if os.Getenv(pluginutil.PluginVaultVersionEnv) == "" { - return - } - - dbType, err := postgresql.New() - if err != nil { - t.Fatalf("Failed to initialize postgres: %s", err) - } - - v5.Serve(dbType.(v5.Database)) -} - -func TestBackend_PluginMain_PostgresMultiplexed(t *testing.T) { - if os.Getenv(pluginutil.PluginVaultVersionEnv) == "" { - return - } - - v5.ServeMultiplex(postgresql.New) -} - -func TestBackend_RoleUpgrade(t *testing.T) { - storage := &logical.InmemStorage{} - backend := &databaseBackend{} - - roleExpected := &roleEntry{ - Statements: v4.Statements{ - CreationStatements: "test", - Creation: []string{"test"}, - }, - } - - entry, err := logical.StorageEntryJSON("role/test", &roleEntry{ - Statements: v4.Statements{ - CreationStatements: "test", - }, - }) - if err != nil { - t.Fatal(err) - } - if err := storage.Put(context.Background(), entry); err != nil { - t.Fatal(err) - } - - role, err := backend.Role(context.Background(), storage, "test") - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(role, roleExpected) { - t.Fatalf("bad role %#v, %#v", role, roleExpected) - } - - // Upgrade case - badJSON := `{"statments":{"creation_statments":"test","revocation_statements":"","rollback_statements":"","renew_statements":""}}` - entry = &logical.StorageEntry{ - Key: "role/test", - Value: []byte(badJSON), - } - if err := storage.Put(context.Background(), entry); err != nil { - t.Fatal(err) - } - - role, err = backend.Role(context.Background(), storage, "test") - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(role, roleExpected) { - t.Fatalf("bad role %#v, %#v", role, roleExpected) - } -} - -func TestBackend_config_connection(t *testing.T) { - var resp *logical.Response - var err error - - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to database backend") - } - defer b.Cleanup(context.Background()) - - // Test creation - { - configData := map[string]interface{}{ - "connection_url": "sample_connection_url", - "someotherdata": "testing", - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "name": "plugin-test", - } - - configReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: configData, - } - - exists, err := b.connectionExistenceCheck()(context.Background(), configReq, &framework.FieldData{ - Raw: configData, - Schema: pathConfigurePluginConnection(b).Fields, - }) - if err != nil { - t.Fatal(err) - } - if exists { - t.Fatal("expected not exists") - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v\n", err, resp) - } - - expected := map[string]interface{}{ - "plugin_name": "postgresql-database-plugin", - "connection_details": map[string]interface{}{ - "connection_url": "sample_connection_url", - "someotherdata": "testing", - }, - "allowed_roles": []string{"*"}, - "root_credentials_rotate_statements": []string{}, - "password_policy": "", - "plugin_version": "", - } - configReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - delete(resp.Data["connection_details"].(map[string]interface{}), "name") - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) - } - } - - // Test existence check and an update to a single connection detail parameter - { - configData := map[string]interface{}{ - "connection_url": "sample_convection_url", - "verify_connection": false, - "name": "plugin-test", - } - - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: configData, - } - - exists, err := b.connectionExistenceCheck()(context.Background(), configReq, &framework.FieldData{ - Raw: configData, - Schema: pathConfigurePluginConnection(b).Fields, - }) - if err != nil { - t.Fatal(err) - } - if !exists { - t.Fatal("expected exists") - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v\n", err, resp) - } - - expected := map[string]interface{}{ - "plugin_name": "postgresql-database-plugin", - "connection_details": map[string]interface{}{ - "connection_url": "sample_convection_url", - "someotherdata": "testing", - }, - "allowed_roles": []string{"*"}, - "root_credentials_rotate_statements": []string{}, - "password_policy": "", - "plugin_version": "", - } - configReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - delete(resp.Data["connection_details"].(map[string]interface{}), "name") - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) - } - } - - // Test an update to a non-details value - { - configData := map[string]interface{}{ - "verify_connection": false, - "allowed_roles": []string{"flu", "barre"}, - "name": "plugin-test", - } - - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: configData, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v\n", err, resp) - } - - expected := map[string]interface{}{ - "plugin_name": "postgresql-database-plugin", - "connection_details": map[string]interface{}{ - "connection_url": "sample_convection_url", - "someotherdata": "testing", - }, - "allowed_roles": []string{"flu", "barre"}, - "root_credentials_rotate_statements": []string{}, - "password_policy": "", - "plugin_version": "", - } - configReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - delete(resp.Data["connection_details"].(map[string]interface{}), "name") - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) - } - } - - req := &logical.Request{ - Operation: logical.ListOperation, - Storage: config.StorageView, - Path: "config/", - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatal(err) - } - keys := resp.Data["keys"].([]string) - key := keys[0] - if key != "plugin-test" { - t.Fatalf("bad key: %q", key) - } -} - -func TestBackend_BadConnectionString(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(context.Background()) - - cleanup, _ := postgreshelper.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - respCheck := func(req *logical.Request) { - t.Helper() - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected error, resp:%#v", resp) - } - err = resp.Error() - if strings.Contains(err.Error(), "localhost") { - t.Fatalf("error should not contain connection info") - } - } - - // Configure a connection - data := map[string]interface{}{ - "connection_url": "postgresql://:pw@[localhost", - "plugin_name": "postgresql-database-plugin", - "allowed_roles": []string{"plugin-role-test"}, - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - respCheck(req) - - time.Sleep(1 * time.Second) -} - -func TestBackend_basic(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": []string{"plugin-role-test"}, - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Create a role - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "max_ttl": "10m", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - // Get creds - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - credsResp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - // Update the role with no max ttl - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "default_ttl": "5m", - "max_ttl": 0, - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - // Get creds - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - credsResp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - // Test for #3812 - if credsResp.Secret.TTL != 5*time.Minute { - t.Fatalf("unexpected TTL of %d", credsResp.Secret.TTL) - } - // Update the role with a max ttl - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "default_ttl": "5m", - "max_ttl": "10m", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Get creds and revoke when the role stays in existence - { - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - credsResp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - // Test for #3812 - if credsResp.Secret.TTL != 5*time.Minute { - t.Fatalf("unexpected TTL of %d", credsResp.Secret.TTL) - } - if !testCredsExist(t, credsResp, connURL) { - t.Fatalf("Creds should exist") - } - - // Revoke creds - resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Operation: logical.RevokeOperation, - Storage: config.StorageView, - Secret: &logical.Secret{ - InternalData: map[string]interface{}{ - "secret_type": "creds", - "username": credsResp.Data["username"], - "role": "plugin-role-test", - }, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - if testCredsExist(t, credsResp, connURL) { - t.Fatalf("Creds should not exist") - } - } - - // Get creds and revoke using embedded revocation data - { - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - credsResp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - if !testCredsExist(t, credsResp, connURL) { - t.Fatalf("Creds should exist") - } - - // Delete role, forcing us to rely on embedded data - req = &logical.Request{ - Operation: logical.DeleteOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Revoke creds - resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Operation: logical.RevokeOperation, - Storage: config.StorageView, - Secret: &logical.Secret{ - InternalData: map[string]interface{}{ - "secret_type": "creds", - "username": credsResp.Data["username"], - "role": "plugin-role-test", - "db_name": "plugin-test", - "revocation_statements": nil, - }, - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - if testCredsExist(t, credsResp, connURL) { - t.Fatalf("Creds should not exist") - } - } -} - -func TestBackend_connectionCrud(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - // Configure a connection - data := map[string]interface{}{ - "connection_url": "test", - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Create a role - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "revocation_statements": defaultRevocationSQL, - "default_ttl": "5m", - "max_ttl": "10m", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Update the connection - data = map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": []string{"plugin-role-test"}, - "username": "postgres", - "password": "secret", - "private_key": "PRIVATE_KEY", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - if len(resp.Warnings) == 0 { - t.Fatalf("expected warning about password in url %s, resp:%#v\n", connURL, resp) - } - - req.Operation = logical.ReadOperation - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - returnedConnectionDetails := resp.Data["connection_details"].(map[string]interface{}) - if strings.Contains(returnedConnectionDetails["connection_url"].(string), "secret") { - t.Fatal("password should not be found in the connection url") - } - // Covered by the filled out `expected` value below, but be explicit about this requirement. - if _, exists := returnedConnectionDetails["password"]; exists { - t.Fatal("password should NOT be found in the returned config") - } - if _, exists := returnedConnectionDetails["private_key"]; exists { - t.Fatal("private_key should NOT be found in the returned config") - } - - // Replace connection url with templated version - req.Operation = logical.UpdateOperation - connURL = strings.ReplaceAll(connURL, "postgres:secret", "{{username}}:{{password}}") - data["connection_url"] = connURL - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read connection - expected := map[string]interface{}{ - "plugin_name": "postgresql-database-plugin", - "connection_details": map[string]interface{}{ - "username": "postgres", - "connection_url": connURL, - }, - "allowed_roles": []string{"plugin-role-test"}, - "root_credentials_rotate_statements": []string(nil), - "password_policy": "", - "plugin_version": "", - } - req.Operation = logical.ReadOperation - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - delete(resp.Data["connection_details"].(map[string]interface{}), "name") - if diff := deep.Equal(resp.Data, expected); diff != nil { - t.Fatal(diff) - } - - // Reset Connection - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "reset/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Get creds - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - credsResp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - - credCheckURL := dbutil.QueryHelper(connURL, map[string]string{ - "username": "postgres", - "password": "secret", - }) - if !testCredsExist(t, credsResp, credCheckURL) { - t.Fatalf("Creds should exist") - } - - // Delete Connection - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.DeleteOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read connection - req.Operation = logical.ReadOperation - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Should be empty - if resp != nil { - t.Fatal("Expected response to be nil") - } -} - -func TestBackend_roleCrud(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Test role creation - { - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "revocation_statements": defaultRevocationSQL, - "default_ttl": "5m", - "max_ttl": "10m", - } - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read the role - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - expected := v4.Statements{ - Creation: []string{strings.TrimSpace(testRole)}, - Revocation: []string{strings.TrimSpace(defaultRevocationSQL)}, - Rollback: []string{}, - Renewal: []string{}, - } - - actual := v4.Statements{ - Creation: resp.Data["creation_statements"].([]string), - Revocation: resp.Data["revocation_statements"].([]string), - Rollback: resp.Data["rollback_statements"].([]string), - Renewal: resp.Data["renew_statements"].([]string), - } - - if diff := deep.Equal(expected, actual); diff != nil { - t.Fatal(diff) - } - - if diff := deep.Equal(resp.Data["db_name"], "plugin-test"); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["default_ttl"], float64(300)); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["max_ttl"], float64(600)); diff != nil { - t.Fatal(diff) - } - } - - // Test role modification of TTL - { - data = map[string]interface{}{ - "name": "plugin-role-test", - "max_ttl": "7m", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v\n", err, resp) - } - - // Read the role - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - expected := v4.Statements{ - Creation: []string{strings.TrimSpace(testRole)}, - Revocation: []string{strings.TrimSpace(defaultRevocationSQL)}, - Rollback: []string{}, - Renewal: []string{}, - } - - actual := v4.Statements{ - Creation: resp.Data["creation_statements"].([]string), - Revocation: resp.Data["revocation_statements"].([]string), - Rollback: resp.Data["rollback_statements"].([]string), - Renewal: resp.Data["renew_statements"].([]string), - } - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("Statements did not match, expected %#v, got %#v", expected, actual) - } - - if diff := deep.Equal(resp.Data["db_name"], "plugin-test"); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["default_ttl"], float64(300)); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["max_ttl"], float64(420)); diff != nil { - t.Fatal(diff) - } - - } - - // Test role modification of statements - { - data = map[string]interface{}{ - "name": "plugin-role-test", - "creation_statements": []string{testRole, testRole}, - "revocation_statements": []string{defaultRevocationSQL, defaultRevocationSQL}, - "rollback_statements": testRole, - "renew_statements": defaultRevocationSQL, - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v\n", err, resp) - } - - // Read the role - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - expected := v4.Statements{ - Creation: []string{strings.TrimSpace(testRole), strings.TrimSpace(testRole)}, - Rollback: []string{strings.TrimSpace(testRole)}, - Revocation: []string{strings.TrimSpace(defaultRevocationSQL), strings.TrimSpace(defaultRevocationSQL)}, - Renewal: []string{strings.TrimSpace(defaultRevocationSQL)}, - } - - actual := v4.Statements{ - Creation: resp.Data["creation_statements"].([]string), - Revocation: resp.Data["revocation_statements"].([]string), - Rollback: resp.Data["rollback_statements"].([]string), - Renewal: resp.Data["renew_statements"].([]string), - } - - if diff := deep.Equal(expected, actual); diff != nil { - t.Fatal(diff) - } - - if diff := deep.Equal(resp.Data["db_name"], "plugin-test"); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["default_ttl"], float64(300)); diff != nil { - t.Fatal(diff) - } - if diff := deep.Equal(resp.Data["max_ttl"], float64(420)); diff != nil { - t.Fatal(diff) - } - } - - // Delete the role - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.DeleteOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read the role - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Should be empty - if resp != nil { - t.Fatal("Expected response to be nil") - } -} - -func TestBackend_allowedRoles(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Create a denied and an allowed role - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "default_ttl": "5m", - "max_ttl": "10m", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/denied", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "default_ttl": "5m", - "max_ttl": "10m", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/allowed", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Get creds from denied role, should fail - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/denied", - Storage: config.StorageView, - Data: data, - } - credsResp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err == nil { - t.Fatal("expected error because role is denied") - } - - // update connection with glob allowed roles connection - data = map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": "allow*", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Get creds, should work. - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/allowed", - Storage: config.StorageView, - Data: data, - } - credsResp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - - if !testCredsExist(t, credsResp, connURL) { - t.Fatalf("Creds should exist") - } - - // update connection with * allowed roles connection - data = map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": "*", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Get creds, should work. - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/allowed", - Storage: config.StorageView, - Data: data, - } - credsResp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - - if !testCredsExist(t, credsResp, connURL) { - t.Fatalf("Creds should exist") - } - - // update connection with allowed roles - data = map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": "allow, allowed", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Get creds from denied role, should fail - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/denied", - Storage: config.StorageView, - Data: data, - } - credsResp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err == nil { - t.Fatal("expected error because role is denied") - } - - // Get creds from allowed role, should work. - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/allowed", - Storage: config.StorageView, - Data: data, - } - credsResp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - - if !testCredsExist(t, credsResp, connURL) { - t.Fatalf("Creds should exist") - } -} - -func TestBackend_RotateRootCredentials(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - connURL = strings.ReplaceAll(connURL, "postgres:secret", "{{username}}:{{password}}") - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": []string{"plugin-role-test"}, - "username": "postgres", - "password": "secret", - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Create a role - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "max_ttl": "10m", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - // Get creds - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - credsResp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "rotate-root/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } - - dbConfig, err := b.(*databaseBackend).DatabaseConfig(context.Background(), config.StorageView, "plugin-test") - if err != nil { - t.Fatalf("err: %#v", err) - } - if dbConfig.ConnectionDetails["password"].(string) == "secret" { - t.Fatal("root credentials not rotated") - } - - // Get creds to make sure it still works - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - credsResp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (credsResp != nil && credsResp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, credsResp) - } -} - -func TestBackend_ConnectionURL_redacted(t *testing.T) { - cluster, sys := getCluster(t) - t.Cleanup(cluster.Cleanup) - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(context.Background()) - - tests := []struct { - name string - password string - }{ - { - name: "basic", - password: "secret", - }, - { - name: "encoded", - password: "yourStrong(!)Password", - }, - } - - respCheck := func(req *logical.Request) *logical.Response { - t.Helper() - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp == nil { - t.Fatalf("expected a response, resp: %#v", resp) - } - - if resp.Error() != nil { - t.Fatalf("unexpected error in response, err: %#v", resp.Error()) - } - - return resp - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cleanup, u := postgreshelper.PrepareTestContainerWithPassword(t, "13.4-buster", tt.password) - t.Cleanup(cleanup) - - p, err := url.Parse(u) - if err != nil { - t.Fatal(err) - } - - actualPassword, _ := p.User.Password() - if tt.password != actualPassword { - t.Fatalf("expected computed URL password %#v, actual %#v", tt.password, actualPassword) - } - - // Configure a connection - data := map[string]interface{}{ - "connection_url": u, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": []string{"plugin-role-test"}, - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("config/%s", tt.name), - Storage: config.StorageView, - Data: data, - } - respCheck(req) - - // read config - readReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: req.Path, - Storage: config.StorageView, - } - resp := respCheck(readReq) - - var connDetails map[string]interface{} - if v, ok := resp.Data["connection_details"]; ok { - connDetails = v.(map[string]interface{}) - } - - if connDetails == nil { - t.Fatalf("response data missing connection_details, resp: %#v", resp) - } - - actual := connDetails["connection_url"].(string) - expected := p.Redacted() - if expected != actual { - t.Fatalf("expected redacted URL %q, actual %q", expected, actual) - } - - if tt.password != "" { - // extra test to ensure that URL.Redacted() is working as expected. - p, err = url.Parse(actual) - if err != nil { - t.Fatal(err) - } - if pp, _ := p.User.Password(); pp == tt.password { - t.Fatalf("password was not redacted by URL.Redacted()") - } - } - }) - } -} - -type hangingPlugin struct{} - -func (h hangingPlugin) Initialize(_ context.Context, req v5.InitializeRequest) (v5.InitializeResponse, error) { - return v5.InitializeResponse{ - Config: req.Config, - }, nil -} - -func (h hangingPlugin) NewUser(_ context.Context, _ v5.NewUserRequest) (v5.NewUserResponse, error) { - return v5.NewUserResponse{}, nil -} - -func (h hangingPlugin) UpdateUser(_ context.Context, _ v5.UpdateUserRequest) (v5.UpdateUserResponse, error) { - return v5.UpdateUserResponse{}, nil -} - -func (h hangingPlugin) DeleteUser(_ context.Context, _ v5.DeleteUserRequest) (v5.DeleteUserResponse, error) { - return v5.DeleteUserResponse{}, nil -} - -func (h hangingPlugin) Type() (string, error) { - return "hanging", nil -} - -func (h hangingPlugin) Close() error { - time.Sleep(1000 * time.Second) - return nil -} - -var _ v5.Database = (*hangingPlugin)(nil) - -func TestBackend_PluginMain_Hanging(t *testing.T) { - if os.Getenv(pluginutil.PluginVaultVersionEnv) == "" { - return - } - v5.Serve(&hangingPlugin{}) -} - -func TestBackend_AsyncClose(t *testing.T) { - // Test that having a plugin that takes a LONG time to close will not cause the cleanup function to take - // longer than 750ms. - cluster, sys := getCluster(t) - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "hanging-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_Hanging", []string{}, "") - t.Cleanup(cluster.Cleanup) - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Configure a connection - data := map[string]interface{}{ - "connection_url": "doesn't matter", - "plugin_name": "hanging-plugin", - "allowed_roles": []string{"plugin-role-test"}, - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/hang", - Storage: config.StorageView, - Data: data, - } - _, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatalf("err: %v", err) - } - timeout := time.NewTimer(750 * time.Millisecond) - done := make(chan bool) - go func() { - b.Cleanup(context.Background()) - // check that clean can be called twice safely - b.Cleanup(context.Background()) - done <- true - }() - select { - case <-timeout.C: - t.Error("Hanging plugin caused Close() to take longer than 750ms") - case <-done: - } -} - -func TestNewDatabaseWrapper_IgnoresBuiltinVersion(t *testing.T) { - cluster, sys := getCluster(t) - t.Cleanup(cluster.Cleanup) - _, err := newDatabaseWrapper(context.Background(), "hana-database-plugin", "v1.0.0+builtin", sys, hclog.Default()) - if err != nil { - t.Fatal(err) - } -} - -func testCredsExist(t *testing.T, resp *logical.Response, connURL string) bool { - t.Helper() - var d struct { - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - log.Printf("[TRACE] Generated credentials: %v", d) - - db, err := sql.Open("pgx", connURL+"&timezone=utc") - if err != nil { - t.Fatal(err) - } - - returnedRows := func() int { - stmt, err := db.Prepare("SELECT DISTINCT schemaname FROM pg_tables WHERE has_table_privilege($1, 'information_schema.role_column_grants', 'select');") - if err != nil { - return -1 - } - defer stmt.Close() - - rows, err := stmt.Query(d.Username) - if err != nil { - return -1 - } - defer rows.Close() - - i := 0 - for rows.Next() { - i++ - } - return i - } - - return returnedRows() == 2 -} - -const testRole = ` -CREATE ROLE "{{name}}" WITH - LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; -` - -const defaultRevocationSQL = ` -REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {{name}}; -REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {{name}}; -REVOKE USAGE ON SCHEMA public FROM {{name}}; - -DROP ROLE IF EXISTS {{name}}; -` diff --git a/builtin/logical/database/credentials_test.go b/builtin/logical/database/credentials_test.go deleted file mode 100644 index 7f2c4eb3d..000000000 --- a/builtin/logical/database/credentials_test.go +++ /dev/null @@ -1,792 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package database - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "testing" - - "github.com/hashicorp/vault/sdk/helper/base62" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -// Test_newClientCertificateGenerator tests the ClientCertificateGenerator struct based on the config -func Test_newClientCertificateGenerator(t *testing.T) { - type args struct { - config map[string]interface{} - } - tests := []struct { - name string - args args - want ClientCertificateGenerator - wantErr bool - }{ - { - name: "newClientCertificateGenerator with nil config", - args: args{ - config: nil, - }, - want: ClientCertificateGenerator{ - CommonNameTemplate: "", - CAPrivateKey: "", - CACert: "", - KeyType: "", - KeyBits: 0, - SignatureBits: 0, - }, - }, - { - name: "newClientCertificateGenerator with zero value key_type", - args: args{ - config: map[string]interface{}{ - "key_type": "", - }, - }, - want: ClientCertificateGenerator{ - KeyType: "", - }, - }, - { - name: "newClientCertificateGenerator with rsa value key_type", - args: args{ - config: map[string]interface{}{ - "key_type": "rsa", - }, - }, - want: ClientCertificateGenerator{ - KeyType: "rsa", - KeyBits: 2048, - SignatureBits: 256, - }, - }, - { - name: "newClientCertificateGenerator with ec value key_type", - args: args{ - config: map[string]interface{}{ - "key_type": "ec", - }, - }, - want: ClientCertificateGenerator{ - KeyType: "ec", - KeyBits: 256, - SignatureBits: 256, - }, - }, - { - name: "newClientCertificateGenerator with ed25519 value key_type", - args: args{ - config: map[string]interface{}{ - "key_type": "ed25519", - }, - }, - want: ClientCertificateGenerator{ - KeyType: "ed25519", - SignatureBits: 256, - }, - }, - { - name: "newClientCertificateGenerator with invalid key_type", - args: args{ - config: map[string]interface{}{ - "key_type": "ece", - }, - }, - wantErr: true, - }, - { - name: "newClientCertificateGenerator with zero value key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "0", - }, - }, - want: ClientCertificateGenerator{ - KeyBits: 0, - }, - }, - { - name: "newClientCertificateGenerator with 2048 value key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "2048", - }, - }, - want: ClientCertificateGenerator{ - KeyBits: 2048, - }, - }, - { - name: "newClientCertificateGenerator with 3072 value key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "3072", - }, - }, - want: ClientCertificateGenerator{ - KeyBits: 3072, - }, - }, - { - name: "newClientCertificateGenerator with 4096 value key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "4096", - }, - }, - want: ClientCertificateGenerator{ - KeyBits: 4096, - }, - }, - { - name: "newClientCertificateGenerator with 224 value key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "224", - }, - }, - want: ClientCertificateGenerator{ - KeyBits: 224, - }, - }, - { - name: "newClientCertificateGenerator with 256 value key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "256", - }, - }, - want: ClientCertificateGenerator{ - KeyBits: 256, - }, - }, - { - name: "newClientCertificateGenerator with 384 value key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "384", - }, - }, - want: ClientCertificateGenerator{ - KeyBits: 384, - }, - }, - { - name: "newClientCertificateGenerator with 521 value key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "521", - }, - }, - want: ClientCertificateGenerator{ - KeyBits: 521, - }, - }, - { - name: "newClientCertificateGenerator with invalid key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "4097", - }, - }, - wantErr: true, - }, - { - name: "newClientCertificateGenerator with zero value signature_bits", - args: args{ - config: map[string]interface{}{ - "signature_bits": "0", - }, - }, - want: ClientCertificateGenerator{ - SignatureBits: 0, - }, - }, - { - name: "newClientCertificateGenerator with 256 value signature_bits", - args: args{ - config: map[string]interface{}{ - "signature_bits": "256", - }, - }, - want: ClientCertificateGenerator{ - SignatureBits: 256, - }, - }, - { - name: "newClientCertificateGenerator with 384 value signature_bits", - args: args{ - config: map[string]interface{}{ - "signature_bits": "384", - }, - }, - want: ClientCertificateGenerator{ - SignatureBits: 384, - }, - }, - { - name: "newClientCertificateGenerator with 512 value signature_bits", - args: args{ - config: map[string]interface{}{ - "signature_bits": "512", - }, - }, - want: ClientCertificateGenerator{ - SignatureBits: 512, - }, - }, - { - name: "newClientCertificateGenerator with invalid signature_bits", - args: args{ - config: map[string]interface{}{ - "signature_bits": "612", - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := newClientCertificateGenerator(tt.args.config) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_newPasswordGenerator(t *testing.T) { - type args struct { - config map[string]interface{} - } - tests := []struct { - name string - args args - want passwordGenerator - wantErr bool - }{ - { - name: "newPasswordGenerator with nil config", - args: args{ - config: nil, - }, - want: passwordGenerator{ - PasswordPolicy: "", - }, - }, - { - name: "newPasswordGenerator without password_policy", - args: args{ - config: map[string]interface{}{}, - }, - want: passwordGenerator{ - PasswordPolicy: "", - }, - }, - { - name: "newPasswordGenerator with password_policy", - args: args{ - config: map[string]interface{}{ - "password_policy": "test-policy", - }, - }, - want: passwordGenerator{ - PasswordPolicy: "test-policy", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := newPasswordGenerator(tt.args.config) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_newRSAKeyGenerator(t *testing.T) { - type args struct { - config map[string]interface{} - } - tests := []struct { - name string - args args - want rsaKeyGenerator - wantErr bool - }{ - { - name: "newRSAKeyGenerator with nil config", - args: args{ - config: nil, - }, - want: rsaKeyGenerator{ - Format: "pkcs8", - KeyBits: 2048, - }, - }, - { - name: "newRSAKeyGenerator with empty config", - args: args{ - config: map[string]interface{}{}, - }, - want: rsaKeyGenerator{ - Format: "pkcs8", - KeyBits: 2048, - }, - }, - { - name: "newRSAKeyGenerator with zero value format", - args: args{ - config: map[string]interface{}{ - "format": "", - }, - }, - want: rsaKeyGenerator{ - Format: "pkcs8", - KeyBits: 2048, - }, - }, - { - name: "newRSAKeyGenerator with zero value key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "0", - }, - }, - want: rsaKeyGenerator{ - Format: "pkcs8", - KeyBits: 2048, - }, - }, - { - name: "newRSAKeyGenerator with format", - args: args{ - config: map[string]interface{}{ - "format": "pkcs8", - }, - }, - want: rsaKeyGenerator{ - Format: "pkcs8", - KeyBits: 2048, - }, - }, - { - name: "newRSAKeyGenerator with format case insensitive", - args: args{ - config: map[string]interface{}{ - "format": "PKCS8", - }, - }, - want: rsaKeyGenerator{ - Format: "PKCS8", - KeyBits: 2048, - }, - }, - { - name: "newRSAKeyGenerator with 3072 key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "3072", - }, - }, - want: rsaKeyGenerator{ - Format: "pkcs8", - KeyBits: 3072, - }, - }, - { - name: "newRSAKeyGenerator with 4096 key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "4096", - }, - }, - want: rsaKeyGenerator{ - Format: "pkcs8", - KeyBits: 4096, - }, - }, - { - name: "newRSAKeyGenerator with invalid key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "4097", - }, - }, - wantErr: true, - }, - { - name: "newRSAKeyGenerator with invalid format", - args: args{ - config: map[string]interface{}{ - "format": "pkcs1", - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := newRSAKeyGenerator(tt.args.config) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_passwordGenerator_generate(t *testing.T) { - config := logical.TestBackendConfig() - b := Backend(config) - b.Setup(context.Background(), config) - - type args struct { - config map[string]interface{} - mock func() interface{} - passGen logical.PasswordGenerator - } - tests := []struct { - name string - args args - wantRegexp string - wantErr bool - }{ - { - name: "wrapper missing v4 and v5 interface", - args: args{ - mock: func() interface{} { - return nil - }, - }, - wantErr: true, - }, - { - name: "v4: generate password using GenerateCredentials", - args: args{ - mock: func() interface{} { - v4Mock := new(mockLegacyDatabase) - v4Mock.On("GenerateCredentials", mock.Anything). - Return("v4-generated-password", nil). - Times(1) - return v4Mock - }, - }, - wantRegexp: "^v4-generated-password$", - }, - { - name: "v5: generate password without policy", - args: args{ - mock: func() interface{} { - return new(mockNewDatabase) - }, - }, - wantRegexp: "^[a-zA-Z0-9-]{20}$", - }, - { - name: "v5: generate password with non-existing policy", - args: args{ - config: map[string]interface{}{ - "password_policy": "not-created", - }, - mock: func() interface{} { - return new(mockNewDatabase) - }, - }, - wantErr: true, - }, - { - name: "v5: generate password with existing policy", - args: args{ - config: map[string]interface{}{ - "password_policy": "test-policy", - }, - mock: func() interface{} { - return new(mockNewDatabase) - }, - passGen: func() (string, error) { - return base62.Random(30) - }, - }, - wantRegexp: "^[a-zA-Z0-9]{30}$", - }, - { - name: "v5: generate password with existing policy static", - args: args{ - config: map[string]interface{}{ - "password_policy": "test-policy", - }, - mock: func() interface{} { - return new(mockNewDatabase) - }, - passGen: func() (string, error) { - return "policy-generated-password", nil - }, - }, - wantRegexp: "^policy-generated-password$", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Set up the version wrapper with a mock database implementation - wrapper := databaseVersionWrapper{} - switch m := tt.args.mock().(type) { - case *mockNewDatabase: - wrapper.v5 = m - case *mockLegacyDatabase: - wrapper.v4 = m - } - - // Set the password policy for the test case - config.System.(*logical.StaticSystemView).SetPasswordPolicy( - "test-policy", tt.args.passGen) - - // Generate the password - pg, err := newPasswordGenerator(tt.args.config) - got, err := pg.generate(context.Background(), b, wrapper) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.Regexp(t, tt.wantRegexp, got) - - // Assert all expected calls took place on the mock - if m, ok := wrapper.v5.(*mockNewDatabase); ok { - m.AssertExpectations(t) - } - if m, ok := wrapper.v4.(*mockLegacyDatabase); ok { - m.AssertExpectations(t) - } - }) - } -} - -func Test_passwordGenerator_configMap(t *testing.T) { - type args struct { - config map[string]interface{} - } - tests := []struct { - name string - args args - want map[string]interface{} - }{ - { - name: "nil config results in empty map", - args: args{ - config: nil, - }, - want: map[string]interface{}{}, - }, - { - name: "empty config results in empty map", - args: args{ - config: map[string]interface{}{}, - }, - want: map[string]interface{}{}, - }, - { - name: "input config is equal to output config", - args: args{ - config: map[string]interface{}{ - "password_policy": "test-policy", - }, - }, - want: map[string]interface{}{ - "password_policy": "test-policy", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - pg, err := newPasswordGenerator(tt.args.config) - assert.NoError(t, err) - cm, err := pg.configMap() - assert.NoError(t, err) - assert.Equal(t, tt.want, cm) - }) - } -} - -func Test_rsaKeyGenerator_generate(t *testing.T) { - type args struct { - config map[string]interface{} - } - tests := []struct { - name string - args args - }{ - { - name: "generate RSA key with nil default config", - args: args{ - config: nil, - }, - }, - { - name: "generate RSA key with empty default config", - args: args{ - config: map[string]interface{}{}, - }, - }, - { - name: "generate RSA key with 2048 key_bits and format", - args: args{ - config: map[string]interface{}{ - "key_bits": "2048", - "format": "pkcs8", - }, - }, - }, - { - name: "generate RSA key with 2048 key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "2048", - }, - }, - }, - { - name: "generate RSA key with 3072 key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "3072", - }, - }, - }, - { - name: "generate RSA key with 4096 key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": "4096", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Generate the RSA key pair - kg, err := newRSAKeyGenerator(tt.args.config) - public, private, err := kg.generate(rand.Reader) - assert.NoError(t, err) - assert.NotEmpty(t, public) - assert.NotEmpty(t, private) - - // Decode the public and private key PEMs - pubBlock, pubRest := pem.Decode(public) - privBlock, privRest := pem.Decode(private) - assert.NotNil(t, pubBlock) - assert.Empty(t, pubRest) - assert.Equal(t, "PUBLIC KEY", pubBlock.Type) - assert.NotNil(t, privBlock) - assert.Empty(t, privRest) - assert.Equal(t, "PRIVATE KEY", privBlock.Type) - - // Assert that we can parse the public key PEM block - pub, err := x509.ParsePKIXPublicKey(pubBlock.Bytes) - assert.NoError(t, err) - assert.NotNil(t, pub) - assert.IsType(t, &rsa.PublicKey{}, pub) - - // Assert that we can parse the private key PEM block in - // the configured format - switch kg.Format { - case "pkcs8": - priv, err := x509.ParsePKCS8PrivateKey(privBlock.Bytes) - assert.NoError(t, err) - assert.NotNil(t, priv) - assert.IsType(t, &rsa.PrivateKey{}, priv) - default: - t.Fatal("unknown format") - } - }) - } -} - -func Test_rsaKeyGenerator_configMap(t *testing.T) { - type args struct { - config map[string]interface{} - } - tests := []struct { - name string - args args - want map[string]interface{} - }{ - { - name: "nil config results in defaults", - args: args{ - config: nil, - }, - want: map[string]interface{}{ - "format": "pkcs8", - "key_bits": 2048, - }, - }, - { - name: "empty config results in defaults", - args: args{ - config: map[string]interface{}{}, - }, - want: map[string]interface{}{ - "format": "pkcs8", - "key_bits": 2048, - }, - }, - { - name: "config with format", - args: args{ - config: map[string]interface{}{ - "format": "pkcs8", - }, - }, - want: map[string]interface{}{ - "format": "pkcs8", - "key_bits": 2048, - }, - }, - { - name: "config with key_bits", - args: args{ - config: map[string]interface{}{ - "key_bits": 4096, - }, - }, - want: map[string]interface{}{ - "format": "pkcs8", - "key_bits": 4096, - }, - }, - { - name: "config with format and key_bits", - args: args{ - config: map[string]interface{}{ - "format": "pkcs8", - "key_bits": 3072, - }, - }, - want: map[string]interface{}{ - "format": "pkcs8", - "key_bits": 3072, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - kg, err := newRSAKeyGenerator(tt.args.config) - assert.NoError(t, err) - cm, err := kg.configMap() - assert.NoError(t, err) - assert.Equal(t, tt.want, cm) - }) - } -} diff --git a/builtin/logical/database/dbplugin/plugin_test.go b/builtin/logical/database/dbplugin/plugin_test.go deleted file mode 100644 index 43f4e2d14..000000000 --- a/builtin/logical/database/dbplugin/plugin_test.go +++ /dev/null @@ -1,278 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package dbplugin_test - -import ( - "context" - "errors" - "os" - "testing" - "time" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/namespace" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/database/dbplugin" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/pluginutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -type mockPlugin struct { - users map[string][]string -} - -var _ dbplugin.Database = &mockPlugin{} - -func (m *mockPlugin) Type() (string, error) { return "mock", nil } -func (m *mockPlugin) CreateUser(_ context.Context, statements dbplugin.Statements, usernameConf dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { - err = errors.New("err") - if usernameConf.DisplayName == "" || expiration.IsZero() { - return "", "", err - } - - if _, ok := m.users[usernameConf.DisplayName]; ok { - return "", "", err - } - - m.users[usernameConf.DisplayName] = []string{password} - - return usernameConf.DisplayName, "test", nil -} - -func (m *mockPlugin) RenewUser(_ context.Context, statements dbplugin.Statements, username string, expiration time.Time) error { - err := errors.New("err") - if username == "" || expiration.IsZero() { - return err - } - - if _, ok := m.users[username]; !ok { - return err - } - - return nil -} - -func (m *mockPlugin) RevokeUser(_ context.Context, statements dbplugin.Statements, username string) error { - err := errors.New("err") - if username == "" { - return err - } - - if _, ok := m.users[username]; !ok { - return err - } - - delete(m.users, username) - return nil -} - -func (m *mockPlugin) RotateRootCredentials(_ context.Context, statements []string) (map[string]interface{}, error) { - return nil, nil -} - -func (m *mockPlugin) Init(_ context.Context, conf map[string]interface{}, _ bool) (map[string]interface{}, error) { - err := errors.New("err") - if len(conf) != 1 { - return nil, err - } - - return conf, nil -} - -func (m *mockPlugin) Initialize(_ context.Context, conf map[string]interface{}, _ bool) error { - err := errors.New("err") - if len(conf) != 1 { - return err - } - - return nil -} - -func (m *mockPlugin) Close() error { - m.users = nil - return nil -} - -func (m *mockPlugin) GenerateCredentials(ctx context.Context) (password string, err error) { - return password, err -} - -func (m *mockPlugin) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticConfig dbplugin.StaticUserConfig) (username string, password string, err error) { - return username, password, err -} - -func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - cores := cluster.Cores - - sys := vault.TestDynamicSystemView(cores[0].Core, nil) - vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin", consts.PluginTypeDatabase, "", "TestPlugin_GRPC_Main", []string{}, "") - - return cluster, sys -} - -// This is not an actual test case, it's a helper function that will be executed -// by the go-plugin client via an exec call. -func TestPlugin_GRPC_Main(t *testing.T) { - if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" && os.Getenv(pluginutil.PluginMetadataModeEnv) != "true" { - return - } - - plugin := &mockPlugin{ - users: make(map[string][]string), - } - - args := []string{"--tls-skip-verify=true"} - - apiClientMeta := &api.PluginAPIClientMeta{} - flags := apiClientMeta.FlagSet() - flags.Parse(args) - - dbplugin.Serve(plugin, api.VaultPluginTLSProvider(apiClientMeta.GetTLSConfig())) -} - -func TestPlugin_Init(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - dbRaw, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger()) - if err != nil { - t.Fatalf("err: %s", err) - } - - connectionDetails := map[string]interface{}{ - "test": 1, - } - - _, err = dbRaw.Init(context.Background(), connectionDetails, true) - if err != nil { - t.Fatalf("err: %s", err) - } - - err = dbRaw.Close() - if err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestPlugin_CreateUser(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - db, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger()) - if err != nil { - t.Fatalf("err: %s", err) - } - defer db.Close() - - connectionDetails := map[string]interface{}{ - "test": 1, - } - - _, err = db.Init(context.Background(), connectionDetails, true) - if err != nil { - t.Fatalf("err: %s", err) - } - - usernameConf := dbplugin.UsernameConfig{ - DisplayName: "test", - RoleName: "test", - } - - us, pw, err := db.CreateUser(context.Background(), dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) - if err != nil { - t.Fatalf("err: %s", err) - } - if us != "test" || pw != "test" { - t.Fatal("expected username and password to be 'test'") - } - - // try and save the same user again to verify it saved the first time, this - // should return an error - _, _, err = db.CreateUser(context.Background(), dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) - if err == nil { - t.Fatal("expected an error, user wasn't created correctly") - } -} - -func TestPlugin_RenewUser(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - db, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger()) - if err != nil { - t.Fatalf("err: %s", err) - } - defer db.Close() - - connectionDetails := map[string]interface{}{ - "test": 1, - } - _, err = db.Init(context.Background(), connectionDetails, true) - if err != nil { - t.Fatalf("err: %s", err) - } - - usernameConf := dbplugin.UsernameConfig{ - DisplayName: "test", - RoleName: "test", - } - - us, _, err := db.CreateUser(context.Background(), dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) - if err != nil { - t.Fatalf("err: %s", err) - } - - err = db.RenewUser(context.Background(), dbplugin.Statements{}, us, time.Now().Add(time.Minute)) - if err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestPlugin_RevokeUser(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - db, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger()) - if err != nil { - t.Fatalf("err: %s", err) - } - defer db.Close() - - connectionDetails := map[string]interface{}{ - "test": 1, - } - _, err = db.Init(context.Background(), connectionDetails, true) - if err != nil { - t.Fatalf("err: %s", err) - } - - usernameConf := dbplugin.UsernameConfig{ - DisplayName: "test", - RoleName: "test", - } - - us, _, err := db.CreateUser(context.Background(), dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Test default revoke statements - err = db.RevokeUser(context.Background(), dbplugin.Statements{}, us) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Try adding the same username back so we can verify it was removed - _, _, err = db.CreateUser(context.Background(), dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) - if err != nil { - t.Fatalf("err: %s", err) - } -} diff --git a/builtin/logical/database/mocks_test.go b/builtin/logical/database/mocks_test.go deleted file mode 100644 index 4182affd2..000000000 --- a/builtin/logical/database/mocks_test.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package database - -import ( - "context" - "time" - - v4 "github.com/hashicorp/vault/sdk/database/dbplugin" - v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/stretchr/testify/mock" -) - -var _ v5.Database = &mockNewDatabase{} - -type mockNewDatabase struct { - mock.Mock -} - -func (m *mockNewDatabase) Initialize(ctx context.Context, req v5.InitializeRequest) (v5.InitializeResponse, error) { - args := m.Called(ctx, req) - return args.Get(0).(v5.InitializeResponse), args.Error(1) -} - -func (m *mockNewDatabase) NewUser(ctx context.Context, req v5.NewUserRequest) (v5.NewUserResponse, error) { - args := m.Called(ctx, req) - return args.Get(0).(v5.NewUserResponse), args.Error(1) -} - -func (m *mockNewDatabase) UpdateUser(ctx context.Context, req v5.UpdateUserRequest) (v5.UpdateUserResponse, error) { - args := m.Called(ctx, req) - return args.Get(0).(v5.UpdateUserResponse), args.Error(1) -} - -func (m *mockNewDatabase) DeleteUser(ctx context.Context, req v5.DeleteUserRequest) (v5.DeleteUserResponse, error) { - args := m.Called(ctx, req) - return args.Get(0).(v5.DeleteUserResponse), args.Error(1) -} - -func (m *mockNewDatabase) Type() (string, error) { - args := m.Called() - return args.String(0), args.Error(1) -} - -func (m *mockNewDatabase) Close() error { - args := m.Called() - return args.Error(0) -} - -var _ v4.Database = &mockLegacyDatabase{} - -type mockLegacyDatabase struct { - mock.Mock -} - -func (m *mockLegacyDatabase) CreateUser(ctx context.Context, statements v4.Statements, usernameConfig v4.UsernameConfig, expiration time.Time) (username string, password string, err error) { - args := m.Called(ctx, statements, usernameConfig, expiration) - return args.String(0), args.String(1), args.Error(2) -} - -func (m *mockLegacyDatabase) RenewUser(ctx context.Context, statements v4.Statements, username string, expiration time.Time) error { - args := m.Called(ctx, statements, username, expiration) - return args.Error(0) -} - -func (m *mockLegacyDatabase) RevokeUser(ctx context.Context, statements v4.Statements, username string) error { - args := m.Called(ctx, statements, username) - return args.Error(0) -} - -func (m *mockLegacyDatabase) RotateRootCredentials(ctx context.Context, statements []string) (config map[string]interface{}, err error) { - args := m.Called(ctx, statements) - return args.Get(0).(map[string]interface{}), args.Error(1) -} - -func (m *mockLegacyDatabase) GenerateCredentials(ctx context.Context) (string, error) { - args := m.Called(ctx) - return args.String(0), args.Error(1) -} - -func (m *mockLegacyDatabase) SetCredentials(ctx context.Context, statements v4.Statements, staticConfig v4.StaticUserConfig) (username string, password string, err error) { - args := m.Called(ctx, statements, staticConfig) - return args.String(0), args.String(1), args.Error(2) -} - -func (m *mockLegacyDatabase) Init(ctx context.Context, config map[string]interface{}, verifyConnection bool) (saveConfig map[string]interface{}, err error) { - args := m.Called(ctx, config, verifyConnection) - return args.Get(0).(map[string]interface{}), args.Error(1) -} - -func (m *mockLegacyDatabase) Type() (string, error) { - args := m.Called() - return args.String(0), args.Error(1) -} - -func (m *mockLegacyDatabase) Close() error { - args := m.Called() - return args.Error(0) -} - -func (m *mockLegacyDatabase) Initialize(ctx context.Context, config map[string]interface{}, verifyConnection bool) (err error) { - panic("Initialize should not be called") -} diff --git a/builtin/logical/database/path_config_connection_test.go b/builtin/logical/database/path_config_connection_test.go deleted file mode 100644 index bfdccd7f3..000000000 --- a/builtin/logical/database/path_config_connection_test.go +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package database - -import ( - "context" - "strings" - "testing" - - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/helper/versions" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestWriteConfig_PluginVersionInStorage(t *testing.T) { - cluster, sys := getCluster(t) - t.Cleanup(cluster.Cleanup) - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(context.Background()) - - const hdb = "hana-database-plugin" - hdbBuiltin := versions.GetBuiltinVersion(consts.PluginTypeDatabase, hdb) - - // Configure a connection - writePluginVersion := func() { - t.Helper() - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: map[string]interface{}{ - "connection_url": "test", - "plugin_name": hdb, - "plugin_version": hdbBuiltin, - "verify_connection": false, - }, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - } - writePluginVersion() - - getPluginVersionFromAPI := func() string { - t.Helper() - req := &logical.Request{ - Operation: logical.ReadOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - } - - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - return resp.Data["plugin_version"].(string) - } - pluginVersion := getPluginVersionFromAPI() - if pluginVersion != "" { - t.Fatalf("expected plugin_version empty but got %s", pluginVersion) - } - - // Directly store config to get the builtin plugin version into storage, - // simulating a write that happened before upgrading to 1.12.2+ - err = storeConfig(context.Background(), config.StorageView, "plugin-test", &DatabaseConfig{ - PluginName: hdb, - PluginVersion: hdbBuiltin, - }) - if err != nil { - t.Fatal(err) - } - - // Now replay the read request, and we still shouldn't get the builtin version back. - pluginVersion = getPluginVersionFromAPI() - if pluginVersion != "" { - t.Fatalf("expected plugin_version empty but got %s", pluginVersion) - } - - // Check the underlying data, which should still have the version in storage. - getPluginVersionFromStorage := func() string { - t.Helper() - entry, err := config.StorageView.Get(context.Background(), "config/plugin-test") - if err != nil { - t.Fatal(err) - } - if entry == nil { - t.Fatal() - } - - var config DatabaseConfig - if err := entry.DecodeJSON(&config); err != nil { - t.Fatal(err) - } - return config.PluginVersion - } - - storagePluginVersion := getPluginVersionFromStorage() - if storagePluginVersion != hdbBuiltin { - t.Fatalf("Expected %s, got: %s", hdbBuiltin, storagePluginVersion) - } - - // Trigger a write to storage, which should clean up plugin version in the storage entry. - writePluginVersion() - - storagePluginVersion = getPluginVersionFromStorage() - if storagePluginVersion != "" { - t.Fatalf("Expected empty, got: %s", storagePluginVersion) - } - - // Finally, confirm API requests still return empty plugin version too - pluginVersion = getPluginVersionFromAPI() - if pluginVersion != "" { - t.Fatalf("expected plugin_version empty but got %s", pluginVersion) - } -} - -func TestWriteConfig_HelpfulErrorMessageWhenBuiltinOverridden(t *testing.T) { - cluster, sys := getCluster(t) - t.Cleanup(cluster.Cleanup) - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(context.Background()) - - const pg = "postgresql-database-plugin" - pgBuiltin := versions.GetBuiltinVersion(consts.PluginTypeDatabase, pg) - - // Configure a connection - data := map[string]interface{}{ - "connection_url": "test", - "plugin_name": pg, - "plugin_version": pgBuiltin, - "verify_connection": false, - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("resp:%#v", resp) - } - if !strings.Contains(resp.Error().Error(), "overridden by an unversioned plugin") { - t.Fatalf("expected overridden error but got: %s", resp.Error()) - } -} diff --git a/builtin/logical/database/path_roles_test.go b/builtin/logical/database/path_roles_test.go deleted file mode 100644 index f012df753..000000000 --- a/builtin/logical/database/path_roles_test.go +++ /dev/null @@ -1,863 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package database - -import ( - "context" - "encoding/json" - "errors" - "strings" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/helper/namespace" - postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql" - v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestBackend_Roles_CredentialTypes(t *testing.T) { - config := logical.TestBackendConfig() - config.System = logical.TestSystemView() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - type args struct { - credentialType v5.CredentialType - credentialConfig map[string]string - } - tests := []struct { - name string - args args - wantErr bool - expectedResp map[string]interface{} - }{ - { - name: "role with invalid credential type", - args: args{ - credentialType: v5.CredentialType(10), - }, - wantErr: true, - }, - { - name: "role with invalid credential type and valid credential config", - args: args{ - credentialType: v5.CredentialType(7), - credentialConfig: map[string]string{ - "password_policy": "test-policy", - }, - }, - wantErr: true, - }, - { - name: "role with password credential type", - args: args{ - credentialType: v5.CredentialTypePassword, - }, - expectedResp: map[string]interface{}{ - "credential_type": v5.CredentialTypePassword.String(), - "credential_config": nil, - }, - }, - { - name: "role with password credential type and configuration", - args: args{ - credentialType: v5.CredentialTypePassword, - credentialConfig: map[string]string{ - "password_policy": "test-policy", - }, - }, - expectedResp: map[string]interface{}{ - "credential_type": v5.CredentialTypePassword.String(), - "credential_config": map[string]interface{}{ - "password_policy": "test-policy", - }, - }, - }, - { - name: "role with rsa_private_key credential type and default configuration", - args: args{ - credentialType: v5.CredentialTypeRSAPrivateKey, - }, - expectedResp: map[string]interface{}{ - "credential_type": v5.CredentialTypeRSAPrivateKey.String(), - "credential_config": map[string]interface{}{ - "key_bits": json.Number("2048"), - "format": "pkcs8", - }, - }, - }, - { - name: "role with rsa_private_key credential type and 2048 bit configuration", - args: args{ - credentialType: v5.CredentialTypeRSAPrivateKey, - credentialConfig: map[string]string{ - "key_bits": "2048", - }, - }, - expectedResp: map[string]interface{}{ - "credential_type": v5.CredentialTypeRSAPrivateKey.String(), - "credential_config": map[string]interface{}{ - "key_bits": json.Number("2048"), - "format": "pkcs8", - }, - }, - }, - { - name: "role with rsa_private_key credential type and 3072 bit configuration", - args: args{ - credentialType: v5.CredentialTypeRSAPrivateKey, - credentialConfig: map[string]string{ - "key_bits": "3072", - }, - }, - expectedResp: map[string]interface{}{ - "credential_type": v5.CredentialTypeRSAPrivateKey.String(), - "credential_config": map[string]interface{}{ - "key_bits": json.Number("3072"), - "format": "pkcs8", - }, - }, - }, - { - name: "role with rsa_private_key credential type and 4096 bit configuration", - args: args{ - credentialType: v5.CredentialTypeRSAPrivateKey, - credentialConfig: map[string]string{ - "key_bits": "4096", - }, - }, - expectedResp: map[string]interface{}{ - "credential_type": v5.CredentialTypeRSAPrivateKey.String(), - "credential_config": map[string]interface{}{ - "key_bits": json.Number("4096"), - "format": "pkcs8", - }, - }, - }, - { - name: "role with rsa_private_key credential type invalid key_bits configuration", - args: args{ - credentialType: v5.CredentialTypeRSAPrivateKey, - credentialConfig: map[string]string{ - "key_bits": "256", - }, - }, - wantErr: true, - }, - { - name: "role with rsa_private_key credential type invalid format configuration", - args: args{ - credentialType: v5.CredentialTypeRSAPrivateKey, - credentialConfig: map[string]string{ - "format": "pkcs1", - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := &logical.Request{ - Operation: logical.CreateOperation, - Path: "roles/test", - Storage: config.StorageView, - Data: map[string]interface{}{ - "db_name": "test-database", - "creation_statements": "CREATE USER {{name}}", - "credential_type": tt.args.credentialType.String(), - "credential_config": tt.args.credentialConfig, - }, - } - - // Create the role - resp, err := b.HandleRequest(context.Background(), req) - if tt.wantErr { - assert.True(t, resp.IsError(), "expected error") - return - } - assert.False(t, resp.IsError()) - assert.Nil(t, err) - - // Read the role - req.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), req) - assert.False(t, resp.IsError()) - assert.Nil(t, err) - for k, v := range tt.expectedResp { - assert.Equal(t, v, resp.Data[k]) - } - - // Delete the role - req.Operation = logical.DeleteOperation - resp, err = b.HandleRequest(context.Background(), req) - assert.False(t, resp.IsError()) - assert.Nil(t, err) - }) - } -} - -func TestBackend_StaticRole_Config(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - // create the database user - createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate) - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"plugin-role-test"}, - "name": "plugin-test", - } - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Test static role creation scenarios. Uses a map, so there is no guaranteed - // ordering, so each case cleans up by deleting the role - testCases := map[string]struct { - account map[string]interface{} - path string - expected map[string]interface{} - err error - }{ - "basic": { - account: map[string]interface{}{ - "username": dbUser, - "rotation_period": "5400s", - }, - path: "plugin-role-test", - expected: map[string]interface{}{ - "username": dbUser, - "rotation_period": float64(5400), - }, - }, - "missing rotation period": { - account: map[string]interface{}{ - "username": dbUser, - }, - path: "plugin-role-test", - err: errors.New("rotation_period is required to create static accounts"), - }, - "disallowed role config": { - account: map[string]interface{}{ - "username": dbUser, - "rotation_period": "5400s", - }, - path: "disallowed-role", - err: errors.New("\"disallowed-role\" is not an allowed role"), - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - data := map[string]interface{}{ - "name": "plugin-role-test", - "db_name": "plugin-test", - "rotation_statements": testRoleStaticUpdate, - } - - for k, v := range tc.account { - data[k] = v - } - - path := "static-roles/" + tc.path - - req := &logical.Request{ - Operation: logical.CreateOperation, - Path: path, - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - if tc.err == nil { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - if err != nil && tc.err.Error() == err.Error() { - // errors match - return - } - if err == nil && tc.err.Error() == resp.Error().Error() { - // errors match - return - } - t.Fatalf("expected err message: (%s), got (%s), response error: (%s)", tc.err, err, resp.Error()) - } - - if tc.err != nil { - if err == nil || (resp == nil || !resp.IsError()) { - t.Fatal("expected error, got none") - } - } - - // Read the role - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - expected := tc.expected - actual := make(map[string]interface{}) - dataKeys := []string{"username", "password", "last_vault_rotation", "rotation_period"} - for _, key := range dataKeys { - if v, ok := resp.Data[key]; ok { - actual[key] = v - } - } - - if len(tc.expected) > 0 { - // verify a password is returned, but we don't care what it's value is - if actual["password"] == "" { - t.Fatalf("expected result to contain password, but none found") - } - if v, ok := actual["last_vault_rotation"].(time.Time); !ok { - t.Fatalf("expected last_vault_rotation to be set to time.Time type, got: %#v", v) - } - - // delete these values before the comparison, since we can't know them in - // advance - delete(actual, "password") - delete(actual, "last_vault_rotation") - if diff := deep.Equal(expected, actual); diff != nil { - t.Fatal(diff) - } - } - - if len(tc.expected) == 0 && resp.Data["static_account"] != nil { - t.Fatalf("got unexpected static_account info: %#v", actual) - } - - if diff := deep.Equal(resp.Data["db_name"], "plugin-test"); diff != nil { - t.Fatal(diff) - } - - // Delete role for next run - req = &logical.Request{ - Operation: logical.DeleteOperation, - Path: "static-roles/plugin-role-test", - Storage: config.StorageView, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - }) - } -} - -func TestBackend_StaticRole_Updates(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - // create the database user - createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate) - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "name": "plugin-test", - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - data = map[string]interface{}{ - "name": "plugin-role-test-updates", - "db_name": "plugin-test", - "rotation_statements": testRoleStaticUpdate, - "username": dbUser, - "rotation_period": "5400s", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/plugin-role-test-updates", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read the role - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-roles/plugin-role-test-updates", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - rotation := resp.Data["rotation_period"].(float64) - - // capture the password to verify it doesn't change - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-creds/plugin-role-test-updates", - Storage: config.StorageView, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - username := resp.Data["username"].(string) - password := resp.Data["password"].(string) - if username == "" || password == "" { - t.Fatalf("expected both username/password, got (%s), (%s)", username, password) - } - - // update rotation_period - updateData := map[string]interface{}{ - "name": "plugin-role-test-updates", - "db_name": "plugin-test", - "username": dbUser, - "rotation_period": "6400s", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "static-roles/plugin-role-test-updates", - Storage: config.StorageView, - Data: updateData, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // re-read the role - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-roles/plugin-role-test-updates", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - newRotation := resp.Data["rotation_period"].(float64) - if newRotation == rotation { - t.Fatalf("expected change in rotation, but got old value: %#v", newRotation) - } - - // re-capture the password to ensure it did not change - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-creds/plugin-role-test-updates", - Storage: config.StorageView, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - if username != resp.Data["username"].(string) { - t.Fatalf("usernames dont match!: (%s) / (%s)", username, resp.Data["username"].(string)) - } - if password != resp.Data["password"].(string) { - t.Fatalf("passwords dont match!: (%s) / (%s)", password, resp.Data["password"].(string)) - } - - // verify that rotation_period is only required when creating - updateData = map[string]interface{}{ - "name": "plugin-role-test-updates", - "db_name": "plugin-test", - "username": dbUser, - "rotation_statements": testRoleStaticUpdateRotation, - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "static-roles/plugin-role-test-updates", - Storage: config.StorageView, - Data: updateData, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // verify updating static username returns an error - updateData = map[string]interface{}{ - "name": "plugin-role-test-updates", - "db_name": "plugin-test", - "username": "statictestmodified", - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "static-roles/plugin-role-test-updates", - Storage: config.StorageView, - Data: updateData, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || !resp.IsError() { - t.Fatal("expected error on updating name") - } - err = resp.Error() - if err.Error() != "cannot update static account username" { - t.Fatalf("expected error on updating name, got: %s", err) - } -} - -func TestBackend_StaticRole_Role_name_check(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - // create the database user - createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate) - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "name": "plugin-test", - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // non-static role - data = map[string]interface{}{ - "name": "plugin-role-test", - "db_name": "plugin-test", - "creation_statements": testRoleStaticCreate, - "rotation_statements": testRoleStaticUpdate, - "revocation_statements": defaultRevocationSQL, - "default_ttl": "5m", - "max_ttl": "10m", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // create a static role with the same name, and expect failure - // static role - data = map[string]interface{}{ - "name": "plugin-role-test", - "db_name": "plugin-test", - "creation_statements": testRoleStaticCreate, - "rotation_statements": testRoleStaticUpdate, - "revocation_statements": defaultRevocationSQL, - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected error, got none") - } - - // repeat, with a static role first - data = map[string]interface{}{ - "name": "plugin-role-test-2", - "db_name": "plugin-test", - "rotation_statements": testRoleStaticUpdate, - "username": dbUser, - "rotation_period": "1h", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/plugin-role-test-2", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // create a non-static role with the same name, and expect failure - data = map[string]interface{}{ - "name": "plugin-role-test-2", - "db_name": "plugin-test", - "creation_statements": testRoleStaticCreate, - "revocation_statements": defaultRevocationSQL, - "default_ttl": "5m", - "max_ttl": "10m", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "roles/plugin-role-test-2", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatal(err) - } - if resp == nil || !resp.IsError() { - t.Fatalf("expected error, got none") - } -} - -func TestWALsStillTrackedAfterUpdate(t *testing.T) { - ctx := context.Background() - b, storage, mockDB := getBackend(t) - defer b.Cleanup(ctx) - configureDBMount(t, storage) - - createRole(t, b, storage, mockDB, "hashicorp") - - generateWALFromFailedRotation(t, b, storage, mockDB, "hashicorp") - requireWALs(t, storage, 1) - - resp, err := b.HandleRequest(ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "static-roles/hashicorp", - Storage: storage, - Data: map[string]interface{}{ - "username": "hashicorp", - "rotation_period": "600s", - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatal(resp, err) - } - walIDs := requireWALs(t, storage, 1) - - // Now when we trigger a manual rotate, it should use the WAL's new password - // which will tell us that the in-memory structure still kept track of the - // WAL in addition to it still being in storage. - wal, err := b.findStaticWAL(ctx, storage, walIDs[0]) - if err != nil { - t.Fatal(err) - } - rotateRole(t, b, storage, mockDB, "hashicorp") - role, err := b.StaticRole(ctx, storage, "hashicorp") - if err != nil { - t.Fatal(err) - } - if role.StaticAccount.Password != wal.NewPassword { - t.Fatal() - } - requireWALs(t, storage, 0) -} - -func TestWALsDeletedOnRoleCreationFailed(t *testing.T) { - ctx := context.Background() - b, storage, mockDB := getBackend(t) - defer b.Cleanup(ctx) - configureDBMount(t, storage) - - for i := 0; i < 3; i++ { - mockDB.On("UpdateUser", mock.Anything, mock.Anything). - Return(v5.UpdateUserResponse{}, errors.New("forced error")). - Once() - resp, err := b.HandleRequest(ctx, &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/hashicorp", - Storage: storage, - Data: map[string]interface{}{ - "username": "hashicorp", - "db_name": "mockv5", - "rotation_period": "5s", - }, - }) - if err == nil { - t.Fatal("expected error from DB") - } - if !strings.Contains(err.Error(), "forced error") { - t.Fatal("expected forced error message", resp, err) - } - } - - requireWALs(t, storage, 0) -} - -func TestWALsDeletedOnRoleDeletion(t *testing.T) { - ctx := context.Background() - b, storage, mockDB := getBackend(t) - defer b.Cleanup(ctx) - configureDBMount(t, storage) - - // Create the roles - roleNames := []string{"hashicorp", "2"} - for _, roleName := range roleNames { - createRole(t, b, storage, mockDB, roleName) - } - - // Fail to rotate the roles - for _, roleName := range roleNames { - generateWALFromFailedRotation(t, b, storage, mockDB, roleName) - } - - // Should have 2 WALs hanging around - requireWALs(t, storage, 2) - - // Delete one of the static roles - resp, err := b.HandleRequest(ctx, &logical.Request{ - Operation: logical.DeleteOperation, - Path: "static-roles/hashicorp", - Storage: storage, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatal(resp, err) - } - - // 1 WAL should be cleared by the delete - requireWALs(t, storage, 1) -} - -func createRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) { - t.Helper() - mockDB.On("UpdateUser", mock.Anything, mock.Anything). - Return(v5.UpdateUserResponse{}, nil). - Once() - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/" + roleName, - Storage: storage, - Data: map[string]interface{}{ - "username": roleName, - "db_name": "mockv5", - "rotation_period": "86400s", - }, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatal(resp, err) - } -} - -const testRoleStaticCreate = ` -CREATE ROLE "{{name}}" WITH - LOGIN - PASSWORD '{{password}}'; -` - -const testRoleStaticUpdate = ` -ALTER USER "{{name}}" WITH PASSWORD '{{password}}'; -` - -const testRoleStaticUpdateRotation = ` -ALTER USER "{{name}}" WITH PASSWORD '{{password}}';GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; -` diff --git a/builtin/logical/database/rollback_test.go b/builtin/logical/database/rollback_test.go deleted file mode 100644 index 0d80c2a9c..000000000 --- a/builtin/logical/database/rollback_test.go +++ /dev/null @@ -1,425 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package database - -import ( - "context" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/helper/namespace" - postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql" - v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -const ( - databaseUser = "postgres" - defaultPassword = "secret" -) - -// Tests that the WAL rollback function rolls back the database password. -// The database password should be rolled back when: -// - A WAL entry exists -// - Password has been altered on the database -// - Password has not been updated in storage -func TestBackend_RotateRootCredentials_WAL_rollback(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - dbBackend, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer lb.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - connURL = strings.ReplaceAll(connURL, "postgres:secret", "{{username}}:{{password}}") - - // Configure a connection to the database - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": []string{"plugin-role-test"}, - "username": databaseUser, - "password": defaultPassword, - } - resp, err := lb.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Create a role - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "max_ttl": "10m", - } - resp, err = lb.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read credentials to verify this initially works - credReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: make(map[string]interface{}), - } - credResp, err := lb.HandleRequest(context.Background(), credReq) - if err != nil || (credResp != nil && credResp.IsError()) { - t.Fatalf("err:%s resp:%v\n", err, credResp) - } - - // Get a connection to the database plugin - dbi, err := dbBackend.GetConnection(context.Background(), - config.StorageView, "plugin-test") - if err != nil { - t.Fatal(err) - } - - // Alter the database password so it no longer matches what is in storage - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - updateReq := v5.UpdateUserRequest{ - Username: databaseUser, - Password: &v5.ChangePassword{ - NewPassword: "newSecret", - }, - } - _, err = dbi.database.UpdateUser(ctx, updateReq, false) - if err != nil { - t.Fatal(err) - } - - // Clear the plugin connection to verify we're no longer able to connect - err = dbBackend.ClearConnection("plugin-test") - if err != nil { - t.Fatal(err) - } - - // Reading credentials should no longer work - credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) - if err == nil { - t.Fatalf("expected authentication to fail when reading credentials") - } - - // Put a WAL entry that will be used for rolling back the database password - walEntry := &rotateRootCredentialsWAL{ - ConnectionName: "plugin-test", - UserName: databaseUser, - OldPassword: defaultPassword, - NewPassword: "newSecret", - } - _, err = framework.PutWAL(context.Background(), config.StorageView, rotateRootWALKey, walEntry) - if err != nil { - t.Fatal(err) - } - assertWALCount(t, config.StorageView, 1, rotateRootWALKey) - - // Trigger an immediate RollbackOperation so that the WAL rollback - // function can use the WAL entry to roll back the database password - _, err = lb.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.RollbackOperation, - Path: "", - Storage: config.StorageView, - Data: map[string]interface{}{ - "immediate": true, - }, - }) - if err != nil { - t.Fatal(err) - } - assertWALCount(t, config.StorageView, 0, rotateRootWALKey) - - // Reading credentials should work again after the database - // password has been rolled back. - credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) - if err != nil || (credResp != nil && credResp.IsError()) { - t.Fatalf("err:%s resp:%v\n", err, credResp) - } -} - -// Tests that the WAL rollback function does not roll back the database password. -// The database password should not be rolled back when: -// - A WAL entry exists -// - Password has not been altered on the database -// - Password has not been updated in storage -func TestBackend_RotateRootCredentials_WAL_no_rollback_1(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer lb.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - connURL = strings.ReplaceAll(connURL, "postgres:secret", "{{username}}:{{password}}") - - // Configure a connection to the database - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": []string{"plugin-role-test"}, - "username": databaseUser, - "password": defaultPassword, - } - resp, err := lb.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Create a role - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "max_ttl": "10m", - } - resp, err = lb.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read credentials to verify this initially works - credReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: make(map[string]interface{}), - } - credResp, err := lb.HandleRequest(context.Background(), credReq) - if err != nil || (credResp != nil && credResp.IsError()) { - t.Fatalf("err:%s resp:%v\n", err, credResp) - } - - // Put a WAL entry - walEntry := &rotateRootCredentialsWAL{ - ConnectionName: "plugin-test", - UserName: databaseUser, - OldPassword: defaultPassword, - NewPassword: "newSecret", - } - _, err = framework.PutWAL(context.Background(), config.StorageView, rotateRootWALKey, walEntry) - if err != nil { - t.Fatal(err) - } - assertWALCount(t, config.StorageView, 1, rotateRootWALKey) - - // Trigger an immediate RollbackOperation - _, err = lb.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.RollbackOperation, - Path: "", - Storage: config.StorageView, - Data: map[string]interface{}{ - "immediate": true, - }, - }) - if err != nil { - t.Fatal(err) - } - assertWALCount(t, config.StorageView, 0, rotateRootWALKey) - - // Reading credentials should work - credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) - if err != nil || (credResp != nil && credResp.IsError()) { - t.Fatalf("err:%s resp:%v\n", err, credResp) - } -} - -// Tests that the WAL rollback function does not roll back the database password. -// The database password should not be rolled back when: -// - A WAL entry exists -// - Password has been altered on the database -// - Password has been updated in storage -func TestBackend_RotateRootCredentials_WAL_no_rollback_2(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - dbBackend, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer lb.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - connURL = strings.ReplaceAll(connURL, "postgres:secret", "{{username}}:{{password}}") - - // Configure a connection to the database - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "allowed_roles": []string{"plugin-role-test"}, - "username": databaseUser, - "password": defaultPassword, - } - resp, err := lb.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Create a role - data = map[string]interface{}{ - "db_name": "plugin-test", - "creation_statements": testRole, - "max_ttl": "10m", - } - resp, err = lb.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read credentials to verify this initially works - credReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: make(map[string]interface{}), - } - credResp, err := lb.HandleRequest(context.Background(), credReq) - if err != nil || (credResp != nil && credResp.IsError()) { - t.Fatalf("err:%s resp:%v\n", err, credResp) - } - - // Get a connection to the database plugin - dbi, err := dbBackend.GetConnection(context.Background(), config.StorageView, "plugin-test") - if err != nil { - t.Fatal(err) - } - - // Alter the database password - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - updateReq := v5.UpdateUserRequest{ - Username: databaseUser, - Password: &v5.ChangePassword{ - NewPassword: "newSecret", - }, - } - _, err = dbi.database.UpdateUser(ctx, updateReq, false) - if err != nil { - t.Fatal(err) - } - - // Update storage with the new password - dbConfig, err := dbBackend.DatabaseConfig(context.Background(), config.StorageView, - "plugin-test") - if err != nil { - t.Fatal(err) - } - dbConfig.ConnectionDetails["password"] = "newSecret" - entry, err := logical.StorageEntryJSON("config/plugin-test", dbConfig) - if err != nil { - t.Fatal(err) - } - err = config.StorageView.Put(context.Background(), entry) - if err != nil { - t.Fatal(err) - } - - // Clear the plugin connection to verify we can connect to the database - err = dbBackend.ClearConnection("plugin-test") - if err != nil { - t.Fatal(err) - } - - // Reading credentials should work - credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) - if err != nil || (credResp != nil && credResp.IsError()) { - t.Fatalf("err:%s resp:%v\n", err, credResp) - } - - // Put a WAL entry - walEntry := &rotateRootCredentialsWAL{ - ConnectionName: "plugin-test", - UserName: databaseUser, - OldPassword: defaultPassword, - NewPassword: "newSecret", - } - _, err = framework.PutWAL(context.Background(), config.StorageView, rotateRootWALKey, walEntry) - if err != nil { - t.Fatal(err) - } - assertWALCount(t, config.StorageView, 1, rotateRootWALKey) - - // Trigger an immediate RollbackOperation - _, err = lb.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.RollbackOperation, - Path: "", - Storage: config.StorageView, - Data: map[string]interface{}{ - "immediate": true, - }, - }) - if err != nil { - t.Fatal(err) - } - assertWALCount(t, config.StorageView, 0, rotateRootWALKey) - - // Reading credentials should work - credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) - if err != nil || (credResp != nil && credResp.IsError()) { - t.Fatalf("err:%s resp:%v\n", err, credResp) - } -} diff --git a/builtin/logical/database/rotation_test.go b/builtin/logical/database/rotation_test.go deleted file mode 100644 index 7e10bc959..000000000 --- a/builtin/logical/database/rotation_test.go +++ /dev/null @@ -1,1365 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package database - -import ( - "context" - "database/sql" - "errors" - "fmt" - "log" - "os" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/helper/namespace" - postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql" - v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/dbtxn" - "github.com/hashicorp/vault/sdk/helper/pluginutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/queue" - _ "github.com/jackc/pgx/v4/stdlib" - "github.com/stretchr/testify/mock" -) - -const ( - dbUser = "vaultstatictest" - dbUserDefaultPassword = "password" -) - -func TestBackend_StaticRole_Rotate_basic(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - // create the database user - createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate) - - verifyPgConn(t, dbUser, dbUserDefaultPassword, connURL) - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "name": "plugin-test", - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - data = map[string]interface{}{ - "name": "plugin-role-test", - "db_name": "plugin-test", - "rotation_statements": testRoleStaticUpdate, - "username": dbUser, - "rotation_period": "5400s", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read the creds - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - username := resp.Data["username"].(string) - password := resp.Data["password"].(string) - if username == "" || password == "" { - t.Fatalf("empty username (%s) or password (%s)", username, password) - } - - // Verify username/password - verifyPgConn(t, dbUser, password, connURL) - - // Re-read the creds, verifying they aren't changing on read - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - if username != resp.Data["username"].(string) || password != resp.Data["password"].(string) { - t.Fatal("expected re-read username/password to match, but didn't") - } - - // Trigger rotation - data = map[string]interface{}{"name": "plugin-role-test"} - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "rotate-role/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - if resp != nil { - t.Fatalf("Expected empty response from rotate-role: (%#v)", resp) - } - - // Re-Read the creds - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - newPassword := resp.Data["password"].(string) - if password == newPassword { - t.Fatalf("expected passwords to differ, got (%s)", newPassword) - } - - // Verify new username/password - verifyPgConn(t, username, newPassword, connURL) -} - -// Sanity check to make sure we don't allow an attempt of rotating credentials -// for non-static accounts, which doesn't make sense anyway, but doesn't hurt to -// verify we return an error -func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - // create the database user - createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate) - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "name": "plugin-test", - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - data = map[string]interface{}{ - "name": "plugin-role-test", - "db_name": "plugin-test", - "creation_statements": testRoleStaticCreate, - "rotation_statements": testRoleStaticUpdate, - "revocation_statements": defaultRevocationSQL, - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read the creds - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - username := resp.Data["username"].(string) - password := resp.Data["password"].(string) - if username == "" || password == "" { - t.Fatalf("empty username (%s) or password (%s)", username, password) - } - - // Verify username/password - verifyPgConn(t, dbUser, dbUserDefaultPassword, connURL) - // Trigger rotation - data = map[string]interface{}{"name": "plugin-role-test"} - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "rotate-role/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - // expect resp to be an error - resp, _ = b.HandleRequest(namespace.RootContext(nil), req) - if !resp.IsError() { - t.Fatalf("expected error rotating non-static role") - } - - if resp.Error().Error() != "no static role found for role name" { - t.Fatalf("wrong error message: %s", err) - } -} - -func TestBackend_StaticRole_Revoke_user(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - // create the database user - createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate) - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "name": "plugin-test", - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - testCases := map[string]struct { - revoke *bool - expectVerifyErr bool - }{ - // Default case: user does not specify, Vault leaves the database user - // untouched, and the final connection check passes because the user still - // exists - "unset": {}, - // Revoke on delete. The final connection check should fail because the user - // no longer exists - "revoke": { - revoke: newBoolPtr(true), - expectVerifyErr: true, - }, - // Revoke false, final connection check should still pass - "persist": { - revoke: newBoolPtr(false), - }, - } - for k, tc := range testCases { - t.Run(k, func(t *testing.T) { - data = map[string]interface{}{ - "name": "plugin-role-test", - "db_name": "plugin-test", - "rotation_statements": testRoleStaticUpdate, - "username": dbUser, - "rotation_period": "5400s", - } - if tc.revoke != nil { - data["revoke_user_on_delete"] = *tc.revoke - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Read the creds - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-creds/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - username := resp.Data["username"].(string) - password := resp.Data["password"].(string) - if username == "" || password == "" { - t.Fatalf("empty username (%s) or password (%s)", username, password) - } - - // Verify username/password - verifyPgConn(t, username, password, connURL) - - // delete the role, expect the default where the user is not destroyed - // Read the creds - req = &logical.Request{ - Operation: logical.DeleteOperation, - Path: "static-roles/plugin-role-test", - Storage: config.StorageView, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Verify new username/password still work - verifyPgConn(t, username, password, connURL) - }) - } -} - -func createTestPGUser(t *testing.T, connURL string, username, password, query string) { - t.Helper() - log.Printf("[TRACE] Creating test user") - - db, err := sql.Open("pgx", connURL) - defer db.Close() - if err != nil { - t.Fatal(err) - } - - // Start a transaction - ctx := context.Background() - tx, err := db.BeginTx(ctx, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = tx.Rollback() - }() - - m := map[string]string{ - "name": username, - "password": password, - } - if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, query); err != nil { - t.Fatal(err) - } - // Commit the transaction - if err := tx.Commit(); err != nil { - t.Fatal(err) - } -} - -func verifyPgConn(t *testing.T, username, password, connURL string) { - t.Helper() - cURL := strings.Replace(connURL, "postgres:secret", username+":"+password, 1) - db, err := sql.Open("pgx", cURL) - if err != nil { - t.Fatal(err) - } - if err := db.Ping(); err != nil { - t.Fatal(err) - } -} - -// WAL testing -// -// First scenario, WAL contains a role name that does not exist. -func TestBackend_Static_QueueWAL_discard_role_not_found(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - ctx := context.Background() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - _, err := framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{ - RoleName: "doesnotexist", - }) - if err != nil { - t.Fatalf("error with PutWAL: %s", err) - } - - assertWALCount(t, config.StorageView, 1, staticWALKey) - - b, err := Factory(ctx, config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(ctx) - - time.Sleep(5 * time.Second) - bd := b.(*databaseBackend) - if bd.credRotationQueue == nil { - t.Fatal("database backend had no credential rotation queue") - } - - // Verify empty queue - if bd.credRotationQueue.Len() != 0 { - t.Fatalf("expected zero queue items, got: %d", bd.credRotationQueue.Len()) - } - - assertWALCount(t, config.StorageView, 0, staticWALKey) -} - -// Second scenario, WAL contains a role name that does exist, but the role's -// LastVaultRotation is greater than the WAL has -func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - ctx := context.Background() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - roleName := "test-discard-by-date" - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - // create the database user - createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate) - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "name": "plugin-test", - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Save Now() to make sure rotation time is after this, as well as the WAL - // time - roleTime := time.Now() - - // Create role - data = map[string]interface{}{ - "name": roleName, - "db_name": "plugin-test", - "rotation_statements": testRoleStaticUpdate, - "username": dbUser, - // Low value here, to make sure the backend rotates this password at least - // once before we compare it to the WAL - "rotation_period": "10s", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/" + roleName, - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Allow the first rotation to occur, setting LastVaultRotation - time.Sleep(time.Second * 12) - - // Cleanup the backend, then create a WAL for the role with a - // LastVaultRotation of 1 hour ago, so that when we recreate the backend the - // WAL will be read but discarded - b.Cleanup(ctx) - b = nil - time.Sleep(time.Second * 3) - - // Make a fake WAL entry with an older time - oldRotationTime := roleTime.Add(time.Hour * -1) - walPassword := "somejunkpassword" - _, err = framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{ - RoleName: roleName, - NewPassword: walPassword, - LastVaultRotation: oldRotationTime, - Username: dbUser, - }) - if err != nil { - t.Fatalf("error with PutWAL: %s", err) - } - - assertWALCount(t, config.StorageView, 1, staticWALKey) - - // Reload backend - lb, err = Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok = lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(ctx) - - // Allow enough time for populateQueue to work after boot - time.Sleep(time.Second * 12) - - // PopulateQueue should have processed the entry - assertWALCount(t, config.StorageView, 0, staticWALKey) - - // Read the role - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-roles/" + roleName, - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - lastVaultRotation := resp.Data["last_vault_rotation"].(time.Time) - if !lastVaultRotation.After(oldRotationTime) { - t.Fatal("last vault rotation time not greater than WAL time") - } - - if !lastVaultRotation.After(roleTime) { - t.Fatal("last vault rotation time not greater than role creation time") - } - - // Grab password to verify it didn't change - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-creds/" + roleName, - Storage: config.StorageView, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - password := resp.Data["password"].(string) - if password == walPassword { - t.Fatalf("expected password to not be changed by WAL, but was") - } -} - -// Helper to assert the number of WAL entries is what we expect -func assertWALCount(t *testing.T, s logical.Storage, expected int, key string) { - t.Helper() - - var count int - ctx := context.Background() - keys, err := framework.ListWAL(ctx, s) - if err != nil { - t.Fatal("error listing WALs") - } - - // Loop through WAL keys and process any rotation ones - for _, k := range keys { - walEntry, _ := framework.GetWAL(ctx, s, k) - if walEntry == nil { - continue - } - - if walEntry.Kind != key { - continue - } - count++ - } - if expected != count { - t.Fatalf("WAL count mismatch, expected (%d), got (%d)", expected, count) - } -} - -// -// End WAL testing -// - -type userCreator func(t *testing.T, username, password string) - -func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) { - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - uc := userCreator(func(t *testing.T, username, password string) { - createTestPGUser(t, connURL, username, password, testRoleStaticCreate) - }) - testBackend_StaticRole_Rotations(t, uc, map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - }) -} - -func testBackend_StaticRole_Rotations(t *testing.T, createUser userCreator, opts map[string]interface{}) { - // We need to set this value for the plugin to run, but it doesn't matter what we set it to. - oldToken := os.Getenv(pluginutil.PluginUnwrapTokenEnv) - os.Setenv(pluginutil.PluginUnwrapTokenEnv, "...") - defer func() { - if oldToken != "" { - os.Setenv(pluginutil.PluginUnwrapTokenEnv, oldToken) - } else { - os.Unsetenv(pluginutil.PluginUnwrapTokenEnv) - } - }() - - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - // Change background task interval to 1s to give more margin - // for it to successfully run during the sleeps below. - config.Config[queueTickIntervalKey] = "1" - - // Rotation ticker starts running in Factory call - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - defer b.Cleanup(context.Background()) - - // allow initQueue to finish - bd := b.(*databaseBackend) - if bd.credRotationQueue == nil { - t.Fatal("database backend had no credential rotation queue") - } - - // Configure a connection - data := map[string]interface{}{ - "verify_connection": false, - "allowed_roles": []string{"*"}, - } - for k, v := range opts { - data[k] = v - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - testCases := []string{"10", "20", "100"} - // Create database users ahead - for _, tc := range testCases { - createUser(t, "statictest"+tc, "test") - } - - // create three static roles with different rotation periods - for _, tc := range testCases { - roleName := "plugin-static-role-" + tc - data = map[string]interface{}{ - "name": roleName, - "db_name": "plugin-test", - "username": "statictest" + tc, - "rotation_period": tc, - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/" + roleName, - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - } - - // verify the queue has 3 items in it - if bd.credRotationQueue.Len() != 3 { - t.Fatalf("expected 3 items in the rotation queue, got: (%d)", bd.credRotationQueue.Len()) - } - - // List the roles - data = map[string]interface{}{} - req = &logical.Request{ - Operation: logical.ListOperation, - Path: "static-roles/", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - keys := resp.Data["keys"].([]string) - if len(keys) != 3 { - t.Fatalf("expected 3 roles, got: (%d)", len(keys)) - } - - // capture initial passwords, before the periodic function is triggered - pws := make(map[string][]string, 0) - pws = capturePasswords(t, b, config, testCases, pws) - - // sleep to make sure the periodic func has time to actually run - time.Sleep(15 * time.Second) - pws = capturePasswords(t, b, config, testCases, pws) - - // sleep more, this should allow both sr10 and sr20 to rotate - time.Sleep(10 * time.Second) - pws = capturePasswords(t, b, config, testCases, pws) - - // verify all pws are as they should - pass := true - for k, v := range pws { - if len(v) < 3 { - t.Fatalf("expected to find 3 passwords for (%s), only found (%d)", k, len(v)) - } - switch { - case k == "plugin-static-role-10": - // expect all passwords to be different - if v[0] == v[1] || v[1] == v[2] || v[0] == v[2] { - pass = false - } - case k == "plugin-static-role-20": - // expect the first two to be equal, but different from the third - if v[0] != v[1] || v[0] == v[2] { - pass = false - } - case k == "plugin-static-role-100": - // expect all passwords to be equal - if v[0] != v[1] || v[1] != v[2] { - pass = false - } - default: - t.Fatalf("unexpected password key: %v", k) - } - } - if !pass { - t.Fatalf("password rotations did not match expected: %#v", pws) - } -} - -type createUserCommand struct { - Username string `bson:"createUser"` - Password string `bson:"pwd"` - Roles []interface{} `bson:"roles"` -} - -// Demonstrates a bug fix for the credential rotation not releasing locks -func TestBackend_StaticRole_LockRegression(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "name": "plugin-test", - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate) - data = map[string]interface{}{ - "name": "plugin-role-test", - "db_name": "plugin-test", - "rotation_statements": testRoleStaticUpdate, - "username": dbUser, - "rotation_period": "7s", - } - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - for i := 0; i < 25; i++ { - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "static-roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // sleeping is needed to trigger the deadlock, otherwise things are - // processed too quickly to trigger the rotation lock on so few roles - time.Sleep(500 * time.Millisecond) - } -} - -func TestBackend_StaticRole_Rotate_Invalid_Role(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to db backend") - } - defer b.Cleanup(context.Background()) - - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") - defer cleanup() - - // create the database user - createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate) - - verifyPgConn(t, dbUser, dbUserDefaultPassword, connURL) - - // Configure a connection - data := map[string]interface{}{ - "connection_url": connURL, - "plugin_name": "postgresql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "name": "plugin-test", - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/plugin-test", - Storage: config.StorageView, - Data: data, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - data = map[string]interface{}{ - "name": "plugin-role-test", - "db_name": "plugin-test", - "rotation_statements": testRoleStaticUpdate, - "username": dbUser, - "rotation_period": "5400s", - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: "static-roles/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Pop manually key to emulate a queue without existing key - b.credRotationQueue.PopByKey("plugin-role-test") - - // Make sure queue is empty - if b.credRotationQueue.Len() != 0 { - t.Fatalf("expected queue length to be 0 but is %d", b.credRotationQueue.Len()) - } - - // Trigger rotation - data = map[string]interface{}{"name": "plugin-role-test"} - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "rotate-role/plugin-role-test", - Storage: config.StorageView, - Data: data, - } - resp, err = b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - // Check if key is in queue - if b.credRotationQueue.Len() != 1 { - t.Fatalf("expected queue length to be 1 but is %d", b.credRotationQueue.Len()) - } -} - -func TestRollsPasswordForwardsUsingWAL(t *testing.T) { - ctx := context.Background() - b, storage, mockDB := getBackend(t) - defer b.Cleanup(ctx) - configureDBMount(t, storage) - createRole(t, b, storage, mockDB, "hashicorp") - - role, err := b.StaticRole(ctx, storage, "hashicorp") - if err != nil { - t.Fatal(err) - } - oldPassword := role.StaticAccount.Password - - generateWALFromFailedRotation(t, b, storage, mockDB, "hashicorp") - - walIDs := requireWALs(t, storage, 1) - wal, err := b.findStaticWAL(ctx, storage, walIDs[0]) - if err != nil { - t.Fatal(err) - } - role, err = b.StaticRole(ctx, storage, "hashicorp") - if err != nil { - t.Fatal(err) - } - // Role's password should still be the WAL's old password - if role.StaticAccount.Password != oldPassword { - t.Fatal(role.StaticAccount.Password, oldPassword) - } - - rotateRole(t, b, storage, mockDB, "hashicorp") - - role, err = b.StaticRole(ctx, storage, "hashicorp") - if err != nil { - t.Fatal(err) - } - if role.StaticAccount.Password != wal.NewPassword { - t.Fatal("role password", role.StaticAccount.Password, "WAL new password", wal.NewPassword) - } - // WAL should be cleared by the successful rotate - requireWALs(t, storage, 0) -} - -func TestStoredWALsCorrectlyProcessed(t *testing.T) { - const walNewPassword = "new-password-from-wal" - for _, tc := range []struct { - name string - shouldRotate bool - wal *setCredentialsWAL - }{ - { - "WAL is kept and used for roll forward", - true, - &setCredentialsWAL{ - RoleName: "hashicorp", - Username: "hashicorp", - NewPassword: walNewPassword, - LastVaultRotation: time.Now().Add(time.Hour), - }, - }, - { - "zero-time WAL is discarded on load", - false, - &setCredentialsWAL{ - RoleName: "hashicorp", - Username: "hashicorp", - NewPassword: walNewPassword, - LastVaultRotation: time.Time{}, - }, - }, - { - "empty-password WAL is kept but a new password is generated", - true, - &setCredentialsWAL{ - RoleName: "hashicorp", - Username: "hashicorp", - NewPassword: "", - LastVaultRotation: time.Now().Add(time.Hour), - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - config := logical.TestBackendConfig() - storage := &logical.InmemStorage{} - config.StorageView = storage - b := Backend(config) - defer b.Cleanup(ctx) - mockDB := setupMockDB(b) - if err := b.Setup(ctx, config); err != nil { - t.Fatal(err) - } - b.credRotationQueue = queue.New() - configureDBMount(t, config.StorageView) - createRole(t, b, config.StorageView, mockDB, "hashicorp") - role, err := b.StaticRole(ctx, config.StorageView, "hashicorp") - if err != nil { - t.Fatal(err) - } - initialPassword := role.StaticAccount.Password - - // Set up a WAL for our test case - framework.PutWAL(ctx, config.StorageView, staticWALKey, tc.wal) - requireWALs(t, config.StorageView, 1) - // Reset the rotation queue to simulate startup memory state - b.credRotationQueue = queue.New() - - // Now finish the startup process by populating the queue, which should discard the WAL - b.initQueue(ctx, config, consts.ReplicationUnknown) - - if tc.shouldRotate { - requireWALs(t, storage, 1) - } else { - requireWALs(t, storage, 0) - } - - // Run one tick - mockDB.On("UpdateUser", mock.Anything, mock.Anything). - Return(v5.UpdateUserResponse{}, nil). - Once() - b.rotateCredentials(ctx, storage) - requireWALs(t, storage, 0) - - role, err = b.StaticRole(ctx, storage, "hashicorp") - if err != nil { - t.Fatal(err) - } - item, err := b.popFromRotationQueueByKey("hashicorp") - if err != nil { - t.Fatal(err) - } - - if tc.shouldRotate { - if tc.wal.NewPassword != "" { - // Should use WAL's new_password field - if role.StaticAccount.Password != walNewPassword { - t.Fatal() - } - } else { - // Should rotate but ignore WAL's new_password field - if role.StaticAccount.Password == initialPassword { - t.Fatal() - } - if role.StaticAccount.Password == walNewPassword { - t.Fatal() - } - } - } else { - // Ensure the role was not promoted for early rotation - if item.Priority < time.Now().Add(time.Hour).Unix() { - t.Fatal("priority should be for about a week away, but was", item.Priority) - } - if role.StaticAccount.Password != initialPassword { - t.Fatal("password should not have been rotated yet") - } - } - }) - } -} - -func TestDeletesOlderWALsOnLoad(t *testing.T) { - ctx := context.Background() - b, storage, mockDB := getBackend(t) - defer b.Cleanup(ctx) - configureDBMount(t, storage) - createRole(t, b, storage, mockDB, "hashicorp") - - // Create 4 WALs, with a clear winner for most recent. - wal := &setCredentialsWAL{ - RoleName: "hashicorp", - Username: "hashicorp", - NewPassword: "some-new-password", - LastVaultRotation: time.Now(), - } - for i := 0; i < 3; i++ { - _, err := framework.PutWAL(ctx, storage, staticWALKey, wal) - if err != nil { - t.Fatal(err) - } - } - time.Sleep(2 * time.Second) - // We expect this WAL to have the latest createdAt timestamp - walID, err := framework.PutWAL(ctx, storage, staticWALKey, wal) - if err != nil { - t.Fatal(err) - } - requireWALs(t, storage, 4) - - walMap, err := b.loadStaticWALs(ctx, storage) - if err != nil { - t.Fatal(err) - } - if len(walMap) != 1 || walMap["hashicorp"] == nil || walMap["hashicorp"].walID != walID { - t.Fatal() - } - requireWALs(t, storage, 1) -} - -func generateWALFromFailedRotation(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) { - t.Helper() - mockDB.On("UpdateUser", mock.Anything, mock.Anything). - Return(v5.UpdateUserResponse{}, errors.New("forced error")). - Once() - _, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "rotate-role/" + roleName, - Storage: storage, - }) - if err == nil { - t.Fatal("expected error") - } -} - -func rotateRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) { - t.Helper() - mockDB.On("UpdateUser", mock.Anything, mock.Anything). - Return(v5.UpdateUserResponse{}, nil). - Once() - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "rotate-role/" + roleName, - Storage: storage, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatal(resp, err) - } -} - -// returns a slice of the WAL IDs in storage -func requireWALs(t *testing.T, storage logical.Storage, expectedCount int) []string { - t.Helper() - wals, err := storage.List(context.Background(), "wal/") - if err != nil { - t.Fatal(err) - } - if len(wals) != expectedCount { - t.Fatal("expected WALs", expectedCount, "got", len(wals)) - } - - return wals -} - -func getBackend(t *testing.T) (*databaseBackend, logical.Storage, *mockNewDatabase) { - t.Helper() - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - // Create and init the backend ourselves instead of using a Factory because - // the factory function kicks off threads that cause racy tests. - b := Backend(config) - if err := b.Setup(context.Background(), config); err != nil { - t.Fatal(err) - } - b.credRotationQueue = queue.New() - b.populateQueue(context.Background(), config.StorageView) - - mockDB := setupMockDB(b) - - return b, config.StorageView, mockDB -} - -func setupMockDB(b *databaseBackend) *mockNewDatabase { - mockDB := &mockNewDatabase{} - mockDB.On("Initialize", mock.Anything, mock.Anything).Return(v5.InitializeResponse{}, nil) - mockDB.On("Close").Return(nil) - mockDB.On("Type").Return("mock", nil) - dbw := databaseVersionWrapper{ - v5: mockDB, - } - - dbi := &dbPluginInstance{ - database: dbw, - id: "foo-id", - name: "mockV5", - } - b.connections["mockv5"] = dbi - - return mockDB -} - -// configureDBMount puts config directly into storage to avoid the DB engine's -// plugin init code paths, allowing us to use a manually populated mock DB object. -func configureDBMount(t *testing.T, storage logical.Storage) { - t.Helper() - entry, err := logical.StorageEntryJSON(fmt.Sprintf("config/mockv5"), &DatabaseConfig{ - AllowedRoles: []string{"*"}, - }) - if err != nil { - t.Fatal(err) - } - - err = storage.Put(context.Background(), entry) - if err != nil { - t.Fatal(err) - } -} - -// capturePasswords captures the current passwords at the time of calling, and -// returns a map of username / passwords building off of the input map -func capturePasswords(t *testing.T, b logical.Backend, config *logical.BackendConfig, testCases []string, pws map[string][]string) map[string][]string { - new := make(map[string][]string, 0) - for _, tc := range testCases { - // Read the role - roleName := "plugin-static-role-" + tc - req := &logical.Request{ - Operation: logical.ReadOperation, - Path: "static-creds/" + roleName, - Storage: config.StorageView, - } - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) - } - - username := resp.Data["username"].(string) - password := resp.Data["password"].(string) - if username == "" || password == "" { - t.Fatalf("expected both username/password for (%s), got (%s), (%s)", roleName, username, password) - } - new[roleName] = append(new[roleName], password) - } - - for k, v := range new { - pws[k] = append(pws[k], v...) - } - - return pws -} - -func newBoolPtr(b bool) *bool { - v := b - return &v -} diff --git a/builtin/logical/database/version_wrapper_test.go b/builtin/logical/database/version_wrapper_test.go deleted file mode 100644 index 478403859..000000000 --- a/builtin/logical/database/version_wrapper_test.go +++ /dev/null @@ -1,853 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package database - -import ( - "context" - "fmt" - "reflect" - "testing" - "time" - - v4 "github.com/hashicorp/vault/sdk/database/dbplugin" - v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -func TestInitDatabase_missingDB(t *testing.T) { - dbw := databaseVersionWrapper{} - - req := v5.InitializeRequest{} - resp, err := dbw.Initialize(context.Background(), req) - if err == nil { - t.Fatalf("err expected, got nil") - } - - expectedResp := v5.InitializeResponse{} - if !reflect.DeepEqual(resp, expectedResp) { - t.Fatalf("Actual resp: %#v\nExpected resp: %#v", resp, expectedResp) - } -} - -func TestInitDatabase_newDB(t *testing.T) { - type testCase struct { - req v5.InitializeRequest - - newInitResp v5.InitializeResponse - newInitErr error - newInitCalls int - - expectedResp v5.InitializeResponse - expectErr bool - } - - tests := map[string]testCase{ - "success": { - req: v5.InitializeRequest{ - Config: map[string]interface{}{ - "foo": "bar", - }, - VerifyConnection: true, - }, - newInitResp: v5.InitializeResponse{ - Config: map[string]interface{}{ - "foo": "bar", - }, - }, - newInitCalls: 1, - expectedResp: v5.InitializeResponse{ - Config: map[string]interface{}{ - "foo": "bar", - }, - }, - expectErr: false, - }, - "error": { - req: v5.InitializeRequest{ - Config: map[string]interface{}{ - "foo": "bar", - }, - VerifyConnection: true, - }, - newInitResp: v5.InitializeResponse{}, - newInitErr: fmt.Errorf("test error"), - newInitCalls: 1, - expectedResp: v5.InitializeResponse{}, - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - newDB := new(mockNewDatabase) - newDB.On("Initialize", mock.Anything, mock.Anything). - Return(test.newInitResp, test.newInitErr) - defer newDB.AssertNumberOfCalls(t, "Initialize", test.newInitCalls) - - dbw := databaseVersionWrapper{ - v5: newDB, - } - - resp, err := dbw.Initialize(context.Background(), test.req) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual resp: %#v\nExpected resp: %#v", resp, test.expectedResp) - } - }) - } -} - -func TestInitDatabase_legacyDB(t *testing.T) { - type testCase struct { - req v5.InitializeRequest - - initConfig map[string]interface{} - initErr error - initCalls int - - expectedResp v5.InitializeResponse - expectErr bool - } - - tests := map[string]testCase{ - "success": { - req: v5.InitializeRequest{ - Config: map[string]interface{}{ - "foo": "bar", - }, - VerifyConnection: true, - }, - initConfig: map[string]interface{}{ - "foo": "bar", - }, - initCalls: 1, - expectedResp: v5.InitializeResponse{ - Config: map[string]interface{}{ - "foo": "bar", - }, - }, - expectErr: false, - }, - "error": { - req: v5.InitializeRequest{ - Config: map[string]interface{}{ - "foo": "bar", - }, - VerifyConnection: true, - }, - initErr: fmt.Errorf("test error"), - initCalls: 1, - expectedResp: v5.InitializeResponse{}, - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - legacyDB := new(mockLegacyDatabase) - legacyDB.On("Init", mock.Anything, mock.Anything, mock.Anything). - Return(test.initConfig, test.initErr) - defer legacyDB.AssertNumberOfCalls(t, "Init", test.initCalls) - - dbw := databaseVersionWrapper{ - v4: legacyDB, - } - - resp, err := dbw.Initialize(context.Background(), test.req) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual resp: %#v\nExpected resp: %#v", resp, test.expectedResp) - } - }) - } -} - -func TestNewUser_missingDB(t *testing.T) { - dbw := databaseVersionWrapper{} - - req := v5.NewUserRequest{} - resp, pass, err := dbw.NewUser(context.Background(), req) - if err == nil { - t.Fatalf("err expected, got nil") - } - - expectedResp := v5.NewUserResponse{} - if !reflect.DeepEqual(resp, expectedResp) { - t.Fatalf("Actual resp: %#v\nExpected resp: %#v", resp, expectedResp) - } - - if pass != "" { - t.Fatalf("Password should be empty but was: %s", pass) - } -} - -func TestNewUser_newDB(t *testing.T) { - type testCase struct { - req v5.NewUserRequest - - newUserResp v5.NewUserResponse - newUserErr error - newUserCalls int - - expectedResp v5.NewUserResponse - expectErr bool - } - - tests := map[string]testCase{ - "success": { - req: v5.NewUserRequest{ - Password: "new_password", - }, - - newUserResp: v5.NewUserResponse{ - Username: "newuser", - }, - newUserCalls: 1, - - expectedResp: v5.NewUserResponse{ - Username: "newuser", - }, - expectErr: false, - }, - "error": { - req: v5.NewUserRequest{ - Password: "new_password", - }, - - newUserErr: fmt.Errorf("test error"), - newUserCalls: 1, - - expectedResp: v5.NewUserResponse{}, - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - newDB := new(mockNewDatabase) - newDB.On("NewUser", mock.Anything, mock.Anything). - Return(test.newUserResp, test.newUserErr) - defer newDB.AssertNumberOfCalls(t, "NewUser", test.newUserCalls) - - dbw := databaseVersionWrapper{ - v5: newDB, - } - - resp, password, err := dbw.NewUser(context.Background(), test.req) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual resp: %#v\nExpected resp: %#v", resp, test.expectedResp) - } - - if password != test.req.Password { - t.Fatalf("Actual password: %s Expected password: %s", password, test.req.Password) - } - }) - } -} - -func TestNewUser_legacyDB(t *testing.T) { - type testCase struct { - req v5.NewUserRequest - - createUserUsername string - createUserPassword string - createUserErr error - createUserCalls int - - expectedResp v5.NewUserResponse - expectedPassword string - expectErr bool - } - - tests := map[string]testCase{ - "success": { - req: v5.NewUserRequest{ - Password: "new_password", - }, - - createUserUsername: "newuser", - createUserPassword: "securepassword", - createUserCalls: 1, - - expectedResp: v5.NewUserResponse{ - Username: "newuser", - }, - expectedPassword: "securepassword", - expectErr: false, - }, - "error": { - req: v5.NewUserRequest{ - Password: "new_password", - }, - - createUserErr: fmt.Errorf("test error"), - createUserCalls: 1, - - expectedResp: v5.NewUserResponse{}, - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - legacyDB := new(mockLegacyDatabase) - legacyDB.On("CreateUser", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(test.createUserUsername, test.createUserPassword, test.createUserErr) - defer legacyDB.AssertNumberOfCalls(t, "CreateUser", test.createUserCalls) - - dbw := databaseVersionWrapper{ - v4: legacyDB, - } - - resp, password, err := dbw.NewUser(context.Background(), test.req) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual resp: %#v\nExpected resp: %#v", resp, test.expectedResp) - } - - if password != test.expectedPassword { - t.Fatalf("Actual password: %s Expected password: %s", password, test.req.Password) - } - }) - } -} - -func TestUpdateUser_missingDB(t *testing.T) { - dbw := databaseVersionWrapper{} - - req := v5.UpdateUserRequest{} - resp, err := dbw.UpdateUser(context.Background(), req, false) - if err == nil { - t.Fatalf("err expected, got nil") - } - - expectedConfig := map[string]interface{}(nil) - if !reflect.DeepEqual(resp, expectedConfig) { - t.Fatalf("Actual config: %#v\nExpected config: %#v", resp, expectedConfig) - } -} - -func TestUpdateUser_newDB(t *testing.T) { - type testCase struct { - req v5.UpdateUserRequest - - updateUserErr error - updateUserCalls int - - expectedResp v5.UpdateUserResponse - expectErr bool - } - - tests := map[string]testCase{ - "success": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - }, - updateUserCalls: 1, - expectErr: false, - }, - "error": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - }, - updateUserErr: fmt.Errorf("test error"), - updateUserCalls: 1, - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - newDB := new(mockNewDatabase) - newDB.On("UpdateUser", mock.Anything, mock.Anything). - Return(v5.UpdateUserResponse{}, test.updateUserErr) - defer newDB.AssertNumberOfCalls(t, "UpdateUser", test.updateUserCalls) - - dbw := databaseVersionWrapper{ - v5: newDB, - } - - _, err := dbw.UpdateUser(context.Background(), test.req, false) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - }) - } -} - -func TestUpdateUser_legacyDB(t *testing.T) { - type testCase struct { - req v5.UpdateUserRequest - isRootUser bool - - setCredentialsErr error - setCredentialsCalls int - - rotateRootConfig map[string]interface{} - rotateRootErr error - rotateRootCalls int - - renewUserErr error - renewUserCalls int - - expectedConfig map[string]interface{} - expectErr bool - } - - tests := map[string]testCase{ - "missing changes": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - }, - isRootUser: false, - - setCredentialsCalls: 0, - rotateRootCalls: 0, - renewUserCalls: 0, - - expectErr: true, - }, - "both password and expiration changes": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - Password: &v5.ChangePassword{}, - Expiration: &v5.ChangeExpiration{}, - }, - isRootUser: false, - - setCredentialsCalls: 0, - rotateRootCalls: 0, - renewUserCalls: 0, - - expectErr: true, - }, - "change password - SetCredentials": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - Password: &v5.ChangePassword{ - NewPassword: "newpassowrd", - }, - }, - isRootUser: false, - - setCredentialsErr: nil, - setCredentialsCalls: 1, - rotateRootCalls: 0, - renewUserCalls: 0, - - expectedConfig: nil, - expectErr: false, - }, - "change password - SetCredentials failed": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - Password: &v5.ChangePassword{ - NewPassword: "newpassowrd", - }, - }, - isRootUser: false, - - setCredentialsErr: fmt.Errorf("set credentials failed"), - setCredentialsCalls: 1, - rotateRootCalls: 0, - renewUserCalls: 0, - - expectedConfig: nil, - expectErr: true, - }, - "change password - SetCredentials unimplemented but not a root user": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - Password: &v5.ChangePassword{ - NewPassword: "newpassowrd", - }, - }, - isRootUser: false, - - setCredentialsErr: status.Error(codes.Unimplemented, "SetCredentials is not implemented"), - setCredentialsCalls: 1, - - rotateRootCalls: 0, - renewUserCalls: 0, - - expectedConfig: nil, - expectErr: true, - }, - "change password - RotateRootCredentials (gRPC Unimplemented)": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - Password: &v5.ChangePassword{ - NewPassword: "newpassowrd", - }, - }, - isRootUser: true, - - setCredentialsErr: status.Error(codes.Unimplemented, "SetCredentials is not implemented"), - setCredentialsCalls: 1, - - rotateRootConfig: map[string]interface{}{ - "foo": "bar", - }, - rotateRootCalls: 1, - - renewUserCalls: 0, - - expectedConfig: map[string]interface{}{ - "foo": "bar", - }, - expectErr: false, - }, - "change password - RotateRootCredentials (ErrPluginStaticUnsupported)": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - Password: &v5.ChangePassword{ - NewPassword: "newpassowrd", - }, - }, - isRootUser: true, - - setCredentialsErr: v4.ErrPluginStaticUnsupported, - setCredentialsCalls: 1, - - rotateRootConfig: map[string]interface{}{ - "foo": "bar", - }, - rotateRootCalls: 1, - - renewUserCalls: 0, - - expectedConfig: map[string]interface{}{ - "foo": "bar", - }, - expectErr: false, - }, - "change password - RotateRootCredentials failed": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - Password: &v5.ChangePassword{ - NewPassword: "newpassowrd", - }, - }, - isRootUser: true, - - setCredentialsErr: status.Error(codes.Unimplemented, "SetCredentials is not implemented"), - setCredentialsCalls: 1, - - rotateRootErr: fmt.Errorf("rotate root failed"), - rotateRootCalls: 1, - renewUserCalls: 0, - - expectedConfig: nil, - expectErr: true, - }, - - "change expiration": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - Expiration: &v5.ChangeExpiration{ - NewExpiration: time.Now(), - }, - }, - isRootUser: false, - - setCredentialsCalls: 0, - rotateRootCalls: 0, - - renewUserErr: nil, - renewUserCalls: 1, - - expectedConfig: nil, - expectErr: false, - }, - "change expiration failed": { - req: v5.UpdateUserRequest{ - Username: "existing_user", - Expiration: &v5.ChangeExpiration{ - NewExpiration: time.Now(), - }, - }, - isRootUser: false, - - setCredentialsCalls: 0, - rotateRootCalls: 0, - - renewUserErr: fmt.Errorf("test error"), - renewUserCalls: 1, - - expectedConfig: nil, - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - legacyDB := new(mockLegacyDatabase) - legacyDB.On("SetCredentials", mock.Anything, mock.Anything, mock.Anything). - Return("", "", test.setCredentialsErr) - defer legacyDB.AssertNumberOfCalls(t, "SetCredentials", test.setCredentialsCalls) - - legacyDB.On("RotateRootCredentials", mock.Anything, mock.Anything). - Return(test.rotateRootConfig, test.rotateRootErr) - defer legacyDB.AssertNumberOfCalls(t, "RotateRootCredentials", test.rotateRootCalls) - - legacyDB.On("RenewUser", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(test.renewUserErr) - defer legacyDB.AssertNumberOfCalls(t, "RenewUser", test.renewUserCalls) - - dbw := databaseVersionWrapper{ - v4: legacyDB, - } - - newConfig, err := dbw.UpdateUser(context.Background(), test.req, test.isRootUser) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(newConfig, test.expectedConfig) { - t.Fatalf("Actual config: %#v\nExpected config: %#v", newConfig, test.expectedConfig) - } - }) - } -} - -func TestDeleteUser_missingDB(t *testing.T) { - dbw := databaseVersionWrapper{} - - req := v5.DeleteUserRequest{} - _, err := dbw.DeleteUser(context.Background(), req) - if err == nil { - t.Fatalf("err expected, got nil") - } -} - -func TestDeleteUser_newDB(t *testing.T) { - type testCase struct { - req v5.DeleteUserRequest - - deleteUserErr error - deleteUserCalls int - - expectErr bool - } - - tests := map[string]testCase{ - "success": { - req: v5.DeleteUserRequest{ - Username: "existing_user", - }, - - deleteUserErr: nil, - deleteUserCalls: 1, - - expectErr: false, - }, - "error": { - req: v5.DeleteUserRequest{ - Username: "existing_user", - }, - - deleteUserErr: fmt.Errorf("test error"), - deleteUserCalls: 1, - - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - newDB := new(mockNewDatabase) - newDB.On("DeleteUser", mock.Anything, mock.Anything). - Return(v5.DeleteUserResponse{}, test.deleteUserErr) - defer newDB.AssertNumberOfCalls(t, "DeleteUser", test.deleteUserCalls) - - dbw := databaseVersionWrapper{ - v5: newDB, - } - - _, err := dbw.DeleteUser(context.Background(), test.req) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - }) - } -} - -func TestDeleteUser_legacyDB(t *testing.T) { - type testCase struct { - req v5.DeleteUserRequest - - revokeUserErr error - revokeUserCalls int - - expectErr bool - } - - tests := map[string]testCase{ - "success": { - req: v5.DeleteUserRequest{ - Username: "existing_user", - }, - - revokeUserErr: nil, - revokeUserCalls: 1, - - expectErr: false, - }, - "error": { - req: v5.DeleteUserRequest{ - Username: "existing_user", - }, - - revokeUserErr: fmt.Errorf("test error"), - revokeUserCalls: 1, - - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - legacyDB := new(mockLegacyDatabase) - legacyDB.On("RevokeUser", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(test.revokeUserErr) - defer legacyDB.AssertNumberOfCalls(t, "RevokeUser", test.revokeUserCalls) - - dbw := databaseVersionWrapper{ - v4: legacyDB, - } - - _, err := dbw.DeleteUser(context.Background(), test.req) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - }) - } -} - -type badValue struct{} - -func (badValue) MarshalJSON() ([]byte, error) { - return nil, fmt.Errorf("this value cannot be marshalled to JSON") -} - -var _ logical.Storage = fakeStorage{} - -type fakeStorage struct { - putErr error -} - -func (f fakeStorage) Put(ctx context.Context, entry *logical.StorageEntry) error { - return f.putErr -} - -func (f fakeStorage) List(ctx context.Context, s string) ([]string, error) { - panic("list not implemented") -} - -func (f fakeStorage) Get(ctx context.Context, s string) (*logical.StorageEntry, error) { - panic("get not implemented") -} - -func (f fakeStorage) Delete(ctx context.Context, s string) error { - panic("delete not implemented") -} - -func TestStoreConfig(t *testing.T) { - type testCase struct { - config *DatabaseConfig - putErr error - expectErr bool - } - - tests := map[string]testCase{ - "bad config": { - config: &DatabaseConfig{ - PluginName: "testplugin", - ConnectionDetails: map[string]interface{}{ - "bad value": badValue{}, - }, - }, - putErr: nil, - expectErr: true, - }, - "storage error": { - config: &DatabaseConfig{ - PluginName: "testplugin", - ConnectionDetails: map[string]interface{}{ - "foo": "bar", - }, - }, - putErr: fmt.Errorf("failed to store config"), - expectErr: true, - }, - "happy path": { - config: &DatabaseConfig{ - PluginName: "testplugin", - ConnectionDetails: map[string]interface{}{ - "foo": "bar", - }, - }, - putErr: nil, - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - storage := fakeStorage{ - putErr: test.putErr, - } - - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - err := storeConfig(ctx, storage, "testconfig", test.config) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - }) - } -} diff --git a/builtin/logical/database/versioning_large_test.go b/builtin/logical/database/versioning_large_test.go deleted file mode 100644 index 482e5f353..000000000 --- a/builtin/logical/database/versioning_large_test.go +++ /dev/null @@ -1,532 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package database - -// This file contains all "large"/expensive tests. These are running requests against a running backend - -import ( - "context" - "fmt" - "os" - "regexp" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/pluginutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestPlugin_lifecycle(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v4-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV4", []string{}, "") - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV5", []string{}, "") - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v6-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV6Multiplexed", []string{}, "") - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to database backend") - } - defer b.Cleanup(context.Background()) - - type testCase struct { - dbName string - dbType string - configData map[string]interface{} - assertDynamicUsername stringAssertion - assertDynamicPassword stringAssertion - } - - tests := map[string]testCase{ - "v4": { - dbName: "mockv4", - dbType: "mock-v4-database-plugin", - configData: map[string]interface{}{ - "name": "mockv4", - "plugin_name": "mock-v4-database-plugin", - "connection_url": "sample_connection_url", - "verify_connection": true, - "allowed_roles": []string{"*"}, - "username": "mockv4-user", - "password": "mysecurepassword", - }, - assertDynamicUsername: assertStringPrefix("mockv4_user_"), - assertDynamicPassword: assertStringPrefix("mockv4_"), - }, - "v5": { - dbName: "mockv5", - dbType: "mock-v5-database-plugin", - configData: map[string]interface{}{ - "connection_url": "sample_connection_url", - "plugin_name": "mock-v5-database-plugin", - "verify_connection": true, - "allowed_roles": []string{"*"}, - "name": "mockv5", - "username": "mockv5-user", - "password": "mysecurepassword", - }, - assertDynamicUsername: assertStringPrefix("mockv5_user_"), - assertDynamicPassword: assertStringRegex("^[a-zA-Z0-9-]{20}"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - var cleanupReqs []*logical.Request - defer func() { - // Do not defer cleanup directly so that we can populate the - // slice before the function gets executed. - cleanup(t, b, cleanupReqs) - }() - - // ///////////////////////////////////////////////////////////////// - // Configure - req := &logical.Request{ - Operation: logical.CreateOperation, - Path: fmt.Sprintf("config/%s", test.dbName), - Storage: config.StorageView, - Data: test.configData, - } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - resp, err := b.HandleRequest(ctx, req) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertNoRespData(t, resp) - - cleanupReqs = append(cleanupReqs, &logical.Request{ - Operation: logical.DeleteOperation, - Path: fmt.Sprintf("config/%s", test.dbName), - Storage: config.StorageView, - }) - - // ///////////////////////////////////////////////////////////////// - // Rotate root credentials - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("rotate-root/%s", test.dbName), - Storage: config.StorageView, - } - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - resp, err = b.HandleRequest(ctx, req) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertNoRespData(t, resp) - - // ///////////////////////////////////////////////////////////////// - // Dynamic credentials - - // Create role - dynamicRoleName := "dynamic-role" - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("roles/%s", dynamicRoleName), - Storage: config.StorageView, - Data: map[string]interface{}{ - "db_name": test.dbName, - "default_ttl": "5s", - "max_ttl": "1m", - }, - } - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - resp, err = b.HandleRequest(ctx, req) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertNoRespData(t, resp) - - cleanupReqs = append(cleanupReqs, &logical.Request{ - Operation: logical.DeleteOperation, - Path: fmt.Sprintf("roles/%s", dynamicRoleName), - Storage: config.StorageView, - }) - - // Generate credentials - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: fmt.Sprintf("creds/%s", dynamicRoleName), - Storage: config.StorageView, - } - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - resp, err = b.HandleRequest(ctx, req) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertRespHasData(t, resp) - - // TODO: Figure out how to make a call to the cluster that gives back a lease ID - // And also rotates the secret out after its TTL - - // ///////////////////////////////////////////////////////////////// - // Static credentials - - // Create static role - staticRoleName := "static-role" - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: fmt.Sprintf("static-roles/%s", staticRoleName), - Storage: config.StorageView, - Data: map[string]interface{}{ - "db_name": test.dbName, - "username": "static-username", - "rotation_period": "5", - }, - } - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - resp, err = b.HandleRequest(ctx, req) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertNoRespData(t, resp) - - cleanupReqs = append(cleanupReqs, &logical.Request{ - Operation: logical.DeleteOperation, - Path: fmt.Sprintf("static-roles/%s", staticRoleName), - Storage: config.StorageView, - }) - - // Get credentials - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: fmt.Sprintf("static-creds/%s", staticRoleName), - Storage: config.StorageView, - } - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - resp, err = b.HandleRequest(ctx, req) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertRespHasData(t, resp) - }) - } -} - -func TestPlugin_VersionSelection(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - for _, version := range []string{"v11.0.0", "v11.0.1-rc1", "v2.0.0"} { - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, version, "TestBackend_PluginMain_MockV5", []string{}, "") - } - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to database backend") - } - defer b.Cleanup(context.Background()) - - test := func(t *testing.T, selectVersion, expectedVersion string) func(t *testing.T) { - return func(t *testing.T) { - req := &logical.Request{ - Operation: logical.CreateOperation, - Path: "config/db", - Storage: config.StorageView, - Data: map[string]interface{}{ - "connection_url": "sample_connection_url", - "plugin_name": "mock-v5-database-plugin", - "plugin_version": selectVersion, - "verify_connection": true, - "allowed_roles": []string{"*"}, - "name": "mockv5", - "username": "mockv5-user", - "password": "mysecurepassword", - }, - } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - resp, err := b.HandleRequest(ctx, req) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertNoRespData(t, resp) - - defer func() { - _, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.DeleteOperation, - Path: "config/db", - Storage: config.StorageView, - }) - if err != nil { - t.Fatal(err) - } - }() - - req = &logical.Request{ - Operation: logical.ReadOperation, - Path: "config/db", - Storage: config.StorageView, - } - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - resp, err = b.HandleRequest(ctx, req) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - if resp.Data["plugin_version"].(string) != expectedVersion { - t.Fatalf("Expected version %q but got %q", expectedVersion, resp.Data["plugin_version"].(string)) - } - } - } - - for name, tc := range map[string]struct { - selectVersion string - expectedVersion string - }{ - "no version specified, selects latest in the absence of unversioned plugins": { - selectVersion: "", - expectedVersion: "v11.0.1-rc1", - }, - "specific version selected": { - selectVersion: "11.0.0", - expectedVersion: "v11.0.0", - }, - } { - t.Run(name, test(t, tc.selectVersion, tc.expectedVersion)) - } - - // Register a newer version of the plugin, and ensure that's the new default version selected. - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "v11.0.1", "TestBackend_PluginMain_MockV5", []string{}, "") - t.Run("no version specified, new latest version selected", test(t, "", "v11.0.1")) - - // Register an unversioned plugin and ensure that is now selected when no version is specified. - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV5", []string{}, "") - for name, tc := range map[string]struct { - selectVersion string - expectedVersion string - }{ - "no version specified, selects unversioned": { - selectVersion: "", - expectedVersion: "", - }, - "specific version selected": { - selectVersion: "v2.0.0", - expectedVersion: "v2.0.0", - }, - } { - t.Run(name, test(t, tc.selectVersion, tc.expectedVersion)) - } -} - -func TestPlugin_VersionMustBeExplicitlyUpgraded(t *testing.T) { - cluster, sys := getCluster(t) - defer cluster.Cleanup() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - config.System = sys - lb, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - b, ok := lb.(*databaseBackend) - if !ok { - t.Fatal("could not convert to database backend") - } - defer b.Cleanup(context.Background()) - - configData := func(extraData ...string) map[string]interface{} { - data := map[string]interface{}{ - "connection_url": "sample_connection_url", - "plugin_name": "mysql-database-plugin", - "verify_connection": false, - "allowed_roles": []string{"*"}, - "username": "mockv5-user", - "password": "mysecurepassword", - } - if len(extraData)%2 != 0 { - t.Fatal("Expected an even number of args in extraData") - } - for i := 0; i < len(extraData); i += 2 { - data[extraData[i]] = extraData[i+1] - } - return data - } - - readVersion := func() string { - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "config/db", - Storage: config.StorageView, - }) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - return resp.Data["plugin_version"].(string) - } - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.CreateOperation, - Path: "config/db", - Storage: config.StorageView, - Data: configData(), - }) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertNoRespData(t, resp) - - version := readVersion() - expectedVersion := "" - if version != expectedVersion { - t.Fatalf("Expected version %q but got %q", expectedVersion, version) - } - - // Register versioned plugin, and check that a new write to existing config doesn't upgrade the plugin implicitly. - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mysql-database-plugin", consts.PluginTypeDatabase, "v1.0.0", "TestBackend_PluginMain_MockV5", []string{}, "") - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/db", - Storage: config.StorageView, - Data: configData(), - }) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertNoRespData(t, resp) - - version = readVersion() - if version != expectedVersion { - t.Fatalf("Expected version %q but got %q", expectedVersion, version) - } - - // Now explicitly upgrade. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/db", - Storage: config.StorageView, - Data: configData("plugin_version", "1.0.0"), - }) - assertErrIsNil(t, err) - assertRespHasNoErr(t, resp) - assertNoRespData(t, resp) - - version = readVersion() - expectedVersion = "v1.0.0" - if version != expectedVersion { - t.Fatalf("Expected version %q but got %q", expectedVersion, version) - } -} - -func cleanup(t *testing.T, b *databaseBackend, reqs []*logical.Request) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // Go in stack order so it works similar to defer - for i := len(reqs) - 1; i >= 0; i-- { - req := reqs[i] - resp, err := b.HandleRequest(ctx, req) - if err != nil { - t.Fatalf("Error cleaning up: %s", err) - } - if resp != nil && resp.IsError() { - t.Fatalf("Error cleaning up: %s", resp.Error()) - } - } -} - -func TestBackend_PluginMain_MockV4(t *testing.T) { - if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" { - return - } - - caPEM := os.Getenv(pluginutil.PluginCACertPEMEnv) - if caPEM == "" { - t.Fatal("CA cert not passed in") - } - - args := []string{"--ca-cert=" + caPEM} - - apiClientMeta := &api.PluginAPIClientMeta{} - flags := apiClientMeta.FlagSet() - flags.Parse(args) - - RunV4(apiClientMeta.GetTLSConfig()) -} - -func TestBackend_PluginMain_MockV5(t *testing.T) { - if os.Getenv(pluginutil.PluginVaultVersionEnv) == "" { - return - } - - RunV5() -} - -func TestBackend_PluginMain_MockV6Multiplexed(t *testing.T) { - if os.Getenv(pluginutil.PluginVaultVersionEnv) == "" { - return - } - - RunV6Multiplexed() -} - -func assertNoRespData(t *testing.T, resp *logical.Response) { - t.Helper() - if resp != nil && len(resp.Data) > 0 { - t.Fatalf("Response had data when none was expected: %#v", resp.Data) - } -} - -func assertRespHasData(t *testing.T, resp *logical.Response) { - t.Helper() - if resp == nil || len(resp.Data) == 0 { - t.Fatalf("Response didn't have any data when some was expected") - } -} - -type stringAssertion func(t *testing.T, str string) - -func assertStringPrefix(expectedPrefix string) stringAssertion { - return func(t *testing.T, str string) { - t.Helper() - if !strings.HasPrefix(str, expectedPrefix) { - t.Fatalf("Missing prefix %q: Actual: %q", expectedPrefix, str) - } - } -} - -func assertStringRegex(expectedRegex string) stringAssertion { - re := regexp.MustCompile(expectedRegex) - return func(t *testing.T, str string) { - if !re.MatchString(str) { - t.Fatalf("Actual: %q did not match regexp %q", str, expectedRegex) - } - } -} - -func assertRespHasNoErr(t *testing.T, resp *logical.Response) { - t.Helper() - if resp != nil && resp.IsError() { - t.Fatalf("response is error: %#v\n", resp) - } -} - -func assertErrIsNil(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Fatalf("No error expected, got: %s", err) - } -} diff --git a/builtin/logical/nomad/backend_test.go b/builtin/logical/nomad/backend_test.go deleted file mode 100644 index 9dc00cdfb..000000000 --- a/builtin/logical/nomad/backend_test.go +++ /dev/null @@ -1,708 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package nomad - -import ( - "context" - "fmt" - "os" - "reflect" - "strings" - "testing" - "time" - - nomadapi "github.com/hashicorp/nomad/api" - "github.com/hashicorp/vault/helper/testhelpers" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/mapstructure" -) - -type Config struct { - docker.ServiceURL - Token string -} - -func (c *Config) APIConfig() *nomadapi.Config { - apiConfig := nomadapi.DefaultConfig() - apiConfig.Address = c.URL().String() - apiConfig.SecretID = c.Token - return apiConfig -} - -func (c *Config) Client() (*nomadapi.Client, error) { - apiConfig := c.APIConfig() - - return nomadapi.NewClient(apiConfig) -} - -func prepareTestContainer(t *testing.T, bootstrap bool) (func(), *Config) { - if retAddress := os.Getenv("NOMAD_ADDR"); retAddress != "" { - s, err := docker.NewServiceURLParse(retAddress) - if err != nil { - t.Fatal(err) - } - return func() {}, &Config{*s, os.Getenv("NOMAD_TOKEN")} - } - - runner, err := docker.NewServiceRunner(docker.RunOptions{ - ImageRepo: "docker.mirror.hashicorp.services/multani/nomad", - ImageTag: "1.1.6", - ContainerName: "nomad", - Ports: []string{"4646/tcp"}, - Cmd: []string{"agent", "-dev"}, - Env: []string{`NOMAD_LOCAL_CONFIG=bind_addr = "0.0.0.0" acl { enabled = true }`}, - }) - if err != nil { - t.Fatalf("Could not start docker Nomad: %s", err) - } - - var nomadToken string - svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - var err error - nomadapiConfig := nomadapi.DefaultConfig() - nomadapiConfig.Address = fmt.Sprintf("http://%s:%d/", host, port) - nomad, err := nomadapi.NewClient(nomadapiConfig) - if err != nil { - return nil, err - } - - _, err = nomad.Status().Leader() - if err != nil { - t.Logf("[DEBUG] Nomad is not ready yet: %s", err) - return nil, err - } - - if bootstrap { - aclbootstrap, _, err := nomad.ACLTokens().Bootstrap(nil) - if err != nil { - return nil, err - } - nomadToken = aclbootstrap.SecretID - t.Logf("[WARN] Generated Master token: %s", nomadToken) - } - - nomadAuthConfig := nomadapi.DefaultConfig() - nomadAuthConfig.Address = nomad.Address() - - if bootstrap { - nomadAuthConfig.SecretID = nomadToken - - nomadAuth, err := nomadapi.NewClient(nomadAuthConfig) - if err != nil { - return nil, err - } - - err = preprePolicies(nomadAuth) - if err != nil { - return nil, err - } - } - - u, _ := docker.NewServiceURLParse(nomadapiConfig.Address) - return &Config{ - ServiceURL: *u, - Token: nomadToken, - }, nil - }) - if err != nil { - t.Fatalf("Could not start docker Nomad: %s", err) - } - - return svc.Cleanup, svc.Config.(*Config) -} - -func preprePolicies(nomadClient *nomadapi.Client) error { - policy := &nomadapi.ACLPolicy{ - Name: "test", - Description: "test", - Rules: `namespace "default" { - policy = "read" - } - `, - } - anonPolicy := &nomadapi.ACLPolicy{ - Name: "anonymous", - Description: "Deny all access for anonymous requests", - Rules: `namespace "default" { - policy = "deny" - } - agent { - policy = "deny" - } - node { - policy = "deny" - } - `, - } - - _, err := nomadClient.ACLPolicies().Upsert(policy, nil) - if err != nil { - return err - } - - _, err = nomadClient.ACLPolicies().Upsert(anonPolicy, nil) - if err != nil { - return err - } - - return nil -} - -func TestBackend_config_Bootstrap(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, svccfg := prepareTestContainer(t, false) - defer cleanup() - - connData := map[string]interface{}{ - "address": svccfg.URL().String(), - "token": "", - } - - confReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/access", - Storage: config.StorageView, - Data: connData, - } - - resp, err := b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) || resp != nil { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - confReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - expected := map[string]interface{}{ - "address": connData["address"].(string), - "max_token_name_length": 0, - "ca_cert": "", - "client_cert": "", - } - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) - } - - nomadClient, err := svccfg.Client() - if err != nil { - t.Fatalf("failed to construct nomaad client, %v", err) - } - - token, _, err := nomadClient.ACLTokens().Bootstrap(nil) - if err == nil { - t.Fatalf("expected acl system to be bootstrapped already, but was able to get the bootstrap token : %v", token) - } - // NOTE: fragile test, but it's the only way, AFAIK, to check that nomad is - // bootstrapped - if !strings.Contains(err.Error(), "bootstrap already done") { - t.Fatalf("expected acl system to be bootstrapped already: err: %v", err) - } -} - -func TestBackend_config_access(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, svccfg := prepareTestContainer(t, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": svccfg.URL().String(), - "token": svccfg.Token, - } - - confReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/access", - Storage: config.StorageView, - Data: connData, - } - - resp, err := b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) || resp != nil { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - confReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - expected := map[string]interface{}{ - "address": connData["address"].(string), - "max_token_name_length": 0, - "ca_cert": "", - "client_cert": "", - } - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) - } - if resp.Data["token"] != nil { - t.Fatalf("token should not be set in the response") - } -} - -func TestBackend_config_access_with_certs(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, svccfg := prepareTestContainer(t, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": svccfg.URL().String(), - "token": svccfg.Token, - "ca_cert": caCert, - "client_cert": clientCert, - "client_key": clientKey, - } - - confReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/access", - Storage: config.StorageView, - Data: connData, - } - - resp, err := b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) || resp != nil { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - confReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - expected := map[string]interface{}{ - "address": connData["address"].(string), - "max_token_name_length": 0, - "ca_cert": caCert, - "client_cert": clientCert, - } - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) - } - if resp.Data["token"] != nil { - t.Fatalf("token should not be set in the response") - } -} - -func TestBackend_renew_revoke(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, svccfg := prepareTestContainer(t, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": svccfg.URL().String(), - "token": svccfg.Token, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Path = "role/test" - req.Data = map[string]interface{}{ - "policies": []string{"policy"}, - "lease": "6h", - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.ReadOperation - req.Path = "creds/test" - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - var d struct { - Token string `mapstructure:"secret_id"` - Accessor string `mapstructure:"accessor_id"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - t.Logf("[WARN] Generated token: %s with accessor %s", d.Token, d.Accessor) - - // Build a client and verify that the credentials work - nomadapiConfig := nomadapi.DefaultConfig() - nomadapiConfig.Address = connData["address"].(string) - nomadapiConfig.SecretID = d.Token - client, err := nomadapi.NewClient(nomadapiConfig) - if err != nil { - t.Fatal(err) - } - - t.Log("[WARN] Verifying that the generated token works...") - _, err = client.Agent().Members, nil - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.RenewOperation - req.Secret = generatedSecret - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - - req.Operation = logical.RevokeOperation - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Build a management client and verify that the token does not exist anymore - nomadmgmtConfig := nomadapi.DefaultConfig() - nomadmgmtConfig.Address = connData["address"].(string) - nomadmgmtConfig.SecretID = connData["token"].(string) - mgmtclient, err := nomadapi.NewClient(nomadmgmtConfig) - if err != nil { - t.Fatal(err) - } - - q := &nomadapi.QueryOptions{ - Namespace: "default", - } - - t.Log("[WARN] Verifying that the generated token does not exist...") - _, _, err = mgmtclient.ACLTokens().Info(d.Accessor, q) - if err == nil { - t.Fatal("err: expected error") - } -} - -func TestBackend_CredsCreateEnvVar(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, svccfg := prepareTestContainer(t, true) - defer cleanup() - - req := logical.TestRequest(t, logical.UpdateOperation, "role/test") - req.Data = map[string]interface{}{ - "policies": []string{"policy"}, - "lease": "6h", - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - os.Setenv("NOMAD_TOKEN", svccfg.Token) - defer os.Unsetenv("NOMAD_TOKEN") - os.Setenv("NOMAD_ADDR", svccfg.URL().String()) - defer os.Unsetenv("NOMAD_ADDR") - - req.Operation = logical.ReadOperation - req.Path = "creds/test" - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } -} - -func TestBackend_max_token_name_length(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, svccfg := prepareTestContainer(t, true) - defer cleanup() - - testCases := []struct { - title string - roleName string - tokenLength int - }{ - { - title: "Default", - }, - { - title: "ConfigOverride", - tokenLength: 64, - }, - { - title: "ConfigOverride-LongName", - roleName: "testlongerrolenametoexceed64charsdddddddddddddddddddddddd", - tokenLength: 64, - }, - { - title: "Notrim", - roleName: "testlongersubrolenametoexceed64charsdddddddddddddddddddddddd", - }, - } - - for _, tc := range testCases { - t.Run(tc.title, func(t *testing.T) { - // setup config/access - connData := map[string]interface{}{ - "address": svccfg.URL().String(), - "token": svccfg.Token, - "max_token_name_length": tc.tokenLength, - } - expected := map[string]interface{}{ - "address": svccfg.URL().String(), - "max_token_name_length": tc.tokenLength, - "ca_cert": "", - "client_cert": "", - } - - expectedMaxTokenNameLength := maxTokenNameLength - if tc.tokenLength != 0 { - expectedMaxTokenNameLength = tc.tokenLength - } - - confReq := logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/access", - Storage: config.StorageView, - Data: connData, - } - - resp, err := b.HandleRequest(context.Background(), &confReq) - if err != nil || (resp != nil && resp.IsError()) || resp != nil { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - confReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), &confReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - // verify token length is returned in the config/access query - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) - } - // verify token is not returned - if resp.Data["token"] != nil { - t.Fatalf("token should not be set in the response") - } - - // create a role to create nomad credentials with - // Seeds random with current timestamp - - if tc.roleName == "" { - tc.roleName = "test" - } - roleTokenName := testhelpers.RandomWithPrefix(tc.roleName) - - confReq.Path = "role/" + roleTokenName - confReq.Operation = logical.UpdateOperation - confReq.Data = map[string]interface{}{ - "policies": []string{"policy"}, - "lease": "6h", - } - resp, err = b.HandleRequest(context.Background(), &confReq) - if err != nil { - t.Fatal(err) - } - - confReq.Operation = logical.ReadOperation - confReq.Path = "creds/" + roleTokenName - resp, err = b.HandleRequest(context.Background(), &confReq) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - // extract the secret, so we can query nomad directly - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - var d struct { - Token string `mapstructure:"secret_id"` - Accessor string `mapstructure:"accessor_id"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - // Build a client and verify that the credentials work - nomadapiConfig := nomadapi.DefaultConfig() - nomadapiConfig.Address = connData["address"].(string) - nomadapiConfig.SecretID = d.Token - client, err := nomadapi.NewClient(nomadapiConfig) - if err != nil { - t.Fatal(err) - } - - // default query options for Nomad queries ... not sure if needed - qOpts := &nomadapi.QueryOptions{ - Namespace: "default", - } - - // connect to Nomad and verify the token name does not exceed the - // max_token_name_length - token, _, err := client.ACLTokens().Self(qOpts) - if err != nil { - t.Fatal(err) - } - - if len(token.Name) > expectedMaxTokenNameLength { - t.Fatalf("token name exceeds max length (%d): %s (%d)", expectedMaxTokenNameLength, token.Name, len(token.Name)) - } - }) - } -} - -const caCert = `-----BEGIN CERTIFICATE----- -MIIF7zCCA9egAwIBAgIINVVQic4bju8wDQYJKoZIhvcNAQELBQAwaDELMAkGA1UE -BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS0zODQzMDY2 -NDA5ODI5MjQwNTU5MSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29t -MB4XDTIyMDYwMjIxMTgxN1oXDTIzMDcwNTIxMTgxN1owaDELMAkGA1UEBhMCVVMx -FDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS0zODQzMDY2NDA5ODI5 -MjQwNTU5MSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29tMIICIjAN -BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA35VilgfqMUKhword7wORXRFyPbpz -8uqO7eRaylMnkAkbk5eoQB/iYfXjJ6ZBs5mJGQVz5ZNvh9EzZsk1J6wqYgbwVKUx -fh4kvW6sXtDirtb4ZQAK7OTLEoapUQGnGcvm+aEYfvC1sTBl4fbex7yyN5FYMJTM -TAUumhdq2pwujaj2xkN9DwZa89Tk7tbj9HE9DTRji7bnciEtrmTAOIOfOrT/1l3x -YW1BwYXpQ0TamJ58pC/iNgEp5FAxKt9d3RggesMA7pvG/f8fNgsa/Tku/PeEXNPA -+Yx4CcAipujmqpBKiKwJ6TOzp80m2zrZ7Da4Av5vVS5GsNJxhFYD1h8hU1ptK9BS -2CaTwBpV421C9BfEmtSAksGDIWYujfiHb6XNaQrt8Hu85GBuPUudVn0lpoXLn2xD -rGK8WEK2gWZ4eez3ZDLbpLui6c1m7AVlMtj374s+LHcD7JIxY475Na7pXmEWReqM -RUyCEq1spOOn70fOdhphhmpY6DoklOTOriPawCLNmkPWRnhrIwqyP1gse9YMqQ2n -LhWUkv/08m/0pb4e5ijVhsZNzv+1PXPWCk968nzt0BMDgJT+0ZiXsaU7FILXuo7Y -Ijgrj7dpXWx2MBdMGPFQdveog7Pa80Yb7r4ERW0DL78TxYC6m/S1p14PHwZpDZzQ -LrPrBcpI5XzI7osCAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAqQwDAYDVR0TBAUw -AwEB/zA0BgNVHR4ELTAroCkwG4IZeHBzMTUubG9jYWwuY2lwaGVyYm95LmNvbTAK -hwh/AAAB/wAAADAkBgNVHREEHTAbghl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29t -MB0GA1UdDgQWBBR3bHgDp5RpzerMKRkaGDFN/ZeImjANBgkqhkiG9w0BAQsFAAOC -AgEArkuDYYWYHYxIoTeZkQz5L1y0H27ZWPJx5jBBuktPonDLQxBGAwkl6NZbJGLU -v+usII+eyjPKIgjhCiTXJAmeIngwWoN3PHOLMIPe9axuNt6qVoP4dQtzfpPR3buK -CWj9i3H0ixK73klk7QWZiBUDinYfEMSNRpU3G7NsqmqCXD4s5gB+8y9c7+zIiJyN -IaJBWpzI4eQBi/4cBhtM7Xa+CMB/8whhWYR6H+GXGZdNcP5f7bwneMstWKceTadk -IEzFucJHDySpEkIA2A9t33pV54FmEp+JVwvxAH4FABCnjPmhg0j1IonWV5pySWpG -hhEZpnRRH1XfpTA5i6dlyUA5DJjL8X1lYrgOK+LaoR52mQh5JBsMoVHFzN50DiMA -RTsbq4Qzozf23hU1BqW4NOzPTukgSGEcbT/DhXKPPPLL8JD0rPelJPq76X3TJjgZ -C9uMnZaDnxjppDXp5oBIXqC05FDxJ5sSODNOpKGyuzOU2qQLMau33yYOgaSAttBk -r29+LNFJ+0QzMuPjYXPznpxbsI+lrlZ3F2tDGGs8+JVceC1YX+cBEsEOiqNGTIip -/DY3b9gu5oiTwhcFyQW8+WFsirRS/g5t+M40WLKVPdK09z96krFXQMkL6a7LHLY1 -n9ivwj+sTG1XmJYXp8naLg4wdzIUf2fJxaFNI5Yq4elZ8sY= ------END CERTIFICATE-----` - -const clientCert = `-----BEGIN CERTIFICATE----- -MIIEsDCCApigAwIBAgIIRY1JBRIynFYwDQYJKoZIhvcNAQELBQAwaDELMAkGA1UE -BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS0zODQzMDY2 -NDA5ODI5MjQwNTU5MSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29t -MB4XDTIyMDYwMjIxMTgxOFoXDTIzMDcwNTIxMTgxOFowRzELMAkGA1UEBhMCVVMx -FDASBgNVBAoMC1Vuc3BlY2lmaWVkMSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBo -ZXJib3kuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+XYhsW2 -vTwN7gY3xMxgbNN8d3aoeqCswOp05BBf0Vgv3febahm422ubXXd5Mg2UGiU7sJVe -4tUpDeupVVRX5Qr/hpiXgEyfRDAAAJKqrl65KSS62TCbT/eJZ0ah25HV1evI4uM2 -0kl5QWhtQjDyaVlTS38YFqXXQvpOuU5DG6UbKnpMcpsCPTyUKEJvJ95ZLcz0HJ8I -kIHrnX0Lt0pOhkllj5Nk4cXhU8CFk8IGNz7SVAycrUsffAUMNNEbrIOIfOTPHR1c -q3X9hO4/5pt80uIDMFwwumoA7nQR0AhlKkw9SskCIzJhKwKwssQY7fmovNG0fOEd -/+vSHK7OsYW+gwIDAQABo38wfTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYI -KwYBBQUHAwIwCQYDVR0TBAIwADAqBgNVHREEIzAhghl4cHMxNS5sb2NhbC5jaXBo -ZXJib3kuY29thwR/AAABMB8GA1UdIwQYMBaAFHdseAOnlGnN6swpGRoYMU39l4ia -MA0GCSqGSIb3DQEBCwUAA4ICAQBUSP4ZJglCCrYkM5Le7McdvfkM5uYv1aQn0sM4 -gbyDEWO0fnv50vLpD3y4ckgHLoD52pAZ0hN8a7rwAUae21GA6DvEchSH5x/yvJiS -7FBlq39sAafe03ZlzDErNYJRkLcnPAqG74lJ1SSsMcs9gCPHM8R7HtNnhAga06L7 -K8/G43dsGZCmEb+xcX2B9McCt8jBG6TJPTGafb3BJ0JTmR/tHdoLFIiNwI+qzd2U -lMnGlkIApULX8tmIMsWO0rjdiFkPWGcmfn9ChC0iDpQOAcKSDBcZlWrDNpzKk0mK -l0TbE6cxcmCUUpiwaXFrbkwVWQw4W0c4b3sWFtWifFbiR1qZ/OT2Y2sHbkbxwvPl -PjjXMDBAdRRwtNcTP1E55I5zvwzzBxUpxOob0miorhTJrZR9So0rgv7Roce4ED6M -WETYa/mGhe+Q7gBQygIVoryfQLgGBsHC+7V4RDvYTazwZkz9nLQxHLI/TAZU5ofM -WqdoUkMd68rxTTEUoMfGbftxjKA0raxGcO7/PjLR3O743EwCqeqYJ7OKWgGRLnui -kIKNUJlZ9umURUFzL++Bx4Pr95jWXb2WYqYYQxhDz0oR5q5smnFm5+/1/MLDMvDU -TrgBK6pey4QF33B/I55H1+7tGdv85Q57Z8UrNi/IQxR2sFlsOTeCwStpBQ56sdZk -Wi4+cQ== ------END CERTIFICATE-----` - -const clientKey = `-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCz5diGxba9PA3u -BjfEzGBs03x3dqh6oKzA6nTkEF/RWC/d95tqGbjba5tdd3kyDZQaJTuwlV7i1SkN -66lVVFflCv+GmJeATJ9EMAAAkqquXrkpJLrZMJtP94lnRqHbkdXV68ji4zbSSXlB -aG1CMPJpWVNLfxgWpddC+k65TkMbpRsqekxymwI9PJQoQm8n3lktzPQcnwiQgeud -fQu3Sk6GSWWPk2ThxeFTwIWTwgY3PtJUDJytSx98BQw00Rusg4h85M8dHVyrdf2E -7j/mm3zS4gMwXDC6agDudBHQCGUqTD1KyQIjMmErArCyxBjt+ai80bR84R3/69Ic -rs6xhb6DAgMBAAECggEAPBcja2kxcCZWNNKo4DiwYMmHwtPE1SlEazAlmWSKzP+b -BZbGt/sdj1VzURYuSnTUqqMTPBm41yYCj57PMix5K42v6sKfoIB3lqw94/MZxiLn -0IFvVErzJhP2NqQWPqSI++rFcFwbHMTkFuAN1tVIs73dn9M1NaNxsvKvRyCIM/wz -5YQSDyTkdW4jQM2RvUFOoqwmeyAlQoBRMgQ4bHfLHxmPEjFgw1MAmmG8bJdkupin -MVzhZyKj4Fh80Xa2MU4KokijjG41hmYbg/sjNHaHJFDA92Rwq13dhWytrauJDxa/ -3yj8pHWc23Y3hXvRAf/cibDVzXmmLj49W1i06KuUCQKBgQDj5yF/DJV0IOkhfbol -+f5AGH4ZrEXA/JwA5SxHU+aKhUuPEqK/LeUWqiy3szFjOz2JOnCC0LMN42nsmMyK -sdQEKHp2SPd2wCxsAKZAuxrEi6yBt1mEPFFU5yzvZbdMqYChKJjm9fbRHtuc63s8 -PyVw67Ii9o4ij+PxfTobIs18xwKBgQDKE59w3uUDt2uoqNC8x4m5onL2p2vtcTHC -CxU57mu1+9CRM8N2BEp2VI5JaXjqt6W4u9ISrmOqmsPgTwosAquKpA/nu3bVvR9g -WlN9dh2Xgza0/AFaA9CB++ier8RJq5xFlcasMUmgkhYt3zgKNgRDfjfREWM0yamm -P++hAYRcZQKBgHEuYQk6k6J3ka/rQ54GmEj2oPFZB88+5K7hIWtO9IhIiGzGYYK2 -ZTYrT0fvuxA/5GCZYDTnNnUoQnuYqsQaamOiQqcpt5QG/kiozegJw9JmV0aYauFs -HyweHsfJaQ2uhE4E3mKdNnVGcORuYeZaqdp5gx8v+QibEyXj/g5p60kTAoGBALKp -TMOHXmW9yqKwtvThWoRU+13WQlcJSFvuXpL8mCCrBgkLAhqaypb6RV7ksLKdMhk1 -fhNkOdxBv0LXvv+QUMhgK2vP084/yrjuw3hecOVfboPvduZ2DuiNp2p9rocQAjeH -p8LgRN+Bqbhe7fYhMf3WX1UqEVM/pQ3G43+vjq39AoGAOyD2/hFSIx6BMddUNTHG -BEsMUc/DHYslZebbF1zAWnkKdTt+URhtHAFB2tYRDgkZfwW+wr/w12dJTIkX965o -HO7tI4FgpU9b0i8FTuwYkBfjwp2j0Xd2/VBR8Qpd17qKl3I6NXDsf3ykjGZAvldH -Tll+qwEZpXSRa5OWWTpGV8I= ------END PRIVATE KEY-----` diff --git a/builtin/logical/pki/acme_billing_test.go b/builtin/logical/pki/acme_billing_test.go deleted file mode 100644 index b17a3492e..000000000 --- a/builtin/logical/pki/acme_billing_test.go +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "crypto/x509/pkix" - "encoding/json" - "strings" - "testing" - "time" - - "golang.org/x/crypto/acme" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/logical/pki/dnstest" - "github.com/hashicorp/vault/helper/constants" - "github.com/hashicorp/vault/helper/timeutil" - "github.com/hashicorp/vault/vault" - "github.com/hashicorp/vault/vault/activity" - - "github.com/stretchr/testify/require" -) - -// TestACMEBilling is a basic test that will validate client counts created via ACME workflows. -func TestACMEBilling(t *testing.T) { - t.Parallel() - timeutil.SkipAtEndOfMonth(t) - - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - dns := dnstest.SetupResolver(t, "dadgarcorp.com") - defer dns.Cleanup() - - // Enable additional mounts. - setupAcmeBackendOnClusterAtPath(t, cluster, client, "pki2") - setupAcmeBackendOnClusterAtPath(t, cluster, client, "ns1/pki") - setupAcmeBackendOnClusterAtPath(t, cluster, client, "ns2/pki") - - // Enable custom DNS resolver for testing. - for _, mount := range []string{"pki", "pki2", "ns1/pki", "ns2/pki"} { - _, err := client.Logical().Write(mount+"/config/acme", map[string]interface{}{ - "dns_resolver": dns.GetLocalAddr(), - }) - require.NoError(t, err, "failed to set local dns resolver address for testing on mount: "+mount) - } - - // Enable client counting. - _, err := client.Logical().Write("/sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - require.NoError(t, err, "failed to enable client counting") - - // Setup ACME clients. We refresh account keys each time for consistency. - acmeClientPKI := getAcmeClientForCluster(t, cluster, "/v1/pki/acme/", nil) - acmeClientPKI2 := getAcmeClientForCluster(t, cluster, "/v1/pki2/acme/", nil) - acmeClientPKINS1 := getAcmeClientForCluster(t, cluster, "/v1/ns1/pki/acme/", nil) - acmeClientPKINS2 := getAcmeClientForCluster(t, cluster, "/v1/ns2/pki/acme/", nil) - - // Get our initial count. - expectedCount := validateClientCount(t, client, "", -1, "initial fetch") - - // Unique identifier: should increase by one. - doACMEForDomainWithDNS(t, dns, acmeClientPKI, []string{"dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "pki", expectedCount+1, "new certificate") - - // Different identifier; should increase by one. - doACMEForDomainWithDNS(t, dns, acmeClientPKI, []string{"example.dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "pki", expectedCount+1, "new certificate") - - // While same identifiers, used together and so thus are unique; increase by one. - doACMEForDomainWithDNS(t, dns, acmeClientPKI, []string{"example.dadgarcorp.com", "dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "pki", expectedCount+1, "new certificate") - - // Same identifiers in different order are not unique; keep the same. - doACMEForDomainWithDNS(t, dns, acmeClientPKI, []string{"dadgarcorp.com", "example.dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "pki", expectedCount, "different order; same identifiers") - - // Using a different mount shouldn't affect counts. - doACMEForDomainWithDNS(t, dns, acmeClientPKI2, []string{"dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "", expectedCount, "different mount; same identifiers") - - // But using a different identifier should. - doACMEForDomainWithDNS(t, dns, acmeClientPKI2, []string{"pki2.dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "pki2", expectedCount+1, "different mount with different identifiers") - - // A new identifier in a unique namespace will affect results. - doACMEForDomainWithDNS(t, dns, acmeClientPKINS1, []string{"unique.dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "ns1/pki", expectedCount+1, "unique identifier in a namespace") - - // But in a different namespace with the existing identifier will not. - doACMEForDomainWithDNS(t, dns, acmeClientPKINS2, []string{"unique.dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "", expectedCount, "existing identifier in a namespace") - doACMEForDomainWithDNS(t, dns, acmeClientPKI2, []string{"unique.dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "", expectedCount, "existing identifier outside of a namespace") - - // Creating a unique identifier in a namespace with a mount with the - // same name as another namespace should increase counts as well. - doACMEForDomainWithDNS(t, dns, acmeClientPKINS2, []string{"very-unique.dadgarcorp.com"}) - expectedCount = validateClientCount(t, client, "ns2/pki", expectedCount+1, "unique identifier in a different namespace") - - // Check the current fragment - fragment := cluster.Cores[0].Core.ResetActivityLog()[0] - if fragment == nil { - t.Fatal("no fragment created") - } - validateAcmeClientTypes(t, fragment, expectedCount) -} - -func validateAcmeClientTypes(t *testing.T, fragment *activity.LogFragment, expectedCount int64) { - t.Helper() - if int64(len(fragment.Clients)) != expectedCount { - t.Fatalf("bad number of entities, expected %v: got %v, entities are: %v", expectedCount, len(fragment.Clients), fragment.Clients) - } - - for _, ac := range fragment.Clients { - if ac.ClientType != vault.ACMEActivityType { - t.Fatalf("Couldn't find expected '%v' client_type in %v", vault.ACMEActivityType, fragment.Clients) - } - } -} - -func validateClientCount(t *testing.T, client *api.Client, mount string, expected int64, message string) int64 { - resp, err := client.Logical().Read("/sys/internal/counters/activity/monthly") - require.NoError(t, err, "failed to fetch client count values") - t.Logf("got client count numbers: %v", resp) - - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.Contains(t, resp.Data, "non_entity_clients") - require.Contains(t, resp.Data, "months") - - rawCount := resp.Data["non_entity_clients"].(json.Number) - count, err := rawCount.Int64() - require.NoError(t, err, "failed to parse number as int64: "+rawCount.String()) - - if expected != -1 { - require.Equal(t, expected, count, "value of client counts did not match expectations: "+message) - } - - if mount == "" { - return count - } - - months := resp.Data["months"].([]interface{}) - if len(months) > 1 { - t.Fatalf("running across a month boundary despite using SkipAtEndOfMonth(...); rerun test from start fully in the next month instead") - } - - require.Equal(t, 1, len(months), "expected only a single month when running this test") - - monthlyInfo := months[0].(map[string]interface{}) - - // Validate this month's aggregate counts match the overall value. - require.Contains(t, monthlyInfo, "counts", "expected monthly info to contain a count key") - monthlyCounts := monthlyInfo["counts"].(map[string]interface{}) - require.Contains(t, monthlyCounts, "non_entity_clients", "expected month[0].counts to contain a non_entity_clients key") - monthlyCountNonEntityRaw := monthlyCounts["non_entity_clients"].(json.Number) - monthlyCountNonEntity, err := monthlyCountNonEntityRaw.Int64() - require.NoError(t, err, "failed to parse number as int64: "+monthlyCountNonEntityRaw.String()) - require.Equal(t, count, monthlyCountNonEntity, "expected equal values for non entity client counts") - - // Validate this mount's namespace is included in the namespaces list, - // if this is enterprise. Otherwise, if its OSS or we don't have a - // namespace, we default to the value root. - mountNamespace := "" - mountPath := mount + "/" - if constants.IsEnterprise && strings.Contains(mount, "/") { - pieces := strings.Split(mount, "/") - require.Equal(t, 2, len(pieces), "we do not support nested namespaces in this test") - mountNamespace = pieces[0] + "/" - mountPath = pieces[1] + "/" - } - - require.Contains(t, monthlyInfo, "namespaces", "expected monthly info to contain a namespaces key") - monthlyNamespaces := monthlyInfo["namespaces"].([]interface{}) - foundNamespace := false - for index, namespaceRaw := range monthlyNamespaces { - namespace := namespaceRaw.(map[string]interface{}) - require.Contains(t, namespace, "namespace_path", "expected monthly.namespaces[%v] to contain a namespace_path key", index) - namespacePath := namespace["namespace_path"].(string) - - if namespacePath != mountNamespace { - t.Logf("skipping non-matching namespace %v: %v != %v / %v", index, namespacePath, mountNamespace, namespace) - continue - } - - foundNamespace = true - - // This namespace must have a non-empty aggregate non-entity count. - require.Contains(t, namespace, "counts", "expected monthly.namespaces[%v] to contain a counts key", index) - namespaceCounts := namespace["counts"].(map[string]interface{}) - require.Contains(t, namespaceCounts, "non_entity_clients", "expected namespace counts to contain a non_entity_clients key") - namespaceCountNonEntityRaw := namespaceCounts["non_entity_clients"].(json.Number) - namespaceCountNonEntity, err := namespaceCountNonEntityRaw.Int64() - require.NoError(t, err, "failed to parse number as int64: "+namespaceCountNonEntityRaw.String()) - require.Greater(t, namespaceCountNonEntity, int64(0), "expected at least one non-entity client count value in the namespace") - - require.Contains(t, namespace, "mounts", "expected monthly.namespaces[%v] to contain a mounts key", index) - namespaceMounts := namespace["mounts"].([]interface{}) - foundMount := false - for mountIndex, mountRaw := range namespaceMounts { - mountInfo := mountRaw.(map[string]interface{}) - require.Contains(t, mountInfo, "mount_path", "expected monthly.namespaces[%v].mounts[%v] to contain a mount_path key", index, mountIndex) - mountInfoPath := mountInfo["mount_path"].(string) - if mountPath != mountInfoPath { - t.Logf("skipping non-matching mount path %v in namespace %v: %v != %v / %v of %v", mountIndex, index, mountPath, mountInfoPath, mountInfo, namespace) - continue - } - - foundMount = true - - // This mount must also have a non-empty non-entity client count. - require.Contains(t, mountInfo, "counts", "expected monthly.namespaces[%v].mounts[%v] to contain a counts key", index, mountIndex) - mountCounts := mountInfo["counts"].(map[string]interface{}) - require.Contains(t, mountCounts, "non_entity_clients", "expected mount counts to contain a non_entity_clients key") - mountCountNonEntityRaw := mountCounts["non_entity_clients"].(json.Number) - mountCountNonEntity, err := mountCountNonEntityRaw.Int64() - require.NoError(t, err, "failed to parse number as int64: "+mountCountNonEntityRaw.String()) - require.Greater(t, mountCountNonEntity, int64(0), "expected at least one non-entity client count value in the mount") - } - - require.True(t, foundMount, "expected to find the mount "+mountPath+" in the list of mounts for namespace, but did not") - } - - require.True(t, foundNamespace, "expected to find the namespace "+mountNamespace+" in the list of namespaces, but did not") - - return count -} - -func doACMEForDomainWithDNS(t *testing.T, dns *dnstest.TestServer, acmeClient *acme.Client, domains []string) *x509.Certificate { - cr := &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: domains[0]}, - DNSNames: domains, - } - - return doACMEForCSRWithDNS(t, dns, acmeClient, domains, cr) -} - -func doACMEForCSRWithDNS(t *testing.T, dns *dnstest.TestServer, acmeClient *acme.Client, domains []string, cr *x509.CertificateRequest) *x509.Certificate { - accountKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed to generate account key") - acmeClient.Key = accountKey - - testCtx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancelFunc() - - // Register the client. - _, err = acmeClient.Register(testCtx, &acme.Account{Contact: []string{"mailto:ipsans@dadgarcorp.com"}}, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - - // Create the Order - var orderIdentifiers []acme.AuthzID - for _, domain := range domains { - orderIdentifiers = append(orderIdentifiers, acme.AuthzID{Type: "dns", Value: domain}) - } - order, err := acmeClient.AuthorizeOrder(testCtx, orderIdentifiers) - require.NoError(t, err, "failed creating ACME order") - - // Fetch its authorizations. - var auths []*acme.Authorization - for _, authUrl := range order.AuthzURLs { - authorization, err := acmeClient.GetAuthorization(testCtx, authUrl) - require.NoError(t, err, "failed to lookup authorization at url: %s", authUrl) - auths = append(auths, authorization) - } - - // For each dns-01 challenge, place the record in the associated DNS resolver. - var challengesToAccept []*acme.Challenge - for _, auth := range auths { - for _, challenge := range auth.Challenges { - if challenge.Status != acme.StatusPending { - t.Logf("ignoring challenge not in status pending: %v", challenge) - continue - } - - if challenge.Type == "dns-01" { - challengeBody, err := acmeClient.DNS01ChallengeRecord(challenge.Token) - require.NoError(t, err, "failed generating challenge response") - - dns.AddRecord("_acme-challenge."+auth.Identifier.Value, "TXT", challengeBody) - defer dns.RemoveRecord("_acme-challenge."+auth.Identifier.Value, "TXT", challengeBody) - - require.NoError(t, err, "failed setting DNS record") - - challengesToAccept = append(challengesToAccept, challenge) - } - } - } - - dns.PushConfig() - require.GreaterOrEqual(t, len(challengesToAccept), 1, "Need at least one challenge, got none") - - // Tell the ACME server, that they can now validate those challenges. - for _, challenge := range challengesToAccept { - _, err = acmeClient.Accept(testCtx, challenge) - require.NoError(t, err, "failed to accept challenge: %v", challenge) - } - - // Wait for the order/challenges to be validated. - _, err = acmeClient.WaitOrder(testCtx, order.URI) - require.NoError(t, err, "failed waiting for order to be ready") - - // Create/sign the CSR and ask ACME server to sign it returning us the final certificate - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - csr, err := x509.CreateCertificateRequest(rand.Reader, cr, csrKey) - require.NoError(t, err, "failed generating csr") - - certs, _, err := acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, false) - require.NoError(t, err, "failed to get a certificate back from ACME") - - acmeCert, err := x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert bytes") - - return acmeCert -} diff --git a/builtin/logical/pki/acme_challenges_test.go b/builtin/logical/pki/acme_challenges_test.go deleted file mode 100644 index 591486d69..000000000 --- a/builtin/logical/pki/acme_challenges_test.go +++ /dev/null @@ -1,759 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/base64" - "fmt" - "math/big" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - "time" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/builtin/logical/pki/dnstest" - - "github.com/stretchr/testify/require" -) - -type keyAuthorizationTestCase struct { - keyAuthz string - token string - thumbprint string - shouldFail bool -} - -var keyAuthorizationTestCases = []keyAuthorizationTestCase{ - { - // Entirely empty - "", - "non-empty-token", - "non-empty-thumbprint", - true, - }, - { - // Both empty - ".", - "non-empty-token", - "non-empty-thumbprint", - true, - }, - { - // Not equal - "non-.non-", - "non-empty-token", - "non-empty-thumbprint", - true, - }, - { - // Empty thumbprint - "non-.", - "non-empty-token", - "non-empty-thumbprint", - true, - }, - { - // Empty token - ".non-", - "non-empty-token", - "non-empty-thumbprint", - true, - }, - { - // Wrong order - "non-empty-thumbprint.non-empty-token", - "non-empty-token", - "non-empty-thumbprint", - true, - }, - { - // Too many pieces - "one.two.three", - "non-empty-token", - "non-empty-thumbprint", - true, - }, - { - // Valid - "non-empty-token.non-empty-thumbprint", - "non-empty-token", - "non-empty-thumbprint", - false, - }, -} - -func TestAcmeValidateKeyAuthorization(t *testing.T) { - t.Parallel() - - for index, tc := range keyAuthorizationTestCases { - t.Run("subtest-"+strconv.Itoa(index), func(st *testing.T) { - isValid, err := ValidateKeyAuthorization(tc.keyAuthz, tc.token, tc.thumbprint) - if !isValid && err == nil { - st.Fatalf("[%d] expected failure to give reason via err (%v / %v)", index, isValid, err) - } - - expectedValid := !tc.shouldFail - if expectedValid != isValid { - st.Fatalf("[%d] got ret=%v, expected ret=%v (shouldFail=%v)", index, isValid, expectedValid, tc.shouldFail) - } - }) - } -} - -func TestAcmeValidateHTTP01Challenge(t *testing.T) { - t.Parallel() - - for index, tc := range keyAuthorizationTestCases { - validFunc := func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(tc.keyAuthz)) - } - withPadding := func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(" " + tc.keyAuthz + " ")) - } - withRedirect := func(w http.ResponseWriter, r *http.Request) { - if strings.Contains(r.URL.Path, "/.well-known/") { - http.Redirect(w, r, "/my-http-01-challenge-response", 301) - return - } - - w.Write([]byte(tc.keyAuthz)) - } - withSleep := func(w http.ResponseWriter, r *http.Request) { - // Long enough to ensure any excessively short timeouts are hit, - // not long enough to trigger a failure (hopefully). - time.Sleep(5 * time.Second) - w.Write([]byte(tc.keyAuthz)) - } - - validHandlers := []http.HandlerFunc{ - http.HandlerFunc(validFunc), http.HandlerFunc(withPadding), - http.HandlerFunc(withRedirect), http.HandlerFunc(withSleep), - } - - for handlerIndex, handler := range validHandlers { - func() { - ts := httptest.NewServer(handler) - defer ts.Close() - - host := ts.URL[7:] - isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{}) - if !isValid && err == nil { - t.Fatalf("[tc=%d/handler=%d] expected failure to give reason via err (%v / %v)", index, handlerIndex, isValid, err) - } - - expectedValid := !tc.shouldFail - if expectedValid != isValid { - t.Fatalf("[tc=%d/handler=%d] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, handlerIndex, isValid, err, expectedValid, tc.shouldFail) - } - }() - } - } - - // Negative test cases for various HTTP-specific scenarios. - redirectLoop := func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/my-http-01-challenge-response", 301) - } - publicRedirect := func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "http://hashicorp.com/", 301) - } - noData := func(w http.ResponseWriter, r *http.Request) {} - noContent := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNoContent) - } - notFound := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - } - simulateHang := func(w http.ResponseWriter, r *http.Request) { - time.Sleep(30 * time.Second) - w.Write([]byte("my-token.my-thumbprint")) - } - tooLarge := func(w http.ResponseWriter, r *http.Request) { - for i := 0; i < 512; i++ { - w.Write([]byte("my-token.my-thumbprint\n")) - } - } - - validHandlers := []http.HandlerFunc{ - http.HandlerFunc(redirectLoop), http.HandlerFunc(publicRedirect), - http.HandlerFunc(noData), http.HandlerFunc(noContent), - http.HandlerFunc(notFound), http.HandlerFunc(simulateHang), - http.HandlerFunc(tooLarge), - } - for handlerIndex, handler := range validHandlers { - func() { - ts := httptest.NewServer(handler) - defer ts.Close() - - host := ts.URL[7:] - isValid, err := ValidateHTTP01Challenge(host, "my-token", "my-thumbprint", &acmeConfigEntry{}) - if isValid || err == nil { - t.Fatalf("[handler=%d] expected failure validating challenge (%v / %v)", handlerIndex, isValid, err) - } - }() - } -} - -func TestAcmeValidateDNS01Challenge(t *testing.T) { - t.Parallel() - - host := "dadgarcorp.com" - resolver := dnstest.SetupResolver(t, host) - defer resolver.Cleanup() - - t.Logf("DNS Server Address: %v", resolver.GetLocalAddr()) - - config := &acmeConfigEntry{ - DNSResolver: resolver.GetLocalAddr(), - } - - for index, tc := range keyAuthorizationTestCases { - checksum := sha256.Sum256([]byte(tc.keyAuthz)) - authz := base64.RawURLEncoding.EncodeToString(checksum[:]) - resolver.AddRecord(DNSChallengePrefix+host, "TXT", authz) - resolver.PushConfig() - - isValid, err := ValidateDNS01Challenge(host, tc.token, tc.thumbprint, config) - if !isValid && err == nil { - t.Fatalf("[tc=%d] expected failure to give reason via err (%v / %v)", index, isValid, err) - } - - expectedValid := !tc.shouldFail - if expectedValid != isValid { - t.Fatalf("[tc=%d] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, isValid, err, expectedValid, tc.shouldFail) - } - - resolver.RemoveAllRecords() - } -} - -func TestAcmeValidateTLSALPN01Challenge(t *testing.T) { - // This test is not parallel because we modify ALPNPort to use a custom - // non-standard port _just for testing purposes_. - host := "localhost" - config := &acmeConfigEntry{} - - log := hclog.L() - - returnedProtocols := []string{ALPNProtocol} - var certificates []*x509.Certificate - var privateKey crypto.PrivateKey - - tlsCfg := &tls.Config{} - tlsCfg.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) { - var retCfg tls.Config = *tlsCfg - retCfg.NextProtos = returnedProtocols - log.Info(fmt.Sprintf("[alpn-server] returned protocol: %v", returnedProtocols)) - return &retCfg, nil - } - tlsCfg.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { - var ret tls.Certificate - for index, cert := range certificates { - ret.Certificate = append(ret.Certificate, cert.Raw) - if index == 0 { - ret.Leaf = cert - } - } - ret.PrivateKey = privateKey - log.Info(fmt.Sprintf("[alpn-server] returned certificates: %v", ret)) - return &ret, nil - } - - ln, err := tls.Listen("tcp", host+":0", tlsCfg) - require.NoError(t, err, "failed to listen with TLS config") - - doOneAccept := func() { - log.Info("[alpn-server] starting accept...") - connRaw, err := ln.Accept() - require.NoError(t, err, "failed to accept TLS connection") - - log.Info("[alpn-server] got connection...") - conn := tls.Server(connRaw.(*tls.Conn), tlsCfg) - - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) - defer func() { - log.Info("[alpn-server] canceling listener connection...") - cancel() - }() - - log.Info("[alpn-server] starting handshake...") - if err := conn.HandshakeContext(ctx); err != nil { - log.Info("[alpn-server] got non-fatal error while handshaking connection: %v", err) - } - - log.Info("[alpn-server] closing connection...") - if err := conn.Close(); err != nil { - log.Info("[alpn-server] got non-fatal error while closing connection: %v", err) - } - } - - ALPNPort = strings.Split(ln.Addr().String(), ":")[1] - - type alpnTestCase struct { - name string - certificates []*x509.Certificate - privateKey crypto.PrivateKey - protocols []string - token string - thumbprint string - shouldFail bool - } - - var alpnTestCases []alpnTestCase - // Add all of our keyAuthorizationTestCases into alpnTestCases - for index, tc := range keyAuthorizationTestCases { - log.Info(fmt.Sprintf("using keyAuthorizationTestCase [tc=%d] as alpnTestCase [tc=%d]...", index, len(alpnTestCases))) - // Properly encode the authorization. - checksum := sha256.Sum256([]byte(tc.keyAuthz)) - authz, err := asn1.Marshal(checksum[:]) - require.NoError(t, err, "failed asn.1 marshalling authz") - - // Build a self-signed certificate. - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generating private key") - tmpl := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: host, - }, - Issuer: pkix.Name{ - CommonName: host, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - PublicKey: key.Public(), - SerialNumber: big.NewInt(1), - DNSNames: []string{host}, - ExtraExtensions: []pkix.Extension{ - { - Id: OIDACMEIdentifier, - Critical: true, - Value: authz, - }, - }, - BasicConstraintsValid: true, - IsCA: false, - } - certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) - require.NoError(t, err, "failed to create certificate") - cert, err := x509.ParseCertificate(certBytes) - require.NoError(t, err, "failed to parse newly generated certificate") - - newTc := alpnTestCase{ - name: fmt.Sprintf("keyAuthorizationTestCase[%d]", index), - certificates: []*x509.Certificate{cert}, - privateKey: key, - protocols: []string{ALPNProtocol}, - token: tc.token, - thumbprint: tc.thumbprint, - shouldFail: tc.shouldFail, - } - alpnTestCases = append(alpnTestCases, newTc) - } - - { - // Test case: Longer chain - // Build a self-signed certificate. - rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generating root private key") - tmpl := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "Root CA", - }, - Issuer: pkix.Name{ - CommonName: "Root CA", - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - PublicKey: rootKey.Public(), - SerialNumber: big.NewInt(1), - BasicConstraintsValid: true, - IsCA: true, - } - rootCertBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootKey.Public(), rootKey) - require.NoError(t, err, "failed to create root certificate") - rootCert, err := x509.ParseCertificate(rootCertBytes) - require.NoError(t, err, "failed to parse newly generated root certificate") - - // Compute our authorization. - checksum := sha256.Sum256([]byte("valid.valid")) - authz, err := asn1.Marshal(checksum[:]) - require.NoError(t, err, "failed to marshal authz with asn.1 ") - - // Build a leaf certificate which _could_ pass validation - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generating leaf private key") - tmpl = &x509.Certificate{ - Subject: pkix.Name{ - CommonName: host, - }, - Issuer: pkix.Name{ - CommonName: "Root CA", - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - PublicKey: key.Public(), - SerialNumber: big.NewInt(2), - DNSNames: []string{host}, - ExtraExtensions: []pkix.Extension{ - { - Id: OIDACMEIdentifier, - Critical: true, - Value: authz, - }, - }, - BasicConstraintsValid: true, - IsCA: false, - } - certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, rootCert, key.Public(), rootKey) - require.NoError(t, err, "failed to create leaf certificate") - cert, err := x509.ParseCertificate(certBytes) - require.NoError(t, err, "failed to parse newly generated leaf certificate") - - newTc := alpnTestCase{ - name: "longer chain with valid leaf", - certificates: []*x509.Certificate{cert, rootCert}, - privateKey: key, - protocols: []string{ALPNProtocol}, - token: "valid", - thumbprint: "valid", - shouldFail: true, - } - alpnTestCases = append(alpnTestCases, newTc) - } - - { - // Test case: cert without DNSSan - // Compute our authorization. - checksum := sha256.Sum256([]byte("valid.valid")) - authz, err := asn1.Marshal(checksum[:]) - require.NoError(t, err, "failed to marshal authz with asn.1 ") - - // Build a leaf certificate without a DNSSan - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generating leaf private key") - tmpl := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: host, - }, - Issuer: pkix.Name{ - CommonName: host, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - PublicKey: key.Public(), - SerialNumber: big.NewInt(2), - // NO DNSNames - ExtraExtensions: []pkix.Extension{ - { - Id: OIDACMEIdentifier, - Critical: true, - Value: authz, - }, - }, - BasicConstraintsValid: true, - IsCA: false, - } - certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) - require.NoError(t, err, "failed to create leaf certificate") - cert, err := x509.ParseCertificate(certBytes) - require.NoError(t, err, "failed to parse newly generated leaf certificate") - - newTc := alpnTestCase{ - name: "valid keyauthz without valid dnsname", - certificates: []*x509.Certificate{cert}, - privateKey: key, - protocols: []string{ALPNProtocol}, - token: "valid", - thumbprint: "valid", - shouldFail: true, - } - alpnTestCases = append(alpnTestCases, newTc) - } - - { - // Test case: cert without matching DNSSan - // Compute our authorization. - checksum := sha256.Sum256([]byte("valid.valid")) - authz, err := asn1.Marshal(checksum[:]) - require.NoError(t, err, "failed to marshal authz with asn.1 ") - - // Build a leaf certificate which fails validation due to bad DNSName - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generating leaf private key") - tmpl := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: host, - }, - Issuer: pkix.Name{ - CommonName: host, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - PublicKey: key.Public(), - SerialNumber: big.NewInt(2), - DNSNames: []string{host + ".dadgarcorp.com" /* not matching host! */}, - ExtraExtensions: []pkix.Extension{ - { - Id: OIDACMEIdentifier, - Critical: true, - Value: authz, - }, - }, - BasicConstraintsValid: true, - IsCA: false, - } - certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) - require.NoError(t, err, "failed to create leaf certificate") - cert, err := x509.ParseCertificate(certBytes) - require.NoError(t, err, "failed to parse newly generated leaf certificate") - - newTc := alpnTestCase{ - name: "valid keyauthz without matching dnsname", - certificates: []*x509.Certificate{cert}, - privateKey: key, - protocols: []string{ALPNProtocol}, - token: "valid", - thumbprint: "valid", - shouldFail: true, - } - alpnTestCases = append(alpnTestCases, newTc) - } - - { - // Test case: cert with additional SAN - // Compute our authorization. - checksum := sha256.Sum256([]byte("valid.valid")) - authz, err := asn1.Marshal(checksum[:]) - require.NoError(t, err, "failed to marshal authz with asn.1 ") - - // Build a leaf certificate which has an invalid additional SAN - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generating leaf private key") - tmpl := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: host, - }, - Issuer: pkix.Name{ - CommonName: host, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - PublicKey: key.Public(), - SerialNumber: big.NewInt(2), - DNSNames: []string{host}, - EmailAddresses: []string{"webmaster@" + host}, /* unexpected */ - ExtraExtensions: []pkix.Extension{ - { - Id: OIDACMEIdentifier, - Critical: true, - Value: authz, - }, - }, - BasicConstraintsValid: true, - IsCA: false, - } - certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) - require.NoError(t, err, "failed to create leaf certificate") - cert, err := x509.ParseCertificate(certBytes) - require.NoError(t, err, "failed to parse newly generated leaf certificate") - - newTc := alpnTestCase{ - name: "valid keyauthz with additional email SANs", - certificates: []*x509.Certificate{cert}, - privateKey: key, - protocols: []string{ALPNProtocol}, - token: "valid", - thumbprint: "valid", - shouldFail: true, - } - alpnTestCases = append(alpnTestCases, newTc) - } - - { - // Test case: cert without CN - // Compute our authorization. - checksum := sha256.Sum256([]byte("valid.valid")) - authz, err := asn1.Marshal(checksum[:]) - require.NoError(t, err, "failed to marshal authz with asn.1 ") - - // Build a leaf certificate which should pass validation - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generating leaf private key") - tmpl := &x509.Certificate{ - Subject: pkix.Name{}, - Issuer: pkix.Name{}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - PublicKey: key.Public(), - SerialNumber: big.NewInt(2), - DNSNames: []string{host}, - ExtraExtensions: []pkix.Extension{ - { - Id: OIDACMEIdentifier, - Critical: true, - Value: authz, - }, - }, - BasicConstraintsValid: true, - IsCA: false, - } - certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) - require.NoError(t, err, "failed to create leaf certificate") - cert, err := x509.ParseCertificate(certBytes) - require.NoError(t, err, "failed to parse newly generated leaf certificate") - - newTc := alpnTestCase{ - name: "valid certificate; no Subject/Issuer (missing CN)", - certificates: []*x509.Certificate{cert}, - privateKey: key, - protocols: []string{ALPNProtocol}, - token: "valid", - thumbprint: "valid", - shouldFail: false, - } - alpnTestCases = append(alpnTestCases, newTc) - } - - { - // Test case: cert without the extension - // Build a leaf certificate which should fail validation - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generating leaf private key") - tmpl := &x509.Certificate{ - Subject: pkix.Name{}, - Issuer: pkix.Name{}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - PublicKey: key.Public(), - SerialNumber: big.NewInt(1), - DNSNames: []string{host}, - BasicConstraintsValid: true, - IsCA: true, - } - certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) - require.NoError(t, err, "failed to create leaf certificate") - cert, err := x509.ParseCertificate(certBytes) - require.NoError(t, err, "failed to parse newly generated leaf certificate") - - newTc := alpnTestCase{ - name: "missing required acmeIdentifier extension", - certificates: []*x509.Certificate{cert}, - privateKey: key, - protocols: []string{ALPNProtocol}, - token: "valid", - thumbprint: "valid", - shouldFail: true, - } - alpnTestCases = append(alpnTestCases, newTc) - } - - { - // Test case: root without a leaf - // Build a self-signed certificate. - rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generating root private key") - tmpl := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "Root CA", - }, - Issuer: pkix.Name{ - CommonName: "Root CA", - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - PublicKey: rootKey.Public(), - SerialNumber: big.NewInt(1), - BasicConstraintsValid: true, - IsCA: true, - } - rootCertBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootKey.Public(), rootKey) - require.NoError(t, err, "failed to create root certificate") - rootCert, err := x509.ParseCertificate(rootCertBytes) - require.NoError(t, err, "failed to parse newly generated root certificate") - - newTc := alpnTestCase{ - name: "root without leaf", - certificates: []*x509.Certificate{rootCert}, - privateKey: rootKey, - protocols: []string{ALPNProtocol}, - token: "valid", - thumbprint: "valid", - shouldFail: true, - } - alpnTestCases = append(alpnTestCases, newTc) - } - - for index, tc := range alpnTestCases { - log.Info(fmt.Sprintf("\n\n[tc=%d/name=%s] starting validation", index, tc.name)) - certificates = tc.certificates - privateKey = tc.privateKey - returnedProtocols = tc.protocols - - // Attempt to validate the challenge. - go doOneAccept() - isValid, err := ValidateTLSALPN01Challenge(host, tc.token, tc.thumbprint, config) - if !isValid && err == nil { - t.Fatalf("[tc=%d/name=%s] expected failure to give reason via err (%v / %v)", index, tc.name, isValid, err) - } - - expectedValid := !tc.shouldFail - if expectedValid != isValid { - t.Fatalf("[tc=%d/name=%s] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, tc.name, isValid, err, expectedValid, tc.shouldFail) - } else if err != nil { - log.Info(fmt.Sprintf("[tc=%d/name=%s] got expected failure: err=%v", index, tc.name, err)) - } - } -} - -// TestAcmeValidateHttp01TLSRedirect verify that we allow a http-01 challenge to redirect -// to a TLS server and not validate the certificate chain is valid. We don't validate the -// TLS chain as we would have accepted the auth over a non-secured channel anyway had -// the original request not redirected us. -func TestAcmeValidateHttp01TLSRedirect(t *testing.T) { - t.Parallel() - - for index, tc := range keyAuthorizationTestCases { - t.Run("subtest-"+strconv.Itoa(index), func(st *testing.T) { - validFunc := func(w http.ResponseWriter, r *http.Request) { - if strings.Contains(r.URL.Path, "/.well-known/") { - w.Write([]byte(tc.keyAuthz)) - return - } - http.Error(w, "status not found", http.StatusNotFound) - } - - tlsTs := httptest.NewTLSServer(http.HandlerFunc(validFunc)) - defer tlsTs.Close() - - // Set up a http server that will redirect to our TLS server - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, tlsTs.URL+r.URL.Path, 301) - })) - defer ts.Close() - - host := ts.URL[len("http://"):] - isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{}) - if !isValid && err == nil { - st.Fatalf("[tc=%d] expected failure to give reason via err (%v / %v)", index, isValid, err) - } - - expectedValid := !tc.shouldFail - if expectedValid != isValid { - st.Fatalf("[tc=%d] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, isValid, err, expectedValid, tc.shouldFail) - } - }) - } -} diff --git a/builtin/logical/pki/acme_state_test.go b/builtin/logical/pki/acme_state_test.go deleted file mode 100644 index ed9586e83..000000000 --- a/builtin/logical/pki/acme_state_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestAcmeNonces(t *testing.T) { - t.Parallel() - - a := NewACMEState() - a.nonces.Initialize() - - // Simple operation should succeed. - nonce, _, err := a.GetNonce() - require.NoError(t, err) - require.NotEmpty(t, nonce) - - require.True(t, a.RedeemNonce(nonce)) - require.False(t, a.RedeemNonce(nonce)) - - // Redeeming in opposite order should work. - var nonces []string - for i := 0; i < len(nonce); i++ { - nonce, _, err = a.GetNonce() - require.NoError(t, err) - require.NotEmpty(t, nonce) - } - - for i := len(nonces) - 1; i >= 0; i-- { - nonce = nonces[i] - require.True(t, a.RedeemNonce(nonce)) - } - - for i := 0; i < len(nonces); i++ { - nonce = nonces[i] - require.False(t, a.RedeemNonce(nonce)) - } -} diff --git a/builtin/logical/pki/acme_wrappers_test.go b/builtin/logical/pki/acme_wrappers_test.go deleted file mode 100644 index f3fb5ef4d..000000000 --- a/builtin/logical/pki/acme_wrappers_test.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" -) - -// TestACMEIssuerRoleLoading validates the role and issuer loading logic within the base -// ACME wrapper is correct. -func TestACMEIssuerRoleLoading(t *testing.T) { - b, s := CreateBackendWithStorage(t) - - _, err := CBWrite(b, s, "config/cluster", map[string]interface{}{ - "path": "http://localhost:8200/v1/pki", - "aia_path": "http://localhost:8200/cdn/pki", - }) - require.NoError(t, err) - - _, err = CBWrite(b, s, "config/acme", map[string]interface{}{ - "enabled": true, - }) - require.NoError(t, err) - - _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault1.com", - "issuer_name": "issuer-1", - "key_type": "ec", - }) - require.NoError(t, err, "failed creating issuer issuer-1") - - _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault2.com", - "issuer_name": "issuer-2", - "key_type": "ec", - }) - require.NoError(t, err, "failed creating issuer issuer-2") - - _, err = CBWrite(b, s, "roles/role-bad-issuer", map[string]interface{}{ - issuerRefParam: "non-existant", - "no_store": "false", - }) - require.NoError(t, err, "failed creating role role-bad-issuer") - - _, err = CBWrite(b, s, "roles/role-no-store-enabled", map[string]interface{}{ - issuerRefParam: "issuer-2", - "no_store": "true", - }) - require.NoError(t, err, "failed creating role role-no-store-enabled") - - _, err = CBWrite(b, s, "roles/role-issuer-2", map[string]interface{}{ - issuerRefParam: "issuer-2", - "no_store": "false", - }) - require.NoError(t, err, "failed creating role role-issuer-2") - - tc := []struct { - name string - roleName string - issuerName string - expectedIssuerName string - expectErr bool - }{ - {name: "pass-default-use-default", roleName: "", issuerName: "", expectedIssuerName: "issuer-1", expectErr: false}, - {name: "pass-role-issuer-2", roleName: "role-issuer-2", issuerName: "", expectedIssuerName: "issuer-2", expectErr: false}, - {name: "pass-issuer-1-no-role", roleName: "", issuerName: "issuer-1", expectedIssuerName: "issuer-1", expectErr: false}, - {name: "fail-role-has-bad-issuer", roleName: "role-bad-issuer", issuerName: "", expectedIssuerName: "", expectErr: true}, - {name: "fail-role-no-store-enabled", roleName: "role-no-store-enabled", issuerName: "", expectedIssuerName: "", expectErr: true}, - {name: "fail-role-no-store-enabled", roleName: "role-no-store-enabled", issuerName: "", expectedIssuerName: "", expectErr: true}, - {name: "fail-role-does-not-exist", roleName: "non-existant", issuerName: "", expectedIssuerName: "", expectErr: true}, - {name: "fail-issuer-does-not-exist", roleName: "", issuerName: "non-existant", expectedIssuerName: "", expectErr: true}, - } - - for _, tt := range tc { - t.Run(tt.name, func(t *testing.T) { - f := b.acmeWrapper(func(acmeCtx *acmeContext, r *logical.Request, _ *framework.FieldData) (*logical.Response, error) { - if tt.roleName != acmeCtx.role.Name { - return nil, fmt.Errorf("expected role %s but got %s", tt.roleName, acmeCtx.role.Name) - } - - if tt.expectedIssuerName != acmeCtx.issuer.Name { - return nil, fmt.Errorf("expected issuer %s but got %s", tt.expectedIssuerName, acmeCtx.issuer.Name) - } - - return nil, nil - }) - - var acmePath string - fieldRaw := map[string]interface{}{} - if tt.issuerName != "" { - fieldRaw[issuerRefParam] = tt.issuerName - acmePath = "issuer/" + tt.issuerName + "/" - } - if tt.roleName != "" { - fieldRaw["role"] = tt.roleName - acmePath = acmePath + "roles/" + tt.roleName + "/" - } - - acmePath = strings.TrimLeft(acmePath+"/acme/directory", "/") - - resp, err := f(context.Background(), &logical.Request{Path: acmePath, Storage: s}, &framework.FieldData{ - Raw: fieldRaw, - Schema: getCsrSignVerbatimSchemaFields(), - }) - require.NoError(t, err, "all errors should be re-encoded") - - if tt.expectErr { - require.NotEqual(t, 200, resp.Data[logical.HTTPStatusCode]) - require.Equal(t, ErrorContentType, resp.Data[logical.HTTPContentType]) - } else { - if resp != nil { - t.Fatalf("expected no error got %s", string(resp.Data[logical.HTTPRawBody].([]uint8))) - } - } - }) - } -} diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go deleted file mode 100644 index b2d2ec892..000000000 --- a/builtin/logical/pki/backend_test.go +++ /dev/null @@ -1,7139 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "bytes" - "context" - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/base64" - "encoding/hex" - "encoding/json" - "encoding/pem" - "fmt" - "math" - "math/big" - mathrand "math/rand" - "net" - "net/url" - "os" - "reflect" - "sort" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/hashicorp/vault/helper/testhelpers/teststorage" - - "github.com/hashicorp/vault/helper/testhelpers" - - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - - "github.com/stretchr/testify/require" - - "github.com/armon/go-metrics" - "github.com/fatih/structs" - "github.com/go-test/deep" - "github.com/hashicorp/go-secure-stdlib/strutil" - "github.com/hashicorp/vault/api" - auth "github.com/hashicorp/vault/api/auth/userpass" - "github.com/hashicorp/vault/builtin/credential/userpass" - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/mapstructure" - "golang.org/x/net/idna" -) - -var stepCount = 0 - -// From builtin/credential/cert/test-fixtures/root/rootcacert.pem -const ( - rootCACertPEM = `-----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE-----` - rootCAKeyPEM = `-----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p -t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 -BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w -/2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv -0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi -18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb -ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn -8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f -nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 -2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t -grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc -bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 -0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN -ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf -lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 -lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj -AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG -ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib -thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU -4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb -iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO -tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y -LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc -4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX -OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= ------END RSA PRIVATE KEY-----` -) - -func TestPKI_RequireCN(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected ca info") - } - - // Create a role which does require CN (default) - _, err = CBWrite(b, s, "roles/example", map[string]interface{}{ - "allowed_domains": "foobar.com,zipzap.com,abc.com,xyz.com", - "allow_bare_domains": true, - "allow_subdomains": true, - "max_ttl": "2h", - }) - if err != nil { - t.Fatal(err) - } - - // Issue a cert with require_cn set to true and with common name supplied. - // It should succeed. - resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{ - "common_name": "foobar.com", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issue/example"), logical.UpdateOperation), resp, true) - if err != nil { - t.Fatal(err) - } - - // Issue a cert with require_cn set to true and with out supplying the - // common name. It should error out. - _, err = CBWrite(b, s, "issue/example", map[string]interface{}{}) - if err == nil { - t.Fatalf("expected an error due to missing common_name") - } - - // Modify the role to make the common name optional - _, err = CBWrite(b, s, "roles/example", map[string]interface{}{ - "allowed_domains": "foobar.com,zipzap.com,abc.com,xyz.com", - "allow_bare_domains": true, - "allow_subdomains": true, - "max_ttl": "2h", - "require_cn": false, - }) - if err != nil { - t.Fatal(err) - } - - // Issue a cert with require_cn set to false and without supplying the - // common name. It should succeed. - resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{}) - if err != nil { - t.Fatal(err) - } - - if resp.Data["certificate"] == "" { - t.Fatalf("expected a cert to be generated") - } - - // Issue a cert with require_cn set to false and with a common name. It - // should succeed. - resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{}) - if err != nil { - t.Fatal(err) - } - - if resp.Data["certificate"] == "" { - t.Fatalf("expected a cert to be generated") - } -} - -func TestPKI_DeviceCert(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault.com", - "not_after": "9999-12-31T23:59:59Z", - "not_before_duration": "2h", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected ca info") - } - var certBundle certutil.CertBundle - err = mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - t.Fatal(err) - } - - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - t.Fatal(err) - } - cert := parsedCertBundle.Certificate - notAfter := cert.NotAfter.Format(time.RFC3339) - if notAfter != "9999-12-31T23:59:59Z" { - t.Fatalf("not after from certificate: %v is not matching with input parameter: %v", cert.NotAfter, "9999-12-31T23:59:59Z") - } - if math.Abs(float64(time.Now().Add(-2*time.Hour).Unix()-cert.NotBefore.Unix())) > 10 { - t.Fatalf("root/generate/internal did not properly set validity period (notBefore): was %v vs expected %v", cert.NotBefore, time.Now().Add(-2*time.Hour)) - } - - // Create a role which does require CN (default) - _, err = CBWrite(b, s, "roles/example", map[string]interface{}{ - "allowed_domains": "foobar.com,zipzap.com,abc.com,xyz.com", - "allow_bare_domains": true, - "allow_subdomains": true, - "not_after": "9999-12-31T23:59:59Z", - }) - if err != nil { - t.Fatal(err) - } - - // Issue a cert with require_cn set to true and with common name supplied. - // It should succeed. - resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{ - "common_name": "foobar.com", - }) - if err != nil { - t.Fatal(err) - } - err = mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - t.Fatal(err) - } - - parsedCertBundle, err = certBundle.ToParsedCertBundle() - if err != nil { - t.Fatal(err) - } - cert = parsedCertBundle.Certificate - notAfter = cert.NotAfter.Format(time.RFC3339) - if notAfter != "9999-12-31T23:59:59Z" { - t.Fatal(fmt.Errorf("not after from certificate is not matching with input parameter")) - } -} - -func TestBackend_InvalidParameter(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - _, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault.com", - "not_after": "9999-12-31T23:59:59Z", - "ttl": "25h", - }) - if err == nil { - t.Fatal(err) - } - - _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault.com", - "not_after": "9999-12-31T23:59:59", - }) - if err == nil { - t.Fatal(err) - } -} - -func TestBackend_CSRValues(t *testing.T) { - t.Parallel() - initTest.Do(setCerts) - b, _ := CreateBackendWithStorage(t) - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{}, - } - - intdata := map[string]interface{}{} - reqdata := map[string]interface{}{} - testCase.Steps = append(testCase.Steps, generateCSRSteps(t, ecCACert, ecCAKey, intdata, reqdata)...) - - logicaltest.Test(t, testCase) -} - -func TestBackend_URLsCRUD(t *testing.T) { - t.Parallel() - initTest.Do(setCerts) - b, _ := CreateBackendWithStorage(t) - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{}, - } - - intdata := map[string]interface{}{} - reqdata := map[string]interface{}{} - testCase.Steps = append(testCase.Steps, generateURLSteps(t, ecCACert, ecCAKey, intdata, reqdata)...) - - logicaltest.Test(t, testCase) -} - -// Generates and tests steps that walk through the various possibilities -// of role flags to ensure that they are properly restricted -func TestBackend_Roles(t *testing.T) { - t.Parallel() - cases := []struct { - name string - key, cert *string - useCSR bool - }{ - {"RSA", &rsaCAKey, &rsaCACert, false}, - {"RSACSR", &rsaCAKey, &rsaCACert, true}, - {"EC", &ecCAKey, &ecCACert, false}, - {"ECCSR", &ecCAKey, &ecCACert, true}, - {"ED", &edCAKey, &edCACert, false}, - {"EDCSR", &edCAKey, &edCACert, true}, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - initTest.Do(setCerts) - b, _ := CreateBackendWithStorage(t) - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - { - Operation: logical.UpdateOperation, - Path: "config/ca", - Data: map[string]interface{}{ - "pem_bundle": *tc.key + "\n" + *tc.cert, - }, - }, - }, - } - - testCase.Steps = append(testCase.Steps, generateRoleSteps(t, tc.useCSR)...) - if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { - for i, v := range testCase.Steps { - data := map[string]interface{}{} - var keys []string - for k := range v.Data { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - interf := v.Data[k] - switch v := interf.(type) { - case bool: - if !v { - continue - } - case int: - if v == 0 { - continue - } - case []string: - if len(v) == 0 { - continue - } - case string: - if v == "" { - continue - } - lines := strings.Split(v, "\n") - if len(lines) > 1 { - data[k] = lines[0] + " ... (truncated)" - continue - } - } - data[k] = interf - - } - t.Logf("Step %d:\n%s %s err=%v %+v\n\n", i+1, v.Operation, v.Path, v.ErrorOk, data) - } - } - - logicaltest.Test(t, testCase) - }) - } -} - -// Performs some validity checking on the returned bundles -func checkCertsAndPrivateKey(keyType string, key crypto.Signer, usage x509.KeyUsage, extUsage x509.ExtKeyUsage, validity time.Duration, certBundle *certutil.CertBundle) (*certutil.ParsedCertBundle, error) { - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return nil, fmt.Errorf("error parsing cert bundle: %s", err) - } - - if key != nil { - switch keyType { - case "rsa": - parsedCertBundle.PrivateKeyType = certutil.RSAPrivateKey - parsedCertBundle.PrivateKey = key - parsedCertBundle.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey)) - case "ec": - parsedCertBundle.PrivateKeyType = certutil.ECPrivateKey - parsedCertBundle.PrivateKey = key - parsedCertBundle.PrivateKeyBytes, err = x509.MarshalECPrivateKey(key.(*ecdsa.PrivateKey)) - if err != nil { - return nil, fmt.Errorf("error parsing EC key: %s", err) - } - case "ed25519": - parsedCertBundle.PrivateKeyType = certutil.Ed25519PrivateKey - parsedCertBundle.PrivateKey = key - parsedCertBundle.PrivateKeyBytes, err = x509.MarshalPKCS8PrivateKey(key.(ed25519.PrivateKey)) - if err != nil { - return nil, fmt.Errorf("error parsing Ed25519 key: %s", err) - } - } - } - - switch { - case parsedCertBundle.Certificate == nil: - return nil, fmt.Errorf("did not find a certificate in the cert bundle") - case len(parsedCertBundle.CAChain) == 0 || parsedCertBundle.CAChain[0].Certificate == nil: - return nil, fmt.Errorf("did not find a CA in the cert bundle") - case parsedCertBundle.PrivateKey == nil: - return nil, fmt.Errorf("did not find a private key in the cert bundle") - case parsedCertBundle.PrivateKeyType == certutil.UnknownPrivateKey: - return nil, fmt.Errorf("could not figure out type of private key") - } - - switch { - case parsedCertBundle.PrivateKeyType == certutil.Ed25519PrivateKey && keyType != "ed25519": - fallthrough - case parsedCertBundle.PrivateKeyType == certutil.RSAPrivateKey && keyType != "rsa": - fallthrough - case parsedCertBundle.PrivateKeyType == certutil.ECPrivateKey && keyType != "ec": - return nil, fmt.Errorf("given key type does not match type found in bundle") - } - - cert := parsedCertBundle.Certificate - - if usage != cert.KeyUsage { - return nil, fmt.Errorf("expected usage of %#v, got %#v; ext usage is %#v", usage, cert.KeyUsage, cert.ExtKeyUsage) - } - - // There should only be one ext usage type, because only one is requested - // in the tests - if len(cert.ExtKeyUsage) != 1 { - return nil, fmt.Errorf("got wrong size key usage in generated cert; expected 1, values are %#v", cert.ExtKeyUsage) - } - switch extUsage { - case x509.ExtKeyUsageEmailProtection: - if cert.ExtKeyUsage[0] != x509.ExtKeyUsageEmailProtection { - return nil, fmt.Errorf("bad extended key usage") - } - case x509.ExtKeyUsageServerAuth: - if cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth { - return nil, fmt.Errorf("bad extended key usage") - } - case x509.ExtKeyUsageClientAuth: - if cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth { - return nil, fmt.Errorf("bad extended key usage") - } - case x509.ExtKeyUsageCodeSigning: - if cert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning { - return nil, fmt.Errorf("bad extended key usage") - } - } - - // TODO: We incremented 20->25 due to CircleCI execution - // being slow and pausing this test. We might consider recording the - // actual issuance time of the cert and calculating the expected - // validity period +/- fuzz, but that'd require recording and passing - // through more information. - if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 25 { - return nil, fmt.Errorf("certificate validity end: %s; expected within 25 seconds of %s", cert.NotAfter.Format(time.RFC3339), time.Now().Add(validity).Format(time.RFC3339)) - } - - return parsedCertBundle, nil -} - -func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[string]interface{}) []logicaltest.TestStep { - expected := certutil.URLEntries{ - IssuingCertificates: []string{ - "http://example.com/ca1", - "http://example.com/ca2", - }, - CRLDistributionPoints: []string{ - "http://example.com/crl1", - "http://example.com/crl2", - }, - OCSPServers: []string{ - "http://example.com/ocsp1", - "http://example.com/ocsp2", - }, - } - csrTemplate := x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: "my@example.com", - }, - } - - priv1024, _ := rsa.GenerateKey(rand.Reader, 1024) - csr1024, _ := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, priv1024) - csrPem1024 := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csr1024, - }))) - - priv2048, _ := rsa.GenerateKey(rand.Reader, 2048) - csr2048, _ := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, priv2048) - csrPem2048 := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csr2048, - }))) - - ret := []logicaltest.TestStep{ - { - Operation: logical.UpdateOperation, - Path: "root/generate/exported", - Data: map[string]interface{}{ - "common_name": "Root Cert", - "ttl": "180h", - }, - Check: func(resp *logical.Response) error { - if resp.Secret != nil && resp.Secret.LeaseID != "" { - return fmt.Errorf("root returned with a lease") - } - return nil - }, - }, - - { - Operation: logical.UpdateOperation, - Path: "config/urls", - Data: map[string]interface{}{ - "issuing_certificates": strings.Join(expected.IssuingCertificates, ","), - "crl_distribution_points": strings.Join(expected.CRLDistributionPoints, ","), - "ocsp_servers": strings.Join(expected.OCSPServers, ","), - }, - }, - - { - Operation: logical.ReadOperation, - Path: "config/urls", - Check: func(resp *logical.Response) error { - if resp.Data == nil { - return fmt.Errorf("no data returned") - } - var entries certutil.URLEntries - err := mapstructure.Decode(resp.Data, &entries) - if err != nil { - return err - } - if !reflect.DeepEqual(entries, expected) { - return fmt.Errorf("expected urls\n%#v\ndoes not match provided\n%#v\n", expected, entries) - } - - return nil - }, - }, - - { - Operation: logical.UpdateOperation, - Path: "root/sign-intermediate", - Data: map[string]interface{}{ - "common_name": "intermediate.cert.com", - "csr": csrPem1024, - "format": "der", - }, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if !resp.IsError() { - return fmt.Errorf("expected an error response but did not get one") - } - if !strings.Contains(resp.Data["error"].(string), "2048") { - return fmt.Errorf("received an error but not about a 1024-bit key, error was: %s", resp.Data["error"].(string)) - } - - return nil - }, - }, - - { - Operation: logical.UpdateOperation, - Path: "root/sign-intermediate", - Data: map[string]interface{}{ - "common_name": "intermediate.cert.com", - "csr": csrPem2048, - "signature_bits": 512, - "format": "der", - "not_before_duration": "2h", - // Let's Encrypt -- R3 SKID - "skid": "14:2E:B3:17:B7:58:56:CB:AE:50:09:40:E6:1F:AF:9D:8B:14:C2:C6", - }, - Check: func(resp *logical.Response) error { - certString := resp.Data["certificate"].(string) - if certString == "" { - return fmt.Errorf("no certificate returned") - } - if resp.Secret != nil && resp.Secret.LeaseID != "" { - return fmt.Errorf("signed intermediate returned with a lease") - } - certBytes, _ := base64.StdEncoding.DecodeString(certString) - certs, err := x509.ParseCertificates(certBytes) - if err != nil { - return fmt.Errorf("returned cert cannot be parsed: %w", err) - } - if len(certs) != 1 { - return fmt.Errorf("unexpected returned length of certificates: %d", len(certs)) - } - cert := certs[0] - - skid, _ := hex.DecodeString("142EB317B75856CBAE500940E61FAF9D8B14C2C6") - - switch { - case !reflect.DeepEqual(expected.IssuingCertificates, cert.IssuingCertificateURL): - return fmt.Errorf("IssuingCertificateURL:\nexpected\n%#v\ngot\n%#v\n", expected.IssuingCertificates, cert.IssuingCertificateURL) - case !reflect.DeepEqual(expected.CRLDistributionPoints, cert.CRLDistributionPoints): - return fmt.Errorf("CRLDistributionPoints:\nexpected\n%#v\ngot\n%#v\n", expected.CRLDistributionPoints, cert.CRLDistributionPoints) - case !reflect.DeepEqual(expected.OCSPServers, cert.OCSPServer): - return fmt.Errorf("OCSPServer:\nexpected\n%#v\ngot\n%#v\n", expected.OCSPServers, cert.OCSPServer) - case !reflect.DeepEqual([]string{"intermediate.cert.com"}, cert.DNSNames): - return fmt.Errorf("DNSNames\nexpected\n%#v\ngot\n%#v\n", []string{"intermediate.cert.com"}, cert.DNSNames) - case !reflect.DeepEqual(x509.SHA512WithRSA, cert.SignatureAlgorithm): - return fmt.Errorf("Signature Algorithm:\nexpected\n%#v\ngot\n%#v\n", x509.SHA512WithRSA, cert.SignatureAlgorithm) - case !reflect.DeepEqual(skid, cert.SubjectKeyId): - return fmt.Errorf("SKID:\nexpected\n%#v\ngot\n%#v\n", skid, cert.SubjectKeyId) - } - - if math.Abs(float64(time.Now().Add(-2*time.Hour).Unix()-cert.NotBefore.Unix())) > 10 { - t.Fatalf("root/sign-intermediate did not properly set validity period (notBefore): was %v vs expected %v", cert.NotBefore, time.Now().Add(-2*time.Hour)) - } - - return nil - }, - }, - - // Same as above but exclude adding to sans - { - Operation: logical.UpdateOperation, - Path: "root/sign-intermediate", - Data: map[string]interface{}{ - "common_name": "intermediate.cert.com", - "csr": csrPem2048, - "format": "der", - "exclude_cn_from_sans": true, - }, - Check: func(resp *logical.Response) error { - certString := resp.Data["certificate"].(string) - if certString == "" { - return fmt.Errorf("no certificate returned") - } - if resp.Secret != nil && resp.Secret.LeaseID != "" { - return fmt.Errorf("signed intermediate returned with a lease") - } - certBytes, _ := base64.StdEncoding.DecodeString(certString) - certs, err := x509.ParseCertificates(certBytes) - if err != nil { - return fmt.Errorf("returned cert cannot be parsed: %w", err) - } - if len(certs) != 1 { - return fmt.Errorf("unexpected returned length of certificates: %d", len(certs)) - } - cert := certs[0] - - switch { - case !reflect.DeepEqual(expected.IssuingCertificates, cert.IssuingCertificateURL): - return fmt.Errorf("expected\n%#v\ngot\n%#v\n", expected.IssuingCertificates, cert.IssuingCertificateURL) - case !reflect.DeepEqual(expected.CRLDistributionPoints, cert.CRLDistributionPoints): - return fmt.Errorf("expected\n%#v\ngot\n%#v\n", expected.CRLDistributionPoints, cert.CRLDistributionPoints) - case !reflect.DeepEqual(expected.OCSPServers, cert.OCSPServer): - return fmt.Errorf("expected\n%#v\ngot\n%#v\n", expected.OCSPServers, cert.OCSPServer) - case !reflect.DeepEqual([]string(nil), cert.DNSNames): - return fmt.Errorf("expected\n%#v\ngot\n%#v\n", []string(nil), cert.DNSNames) - } - - return nil - }, - }, - } - return ret -} - -func generateCSR(t *testing.T, csrTemplate *x509.CertificateRequest, keyType string, keyBits int) (interface{}, []byte, string) { - t.Helper() - - var priv interface{} - var err error - switch keyType { - case "rsa": - priv, err = rsa.GenerateKey(rand.Reader, keyBits) - case "ec": - switch keyBits { - case 224: - priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) - case 256: - priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - case 384: - priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case 521: - priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - default: - t.Fatalf("Got unknown ec< key bits: %v", keyBits) - } - case "ed25519": - _, priv, err = ed25519.GenerateKey(rand.Reader) - } - - if err != nil { - t.Fatalf("Got error generating private key for CSR: %v", err) - } - - csr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, priv) - if err != nil { - t.Fatalf("Got error generating CSR: %v", err) - } - - csrPem := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csr, - }))) - - return priv, csr, csrPem -} - -func generateCSRSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[string]interface{}) []logicaltest.TestStep { - csrTemplate, csrPem := generateTestCsr(t, certutil.RSAPrivateKey, 2048) - - ret := []logicaltest.TestStep{ - { - Operation: logical.UpdateOperation, - Path: "root/generate/exported", - Data: map[string]interface{}{ - "common_name": "Root Cert", - "ttl": "180h", - "max_path_length": 0, - }, - }, - - { - Operation: logical.UpdateOperation, - Path: "root/sign-intermediate", - Data: map[string]interface{}{ - "use_csr_values": true, - "csr": csrPem, - "format": "der", - }, - ErrorOk: true, - }, - - { - Operation: logical.DeleteOperation, - Path: "root", - }, - - { - Operation: logical.UpdateOperation, - Path: "root/generate/exported", - Data: map[string]interface{}{ - "common_name": "Root Cert", - "ttl": "180h", - "max_path_length": 1, - }, - }, - - { - Operation: logical.UpdateOperation, - Path: "root/sign-intermediate", - Data: map[string]interface{}{ - "use_csr_values": true, - "csr": csrPem, - "format": "der", - }, - Check: func(resp *logical.Response) error { - certString := resp.Data["certificate"].(string) - if certString == "" { - return fmt.Errorf("no certificate returned") - } - certBytes, _ := base64.StdEncoding.DecodeString(certString) - certs, err := x509.ParseCertificates(certBytes) - if err != nil { - return fmt.Errorf("returned cert cannot be parsed: %w", err) - } - if len(certs) != 1 { - return fmt.Errorf("unexpected returned length of certificates: %d", len(certs)) - } - cert := certs[0] - - if cert.MaxPathLen != 0 { - return fmt.Errorf("max path length of %d does not match the requested of 3", cert.MaxPathLen) - } - if !cert.MaxPathLenZero { - return fmt.Errorf("max path length zero is not set") - } - - // We need to set these as they are filled in with unparsed values in the final cert - csrTemplate.Subject.Names = cert.Subject.Names - csrTemplate.Subject.ExtraNames = cert.Subject.ExtraNames - - switch { - case !reflect.DeepEqual(cert.Subject, csrTemplate.Subject): - return fmt.Errorf("cert subject\n%#v\ndoes not match csr subject\n%#v\n", cert.Subject, csrTemplate.Subject) - case !reflect.DeepEqual(cert.DNSNames, csrTemplate.DNSNames): - return fmt.Errorf("cert dns names\n%#v\ndoes not match csr dns names\n%#v\n", cert.DNSNames, csrTemplate.DNSNames) - case !reflect.DeepEqual(cert.EmailAddresses, csrTemplate.EmailAddresses): - return fmt.Errorf("cert email addresses\n%#v\ndoes not match csr email addresses\n%#v\n", cert.EmailAddresses, csrTemplate.EmailAddresses) - case !reflect.DeepEqual(cert.IPAddresses, csrTemplate.IPAddresses): - return fmt.Errorf("cert ip addresses\n%#v\ndoes not match csr ip addresses\n%#v\n", cert.IPAddresses, csrTemplate.IPAddresses) - } - return nil - }, - }, - } - return ret -} - -func generateTestCsr(t *testing.T, keyType certutil.PrivateKeyType, keyBits int) (x509.CertificateRequest, string) { - t.Helper() - - csrTemplate := x509.CertificateRequest{ - Subject: pkix.Name{ - Country: []string{"MyCountry"}, - PostalCode: []string{"MyPostalCode"}, - SerialNumber: "MySerialNumber", - CommonName: "my@example.com", - }, - DNSNames: []string{ - "name1.example.com", - "name2.example.com", - "name3.example.com", - }, - EmailAddresses: []string{ - "name1@example.com", - "name2@example.com", - "name3@example.com", - }, - IPAddresses: []net.IP{ - net.ParseIP("::ff:1:2:3:4"), - net.ParseIP("::ff:5:6:7:8"), - }, - } - - _, _, csrPem := generateCSR(t, &csrTemplate, string(keyType), keyBits) - return csrTemplate, csrPem -} - -// Generates steps to test out various role permutations -func generateRoleSteps(t *testing.T, useCSRs bool) []logicaltest.TestStep { - roleVals := roleEntry{ - MaxTTL: 12 * time.Hour, - KeyType: "rsa", - KeyBits: 2048, - RequireCN: true, - AllowWildcardCertificates: new(bool), - } - *roleVals.AllowWildcardCertificates = true - - issueVals := certutil.IssueData{} - ret := []logicaltest.TestStep{} - - roleTestStep := logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "roles/test", - } - var issueTestStep logicaltest.TestStep - if useCSRs { - issueTestStep = logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "sign/test", - } - } else { - issueTestStep = logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "issue/test", - } - } - - generatedRSAKeys := map[int]crypto.Signer{} - generatedECKeys := map[int]crypto.Signer{} - generatedEdKeys := map[int]crypto.Signer{} - /* - // For the number of tests being run, a seed of 1 has been tested - // to hit all of the various values below. However, for normal - // testing we use a randomized time for maximum fuzziness. - */ - var seed int64 = 1 - fixedSeed := os.Getenv("VAULT_PKITESTS_FIXED_SEED") - if len(fixedSeed) == 0 { - seed = time.Now().UnixNano() - } else { - var err error - seed, err = strconv.ParseInt(fixedSeed, 10, 64) - if err != nil { - t.Fatalf("error parsing fixed seed of %s: %v", fixedSeed, err) - } - } - mathRand := mathrand.New(mathrand.NewSource(seed)) - // t.Logf("seed under test: %v", seed) - - // Used by tests not toggling common names to turn off the behavior of random key bit fuzziness - keybitSizeRandOff := false - - genericErrorOkCheck := func(resp *logical.Response) error { - if resp.IsError() { - return nil - } - return fmt.Errorf("expected an error, but did not seem to get one") - } - - // Adds tests with the currently configured issue/role information - addTests := func(testCheck logicaltest.TestCheckFunc) { - stepCount++ - // t.Logf("test step %d\nrole vals: %#v\n", stepCount, roleVals) - stepCount++ - // t.Logf("test step %d\nissue vals: %#v\n", stepCount, issueTestStep) - roleTestStep.Data = roleVals.ToResponseData() - roleTestStep.Data["generate_lease"] = false - ret = append(ret, roleTestStep) - issueTestStep.Data = structs.New(issueVals).Map() - switch { - case issueTestStep.ErrorOk: - issueTestStep.Check = genericErrorOkCheck - case testCheck != nil: - issueTestStep.Check = testCheck - default: - issueTestStep.Check = nil - } - ret = append(ret, issueTestStep) - } - - getCountryCheck := func(role roleEntry) logicaltest.TestCheckFunc { - var certBundle certutil.CertBundle - return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error checking generated certificate: %s", err) - } - cert := parsedCertBundle.Certificate - - expected := strutil.RemoveDuplicates(role.Country, true) - if !reflect.DeepEqual(cert.Subject.Country, expected) { - return fmt.Errorf("error: returned certificate has Country of %s but %s was specified in the role", cert.Subject.Country, expected) - } - return nil - } - } - - getOuCheck := func(role roleEntry) logicaltest.TestCheckFunc { - var certBundle certutil.CertBundle - return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error checking generated certificate: %s", err) - } - cert := parsedCertBundle.Certificate - - expected := strutil.RemoveDuplicatesStable(role.OU, true) - if !reflect.DeepEqual(cert.Subject.OrganizationalUnit, expected) { - return fmt.Errorf("error: returned certificate has OU of %s but %s was specified in the role", cert.Subject.OrganizationalUnit, expected) - } - return nil - } - } - - getOrganizationCheck := func(role roleEntry) logicaltest.TestCheckFunc { - var certBundle certutil.CertBundle - return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error checking generated certificate: %s", err) - } - cert := parsedCertBundle.Certificate - - expected := strutil.RemoveDuplicates(role.Organization, true) - if !reflect.DeepEqual(cert.Subject.Organization, expected) { - return fmt.Errorf("error: returned certificate has Organization of %s but %s was specified in the role", cert.Subject.Organization, expected) - } - return nil - } - } - - getLocalityCheck := func(role roleEntry) logicaltest.TestCheckFunc { - var certBundle certutil.CertBundle - return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error checking generated certificate: %s", err) - } - cert := parsedCertBundle.Certificate - - expected := strutil.RemoveDuplicates(role.Locality, true) - if !reflect.DeepEqual(cert.Subject.Locality, expected) { - return fmt.Errorf("error: returned certificate has Locality of %s but %s was specified in the role", cert.Subject.Locality, expected) - } - return nil - } - } - - getProvinceCheck := func(role roleEntry) logicaltest.TestCheckFunc { - var certBundle certutil.CertBundle - return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error checking generated certificate: %s", err) - } - cert := parsedCertBundle.Certificate - - expected := strutil.RemoveDuplicates(role.Province, true) - if !reflect.DeepEqual(cert.Subject.Province, expected) { - return fmt.Errorf("error: returned certificate has Province of %s but %s was specified in the role", cert.Subject.Province, expected) - } - return nil - } - } - - getStreetAddressCheck := func(role roleEntry) logicaltest.TestCheckFunc { - var certBundle certutil.CertBundle - return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error checking generated certificate: %s", err) - } - cert := parsedCertBundle.Certificate - - expected := strutil.RemoveDuplicates(role.StreetAddress, true) - if !reflect.DeepEqual(cert.Subject.StreetAddress, expected) { - return fmt.Errorf("error: returned certificate has StreetAddress of %s but %s was specified in the role", cert.Subject.StreetAddress, expected) - } - return nil - } - } - - getPostalCodeCheck := func(role roleEntry) logicaltest.TestCheckFunc { - var certBundle certutil.CertBundle - return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error checking generated certificate: %s", err) - } - cert := parsedCertBundle.Certificate - - expected := strutil.RemoveDuplicates(role.PostalCode, true) - if !reflect.DeepEqual(cert.Subject.PostalCode, expected) { - return fmt.Errorf("error: returned certificate has PostalCode of %s but %s was specified in the role", cert.Subject.PostalCode, expected) - } - return nil - } - } - - getNotBeforeCheck := func(role roleEntry) logicaltest.TestCheckFunc { - var certBundle certutil.CertBundle - return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error checking generated certificate: %s", err) - } - cert := parsedCertBundle.Certificate - - actualDiff := time.Since(cert.NotBefore) - certRoleDiff := (role.NotBeforeDuration - actualDiff).Truncate(time.Second) - // These times get truncated, so give a 1 second buffer on each side - if certRoleDiff >= -1*time.Second && certRoleDiff <= 1*time.Second { - return nil - } - return fmt.Errorf("validity period out of range diff: %v", certRoleDiff) - } - } - - // Returns a TestCheckFunc that performs various validity checks on the - // returned certificate information, mostly within checkCertsAndPrivateKey - getCnCheck := func(name string, role roleEntry, key crypto.Signer, usage x509.KeyUsage, extUsage x509.ExtKeyUsage, validity time.Duration) logicaltest.TestCheckFunc { - var certBundle certutil.CertBundle - return func(resp *logical.Response) error { - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := checkCertsAndPrivateKey(role.KeyType, key, usage, extUsage, validity, &certBundle) - if err != nil { - return fmt.Errorf("error checking generated certificate: %s", err) - } - cert := parsedCertBundle.Certificate - if cert.Subject.CommonName != name { - return fmt.Errorf("error: returned certificate has CN of %s but %s was requested", cert.Subject.CommonName, name) - } - if strings.Contains(cert.Subject.CommonName, "@") { - if len(cert.DNSNames) != 0 || len(cert.EmailAddresses) != 1 { - return fmt.Errorf("error: found more than one DNS SAN or not one Email SAN but only one was requested, cert.DNSNames = %#v, cert.EmailAddresses = %#v", cert.DNSNames, cert.EmailAddresses) - } - } else { - if len(cert.DNSNames) != 1 || len(cert.EmailAddresses) != 0 { - return fmt.Errorf("error: found more than one Email SAN or not one DNS SAN but only one was requested, cert.DNSNames = %#v, cert.EmailAddresses = %#v", cert.DNSNames, cert.EmailAddresses) - } - } - var retName string - if len(cert.DNSNames) > 0 { - retName = cert.DNSNames[0] - } - if len(cert.EmailAddresses) > 0 { - retName = cert.EmailAddresses[0] - } - if retName != name { - // Check IDNA - p := idna.New( - idna.StrictDomainName(true), - idna.VerifyDNSLength(true), - ) - converted, err := p.ToUnicode(retName) - if err != nil { - t.Fatal(err) - } - if converted != name { - return fmt.Errorf("error: returned certificate has a DNS SAN of %s (from idna: %s) but %s was requested", retName, converted, name) - } - } - return nil - } - } - - type csrPlan struct { - errorOk bool - roleKeyBits int - cert string - privKey crypto.Signer - } - - getCsr := func(keyType string, keyBits int, csrTemplate *x509.CertificateRequest) (*pem.Block, crypto.Signer) { - var privKey crypto.Signer - var ok bool - switch keyType { - case "rsa": - privKey, ok = generatedRSAKeys[keyBits] - if !ok { - privKey, _ = rsa.GenerateKey(rand.Reader, keyBits) - generatedRSAKeys[keyBits] = privKey - } - - case "ec": - var curve elliptic.Curve - - switch keyBits { - case 224: - curve = elliptic.P224() - case 256: - curve = elliptic.P256() - case 384: - curve = elliptic.P384() - case 521: - curve = elliptic.P521() - } - - privKey, ok = generatedECKeys[keyBits] - if !ok { - privKey, _ = ecdsa.GenerateKey(curve, rand.Reader) - generatedECKeys[keyBits] = privKey - } - - case "ed25519": - privKey, ok = generatedEdKeys[keyBits] - if !ok { - _, privKey, _ = ed25519.GenerateKey(rand.Reader) - generatedEdKeys[keyBits] = privKey - } - - default: - panic("invalid key type: " + keyType) - } - - csr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, privKey) - if err != nil { - t.Fatalf("Error creating certificate request: %s", err) - } - block := pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csr, - } - return &block, privKey - } - - getRandCsr := func(keyType string, errorOk bool, csrTemplate *x509.CertificateRequest) csrPlan { - rsaKeyBits := []int{2048, 3072, 4096} - ecKeyBits := []int{224, 256, 384, 521} - plan := csrPlan{errorOk: errorOk} - - var testBitSize int - switch keyType { - case "rsa": - plan.roleKeyBits = rsaKeyBits[mathRand.Int()%len(rsaKeyBits)] - testBitSize = plan.roleKeyBits - - // If we don't expect an error already, randomly choose a - // key size and expect an error if it's less than the role - // setting - if !keybitSizeRandOff && !errorOk { - testBitSize = rsaKeyBits[mathRand.Int()%len(rsaKeyBits)] - } - - if testBitSize < plan.roleKeyBits { - plan.errorOk = true - } - - case "ec": - plan.roleKeyBits = ecKeyBits[mathRand.Int()%len(ecKeyBits)] - testBitSize = plan.roleKeyBits - - // If we don't expect an error already, randomly choose a - // key size and expect an error if it's less than the role - // setting - if !keybitSizeRandOff && !errorOk { - testBitSize = ecKeyBits[mathRand.Int()%len(ecKeyBits)] - } - - if testBitSize < plan.roleKeyBits { - plan.errorOk = true - } - - default: - panic("invalid key type: " + keyType) - } - if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { - t.Logf("roleKeyBits=%d testBitSize=%d errorOk=%v", plan.roleKeyBits, testBitSize, plan.errorOk) - } - - block, privKey := getCsr(keyType, testBitSize, csrTemplate) - plan.cert = strings.TrimSpace(string(pem.EncodeToMemory(block))) - plan.privKey = privKey - return plan - } - - // Common names to test with the various role flags toggled - var commonNames struct { - Localhost bool `structs:"localhost"` - BareDomain bool `structs:"example.com"` - SecondDomain bool `structs:"foobar.com"` - SubDomain bool `structs:"foo.example.com"` - Wildcard bool `structs:"*.example.com"` - SubSubdomain bool `structs:"foo.bar.example.com"` - SubSubdomainWildcard bool `structs:"*.bar.example.com"` - GlobDomain bool `structs:"fooexample.com"` - IDN bool `structs:"daɪˈɛrɨsɨs"` - AnyHost bool `structs:"porkslap.beer"` - } - - // Adds a series of tests based on the current selection of - // allowed common names; contains some (seeded) randomness - // - // This allows for a variety of common names to be tested in various - // combinations with allowed toggles of the role - addCnTests := func() { - cnMap := structs.New(commonNames).Map() - for name, allowedInt := range cnMap { - roleVals.KeyType = "rsa" - roleVals.KeyBits = 2048 - if mathRand.Int()%3 == 1 { - roleVals.KeyType = "ec" - roleVals.KeyBits = 224 - } - - roleVals.ServerFlag = false - roleVals.ClientFlag = false - roleVals.CodeSigningFlag = false - roleVals.EmailProtectionFlag = false - - var usage []string - if mathRand.Int()%2 == 1 { - usage = append(usage, "DigitalSignature") - } - if mathRand.Int()%2 == 1 { - usage = append(usage, "ContentCoMmitment") - } - if mathRand.Int()%2 == 1 { - usage = append(usage, "KeyEncipherment") - } - if mathRand.Int()%2 == 1 { - usage = append(usage, "DataEncipherment") - } - if mathRand.Int()%2 == 1 { - usage = append(usage, "KeyAgreemEnt") - } - if mathRand.Int()%2 == 1 { - usage = append(usage, "CertSign") - } - if mathRand.Int()%2 == 1 { - usage = append(usage, "CRLSign") - } - if mathRand.Int()%2 == 1 { - usage = append(usage, "EncipherOnly") - } - if mathRand.Int()%2 == 1 { - usage = append(usage, "DecipherOnly") - } - - roleVals.KeyUsage = usage - parsedKeyUsage := parseKeyUsages(roleVals.KeyUsage) - if parsedKeyUsage == 0 && len(usage) != 0 { - panic("parsed key usages was zero") - } - - var extUsage x509.ExtKeyUsage - i := mathRand.Int() % 4 - switch { - case i == 0: - // Punt on this for now since I'm not clear the actual proper - // way to format these - if name != "daɪˈɛrɨsɨs" { - extUsage = x509.ExtKeyUsageEmailProtection - roleVals.EmailProtectionFlag = true - break - } - fallthrough - case i == 1: - extUsage = x509.ExtKeyUsageServerAuth - roleVals.ServerFlag = true - case i == 2: - extUsage = x509.ExtKeyUsageClientAuth - roleVals.ClientFlag = true - default: - extUsage = x509.ExtKeyUsageCodeSigning - roleVals.CodeSigningFlag = true - } - - allowed := allowedInt.(bool) - issueVals.CommonName = name - if roleVals.EmailProtectionFlag { - if !strings.HasPrefix(name, "*") { - issueVals.CommonName = "user@" + issueVals.CommonName - } - } - - issueTestStep.ErrorOk = !allowed - - validity := roleVals.MaxTTL - - if useCSRs { - templ := &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: issueVals.CommonName, - }, - } - plan := getRandCsr(roleVals.KeyType, issueTestStep.ErrorOk, templ) - issueVals.CSR = plan.cert - roleVals.KeyBits = plan.roleKeyBits - issueTestStep.ErrorOk = plan.errorOk - - addTests(getCnCheck(issueVals.CommonName, roleVals, plan.privKey, x509.KeyUsage(parsedKeyUsage), extUsage, validity)) - } else { - addTests(getCnCheck(issueVals.CommonName, roleVals, nil, x509.KeyUsage(parsedKeyUsage), extUsage, validity)) - } - } - } - - funcs := []interface{}{ - addCnTests, getCnCheck, getCountryCheck, getLocalityCheck, getNotBeforeCheck, - getOrganizationCheck, getOuCheck, getPostalCodeCheck, getRandCsr, getStreetAddressCheck, - getProvinceCheck, - } - if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { - t.Logf("funcs=%d", len(funcs)) - } - - // Common Name tests - { - // common_name not provided - issueVals.CommonName = "" - issueTestStep.ErrorOk = true - addTests(nil) - - // Nothing is allowed - addCnTests() - - roleVals.AllowLocalhost = true - commonNames.Localhost = true - addCnTests() - - roleVals.AllowedDomains = []string{"foobar.com"} - addCnTests() - - roleVals.AllowedDomains = []string{"example.com"} - roleVals.AllowSubdomains = true - commonNames.SubDomain = true - commonNames.Wildcard = true - commonNames.SubSubdomain = true - commonNames.SubSubdomainWildcard = true - addCnTests() - - roleVals.AllowedDomains = []string{"foobar.com", "example.com"} - commonNames.SecondDomain = true - roleVals.AllowBareDomains = true - commonNames.BareDomain = true - addCnTests() - - roleVals.AllowedDomains = []string{"foobar.com", "*example.com"} - roleVals.AllowGlobDomains = true - commonNames.GlobDomain = true - addCnTests() - - roleVals.AllowAnyName = true - roleVals.EnforceHostnames = true - commonNames.AnyHost = true - commonNames.IDN = true - addCnTests() - - roleVals.EnforceHostnames = false - addCnTests() - - // Ensure that we end up with acceptable key sizes since they won't be - // toggled any longer - keybitSizeRandOff = true - addCnTests() - } - // Country tests - { - roleVals.Country = []string{"foo"} - addTests(getCountryCheck(roleVals)) - - roleVals.Country = []string{"foo", "bar"} - addTests(getCountryCheck(roleVals)) - } - // OU tests - { - roleVals.OU = []string{"foo"} - addTests(getOuCheck(roleVals)) - - roleVals.OU = []string{"bar", "foo"} - addTests(getOuCheck(roleVals)) - } - // Organization tests - { - roleVals.Organization = []string{"system:masters"} - addTests(getOrganizationCheck(roleVals)) - - roleVals.Organization = []string{"foo", "bar"} - addTests(getOrganizationCheck(roleVals)) - } - // Locality tests - { - roleVals.Locality = []string{"foo"} - addTests(getLocalityCheck(roleVals)) - - roleVals.Locality = []string{"foo", "bar"} - addTests(getLocalityCheck(roleVals)) - } - // Province tests - { - roleVals.Province = []string{"foo"} - addTests(getProvinceCheck(roleVals)) - - roleVals.Province = []string{"foo", "bar"} - addTests(getProvinceCheck(roleVals)) - } - // StreetAddress tests - { - roleVals.StreetAddress = []string{"123 foo street"} - addTests(getStreetAddressCheck(roleVals)) - - roleVals.StreetAddress = []string{"123 foo street", "456 bar avenue"} - addTests(getStreetAddressCheck(roleVals)) - } - // PostalCode tests - { - roleVals.PostalCode = []string{"f00"} - addTests(getPostalCodeCheck(roleVals)) - - roleVals.PostalCode = []string{"f00", "b4r"} - addTests(getPostalCodeCheck(roleVals)) - } - // NotBefore tests - { - roleVals.NotBeforeDuration = 10 * time.Second - addTests(getNotBeforeCheck(roleVals)) - - roleVals.NotBeforeDuration = 30 * time.Second - addTests(getNotBeforeCheck(roleVals)) - - roleVals.NotBeforeDuration = 0 - } - - // IP SAN tests - { - getIpCheck := func(expectedIp ...net.IP) logicaltest.TestCheckFunc { - return func(resp *logical.Response) error { - var certBundle certutil.CertBundle - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error parsing cert bundle: %s", err) - } - cert := parsedCertBundle.Certificate - var expected []net.IP - expected = append(expected, expectedIp...) - if diff := deep.Equal(cert.IPAddresses, expected); len(diff) > 0 { - return fmt.Errorf("wrong SAN IPs, diff: %v", diff) - } - return nil - } - } - addIPSANTests := func(useCSRs, useCSRSANs, allowIPSANs, errorOk bool, ipSANs string, csrIPSANs []net.IP, check logicaltest.TestCheckFunc) { - if useCSRs { - csrTemplate := &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: issueVals.CommonName, - }, - IPAddresses: csrIPSANs, - } - block, _ := getCsr(roleVals.KeyType, roleVals.KeyBits, csrTemplate) - issueVals.CSR = strings.TrimSpace(string(pem.EncodeToMemory(block))) - } - oldRoleVals, oldIssueVals, oldIssueTestStep := roleVals, issueVals, issueTestStep - roleVals.UseCSRSANs = useCSRSANs - roleVals.AllowIPSANs = allowIPSANs - issueVals.CommonName = "someone@example.com" - issueVals.IPSANs = ipSANs - issueTestStep.ErrorOk = errorOk - addTests(check) - roleVals, issueVals, issueTestStep = oldRoleVals, oldIssueVals, oldIssueTestStep - } - roleVals.AllowAnyName = true - roleVals.EnforceHostnames = true - roleVals.AllowLocalhost = true - roleVals.UseCSRCommonName = true - commonNames.Localhost = true - - netip1, netip2 := net.IP{127, 0, 0, 1}, net.IP{170, 171, 172, 173} - textip1, textip3 := "127.0.0.1", "::1" - - // IPSANs not allowed and not provided, should not be an error. - addIPSANTests(useCSRs, false, false, false, "", nil, getIpCheck()) - - // IPSANs not allowed, valid IPSANs provided, should be an error. - addIPSANTests(useCSRs, false, false, true, textip1+","+textip3, nil, nil) - - // IPSANs allowed, bogus IPSANs provided, should be an error. - addIPSANTests(useCSRs, false, true, true, "foobar", nil, nil) - - // Given IPSANs as API argument and useCSRSANs false, CSR arg ignored. - addIPSANTests(useCSRs, false, true, false, textip1, - []net.IP{netip2}, getIpCheck(netip1)) - - if useCSRs { - // IPSANs not allowed, valid IPSANs provided via CSR, should be an error. - addIPSANTests(useCSRs, true, false, true, "", []net.IP{netip1}, nil) - - // Given IPSANs as both API and CSR arguments and useCSRSANs=true, API arg ignored. - addIPSANTests(useCSRs, true, true, false, textip3, - []net.IP{netip1, netip2}, getIpCheck(netip1, netip2)) - } - } - - { - getOtherCheck := func(expectedOthers ...otherNameUtf8) logicaltest.TestCheckFunc { - return func(resp *logical.Response) error { - var certBundle certutil.CertBundle - err := mapstructure.Decode(resp.Data, &certBundle) - if err != nil { - return err - } - parsedCertBundle, err := certBundle.ToParsedCertBundle() - if err != nil { - return fmt.Errorf("error parsing cert bundle: %s", err) - } - cert := parsedCertBundle.Certificate - foundOthers, err := getOtherSANsFromX509Extensions(cert.Extensions) - if err != nil { - return err - } - var expected []otherNameUtf8 - expected = append(expected, expectedOthers...) - if diff := deep.Equal(foundOthers, expected); len(diff) > 0 { - return fmt.Errorf("wrong SAN IPs, diff: %v", diff) - } - return nil - } - } - - addOtherSANTests := func(useCSRs, useCSRSANs bool, allowedOtherSANs []string, errorOk bool, otherSANs []string, csrOtherSANs []otherNameUtf8, check logicaltest.TestCheckFunc) { - otherSansMap := func(os []otherNameUtf8) map[string][]string { - ret := make(map[string][]string) - for _, o := range os { - ret[o.oid] = append(ret[o.oid], o.value) - } - return ret - } - if useCSRs { - csrTemplate := &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: issueVals.CommonName, - }, - } - if err := handleOtherCSRSANs(csrTemplate, otherSansMap(csrOtherSANs)); err != nil { - t.Fatal(err) - } - block, _ := getCsr(roleVals.KeyType, roleVals.KeyBits, csrTemplate) - issueVals.CSR = strings.TrimSpace(string(pem.EncodeToMemory(block))) - } - oldRoleVals, oldIssueVals, oldIssueTestStep := roleVals, issueVals, issueTestStep - roleVals.UseCSRSANs = useCSRSANs - roleVals.AllowedOtherSANs = allowedOtherSANs - issueVals.CommonName = "someone@example.com" - issueVals.OtherSANs = strings.Join(otherSANs, ",") - issueTestStep.ErrorOk = errorOk - addTests(check) - roleVals, issueVals, issueTestStep = oldRoleVals, oldIssueVals, oldIssueTestStep - } - roleVals.AllowAnyName = true - roleVals.EnforceHostnames = true - roleVals.AllowLocalhost = true - roleVals.UseCSRCommonName = true - commonNames.Localhost = true - - newOtherNameUtf8 := func(s string) (ret otherNameUtf8) { - pieces := strings.Split(s, ";") - if len(pieces) == 2 { - piecesRest := strings.Split(pieces[1], ":") - if len(piecesRest) == 2 { - switch strings.ToUpper(piecesRest[0]) { - case "UTF-8", "UTF8": - return otherNameUtf8{oid: pieces[0], value: piecesRest[1]} - } - } - } - t.Fatalf("error parsing otherName: %q", s) - return - } - oid1 := "1.3.6.1.4.1.311.20.2.3" - oth1str := oid1 + ";utf8:devops@nope.com" - oth1 := newOtherNameUtf8(oth1str) - oth2 := otherNameUtf8{oid1, "me@example.com"} - // allowNone, allowAll := []string{}, []string{oid1 + ";UTF-8:*"} - allowNone, allowAll := []string{}, []string{"*"} - - // OtherSANs not allowed and not provided, should not be an error. - addOtherSANTests(useCSRs, false, allowNone, false, nil, nil, getOtherCheck()) - - // OtherSANs not allowed, valid OtherSANs provided, should be an error. - addOtherSANTests(useCSRs, false, allowNone, true, []string{oth1str}, nil, nil) - - // OtherSANs allowed, bogus OtherSANs provided, should be an error. - addOtherSANTests(useCSRs, false, allowAll, true, []string{"foobar"}, nil, nil) - - // Given OtherSANs as API argument and useCSRSANs false, CSR arg ignored. - addOtherSANTests(useCSRs, false, allowAll, false, []string{oth1str}, - []otherNameUtf8{oth2}, getOtherCheck(oth1)) - - if useCSRs { - // OtherSANs not allowed, valid OtherSANs provided via CSR, should be an error. - addOtherSANTests(useCSRs, true, allowNone, true, nil, []otherNameUtf8{oth1}, nil) - - // Given OtherSANs as both API and CSR arguments and useCSRSANs=true, API arg ignored. - addOtherSANTests(useCSRs, false, allowAll, false, []string{oth2.String()}, - []otherNameUtf8{oth1}, getOtherCheck(oth2)) - } - } - - // Lease tests - { - roleTestStep.ErrorOk = true - roleVals.Lease = "" - roleVals.MaxTTL = 0 - addTests(nil) - - roleVals.Lease = "12h" - roleVals.MaxTTL = 6 * time.Hour - addTests(nil) - - roleTestStep.ErrorOk = false - roleVals.TTL = 0 - roleVals.MaxTTL = 12 * time.Hour - } - - // Listing test - ret = append(ret, logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "roles/", - Check: func(resp *logical.Response) error { - if resp.Data == nil { - return fmt.Errorf("nil data") - } - - keysRaw, ok := resp.Data["keys"] - if !ok { - return fmt.Errorf("no keys found") - } - - keys, ok := keysRaw.([]string) - if !ok { - return fmt.Errorf("could not convert keys to a string list") - } - - if len(keys) != 1 { - return fmt.Errorf("unexpected keys length of %d", len(keys)) - } - - if keys[0] != "test" { - return fmt.Errorf("unexpected key value of %s", keys[0]) - } - - return nil - }, - }) - - return ret -} - -func TestRolesAltIssuer(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // Create two issuers. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root a - example.com", - "issuer_name": "root-a", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - rootAPem := resp.Data["certificate"].(string) - rootACert := parseCert(t, rootAPem) - - resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root b - example.com", - "issuer_name": "root-b", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - rootBPem := resp.Data["certificate"].(string) - rootBCert := parseCert(t, rootBPem) - - // Create three roles: one with no assignment, one with explicit root-a, - // one with explicit root-b. - _, err = CBWrite(b, s, "roles/use-default", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - }) - require.NoError(t, err) - - _, err = CBWrite(b, s, "roles/use-root-a", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - "issuer_ref": "root-a", - }) - require.NoError(t, err) - - _, err = CBWrite(b, s, "roles/use-root-b", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "issuer_ref": "root-b", - }) - require.NoError(t, err) - - // Now issue certs against these roles. - resp, err = CBWrite(b, s, "issue/use-default", map[string]interface{}{ - "common_name": "testing", - "ttl": "5s", - }) - require.NoError(t, err) - leafPem := resp.Data["certificate"].(string) - leafCert := parseCert(t, leafPem) - err = leafCert.CheckSignatureFrom(rootACert) - require.NoError(t, err, "should be signed by root-a but wasn't") - - resp, err = CBWrite(b, s, "issue/use-root-a", map[string]interface{}{ - "common_name": "testing", - "ttl": "5s", - }) - require.NoError(t, err) - leafPem = resp.Data["certificate"].(string) - leafCert = parseCert(t, leafPem) - err = leafCert.CheckSignatureFrom(rootACert) - require.NoError(t, err, "should be signed by root-a but wasn't") - - resp, err = CBWrite(b, s, "issue/use-root-b", map[string]interface{}{ - "common_name": "testing", - "ttl": "5s", - }) - require.NoError(t, err) - leafPem = resp.Data["certificate"].(string) - leafCert = parseCert(t, leafPem) - err = leafCert.CheckSignatureFrom(rootBCert) - require.NoError(t, err, "should be signed by root-b but wasn't") - - // Update the default issuer to be root B and make sure that the - // use-default role updates. - _, err = CBWrite(b, s, "config/issuers", map[string]interface{}{ - "default": "root-b", - }) - require.NoError(t, err) - - resp, err = CBWrite(b, s, "issue/use-default", map[string]interface{}{ - "common_name": "testing", - "ttl": "5s", - }) - require.NoError(t, err) - leafPem = resp.Data["certificate"].(string) - leafCert = parseCert(t, leafPem) - err = leafCert.CheckSignatureFrom(rootBCert) - require.NoError(t, err, "should be signed by root-b but wasn't") -} - -func TestBackend_PathFetchValidRaw(t *testing.T) { - t.Parallel() - b, storage := CreateBackendWithStorage(t) - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/generate/internal", - Storage: storage, - Data: map[string]interface{}{ - "common_name": "test.com", - "ttl": "6h", - }, - MountPoint: "pki/", - }) - require.NoError(t, err) - if resp != nil && resp.IsError() { - t.Fatalf("failed to generate root, %#v", resp) - } - rootCaAsPem := resp.Data["certificate"].(string) - - // Chain should contain the root. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "ca_chain", - Storage: storage, - Data: map[string]interface{}{}, - MountPoint: "pki/", - }) - require.NoError(t, err) - if resp != nil && resp.IsError() { - t.Fatalf("failed read ca_chain, %#v", resp) - } - if strings.Count(string(resp.Data[logical.HTTPRawBody].([]byte)), rootCaAsPem) != 1 { - t.Fatalf("expected raw chain to contain the root cert") - } - - // The ca/pem should return us the actual CA... - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "ca/pem", - Storage: storage, - Data: map[string]interface{}{}, - MountPoint: "pki/", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("ca/pem"), logical.ReadOperation), resp, true) - require.NoError(t, err) - if resp != nil && resp.IsError() { - t.Fatalf("failed read ca/pem, %#v", resp) - } - // check the raw cert matches the response body - if !bytes.Equal(resp.Data[logical.HTTPRawBody].([]byte), []byte(rootCaAsPem)) { - t.Fatalf("failed to get raw cert") - } - - _, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/example", - Storage: storage, - Data: map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "max_ttl": "1h", - "no_store": "false", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "error setting up pki role: %v", err) - - // Now issue a short-lived certificate from our pki-external. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issue/example", - Storage: storage, - Data: map[string]interface{}{ - "common_name": "test.example.com", - "ttl": "5m", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "error issuing certificate: %v", err) - require.NotNil(t, resp, "got nil response from issuing request") - - issueCrtAsPem := resp.Data["certificate"].(string) - issuedCrt := parseCert(t, issueCrtAsPem) - expectedSerial := serialFromCert(issuedCrt) - expectedCert := []byte(issueCrtAsPem) - - // get der cert - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: fmt.Sprintf("cert/%s/raw", expectedSerial), - Storage: storage, - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to get raw cert, %#v", resp) - } - if err != nil { - t.Fatal(err) - } - - // check the raw cert matches the response body - rawBody := resp.Data[logical.HTTPRawBody].([]byte) - bodyAsPem := []byte(strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rawBody})))) - if !bytes.Equal(bodyAsPem, expectedCert) { - t.Fatalf("failed to get raw cert for serial number: %s", expectedSerial) - } - if resp.Data[logical.HTTPContentType] != "application/pkix-cert" { - t.Fatalf("failed to get raw cert content-type") - } - - // get pem - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: fmt.Sprintf("cert/%s/raw/pem", expectedSerial), - Storage: storage, - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to get raw, %#v", resp) - } - if err != nil { - t.Fatal(err) - } - - // check the pem cert matches the response body - if !bytes.Equal(resp.Data[logical.HTTPRawBody].([]byte), expectedCert) { - t.Fatalf("failed to get pem cert") - } - if resp.Data[logical.HTTPContentType] != "application/pem-certificate-chain" { - t.Fatalf("failed to get raw cert content-type") - } -} - -func TestBackend_PathFetchCertList(t *testing.T) { - t.Parallel() - // create the backend - b, storage := CreateBackendWithStorage(t) - - // generate root - rootData := map[string]interface{}{ - "common_name": "test.com", - "ttl": "6h", - } - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/generate/internal", - Storage: storage, - Data: rootData, - MountPoint: "pki/", - }) - - if resp != nil && resp.IsError() { - t.Fatalf("failed to generate root, %#v", resp) - } - if err != nil { - t.Fatal(err) - } - - // config urls - urlsData := map[string]interface{}{ - "issuing_certificates": "http://127.0.0.1:8200/v1/pki/ca", - "crl_distribution_points": "http://127.0.0.1:8200/v1/pki/crl", - } - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/urls", - Storage: storage, - Data: urlsData, - MountPoint: "pki/", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/urls"), logical.UpdateOperation), resp, true) - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "config/urls", - Storage: storage, - MountPoint: "pki/", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/urls"), logical.ReadOperation), resp, true) - - if resp != nil && resp.IsError() { - t.Fatalf("failed to config urls, %#v", resp) - } - if err != nil { - t.Fatal(err) - } - - // create a role entry - roleData := map[string]interface{}{ - "allowed_domains": "test.com", - "allow_subdomains": "true", - "max_ttl": "4h", - } - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/test-example", - Storage: storage, - Data: roleData, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to create a role, %#v", resp) - } - if err != nil { - t.Fatal(err) - } - - // issue some certs - i := 1 - for i < 10 { - certData := map[string]interface{}{ - "common_name": "example.test.com", - } - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issue/test-example", - Storage: storage, - Data: certData, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to issue a cert, %#v", resp) - } - if err != nil { - t.Fatal(err) - } - - i = i + 1 - } - - // list certs - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ListOperation, - Path: "certs", - Storage: storage, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to list certs, %#v", resp) - } - if err != nil { - t.Fatal(err) - } - // check that the root and 9 additional certs are all listed - if len(resp.Data["keys"].([]string)) != 10 { - t.Fatalf("failed to list all 10 certs") - } - - // list certs/ - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ListOperation, - Path: "certs/", - Storage: storage, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to list certs, %#v", resp) - } - if err != nil { - t.Fatal(err) - } - // check that the root and 9 additional certs are all listed - if len(resp.Data["keys"].([]string)) != 10 { - t.Fatalf("failed to list all 10 certs") - } -} - -func TestBackend_SignVerbatim(t *testing.T) { - t.Parallel() - testCases := []struct { - testName string - keyType string - }{ - {testName: "RSA", keyType: "rsa"}, - {testName: "ED25519", keyType: "ed25519"}, - {testName: "EC", keyType: "ec"}, - {testName: "Any", keyType: "any"}, - } - for _, tc := range testCases { - tc := tc - t.Run(tc.testName, func(t *testing.T) { - runTestSignVerbatim(t, tc.keyType) - }) - } -} - -func runTestSignVerbatim(t *testing.T, keyType string) { - // create the backend - b, storage := CreateBackendWithStorage(t) - - // generate root - rootData := map[string]interface{}{ - "common_name": "test.com", - "not_after": "9999-12-31T23:59:59Z", - } - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/generate/internal", - Storage: storage, - Data: rootData, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to generate root, %#v", *resp) - } - if err != nil { - t.Fatal(err) - } - - // create a CSR and key - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatal(err) - } - csrReq := &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: "foo.bar.com", - }, - // Check that otherName extensions are not duplicated (see hashicorp/vault#16700). - // If these extensions are duplicated, sign-verbatim will fail when parsing the signed certificate on Go 1.19+ (see golang/go#50988). - // On older versions of Go this test will fail due to an explicit check for duplicate otherNames later in this test. - ExtraExtensions: []pkix.Extension{ - { - Id: oidExtensionSubjectAltName, - Critical: false, - Value: []byte{0x30, 0x26, 0xA0, 0x24, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, 0x02, 0x03, 0xA0, 0x16, 0x0C, 0x14, 0x75, 0x73, 0x65, 0x72, 0x6E, 0x61, 0x6D, 0x65, 0x40, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D}, - }, - }, - } - csr, err := x509.CreateCertificateRequest(rand.Reader, csrReq, key) - if err != nil { - t.Fatal(err) - } - if len(csr) == 0 { - t.Fatal("generated csr is empty") - } - pemCSR := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csr, - }))) - if len(pemCSR) == 0 { - t.Fatal("pem csr is empty") - } - - signVerbatimData := map[string]interface{}{ - "csr": pemCSR, - } - if keyType == "rsa" { - signVerbatimData["signature_bits"] = 512 - } - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sign-verbatim", - Storage: storage, - Data: signVerbatimData, - MountPoint: "pki/", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("sign-verbatim"), logical.UpdateOperation), resp, true) - - if resp != nil && resp.IsError() { - t.Fatalf("failed to sign-verbatim basic CSR: %#v", *resp) - } - if err != nil { - t.Fatal(err) - } - if resp.Secret != nil { - t.Fatal("secret is not nil") - } - - // create a role entry; we use this to check that sign-verbatim when used with a role is still honoring TTLs - roleData := map[string]interface{}{ - "ttl": "4h", - "max_ttl": "8h", - "key_type": keyType, - "not_before_duration": "2h", - } - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/test", - Storage: storage, - Data: roleData, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to create a role, %#v", *resp) - } - if err != nil { - t.Fatal(err) - } - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sign-verbatim/test", - Storage: storage, - Data: map[string]interface{}{ - "csr": pemCSR, - "ttl": "5h", - }, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to sign-verbatim ttl'd CSR: %#v", *resp) - } - if err != nil { - t.Fatal(err) - } - if resp.Secret != nil { - t.Fatal("got a lease when we should not have") - } - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sign-verbatim/test", - Storage: storage, - Data: map[string]interface{}{ - "csr": pemCSR, - "ttl": "12h", - }, - MountPoint: "pki/", - }) - if err != nil { - t.Fatal(err) - } - if resp != nil && resp.IsError() { - t.Fatalf(resp.Error().Error()) - } - if resp.Data == nil || resp.Data["certificate"] == nil { - t.Fatal("did not get expected data") - } - certString := resp.Data["certificate"].(string) - block, _ := pem.Decode([]byte(certString)) - if block == nil { - t.Fatal("nil pem block") - } - certs, err := x509.ParseCertificates(block.Bytes) - if err != nil { - t.Fatal(err) - } - if len(certs) != 1 { - t.Fatalf("expected a single cert, got %d", len(certs)) - } - cert := certs[0] - if math.Abs(float64(time.Now().Add(12*time.Hour).Unix()-cert.NotAfter.Unix())) < 10 { - t.Fatalf("sign-verbatim did not properly cap validity period (notAfter) on signed CSR: was %v vs requested %v but should've been %v", cert.NotAfter, time.Now().Add(12*time.Hour), time.Now().Add(8*time.Hour)) - } - if math.Abs(float64(time.Now().Add(-2*time.Hour).Unix()-cert.NotBefore.Unix())) > 10 { - t.Fatalf("sign-verbatim did not properly cap validity period (notBefore) on signed CSR: was %v vs expected %v", cert.NotBefore, time.Now().Add(-2*time.Hour)) - } - - // Now check signing a certificate using the not_after input using the Y10K value - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sign-verbatim/test", - Storage: storage, - Data: map[string]interface{}{ - "csr": pemCSR, - "not_after": "9999-12-31T23:59:59Z", - }, - MountPoint: "pki/", - }) - if err != nil { - t.Fatal(err) - } - if resp != nil && resp.IsError() { - t.Fatalf(resp.Error().Error()) - } - if resp.Data == nil || resp.Data["certificate"] == nil { - t.Fatal("did not get expected data") - } - certString = resp.Data["certificate"].(string) - block, _ = pem.Decode([]byte(certString)) - if block == nil { - t.Fatal("nil pem block") - } - certs, err = x509.ParseCertificates(block.Bytes) - if err != nil { - t.Fatal(err) - } - if len(certs) != 1 { - t.Fatalf("expected a single cert, got %d", len(certs)) - } - cert = certs[0] - - // Fallback check for duplicate otherName, necessary on Go versions before 1.19. - // We assume that there is only one SAN in the original CSR and that it is an otherName. - san_count := 0 - for _, ext := range cert.Extensions { - if ext.Id.Equal(oidExtensionSubjectAltName) { - san_count += 1 - } - } - if san_count != 1 { - t.Fatalf("expected one SAN extension, got %d", san_count) - } - - notAfter := cert.NotAfter.Format(time.RFC3339) - if notAfter != "9999-12-31T23:59:59Z" { - t.Fatal(fmt.Errorf("not after from certificate is not matching with input parameter")) - } - - // now check that if we set generate-lease it takes it from the role and the TTLs match - roleData = map[string]interface{}{ - "ttl": "4h", - "max_ttl": "8h", - "generate_lease": true, - "key_type": keyType, - } - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/test", - Storage: storage, - Data: roleData, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to create a role, %#v", *resp) - } - if err != nil { - t.Fatal(err) - } - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sign-verbatim/test", - Storage: storage, - Data: map[string]interface{}{ - "csr": pemCSR, - "ttl": "5h", - }, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to sign-verbatim role-leased CSR: %#v", *resp) - } - if err != nil { - t.Fatal(err) - } - if resp.Secret == nil { - t.Fatalf("secret is nil, response is %#v", *resp) - } - if math.Abs(float64(resp.Secret.TTL-(5*time.Hour))) > float64(5*time.Hour) { - t.Fatalf("ttl not default; wanted %v, got %v", b.System().DefaultLeaseTTL(), resp.Secret.TTL) - } -} - -func TestBackend_Root_Idempotency(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // This is a change within 1.11, we are no longer idempotent across generate/internal calls. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault.com", - }) - require.NoError(t, err) - require.NotNil(t, resp, "expected ca info") - keyId1 := resp.Data["key_id"] - issuerId1 := resp.Data["issuer_id"] - cert := parseCert(t, resp.Data["certificate"].(string)) - certSkid := certutil.GetHexFormatted(cert.SubjectKeyId, ":") - - // -> Validate the SKID matches between the root cert and the key - resp, err = CBRead(b, s, "key/"+keyId1.(keyID).String()) - require.NoError(t, err) - require.NotNil(t, resp, "expected a response") - require.Equal(t, resp.Data["subject_key_id"], certSkid) - - resp, err = CBRead(b, s, "cert/ca_chain") - require.NoError(t, err, "error reading ca_chain: %v", err) - - r1Data := resp.Data - - // Calling generate/internal should generate a new CA as well. - resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault.com", - }) - require.NoError(t, err) - require.NotNil(t, resp, "expected ca info") - keyId2 := resp.Data["key_id"] - issuerId2 := resp.Data["issuer_id"] - cert = parseCert(t, resp.Data["certificate"].(string)) - certSkid = certutil.GetHexFormatted(cert.SubjectKeyId, ":") - - // -> Validate the SKID matches between the root cert and the key - resp, err = CBRead(b, s, "key/"+keyId2.(keyID).String()) - require.NoError(t, err) - require.NotNil(t, resp, "expected a response") - require.Equal(t, resp.Data["subject_key_id"], certSkid) - - // Make sure that we actually generated different issuer and key values - require.NotEqual(t, keyId1, keyId2) - require.NotEqual(t, issuerId1, issuerId2) - - // Now because the issued CA's have no links, the call to ca_chain should return the same data (ca chain from default) - resp, err = CBRead(b, s, "cert/ca_chain") - require.NoError(t, err, "error reading ca_chain: %v", err) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("cert/ca_chain"), logical.ReadOperation), resp, true) - - r2Data := resp.Data - if !reflect.DeepEqual(r1Data, r2Data) { - t.Fatal("got different ca certs") - } - - // Now let's validate that the import bundle is idempotent. - pemBundleRootCA := rootCACertPEM + "\n" + rootCAKeyPEM - resp, err = CBWrite(b, s, "config/ca", map[string]interface{}{ - "pem_bundle": pemBundleRootCA, - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/ca"), logical.UpdateOperation), resp, true) - - require.NoError(t, err) - require.NotNil(t, resp, "expected ca info") - firstMapping := resp.Data["mapping"].(map[string]string) - firstImportedKeys := resp.Data["imported_keys"].([]string) - firstImportedIssuers := resp.Data["imported_issuers"].([]string) - firstExistingKeys := resp.Data["existing_keys"].([]string) - firstExistingIssuers := resp.Data["existing_issuers"].([]string) - - require.NotContains(t, firstImportedKeys, keyId1) - require.NotContains(t, firstImportedKeys, keyId2) - require.NotContains(t, firstImportedIssuers, issuerId1) - require.NotContains(t, firstImportedIssuers, issuerId2) - require.Empty(t, firstExistingKeys) - require.Empty(t, firstExistingIssuers) - require.NotEmpty(t, firstMapping) - require.Equal(t, 1, len(firstMapping)) - - var issuerId3 string - var keyId3 string - for i, k := range firstMapping { - issuerId3 = i - keyId3 = k - } - - // Performing this again should result in no key/issuer ids being imported/generated. - resp, err = CBWrite(b, s, "config/ca", map[string]interface{}{ - "pem_bundle": pemBundleRootCA, - }) - require.NoError(t, err) - require.NotNil(t, resp, "expected ca info") - secondMapping := resp.Data["mapping"].(map[string]string) - secondImportedKeys := resp.Data["imported_keys"] - secondImportedIssuers := resp.Data["imported_issuers"] - secondExistingKeys := resp.Data["existing_keys"] - secondExistingIssuers := resp.Data["existing_issuers"] - - require.Empty(t, secondImportedKeys) - require.Empty(t, secondImportedIssuers) - require.Contains(t, secondExistingKeys, keyId3) - require.Contains(t, secondExistingIssuers, issuerId3) - require.Equal(t, 1, len(secondMapping)) - - resp, err = CBDelete(b, s, "root") - require.NoError(t, err) - require.NotNil(t, resp) - require.Equal(t, 1, len(resp.Warnings)) - - // Make sure we can delete twice... - resp, err = CBDelete(b, s, "root") - require.NoError(t, err) - require.NotNil(t, resp) - require.Equal(t, 1, len(resp.Warnings)) - - _, err = CBRead(b, s, "cert/ca_chain") - require.Error(t, err, "expected an error fetching deleted ca_chain") - - // We should be able to import the same ca bundle as before and get a different key/issuer ids - resp, err = CBWrite(b, s, "config/ca", map[string]interface{}{ - "pem_bundle": pemBundleRootCA, - }) - require.NoError(t, err) - require.NotNil(t, resp, "expected ca info") - postDeleteImportedKeys := resp.Data["imported_keys"] - postDeleteImportedIssuers := resp.Data["imported_issuers"] - - // Make sure that we actually generated different issuer and key values, then the previous import - require.NotNil(t, postDeleteImportedKeys) - require.NotNil(t, postDeleteImportedIssuers) - require.NotEqual(t, postDeleteImportedKeys, firstImportedKeys) - require.NotEqual(t, postDeleteImportedIssuers, firstImportedIssuers) - - resp, err = CBRead(b, s, "cert/ca_chain") - require.NoError(t, err) - - caChainPostDelete := resp.Data - if reflect.DeepEqual(r1Data, caChainPostDelete) { - t.Fatal("ca certs from ca_chain were the same post delete, should have changed.") - } -} - -func TestBackend_SignIntermediate_AllowedPastCAValidity(t *testing.T) { - t.Parallel() - b_root, s_root := CreateBackendWithStorage(t) - b_int, s_int := CreateBackendWithStorage(t) - var err error - - // Direct issuing from root - _, err = CBWrite(b_root, s_root, "root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - - _, err = CBWrite(b_root, s_root, "roles/test", map[string]interface{}{ - "allow_bare_domains": true, - "allow_subdomains": true, - "allow_any_name": true, - }) - if err != nil { - t.Fatal(err) - } - - resp, err := CBWrite(b_int, s_int, "intermediate/generate/internal", map[string]interface{}{ - "common_name": "myint.com", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b_root.Route("intermediate/generate/internal"), logical.UpdateOperation), resp, true) - require.Contains(t, resp.Data, "key_id") - intKeyId := resp.Data["key_id"].(keyID) - csr := resp.Data["csr"] - - resp, err = CBRead(b_int, s_int, "key/"+intKeyId.String()) - require.NoError(t, err) - require.NotNil(t, resp, "expected a response") - intSkid := resp.Data["subject_key_id"].(string) - - if err != nil { - t.Fatal(err) - } - - _, err = CBWrite(b_root, s_root, "sign/test", map[string]interface{}{ - "common_name": "myint.com", - "csr": csr, - "ttl": "60h", - }) - require.ErrorContains(t, err, "that is beyond the expiration of the CA certificate") - - _, err = CBWrite(b_root, s_root, "sign-verbatim/test", map[string]interface{}{ - "common_name": "myint.com", - "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", - "csr": csr, - "ttl": "60h", - }) - require.ErrorContains(t, err, "that is beyond the expiration of the CA certificate") - - resp, err = CBWrite(b_root, s_root, "root/sign-intermediate", map[string]interface{}{ - "common_name": "myint.com", - "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", - "csr": csr, - "ttl": "60h", - }) - if err != nil { - t.Fatalf("got error: %v", err) - } - if resp == nil { - t.Fatal("got nil response") - } - if len(resp.Warnings) == 0 { - t.Fatalf("expected warnings, got %#v", *resp) - } - - cert := parseCert(t, resp.Data["certificate"].(string)) - certSkid := certutil.GetHexFormatted(cert.SubjectKeyId, ":") - require.Equal(t, intSkid, certSkid) -} - -func TestBackend_ConsulSignLeafWithLegacyRole(t *testing.T) { - t.Parallel() - // create the backend - b, s := CreateBackendWithStorage(t) - - // generate root - data, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - require.NoError(t, err, "failed generating internal root cert") - rootCaPem := data.Data["certificate"].(string) - - // Create a signing role like Consul did with the default args prior to Vault 1.10 - _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ - "allow_any_name": true, - "allowed_serial_numbers": []string{"MySerialNumber"}, - "key_type": "any", - "key_bits": "2048", - "signature_bits": "256", - }) - require.NoError(t, err, "failed creating legacy role") - - _, csrPem := generateTestCsr(t, certutil.ECPrivateKey, 256) - data, err = CBWrite(b, s, "sign/test", map[string]interface{}{ - "csr": csrPem, - }) - require.NoError(t, err, "failed signing csr") - certAsPem := data.Data["certificate"].(string) - - signedCert := parseCert(t, certAsPem) - rootCert := parseCert(t, rootCaPem) - requireSignedBy(t, signedCert, rootCert) -} - -func TestBackend_SignSelfIssued(t *testing.T) { - t.Parallel() - // create the backend - b, storage := CreateBackendWithStorage(t) - - // generate root - rootData := map[string]interface{}{ - "common_name": "test.com", - "ttl": "172800", - } - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/generate/internal", - Storage: storage, - Data: rootData, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to generate root, %#v", *resp) - } - if err != nil { - t.Fatal(err) - } - - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatal(err) - } - - template := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "foo.bar.com", - }, - SerialNumber: big.NewInt(1234), - IsCA: false, - BasicConstraintsValid: true, - } - - ss, _ := getSelfSigned(t, template, template, key) - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/sign-self-issued", - Storage: storage, - Data: map[string]interface{}{ - "certificate": ss, - }, - MountPoint: "pki/", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response") - } - if !resp.IsError() { - t.Fatalf("expected error due to non-CA; got: %#v", *resp) - } - - // Set CA to true, but leave issuer alone - template.IsCA = true - - issuer := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "bar.foo.com", - }, - SerialNumber: big.NewInt(2345), - IsCA: true, - BasicConstraintsValid: true, - } - ss, ssCert := getSelfSigned(t, template, issuer, key) - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/sign-self-issued", - Storage: storage, - Data: map[string]interface{}{ - "certificate": ss, - }, - MountPoint: "pki/", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response") - } - if !resp.IsError() { - t.Fatalf("expected error due to different issuer; cert info is\nIssuer\n%#v\nSubject\n%#v\n", ssCert.Issuer, ssCert.Subject) - } - - ss, _ = getSelfSigned(t, template, template, key) - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/sign-self-issued", - Storage: storage, - Data: map[string]interface{}{ - "certificate": ss, - }, - MountPoint: "pki/", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("root/sign-self-issued"), logical.UpdateOperation), resp, true) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response") - } - if resp.IsError() { - t.Fatalf("error in response: %s", resp.Error().Error()) - } - - newCertString := resp.Data["certificate"].(string) - block, _ := pem.Decode([]byte(newCertString)) - newCert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatal(err) - } - - sc := b.makeStorageContext(context.Background(), storage) - signingBundle, err := sc.fetchCAInfo(defaultRef, ReadOnlyUsage) - if err != nil { - t.Fatal(err) - } - if reflect.DeepEqual(newCert.Subject, newCert.Issuer) { - t.Fatal("expected different subject/issuer") - } - if !reflect.DeepEqual(newCert.Issuer, signingBundle.Certificate.Subject) { - t.Fatalf("expected matching issuer/CA subject\n\nIssuer:\n%#v\nSubject:\n%#v\n", newCert.Issuer, signingBundle.Certificate.Subject) - } - if bytes.Equal(newCert.AuthorityKeyId, newCert.SubjectKeyId) { - t.Fatal("expected different authority/subject") - } - if !bytes.Equal(newCert.AuthorityKeyId, signingBundle.Certificate.SubjectKeyId) { - t.Fatal("expected authority on new cert to be same as signing subject") - } - if newCert.Subject.CommonName != "foo.bar.com" { - t.Fatalf("unexpected common name on new cert: %s", newCert.Subject.CommonName) - } -} - -// TestBackend_SignSelfIssued_DifferentTypes tests the functionality of the -// require_matching_certificate_algorithms flag. -func TestBackend_SignSelfIssued_DifferentTypes(t *testing.T) { - t.Parallel() - // create the backend - b, storage := CreateBackendWithStorage(t) - - // generate root - rootData := map[string]interface{}{ - "common_name": "test.com", - "ttl": "172800", - "key_type": "ec", - "key_bits": "521", - } - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/generate/internal", - Storage: storage, - Data: rootData, - MountPoint: "pki/", - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to generate root, %#v", *resp) - } - if err != nil { - t.Fatal(err) - } - - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatal(err) - } - - template := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "foo.bar.com", - }, - SerialNumber: big.NewInt(1234), - IsCA: true, - BasicConstraintsValid: true, - } - - // Tests absent the flag - ss, _ := getSelfSigned(t, template, template, key) - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/sign-self-issued", - Storage: storage, - Data: map[string]interface{}{ - "certificate": ss, - }, - MountPoint: "pki/", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response") - } - - // Set CA to true, but leave issuer alone - template.IsCA = true - - // Tests with flag present but false - ss, _ = getSelfSigned(t, template, template, key) - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/sign-self-issued", - Storage: storage, - Data: map[string]interface{}{ - "certificate": ss, - "require_matching_certificate_algorithms": false, - }, - MountPoint: "pki/", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response") - } - - // Test with flag present and true - ss, _ = getSelfSigned(t, template, template, key) - _, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/sign-self-issued", - Storage: storage, - Data: map[string]interface{}{ - "certificate": ss, - "require_matching_certificate_algorithms": true, - }, - MountPoint: "pki/", - }) - if err == nil { - t.Fatal("expected error due to mismatched algorithms") - } -} - -// This is a really tricky test because the Go stdlib asn1 package is incapable -// of doing the right thing with custom OID SANs (see comments in the package, -// it's readily admitted that it's too magic) but that means that any -// validation logic written for this test isn't being independently verified, -// as in, if cryptobytes is used to decode it to make the test work, that -// doesn't mean we're encoding and decoding correctly, only that we made the -// test pass. Instead, when run verbosely it will first perform a bunch of -// checks to verify that the OID SAN logic doesn't screw up other SANs, then -// will spit out the PEM. This can be validated independently. -// -// You want the hex dump of the octet string corresponding to the X509v3 -// Subject Alternative Name. There's a nice online utility at -// https://lapo.it/asn1js that can be used to view the structure of an -// openssl-generated other SAN at -// https://lapo.it/asn1js/#3022A020060A2B060104018237140203A0120C106465766F7073406C6F63616C686F7374 -// (openssl asn1parse can also be used with -strparse using an offset of the -// hex blob for the subject alternative names extension). -// -// The structure output from here should match that precisely (even if the OID -// itself doesn't) in the second test. -// -// The test that encodes two should have them be in separate elements in the -// top-level sequence; see -// https://lapo.it/asn1js/#3046A020060A2B060104018237140203A0120C106465766F7073406C6F63616C686F7374A022060A2B060104018237140204A0140C12322D6465766F7073406C6F63616C686F7374 for an openssl-generated example. -// -// The good news is that it's valid to simply copy and paste the PEM output from -// here into the form at that site as it will do the right thing so it's pretty -// easy to validate. -func TestBackend_OID_SANs(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - var err error - var resp *logical.Response - var certStr string - var block *pem.Block - var cert *x509.Certificate - - _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - - _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ - "allowed_domains": []string{"foobar.com", "zipzap.com"}, - "allow_bare_domains": true, - "allow_subdomains": true, - "allow_ip_sans": true, - "allowed_other_sans": "1.3.6.1.4.1.311.20.2.3;UTF8:devops@*,1.3.6.1.4.1.311.20.2.4;utf8:d*e@foobar.com", - }) - if err != nil { - t.Fatal(err) - } - - // Get a baseline before adding OID SANs. In the next sections we'll verify - // that the SANs are all added even as the OID SAN inclusion forces other - // adding logic (custom rather than built-in Golang logic) - resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foobar.com,foo.foobar.com,bar.foobar.com", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - certStr = resp.Data["certificate"].(string) - block, _ = pem.Decode([]byte(certStr)) - cert, err = x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatal(err) - } - if cert.IPAddresses[0].String() != "1.2.3.4" { - t.Fatalf("unexpected IP SAN %q", cert.IPAddresses[0].String()) - } - if len(cert.DNSNames) != 3 || - cert.DNSNames[0] != "bar.foobar.com" || - cert.DNSNames[1] != "foo.foobar.com" || - cert.DNSNames[2] != "foobar.com" { - t.Fatalf("unexpected DNS SANs %v", cert.DNSNames) - } - - // First test some bad stuff that shouldn't work - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - // Not a valid value for the first possibility - "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF8:devop@nope.com", - }) - if err == nil { - t.Fatal("expected error") - } - - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - // Not a valid OID for the first possibility - "other_sans": "1.3.6.1.4.1.311.20.2.5;UTF8:devops@nope.com", - }) - if err == nil { - t.Fatal("expected error") - } - - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - // Not a valid name for the second possibility - "other_sans": "1.3.6.1.4.1.311.20.2.4;UTF8:d34g@foobar.com", - }) - if err == nil { - t.Fatal("expected error") - } - - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - // Not a valid OID for the second possibility - "other_sans": "1.3.6.1.4.1.311.20.2.5;UTF8:d34e@foobar.com", - }) - if err == nil { - t.Fatal("expected error") - } - - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - // Not a valid type - "other_sans": "1.3.6.1.4.1.311.20.2.5;UTF2:d34e@foobar.com", - }) - if err == nil { - t.Fatal("expected error") - } - - // Valid for first possibility - resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:devops@nope.com", - }) - if err != nil { - t.Fatal(err) - } - certStr = resp.Data["certificate"].(string) - block, _ = pem.Decode([]byte(certStr)) - cert, err = x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatal(err) - } - if cert.IPAddresses[0].String() != "1.2.3.4" { - t.Fatalf("unexpected IP SAN %q", cert.IPAddresses[0].String()) - } - if len(cert.DNSNames) != 3 || - cert.DNSNames[0] != "bar.foobar.com" || - cert.DNSNames[1] != "foo.foobar.com" || - cert.DNSNames[2] != "foobar.com" { - t.Fatalf("unexpected DNS SANs %v", cert.DNSNames) - } - if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { - t.Logf("certificate 1 to check:\n%s", certStr) - } - - // Valid for second possibility - resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - "other_sans": "1.3.6.1.4.1.311.20.2.4;UTF8:d234e@foobar.com", - }) - if err != nil { - t.Fatal(err) - } - certStr = resp.Data["certificate"].(string) - block, _ = pem.Decode([]byte(certStr)) - cert, err = x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatal(err) - } - if cert.IPAddresses[0].String() != "1.2.3.4" { - t.Fatalf("unexpected IP SAN %q", cert.IPAddresses[0].String()) - } - if len(cert.DNSNames) != 3 || - cert.DNSNames[0] != "bar.foobar.com" || - cert.DNSNames[1] != "foo.foobar.com" || - cert.DNSNames[2] != "foobar.com" { - t.Fatalf("unexpected DNS SANs %v", cert.DNSNames) - } - if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { - t.Logf("certificate 2 to check:\n%s", certStr) - } - - // Valid for both - oid1, type1, val1 := "1.3.6.1.4.1.311.20.2.3", "utf8", "devops@nope.com" - oid2, type2, val2 := "1.3.6.1.4.1.311.20.2.4", "utf-8", "d234e@foobar.com" - otherNames := []string{ - fmt.Sprintf("%s;%s:%s", oid1, type1, val1), - fmt.Sprintf("%s;%s:%s", oid2, type2, val2), - } - resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - "other_sans": strings.Join(otherNames, ","), - }) - if err != nil { - t.Fatal(err) - } - certStr = resp.Data["certificate"].(string) - block, _ = pem.Decode([]byte(certStr)) - cert, err = x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatal(err) - } - if cert.IPAddresses[0].String() != "1.2.3.4" { - t.Fatalf("unexpected IP SAN %q", cert.IPAddresses[0].String()) - } - if len(cert.DNSNames) != 3 || - cert.DNSNames[0] != "bar.foobar.com" || - cert.DNSNames[1] != "foo.foobar.com" || - cert.DNSNames[2] != "foobar.com" { - t.Fatalf("unexpected DNS SANs %v", cert.DNSNames) - } - expectedOtherNames := []otherNameUtf8{{oid1, val1}, {oid2, val2}} - foundOtherNames, err := getOtherSANsFromX509Extensions(cert.Extensions) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(expectedOtherNames, foundOtherNames); len(diff) != 0 { - t.Errorf("unexpected otherNames: %v", diff) - } - if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { - t.Logf("certificate 3 to check:\n%s", certStr) - } -} - -func TestBackend_AllowedSerialNumbers(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - var err error - var resp *logical.Response - var certStr string - var block *pem.Block - var cert *x509.Certificate - - _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - - // First test that Serial Numbers are not allowed - _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - }) - if err != nil { - t.Fatal(err) - } - - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar", - "ttl": "1h", - "serial_number": "foobar", - }) - if err == nil { - t.Fatal("expected error") - } - - // Update the role to allow serial numbers - _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "allowed_serial_numbers": "f00*,b4r*", - }) - if err != nil { - t.Fatal(err) - } - - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar", - "ttl": "1h", - // Not a valid serial number - "serial_number": "foobar", - }) - if err == nil { - t.Fatal("expected error") - } - - // Valid for first possibility - resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar", - "serial_number": "f00bar", - }) - if err != nil { - t.Fatal(err) - } - certStr = resp.Data["certificate"].(string) - block, _ = pem.Decode([]byte(certStr)) - cert, err = x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatal(err) - } - if cert.Subject.SerialNumber != "f00bar" { - t.Fatalf("unexpected Subject SerialNumber %s", cert.Subject.SerialNumber) - } - if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { - t.Logf("certificate 1 to check:\n%s", certStr) - } - - // Valid for second possibility - resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar", - "serial_number": "b4rf00", - }) - if err != nil { - t.Fatal(err) - } - certStr = resp.Data["certificate"].(string) - block, _ = pem.Decode([]byte(certStr)) - cert, err = x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatal(err) - } - if cert.Subject.SerialNumber != "b4rf00" { - t.Fatalf("unexpected Subject SerialNumber %s", cert.Subject.SerialNumber) - } - if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { - t.Logf("certificate 2 to check:\n%s", certStr) - } -} - -func TestBackend_URI_SANs(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - var err error - - _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - - _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ - "allowed_domains": []string{"foobar.com", "zipzap.com"}, - "allow_bare_domains": true, - "allow_subdomains": true, - "allow_ip_sans": true, - "allowed_uri_sans": []string{"http://someuri/abc", "spiffe://host.com/*"}, - }) - if err != nil { - t.Fatal(err) - } - - // First test some bad stuff that shouldn't work - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - "uri_sans": "http://www.mydomain.com/zxf", - }) - if err == nil { - t.Fatal("expected error") - } - - // Test valid single entry - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - "uri_sans": "http://someuri/abc", - }) - if err != nil { - t.Fatal(err) - } - - // Test globed entry - _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - "uri_sans": "spiffe://host.com/something", - }) - if err != nil { - t.Fatal(err) - } - - // Test multiple entries - resp, err := CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "foobar.com", - "ip_sans": "1.2.3.4", - "alt_names": "foo.foobar.com,bar.foobar.com", - "ttl": "1h", - "uri_sans": "spiffe://host.com/something,http://someuri/abc", - }) - if err != nil { - t.Fatal(err) - } - - certStr := resp.Data["certificate"].(string) - block, _ := pem.Decode([]byte(certStr)) - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatal(err) - } - - URI0, _ := url.Parse("spiffe://host.com/something") - URI1, _ := url.Parse("http://someuri/abc") - - if len(cert.URIs) != 2 { - t.Fatalf("expected 2 valid URIs SANs %v", cert.URIs) - } - - if cert.URIs[0].String() != URI0.String() || cert.URIs[1].String() != URI1.String() { - t.Fatalf( - "expected URIs SANs %v to equal provided values spiffe://host.com/something, http://someuri/abc", - cert.URIs) - } -} - -func TestBackend_AllowedURISANsTemplate(t *testing.T) { - t.Parallel() - coreConfig := &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": userpass.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Write test policy for userpass auth method. - err := client.Sys().PutPolicy("test", ` - path "pki/*" { - capabilities = ["update"] - }`) - if err != nil { - t.Fatal(err) - } - - // Enable userpass auth method. - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - - // Configure test role for userpass. - if _, err := client.Logical().Write("auth/userpass/users/userpassname", map[string]interface{}{ - "password": "test", - "policies": "test", - }); err != nil { - t.Fatal(err) - } - - // Login userpass for test role and keep client token. - secret, err := client.Logical().Write("auth/userpass/login/userpassname", map[string]interface{}{ - "password": "test", - }) - if err != nil || secret == nil { - t.Fatal(err) - } - userpassToken := secret.Auth.ClientToken - - // Get auth accessor for identity template. - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - userpassAccessor := auths["userpass/"].Accessor - - // Mount PKI. - err = client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Generate internal CA. - _, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - - // Write role PKI. - _, err = client.Logical().Write("pki/roles/test", map[string]interface{}{ - "allowed_uri_sans": []string{ - "spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}", - "spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}/*", "spiffe://domain/foo", - }, - "allowed_uri_sans_template": true, - "require_cn": false, - }) - if err != nil { - t.Fatal(err) - } - - // Issue certificate with identity templating - client.SetToken(userpassToken) - _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname, spiffe://domain/foo"}) - if err != nil { - t.Fatal(err) - } - - // Issue certificate with identity templating and glob - client.SetToken(userpassToken) - _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname/bar"}) - if err != nil { - t.Fatal(err) - } - - // Issue certificate with non-matching identity template parameter - client.SetToken(userpassToken) - _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/unknownuser"}) - if err == nil { - t.Fatal(err) - } - - // Set allowed_uri_sans_template to false. - _, err = client.Logical().Write("pki/roles/test", map[string]interface{}{ - "allowed_uri_sans_template": false, - }) - if err != nil { - t.Fatal(err) - } - - // Issue certificate with userpassToken. - _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/users/userpassname"}) - if err == nil { - t.Fatal("expected error") - } -} - -func TestBackend_AllowedDomainsTemplate(t *testing.T) { - t.Parallel() - coreConfig := &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": userpass.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Write test policy for userpass auth method. - err := client.Sys().PutPolicy("test", ` - path "pki/*" { - capabilities = ["update"] - }`) - if err != nil { - t.Fatal(err) - } - - // Enable userpass auth method. - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - - // Configure test role for userpass. - if _, err := client.Logical().Write("auth/userpass/users/userpassname", map[string]interface{}{ - "password": "test", - "policies": "test", - }); err != nil { - t.Fatal(err) - } - - // Login userpass for test role and set client token - userpassAuth, err := auth.NewUserpassAuth("userpassname", &auth.Password{FromString: "test"}) - if err != nil { - t.Fatal(err) - } - - // Get auth accessor for identity template. - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - userpassAccessor := auths["userpass/"].Accessor - - // Mount PKI. - err = client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Generate internal CA. - _, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - - // Write role PKI. - _, err = client.Logical().Write("pki/roles/test", map[string]interface{}{ - "allowed_domains": []string{ - "foobar.com", "zipzap.com", "{{identity.entity.aliases." + userpassAccessor + ".name}}", - "foo.{{identity.entity.aliases." + userpassAccessor + ".name}}.example.com", - }, - "allowed_domains_template": true, - "allow_bare_domains": true, - }) - if err != nil { - t.Fatal(err) - } - - // Issue certificate with userpassToken. - secret, err := client.Auth().Login(context.TODO(), userpassAuth) - if err != nil { - t.Fatal(err) - } - if err != nil || secret == nil { - t.Fatal(err) - } - _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "userpassname"}) - if err != nil { - t.Fatal(err) - } - - // Issue certificate for foobar.com to verify allowed_domain_template doesn't break plain domains. - _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "foobar.com"}) - if err != nil { - t.Fatal(err) - } - - // Issue certificate for unknown userpassname. - _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "unknownuserpassname"}) - if err == nil { - t.Fatal("expected error") - } - - // Issue certificate for foo.userpassname.domain. - _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "foo.userpassname.example.com"}) - if err != nil { - t.Fatal("expected error") - } - - // Set allowed_domains_template to false. - _, err = client.Logical().Write("pki/roles/test", map[string]interface{}{ - "allowed_domains_template": false, - }) - if err != nil { - t.Fatal(err) - } - - // Issue certificate with userpassToken. - _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "userpassname"}) - if err == nil { - t.Fatal("expected error") - } -} - -func TestReadWriteDeleteRoles(t *testing.T) { - t.Parallel() - ctx := context.Background() - coreConfig := &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": userpass.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Mount PKI. - err := client.Sys().MountWithContext(ctx, "pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }) - if err != nil { - t.Fatal(err) - } - - resp, err := client.Logical().ReadWithContext(ctx, "pki/roles/test") - if err != nil { - t.Fatal(err) - } - - if resp != nil { - t.Fatalf("response should have been emtpy but was:\n%#v", resp) - } - - // Write role PKI. - _, err = client.Logical().WriteWithContext(ctx, "pki/roles/test", map[string]interface{}{}) - if err != nil { - t.Fatal(err) - } - - // Read the role. - resp, err = client.Logical().ReadWithContext(ctx, "pki/roles/test") - if err != nil { - t.Fatal(err) - } - - if resp.Data == nil { - t.Fatal("default data within response was nil when it should have contained data") - } - - // Validate that we have not changed any defaults unknowingly - expectedData := map[string]interface{}{ - "key_type": "rsa", - "use_csr_sans": true, - "client_flag": true, - "allowed_serial_numbers": []interface{}{}, - "generate_lease": false, - "signature_bits": json.Number("256"), - "use_pss": false, - "allowed_domains": []interface{}{}, - "allowed_uri_sans_template": false, - "enforce_hostnames": true, - "policy_identifiers": []interface{}{}, - "require_cn": true, - "allowed_domains_template": false, - "allow_token_displayname": false, - "country": []interface{}{}, - "not_after": "", - "postal_code": []interface{}{}, - "use_csr_common_name": true, - "allow_localhost": true, - "allow_subdomains": false, - "allow_wildcard_certificates": true, - "allowed_other_sans": []interface{}{}, - "allowed_uri_sans": []interface{}{}, - "basic_constraints_valid_for_non_ca": false, - "key_usage": []interface{}{"DigitalSignature", "KeyAgreement", "KeyEncipherment"}, - "not_before_duration": json.Number("30"), - "allow_glob_domains": false, - "ttl": json.Number("0"), - "ou": []interface{}{}, - "email_protection_flag": false, - "locality": []interface{}{}, - "server_flag": true, - "allow_bare_domains": false, - "allow_ip_sans": true, - "ext_key_usage_oids": []interface{}{}, - "allow_any_name": false, - "ext_key_usage": []interface{}{}, - "key_bits": json.Number("2048"), - "max_ttl": json.Number("0"), - "no_store": false, - "organization": []interface{}{}, - "province": []interface{}{}, - "street_address": []interface{}{}, - "code_signing_flag": false, - "issuer_ref": "default", - "cn_validations": []interface{}{"email", "hostname"}, - "allowed_user_ids": []interface{}{}, - } - - if diff := deep.Equal(expectedData, resp.Data); len(diff) > 0 { - t.Fatalf("pki role default values have changed, diff: %v", diff) - } - - _, err = client.Logical().DeleteWithContext(ctx, "pki/roles/test") - if err != nil { - t.Fatal(err) - } - - resp, err = client.Logical().ReadWithContext(ctx, "pki/roles/test") - if err != nil { - t.Fatal(err) - } - - if resp != nil { - t.Fatalf("response should have been empty but was:\n%#v", resp) - } -} - -func setCerts() { - cak, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(err) - } - marshaledKey, err := x509.MarshalECPrivateKey(cak) - if err != nil { - panic(err) - } - keyPEMBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: marshaledKey, - } - ecCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - if err != nil { - panic(err) - } - subjKeyID, err := certutil.GetSubjKeyID(cak) - if err != nil { - panic(err) - } - caCertTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "root.localhost", - }, - SubjectKeyId: subjKeyID, - DNSNames: []string{"root.localhost"}, - KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), - SerialNumber: big.NewInt(mathrand.Int63()), - NotAfter: time.Now().Add(262980 * time.Hour), - BasicConstraintsValid: true, - IsCA: true, - } - caBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, cak.Public(), cak) - if err != nil { - panic(err) - } - caCertPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - } - ecCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) - - rak, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - panic(err) - } - marshaledKey = x509.MarshalPKCS1PrivateKey(rak) - keyPEMBlock = &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: marshaledKey, - } - rsaCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - if err != nil { - panic(err) - } - _, err = certutil.GetSubjKeyID(rak) - if err != nil { - panic(err) - } - caBytes, err = x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, rak.Public(), rak) - if err != nil { - panic(err) - } - caCertPEMBlock = &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - } - rsaCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) - - _, edk, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - panic(err) - } - marshaledKey, err = x509.MarshalPKCS8PrivateKey(edk) - if err != nil { - panic(err) - } - keyPEMBlock = &pem.Block{ - Type: "PRIVATE KEY", - Bytes: marshaledKey, - } - edCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - if err != nil { - panic(err) - } - _, err = certutil.GetSubjKeyID(edk) - if err != nil { - panic(err) - } - caBytes, err = x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, edk.Public(), edk) - if err != nil { - panic(err) - } - caCertPEMBlock = &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - } - edCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) -} - -func TestBackend_RevokePlusTidy_Intermediate(t *testing.T) { - // Use a ridiculously long time to minimize the chance - // that we have to deal with more than one interval. - // InMemSink rounds down to an interval boundary rather than - // starting one at the time of initialization. - // - // This test is not parallelizable. - inmemSink := metrics.NewInmemSink( - 1000000*time.Hour, - 2000000*time.Hour) - - metricsConf := metrics.DefaultConfig("") - metricsConf.EnableHostname = false - metricsConf.EnableHostnameLabel = false - metricsConf.EnableServiceLabel = false - metricsConf.EnableTypePrefix = false - - _, err := metrics.NewGlobal(metricsConf, inmemSink) - if err != nil { - t.Fatal(err) - } - - // Enable PKI secret engine - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - client := cores[0].Client - - // Mount /pki as a root CA - err = client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "32h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Set up Metric Configuration, then restart to enable it - _, err = client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ - "maintain_stored_certificate_counts": true, - "publish_stored_certificate_count_metrics": true, - }) - _, err = client.Logical().Write("/sys/plugins/reload/backend", map[string]interface{}{ - "mounts": "pki/", - }) - - // Check the metrics initialized in order to calculate backendUUID for /pki - // BackendUUID not consistent during tests with UUID from /sys/mounts/pki - metricsSuffix := "total_certificates_stored" - backendUUID := "" - mostRecentInterval := inmemSink.Data()[len(inmemSink.Data())-1] - for _, existingGauge := range mostRecentInterval.Gauges { - if strings.HasSuffix(existingGauge.Name, metricsSuffix) { - expandedGaugeName := existingGauge.Name - backendUUID = strings.Split(expandedGaugeName, ".")[2] - break - } - } - if backendUUID == "" { - t.Fatalf("No Gauge Found ending with %s", metricsSuffix) - } - - // Set the cluster's certificate as the root CA in /pki - pemBundleRootCA := string(cluster.CACertPEM) + string(cluster.CAKeyPEM) - _, err = client.Logical().Write("pki/config/ca", map[string]interface{}{ - "pem_bundle": pemBundleRootCA, - }) - if err != nil { - t.Fatal(err) - } - - // Mount /pki2 to operate as an intermediate CA - err = client.Sys().Mount("pki2", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "32h", - }, - }) - if err != nil { - t.Fatal(err) - } - // Set up Metric Configuration, then restart to enable it - _, err = client.Logical().Write("pki2/config/auto-tidy", map[string]interface{}{ - "maintain_stored_certificate_counts": true, - "publish_stored_certificate_count_metrics": true, - }) - _, err = client.Logical().Write("/sys/plugins/reload/backend", map[string]interface{}{ - "mounts": "pki2/", - }) - - // Create a CSR for the intermediate CA - secret, err := client.Logical().Write("pki2/intermediate/generate/internal", nil) - if err != nil { - t.Fatal(err) - } - intermediateCSR := secret.Data["csr"].(string) - - // Sign the intermediate CSR using /pki - secret, err = client.Logical().Write("pki/root/sign-intermediate", map[string]interface{}{ - "permitted_dns_domains": ".myvault.com", - "csr": intermediateCSR, - "ttl": "10s", - }) - if err != nil { - t.Fatal(err) - } - intermediateCertSerial := secret.Data["serial_number"].(string) - intermediateCASerialColon := strings.ReplaceAll(strings.ToLower(intermediateCertSerial), ":", "-") - - // Get the intermediate cert after signing - secret, err = client.Logical().Read("pki/cert/" + intermediateCASerialColon) - if err != nil { - t.Fatal(err) - } - - if secret == nil || len(secret.Data) == 0 || len(secret.Data["certificate"].(string)) == 0 { - t.Fatal("expected certificate information from read operation") - } - - // Issue a revoke on on /pki - _, err = client.Logical().Write("pki/revoke", map[string]interface{}{ - "serial_number": intermediateCertSerial, - }) - if err != nil { - t.Fatal(err) - } - - // Check the cert-count metrics - expectedCertCountGaugeMetrics := map[string]float32{ - "secrets.pki." + backendUUID + ".total_revoked_certificates_stored": 1, - "secrets.pki." + backendUUID + ".total_certificates_stored": 1, - } - mostRecentInterval = inmemSink.Data()[len(inmemSink.Data())-1] - for gauge, value := range expectedCertCountGaugeMetrics { - if _, ok := mostRecentInterval.Gauges[gauge]; !ok { - t.Fatalf("Expected metrics to include a value for gauge %s", gauge) - } - if value != mostRecentInterval.Gauges[gauge].Value { - t.Fatalf("Expected value metric %s to be %f but got %f", gauge, value, mostRecentInterval.Gauges[gauge].Value) - } - } - - // Revoke adds a fixed 2s buffer, so we sleep for a bit longer to ensure - // the revocation time is past the current time. - time.Sleep(3 * time.Second) - - // Issue a tidy on /pki - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_cert_store": true, - "tidy_revoked_certs": true, - "safety_buffer": "1s", - }) - if err != nil { - t.Fatal(err) - } - - // Sleep a bit to make sure we're past the safety buffer - time.Sleep(2 * time.Second) - - // Get CRL and ensure the tidied cert is still in the list after the tidy - // operation since it's not past the NotAfter (ttl) value yet. - crl := getParsedCrl(t, client, "pki") - - revokedCerts := crl.TBSCertList.RevokedCertificates - if len(revokedCerts) == 0 { - t.Fatal("expected CRL to be non-empty") - } - - sn := certutil.GetHexFormatted(revokedCerts[0].SerialNumber.Bytes(), ":") - if sn != intermediateCertSerial { - t.Fatalf("expected: %v, got: %v", intermediateCertSerial, sn) - } - - // Wait for cert to expire - time.Sleep(10 * time.Second) - - // Issue a tidy on /pki - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_cert_store": true, - "tidy_revoked_certs": true, - "safety_buffer": "1s", - }) - if err != nil { - t.Fatal(err) - } - - // Sleep a bit to make sure we're past the safety buffer - time.Sleep(2 * time.Second) - - // Issue a tidy-status on /pki - { - tidyStatus, err := client.Logical().Read("pki/tidy-status") - if err != nil { - t.Fatal(err) - } - expectedData := map[string]interface{}{ - "safety_buffer": json.Number("1"), - "issuer_safety_buffer": json.Number("31536000"), - "revocation_queue_safety_buffer": json.Number("172800"), - "tidy_cert_store": true, - "tidy_revoked_certs": true, - "tidy_revoked_cert_issuer_associations": false, - "tidy_expired_issuers": false, - "tidy_move_legacy_ca_bundle": false, - "tidy_revocation_queue": false, - "tidy_cross_cluster_revoked_certs": false, - "pause_duration": "0s", - "state": "Finished", - "error": nil, - "time_started": nil, - "time_finished": nil, - "last_auto_tidy_finished": nil, - "message": nil, - "cert_store_deleted_count": json.Number("1"), - "revoked_cert_deleted_count": json.Number("1"), - "missing_issuer_cert_count": json.Number("0"), - "current_cert_store_count": json.Number("0"), - "current_revoked_cert_count": json.Number("0"), - "revocation_queue_deleted_count": json.Number("0"), - "cross_revoked_cert_deleted_count": json.Number("0"), - "internal_backend_uuid": backendUUID, - "tidy_acme": false, - "acme_account_safety_buffer": json.Number("2592000"), - "acme_orders_deleted_count": json.Number("0"), - "acme_account_revoked_count": json.Number("0"), - "acme_account_deleted_count": json.Number("0"), - "total_acme_account_count": json.Number("0"), - } - // Let's copy the times from the response so that we can use deep.Equal() - timeStarted, ok := tidyStatus.Data["time_started"] - if !ok || timeStarted == "" { - t.Fatal("Expected tidy status response to include a value for time_started") - } - expectedData["time_started"] = timeStarted - timeFinished, ok := tidyStatus.Data["time_finished"] - if !ok || timeFinished == "" { - t.Fatal("Expected tidy status response to include a value for time_finished") - } - expectedData["time_finished"] = timeFinished - expectedData["last_auto_tidy_finished"] = tidyStatus.Data["last_auto_tidy_finished"] - - if diff := deep.Equal(expectedData, tidyStatus.Data); diff != nil { - t.Fatal(diff) - } - } - // Check the tidy metrics - { - // Map of gauges to expected value - expectedGauges := map[string]float32{ - "secrets.pki.tidy.cert_store_current_entry": 0, - "secrets.pki.tidy.cert_store_total_entries": 1, - "secrets.pki.tidy.revoked_cert_current_entry": 0, - "secrets.pki.tidy.revoked_cert_total_entries": 1, - "secrets.pki.tidy.start_time_epoch": 0, - "secrets.pki." + backendUUID + ".total_certificates_stored": 0, - "secrets.pki." + backendUUID + ".total_revoked_certificates_stored": 0, - "secrets.pki.tidy.cert_store_total_entries_remaining": 0, - "secrets.pki.tidy.revoked_cert_total_entries_remaining": 0, - } - // Map of counters to the sum of the metrics for that counter - expectedCounters := map[string]float64{ - "secrets.pki.tidy.cert_store_deleted_count": 1, - "secrets.pki.tidy.revoked_cert_deleted_count": 1, - "secrets.pki.tidy.success": 2, - // Note that "secrets.pki.tidy.failure" won't be in the captured metrics - } - - // If the metrics span more than one interval, skip the checks - intervals := inmemSink.Data() - if len(intervals) == 1 { - interval := inmemSink.Data()[0] - - for gauge, value := range expectedGauges { - if _, ok := interval.Gauges[gauge]; !ok { - t.Fatalf("Expected metrics to include a value for gauge %s", gauge) - } - if value != interval.Gauges[gauge].Value { - t.Fatalf("Expected value metric %s to be %f but got %f", gauge, value, interval.Gauges[gauge].Value) - } - - } - for counter, value := range expectedCounters { - if _, ok := interval.Counters[counter]; !ok { - t.Fatalf("Expected metrics to include a value for couter %s", counter) - } - if value != interval.Counters[counter].Sum { - t.Fatalf("Expected the sum of metric %s to be %f but got %f", counter, value, interval.Counters[counter].Sum) - } - } - - tidyDuration, ok := interval.Samples["secrets.pki.tidy.duration"] - if !ok { - t.Fatal("Expected metrics to include a value for sample secrets.pki.tidy.duration") - } - if tidyDuration.Count <= 0 { - t.Fatalf("Expected metrics to have count > 0 for sample secrets.pki.tidy.duration, but got %d", tidyDuration.Count) - } - } - } - - crl = getParsedCrl(t, client, "pki") - - revokedCerts = crl.TBSCertList.RevokedCertificates - if len(revokedCerts) != 0 { - t.Fatal("expected CRL to be empty") - } -} - -func TestBackend_Root_FullCAChain(t *testing.T) { - t.Parallel() - testCases := []struct { - testName string - keyType string - }{ - {testName: "RSA", keyType: "rsa"}, - {testName: "ED25519", keyType: "ed25519"}, - {testName: "EC", keyType: "ec"}, - } - for _, tc := range testCases { - tc := tc - t.Run(tc.testName, func(t *testing.T) { - runFullCAChainTest(t, tc.keyType) - }) - } -} - -func runFullCAChainTest(t *testing.T, keyType string) { - // Generate a root CA at /pki-root - b_root, s_root := CreateBackendWithStorage(t) - - var err error - - resp, err := CBWrite(b_root, s_root, "root/generate/exported", map[string]interface{}{ - "common_name": "root myvault.com", - "key_type": keyType, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected ca info") - } - rootData := resp.Data - rootCert := rootData["certificate"].(string) - - // Validate that root's /cert/ca-chain now contains the certificate. - resp, err = CBRead(b_root, s_root, "cert/ca_chain") - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected intermediate chain information") - } - - fullChain := resp.Data["ca_chain"].(string) - requireCertInCaChainString(t, fullChain, rootCert, "expected root cert within root cert/ca_chain") - - // Make sure when we issue a leaf certificate we get the full chain back. - _, err = CBWrite(b_root, s_root, "roles/example", map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "max_ttl": "1h", - }) - require.NoError(t, err, "error setting up pki root role: %v", err) - - resp, err = CBWrite(b_root, s_root, "issue/example", map[string]interface{}{ - "common_name": "test.example.com", - "ttl": "5m", - }) - require.NoError(t, err, "error issuing certificate from pki root: %v", err) - fullChainArray := resp.Data["ca_chain"].([]string) - requireCertInCaChainArray(t, fullChainArray, rootCert, "expected root cert within root issuance pki-root/issue/example") - - // Now generate an intermediate at /pki-intermediate, signed by the root. - b_int, s_int := CreateBackendWithStorage(t) - - resp, err = CBWrite(b_int, s_int, "intermediate/generate/exported", map[string]interface{}{ - "common_name": "intermediate myvault.com", - "key_type": keyType, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected intermediate CSR info") - } - intermediateData := resp.Data - intermediateKey := intermediateData["private_key"].(string) - - resp, err = CBWrite(b_root, s_root, "root/sign-intermediate", map[string]interface{}{ - "csr": intermediateData["csr"], - "format": "pem", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected signed intermediate info") - } - intermediateSignedData := resp.Data - intermediateCert := intermediateSignedData["certificate"].(string) - - rootCaCert := parseCert(t, rootCert) - intermediaryCaCert := parseCert(t, intermediateCert) - requireSignedBy(t, intermediaryCaCert, rootCaCert) - intermediateCaChain := intermediateSignedData["ca_chain"].([]string) - - require.Equal(t, parseCert(t, intermediateCaChain[0]), intermediaryCaCert, "intermediate signed cert should have been part of ca_chain") - require.Equal(t, parseCert(t, intermediateCaChain[1]), rootCaCert, "root cert should have been part of ca_chain") - - _, err = CBWrite(b_int, s_int, "intermediate/set-signed", map[string]interface{}{ - "certificate": intermediateCert + "\n" + rootCert + "\n", - }) - if err != nil { - t.Fatal(err) - } - - // Validate that intermediate's ca_chain field now includes the full - // chain. - resp, err = CBRead(b_int, s_int, "cert/ca_chain") - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected intermediate chain information") - } - - // Verify we have a proper CRL now - crl := getParsedCrlFromBackend(t, b_int, s_int, "crl") - require.Equal(t, 0, len(crl.TBSCertList.RevokedCertificates)) - - fullChain = resp.Data["ca_chain"].(string) - requireCertInCaChainString(t, fullChain, intermediateCert, "expected full chain to contain intermediate certificate from pki-intermediate/cert/ca_chain") - requireCertInCaChainString(t, fullChain, rootCert, "expected full chain to contain root certificate from pki-intermediate/cert/ca_chain") - - // Make sure when we issue a leaf certificate we get the full chain back. - _, err = CBWrite(b_int, s_int, "roles/example", map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "max_ttl": "1h", - }) - require.NoError(t, err, "error setting up pki intermediate role: %v", err) - - resp, err = CBWrite(b_int, s_int, "issue/example", map[string]interface{}{ - "common_name": "test.example.com", - "ttl": "5m", - }) - require.NoError(t, err, "error issuing certificate from pki intermediate: %v", err) - fullChainArray = resp.Data["ca_chain"].([]string) - requireCertInCaChainArray(t, fullChainArray, intermediateCert, "expected full chain to contain intermediate certificate from pki-intermediate/issue/example") - requireCertInCaChainArray(t, fullChainArray, rootCert, "expected full chain to contain root certificate from pki-intermediate/issue/example") - - // Finally, import this signing cert chain into a new mount to ensure - // "external" CAs behave as expected. - b_ext, s_ext := CreateBackendWithStorage(t) - - _, err = CBWrite(b_ext, s_ext, "config/ca", map[string]interface{}{ - "pem_bundle": intermediateKey + "\n" + intermediateCert + "\n" + rootCert + "\n", - }) - if err != nil { - t.Fatal(err) - } - - // Validate the external chain information was loaded correctly. - resp, err = CBRead(b_ext, s_ext, "cert/ca_chain") - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected intermediate chain information") - } - - fullChain = resp.Data["ca_chain"].(string) - if strings.Count(fullChain, intermediateCert) != 1 { - t.Fatalf("expected full chain to contain intermediate certificate; got %v occurrences", strings.Count(fullChain, intermediateCert)) - } - if strings.Count(fullChain, rootCert) != 1 { - t.Fatalf("expected full chain to contain root certificate; got %v occurrences", strings.Count(fullChain, rootCert)) - } - - // Now issue a short-lived certificate from our pki-external. - _, err = CBWrite(b_ext, s_ext, "roles/example", map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "max_ttl": "1h", - }) - require.NoError(t, err, "error setting up pki role: %v", err) - - resp, err = CBWrite(b_ext, s_ext, "issue/example", map[string]interface{}{ - "common_name": "test.example.com", - "ttl": "5m", - }) - require.NoError(t, err, "error issuing certificate: %v", err) - require.NotNil(t, resp, "got nil response from issuing request") - issueCrtAsPem := resp.Data["certificate"].(string) - issuedCrt := parseCert(t, issueCrtAsPem) - - // Verify that the certificates are signed by the intermediary CA key... - requireSignedBy(t, issuedCrt, intermediaryCaCert) - - // Test that we can request that the root ca certificate not appear in the ca_chain field - resp, err = CBWrite(b_ext, s_ext, "issue/example", map[string]interface{}{ - "common_name": "test.example.com", - "ttl": "5m", - "remove_roots_from_chain": "true", - }) - requireSuccessNonNilResponse(t, resp, err, "error issuing certificate when removing self signed") - fullChain = strings.Join(resp.Data["ca_chain"].([]string), "\n") - if strings.Count(fullChain, intermediateCert) != 1 { - t.Fatalf("expected full chain to contain intermediate certificate; got %v occurrences", strings.Count(fullChain, intermediateCert)) - } - if strings.Count(fullChain, rootCert) != 0 { - t.Fatalf("expected full chain to NOT contain root certificate; got %v occurrences", strings.Count(fullChain, rootCert)) - } -} - -func requireCertInCaChainArray(t *testing.T, chain []string, cert string, msgAndArgs ...interface{}) { - var fullChain string - for _, caCert := range chain { - fullChain = fullChain + "\n" + caCert - } - - requireCertInCaChainString(t, fullChain, cert, msgAndArgs) -} - -func requireCertInCaChainString(t *testing.T, chain string, cert string, msgAndArgs ...interface{}) { - count := strings.Count(chain, cert) - if count != 1 { - failMsg := fmt.Sprintf("Found %d occurrances of the cert in the provided chain", count) - require.FailNow(t, failMsg, msgAndArgs...) - } -} - -type MultiBool int - -const ( - MFalse MultiBool = iota - MTrue MultiBool = iota - MAny MultiBool = iota -) - -func (o MultiBool) ToValues() []bool { - if o == MTrue { - return []bool{true} - } - - if o == MFalse { - return []bool{false} - } - - if o == MAny { - return []bool{true, false} - } - - return []bool{} -} - -type IssuanceRegression struct { - AllowedDomains []string - AllowBareDomains MultiBool - AllowGlobDomains MultiBool - AllowSubdomains MultiBool - AllowLocalhost MultiBool - AllowWildcardCertificates MultiBool - CNValidations []string - CommonName string - Issued bool -} - -func RoleIssuanceRegressionHelper(t *testing.T, b *backend, s logical.Storage, index int, test IssuanceRegression) int { - tested := 0 - for _, AllowBareDomains := range test.AllowBareDomains.ToValues() { - for _, AllowGlobDomains := range test.AllowGlobDomains.ToValues() { - for _, AllowSubdomains := range test.AllowSubdomains.ToValues() { - for _, AllowLocalhost := range test.AllowLocalhost.ToValues() { - for _, AllowWildcardCertificates := range test.AllowWildcardCertificates.ToValues() { - role := fmt.Sprintf("issuance-regression-%d-bare-%v-glob-%v-subdomains-%v-localhost-%v-wildcard-%v", index, AllowBareDomains, AllowGlobDomains, AllowSubdomains, AllowLocalhost, AllowWildcardCertificates) - _, err := CBWrite(b, s, "roles/"+role, map[string]interface{}{ - "allowed_domains": test.AllowedDomains, - "allow_bare_domains": AllowBareDomains, - "allow_glob_domains": AllowGlobDomains, - "allow_subdomains": AllowSubdomains, - "allow_localhost": AllowLocalhost, - "allow_wildcard_certificates": AllowWildcardCertificates, - "cn_validations": test.CNValidations, - // TODO: test across this vector as well. Currently certain wildcard - // matching is broken with it enabled (such as x*x.foo). - "enforce_hostnames": false, - "key_type": "ec", - "key_bits": 256, - "no_store": true, - // With the CN Validations field, ensure we prevent CN from appearing - // in SANs. - }) - if err != nil { - t.Fatal(err) - } - - resp, err := CBWrite(b, s, "issue/"+role, map[string]interface{}{ - "common_name": test.CommonName, - "exclude_cn_from_sans": true, - }) - - haveErr := err != nil || resp == nil - expectErr := !test.Issued - - if haveErr != expectErr { - t.Fatalf("issuance regression test [%d] failed: haveErr: %v, expectErr: %v, err: %v, resp: %v, test case: %v, role: %v", index, haveErr, expectErr, err, resp, test, role) - } - - tested += 1 - } - } - } - } - } - - return tested -} - -func TestBackend_Roles_IssuanceRegression(t *testing.T) { - t.Parallel() - // Regression testing of role's issuance policy. - testCases := []IssuanceRegression{ - // allowed, bare, glob, subdomains, localhost, wildcards, cn, issued - - // === Globs not allowed but used === // - // Allowed contains globs, but globbing not allowed, resulting in all - // issuances failing. Note that tests against issuing a wildcard with - // a bare domain will be covered later. - /* 0 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "baz.fud.bar.foo", false}, - /* 1 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.fud.bar.foo", false}, - /* 2 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "fud.bar.foo", false}, - /* 3 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.bar.foo", false}, - /* 4 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "bar.foo", false}, - /* 5 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.foo", false}, - /* 6 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "foo", false}, - /* 7 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "baz.fud.bar.foo", false}, - /* 8 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.fud.bar.foo", false}, - /* 9 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "fud.bar.foo", false}, - /* 10 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.bar.foo", false}, - /* 11 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "bar.foo", false}, - /* 12 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "foo", false}, - - // === Localhost sanity === // - // Localhost forbidden, not matching allowed domains -> not issued - /* 13 */ {[]string{"*.*.foo"}, MAny, MAny, MAny, MFalse, MAny, nil, "localhost", false}, - // Localhost allowed, not matching allowed domains -> issued - /* 14 */ {[]string{"*.*.foo"}, MAny, MAny, MAny, MTrue, MAny, nil, "localhost", true}, - // Localhost allowed via allowed domains (and bare allowed), not by AllowLocalhost -> issued - /* 15 */ {[]string{"localhost"}, MTrue, MAny, MAny, MFalse, MAny, nil, "localhost", true}, - // Localhost allowed via allowed domains (and bare not allowed), not by AllowLocalhost -> not issued - /* 16 */ {[]string{"localhost"}, MFalse, MAny, MAny, MFalse, MAny, nil, "localhost", false}, - // Localhost allowed via allowed domains (but bare not allowed), and by AllowLocalhost -> issued - /* 17 */ {[]string{"localhost"}, MFalse, MAny, MAny, MTrue, MAny, nil, "localhost", true}, - - // === Bare wildcard issuance == // - // allowed_domains contains one or more wildcards and bare domains allowed, - // resulting in the cert being issued. - /* 18 */ {[]string{"*.foo"}, MTrue, MAny, MAny, MAny, MTrue, nil, "*.foo", true}, - /* 19 */ {[]string{"*.*.foo"}, MTrue, MAny, MAny, MAny, MAny, nil, "*.*.foo", false}, // Does not conform to RFC 6125 - - // === Double Leading Glob Testing === // - // Allowed contains globs, but glob allowed so certain matches work. - // The value of bare and localhost does not impact these results. - /* 20 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "baz.fud.bar.foo", true}, // glob domains allow infinite subdomains - /* 21 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MTrue, nil, "*.fud.bar.foo", true}, // glob domain allows wildcard of subdomains - /* 22 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "fud.bar.foo", true}, - /* 23 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MTrue, nil, "*.bar.foo", true}, // Regression fix: Vault#13530 - /* 24 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "bar.foo", false}, - /* 25 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "*.foo", false}, - /* 26 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "foo", false}, - - // Allowed contains globs, but glob and subdomain both work, so we expect - // wildcard issuance to work as well. The value of bare and localhost does - // not impact these results. - /* 27 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "baz.fud.bar.foo", true}, - /* 28 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MTrue, nil, "*.fud.bar.foo", true}, - /* 29 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "fud.bar.foo", true}, - /* 30 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MTrue, nil, "*.bar.foo", true}, // Regression fix: Vault#13530 - /* 31 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "bar.foo", false}, - /* 32 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "*.foo", false}, - /* 33 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "foo", false}, - - // === Single Leading Glob Testing === // - // Allowed contains globs, but glob allowed so certain matches work. - // The value of bare and localhost does not impact these results. - /* 34 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "baz.fud.bar.foo", true}, // glob domains allow infinite subdomains - /* 35 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MTrue, nil, "*.fud.bar.foo", true}, // glob domain allows wildcard of subdomains - /* 36 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "fud.bar.foo", true}, // glob domains allow infinite subdomains - /* 37 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MTrue, nil, "*.bar.foo", true}, // glob domain allows wildcards of subdomains - /* 38 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "bar.foo", true}, - /* 39 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "foo", false}, - - // Allowed contains globs, but glob and subdomain both work, so we expect - // wildcard issuance to work as well. The value of bare and localhost does - // not impact these results. - /* 40 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "baz.fud.bar.foo", true}, - /* 41 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MTrue, nil, "*.fud.bar.foo", true}, - /* 42 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "fud.bar.foo", true}, - /* 43 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MTrue, nil, "*.bar.foo", true}, - /* 44 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "bar.foo", true}, - /* 45 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "foo", false}, - - // === Only base domain name === // - // Allowed contains only domain components, but subdomains not allowed. This - // results in most issuances failing unless we allow bare domains, in which - // case only the final issuance for "foo" will succeed. - /* 46 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "baz.fud.bar.foo", false}, - /* 47 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "*.fud.bar.foo", false}, - /* 48 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "fud.bar.foo", false}, - /* 49 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "*.bar.foo", false}, - /* 50 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "bar.foo", false}, - /* 51 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "*.foo", false}, - /* 52 */ {[]string{"foo"}, MFalse, MAny, MFalse, MAny, MAny, nil, "foo", false}, - /* 53 */ {[]string{"foo"}, MTrue, MAny, MFalse, MAny, MAny, nil, "foo", true}, - - // Allowed contains only domain components, and subdomains are now allowed. - // This results in most issuances succeeding, with the exception of the - // base foo, which is still governed by base's value. - /* 54 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MAny, nil, "baz.fud.bar.foo", true}, - /* 55 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "*.fud.bar.foo", true}, - /* 56 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MAny, nil, "fud.bar.foo", true}, - /* 57 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "*.bar.foo", true}, - /* 58 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MAny, nil, "bar.foo", true}, - /* 59 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "*.foo", true}, - /* 60 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "x*x.foo", true}, // internal wildcards should be allowed per RFC 6125/6.4.3 - /* 61 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "*x.foo", true}, // prefix wildcards should be allowed per RFC 6125/6.4.3 - /* 62 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "x*.foo", true}, // suffix wildcards should be allowed per RFC 6125/6.4.3 - /* 63 */ {[]string{"foo"}, MFalse, MAny, MTrue, MAny, MAny, nil, "foo", false}, - /* 64 */ {[]string{"foo"}, MTrue, MAny, MTrue, MAny, MAny, nil, "foo", true}, - - // === Internal Glob Matching === // - // Basic glob matching requirements - /* 65 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xerox.foo", true}, - /* 66 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xylophone.files.pyrex.foo", true}, // globs can match across subdomains - /* 67 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xercex.bar.foo", false}, // x.foo isn't matched - /* 68 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "bar.foo", false}, // x*x isn't matched. - /* 69 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.foo", false}, // unrelated wildcard - /* 70 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.x*x.foo", false}, // Does not conform to RFC 6125 - /* 71 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.xyx.foo", false}, // Globs and Subdomains do not layer per docs. - - // Various requirements around x*x.foo wildcard matching. - /* 72 */ {[]string{"x*x.foo"}, MFalse, MFalse, MAny, MAny, MAny, nil, "x*x.foo", false}, // base disabled, shouldn't match wildcard - /* 73 */ {[]string{"x*x.foo"}, MFalse, MTrue, MAny, MAny, MTrue, nil, "x*x.foo", true}, // base disallowed, but globbing allowed and should match - /* 74 */ {[]string{"x*x.foo"}, MTrue, MAny, MAny, MAny, MTrue, nil, "x*x.foo", true}, // base allowed, should match wildcard - - // Basic glob matching requirements with internal dots. - /* 75 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xerox.foo", false}, // missing dots - /* 76 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "x.ero.x.foo", true}, - /* 77 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xylophone.files.pyrex.foo", false}, // missing dots - /* 78 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "x.ylophone.files.pyre.x.foo", true}, // globs can match across subdomains - /* 79 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xercex.bar.foo", false}, // x.foo isn't matched - /* 80 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "bar.foo", false}, // x.*.x isn't matched. - /* 81 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.foo", false}, // unrelated wildcard - /* 82 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.x.*.x.foo", false}, // Does not conform to RFC 6125 - /* 83 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.x.y.x.foo", false}, // Globs and Subdomains do not layer per docs. - - // === Wildcard restriction testing === // - /* 84 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MFalse, nil, "*.fud.bar.foo", false}, // glob domain allows wildcard of subdomains - /* 85 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MFalse, nil, "*.bar.foo", false}, // glob domain allows wildcards of subdomains - /* 86 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "*.fud.bar.foo", false}, - /* 87 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "*.bar.foo", false}, - /* 88 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "*.foo", false}, - /* 89 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "x*x.foo", false}, - /* 90 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "*x.foo", false}, - /* 91 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "x*.foo", false}, - /* 92 */ {[]string{"x*x.foo"}, MTrue, MAny, MAny, MAny, MFalse, nil, "x*x.foo", false}, - /* 93 */ {[]string{"*.foo"}, MFalse, MFalse, MAny, MAny, MAny, nil, "*.foo", false}, // Bare and globs forbidden despite (potentially) allowing wildcards. - /* 94 */ {[]string{"x.*.x.foo"}, MAny, MAny, MAny, MAny, MAny, nil, "x.*.x.foo", false}, // Does not conform to RFC 6125 - - // === CN validation allowances === // - /* 95 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "*.fud.bar.foo", true}, - /* 96 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "*.fud.*.foo", true}, - /* 97 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "*.bar.*.bar", true}, - /* 98 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "foo@foo", true}, - /* 99 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "foo@foo@foo", true}, - /* 100 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "bar@bar@bar", true}, - /* 101 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar@bar@bar", false}, - /* 102 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar@bar", false}, - /* 103 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar@foo", true}, - /* 104 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"hostname"}, "bar@foo", false}, - /* 105 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"hostname"}, "bar@bar", false}, - /* 106 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"hostname"}, "bar.foo", true}, - /* 107 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"hostname"}, "bar.bar", false}, - /* 108 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar.foo", false}, - /* 109 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar.bar", false}, - } - - if len(testCases) != 110 { - t.Fatalf("misnumbered test case entries will make it hard to find bugs: %v", len(testCases)) - } - - b, s := CreateBackendWithStorage(t) - - // We need a RSA key so all signature sizes are valid with it. - resp, err := CBWrite(b, s, "root/generate/exported", map[string]interface{}{ - "common_name": "myvault.com", - "ttl": "128h", - "key_type": "rsa", - "key_bits": 2048, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected ca info") - } - - tested := 0 - for index, test := range testCases { - tested += RoleIssuanceRegressionHelper(t, b, s, index, test) - } - - t.Logf("Issuance regression expanded matrix test scenarios: %d", tested) -} - -type KeySizeRegression struct { - // Values reused for both Role and CA configuration. - RoleKeyType string - RoleKeyBits []int - - // Signature Bits presently is only specified on the role. - RoleSignatureBits []int - RoleUsePSS bool - - // These are tuples; must be of the same length. - TestKeyTypes []string - TestKeyBits []int - - // All of the above key types/sizes must pass or fail together. - ExpectError bool -} - -func (k KeySizeRegression) KeyTypeValues() []string { - if k.RoleKeyType == "any" { - return []string{"rsa", "ec", "ed25519"} - } - - return []string{k.RoleKeyType} -} - -func RoleKeySizeRegressionHelper(t *testing.T, b *backend, s logical.Storage, index int, test KeySizeRegression) int { - tested := 0 - - for _, caKeyType := range test.KeyTypeValues() { - for _, caKeyBits := range test.RoleKeyBits { - // Generate a new CA key. - resp, err := CBWrite(b, s, "root/generate/exported", map[string]interface{}{ - "common_name": "myvault.com", - "ttl": "128h", - "key_type": caKeyType, - "key_bits": caKeyBits, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected ca info") - } - - for _, roleKeyBits := range test.RoleKeyBits { - for _, roleSignatureBits := range test.RoleSignatureBits { - role := fmt.Sprintf("key-size-regression-%d-keytype-%v-keybits-%d-signature-bits-%d", index, test.RoleKeyType, roleKeyBits, roleSignatureBits) - _, err := CBWrite(b, s, "roles/"+role, map[string]interface{}{ - "key_type": test.RoleKeyType, - "key_bits": roleKeyBits, - "signature_bits": roleSignatureBits, - "use_pss": test.RoleUsePSS, - }) - if err != nil { - t.Fatal(err) - } - - for index, keyType := range test.TestKeyTypes { - keyBits := test.TestKeyBits[index] - - _, _, csrPem := generateCSR(t, &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - }, keyType, keyBits) - - resp, err = CBWrite(b, s, "sign/"+role, map[string]interface{}{ - "common_name": "localhost", - "csr": csrPem, - }) - - haveErr := err != nil || resp == nil - - if haveErr != test.ExpectError { - t.Fatalf("key size regression test [%d] failed: haveErr: %v, expectErr: %v, err: %v, resp: %v, test case: %v, caKeyType: %v, caKeyBits: %v, role: %v, keyType: %v, keyBits: %v", index, haveErr, test.ExpectError, err, resp, test, caKeyType, caKeyBits, role, keyType, keyBits) - } - - if resp != nil && test.RoleUsePSS && caKeyType == "rsa" { - leafCert := parseCert(t, resp.Data["certificate"].(string)) - switch leafCert.SignatureAlgorithm { - case x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS: - default: - t.Fatalf("key size regression test [%d] failed on role %v: unexpected signature algorithm; expected RSA-type CA to sign a leaf cert with PSS algorithm; got %v", index, role, leafCert.SignatureAlgorithm.String()) - } - } - - tested += 1 - } - } - } - - _, err = CBDelete(b, s, "root") - if err != nil { - t.Fatal(err) - } - } - } - - return tested -} - -func TestBackend_Roles_KeySizeRegression(t *testing.T) { - t.Parallel() - // Regression testing of role's issuance policy. - testCases := []KeySizeRegression{ - // RSA with default parameters should fail to issue smaller RSA keys - // and any size ECDSA/Ed25519 keys. - /* 0 */ {"rsa", []int{0, 2048}, []int{0, 256, 384, 512}, false, []string{"rsa", "ec", "ec", "ec", "ec", "ed25519"}, []int{1024, 224, 256, 384, 521, 0}, true}, - // But it should work to issue larger RSA keys. - /* 1 */ {"rsa", []int{0, 2048}, []int{0, 256, 384, 512}, false, []string{"rsa", "rsa"}, []int{2048, 3072}, false}, - - // EC with default parameters should fail to issue smaller EC keys - // and any size RSA/Ed25519 keys. - /* 2 */ {"ec", []int{0}, []int{0}, false, []string{"rsa", "ec", "ed25519"}, []int{2048, 224, 0}, true}, - // But it should work to issue larger EC keys. Note that we should be - // independent of signature bits as that's computed from the issuer - // type (for EC based issuers). - /* 3 */ {"ec", []int{224}, []int{0, 256, 384, 521}, false, []string{"ec", "ec", "ec", "ec"}, []int{224, 256, 384, 521}, false}, - /* 4 */ {"ec", []int{0, 256}, []int{0, 256, 384, 521}, false, []string{"ec", "ec", "ec"}, []int{256, 384, 521}, false}, - /* 5 */ {"ec", []int{384}, []int{0, 256, 384, 521}, false, []string{"ec", "ec"}, []int{384, 521}, false}, - /* 6 */ {"ec", []int{521}, []int{0, 256, 384, 512}, false, []string{"ec"}, []int{521}, false}, - - // Ed25519 should reject RSA and EC keys. - /* 7 */ {"ed25519", []int{0}, []int{0}, false, []string{"rsa", "ec", "ec"}, []int{2048, 256, 521}, true}, - // But it should work to issue Ed25519 keys. - /* 8 */ {"ed25519", []int{0}, []int{0}, false, []string{"ed25519"}, []int{0}, false}, - - // Any key type should reject insecure RSA key sizes. - /* 9 */ {"any", []int{0}, []int{0, 256, 384, 512}, false, []string{"rsa", "rsa"}, []int{512, 1024}, true}, - // But work for everything else. - /* 10 */ {"any", []int{0}, []int{0, 256, 384, 512}, false, []string{"rsa", "rsa", "ec", "ec", "ec", "ec", "ed25519"}, []int{2048, 3072, 224, 256, 384, 521, 0}, false}, - - // RSA with larger than default key size should reject smaller ones. - /* 11 */ {"rsa", []int{3072}, []int{0, 256, 384, 512}, false, []string{"rsa"}, []int{2048}, true}, - - // We should be able to sign with PSS with any CA key type. - /* 12 */ {"rsa", []int{0}, []int{0, 256, 384, 512}, true, []string{"rsa"}, []int{2048}, false}, - /* 13 */ {"ec", []int{0}, []int{0}, true, []string{"ec"}, []int{256}, false}, - /* 14 */ {"ed25519", []int{0}, []int{0}, true, []string{"ed25519"}, []int{0}, false}, - } - - if len(testCases) != 15 { - t.Fatalf("misnumbered test case entries will make it hard to find bugs: %v", len(testCases)) - } - - b, s := CreateBackendWithStorage(t) - - tested := 0 - for index, test := range testCases { - tested += RoleKeySizeRegressionHelper(t, b, s, index, test) - } - - t.Logf("Key size regression expanded matrix test scenarios: %d", tested) -} - -func TestRootWithExistingKey(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - var err error - - // Fail requests if type is existing, and we specify the key_type param - _, err = CBWrite(b, s, "root/generate/existing", map[string]interface{}{ - "common_name": "root myvault.com", - "key_type": "rsa", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "key_type nor key_bits arguments can be set in this mode") - - // Fail requests if type is existing, and we specify the key_bits param - _, err = CBWrite(b, s, "root/generate/existing", map[string]interface{}{ - "common_name": "root myvault.com", - "key_bits": "2048", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "key_type nor key_bits arguments can be set in this mode") - - // Fail if the specified key does not exist. - _, err = CBWrite(b, s, "issuers/generate/root/existing", map[string]interface{}{ - "common_name": "root myvault.com", - "issuer_name": "my-issuer1", - "key_ref": "my-key1", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "unable to find PKI key for reference: my-key1") - - // Fail if the specified key name is default. - _, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ - "common_name": "root myvault.com", - "issuer_name": "my-issuer1", - "key_name": "Default", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "reserved keyword 'default' can not be used as key name") - - // Fail if the specified issuer name is default. - _, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ - "common_name": "root myvault.com", - "issuer_name": "DEFAULT", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "reserved keyword 'default' can not be used as issuer name") - - // Create the first CA - resp, err := CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ - "common_name": "root myvault.com", - "key_type": "rsa", - "issuer_name": "my-issuer1", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuers/generate/root/internal"), logical.UpdateOperation), resp, true) - require.NoError(t, err) - require.NotNil(t, resp.Data["certificate"]) - myIssuerId1 := resp.Data["issuer_id"] - myKeyId1 := resp.Data["key_id"] - require.NotEmpty(t, myIssuerId1) - require.NotEmpty(t, myKeyId1) - - // Fetch the parsed CRL; it should be empty as we've not revoked anything - parsedCrl := getParsedCrlFromBackend(t, b, s, "issuer/my-issuer1/crl/der") - require.Equal(t, len(parsedCrl.TBSCertList.RevokedCertificates), 0, "should have no revoked certificates") - - // Fail if the specified issuer name is re-used. - _, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ - "common_name": "root myvault.com", - "issuer_name": "my-issuer1", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "issuer name already in use") - - // Create the second CA - resp, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ - "common_name": "root myvault.com", - "key_type": "rsa", - "issuer_name": "my-issuer2", - "key_name": "root-key2", - }) - require.NoError(t, err) - require.NotNil(t, resp.Data["certificate"]) - myIssuerId2 := resp.Data["issuer_id"] - myKeyId2 := resp.Data["key_id"] - require.NotEmpty(t, myIssuerId2) - require.NotEmpty(t, myKeyId2) - - // Fetch the parsed CRL; it should be empty as we've not revoked anything - parsedCrl = getParsedCrlFromBackend(t, b, s, "issuer/my-issuer2/crl/der") - require.Equal(t, len(parsedCrl.TBSCertList.RevokedCertificates), 0, "should have no revoked certificates") - - // Fail if the specified key name is re-used. - _, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ - "common_name": "root myvault.com", - "issuer_name": "my-issuer3", - "key_name": "root-key2", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "key name already in use") - - // Create a third CA re-using key from CA 1 - resp, err = CBWrite(b, s, "issuers/generate/root/existing", map[string]interface{}{ - "common_name": "root myvault.com", - "issuer_name": "my-issuer3", - "key_ref": myKeyId1, - }) - require.NoError(t, err) - require.NotNil(t, resp.Data["certificate"]) - myIssuerId3 := resp.Data["issuer_id"] - myKeyId3 := resp.Data["key_id"] - require.NotEmpty(t, myIssuerId3) - require.NotEmpty(t, myKeyId3) - - // Fetch the parsed CRL; it should be empty as we've not revoking anything. - parsedCrl = getParsedCrlFromBackend(t, b, s, "issuer/my-issuer3/crl/der") - require.Equal(t, len(parsedCrl.TBSCertList.RevokedCertificates), 0, "should have no revoked certificates") - // Signatures should be the same since this is just a reissued cert. We - // use signature as a proxy for "these two CRLs are equal". - firstCrl := getParsedCrlFromBackend(t, b, s, "issuer/my-issuer1/crl/der") - require.Equal(t, parsedCrl.SignatureValue, firstCrl.SignatureValue) - - require.NotEqual(t, myIssuerId1, myIssuerId2) - require.NotEqual(t, myIssuerId1, myIssuerId3) - require.NotEqual(t, myKeyId1, myKeyId2) - require.Equal(t, myKeyId1, myKeyId3) - - resp, err = CBList(b, s, "issuers") - require.NoError(t, err) - require.Equal(t, 3, len(resp.Data["keys"].([]string))) - require.Contains(t, resp.Data["keys"], string(myIssuerId1.(issuerID))) - require.Contains(t, resp.Data["keys"], string(myIssuerId2.(issuerID))) - require.Contains(t, resp.Data["keys"], string(myIssuerId3.(issuerID))) -} - -func TestIntermediateWithExistingKey(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - var err error - - // Fail requests if type is existing, and we specify the key_type param - _, err = CBWrite(b, s, "intermediate/generate/existing", map[string]interface{}{ - "common_name": "root myvault.com", - "key_type": "rsa", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "key_type nor key_bits arguments can be set in this mode") - - // Fail requests if type is existing, and we specify the key_bits param - _, err = CBWrite(b, s, "intermediate/generate/existing", map[string]interface{}{ - "common_name": "root myvault.com", - "key_bits": "2048", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "key_type nor key_bits arguments can be set in this mode") - - // Fail if the specified key does not exist. - _, err = CBWrite(b, s, "issuers/generate/intermediate/existing", map[string]interface{}{ - "common_name": "root myvault.com", - "key_ref": "my-key1", - }) - require.Error(t, err) - require.Contains(t, err.Error(), "unable to find PKI key for reference: my-key1") - - // Create the first intermediate CA - resp, err := CBWrite(b, s, "issuers/generate/intermediate/internal", map[string]interface{}{ - "common_name": "root myvault.com", - "key_type": "rsa", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuers/generate/intermediate/internal"), logical.UpdateOperation), resp, true) - require.NoError(t, err) - // csr1 := resp.Data["csr"] - myKeyId1 := resp.Data["key_id"] - require.NotEmpty(t, myKeyId1) - - // Create the second intermediate CA - resp, err = CBWrite(b, s, "issuers/generate/intermediate/internal", map[string]interface{}{ - "common_name": "root myvault.com", - "key_type": "rsa", - "key_name": "interkey1", - }) - require.NoError(t, err) - // csr2 := resp.Data["csr"] - myKeyId2 := resp.Data["key_id"] - require.NotEmpty(t, myKeyId2) - - // Create a third intermediate CA re-using key from intermediate CA 1 - resp, err = CBWrite(b, s, "issuers/generate/intermediate/existing", map[string]interface{}{ - "common_name": "root myvault.com", - "key_ref": myKeyId1, - }) - require.NoError(t, err) - // csr3 := resp.Data["csr"] - myKeyId3 := resp.Data["key_id"] - require.NotEmpty(t, myKeyId3) - - require.NotEqual(t, myKeyId1, myKeyId2) - require.Equal(t, myKeyId1, myKeyId3, "our new ca did not seem to reuse the key as we expected.") -} - -func TestIssuanceTTLs(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root example.com", - "issuer_name": "root", - "ttl": "10s", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - rootCert := parseCert(t, resp.Data["certificate"].(string)) - - _, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - }) - require.NoError(t, err) - - _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing", - "ttl": "1s", - }) - require.NoError(t, err, "expected issuance to succeed due to shorter ttl than cert ttl") - - _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing", - }) - require.Error(t, err, "expected issuance to fail due to longer default ttl than cert ttl") - - resp, err = CBPatch(b, s, "issuer/root", map[string]interface{}{ - "leaf_not_after_behavior": "permit", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.Equal(t, resp.Data["leaf_not_after_behavior"], "permit") - - _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing", - }) - require.NoError(t, err, "expected issuance to succeed due to permitted longer TTL") - - resp, err = CBWrite(b, s, "issuer/root", map[string]interface{}{ - "issuer_name": "root", - "leaf_not_after_behavior": "truncate", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.Equal(t, resp.Data["leaf_not_after_behavior"], "truncate") - - _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing", - }) - require.NoError(t, err, "expected issuance to succeed due to truncated ttl") - - // Sleep until the parent cert expires and the clock rolls over - // to the next second. - time.Sleep(time.Until(rootCert.NotAfter) + (1500 * time.Millisecond)) - - resp, err = CBWrite(b, s, "issuer/root", map[string]interface{}{ - "issuer_name": "root", - "leaf_not_after_behavior": "err", - }) - require.NoError(t, err) - require.NotNil(t, resp) - - // Even 1s ttl should now fail. - _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing", - "ttl": "1s", - }) - require.Error(t, err, "expected issuance to fail due to longer default ttl than cert ttl") -} - -func TestSealWrappedStorageConfigured(t *testing.T) { - t.Parallel() - b, _ := CreateBackendWithStorage(t) - wrappedEntries := b.Backend.PathsSpecial.SealWrapStorage - - // Make sure our legacy bundle is within the list - // NOTE: do not convert these test values to constants, we should always have these paths within seal wrap config - require.Contains(t, wrappedEntries, "config/ca_bundle", "Legacy bundle missing from seal wrap") - // The trailing / is important as it treats the entire folder requiring seal wrapping, not just config/key - require.Contains(t, wrappedEntries, "config/key/", "key prefix with trailing / missing from seal wrap.") -} - -func TestBackend_ConfigCA_WithECParams(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // Generated key with OpenSSL: - // $ openssl ecparam -out p256.key -name prime256v1 -genkey - // - // Regression test for https://github.com/hashicorp/vault/issues/16667 - resp, err := CBWrite(b, s, "config/ca", map[string]interface{}{ - "pem_bundle": ` ------BEGIN EC PARAMETERS----- -BggqhkjOPQMBBw== ------END EC PARAMETERS----- ------BEGIN EC PRIVATE KEY----- -MHcCAQEEINzXthCZdhyV7+wIEBl/ty+ctNsUS99ykTeax6EbYZtvoAoGCCqGSM49 -AwEHoUQDQgAE57NX8bR/nDoW8yRgLswoXBQcjHrdyfuHS0gPwki6BNnfunUzryVb -8f22/JWj6fsEF6AOADZlrswKIbR2Es9e/w== ------END EC PRIVATE KEY----- - `, - }) - require.NoError(t, err) - require.NotNil(t, resp, "expected ca info") - importedKeys := resp.Data["imported_keys"].([]string) - importedIssuers := resp.Data["imported_issuers"].([]string) - - require.Equal(t, len(importedKeys), 1) - require.Equal(t, len(importedIssuers), 0) -} - -func TestPerIssuerAIA(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // Generating a root without anything should not have AIAs. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root example.com", - "issuer_name": "root", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - rootCert := parseCert(t, resp.Data["certificate"].(string)) - require.Empty(t, rootCert.OCSPServer) - require.Empty(t, rootCert.IssuingCertificateURL) - require.Empty(t, rootCert.CRLDistributionPoints) - - // Set some local URLs on the issuer. - resp, err = CBWrite(b, s, "issuer/default", map[string]interface{}{ - "issuing_certificates": []string{"https://google.com"}, - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuer/default"), logical.UpdateOperation), resp, true) - - require.NoError(t, err) - - _, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allow_any_name": true, - "ttl": "85s", - "key_type": "ec", - }) - require.NoError(t, err) - - // Issue something with this re-configured issuer. - resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{ - "common_name": "localhost.com", - }) - require.NoError(t, err) - require.NotNil(t, resp) - leafCert := parseCert(t, resp.Data["certificate"].(string)) - require.Empty(t, leafCert.OCSPServer) - require.Equal(t, leafCert.IssuingCertificateURL, []string{"https://google.com"}) - require.Empty(t, leafCert.CRLDistributionPoints) - - // Set global URLs and ensure they don't appear on this issuer's leaf. - _, err = CBWrite(b, s, "config/urls", map[string]interface{}{ - "issuing_certificates": []string{"https://example.com/ca", "https://backup.example.com/ca"}, - "crl_distribution_points": []string{"https://example.com/crl", "https://backup.example.com/crl"}, - "ocsp_servers": []string{"https://example.com/ocsp", "https://backup.example.com/ocsp"}, - }) - require.NoError(t, err) - resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{ - "common_name": "localhost.com", - }) - require.NoError(t, err) - require.NotNil(t, resp) - leafCert = parseCert(t, resp.Data["certificate"].(string)) - require.Empty(t, leafCert.OCSPServer) - require.Equal(t, leafCert.IssuingCertificateURL, []string{"https://google.com"}) - require.Empty(t, leafCert.CRLDistributionPoints) - - // Now come back and remove the local modifications and ensure we get - // the defaults again. - _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - "issuing_certificates": []string{}, - }) - require.NoError(t, err) - resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{ - "common_name": "localhost.com", - }) - require.NoError(t, err) - require.NotNil(t, resp) - leafCert = parseCert(t, resp.Data["certificate"].(string)) - require.Equal(t, leafCert.IssuingCertificateURL, []string{"https://example.com/ca", "https://backup.example.com/ca"}) - require.Equal(t, leafCert.OCSPServer, []string{"https://example.com/ocsp", "https://backup.example.com/ocsp"}) - require.Equal(t, leafCert.CRLDistributionPoints, []string{"https://example.com/crl", "https://backup.example.com/crl"}) - - // Validate that we can set an issuer name and remove it. - _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - "issuer_name": "my-issuer", - }) - require.NoError(t, err) - _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - "issuer_name": "", - }) - require.NoError(t, err) -} - -func TestIssuersWithoutCRLBits(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // Importing a root without CRL signing bits should work fine. - customBundleWithoutCRLBits := ` ------BEGIN CERTIFICATE----- -MIIDGTCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhyb290 -LW5ldzAeFw0yMjA4MjQxMjEzNTVaFw0yMzA5MDMxMjEzNTVaMBMxETAPBgNVBAMM -CHJvb3QtbmV3MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAojTA/Mx7 -LVW/Zgn/N4BqZbaF82MrTIBFug3ob7mqycNRlWp4/PH8v37+jYn8e691HUsKjden -rDTrO06kiQKiJinAzmlLJvgcazE3aXoh7wSzVG9lFHYvljEmVj+yDbkeaqaCktup -skuNjxCoN9BLmKzZIwVCHn92ZHlhN6LI7CNaU3SDJdu7VftWF9Ugzt9FIvI+6Gcn -/WNE9FWvZ9o7035rZ+1vvTn7/tgxrj2k3XvD51Kq4tsSbqjnSf3QieXT6E6uvtUE -TbPp3xjBElgBCKmeogR1l28rs1aujqqwzZ0B/zOeF8ptaH0aZOIBsVDJR8yTwHzq -s34hNdNfKLHzOwIDAQABo3gwdjAdBgNVHQ4EFgQUF4djNmx+1+uJINhZ82pN+7jz -H8EwHwYDVR0jBBgwFoAUF4djNmx+1+uJINhZ82pN+7jzH8EwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAoQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZI -hvcNAQELBQADggEBAICQovBz4KLWlLmXeZ2Vf6WfQYyGNgGyJa10XNXtWQ5dM2NU -OLAit4x1c2dz+aFocc8ZsX/ikYi/bruT2rsGWqMAGC4at3U4GuaYGO5a6XzMKIDC -nxIlbiO+Pn6Xum7fAqUri7+ZNf/Cygmc5sByi3MAAIkszeObUDZFTJL7gEOuXIMT -rKIXCINq/U+qc7m9AQ8vKhF1Ddj+dLGLzNQ5j3cKfilPs/wRaYqbMQvnmarX+5Cs -k1UL6kWSQsiP3+UWaBlcWkmD6oZ3fIG7c0aMxf7RISq1eTAM9XjH3vMxWQJlS5q3 -2weJ2LYoPe/DwX5CijR0IezapBCrin1BscJMLFQ= ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCiNMD8zHstVb9m -Cf83gGpltoXzYytMgEW6DehvuarJw1GVanj88fy/fv6Nifx7r3UdSwqN16esNOs7 -TqSJAqImKcDOaUsm+BxrMTdpeiHvBLNUb2UUdi+WMSZWP7INuR5qpoKS26myS42P -EKg30EuYrNkjBUIef3ZkeWE3osjsI1pTdIMl27tV+1YX1SDO30Ui8j7oZyf9Y0T0 -Va9n2jvTfmtn7W+9Ofv+2DGuPaTde8PnUqri2xJuqOdJ/dCJ5dPoTq6+1QRNs+nf -GMESWAEIqZ6iBHWXbyuzVq6OqrDNnQH/M54Xym1ofRpk4gGxUMlHzJPAfOqzfiE1 -018osfM7AgMBAAECggEAAVd6kZZaN69IZITIc1vHRYa2rlZpKS2JP7c8Vd3Z/4Fz -ZZvnJ7LgVAmUYg5WPZ2sOqBNLfKVN/oke5Q0dALgdxYl7dWQIhPjHeRFbZFtjqEV -OXZGBniamMO/HSKGWGrqFf7BM/H7AhClUwQgjnzVSz+B+LJJidM+SVys3n1xuDmC -EP+iOda+bAHqHv/7oCELQKhLmCvPc9v2fDy+180ttdo8EHuxwVnKiyR/ryKFhSyx -K1wgAPQ9jO+V+GESL90rqpX/r501REsIOOpm4orueelHTD4+dnHxvUPqJ++9aYGX -79qBNPPUhxrQI1yoHxwW0cTxW5EqkZ9bT2lSd5rjcQKBgQDNyPBpidkHPrYemQDT -RldtS6FiW/jc1It/CRbjU4A6Gi7s3Cda43pEUObKNLeXMyLQaMf4GbDPDX+eh7B8 -RkUq0Q/N0H4bn1hbxYSUdgv0j/6czpMo6rLcJHGwOTSpHGsNsxSLL7xlpgzuzqrG -FzEgjMA1aD3w8B9+/77AoSLoMQKBgQDJyYMw82+euLYRbR5Wc/SbrWfh2n1Mr2BG -pp1ZNYorXE5CL4ScdLcgH1q/b8r5XGwmhMcpeA+geAAaKmk1CGG+gPLoq20c9Q1Y -Ykq9tUVJasIkelvbb/SPxyjkJdBwylzcPP14IJBsqQM0be+yVqLJJVHSaoKhXZcl -IW2xgCpjKwKBgFpeX5U5P+F6nKebMU2WmlYY3GpBUWxIummzKCX0SV86mFjT5UR4 -mPzfOjqaI/V2M1eqbAZ74bVLjDumAs7QXReMb5BGetrOgxLqDmrT3DQt9/YMkXtq -ddlO984XkRSisjB18BOfhvBsl0lX4I7VKHHO3amWeX0RNgOjc7VMDfRBAoGAWAQH -r1BfvZHACLXZ58fISCdJCqCsysgsbGS8eW77B5LJp+DmLQBT6DUE9j+i/0Wq/ton -rRTrbAkrsj4RicpQKDJCwe4UN+9DlOu6wijRQgbJC/Q7IOoieJxcX7eGxcve2UnZ -HY7GsD7AYRwa02UquCYJHIjM1enmxZFhMW1AD+UCgYEAm4jdNz5e4QjA4AkNF+cB -ZenrAZ0q3NbTyiSsJEAtRe/c5fNFpmXo3mqgCannarREQYYDF0+jpSoTUY8XAc4q -wL7EZNzwxITLqBnnHQbdLdAvYxB43kvWTy+JRK8qY9LAMCCFeDoYwXkWV4Wkx/b0 -TgM7RZnmEjNdeaa4M52o7VY= ------END PRIVATE KEY----- - ` - resp, err := CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": customBundleWithoutCRLBits, - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuers/import/bundle"), logical.UpdateOperation), resp, true) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data) - require.NotEmpty(t, resp.Data["imported_issuers"]) - require.NotEmpty(t, resp.Data["imported_keys"]) - require.NotEmpty(t, resp.Data["mapping"]) - - // Shouldn't have crl-signing on the newly imported issuer's usage. - resp, err = CBRead(b, s, "issuer/default") - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data) - require.NotEmpty(t, resp.Data["usage"]) - require.NotContains(t, resp.Data["usage"], "crl-signing") - - // Modifying to set CRL should fail. - resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - "usage": "issuing-certificates,crl-signing", - }) - require.Error(t, err) - require.True(t, resp.IsError()) - - // Modifying to set issuing-certificates and ocsp-signing should succeed. - resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - "usage": "issuing-certificates,ocsp-signing", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data) - require.NotEmpty(t, resp.Data["usage"]) - require.NotContains(t, resp.Data["usage"], "crl-signing") -} - -func TestBackend_IfModifiedSinceHeaders(t *testing.T) { - t.Parallel() - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - RequestResponseCallback: schema.ResponseValidatingCallback(t), - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Mount PKI. - err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - // Required to allow the header to be passed through. - PassthroughRequestHeaders: []string{"if-modified-since"}, - AllowedResponseHeaders: []string{"Last-Modified"}, - }, - }) - require.NoError(t, err) - - // Get a time before CA generation. Subtract two seconds to ensure - // the value in the seconds field is different than the time the CA - // is actually generated at. - beforeOldCAGeneration := time.Now().Add(-2 * time.Second) - - // Generate an internal CA. This one is the default. - resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "Root X1", - "key_type": "ec", - "issuer_name": "old-root", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["certificate"]) - - // CA is generated, but give a grace window. - afterOldCAGeneration := time.Now().Add(2 * time.Second) - - // When you _save_ headers, client returns a copy. But when you go to - // reset them, it doesn't create a new copy (and instead directly - // assigns). This means we have to continually refresh our view of the - // last headers, otherwise the headers added after the last set operation - // leak into this copy... Yuck! - lastHeaders := client.Headers() - for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/old-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta"} { - t.Logf("path: %v", path) - field := "certificate" - if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { - field = "crl" - } - - // Reading the CA should work, without a header. - resp, err := client.Logical().Read(path) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data[field]) - - // Ensure that the CA is returned correctly if we give it the old time. - client.AddHeader("If-Modified-Since", beforeOldCAGeneration.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data[field]) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - - // Ensure that the CA is elided if we give it the present time (plus a - // grace window). - client.AddHeader("If-Modified-Since", afterOldCAGeneration.Format(time.RFC1123)) - t.Logf("headers: %v", client.Headers()) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.Nil(t, resp) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } - - // Wait three seconds. This ensures we have adequate grace period - // to distinguish the two cases, even with grace periods. - time.Sleep(3 * time.Second) - - // Generating a second root. This one isn't the default. - beforeNewCAGeneration := time.Now().Add(-2 * time.Second) - - // Generate an internal CA. This one is the default. - _, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "Root X1", - "key_type": "ec", - "issuer_name": "new-root", - }) - require.NoError(t, err) - - // As above. - afterNewCAGeneration := time.Now().Add(2 * time.Second) - - // New root isn't the default, so it has fewer paths. - for _, path := range []string{"pki/issuer/new-root/json", "pki/issuer/new-root/crl", "pki/issuer/new-root/crl/delta"} { - t.Logf("path: %v", path) - field := "certificate" - if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { - field = "crl" - } - - // Reading the CA should work, without a header. - resp, err := client.Logical().Read(path) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data[field]) - - // Ensure that the CA is returned correctly if we give it the old time. - client.AddHeader("If-Modified-Since", beforeNewCAGeneration.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data[field]) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - - // Ensure that the CA is elided if we give it the present time (plus a - // grace window). - client.AddHeader("If-Modified-Since", afterNewCAGeneration.Format(time.RFC1123)) - t.Logf("headers: %v", client.Headers()) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.Nil(t, resp) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } - - // Wait three seconds. This ensures we have adequate grace period - // to distinguish the two cases, even with grace periods. - time.Sleep(3 * time.Second) - - // Now swap the default issuers around. - _, err = client.Logical().Write("pki/config/issuers", map[string]interface{}{ - "default": "new-root", - }) - require.NoError(t, err) - - // Reading both with the last modified date should return new values. - for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { - t.Logf("path: %v", path) - field := "certificate" - if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { - field = "crl" - } - - // Ensure that the CA is returned correctly if we give it the old time. - client.AddHeader("If-Modified-Since", afterOldCAGeneration.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data[field]) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - - // Ensure that the CA is returned correctly if we give it the old time. - client.AddHeader("If-Modified-Since", afterNewCAGeneration.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data[field]) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } - - // Wait for things to settle, record the present time, and wait for the - // clock to definitely tick over again. - time.Sleep(2 * time.Second) - preRevocationTimestamp := time.Now() - time.Sleep(2 * time.Second) - - // The above tests should say everything is cached. - for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { - t.Logf("path: %v", path) - - // Ensure that the CA is returned correctly if we give it the new time. - client.AddHeader("If-Modified-Since", preRevocationTimestamp.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.Nil(t, resp) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } - - // We could generate some leaves and verify the revocation updates the - // CRL. But, revoking the issuer behaves the same, so let's do that - // instead. - _, err = client.Logical().Write("pki/issuer/old-root/revoke", map[string]interface{}{}) - require.NoError(t, err) - - // CA should still be valid. - for _, path := range []string{"pki/cert/ca", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json"} { - t.Logf("path: %v", path) - - // Ensure that the CA is returned correctly if we give it the old time. - client.AddHeader("If-Modified-Since", preRevocationTimestamp.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.Nil(t, resp) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } - - // CRL should be invalidated - for _, path := range []string{"pki/cert/crl", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { - t.Logf("path: %v", path) - field := "certificate" - if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { - field = "crl" - } - - client.AddHeader("If-Modified-Since", preRevocationTimestamp.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data[field]) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } - - // If we send some time in the future, everything should be cached again! - futureTime := time.Now().Add(30 * time.Second) - for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { - t.Logf("path: %v", path) - - // Ensure that the CA is returned correctly if we give it the new time. - client.AddHeader("If-Modified-Since", futureTime.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.Nil(t, resp) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } - - beforeThreeWaySwap := time.Now().Add(-2 * time.Second) - - // Now, do a three-way swap of names (old->tmp; new->old; tmp->new). This - // should result in all names/CRLs being invalidated. - _, err = client.Logical().JSONMergePatch(ctx, "pki/issuer/old-root", map[string]interface{}{ - "issuer_name": "tmp-root", - }) - require.NoError(t, err) - _, err = client.Logical().JSONMergePatch(ctx, "pki/issuer/new-root", map[string]interface{}{ - "issuer_name": "old-root", - }) - require.NoError(t, err) - _, err = client.Logical().JSONMergePatch(ctx, "pki/issuer/tmp-root", map[string]interface{}{ - "issuer_name": "new-root", - }) - require.NoError(t, err) - - afterThreeWaySwap := time.Now().Add(2 * time.Second) - - for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { - t.Logf("path: %v", path) - field := "certificate" - if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { - field = "crl" - } - - // Ensure that the CA is returned if we give it the pre-update time. - client.AddHeader("If-Modified-Since", beforeThreeWaySwap.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data[field]) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - - // Ensure that the CA is elided correctly if we give it the after time. - client.AddHeader("If-Modified-Since", afterThreeWaySwap.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.Nil(t, resp) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } - - // Finally, rebuild the delta CRL and ensure that only that is - // invalidated. We first need to enable it though, and wait for - // all CRLs to rebuild. - _, err = client.Logical().Write("pki/config/crl", map[string]interface{}{ - "auto_rebuild": true, - "enable_delta": true, - }) - require.NoError(t, err) - time.Sleep(4 * time.Second) - beforeDeltaRotation := time.Now().Add(-2 * time.Second) - - resp, err = client.Logical().Read("pki/crl/rotate-delta") - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.Equal(t, resp.Data["success"], true) - - afterDeltaRotation := time.Now().Add(2 * time.Second) - - for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl"} { - t.Logf("path: %v", path) - - for _, when := range []time.Time{beforeDeltaRotation, afterDeltaRotation} { - client.AddHeader("If-Modified-Since", when.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.Nil(t, resp) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } - } - - for _, path := range []string{"pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { - t.Logf("path: %v", path) - field := "certificate" - if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { - field = "crl" - } - - // Ensure that the CRL is present if we give it the pre-update time. - client.AddHeader("If-Modified-Since", beforeDeltaRotation.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data[field]) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - - client.AddHeader("If-Modified-Since", afterDeltaRotation.Format(time.RFC1123)) - resp, err = client.Logical().Read(path) - require.NoError(t, err) - require.Nil(t, resp) - client.SetHeaders(lastHeaders) - lastHeaders = client.Headers() - } -} - -func TestBackend_InitializeCertificateCounts(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - ctx := context.Background() - - // Set up an Issuer and Role - // We need a root certificate to write/revoke certificates with - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected ca info") - } - - // Create a role - _, err = CBWrite(b, s, "roles/example", map[string]interface{}{ - "allowed_domains": "myvault.com", - "allow_bare_domains": true, - "allow_subdomains": true, - "max_ttl": "2h", - }) - if err != nil { - t.Fatal(err) - } - - // Put certificates A, B, C, D, E in backend - var certificates []string = []string{"a", "b", "c", "d", "e"} - serials := make([]string, 5) - for i, cn := range certificates { - resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{ - "common_name": cn + ".myvault.com", - }) - if err != nil { - t.Fatal(err) - } - serials[i] = resp.Data["serial_number"].(string) - } - - // Turn on certificate counting: - CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ - "maintain_stored_certificate_counts": true, - "publish_stored_certificate_count_metrics": false, - }) - // Assert initialize from clean is correct: - b.initializeStoredCertificateCounts(ctx) - - // Revoke certificates A + B - revocations := serials[0:2] - for _, key := range revocations { - resp, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": key, - }) - if err != nil { - t.Fatal(err) - } - } - - if b.certCount.Load() != 6 { - t.Fatalf("Failed to count six certificates root,A,B,C,D,E, instead counted %d certs", b.certCount.Load()) - } - if b.revokedCertCount.Load() != 2 { - t.Fatalf("Failed to count two revoked certificates A+B, instead counted %d certs", b.revokedCertCount.Load()) - } - - // Simulates listing while initialize in progress, by "restarting it" - b.certCount.Store(0) - b.revokedCertCount.Store(0) - b.certsCounted.Store(false) - - // Revoke certificates C, D - dirtyRevocations := serials[2:4] - for _, key := range dirtyRevocations { - resp, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": key, - }) - if err != nil { - t.Fatal(err) - } - } - - // Put certificates F, G in the backend - dirtyCertificates := []string{"f", "g"} - for _, cn := range dirtyCertificates { - resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{ - "common_name": cn + ".myvault.com", - }) - if err != nil { - t.Fatal(err) - } - } - - // Run initialize - b.initializeStoredCertificateCounts(ctx) - - // Test certificate count - if b.certCount.Load() != 8 { - t.Fatalf("Failed to initialize count of certificates root, A,B,C,D,E,F,G counted %d certs", b.certCount.Load()) - } - - if b.revokedCertCount.Load() != 4 { - t.Fatalf("Failed to count revoked certificates A,B,C,D counted %d certs", b.revokedCertCount.Load()) - } - - return -} - -// Verify that our default values are consistent when creating an issuer and when we do an -// empty POST update to it. This will hopefully identify if we have different default values -// for fields across the two APIs. -func TestBackend_VerifyIssuerUpdateDefaultsMatchCreation(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "myvault.com", - }) - requireSuccessNonNilResponse(t, resp, err, "failed generating root issuer") - - resp, err = CBRead(b, s, "issuer/default") - requireSuccessNonNilResponse(t, resp, err, "failed reading default issuer") - preUpdateValues := resp.Data - - // This field gets reset during issuer update to the empty string - // (meaning Go will auto-detect the rev-sig-algo). - preUpdateValues["revocation_signature_algorithm"] = "" - - resp, err = CBWrite(b, s, "issuer/default", map[string]interface{}{}) - requireSuccessNonNilResponse(t, resp, err, "failed updating default issuer with no values") - - resp, err = CBRead(b, s, "issuer/default") - requireSuccessNonNilResponse(t, resp, err, "failed reading default issuer") - postUpdateValues := resp.Data - - require.Equal(t, preUpdateValues, postUpdateValues, - "A value was updated based on the empty update of an issuer, "+ - "most likely we have a different set of field parameters across create and update of issuers.") -} - -func TestBackend_VerifyPSSKeysIssuersFailImport(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // PKCS8 parsing fails on this key due to rsaPSS OID - rsaOIDKey := ` ------BEGIN PRIVATE KEY----- -MIIEugIBADALBgkqhkiG9w0BAQoEggSmMIIEogIBAAKCAQEAtN0/NPuJHLuyEdBr -tUikXoXOV741XZcNvLAIVBIqDA0ege2gXt9A15FGUI4X3u6kT16Fl6MRdtUZ/qNS -Vs15nK9A1PI/AVekMgTVFTnoCzs550CKN8iRk9Om+lwHimpyXxKkFW69v8fsXwKE -Bsz69jjT7HV9VZQ7fQhmE79brAMuwKP1fUQKdHq5OBKtQ7Cl3Gmipp0izCsVuQIE -kBHvT3UUgyaSp2n+FONpOiyuBoYUH5tVEv9sZzBqSsrYBJYF+GvfnFy9AcTdqRe2 -VX2SjjWjDF84T30OBA798gIFIPwu9R4OjWOlPeh2bo2kGeo3AITjwFZ28m7kS7kc -OtvHpwIDAQABAoIBAFQxmjbj0RQbG+3HBBzD0CBgUYnu9ZC3vKFVoMriGci6YrVB -FSKU8u5mpkDhpKMWnE6GRdItCvgyg4NSLAZUaIRT4O5ARqwtTDYsobTb2/U+gNnx -5WXKbFpQcK6jIK+ClfNEDjYb8yDPxG0GEsfHrBvqoFy25L1t37N4sWwH7HjJyZIe -Hbqx4NVDur9qgqaUwkfSeufn4ycHqFtkzKNzCUarDkST9cxE6/1AKfhl09PPuMEa -lAY2JLiEplQL5sh9cxG5FObJbutJo5EIhR2OdM0VcPf0MTD9LXKRoGR3SNlG7IlS -llJzBjlh4J1ByMX32btKMHzEvlhyrMI90E1SEGECgYEAx1yDQWe4/b1MBqCxA3d0 -20dDmUHSRQFhkd/Mzkl5dPzRkG42W3ryNbMKdeuL0ZgK9AhfaLCjcj1i+44O7dHb -qBTVwfRrer2uoQVCqqJ6z8PGxPJJxTaqh9QuJxkoQ0i43ZNPcjc2M2sWLn+lkkdE -MaGMiyrmjIQEC6tmgCtZ1VUCgYEA6D9xoT9VuAnQjDvW2tO5N2U2H/8ZyRd1pC3z -H1CzjwShhxsP4YOUaVdw59K95JL4SMxSmpRrhthlW3cRaiT/exBcXLEvz0Qu0OhW -a6155ZFjK3UaLDKlwvmtuoAsuAFqX084LO0B1oxvUJESgyPncQ36fv2lZGV7A66z -Uo+BKQsCgYB2yGBMMAjA5nDN4iCV+C7gF+3m+pjWFKSVzcqxfoWndptGeuRYTUDT -TgIFkHqWPwkHrZVrQxOflYPMbi/m8wr1crSKA5+mWi4aMpAuKvERqYxc/B+IKbIh -jAKTuSGMNWAwZP0JCGx65mso+VUleuDe0Wpz4PPM9TuT2GQSKcI0oQKBgHAHcouC -npmo+lU65DgoWzaydrpWdpy+2Tt6AsW/Su4ZIMWoMy/oJaXuzQK2cG0ay/NpxArW -v0uLhNDrDZZzBF3blYIM4nALhr205UMJqjwntnuXACoDwFvdzoShIXEdFa+l6gYZ -yYIxudxWLmTd491wDb5GIgrcvMsY8V1I5dfjAoGAM9g2LtdqgPgK33dCDtZpBm8m -y4ri9PqHxnpps9WJ1dO6MW/YbW+a7vbsmNczdJ6XNLEfy2NWho1dw3xe7ztFVDjF -cWNUzs1+/6aFsi41UX7EFn3zAFhQUPxT59hXspuWuKbRAWc5fMnxbCfI/Cr8wTLJ -E/0kiZ4swUMyI4tYSbM= ------END PRIVATE KEY----- -` - _, err := CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": rsaOIDKey, - }) - require.Error(t, err, "expected error importing PKCS8 rsaPSS OID key") - - _, err = CBWrite(b, s, "keys/import", map[string]interface{}{ - "key": rsaOIDKey, - }) - require.Error(t, err, "expected error importing PKCS8 rsaPSS OID key") - - // Importing a cert with rsaPSS OID should also fail - rsaOIDCert := ` ------BEGIN CERTIFICATE----- -MIIDfjCCAjGgAwIBAgIBATBCBgkqhkiG9w0BAQowNaAPMA0GCWCGSAFlAwQCAQUA -oRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAQUAogQCAgDeMBMxETAPBgNVBAMM -CHJvb3Qtb2xkMB4XDTIyMDkxNjE0MDEwM1oXDTIzMDkyNjE0MDEwM1owEzERMA8G -A1UEAwwIcm9vdC1vbGQwggEgMAsGCSqGSIb3DQEBCgOCAQ8AMIIBCgKCAQEAtN0/ -NPuJHLuyEdBrtUikXoXOV741XZcNvLAIVBIqDA0ege2gXt9A15FGUI4X3u6kT16F -l6MRdtUZ/qNSVs15nK9A1PI/AVekMgTVFTnoCzs550CKN8iRk9Om+lwHimpyXxKk -FW69v8fsXwKEBsz69jjT7HV9VZQ7fQhmE79brAMuwKP1fUQKdHq5OBKtQ7Cl3Gmi -pp0izCsVuQIEkBHvT3UUgyaSp2n+FONpOiyuBoYUH5tVEv9sZzBqSsrYBJYF+Gvf -nFy9AcTdqRe2VX2SjjWjDF84T30OBA798gIFIPwu9R4OjWOlPeh2bo2kGeo3AITj -wFZ28m7kS7kcOtvHpwIDAQABo3UwczAdBgNVHQ4EFgQUVGkTAUJ8inxIVGBlfxf4 -cDhRSnowHwYDVR0jBBgwFoAUVGkTAUJ8inxIVGBlfxf4cDhRSnowDAYDVR0TBAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwEwQgYJKoZI -hvcNAQEKMDWgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgB -ZQMEAgEFAKIEAgIA3gOCAQEAQZ3iQ3NjvS4FYJ5WG41huZI0dkvNFNan+ZYWlYHJ -MIQhbFogb/UQB0rlsuldG0+HF1RDXoYNuThfzt5hiBWYEtMBNurezvnOn4DF0hrl -Uk3sBVnvTalVXg+UVjqh9hBGB75JYJl6a5Oa2Zrq++4qGNwjd0FqgnoXzqS5UGuB -TJL8nlnXPuOIK3VHoXEy7l9GtvEzKcys0xa7g1PYpaJ5D2kpbBJmuQGmU6CDcbP+ -m0hI4QDfVfHtnBp2VMCvhj0yzowtwF4BFIhv4EXZBU10mzxVj0zyKKft9++X8auH -nebuK22ZwzbPe4NhOvAdfNDElkrrtGvTnzkDB7ezPYjelA== ------END CERTIFICATE----- -` - _, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": rsaOIDCert, - }) - require.Error(t, err, "expected error importing PKCS8 rsaPSS OID cert") - - _, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": rsaOIDKey + "\n" + rsaOIDCert, - }) - require.Error(t, err, "expected error importing PKCS8 rsaPSS OID key+cert") - - _, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": rsaOIDCert + "\n" + rsaOIDKey, - }) - require.Error(t, err, "expected error importing PKCS8 rsaPSS OID cert+key") - - // After all these errors, we should have zero issuers and keys. - resp, err := CBList(b, s, "issuers") - require.NoError(t, err) - require.Equal(t, nil, resp.Data["keys"]) - - resp, err = CBList(b, s, "keys") - require.NoError(t, err) - require.Equal(t, nil, resp.Data["keys"]) - - // If we create a new PSS root, we should be able to issue an intermediate - // under it. - resp, err = CBWrite(b, s, "root/generate/exported", map[string]interface{}{ - "use_pss": "true", - "common_name": "root x1 - pss", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["certificate"]) - require.NotEmpty(t, resp.Data["private_key"]) - - resp, err = CBWrite(b, s, "intermediate/generate/exported", map[string]interface{}{ - "use_pss": "true", - "common_name": "int x1 - pss", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["csr"]) - require.NotEmpty(t, resp.Data["private_key"]) - - resp, err = CBWrite(b, s, "issuer/default/sign-intermediate", map[string]interface{}{ - "use_pss": "true", - "common_name": "int x1 - pss", - "csr": resp.Data["csr"].(string), - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["certificate"]) - - resp, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": resp.Data["certificate"].(string), - }) - require.NoError(t, err) - - // Finally, if we were to take an rsaPSS OID'd CSR and use it against this - // mount, it will fail. - _, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allow_any_name": true, - "ttl": "85s", - "key_type": "any", - }) - require.NoError(t, err) - - // Issuing a leaf from a CSR with rsaPSS OID should fail... - rsaOIDCSR := `-----BEGIN CERTIFICATE REQUEST----- -MIICkTCCAUQCAQAwGTEXMBUGA1UEAwwOcmFuY2hlci5teS5vcmcwggEgMAsGCSqG -SIb3DQEBCgOCAQ8AMIIBCgKCAQEAtzHuGEUK55lXI08yp9DXoye9yCZbkJZO+Hej -1TWGEkbX4hzauRJeNp2+wn8xU5y8ITjWSIXEVDHeezosLCSy0Y2QT7/V45zWPUYY -ld0oUnPiwsb9CPFlBRFnX3dO9SS5MONIrNCJGKXmLdF3lgSl8zPT6J/hWM+JBjHO -hBzK6L8IYwmcEujrQfnOnOztzgMEBJtWG8rnI8roz1adpczTddDKGymh2QevjhlL -X9CLeYSSQZInOMsgaDYl98Hn00K5x0CBp8ADzzXtaPSQ9nsnihN8VvZ/wHw6YbBS -BSHa6OD+MrYnw3Sao6/YgBRNT2glIX85uro4ARW9zGB9/748dwIDAQABoAAwQgYJ -KoZIhvcNAQEKMDWgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglg -hkgBZQMEAgEFAKIEAgIA3gOCAQEARGAa0HiwzWCpvAdLOVc4/srEyOYFZPLbtv+Y -ezZIaUBNaWhOvkunqpa48avmcbGlji7r6fxJ5sT28lHt7ODWcJfn1XPAnqesXErm -EBuOIhCv6WiwVyGeTVynuHYkHyw3rIL/zU7N8+zIFV2G2M1UAv5D/eyh/74cr9Of -+nvm9jAbkHix8UwOBCFY2LLNl6bXvbIeJEdDOEtA9UmDXs8QGBg4lngyqcE2Z7rz -+5N/x4guMk2FqblbFGiCc5fLB0Gp6lFFOqhX9Q8nLJ6HteV42xGJUUtsFpppNCRm -82dGIH2PTbXZ0k7iAAwLaPjzOv1v58Wq90o35d4iEsOfJ8v98Q== ------END CERTIFICATE REQUEST-----` - - _, err = CBWrite(b, s, "issuer/default/sign/testing", map[string]interface{}{ - "common_name": "example.com", - "csr": rsaOIDCSR, - }) - require.Error(t, err) - - _, err = CBWrite(b, s, "issuer/default/sign-verbatim", map[string]interface{}{ - "common_name": "example.com", - "use_pss": true, - "csr": rsaOIDCSR, - }) - require.Error(t, err) - - _, err = CBWrite(b, s, "issuer/default/sign-intermediate", map[string]interface{}{ - "common_name": "faulty x1 - pss", - "use_pss": true, - "csr": rsaOIDCSR, - }) - require.Error(t, err) - - // Vault has a weird API for signing self-signed certificates. Ensure - // that doesn't accept rsaPSS OID'd certificates either. - _, err = CBWrite(b, s, "issuer/default/sign-self-issued", map[string]interface{}{ - "use_pss": true, - "certificate": rsaOIDCert, - }) - require.Error(t, err) - - // Issuing a regular leaf should succeed. - _, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allow_any_name": true, - "ttl": "85s", - "key_type": "rsa", - "use_pss": "true", - }) - require.NoError(t, err) - - resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{ - "common_name": "example.com", - "use_pss": "true", - }) - requireSuccessNonNilResponse(t, resp, err, "failed to issue PSS leaf") -} - -func TestPKI_EmptyCRLConfigUpgraded(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // Write an empty CRLConfig into storage. - crlConfigEntry, err := logical.StorageEntryJSON("config/crl", &crlConfig{}) - require.NoError(t, err) - err = s.Put(ctx, crlConfigEntry) - require.NoError(t, err) - - resp, err := CBRead(b, s, "config/crl") - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.Equal(t, resp.Data["expiry"], defaultCrlConfig.Expiry) - require.Equal(t, resp.Data["disable"], defaultCrlConfig.Disable) - require.Equal(t, resp.Data["ocsp_disable"], defaultCrlConfig.OcspDisable) - require.Equal(t, resp.Data["auto_rebuild"], defaultCrlConfig.AutoRebuild) - require.Equal(t, resp.Data["auto_rebuild_grace_period"], defaultCrlConfig.AutoRebuildGracePeriod) - require.Equal(t, resp.Data["enable_delta"], defaultCrlConfig.EnableDelta) - require.Equal(t, resp.Data["delta_rebuild_interval"], defaultCrlConfig.DeltaRebuildInterval) -} - -func TestPKI_ListRevokedCerts(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // Test empty cluster - resp, err := CBList(b, s, "certs/revoked") - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("certs/revoked"), logical.ListOperation), resp, true) - requireSuccessNonNilResponse(t, resp, err, "failed listing empty cluster") - require.Empty(t, resp.Data, "response map contained data that we did not expect") - - // Set up a mount that we can revoke under (We will create 3 leaf certs, 2 of which will be revoked) - resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "test.com", - "key_type": "ec", - }) - requireSuccessNonNilResponse(t, resp, err, "error generating root CA") - requireFieldsSetInResp(t, resp, "serial_number") - issuerSerial := resp.Data["serial_number"] - - resp, err = CBWrite(b, s, "roles/test", map[string]interface{}{ - "allowed_domains": "test.com", - "allow_subdomains": "true", - "max_ttl": "1h", - }) - requireSuccessNonNilResponse(t, resp, err, "error setting up pki role") - - resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "test1.test.com", - }) - requireSuccessNonNilResponse(t, resp, err, "error issuing cert 1") - requireFieldsSetInResp(t, resp, "serial_number") - serial1 := resp.Data["serial_number"] - - resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "test2.test.com", - }) - requireSuccessNonNilResponse(t, resp, err, "error issuing cert 2") - requireFieldsSetInResp(t, resp, "serial_number") - serial2 := resp.Data["serial_number"] - - resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "test3.test.com", - }) - requireSuccessNonNilResponse(t, resp, err, "error issuing cert 2") - requireFieldsSetInResp(t, resp, "serial_number") - serial3 := resp.Data["serial_number"] - - resp, err = CBWrite(b, s, "revoke", map[string]interface{}{"serial_number": serial1}) - requireSuccessNonNilResponse(t, resp, err, "error revoking cert 1") - - resp, err = CBWrite(b, s, "revoke", map[string]interface{}{"serial_number": serial2}) - requireSuccessNonNilResponse(t, resp, err, "error revoking cert 2") - - // Test that we get back the expected revoked serial numbers. - resp, err = CBList(b, s, "certs/revoked") - requireSuccessNonNilResponse(t, resp, err, "failed listing revoked certs") - requireFieldsSetInResp(t, resp, "keys") - revokedKeys := resp.Data["keys"].([]string) - - require.Contains(t, revokedKeys, serial1) - require.Contains(t, revokedKeys, serial2) - require.Equal(t, 2, len(revokedKeys), "Expected 2 revoked entries got %d: %v", len(revokedKeys), revokedKeys) - - // Test that listing our certs returns a different response - resp, err = CBList(b, s, "certs") - requireSuccessNonNilResponse(t, resp, err, "failed listing written certs") - requireFieldsSetInResp(t, resp, "keys") - certKeys := resp.Data["keys"].([]string) - - require.Contains(t, certKeys, serial1) - require.Contains(t, certKeys, serial2) - require.Contains(t, certKeys, serial3) - require.Contains(t, certKeys, issuerSerial) - require.Equal(t, 4, len(certKeys), "Expected 4 cert entries got %d: %v", len(certKeys), certKeys) -} - -func TestPKI_TemplatedAIAs(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // Setting templated AIAs should succeed. - resp, err := CBWrite(b, s, "config/cluster", map[string]interface{}{ - "path": "http://localhost:8200/v1/pki", - "aia_path": "http://localhost:8200/cdn/pki", - }) - require.NoError(t, err) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/cluster"), logical.UpdateOperation), resp, true) - - resp, err = CBRead(b, s, "config/cluster") - require.NoError(t, err) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/cluster"), logical.ReadOperation), resp, true) - - aiaData := map[string]interface{}{ - "crl_distribution_points": "{{cluster_path}}/issuer/{{issuer_id}}/crl/der", - "issuing_certificates": "{{cluster_aia_path}}/issuer/{{issuer_id}}/der", - "ocsp_servers": "{{cluster_path}}/ocsp", - "enable_templating": true, - } - _, err = CBWrite(b, s, "config/urls", aiaData) - require.NoError(t, err) - - // Root generation should succeed, but without AIA info. - rootData := map[string]interface{}{ - "common_name": "Long-Lived Root X1", - "issuer_name": "long-root-x1", - "key_type": "ec", - } - resp, err = CBWrite(b, s, "root/generate/internal", rootData) - require.NoError(t, err) - _, err = CBDelete(b, s, "root") - require.NoError(t, err) - - // Clearing the config and regenerating the root should still succeed. - _, err = CBWrite(b, s, "config/urls", map[string]interface{}{ - "crl_distribution_points": "{{cluster_path}}/issuer/my-root-id/crl/der", - "issuing_certificates": "{{cluster_aia_path}}/issuer/my-root-id/der", - "ocsp_servers": "{{cluster_path}}/ocsp", - "enable_templating": true, - }) - require.NoError(t, err) - resp, err = CBWrite(b, s, "root/generate/internal", rootData) - requireSuccessNonNilResponse(t, resp, err) - issuerId := string(resp.Data["issuer_id"].(issuerID)) - - // Now write the original AIA config and sign a leaf. - _, err = CBWrite(b, s, "config/urls", aiaData) - require.NoError(t, err) - _, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allow_any_name": "true", - "key_type": "ec", - "ttl": "50m", - }) - require.NoError(t, err) - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "example.com", - }) - requireSuccessNonNilResponse(t, resp, err) - - // Validate the AIA info is correctly templated. - cert := parseCert(t, resp.Data["certificate"].(string)) - require.Equal(t, cert.OCSPServer, []string{"http://localhost:8200/v1/pki/ocsp"}) - require.Equal(t, cert.IssuingCertificateURL, []string{"http://localhost:8200/cdn/pki/issuer/" + issuerId + "/der"}) - require.Equal(t, cert.CRLDistributionPoints, []string{"http://localhost:8200/v1/pki/issuer/" + issuerId + "/crl/der"}) - - // Modify our issuer to set custom AIAs: these URLs are bad. - _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - "enable_aia_url_templating": "false", - "crl_distribution_points": "a", - "issuing_certificates": "b", - "ocsp_servers": "c", - }) - require.Error(t, err) - - // These URLs are good. - _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - "enable_aia_url_templating": "false", - "crl_distribution_points": "http://localhost/a", - "issuing_certificates": "http://localhost/b", - "ocsp_servers": "http://localhost/c", - }) - - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "example.com", - }) - requireSuccessNonNilResponse(t, resp, err) - - // Validate the AIA info is correctly templated. - cert = parseCert(t, resp.Data["certificate"].(string)) - require.Equal(t, cert.OCSPServer, []string{"http://localhost/c"}) - require.Equal(t, cert.IssuingCertificateURL, []string{"http://localhost/b"}) - require.Equal(t, cert.CRLDistributionPoints, []string{"http://localhost/a"}) - - // These URLs are bad, but will fail at issuance time due to AIA templating. - resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - "enable_aia_url_templating": "true", - "crl_distribution_points": "a", - "issuing_certificates": "b", - "ocsp_servers": "c", - }) - requireSuccessNonNilResponse(t, resp, err) - require.NotEmpty(t, resp.Warnings) - _, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "example.com", - }) - require.Error(t, err) -} - -func requireSubjectUserIDAttr(t *testing.T, cert string, target string) { - xCert := parseCert(t, cert) - - for _, attr := range xCert.Subject.Names { - var userID string - if attr.Type.Equal(certutil.SubjectPilotUserIDAttributeOID) { - if target == "" { - t.Fatalf("expected no UserID (OID: %v) subject attributes in cert:\n%v", certutil.SubjectPilotUserIDAttributeOID, cert) - } - - switch aValue := attr.Value.(type) { - case string: - userID = aValue - case []byte: - userID = string(aValue) - default: - t.Fatalf("unknown type for UserID attribute: %v\nCert: %v", attr, cert) - } - - if userID == target { - return - } - } - } - - if target != "" { - t.Fatalf("failed to find UserID (OID: %v) matching %v in cert:\n%v", certutil.SubjectPilotUserIDAttributeOID, target, cert) - } -} - -func TestUserIDsInLeafCerts(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // 1. Setup root issuer. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "Vault Root CA", - "key_type": "ec", - "ttl": "7200h", - }) - requireSuccessNonNilResponse(t, resp, err, "failed generating root issuer") - - // 2. Allow no user IDs. - resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allowed_user_ids": "", - "key_type": "ec", - }) - requireSuccessNonNilResponse(t, resp, err, "failed setting up role") - - // - Issue cert without user IDs should work. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") - - // - Issue cert with user ID should fail. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "humanoid", - }) - require.Error(t, err) - require.True(t, resp.IsError()) - - // 3. Allow any user IDs. - resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allowed_user_ids": "*", - "key_type": "ec", - }) - requireSuccessNonNilResponse(t, resp, err, "failed setting up role") - - // - Issue cert without user IDs. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") - - // - Issue cert with one user ID. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "humanoid", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") - - // - Issue cert with two user IDs. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "humanoid,robot", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "robot") - - // 4. Allow one specific user ID. - resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allowed_user_ids": "humanoid", - "key_type": "ec", - }) - requireSuccessNonNilResponse(t, resp, err, "failed setting up role") - - // - Issue cert without user IDs. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") - - // - Issue cert with approved ID. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "humanoid", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") - - // - Issue cert with non-approved user ID should fail. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "robot", - }) - require.Error(t, err) - require.True(t, resp.IsError()) - - // - Issue cert with one approved and one non-approved should also fail. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "humanoid,robot", - }) - require.Error(t, err) - require.True(t, resp.IsError()) - - // 5. Allow two specific user IDs. - resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allowed_user_ids": "humanoid,robot", - "key_type": "ec", - }) - requireSuccessNonNilResponse(t, resp, err, "failed setting up role") - - // - Issue cert without user IDs. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") - - // - Issue cert with one approved ID. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "humanoid", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") - - // - Issue cert with other user ID. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "robot", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "robot") - - // - Issue cert with unknown user ID will fail. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "robot2", - }) - require.Error(t, err) - require.True(t, resp.IsError()) - - // - Issue cert with both should succeed. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "humanoid,robot", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "robot") - - // 6. Use a glob. - resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allowed_user_ids": "human*", - "key_type": "ec", - "use_csr_sans": true, // setup for further testing. - }) - requireSuccessNonNilResponse(t, resp, err, "failed setting up role") - - // - Issue cert without user IDs. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") - - // - Issue cert with approved ID. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "humanoid", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") - - // - Issue cert with another approved ID. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "human", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "human") - - // - Issue cert with literal glob. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "human*", - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "human*") - - // - Still no robotic certs are allowed; will fail. - resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "localhost", - "user_ids": "robot", - }) - require.Error(t, err) - require.True(t, resp.IsError()) - - // Create a CSR and validate it works with both sign/ and sign-verbatim. - csrTemplate := x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: "localhost", - ExtraNames: []pkix.AttributeTypeAndValue{ - { - Type: certutil.SubjectPilotUserIDAttributeOID, - Value: "humanoid", - }, - }, - }, - } - _, _, csrPem := generateCSR(t, &csrTemplate, "ec", 256) - - // Should work with role-based signing. - resp, err = CBWrite(b, s, "sign/testing", map[string]interface{}{ - "csr": csrPem, - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("sign/testing"), logical.UpdateOperation), resp, true) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") - - // - Definitely will work with sign-verbatim. - resp, err = CBWrite(b, s, "sign-verbatim", map[string]interface{}{ - "csr": csrPem, - }) - requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") - requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") -} - -// TestStandby_Operations test proper forwarding for PKI requests from a standby node to the -// active node within a cluster. -func TestStandby_Operations(t *testing.T) { - conf, opts := teststorage.ClusterSetup(&vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - }, nil, teststorage.InmemBackendSetup) - cluster := vault.NewTestCluster(t, conf, opts) - cluster.Start() - defer cluster.Cleanup() - - testhelpers.WaitForActiveNodeAndStandbys(t, cluster) - standbyCores := testhelpers.DeriveStandbyCores(t, cluster) - require.Greater(t, len(standbyCores), 0, "Need at least one standby core.") - client := standbyCores[0].Client - - mountPKIEndpoint(t, client, "pki") - - _, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "root-ca.com", - "ttl": "600h", - }) - require.NoError(t, err, "error setting up pki role: %v", err) - - _, err = client.Logical().Write("pki/roles/example", map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "no_store": "false", // make sure we store this cert - "ttl": "5h", - "key_type": "ec", - }) - require.NoError(t, err, "error setting up pki role: %v", err) - - resp, err := client.Logical().Write("pki/issue/example", map[string]interface{}{ - "common_name": "test.example.com", - }) - require.NoError(t, err, "error issuing certificate: %v", err) - require.NotNil(t, resp, "got nil response from issuing request") - serialOfCert := resp.Data["serial_number"].(string) - - resp, err = client.Logical().Write("pki/revoke", map[string]interface{}{ - "serial_number": serialOfCert, - }) - require.NoError(t, err, "error revoking certificate: %v", err) - require.NotNil(t, resp, "got nil response from revoke request") -} - -type pathAuthCheckerFunc func(t *testing.T, client *api.Client, path string, token string) - -func isPermDenied(err error) bool { - return err != nil && strings.Contains(err.Error(), "permission denied") -} - -func isUnsupportedPathOperation(err error) bool { - return err != nil && (strings.Contains(err.Error(), "unsupported path") || strings.Contains(err.Error(), "unsupported operation")) -} - -func isDeniedOp(err error) bool { - return isPermDenied(err) || isUnsupportedPathOperation(err) -} - -func pathShouldBeAuthed(t *testing.T, client *api.Client, path string, token string) { - client.SetToken("") - resp, err := client.Logical().ReadWithContext(ctx, path) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to read %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().ListWithContext(ctx, path) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to list %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to write %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to delete %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to patch %v while unauthed: %v / %v", path, err, resp) - } -} - -func pathShouldBeUnauthedReadList(t *testing.T, client *api.Client, path string, token string) { - // Should be able to read both with and without a token. - client.SetToken("") - resp, err := client.Logical().ReadWithContext(ctx, path) - if err != nil && isPermDenied(err) { - // Read will sometimes return permission denied, when the handler - // does not support the given operation. Retry with the token. - client.SetToken(token) - resp2, err2 := client.Logical().ReadWithContext(ctx, path) - if err2 != nil && !isUnsupportedPathOperation(err2) { - t.Fatalf("unexpected failure to read %v while unauthed: %v / %v\nWhile authed: %v / %v", path, err, resp, err2, resp2) - } - client.SetToken("") - } - resp, err = client.Logical().ListWithContext(ctx, path) - if err != nil && isPermDenied(err) { - // List will sometimes return permission denied, when the handler - // does not support the given operation. Retry with the token. - client.SetToken(token) - resp2, err2 := client.Logical().ListWithContext(ctx, path) - if err2 != nil && !isUnsupportedPathOperation(err2) { - t.Fatalf("unexpected failure to list %v while unauthed: %v / %v\nWhile authed: %v / %v", path, err, resp, err2, resp2) - } - client.SetToken("") - } - - // These should all be denied. - resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - if !strings.Contains(path, "ocsp") || !strings.Contains(err.Error(), "Code: 40") { - t.Fatalf("unexpected failure during write on read-only path %v while unauthed: %v / %v", path, err, resp) - } - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during delete on read-only path %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during patch on read-only path %v while unauthed: %v / %v", path, err, resp) - } - - // Retrying with token should allow read/list, but not modification still. - client.SetToken(token) - resp, err = client.Logical().ReadWithContext(ctx, path) - if err != nil && isPermDenied(err) { - t.Fatalf("unexpected failure to read %v while authed: %v / %v", path, err, resp) - } - resp, err = client.Logical().ListWithContext(ctx, path) - if err != nil && isPermDenied(err) { - t.Fatalf("unexpected failure to list %v while authed: %v / %v", path, err, resp) - } - - // Should all be denied. - resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - if !strings.Contains(path, "ocsp") || !strings.Contains(err.Error(), "Code: 40") { - t.Fatalf("unexpected failure during write on read-only path %v while authed: %v / %v", path, err, resp) - } - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during delete on read-only path %v while authed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during patch on read-only path %v while authed: %v / %v", path, err, resp) - } -} - -func pathShouldBeUnauthedWriteOnly(t *testing.T, client *api.Client, path string, token string) { - client.SetToken("") - resp, err := client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err != nil && isPermDenied(err) { - t.Fatalf("unexpected failure to write %v while unauthed: %v / %v", path, err, resp) - } - - // These should all be denied. However, on OSS, we might end up with - // a regular 404, which looks like err == resp == nil; hence we only - // fail when there's a non-nil response and/or a non-nil err. - resp, err = client.Logical().ReadWithContext(ctx, path) - if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { - t.Fatalf("unexpected failure during read on write-only path %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().ListWithContext(ctx, path) - if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { - t.Fatalf("unexpected failure during list on write-only path %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { - t.Fatalf("unexpected failure during delete on write-only path %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { - t.Fatalf("unexpected failure during patch on write-only path %v while unauthed: %v / %v", path, err, resp) - } - - // Retrying with token should allow writing, but nothing else. - client.SetToken(token) - resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err != nil && isPermDenied(err) { - t.Fatalf("unexpected failure to write %v while unauthed: %v / %v", path, err, resp) - } - - // These should all be denied. - resp, err = client.Logical().ReadWithContext(ctx, path) - if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { - t.Fatalf("unexpected failure during read on write-only path %v while authed: %v / %v", path, err, resp) - } - resp, err = client.Logical().ListWithContext(ctx, path) - if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { - if resp != nil || err != nil { - t.Fatalf("unexpected failure during list on write-only path %v while authed: %v / %v", path, err, resp) - } - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { - t.Fatalf("unexpected failure during delete on write-only path %v while authed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { - t.Fatalf("unexpected failure during patch on write-only path %v while authed: %v / %v", path, err, resp) - } -} - -type pathAuthChecker int - -const ( - shouldBeAuthed pathAuthChecker = iota - shouldBeUnauthedReadList - shouldBeUnauthedWriteOnly -) - -var pathAuthChckerMap = map[pathAuthChecker]pathAuthCheckerFunc{ - shouldBeAuthed: pathShouldBeAuthed, - shouldBeUnauthedReadList: pathShouldBeUnauthedReadList, - shouldBeUnauthedWriteOnly: pathShouldBeUnauthedWriteOnly, -} - -func TestProperAuthing(t *testing.T) { - t.Parallel() - ctx := context.Background() - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - token := client.Token() - - // Mount PKI. - err := client.Sys().MountWithContext(ctx, "pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Setup basic configuration. - _, err = client.Logical().WriteWithContext(ctx, "pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().WriteWithContext(ctx, "pki/roles/test", map[string]interface{}{ - "allow_localhost": true, - }) - if err != nil { - t.Fatal(err) - } - - resp, err := client.Logical().WriteWithContext(ctx, "pki/issue/test", map[string]interface{}{ - "common_name": "localhost", - }) - if err != nil || resp == nil { - t.Fatal(err) - } - serial := resp.Data["serial_number"].(string) - eabKid := "13b80844-e60d-42d2-b7e9-152a8e834b90" - paths := map[string]pathAuthChecker{ - "ca_chain": shouldBeUnauthedReadList, - "cert/ca_chain": shouldBeUnauthedReadList, - "ca": shouldBeUnauthedReadList, - "ca/pem": shouldBeUnauthedReadList, - "cert/" + serial: shouldBeUnauthedReadList, - "cert/" + serial + "/raw": shouldBeUnauthedReadList, - "cert/" + serial + "/raw/pem": shouldBeUnauthedReadList, - "cert/crl": shouldBeUnauthedReadList, - "cert/crl/raw": shouldBeUnauthedReadList, - "cert/crl/raw/pem": shouldBeUnauthedReadList, - "cert/delta-crl": shouldBeUnauthedReadList, - "cert/delta-crl/raw": shouldBeUnauthedReadList, - "cert/delta-crl/raw/pem": shouldBeUnauthedReadList, - "cert/unified-crl": shouldBeUnauthedReadList, - "cert/unified-crl/raw": shouldBeUnauthedReadList, - "cert/unified-crl/raw/pem": shouldBeUnauthedReadList, - "cert/unified-delta-crl": shouldBeUnauthedReadList, - "cert/unified-delta-crl/raw": shouldBeUnauthedReadList, - "cert/unified-delta-crl/raw/pem": shouldBeUnauthedReadList, - "certs": shouldBeAuthed, - "certs/revoked": shouldBeAuthed, - "certs/revocation-queue": shouldBeAuthed, - "certs/revocation-queue/": shouldBeAuthed, - "certs/unified-revoked": shouldBeAuthed, - "certs/unified-revoked/": shouldBeAuthed, - "config/acme": shouldBeAuthed, - "config/auto-tidy": shouldBeAuthed, - "config/ca": shouldBeAuthed, - "config/cluster": shouldBeAuthed, - "config/crl": shouldBeAuthed, - "config/issuers": shouldBeAuthed, - "config/keys": shouldBeAuthed, - "config/urls": shouldBeAuthed, - "crl": shouldBeUnauthedReadList, - "crl/pem": shouldBeUnauthedReadList, - "crl/delta": shouldBeUnauthedReadList, - "crl/delta/pem": shouldBeUnauthedReadList, - "crl/rotate": shouldBeAuthed, - "crl/rotate-delta": shouldBeAuthed, - "intermediate/cross-sign": shouldBeAuthed, - "intermediate/generate/exported": shouldBeAuthed, - "intermediate/generate/internal": shouldBeAuthed, - "intermediate/generate/existing": shouldBeAuthed, - "intermediate/generate/kms": shouldBeAuthed, - "intermediate/set-signed": shouldBeAuthed, - "issue/test": shouldBeAuthed, - "issuer/default": shouldBeAuthed, - "issuer/default/der": shouldBeUnauthedReadList, - "issuer/default/json": shouldBeUnauthedReadList, - "issuer/default/pem": shouldBeUnauthedReadList, - "issuer/default/crl": shouldBeUnauthedReadList, - "issuer/default/crl/pem": shouldBeUnauthedReadList, - "issuer/default/crl/der": shouldBeUnauthedReadList, - "issuer/default/crl/delta": shouldBeUnauthedReadList, - "issuer/default/crl/delta/der": shouldBeUnauthedReadList, - "issuer/default/crl/delta/pem": shouldBeUnauthedReadList, - "issuer/default/unified-crl": shouldBeUnauthedReadList, - "issuer/default/unified-crl/pem": shouldBeUnauthedReadList, - "issuer/default/unified-crl/der": shouldBeUnauthedReadList, - "issuer/default/unified-crl/delta": shouldBeUnauthedReadList, - "issuer/default/unified-crl/delta/der": shouldBeUnauthedReadList, - "issuer/default/unified-crl/delta/pem": shouldBeUnauthedReadList, - "issuer/default/issue/test": shouldBeAuthed, - "issuer/default/resign-crls": shouldBeAuthed, - "issuer/default/revoke": shouldBeAuthed, - "issuer/default/sign-intermediate": shouldBeAuthed, - "issuer/default/sign-revocation-list": shouldBeAuthed, - "issuer/default/sign-self-issued": shouldBeAuthed, - "issuer/default/sign-verbatim": shouldBeAuthed, - "issuer/default/sign-verbatim/test": shouldBeAuthed, - "issuer/default/sign/test": shouldBeAuthed, - "issuers": shouldBeUnauthedReadList, - "issuers/generate/intermediate/exported": shouldBeAuthed, - "issuers/generate/intermediate/internal": shouldBeAuthed, - "issuers/generate/intermediate/existing": shouldBeAuthed, - "issuers/generate/intermediate/kms": shouldBeAuthed, - "issuers/generate/root/exported": shouldBeAuthed, - "issuers/generate/root/internal": shouldBeAuthed, - "issuers/generate/root/existing": shouldBeAuthed, - "issuers/generate/root/kms": shouldBeAuthed, - "issuers/import/cert": shouldBeAuthed, - "issuers/import/bundle": shouldBeAuthed, - "key/default": shouldBeAuthed, - "keys": shouldBeAuthed, - "keys/generate/internal": shouldBeAuthed, - "keys/generate/exported": shouldBeAuthed, - "keys/generate/kms": shouldBeAuthed, - "keys/import": shouldBeAuthed, - "ocsp": shouldBeUnauthedWriteOnly, - "ocsp/dGVzdAo=": shouldBeUnauthedReadList, - "revoke": shouldBeAuthed, - "revoke-with-key": shouldBeAuthed, - "roles/test": shouldBeAuthed, - "roles": shouldBeAuthed, - "root": shouldBeAuthed, - "root/generate/exported": shouldBeAuthed, - "root/generate/internal": shouldBeAuthed, - "root/generate/existing": shouldBeAuthed, - "root/generate/kms": shouldBeAuthed, - "root/replace": shouldBeAuthed, - "root/rotate/internal": shouldBeAuthed, - "root/rotate/exported": shouldBeAuthed, - "root/rotate/existing": shouldBeAuthed, - "root/rotate/kms": shouldBeAuthed, - "root/sign-intermediate": shouldBeAuthed, - "root/sign-self-issued": shouldBeAuthed, - "sign-verbatim": shouldBeAuthed, - "sign-verbatim/test": shouldBeAuthed, - "sign/test": shouldBeAuthed, - "tidy": shouldBeAuthed, - "tidy-cancel": shouldBeAuthed, - "tidy-status": shouldBeAuthed, - "unified-crl": shouldBeUnauthedReadList, - "unified-crl/pem": shouldBeUnauthedReadList, - "unified-crl/delta": shouldBeUnauthedReadList, - "unified-crl/delta/pem": shouldBeUnauthedReadList, - "unified-ocsp": shouldBeUnauthedWriteOnly, - "unified-ocsp/dGVzdAo=": shouldBeUnauthedReadList, - "eab": shouldBeAuthed, - "eab/" + eabKid: shouldBeAuthed, - } - - // Add ACME based paths to the test suite - for _, acmePrefix := range []string{"", "issuer/default/", "roles/test/", "issuer/default/roles/test/"} { - paths[acmePrefix+"acme/directory"] = shouldBeUnauthedReadList - paths[acmePrefix+"acme/new-nonce"] = shouldBeUnauthedReadList - paths[acmePrefix+"acme/new-account"] = shouldBeUnauthedWriteOnly - paths[acmePrefix+"acme/revoke-cert"] = shouldBeUnauthedWriteOnly - paths[acmePrefix+"acme/new-order"] = shouldBeUnauthedWriteOnly - paths[acmePrefix+"acme/orders"] = shouldBeUnauthedWriteOnly - paths[acmePrefix+"acme/account/hrKmDYTvicHoHGVN2-3uzZV_BPGdE0W_dNaqYTtYqeo="] = shouldBeUnauthedWriteOnly - paths[acmePrefix+"acme/authorization/29da8c38-7a09-465e-b9a6-3d76802b1afd"] = shouldBeUnauthedWriteOnly - paths[acmePrefix+"acme/challenge/29da8c38-7a09-465e-b9a6-3d76802b1afd/http-01"] = shouldBeUnauthedWriteOnly - paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90"] = shouldBeUnauthedWriteOnly - paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90/finalize"] = shouldBeUnauthedWriteOnly - paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90/cert"] = shouldBeUnauthedWriteOnly - - // Make sure this new-eab path is auth'd - paths[acmePrefix+"acme/new-eab"] = shouldBeAuthed - } - - for path, checkerType := range paths { - checker := pathAuthChckerMap[checkerType] - checker(t, client, "pki/"+path, token) - } - - client.SetToken(token) - openAPIResp, err := client.Logical().ReadWithContext(ctx, "sys/internal/specs/openapi") - if err != nil { - t.Fatalf("failed to get openapi data: %v", err) - } - - validatedPath := false - for openapi_path, raw_data := range openAPIResp.Data["paths"].(map[string]interface{}) { - if !strings.HasPrefix(openapi_path, "/pki/") { - t.Logf("Skipping path: %v", openapi_path) - continue - } - - t.Logf("Validating path: %v", openapi_path) - validatedPath = true - // Substitute values in from our testing map. - raw_path := openapi_path[5:] - if strings.Contains(raw_path, "roles/") && strings.Contains(raw_path, "{name}") { - raw_path = strings.ReplaceAll(raw_path, "{name}", "test") - } - if strings.Contains(raw_path, "{role}") { - raw_path = strings.ReplaceAll(raw_path, "{role}", "test") - } - if strings.Contains(raw_path, "ocsp/") && strings.Contains(raw_path, "{req}") { - raw_path = strings.ReplaceAll(raw_path, "{req}", "dGVzdAo=") - } - if strings.Contains(raw_path, "{issuer_ref}") { - raw_path = strings.ReplaceAll(raw_path, "{issuer_ref}", "default") - } - if strings.Contains(raw_path, "{key_ref}") { - raw_path = strings.ReplaceAll(raw_path, "{key_ref}", "default") - } - if strings.Contains(raw_path, "{exported}") { - raw_path = strings.ReplaceAll(raw_path, "{exported}", "internal") - } - if strings.Contains(raw_path, "{serial}") { - raw_path = strings.ReplaceAll(raw_path, "{serial}", serial) - } - if strings.Contains(raw_path, "acme/account/") && strings.Contains(raw_path, "{kid}") { - raw_path = strings.ReplaceAll(raw_path, "{kid}", "hrKmDYTvicHoHGVN2-3uzZV_BPGdE0W_dNaqYTtYqeo=") - } - if strings.Contains(raw_path, "acme/") && strings.Contains(raw_path, "{auth_id}") { - raw_path = strings.ReplaceAll(raw_path, "{auth_id}", "29da8c38-7a09-465e-b9a6-3d76802b1afd") - } - if strings.Contains(raw_path, "acme/") && strings.Contains(raw_path, "{challenge_type}") { - raw_path = strings.ReplaceAll(raw_path, "{challenge_type}", "http-01") - } - if strings.Contains(raw_path, "acme/") && strings.Contains(raw_path, "{order_id}") { - raw_path = strings.ReplaceAll(raw_path, "{order_id}", "13b80844-e60d-42d2-b7e9-152a8e834b90") - } - if strings.Contains(raw_path, "eab") && strings.Contains(raw_path, "{key_id}") { - raw_path = strings.ReplaceAll(raw_path, "{key_id}", eabKid) - } - - handler, present := paths[raw_path] - if !present { - t.Fatalf("OpenAPI reports PKI mount contains %v->%v but was not tested to be authed or authed.", openapi_path, raw_path) - } - - openapi_data := raw_data.(map[string]interface{}) - hasList := false - rawGetData, hasGet := openapi_data["get"] - if hasGet { - getData := rawGetData.(map[string]interface{}) - getParams, paramsPresent := getData["parameters"].(map[string]interface{}) - if getParams != nil && paramsPresent { - if _, hasList = getParams["list"]; hasList { - // LIST is exclusive from GET on the same endpoint usually. - hasGet = false - } - } - } - _, hasPost := openapi_data["post"] - _, hasDelete := openapi_data["delete"] - - if handler == shouldBeUnauthedReadList { - if hasPost || hasDelete { - t.Fatalf("Unauthed read-only endpoints should not have POST/DELETE capabilities: %v->%v", openapi_path, raw_path) - } - } else if handler == shouldBeUnauthedWriteOnly { - if hasGet || hasList { - t.Fatalf("Unauthed write-only endpoints should not have GET/LIST capabilities: %v->%v", openapi_path, raw_path) - } - } - } - - if !validatedPath { - t.Fatalf("Expected to have validated at least one path.") - } -} - -func TestPatchIssuer(t *testing.T) { - t.Parallel() - - type TestCase struct { - Field string - Before interface{} - Patched interface{} - } - testCases := []TestCase{ - { - Field: "issuer_name", - Before: "root", - Patched: "root-new", - }, - { - Field: "leaf_not_after_behavior", - Before: "err", - Patched: "permit", - }, - { - Field: "usage", - Before: "crl-signing,issuing-certificates,ocsp-signing,read-only", - Patched: "issuing-certificates,read-only", - }, - { - Field: "revocation_signature_algorithm", - Before: "ECDSAWithSHA256", - Patched: "ECDSAWithSHA384", - }, - { - Field: "issuing_certificates", - Before: []string{"http://localhost/v1/pki-1/ca"}, - Patched: []string{"http://localhost/v1/pki/ca"}, - }, - { - Field: "crl_distribution_points", - Before: []string{"http://localhost/v1/pki-1/crl"}, - Patched: []string{"http://localhost/v1/pki/crl"}, - }, - { - Field: "ocsp_servers", - Before: []string{"http://localhost/v1/pki-1/ocsp"}, - Patched: []string{"http://localhost/v1/pki/ocsp"}, - }, - { - Field: "enable_aia_url_templating", - Before: false, - Patched: true, - }, - { - Field: "manual_chain", - Before: []string(nil), - Patched: []string{"self"}, - }, - } - - for index, testCase := range testCases { - t.Logf("index: %v / tc: %v", index, testCase) - - b, s := CreateBackendWithStorage(t) - - // 1. Setup root issuer. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "Vault Root CA", - "key_type": "ec", - "ttl": "7200h", - "issuer_name": "root", - }) - requireSuccessNonNilResponse(t, resp, err, "failed generating root issuer") - id := string(resp.Data["issuer_id"].(issuerID)) - - // 2. Enable Cluster paths - resp, err = CBWrite(b, s, "config/urls", map[string]interface{}{ - "path": "https://localhost/v1/pki", - "aia_path": "http://localhost/v1/pki", - }) - requireSuccessNonNilResponse(t, resp, err, "failed updating AIA config") - - // 3. Add AIA information - resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - "issuing_certificates": "http://localhost/v1/pki-1/ca", - "crl_distribution_points": "http://localhost/v1/pki-1/crl", - "ocsp_servers": "http://localhost/v1/pki-1/ocsp", - }) - requireSuccessNonNilResponse(t, resp, err, "failed setting up issuer") - - // 4. Read the issuer before. - resp, err = CBRead(b, s, "issuer/default") - requireSuccessNonNilResponse(t, resp, err, "failed reading root issuer before") - require.Equal(t, testCase.Before, resp.Data[testCase.Field], "bad expectations") - - // 5. Perform modification. - resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ - testCase.Field: testCase.Patched, - }) - requireSuccessNonNilResponse(t, resp, err, "failed patching root issuer") - - if testCase.Field != "manual_chain" { - require.Equal(t, testCase.Patched, resp.Data[testCase.Field], "failed persisting value") - } else { - // self->id - require.Equal(t, []string{id}, resp.Data[testCase.Field], "failed persisting value") - } - - // 6. Ensure it stuck - resp, err = CBRead(b, s, "issuer/default") - requireSuccessNonNilResponse(t, resp, err, "failed reading root issuer after") - - if testCase.Field != "manual_chain" { - require.Equal(t, testCase.Patched, resp.Data[testCase.Field]) - } else { - // self->id - require.Equal(t, []string{id}, resp.Data[testCase.Field], "failed persisting value") - } - } -} - -func TestGenerateRootCAWithAIA(t *testing.T) { - // Generate a root CA at /pki-root - b_root, s_root := CreateBackendWithStorage(t) - - // Setup templated AIA information - _, err := CBWrite(b_root, s_root, "config/cluster", map[string]interface{}{ - "path": "https://localhost:8200", - "aia_path": "https://localhost:8200", - }) - require.NoError(t, err, "failed to write AIA settings") - - _, err = CBWrite(b_root, s_root, "config/urls", map[string]interface{}{ - "crl_distribution_points": "{{cluster_path}}/issuer/{{issuer_id}}/crl/der", - "issuing_certificates": "{{cluster_aia_path}}/issuer/{{issuer_id}}/der", - "ocsp_servers": "{{cluster_path}}/ocsp", - "enable_templating": true, - }) - require.NoError(t, err, "failed to write AIA settings") - - // Write a root issuer, this should succeed. - resp, err := CBWrite(b_root, s_root, "root/generate/exported", map[string]interface{}{ - "common_name": "root myvault.com", - "key_type": "ec", - }) - requireSuccessNonNilResponse(t, resp, err, "expected root generation to succeed") -} - -var ( - initTest sync.Once - rsaCAKey string - rsaCACert string - ecCAKey string - ecCACert string - edCAKey string - edCACert string -) diff --git a/builtin/logical/pki/ca_test.go b/builtin/logical/pki/ca_test.go deleted file mode 100644 index 4517604f8..000000000 --- a/builtin/logical/pki/ca_test.go +++ /dev/null @@ -1,712 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/json" - "encoding/pem" - "math/big" - mathrand "math/rand" - "strings" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/api" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestBackend_CA_Steps(t *testing.T) { - t.Parallel() - var b *backend - - factory := func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { - be, err := Factory(ctx, conf) - if err == nil { - b = be.(*backend) - } - return be, err - } - - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - client := cluster.Cores[0].Client - - // Set RSA/EC CA certificates - var rsaCAKey, rsaCACert, ecCAKey, ecCACert, edCAKey, edCACert string - { - cak, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(err) - } - marshaledKey, err := x509.MarshalECPrivateKey(cak) - if err != nil { - panic(err) - } - keyPEMBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: marshaledKey, - } - ecCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - if err != nil { - panic(err) - } - subjKeyID, err := certutil.GetSubjKeyID(cak) - if err != nil { - panic(err) - } - caCertTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "root.localhost", - }, - SubjectKeyId: subjKeyID, - DNSNames: []string{"root.localhost"}, - KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), - SerialNumber: big.NewInt(mathrand.Int63()), - NotAfter: time.Now().Add(262980 * time.Hour), - BasicConstraintsValid: true, - IsCA: true, - } - caBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, cak.Public(), cak) - if err != nil { - panic(err) - } - caCertPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - } - ecCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) - - rak, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - panic(err) - } - marshaledKey = x509.MarshalPKCS1PrivateKey(rak) - keyPEMBlock = &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: marshaledKey, - } - rsaCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - if err != nil { - panic(err) - } - _, err = certutil.GetSubjKeyID(rak) - if err != nil { - panic(err) - } - caBytes, err = x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, rak.Public(), rak) - if err != nil { - panic(err) - } - caCertPEMBlock = &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - } - rsaCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) - - _, edk, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - panic(err) - } - marshaledKey, err = x509.MarshalPKCS8PrivateKey(edk) - if err != nil { - panic(err) - } - keyPEMBlock = &pem.Block{ - Type: "PRIVATE KEY", - Bytes: marshaledKey, - } - edCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - if err != nil { - panic(err) - } - _, err = certutil.GetSubjKeyID(edk) - if err != nil { - panic(err) - } - caBytes, err = x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, edk.Public(), edk) - if err != nil { - panic(err) - } - caCertPEMBlock = &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - } - edCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) - } - - // Setup backends - var rsaRoot, rsaInt, ecRoot, ecInt, edRoot, edInt *backend - { - if err := client.Sys().Mount("rsaroot", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }); err != nil { - t.Fatal(err) - } - rsaRoot = b - - if err := client.Sys().Mount("rsaint", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }); err != nil { - t.Fatal(err) - } - rsaInt = b - - if err := client.Sys().Mount("ecroot", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }); err != nil { - t.Fatal(err) - } - ecRoot = b - - if err := client.Sys().Mount("ecint", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }); err != nil { - t.Fatal(err) - } - ecInt = b - - if err := client.Sys().Mount("ed25519root", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }); err != nil { - t.Fatal(err) - } - edRoot = b - - if err := client.Sys().Mount("ed25519int", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }); err != nil { - t.Fatal(err) - } - edInt = b - } - - t.Run("teststeps", func(t *testing.T) { - t.Run("rsa", func(t *testing.T) { - t.Parallel() - subClient, err := client.Clone() - if err != nil { - t.Fatal(err) - } - subClient.SetToken(client.Token()) - runSteps(t, rsaRoot, rsaInt, subClient, "rsaroot/", "rsaint/", rsaCACert, rsaCAKey) - }) - t.Run("ec", func(t *testing.T) { - t.Parallel() - subClient, err := client.Clone() - if err != nil { - t.Fatal(err) - } - subClient.SetToken(client.Token()) - runSteps(t, ecRoot, ecInt, subClient, "ecroot/", "ecint/", ecCACert, ecCAKey) - }) - t.Run("ed25519", func(t *testing.T) { - t.Parallel() - subClient, err := client.Clone() - if err != nil { - t.Fatal(err) - } - subClient.SetToken(client.Token()) - runSteps(t, edRoot, edInt, subClient, "ed25519root/", "ed25519int/", edCACert, edCAKey) - }) - }) -} - -func runSteps(t *testing.T, rootB, intB *backend, client *api.Client, rootName, intName, caCert, caKey string) { - // Load CA cert/key in and ensure we can fetch it back in various formats, - // unauthenticated - { - // Attempt import but only provide one the cert; this should work. - { - _, err := client.Logical().Write(rootName+"config/ca", map[string]interface{}{ - "pem_bundle": caCert, - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - } - - // Same but with only the key - { - _, err := client.Logical().Write(rootName+"config/ca", map[string]interface{}{ - "pem_bundle": caKey, - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - } - - // Import entire CA bundle; this should work as well - { - _, err := client.Logical().Write(rootName+"config/ca", map[string]interface{}{ - "pem_bundle": strings.Join([]string{caKey, caCert}, "\n"), - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - } - - prevToken := client.Token() - client.SetToken("") - - // cert/ca and issuer/default/json path - for _, path := range []string{"cert/ca", "issuer/default/json"} { - resp, err := client.Logical().Read(rootName + path) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - expected := caCert - if path == "issuer/default/json" { - // Preserves the new line. - expected += "\n" - _, present := resp.Data["issuer_id"] - if !present { - t.Fatalf("expected issuer/default/json to include issuer_id") - } - _, present = resp.Data["issuer_name"] - if !present { - t.Fatalf("expected issuer/default/json to include issuer_name") - } - } - if diff := deep.Equal(resp.Data["certificate"].(string), expected); diff != nil { - t.Fatal(diff) - } - } - - // ca/pem and issuer/default/pem path (raw string) - for _, path := range []string{"ca/pem", "issuer/default/pem"} { - req := &logical.Request{ - Path: path, - Operation: logical.ReadOperation, - Storage: rootB.storage, - } - resp, err := rootB.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - expected := []byte(caCert) - if path == "issuer/default/pem" { - // Preserves the new line. - expected = []byte(caCert + "\n") - } - if diff := deep.Equal(resp.Data["http_raw_body"].([]byte), expected); diff != nil { - t.Fatal(diff) - } - if resp.Data["http_content_type"].(string) != "application/pem-certificate-chain" { - t.Fatal("wrong content type") - } - } - - // ca and issuer/default/der (raw DER bytes) - for _, path := range []string{"ca", "issuer/default/der"} { - req := &logical.Request{ - Path: path, - Operation: logical.ReadOperation, - Storage: rootB.storage, - } - resp, err := rootB.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - rawBytes := resp.Data["http_raw_body"].([]byte) - pemBytes := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: rawBytes, - }))) - if diff := deep.Equal(pemBytes, caCert); diff != nil { - t.Fatal(diff) - } - if resp.Data["http_content_type"].(string) != "application/pkix-cert" { - t.Fatal("wrong content type") - } - } - - client.SetToken(prevToken) - } - - // Configure an expiry on the CRL and verify what comes back - { - // Set CRL config - { - _, err := client.Logical().Write(rootName+"config/crl", map[string]interface{}{ - "expiry": "16h", - }) - if err != nil { - t.Fatal(err) - } - } - - // Verify it - { - resp, err := client.Logical().Read(rootName + "config/crl") - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - if resp.Data["expiry"].(string) != "16h" { - t.Fatal("expected a 16 hour expiry") - } - } - } - - // Test generating a root, an intermediate, signing it, setting signed, and - // revoking it - - // We'll need this later - var intSerialNumber string - { - // First, delete the existing CA info - { - _, err := client.Logical().Delete(rootName + "root") - if err != nil { - t.Fatal(err) - } - } - - var rootPEM, rootKey, rootPEMBundle string - // Test exported root generation - { - resp, err := client.Logical().Write(rootName+"root/generate/exported", map[string]interface{}{ - "common_name": "Root Cert", - "ttl": "180h", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - rootPEM = resp.Data["certificate"].(string) - rootKey = resp.Data["private_key"].(string) - rootPEMBundle = strings.Join([]string{rootPEM, rootKey}, "\n") - // This is really here to keep the use checker happy - if rootPEMBundle == "" { - t.Fatal("bad root pem bundle") - } - } - - var intPEM, intCSR, intKey string - // Test exported intermediate CSR generation - { - resp, err := client.Logical().Write(intName+"intermediate/generate/exported", map[string]interface{}{ - "common_name": "intermediate.cert.com", - "ttl": "180h", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - intCSR = resp.Data["csr"].(string) - intKey = resp.Data["private_key"].(string) - // This is really here to keep the use checker happy - if intCSR == "" || intKey == "" { - t.Fatal("int csr or key empty") - } - } - - // Test signing - { - resp, err := client.Logical().Write(rootName+"root/sign-intermediate", map[string]interface{}{ - "common_name": "intermediate.cert.com", - "ttl": "10s", - "csr": intCSR, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - intPEM = resp.Data["certificate"].(string) - intSerialNumber = resp.Data["serial_number"].(string) - } - - // Test setting signed - { - resp, err := client.Logical().Write(intName+"intermediate/set-signed", map[string]interface{}{ - "certificate": intPEM, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - } - - // Verify we can find it via the root - { - resp, err := client.Logical().Read(rootName + "cert/" + intSerialNumber) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - if resp.Data["revocation_time"].(json.Number).String() != "0" { - t.Fatal("expected a zero revocation time") - } - } - - // Revoke the intermediate - { - resp, err := client.Logical().Write(rootName+"revoke", map[string]interface{}{ - "serial_number": intSerialNumber, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - } - } - - verifyRevocation := func(t *testing.T, serial string, shouldFind bool) { - t.Helper() - // Verify it is now revoked - { - resp, err := client.Logical().Read(rootName + "cert/" + intSerialNumber) - if err != nil { - t.Fatal(err) - } - switch shouldFind { - case true: - if resp == nil { - t.Fatal("nil response") - } - if resp.Data["revocation_time"].(json.Number).String() == "0" { - t.Fatal("expected a non-zero revocation time") - } - default: - if resp != nil { - t.Fatalf("expected nil response, got %#v", *resp) - } - } - } - - // Fetch the CRL and make sure it shows up - for path, derPemOrJSON := range map[string]int{ - "crl": 0, - "issuer/default/crl/der": 0, - "crl/pem": 1, - "issuer/default/crl/pem": 1, - "cert/crl": 2, - "issuer/default/crl": 3, - } { - req := &logical.Request{ - Path: path, - Operation: logical.ReadOperation, - Storage: rootB.storage, - } - resp, err := rootB.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("nil response") - } - - var crlBytes []byte - if derPemOrJSON == 2 { - // Old endpoint - crlBytes = []byte(resp.Data["certificate"].(string)) - } else if derPemOrJSON == 3 { - // New endpoint - crlBytes = []byte(resp.Data["crl"].(string)) - } else { - // DER or PEM - crlBytes = resp.Data["http_raw_body"].([]byte) - } - - if derPemOrJSON >= 1 { - // Do for both PEM and JSON endpoints - pemBlock, _ := pem.Decode(crlBytes) - crlBytes = pemBlock.Bytes - } - - certList, err := x509.ParseCRL(crlBytes) - if err != nil { - t.Fatal(err) - } - switch shouldFind { - case true: - revokedList := certList.TBSCertList.RevokedCertificates - if len(revokedList) != 1 { - t.Fatalf("bad length of revoked list: %d", len(revokedList)) - } - revokedString := certutil.GetHexFormatted(revokedList[0].SerialNumber.Bytes(), ":") - if revokedString != intSerialNumber { - t.Fatalf("bad revoked serial: %s", revokedString) - } - default: - revokedList := certList.TBSCertList.RevokedCertificates - if len(revokedList) != 0 { - t.Fatalf("bad length of revoked list: %d", len(revokedList)) - } - } - } - } - - verifyTidyStatus := func(expectedCertStoreDeleteCount int, expectedRevokedCertDeletedCount int) { - tidyStatus, err := client.Logical().Read(rootName + "tidy-status") - if err != nil { - t.Fatal(err) - } - - if tidyStatus.Data["state"] != "Finished" { - t.Fatalf("Expected tidy operation to be finished, but tidy-status reports its state is %v", tidyStatus.Data) - } - - var count int64 - if count, err = tidyStatus.Data["cert_store_deleted_count"].(json.Number).Int64(); err != nil { - t.Fatal(err) - } - if int64(expectedCertStoreDeleteCount) != count { - t.Fatalf("Expected %d for cert_store_deleted_count, but got %d", expectedCertStoreDeleteCount, count) - } - - if count, err = tidyStatus.Data["revoked_cert_deleted_count"].(json.Number).Int64(); err != nil { - t.Fatal(err) - } - if int64(expectedRevokedCertDeletedCount) != count { - t.Fatalf("Expected %d for revoked_cert_deleted_count, but got %d", expectedRevokedCertDeletedCount, count) - } - } - - // Validate current state of revoked certificates - verifyRevocation(t, intSerialNumber, true) - - // Give time for the safety buffer to pass before tidying - time.Sleep(10 * time.Second) - - // Test tidying - { - // Run with a high safety buffer, nothing should happen - { - resp, err := client.Logical().Write(rootName+"tidy", map[string]interface{}{ - "safety_buffer": "3h", - "tidy_cert_store": true, - "tidy_revoked_certs": true, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected warnings") - } - - // Wait a few seconds as it runs in a goroutine - time.Sleep(5 * time.Second) - - // Check to make sure we still find the cert and see it on the CRL - verifyRevocation(t, intSerialNumber, true) - - verifyTidyStatus(0, 0) - } - - // Run with both values set false, nothing should happen - { - resp, err := client.Logical().Write(rootName+"tidy", map[string]interface{}{ - "safety_buffer": "1s", - "tidy_cert_store": false, - "tidy_revoked_certs": false, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected warnings") - } - - // Wait a few seconds as it runs in a goroutine - time.Sleep(5 * time.Second) - - // Check to make sure we still find the cert and see it on the CRL - verifyRevocation(t, intSerialNumber, true) - - verifyTidyStatus(0, 0) - } - - // Run with a short safety buffer and both set to true, both should be cleared - { - resp, err := client.Logical().Write(rootName+"tidy", map[string]interface{}{ - "safety_buffer": "1s", - "tidy_cert_store": true, - "tidy_revoked_certs": true, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected warnings") - } - - // Wait a few seconds as it runs in a goroutine - time.Sleep(5 * time.Second) - - // Check to make sure we still find the cert and see it on the CRL - verifyRevocation(t, intSerialNumber, false) - - verifyTidyStatus(1, 1) - } - } -} diff --git a/builtin/logical/pki/cert_util_test.go b/builtin/logical/pki/cert_util_test.go deleted file mode 100644 index cd44c8211..000000000 --- a/builtin/logical/pki/cert_util_test.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "fmt" - "reflect" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestPki_FetchCertBySerial(t *testing.T) { - t.Parallel() - b, storage := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, storage) - - cases := map[string]struct { - Req *logical.Request - Prefix string - Serial string - }{ - "valid cert": { - &logical.Request{ - Storage: storage, - }, - "certs/", - "00:00:00:00:00:00:00:00", - }, - "revoked cert": { - &logical.Request{ - Storage: storage, - }, - "revoked/", - "11:11:11:11:11:11:11:11", - }, - } - - // Test for colon-based paths in storage - for name, tc := range cases { - storageKey := fmt.Sprintf("%s%s", tc.Prefix, tc.Serial) - err := storage.Put(context.Background(), &logical.StorageEntry{ - Key: storageKey, - Value: []byte("some data"), - }) - if err != nil { - t.Fatalf("error writing to storage on %s colon-based storage path: %s", name, err) - } - - certEntry, err := fetchCertBySerial(sc, tc.Prefix, tc.Serial) - if err != nil { - t.Fatalf("error on %s for colon-based storage path: %s", name, err) - } - - // Check for non-nil on valid/revoked certs - if certEntry == nil { - t.Fatalf("nil on %s for colon-based storage path", name) - } - - // Ensure that cert serials are converted/updated after fetch - expectedKey := tc.Prefix + normalizeSerial(tc.Serial) - se, err := storage.Get(context.Background(), expectedKey) - if err != nil { - t.Fatalf("error on %s for colon-based storage path:%s", name, err) - } - if strings.Compare(expectedKey, se.Key) != 0 { - t.Fatalf("expected: %s, got: %s", expectedKey, certEntry.Key) - } - } - - // Reset storage - storage = &logical.InmemStorage{} - - // Test for hyphen-base paths in storage - for name, tc := range cases { - storageKey := tc.Prefix + normalizeSerial(tc.Serial) - err := storage.Put(context.Background(), &logical.StorageEntry{ - Key: storageKey, - Value: []byte("some data"), - }) - if err != nil { - t.Fatalf("error writing to storage on %s hyphen-based storage path: %s", name, err) - } - - certEntry, err := fetchCertBySerial(sc, tc.Prefix, tc.Serial) - if err != nil || certEntry == nil { - t.Fatalf("error on %s for hyphen-based storage path: err: %v, entry: %v", name, err, certEntry) - } - } -} - -// Demonstrate that multiple OUs in the name are handled in an -// order-preserving way. -func TestPki_MultipleOUs(t *testing.T) { - t.Parallel() - var b backend - fields := addCACommonFields(map[string]*framework.FieldSchema{}) - - apiData := &framework.FieldData{ - Schema: fields, - Raw: map[string]interface{}{ - "cn": "example.com", - "ttl": 3600, - }, - } - input := &inputBundle{ - apiData: apiData, - role: &roleEntry{ - MaxTTL: 3600, - OU: []string{"Z", "E", "V"}, - }, - } - cb, _, err := generateCreationBundle(&b, input, nil, nil) - if err != nil { - t.Fatalf("Error: %v", err) - } - - expected := []string{"Z", "E", "V"} - actual := cb.Params.Subject.OrganizationalUnit - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("Expected %v, got %v", expected, actual) - } -} - -func TestPki_PermitFQDNs(t *testing.T) { - t.Parallel() - var b backend - fields := addCACommonFields(map[string]*framework.FieldSchema{}) - - cases := map[string]struct { - input *inputBundle - expectedDnsNames []string - expectedEmails []string - }{ - "base valid case": { - input: &inputBundle{ - apiData: &framework.FieldData{ - Schema: fields, - Raw: map[string]interface{}{ - "common_name": "example.com.", - "ttl": 3600, - }, - }, - role: &roleEntry{ - AllowAnyName: true, - MaxTTL: 3600, - EnforceHostnames: true, - }, - }, - expectedDnsNames: []string{"example.com."}, - expectedEmails: []string{}, - }, - "case insensitivity validation": { - input: &inputBundle{ - apiData: &framework.FieldData{ - Schema: fields, - Raw: map[string]interface{}{ - "common_name": "Example.Net", - "alt_names": "eXaMPLe.COM", - "ttl": 3600, - }, - }, - role: &roleEntry{ - AllowedDomains: []string{"example.net", "EXAMPLE.COM"}, - AllowBareDomains: true, - MaxTTL: 3600, - }, - }, - expectedDnsNames: []string{"Example.Net", "eXaMPLe.COM"}, - expectedEmails: []string{}, - }, - "case email as AllowedDomain with bare domains": { - input: &inputBundle{ - apiData: &framework.FieldData{ - Schema: fields, - Raw: map[string]interface{}{ - "common_name": "test@testemail.com", - "ttl": 3600, - }, - }, - role: &roleEntry{ - AllowedDomains: []string{"test@testemail.com"}, - AllowBareDomains: true, - MaxTTL: 3600, - }, - }, - expectedDnsNames: []string{}, - expectedEmails: []string{"test@testemail.com"}, - }, - "case email common name with bare domains": { - input: &inputBundle{ - apiData: &framework.FieldData{ - Schema: fields, - Raw: map[string]interface{}{ - "common_name": "test@testemail.com", - "ttl": 3600, - }, - }, - role: &roleEntry{ - AllowedDomains: []string{"testemail.com"}, - AllowBareDomains: true, - MaxTTL: 3600, - }, - }, - expectedDnsNames: []string{}, - expectedEmails: []string{"test@testemail.com"}, - }, - } - - for name, testCase := range cases { - name := name - testCase := testCase - t.Run(name, func(t *testing.T) { - cb, _, err := generateCreationBundle(&b, testCase.input, nil, nil) - if err != nil { - t.Fatalf("Error: %v", err) - } - - actualDnsNames := cb.Params.DNSNames - - if !reflect.DeepEqual(testCase.expectedDnsNames, actualDnsNames) { - t.Fatalf("Expected dns names %v, got %v", testCase.expectedDnsNames, actualDnsNames) - } - - actualEmails := cb.Params.EmailAddresses - - if !reflect.DeepEqual(testCase.expectedEmails, actualEmails) { - t.Fatalf("Expected email addresses %v, got %v", testCase.expectedEmails, actualEmails) - } - }) - } -} diff --git a/builtin/logical/pki/chain_test.go b/builtin/logical/pki/chain_test.go deleted file mode 100644 index 3aebbcb7f..000000000 --- a/builtin/logical/pki/chain_test.go +++ /dev/null @@ -1,1647 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "bytes" - "context" - "crypto/x509" - "crypto/x509/pkix" - "encoding/hex" - "encoding/pem" - "fmt" - "strconv" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/logical" -) - -// For speed, all keys are ECDSA. -type CBGenerateKey struct { - Name string -} - -func (c CBGenerateKey) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) { - resp, err := CBWrite(b, s, "keys/generate/exported", map[string]interface{}{ - "name": c.Name, - "algo": "ec", - "bits": 256, - }) - if err != nil { - t.Fatalf("failed to provision key (%v): %v", c.Name, err) - } - knownKeys[c.Name] = resp.Data["private"].(string) -} - -// Generate a root. -type CBGenerateRoot struct { - Key string - Existing bool - Name string - CommonName string - ErrorMessage string -} - -func (c CBGenerateRoot) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) { - url := "issuers/generate/root/" - data := make(map[string]interface{}) - - if c.Existing { - url += "existing" - data["key_ref"] = c.Key - } else { - url += "exported" - data["key_type"] = "ec" - data["key_bits"] = 256 - data["key_name"] = c.Key - } - - data["issuer_name"] = c.Name - data["common_name"] = c.Name - if len(c.CommonName) > 0 { - data["common_name"] = c.CommonName - } - - resp, err := CBWrite(b, s, url, data) - if err != nil { - if len(c.ErrorMessage) > 0 { - if !strings.Contains(err.Error(), c.ErrorMessage) { - t.Fatalf("failed to generate root cert for issuer (%v): expected (%v) in error message but got %v", c.Name, c.ErrorMessage, err) - } - return - } - t.Fatalf("failed to provision issuer (%v): %v / body: %v", c.Name, err, data) - } else if len(c.ErrorMessage) > 0 { - t.Fatalf("expected to fail generation of issuer (%v) with error message containing (%v)", c.Name, c.ErrorMessage) - } - - if !c.Existing { - knownKeys[c.Key] = resp.Data["private_key"].(string) - } - - knownCerts[c.Name] = resp.Data["certificate"].(string) - - // Validate key_id matches. - url = "key/" + c.Key - resp, err = CBRead(b, s, url) - if err != nil { - t.Fatalf("failed to fetch key for name %v: %v", c.Key, err) - } - if resp == nil { - t.Fatalf("failed to fetch key for name %v: nil response", c.Key) - } - - expectedKeyId := resp.Data["key_id"] - - url = "issuer/" + c.Name - resp, err = CBRead(b, s, url) - if err != nil { - t.Fatalf("failed to fetch issuer for name %v: %v", c.Name, err) - } - if resp == nil { - t.Fatalf("failed to fetch issuer for name %v: nil response", c.Name) - } - - actualKeyId := resp.Data["key_id"] - if expectedKeyId != actualKeyId { - t.Fatalf("expected issuer %v to have key matching %v but got mismatch: %v vs %v", c.Name, c.Key, actualKeyId, expectedKeyId) - } -} - -// Generate an intermediate. Might not really be an intermediate; might be -// a cross-signed cert. -type CBGenerateIntermediate struct { - Key string - Existing bool - Name string - CommonName string - SKID string - Parent string - ImportErrorMessage string -} - -func (c CBGenerateIntermediate) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) { - // Build CSR - url := "issuers/generate/intermediate/" - data := make(map[string]interface{}) - - if c.Existing { - url += "existing" - data["key_ref"] = c.Key - } else { - url += "exported" - data["key_type"] = "ec" - data["key_bits"] = 256 - data["key_name"] = c.Key - } - - resp, err := CBWrite(b, s, url, data) - if err != nil { - t.Fatalf("failed to generate CSR for issuer (%v): %v / body: %v", c.Name, err, data) - } - - if !c.Existing { - knownKeys[c.Key] = resp.Data["private_key"].(string) - } - - csr := resp.Data["csr"].(string) - - // Sign CSR - url = fmt.Sprintf("issuer/%s/sign-intermediate", c.Parent) - data = make(map[string]interface{}) - data["csr"] = csr - data["common_name"] = c.Name - if len(c.CommonName) > 0 { - data["common_name"] = c.CommonName - } - if len(c.SKID) > 0 { - // Copy the SKID from an existing, already-issued cert. - otherPEM := knownCerts[c.SKID] - otherCert := ToCertificate(t, otherPEM) - - data["skid"] = hex.EncodeToString(otherCert.SubjectKeyId) - } - - resp, err = CBWrite(b, s, url, data) - if err != nil { - t.Fatalf("failed to sign CSR for issuer (%v): %v / body: %v", c.Name, err, data) - } - - knownCerts[c.Name] = strings.TrimSpace(resp.Data["certificate"].(string)) - - // Verify SKID if one was requested. - if len(c.SKID) > 0 { - otherPEM := knownCerts[c.SKID] - otherCert := ToCertificate(t, otherPEM) - ourCert := ToCertificate(t, knownCerts[c.Name]) - - if !bytes.Equal(otherCert.SubjectKeyId, ourCert.SubjectKeyId) { - t.Fatalf("Expected two certs to have equal SKIDs but differed: them: %v vs us: %v", otherCert.SubjectKeyId, ourCert.SubjectKeyId) - } - } - - // Set the signed intermediate - url = "intermediate/set-signed" - data = make(map[string]interface{}) - data["certificate"] = knownCerts[c.Name] - data["issuer_name"] = c.Name - - resp, err = CBWrite(b, s, url, data) - if err != nil { - if len(c.ImportErrorMessage) > 0 { - if !strings.Contains(err.Error(), c.ImportErrorMessage) { - t.Fatalf("failed to import signed cert for issuer (%v): expected (%v) in error message but got %v", c.Name, c.ImportErrorMessage, err) - } - return - } - - t.Fatalf("failed to import signed cert for issuer (%v): %v / body: %v", c.Name, err, data) - } else if len(c.ImportErrorMessage) > 0 { - t.Fatalf("expected to fail import (with error %v) of cert for issuer (%v) but was success: response: %v", c.ImportErrorMessage, c.Name, resp) - } - - // Update the name since set-signed doesn't actually take an issuer name - // parameter. - rawNewCerts := resp.Data["imported_issuers"].([]string) - if len(rawNewCerts) != 1 { - t.Fatalf("Expected a single new certificate during import of signed cert for %v: got %v\nresp: %v", c.Name, len(rawNewCerts), resp) - } - - newCertId := rawNewCerts[0] - _, err = CBWrite(b, s, "issuer/"+newCertId, map[string]interface{}{ - "issuer_name": c.Name, - }) - if err != nil { - t.Fatalf("failed to update name for issuer (%v/%v): %v", c.Name, newCertId, err) - } - - // Validate key_id matches. - url = "key/" + c.Key - resp, err = CBRead(b, s, url) - if err != nil { - t.Fatalf("failed to fetch key for name %v: %v", c.Key, err) - } - if resp == nil { - t.Fatalf("failed to fetch key for name %v: nil response", c.Key) - } - - expectedKeyId := resp.Data["key_id"] - - url = "issuer/" + c.Name - resp, err = CBRead(b, s, url) - if err != nil { - t.Fatalf("failed to fetch issuer for name %v: %v", c.Name, err) - } - if resp == nil { - t.Fatalf("failed to fetch issuer for name %v: nil response", c.Name) - } - - actualKeyId := resp.Data["key_id"] - if expectedKeyId != actualKeyId { - t.Fatalf("expected issuer %v to have key matching %v but got mismatch: %v vs %v", c.Name, c.Key, actualKeyId, expectedKeyId) - } -} - -// Delete an issuer; breaks chains. -type CBDeleteIssuer struct { - Issuer string -} - -func (c CBDeleteIssuer) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) { - url := fmt.Sprintf("issuer/%v", c.Issuer) - _, err := CBDelete(b, s, url) - if err != nil { - t.Fatalf("failed to delete issuer (%v): %v", c.Issuer, err) - } - - delete(knownCerts, c.Issuer) -} - -// Validate the specified chain exists, by name. -type CBValidateChain struct { - Chains map[string][]string - Aliases map[string]string -} - -func (c CBValidateChain) ChainToPEMs(t testing.TB, parent string, chain []string, knownCerts map[string]string) []string { - var result []string - for entryIndex, entry := range chain { - var chainEntry string - modifiedEntry := entry - if entryIndex == 0 && entry == "self" { - modifiedEntry = parent - } - for pattern, replacement := range c.Aliases { - modifiedEntry = strings.ReplaceAll(modifiedEntry, pattern, replacement) - } - for _, issuer := range strings.Split(modifiedEntry, ",") { - cert, ok := knownCerts[issuer] - if !ok { - t.Fatalf("Unknown issuer %v in chain for %v: %v", issuer, parent, chain) - } - - chainEntry += cert - } - result = append(result, chainEntry) - } - - return result -} - -func (c CBValidateChain) FindNameForCert(t testing.TB, cert string, knownCerts map[string]string) string { - for issuer, known := range knownCerts { - if strings.TrimSpace(known) == strings.TrimSpace(cert) { - return issuer - } - } - - t.Fatalf("Unable to find cert:\n[%v]\nin known map:\n%v\n", cert, knownCerts) - return "" -} - -func (c CBValidateChain) PrettyChain(t testing.TB, chain []string, knownCerts map[string]string) []string { - var prettyChain []string - for _, cert := range chain { - prettyChain = append(prettyChain, c.FindNameForCert(t, cert, knownCerts)) - } - - return prettyChain -} - -func ToCertificate(t testing.TB, cert string) *x509.Certificate { - t.Helper() - - block, _ := pem.Decode([]byte(cert)) - if block == nil { - t.Fatalf("Unable to parse certificate: nil PEM block\n[%v]\n", cert) - } - - ret, err := x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatalf("Unable to parse certificate: %v\n[%v]\n", err, cert) - } - - return ret -} - -func ToCRL(t testing.TB, crl string, issuer *x509.Certificate) *pkix.CertificateList { - t.Helper() - - block, _ := pem.Decode([]byte(crl)) - if block == nil { - t.Fatalf("Unable to parse CRL: nil PEM block\n[%v]\n", crl) - } - - ret, err := x509.ParseCRL(block.Bytes) - if err != nil { - t.Fatalf("Unable to parse CRL: %v\n[%v]\n", err, crl) - } - - if issuer != nil { - if err := issuer.CheckCRLSignature(ret); err != nil { - t.Fatalf("Unable to check CRL signature: %v\n[%v]\n[%v]\n", err, crl, issuer) - } - } - - return ret -} - -func (c CBValidateChain) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) { - for issuer, chain := range c.Chains { - resp, err := CBRead(b, s, "issuer/"+issuer) - if err != nil { - t.Fatalf("failed to get chain for issuer (%v): %v", issuer, err) - } - - rawCurrentChain := resp.Data["ca_chain"].([]string) - var currentChain []string - for _, entry := range rawCurrentChain { - currentChain = append(currentChain, strings.TrimSpace(entry)) - } - - // Ensure the issuer cert is always first. - if currentChain[0] != knownCerts[issuer] { - pretty := c.FindNameForCert(t, currentChain[0], knownCerts) - t.Fatalf("expected certificate at index 0 to be self:\n[%v]\n[pretty: %v]\nis not the issuer's cert:\n[%v]\n[pretty: %v]", currentChain[0], pretty, knownCerts[issuer], issuer) - } - - // Validate it against the expected chain. - expectedChain := c.ChainToPEMs(t, issuer, chain, knownCerts) - if len(currentChain) != len(expectedChain) { - prettyCurrentChain := c.PrettyChain(t, currentChain, knownCerts) - t.Fatalf("Lengths of chains for issuer %v mismatched: got %v vs expected %v:\n[%v]\n[pretty: %v]\n[%v]\n[pretty: %v]", issuer, len(currentChain), len(expectedChain), currentChain, prettyCurrentChain, expectedChain, chain) - } - - for currentIndex, currentCert := range currentChain { - // Chains might be forked so we may not be able to strictly validate - // the chain against a single value. Instead, use strings.Contains - // to validate the current cert is in the list of allowed - // possibilities. - if !strings.Contains(expectedChain[currentIndex], currentCert) { - pretty := c.FindNameForCert(t, currentCert, knownCerts) - t.Fatalf("chain mismatch at index %v for issuer %v: got cert:\n[%v]\n[pretty: %v]\nbut expected one of\n[%v]\n[pretty: %v]\n", currentIndex, issuer, currentCert, pretty, expectedChain[currentIndex], chain[currentIndex]) - } - } - - // Due to alternate paths, the above doesn't ensure ensure each cert - // in the chain is only used once. Validate that now. - for thisIndex, thisCert := range currentChain { - for otherIndex, otherCert := range currentChain[thisIndex+1:] { - if thisCert == otherCert { - thisPretty := c.FindNameForCert(t, thisCert, knownCerts) - otherPretty := c.FindNameForCert(t, otherCert, knownCerts) - otherIndex += thisIndex + 1 - t.Fatalf("cert reused in chain for %v:\n[%v]\n[pretty: %v / index: %v]\n[%v]\n[pretty: %v / index: %v]\n", issuer, thisCert, thisPretty, thisIndex, otherCert, otherPretty, otherIndex) - } - } - } - - // Finally, validate that all certs verify something that came before - // it. In the linear chain sense, this should strictly mean that the - // parent comes before the child. - for thisIndex, thisCertPem := range currentChain[1:] { - thisIndex += 1 // Absolute index. - parentCert := ToCertificate(t, thisCertPem) - - // Iterate backwards; prefer the most recent cert to the older - // certs. - foundCert := false - for otherIndex := thisIndex - 1; otherIndex >= 0; otherIndex-- { - otherCertPem := currentChain[otherIndex] - childCert := ToCertificate(t, otherCertPem) - - if err := childCert.CheckSignatureFrom(parentCert); err == nil { - foundCert = true - } - } - - if !foundCert { - pretty := c.FindNameForCert(t, thisCertPem, knownCerts) - t.Fatalf("malformed test scenario: certificate at chain index %v when validating %v does not validate any previous certificates:\n[%v]\n[pretty: %v]\n", thisIndex, issuer, thisCertPem, pretty) - } - } - } -} - -// Update an issuer -type CBUpdateIssuer struct { - Name string - CAChain []string - Usage string -} - -func (c CBUpdateIssuer) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) { - url := "issuer/" + c.Name - data := make(map[string]interface{}) - data["issuer_name"] = c.Name - - resp, err := CBRead(b, s, url) - if err != nil { - t.Fatalf("failed to read issuer (%v): %v", c.Name, err) - } - - if len(c.CAChain) == 1 && c.CAChain[0] == "existing" { - data["manual_chain"] = resp.Data["manual_chain"] - } else { - data["manual_chain"] = c.CAChain - } - - if c.Usage == "existing" { - data["usage"] = resp.Data["usage"] - } else if len(c.Usage) == 0 { - data["usage"] = "read-only,issuing-certificates,crl-signing" - } else { - data["usage"] = c.Usage - } - - _, err = CBWrite(b, s, url, data) - if err != nil { - t.Fatalf("failed to update issuer (%v): %v / body: %v", c.Name, err, data) - } -} - -// Issue a leaf, revoke it, and then validate it appears on the CRL. -type CBIssueLeaf struct { - Issuer string - Role string -} - -func (c CBIssueLeaf) IssueLeaf(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string, errorMessage string) *logical.Response { - // Write a role - url := "roles/" + c.Role - data := make(map[string]interface{}) - data["allow_localhost"] = true - data["ttl"] = "200s" - data["key_type"] = "ec" - - _, err := CBWrite(b, s, url, data) - if err != nil { - t.Fatalf("failed to update role (%v): %v / body: %v", c.Role, err, data) - } - - // Issue the certificate. - url = "issuer/" + c.Issuer + "/issue/" + c.Role - data = make(map[string]interface{}) - data["common_name"] = "localhost" - - resp, err := CBWrite(b, s, url, data) - if err != nil { - if len(errorMessage) >= 0 { - if !strings.Contains(err.Error(), errorMessage) { - t.Fatalf("failed to issue cert (%v via %v): %v / body: %v\nExpected error message: %v", c.Issuer, c.Role, err, data, errorMessage) - } - - return nil - } - - t.Fatalf("failed to issue cert (%v via %v): %v / body: %v", c.Issuer, c.Role, err, data) - } - if resp == nil { - t.Fatalf("failed to issue cert (%v via %v): nil response / body: %v", c.Issuer, c.Role, data) - } - - raw_cert := resp.Data["certificate"].(string) - cert := ToCertificate(t, raw_cert) - raw_issuer := resp.Data["issuing_ca"].(string) - issuer := ToCertificate(t, raw_issuer) - - // Validate issuer and signatures are good. - if strings.TrimSpace(raw_issuer) != strings.TrimSpace(knownCerts[c.Issuer]) { - t.Fatalf("signing certificate ended with wrong certificate for issuer %v:\n[%v]\n\nvs\n\n[%v]\n", c.Issuer, raw_issuer, knownCerts[c.Issuer]) - } - - if err := cert.CheckSignatureFrom(issuer); err != nil { - t.Fatalf("failed to verify signature on issued certificate from %v: %v\n[%v]\n[%v]\n", c.Issuer, err, raw_cert, raw_issuer) - } - - return resp -} - -func (c CBIssueLeaf) RevokeLeaf(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string, issueResponse *logical.Response, hasCRL bool, isDefault bool) { - api_serial := issueResponse.Data["serial_number"].(string) - raw_cert := issueResponse.Data["certificate"].(string) - cert := ToCertificate(t, raw_cert) - raw_issuer := issueResponse.Data["issuing_ca"].(string) - issuer := ToCertificate(t, raw_issuer) - - // Revoke the certificate. - url := "revoke" - data := make(map[string]interface{}) - data["serial_number"] = api_serial - resp, err := CBWrite(b, s, url, data) - if err != nil { - t.Fatalf("failed to revoke issued certificate (%v) under role %v / issuer %v: %v", api_serial, c.Role, c.Issuer, err) - } - if resp == nil { - t.Fatalf("failed to revoke issued certificate (%v) under role %v / issuer %v: nil response", api_serial, c.Role, c.Issuer) - } - if _, ok := resp.Data["revocation_time"]; !ok { - t.Fatalf("failed to revoke issued certificate (%v) under role %v / issuer %v: expected response parameter revocation_time was missing from response:\n%v", api_serial, c.Role, c.Issuer, resp.Data) - } - - if !hasCRL { - // Nothing further we can test here. We could re-enable CRL building - // and check that it works, but that seems like a stretch. Other - // issuers might be functionally the same as this issuer (and thus - // this CRL will still be issued), but that requires more work to - // check and verify. - return - } - - // Verify it is on this issuer's CRL. - url = "issuer/" + c.Issuer + "/crl" - resp, err = CBRead(b, s, url) - if err != nil { - t.Fatalf("failed to fetch CRL for issuer %v: %v", c.Issuer, err) - } - if resp == nil { - t.Fatalf("failed to fetch CRL for issuer %v: nil response", c.Issuer) - } - - raw_crl := resp.Data["crl"].(string) - crl := ToCRL(t, raw_crl, issuer) - - foundCert := requireSerialNumberInCRL(nil, crl.TBSCertList, api_serial) - if !foundCert { - if !hasCRL && !isDefault { - // Update the issuer we expect to find this on. - resp, err := CBRead(b, s, "config/issuers") - if err != nil { - t.Fatalf("failed to read default issuer config: %v", err) - } - if resp == nil { - t.Fatalf("failed to read default issuer config: nil response") - } - defaultID := resp.Data["default"].(issuerID).String() - c.Issuer = defaultID - issuer = nil - } - - // Verify it is on the default issuer's CRL. - url = "issuer/" + c.Issuer + "/crl" - resp, err = CBRead(b, s, url) - if err != nil { - t.Fatalf("failed to fetch CRL for issuer %v: %v", c.Issuer, err) - } - if resp == nil { - t.Fatalf("failed to fetch CRL for issuer %v: nil response", c.Issuer) - } - - raw_crl = resp.Data["crl"].(string) - crl = ToCRL(t, raw_crl, issuer) - - foundCert = requireSerialNumberInCRL(nil, crl.TBSCertList, api_serial) - } - - if !foundCert { - // If CRL building is broken, this is useful for finding which issuer's - // CRL the revoked cert actually appears on. - for issuerName := range knownCerts { - url = "issuer/" + issuerName + "/crl" - resp, err = CBRead(b, s, url) - if err != nil { - t.Fatalf("failed to fetch CRL for issuer %v: %v", issuerName, err) - } - if resp == nil { - t.Fatalf("failed to fetch CRL for issuer %v: nil response", issuerName) - } - - raw_crl := resp.Data["crl"].(string) - crl := ToCRL(t, raw_crl, nil) - - for index, revoked := range crl.TBSCertList.RevokedCertificates { - // t.Logf("[%v] revoked serial number: %v -- vs -- %v", index, revoked.SerialNumber, cert.SerialNumber) - if revoked.SerialNumber.Cmp(cert.SerialNumber) == 0 { - t.Logf("found revoked cert at index: %v for unexpected issuer: %v", index, issuerName) - break - } - } - } - - t.Fatalf("expected to find certificate with serial [%v] on issuer %v's CRL but was missing: %v revoked certs\n\nCRL:\n[%v]\n\nLeaf:\n[%v]\n\nIssuer (hasCRL: %v):\n[%v]\n", api_serial, c.Issuer, len(crl.TBSCertList.RevokedCertificates), raw_crl, raw_cert, hasCRL, raw_issuer) - } -} - -func (c CBIssueLeaf) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) { - if len(c.Role) == 0 { - c.Role = "testing" - } - - resp, err := CBRead(b, s, "config/issuers") - if err != nil { - t.Fatalf("failed to read default issuer config: %v", err) - } - if resp == nil { - t.Fatalf("failed to read default issuer config: nil response") - } - defaultID := resp.Data["default"].(issuerID).String() - - resp, err = CBRead(b, s, "issuer/"+c.Issuer) - if err != nil { - t.Fatalf("failed to read issuer %v: %v", c.Issuer, err) - } - if resp == nil { - t.Fatalf("failed to read issuer %v: nil response", c.Issuer) - } - ourID := resp.Data["issuer_id"].(issuerID).String() - areDefault := ourID == defaultID - - for _, usage := range []string{"read-only", "crl-signing", "issuing-certificates", "issuing-certificates,crl-signing"} { - ui := CBUpdateIssuer{ - Name: c.Issuer, - CAChain: []string{"existing"}, - Usage: usage, - } - ui.Run(t, b, s, knownKeys, knownCerts) - - ilError := "requested usage issuing-certificates for issuer" - hasIssuing := strings.Contains(usage, "issuing-certificates") - if hasIssuing { - ilError = "" - } - - hasCRL := strings.Contains(usage, "crl-signing") - - resp := c.IssueLeaf(t, b, s, knownKeys, knownCerts, ilError) - if resp == nil && !hasIssuing { - continue - } - - c.RevokeLeaf(t, b, s, knownKeys, knownCerts, resp, hasCRL, areDefault) - } -} - -// Stable ordering -func ensureStableOrderingOfChains(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) { - // Start by fetching all chains - certChains := make(map[string][]string) - for issuer := range knownCerts { - resp, err := CBRead(b, s, "issuer/"+issuer) - if err != nil { - t.Fatalf("failed to get chain for issuer (%v): %v", issuer, err) - } - - rawCurrentChain := resp.Data["ca_chain"].([]string) - var currentChain []string - for _, entry := range rawCurrentChain { - currentChain = append(currentChain, strings.TrimSpace(entry)) - } - - certChains[issuer] = currentChain - } - - // Now, generate a bunch of arbitrary roots and validate the chain is - // consistent. - var runs []time.Duration - for i := 0; i < 10; i++ { - name := "stable-order-root-" + strconv.Itoa(i) - step := CBGenerateRoot{ - Key: name, - Name: name, - } - step.Run(t, b, s, make(map[string]string), make(map[string]string)) - - before := time.Now() - _, err := CBDelete(b, s, "issuer/"+name) - if err != nil { - t.Fatalf("failed to delete temporary testing issuer %v: %v", name, err) - } - after := time.Now() - elapsed := after.Sub(before) - runs = append(runs, elapsed) - - for issuer := range knownCerts { - resp, err := CBRead(b, s, "issuer/"+issuer) - if err != nil { - t.Fatalf("failed to get chain for issuer (%v): %v", issuer, err) - } - - rawCurrentChain := resp.Data["ca_chain"].([]string) - for index, entry := range rawCurrentChain { - if strings.TrimSpace(entry) != certChains[issuer][index] { - t.Fatalf("iteration %d - chain for issuer %v differed at index %d\n%v\nvs\n%v", i, issuer, index, entry, certChains[issuer][index]) - } - } - } - } - - min := runs[0] - max := runs[0] - var avg time.Duration - for _, run := range runs { - if run < min { - min = run - } - - if run > max { - max = run - } - - avg += run - } - avg = avg / time.Duration(len(runs)) - - t.Logf("Chain building run time (deletion) - min: %v / avg: %v / max: %v - entries: %v", min, avg, max, runs) -} - -type CBTestStep interface { - Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) -} - -type CBTestScenario struct { - Steps []CBTestStep -} - -var chainBuildingTestCases = []CBTestScenario{ - { - // This test builds up two cliques lined by a cycle, dropping into - // a single intermediate. - Steps: []CBTestStep{ - // Create a reissued certificate using the same key. These - // should validate themselves. - CBGenerateRoot{ - Key: "key-root-old", - Name: "root-old-a", - CommonName: "root-old", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self"}, - }, - }, - // After adding the second root using the same key and common - // name, there should now be two certs in each chain. - CBGenerateRoot{ - Key: "key-root-old", - Existing: true, - Name: "root-old-b", - CommonName: "root-old", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self", "root-old-b"}, - "root-old-b": {"self", "root-old-a"}, - }, - }, - // After adding a third root, there are now two possibilities for - // each later chain entry. - CBGenerateRoot{ - Key: "key-root-old", - Existing: true, - Name: "root-old-c", - CommonName: "root-old", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self", "root-old-bc", "root-old-bc"}, - "root-old-b": {"self", "root-old-ac", "root-old-ac"}, - "root-old-c": {"self", "root-old-ab", "root-old-ab"}, - }, - Aliases: map[string]string{ - "root-old-ac": "root-old-a,root-old-c", - "root-old-ab": "root-old-a,root-old-b", - "root-old-bc": "root-old-b,root-old-c", - }, - }, - // If we generate an unrelated issuer, it shouldn't affect either - // chain. - CBGenerateRoot{ - Key: "key-root-new", - Name: "root-new-a", - CommonName: "root-new", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self", "root-old-bc", "root-old-bc"}, - "root-old-b": {"self", "root-old-ac", "root-old-ac"}, - "root-old-c": {"self", "root-old-ab", "root-old-ab"}, - "root-new-a": {"self"}, - }, - Aliases: map[string]string{ - "root-old-ac": "root-old-a,root-old-c", - "root-old-ab": "root-old-a,root-old-b", - "root-old-bc": "root-old-b,root-old-c", - }, - }, - // Reissuing this new root should form another clique. - CBGenerateRoot{ - Key: "key-root-new", - Existing: true, - Name: "root-new-b", - CommonName: "root-new", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self", "root-old-bc", "root-old-bc"}, - "root-old-b": {"self", "root-old-ac", "root-old-ac"}, - "root-old-c": {"self", "root-old-ab", "root-old-ab"}, - "root-new-a": {"self", "root-new-b"}, - "root-new-b": {"self", "root-new-a"}, - }, - Aliases: map[string]string{ - "root-old-ac": "root-old-a,root-old-c", - "root-old-ab": "root-old-a,root-old-b", - "root-old-bc": "root-old-b,root-old-c", - }, - }, - // Generating a cross-signed cert from old->new should result - // in all old clique certs showing up in the new root's paths. - // This does not form a cycle. - CBGenerateIntermediate{ - // In order to validate the existing root-new clique, we - // have to reuse the key and common name here for - // cross-signing. - Key: "key-root-new", - Existing: true, - Name: "cross-old-new", - CommonName: "root-new", - SKID: "root-new-a", - // Which old issuer is used here doesn't matter as they have - // the same CN and key. - Parent: "root-old-a", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self", "root-old-bc", "root-old-bc"}, - "root-old-b": {"self", "root-old-ac", "root-old-ac"}, - "root-old-c": {"self", "root-old-ab", "root-old-ab"}, - "cross-old-new": {"self", "root-old-abc", "root-old-abc", "root-old-abc"}, - "root-new-a": {"self", "root-new-b", "cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - "root-new-b": {"self", "root-new-a", "cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - }, - Aliases: map[string]string{ - "root-old-ac": "root-old-a,root-old-c", - "root-old-ab": "root-old-a,root-old-b", - "root-old-bc": "root-old-b,root-old-c", - "root-old-abc": "root-old-a,root-old-b,root-old-c", - }, - }, - // If we create a new intermediate off of the root-new, we should - // simply add to the existing chain. - CBGenerateIntermediate{ - Key: "key-inter-a-root-new", - Name: "inter-a-root-new", - Parent: "root-new-a", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self", "root-old-bc", "root-old-bc"}, - "root-old-b": {"self", "root-old-ac", "root-old-ac"}, - "root-old-c": {"self", "root-old-ab", "root-old-ab"}, - "cross-old-new": {"self", "root-old-abc", "root-old-abc", "root-old-abc"}, - "root-new-a": {"self", "root-new-b", "cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - "root-new-b": {"self", "root-new-a", "cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - // If we find cross-old-new first, the old clique will be ahead - // of the new clique; otherwise the new clique will appear first. - "inter-a-root-new": {"self", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle"}, - }, - Aliases: map[string]string{ - "root-old-ac": "root-old-a,root-old-c", - "root-old-ab": "root-old-a,root-old-b", - "root-old-bc": "root-old-b,root-old-c", - "root-old-abc": "root-old-a,root-old-b,root-old-c", - "full-cycle": "root-old-a,root-old-b,root-old-c,cross-old-new,root-new-a,root-new-b", - }, - }, - // Now, if we cross-sign back from new to old, we should - // form cycle with multiple reissued cliques. This means - // all nodes will have the same chain. - CBGenerateIntermediate{ - // In order to validate the existing root-old clique, we - // have to reuse the key and common name here for - // cross-signing. - Key: "key-root-old", - Existing: true, - Name: "cross-new-old", - CommonName: "root-old", - SKID: "root-old-a", - // Which new issuer is used here doesn't matter as they have - // the same CN and key. - Parent: "root-new-a", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self", "root-old-bc", "root-old-bc", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"}, - "root-old-b": {"self", "root-old-ac", "root-old-ac", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"}, - "root-old-c": {"self", "root-old-ab", "root-old-ab", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"}, - "cross-old-new": {"self", "cross-new-old", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"}, - "cross-new-old": {"self", "cross-old-new", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"}, - "root-new-a": {"self", "root-new-b", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - "root-new-b": {"self", "root-new-a", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - "inter-a-root-new": {"self", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle"}, - }, - Aliases: map[string]string{ - "root-old-ac": "root-old-a,root-old-c", - "root-old-ab": "root-old-a,root-old-b", - "root-old-bc": "root-old-b,root-old-c", - "root-old-abc": "root-old-a,root-old-b,root-old-c", - "root-new-ab": "root-new-a,root-new-b", - "both-cross-old-new": "cross-old-new,cross-new-old", - "both-cliques": "root-old-a,root-old-b,root-old-c,root-new-a,root-new-b", - "full-cycle": "root-old-a,root-old-b,root-old-c,cross-old-new,cross-new-old,root-new-a,root-new-b", - }, - }, - // Update each old root to only include itself. - CBUpdateIssuer{ - Name: "root-old-a", - CAChain: []string{"root-old-a"}, - }, - CBUpdateIssuer{ - Name: "root-old-b", - CAChain: []string{"root-old-b"}, - }, - CBUpdateIssuer{ - Name: "root-old-c", - CAChain: []string{"root-old-c"}, - }, - // Step 19 - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self"}, - "root-old-b": {"self"}, - "root-old-c": {"self"}, - "cross-old-new": {"self", "cross-new-old", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"}, - "cross-new-old": {"self", "cross-old-new", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"}, - "root-new-a": {"self", "root-new-b", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - "root-new-b": {"self", "root-new-a", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - "inter-a-root-new": {"self", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle"}, - }, - Aliases: map[string]string{ - "root-old-ac": "root-old-a,root-old-c", - "root-old-ab": "root-old-a,root-old-b", - "root-old-bc": "root-old-b,root-old-c", - "root-old-abc": "root-old-a,root-old-b,root-old-c", - "root-new-ab": "root-new-a,root-new-b", - "both-cross-old-new": "cross-old-new,cross-new-old", - "both-cliques": "root-old-a,root-old-b,root-old-c,root-new-a,root-new-b", - "full-cycle": "root-old-a,root-old-b,root-old-c,cross-old-new,cross-new-old,root-new-a,root-new-b", - }, - }, - // Reset the old roots; should get the original chains back. - CBUpdateIssuer{ - Name: "root-old-a", - }, - CBUpdateIssuer{ - Name: "root-old-b", - }, - CBUpdateIssuer{ - Name: "root-old-c", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-old-a": {"self", "root-old-bc", "root-old-bc", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"}, - "root-old-b": {"self", "root-old-ac", "root-old-ac", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"}, - "root-old-c": {"self", "root-old-ab", "root-old-ab", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"}, - "cross-old-new": {"self", "cross-new-old", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"}, - "cross-new-old": {"self", "cross-old-new", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"}, - "root-new-a": {"self", "root-new-b", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - "root-new-b": {"self", "root-new-a", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"}, - "inter-a-root-new": {"self", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle"}, - }, - Aliases: map[string]string{ - "root-old-ac": "root-old-a,root-old-c", - "root-old-ab": "root-old-a,root-old-b", - "root-old-bc": "root-old-b,root-old-c", - "root-old-abc": "root-old-a,root-old-b,root-old-c", - "root-new-ab": "root-new-a,root-new-b", - "both-cross-old-new": "cross-old-new,cross-new-old", - "both-cliques": "root-old-a,root-old-b,root-old-c,root-new-a,root-new-b", - "full-cycle": "root-old-a,root-old-b,root-old-c,cross-old-new,cross-new-old,root-new-a,root-new-b", - }, - }, - CBIssueLeaf{Issuer: "root-old-a"}, - CBIssueLeaf{Issuer: "root-old-b"}, - CBIssueLeaf{Issuer: "root-old-c"}, - CBIssueLeaf{Issuer: "cross-old-new"}, - CBIssueLeaf{Issuer: "cross-new-old"}, - CBIssueLeaf{Issuer: "root-new-a"}, - CBIssueLeaf{Issuer: "root-new-b"}, - CBIssueLeaf{Issuer: "inter-a-root-new"}, - }, - }, - { - // Here we're testing our chain capacity. First we'll create a - // bunch of unique roots to form a cycle of length 10. - Steps: []CBTestStep{ - CBGenerateRoot{ - Key: "key-root-a", - Name: "root-a", - CommonName: "root-a", - }, - CBGenerateRoot{ - Key: "key-root-b", - Name: "root-b", - CommonName: "root-b", - }, - CBGenerateRoot{ - Key: "key-root-c", - Name: "root-c", - CommonName: "root-c", - }, - CBGenerateRoot{ - Key: "key-root-d", - Name: "root-d", - CommonName: "root-d", - }, - CBGenerateRoot{ - Key: "key-root-e", - Name: "root-e", - CommonName: "root-e", - }, - // They should all be disjoint to start. - CBValidateChain{ - Chains: map[string][]string{ - "root-a": {"self"}, - "root-b": {"self"}, - "root-c": {"self"}, - "root-d": {"self"}, - "root-e": {"self"}, - }, - }, - // Start the cross-signing chains. These are all linear, so there's - // no error expected; they're just long. - CBGenerateIntermediate{ - Key: "key-root-b", - Existing: true, - Name: "cross-a-b", - CommonName: "root-b", - Parent: "root-a", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-a": {"self"}, - "cross-a-b": {"self", "root-a"}, - "root-b": {"self", "cross-a-b", "root-a"}, - "root-c": {"self"}, - "root-d": {"self"}, - "root-e": {"self"}, - }, - }, - CBGenerateIntermediate{ - Key: "key-root-c", - Existing: true, - Name: "cross-b-c", - CommonName: "root-c", - Parent: "root-b", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-a": {"self"}, - "cross-a-b": {"self", "root-a"}, - "root-b": {"self", "cross-a-b", "root-a"}, - "cross-b-c": {"self", "b-or-cross", "b-chained-cross", "b-chained-cross"}, - "root-c": {"self", "cross-b-c", "b-or-cross", "b-chained-cross", "b-chained-cross"}, - "root-d": {"self"}, - "root-e": {"self"}, - }, - Aliases: map[string]string{ - "b-or-cross": "root-b,cross-a-b", - "b-chained-cross": "root-b,cross-a-b,root-a", - }, - }, - CBGenerateIntermediate{ - Key: "key-root-d", - Existing: true, - Name: "cross-c-d", - CommonName: "root-d", - Parent: "root-c", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-a": {"self"}, - "cross-a-b": {"self", "root-a"}, - "root-b": {"self", "cross-a-b", "root-a"}, - "cross-b-c": {"self", "b-or-cross", "b-chained-cross", "b-chained-cross"}, - "root-c": {"self", "cross-b-c", "b-or-cross", "b-chained-cross", "b-chained-cross"}, - "cross-c-d": {"self", "c-or-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross"}, - "root-d": {"self", "cross-c-d", "c-or-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross"}, - "root-e": {"self"}, - }, - Aliases: map[string]string{ - "b-or-cross": "root-b,cross-a-b", - "b-chained-cross": "root-b,cross-a-b,root-a", - "c-or-cross": "root-c,cross-b-c", - "c-chained-cross": "root-c,cross-b-c,root-b,cross-a-b,root-a", - }, - }, - CBGenerateIntermediate{ - Key: "key-root-e", - Existing: true, - Name: "cross-d-e", - CommonName: "root-e", - Parent: "root-d", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-a": {"self"}, - "cross-a-b": {"self", "root-a"}, - "root-b": {"self", "cross-a-b", "root-a"}, - "cross-b-c": {"self", "b-or-cross", "b-chained-cross", "b-chained-cross"}, - "root-c": {"self", "cross-b-c", "b-or-cross", "b-chained-cross", "b-chained-cross"}, - "cross-c-d": {"self", "c-or-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross"}, - "root-d": {"self", "cross-c-d", "c-or-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross"}, - "cross-d-e": {"self", "d-or-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross"}, - "root-e": {"self", "cross-d-e", "d-or-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross"}, - }, - Aliases: map[string]string{ - "b-or-cross": "root-b,cross-a-b", - "b-chained-cross": "root-b,cross-a-b,root-a", - "c-or-cross": "root-c,cross-b-c", - "c-chained-cross": "root-c,cross-b-c,root-b,cross-a-b,root-a", - "d-or-cross": "root-d,cross-c-d", - "d-chained-cross": "root-d,cross-c-d,root-c,cross-b-c,root-b,cross-a-b,root-a", - }, - }, - CBIssueLeaf{Issuer: "root-a"}, - CBIssueLeaf{Issuer: "cross-a-b"}, - CBIssueLeaf{Issuer: "root-b"}, - CBIssueLeaf{Issuer: "cross-b-c"}, - CBIssueLeaf{Issuer: "root-c"}, - CBIssueLeaf{Issuer: "cross-c-d"}, - CBIssueLeaf{Issuer: "root-d"}, - CBIssueLeaf{Issuer: "cross-d-e"}, - CBIssueLeaf{Issuer: "root-e"}, - // Importing the new e->a cross fails because the cycle - // it builds is too long. - CBGenerateIntermediate{ - Key: "key-root-a", - Existing: true, - Name: "cross-e-a", - CommonName: "root-a", - Parent: "root-e", - ImportErrorMessage: "exceeds max size", - }, - // Deleting any root and one of its crosses (either a->b or b->c) - // should fix this. - CBDeleteIssuer{"root-b"}, - CBDeleteIssuer{"cross-b-c"}, - // Importing the new e->a cross fails because the cycle - // it builds is too long. - CBGenerateIntermediate{ - Key: "key-root-a", - Existing: true, - Name: "cross-e-a", - CommonName: "root-a", - Parent: "root-e", - }, - CBIssueLeaf{Issuer: "root-a"}, - CBIssueLeaf{Issuer: "cross-a-b"}, - CBIssueLeaf{Issuer: "root-c"}, - CBIssueLeaf{Issuer: "cross-c-d"}, - CBIssueLeaf{Issuer: "root-d"}, - CBIssueLeaf{Issuer: "cross-d-e"}, - CBIssueLeaf{Issuer: "root-e"}, - CBIssueLeaf{Issuer: "cross-e-a"}, - }, - }, - { - // Here we're testing our clique capacity. First we'll create a - // bunch of unique roots to form a cycle of length 10. - Steps: []CBTestStep{ - CBGenerateRoot{ - Key: "key-root", - Name: "root-a", - CommonName: "root", - }, - CBGenerateRoot{ - Key: "key-root", - Existing: true, - Name: "root-b", - CommonName: "root", - }, - CBGenerateRoot{ - Key: "key-root", - Existing: true, - Name: "root-c", - CommonName: "root", - }, - CBGenerateRoot{ - Key: "key-root", - Existing: true, - Name: "root-d", - CommonName: "root", - }, - CBGenerateRoot{ - Key: "key-root", - Existing: true, - Name: "root-e", - CommonName: "root", - }, - CBGenerateRoot{ - Key: "key-root", - Existing: true, - Name: "root-f", - CommonName: "root", - }, - CBIssueLeaf{Issuer: "root-a"}, - CBIssueLeaf{Issuer: "root-b"}, - CBIssueLeaf{Issuer: "root-c"}, - CBIssueLeaf{Issuer: "root-d"}, - CBIssueLeaf{Issuer: "root-e"}, - CBIssueLeaf{Issuer: "root-f"}, - // Seventh reissuance fails. - CBGenerateRoot{ - Key: "key-root", - Existing: true, - Name: "root-g", - CommonName: "root", - ErrorMessage: "excessively reissued certificate", - }, - // Deleting one and trying again should succeed. - CBDeleteIssuer{"root-a"}, - CBGenerateRoot{ - Key: "key-root", - Existing: true, - Name: "root-g", - CommonName: "root", - }, - CBIssueLeaf{Issuer: "root-b"}, - CBIssueLeaf{Issuer: "root-c"}, - CBIssueLeaf{Issuer: "root-d"}, - CBIssueLeaf{Issuer: "root-e"}, - CBIssueLeaf{Issuer: "root-f"}, - CBIssueLeaf{Issuer: "root-g"}, - }, - }, - { - // There's one more pathological case here: we have a cycle - // which validates a clique/cycle via cross-signing. We call - // the parent cycle new roots and the child cycle/clique the - // old roots. - Steps: []CBTestStep{ - // New Cycle - CBGenerateRoot{ - Key: "key-root-new-a", - Name: "root-new-a", - }, - CBGenerateRoot{ - Key: "key-root-new-b", - Name: "root-new-b", - }, - CBGenerateIntermediate{ - Key: "key-root-new-b", - Existing: true, - Name: "cross-root-new-b-sig-a", - CommonName: "root-new-b", - Parent: "root-new-a", - }, - CBGenerateIntermediate{ - Key: "key-root-new-a", - Existing: true, - Name: "cross-root-new-a-sig-b", - CommonName: "root-new-a", - Parent: "root-new-b", - }, - // Old Cycle + Clique - CBGenerateRoot{ - Key: "key-root-old-a", - Name: "root-old-a", - }, - CBGenerateRoot{ - Key: "key-root-old-a", - Existing: true, - Name: "root-old-a-reissued", - CommonName: "root-old-a", - }, - CBGenerateRoot{ - Key: "key-root-old-b", - Name: "root-old-b", - }, - CBGenerateRoot{ - Key: "key-root-old-b", - Existing: true, - Name: "root-old-b-reissued", - CommonName: "root-old-b", - }, - CBGenerateIntermediate{ - Key: "key-root-old-b", - Existing: true, - Name: "cross-root-old-b-sig-a", - CommonName: "root-old-b", - Parent: "root-old-a", - }, - CBGenerateIntermediate{ - Key: "key-root-old-a", - Existing: true, - Name: "cross-root-old-a-sig-b", - CommonName: "root-old-a", - Parent: "root-old-b", - }, - // Validate the chains are separate before linking them. - CBValidateChain{ - Chains: map[string][]string{ - // New stuff - "root-new-a": {"self", "cross-root-new-a-sig-b", "root-new-b-or-cross", "root-new-b-or-cross"}, - "root-new-b": {"self", "cross-root-new-b-sig-a", "root-new-a-or-cross", "root-new-a-or-cross"}, - "cross-root-new-b-sig-a": {"self", "any-root-new", "any-root-new", "any-root-new"}, - "cross-root-new-a-sig-b": {"self", "any-root-new", "any-root-new", "any-root-new"}, - - // Old stuff - "root-old-a": {"self", "root-old-a-reissued", "cross-root-old-a-sig-b", "cross-root-old-b-sig-a", "both-root-old-b", "both-root-old-b"}, - "root-old-a-reissued": {"self", "root-old-a", "cross-root-old-a-sig-b", "cross-root-old-b-sig-a", "both-root-old-b", "both-root-old-b"}, - "root-old-b": {"self", "root-old-b-reissued", "cross-root-old-b-sig-a", "cross-root-old-a-sig-b", "both-root-old-a", "both-root-old-a"}, - "root-old-b-reissued": {"self", "root-old-b", "cross-root-old-b-sig-a", "cross-root-old-a-sig-b", "both-root-old-a", "both-root-old-a"}, - "cross-root-old-b-sig-a": {"self", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "all-root-old"}, - "cross-root-old-a-sig-b": {"self", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "all-root-old"}, - }, - Aliases: map[string]string{ - "root-new-a-or-cross": "root-new-a,cross-root-new-a-sig-b", - "root-new-b-or-cross": "root-new-b,cross-root-new-b-sig-a", - "both-root-new": "root-new-a,root-new-b", - "any-root-new": "root-new-a,cross-root-new-a-sig-b,root-new-b,cross-root-new-b-sig-a", - "both-root-old-a": "root-old-a,root-old-a-reissued", - "both-root-old-b": "root-old-b,root-old-b-reissued", - "all-root-old": "root-old-a,root-old-a-reissued,root-old-b,root-old-b-reissued,cross-root-old-b-sig-a,cross-root-old-a-sig-b", - }, - }, - // Finally, generate an intermediate to link new->old. We - // link root-new-a into root-old-a. - CBGenerateIntermediate{ - Key: "key-root-old-a", - Existing: true, - Name: "cross-root-old-a-sig-root-new-a", - CommonName: "root-old-a", - Parent: "root-new-a", - }, - CBValidateChain{ - Chains: map[string][]string{ - // New stuff should be unchanged. - "root-new-a": {"self", "cross-root-new-a-sig-b", "root-new-b-or-cross", "root-new-b-or-cross"}, - "root-new-b": {"self", "cross-root-new-b-sig-a", "root-new-a-or-cross", "root-new-a-or-cross"}, - "cross-root-new-b-sig-a": {"self", "any-root-new", "any-root-new", "any-root-new"}, - "cross-root-new-a-sig-b": {"self", "any-root-new", "any-root-new", "any-root-new"}, - - // Old stuff - "root-old-a": {"self", "root-old-a-reissued", "cross-root-old-a-sig-b", "cross-root-old-b-sig-a", "both-root-old-b", "both-root-old-b", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"}, - "root-old-a-reissued": {"self", "root-old-a", "cross-root-old-a-sig-b", "cross-root-old-b-sig-a", "both-root-old-b", "both-root-old-b", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"}, - "root-old-b": {"self", "root-old-b-reissued", "cross-root-old-b-sig-a", "cross-root-old-a-sig-b", "both-root-old-a", "both-root-old-a", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"}, - "root-old-b-reissued": {"self", "root-old-b", "cross-root-old-b-sig-a", "cross-root-old-a-sig-b", "both-root-old-a", "both-root-old-a", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"}, - "cross-root-old-b-sig-a": {"self", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"}, - "cross-root-old-a-sig-b": {"self", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"}, - - // Link - "cross-root-old-a-sig-root-new-a": {"self", "root-new-a-or-cross", "any-root-new", "any-root-new", "any-root-new"}, - }, - Aliases: map[string]string{ - "root-new-a-or-cross": "root-new-a,cross-root-new-a-sig-b", - "root-new-b-or-cross": "root-new-b,cross-root-new-b-sig-a", - "both-root-new": "root-new-a,root-new-b", - "any-root-new": "root-new-a,cross-root-new-a-sig-b,root-new-b,cross-root-new-b-sig-a", - "both-root-old-a": "root-old-a,root-old-a-reissued", - "both-root-old-b": "root-old-b,root-old-b-reissued", - "all-root-old": "root-old-a,root-old-a-reissued,root-old-b,root-old-b-reissued,cross-root-old-b-sig-a,cross-root-old-a-sig-b", - }, - }, - CBIssueLeaf{Issuer: "root-new-a"}, - CBIssueLeaf{Issuer: "root-new-b"}, - CBIssueLeaf{Issuer: "cross-root-new-b-sig-a"}, - CBIssueLeaf{Issuer: "cross-root-new-a-sig-b"}, - CBIssueLeaf{Issuer: "root-old-a"}, - CBIssueLeaf{Issuer: "root-old-a-reissued"}, - CBIssueLeaf{Issuer: "root-old-b"}, - CBIssueLeaf{Issuer: "root-old-b-reissued"}, - CBIssueLeaf{Issuer: "cross-root-old-b-sig-a"}, - CBIssueLeaf{Issuer: "cross-root-old-a-sig-b"}, - CBIssueLeaf{Issuer: "cross-root-old-a-sig-root-new-a"}, - }, - }, - { - // Test a dual-root of trust chaining example with different - // lengths of chains. - Steps: []CBTestStep{ - CBGenerateRoot{ - Key: "key-root-new", - Name: "root-new", - }, - CBGenerateIntermediate{ - Key: "key-inter-new", - Name: "inter-new", - Parent: "root-new", - }, - CBGenerateRoot{ - Key: "key-root-old", - Name: "root-old", - }, - CBGenerateIntermediate{ - Key: "key-inter-old-a", - Name: "inter-old-a", - Parent: "root-old", - }, - CBGenerateIntermediate{ - Key: "key-inter-old-b", - Name: "inter-old-b", - Parent: "inter-old-a", - }, - // Now generate a cross-signed intermediate to merge these - // two chains. - CBGenerateIntermediate{ - Key: "key-cross-old-new", - Name: "cross-old-new-signed-new", - CommonName: "cross-old-new", - Parent: "inter-new", - }, - CBGenerateIntermediate{ - Key: "key-cross-old-new", - Existing: true, - Name: "cross-old-new-signed-old", - CommonName: "cross-old-new", - Parent: "inter-old-b", - }, - CBGenerateIntermediate{ - Key: "key-leaf-inter", - Name: "leaf-inter", - Parent: "cross-old-new-signed-new", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-new": {"self"}, - "inter-new": {"self", "root-new"}, - "cross-old-new-signed-new": {"self", "inter-new", "root-new"}, - "root-old": {"self"}, - "inter-old-a": {"self", "root-old"}, - "inter-old-b": {"self", "inter-old-a", "root-old"}, - "cross-old-new-signed-old": {"self", "inter-old-b", "inter-old-a", "root-old"}, - "leaf-inter": {"self", "either-cross", "one-intermediate", "other-inter-or-root", "everything-else", "everything-else", "everything-else", "everything-else"}, - }, - Aliases: map[string]string{ - "either-cross": "cross-old-new-signed-new,cross-old-new-signed-old", - "one-intermediate": "inter-new,inter-old-b", - "other-inter-or-root": "root-new,inter-old-a", - "everything-else": "cross-old-new-signed-new,cross-old-new-signed-old,inter-new,inter-old-b,root-new,inter-old-a,root-old", - }, - }, - CBIssueLeaf{Issuer: "root-new"}, - CBIssueLeaf{Issuer: "inter-new"}, - CBIssueLeaf{Issuer: "root-old"}, - CBIssueLeaf{Issuer: "inter-old-a"}, - CBIssueLeaf{Issuer: "inter-old-b"}, - CBIssueLeaf{Issuer: "cross-old-new-signed-new"}, - CBIssueLeaf{Issuer: "cross-old-new-signed-old"}, - CBIssueLeaf{Issuer: "leaf-inter"}, - }, - }, - { - // Test just a single root. - Steps: []CBTestStep{ - CBGenerateRoot{ - Key: "key-root", - Name: "root", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root": {"self"}, - }, - }, - CBIssueLeaf{Issuer: "root"}, - }, - }, - { - // Test root + intermediate. - Steps: []CBTestStep{ - CBGenerateRoot{ - Key: "key-root", - Name: "root", - }, - CBGenerateIntermediate{ - Key: "key-inter", - Name: "inter", - Parent: "root", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root": {"self"}, - "inter": {"self", "root"}, - }, - }, - CBIssueLeaf{Issuer: "root"}, - CBIssueLeaf{Issuer: "inter"}, - }, - }, - { - // Test root + intermediate, twice (simulating rotation without - // chaining). - Steps: []CBTestStep{ - CBGenerateRoot{ - Key: "key-root-a", - Name: "root-a", - }, - CBGenerateIntermediate{ - Key: "key-inter-a", - Name: "inter-a", - Parent: "root-a", - }, - CBGenerateRoot{ - Key: "key-root-b", - Name: "root-b", - }, - CBGenerateIntermediate{ - Key: "key-inter-b", - Name: "inter-b", - Parent: "root-b", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-a": {"self"}, - "inter-a": {"self", "root-a"}, - "root-b": {"self"}, - "inter-b": {"self", "root-b"}, - }, - }, - CBIssueLeaf{Issuer: "root-a"}, - CBIssueLeaf{Issuer: "inter-a"}, - CBIssueLeaf{Issuer: "root-b"}, - CBIssueLeaf{Issuer: "inter-b"}, - }, - }, - { - // Test root + intermediate, twice, chained a->b. - Steps: []CBTestStep{ - CBGenerateRoot{ - Key: "key-root-a", - Name: "root-a", - }, - CBGenerateIntermediate{ - Key: "key-inter-a", - Name: "inter-a", - Parent: "root-a", - }, - CBGenerateRoot{ - Key: "key-root-b", - Name: "root-b", - }, - CBGenerateIntermediate{ - Key: "key-inter-b", - Name: "inter-b", - Parent: "root-b", - }, - CBGenerateIntermediate{ - Key: "key-root-b", - Existing: true, - Name: "cross-a-b", - CommonName: "root-b", - Parent: "root-a", - }, - CBValidateChain{ - Chains: map[string][]string{ - "root-a": {"self"}, - "inter-a": {"self", "root-a"}, - "root-b": {"self", "cross-a-b", "root-a"}, - "inter-b": {"self", "root-b", "cross-a-b", "root-a"}, - "cross-a-b": {"self", "root-a"}, - }, - }, - CBIssueLeaf{Issuer: "root-a"}, - CBIssueLeaf{Issuer: "inter-a"}, - CBIssueLeaf{Issuer: "root-b"}, - CBIssueLeaf{Issuer: "inter-b"}, - CBIssueLeaf{Issuer: "cross-a-b"}, - }, - }, -} - -func Test_CAChainBuilding(t *testing.T) { - t.Parallel() - for testIndex, testCase := range chainBuildingTestCases { - b, s := CreateBackendWithStorage(t) - - knownKeys := make(map[string]string) - knownCerts := make(map[string]string) - for stepIndex, testStep := range testCase.Steps { - t.Logf("Running %v / %v", testIndex, stepIndex) - testStep.Run(t, b, s, knownKeys, knownCerts) - } - - t.Logf("Checking stable ordering of chains...") - ensureStableOrderingOfChains(t, b, s, knownKeys, knownCerts) - } -} - -func BenchmarkChainBuilding(benchies *testing.B) { - for testIndex, testCase := range chainBuildingTestCases { - name := "test-case-" + strconv.Itoa(testIndex) - benchies.Run(name, func(bench *testing.B) { - // Stop the timer as we setup the infra and certs. - bench.StopTimer() - bench.ResetTimer() - - b, s := CreateBackendWithStorage(bench) - - knownKeys := make(map[string]string) - knownCerts := make(map[string]string) - for _, testStep := range testCase.Steps { - testStep.Run(bench, b, s, knownKeys, knownCerts) - } - - // Run the benchmark. - ctx := context.Background() - sc := b.makeStorageContext(ctx, s) - bench.StartTimer() - for n := 0; n < bench.N; n++ { - sc.rebuildIssuersChains(nil) - } - }) - } -} diff --git a/builtin/logical/pki/crl_test.go b/builtin/logical/pki/crl_test.go deleted file mode 100644 index a18d4bcb4..000000000 --- a/builtin/logical/pki/crl_test.go +++ /dev/null @@ -1,1528 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "encoding/asn1" - "encoding/json" - "fmt" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/constants" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - - "github.com/hashicorp/go-secure-stdlib/parseutil" - - "github.com/stretchr/testify/require" -) - -func TestBackend_CRL_EnableDisableRoot(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - caSerial := resp.Data["serial_number"].(string) - - crlEnableDisableTestForBackend(t, b, s, []string{caSerial}) -} - -func TestBackend_CRLConfigUpdate(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // Write a legacy config to storage. - type legacyConfig struct { - Expiry string `json:"expiry"` - Disable bool `json:"disable"` - } - oldConfig := legacyConfig{Expiry: "24h", Disable: false} - entry, err := logical.StorageEntryJSON("config/crl", oldConfig) - require.NoError(t, err, "generate storage entry objection with legacy config") - err = s.Put(ctx, entry) - require.NoError(t, err, "failed writing legacy config") - - // Now lets read it. - resp, err := CBRead(b, s, "config/crl") - requireSuccessNonNilResponse(t, resp, err) - requireFieldsSetInResp(t, resp, "disable", "expiry", "ocsp_disable", "auto_rebuild", "auto_rebuild_grace_period") - - require.Equal(t, "24h", resp.Data["expiry"]) - require.Equal(t, false, resp.Data["disable"]) - require.Equal(t, defaultCrlConfig.OcspDisable, resp.Data["ocsp_disable"]) - require.Equal(t, defaultCrlConfig.OcspExpiry, resp.Data["ocsp_expiry"]) - require.Equal(t, defaultCrlConfig.AutoRebuild, resp.Data["auto_rebuild"]) - require.Equal(t, defaultCrlConfig.AutoRebuildGracePeriod, resp.Data["auto_rebuild_grace_period"]) -} - -func TestBackend_CRLConfig(t *testing.T) { - t.Parallel() - - tests := []struct { - expiry string - disable bool - ocspDisable bool - ocspExpiry string - autoRebuild bool - autoRebuildGracePeriod string - }{ - {expiry: "24h", disable: true, ocspDisable: true, ocspExpiry: "72h", autoRebuild: false, autoRebuildGracePeriod: "36h"}, - {expiry: "16h", disable: false, ocspDisable: true, ocspExpiry: "0h", autoRebuild: true, autoRebuildGracePeriod: "1h"}, - {expiry: "8h", disable: true, ocspDisable: false, ocspExpiry: "24h", autoRebuild: false, autoRebuildGracePeriod: "24h"}, - } - for _, tc := range tests { - name := fmt.Sprintf("%s-%t-%t", tc.expiry, tc.disable, tc.ocspDisable) - t.Run(name, func(t *testing.T) { - b, s := CreateBackendWithStorage(t) - - resp, err := CBWrite(b, s, "config/crl", map[string]interface{}{ - "expiry": tc.expiry, - "disable": tc.disable, - "ocsp_disable": tc.ocspDisable, - "ocsp_expiry": tc.ocspExpiry, - "auto_rebuild": tc.autoRebuild, - "auto_rebuild_grace_period": tc.autoRebuildGracePeriod, - }) - requireSuccessNonNilResponse(t, resp, err) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/crl"), logical.UpdateOperation), resp, true) - - resp, err = CBRead(b, s, "config/crl") - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/crl"), logical.ReadOperation), resp, true) - - requireSuccessNonNilResponse(t, resp, err) - requireFieldsSetInResp(t, resp, "disable", "expiry", "ocsp_disable", "auto_rebuild", "auto_rebuild_grace_period") - - require.Equal(t, tc.expiry, resp.Data["expiry"]) - require.Equal(t, tc.disable, resp.Data["disable"]) - require.Equal(t, tc.ocspDisable, resp.Data["ocsp_disable"]) - require.Equal(t, tc.ocspExpiry, resp.Data["ocsp_expiry"]) - require.Equal(t, tc.autoRebuild, resp.Data["auto_rebuild"]) - require.Equal(t, tc.autoRebuildGracePeriod, resp.Data["auto_rebuild_grace_period"]) - }) - } - - badValueTests := []struct { - expiry string - disable string - ocspDisable string - ocspExpiry string - autoRebuild string - autoRebuildGracePeriod string - }{ - {expiry: "not a duration", disable: "true", ocspDisable: "true", ocspExpiry: "72h", autoRebuild: "true", autoRebuildGracePeriod: "1d"}, - {expiry: "16h", disable: "not a boolean", ocspDisable: "true", ocspExpiry: "72h", autoRebuild: "true", autoRebuildGracePeriod: "1d"}, - {expiry: "8h", disable: "true", ocspDisable: "not a boolean", ocspExpiry: "72h", autoRebuild: "true", autoRebuildGracePeriod: "1d"}, - {expiry: "8h", disable: "true", ocspDisable: "true", ocspExpiry: "not a duration", autoRebuild: "true", autoRebuildGracePeriod: "1d"}, - {expiry: "8h", disable: "true", ocspDisable: "true", ocspExpiry: "-1", autoRebuild: "true", autoRebuildGracePeriod: "1d"}, - {expiry: "8h", disable: "true", ocspDisable: "true", ocspExpiry: "72h", autoRebuild: "not a boolean", autoRebuildGracePeriod: "1d"}, - {expiry: "8h", disable: "true", ocspDisable: "true", ocspExpiry: "-1", autoRebuild: "true", autoRebuildGracePeriod: "not a duration"}, - } - for _, tc := range badValueTests { - name := fmt.Sprintf("bad-%s-%s-%s", tc.expiry, tc.disable, tc.ocspDisable) - t.Run(name, func(t *testing.T) { - b, s := CreateBackendWithStorage(t) - - _, err := CBWrite(b, s, "config/crl", map[string]interface{}{ - "expiry": tc.expiry, - "disable": tc.disable, - "ocsp_disable": tc.ocspDisable, - "ocsp_expiry": tc.ocspExpiry, - "auto_rebuild": tc.autoRebuild, - "auto_rebuild_grace_period": tc.autoRebuildGracePeriod, - }) - require.Error(t, err) - }) - } -} - -func TestBackend_CRL_AllKeyTypeSigAlgos(t *testing.T) { - t.Parallel() - - type testCase struct { - KeyType string - KeyBits int - SigBits int - UsePSS bool - SigAlgo string - } - - testCases := []testCase{ - {"rsa", 2048, 256, false, "SHA256WithRSA"}, - {"rsa", 2048, 384, false, "SHA384WithRSA"}, - {"rsa", 2048, 512, false, "SHA512WithRSA"}, - {"rsa", 2048, 256, true, "SHA256WithRSAPSS"}, - {"rsa", 2048, 384, true, "SHA384WithRSAPSS"}, - {"rsa", 2048, 512, true, "SHA512WithRSAPSS"}, - {"ec", 256, 256, false, "ECDSAWithSHA256"}, - {"ec", 384, 384, false, "ECDSAWithSHA384"}, - {"ec", 521, 521, false, "ECDSAWithSHA512"}, - {"ed25519", 0, 0, false, "Ed25519"}, - } - - for index, tc := range testCases { - t.Logf("tv %v", index) - b, s := CreateBackendWithStorage(t) - - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - "key_type": tc.KeyType, - "key_bits": tc.KeyBits, - "signature_bits": tc.SigBits, - "use_pss": tc.UsePSS, - }) - if err != nil { - t.Fatalf("tc %v: %v", index, err) - } - caSerial := resp.Data["serial_number"].(string) - - resp, err = CBRead(b, s, "issuer/default") - requireSuccessNonNilResponse(t, resp, err, "fetching issuer should return data") - require.Equal(t, tc.SigAlgo, resp.Data["revocation_signature_algorithm"]) - - crlEnableDisableTestForBackend(t, b, s, []string{caSerial}) - - crl := getParsedCrlFromBackend(t, b, s, "crl") - if strings.HasSuffix(tc.SigAlgo, "PSS") { - algo := crl.SignatureAlgorithm - pssOid := asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} - if !algo.Algorithm.Equal(pssOid) { - t.Fatalf("tc %v failed: expected sig-alg to be %v / got %v", index, pssOid, algo) - } - } - } -} - -func TestBackend_CRL_EnableDisableIntermediateWithRoot(t *testing.T) { - t.Parallel() - crlEnableDisableIntermediateTestForBackend(t, true) -} - -func TestBackend_CRL_EnableDisableIntermediateWithoutRoot(t *testing.T) { - t.Parallel() - crlEnableDisableIntermediateTestForBackend(t, false) -} - -func crlEnableDisableIntermediateTestForBackend(t *testing.T, withRoot bool) { - b_root, s_root := CreateBackendWithStorage(t) - - resp, err := CBWrite(b_root, s_root, "root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - if err != nil { - t.Fatal(err) - } - rootSerial := resp.Data["serial_number"].(string) - - b_int, s_int := CreateBackendWithStorage(t) - - resp, err = CBWrite(b_int, s_int, "intermediate/generate/internal", map[string]interface{}{ - "common_name": "intermediate myvault.com", - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected intermediate CSR info") - } - intermediateData := resp.Data - - resp, err = CBWrite(b_root, s_root, "root/sign-intermediate", map[string]interface{}{ - "ttl": "30h", - "csr": intermediateData["csr"], - }) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected signed intermediate info") - } - intermediateSignedData := resp.Data - certs := intermediateSignedData["certificate"].(string) - caSerial := intermediateSignedData["serial_number"].(string) - caSerials := []string{caSerial} - if withRoot { - intermediateAndRootCert := intermediateSignedData["ca_chain"].([]string) - certs = strings.Join(intermediateAndRootCert, "\n") - caSerials = append(caSerials, rootSerial) - } - - _, err = CBWrite(b_int, s_int, "intermediate/set-signed", map[string]interface{}{ - "certificate": certs, - }) - if err != nil { - t.Fatal(err) - } - crlEnableDisableTestForBackend(t, b_int, s_int, caSerials) -} - -func crlEnableDisableTestForBackend(t *testing.T, b *backend, s logical.Storage, caSerials []string) { - var err error - - _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ - "allow_bare_domains": true, - "allow_subdomains": true, - "allowed_domains": "foobar.com", - "generate_lease": true, - }) - if err != nil { - t.Fatal(err) - } - - serials := make(map[int]string) - for i := 0; i < 6; i++ { - resp, err := CBWrite(b, s, "issue/test", map[string]interface{}{ - "common_name": "test.foobar.com", - }) - if err != nil { - t.Fatal(err) - } - serials[i] = resp.Data["serial_number"].(string) - } - - test := func(numRevokedExpected int, expectedSerials ...string) { - certList := getParsedCrlFromBackend(t, b, s, "crl").TBSCertList - lenList := len(certList.RevokedCertificates) - if lenList != numRevokedExpected { - t.Fatalf("expected %d revoked certificates, found %d", numRevokedExpected, lenList) - } - - for _, serialNum := range expectedSerials { - requireSerialNumberInCRL(t, certList, serialNum) - } - - if len(certList.Extensions) > 2 { - t.Fatalf("expected up to 2 extensions on main CRL but got %v", len(certList.Extensions)) - } - - // Since this test assumes a complete CRL was rebuilt, we can grab - // the delta CRL and ensure it is empty. - deltaList := getParsedCrlFromBackend(t, b, s, "crl/delta").TBSCertList - lenDeltaList := len(deltaList.RevokedCertificates) - if lenDeltaList != 0 { - t.Fatalf("expected zero revoked certificates on the delta CRL due to complete CRL rebuild, found %d", lenDeltaList) - } - - if len(deltaList.Extensions) != len(certList.Extensions)+1 { - t.Fatalf("expected one more extensions on delta CRL than main but got %v on main vs %v on delta", len(certList.Extensions), len(deltaList.Extensions)) - } - } - - revoke := func(serialIndex int) { - _, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": serials[serialIndex], - }) - if err != nil { - t.Fatal(err) - } - - for _, caSerial := range caSerials { - _, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": caSerial, - }) - if err == nil { - t.Fatal("expected error") - } - } - } - - toggle := func(disabled bool) { - _, err = CBWrite(b, s, "config/crl", map[string]interface{}{ - "disable": disabled, - }) - if err != nil { - t.Fatal(err) - } - } - - test(0) - revoke(0) - revoke(1) - test(2, serials[0], serials[1]) - toggle(true) - test(0) - revoke(2) - revoke(3) - test(0) - toggle(false) - test(4, serials[0], serials[1], serials[2], serials[3]) - revoke(4) - revoke(5) - test(6) - toggle(true) - test(0) - toggle(false) - test(6) - - // The rotate command should reset the update time of the CRL. - crlCreationTime1 := getParsedCrlFromBackend(t, b, s, "crl").TBSCertList.ThisUpdate - time.Sleep(1 * time.Second) - _, err = CBRead(b, s, "crl/rotate") - require.NoError(t, err) - - crlCreationTime2 := getParsedCrlFromBackend(t, b, s, "crl").TBSCertList.ThisUpdate - require.NotEqual(t, crlCreationTime1, crlCreationTime2) -} - -func TestBackend_Secondary_CRL_Rebuilding(t *testing.T) { - t.Parallel() - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Write out the issuer/key to storage without going through the api call as replication would. - bundle := genCertBundle(t, b, s) - issuer, _, err := sc.writeCaBundle(bundle, "", "") - require.NoError(t, err) - - // Just to validate, before we call the invalidate function, make sure our CRL has not been generated - // and we get a nil response - resp := requestCrlFromBackend(t, s, b) - require.Nil(t, resp.Data["http_raw_body"]) - - // This should force any calls from now on to rebuild our CRL even a read - b.invalidate(ctx, issuerPrefix+issuer.ID.String()) - - // Perform the read operation again, we should have a valid CRL now... - resp = requestCrlFromBackend(t, s, b) - crl := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte)) - require.Equal(t, 0, len(crl.RevokedCertificates)) -} - -func TestCrlRebuilder(t *testing.T) { - t.Parallel() - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Write out the issuer/key to storage without going through the api call as replication would. - bundle := genCertBundle(t, b, s) - _, _, err := sc.writeCaBundle(bundle, "", "") - require.NoError(t, err) - - cb := newCRLBuilder(true /* can rebuild and write CRLs */) - - // Force an initial build - warnings, err := cb.rebuild(sc, true) - require.NoError(t, err, "Failed to rebuild CRL") - require.Empty(t, warnings, "unexpectedly got warnings rebuilding CRL") - - resp := requestCrlFromBackend(t, s, b) - crl1 := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte)) - - // We shouldn't rebuild within this call. - warnings, err = cb.rebuildIfForced(sc) - require.NoError(t, err, "Failed to rebuild if forced CRL") - require.Empty(t, warnings, "unexpectedly got warnings rebuilding CRL") - - resp = requestCrlFromBackend(t, s, b) - crl2 := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte)) - require.Equal(t, crl1.ThisUpdate, crl2.ThisUpdate, "According to the update field, we rebuilt the CRL") - - // Make sure we have ticked over to the next second - for { - diff := time.Since(crl1.ThisUpdate) - if diff.Seconds() >= 1 { - break - } - time.Sleep(100 * time.Millisecond) - } - - // This should rebuild the CRL - cb.requestRebuildIfActiveNode(b) - warnings, err = cb.rebuildIfForced(sc) - require.NoError(t, err, "Failed to rebuild if forced CRL") - require.Empty(t, warnings, "unexpectedly got warnings rebuilding CRL") - resp = requestCrlFromBackend(t, s, b) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("crl/pem"), logical.ReadOperation), resp, true) - - crl3 := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte)) - require.True(t, crl1.ThisUpdate.Before(crl3.ThisUpdate), - "initial crl time: %#v not before next crl rebuild time: %#v", crl1.ThisUpdate, crl3.ThisUpdate) -} - -func TestBYOC(t *testing.T) { - t.Parallel() - - b, s := CreateBackendWithStorage(t) - - // Create a root CA. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root example.com", - "issuer_name": "root", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - oldRoot := resp.Data["certificate"].(string) - - // Create a role for issuance. - _, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - "ttl": "75s", - "no_store": "true", - }) - require.NoError(t, err) - - // Issue a leaf cert and ensure we can revoke it. - resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - - _, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "certificate": resp.Data["certificate"], - }) - require.NoError(t, err) - - // Issue a second leaf, but hold onto it for now. - resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing2", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - notStoredCert := resp.Data["certificate"].(string) - - // Update the role to make things stored and issue another cert. - _, err = CBWrite(b, s, "roles/stored-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - "ttl": "75s", - "no_store": "false", - }) - require.NoError(t, err) - - // Issue a leaf cert and ensure we can revoke it. - resp, err = CBWrite(b, s, "issue/stored-testing", map[string]interface{}{ - "common_name": "testing", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - storedCert := resp.Data["certificate"].(string) - - // Delete the root and regenerate a new one. - _, err = CBDelete(b, s, "issuer/default") - require.NoError(t, err) - - resp, err = CBList(b, s, "issuers") - require.NoError(t, err) - require.Equal(t, len(resp.Data), 0) - - _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root2 example.com", - "issuer_name": "root2", - "key_type": "ec", - }) - require.NoError(t, err) - - // Issue a new leaf and revoke that one. - resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing3", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - - _, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "certificate": resp.Data["certificate"], - }) - require.NoError(t, err) - - // Now attempt to revoke the earlier leaves. The first should fail since - // we deleted its issuer, but the stored one should succeed. - _, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "certificate": notStoredCert, - }) - require.Error(t, err) - - _, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "certificate": storedCert, - }) - require.NoError(t, err) - - // Import the old root again and revoke the no stored leaf should work. - _, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": oldRoot, - }) - require.NoError(t, err) - - _, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "certificate": notStoredCert, - }) - require.NoError(t, err) -} - -func TestPoP(t *testing.T) { - t.Parallel() - - b, s := CreateBackendWithStorage(t) - - // Create a root CA. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root example.com", - "issuer_name": "root", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - oldRoot := resp.Data["certificate"].(string) - - // Create a role for issuance. - _, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - "ttl": "75s", - "no_store": "true", - }) - require.NoError(t, err) - - // Issue a leaf cert and ensure we can revoke it with the private key and - // an explicit certificate. - resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing1", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - - resp, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{ - "certificate": resp.Data["certificate"], - "private_key": resp.Data["private_key"], - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("revoke-with-key"), logical.UpdateOperation), resp, true) - require.NoError(t, err) - - // Issue a second leaf, but hold onto it for now. - resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing2", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - notStoredCert := resp.Data["certificate"].(string) - notStoredKey := resp.Data["private_key"].(string) - - // Update the role to make things stored and issue another cert. - _, err = CBWrite(b, s, "roles/stored-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - "ttl": "75s", - "no_store": "false", - }) - require.NoError(t, err) - - // Issue a leaf and ensure we can revoke it via serial number and private key. - resp, err = CBWrite(b, s, "issue/stored-testing", map[string]interface{}{ - "common_name": "testing3", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - require.NotEmpty(t, resp.Data["serial_number"]) - require.NotEmpty(t, resp.Data["private_key"]) - - _, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{ - "serial_number": resp.Data["serial_number"], - "private_key": resp.Data["private_key"], - }) - require.NoError(t, err) - - // Issue a leaf cert and ensure we can revoke it after removing its root; - // hold onto it for now. - resp, err = CBWrite(b, s, "issue/stored-testing", map[string]interface{}{ - "common_name": "testing4", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - storedCert := resp.Data["certificate"].(string) - storedKey := resp.Data["private_key"].(string) - - // Delete the root and regenerate a new one. - _, err = CBDelete(b, s, "issuer/default") - require.NoError(t, err) - - resp, err = CBList(b, s, "issuers") - require.NoError(t, err) - require.Equal(t, len(resp.Data), 0) - - _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root2 example.com", - "issuer_name": "root2", - "key_type": "ec", - }) - require.NoError(t, err) - - // Issue a new leaf and revoke that one. - resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing5", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - - _, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{ - "certificate": resp.Data["certificate"], - "private_key": resp.Data["private_key"], - }) - require.NoError(t, err) - - // Now attempt to revoke the earlier leaves. The first should fail since - // we deleted its issuer, but the stored one should succeed. - _, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{ - "certificate": notStoredCert, - "private_key": notStoredKey, - }) - require.Error(t, err) - - // Incorrect combination (stored with not stored key) should fail. - _, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{ - "certificate": storedCert, - "private_key": notStoredKey, - }) - require.Error(t, err) - - // Correct combination (stored with stored) should succeed. - _, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{ - "certificate": storedCert, - "private_key": storedKey, - }) - require.NoError(t, err) - - // Import the old root again and revoke the no stored leaf should work. - _, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": oldRoot, - }) - require.NoError(t, err) - - // Incorrect combination (not stored with stored key) should fail. - _, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{ - "certificate": notStoredCert, - "private_key": storedKey, - }) - require.Error(t, err) - - // Correct combination (not stored with not stored) should succeed. - _, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{ - "certificate": notStoredCert, - "private_key": notStoredKey, - }) - require.NoError(t, err) -} - -func TestIssuerRevocation(t *testing.T) { - t.Parallel() - - b, s := CreateBackendWithStorage(t) - - // Write a config with auto-rebuilding so that we can verify stuff doesn't - // appear on the delta CRL. - _, err := CBWrite(b, s, "config/crl", map[string]interface{}{ - "auto_rebuild": true, - "enable_delta": true, - }) - require.NoError(t, err) - - // Create a root CA. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root example.com", - "issuer_name": "root", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - require.NotEmpty(t, resp.Data["serial_number"]) - // oldRoot := resp.Data["certificate"].(string) - oldRootSerial := resp.Data["serial_number"].(string) - - // Create a second root CA. We'll revoke this one and ensure it - // doesn't appear on the former's CRL. - resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root2 example.com", - "issuer_name": "root2", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - require.NotEmpty(t, resp.Data["serial_number"]) - // revokedRoot := resp.Data["certificate"].(string) - revokedRootSerial := resp.Data["serial_number"].(string) - - // Shouldn't be able to revoke it by serial number. - _, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": revokedRootSerial, - }) - require.Error(t, err) - - // Revoke it. - resp, err = CBWrite(b, s, "issuer/root2/revoke", map[string]interface{}{}) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuer/root2/revoke"), logical.UpdateOperation), resp, true) - - require.NoError(t, err) - require.NotNil(t, resp) - require.NotZero(t, resp.Data["revocation_time"]) - - // Regenerate the CRLs - resp, err = CBRead(b, s, "crl/rotate") - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("crl/rotate"), logical.ReadOperation), resp, true) - - require.NoError(t, err) - - // Ensure the old cert isn't on its own CRL. - crl := getParsedCrlFromBackend(t, b, s, "issuer/root2/crl/der") - if requireSerialNumberInCRL(nil, crl.TBSCertList, revokedRootSerial) { - t.Fatalf("the serial number %v should not be on its own CRL as self-CRL appearance should not occur", revokedRootSerial) - } - - // Ensure the old cert isn't on the one's CRL. - crl = getParsedCrlFromBackend(t, b, s, "issuer/root/crl/der") - if requireSerialNumberInCRL(nil, crl.TBSCertList, revokedRootSerial) { - t.Fatalf("the serial number %v should not be on %v's CRL as they're separate roots", revokedRootSerial, oldRootSerial) - } - - // Create a role and ensure we can't use the revoked root. - _, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - "ttl": "75s", - }) - require.NoError(t, err) - - // Issue a leaf cert and ensure it fails (because the issuer is revoked). - resp, err = CBWrite(b, s, "issuer/root2/issue/local-testing", map[string]interface{}{ - "common_name": "testing", - }) - require.Error(t, err) - - // Issue an intermediate and ensure we can revoke it. - resp, err = CBWrite(b, s, "intermediate/generate/internal", map[string]interface{}{ - "common_name": "intermediate example.com", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["csr"]) - intCsr := resp.Data["csr"].(string) - resp, err = CBWrite(b, s, "root/sign-intermediate", map[string]interface{}{ - "ttl": "30h", - "csr": intCsr, - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - require.NotEmpty(t, resp.Data["serial_number"]) - intCert := resp.Data["certificate"].(string) - intCertSerial := resp.Data["serial_number"].(string) - resp, err = CBWrite(b, s, "intermediate/set-signed", map[string]interface{}{ - "certificate": intCert, - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("intermediate/set-signed"), logical.UpdateOperation), resp, true) - - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["imported_issuers"]) - importedIssuers := resp.Data["imported_issuers"].([]string) - require.Equal(t, len(importedIssuers), 1) - intId := importedIssuers[0] - _, err = CBPatch(b, s, "issuer/"+intId, map[string]interface{}{ - "issuer_name": "int1", - }) - require.NoError(t, err) - - // Now issue a leaf with the intermediate. - resp, err = CBWrite(b, s, "issuer/int1/issue/local-testing", map[string]interface{}{ - "common_name": "testing", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuer/int1/issue/local-testing"), logical.UpdateOperation), resp, true) - - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - require.NotEmpty(t, resp.Data["serial_number"]) - issuedSerial := resp.Data["serial_number"].(string) - - // Now revoke the intermediate. - resp, err = CBWrite(b, s, "issuer/int1/revoke", map[string]interface{}{}) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotZero(t, resp.Data["revocation_time"]) - - // Update the CRLs and ensure it appears. - _, err = CBRead(b, s, "crl/rotate") - require.NoError(t, err) - crl = getParsedCrlFromBackend(t, b, s, "issuer/root/crl/der") - requireSerialNumberInCRL(t, crl.TBSCertList, intCertSerial) - crl = getParsedCrlFromBackend(t, b, s, "issuer/root/crl/delta/der") - if requireSerialNumberInCRL(nil, crl.TBSCertList, intCertSerial) { - t.Fatalf("expected intermediate serial NOT to appear on root's delta CRL, but did") - } - - // Ensure we can still revoke the issued leaf. - resp, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": issuedSerial, - }) - require.NoError(t, err) - require.NotNil(t, resp) - - // Ensure it appears on the intermediate's CRL. - _, err = CBRead(b, s, "crl/rotate") - require.NoError(t, err) - crl = getParsedCrlFromBackend(t, b, s, "issuer/int1/crl/der") - requireSerialNumberInCRL(t, crl.TBSCertList, issuedSerial) - - // Ensure we can't fetch the intermediate's cert by serial any more. - resp, err = CBRead(b, s, "cert/"+intCertSerial) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["revocation_time"]) -} - -func TestAutoRebuild(t *testing.T) { - t.Parallel() - - // While we'd like to reduce this duration, we need to wait until - // the rollback manager timer ticks. With the new helper, we can - // modify the rollback manager timer period directly, allowing us - // to shorten the total test time significantly. - // - // We set the delta CRL time to ensure it executes prior to the - // main CRL rebuild, and the new CRL doesn't rebuild until after - // we're done. - newPeriod := 1 * time.Second - deltaPeriod := (newPeriod + 1*time.Second).String() - crlTime := (6*newPeriod + 2*time.Second).String() - gracePeriod := (3 * newPeriod).String() - delta := 2 * newPeriod - - // This test requires the periodicFunc to trigger, which requires we stand - // up a full test cluster. - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - // See notes below about usage of /sys/raw for reading cluster - // storage without barrier encryption. - EnableRaw: true, - RollbackPeriod: newPeriod, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Mount PKI - err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }) - require.NoError(t, err) - - // Generate root. - resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "Root X1", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data) - require.NotEmpty(t, resp.Data["issuer_id"]) - rootIssuer := resp.Data["issuer_id"].(string) - - // Setup a testing role. - _, err = client.Logical().Write("pki/roles/local-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - }) - require.NoError(t, err) - - // Regression test: ensure we respond with the default values for CRL - // config when we haven't set any values yet. - resp, err = client.Logical().Read("pki/config/crl") - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.Equal(t, resp.Data["expiry"], defaultCrlConfig.Expiry) - require.Equal(t, resp.Data["disable"], defaultCrlConfig.Disable) - require.Equal(t, resp.Data["ocsp_disable"], defaultCrlConfig.OcspDisable) - require.Equal(t, resp.Data["auto_rebuild"], defaultCrlConfig.AutoRebuild) - require.Equal(t, resp.Data["auto_rebuild_grace_period"], defaultCrlConfig.AutoRebuildGracePeriod) - require.Equal(t, resp.Data["enable_delta"], defaultCrlConfig.EnableDelta) - require.Equal(t, resp.Data["delta_rebuild_interval"], defaultCrlConfig.DeltaRebuildInterval) - - // Safety guard: we play with rebuild timing below. - _, err = client.Logical().Write("pki/config/crl", map[string]interface{}{ - "expiry": crlTime, - }) - require.NoError(t, err) - - // Issue a cert and revoke it. It should appear on the CRL right away. - resp, err = client.Logical().Write("pki/issue/local-testing", map[string]interface{}{ - "common_name": "example.com", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["serial_number"]) - leafSerial := resp.Data["serial_number"].(string) - - _, err = client.Logical().Write("pki/revoke", map[string]interface{}{ - "serial_number": leafSerial, - }) - require.NoError(t, err) - - defaultCrlPath := "/v1/pki/crl" - crl := getParsedCrlAtPath(t, client, defaultCrlPath).TBSCertList - lastCRLNumber := getCRLNumber(t, crl) - lastCRLExpiry := crl.NextUpdate - requireSerialNumberInCRL(t, crl, leafSerial) - - // Enable periodic rebuild of the CRL. - _, err = client.Logical().Write("pki/config/crl", map[string]interface{}{ - "expiry": crlTime, - "auto_rebuild": true, - "auto_rebuild_grace_period": gracePeriod, - "enable_delta": true, - "delta_rebuild_interval": deltaPeriod, - }) - require.NoError(t, err) - - // Wait for the CRL to update based on the configuration change we just did - // so that it doesn't grab the revocation we are going to do afterwards. - crl = waitForUpdatedCrl(t, client, defaultCrlPath, lastCRLNumber, lastCRLExpiry.Sub(time.Now())) - lastCRLNumber = getCRLNumber(t, crl) - lastCRLExpiry = crl.NextUpdate - - // Issue a cert and revoke it. - resp, err = client.Logical().Write("pki/issue/local-testing", map[string]interface{}{ - "common_name": "example.com", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["serial_number"]) - newLeafSerial := resp.Data["serial_number"].(string) - - _, err = client.Logical().Write("pki/revoke", map[string]interface{}{ - "serial_number": newLeafSerial, - }) - require.NoError(t, err) - - // Now, we want to test the issuer identification on revocation. This - // only happens as a distinct "step" when CRL building isn't done on - // each revocation. Pull the storage from the cluster (via the sys/raw - // endpoint which requires the mount UUID) and verify the revInfo contains - // a matching issuer. - pkiMount := findStorageMountUuid(t, client, "pki") - revEntryPath := "logical/" + pkiMount + "/" + revokedPath + normalizeSerial(newLeafSerial) - - // storage from cluster.Core[0] is a physical storage copy, not a logical - // storage. This difference means, if we were to do a storage.Get(...) - // on the above path, we'd read the barrier-encrypted value. This is less - // than useful for decoding, and fetching the proper storage view is a - // touch much work. So, assert EnableRaw above and (ab)use it here. - resp, err = client.Logical().Read("sys/raw/" + revEntryPath) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["value"]) - revEntryValue := resp.Data["value"].(string) - var revInfo revocationInfo - err = json.Unmarshal([]byte(revEntryValue), &revInfo) - require.NoError(t, err) - require.Equal(t, revInfo.CertificateIssuer, issuerID(rootIssuer)) - - // New serial should not appear on CRL. - crl = getCrlCertificateList(t, client, "pki") - thisCRLNumber := getCRLNumber(t, crl) - requireSerialNumberInCRL(t, crl, leafSerial) // But the old one should. - now := time.Now() - graceInterval, _ := parseutil.ParseDurationSecond(gracePeriod) - expectedUpdate := lastCRLExpiry.Add(-1 * graceInterval) - if requireSerialNumberInCRL(nil, crl, newLeafSerial) { - // If we somehow lagged and we ended up needing to rebuild - // the CRL, we should avoid throwing an error. - - if thisCRLNumber == lastCRLNumber { - t.Fatalf("unexpected failure: last (%v) and current (%v) leaf certificate might have the same serial number?", leafSerial, newLeafSerial) - } - - if !now.After(expectedUpdate) { - t.Fatalf("expected newly generated certificate with serial %v not to appear on this CRL but it did, prematurely: %v", newLeafSerial, crl) - } - - t.Fatalf("shouldn't be here") - } - - // This serial should exist in the delta WAL section for the mount... - resp, err = client.Logical().List("sys/raw/logical/" + pkiMount + "/" + localDeltaWALPath) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data) - require.NotEmpty(t, resp.Data["keys"]) - require.Contains(t, resp.Data["keys"], normalizeSerial(newLeafSerial)) - - haveUpdatedDeltaCRL := false - interruptChan := time.After(4*newPeriod + delta) - for { - if haveUpdatedDeltaCRL { - break - } - - select { - case <-interruptChan: - t.Fatalf("expected to regenerate delta CRL within a couple of periodicFunc invocations (plus %v grace period)", delta) - default: - // Check and see if there's a storage entry for the last rebuild - // serial. If so, validate the delta CRL contains this entry. - resp, err = client.Logical().List("sys/raw/logical/" + pkiMount + "/" + localDeltaWALPath) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data) - require.NotEmpty(t, resp.Data["keys"]) - - haveRebuildMarker := false - for _, rawEntry := range resp.Data["keys"].([]interface{}) { - entry := rawEntry.(string) - if entry == deltaWALLastRevokedSerialName { - haveRebuildMarker = true - break - } - } - - if !haveRebuildMarker { - time.Sleep(1 * time.Second) - continue - } - - // Read the marker and see if its correct. - resp, err = client.Logical().Read("sys/raw/logical/" + pkiMount + "/" + localDeltaWALLastBuildSerial) - require.NoError(t, err) - if resp == nil { - time.Sleep(1 * time.Second) - continue - } - - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data) - require.NotEmpty(t, resp.Data["value"]) - - // Easy than JSON decoding... - if !strings.Contains(resp.Data["value"].(string), newLeafSerial) { - time.Sleep(1 * time.Second) - continue - } - - haveUpdatedDeltaCRL = true - - // Ensure it has what we want. - deltaCrl := getParsedCrlAtPath(t, client, "/v1/pki/crl/delta").TBSCertList - if !requireSerialNumberInCRL(nil, deltaCrl, newLeafSerial) { - // Check if it is on the main CRL because its already regenerated. - mainCRL := getParsedCrlAtPath(t, client, defaultCrlPath).TBSCertList - requireSerialNumberInCRL(t, mainCRL, newLeafSerial) - } else { - referenceCrlNum := getCrlReferenceFromDelta(t, deltaCrl) - if lastCRLNumber < referenceCrlNum { - lastCRLNumber = referenceCrlNum - } - } - } - } - - // Now, wait until we're within the grace period... Then start prompting - // for regeneration. - if expectedUpdate.After(now) { - time.Sleep(expectedUpdate.Sub(now)) - } - - crl = waitForUpdatedCrl(t, client, defaultCrlPath, lastCRLNumber, lastCRLExpiry.Sub(now)+delta) - requireSerialNumberInCRL(t, crl, leafSerial) - requireSerialNumberInCRL(t, crl, newLeafSerial) -} - -func findStorageMountUuid(t *testing.T, client *api.Client, mount string) string { - resp, err := client.Logical().Read("sys/mounts/" + mount) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["uuid"]) - pkiMount := resp.Data["uuid"].(string) - require.NotEmpty(t, pkiMount) - return pkiMount -} - -func TestTidyIssuerAssociation(t *testing.T) { - t.Parallel() - - b, s := CreateBackendWithStorage(t) - - // Create a root CA. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root example.com", - "issuer_name": "root", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["certificate"]) - require.NotEmpty(t, resp.Data["issuer_id"]) - rootCert := resp.Data["certificate"].(string) - rootID := resp.Data["issuer_id"].(issuerID) - - // Create a role for issuance. - _, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - "ttl": "75m", - }) - require.NoError(t, err) - - // Issue a leaf cert and ensure we can revoke it. - resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data["serial_number"]) - leafSerial := resp.Data["serial_number"].(string) - - _, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": leafSerial, - }) - require.NoError(t, err) - - // This leaf's revInfo entry should have an issuer associated - // with it. - entry, err := s.Get(ctx, revokedPath+normalizeSerial(leafSerial)) - require.NoError(t, err) - require.NotNil(t, entry) - require.NotNil(t, entry.Value) - - var leafInfo revocationInfo - err = entry.DecodeJSON(&leafInfo) - require.NoError(t, err) - require.Equal(t, rootID, leafInfo.CertificateIssuer) - - // Now remove the root and run tidy. - _, err = CBDelete(b, s, "issuer/default") - require.NoError(t, err) - _, err = CBWrite(b, s, "tidy", map[string]interface{}{ - "tidy_revoked_cert_issuer_associations": true, - }) - require.NoError(t, err) - - // Wait for tidy to finish. - for { - time.Sleep(125 * time.Millisecond) - - resp, err = CBRead(b, s, "tidy-status") - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["state"]) - state := resp.Data["state"].(string) - - if state == "Finished" { - break - } - if state == "Error" { - t.Fatalf("unexpected state for tidy operation: Error:\nStatus: %v", resp.Data) - } - } - - // Ensure we don't have an association on this leaf any more. - entry, err = s.Get(ctx, revokedPath+normalizeSerial(leafSerial)) - require.NoError(t, err) - require.NotNil(t, entry) - require.NotNil(t, entry.Value) - - err = entry.DecodeJSON(&leafInfo) - require.NoError(t, err) - require.Empty(t, leafInfo.CertificateIssuer) - - // Now, re-import the root and try again. - resp, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": rootCert, - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotNil(t, resp.Data["imported_issuers"]) - importedIssuers := resp.Data["imported_issuers"].([]string) - require.Equal(t, 1, len(importedIssuers)) - newRootID := importedIssuers[0] - require.NotEmpty(t, newRootID) - - // Re-run tidy... - _, err = CBWrite(b, s, "tidy", map[string]interface{}{ - "tidy_revoked_cert_issuer_associations": true, - }) - require.NoError(t, err) - - // Wait for tidy to finish. - for { - time.Sleep(125 * time.Millisecond) - - resp, err = CBRead(b, s, "tidy-status") - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["state"]) - state := resp.Data["state"].(string) - - if state == "Finished" { - break - } - if state == "Error" { - t.Fatalf("unexpected state for tidy operation: Error:\nStatus: %v", resp.Data) - } - } - - // Finally, double-check we associated things correctly. - entry, err = s.Get(ctx, revokedPath+normalizeSerial(leafSerial)) - require.NoError(t, err) - require.NotNil(t, entry) - require.NotNil(t, entry.Value) - - err = entry.DecodeJSON(&leafInfo) - require.NoError(t, err) - require.Equal(t, newRootID, string(leafInfo.CertificateIssuer)) -} - -func requestCrlFromBackend(t *testing.T, s logical.Storage, b *backend) *logical.Response { - crlReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "crl/pem", - Storage: s, - } - resp, err := b.HandleRequest(context.Background(), crlReq) - require.NoError(t, err, "crl req failed with an error") - require.NotNil(t, resp, "crl response was nil with no error") - require.False(t, resp.IsError(), "crl error response: %v", resp) - return resp -} - -func TestCRLWarningsEmptyKeyUsage(t *testing.T) { - t.Parallel() - - b, s := CreateBackendWithStorage(t) - - // Generated using OpenSSL with a configuration lacking KeyUsage on - // the CA certificate. - cert := `-----BEGIN CERTIFICATE----- -MIIDBjCCAe6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhyb290 -LW9sZDAeFw0yMDAxMDEwMTAxMDFaFw0yMTAxMDEwMTAxMDFaMBMxETAPBgNVBAMM -CHJvb3Qtb2xkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzqhSZxAL -PwFhCIPL1jFPq6jxp1wFgo6YNSfVI13gfaGIjfErxsQUbosmlEuTeOc50zXXN3kb -SDufy5Yi1OeSkFZRdJ78zdKzsEDIVR1ukUngVsSrt05gdNMJlh8XOPbcrJo78jYG -lRgtkkFSc/wCu+ue6JqkfKrbUY/G9WK0UM8ppHm1Ux67ZGoypyEgaqqxKHBRC4Yl -D+lAs1vP4C6cavqdUMKgAPTKmMBzlbpCuYPLHSzWh9Com3WQSqCbrlo3uH5RT3V9 -5Gjuk3mMUhY1l6fRL7wG3f+4x+DS+ICQNT0o4lnMxpIsiTh0cEHUFgY7G0iHWYPj -CIN8UDhpZIpoCQIDAQABo2UwYzAdBgNVHQ4EFgQUJlHk3PN7pfC22FGxAb0rWgQt -L4cwHwYDVR0jBBgwFoAUJlHk3PN7pfC22FGxAb0rWgQtL4cwDAYDVR0TBAUwAwEB -/zATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAcaU0FbXb -FfXluBrjKfOzVKz+kvQ1CVv3xe3MBkS6wvqybBjJCFChnqCPxEe57BdSbBXNU5LZ -zCR/OqYas4Csv9+msSn9BI2FSMAmfMDTsp5/6iIQJqlJx9L8a7bjzVMGX6QJm/3x -S/EgGsMETAgewQXeu4jhI6StgJ2V/4Ofe498hYw4LAiBapJmkU/nHezWodNBZJ7h -LcLOzVj0Hu5MZplGBgJFgRqBCVVkqXA0q7tORuhNzYtNdJFpv3pZIhvVFFu3HUPf -wYQPhLye5WNtosz5xKe8X0Q9qp8g6azMTk+5Qe7u1d8MYAA2AIlGuKUvPHRruOmN -NC+gQnS7AK1lCw== ------END CERTIFICATE-----` - privKey := `-----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOqFJnEAs/AWEI -g8vWMU+rqPGnXAWCjpg1J9UjXeB9oYiN8SvGxBRuiyaUS5N45znTNdc3eRtIO5/L -liLU55KQVlF0nvzN0rOwQMhVHW6RSeBWxKu3TmB00wmWHxc49tysmjvyNgaVGC2S -QVJz/AK7657omqR8qttRj8b1YrRQzymkebVTHrtkajKnISBqqrEocFELhiUP6UCz -W8/gLpxq+p1QwqAA9MqYwHOVukK5g8sdLNaH0KibdZBKoJuuWje4flFPdX3kaO6T -eYxSFjWXp9EvvAbd/7jH4NL4gJA1PSjiWczGkiyJOHRwQdQWBjsbSIdZg+MIg3xQ -OGlkimgJAgMBAAECggEABKmCdmXDwy+eR0ll41aoc/hzPzHRxADAiU51Pf+DrYHj -6UPcF3db+KR2Adl0ocEhqlSoHs3CIk6KC9c+wOvagBwaaVWe4WvT9vF3M4he8rMm -dv6n2xJPFcOfDz5zUSssjk5KdOvoGRv7BzYnDIvOafvmUVwPwuo92Wizddy8saf4 -Xuea0Cupz1PELPKkbXcAqb+TzbAZrwdPj1Y7vTe/KGE4+aoDqCW/sFB1E0UsMGlt -/yfGwFP48b7kdkqSpcEQW5H8+WL3TfqRcolCD9To4vo2J+1Po0S/8qPNRvkNQDDX -AypHtrXFBOWHpJgXT4rKyH+ZGJchrCRDblt9s/sNQwKBgQD7NytvYET3pWemYiX+ -MB9uc6cPuMFONvlzjA9T6dbOSi/HLaeDoW027aMUZqb7QeaQCoWcUwh13dI2SZq0 -5+l9hei4JkWjoDhbWmPe7zDuQr3UMl0CSk3egz3BSHkjAhRAuUxK0QLKGB23zWxz -k8mUWYZaZRA39C6aqMt/jbJjDwKBgQDSl+eO+DjpwPzrjPSphpF4xYo4XDje9ovK -9q4KTHye7Flc3cMCX3WZBmzdt0zbqu6gWZjJH0XbWX/+SkJBGh77XWD0FeQnU7Vk -ipoeb8zTsCVxD9EytQuXti3cqBgClcCMvLKgLOJIcNYTnygojwg3t+jboQqbtV7p -VpQfAC6jZwKBgQCxJ46x1CnOmg4l/0DbqAQCV/yP0bI//fSbz0Ff459fimF3DHL9 -GHF0MtC2Kk3HEgoNud3PB58Hv43mSrGWsZSuuCgM9LBXWz1i7rNPG05eNyK26W09 -mDihmduK2hjS3zx5CDMM76gP7EHIxEyelLGqtBdS18JAMypKVo5rPPl3cQKBgQCG -ueXLImQOr4dfDntLpSqV0BLAQcekZKhEPZJURmCHr37wGXNzpixurJyjL2w9MFqf -PRKwwJAJZ3Wp8kn2qkZd23x2Szb+LeBjJQS6Kh4o44zgixTz0r1K3qLygptxs+pO -Xz4LmQte+skKHo0rfW3tb3vKXnmR6fOBZgE23//2SwKBgHck44hoE1Ex2gDEfIq1 -04OBoS1cpuc9ge4uHEmv+8uANjzwlsYf8hY1qae513MGixRBOkxcI5xX/fYPQV9F -t3Jfh8QX85JjnGntuXuraYZJMUjpwXr3QHPx0jpvAM3Au5j6qD3biC9Vrwq9Chkg -hbiiPARizZA/Tsna/9ox1qDT ------END PRIVATE KEY-----` - resp, err := CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": cert + "\n" + privKey, - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Warnings) - originalWarnings := resp.Warnings - - resp, err = CBRead(b, s, "crl/rotate") - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Warnings) - - // All CRL-specific warnings should've already occurred earlier on the - // import's CRL rebuild. - for _, warning := range resp.Warnings { - require.Contains(t, originalWarnings, warning) - } - - // Deleting the issuer and key should remove the warning. - _, err = CBDelete(b, s, "root") - require.NoError(t, err) - - resp, err = CBRead(b, s, "crl/rotate") - require.NoError(t, err) - require.NotNil(t, resp) - require.Empty(t, resp.Warnings) - - // Adding back just the cert shouldn't cause CRL rebuild warnings. - resp, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": cert, - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotNil(t, resp.Data["mapping"]) - require.NotEmpty(t, resp.Data["mapping"]) - require.Equal(t, len(resp.Data["mapping"].(map[string]string)), 1) - for key, value := range resp.Data["mapping"].(map[string]string) { - require.NotEmpty(t, key) - require.Empty(t, value) - } - - resp, err = CBRead(b, s, "crl/rotate") - require.NoError(t, err) - require.NotNil(t, resp) - require.Empty(t, resp.Warnings) -} - -func TestCRLIssuerRemoval(t *testing.T) { - t.Parallel() - - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - - if constants.IsEnterprise { - // We don't really care about the whole cross cluster replication - // stuff, but we do want to enable unified CRLs if we can, so that - // unified CRLs get built. - _, err := CBWrite(b, s, "config/crl", map[string]interface{}{ - "cross_cluster_revocation": true, - "auto_rebuild": true, - }) - require.NoError(t, err, "failed enabling unified CRLs on enterprise") - } - - // Create a single root, configure delta CRLs, and rotate CRLs to prep a - // starting state. - _, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "Root R1", - "key_type": "ec", - }) - require.NoError(t, err) - _, err = CBWrite(b, s, "config/crl", map[string]interface{}{ - "enable_delta": true, - "auto_rebuild": true, - }) - require.NoError(t, err) - _, err = CBRead(b, s, "crl/rotate") - require.NoError(t, err) - - // List items in storage under both CRL paths so we know what is there in - // the "good" state. - crlList, err := s.List(ctx, "crls/") - require.NoError(t, err) - require.Contains(t, crlList, "config") - require.Greater(t, len(crlList), 1) - - unifiedCRLList, err := s.List(ctx, "unified-crls/") - require.NoError(t, err) - require.Contains(t, unifiedCRLList, "config") - require.Greater(t, len(unifiedCRLList), 1) - - // Now, create a bunch of issuers, generate CRLs, and remove them. - var keyIDs []string - var issuerIDs []string - for i := 1; i <= 25; i++ { - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": fmt.Sprintf("Root X%v", i), - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - - key := string(resp.Data["key_id"].(keyID)) - keyIDs = append(keyIDs, key) - issuer := string(resp.Data["issuer_id"].(issuerID)) - issuerIDs = append(issuerIDs, issuer) - } - _, err = CBRead(b, s, "crl/rotate") - require.NoError(t, err) - for _, issuer := range issuerIDs { - _, err := CBDelete(b, s, "issuer/"+issuer) - require.NoError(t, err) - } - for _, key := range keyIDs { - _, err := CBDelete(b, s, "key/"+key) - require.NoError(t, err) - } - - // Finally list storage entries again to ensure they are cleaned up. - afterCRLList, err := s.List(ctx, "crls/") - require.NoError(t, err) - for _, entry := range crlList { - require.Contains(t, afterCRLList, entry) - } - require.Equal(t, len(afterCRLList), len(crlList)) - - afterUnifiedCRLList, err := s.List(ctx, "unified-crls/") - require.NoError(t, err) - for _, entry := range unifiedCRLList { - require.Contains(t, afterUnifiedCRLList, entry) - } - require.Equal(t, len(afterUnifiedCRLList), len(unifiedCRLList)) -} diff --git a/builtin/logical/pki/dnstest/server.go b/builtin/logical/pki/dnstest/server.go deleted file mode 100644 index 751c0ae87..000000000 --- a/builtin/logical/pki/dnstest/server.go +++ /dev/null @@ -1,428 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package dnstest - -import ( - "context" - "fmt" - "net" - "strings" - "sync" - "testing" - "time" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/stretchr/testify/require" -) - -type TestServer struct { - t *testing.T - ctx context.Context - log hclog.Logger - - runner *docker.Runner - network string - startup *docker.Service - - lock sync.Mutex - serial int - forwarders []string - domains []string - records map[string]map[string][]string // domain -> record -> value(s). - - cleanup func() -} - -func SetupResolver(t *testing.T, domain string) *TestServer { - return SetupResolverOnNetwork(t, domain, "") -} - -func SetupResolverOnNetwork(t *testing.T, domain string, network string) *TestServer { - var ts TestServer - ts.t = t - ts.ctx = context.Background() - ts.domains = []string{domain} - ts.records = map[string]map[string][]string{} - ts.network = network - ts.log = hclog.L() - - ts.setupRunner(domain, network) - ts.startContainer(network) - ts.PushConfig() - - return &ts -} - -func (ts *TestServer) setupRunner(domain string, network string) { - var err error - ts.runner, err = docker.NewServiceRunner(docker.RunOptions{ - ImageRepo: "ubuntu/bind9", - ImageTag: "latest", - ContainerName: "bind9-dns-" + strings.ReplaceAll(domain, ".", "-"), - NetworkName: network, - Ports: []string{"53/udp"}, - // DNS container logging was disabled to reduce content within CI logs. - //LogConsumer: func(s string) { - // ts.log.Info(s) - //}, - }) - require.NoError(ts.t, err) -} - -func (ts *TestServer) startContainer(network string) { - connUpFunc := func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - // Perform a simple connection to this resolver, even though the - // default configuration doesn't do anything useful. - peer, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host, port)) - if err != nil { - return nil, fmt.Errorf("failed to resolve peer: %v / %v: %w", host, port, err) - } - - conn, err := net.DialUDP("udp", nil, peer) - if err != nil { - return nil, fmt.Errorf("failed to dial peer: %v / %v / %v: %w", host, port, peer, err) - } - defer conn.Close() - - _, err = conn.Write([]byte("garbage-in")) - if err != nil { - return nil, fmt.Errorf("failed to write to peer: %v / %v / %v: %w", host, port, peer, err) - } - - // Connection worked. - return docker.NewServiceHostPort(host, port), nil - } - - result, _, err := ts.runner.StartNewService(ts.ctx, true, true, connUpFunc) - require.NoError(ts.t, err, "failed to start dns resolver for "+ts.domains[0]) - ts.startup = result - - if ts.startup.StartResult.RealIP == "" { - mapping, err := ts.runner.GetNetworkAndAddresses(ts.startup.Container.ID) - require.NoError(ts.t, err, "failed to fetch network addresses to correct missing real IP address") - if len(network) == 0 { - require.Equal(ts.t, 1, len(mapping), "expected exactly one network address") - for network = range mapping { - // Because mapping is a map of network name->ip, we need - // to use the above range's assignment to get the name, - // as there is no other way of getting the keys of a map. - } - } - require.Contains(ts.t, mapping, network, "expected network to be part of the mapping") - ts.startup.StartResult.RealIP = mapping[network] - } - - ts.log.Info(fmt.Sprintf("[dnsserv] Addresses of DNS resolver: local=%v / container=%v", ts.GetLocalAddr(), ts.GetRemoteAddr())) -} - -func (ts *TestServer) buildNamedConf() string { - forwarders := "\n" - if len(ts.forwarders) > 0 { - forwarders = "\tforwarders {\n" - for _, forwarder := range ts.forwarders { - forwarders += "\t\t" + forwarder + ";\n" - } - forwarders += "\t};\n" - } - - zones := "\n" - for _, domain := range ts.domains { - zones += fmt.Sprintf("zone \"%s\" {\n", domain) - zones += "\ttype primary;\n" - zones += fmt.Sprintf("\tfile \"%s.zone\";\n", domain) - zones += "\tallow-update {\n\t\tnone;\n\t};\n" - zones += "\tnotify no;\n" - zones += "};\n\n" - } - - // Reverse lookups are not handles as they're not presently necessary. - - cfg := `options { - directory "/var/cache/bind"; - - dnssec-validation no; - - ` + forwarders + ` -}; - -` + zones - - return cfg -} - -func (ts *TestServer) buildZoneFile(target string) string { - // One second TTL by default to allow quick refreshes. - zone := "$TTL 1;\n" - - ts.serial += 1 - zone += fmt.Sprintf("@\tIN\tSOA\tns.%v.\troot.%v.\t(\n", target, target) - zone += fmt.Sprintf("\t\t\t%d;\n\t\t\t1;\n\t\t\t1;\n\t\t\t2;\n\t\t\t1;\n\t\t\t)\n\n", ts.serial) - zone += fmt.Sprintf("@\tIN\tNS\tns%d.%v.\n", ts.serial, target) - zone += fmt.Sprintf("ns%d.%v.\tIN\tA\t%v\n", ts.serial, target, "127.0.0.1") - - for domain, records := range ts.records { - if !strings.HasSuffix(domain, target) { - continue - } - - for recordType, values := range records { - for _, value := range values { - zone += fmt.Sprintf("%s.\tIN\t%s\t%s\n", domain, recordType, value) - } - } - } - - return zone -} - -func (ts *TestServer) pushNamedConf() { - contents := docker.NewBuildContext() - cfgPath := "/etc/bind/named.conf.options" - namedCfg := ts.buildNamedConf() - contents[cfgPath] = docker.PathContentsFromString(namedCfg) - contents[cfgPath].SetOwners(0, 142) // root, bind - - ts.log.Info(fmt.Sprintf("Generated bind9 config (%s):\n%v\n", cfgPath, namedCfg)) - - err := ts.runner.CopyTo(ts.startup.Container.ID, "/", contents) - require.NoError(ts.t, err, "failed pushing updated named.conf.options to container") -} - -func (ts *TestServer) pushZoneFiles() { - contents := docker.NewBuildContext() - - for _, domain := range ts.domains { - path := "/var/cache/bind/" + domain + ".zone" - zoneFile := ts.buildZoneFile(domain) - contents[path] = docker.PathContentsFromString(zoneFile) - contents[path].SetOwners(0, 142) // root, bind - - ts.log.Info(fmt.Sprintf("Generated bind9 zone file for %v (%s):\n%v\n", domain, path, zoneFile)) - } - - err := ts.runner.CopyTo(ts.startup.Container.ID, "/", contents) - require.NoError(ts.t, err, "failed pushing updated named.conf.options to container") -} - -func (ts *TestServer) PushConfig() { - ts.lock.Lock() - defer ts.lock.Unlock() - - _, _, _, err := ts.runner.RunCmdWithOutput(ts.ctx, ts.startup.Container.ID, []string{"rndc", "freeze"}) - require.NoError(ts.t, err, "failed to freeze DNS config") - - // There's two cases here: - // - // 1. We've added a new top-level domain name. Here, we want to make - // sure the new zone file is pushed before we push the reference - // to it. - // 2. We've just added a new. Here, the order doesn't matter, but - // mostly likely the second push will be a no-op. - ts.pushZoneFiles() - ts.pushNamedConf() - - _, _, _, err = ts.runner.RunCmdWithOutput(ts.ctx, ts.startup.Container.ID, []string{"rndc", "thaw"}) - require.NoError(ts.t, err, "failed to thaw DNS config") - - // Wait until our config has taken. - corehelpers.RetryUntil(ts.t, 15*time.Second, func() error { - // bind reloads based on file mtime, touch files before starting - // to make sure it has been updated more recently than when the - // last update was written. Then issue a new SIGHUP. - for _, domain := range ts.domains { - path := "/var/cache/bind/" + domain + ".zone" - touchCmd := []string{"touch", path} - - _, _, _, err := ts.runner.RunCmdWithOutput(ts.ctx, ts.startup.Container.ID, touchCmd) - if err != nil { - return fmt.Errorf("failed to update zone mtime: %w", err) - } - } - ts.runner.DockerAPI.ContainerKill(ts.ctx, ts.startup.Container.ID, "SIGHUP") - - // Connect to our bind resolver. - resolver := &net.Resolver{ - PreferGo: true, - StrictErrors: false, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{ - Timeout: 10 * time.Second, - } - return d.DialContext(ctx, network, ts.GetLocalAddr()) - }, - } - - // last domain has the given serial number, which also appears in the - // NS record so we can fetch it via Go. - lastDomain := ts.domains[len(ts.domains)-1] - records, err := resolver.LookupNS(ts.ctx, lastDomain) - if err != nil { - return fmt.Errorf("failed to lookup NS record for %v: %w", lastDomain, err) - } - - if len(records) != 1 { - return fmt.Errorf("expected only 1 NS record for %v, got %v/%v", lastDomain, len(records), records) - } - - expectedNS := fmt.Sprintf("ns%d.%v.", ts.serial, lastDomain) - if records[0].Host != expectedNS { - return fmt.Errorf("expected to find NS %v, got %v indicating reload hadn't completed", expectedNS, records[0]) - } - - return nil - }) -} - -func (ts *TestServer) GetLocalAddr() string { - return ts.startup.Config.Address() -} - -func (ts *TestServer) GetRemoteAddr() string { - return fmt.Sprintf("%s:%d", ts.startup.StartResult.RealIP, 53) -} - -func (ts *TestServer) AddDomain(domain string) { - ts.lock.Lock() - defer ts.lock.Unlock() - - for _, existing := range ts.domains { - if existing == domain { - return - } - } - - ts.domains = append(ts.domains, domain) -} - -func (ts *TestServer) AddRecord(domain string, record string, value string) { - ts.lock.Lock() - defer ts.lock.Unlock() - - foundDomain := false - for _, existing := range ts.domains { - if strings.HasSuffix(domain, existing) { - foundDomain = true - break - } - } - if !foundDomain { - ts.t.Fatalf("cannot add record %v/%v :: [%v] -- no domain zone matching (%v)", record, domain, value, ts.domains) - } - - value = strings.TrimSpace(value) - if _, present := ts.records[domain]; !present { - ts.records[domain] = map[string][]string{} - } - - if values, present := ts.records[domain][record]; present { - for _, candidate := range values { - if candidate == value { - // Already present; skip adding. - return - } - } - } - - ts.records[domain][record] = append(ts.records[domain][record], value) -} - -func (ts *TestServer) RemoveRecord(domain string, record string, value string) { - ts.lock.Lock() - defer ts.lock.Unlock() - - foundDomain := false - for _, existing := range ts.domains { - if strings.HasSuffix(domain, existing) { - foundDomain = true - break - } - } - if !foundDomain { - // Not found. - return - } - - value = strings.TrimSpace(value) - if _, present := ts.records[domain]; !present { - // Not found. - return - } - - var remaining []string - if values, present := ts.records[domain][record]; present { - for _, candidate := range values { - if candidate != value { - remaining = append(remaining, candidate) - } - } - } - - ts.records[domain][record] = remaining -} - -func (ts *TestServer) RemoveRecordsOfTypeForDomain(domain string, record string) { - ts.lock.Lock() - defer ts.lock.Unlock() - - foundDomain := false - for _, existing := range ts.domains { - if strings.HasSuffix(domain, existing) { - foundDomain = true - break - } - } - if !foundDomain { - // Not found. - return - } - - if _, present := ts.records[domain]; !present { - // Not found. - return - } - - delete(ts.records[domain], record) -} - -func (ts *TestServer) RemoveRecordsForDomain(domain string) { - ts.lock.Lock() - defer ts.lock.Unlock() - - foundDomain := false - for _, existing := range ts.domains { - if strings.HasSuffix(domain, existing) { - foundDomain = true - break - } - } - if !foundDomain { - // Not found. - return - } - - if _, present := ts.records[domain]; !present { - // Not found. - return - } - - ts.records[domain] = map[string][]string{} -} - -func (ts *TestServer) RemoveAllRecords() { - ts.lock.Lock() - defer ts.lock.Unlock() - - ts.records = map[string]map[string][]string{} -} - -func (ts *TestServer) Cleanup() { - if ts.cleanup != nil { - ts.cleanup() - } - if ts.startup != nil && ts.startup.Cleanup != nil { - ts.startup.Cleanup() - } -} diff --git a/builtin/logical/pki/integration_test.go b/builtin/logical/pki/integration_test.go deleted file mode 100644 index a7d8a530c..000000000 --- a/builtin/logical/pki/integration_test.go +++ /dev/null @@ -1,753 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" - "testing" - - "github.com/hashicorp/vault/api" - vaulthttp "github.com/hashicorp/vault/http" - vaultocsp "github.com/hashicorp/vault/sdk/helper/ocsp" - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - - "github.com/hashicorp/go-hclog" - "github.com/stretchr/testify/require" -) - -func TestIntegration_RotateRootUsesNext(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/rotate/internal", - Storage: s, - Data: map[string]interface{}{ - "common_name": "test.com", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed rotate root") - require.NotNil(t, resp, "got nil response from rotate root") - require.False(t, resp.IsError(), "got an error from rotate root: %#v", resp) - - issuerId1 := resp.Data["issuer_id"].(issuerID) - issuerName1 := resp.Data["issuer_name"] - - require.NotEmpty(t, issuerId1, "issuer id was empty on initial rotate root command") - require.Equal(t, "next", issuerName1, "expected an issuer name of next on initial rotate root command") - - // Call it again, we should get a new issuer id, but since next issuer_name is used we should get a blank value. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/rotate/internal", - Storage: s, - Data: map[string]interface{}{ - "common_name": "test.com", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed rotate root") - require.NotNil(t, resp, "got nil response from rotate root") - require.False(t, resp.IsError(), "got an error from rotate root: %#v", resp) - - issuerId2 := resp.Data["issuer_id"].(issuerID) - issuerName2 := resp.Data["issuer_name"] - - require.NotEmpty(t, issuerId2, "issuer id was empty on second rotate root command") - require.NotEqual(t, issuerId1, issuerId2, "should have been different issuer ids") - require.Empty(t, issuerName2, "expected a blank issuer name on the second rotate root command") - - // Call it again, making sure we can use our own name if desired. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/rotate/internal", - Storage: s, - Data: map[string]interface{}{ - "common_name": "test.com", - "issuer_name": "next-cert", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed rotate root") - require.NotNil(t, resp, "got nil response from rotate root") - require.False(t, resp.IsError(), "got an error from rotate root: %#v", resp) - - issuerId3 := resp.Data["issuer_id"].(issuerID) - issuerName3 := resp.Data["issuer_name"] - - require.NotEmpty(t, issuerId3, "issuer id was empty on third rotate root command") - require.NotEqual(t, issuerId3, issuerId1, "should have been different issuer id from initial") - require.NotEqual(t, issuerId3, issuerId2, "should have been different issuer id from second call") - require.Equal(t, "next-cert", issuerName3, "expected an issuer name that we specified on third rotate root command") -} - -func TestIntegration_ReplaceRootNormal(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // generate roots - genTestRootCa(t, b, s) - issuerId2, _ := genTestRootCa(t, b, s) - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/replace", - Storage: s, - Data: map[string]interface{}{ - "default": issuerId2.String(), - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed replacing root") - require.NotNil(t, resp, "got nil response from replacing root") - require.False(t, resp.IsError(), "got an error from replacing root: %#v", resp) - - replacedIssuer := resp.Data["default"] - require.Equal(t, issuerId2, replacedIssuer, "expected return value to match issuer we set") - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "config/issuers", - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed replacing root") - require.NotNil(t, resp, "got nil response from replacing root") - require.False(t, resp.IsError(), "got an error from replacing root: %#v", resp) - - defaultIssuer := resp.Data["default"] - require.Equal(t, issuerId2, defaultIssuer, "expected default issuer to be updated") -} - -func TestIntegration_ReplaceRootDefaultsToNext(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // generate roots - genTestRootCa(t, b, s) - issuerId2, _ := genTestRootCaWithIssuerName(t, b, s, "next") - - // Do not specify the default value to replace. - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/replace", - Storage: s, - Data: map[string]interface{}{}, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed replacing root") - require.NotNil(t, resp, "got nil response from replacing root") - require.False(t, resp.IsError(), "got an error from replacing root: %#v", resp) - - replacedIssuer := resp.Data["default"] - require.Equal(t, issuerId2, replacedIssuer, "expected return value to match the 'next' issuer we set") - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "config/issuers", - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed replacing root") - require.NotNil(t, resp, "got nil response from replacing root") - require.False(t, resp.IsError(), "got an error from replacing root: %#v", resp) - - defaultIssuer := resp.Data["default"] - require.Equal(t, issuerId2, defaultIssuer, "expected default issuer to be updated") -} - -func TestIntegration_ReplaceRootBadIssuer(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // generate roots - genTestRootCa(t, b, s) - genTestRootCa(t, b, s) - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/replace", - Storage: s, - Data: map[string]interface{}{ - "default": "a-bad-issuer-id", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed replacing root, should have been an error in the response.") - require.NotNil(t, resp, "got nil response from replacing root") - require.True(t, resp.IsError(), "did not get an error from replacing root: %#v", resp) - - // Make sure we trap replacing with default. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/replace", - Storage: s, - Data: map[string]interface{}{ - "default": "default", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed replacing root, should have been an error in the response.") - require.NotNil(t, resp, "got nil response from replacing root") - require.True(t, resp.IsError(), "did not get an error from replacing root: %#v", resp) - - // Make sure we trap replacing with blank string. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/replace", - Storage: s, - Data: map[string]interface{}{ - "default": "", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed replacing root, should have been an error in the response.") - require.NotNil(t, resp, "got nil response from replacing root") - require.True(t, resp.IsError(), "did not get an error from replacing root: %#v", resp) -} - -func TestIntegration_SetSignedWithBackwardsPemBundles(t *testing.T) { - t.Parallel() - rootBackend, rootStorage := CreateBackendWithStorage(t) - intBackend, intStorage := CreateBackendWithStorage(t) - - // generate root - resp, err := rootBackend.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issuers/generate/root/internal", - Storage: rootStorage, - Data: map[string]interface{}{ - "common_name": "test.com", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed generating root ca") - require.NotNil(t, resp, "got nil response from generating root ca") - require.False(t, resp.IsError(), "got an error from generating root ca: %#v", resp) - rootCert := resp.Data["certificate"].(string) - - schema.ValidateResponse(t, schema.GetResponseSchema(t, rootBackend.Route("issuers/generate/root/internal"), logical.UpdateOperation), resp, true) - - // generate intermediate - resp, err = intBackend.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issuers/generate/intermediate/internal", - Storage: intStorage, - Data: map[string]interface{}{ - "common_name": "test.com", - }, - MountPoint: "pki-int/", - }) - require.NoError(t, err, "failed generating int ca") - require.NotNil(t, resp, "got nil response from generating int ca") - require.False(t, resp.IsError(), "got an error from generating int ca: %#v", resp) - intCsr := resp.Data["csr"].(string) - - // sign csr - resp, err = rootBackend.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/sign-intermediate", - Storage: rootStorage, - Data: map[string]interface{}{ - "csr": intCsr, - "format": "pem_bundle", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed generating root ca") - require.NotNil(t, resp, "got nil response from generating root ca") - require.False(t, resp.IsError(), "got an error from generating root ca: %#v", resp) - - intCert := resp.Data["certificate"].(string) - - // Send in the chain backwards now and make sure we link intCert as default. - resp, err = intBackend.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "intermediate/set-signed", - Storage: intStorage, - Data: map[string]interface{}{ - "certificate": rootCert + "\n" + intCert + "\n", - }, - MountPoint: "pki-int/", - }) - require.NoError(t, err, "failed generating root ca") - require.NotNil(t, resp, "got nil response from generating root ca") - require.False(t, resp.IsError(), "got an error from generating root ca: %#v", resp) - - // setup role - resp, err = intBackend.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/example", - Storage: intStorage, - Data: map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "max_ttl": "1h", - }, - MountPoint: "pki-int/", - }) - require.NoError(t, err, "failed setting up role example") - require.NotNil(t, resp, "got nil response from setting up role example: %#v", resp) - - schema.ValidateResponse(t, schema.GetResponseSchema(t, intBackend.Route("roles/example"), logical.UpdateOperation), resp, true) - - // Issue cert - resp, err = intBackend.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issue/example", - Storage: intStorage, - Data: map[string]interface{}{ - "common_name": "test.example.com", - "ttl": "5m", - }, - MountPoint: "pki-int/", - }) - require.NoError(t, err, "failed issuing a leaf cert from int ca") - require.NotNil(t, resp, "got nil response issuing a leaf cert from int ca") - require.False(t, resp.IsError(), "got an error issuing a leaf cert from int ca: %#v", resp) - - schema.ValidateResponse(t, schema.GetResponseSchema(t, intBackend.Route("issue/example"), logical.UpdateOperation), resp, true) -} - -func TestIntegration_CSRGeneration(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - testCases := []struct { - keyType string - usePss bool - keyBits int - sigBits int - expectedPublicKeyType crypto.PublicKey - expectedSignature x509.SignatureAlgorithm - }{ - {"rsa", false, 2048, 0, &rsa.PublicKey{}, x509.SHA256WithRSA}, - {"rsa", false, 2048, 384, &rsa.PublicKey{}, x509.SHA384WithRSA}, - // Add back once https://github.com/golang/go/issues/45990 is fixed. - // {"rsa", true, 2048, 0, &rsa.PublicKey{}, x509.SHA256WithRSAPSS}, - // {"rsa", true, 2048, 512, &rsa.PublicKey{}, x509.SHA512WithRSAPSS}, - {"ec", false, 224, 0, &ecdsa.PublicKey{}, x509.ECDSAWithSHA256}, - {"ec", false, 256, 0, &ecdsa.PublicKey{}, x509.ECDSAWithSHA256}, - {"ec", false, 384, 0, &ecdsa.PublicKey{}, x509.ECDSAWithSHA384}, - {"ec", false, 521, 0, &ecdsa.PublicKey{}, x509.ECDSAWithSHA512}, - {"ec", false, 521, 224, &ecdsa.PublicKey{}, x509.ECDSAWithSHA512}, // We ignore signature_bits for ec - {"ed25519", false, 0, 0, ed25519.PublicKey{}, x509.PureEd25519}, // We ignore both fields for ed25519 - } - for _, tc := range testCases { - keyTypeName := tc.keyType - if tc.usePss { - keyTypeName = tc.keyType + "-pss" - } - testName := fmt.Sprintf("%s-%d-%d", keyTypeName, tc.keyBits, tc.sigBits) - t.Run(testName, func(t *testing.T) { - resp, err := CBWrite(b, s, "intermediate/generate/internal", map[string]interface{}{ - "common_name": "myint.com", - "key_type": tc.keyType, - "key_bits": tc.keyBits, - "signature_bits": tc.sigBits, - "use_pss": tc.usePss, - }) - requireSuccessNonNilResponse(t, resp, err) - requireFieldsSetInResp(t, resp, "csr") - - csrString := resp.Data["csr"].(string) - pemBlock, _ := pem.Decode([]byte(csrString)) - require.NotNil(t, pemBlock, "failed to parse returned csr pem block") - csr, err := x509.ParseCertificateRequest(pemBlock.Bytes) - require.NoError(t, err, "failed parsing certificate request") - - require.Equal(t, tc.expectedSignature, csr.SignatureAlgorithm, - "Expected %s, got %s", tc.expectedSignature.String(), csr.SignatureAlgorithm.String()) - require.IsType(t, tc.expectedPublicKeyType, csr.PublicKey) - }) - } -} - -func TestIntegration_AutoIssuer(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - // Generate two roots. The first should become default under the existing - // behavior; when we update the config and generate a second, it should - // take over as default. Deleting the first and re-importing it will make - // it default again, and then disabling the option and removing and - // reimporting the second and creating a new root won't affect it again. - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "Root X1", - "issuer_name": "root-1", - "key_type": "ec", - }) - - requireSuccessNonNilResponse(t, resp, err) - issuerIdOne := resp.Data["issuer_id"] - require.NotEmpty(t, issuerIdOne) - certOne := resp.Data["certificate"] - require.NotEmpty(t, certOne) - - resp, err = CBRead(b, s, "config/issuers") - requireSuccessNonNilResponse(t, resp, err) - require.Equal(t, issuerIdOne, resp.Data["default"]) - - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/issuers"), logical.ReadOperation), resp, true) - - // Enable the new config option. - resp, err = CBWrite(b, s, "config/issuers", map[string]interface{}{ - "default": issuerIdOne, - "default_follows_latest_issuer": true, - }) - require.NoError(t, err) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/issuers"), logical.UpdateOperation), resp, true) - - // Now generate the second root; it should become default. - resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "Root X2", - "issuer_name": "root-2", - "key_type": "ec", - }) - requireSuccessNonNilResponse(t, resp, err) - issuerIdTwo := resp.Data["issuer_id"] - require.NotEmpty(t, issuerIdTwo) - certTwo := resp.Data["certificate"] - require.NotEmpty(t, certTwo) - - resp, err = CBRead(b, s, "config/issuers") - requireSuccessNonNilResponse(t, resp, err) - require.Equal(t, issuerIdTwo, resp.Data["default"]) - - // Deleting the first shouldn't affect the default issuer. - _, err = CBDelete(b, s, "issuer/root-1") - require.NoError(t, err) - resp, err = CBRead(b, s, "config/issuers") - requireSuccessNonNilResponse(t, resp, err) - require.Equal(t, issuerIdTwo, resp.Data["default"]) - - // But reimporting it should update it to the new issuer's value. - resp, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": certOne, - }) - requireSuccessNonNilResponse(t, resp, err) - issuerIdOneReimported := issuerID(resp.Data["imported_issuers"].([]string)[0]) - - resp, err = CBRead(b, s, "config/issuers") - requireSuccessNonNilResponse(t, resp, err) - require.Equal(t, issuerIdOneReimported, resp.Data["default"]) - - // Now update the config to disable this option again. - _, err = CBWrite(b, s, "config/issuers", map[string]interface{}{ - "default": issuerIdOneReimported, - "default_follows_latest_issuer": false, - }) - require.NoError(t, err) - - // Generating a new root shouldn't update the default. - resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "Root X3", - "issuer_name": "root-3", - "key_type": "ec", - }) - requireSuccessNonNilResponse(t, resp, err) - issuerIdThree := resp.Data["issuer_id"] - require.NotEmpty(t, issuerIdThree) - - resp, err = CBRead(b, s, "config/issuers") - requireSuccessNonNilResponse(t, resp, err) - require.Equal(t, issuerIdOneReimported, resp.Data["default"]) - - // Deleting and re-importing root 2 should also not affect it. - _, err = CBDelete(b, s, "issuer/root-2") - require.NoError(t, err) - resp, err = CBRead(b, s, "config/issuers") - requireSuccessNonNilResponse(t, resp, err) - require.Equal(t, issuerIdOneReimported, resp.Data["default"]) - - resp, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": certTwo, - }) - requireSuccessNonNilResponse(t, resp, err) - require.Equal(t, 1, len(resp.Data["imported_issuers"].([]string))) - resp, err = CBRead(b, s, "config/issuers") - requireSuccessNonNilResponse(t, resp, err) - require.Equal(t, issuerIdOneReimported, resp.Data["default"]) -} - -// TestLDAPAiaCrlUrls validates we can properly handle CRL urls that are ldap based. -func TestLDAPAiaCrlUrls(t *testing.T) { - t.Parallel() - - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - singleCore := cluster.Cores[0] - vault.TestWaitActive(t, singleCore.Core) - client := singleCore.Client - - mountPKIEndpoint(t, client, "pki") - - // Attempt multiple urls - crls := []string{ - "ldap://ldap.example.com/cn=example%20CA,dc=example,dc=com?certificateRevocationList;binary", - "ldap://ldap.example.com/cn=CA,dc=example,dc=com?authorityRevocationList;binary", - } - - _, err := client.Logical().Write("pki/config/urls", map[string]interface{}{ - "crl_distribution_points": crls, - }) - require.NoError(t, err) - - resp, err := client.Logical().Read("pki/config/urls") - require.NoError(t, err, "failed reading config/urls") - require.NotNil(t, resp, "resp was nil") - require.NotNil(t, resp.Data, "data within response was nil") - require.NotEmpty(t, resp.Data["crl_distribution_points"], "crl_distribution_points was nil within data") - require.Len(t, resp.Data["crl_distribution_points"], len(crls)) - - for _, crlVal := range crls { - require.Contains(t, resp.Data["crl_distribution_points"], crlVal) - } - - resp, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "Root R1", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["issuer_id"]) - rootIssuerId := resp.Data["issuer_id"].(string) - - _, err = client.Logical().Write("pki/roles/example-root", map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "max_ttl": "1h", - "key_type": "ec", - "issuer_ref": rootIssuerId, - }) - require.NoError(t, err) - - resp, err = client.Logical().Write("pki/issue/example-root", map[string]interface{}{ - "common_name": "test.example.com", - "ttl": "5m", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["certificate"]) - - certPEM := resp.Data["certificate"].(string) - certBlock, _ := pem.Decode([]byte(certPEM)) - require.NotNil(t, certBlock) - cert, err := x509.ParseCertificate(certBlock.Bytes) - require.NoError(t, err) - - require.EqualValues(t, crls, cert.CRLDistributionPoints) -} - -func TestIntegrationOCSPClientWithPKI(t *testing.T) { - t.Parallel() - - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - client := cores[0].Client - - err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "32h", - }, - }) - require.NoError(t, err) - - resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "Root R1", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["issuer_id"]) - rootIssuerId := resp.Data["issuer_id"].(string) - - // Set URLs pointing to the issuer. - _, err = client.Logical().Write("pki/config/cluster", map[string]interface{}{ - "path": client.Address() + "/v1/pki", - "aia_path": client.Address() + "/v1/pki", - }) - require.NoError(t, err) - - _, err = client.Logical().Write("pki/config/urls", map[string]interface{}{ - "enable_templating": true, - "crl_distribution_points": "{{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der", - "issuing_certificates": "{{cluster_aia_path}}/issuer/{{issuer_id}}/der", - "ocsp_servers": "{{cluster_aia_path}}/ocsp", - }) - require.NoError(t, err) - - // Build an intermediate CA - resp, err = client.Logical().Write("pki/intermediate/generate/internal", map[string]interface{}{ - "common_name": "Int X1", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["csr"]) - intermediateCSR := resp.Data["csr"].(string) - - resp, err = client.Logical().Write("pki/root/sign-intermediate", map[string]interface{}{ - "csr": intermediateCSR, - "ttl": "20h", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["certificate"]) - intermediateCert := resp.Data["certificate"] - - resp, err = client.Logical().Write("pki/intermediate/set-signed", map[string]interface{}{ - "certificate": intermediateCert, - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["imported_issuers"]) - rawImportedIssuers := resp.Data["imported_issuers"].([]interface{}) - require.Equal(t, len(rawImportedIssuers), 1) - importedIssuer := rawImportedIssuers[0].(string) - require.NotEmpty(t, importedIssuer) - - // Set intermediate as default. - _, err = client.Logical().Write("pki/config/issuers", map[string]interface{}{ - "default": importedIssuer, - }) - require.NoError(t, err) - - // Setup roles for root, intermediate. - _, err = client.Logical().Write("pki/roles/example-root", map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "max_ttl": "1h", - "key_type": "ec", - "issuer_ref": rootIssuerId, - }) - require.NoError(t, err) - - _, err = client.Logical().Write("pki/roles/example-int", map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "max_ttl": "1h", - "key_type": "ec", - }) - require.NoError(t, err) - - // Issue certs and validate them against OCSP. - for _, path := range []string{"pki/issue/example-int", "pki/issue/example-root"} { - t.Logf("Validating against path: %v", path) - resp, err = client.Logical().Write(path, map[string]interface{}{ - "common_name": "test.example.com", - "ttl": "5m", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["certificate"]) - require.NotEmpty(t, resp.Data["issuing_ca"]) - require.NotEmpty(t, resp.Data["serial_number"]) - - certPEM := resp.Data["certificate"].(string) - certBlock, _ := pem.Decode([]byte(certPEM)) - require.NotNil(t, certBlock) - cert, err := x509.ParseCertificate(certBlock.Bytes) - require.NoError(t, err) - require.NotNil(t, cert) - - issuerPEM := resp.Data["issuing_ca"].(string) - issuerBlock, _ := pem.Decode([]byte(issuerPEM)) - require.NotNil(t, issuerBlock) - issuer, err := x509.ParseCertificate(issuerBlock.Bytes) - require.NoError(t, err) - require.NotNil(t, issuer) - - serialNumber := resp.Data["serial_number"].(string) - - testLogger := hclog.New(hclog.DefaultOptions) - - conf := &vaultocsp.VerifyConfig{ - OcspFailureMode: vaultocsp.FailOpenFalse, - ExtraCas: []*x509.Certificate{cluster.CACert}, - } - ocspClient := vaultocsp.New(func() hclog.Logger { - return testLogger - }, 10) - - _, err = client.Logical().Write("pki/revoke", map[string]interface{}{ - "serial_number": serialNumber, - }) - require.NoError(t, err) - - err = ocspClient.VerifyLeafCertificate(context.Background(), cert, issuer, conf) - require.Error(t, err) - } -} - -func genTestRootCa(t *testing.T, b *backend, s logical.Storage) (issuerID, keyID) { - return genTestRootCaWithIssuerName(t, b, s, "") -} - -func genTestRootCaWithIssuerName(t *testing.T, b *backend, s logical.Storage, issuerName string) (issuerID, keyID) { - data := map[string]interface{}{ - "common_name": "test.com", - } - if len(issuerName) > 0 { - data["issuer_name"] = issuerName - } - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issuers/generate/root/internal", - Storage: s, - Data: data, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed generating root ca") - require.NotNil(t, resp, "got nil response from generating root ca") - require.False(t, resp.IsError(), "got an error from generating root ca: %#v", resp) - - issuerId := resp.Data["issuer_id"].(issuerID) - keyId := resp.Data["key_id"].(keyID) - - require.NotEmpty(t, issuerId, "returned issuer id was empty") - require.NotEmpty(t, keyId, "returned key id was empty") - - return issuerId, keyId -} diff --git a/builtin/logical/pki/path_acme_order_test.go b/builtin/logical/pki/path_acme_order_test.go deleted file mode 100644 index e6653ef25..000000000 --- a/builtin/logical/pki/path_acme_order_test.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "net" - "testing" - - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" -) - -// TestACME_ValidateIdentifiersAgainstRole Verify the ACME order creation -// function verifies somewhat the identifiers that were provided have a -// decent chance of being allowed by the selected role. -func TestACME_ValidateIdentifiersAgainstRole(t *testing.T) { - b, _ := CreateBackendWithStorage(t) - - tests := []struct { - name string - role *roleEntry - identifiers []*ACMEIdentifier - expectErr bool - }{ - { - name: "verbatim-role-allows-dns-ip", - role: buildSignVerbatimRoleWithNoData(nil), - identifiers: _buildACMEIdentifiers("test.com", "127.0.0.1"), - expectErr: false, - }, - { - name: "default-role-does-not-allow-dns", - role: buildTestRole(t, nil), - identifiers: _buildACMEIdentifiers("www.test.com"), - expectErr: true, - }, - { - name: "default-role-allows-ip", - role: buildTestRole(t, nil), - identifiers: _buildACMEIdentifiers("192.168.0.1"), - expectErr: false, - }, - { - name: "disable-ip-sans-forbids-ip", - role: buildTestRole(t, map[string]interface{}{"allow_ip_sans": false}), - identifiers: _buildACMEIdentifiers("192.168.0.1"), - expectErr: true, - }, - { - name: "role-no-wildcards-allowed-without", - role: buildTestRole(t, map[string]interface{}{ - "allow_subdomains": true, - "allow_bare_domains": true, - "allowed_domains": []string{"test.com"}, - "allow_wildcard_certificates": false, - }), - identifiers: _buildACMEIdentifiers("www.test.com", "test.com"), - expectErr: false, - }, - { - name: "role-no-wildcards-allowed-with-wildcard", - role: buildTestRole(t, map[string]interface{}{ - "allow_subdomains": true, - "allowed_domains": []string{"test.com"}, - "allow_wildcard_certificates": false, - }), - identifiers: _buildACMEIdentifiers("*.test.com"), - expectErr: true, - }, - { - name: "role-wildcards-allowed-with-wildcard", - role: buildTestRole(t, map[string]interface{}{ - "allow_subdomains": true, - "allowed_domains": []string{"test.com"}, - "allow_wildcard_certificates": true, - }), - identifiers: _buildACMEIdentifiers("*.test.com"), - expectErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := b.validateIdentifiersAgainstRole(tt.role, tt.identifiers) - - if tt.expectErr { - require.Error(t, err, "validateIdentifiersAgainstRole(%v, %v)", tt.role.ToResponseData(), tt.identifiers) - // If we did return an error if should be classified as a ErrRejectedIdentifier - require.ErrorIs(t, err, ErrRejectedIdentifier) - } else { - require.NoError(t, err, "validateIdentifiersAgainstRole(%v, %v)", tt.role.ToResponseData(), tt.identifiers) - } - }) - } -} - -func _buildACMEIdentifiers(values ...string) []*ACMEIdentifier { - var identifiers []*ACMEIdentifier - - for _, value := range values { - identifiers = append(identifiers, _buildACMEIdentifier(value)) - } - - return identifiers -} - -func _buildACMEIdentifier(val string) *ACMEIdentifier { - ip := net.ParseIP(val) - if ip == nil { - identifier := &ACMEIdentifier{Type: "dns", Value: val, OriginalValue: val, IsWildcard: false} - _, _, _ = identifier.MaybeParseWildcard() - return identifier - } - - return &ACMEIdentifier{Type: "ip", Value: val, OriginalValue: val, IsWildcard: false} -} - -// Easily allow tests to create valid roles with proper defaults, since we don't have an easy -// way to generate roles with proper defaults, go through the createRole handler with the handlers -// field data so we pickup all the defaults specified there. -func buildTestRole(t *testing.T, config map[string]interface{}) *roleEntry { - b, s := CreateBackendWithStorage(t) - - path := pathRoles(b) - fields := path.Fields - if config == nil { - config = map[string]interface{}{} - } - - if _, exists := config["name"]; !exists { - config["name"] = genUuid() - } - - _, err := b.pathRoleCreate(ctx, &logical.Request{Storage: s}, &framework.FieldData{Raw: config, Schema: fields}) - require.NoError(t, err, "failed generating role with config %v", config) - - role, err := b.getRole(ctx, s, config["name"].(string)) - require.NoError(t, err, "failed loading stored role") - - return role -} diff --git a/builtin/logical/pki/path_acme_test.go b/builtin/logical/pki/path_acme_test.go deleted file mode 100644 index a0663ad00..000000000 --- a/builtin/logical/pki/path_acme_test.go +++ /dev/null @@ -1,1832 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "os" - "path" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/helper/certutil" - - "github.com/go-test/deep" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/acme" - "golang.org/x/net/http2" - - "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/logical/pki/dnstest" - "github.com/hashicorp/vault/helper/constants" - "github.com/hashicorp/vault/helper/testhelpers" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -// TestAcmeBasicWorkflow a test that will validate a basic ACME workflow using the Golang ACME client. -func TestAcmeBasicWorkflow(t *testing.T) { - t.Parallel() - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - cases := []struct { - name string - prefixUrl string - }{ - {"root", "acme/"}, - {"role", "roles/test-role/acme/"}, - {"issuer", "issuer/int-ca/acme/"}, - {"issuer_role", "issuer/int-ca/roles/test-role/acme/"}, - } - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - baseAcmeURL := "/v1/pki/" + tc.prefixUrl - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - t.Logf("Testing discover on %s", baseAcmeURL) - discovery, err := acmeClient.Discover(testCtx) - require.NoError(t, err, "failed acme discovery call") - - discoveryBaseUrl := client.Address() + baseAcmeURL - require.Equal(t, discoveryBaseUrl+"new-nonce", discovery.NonceURL) - require.Equal(t, discoveryBaseUrl+"new-account", discovery.RegURL) - require.Equal(t, discoveryBaseUrl+"new-order", discovery.OrderURL) - require.Equal(t, discoveryBaseUrl+"revoke-cert", discovery.RevokeURL) - require.Equal(t, discoveryBaseUrl+"key-change", discovery.KeyChangeURL) - require.False(t, discovery.ExternalAccountRequired, "bad value for external account required in directory") - - // Attempt to update prior to creating an account - t.Logf("Testing updates with no proper account fail on %s", baseAcmeURL) - _, err = acmeClient.UpdateReg(testCtx, &acme.Account{Contact: []string{"mailto:shouldfail@example.com"}}) - require.ErrorIs(t, err, acme.ErrNoAccount, "expected failure attempting to update prior to account registration") - - // Create new account - t.Logf("Testing register on %s", baseAcmeURL) - acct, err := acmeClient.Register(testCtx, &acme.Account{ - Contact: []string{"mailto:test@example.com", "mailto:test2@test.com"}, - }, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - require.Equal(t, acme.StatusValid, acct.Status) - require.Contains(t, acct.Contact, "mailto:test@example.com") - require.Contains(t, acct.Contact, "mailto:test2@test.com") - require.Len(t, acct.Contact, 2) - - // Call register again we should get existing account - t.Logf("Testing duplicate register returns existing account on %s", baseAcmeURL) - _, err = acmeClient.Register(testCtx, acct, func(tosURL string) bool { return true }) - require.ErrorIs(t, err, acme.ErrAccountAlreadyExists, - "We should have returned a 200 status code which would have triggered an error in the golang acme"+ - " library") - - // Update contact - t.Logf("Testing Update account contacts on %s", baseAcmeURL) - acct.Contact = []string{"mailto:test3@example.com"} - acct2, err := acmeClient.UpdateReg(testCtx, acct) - require.NoError(t, err, "failed updating account") - require.Equal(t, acme.StatusValid, acct2.Status) - // We should get this back, not the original values. - require.Contains(t, acct2.Contact, "mailto:test3@example.com") - require.Len(t, acct2.Contact, 1) - - // Make sure order's do not accept dates - _, err = acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{{Type: "dns", Value: "localhost"}}, - acme.WithOrderNotBefore(time.Now().Add(10*time.Minute))) - require.Error(t, err, "should have rejected a new order with NotBefore set") - - _, err = acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{{Type: "dns", Value: "localhost"}}, - acme.WithOrderNotAfter(time.Now().Add(10*time.Minute))) - require.Error(t, err, "should have rejected a new order with NotAfter set") - - // Make sure DNS identifiers cannot include IP addresses - _, err = acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{{Type: "dns", Value: "127.0.0.1"}}, - acme.WithOrderNotAfter(time.Now().Add(10*time.Minute))) - require.Error(t, err, "should have rejected a new order with IP-like DNS-type identifier") - _, err = acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{{Type: "dns", Value: "*.127.0.0.1"}}, - acme.WithOrderNotAfter(time.Now().Add(10*time.Minute))) - require.Error(t, err, "should have rejected a new order with IP-like DNS-type identifier") - - // Create an order - t.Logf("Testing Authorize Order on %s", baseAcmeURL) - identifiers := []string{"localhost.localdomain", "*.localdomain"} - createOrder, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{ - {Type: "dns", Value: identifiers[0]}, - {Type: "dns", Value: identifiers[1]}, - }) - require.NoError(t, err, "failed creating order") - require.Equal(t, acme.StatusPending, createOrder.Status) - require.Empty(t, createOrder.CertURL) - require.Equal(t, createOrder.URI+"/finalize", createOrder.FinalizeURL) - require.Len(t, createOrder.AuthzURLs, 2, "expected two authzurls") - - // Get order - t.Logf("Testing GetOrder on %s", baseAcmeURL) - getOrder, err := acmeClient.GetOrder(testCtx, createOrder.URI) - require.NoError(t, err, "failed fetching order") - require.Equal(t, acme.StatusPending, createOrder.Status) - if diffs := deep.Equal(createOrder, getOrder); diffs != nil { - t.Fatalf("Differences exist between create and get order: \n%v", strings.Join(diffs, "\n")) - } - - // Make sure the identifiers returned in the order contain the original values - var ids []string - for _, id := range getOrder.Identifiers { - require.Equal(t, "dns", id.Type) - ids = append(ids, id.Value) - } - require.ElementsMatch(t, identifiers, ids, "order responses should have all original identifiers") - - // Load authorizations - var authorizations []*acme.Authorization - for _, authUrl := range getOrder.AuthzURLs { - auth, err := acmeClient.GetAuthorization(testCtx, authUrl) - require.NoError(t, err, "failed fetching authorization: %s", authUrl) - - authorizations = append(authorizations, auth) - } - - // We should have 2 separate auth challenges as we have two separate identifier - require.Len(t, authorizations, 2, "expected 2 authorizations in order") - - var wildcardAuth *acme.Authorization - var domainAuth *acme.Authorization - for _, auth := range authorizations { - if auth.Wildcard { - wildcardAuth = auth - } else { - domainAuth = auth - } - } - - // Test the values for the domain authentication - require.Equal(t, acme.StatusPending, domainAuth.Status) - require.Equal(t, "dns", domainAuth.Identifier.Type) - require.Equal(t, "localhost.localdomain", domainAuth.Identifier.Value) - require.False(t, domainAuth.Wildcard, "should not be a wildcard") - require.True(t, domainAuth.Expires.IsZero(), "authorization should only have expiry set on valid status") - - require.Len(t, domainAuth.Challenges, 3, "expected three challenges") - require.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status) - require.True(t, domainAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge") - require.Equal(t, "http-01", domainAuth.Challenges[0].Type) - require.NotEmpty(t, domainAuth.Challenges[0].Token, "missing challenge token") - require.Equal(t, acme.StatusPending, domainAuth.Challenges[1].Status) - require.True(t, domainAuth.Challenges[1].Validated.IsZero(), "validated time should be 0 on challenge") - require.Equal(t, "dns-01", domainAuth.Challenges[1].Type) - require.NotEmpty(t, domainAuth.Challenges[1].Token, "missing challenge token") - require.Equal(t, acme.StatusPending, domainAuth.Challenges[2].Status) - require.True(t, domainAuth.Challenges[2].Validated.IsZero(), "validated time should be 0 on challenge") - require.Equal(t, "tls-alpn-01", domainAuth.Challenges[2].Type) - require.NotEmpty(t, domainAuth.Challenges[2].Token, "missing challenge token") - - // Test the values for the wildcard authentication - require.Equal(t, acme.StatusPending, wildcardAuth.Status) - require.Equal(t, "dns", wildcardAuth.Identifier.Type) - require.Equal(t, "localdomain", wildcardAuth.Identifier.Value) // Make sure we strip the *. in auth responses - require.True(t, wildcardAuth.Wildcard, "should be a wildcard") - require.True(t, wildcardAuth.Expires.IsZero(), "authorization should only have expiry set on valid status") - - require.Len(t, wildcardAuth.Challenges, 1, "expected one challenge") - require.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status) - require.True(t, wildcardAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge") - require.Equal(t, "dns-01", wildcardAuth.Challenges[0].Type) - require.NotEmpty(t, domainAuth.Challenges[0].Token, "missing challenge token") - - // Make sure that getting a challenge does not start it. - challenge, err := acmeClient.GetChallenge(testCtx, domainAuth.Challenges[0].URI) - require.NoError(t, err, "failed to load challenge") - require.Equal(t, acme.StatusPending, challenge.Status) - require.True(t, challenge.Validated.IsZero(), "validated time should be 0 on challenge") - require.Equal(t, "http-01", challenge.Type) - - // Accept a challenge; this triggers validation to start. - challenge, err = acmeClient.Accept(testCtx, domainAuth.Challenges[0]) - require.NoError(t, err, "failed to load challenge") - require.Equal(t, acme.StatusProcessing, challenge.Status) - require.True(t, challenge.Validated.IsZero(), "validated time should be 0 on challenge") - require.Equal(t, "http-01", challenge.Type) - - require.NotEmpty(t, challenge.Token, "missing challenge token") - - // HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow - // test. - markAuthorizationSuccess(t, client, acmeClient, acct, getOrder) - - // Make sure sending a CSR with the account key gets rejected. - goodCr := &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: identifiers[1]}, - DNSNames: []string{identifiers[0], identifiers[1]}, - } - t.Logf("csr: %v", goodCr) - - // We want to make sure people are not using the same keys for CSR/Certs and their ACME account. - csrSignedWithAccountKey, err := x509.CreateCertificateRequest(rand.Reader, goodCr, accountKey) - require.NoError(t, err, "failed generating csr") - _, _, err = acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csrSignedWithAccountKey, true) - require.Error(t, err, "should not be allowed to use the account key for a CSR") - - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated key for CSR") - - // Validate we reject CSRs that contain CN that aren't in the original order - badCr := &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: "not-in-original-order.com"}, - DNSNames: []string{identifiers[0], identifiers[1]}, - } - t.Logf("csr: %v", badCr) - - csrWithBadCName, err := x509.CreateCertificateRequest(rand.Reader, badCr, csrKey) - require.NoError(t, err, "failed generating csr with bad common name") - - _, _, err = acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csrWithBadCName, true) - require.Error(t, err, "should not be allowed to csr with different common names than order") - - // Validate we reject CSRs that contain DNS names that aren't in the original order - badCr = &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: createOrder.Identifiers[0].Value}, - DNSNames: []string{"www.notinorder.com"}, - } - - csrWithBadName, err := x509.CreateCertificateRequest(rand.Reader, badCr, csrKey) - require.NoError(t, err, "failed generating csr with bad name") - - _, _, err = acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csrWithBadName, true) - require.Error(t, err, "should not be allowed to csr with different names than order") - - // Validate we reject CSRs that contain IP addresses that weren't in the original order - badCr = &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: createOrder.Identifiers[0].Value}, - IPAddresses: []net.IP{{127, 0, 0, 1}}, - } - - csrWithBadIP, err := x509.CreateCertificateRequest(rand.Reader, badCr, csrKey) - require.NoError(t, err, "failed generating csr with bad name") - - _, _, err = acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csrWithBadIP, true) - require.Error(t, err, "should not be allowed to csr with different ip address than order") - - // Validate we reject CSRs that contains fewer names than in the original order. - badCr = &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: identifiers[0]}, - } - - csrWithBadName, err = x509.CreateCertificateRequest(rand.Reader, badCr, csrKey) - require.NoError(t, err, "failed generating csr with bad name") - - _, _, err = acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csrWithBadName, true) - require.Error(t, err, "should not be allowed to csr with different names than order") - - // Finally test a proper CSR, with the correct name and signed with a different key works. - csr, err := x509.CreateCertificateRequest(rand.Reader, goodCr, csrKey) - require.NoError(t, err, "failed generating csr") - - certs, _, err := acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csr, true) - require.NoError(t, err, "failed finalizing order") - require.Len(t, certs, 3, "expected three items within the returned certs") - - testAcmeCertSignedByCa(t, client, certs, "int-ca") - - // Make sure the certificate has a NotAfter date of a maximum of 90 days - acmeCert, err := x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert bytes") - maxAcmeNotAfter := time.Now().Add(maxAcmeCertTTL) - if maxAcmeNotAfter.Before(acmeCert.NotAfter) { - require.Fail(t, fmt.Sprintf("certificate has a NotAfter value %v greater than ACME max ttl %v", acmeCert.NotAfter, maxAcmeNotAfter)) - } - - // Can we revoke it using the account key revocation - err = acmeClient.RevokeCert(ctx, nil, certs[0], acme.CRLReasonUnspecified) - require.NoError(t, err, "failed to revoke certificate through account key") - - // Make sure it was actually revoked - certResp, err := client.Logical().ReadWithContext(ctx, "pki/cert/"+serialFromCert(acmeCert)) - require.NoError(t, err, "failed to read certificate status") - require.NotNil(t, certResp, "certificate status response was nil") - revocationTime := certResp.Data["revocation_time"].(json.Number) - revocationTimeInt, err := revocationTime.Int64() - require.NoError(t, err, "failed converting revocation_time value: %v", revocationTime) - require.Greater(t, revocationTimeInt, int64(0), - "revocation time was not greater than 0, revocation did not work value was: %v", revocationTimeInt) - - // Make sure we can revoke an authorization as a client - err = acmeClient.RevokeAuthorization(ctx, authorizations[0].URI) - require.NoError(t, err, "failed revoking authorization status") - - revokedAuth, err := acmeClient.GetAuthorization(ctx, authorizations[0].URI) - require.NoError(t, err, "failed fetching authorization") - require.Equal(t, acme.StatusDeactivated, revokedAuth.Status) - - // Deactivate account - t.Logf("Testing deactivate account on %s", baseAcmeURL) - err = acmeClient.DeactivateReg(testCtx) - require.NoError(t, err, "failed deactivating account") - - // Make sure we get an unauthorized error trying to update the account again. - t.Logf("Testing update on deactivated account fails on %s", baseAcmeURL) - _, err = acmeClient.UpdateReg(testCtx, acct) - require.Error(t, err, "expected account to be deactivated") - require.IsType(t, &acme.Error{}, err, "expected acme error type") - acmeErr := err.(*acme.Error) - require.Equal(t, "urn:ietf:params:acme:error:unauthorized", acmeErr.ProblemType) - }) - } -} - -// TestAcmeBasicWorkflowWithEab verify that new accounts require EAB's if enforced by configuration. -func TestAcmeBasicWorkflowWithEab(t *testing.T) { - t.Parallel() - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - // Enable EAB - _, err := client.Logical().WriteWithContext(context.Background(), "pki/config/acme", map[string]interface{}{ - "enabled": true, - "eab_policy": "always-required", - }) - require.NoError(t, err) - - cases := []struct { - name string - prefixUrl string - }{ - {"root", "acme/"}, - {"role", "roles/test-role/acme/"}, - {"issuer", "issuer/int-ca/acme/"}, - {"issuer_role", "issuer/int-ca/roles/test-role/acme/"}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - baseAcmeURL := "/v1/pki/" + tc.prefixUrl - accountKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed creating ec key") - - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - t.Logf("Testing discover on %s", baseAcmeURL) - discovery, err := acmeClient.Discover(testCtx) - require.NoError(t, err, "failed acme discovery call") - require.True(t, discovery.ExternalAccountRequired, "bad value for external account required in directory") - - // Create new account without EAB, should fail - t.Logf("Testing register on %s", baseAcmeURL) - _, err = acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - require.ErrorContains(t, err, "urn:ietf:params:acme:error:externalAccountRequired", - "expected failure creating an account without eab") - - // Test fetch, list, delete workflow - kid, _ := getEABKey(t, client, tc.prefixUrl) - resp, err := client.Logical().ListWithContext(testCtx, "pki/eab") - require.NoError(t, err, "failed to list eab tokens") - require.NotNil(t, resp, "list response for eab tokens should not be nil") - require.Contains(t, resp.Data, "keys") - require.Contains(t, resp.Data, "key_info") - require.Len(t, resp.Data["keys"], 1) - require.Contains(t, resp.Data["keys"], kid) - - _, err = client.Logical().DeleteWithContext(testCtx, "pki/eab/"+kid) - require.NoError(t, err, "failed to delete eab") - - // List eabs should return zero results - resp, err = client.Logical().ListWithContext(testCtx, "pki/eab") - require.NoError(t, err, "failed to list eab tokens") - require.Nil(t, resp, "list response for eab tokens should have been nil") - - // fetch a new EAB - kid, eabKeyBytes := getEABKey(t, client, tc.prefixUrl) - acct := &acme.Account{ - ExternalAccountBinding: &acme.ExternalAccountBinding{ - KID: kid, - Key: eabKeyBytes, - }, - } - - // Make sure we can list our key - resp, err = client.Logical().ListWithContext(testCtx, "pki/eab") - require.NoError(t, err, "failed to list eab tokens") - require.NotNil(t, resp, "list response for eab tokens should not be nil") - require.Contains(t, resp.Data, "keys") - require.Contains(t, resp.Data, "key_info") - require.Len(t, resp.Data["keys"], 1) - require.Contains(t, resp.Data["keys"], kid) - - keyInfo := resp.Data["key_info"].(map[string]interface{}) - require.Contains(t, keyInfo, kid) - - infoForKid := keyInfo[kid].(map[string]interface{}) - require.Equal(t, "hs", infoForKid["key_type"]) - require.Equal(t, tc.prefixUrl+"directory", infoForKid["acme_directory"]) - - // Create new account with EAB - t.Logf("Testing register on %s", baseAcmeURL) - _, err = acmeClient.Register(testCtx, acct, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering new account with eab") - - // Make sure our EAB is no longer available - resp, err = client.Logical().ListWithContext(context.Background(), "pki/eab") - require.NoError(t, err, "failed to list eab tokens") - require.Nil(t, resp, "list response for eab tokens should have been nil due to empty list") - - // Attempt to create another account with the same EAB as before -- should fail - accountKey2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed creating ec key") - - acmeClient2 := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey2) - acct2 := &acme.Account{ - ExternalAccountBinding: &acme.ExternalAccountBinding{ - KID: kid, - Key: eabKeyBytes, - }, - } - - _, err = acmeClient2.Register(testCtx, acct2, func(tosURL string) bool { return true }) - require.ErrorContains(t, err, "urn:ietf:params:acme:error:unauthorized", "should fail due to EAB re-use") - - // We can lookup/find an existing account without EAB if we have the account key - _, err = acmeClient.GetReg(testCtx /* unused url */, "") - require.NoError(t, err, "expected to lookup existing account without eab") - }) - } -} - -// TestAcmeNonce a basic test that will validate we get back a nonce with the proper status codes -// based on the -func TestAcmeNonce(t *testing.T) { - t.Parallel() - cluster, client, pathConfig := setupAcmeBackend(t) - defer cluster.Cleanup() - - cases := []struct { - name string - prefixUrl string - directoryUrl string - }{ - {"root", "", "pki/acme/new-nonce"}, - {"role", "/roles/test-role", "pki/roles/test-role/acme/new-nonce"}, - {"issuer", "/issuer/default", "pki/issuer/default/acme/new-nonce"}, - {"issuer_role", "/issuer/default/roles/test-role", "pki/issuer/default/roles/test-role/acme/new-nonce"}, - } - - for _, tc := range cases { - for _, httpOp := range []string{"get", "header"} { - t.Run(fmt.Sprintf("%s-%s", tc.name, httpOp), func(t *testing.T) { - var req *api.Request - switch httpOp { - case "get": - req = client.NewRequest(http.MethodGet, "/v1/"+tc.directoryUrl) - case "header": - req = client.NewRequest(http.MethodHead, "/v1/"+tc.directoryUrl) - } - res, err := client.RawRequestWithContext(ctx, req) - require.NoError(t, err, "failed sending raw request") - _ = res.Body.Close() - - // Proper Status Code - switch httpOp { - case "get": - require.Equal(t, http.StatusNoContent, res.StatusCode) - case "header": - require.Equal(t, http.StatusOK, res.StatusCode) - } - - // Make sure we don't have a Content-Type header. - require.Equal(t, "", res.Header.Get("Content-Type")) - - // Make sure we return the Cache-Control header - require.Contains(t, res.Header.Get("Cache-Control"), "no-store", - "missing Cache-Control header with no-store header value") - - // Test for our nonce header value - require.NotEmpty(t, res.Header.Get("Replay-Nonce"), "missing Replay-Nonce header with an actual value") - - // Test Link header value - expectedLinkHeader := fmt.Sprintf("<%s>;rel=\"index\"", pathConfig+tc.prefixUrl+"/acme/directory") - require.Contains(t, res.Header.Get("Link"), expectedLinkHeader, - "different value for link header than expected") - }) - } - } -} - -// TestAcmeClusterPathNotConfigured basic testing of the ACME error handler. -func TestAcmeClusterPathNotConfigured(t *testing.T) { - t.Parallel() - cluster, client := setupTestPkiCluster(t) - defer cluster.Cleanup() - - // Go sneaky, sneaky and update the acme configuration through sys/raw to bypass config/cluster path checks - pkiMount := findStorageMountUuid(t, client, "pki") - rawPath := path.Join("/sys/raw/logical/", pkiMount, storageAcmeConfig) - _, err := client.Logical().WriteWithContext(context.Background(), rawPath, map[string]interface{}{ - "value": "{\"enabled\": true, \"eab_policy_name\": \"not-required\"}", - }) - require.NoError(t, err, "failed updating acme config through sys/raw") - - // Force reload the plugin so we read the new config we slipped in. - _, err = client.Sys().ReloadPluginWithContext(context.Background(), &api.ReloadPluginInput{Mounts: []string{"pki"}}) - require.NoError(t, err, "failed reloading plugin") - - // Do not fill in the path option within the local cluster configuration - cases := []struct { - name string - directoryUrl string - }{ - {"root", "pki/acme/directory"}, - {"role", "pki/roles/test-role/acme/directory"}, - {"issuer", "pki/issuer/default/acme/directory"}, - {"issuer_role", "pki/issuer/default/roles/test-role/acme/directory"}, - } - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - dirResp, err := client.Logical().ReadRawWithContext(testCtx, tc.directoryUrl) - require.Error(t, err, "expected failure reading ACME directory configuration got none") - - require.Equal(t, "application/problem+json", dirResp.Header.Get("Content-Type")) - require.Equal(t, http.StatusInternalServerError, dirResp.StatusCode) - - rawBodyBytes, err := io.ReadAll(dirResp.Body) - require.NoError(t, err, "failed reading from directory response body") - _ = dirResp.Body.Close() - - respType := map[string]interface{}{} - err = json.Unmarshal(rawBodyBytes, &respType) - require.NoError(t, err, "failed unmarshalling ACME directory response body") - - require.Equal(t, "urn:ietf:params:acme:error:serverInternal", respType["type"]) - require.NotEmpty(t, respType["detail"]) - }) - } -} - -// TestAcmeAccountsCrossingDirectoryPath make sure that if an account attempts to use a different ACME -// directory path that we get an error. -func TestAcmeAccountsCrossingDirectoryPath(t *testing.T) { - t.Parallel() - cluster, _, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - baseAcmeURL := "/v1/pki/acme/" - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - // Create new account - acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - - // Try to update the account under another ACME directory - baseAcmeURL2 := "/v1/pki/roles/test-role/acme/" - acmeClient2 := getAcmeClientForCluster(t, cluster, baseAcmeURL2, accountKey) - acct.Contact = []string{"mailto:test3@example.com"} - _, err = acmeClient2.UpdateReg(testCtx, acct) - require.Error(t, err, "successfully updated account when we should have failed due to different directory") - // We don't test for the specific error about using the wrong directory, as the golang library - // swallows the error we are sending back to a no account error -} - -// TestAcmeEabCrossingDirectoryPath make sure that if an account attempts to use a different ACME -// directory path that an EAB was created within we get an error. -func TestAcmeEabCrossingDirectoryPath(t *testing.T) { - t.Parallel() - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - // Enable EAB - _, err := client.Logical().WriteWithContext(context.Background(), "pki/config/acme", map[string]interface{}{ - "enabled": true, - "eab_policy": "always-required", - }) - require.NoError(t, err) - - baseAcmeURL := "/v1/pki/acme/" - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - // fetch a new EAB - kid, eabKeyBytes := getEABKey(t, client, "roles/test-role/acme/") - acct := &acme.Account{ - ExternalAccountBinding: &acme.ExternalAccountBinding{ - KID: kid, - Key: eabKeyBytes, - }, - } - - // Create new account - _, err = acmeClient.Register(testCtx, acct, func(tosURL string) bool { return true }) - require.ErrorContains(t, err, "failed to verify eab", "should have failed as EAB is for a different directory") -} - -// TestAcmeDisabledWithEnvVar verifies if VAULT_DISABLE_PUBLIC_ACME is set that we completely -// disable the ACME service -func TestAcmeDisabledWithEnvVar(t *testing.T) { - // Setup a cluster with the configuration set to not-required, initially as the - // configuration will validate if the environment var is set - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - // Seal setup the environment variable, and unseal which now means we have a cluster - // with ACME configuration saying it is enabled with a bad EAB policy. - cluster.EnsureCoresSealed(t) - t.Setenv("VAULT_DISABLE_PUBLIC_ACME", "true") - cluster.UnsealCores(t) - - // Make sure that ACME is disabled now. - for _, method := range []string{http.MethodHead, http.MethodGet} { - t.Run(fmt.Sprintf("%s", method), func(t *testing.T) { - req := client.NewRequest(method, "/v1/pki/acme/new-nonce") - _, err := client.RawRequestWithContext(ctx, req) - require.Error(t, err, "should have received an error as ACME should have been disabled") - - if apiError, ok := err.(*api.ResponseError); ok { - require.Equal(t, 404, apiError.StatusCode) - } - }) - } -} - -// TestAcmeConfigChecksPublicAcmeEnv verifies certain EAB policy values can not be set if ENV var is enabled -func TestAcmeConfigChecksPublicAcmeEnv(t *testing.T) { - t.Setenv("VAULT_DISABLE_PUBLIC_ACME", "true") - cluster, client := setupTestPkiCluster(t) - defer cluster.Cleanup() - - _, err := client.Logical().WriteWithContext(context.Background(), "pki/config/cluster", map[string]interface{}{ - "path": "https://dadgarcorp.com/v1/pki", - }) - require.NoError(t, err) - - _, err = client.Logical().WriteWithContext(context.Background(), "pki/config/acme", map[string]interface{}{ - "enabled": true, - "eab_policy": string(eabPolicyAlwaysRequired), - }) - require.NoError(t, err) - - for _, policyName := range []EabPolicyName{eabPolicyNewAccountRequired, eabPolicyNotRequired} { - _, err = client.Logical().WriteWithContext(context.Background(), "pki/config/acme", map[string]interface{}{ - "enabled": true, - "eab_policy": string(policyName), - }) - require.Error(t, err, "eab policy %s should have not been allowed to be set") - } - - // Make sure we can disable ACME and the eab policy is not checked - _, err = client.Logical().WriteWithContext(context.Background(), "pki/config/acme", map[string]interface{}{ - "enabled": false, - "eab_policy": string(eabPolicyNotRequired), - }) - require.NoError(t, err) -} - -// TestAcmeTruncatesToIssuerExpiry make sure that if the selected issuer's expiry is shorter than the -// CSR's selected TTL value in ACME and the issuer's leaf_not_after_behavior setting is set to Err, -// we will override the configured behavior and truncate to the issuer's NotAfter -func TestAcmeTruncatesToIssuerExpiry(t *testing.T) { - t.Parallel() - - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - mount := "pki" - resp, err := client.Logical().WriteWithContext(context.Background(), mount+"/issuers/generate/intermediate/internal", - map[string]interface{}{ - "key_name": "short-key", - "key_type": "ec", - "common_name": "test.com", - }) - require.NoError(t, err, "failed creating intermediary CSR") - intermediateCSR := resp.Data["csr"].(string) - - // Sign the intermediate CSR using /pki - resp, err = client.Logical().Write(mount+"/issuer/root-ca/sign-intermediate", map[string]interface{}{ - "csr": intermediateCSR, - "ttl": "10m", - "max_ttl": "1h", - }) - require.NoError(t, err, "failed signing intermediary CSR") - intermediateCertPEM := resp.Data["certificate"].(string) - - shortCa := parseCert(t, intermediateCertPEM) - - // Configure the intermediate cert as the CA in /pki2 - resp, err = client.Logical().Write(mount+"/issuers/import/cert", map[string]interface{}{ - "pem_bundle": intermediateCertPEM, - }) - require.NoError(t, err, "failed importing intermediary cert") - importedIssuersRaw := resp.Data["imported_issuers"].([]interface{}) - require.Len(t, importedIssuersRaw, 1) - shortCaUuid := importedIssuersRaw[0].(string) - - _, err = client.Logical().Write(mount+"/issuer/"+shortCaUuid, map[string]interface{}{ - "leaf_not_after_behavior": "err", - "issuer_name": "short-ca", - }) - require.NoError(t, err, "failed updating issuer name") - - baseAcmeURL := "/v1/pki/issuer/short-ca/acme/" - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - // Create new account - t.Logf("Testing register on %s", baseAcmeURL) - acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - - // Create an order - t.Logf("Testing Authorize Order on %s", baseAcmeURL) - identifiers := []string{"*.localdomain"} - order, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{ - {Type: "dns", Value: identifiers[0]}, - }) - require.NoError(t, err, "failed creating order") - - // HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow - // test. - markAuthorizationSuccess(t, client, acmeClient, acct, order) - - // Build a proper CSR, with the correct name and signed with a different key works. - goodCr := &x509.CertificateRequest{DNSNames: []string{identifiers[0]}} - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated key for CSR") - csr, err := x509.CreateCertificateRequest(rand.Reader, goodCr, csrKey) - require.NoError(t, err, "failed generating csr") - - certs, _, err := acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, true) - require.NoError(t, err, "failed finalizing order") - require.Len(t, certs, 3, "expected full acme chain") - - testAcmeCertSignedByCa(t, client, certs, "short-ca") - - acmeCert, err := x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert") - - require.Equal(t, shortCa.NotAfter, acmeCert.NotAfter, "certificate times aren't the same") -} - -// TestAcmeRoleExtKeyUsage verify that ACME by default ignores the role's various ExtKeyUsage flags, -// but if the ACME configuration override of allow_role_ext_key_usage is set that we then honor -// the role's flag. -func TestAcmeRoleExtKeyUsage(t *testing.T) { - t.Parallel() - - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - roleName := "test-role" - - roleOpt := map[string]interface{}{ - "ttl": "365h", - "max_ttl": "720h", - "key_type": "any", - "allowed_domains": "localdomain", - "allow_subdomains": "true", - "allow_wildcard_certificates": "true", - "require_cn": "true", /* explicit default */ - "server_flag": "true", - "client_flag": "true", - "code_signing_flag": "true", - "email_protection_flag": "true", - } - - _, err := client.Logical().Write("pki/roles/"+roleName, roleOpt) - - baseAcmeURL := "/v1/pki/roles/" + roleName + "/acme/" - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - require.NoError(t, err, "failed creating role test-role") - - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - // Create new account - t.Logf("Testing register on %s", baseAcmeURL) - acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - - // Create an order - t.Logf("Testing Authorize Order on %s", baseAcmeURL) - identifiers := []string{"*.localdomain"} - order, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{ - {Type: "dns", Value: identifiers[0]}, - }) - require.NoError(t, err, "failed creating order") - - // HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow test. - markAuthorizationSuccess(t, client, acmeClient, acct, order) - - // Build a proper CSR, with the correct name and signed with a different key works. - goodCr := &x509.CertificateRequest{DNSNames: []string{identifiers[0]}} - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated key for CSR") - csr, err := x509.CreateCertificateRequest(rand.Reader, goodCr, csrKey) - require.NoError(t, err, "failed generating csr") - - certs, _, err := acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, true) - require.NoError(t, err, "order finalization failed") - require.GreaterOrEqual(t, len(certs), 1, "expected at least one cert in bundle") - acmeCert, err := x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert") - - require.Equal(t, 1, len(acmeCert.ExtKeyUsage), "mis-match on expected ExtKeyUsages") - require.ElementsMatch(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, acmeCert.ExtKeyUsage, - "mismatch of ExtKeyUsage flags") - - // Now turn the ACME configuration allow_role_ext_key_usage and retest to make sure we get a certificate - // with them all - _, err = client.Logical().WriteWithContext(context.Background(), "pki/config/acme", map[string]interface{}{ - "enabled": true, - "eab_policy": "not-required", - "allow_role_ext_key_usage": true, - }) - require.NoError(t, err, "failed updating ACME configuration") - - t.Logf("Testing Authorize Order on %s", baseAcmeURL) - order, err = acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{ - {Type: "dns", Value: identifiers[0]}, - }) - require.NoError(t, err, "failed creating order") - - // HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow test. - markAuthorizationSuccess(t, client, acmeClient, acct, order) - - certs, _, err = acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, true) - require.NoError(t, err, "order finalization failed") - require.GreaterOrEqual(t, len(certs), 1, "expected at least one cert in bundle") - acmeCert, err = x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert") - - require.Equal(t, 4, len(acmeCert.ExtKeyUsage), "mis-match on expected ExtKeyUsages") - require.ElementsMatch(t, []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth, - x509.ExtKeyUsageCodeSigning, x509.ExtKeyUsageEmailProtection, - }, - acmeCert.ExtKeyUsage, "mismatch of ExtKeyUsage flags") -} - -func TestIssuerRoleDirectoryAssociations(t *testing.T) { - t.Parallel() - - // This creates two issuers for us (root-ca, int-ca) and two - // roles (test-role, acme) that we can use with various directory - // configurations. - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - // Setup DNS for validations. - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - dns := dnstest.SetupResolver(t, "dadgarcorp.com") - defer dns.Cleanup() - _, err := client.Logical().WriteWithContext(testCtx, "pki/config/acme", map[string]interface{}{ - "dns_resolver": dns.GetLocalAddr(), - }) - require.NoError(t, err, "failed to specify dns resolver") - - // 1. Use a forbidden role should fail. - resp, err := client.Logical().WriteWithContext(testCtx, "pki/config/acme", map[string]interface{}{ - "enabled": true, - "allowed_roles": []string{"acme"}, - }) - require.NoError(t, err, "failed to write config") - require.NotNil(t, resp) - - _, err = client.Logical().ReadWithContext(testCtx, "pki/roles/test-role/acme/directory") - require.Error(t, err, "failed to forbid usage of test-role") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/default/roles/test-role/acme/directory") - require.Error(t, err, "failed to forbid usage of test-role under default issuer") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/int-ca/roles/test-role/acme/directory") - require.Error(t, err, "failed to forbid usage of test-role under int-ca issuer") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/root-ca/roles/test-role/acme/directory") - require.Error(t, err, "failed to forbid usage of test-role under root-ca issuer") - - _, err = client.Logical().ReadWithContext(testCtx, "pki/roles/acme/acme/directory") - require.NoError(t, err, "failed to allow usage of acme") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/default/roles/acme/acme/directory") - require.NoError(t, err, "failed to allow usage of acme under default issuer") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/int-ca/roles/acme/acme/directory") - require.NoError(t, err, "failed to allow usage of acme under int-ca issuer") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/root-ca/roles/acme/acme/directory") - require.NoError(t, err, "failed to allow usage of acme under root-ca issuer") - - // 2. Use a forbidden issuer should fail. - resp, err = client.Logical().WriteWithContext(testCtx, "pki/config/acme", map[string]interface{}{ - "allowed_roles": []string{"acme"}, - "allowed_issuers": []string{"int-ca"}, - }) - require.NoError(t, err, "failed to write config") - require.NotNil(t, resp) - - _, err = client.Logical().ReadWithContext(testCtx, "pki/roles/test-role/acme/directory") - require.Error(t, err, "failed to forbid usage of test-role") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/default/roles/test-role/acme/directory") - require.Error(t, err, "failed to forbid usage of test-role under default issuer") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/int-ca/roles/test-role/acme/directory") - require.Error(t, err, "failed to forbid usage of test-role under int-ca issuer") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/root-ca/roles/test-role/acme/directory") - require.Error(t, err, "failed to forbid usage of test-role under root-ca issuer") - - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/root-ca/roles/acme/acme/directory") - require.Error(t, err, "failed to forbid usage of acme under root-ca issuer") - - _, err = client.Logical().ReadWithContext(testCtx, "pki/roles/acme/acme/directory") - require.NoError(t, err, "failed to allow usage of acme") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/default/roles/acme/acme/directory") - require.NoError(t, err, "failed to allow usage of acme under default issuer") - _, err = client.Logical().ReadWithContext(testCtx, "pki/issuer/int-ca/roles/acme/acme/directory") - require.NoError(t, err, "failed to allow usage of acme under int-ca issuer") - - // 3. Setting the default directory to be a sign-verbatim policy and - // using two different CAs should result in certs signed by each CA. - resp, err = client.Logical().WriteWithContext(testCtx, "pki/config/acme", map[string]interface{}{ - "allowed_roles": []string{"*"}, - "allowed_issuers": []string{"*"}, - "default_directory_policy": "sign-verbatim", - }) - require.NoError(t, err, "failed to write config") - require.NotNil(t, resp) - - // default == int-ca - acmeClientDefault := getAcmeClientForCluster(t, cluster, "/v1/pki/issuer/default/acme/", nil) - defaultLeafCert := doACMEForDomainWithDNS(t, dns, acmeClientDefault, []string{"default-ca.dadgarcorp.com"}) - requireSignedByAtPath(t, client, defaultLeafCert, "pki/issuer/int-ca") - - acmeClientIntCA := getAcmeClientForCluster(t, cluster, "/v1/pki/issuer/int-ca/acme/", nil) - intCALeafCert := doACMEForDomainWithDNS(t, dns, acmeClientIntCA, []string{"int-ca.dadgarcorp.com"}) - requireSignedByAtPath(t, client, intCALeafCert, "pki/issuer/int-ca") - - acmeClientRootCA := getAcmeClientForCluster(t, cluster, "/v1/pki/issuer/root-ca/acme/", nil) - rootCALeafCert := doACMEForDomainWithDNS(t, dns, acmeClientRootCA, []string{"root-ca.dadgarcorp.com"}) - requireSignedByAtPath(t, client, rootCALeafCert, "pki/issuer/root-ca") - - // 4. Using a role-based default directory should allow us to control leaf - // issuance on the base and issuer-specific directories. - resp, err = client.Logical().WriteWithContext(testCtx, "pki/config/acme", map[string]interface{}{ - "allowed_roles": []string{"*"}, - "allowed_issuers": []string{"*"}, - "default_directory_policy": "role:acme", - }) - require.NoError(t, err, "failed to write config") - require.NotNil(t, resp) - - resp, err = client.Logical().JSONMergePatch(testCtx, "pki/roles/acme", map[string]interface{}{ - "ou": "IT Security", - "organization": []string{"Dadgar Corporation, Limited"}, - "allow_any_name": true, - }) - require.NoError(t, err, "failed to write role differentiator") - require.NotNil(t, resp) - - for _, issuer := range []string{"", "default", "int-ca", "root-ca"} { - // Path should override role. - directory := "/v1/pki/issuer/" + issuer + "/acme/" - issuerPath := "/pki/issuer/" + issuer - if issuer == "" { - directory = "/v1/pki/acme/" - issuerPath = "/pki/issuer/int-ca" - } else if issuer == "default" { - issuerPath = "/pki/issuer/int-ca" - } - - t.Logf("using directory: %v / issuer: %v", directory, issuerPath) - - acmeClient := getAcmeClientForCluster(t, cluster, directory, nil) - leafCert := doACMEForDomainWithDNS(t, dns, acmeClient, []string{"role-restricted.dadgarcorp.com"}) - require.Contains(t, leafCert.Subject.Organization, "Dadgar Corporation, Limited", "on directory: %v", directory) - require.Contains(t, leafCert.Subject.OrganizationalUnit, "IT Security", "on directory: %v", directory) - requireSignedByAtPath(t, client, leafCert, issuerPath) - } -} - -func TestACMESubjectFieldsAndExtensionsIgnored(t *testing.T) { - t.Parallel() - - // This creates two issuers for us (root-ca, int-ca) and two - // roles (test-role, acme) that we can use with various directory - // configurations. - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - // Setup DNS for validations. - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - dns := dnstest.SetupResolver(t, "dadgarcorp.com") - defer dns.Cleanup() - _, err := client.Logical().WriteWithContext(testCtx, "pki/config/acme", map[string]interface{}{ - "dns_resolver": dns.GetLocalAddr(), - }) - require.NoError(t, err, "failed to specify dns resolver") - - // Use the default sign-verbatim policy and ensure OU does not get set. - directory := "/v1/pki/acme/" - domains := []string{"no-ou.dadgarcorp.com"} - acmeClient := getAcmeClientForCluster(t, cluster, directory, nil) - cr := &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: domains[0], OrganizationalUnit: []string{"DadgarCorp IT"}}, - DNSNames: domains, - } - cert := doACMEForCSRWithDNS(t, dns, acmeClient, domains, cr) - t.Logf("Got certificate: %v", cert) - require.Empty(t, cert.Subject.OrganizationalUnit) - - // Use the default sign-verbatim policy and ensure extension does not get set. - domains = []string{"no-ext.dadgarcorp.com"} - extension, err := certutil.CreateDeltaCRLIndicatorExt(12345) - require.NoError(t, err) - cr = &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: domains[0]}, - DNSNames: domains, - ExtraExtensions: []pkix.Extension{extension}, - } - cert = doACMEForCSRWithDNS(t, dns, acmeClient, domains, cr) - t.Logf("Got certificate: %v", cert) - for _, ext := range cert.Extensions { - require.False(t, ext.Id.Equal(certutil.DeltaCRLIndicatorOID)) - } - require.NotEmpty(t, cert.Extensions) -} - -// TestAcmeWithCsrIncludingBasicConstraintExtension verify that we error out for a CSR that is requesting a -// certificate with the IsCA set to true, false is okay, within the basic constraints extension and that no matter what -// the extension is not present on the returned certificate. -func TestAcmeWithCsrIncludingBasicConstraintExtension(t *testing.T) { - t.Parallel() - - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - baseAcmeURL := "/v1/pki/acme/" - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - // Create new account - t.Logf("Testing register on %s", baseAcmeURL) - acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - - // Create an order - t.Logf("Testing Authorize Order on %s", baseAcmeURL) - identifiers := []string{"*.localdomain"} - order, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{ - {Type: "dns", Value: identifiers[0]}, - }) - require.NoError(t, err, "failed creating order") - - // HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow test. - markAuthorizationSuccess(t, client, acmeClient, acct, order) - - // Build a CSR with IsCA set to true, making sure we reject it - extension, err := certutil.CreateBasicConstraintExtension(true, -1) - require.NoError(t, err, "failed generating basic constraint extension") - - isCATrueCSR := &x509.CertificateRequest{ - DNSNames: []string{identifiers[0]}, - ExtraExtensions: []pkix.Extension{extension}, - } - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated key for CSR") - csr, err := x509.CreateCertificateRequest(rand.Reader, isCATrueCSR, csrKey) - require.NoError(t, err, "failed generating csr") - - _, _, err = acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, true) - require.Error(t, err, "order finalization should have failed with IsCA set to true") - - extension, err = certutil.CreateBasicConstraintExtension(false, -1) - require.NoError(t, err, "failed generating basic constraint extension") - isCAFalseCSR := &x509.CertificateRequest{ - DNSNames: []string{identifiers[0]}, - Extensions: []pkix.Extension{extension}, - } - - csr, err = x509.CreateCertificateRequest(rand.Reader, isCAFalseCSR, csrKey) - require.NoError(t, err, "failed generating csr") - - certs, _, err := acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, true) - require.NoError(t, err, "order finalization should have failed with IsCA set to false") - - require.GreaterOrEqual(t, len(certs), 1, "expected at least one cert in bundle") - acmeCert, err := x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert") - - // Make sure we don't have any basic constraint extension within the returned cert - for _, ext := range acmeCert.Extensions { - if ext.Id.Equal(certutil.ExtensionBasicConstraintsOID) { - // We shouldn't have this extension in our cert - t.Fatalf("acme csr contained a basic constraints extension") - } - } -} - -func markAuthorizationSuccess(t *testing.T, client *api.Client, acmeClient *acme.Client, acct *acme.Account, order *acme.Order) { - testCtx := context.Background() - - pkiMount := findStorageMountUuid(t, client, "pki") - - // Delete any and all challenge validation entries to stop the engine from overwriting our hack here - i := 0 - for { - deleteCvEntries(t, client, pkiMount) - - accountId := acct.URI[strings.LastIndex(acct.URI, "/"):] - for _, authURI := range order.AuthzURLs { - authId := authURI[strings.LastIndex(authURI, "/"):] - - // sys/raw does not work with namespaces - baseClient := client.WithNamespace("") - - values, err := baseClient.Logical().ListWithContext(testCtx, "sys/raw/logical/") - require.NoError(t, err) - require.True(t, true, "values: %v", values) - - rawPath := path.Join("sys/raw/logical/", pkiMount, getAuthorizationPath(accountId, authId)) - resp, err := baseClient.Logical().ReadWithContext(testCtx, rawPath) - require.NoError(t, err, "failed looking up authorization storage") - require.NotNil(t, resp, "sys raw response was nil") - require.NotEmpty(t, resp.Data["value"], "no value field in sys raw response") - - var authz ACMEAuthorization - err = jsonutil.DecodeJSON([]byte(resp.Data["value"].(string)), &authz) - require.NoError(t, err, "error decoding authorization: %w", err) - authz.Status = ACMEAuthorizationValid - for _, challenge := range authz.Challenges { - challenge.Status = ACMEChallengeValid - } - - encodeJSON, err := jsonutil.EncodeJSON(authz) - require.NoError(t, err, "failed encoding authz json") - _, err = baseClient.Logical().WriteWithContext(testCtx, rawPath, map[string]interface{}{ - "value": base64.StdEncoding.EncodeToString(encodeJSON), - "encoding": "base64", - }) - require.NoError(t, err, "failed writing authorization storage") - } - - // Give some time - time.Sleep(200 * time.Millisecond) - - // Check to see if we have fixed up the status and no new entries have appeared. - if !deleteCvEntries(t, client, pkiMount) { - // No entries found - // Look to see if we raced against the engine - orderLookup, err := acmeClient.GetOrder(testCtx, order.URI) - require.NoError(t, err, "failed loading order status after manually ") - - if orderLookup.Status == string(ACMEOrderReady) { - // Our order seems to be in the proper status, should be safe-ish to go ahead now - break - } else { - t.Logf("order status was not ready, retrying") - } - } else { - t.Logf("new challenge entries appeared after deletion, retrying") - } - - if i > 5 { - t.Fatalf("We are constantly deleting cv entries or order status is not changing, something is wrong") - } - - i++ - } -} - -func deleteCvEntries(t *testing.T, client *api.Client, pkiMount string) bool { - testCtx := context.Background() - - baseClient := client.WithNamespace("") - - cvPath := path.Join("sys/raw/logical/", pkiMount, acmeValidationPrefix) - resp, err := baseClient.Logical().ListWithContext(testCtx, cvPath) - require.NoError(t, err, "failed listing cv path items") - - deletedEntries := false - if resp != nil { - cvEntries := resp.Data["keys"].([]interface{}) - for _, cvEntry := range cvEntries { - cvEntryPath := path.Join(cvPath, cvEntry.(string)) - _, err = baseClient.Logical().DeleteWithContext(testCtx, cvEntryPath) - require.NoError(t, err, "failed to delete cv entry") - deletedEntries = true - } - } - - return deletedEntries -} - -func setupAcmeBackend(t *testing.T) (*vault.TestCluster, *api.Client, string) { - cluster, client := setupTestPkiCluster(t) - - return setupAcmeBackendOnClusterAtPath(t, cluster, client, "pki") -} - -func setupAcmeBackendOnClusterAtPath(t *testing.T, cluster *vault.TestCluster, client *api.Client, mount string) (*vault.TestCluster, *api.Client, string) { - mount = strings.Trim(mount, "/") - - // Setting templated AIAs should succeed. - pathConfig := client.Address() + "/v1/" + mount - - namespace := "" - mountName := mount - if mount != "pki" { - if strings.Contains(mount, "/") && constants.IsEnterprise { - ns_pieces := strings.Split(mount, "/") - c := len(ns_pieces) - // mount is c-1 - ns_name := ns_pieces[c-2] - if len(ns_pieces) > 2 { - // Parent's namespaces - parent := strings.Join(ns_pieces[0:c-2], "/") - _, err := client.WithNamespace(parent).Logical().Write("/sys/namespaces/"+ns_name, nil) - require.NoError(t, err, "failed to create nested namespaces "+parent+" -> "+ns_name) - } else { - _, err := client.Logical().Write("/sys/namespaces/"+ns_name, nil) - require.NoError(t, err, "failed to create nested namespace "+ns_name) - } - namespace = strings.Join(ns_pieces[0:c-1], "/") - mountName = ns_pieces[c-1] - } - - err := client.WithNamespace(namespace).Sys().Mount(mountName, &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "3000h", - MaxLeaseTTL: "600000h", - }, - }) - require.NoError(t, err, "failed to mount new PKI instance at "+mount) - } - - err := client.Sys().TuneMountWithContext(ctx, mount, api.MountConfigInput{ - DefaultLeaseTTL: "3000h", - MaxLeaseTTL: "600000h", - }) - require.NoError(t, err, "failed updating mount lease times "+mount) - - _, err = client.Logical().WriteWithContext(context.Background(), mount+"/config/cluster", map[string]interface{}{ - "path": pathConfig, - "aia_path": "http://localhost:8200/cdn/" + mount, - }) - require.NoError(t, err) - - _, err = client.Logical().WriteWithContext(context.Background(), mount+"/config/acme", map[string]interface{}{ - "enabled": true, - "eab_policy": "not-required", - }) - require.NoError(t, err) - - // Allow certain headers to pass through for ACME support - _, err = client.WithNamespace(namespace).Logical().WriteWithContext(context.Background(), "sys/mounts/"+mountName+"/tune", map[string]interface{}{ - "allowed_response_headers": []string{"Last-Modified", "Replay-Nonce", "Link", "Location"}, - "max_lease_ttl": "920000h", - }) - require.NoError(t, err, "failed tuning mount response headers") - - resp, err := client.Logical().WriteWithContext(context.Background(), mount+"/issuers/generate/root/internal", - map[string]interface{}{ - "issuer_name": "root-ca", - "key_name": "root-key", - "key_type": "ec", - "common_name": "Test Root R1 " + mount, - "ttl": "7200h", - "max_ttl": "920000h", - }) - require.NoError(t, err, "failed creating root CA") - - resp, err = client.Logical().WriteWithContext(context.Background(), mount+"/issuers/generate/intermediate/internal", - map[string]interface{}{ - "key_name": "int-key", - "key_type": "ec", - "common_name": "Test Int X1 " + mount, - }) - require.NoError(t, err, "failed creating intermediary CSR") - intermediateCSR := resp.Data["csr"].(string) - - // Sign the intermediate CSR using /pki - resp, err = client.Logical().Write(mount+"/issuer/root-ca/sign-intermediate", map[string]interface{}{ - "csr": intermediateCSR, - "ttl": "7100h", - "max_ttl": "910000h", - }) - require.NoError(t, err, "failed signing intermediary CSR") - intermediateCertPEM := resp.Data["certificate"].(string) - - // Configure the intermediate cert as the CA in /pki2 - resp, err = client.Logical().Write(mount+"/issuers/import/cert", map[string]interface{}{ - "pem_bundle": intermediateCertPEM, - }) - require.NoError(t, err, "failed importing intermediary cert") - importedIssuersRaw := resp.Data["imported_issuers"].([]interface{}) - require.Len(t, importedIssuersRaw, 1) - intCaUuid := importedIssuersRaw[0].(string) - - _, err = client.Logical().Write(mount+"/issuer/"+intCaUuid, map[string]interface{}{ - "issuer_name": "int-ca", - }) - require.NoError(t, err, "failed updating issuer name") - - _, err = client.Logical().Write(mount+"/config/issuers", map[string]interface{}{ - "default": "int-ca", - }) - require.NoError(t, err, "failed updating default issuer") - - _, err = client.Logical().Write(mount+"/roles/test-role", map[string]interface{}{ - "ttl": "168h", - "max_ttl": "168h", - "key_type": "any", - "allowed_domains": "localdomain", - "allow_subdomains": "true", - "allow_wildcard_certificates": "true", - }) - require.NoError(t, err, "failed creating role test-role") - - _, err = client.Logical().Write(mount+"/roles/acme", map[string]interface{}{ - "ttl": "3650h", - "max_ttl": "7200h", - "key_type": "any", - }) - require.NoError(t, err, "failed creating role acme") - - return cluster, client, pathConfig -} - -func testAcmeCertSignedByCa(t *testing.T, client *api.Client, derCerts [][]byte, issuerRef string) { - t.Helper() - require.NotEmpty(t, derCerts) - acmeCert, err := x509.ParseCertificate(derCerts[0]) - require.NoError(t, err, "failed parsing acme cert bytes") - - resp, err := client.Logical().ReadWithContext(context.Background(), "pki/issuer/"+issuerRef) - require.NoError(t, err, "failed reading issuer with name %s", issuerRef) - issuerCert := parseCert(t, resp.Data["certificate"].(string)) - issuerChainRaw := resp.Data["ca_chain"].([]interface{}) - - err = acmeCert.CheckSignatureFrom(issuerCert) - require.NoError(t, err, "issuer %s did not sign provided cert", issuerRef) - - expectedCerts := [][]byte{derCerts[0]} - - for _, entry := range issuerChainRaw { - chainCert := parseCert(t, entry.(string)) - expectedCerts = append(expectedCerts, chainCert.Raw) - } - - if diffs := deep.Equal(expectedCerts, derCerts); diffs != nil { - t.Fatalf("diffs were found between the acme chain returned and the expected value: \n%v", diffs) - } -} - -// TestAcmeValidationError make sure that we properly return errors on validation errors. -func TestAcmeValidationError(t *testing.T) { - t.Parallel() - cluster, _, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - baseAcmeURL := "/v1/pki/acme/" - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - // Create new account - t.Logf("Testing register on %s", baseAcmeURL) - _, err = acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - - // Create an order - t.Logf("Testing Authorize Order on %s", baseAcmeURL) - identifiers := []string{"www.dadgarcorp.com"} - order, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{ - {Type: "dns", Value: identifiers[0]}, - }) - require.NoError(t, err, "failed creating order") - - // Load authorizations - var authorizations []*acme.Authorization - for _, authUrl := range order.AuthzURLs { - auth, err := acmeClient.GetAuthorization(testCtx, authUrl) - require.NoError(t, err, "failed fetching authorization: %s", authUrl) - - authorizations = append(authorizations, auth) - } - require.Len(t, authorizations, 1, "expected a certain number of authorizations") - require.Len(t, authorizations[0].Challenges, 3, "expected a certain number of challenges associated with authorization") - - acceptedAuth, err := acmeClient.Accept(testCtx, authorizations[0].Challenges[0]) - require.NoError(t, err, "Should have been allowed to accept challenge 1") - require.Equal(t, string(ACMEChallengeProcessing), acceptedAuth.Status) - - _, err = acmeClient.Accept(testCtx, authorizations[0].Challenges[1]) - require.Error(t, err, "Should have been prevented to accept challenge 2") - - // Make sure our challenge returns errors - testhelpers.RetryUntil(t, 30*time.Second, func() error { - challenge, err := acmeClient.GetChallenge(testCtx, authorizations[0].Challenges[0].URI) - if err != nil { - return err - } - - if challenge.Error == nil { - return fmt.Errorf("no error set in challenge yet") - } - - acmeError, ok := challenge.Error.(*acme.Error) - if !ok { - return fmt.Errorf("unexpected error back: %v", err) - } - - if acmeError.ProblemType != "urn:ietf:params:acme:error:incorrectResponse" { - return fmt.Errorf("unexpected ACME error back: %v", acmeError) - } - - return nil - }) - - // Make sure our challenge,auth and order status change. - // This takes a little too long to run in CI properly, we need the ability to influence - // how long the validations take before CI can go wild on this. - if os.Getenv("CI") == "" { - testhelpers.RetryUntil(t, 10*time.Minute, func() error { - challenge, err := acmeClient.GetChallenge(testCtx, authorizations[0].Challenges[0].URI) - if err != nil { - return fmt.Errorf("failed to load challenge: %w", err) - } - - if challenge.Status != string(ACMEChallengeInvalid) { - return fmt.Errorf("challenge state was not changed to invalid: %v", challenge) - } - - authz, err := acmeClient.GetAuthorization(testCtx, authorizations[0].URI) - if err != nil { - return fmt.Errorf("failed to load authorization: %w", err) - } - - if authz.Status != string(ACMEAuthorizationInvalid) { - return fmt.Errorf("authz state was not changed to invalid: %v", authz) - } - - myOrder, err := acmeClient.GetOrder(testCtx, order.URI) - if err != nil { - return fmt.Errorf("failed to load order: %w", err) - } - - if myOrder.Status != string(ACMEOrderInvalid) { - return fmt.Errorf("order state was not changed to invalid: %v", order) - } - - return nil - }) - } -} - -// TestAcmeRevocationAcrossAccounts makes sure that we can revoke certificates using different accounts if -// we have another ACME account or not but access to the certificate key. Also verifies we can't revoke -// certificates across account keys. -func TestAcmeRevocationAcrossAccounts(t *testing.T) { - t.Parallel() - - cluster, vaultClient, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - baseAcmeURL := "/v1/pki/acme/" - accountKey1, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - acmeClient1 := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey1) - - leafKey, certs := doACMEWorkflow(t, vaultClient, acmeClient1) - acmeCert, err := x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert bytes") - - // Make sure our cert is not revoked - certResp, err := vaultClient.Logical().ReadWithContext(ctx, "pki/cert/"+serialFromCert(acmeCert)) - require.NoError(t, err, "failed to read certificate status") - require.NotNil(t, certResp, "certificate status response was nil") - revocationTime := certResp.Data["revocation_time"].(json.Number) - revocationTimeInt, err := revocationTime.Int64() - require.NoError(t, err, "failed converting revocation_time value: %v", revocationTime) - require.Equal(t, revocationTimeInt, int64(0), - "revocation time was not 0, cert was already revoked: %v", revocationTimeInt) - - // Test that we can't revoke the certificate with another account's key - accountKey2, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - require.NoError(t, err, "failed creating rsa key") - - acmeClient2 := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey2) - _, err = acmeClient2.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering second account") - - err = acmeClient2.RevokeCert(ctx, nil, certs[0], acme.CRLReasonUnspecified) - require.Error(t, err, "should have failed revoking the certificate with a different account") - - // Make sure our cert is not revoked - certResp, err = vaultClient.Logical().ReadWithContext(ctx, "pki/cert/"+serialFromCert(acmeCert)) - require.NoError(t, err, "failed to read certificate status") - require.NotNil(t, certResp, "certificate status response was nil") - revocationTime = certResp.Data["revocation_time"].(json.Number) - revocationTimeInt, err = revocationTime.Int64() - require.NoError(t, err, "failed converting revocation_time value: %v", revocationTime) - require.Equal(t, revocationTimeInt, int64(0), - "revocation time was not 0, cert was already revoked: %v", revocationTimeInt) - - // But we can revoke if we sign the request with the certificate's key and a different account - err = acmeClient2.RevokeCert(ctx, leafKey, certs[0], acme.CRLReasonUnspecified) - require.NoError(t, err, "should have been allowed to revoke certificate with csr key across accounts") - - // Make sure our cert is now revoked - certResp, err = vaultClient.Logical().ReadWithContext(ctx, "pki/cert/"+serialFromCert(acmeCert)) - require.NoError(t, err, "failed to read certificate status") - require.NotNil(t, certResp, "certificate status response was nil") - revocationTime = certResp.Data["revocation_time"].(json.Number) - revocationTimeInt, err = revocationTime.Int64() - require.NoError(t, err, "failed converting revocation_time value: %v", revocationTime) - require.Greater(t, revocationTimeInt, int64(0), - "revocation time was not greater than 0, cert was not revoked: %v", revocationTimeInt) - - // Make sure we can revoke a certificate without a registered ACME account - leafKey2, certs2 := doACMEWorkflow(t, vaultClient, acmeClient1) - - acmeClient3 := getAcmeClientForCluster(t, cluster, baseAcmeURL, nil) - err = acmeClient3.RevokeCert(ctx, leafKey2, certs2[0], acme.CRLReasonUnspecified) - require.NoError(t, err, "should be allowed to revoke a cert with no ACME account but with cert key") - - // Make sure our cert is now revoked - acmeCert2, err := x509.ParseCertificate(certs2[0]) - require.NoError(t, err, "failed parsing acme cert 2 bytes") - - certResp, err = vaultClient.Logical().ReadWithContext(ctx, "pki/cert/"+serialFromCert(acmeCert2)) - require.NoError(t, err, "failed to read certificate status") - require.NotNil(t, certResp, "certificate status response was nil") - revocationTime = certResp.Data["revocation_time"].(json.Number) - revocationTimeInt, err = revocationTime.Int64() - require.NoError(t, err, "failed converting revocation_time value: %v", revocationTime) - require.Greater(t, revocationTimeInt, int64(0), - "revocation time was not greater than 0, cert was not revoked: %v", revocationTimeInt) -} - -func doACMEWorkflow(t *testing.T, vaultClient *api.Client, acmeClient *acme.Client) (*ecdsa.PrivateKey, [][]byte) { - testCtx := context.Background() - - // Create new account - acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - if err != nil { - if strings.Contains(err.Error(), "acme: account already exists") { - acct, err = acmeClient.GetReg(testCtx, "") - require.NoError(t, err, "failed looking up account after account exists error?") - } else { - require.NoError(t, err, "failed registering account") - } - } - - // Create an order - identifiers := []string{"*.localdomain"} - order, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{ - {Type: "dns", Value: identifiers[0]}, - }) - require.NoError(t, err, "failed creating order") - - // HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow - // test. - markAuthorizationSuccess(t, vaultClient, acmeClient, acct, order) - - // Build a proper CSR, with the correct name and signed with a different key works. - goodCr := &x509.CertificateRequest{DNSNames: []string{identifiers[0]}} - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated key for CSR") - csr, err := x509.CreateCertificateRequest(rand.Reader, goodCr, csrKey) - require.NoError(t, err, "failed generating csr") - - certs, _, err := acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, true) - require.NoError(t, err, "failed finalizing order") - require.Len(t, certs, 3, "expected full acme chain") - - return csrKey, certs -} - -func setupTestPkiCluster(t *testing.T) (*vault.TestCluster, *api.Client) { - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - EnableRaw: true, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - client := cluster.Cores[0].Client - mountPKIEndpoint(t, client, "pki") - return cluster, client -} - -func getAcmeClientForCluster(t *testing.T, cluster *vault.TestCluster, baseUrl string, key crypto.Signer) *acme.Client { - coreAddr := cluster.Cores[0].Listeners[0].Address - tlsConfig := cluster.Cores[0].TLSConfig() - - transport := cleanhttp.DefaultPooledTransport() - transport.TLSClientConfig = tlsConfig.Clone() - if err := http2.ConfigureTransport(transport); err != nil { - t.Fatal(err) - } - httpClient := &http.Client{Transport: transport} - if baseUrl[0] == '/' { - baseUrl = baseUrl[1:] - } - if !strings.HasPrefix(baseUrl, "v1/") { - baseUrl = "v1/" + baseUrl - } - if !strings.HasSuffix(baseUrl, "/") { - baseUrl = baseUrl + "/" - } - baseAcmeURL := fmt.Sprintf("https://%s/%s", coreAddr.String(), baseUrl) - return &acme.Client{ - Key: key, - HTTPClient: httpClient, - DirectoryURL: baseAcmeURL + "directory", - } -} - -func getEABKey(t *testing.T, client *api.Client, baseUrl string) (string, []byte) { - resp, err := client.Logical().WriteWithContext(ctx, path.Join("pki/", baseUrl, "/new-eab"), map[string]interface{}{}) - require.NoError(t, err, "failed getting eab key") - require.NotNil(t, resp, "eab key returned nil response") - require.NotEmpty(t, resp.Data["id"], "eab key response missing id field") - kid := resp.Data["id"].(string) - - require.NotEmpty(t, resp.Data["key"], "eab key response missing private_key field") - base64Key := resp.Data["key"].(string) - require.True(t, strings.HasPrefix(base64Key, "vault-eab-0-"), "%s should have had a prefix of vault-eab-0-", base64Key) - privateKeyBytes, err := base64.RawURLEncoding.DecodeString(base64Key) - require.NoError(t, err, "failed base 64 decoding eab key response") - - require.Equal(t, "hs", resp.Data["key_type"], "eab key_type field mis-match") - require.Equal(t, baseUrl+"directory", resp.Data["acme_directory"], "eab acme_directory field mis-match") - require.NotEmpty(t, resp.Data["created_on"], "empty created_on field") - _, err = time.Parse(time.RFC3339, resp.Data["created_on"].(string)) - require.NoError(t, err, "failed parsing eab created_on field") - - return kid, privateKeyBytes -} - -func TestACMEClientRequestLimits(t *testing.T) { - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - cases := []struct { - name string - authorizations []acme.AuthzID - requestCSR x509.CertificateRequest - valid bool - }{ - { - "validate-only-cn", - []acme.AuthzID{ - {"dns", "localhost"}, - }, - x509.CertificateRequest{ - Subject: pkix.Name{CommonName: "localhost"}, - }, - true, - }, - { - "validate-only-san", - []acme.AuthzID{ - {"dns", "localhost"}, - }, - x509.CertificateRequest{ - DNSNames: []string{"localhost"}, - }, - true, - }, - { - "validate-only-ip-address", - []acme.AuthzID{ - {"ip", "127.0.0.1"}, - }, - x509.CertificateRequest{ - IPAddresses: []net.IP{{127, 0, 0, 1}}, - }, - true, - }, - } - - testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - acmeConfig := map[string]interface{}{ - "enabled": true, - "allowed_issuers": "*", - "allowed_roles": "*", - "default_directory_policy": "sign-verbatim", - "dns_resolver": "", - "eab_policy_name": "", - } - _, err := client.Logical().WriteWithContext(testCtx, "pki/config/acme", acmeConfig) - require.NoError(t, err, "error configuring acme") - - for _, tc := range cases { - - // First Create Our Client - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - acmeClient := getAcmeClientForCluster(t, cluster, "/v1/pki/acme/", accountKey) - - discovery, err := acmeClient.Discover(testCtx) - require.NoError(t, err, "failed acme discovery call") - t.Logf("%v", discovery) - - acct, err := acmeClient.Register(testCtx, &acme.Account{ - Contact: []string{"mailto:test@example.com"}, - }, func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - require.Equal(t, acme.StatusValid, acct.Status) - require.Contains(t, acct.Contact, "mailto:test@example.com") - require.Len(t, acct.Contact, 1) - - // Create an order - t.Logf("Testing Authorize Order on %s", "pki/acme") - identifiers := make([]string, len(tc.authorizations)) - for index, auth := range tc.authorizations { - identifiers[index] = auth.Value - } - - createOrder, err := acmeClient.AuthorizeOrder(testCtx, tc.authorizations) - require.NoError(t, err, "failed creating order") - require.Equal(t, acme.StatusPending, createOrder.Status) - require.Empty(t, createOrder.CertURL) - require.Equal(t, createOrder.URI+"/finalize", createOrder.FinalizeURL) - require.Len(t, createOrder.AuthzURLs, len(tc.authorizations), "expected same number of authzurls as identifiers") - - // HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow - // test. - markAuthorizationSuccess(t, client, acmeClient, acct, createOrder) - - // Submit the CSR - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated key for CSR") - csr, err := x509.CreateCertificateRequest(rand.Reader, &tc.requestCSR, csrKey) - require.NoError(t, err, "failed generating csr") - - certs, _, err := acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csr, true) - - if tc.valid { - require.NoError(t, err, "failed finalizing order") - - // Validate we get a signed cert back - testAcmeCertSignedByCa(t, client, certs, "int-ca") - } else { - require.Error(t, err, "Not a valid CSR, should err") - } - } -} diff --git a/builtin/logical/pki/path_config_acme_test.go b/builtin/logical/pki/path_config_acme_test.go deleted file mode 100644 index 0137e2eac..000000000 --- a/builtin/logical/pki/path_config_acme_test.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestAcmeConfig(t *testing.T) { - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - - cases := []struct { - name string - AcmeConfig map[string]interface{} - prefixUrl string - validConfig bool - works bool - }{ - {"unspecified-root", map[string]interface{}{ - "enabled": true, - "allowed_issuers": "*", - "allowed_roles": "*", - "dns_resolver": "", - "eab_policy_name": "", - }, "acme/", true, true}, - {"bad-policy-root", map[string]interface{}{ - "enabled": true, - "allowed_issuers": "*", - "allowed_roles": "*", - "default_directory_policy": "bad", - "dns_resolver": "", - "eab_policy_name": "", - }, "acme/", false, false}, - {"forbid-root", map[string]interface{}{ - "enabled": true, - "allowed_issuers": "*", - "allowed_roles": "*", - "default_directory_policy": "forbid", - "dns_resolver": "", - "eab_policy_name": "", - }, "acme/", true, false}, - {"sign-verbatim-root", map[string]interface{}{ - "enabled": true, - "allowed_issuers": "*", - "allowed_roles": "*", - "default_directory_policy": "sign-verbatim", - "dns_resolver": "", - "eab_policy_name": "", - }, "acme/", true, true}, - {"role-root", map[string]interface{}{ - "enabled": true, - "allowed_issuers": "*", - "allowed_roles": "*", - "default_directory_policy": "role:exists", - "dns_resolver": "", - "eab_policy_name": "", - }, "acme/", true, true}, - {"bad-role-root", map[string]interface{}{ - "enabled": true, - "allowed_issuers": "*", - "allowed_roles": "*", - "default_directory_policy": "role:notgood", - "dns_resolver": "", - "eab_policy_name": "", - }, "acme/", false, true}, - {"disallowed-role-root", map[string]interface{}{ - "enabled": true, - "allowed_issuers": "*", - "allowed_roles": "good", - "default_directory_policy": "role:exists", - "dns_resolver": "", - "eab_policy_name": "", - }, "acme/", false, false}, - } - - roleConfig := map[string]interface{}{ - "issuer_ref": "default", - "allowed_domains": "example.com", - "allow_subdomains": true, - "max_ttl": "720h", - } - - testCtx := context.Background() - - for _, tc := range cases { - deadline := time.Now().Add(1 * time.Minute) - subTestCtx, _ := context.WithDeadline(testCtx, deadline) - - _, err := client.Logical().WriteWithContext(subTestCtx, "pki/roles/exists", roleConfig) - require.NoError(t, err) - _, err = client.Logical().WriteWithContext(subTestCtx, "pki/roles/good", roleConfig) - require.NoError(t, err) - - t.Run(tc.name, func(t *testing.T) { - _, err := client.Logical().WriteWithContext(subTestCtx, "pki/config/acme", tc.AcmeConfig) - - if tc.validConfig { - require.NoError(t, err) - } else { - require.Error(t, err) - return - } - - _, err = client.Logical().ReadWithContext(subTestCtx, "pki/acme/directory") - if tc.works { - require.NoError(t, err) - - baseAcmeURL := "/v1/pki/" + tc.prefixUrl - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - // Create new account - _, err = acmeClient.Discover(subTestCtx) - require.NoError(t, err, "failed acme discovery call") - } else { - require.Error(t, err, "Acme Configuration should prevent usage") - } - - t.Logf("Completed case %v", tc.name) - }) - } -} diff --git a/builtin/logical/pki/path_manage_keys_test.go b/builtin/logical/pki/path_manage_keys_test.go deleted file mode 100644 index 68d31ef86..000000000 --- a/builtin/logical/pki/path_manage_keys_test.go +++ /dev/null @@ -1,441 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "encoding/pem" - "fmt" - "testing" - - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - - "github.com/hashicorp/vault/sdk/helper/certutil" - - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" -) - -func TestPKI_PathManageKeys_GenerateInternalKeys(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - tests := []struct { - name string - keyType string - keyBits []int - wantLogicalErr bool - }{ - {"all-defaults", "", []int{0}, false}, - {"rsa", "rsa", []int{0, 2048, 3072, 4096}, false}, - {"ec", "ec", []int{0, 224, 256, 384, 521}, false}, - {"ed25519", "ed25519", []int{0}, false}, - {"error-rsa", "rsa", []int{-1, 343444}, true}, - {"error-ec", "ec", []int{-1, 3434324}, true}, - {"error-bad-type", "dskjfkdsfjdkf", []int{0}, true}, - } - for _, tt := range tests { - tt := tt - for _, keyBitParam := range tt.keyBits { - keyName := fmt.Sprintf("%s-%d", tt.name, keyBitParam) - t.Run(keyName, func(t *testing.T) { - data := make(map[string]interface{}) - if tt.keyType != "" { - data["key_type"] = tt.keyType - } - if keyBitParam != 0 { - data["key_bits"] = keyBitParam - } - keyName = genUuid() + "-" + tt.keyType + "-key-name" - data["key_name"] = keyName - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/generate/internal", - Storage: s, - Data: data, - MountPoint: "pki/", - }) - require.NoError(t, err, - "Failed generating key with values key_type:%s key_bits:%d key_name:%s", tt.keyType, keyBitParam, keyName) - require.NotNil(t, resp, - "Got nil response generating key with values key_type:%s key_bits:%d key_name:%s", tt.keyType, keyBitParam, keyName) - if tt.wantLogicalErr { - require.True(t, resp.IsError(), "expected logical error but the request passed:\n%#v", resp) - } else { - require.False(t, resp.IsError(), - "Got logical error response when not expecting one, "+ - "generating key with values key_type:%s key_bits:%d key_name:%s\n%s", tt.keyType, keyBitParam, keyName, resp.Error()) - - // Special case our all-defaults - if tt.keyType == "" { - tt.keyType = "rsa" - } - - require.Equal(t, tt.keyType, resp.Data["key_type"], "key_type field contained an invalid type") - require.NotEmpty(t, resp.Data["key_id"], "returned an empty key_id field, should never happen") - require.Equal(t, keyName, resp.Data["key_name"], "key name was not processed correctly") - require.Nil(t, resp.Data["private_key"], "private_key field should not appear in internal generation type.") - } - }) - } - } -} - -func TestPKI_PathManageKeys_GenerateExportedKeys(t *testing.T) { - t.Parallel() - // We tested a lot of the logic above within the internal test, so just make sure we honor the exported contract - b, s := CreateBackendWithStorage(t) - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/generate/exported", - Storage: s, - Data: map[string]interface{}{ - "key_type": "ec", - "key_bits": 224, - }, - MountPoint: "pki/", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("keys/generate/exported"), logical.UpdateOperation), resp, true) - - require.NoError(t, err, "Failed generating exported key") - require.NotNil(t, resp, "Got nil response generating exported key") - require.Equal(t, "ec", resp.Data["key_type"], "key_type field contained an invalid type") - require.NotEmpty(t, resp.Data["key_id"], "returned an empty key_id field, should never happen") - require.Empty(t, resp.Data["key_name"], "key name should have been empty but was not") - require.NotEmpty(t, resp.Data["private_key"], "private_key field should not be empty in exported generation type.") - - // Make sure we can decode our private key as expected - keyData := resp.Data["private_key"].(string) - block, rest := pem.Decode([]byte(keyData)) - require.Empty(t, rest, "should not have had any trailing data") - require.NotEmpty(t, block, "failed decoding pem block") - - key, err := x509.ParseECPrivateKey(block.Bytes) - require.NoError(t, err, "failed parsing pem block as ec private key") - require.Equal(t, elliptic.P224(), key.Curve, "got unexpected curve value in returned private key") -} - -func TestPKI_PathManageKeys_ImportKeyBundle(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - bundle1, err := certutil.CreateKeyBundle("ec", 224, rand.Reader) - require.NoError(t, err, "failed generating an ec key bundle") - bundle2, err := certutil.CreateKeyBundle("rsa", 2048, rand.Reader) - require.NoError(t, err, "failed generating an rsa key bundle") - pem1, err := bundle1.ToPrivateKeyPemString() - require.NoError(t, err, "failed converting ec key to pem") - pem2, err := bundle2.ToPrivateKeyPemString() - require.NoError(t, err, "failed converting rsa key to pem") - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/import", - Storage: s, - Data: map[string]interface{}{ - "key_name": "my-ec-key", - "pem_bundle": pem1, - }, - MountPoint: "pki/", - }) - - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("keys/import"), logical.UpdateOperation), resp, true) - - require.NoError(t, err, "Failed importing ec key") - require.NotNil(t, resp, "Got nil response importing ec key") - require.False(t, resp.IsError(), "received an error response: %v", resp.Error()) - require.NotEmpty(t, resp.Data["key_id"], "key id for ec import response was empty") - require.Equal(t, "my-ec-key", resp.Data["key_name"], "key_name was incorrect for ec key") - require.Equal(t, certutil.ECPrivateKey, resp.Data["key_type"]) - keyId1 := resp.Data["key_id"].(keyID) - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/import", - Storage: s, - Data: map[string]interface{}{ - "key_name": "my-rsa-key", - "pem_bundle": pem2, - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "Failed importing rsa key") - require.NotNil(t, resp, "Got nil response importing rsa key") - require.False(t, resp.IsError(), "received an error response: %v", resp.Error()) - require.NotEmpty(t, resp.Data["key_id"], "key id for rsa import response was empty") - require.Equal(t, "my-rsa-key", resp.Data["key_name"], "key_name was incorrect for ec key") - require.Equal(t, certutil.RSAPrivateKey, resp.Data["key_type"]) - keyId2 := resp.Data["key_id"].(keyID) - - require.NotEqual(t, keyId1, keyId2) - - // Attempt to reimport the same key with a different name. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/import", - Storage: s, - Data: map[string]interface{}{ - "key_name": "my-new-ec-key", - "pem_bundle": pem1, - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "Failed importing the same ec key") - require.NotNil(t, resp, "Got nil response importing the same ec key") - require.False(t, resp.IsError(), "received an error response: %v", resp.Error()) - require.NotEmpty(t, resp.Data["key_id"], "key id for ec import response was empty") - // Note we should receive back the original name, not the new updated name. - require.Equal(t, "my-ec-key", resp.Data["key_name"], "key_name was incorrect for ec key") - require.Equal(t, certutil.ECPrivateKey, resp.Data["key_type"]) - keyIdReimport := resp.Data["key_id"] - require.Equal(t, keyId1, keyIdReimport, "the re-imported key did not return the same key id") - - // Make sure we can not reuse an existing name across different keys. - bundle3, err := certutil.CreateKeyBundle("ec", 224, rand.Reader) - require.NoError(t, err, "failed generating an ec key bundle") - pem3, err := bundle3.ToPrivateKeyPemString() - require.NoError(t, err, "failed converting rsa key to pem") - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/import", - Storage: s, - Data: map[string]interface{}{ - "key_name": "my-ec-key", - "pem_bundle": pem3, - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "Failed importing the same ec key") - require.NotNil(t, resp, "Got nil response importing the same ec key") - require.True(t, resp.IsError(), "should have received an error response importing a key with a re-used name") - - // Delete the key to make sure re-importing gets another ID - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.DeleteOperation, - Path: "key/" + keyId2.String(), - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed deleting keyId 2") - require.Nil(t, resp, "Got non-nil response deleting the key: %#v", resp) - - // Deleting a non-existent key should be okay... - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.DeleteOperation, - Path: "key/" + keyId2.String(), - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed deleting keyId 2") - require.Nil(t, resp, "Got non-nil response deleting the key: %#v", resp) - - // Let's reimport key 2 post-deletion to make sure we re-generate a new key id - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/import", - Storage: s, - Data: map[string]interface{}{ - "key_name": "my-rsa-key", - "pem_bundle": pem2, - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "Failed importing rsa key") - require.NotNil(t, resp, "Got nil response importing rsa key") - require.False(t, resp.IsError(), "received an error response: %v", resp.Error()) - require.NotEmpty(t, resp.Data["key_id"], "key id for rsa import response was empty") - require.Equal(t, "my-rsa-key", resp.Data["key_name"], "key_name was incorrect for ec key") - require.Equal(t, certutil.RSAPrivateKey, resp.Data["key_type"]) - keyId2Reimport := resp.Data["key_id"].(keyID) - - require.NotEqual(t, keyId2, keyId2Reimport, "re-importing key 2 did not generate a new key id") -} - -func TestPKI_PathManageKeys_DeleteDefaultKeyWarns(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/generate/internal", - Storage: s, - Data: map[string]interface{}{"key_type": "ec"}, - MountPoint: "pki/", - }) - require.NoError(t, err, "Failed generating key") - require.NotNil(t, resp, "Got nil response generating key") - require.False(t, resp.IsError(), "resp contained errors generating key: %#v", resp.Error()) - keyId := resp.Data["key_id"].(keyID) - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.DeleteOperation, - Path: "key/" + keyId.String(), - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed deleting default key") - require.NotNil(t, resp, "Got nil response deleting the default key") - require.False(t, resp.IsError(), "expected no errors deleting default key: %#v", resp.Error()) - require.NotEmpty(t, resp.Warnings, "expected warnings to be populated on deleting default key") -} - -func TestPKI_PathManageKeys_DeleteUsedKeyFails(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issuers/generate/root/internal", - Storage: s, - Data: map[string]interface{}{"common_name": "test.com"}, - MountPoint: "pki/", - }) - require.NoError(t, err, "Failed generating issuer") - require.NotNil(t, resp, "Got nil response generating issuer") - require.False(t, resp.IsError(), "resp contained errors generating issuer: %#v", resp.Error()) - keyId := resp.Data["key_id"].(keyID) - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.DeleteOperation, - Path: "key/" + keyId.String(), - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed deleting key used by an issuer") - require.NotNil(t, resp, "Got nil response deleting key used by an issuer") - require.True(t, resp.IsError(), "expected an error deleting key used by an issuer") -} - -func TestPKI_PathManageKeys_UpdateKeyDetails(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/generate/internal", - Storage: s, - Data: map[string]interface{}{"key_type": "ec"}, - MountPoint: "pki/", - }) - require.NoError(t, err, "Failed generating key") - require.NotNil(t, resp, "Got nil response generating key") - require.False(t, resp.IsError(), "resp contained errors generating key: %#v", resp.Error()) - keyId := resp.Data["key_id"].(keyID) - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "key/" + keyId.String(), - Storage: s, - Data: map[string]interface{}{"key_name": "new-name"}, - MountPoint: "pki/", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("key/"+keyId.String()), logical.UpdateOperation), resp, true) - - require.NoError(t, err, "failed updating key with new name") - require.NotNil(t, resp, "Got nil response updating key with new name") - require.False(t, resp.IsError(), "unexpected error updating key with new name: %#v", resp.Error()) - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "key/" + keyId.String(), - Storage: s, - MountPoint: "pki/", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("key/"+keyId.String()), logical.ReadOperation), resp, true) - - require.NoError(t, err, "failed reading key after name update") - require.NotNil(t, resp, "Got nil response reading key after name update") - require.False(t, resp.IsError(), "unexpected error reading key: %#v", resp.Error()) - keyName := resp.Data["key_name"].(string) - - require.Equal(t, "new-name", keyName, "failed to update key_name expected: new-name was: %s", keyName) - - // Make sure we do not allow updates to invalid name values - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "key/" + keyId.String(), - Storage: s, - Data: map[string]interface{}{"key_name": "a-bad\\-name"}, - MountPoint: "pki/", - }) - require.NoError(t, err, "failed updating key with a bad name") - require.NotNil(t, resp, "Got nil response updating key with a bad name") - require.True(t, resp.IsError(), "expected an error updating key with a bad name, but did not get one.") -} - -func TestPKI_PathManageKeys_ImportKeyBundleBadData(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/import", - Storage: s, - Data: map[string]interface{}{ - "key_name": "my-ec-key", - "pem_bundle": "this-is-not-a-pem-bundle", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "got a 500 error type response from a bad pem bundle") - require.NotNil(t, resp, "Got nil response importing a bad pem bundle") - require.True(t, resp.IsError(), "should have received an error response importing a bad pem bundle") - - // Make sure we also bomb on a proper certificate - bundle := genCertBundle(t, b, s) - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/import", - Storage: s, - Data: map[string]interface{}{ - "pem_bundle": bundle.Certificate, - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "got a 500 error type response from a certificate pem bundle") - require.NotNil(t, resp, "Got nil response importing a certificate bundle") - require.True(t, resp.IsError(), "should have received an error response importing a certificate pem bundle") -} - -func TestPKI_PathManageKeys_ImportKeyRejectsMultipleKeys(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - - bundle1, err := certutil.CreateKeyBundle("ec", 224, rand.Reader) - require.NoError(t, err, "failed generating an ec key bundle") - bundle2, err := certutil.CreateKeyBundle("rsa", 2048, rand.Reader) - require.NoError(t, err, "failed generating an rsa key bundle") - pem1, err := bundle1.ToPrivateKeyPemString() - require.NoError(t, err, "failed converting ec key to pem") - pem2, err := bundle2.ToPrivateKeyPemString() - require.NoError(t, err, "failed converting rsa key to pem") - - importPem := pem1 + "\n" + pem2 - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/import", - Storage: s, - Data: map[string]interface{}{ - "key_name": "my-ec-key", - "pem_bundle": importPem, - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "got a 500 error type response from a bad pem bundle") - require.NotNil(t, resp, "Got nil response importing a bad pem bundle") - require.True(t, resp.IsError(), "should have received an error response importing a pem bundle with more than 1 key") - - ctx := context.Background() - sc := b.makeStorageContext(ctx, s) - keys, _ := sc.listKeys() - for _, keyId := range keys { - id, _ := sc.fetchKeyById(keyId) - t.Logf("%s:%s", id.ID, id.Name) - } -} diff --git a/builtin/logical/pki/path_ocsp_test.go b/builtin/logical/pki/path_ocsp_test.go deleted file mode 100644 index 20f706fa4..000000000 --- a/builtin/logical/pki/path_ocsp_test.go +++ /dev/null @@ -1,742 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "bytes" - "context" - "crypto" - "crypto/x509" - "encoding/base64" - "fmt" - "io" - "net/http" - "strconv" - "strings" - "testing" - "time" - - "github.com/hashicorp/go-secure-stdlib/parseutil" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/ocsp" -) - -// If the ocsp_disabled flag is set to true in the crl configuration make sure we always -// return an Unauthorized error back as we assume an end-user disabling the feature does -// not want us to act as the OCSP authority and the RFC specifies this is the appropriate response. -func TestOcsp_Disabled(t *testing.T) { - t.Parallel() - type testArgs struct { - reqType string - } - var tests []testArgs - for _, reqType := range []string{"get", "post"} { - tests = append(tests, testArgs{ - reqType: reqType, - }) - } - for _, tt := range tests { - localTT := tt - t.Run(localTT.reqType, func(t *testing.T) { - b, s, testEnv := setupOcspEnv(t, "rsa") - resp, err := CBWrite(b, s, "config/crl", map[string]interface{}{ - "ocsp_disable": "true", - }) - requireSuccessNonNilResponse(t, resp, err) - resp, err = SendOcspRequest(t, b, s, localTT.reqType, testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1) - require.NoError(t, err) - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 401, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer) - }) - } -} - -// If we can't find the issuer within the request and have no default issuer to sign an Unknown response -// with return an UnauthorizedErrorResponse/according to/the RFC, similar to if we are disabled (lack of authority) -// This behavior differs from CRLs when an issuer is removed from a mount. -func TestOcsp_UnknownIssuerWithNoDefault(t *testing.T) { - t.Parallel() - - _, _, testEnv := setupOcspEnv(t, "ec") - // Create another completely empty mount so the created issuer/certificate above is unknown - b, s := CreateBackendWithStorage(t) - - resp, err := SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1) - require.NoError(t, err) - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 401, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer) -} - -// If the issuer in the request does exist, but the request coming in associates the serial with the -// wrong issuer return an Unknown response back to the caller. -func TestOcsp_WrongIssuerInRequest(t *testing.T) { - t.Parallel() - - b, s, testEnv := setupOcspEnv(t, "ec") - serial := serialFromCert(testEnv.leafCertIssuer1) - resp, err := CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": serial, - }) - requireSuccessNonNilResponse(t, resp, err, "revoke") - - resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer2, crypto.SHA1) - require.NoError(t, err) - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 200, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1) - require.NoError(t, err, "parsing ocsp get response") - - require.Equal(t, ocsp.Unknown, ocspResp.Status) -} - -// Verify that requests we can't properly decode result in the correct response of MalformedRequestError -func TestOcsp_MalformedRequests(t *testing.T) { - t.Parallel() - type testArgs struct { - reqType string - } - var tests []testArgs - for _, reqType := range []string{"get", "post"} { - tests = append(tests, testArgs{ - reqType: reqType, - }) - } - for _, tt := range tests { - localTT := tt - t.Run(localTT.reqType, func(t *testing.T) { - b, s, _ := setupOcspEnv(t, "rsa") - badReq := []byte("this is a bad request") - var resp *logical.Response - var err error - switch localTT.reqType { - case "get": - resp, err = sendOcspGetRequest(b, s, badReq) - case "post": - resp, err = sendOcspPostRequest(b, s, badReq) - default: - t.Fatalf("bad request type") - } - require.NoError(t, err) - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 400, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - require.Equal(t, ocsp.MalformedRequestErrorResponse, respDer) - }) - } -} - -// Validate that we properly handle a revocation entry that contains an issuer ID that no longer exists, -// the best we can do in this use case is to respond back with the default issuer that we don't know -// the issuer that they are requesting (we can't guarantee that the client is actually requesting a serial -// from that issuer) -func TestOcsp_InvalidIssuerIdInRevocationEntry(t *testing.T) { - t.Parallel() - - b, s, testEnv := setupOcspEnv(t, "ec") - ctx := context.Background() - - // Revoke the entry - serial := serialFromCert(testEnv.leafCertIssuer1) - resp, err := CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": serial, - }) - requireSuccessNonNilResponse(t, resp, err, "revoke") - - // Twiddle the entry so that the issuer id is no longer valid. - storagePath := revokedPath + normalizeSerial(serial) - var revInfo revocationInfo - revEntry, err := s.Get(ctx, storagePath) - require.NoError(t, err, "failed looking up storage path: %s", storagePath) - err = revEntry.DecodeJSON(&revInfo) - require.NoError(t, err, "failed decoding storage entry: %v", revEntry) - revInfo.CertificateIssuer = "00000000-0000-0000-0000-000000000000" - revEntry, err = logical.StorageEntryJSON(storagePath, revInfo) - require.NoError(t, err, "failed re-encoding revocation info: %v", revInfo) - err = s.Put(ctx, revEntry) - require.NoError(t, err, "failed writing out new revocation entry: %v", revEntry) - - // Send the request - resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1) - require.NoError(t, err) - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 200, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1) - require.NoError(t, err, "parsing ocsp get response") - - require.Equal(t, ocsp.Unknown, ocspResp.Status) -} - -// Validate that we properly handle an unknown issuer use-case but that the default issuer -// does not have the OCSP usage flag set, we can't do much else other than reply with an -// Unauthorized response. -func TestOcsp_UnknownIssuerIdWithDefaultHavingOcspUsageRemoved(t *testing.T) { - t.Parallel() - - b, s, testEnv := setupOcspEnv(t, "ec") - ctx := context.Background() - - // Revoke the entry - serial := serialFromCert(testEnv.leafCertIssuer1) - resp, err := CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": serial, - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("revoke"), logical.UpdateOperation), resp, true) - requireSuccessNonNilResponse(t, resp, err, "revoke") - - // Twiddle the entry so that the issuer id is no longer valid. - storagePath := revokedPath + normalizeSerial(serial) - var revInfo revocationInfo - revEntry, err := s.Get(ctx, storagePath) - require.NoError(t, err, "failed looking up storage path: %s", storagePath) - err = revEntry.DecodeJSON(&revInfo) - require.NoError(t, err, "failed decoding storage entry: %v", revEntry) - revInfo.CertificateIssuer = "00000000-0000-0000-0000-000000000000" - revEntry, err = logical.StorageEntryJSON(storagePath, revInfo) - require.NoError(t, err, "failed re-encoding revocation info: %v", revInfo) - err = s.Put(ctx, revEntry) - require.NoError(t, err, "failed writing out new revocation entry: %v", revEntry) - - // Update our issuers to no longer have the OcspSigning usage - resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId1.String(), map[string]interface{}{ - "usage": "read-only,issuing-certificates,crl-signing", - }) - requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer1") - resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId2.String(), map[string]interface{}{ - "usage": "read-only,issuing-certificates,crl-signing", - }) - requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer2") - - // Send the request - resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1) - require.NoError(t, err) - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 401, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer) -} - -// Verify that if we do have a revoked certificate entry for the request, that matches an -// issuer but that issuer does not have the OcspUsage flag set that we return an Unauthorized -// response back to the caller -func TestOcsp_RevokedCertHasIssuerWithoutOcspUsage(t *testing.T) { - b, s, testEnv := setupOcspEnv(t, "ec") - - // Revoke our certificate - resp, err := CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": serialFromCert(testEnv.leafCertIssuer1), - }) - requireSuccessNonNilResponse(t, resp, err, "revoke") - - // Update our issuer to no longer have the OcspSigning usage - resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId1.String(), map[string]interface{}{ - "usage": "read-only,issuing-certificates,crl-signing", - }) - requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer") - requireFieldsSetInResp(t, resp, "usage") - - // Do not assume a specific ordering for usage... - usages, err := NewIssuerUsageFromNames(strings.Split(resp.Data["usage"].(string), ",")) - require.NoError(t, err, "failed parsing usage return value") - require.True(t, usages.HasUsage(IssuanceUsage)) - require.True(t, usages.HasUsage(CRLSigningUsage)) - require.False(t, usages.HasUsage(OCSPSigningUsage)) - - // Request an OCSP request from it, we should get an Unauthorized response back - resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1) - requireSuccessNonNilResponse(t, resp, err, "ocsp get request") - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 401, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer) -} - -// Verify if our matching issuer for a revocation entry has no key associated with it that -// we bail with an Unauthorized response. -func TestOcsp_RevokedCertHasIssuerWithoutAKey(t *testing.T) { - b, s, testEnv := setupOcspEnv(t, "ec") - - // Revoke our certificate - resp, err := CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": serialFromCert(testEnv.leafCertIssuer1), - }) - requireSuccessNonNilResponse(t, resp, err, "revoke") - - // Delete the key associated with our issuer - resp, err = CBRead(b, s, "issuer/"+testEnv.issuerId1.String()) - requireSuccessNonNilResponse(t, resp, err, "failed reading issuer") - requireFieldsSetInResp(t, resp, "key_id") - keyId := resp.Data["key_id"].(keyID) - - // This is a bit naughty but allow me to delete the key... - sc := b.makeStorageContext(context.Background(), s) - issuer, err := sc.fetchIssuerById(testEnv.issuerId1) - require.NoError(t, err, "failed to get issuer from storage") - issuer.KeyID = "" - err = sc.writeIssuer(issuer) - require.NoError(t, err, "failed to write issuer update") - - resp, err = CBDelete(b, s, "key/"+keyId.String()) - requireSuccessNonNilResponse(t, resp, err, "failed deleting key") - - // Request an OCSP request from it, we should get an Unauthorized response back - resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1) - requireSuccessNonNilResponse(t, resp, err, "ocsp get request") - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 401, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer) -} - -// Verify if for some reason an end-user has rotated an existing certificate using the same -// key so our algo matches multiple issuers and one has OCSP usage disabled. We expect that -// even if a prior issuer issued the certificate, the new matching issuer can respond and sign -// the response to the caller on its behalf. -// -// NOTE: This test is a bit at the mercy of iteration order of the issuer ids. -// -// If it becomes flaky, most likely something is wrong in the code -// and not the test. -func TestOcsp_MultipleMatchingIssuersOneWithoutSigningUsage(t *testing.T) { - b, s, testEnv := setupOcspEnv(t, "ec") - - // Create a matching issuer as issuer1 with the same backing key - resp, err := CBWrite(b, s, "root/rotate/existing", map[string]interface{}{ - "key_ref": testEnv.keyId1, - "ttl": "40h", - "common_name": "example-ocsp.com", - }) - requireSuccessNonNilResponse(t, resp, err, "rotate issuer failed") - requireFieldsSetInResp(t, resp, "issuer_id") - rotatedCert := parseCert(t, resp.Data["certificate"].(string)) - - // Remove ocsp signing from our issuer - resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId1.String(), map[string]interface{}{ - "usage": "read-only,issuing-certificates,crl-signing", - }) - requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer") - requireFieldsSetInResp(t, resp, "usage") - // Do not assume a specific ordering for usage... - usages, err := NewIssuerUsageFromNames(strings.Split(resp.Data["usage"].(string), ",")) - require.NoError(t, err, "failed parsing usage return value") - require.True(t, usages.HasUsage(IssuanceUsage)) - require.True(t, usages.HasUsage(CRLSigningUsage)) - require.False(t, usages.HasUsage(OCSPSigningUsage)) - - // Request an OCSP request from it, we should get a Good response back, from the rotated cert - resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1) - requireSuccessNonNilResponse(t, resp, err, "ocsp get request") - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 200, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1) - require.NoError(t, err, "parsing ocsp get response") - - require.Equal(t, ocsp.Good, ocspResp.Status) - require.Equal(t, crypto.SHA1, ocspResp.IssuerHash) - require.Equal(t, 0, ocspResp.RevocationReason) - require.Equal(t, testEnv.leafCertIssuer1.SerialNumber, ocspResp.SerialNumber) - - requireOcspSignatureAlgoForKey(t, rotatedCert.SignatureAlgorithm, ocspResp.SignatureAlgorithm) - requireOcspResponseSignedBy(t, ocspResp, rotatedCert) -} - -// Make sure OCSP GET/POST requests work through the entire stack, and not just -// through the quicker backend layer the other tests are doing. -func TestOcsp_HigherLevel(t *testing.T) { - t.Parallel() - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - mountPKIEndpoint(t, client, "pki") - resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "root-ca.com", - "ttl": "600h", - }) - - require.NoError(t, err, "error generating root ca: %v", err) - require.NotNil(t, resp, "expected ca info from root") - - issuerCert := parseCert(t, resp.Data["certificate"].(string)) - - resp, err = client.Logical().Write("pki/roles/example", map[string]interface{}{ - "allowed_domains": "example.com", - "allow_subdomains": "true", - "no_store": "false", // make sure we store this cert - "max_ttl": "1h", - "key_type": "ec", - }) - require.NoError(t, err, "error setting up pki role: %v", err) - - resp, err = client.Logical().Write("pki/issue/example", map[string]interface{}{ - "common_name": "test.example.com", - "ttl": "15m", - }) - require.NoError(t, err, "error issuing certificate: %v", err) - require.NotNil(t, resp, "got nil response from issuing request") - certToRevoke := parseCert(t, resp.Data["certificate"].(string)) - serialNum := resp.Data["serial_number"].(string) - - // Revoke the certificate - resp, err = client.Logical().Write("pki/revoke", map[string]interface{}{ - "serial_number": serialNum, - }) - require.NoError(t, err, "error revoking certificate: %v", err) - require.NotNil(t, resp, "got nil response from revoke") - - // Make sure that OCSP handler responds properly - ocspReq := generateRequest(t, crypto.SHA256, certToRevoke, issuerCert) - ocspPostReq := client.NewRequest(http.MethodPost, "/v1/pki/ocsp") - ocspPostReq.Headers.Set("Content-Type", "application/ocsp-request") - ocspPostReq.BodyBytes = ocspReq - rawResp, err := client.RawRequest(ocspPostReq) - require.NoError(t, err, "failed sending ocsp post request") - - require.Equal(t, 200, rawResp.StatusCode) - require.Equal(t, ocspResponseContentType, rawResp.Header.Get("Content-Type")) - bodyReader := rawResp.Body - respDer, err := io.ReadAll(bodyReader) - bodyReader.Close() - require.NoError(t, err, "failed reading response body") - - ocspResp, err := ocsp.ParseResponse(respDer, issuerCert) - require.NoError(t, err, "parsing ocsp get response") - - require.Equal(t, ocsp.Revoked, ocspResp.Status) - require.Equal(t, certToRevoke.SerialNumber, ocspResp.SerialNumber) - - // Test OCSP Get request for ocsp - urlEncoded := base64.StdEncoding.EncodeToString(ocspReq) - if strings.Contains(urlEncoded, "//") { - // workaround known redirect bug that is difficult to fix - t.Skipf("VAULT-13630 - Skipping GET OCSP test with encoded issuer cert containing // triggering redirection bug") - } - - ocspGetReq := client.NewRequest(http.MethodGet, "/v1/pki/ocsp/"+urlEncoded) - ocspGetReq.Headers.Set("Content-Type", "application/ocsp-request") - rawResp, err = client.RawRequest(ocspGetReq) - require.NoError(t, err, "failed sending ocsp get request") - - require.Equal(t, 200, rawResp.StatusCode) - require.Equal(t, ocspResponseContentType, rawResp.Header.Get("Content-Type")) - bodyReader = rawResp.Body - respDer, err = io.ReadAll(bodyReader) - bodyReader.Close() - require.NoError(t, err, "failed reading response body") - - ocspResp, err = ocsp.ParseResponse(respDer, issuerCert) - require.NoError(t, err, "parsing ocsp get response") - - require.Equal(t, ocsp.Revoked, ocspResp.Status) - require.Equal(t, certToRevoke.SerialNumber, ocspResp.SerialNumber) -} - -// TestOcsp_NextUpdate make sure that we are setting the appropriate values -// for the NextUpdate field within our responses. -func TestOcsp_NextUpdate(t *testing.T) { - // Within the runOcspRequestTest, with a ocspExpiry of 0, - // we will validate that NextUpdate was not set in the response - runOcspRequestTest(t, "POST", "ec", 0, 0, crypto.SHA256, 0) - - // Within the runOcspRequestTest, with a ocspExpiry of 24 hours, we will validate - // that NextUpdate is set and has a time 24 hours larger than ThisUpdate - runOcspRequestTest(t, "POST", "ec", 0, 0, crypto.SHA256, 24*time.Hour) -} - -func TestOcsp_ValidRequests(t *testing.T) { - type caKeyConf struct { - keyType string - keyBits int - sigBits int - } - t.Parallel() - type testArgs struct { - reqType string - keyConf caKeyConf - reqHash crypto.Hash - } - var tests []testArgs - for _, reqType := range []string{"get", "post"} { - for _, keyConf := range []caKeyConf{ - {"rsa", 0, 0}, - {"rsa", 0, 384}, - {"rsa", 0, 512}, - {"ec", 0, 0}, - {"ec", 521, 0}, - } { - // "ed25519" is not supported at the moment in x/crypto/ocsp - for _, requestHash := range []crypto.Hash{crypto.SHA1, crypto.SHA256, crypto.SHA384, crypto.SHA512} { - tests = append(tests, testArgs{ - reqType: reqType, - keyConf: keyConf, - reqHash: requestHash, - }) - } - } - } - for _, tt := range tests { - localTT := tt - testName := fmt.Sprintf("%s-%s-keybits-%d-sigbits-%d-reqHash-%s", localTT.reqType, localTT.keyConf.keyType, - localTT.keyConf.keyBits, - localTT.keyConf.sigBits, - localTT.reqHash) - t.Run(testName, func(t *testing.T) { - runOcspRequestTest(t, localTT.reqType, localTT.keyConf.keyType, localTT.keyConf.keyBits, - localTT.keyConf.sigBits, localTT.reqHash, 12*time.Hour) - }) - } -} - -func runOcspRequestTest(t *testing.T, requestType string, caKeyType string, - caKeyBits int, caKeySigBits int, requestHash crypto.Hash, ocspExpiry time.Duration, -) { - b, s, testEnv := setupOcspEnvWithCaKeyConfig(t, caKeyType, caKeyBits, caKeySigBits, ocspExpiry) - - // Non-revoked cert - resp, err := SendOcspRequest(t, b, s, requestType, testEnv.leafCertIssuer1, testEnv.issuer1, requestHash) - requireSuccessNonNilResponse(t, resp, err, "ocsp get request") - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 200, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer := resp.Data["http_raw_body"].([]byte) - - ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1) - require.NoError(t, err, "parsing ocsp get response") - - require.Equal(t, ocsp.Good, ocspResp.Status) - require.Equal(t, requestHash, ocspResp.IssuerHash) - require.Equal(t, 0, ocspResp.RevocationReason) - require.Equal(t, testEnv.leafCertIssuer1.SerialNumber, ocspResp.SerialNumber) - - requireOcspSignatureAlgoForKey(t, testEnv.issuer1.SignatureAlgorithm, ocspResp.SignatureAlgorithm) - requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer1) - - // Now revoke it - resp, err = CBWrite(b, s, "revoke", map[string]interface{}{ - "serial_number": serialFromCert(testEnv.leafCertIssuer1), - }) - requireSuccessNonNilResponse(t, resp, err, "revoke") - - resp, err = SendOcspRequest(t, b, s, requestType, testEnv.leafCertIssuer1, testEnv.issuer1, requestHash) - requireSuccessNonNilResponse(t, resp, err, "ocsp get request with revoked") - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 200, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer = resp.Data["http_raw_body"].([]byte) - - ocspResp, err = ocsp.ParseResponse(respDer, testEnv.issuer1) - require.NoError(t, err, "parsing ocsp get response with revoked") - - require.Equal(t, ocsp.Revoked, ocspResp.Status) - require.Equal(t, requestHash, ocspResp.IssuerHash) - require.Equal(t, 0, ocspResp.RevocationReason) - require.Equal(t, testEnv.leafCertIssuer1.SerialNumber, ocspResp.SerialNumber) - - requireOcspSignatureAlgoForKey(t, testEnv.issuer1.SignatureAlgorithm, ocspResp.SignatureAlgorithm) - requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer1) - - // Request status for our second issuer - resp, err = SendOcspRequest(t, b, s, requestType, testEnv.leafCertIssuer2, testEnv.issuer2, requestHash) - requireSuccessNonNilResponse(t, resp, err, "ocsp get request") - requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body") - require.Equal(t, 200, resp.Data["http_status_code"]) - require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"]) - respDer = resp.Data["http_raw_body"].([]byte) - - ocspResp, err = ocsp.ParseResponse(respDer, testEnv.issuer2) - require.NoError(t, err, "parsing ocsp get response") - - require.Equal(t, ocsp.Good, ocspResp.Status) - require.Equal(t, requestHash, ocspResp.IssuerHash) - require.Equal(t, 0, ocspResp.RevocationReason) - require.Equal(t, testEnv.leafCertIssuer2.SerialNumber, ocspResp.SerialNumber) - - // Verify that our thisUpdate and nextUpdate fields are updated as expected - resp, err = CBRead(b, s, "config/crl") - requireSuccessNonNilResponse(t, resp, err, "failed reading from config/crl") - requireFieldsSetInResp(t, resp, "ocsp_expiry") - ocspExpiryRaw := resp.Data["ocsp_expiry"].(string) - expectedDiff, err := parseutil.ParseDurationSecond(ocspExpiryRaw) - require.NoError(t, err, "failed to parse default ocsp expiry value") - - thisUpdate := ocspResp.ThisUpdate - require.Less(t, time.Since(thisUpdate), 10*time.Second, "expected ThisUpdate field to be within the last 10 seconds") - if expectedDiff != 0 { - nextUpdate := ocspResp.NextUpdate - require.False(t, nextUpdate.IsZero(), "nextUpdate field value should have been a non-zero time") - require.True(t, thisUpdate.Before(nextUpdate), - fmt.Sprintf("thisUpdate %s, should have been before nextUpdate: %s", thisUpdate, nextUpdate)) - nextUpdateDiff := nextUpdate.Sub(thisUpdate) - require.Equal(t, expectedDiff, nextUpdateDiff, - fmt.Sprintf("the delta between thisUpdate %s and nextUpdate: %s should have been around: %s but was %s", - thisUpdate, nextUpdate, defaultCrlConfig.OcspExpiry, nextUpdateDiff)) - } else { - // With the config value set to 0, we shouldn't have a NextUpdate field set - require.True(t, ocspResp.NextUpdate.IsZero(), "nextUpdate value was not zero as expected was: %v", ocspResp.NextUpdate) - } - requireOcspSignatureAlgoForKey(t, testEnv.issuer2.SignatureAlgorithm, ocspResp.SignatureAlgorithm) - requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer2) -} - -func requireOcspSignatureAlgoForKey(t *testing.T, expected x509.SignatureAlgorithm, actual x509.SignatureAlgorithm) { - t.Helper() - - require.Equal(t, expected.String(), actual.String()) -} - -type ocspTestEnv struct { - issuer1 *x509.Certificate - issuer2 *x509.Certificate - - issuerId1 issuerID - issuerId2 issuerID - - leafCertIssuer1 *x509.Certificate - leafCertIssuer2 *x509.Certificate - - keyId1 keyID - keyId2 keyID -} - -func setupOcspEnv(t *testing.T, keyType string) (*backend, logical.Storage, *ocspTestEnv) { - return setupOcspEnvWithCaKeyConfig(t, keyType, 0, 0, 12*time.Hour) -} - -func setupOcspEnvWithCaKeyConfig(t *testing.T, keyType string, caKeyBits int, caKeySigBits int, ocspExpiry time.Duration) (*backend, logical.Storage, *ocspTestEnv) { - b, s := CreateBackendWithStorage(t) - var issuerCerts []*x509.Certificate - var leafCerts []*x509.Certificate - var issuerIds []issuerID - var keyIds []keyID - - resp, err := CBWrite(b, s, "config/crl", map[string]interface{}{ - "ocsp_enable": true, - "ocsp_expiry": fmt.Sprintf("%ds", int(ocspExpiry.Seconds())), - }) - requireSuccessNonNilResponse(t, resp, err, "config/crl failed") - - for i := 0; i < 2; i++ { - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "key_type": keyType, - "key_bits": caKeyBits, - "signature_bits": caKeySigBits, - "ttl": "40h", - "common_name": "example-ocsp.com", - }) - requireSuccessNonNilResponse(t, resp, err, "root/generate/internal") - requireFieldsSetInResp(t, resp, "issuer_id", "key_id") - issuerId := resp.Data["issuer_id"].(issuerID) - keyId := resp.Data["key_id"].(keyID) - - resp, err = CBWrite(b, s, "roles/test"+strconv.FormatInt(int64(i), 10), map[string]interface{}{ - "allow_bare_domains": true, - "allow_subdomains": true, - "allowed_domains": "foobar.com", - "no_store": false, - "generate_lease": false, - "issuer_ref": issuerId, - "key_type": keyType, - }) - requireSuccessNonNilResponse(t, resp, err, "roles/test"+strconv.FormatInt(int64(i), 10)) - - resp, err = CBWrite(b, s, "issue/test"+strconv.FormatInt(int64(i), 10), map[string]interface{}{ - "common_name": "test.foobar.com", - }) - requireSuccessNonNilResponse(t, resp, err, "roles/test"+strconv.FormatInt(int64(i), 10)) - requireFieldsSetInResp(t, resp, "certificate", "issuing_ca", "serial_number") - leafCert := parseCert(t, resp.Data["certificate"].(string)) - issuingCa := parseCert(t, resp.Data["issuing_ca"].(string)) - - issuerCerts = append(issuerCerts, issuingCa) - leafCerts = append(leafCerts, leafCert) - issuerIds = append(issuerIds, issuerId) - keyIds = append(keyIds, keyId) - } - - testEnv := &ocspTestEnv{ - issuerId1: issuerIds[0], - issuer1: issuerCerts[0], - leafCertIssuer1: leafCerts[0], - keyId1: keyIds[0], - - issuerId2: issuerIds[1], - issuer2: issuerCerts[1], - leafCertIssuer2: leafCerts[1], - keyId2: keyIds[1], - } - - return b, s, testEnv -} - -func SendOcspRequest(t *testing.T, b *backend, s logical.Storage, getOrPost string, cert, issuer *x509.Certificate, requestHash crypto.Hash) (*logical.Response, error) { - t.Helper() - - ocspRequest := generateRequest(t, requestHash, cert, issuer) - - switch strings.ToLower(getOrPost) { - case "get": - return sendOcspGetRequest(b, s, ocspRequest) - case "post": - return sendOcspPostRequest(b, s, ocspRequest) - default: - t.Fatalf("unsupported value for SendOcspRequest getOrPost arg: %s", getOrPost) - } - return nil, nil -} - -func sendOcspGetRequest(b *backend, s logical.Storage, ocspRequest []byte) (*logical.Response, error) { - urlEncoded := base64.StdEncoding.EncodeToString(ocspRequest) - return CBRead(b, s, "ocsp/"+urlEncoded) -} - -func sendOcspPostRequest(b *backend, s logical.Storage, ocspRequest []byte) (*logical.Response, error) { - reader := io.NopCloser(bytes.NewReader(ocspRequest)) - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "ocsp", - Storage: s, - MountPoint: "pki/", - HTTPRequest: &http.Request{ - Body: reader, - }, - }) - - return resp, err -} diff --git a/builtin/logical/pki/path_resign_crls_test.go b/builtin/logical/pki/path_resign_crls_test.go deleted file mode 100644 index d586a2353..000000000 --- a/builtin/logical/pki/path_resign_crls_test.go +++ /dev/null @@ -1,512 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/base64" - "fmt" - "math/big" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - - "github.com/hashicorp/vault/api" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/vault" - - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" -) - -func TestResignCrls_ForbidSigningOtherIssuerCRL(t *testing.T) { - t.Parallel() - - // Some random CRL from another issuer - pem1 := "-----BEGIN X509 CRL-----\nMIIBvjCBpwIBATANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDExByb290LWV4YW1w\nbGUuY29tFw0yMjEwMjYyMTI5MzlaFw0yMjEwMjkyMTI5MzlaMCcwJQIUSnVf8wsd\nHjOt9drCYFhWxS9QqGoXDTIyMTAyNjIxMjkzOVqgLzAtMB8GA1UdIwQYMBaAFHki\nZ0XDUQVSajNRGXrg66OaIFlYMAoGA1UdFAQDAgEDMA0GCSqGSIb3DQEBCwUAA4IB\nAQBGIdtqTwemnLZF5AoP+jzvKZ26S3y7qvRIzd7f4A0EawzYmWXSXfwqo4TQ4DG3\nnvT+AaA1zCCOlH/1U+ufN9gSSN0j9ax58brSYMnMskMCqhLKIp0qnvS4jr/gopmF\nv8grbvLHEqNYTu1T7umMLdNQUsWT3Qc+EIjfoKj8xD2FHsZwJ+EMbytwl8Unipjr\nhz4rmcES/65vavfdFpOI6YXfi+UAaHBdkTqmHgg4BdpuXfYtlf+iotFSOkygD5fl\n0D+RVFW9uJv2WfbQ7kRt1X/VcFk/onw0AQqxZRVUzvjoMw+EMcxSq3UKOlXcWDxm\nEFz9rFQQ66L388EP8RD7Dh3X\n-----END X509 CRL-----" - - b, s := CreateBackendWithStorage(t) - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "test.com", - "key_type": "ec", - }) - requireSuccessNonNilResponse(t, resp, err) - - resp, err = CBWrite(b, s, "issuer/default/resign-crls", map[string]interface{}{ - "crl_number": "2", - "next_update": "1h", - "format": "pem", - "crls": []string{pem1}, - }) - require.ErrorContains(t, err, "was not signed by requested issuer") -} - -func TestResignCrls_NormalCrl(t *testing.T) { - t.Parallel() - b1, s1 := CreateBackendWithStorage(t) - b2, s2 := CreateBackendWithStorage(t) - - // Setup two backends, with the same key material/certificate with a different leaf in each that is revoked. - caCert, serial1, serial2, crl1, crl2 := setupResignCrlMounts(t, b1, s1, b2, s2) - - // Attempt to combine the CRLs - resp, err := CBWrite(b1, s1, "issuer/default/resign-crls", map[string]interface{}{ - "crl_number": "2", - "next_update": "1h", - "format": "pem", - "crls": []string{crl1, crl2}, - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b1.Route("issuer/default/resign-crls"), logical.UpdateOperation), resp, true) - requireSuccessNonNilResponse(t, resp, err) - requireFieldsSetInResp(t, resp, "crl") - pemCrl := resp.Data["crl"].(string) - combinedCrl, err := decodePemCrl(pemCrl) - require.NoError(t, err, "failed decoding combined CRL") - serials := extractSerialsFromCrl(t, combinedCrl) - - require.Contains(t, serials, serial1) - require.Contains(t, serials, serial2) - require.Equal(t, 2, len(serials), "serials contained more serials than expected") - - require.Equal(t, big.NewInt(int64(2)), combinedCrl.Number) - require.Equal(t, combinedCrl.ThisUpdate.Add(1*time.Hour), combinedCrl.NextUpdate) - - extensions := combinedCrl.Extensions - requireExtensionOid(t, []int{2, 5, 29, 20}, extensions) // CRL Number Extension - requireExtensionOid(t, []int{2, 5, 29, 35}, extensions) // akidOid - require.Equal(t, 2, len(extensions)) - - err = combinedCrl.CheckSignatureFrom(caCert) - require.NoError(t, err, "failed signature check of CRL") -} - -func TestResignCrls_EliminateDuplicates(t *testing.T) { - t.Parallel() - b1, s1 := CreateBackendWithStorage(t) - b2, s2 := CreateBackendWithStorage(t) - - // Setup two backends, with the same key material/certificate with a different leaf in each that is revoked. - _, serial1, _, crl1, _ := setupResignCrlMounts(t, b1, s1, b2, s2) - - // Pass in the same CRLs to make sure we do not duplicate entries - resp, err := CBWrite(b1, s1, "issuer/default/resign-crls", map[string]interface{}{ - "crl_number": "2", - "next_update": "1h", - "format": "pem", - "crls": []string{crl1, crl1}, - }) - requireSuccessNonNilResponse(t, resp, err) - requireFieldsSetInResp(t, resp, "crl") - pemCrl := resp.Data["crl"].(string) - combinedCrl, err := decodePemCrl(pemCrl) - require.NoError(t, err, "failed decoding combined CRL") - - // Technically this will die if we have duplicates. - serials := extractSerialsFromCrl(t, combinedCrl) - - // We should have no warnings about collisions if they have the same revoked time - require.Empty(t, resp.Warnings, "expected no warnings in response") - - require.Contains(t, serials, serial1) - require.Equal(t, 1, len(serials), "serials contained more serials than expected") -} - -func TestResignCrls_ConflictingExpiry(t *testing.T) { - t.Parallel() - b1, s1 := CreateBackendWithStorage(t) - b2, s2 := CreateBackendWithStorage(t) - - // Setup two backends, with the same key material/certificate with a different leaf in each that is revoked. - _, serial1, serial2, crl1, _ := setupResignCrlMounts(t, b1, s1, b2, s2) - - timeAfterMountSetup := time.Now() - - // Read in serial1 from mount 1 - resp, err := CBRead(b1, s1, "cert/"+serial1) - requireSuccessNonNilResponse(t, resp, err, "failed reading serial 1's certificate") - requireFieldsSetInResp(t, resp, "certificate") - cert1 := resp.Data["certificate"].(string) - - // Wait until at least we have rolled over to the next second to match sure the generated CRL time - // on backend 2 for the serial 1 will be different - for { - if time.Now().After(timeAfterMountSetup.Add(1 * time.Second)) { - break - } - } - - // Use BYOC to revoke the same certificate on backend 2 now - resp, err = CBWrite(b2, s2, "revoke", map[string]interface{}{ - "certificate": cert1, - }) - requireSuccessNonNilResponse(t, resp, err, "failed revoking serial 1 on backend 2") - - // Fetch the new CRL from backend2 now - resp, err = CBRead(b2, s2, "cert/crl") - requireSuccessNonNilResponse(t, resp, err, "error fetch crl from backend 2") - requireFieldsSetInResp(t, resp, "certificate") - crl2 := resp.Data["certificate"].(string) - - // Attempt to combine the CRLs - resp, err = CBWrite(b1, s1, "issuer/default/resign-crls", map[string]interface{}{ - "crl_number": "2", - "next_update": "1h", - "format": "pem", - "crls": []string{crl2, crl1}, // Make sure we don't just grab the first colliding entry... - }) - requireSuccessNonNilResponse(t, resp, err) - requireFieldsSetInResp(t, resp, "crl") - pemCrl := resp.Data["crl"].(string) - combinedCrl, err := decodePemCrl(pemCrl) - require.NoError(t, err, "failed decoding combined CRL") - combinedSerials := extractSerialsFromCrl(t, combinedCrl) - - require.Contains(t, combinedSerials, serial1) - require.Contains(t, combinedSerials, serial2) - require.Equal(t, 2, len(combinedSerials), "serials contained more serials than expected") - - // Make sure we issued a warning about the time collision - require.NotEmpty(t, resp.Warnings, "expected at least one warning") - require.Contains(t, resp.Warnings[0], "different revocation times detected") - - // Make sure we have the initial revocation time from backend 1 within the combined CRL. - decodedCrl1, err := decodePemCrl(crl1) - require.NoError(t, err, "failed decoding crl from backend 1") - serialsFromBackend1 := extractSerialsFromCrl(t, decodedCrl1) - - require.Equal(t, serialsFromBackend1[serial1], combinedSerials[serial1]) - - // Make sure we have the initial revocation time from backend 1 does not match with backend 2's time - decodedCrl2, err := decodePemCrl(crl2) - require.NoError(t, err, "failed decoding crl from backend 2") - serialsFromBackend2 := extractSerialsFromCrl(t, decodedCrl2) - - require.NotEqual(t, serialsFromBackend1[serial1], serialsFromBackend2[serial1]) -} - -func TestResignCrls_DeltaCrl(t *testing.T) { - t.Parallel() - - b1, s1 := CreateBackendWithStorage(t) - b2, s2 := CreateBackendWithStorage(t) - - // Setup two backends, with the same key material/certificate with a different leaf in each that is revoked. - caCert, serial1, serial2, crl1, crl2 := setupResignCrlMounts(t, b1, s1, b2, s2) - - resp, err := CBWrite(b1, s1, "issuer/default/resign-crls", map[string]interface{}{ - "crl_number": "5", - "delta_crl_base_number": "4", - "next_update": "12h", - "format": "pem", - "crls": []string{crl1, crl2}, - }) - requireSuccessNonNilResponse(t, resp, err) - requireFieldsSetInResp(t, resp, "crl") - pemCrl := resp.Data["crl"].(string) - combinedCrl, err := decodePemCrl(pemCrl) - require.NoError(t, err, "failed decoding combined CRL") - serials := extractSerialsFromCrl(t, combinedCrl) - - require.Contains(t, serials, serial1) - require.Contains(t, serials, serial2) - require.Equal(t, 2, len(serials), "serials contained more serials than expected") - - require.Equal(t, big.NewInt(int64(5)), combinedCrl.Number) - require.Equal(t, combinedCrl.ThisUpdate.Add(12*time.Hour), combinedCrl.NextUpdate) - - extensions := combinedCrl.Extensions - requireExtensionOid(t, []int{2, 5, 29, 27}, extensions) // Delta CRL Extension - requireExtensionOid(t, []int{2, 5, 29, 20}, extensions) // CRL Number Extension - requireExtensionOid(t, []int{2, 5, 29, 35}, extensions) // akidOid - require.Equal(t, 3, len(extensions)) - - err = combinedCrl.CheckSignatureFrom(caCert) - require.NoError(t, err, "failed signature check of CRL") -} - -func TestSignRevocationList(t *testing.T) { - t.Parallel() - - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Mount PKI, use this form of backend so our request is closer to reality (json parsed) - err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }) - require.NoError(t, err) - - // Generate internal CA. - resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "myvault.com", - }) - require.NoError(t, err) - caCert := parseCert(t, resp.Data["certificate"].(string)) - - resp, err = client.Logical().Write("pki/issuer/default/sign-revocation-list", map[string]interface{}{ - "crl_number": "1", - "next_update": "12h", - "format": "pem", - "revoked_certs": []map[string]interface{}{ - { - "serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", - "revocation_time": "1668614976", - "extensions": []map[string]interface{}{}, - }, - { - "serial_number": "27:03:89:76:5a:d4:d8:19:48:47:ca:96:db:6f:27:86:31:92:9f:82", - "revocation_time": "2022-11-16T16:09:36.739592Z", - }, - { - "serial_number": "27:03:89:76:5a:d4:d8:19:48:47:ca:96:db:6f:27:86:31:92:9f:81", - "revocation_time": "2022-10-16T16:09:36.739592Z", - "extensions": []map[string]interface{}{ - { - "id": "2.5.29.100", - "critical": "true", - "value": "aGVsbG8=", // "hello" base64 encoded - }, - { - "id": "2.5.29.101", - "critical": "false", - "value": "Ynll", // "bye" base64 encoded - }, - }, - }, - }, - "extensions": []map[string]interface{}{ - { - "id": "2.5.29.200", - "critical": "true", - "value": "aGVsbG8=", // "hello" base64 encoded - }, - { - "id": "2.5.29.201", - "critical": "false", - "value": "Ynll", // "bye" base64 encoded - }, - }, - }) - require.NoError(t, err) - pemCrl := resp.Data["crl"].(string) - crl, err := decodePemCrl(pemCrl) - require.NoError(t, err, "failed decoding CRL") - serials := extractSerialsFromCrl(t, crl) - require.Contains(t, serials, "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8") - require.Contains(t, serials, "27:03:89:76:5a:d4:d8:19:48:47:ca:96:db:6f:27:86:31:92:9f:82") - require.Contains(t, serials, "27:03:89:76:5a:d4:d8:19:48:47:ca:96:db:6f:27:86:31:92:9f:81") - require.Equal(t, 3, len(serials), "expected 3 serials within CRL") - - // Make sure extensions on serials match what we expect. - require.Equal(t, 0, len(crl.RevokedCertificates[0].Extensions), "Expected no extensions on 1st serial") - require.Equal(t, 0, len(crl.RevokedCertificates[1].Extensions), "Expected no extensions on 2nd serial") - require.Equal(t, 2, len(crl.RevokedCertificates[2].Extensions), "Expected 2 extensions on 3 serial") - require.Equal(t, "2.5.29.100", crl.RevokedCertificates[2].Extensions[0].Id.String()) - require.True(t, crl.RevokedCertificates[2].Extensions[0].Critical) - require.Equal(t, []byte("hello"), crl.RevokedCertificates[2].Extensions[0].Value) - - require.Equal(t, "2.5.29.101", crl.RevokedCertificates[2].Extensions[1].Id.String()) - require.False(t, crl.RevokedCertificates[2].Extensions[1].Critical) - require.Equal(t, []byte("bye"), crl.RevokedCertificates[2].Extensions[1].Value) - - // CRL Number and times - require.Equal(t, big.NewInt(int64(1)), crl.Number) - require.Equal(t, crl.ThisUpdate.Add(12*time.Hour), crl.NextUpdate) - - // Verify top level extensions are present - extensions := crl.Extensions - requireExtensionOid(t, []int{2, 5, 29, 20}, extensions) // CRL Number Extension - requireExtensionOid(t, []int{2, 5, 29, 35}, extensions) // akidOid - requireExtensionOid(t, []int{2, 5, 29, 200}, extensions) // Added value from param - requireExtensionOid(t, []int{2, 5, 29, 201}, extensions) // Added value from param - require.Equal(t, 4, len(extensions)) - - // Signature - err = crl.CheckSignatureFrom(caCert) - require.NoError(t, err, "failed signature check of CRL") -} - -func TestSignRevocationList_NoRevokedCerts(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "test.com", - }) - requireSuccessNonNilResponse(t, resp, err) - - resp, err = CBWrite(b, s, "issuer/default/sign-revocation-list", map[string]interface{}{ - "crl_number": "10000", - "next_update": "12h", - "format": "pem", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuer/default/sign-revocation-list"), logical.UpdateOperation), resp, true) - requireSuccessNonNilResponse(t, resp, err) - requireFieldsSetInResp(t, resp, "crl") - pemCrl := resp.Data["crl"].(string) - crl, err := decodePemCrl(pemCrl) - require.NoError(t, err, "failed decoding CRL") - - serials := extractSerialsFromCrl(t, crl) - require.Equal(t, 0, len(serials), "no serials were expected in CRL") - - require.Equal(t, big.NewInt(int64(10000)), crl.Number) - require.Equal(t, crl.ThisUpdate.Add(12*time.Hour), crl.NextUpdate) -} - -func TestSignRevocationList_ReservedExtensions(t *testing.T) { - t.Parallel() - - reservedOids := []asn1.ObjectIdentifier{ - akOid, deltaCrlOid, crlNumOid, - } - // Validate there isn't copy/paste issues with our constants... - require.Equal(t, asn1.ObjectIdentifier{2, 5, 29, 27}, deltaCrlOid) // Delta CRL Extension - require.Equal(t, asn1.ObjectIdentifier{2, 5, 29, 20}, crlNumOid) // CRL Number Extension - require.Equal(t, asn1.ObjectIdentifier{2, 5, 29, 35}, akOid) // akidOid - - for _, reservedOid := range reservedOids { - t.Run(reservedOid.String(), func(t *testing.T) { - b, s := CreateBackendWithStorage(t) - resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "test.com", - }) - requireSuccessNonNilResponse(t, resp, err) - - resp, err = CBWrite(b, s, "issuer/default/sign-revocation-list", map[string]interface{}{ - "crl_number": "1", - "next_update": "12h", - "format": "pem", - "extensions": []map[string]interface{}{ - { - "id": reservedOid.String(), - "critical": "false", - "value": base64.StdEncoding.EncodeToString([]byte("hello")), - }, - }, - }) - - require.ErrorContains(t, err, "is reserved") - }) - } -} - -func setupResignCrlMounts(t *testing.T, b1 *backend, s1 logical.Storage, b2 *backend, s2 logical.Storage) (*x509.Certificate, string, string, string, string) { - t.Helper() - - // Setup two mounts with the same CA/key material - resp, err := CBWrite(b1, s1, "root/generate/exported", map[string]interface{}{ - "common_name": "test.com", - }) - requireSuccessNonNilResponse(t, resp, err) - requireFieldsSetInResp(t, resp, "certificate", "private_key") - pemCaCert := resp.Data["certificate"].(string) - caCert := parseCert(t, pemCaCert) - privKey := resp.Data["private_key"].(string) - - // Import the above key/cert into another mount - resp, err = CBWrite(b2, s2, "config/ca", map[string]interface{}{ - "pem_bundle": pemCaCert + "\n" + privKey, - }) - requireSuccessNonNilResponse(t, resp, err, "error setting up CA on backend 2") - - // Create the same role in both mounts - resp, err = CBWrite(b1, s1, "roles/test", map[string]interface{}{ - "allowed_domains": "test.com", - "allow_subdomains": "true", - "max_ttl": "1h", - }) - requireSuccessNonNilResponse(t, resp, err, "error setting up pki role on backend 1") - - resp, err = CBWrite(b2, s2, "roles/test", map[string]interface{}{ - "allowed_domains": "test.com", - "allow_subdomains": "true", - "max_ttl": "1h", - }) - requireSuccessNonNilResponse(t, resp, err, "error setting up pki role on backend 2") - - // Issue and revoke a cert in backend 1 - resp, err = CBWrite(b1, s1, "issue/test", map[string]interface{}{ - "common_name": "test1.test.com", - }) - requireSuccessNonNilResponse(t, resp, err, "error issuing cert from backend 1") - requireFieldsSetInResp(t, resp, "serial_number") - serial1 := resp.Data["serial_number"].(string) - - resp, err = CBWrite(b1, s1, "revoke", map[string]interface{}{"serial_number": serial1}) - requireSuccessNonNilResponse(t, resp, err, "error revoking cert from backend 2") - - // Issue and revoke a cert in backend 2 - resp, err = CBWrite(b2, s2, "issue/test", map[string]interface{}{ - "common_name": "test1.test.com", - }) - requireSuccessNonNilResponse(t, resp, err, "error issuing cert from backend 2") - requireFieldsSetInResp(t, resp, "serial_number") - serial2 := resp.Data["serial_number"].(string) - - resp, err = CBWrite(b2, s2, "revoke", map[string]interface{}{"serial_number": serial2}) - requireSuccessNonNilResponse(t, resp, err, "error revoking cert from backend 2") - - // Fetch PEM CRLs from each - resp, err = CBRead(b1, s1, "cert/crl") - requireSuccessNonNilResponse(t, resp, err, "error fetch crl from backend 1") - requireFieldsSetInResp(t, resp, "certificate") - crl1 := resp.Data["certificate"].(string) - - resp, err = CBRead(b2, s2, "cert/crl") - requireSuccessNonNilResponse(t, resp, err, "error fetch crl from backend 2") - requireFieldsSetInResp(t, resp, "certificate") - crl2 := resp.Data["certificate"].(string) - return caCert, serial1, serial2, crl1, crl2 -} - -func requireExtensionOid(t *testing.T, identifier asn1.ObjectIdentifier, extensions []pkix.Extension, msgAndArgs ...interface{}) { - t.Helper() - - found := false - var oidsInExtensions []string - for _, extension := range extensions { - oidsInExtensions = append(oidsInExtensions, extension.Id.String()) - if extension.Id.Equal(identifier) { - found = true - break - } - } - - if !found { - msg := fmt.Sprintf("Failed to find matching asn oid %s out of %v", identifier.String(), oidsInExtensions) - require.Fail(t, msg, msgAndArgs) - } -} - -func extractSerialsFromCrl(t *testing.T, crl *x509.RevocationList) map[string]time.Time { - t.Helper() - - serials := map[string]time.Time{} - - for _, revokedCert := range crl.RevokedCertificates { - serial := serialFromBigInt(revokedCert.SerialNumber) - if _, exists := serials[serial]; exists { - t.Fatalf("Serial number %s was duplicated in CRL", serial) - } - serials[serial] = revokedCert.RevocationTime - } - return serials -} diff --git a/builtin/logical/pki/path_roles_test.go b/builtin/logical/pki/path_roles_test.go deleted file mode 100644 index 544cafcc7..000000000 --- a/builtin/logical/pki/path_roles_test.go +++ /dev/null @@ -1,1162 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto/x509" - "encoding/asn1" - "encoding/base64" - "encoding/pem" - "fmt" - "testing" - - "github.com/go-errors/errors" - "github.com/hashicorp/go-secure-stdlib/strutil" - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPki_RoleGenerateLease(t *testing.T) { - t.Parallel() - var resp *logical.Response - var err error - b, storage := CreateBackendWithStorage(t) - - roleData := map[string]interface{}{ - "allowed_domains": "myvault.com", - "ttl": "5h", - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleReq.Operation = logical.ReadOperation - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - // generate_lease cannot be nil. It either has to be set during role - // creation or has to be filled in by the upgrade code - generateLease := resp.Data["generate_lease"].(*bool) - if generateLease == nil { - t.Fatalf("generate_lease should not be nil") - } - - // By default, generate_lease should be `false` - if *generateLease { - t.Fatalf("generate_lease should not be set by default") - } - - // To test upgrade of generate_lease, we read the storage entry, - // modify it to remove generate_lease, and rewrite it. - entry, err := storage.Get(context.Background(), "role/testrole") - if err != nil || entry == nil { - t.Fatal(err) - } - - var role roleEntry - if err := entry.DecodeJSON(&role); err != nil { - t.Fatal(err) - } - - role.GenerateLease = nil - - entry, err = logical.StorageEntryJSON("role/testrole", role) - if err != nil { - t.Fatal(err) - } - if err := storage.Put(context.Background(), entry); err != nil { - t.Fatal(err) - } - - // Reading should upgrade generate_lease - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - generateLease = resp.Data["generate_lease"].(*bool) - if generateLease == nil { - t.Fatalf("generate_lease should not be nil") - } - - // Upgrade should set generate_lease to `true` - if !*generateLease { - t.Fatalf("generate_lease should be set after an upgrade") - } - - // Make sure that setting generate_lease to `true` works properly - roleReq.Operation = logical.UpdateOperation - roleReq.Path = "roles/testrole2" - roleReq.Data["generate_lease"] = true - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - generateLease = resp.Data["generate_lease"].(*bool) - if generateLease == nil { - t.Fatalf("generate_lease should not be nil") - } - if !*generateLease { - t.Fatalf("generate_lease should have been set") - } -} - -func TestPki_RoleKeyUsage(t *testing.T) { - t.Parallel() - var resp *logical.Response - var err error - b, storage := CreateBackendWithStorage(t) - - roleData := map[string]interface{}{ - "allowed_domains": "myvault.com", - "ttl": "5h", - "key_usage": []string{"KeyEncipherment", "DigitalSignature"}, - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route(roleReq.Path), logical.UpdateOperation), resp, true) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route(roleReq.Path), logical.ReadOperation), resp, true) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - keyUsage := resp.Data["key_usage"].([]string) - if len(keyUsage) != 2 { - t.Fatalf("key_usage should have 2 values") - } - - // To test the upgrade of KeyUsageOld into KeyUsage, we read - // the storage entry, modify it to set KUO and unset KU, and - // rewrite it. - entry, err := storage.Get(context.Background(), "role/testrole") - if err != nil || entry == nil { - t.Fatal(err) - } - - var role roleEntry - if err := entry.DecodeJSON(&role); err != nil { - t.Fatal(err) - } - - role.KeyUsageOld = "KeyEncipherment,DigitalSignature" - role.KeyUsage = nil - - entry, err = logical.StorageEntryJSON("role/testrole", role) - if err != nil { - t.Fatal(err) - } - if err := storage.Put(context.Background(), entry); err != nil { - t.Fatal(err) - } - - // Reading should upgrade key_usage - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - keyUsage = resp.Data["key_usage"].([]string) - if len(keyUsage) != 2 { - t.Fatalf("key_usage should have 2 values") - } - - // Read back from storage to ensure upgrade - entry, err = storage.Get(context.Background(), "role/testrole") - if err != nil { - t.Fatalf("err: %v", err) - } - if entry == nil { - t.Fatalf("role should not be nil") - } - var result roleEntry - if err := entry.DecodeJSON(&result); err != nil { - t.Fatalf("err: %v", err) - } - - if result.KeyUsageOld != "" { - t.Fatal("old key usage value should be blank") - } - if len(result.KeyUsage) != 2 { - t.Fatal("key_usage should have 2 values") - } -} - -func TestPki_RoleOUOrganizationUpgrade(t *testing.T) { - t.Parallel() - var resp *logical.Response - var err error - b, storage := CreateBackendWithStorage(t) - - roleData := map[string]interface{}{ - "allowed_domains": "myvault.com", - "ttl": "5h", - "ou": []string{"abc", "123"}, - "organization": []string{"org1", "org2"}, - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - ou := resp.Data["ou"].([]string) - if len(ou) != 2 { - t.Fatalf("ou should have 2 values") - } - organization := resp.Data["organization"].([]string) - if len(organization) != 2 { - t.Fatalf("organization should have 2 values") - } - - // To test upgrade of O/OU, we read the storage entry, modify it to set - // the old O/OU value over the new one, and rewrite it. - entry, err := storage.Get(context.Background(), "role/testrole") - if err != nil || entry == nil { - t.Fatal(err) - } - - var role roleEntry - if err := entry.DecodeJSON(&role); err != nil { - t.Fatal(err) - } - role.OUOld = "abc,123" - role.OU = nil - role.OrganizationOld = "org1,org2" - role.Organization = nil - - entry, err = logical.StorageEntryJSON("role/testrole", role) - if err != nil { - t.Fatal(err) - } - if err := storage.Put(context.Background(), entry); err != nil { - t.Fatal(err) - } - - // Reading should upgrade key_usage - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - ou = resp.Data["ou"].([]string) - if len(ou) != 2 { - t.Fatalf("ou should have 2 values") - } - organization = resp.Data["organization"].([]string) - if len(organization) != 2 { - t.Fatalf("organization should have 2 values") - } - - // Read back from storage to ensure upgrade - entry, err = storage.Get(context.Background(), "role/testrole") - if err != nil { - t.Fatalf("err: %v", err) - } - if entry == nil { - t.Fatalf("role should not be nil") - } - var result roleEntry - if err := entry.DecodeJSON(&result); err != nil { - t.Fatalf("err: %v", err) - } - - if result.OUOld != "" { - t.Fatal("old ou value should be blank") - } - if len(result.OU) != 2 { - t.Fatal("ou should have 2 values") - } - if result.OrganizationOld != "" { - t.Fatal("old organization value should be blank") - } - if len(result.Organization) != 2 { - t.Fatal("organization should have 2 values") - } -} - -func TestPki_RoleAllowedDomains(t *testing.T) { - t.Parallel() - var resp *logical.Response - var err error - b, storage := CreateBackendWithStorage(t) - - roleData := map[string]interface{}{ - "allowed_domains": []string{"foobar.com", "*example.com"}, - "ttl": "5h", - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - allowedDomains := resp.Data["allowed_domains"].([]string) - if len(allowedDomains) != 2 { - t.Fatalf("allowed_domains should have 2 values") - } - - // To test upgrade of allowed_domains, we read the storage entry, - // set the old one, and rewrite it. - entry, err := storage.Get(context.Background(), "role/testrole") - if err != nil || entry == nil { - t.Fatal(err) - } - - var role roleEntry - if err := entry.DecodeJSON(&role); err != nil { - t.Fatal(err) - } - role.AllowedDomainsOld = "foobar.com,*example.com" - role.AllowedDomains = nil - - entry, err = logical.StorageEntryJSON("role/testrole", role) - if err != nil { - t.Fatal(err) - } - if err := storage.Put(context.Background(), entry); err != nil { - t.Fatal(err) - } - - // Reading should upgrade key_usage - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - allowedDomains = resp.Data["allowed_domains"].([]string) - if len(allowedDomains) != 2 { - t.Fatalf("allowed_domains should have 2 values") - } - - // Read back from storage to ensure upgrade - entry, err = storage.Get(context.Background(), "role/testrole") - if err != nil { - t.Fatalf("err: %v", err) - } - if entry == nil { - t.Fatalf("role should not be nil") - } - var result roleEntry - if err := entry.DecodeJSON(&result); err != nil { - t.Fatalf("err: %v", err) - } - - if result.AllowedDomainsOld != "" { - t.Fatal("old allowed_domains value should be blank") - } - if len(result.AllowedDomains) != 2 { - t.Fatal("allowed_domains should have 2 values") - } -} - -func TestPki_RoleAllowedURISANs(t *testing.T) { - t.Parallel() - var resp *logical.Response - var err error - b, storage := CreateBackendWithStorage(t) - - roleData := map[string]interface{}{ - "allowed_uri_sans": []string{"http://foobar.com", "spiffe://*"}, - "ttl": "5h", - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - allowedURISANs := resp.Data["allowed_uri_sans"].([]string) - if len(allowedURISANs) != 2 { - t.Fatalf("allowed_uri_sans should have 2 values") - } -} - -func TestPki_RolePkixFields(t *testing.T) { - t.Parallel() - var resp *logical.Response - var err error - b, storage := CreateBackendWithStorage(t) - - roleData := map[string]interface{}{ - "ttl": "5h", - "country": []string{"c1", "c2"}, - "ou": []string{"abc", "123"}, - "organization": []string{"org1", "org2"}, - "locality": []string{"foocity", "bartown"}, - "province": []string{"bar", "foo"}, - "street_address": []string{"123 foo street", "789 bar avenue"}, - "postal_code": []string{"f00", "b4r"}, - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole_pkixfields", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - origCountry := roleData["country"].([]string) - respCountry := resp.Data["country"].([]string) - if !strutil.StrListSubset(origCountry, respCountry) { - t.Fatalf("country did not match values set in role") - } else if len(origCountry) != len(respCountry) { - t.Fatalf("country did not have same number of values set in role") - } - - origOU := roleData["ou"].([]string) - respOU := resp.Data["ou"].([]string) - if !strutil.StrListSubset(origOU, respOU) { - t.Fatalf("ou did not match values set in role") - } else if len(origOU) != len(respOU) { - t.Fatalf("ou did not have same number of values set in role") - } - - origOrganization := roleData["organization"].([]string) - respOrganization := resp.Data["organization"].([]string) - if !strutil.StrListSubset(origOrganization, respOrganization) { - t.Fatalf("organization did not match values set in role") - } else if len(origOrganization) != len(respOrganization) { - t.Fatalf("organization did not have same number of values set in role") - } - - origLocality := roleData["locality"].([]string) - respLocality := resp.Data["locality"].([]string) - if !strutil.StrListSubset(origLocality, respLocality) { - t.Fatalf("locality did not match values set in role") - } else if len(origLocality) != len(respLocality) { - t.Fatalf("locality did not have same number of values set in role: ") - } - - origProvince := roleData["province"].([]string) - respProvince := resp.Data["province"].([]string) - if !strutil.StrListSubset(origProvince, respProvince) { - t.Fatalf("province did not match values set in role") - } else if len(origProvince) != len(respProvince) { - t.Fatalf("province did not have same number of values set in role") - } - - origStreetAddress := roleData["street_address"].([]string) - respStreetAddress := resp.Data["street_address"].([]string) - if !strutil.StrListSubset(origStreetAddress, respStreetAddress) { - t.Fatalf("street_address did not match values set in role") - } else if len(origStreetAddress) != len(respStreetAddress) { - t.Fatalf("street_address did not have same number of values set in role") - } - - origPostalCode := roleData["postal_code"].([]string) - respPostalCode := resp.Data["postal_code"].([]string) - if !strutil.StrListSubset(origPostalCode, respPostalCode) { - t.Fatalf("postal_code did not match values set in role") - } else if len(origPostalCode) != len(respPostalCode) { - t.Fatalf("postal_code did not have same number of values set in role") - } -} - -func TestPki_RoleNoStore(t *testing.T) { - t.Parallel() - var resp *logical.Response - var err error - b, storage := CreateBackendWithStorage(t) - - roleData := map[string]interface{}{ - "allowed_domains": "myvault.com", - "ttl": "5h", - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleReq.Operation = logical.ReadOperation - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - // By default, no_store should be `false` - noStore := resp.Data["no_store"].(bool) - if noStore { - t.Fatalf("no_store should not be set by default") - } - - // By default, allowed_domains_template should be `false` - allowedDomainsTemplate := resp.Data["allowed_domains_template"].(bool) - if allowedDomainsTemplate { - t.Fatalf("allowed_domains_template should not be set by default") - } - - // By default, allowed_uri_sans_template should be `false` - allowedURISANsTemplate := resp.Data["allowed_uri_sans_template"].(bool) - if allowedURISANsTemplate { - t.Fatalf("allowed_uri_sans_template should not be set by default") - } - - // Make sure that setting no_store to `true` works properly - roleReq.Operation = logical.UpdateOperation - roleReq.Path = "roles/testrole_nostore" - roleReq.Data["no_store"] = true - roleReq.Data["allowed_domain"] = "myvault.com" - roleReq.Data["allow_subdomains"] = true - roleReq.Data["ttl"] = "5h" - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - noStore = resp.Data["no_store"].(bool) - if !noStore { - t.Fatalf("no_store should have been set to true") - } - - // issue a certificate and test that it's not stored - caData := map[string]interface{}{ - "common_name": "myvault.com", - "ttl": "5h", - "ip_sans": "127.0.0.1", - } - caReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/generate/internal", - Storage: storage, - Data: caData, - } - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - issueData := map[string]interface{}{ - "common_name": "cert.myvault.com", - "format": "pem", - "ip_sans": "127.0.0.1", - "ttl": "1h", - } - issueReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issue/testrole_nostore", - Storage: storage, - Data: issueData, - } - - resp, err = b.HandleRequest(context.Background(), issueReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - // list certs - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ListOperation, - Path: "certs", - Storage: storage, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - if len(resp.Data["keys"].([]string)) != 1 { - t.Fatalf("Only the CA certificate should be stored: %#v", resp) - } -} - -func TestPki_CertsLease(t *testing.T) { - t.Parallel() - var resp *logical.Response - var err error - b, storage := CreateBackendWithStorage(t) - - caData := map[string]interface{}{ - "common_name": "myvault.com", - "ttl": "5h", - "ip_sans": "127.0.0.1", - } - - caReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/generate/internal", - Storage: storage, - Data: caData, - } - - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - roleData := map[string]interface{}{ - "allowed_domains": "myvault.com", - "allow_subdomains": true, - "ttl": "2h", - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - issueData := map[string]interface{}{ - "common_name": "cert.myvault.com", - "format": "pem", - "ip_sans": "127.0.0.1", - } - issueReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issue/testrole", - Storage: storage, - Data: issueData, - } - - resp, err = b.HandleRequest(context.Background(), issueReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - if resp.Secret != nil { - t.Fatalf("expected a response that does not contain a secret") - } - - // Turn on the lease generation and issue a certificate. The response - // should have a `Secret` object populated. - roleData["generate_lease"] = true - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - resp, err = b.HandleRequest(context.Background(), issueReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, resp) - } - - if resp.Secret == nil { - t.Fatalf("expected a response that contains a secret") - } -} - -func TestPki_RolePatch(t *testing.T) { - t.Parallel() - type TestCase struct { - Field string - Before interface{} - Patched interface{} - } - - testCases := []TestCase{ - { - Field: "ttl", - Before: int64(5), - Patched: int64(10), - }, - { - Field: "max_ttl", - Before: int64(5), - Patched: int64(10), - }, - { - Field: "allow_localhost", - Before: true, - Patched: false, - }, - { - Field: "allowed_domains", - Before: []string{"alex", "bob"}, - Patched: []string{"sam", "alex", "frank"}, - }, - { - Field: "allowed_domains_template", - Before: false, - Patched: true, - }, - { - Field: "allow_bare_domains", - Before: true, - Patched: false, - }, - { - Field: "allow_subdomains", - Before: false, - Patched: true, - }, - { - Field: "allow_glob_domains", - Before: true, - Patched: false, - }, - { - Field: "allow_wildcard_certificates", - Before: false, - Patched: true, - }, - { - Field: "allow_any_name", - Before: true, - Patched: false, - }, - { - Field: "enforce_hostnames", - Before: false, - Patched: true, - }, - { - Field: "allow_ip_sans", - Before: true, - Patched: false, - }, - { - Field: "allowed_uri_sans", - Before: []string{"gopher://*"}, - Patched: []string{"https://*"}, - }, - { - Field: "allowed_uri_sans_template", - Before: false, - Patched: true, - }, - { - Field: "allowed_other_sans", - Before: []string{"1.2.3.4;UTF8:magic"}, - Patched: []string{"4.3.2.1;UTF8:cigam"}, - }, - { - Field: "allowed_serial_numbers", - Before: []string{"*"}, - Patched: []string{""}, - }, - { - Field: "server_flag", - Before: true, - Patched: false, - }, - { - Field: "client_flag", - Before: false, - Patched: true, - }, - { - Field: "code_signing_flag", - Before: true, - Patched: false, - }, - { - Field: "email_protection_flag", - Before: false, - Patched: true, - }, - // key_type, key_bits, and signature_bits can't be tested in this setup - // due to their non-default stored nature. - { - Field: "key_usage", - Before: []string{"DigitialSignature"}, - Patched: []string{"DigitalSignature", "KeyAgreement"}, - }, - { - Field: "ext_key_usage", - Before: []string{"ServerAuth"}, - Patched: []string{"ClientAuth"}, - }, - { - Field: "ext_key_usage_oids", - Before: []string{"1.2.3.4"}, - Patched: []string{"4.3.2.1"}, - }, - { - Field: "use_csr_common_name", - Before: true, - Patched: false, - }, - { - Field: "use_csr_sans", - Before: false, - Patched: true, - }, - { - Field: "ou", - Before: []string{"crypto"}, - Patched: []string{"cryptosec"}, - }, - { - Field: "organization", - Before: []string{"hashicorp"}, - Patched: []string{"dadgarcorp"}, - }, - { - Field: "country", - Before: []string{"US"}, - Patched: []string{"USA"}, - }, - { - Field: "locality", - Before: []string{"Orange"}, - Patched: []string{"Blue"}, - }, - { - Field: "province", - Before: []string{"CA"}, - Patched: []string{"AC"}, - }, - { - Field: "street_address", - Before: []string{"101 First"}, - Patched: []string{"202 Second", "Unit 020"}, - }, - { - Field: "postal_code", - Before: []string{"12345"}, - Patched: []string{"54321-1234"}, - }, - { - Field: "generate_lease", - Before: false, - Patched: true, - }, - { - Field: "no_store", - Before: true, - Patched: false, - }, - { - Field: "require_cn", - Before: false, - Patched: true, - }, - { - Field: "policy_identifiers", - Before: []string{"1.3.6.1.4.1.1.1"}, - Patched: []string{"1.3.6.1.4.1.1.2"}, - }, - { - Field: "basic_constraints_valid_for_non_ca", - Before: true, - Patched: false, - }, - { - Field: "not_before_duration", - Before: int64(30), - Patched: int64(300), - }, - { - Field: "not_after", - Before: "9999-12-31T23:59:59Z", - Patched: "1230-12-31T23:59:59Z", - }, - { - Field: "issuer_ref", - Before: "default", - Patched: "missing", - }, - } - - b, storage := CreateBackendWithStorage(t) - - for index, testCase := range testCases { - var resp *logical.Response - var roleDataResp *logical.Response - var afterRoleDataResp *logical.Response - var err error - - // Create the role - roleData := map[string]interface{}{} - roleData[testCase.Field] = testCase.Before - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole", - Storage: storage, - Data: roleData, - } - - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad [%d/%v] create: err: %v resp: %#v", index, testCase.Field, err, resp) - } - - // Read the role after creation - roleReq.Operation = logical.ReadOperation - roleDataResp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (roleDataResp != nil && roleDataResp.IsError()) { - t.Fatalf("bad [%d/%v] read: err: %v resp: %#v", index, testCase.Field, err, resp) - } - - beforeRoleData := roleDataResp.Data - - // Patch the role - roleReq.Operation = logical.PatchOperation - roleReq.Data[testCase.Field] = testCase.Patched - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad [%d/%v] patch: err: %v resp: %#v", index, testCase.Field, err, resp) - } - - // Re-read and verify the role - roleReq.Operation = logical.ReadOperation - afterRoleDataResp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (afterRoleDataResp != nil && afterRoleDataResp.IsError()) { - t.Fatalf("bad [%d/%v] read: err: %v resp: %#v", index, testCase.Field, err, resp) - } - - afterRoleData := afterRoleDataResp.Data - - for field, before := range beforeRoleData { - switch typed := before.(type) { - case *bool: - before = *typed - afterRoleData[field] = *(afterRoleData[field].(*bool)) - } - - if field != testCase.Field { - require.Equal(t, before, afterRoleData[field], fmt.Sprintf("bad [%d/%v] compare: non-modified field %v should not be changed", index, testCase.Field, field)) - } else { - require.Equal(t, before, testCase.Before, fmt.Sprintf("bad [%d] compare: modified field %v before should be correct", index, field)) - require.Equal(t, afterRoleData[field], testCase.Patched, fmt.Sprintf("bad [%d] compare: modified field %v after should be correct", index, field)) - } - } - } -} - -func TestPKI_RolePolicyInformation_Flat(t *testing.T) { - t.Parallel() - type TestCase struct { - Input interface{} - ASN interface{} - OidList []string - } - - expectedSimpleAsnExtension := "MBYwCQYHKwYBBAEBATAJBgcrBgEEAQEC" - expectedSimpleOidList := append(*new([]string), "1.3.6.1.4.1.1.1", "1.3.6.1.4.1.1.2") - - testCases := []TestCase{ - { - Input: "1.3.6.1.4.1.1.1,1.3.6.1.4.1.1.2", - ASN: expectedSimpleAsnExtension, - OidList: expectedSimpleOidList, - }, - { - Input: "[{\"oid\":\"1.3.6.1.4.1.1.1\"},{\"oid\":\"1.3.6.1.4.1.1.2\"}]", - ASN: expectedSimpleAsnExtension, - OidList: expectedSimpleOidList, - }, - { - Input: "[{\"oid\":\"1.3.6.1.4.1.7.8\",\"notice\":\"I am a user Notice\"},{\"oid\":\"1.3.6.1.44947.1.2.4\",\"cps\":\"https://example.com\"}]", - ASN: "MF8wLQYHKwYBBAEHCDAiMCAGCCsGAQUFBwICMBQMEkkgYW0gYSB1c2VyIE5vdGljZTAuBgkrBgGC3xMBAgQwITAfBggrBgEFBQcCARYTaHR0cHM6Ly9leGFtcGxlLmNvbQ==", - OidList: append(*new([]string), "1.3.6.1.4.1.7.8", "1.3.6.1.44947.1.2.4"), - }, - } - - b, storage := CreateBackendWithStorage(t) - - caData := map[string]interface{}{ - "common_name": "myvault.com", - "ttl": "5h", - "ip_sans": "127.0.0.1", - } - caReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/generate/internal", - Storage: storage, - Data: caData, - } - caResp, err := b.HandleRequest(context.Background(), caReq) - if err != nil || (caResp != nil && caResp.IsError()) { - t.Fatalf("bad: err: %v resp: %#v", err, caResp) - } - - for index, testCase := range testCases { - var roleResp *logical.Response - var issueResp *logical.Response - var err error - - // Create/update the role - roleData := map[string]interface{}{} - roleData[policyIdentifiersParam] = testCase.Input - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/testrole", - Storage: storage, - Data: roleData, - } - - roleResp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (roleResp != nil && roleResp.IsError()) { - t.Fatalf("bad [%d], setting policy identifier %v err: %v resp: %#v", index, testCase.Input, err, roleResp) - } - - // Issue Using this role - issueData := map[string]interface{}{} - issueData["common_name"] = "localhost" - issueData["ttl"] = "2s" - - issueReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issue/testrole", - Storage: storage, - Data: issueData, - } - - issueResp, err = b.HandleRequest(context.Background(), issueReq) - if err != nil || (issueResp != nil && issueResp.IsError()) { - t.Fatalf("bad [%d], setting policy identifier %v err: %v resp: %#v", index, testCase.Input, err, issueResp) - } - - // Validate the OIDs - policyIdentifiers, err := getPolicyIdentifiersOffCertificate(*issueResp) - if err != nil { - t.Fatalf("bad [%d], getting policy identifier from %v err: %v resp: %#v", index, testCase.Input, err, issueResp) - } - if len(policyIdentifiers) != len(testCase.OidList) { - t.Fatalf("bad [%d], wrong certificate policy identifier from %v len expected: %d got %d", index, testCase.Input, len(testCase.OidList), len(policyIdentifiers)) - } - for i, identifier := range policyIdentifiers { - if identifier != testCase.OidList[i] { - t.Fatalf("bad [%d], wrong certificate policy identifier from %v expected: %v got %v", index, testCase.Input, testCase.OidList[i], policyIdentifiers[i]) - } - } - // Validate the ASN - certificateAsn, err := getPolicyInformationExtensionOffCertificate(*issueResp) - if err != nil { - t.Fatalf("bad [%d], getting extension from %v err: %v resp: %#v", index, testCase.Input, err, issueResp) - } - certificateB64 := make([]byte, len(certificateAsn)*2) - base64.StdEncoding.Encode(certificateB64, certificateAsn) - certificateString := string(certificateB64[:]) - assert.Contains(t, certificateString, testCase.ASN) - } -} - -func getPolicyIdentifiersOffCertificate(resp logical.Response) ([]string, error) { - stringCertificate := resp.Data["certificate"].(string) - block, _ := pem.Decode([]byte(stringCertificate)) - certificate, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, err - } - policyIdentifierStrings := make([]string, len(certificate.PolicyIdentifiers)) - for index, asnOid := range certificate.PolicyIdentifiers { - policyIdentifierStrings[index] = asnOid.String() - } - return policyIdentifierStrings, nil -} - -func getPolicyInformationExtensionOffCertificate(resp logical.Response) ([]byte, error) { - stringCertificate := resp.Data["certificate"].(string) - block, _ := pem.Decode([]byte(stringCertificate)) - certificate, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, err - } - for _, extension := range certificate.Extensions { - if extension.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 32}) { - return extension.Value, nil - } - } - return *new([]byte), errors.New("No Policy Information Extension Found") -} diff --git a/builtin/logical/pki/path_tidy_test.go b/builtin/logical/pki/path_tidy_test.go deleted file mode 100644 index 1eb00c7f5..000000000 --- a/builtin/logical/pki/path_tidy_test.go +++ /dev/null @@ -1,1322 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "path" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "golang.org/x/crypto/acme" - - "github.com/hashicorp/vault/helper/testhelpers" - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - - "github.com/armon/go-metrics" - - "github.com/hashicorp/vault/api" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - - "github.com/stretchr/testify/require" -) - -func TestTidyConfigs(t *testing.T) { - t.Parallel() - - var cfg tidyConfig - operations := strings.Split(cfg.AnyTidyConfig(), " / ") - require.Greater(t, len(operations), 1, "expected more than one operation") - t.Logf("Got tidy operations: %v", operations) - - lastOp := operations[len(operations)-1] - - for _, operation := range operations { - b, s := CreateBackendWithStorage(t) - - resp, err := CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ - "enabled": true, - operation: true, - }) - requireSuccessNonNilResponse(t, resp, err, "expected to be able to enable auto-tidy operation "+operation) - - resp, err = CBRead(b, s, "config/auto-tidy") - requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for operation "+operation) - require.True(t, resp.Data[operation].(bool), "expected operation to be enabled after reading auto-tidy config "+operation) - - resp, err = CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ - "enabled": true, - operation: false, - lastOp: true, - }) - requireSuccessNonNilResponse(t, resp, err, "expected to be able to disable auto-tidy operation "+operation) - - resp, err = CBRead(b, s, "config/auto-tidy") - requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for operation "+operation) - require.False(t, resp.Data[operation].(bool), "expected operation to be disabled after reading auto-tidy config "+operation) - - resp, err = CBWrite(b, s, "tidy", map[string]interface{}{ - operation: true, - }) - requireSuccessNonNilResponse(t, resp, err, "expected to be able to start tidy operation with "+operation) - if len(resp.Warnings) > 0 { - t.Logf("got warnings while starting manual tidy: %v", resp.Warnings) - for _, warning := range resp.Warnings { - if strings.Contains(warning, "Manual tidy requested but no tidy operations were set.") { - t.Fatalf("expected to be able to enable tidy operation with just %v but got warning: %v / (resp=%v)", operation, warning, resp) - } - } - } - - lastOp = operation - } - - // pause_duration is tested elsewhere in other tests. - type configSafetyBufferValueStr struct { - Config string - FirstValue int - SecondValue int - DefaultValue int - } - configSafetyBufferValues := []configSafetyBufferValueStr{ - { - Config: "safety_buffer", - FirstValue: 1, - SecondValue: 2, - DefaultValue: int(defaultTidyConfig.SafetyBuffer / time.Second), - }, - { - Config: "issuer_safety_buffer", - FirstValue: 1, - SecondValue: 2, - DefaultValue: int(defaultTidyConfig.IssuerSafetyBuffer / time.Second), - }, - { - Config: "acme_account_safety_buffer", - FirstValue: 1, - SecondValue: 2, - DefaultValue: int(defaultTidyConfig.AcmeAccountSafetyBuffer / time.Second), - }, - { - Config: "revocation_queue_safety_buffer", - FirstValue: 1, - SecondValue: 2, - DefaultValue: int(defaultTidyConfig.QueueSafetyBuffer / time.Second), - }, - } - - for _, flag := range configSafetyBufferValues { - b, s := CreateBackendWithStorage(t) - - resp, err := CBRead(b, s, "config/auto-tidy") - requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for flag "+flag.Config) - require.Equal(t, resp.Data[flag.Config].(int), flag.DefaultValue, "expected initial auto-tidy config to match default value for "+flag.Config) - - resp, err = CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ - "enabled": true, - "tidy_cert_store": true, - flag.Config: flag.FirstValue, - }) - requireSuccessNonNilResponse(t, resp, err, "expected to be able to set auto-tidy config option "+flag.Config) - - resp, err = CBRead(b, s, "config/auto-tidy") - requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for config "+flag.Config) - require.Equal(t, resp.Data[flag.Config].(int), flag.FirstValue, "expected value to be set after reading auto-tidy config "+flag.Config) - - resp, err = CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ - "enabled": true, - "tidy_cert_store": true, - flag.Config: flag.SecondValue, - }) - requireSuccessNonNilResponse(t, resp, err, "expected to be able to set auto-tidy config option "+flag.Config) - - resp, err = CBRead(b, s, "config/auto-tidy") - requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for config "+flag.Config) - require.Equal(t, resp.Data[flag.Config].(int), flag.SecondValue, "expected value to be set after reading auto-tidy config "+flag.Config) - - resp, err = CBWrite(b, s, "tidy", map[string]interface{}{ - "tidy_cert_store": true, - flag.Config: flag.FirstValue, - }) - t.Logf("tidy run results: resp=%v/err=%v", resp, err) - requireSuccessNonNilResponse(t, resp, err, "expected to be able to start tidy operation with "+flag.Config) - if len(resp.Warnings) > 0 { - for _, warning := range resp.Warnings { - if strings.Contains(warning, "unrecognized parameter") && strings.Contains(warning, flag.Config) { - t.Fatalf("warning '%v' claims parameter '%v' is unknown", warning, flag.Config) - } - } - } - - time.Sleep(2 * time.Second) - - resp, err = CBRead(b, s, "tidy-status") - requireSuccessNonNilResponse(t, resp, err, "expected to be able to start tidy operation with "+flag.Config) - t.Logf("got response: %v for config: %v", resp, flag.Config) - require.Equal(t, resp.Data[flag.Config].(int), flag.FirstValue, "expected flag to be set in tidy-status for config "+flag.Config) - } -} - -func TestAutoTidy(t *testing.T) { - t.Parallel() - - // While we'd like to reduce this duration, we need to wait until - // the rollback manager timer ticks. With the new helper, we can - // modify the rollback manager timer period directly, allowing us - // to shorten the total test time significantly. - // - // We set the delta CRL time to ensure it executes prior to the - // main CRL rebuild, and the new CRL doesn't rebuild until after - // we're done. - newPeriod := 1 * time.Second - - // This test requires the periodicFunc to trigger, which requires we stand - // up a full test cluster. - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - // See notes below about usage of /sys/raw for reading cluster - // storage without barrier encryption. - EnableRaw: true, - RollbackPeriod: newPeriod, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Mount PKI - err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "10m", - MaxLeaseTTL: "60m", - }, - }) - require.NoError(t, err) - - // Generate root. - resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "Root X1", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data) - require.NotEmpty(t, resp.Data["issuer_id"]) - issuerId := resp.Data["issuer_id"] - - // Run tidy so status is not empty when we run it later... - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_revoked_certs": true, - }) - require.NoError(t, err) - - // Setup a testing role. - _, err = client.Logical().Write("pki/roles/local-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - }) - require.NoError(t, err) - - // Write the auto-tidy config. - _, err = client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ - "enabled": true, - "interval_duration": "1s", - "tidy_cert_store": true, - "tidy_revoked_certs": true, - "safety_buffer": "1s", - }) - require.NoError(t, err) - - // Issue a cert and revoke it. - resp, err = client.Logical().Write("pki/issue/local-testing", map[string]interface{}{ - "common_name": "example.com", - "ttl": "10s", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["serial_number"]) - require.NotEmpty(t, resp.Data["certificate"]) - leafSerial := resp.Data["serial_number"].(string) - leafCert := parseCert(t, resp.Data["certificate"].(string)) - - // Read cert before revoking - resp, err = client.Logical().Read("pki/cert/" + leafSerial) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["certificate"]) - revocationTime, err := (resp.Data["revocation_time"].(json.Number)).Int64() - require.Equal(t, int64(0), revocationTime, "revocation time was not zero") - require.Empty(t, resp.Data["revocation_time_rfc3339"], "revocation_time_rfc3339 was not empty") - require.Empty(t, resp.Data["issuer_id"], "issuer_id was not empty") - - _, err = client.Logical().Write("pki/revoke", map[string]interface{}{ - "serial_number": leafSerial, - }) - require.NoError(t, err) - - // Cert should still exist. - resp, err = client.Logical().Read("pki/cert/" + leafSerial) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["certificate"]) - revocationTime, err = (resp.Data["revocation_time"].(json.Number)).Int64() - require.NoError(t, err, "failed converting %s to int", resp.Data["revocation_time"]) - revTime := time.Unix(revocationTime, 0) - now := time.Now() - if !(now.After(revTime) && now.Add(-10*time.Minute).Before(revTime)) { - t.Fatalf("parsed revocation time not within the last 10 minutes current time: %s, revocation time: %s", now, revTime) - } - utcLoc, err := time.LoadLocation("UTC") - require.NoError(t, err, "failed to parse UTC location?") - - rfc3339RevocationTime, err := time.Parse(time.RFC3339Nano, resp.Data["revocation_time_rfc3339"].(string)) - require.NoError(t, err, "failed parsing revocation_time_rfc3339 field: %s", resp.Data["revocation_time_rfc3339"]) - - require.Equal(t, revTime.In(utcLoc), rfc3339RevocationTime.Truncate(time.Second), - "revocation times did not match revocation_time: %s, "+"rfc3339 time: %s", revTime, rfc3339RevocationTime) - require.Equal(t, issuerId, resp.Data["issuer_id"], "issuer_id on leaf cert did not match") - - // Wait for cert to expire and the safety buffer to elapse. - time.Sleep(time.Until(leafCert.NotAfter) + 3*time.Second) - - // Wait for auto-tidy to run afterwards. - var foundTidyRunning string - var foundTidyFinished bool - timeoutChan := time.After(120 * time.Second) - for { - if foundTidyRunning != "" && foundTidyFinished { - break - } - - select { - case <-timeoutChan: - t.Fatalf("expected auto-tidy to run (%v) and finish (%v) before 120 seconds elapsed", foundTidyRunning, foundTidyFinished) - default: - time.Sleep(250 * time.Millisecond) - - resp, err = client.Logical().Read("pki/tidy-status") - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["state"]) - require.NotEmpty(t, resp.Data["time_started"]) - state := resp.Data["state"].(string) - started := resp.Data["time_started"].(string) - t.Logf("Resp: %v", resp.Data) - - // We want the _next_ tidy run after the cert expires. This - // means if we're currently finished when we hit this the - // first time, we want to wait for the next run. - if foundTidyRunning == "" { - foundTidyRunning = started - } else if foundTidyRunning != started && !foundTidyFinished && state == "Finished" { - foundTidyFinished = true - } - } - } - - // Cert should no longer exist. - resp, err = client.Logical().Read("pki/cert/" + leafSerial) - require.Nil(t, err) - require.Nil(t, resp) -} - -func TestTidyCancellation(t *testing.T) { - t.Parallel() - - numLeaves := 100 - - b, s := CreateBackendWithStorage(t) - - // Create a root, a role, and a bunch of leaves. - _, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root example.com", - "issuer_name": "root", - "ttl": "20m", - "key_type": "ec", - }) - require.NoError(t, err) - _, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - }) - require.NoError(t, err) - for i := 0; i < numLeaves; i++ { - _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ - "common_name": "testing", - "ttl": "1s", - }) - require.NoError(t, err) - } - - // Kick off a tidy operation (which runs in the background), but with - // a slow-ish pause between certificates. - resp, err := CBWrite(b, s, "tidy", map[string]interface{}{ - "tidy_cert_store": true, - "safety_buffer": "1s", - "pause_duration": "1s", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("tidy"), logical.UpdateOperation), resp, true) - - // If we wait six seconds, the operation should still be running. That's - // how we check that pause_duration works. - time.Sleep(3 * time.Second) - - resp, err = CBRead(b, s, "tidy-status") - - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.Equal(t, resp.Data["state"], "Running") - - // If we now cancel the operation, the response should say Cancelling. - cancelResp, err := CBWrite(b, s, "tidy-cancel", map[string]interface{}{}) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("tidy-cancel"), logical.UpdateOperation), resp, true) - require.NoError(t, err) - require.NotNil(t, cancelResp) - require.NotNil(t, cancelResp.Data) - state := cancelResp.Data["state"].(string) - howMany := cancelResp.Data["cert_store_deleted_count"].(uint) - - if state == "Cancelled" { - // Rest of the test can't run; log and exit. - t.Log("Went to cancel the operation but response was already cancelled") - return - } - - require.Equal(t, state, "Cancelling") - - // Wait a little longer, and ensure we only processed at most 2 more certs - // after the cancellation respon. - time.Sleep(3 * time.Second) - - statusResp, err := CBRead(b, s, "tidy-status") - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("tidy-status"), logical.ReadOperation), resp, true) - require.NoError(t, err) - require.NotNil(t, statusResp) - require.NotNil(t, statusResp.Data) - require.Equal(t, statusResp.Data["state"], "Cancelled") - nowMany := statusResp.Data["cert_store_deleted_count"].(uint) - if howMany+3 <= nowMany { - t.Fatalf("expected to only process at most 3 more certificates, but processed (%v >>> %v) certs", nowMany, howMany) - } -} - -func TestTidyIssuers(t *testing.T) { - t.Parallel() - - b, s := CreateBackendWithStorage(t) - - // Create a root that expires quickly and one valid for longer. - _, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root1 example.com", - "issuer_name": "root-expired", - "ttl": "1s", - "key_type": "ec", - }) - require.NoError(t, err) - - _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "root2 example.com", - "issuer_name": "root-valid", - "ttl": "60m", - "key_type": "rsa", - }) - require.NoError(t, err) - - // Sleep long enough to expire the root. - time.Sleep(2 * time.Second) - - // First tidy run shouldn't remove anything; too long of safety buffer. - _, err = CBWrite(b, s, "tidy", map[string]interface{}{ - "tidy_expired_issuers": true, - "issuer_safety_buffer": "60m", - }) - require.NoError(t, err) - - // Wait for tidy to finish. - time.Sleep(2 * time.Second) - - // Expired issuer should exist. - resp, err := CBRead(b, s, "issuer/root-expired") - requireSuccessNonNilResponse(t, resp, err, "expired should still be present") - resp, err = CBRead(b, s, "issuer/root-valid") - requireSuccessNonNilResponse(t, resp, err, "valid should still be present") - - // Second tidy run with shorter safety buffer shouldn't remove the - // expired one, as it should be the default issuer. - _, err = CBWrite(b, s, "tidy", map[string]interface{}{ - "tidy_expired_issuers": true, - "issuer_safety_buffer": "1s", - }) - require.NoError(t, err) - - // Wait for tidy to finish. - time.Sleep(2 * time.Second) - - // Expired issuer should still exist. - resp, err = CBRead(b, s, "issuer/root-expired") - requireSuccessNonNilResponse(t, resp, err, "expired should still be present") - resp, err = CBRead(b, s, "issuer/root-valid") - requireSuccessNonNilResponse(t, resp, err, "valid should still be present") - - // Update the default issuer. - _, err = CBWrite(b, s, "config/issuers", map[string]interface{}{ - "default": "root-valid", - }) - require.NoError(t, err) - - // Third tidy run should remove the expired one. - _, err = CBWrite(b, s, "tidy", map[string]interface{}{ - "tidy_expired_issuers": true, - "issuer_safety_buffer": "1s", - }) - require.NoError(t, err) - - // Wait for tidy to finish. - time.Sleep(2 * time.Second) - - // Valid issuer should exist still; other should be removed. - resp, err = CBRead(b, s, "issuer/root-expired") - require.Error(t, err) - require.Nil(t, resp) - resp, err = CBRead(b, s, "issuer/root-valid") - requireSuccessNonNilResponse(t, resp, err, "valid should still be present") - - // Finally, one more tidy should cause no changes. - _, err = CBWrite(b, s, "tidy", map[string]interface{}{ - "tidy_expired_issuers": true, - "issuer_safety_buffer": "1s", - }) - require.NoError(t, err) - - // Wait for tidy to finish. - time.Sleep(2 * time.Second) - - // Valid issuer should exist still; other should be removed. - resp, err = CBRead(b, s, "issuer/root-expired") - require.Error(t, err) - require.Nil(t, resp) - resp, err = CBRead(b, s, "issuer/root-valid") - requireSuccessNonNilResponse(t, resp, err, "valid should still be present") - - // Ensure we have safety buffer and expired issuers set correctly. - statusResp, err := CBRead(b, s, "tidy-status") - require.NoError(t, err) - require.NotNil(t, statusResp) - require.NotNil(t, statusResp.Data) - require.Equal(t, statusResp.Data["issuer_safety_buffer"], 1) - require.Equal(t, statusResp.Data["tidy_expired_issuers"], true) -} - -func TestTidyIssuerConfig(t *testing.T) { - t.Parallel() - - b, s := CreateBackendWithStorage(t) - - // Ensure the default auto-tidy config matches expectations - resp, err := CBRead(b, s, "config/auto-tidy") - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/auto-tidy"), logical.ReadOperation), resp, true) - requireSuccessNonNilResponse(t, resp, err) - - jsonBlob, err := json.Marshal(&defaultTidyConfig) - require.NoError(t, err) - var defaultConfigMap map[string]interface{} - err = json.Unmarshal(jsonBlob, &defaultConfigMap) - require.NoError(t, err) - - // Coerce defaults to API response types. - defaultConfigMap["interval_duration"] = int(time.Duration(defaultConfigMap["interval_duration"].(float64)) / time.Second) - defaultConfigMap["issuer_safety_buffer"] = int(time.Duration(defaultConfigMap["issuer_safety_buffer"].(float64)) / time.Second) - defaultConfigMap["safety_buffer"] = int(time.Duration(defaultConfigMap["safety_buffer"].(float64)) / time.Second) - defaultConfigMap["pause_duration"] = time.Duration(defaultConfigMap["pause_duration"].(float64)).String() - defaultConfigMap["revocation_queue_safety_buffer"] = int(time.Duration(defaultConfigMap["revocation_queue_safety_buffer"].(float64)) / time.Second) - defaultConfigMap["acme_account_safety_buffer"] = int(time.Duration(defaultConfigMap["acme_account_safety_buffer"].(float64)) / time.Second) - - require.Equal(t, defaultConfigMap, resp.Data) - - // Ensure setting issuer-tidy related fields stick. - resp, err = CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ - "tidy_expired_issuers": true, - "issuer_safety_buffer": "5s", - }) - schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/auto-tidy"), logical.UpdateOperation), resp, true) - - requireSuccessNonNilResponse(t, resp, err) - require.Equal(t, true, resp.Data["tidy_expired_issuers"]) - require.Equal(t, 5, resp.Data["issuer_safety_buffer"]) -} - -// TestCertStorageMetrics ensures that when enabled, metrics are able to count the number of certificates in storage and -// number of revoked certificates in storage. Moreover, this test ensures that the gauge is emitted periodically, so -// that the metric does not disappear or go stale. -func TestCertStorageMetrics(t *testing.T) { - // This tests uses the same setup as TestAutoTidy - newPeriod := 1 * time.Second - - // We set up a metrics accumulator - inmemSink := metrics.NewInmemSink( - 2*newPeriod, // A short time period is ideal here to test metrics are emitted every periodic func - 10*newPeriod) // Do not keep a huge amount of metrics in the sink forever, clear them out to save memory usage. - - metricsConf := metrics.DefaultConfig("") - metricsConf.EnableHostname = false - metricsConf.EnableHostnameLabel = false - metricsConf.EnableServiceLabel = false - metricsConf.EnableTypePrefix = false - - _, err := metrics.NewGlobal(metricsConf, inmemSink) - if err != nil { - t.Fatal(err) - } - - // This test requires the periodicFunc to trigger, which requires we stand - // up a full test cluster. - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "pki": Factory, - }, - // See notes below about usage of /sys/raw for reading cluster - // storage without barrier encryption. - EnableRaw: true, - RollbackPeriod: newPeriod, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - NumCores: 1, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Mount PKI - err = client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "10m", - MaxLeaseTTL: "60m", - }, - }) - require.NoError(t, err) - - // Generate root. - resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "ttl": "40h", - "common_name": "Root X1", - "key_type": "ec", - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotEmpty(t, resp.Data) - require.NotEmpty(t, resp.Data["issuer_id"]) - - // Set up a testing role. - _, err = client.Logical().Write("pki/roles/local-testing", map[string]interface{}{ - "allow_any_name": true, - "enforce_hostnames": false, - "key_type": "ec", - }) - require.NoError(t, err) - - // Run tidy so that tidy-status is not empty - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_revoked_certs": true, - }) - require.NoError(t, err) - - // Since certificate counts are off by default, we shouldn't see counts in the tidy status - tidyStatus, err := client.Logical().Read("pki/tidy-status") - if err != nil { - t.Fatal(err) - } - // backendUUID should exist, we need this for metrics - backendUUID := tidyStatus.Data["internal_backend_uuid"].(string) - // "current_cert_store_count", "current_revoked_cert_count" - countData, ok := tidyStatus.Data["current_cert_store_count"] - if ok && countData != nil { - t.Fatalf("Certificate counting should be off by default, but current cert store count %v appeared in tidy status in unconfigured mount", countData) - } - revokedCountData, ok := tidyStatus.Data["current_revoked_cert_count"] - if ok && revokedCountData != nil { - t.Fatalf("Certificate counting should be off by default, but revoked cert count %v appeared in tidy status in unconfigured mount", revokedCountData) - } - - // Since certificate counts are off by default, those metrics should not exist yet - stableMetric := inmemSink.Data() - mostRecentInterval := stableMetric[len(stableMetric)-1] - _, ok = mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_revoked_certificates_stored"] - if ok { - t.Fatalf("Certificate counting should be off by default, but revoked cert count was emitted as a metric in an unconfigured mount") - } - _, ok = mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_certificates_stored"] - if ok { - t.Fatalf("Certificate counting should be off by default, but total certificate count was emitted as a metric in an unconfigured mount") - } - - // Write the auto-tidy config. - _, err = client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ - "enabled": true, - "interval_duration": "1s", - "tidy_cert_store": true, - "tidy_revoked_certs": true, - "safety_buffer": "1s", - "maintain_stored_certificate_counts": true, - "publish_stored_certificate_count_metrics": false, - }) - require.NoError(t, err) - - // Reload the Mount - Otherwise Stored Certificate Counts Will Not Be Populated - // Sealing cores as plugin reload triggers the race detector - VAULT-13635 - testhelpers.EnsureCoresSealed(t, cluster) - testhelpers.EnsureCoresUnsealed(t, cluster) - - // Wait until a tidy run has completed. - testhelpers.RetryUntil(t, 5*time.Second, func() error { - resp, err = client.Logical().Read("pki/tidy-status") - if err != nil { - return fmt.Errorf("error reading tidy status: %w", err) - } - if finished, ok := resp.Data["time_finished"]; !ok || finished == "" || finished == nil { - return fmt.Errorf("tidy time_finished not run yet: %v", finished) - } - return nil - }) - - // Since publish_stored_certificate_count_metrics is still false, these metrics should still not exist yet - stableMetric = inmemSink.Data() - mostRecentInterval = stableMetric[len(stableMetric)-1] - _, ok = mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_revoked_certificates_stored"] - if ok { - t.Fatalf("Certificate counting should be off by default, but revoked cert count was emitted as a metric in an unconfigured mount") - } - _, ok = mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_certificates_stored"] - if ok { - t.Fatalf("Certificate counting should be off by default, but total certificate count was emitted as a metric in an unconfigured mount") - } - - // But since certificate counting is on, the metrics should exist on tidyStatus endpoint: - tidyStatus, err = client.Logical().Read("pki/tidy-status") - require.NoError(t, err, "failed reading tidy-status endpoint") - - // backendUUID should exist, we need this for metrics - backendUUID = tidyStatus.Data["internal_backend_uuid"].(string) - // "current_cert_store_count", "current_revoked_cert_count" - certStoreCount, ok := tidyStatus.Data["current_cert_store_count"] - if !ok { - t.Fatalf("Certificate counting has been turned on, but current cert store count does not appear in tidy status") - } - if certStoreCount != json.Number("1") { - t.Fatalf("Only created one certificate, but a got a certificate count of %v", certStoreCount) - } - revokedCertCount, ok := tidyStatus.Data["current_revoked_cert_count"] - if !ok { - t.Fatalf("Certificate counting has been turned on, but revoked cert store count does not appear in tidy status") - } - if revokedCertCount != json.Number("0") { - t.Fatalf("Have not yet revoked a certificate, but got a revoked cert store count of %v", revokedCertCount) - } - - // Write the auto-tidy config, again, this time turning on metrics - _, err = client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ - "enabled": true, - "interval_duration": "1s", - "tidy_cert_store": true, - "tidy_revoked_certs": true, - "safety_buffer": "1s", - "maintain_stored_certificate_counts": true, - "publish_stored_certificate_count_metrics": true, - }) - require.NoError(t, err, "failed updating auto-tidy configuration") - - // Issue a cert and revoke it. - resp, err = client.Logical().Write("pki/issue/local-testing", map[string]interface{}{ - "common_name": "example.com", - "ttl": "10s", - }) - require.NoError(t, err, "failed to issue leaf certificate") - require.NotNil(t, resp, "nil response without error on issuing leaf certificate") - require.NotNil(t, resp.Data, "empty Data without error on issuing leaf certificate") - require.NotEmpty(t, resp.Data["serial_number"]) - require.NotEmpty(t, resp.Data["certificate"]) - leafSerial := resp.Data["serial_number"].(string) - leafCert := parseCert(t, resp.Data["certificate"].(string)) - - // Read cert before revoking - resp, err = client.Logical().Read("pki/cert/" + leafSerial) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["certificate"]) - revocationTime, err := (resp.Data["revocation_time"].(json.Number)).Int64() - require.Equal(t, int64(0), revocationTime, "revocation time was not zero") - require.Empty(t, resp.Data["revocation_time_rfc3339"], "revocation_time_rfc3339 was not empty") - require.Empty(t, resp.Data["issuer_id"], "issuer_id was not empty") - - revokeResp, err := client.Logical().Write("pki/revoke", map[string]interface{}{ - "serial_number": leafSerial, - }) - require.NoError(t, err, "failed revoking serial number: %s", leafSerial) - - for _, warning := range revokeResp.Warnings { - if strings.Contains(warning, "already expired; refusing to add to CRL") { - t.Skipf("Skipping test as we missed the revocation window of our leaf cert") - } - } - - // We read the auto-tidy endpoint again, to ensure any metrics logic has completed (lock on config) - _, err = client.Logical().Read("/pki/config/auto-tidy") - require.NoError(t, err, "failed to read auto-tidy configuration") - - // Check Metrics After Cert Has Be Created and Revoked - tidyStatus, err = client.Logical().Read("pki/tidy-status") - require.NoError(t, err, "failed to read tidy-status") - - backendUUID = tidyStatus.Data["internal_backend_uuid"].(string) - certStoreCount, ok = tidyStatus.Data["current_cert_store_count"] - if !ok { - t.Fatalf("Certificate counting has been turned on, but current cert store count does not appear in tidy status") - } - if certStoreCount != json.Number("2") { - t.Fatalf("Created root and leaf certificate, but a got a certificate count of %v", certStoreCount) - } - revokedCertCount, ok = tidyStatus.Data["current_revoked_cert_count"] - if !ok { - t.Fatalf("Certificate counting has been turned on, but revoked cert store count does not appear in tidy status") - } - if revokedCertCount != json.Number("1") { - t.Fatalf("Revoked one certificate, but got a revoked cert store count of %v\n:%v", revokedCertCount, tidyStatus) - } - // This should now be initialized - certCountError, ok := tidyStatus.Data["certificate_counting_error"] - if ok && certCountError.(string) != "" { - t.Fatalf("Expected certificate count error to disappear after initialization, but got error %v", certCountError) - } - - testhelpers.RetryUntil(t, newPeriod*5, func() error { - stableMetric = inmemSink.Data() - mostRecentInterval = stableMetric[len(stableMetric)-1] - revokedCertCountGaugeValue, ok := mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_revoked_certificates_stored"] - if !ok { - return errors.New("turned on metrics, but revoked cert count was not emitted") - } - if revokedCertCountGaugeValue.Value != 1 { - return fmt.Errorf("revoked one certificate, but metrics emitted a revoked cert store count of %v", revokedCertCountGaugeValue) - } - certStoreCountGaugeValue, ok := mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_certificates_stored"] - if !ok { - return errors.New("turned on metrics, but total certificate count was not emitted") - } - if certStoreCountGaugeValue.Value != 2 { - return fmt.Errorf("stored two certificiates, but total certificate count emitted was %v", certStoreCountGaugeValue.Value) - } - return nil - }) - - // Wait for cert to expire and the safety buffer to elapse. - sleepFor := time.Until(leafCert.NotAfter) + 3*time.Second - t.Logf("%v: Sleeping for %v, leaf certificate expires: %v", time.Now().Format(time.RFC3339), sleepFor, leafCert.NotAfter) - time.Sleep(sleepFor) - - // Wait for auto-tidy to run afterwards. - var foundTidyRunning string - var foundTidyFinished bool - timeoutChan := time.After(120 * time.Second) - for { - if foundTidyRunning != "" && foundTidyFinished { - break - } - - select { - case <-timeoutChan: - t.Fatalf("expected auto-tidy to run (%v) and finish (%v) before 120 seconds elapsed", foundTidyRunning, foundTidyFinished) - default: - time.Sleep(250 * time.Millisecond) - - resp, err = client.Logical().Read("pki/tidy-status") - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotEmpty(t, resp.Data["state"]) - require.NotEmpty(t, resp.Data["time_started"]) - state := resp.Data["state"].(string) - started := resp.Data["time_started"].(string) - - t.Logf("%v: Resp: %v", time.Now().Format(time.RFC3339), resp.Data) - - // We want the _next_ tidy run after the cert expires. This - // means if we're currently finished when we hit this the - // first time, we want to wait for the next run. - if foundTidyRunning == "" { - foundTidyRunning = started - } else if foundTidyRunning != started && !foundTidyFinished && state == "Finished" { - foundTidyFinished = true - } - } - } - - // After Tidy, Cert Store Count Should Still Be Available, and Be Updated: - // Check Metrics After Cert Has Be Created and Revoked - tidyStatus, err = client.Logical().Read("pki/tidy-status") - if err != nil { - t.Fatal(err) - } - backendUUID = tidyStatus.Data["internal_backend_uuid"].(string) - // "current_cert_store_count", "current_revoked_cert_count" - certStoreCount, ok = tidyStatus.Data["current_cert_store_count"] - if !ok { - t.Fatalf("Certificate counting has been turned on, but current cert store count does not appear in tidy status") - } - if certStoreCount != json.Number("1") { - t.Fatalf("Created root and leaf certificate, deleted leaf, but a got a certificate count of %v", certStoreCount) - } - revokedCertCount, ok = tidyStatus.Data["current_revoked_cert_count"] - if !ok { - t.Fatalf("Certificate counting has been turned on, but revoked cert store count does not appear in tidy status") - } - if revokedCertCount != json.Number("0") { - t.Fatalf("Revoked certificate has been tidied, but got a revoked cert store count of %v", revokedCertCount) - } - - testhelpers.RetryUntil(t, newPeriod*5, func() error { - stableMetric = inmemSink.Data() - mostRecentInterval = stableMetric[len(stableMetric)-1] - revokedCertCountGaugeValue, ok := mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_revoked_certificates_stored"] - if !ok { - return errors.New("turned on metrics, but revoked cert count was not emitted") - } - if revokedCertCountGaugeValue.Value != 0 { - return fmt.Errorf("revoked certificate has been tidied, but metrics emitted a revoked cert store count of %v", revokedCertCountGaugeValue) - } - certStoreCountGaugeValue, ok := mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_certificates_stored"] - if !ok { - return errors.New("turned on metrics, but total certificate count was not emitted") - } - if certStoreCountGaugeValue.Value != 1 { - return fmt.Errorf("only one of two certificates left after tidy, but total certificate count emitted was %v", certStoreCountGaugeValue.Value) - } - return nil - }) -} - -// This test uses the default safety buffer with backdating. -func TestTidyAcmeWithBackdate(t *testing.T) { - t.Parallel() - - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - testCtx := context.Background() - - // Grab the mount UUID for sys/raw invocations. - pkiMount := findStorageMountUuid(t, client, "pki") - - // Register an Account, do nothing with it - baseAcmeURL := "/v1/pki/acme/" - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - // Create new account with order/cert - t.Logf("Testing register on %s", baseAcmeURL) - acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - t.Logf("got account URI: %v", acct.URI) - require.NoError(t, err, "failed registering account") - identifiers := []string{"*.localdomain"} - order, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{ - {Type: "dns", Value: identifiers[0]}, - }) - require.NoError(t, err, "failed creating order") - - // HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow test. - markAuthorizationSuccess(t, client, acmeClient, acct, order) - - goodCr := &x509.CertificateRequest{DNSNames: []string{identifiers[0]}} - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated key for CSR") - csr, err := x509.CreateCertificateRequest(rand.Reader, goodCr, csrKey) - require.NoError(t, err, "failed generating csr") - certs, _, err := acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, true) - require.NoError(t, err, "order finalization failed") - require.GreaterOrEqual(t, len(certs), 1, "expected at least one cert in bundle") - - acmeCert, err := x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert") - - // -> Ensure we see it in storage. Since we don't have direct storage - // access, use sys/raw interface. - acmeThumbprintsPath := path.Join("sys/raw/logical", pkiMount, acmeThumbprintPrefix) - listResp, err := client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) - require.NoError(t, err, "failed listing ACME thumbprints") - require.NotEmpty(t, listResp.Data["keys"], "expected non-empty list response") - - // Run Tidy - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_acme": true, - }) - require.NoError(t, err) - - // Wait for tidy to finish. - waitForTidyToFinish(t, client, "pki") - - // Check that the Account is Still There, Still Valid. - account, err := acmeClient.GetReg(context.Background(), "" /* legacy unused param*/) - require.NoError(t, err, "received account looking up acme account") - require.Equal(t, acme.StatusValid, account.Status) - - // Find the associated thumbprint - listResp, err = client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) - require.NoError(t, err) - require.NotNil(t, listResp) - thumbprintEntries := listResp.Data["keys"].([]interface{}) - require.Equal(t, len(thumbprintEntries), 1) - thumbprint := thumbprintEntries[0].(string) - - // Let "Time Pass"; this is a HACK, this function sys-writes to overwrite the date on objects in storage - duration := time.Until(acmeCert.NotAfter) + 31*24*time.Hour - accountId := acmeClient.KID[strings.LastIndex(string(acmeClient.KID), "/")+1:] - orderId := order.URI[strings.LastIndex(order.URI, "/")+1:] - backDateAcmeOrderSys(t, testCtx, client, string(accountId), orderId, duration, pkiMount) - - // Run Tidy -> clean up order - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_acme": true, - }) - require.NoError(t, err) - - // Wait for tidy to finish. - tidyResp := waitForTidyToFinish(t, client, "pki") - - require.Equal(t, tidyResp.Data["acme_orders_deleted_count"], json.Number("1"), - "expected to revoke a single ACME order: %v", tidyResp) - require.Equal(t, tidyResp.Data["acme_account_revoked_count"], json.Number("0"), - "no ACME account should have been revoked: %v", tidyResp) - require.Equal(t, tidyResp.Data["acme_account_deleted_count"], json.Number("0"), - "no ACME account should have been revoked: %v", tidyResp) - - // Make sure our order is indeed deleted. - _, err = acmeClient.GetOrder(context.Background(), order.URI) - require.ErrorContains(t, err, "order does not exist") - - // Check that the Account is Still There, Still Valid. - account, err = acmeClient.GetReg(context.Background(), "" /* legacy unused param*/) - require.NoError(t, err, "received account looking up acme account") - require.Equal(t, acme.StatusValid, account.Status) - - // Now back date the account to make sure we revoke it - backDateAcmeAccountSys(t, testCtx, client, thumbprint, duration, pkiMount) - - // Run Tidy -> mark account revoked - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_acme": true, - }) - require.NoError(t, err) - - // Wait for tidy to finish. - tidyResp = waitForTidyToFinish(t, client, "pki") - require.Equal(t, tidyResp.Data["acme_orders_deleted_count"], json.Number("0"), - "no ACME orders should have been deleted: %v", tidyResp) - require.Equal(t, tidyResp.Data["acme_account_revoked_count"], json.Number("1"), - "expected to revoke a single ACME account: %v", tidyResp) - require.Equal(t, tidyResp.Data["acme_account_deleted_count"], json.Number("0"), - "no ACME account should have been revoked: %v", tidyResp) - - // Lookup our account to make sure we get the appropriate revoked status - account, err = acmeClient.GetReg(context.Background(), "" /* legacy unused param*/) - require.NoError(t, err, "received account looking up acme account") - require.Equal(t, acme.StatusRevoked, account.Status) - - // Let "Time Pass"; this is a HACK, this function sys-writes to overwrite the date on objects in storage - backDateAcmeAccountSys(t, testCtx, client, thumbprint, duration, pkiMount) - - // Run Tidy -> remove account - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_acme": true, - }) - require.NoError(t, err) - - // Wait for tidy to finish. - waitForTidyToFinish(t, client, "pki") - - // Check Account No Longer Appears - listResp, err = client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) - require.NoError(t, err) - if listResp != nil { - thumbprintEntries = listResp.Data["keys"].([]interface{}) - require.Equal(t, 0, len(thumbprintEntries)) - } - - // Nor Under Account - _, acctKID := path.Split(acct.URI) - acctPath := path.Join("sys/raw/logical", pkiMount, acmeAccountPrefix, acctKID) - t.Logf("account path: %v", acctPath) - getResp, err := client.Logical().ReadWithContext(testCtx, acctPath) - require.NoError(t, err) - require.Nil(t, getResp) -} - -// This test uses a smaller safety buffer. -func TestTidyAcmeWithSafetyBuffer(t *testing.T) { - t.Parallel() - - // This would still be way easier if I could do both sides - cluster, client, _ := setupAcmeBackend(t) - defer cluster.Cleanup() - testCtx := context.Background() - - // Grab the mount UUID for sys/raw invocations. - pkiMount := findStorageMountUuid(t, client, "pki") - - // Register an Account, do nothing with it - baseAcmeURL := "/v1/pki/acme/" - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa key") - - acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) - - // Create new account - t.Logf("Testing register on %s", baseAcmeURL) - acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) - t.Logf("got account URI: %v", acct.URI) - require.NoError(t, err, "failed registering account") - - // -> Ensure we see it in storage. Since we don't have direct storage - // access, use sys/raw interface. - acmeThumbprintsPath := path.Join("sys/raw/logical", pkiMount, acmeThumbprintPrefix) - listResp, err := client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) - require.NoError(t, err, "failed listing ACME thumbprints") - require.NotEmpty(t, listResp.Data["keys"], "expected non-empty list response") - thumbprintEntries := listResp.Data["keys"].([]interface{}) - require.Equal(t, len(thumbprintEntries), 1) - - // Wait for the account to expire. - time.Sleep(2 * time.Second) - - // Run Tidy -> mark account revoked - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_acme": true, - "acme_account_safety_buffer": "1s", - }) - require.NoError(t, err) - - // Wait for tidy to finish. - statusResp := waitForTidyToFinish(t, client, "pki") - require.Equal(t, statusResp.Data["acme_account_revoked_count"], json.Number("1"), "expected to revoke a single ACME account") - - // Wait for the account to expire. - time.Sleep(2 * time.Second) - - // Run Tidy -> remove account - _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_acme": true, - "acme_account_safety_buffer": "1s", - }) - require.NoError(t, err) - - // Wait for tidy to finish. - waitForTidyToFinish(t, client, "pki") - - // Check Account No Longer Appears - listResp, err = client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) - require.NoError(t, err) - if listResp != nil { - thumbprintEntries = listResp.Data["keys"].([]interface{}) - require.Equal(t, 0, len(thumbprintEntries)) - } - - // Nor Under Account - _, acctKID := path.Split(acct.URI) - acctPath := path.Join("sys/raw/logical", pkiMount, acmeAccountPrefix, acctKID) - t.Logf("account path: %v", acctPath) - getResp, err := client.Logical().ReadWithContext(testCtx, acctPath) - require.NoError(t, err) - require.Nil(t, getResp) -} - -// The sys tests refer to all of the tests using sys/raw/logical which work off of a client -func backDateAcmeAccountSys(t *testing.T, testContext context.Context, client *api.Client, thumbprintString string, backdateAmount time.Duration, mount string) { - rawThumbprintPath := path.Join("sys/raw/logical/", mount, acmeThumbprintPrefix+thumbprintString) - thumbprintResp, err := client.Logical().ReadWithContext(testContext, rawThumbprintPath) - if err != nil { - t.Fatalf("unable to fetch thumbprint response at %v: %v", rawThumbprintPath, err) - } - - var thumbprint acmeThumbprint - err = jsonutil.DecodeJSON([]byte(thumbprintResp.Data["value"].(string)), &thumbprint) - if err != nil { - t.Fatalf("unable to decode thumbprint response %v to find account entry: %v", thumbprintResp.Data, err) - } - - accountPath := path.Join("sys/raw/logical", mount, acmeAccountPrefix+thumbprint.Kid) - accountResp, err := client.Logical().ReadWithContext(testContext, accountPath) - if err != nil { - t.Fatalf("unable to fetch account entry %v: %v", thumbprint.Kid, err) - } - - var account acmeAccount - err = jsonutil.DecodeJSON([]byte(accountResp.Data["value"].(string)), &account) - if err != nil { - t.Fatalf("unable to decode acme account %v: %v", accountResp, err) - } - - t.Logf("got account before update: %v", account) - - account.AccountCreatedDate = backDate(account.AccountCreatedDate, backdateAmount) - account.MaxCertExpiry = backDate(account.MaxCertExpiry, backdateAmount) - account.AccountRevokedDate = backDate(account.AccountRevokedDate, backdateAmount) - - t.Logf("got account after update: %v", account) - - encodeJSON, err := jsonutil.EncodeJSON(account) - _, err = client.Logical().WriteWithContext(context.Background(), accountPath, map[string]interface{}{ - "value": base64.StdEncoding.EncodeToString(encodeJSON), - "encoding": "base64", - }) - if err != nil { - t.Fatalf("error saving backdated account entry at %v: %v", accountPath, err) - } - - ordersPath := path.Join("sys/raw/logical", mount, acmeAccountPrefix, thumbprint.Kid, "/orders/") - ordersRaw, err := client.Logical().ListWithContext(context.Background(), ordersPath) - require.NoError(t, err, "failed listing orders") - - if ordersRaw == nil { - t.Logf("skipping backdating orders as there are none") - return - } - - require.NotNil(t, ordersRaw, "got no response data") - require.NotNil(t, ordersRaw.Data, "got no response data") - - orders := ordersRaw.Data - - for _, orderId := range orders["keys"].([]interface{}) { - backDateAcmeOrderSys(t, testContext, client, thumbprint.Kid, orderId.(string), backdateAmount, mount) - } - - // No need to change certificates entries here - no time is stored on AcmeCertEntry -} - -func backDateAcmeOrderSys(t *testing.T, testContext context.Context, client *api.Client, accountKid string, orderId string, backdateAmount time.Duration, mount string) { - rawOrderPath := path.Join("sys/raw/logical/", mount, acmeAccountPrefix, accountKid, "orders", orderId) - orderResp, err := client.Logical().ReadWithContext(testContext, rawOrderPath) - if err != nil { - t.Fatalf("unable to fetch order entry %v on account %v at %v", orderId, accountKid, rawOrderPath) - } - - var order *acmeOrder - err = jsonutil.DecodeJSON([]byte(orderResp.Data["value"].(string)), &order) - if err != nil { - t.Fatalf("error decoding order entry %v on account %v, %v produced: %v", orderId, accountKid, orderResp, err) - } - - order.Expires = backDate(order.Expires, backdateAmount) - order.CertificateExpiry = backDate(order.CertificateExpiry, backdateAmount) - - encodeJSON, err := jsonutil.EncodeJSON(order) - _, err = client.Logical().WriteWithContext(context.Background(), rawOrderPath, map[string]interface{}{ - "value": base64.StdEncoding.EncodeToString(encodeJSON), - "encoding": "base64", - }) - if err != nil { - t.Fatalf("error saving backdated order entry %v on account %v : %v", orderId, accountKid, err) - } - - for _, authId := range order.AuthorizationIds { - backDateAcmeAuthorizationSys(t, testContext, client, accountKid, authId, backdateAmount, mount) - } -} - -func backDateAcmeAuthorizationSys(t *testing.T, testContext context.Context, client *api.Client, accountKid string, authId string, backdateAmount time.Duration, mount string) { - rawAuthPath := path.Join("sys/raw/logical/", mount, acmeAccountPrefix, accountKid, "/authorizations/", authId) - - authResp, err := client.Logical().ReadWithContext(testContext, rawAuthPath) - if err != nil { - t.Fatalf("unable to fetch authorization %v : %v", rawAuthPath, err) - } - - var auth *ACMEAuthorization - err = jsonutil.DecodeJSON([]byte(authResp.Data["value"].(string)), &auth) - if err != nil { - t.Fatalf("error decoding auth %v, auth entry %v produced %v", rawAuthPath, authResp, err) - } - - expiry, err := auth.GetExpires() - if err != nil { - t.Fatalf("could not get expiry on %v: %v", rawAuthPath, err) - } - newExpiry := backDate(expiry, backdateAmount) - auth.Expires = time.Time.Format(newExpiry, time.RFC3339) - - encodeJSON, err := jsonutil.EncodeJSON(auth) - _, err = client.Logical().WriteWithContext(context.Background(), rawAuthPath, map[string]interface{}{ - "value": base64.StdEncoding.EncodeToString(encodeJSON), - "encoding": "base64", - }) - if err != nil { - t.Fatalf("error updating authorization date on %v: %v", rawAuthPath, err) - } -} - -func backDate(original time.Time, change time.Duration) time.Time { - if original.IsZero() { - return original - } - - zeroTime := time.Time{} - - if original.Before(zeroTime.Add(change)) { - return zeroTime - } - - return original.Add(-change) -} - -func waitForTidyToFinish(t *testing.T, client *api.Client, mount string) *api.Secret { - var statusResp *api.Secret - testhelpers.RetryUntil(t, 5*time.Second, func() error { - var err error - - tidyStatusPath := mount + "/tidy-status" - statusResp, err = client.Logical().Read(tidyStatusPath) - if err != nil { - return fmt.Errorf("failed reading path: %s: %w", tidyStatusPath, err) - } - if state, ok := statusResp.Data["state"]; !ok || state == "Running" { - return fmt.Errorf("tidy status state is still running") - } - - if errorOccurred, ok := statusResp.Data["error"]; !ok || !(errorOccurred == nil || errorOccurred == "") { - return fmt.Errorf("tidy status returned an error: %s", errorOccurred) - } - - return nil - }) - - t.Logf("got tidy status: %v", statusResp.Data) - return statusResp -} diff --git a/builtin/logical/pki/storage_migrations_test.go b/builtin/logical/pki/storage_migrations_test.go deleted file mode 100644 index e490bd75d..000000000 --- a/builtin/logical/pki/storage_migrations_test.go +++ /dev/null @@ -1,982 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" -) - -func Test_migrateStorageEmptyStorage(t *testing.T) { - t.Parallel() - startTime := time.Now() - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Reset the version the helper above set to 1. - b.pkiStorageVersion.Store(0) - require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.") - - request := &logical.InitializationRequest{Storage: s} - err := b.initialize(ctx, request) - require.NoError(t, err) - - issuerIds, err := sc.listIssuers() - require.NoError(t, err) - require.Empty(t, issuerIds) - - keyIds, err := sc.listKeys() - require.NoError(t, err) - require.Empty(t, keyIds) - - logEntry, err := getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - require.NotNil(t, logEntry) - require.Equal(t, latestMigrationVersion, logEntry.MigrationVersion) - require.True(t, len(strings.TrimSpace(logEntry.Hash)) > 0, - "Hash value (%s) should not have been empty", logEntry.Hash) - require.True(t, startTime.Before(logEntry.Created), - "created log entry time (%v) was before our start time(%v)?", logEntry.Created, startTime) - require.Empty(t, logEntry.CreatedIssuer) - require.Empty(t, logEntry.CreatedKey) - - require.False(t, b.useLegacyBundleCaStorage(), "post migration we are still told to use legacy storage") - - // Make sure we can re-run the migration without issues - request = &logical.InitializationRequest{Storage: s} - err = b.initialize(ctx, request) - require.NoError(t, err) - logEntry2, err := getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - require.NotNil(t, logEntry2) - - // Make sure the hash and created times have not changed. - require.Equal(t, logEntry.Created, logEntry2.Created) - require.Equal(t, logEntry.Hash, logEntry2.Hash) -} - -func Test_migrateStorageOnlyKey(t *testing.T) { - t.Parallel() - startTime := time.Now() - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Reset the version the helper above set to 1. - b.pkiStorageVersion.Store(0) - require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.") - - bundle := genCertBundle(t, b, s) - // Clear everything except for the key - bundle.SerialNumber = "" - bundle.CAChain = []string{} - bundle.Certificate = "" - bundle.IssuingCA = "" - - json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle) - require.NoError(t, err) - err = s.Put(ctx, json) - require.NoError(t, err) - - request := &logical.InitializationRequest{Storage: s} - err = b.initialize(ctx, request) - require.NoError(t, err) - - issuerIds, err := sc.listIssuers() - require.NoError(t, err) - require.Equal(t, 0, len(issuerIds)) - - keyIds, err := sc.listKeys() - require.NoError(t, err) - require.Equal(t, 1, len(keyIds)) - - logEntry, err := getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - require.NotNil(t, logEntry) - require.Equal(t, latestMigrationVersion, logEntry.MigrationVersion) - require.True(t, len(strings.TrimSpace(logEntry.Hash)) > 0, - "Hash value (%s) should not have been empty", logEntry.Hash) - require.True(t, startTime.Before(logEntry.Created), - "created log entry time (%v) was before our start time(%v)?", logEntry.Created, startTime) - require.Equal(t, logEntry.CreatedIssuer, issuerID("")) - require.Equal(t, logEntry.CreatedKey, keyIds[0]) - - keyId := keyIds[0] - key, err := sc.fetchKeyById(keyId) - require.NoError(t, err) - require.True(t, strings.HasPrefix(key.Name, "current-"), - "expected key name to start with current- was %s", key.Name) - require.Equal(t, keyId, key.ID) - require.Equal(t, strings.TrimSpace(bundle.PrivateKey), strings.TrimSpace(key.PrivateKey)) - require.Equal(t, bundle.PrivateKeyType, key.PrivateKeyType) - - // Make sure we kept the old bundle - _, certBundle, err := getLegacyCertBundle(ctx, s) - require.NoError(t, err) - require.Equal(t, bundle, certBundle) - - // Make sure we setup the default values - keysConfig, err := sc.getKeysConfig() - require.NoError(t, err) - require.Equal(t, &keyConfigEntry{DefaultKeyId: keyId}, keysConfig) - - issuersConfig, err := sc.getIssuersConfig() - require.NoError(t, err) - require.Equal(t, issuerID(""), issuersConfig.DefaultIssuerId) - - // Make sure if we attempt to re-run the migration nothing happens... - err = migrateStorage(ctx, b, s) - require.NoError(t, err) - logEntry2, err := getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - require.NotNil(t, logEntry2) - - require.Equal(t, logEntry.Created, logEntry2.Created) - require.Equal(t, logEntry.Hash, logEntry2.Hash) - - require.False(t, b.useLegacyBundleCaStorage(), "post migration we are still told to use legacy storage") -} - -func Test_migrateStorageSimpleBundle(t *testing.T) { - t.Parallel() - startTime := time.Now() - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Reset the version the helper above set to 1. - b.pkiStorageVersion.Store(0) - require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.") - - bundle := genCertBundle(t, b, s) - json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle) - require.NoError(t, err) - err = s.Put(ctx, json) - require.NoError(t, err) - - request := &logical.InitializationRequest{Storage: s} - err = b.initialize(ctx, request) - require.NoError(t, err) - - issuerIds, err := sc.listIssuers() - require.NoError(t, err) - require.Equal(t, 1, len(issuerIds)) - - keyIds, err := sc.listKeys() - require.NoError(t, err) - require.Equal(t, 1, len(keyIds)) - - logEntry, err := getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - require.NotNil(t, logEntry) - require.Equal(t, latestMigrationVersion, logEntry.MigrationVersion) - require.True(t, len(strings.TrimSpace(logEntry.Hash)) > 0, - "Hash value (%s) should not have been empty", logEntry.Hash) - require.True(t, startTime.Before(logEntry.Created), - "created log entry time (%v) was before our start time(%v)?", logEntry.Created, startTime) - require.Equal(t, logEntry.CreatedIssuer, issuerIds[0]) - require.Equal(t, logEntry.CreatedKey, keyIds[0]) - - issuerId := issuerIds[0] - keyId := keyIds[0] - issuer, err := sc.fetchIssuerById(issuerId) - require.NoError(t, err) - require.True(t, strings.HasPrefix(issuer.Name, "current-"), - "expected issuer name to start with current- was %s", issuer.Name) - require.Equal(t, certutil.ErrNotAfterBehavior, issuer.LeafNotAfterBehavior) - - key, err := sc.fetchKeyById(keyId) - require.NoError(t, err) - require.True(t, strings.HasPrefix(key.Name, "current-"), - "expected key name to start with current- was %s", key.Name) - - require.Equal(t, issuerId, issuer.ID) - require.Equal(t, bundle.SerialNumber, issuer.SerialNumber) - require.Equal(t, strings.TrimSpace(bundle.Certificate), strings.TrimSpace(issuer.Certificate)) - require.Equal(t, keyId, issuer.KeyID) - require.Empty(t, issuer.ManualChain) - require.Equal(t, []string{bundle.Certificate + "\n"}, issuer.CAChain) - require.Equal(t, AllIssuerUsages, issuer.Usage) - require.Equal(t, certutil.ErrNotAfterBehavior, issuer.LeafNotAfterBehavior) - - require.Equal(t, keyId, key.ID) - require.Equal(t, strings.TrimSpace(bundle.PrivateKey), strings.TrimSpace(key.PrivateKey)) - require.Equal(t, bundle.PrivateKeyType, key.PrivateKeyType) - - // Make sure we kept the old bundle - _, certBundle, err := getLegacyCertBundle(ctx, s) - require.NoError(t, err) - require.Equal(t, bundle, certBundle) - - // Make sure we setup the default values - keysConfig, err := sc.getKeysConfig() - require.NoError(t, err) - require.Equal(t, &keyConfigEntry{DefaultKeyId: keyId}, keysConfig) - - issuersConfig, err := sc.getIssuersConfig() - require.NoError(t, err) - require.Equal(t, issuerId, issuersConfig.DefaultIssuerId) - - // Make sure if we attempt to re-run the migration nothing happens... - err = migrateStorage(ctx, b, s) - require.NoError(t, err) - logEntry2, err := getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - require.NotNil(t, logEntry2) - - require.Equal(t, logEntry.Created, logEntry2.Created) - require.Equal(t, logEntry.Hash, logEntry2.Hash) - - require.False(t, b.useLegacyBundleCaStorage(), "post migration we are still told to use legacy storage") - - // Make sure we can re-process a migration from scratch for whatever reason - err = s.Delete(ctx, legacyMigrationBundleLogKey) - require.NoError(t, err) - - err = migrateStorage(ctx, b, s) - require.NoError(t, err) - - logEntry3, err := getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - require.NotNil(t, logEntry3) - - require.NotEqual(t, logEntry.Created, logEntry3.Created) - require.Equal(t, logEntry.Hash, logEntry3.Hash) -} - -func TestMigration_OnceChainRebuild(t *testing.T) { - t.Parallel() - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Create a legacy CA bundle that we'll migrate to the new layout. We call - // ToParsedCertBundle just to make sure it works and to populate - // bundle.SerialNumber for us. - bundle := &certutil.CertBundle{ - PrivateKeyType: certutil.RSAPrivateKey, - Certificate: migIntCA, - IssuingCA: migRootCA, - CAChain: []string{migRootCA}, - PrivateKey: migIntPrivKey, - } - _, err := bundle.ToParsedCertBundle() - require.NoError(t, err) - writeLegacyBundle(t, b, s, bundle) - - // Do an initial migration. Ensure we end up at least on version 2. - request := &logical.InitializationRequest{Storage: s} - err = b.initialize(ctx, request) - require.NoError(t, err) - - issuerIds, err := sc.listIssuers() - require.NoError(t, err) - require.Equal(t, 2, len(issuerIds)) - - keyIds, err := sc.listKeys() - require.NoError(t, err) - require.Equal(t, 1, len(keyIds)) - - logEntry, err := getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - require.NotNil(t, logEntry) - require.GreaterOrEqual(t, logEntry.MigrationVersion, 2) - require.GreaterOrEqual(t, latestMigrationVersion, 2) - - // Verify the chain built correctly: current should have a CA chain of - // length two. - // - // Afterwards, we mutate these issuers to only point at themselves and - // write back out. - var rootIssuerId issuerID - var intIssuerId issuerID - for _, issuerId := range issuerIds { - issuer, err := sc.fetchIssuerById(issuerId) - require.NoError(t, err) - require.NotNil(t, issuer) - - if strings.HasPrefix(issuer.Name, "current-") { - require.Equal(t, 2, len(issuer.CAChain)) - require.Equal(t, migIntCA, issuer.CAChain[0]) - require.Equal(t, migRootCA, issuer.CAChain[1]) - intIssuerId = issuerId - - issuer.CAChain = []string{migIntCA} - err = sc.writeIssuer(issuer) - require.NoError(t, err) - } else { - require.Equal(t, 1, len(issuer.CAChain)) - require.Equal(t, migRootCA, issuer.CAChain[0]) - rootIssuerId = issuerId - } - } - - // Reset our migration version back to one, as if this never - // happened... - logEntry.MigrationVersion = 1 - err = setLegacyBundleMigrationLog(ctx, s, logEntry) - require.NoError(t, err) - b.pkiStorageVersion.Store(1) - - // Re-attempt the migration by reinitializing the mount. - err = b.initialize(ctx, request) - require.NoError(t, err) - - newIssuerIds, err := sc.listIssuers() - require.NoError(t, err) - require.Equal(t, 2, len(newIssuerIds)) - require.Equal(t, issuerIds, newIssuerIds) - - newKeyIds, err := sc.listKeys() - require.NoError(t, err) - require.Equal(t, 1, len(newKeyIds)) - require.Equal(t, keyIds, newKeyIds) - - logEntry, err = getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - require.NotNil(t, logEntry) - require.Equal(t, logEntry.MigrationVersion, latestMigrationVersion) - - // Ensure the chains are correct on the intermediate. By using the - // issuerId saved above, this ensures we didn't change any issuerIds, - // we merely updated the existing issuers. - intIssuer, err := sc.fetchIssuerById(intIssuerId) - require.NoError(t, err) - require.NotNil(t, intIssuer) - require.Equal(t, 2, len(intIssuer.CAChain)) - require.Equal(t, migIntCA, intIssuer.CAChain[0]) - require.Equal(t, migRootCA, intIssuer.CAChain[1]) - - rootIssuer, err := sc.fetchIssuerById(rootIssuerId) - require.NoError(t, err) - require.NotNil(t, rootIssuer) - require.Equal(t, 1, len(rootIssuer.CAChain)) - require.Equal(t, migRootCA, rootIssuer.CAChain[0]) -} - -func TestExpectedOpsWork_PreMigration(t *testing.T) { - t.Parallel() - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - // Reset the version the helper above set to 1. - b.pkiStorageVersion.Store(0) - require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.") - - bundle := genCertBundle(t, b, s) - json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle) - require.NoError(t, err) - err = s.Put(ctx, json) - require.NoError(t, err) - - // generate role - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/allow-all", - Storage: s, - Data: map[string]interface{}{ - "allow_any_name": "true", - "no_store": "false", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "error from creating role") - require.NotNil(t, resp, "got nil response object from creating role") - - // List roles - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ListOperation, - Path: "roles", - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "error from listing roles") - require.NotNil(t, resp, "got nil response object from listing roles") - require.False(t, resp.IsError(), "got error response from listing roles: %#v", resp) - require.Contains(t, resp.Data["keys"], "allow-all", "failed to list our roles") - - // Read roles - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "roles/allow-all", - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "error from reading role") - require.NotNil(t, resp, "got nil response object from reading role") - require.False(t, resp.IsError(), "got error response from reading role: %#v", resp) - require.NotEmpty(t, resp.Data, "data map should not have been empty of reading role") - - // Issue a cert from our legacy bundle. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issue/allow-all", - Storage: s, - Data: map[string]interface{}{ - "common_name": "test.com", - "ttl": "60s", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "error issue on allow-all") - require.NotNil(t, resp, "got nil response object from issue allow-all") - require.False(t, resp.IsError(), "got error response from issue on allow-all: %#v", resp) - serialNum := resp.Data["serial_number"].(string) - require.NotEmpty(t, serialNum) - - // Make sure we can list - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ListOperation, - Path: "certs", - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "error listing certs") - require.NotNil(t, resp, "got nil response object from listing certs") - require.False(t, resp.IsError(), "got error response from listing certs: %#v", resp) - require.Contains(t, resp.Data["keys"], serialNum, "failed to list our cert") - - // Revoke the cert now. - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "revoke", - Storage: s, - Data: map[string]interface{}{ - "serial_number": serialNum, - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "error revoking cert") - require.NotNil(t, resp, "got nil response object from revoke cert") - require.False(t, resp.IsError(), "got error response from revoke cert: %#v", resp) - - // Check our CRL includes the revoked cert. - resp = requestCrlFromBackend(t, s, b) - crl := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte)) - requireSerialNumberInCRL(t, crl, serialNum) - - // Set CRL config - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/crl", - Storage: s, - Data: map[string]interface{}{ - "expiry": "72h", - "disable": "false", - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "error setting CRL config") - require.NotNil(t, resp, "got nil response setting CRL config") - - // Set URL config - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/urls", - Storage: s, - Data: map[string]interface{}{ - "ocsp_servers": []string{"https://localhost:8080"}, - }, - MountPoint: "pki/", - }) - requireSuccessNonNilResponse(t, resp, err) - - // Make sure we can fetch the old values... - for _, path := range []string{"ca/pem", "ca_chain", "cert/" + serialNum, "cert/ca", "cert/crl", "cert/ca_chain", "config/crl", "config/urls"} { - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: path, - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "error reading cert %s", path) - require.NotNil(t, resp, "got nil response object from reading cert %s", path) - require.False(t, resp.IsError(), "got error response from reading cert %s: %#v", path, resp) - } - - // Sign CSR - _, csr := generateTestCsr(t, certutil.ECPrivateKey, 224) - for _, path := range []string{"sign/allow-all", "root/sign-intermediate", "sign-verbatim"} { - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: path, - Storage: s, - Data: map[string]interface{}{ - "csr": csr, - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "error signing csr from path %s", path) - require.NotNil(t, resp, "got nil response object from path %s", path) - require.NotEmpty(t, resp.Data, "data map response was empty from path %s", path) - } - - // Sign self-issued - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "root/sign-self-issued", - Storage: s, - Data: map[string]interface{}{ - "certificate": csr, - }, - MountPoint: "pki/", - }) - require.NoError(t, err, "error signing csr from path root/sign-self-issued") - require.NotNil(t, resp, "got nil response object from path root/sign-self-issued") - require.NotEmpty(t, resp.Data, "data map response was empty from path root/sign-self-issued") - - // Delete Role - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.DeleteOperation, - Path: "roles/allow-all", - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "error deleting role") - require.Nil(t, resp, "got non-nil response object from deleting role") - - // Delete Root - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.DeleteOperation, - Path: "root", - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "error deleting root") - require.NotNil(t, resp, "got nil response object from deleting root") - require.NotEmpty(t, resp.Warnings, "expected warnings set on delete root") - - /////////////////////////////// - // Legacy calls we expect to fail when in migration mode - /////////////////////////////// - requireFailInMigration(t, b, s, logical.UpdateOperation, "config/ca") - requireFailInMigration(t, b, s, logical.UpdateOperation, "intermediate/generate/internal") - requireFailInMigration(t, b, s, logical.UpdateOperation, "intermediate/set-signed") - requireFailInMigration(t, b, s, logical.UpdateOperation, "root/generate/internal") - - /////////////////////////////// - // New apis should be unavailable - /////////////////////////////// - requireFailInMigration(t, b, s, logical.ListOperation, "issuers") - requireFailInMigration(t, b, s, logical.UpdateOperation, "issuers/generate/root/internal") - requireFailInMigration(t, b, s, logical.UpdateOperation, "issuers/generate/intermediate/internal") - requireFailInMigration(t, b, s, logical.UpdateOperation, "issuers/import/cert") - requireFailInMigration(t, b, s, logical.ReadOperation, "issuer/default/json") - requireFailInMigration(t, b, s, logical.ReadOperation, "issuer/default/crl/pem") - requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/test-role") - - // The following calls work as they are shared handlers with existing paths. - // requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/issue/test-role") - // requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/sign/test-role") - // requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/sign-verbatim") - // requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/sign-self-issued") - - requireFailInMigration(t, b, s, logical.UpdateOperation, "root/replace") - requireFailInMigration(t, b, s, logical.UpdateOperation, "root/rotate/internal") - requireFailInMigration(t, b, s, logical.UpdateOperation, "intermediate/cross-sign") - - requireFailInMigration(t, b, s, logical.UpdateOperation, "config/issuers") - requireFailInMigration(t, b, s, logical.ReadOperation, "config/issuers") - - requireFailInMigration(t, b, s, logical.ListOperation, "keys") - requireFailInMigration(t, b, s, logical.UpdateOperation, "keys/generate/internal") - requireFailInMigration(t, b, s, logical.UpdateOperation, "keys/import") - requireFailInMigration(t, b, s, logical.ReadOperation, "key/default") - requireFailInMigration(t, b, s, logical.UpdateOperation, "config/keys") - requireFailInMigration(t, b, s, logical.ReadOperation, "config/keys") -} - -func TestBackupBundle(t *testing.T) { - t.Parallel() - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Reset the version the helper above set to 1. - b.pkiStorageVersion.Store(0) - require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.") - - // Create an empty request and tidy configuration for us. - req := &logical.Request{ - Storage: s, - MountPoint: "pki/", - } - cfg := &tidyConfig{ - BackupBundle: true, - IssuerSafetyBuffer: 120 * time.Second, - } - - // Migration should do nothing if we're on an empty mount. - err := b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg) - require.NoError(t, err) - requireFileNotExists(t, sc, legacyCertBundlePath) - requireFileNotExists(t, sc, legacyCertBundleBackupPath) - issuerIds, err := sc.listIssuers() - require.NoError(t, err) - require.Empty(t, issuerIds) - keyIds, err := sc.listKeys() - require.NoError(t, err) - require.Empty(t, keyIds) - - // Create a legacy CA bundle and write it out. - bundle := genCertBundle(t, b, s) - json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle) - require.NoError(t, err) - err = s.Put(ctx, json) - require.NoError(t, err) - legacyContents := requireFileExists(t, sc, legacyCertBundlePath, nil) - - // Doing another tidy should maintain the status quo since we've - // still not done our migration. - err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg) - require.NoError(t, err) - requireFileExists(t, sc, legacyCertBundlePath, legacyContents) - requireFileNotExists(t, sc, legacyCertBundleBackupPath) - issuerIds, err = sc.listIssuers() - require.NoError(t, err) - require.Empty(t, issuerIds) - keyIds, err = sc.listKeys() - require.NoError(t, err) - require.Empty(t, keyIds) - - // Do a migration; this should provision an issuer and key. - initReq := &logical.InitializationRequest{Storage: s} - err = b.initialize(ctx, initReq) - require.NoError(t, err) - requireFileExists(t, sc, legacyCertBundlePath, legacyContents) - issuerIds, err = sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, issuerIds) - keyIds, err = sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, keyIds) - - // Doing another tidy should maintain the status quo since we've - // done our migration too recently relative to the safety buffer. - err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg) - require.NoError(t, err) - requireFileExists(t, sc, legacyCertBundlePath, legacyContents) - requireFileNotExists(t, sc, legacyCertBundleBackupPath) - issuerIds, err = sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, issuerIds) - keyIds, err = sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, keyIds) - - // Shortening our buffer should ensure the migration occurs, removing - // the legacy bundle but creating the backup one. - time.Sleep(2 * time.Second) - cfg.IssuerSafetyBuffer = 1 * time.Second - err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg) - require.NoError(t, err) - requireFileNotExists(t, sc, legacyCertBundlePath) - requireFileExists(t, sc, legacyCertBundleBackupPath, legacyContents) - issuerIds, err = sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, issuerIds) - keyIds, err = sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, keyIds) - - // A new initialization should do nothing. - err = b.initialize(ctx, initReq) - require.NoError(t, err) - requireFileNotExists(t, sc, legacyCertBundlePath) - requireFileExists(t, sc, legacyCertBundleBackupPath, legacyContents) - issuerIds, err = sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, issuerIds) - require.Equal(t, len(issuerIds), 1) - keyIds, err = sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, keyIds) - require.Equal(t, len(keyIds), 1) - - // Restoring the legacy bundles with new issuers should redo the - // migration. - newBundle := genCertBundle(t, b, s) - json, err = logical.StorageEntryJSON(legacyCertBundlePath, newBundle) - require.NoError(t, err) - err = s.Put(ctx, json) - require.NoError(t, err) - newLegacyContents := requireFileExists(t, sc, legacyCertBundlePath, nil) - - // -> reinit - err = b.initialize(ctx, initReq) - require.NoError(t, err) - requireFileExists(t, sc, legacyCertBundlePath, newLegacyContents) - requireFileExists(t, sc, legacyCertBundleBackupPath, legacyContents) - issuerIds, err = sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, issuerIds) - require.Equal(t, len(issuerIds), 2) - keyIds, err = sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, keyIds) - require.Equal(t, len(keyIds), 2) - - // -> when we tidy again, we'll overwrite the old backup with the new - // one. - time.Sleep(2 * time.Second) - err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg) - require.NoError(t, err) - requireFileNotExists(t, sc, legacyCertBundlePath) - requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents) - issuerIds, err = sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, issuerIds) - keyIds, err = sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, keyIds) - - // Finally, restoring the legacy bundle and re-migrating should redo - // the migration. - err = s.Put(ctx, json) - require.NoError(t, err) - requireFileExists(t, sc, legacyCertBundlePath, newLegacyContents) - requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents) - - // -> overwrite the version and re-migrate - logEntry, err := getLegacyBundleMigrationLog(ctx, s) - require.NoError(t, err) - logEntry.MigrationVersion = 0 - err = setLegacyBundleMigrationLog(ctx, s, logEntry) - require.NoError(t, err) - err = b.initialize(ctx, initReq) - require.NoError(t, err) - requireFileExists(t, sc, legacyCertBundlePath, newLegacyContents) - requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents) - issuerIds, err = sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, issuerIds) - require.Equal(t, len(issuerIds), 2) - keyIds, err = sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, keyIds) - require.Equal(t, len(keyIds), 2) - - // -> Re-tidy should remove the legacy one. - time.Sleep(2 * time.Second) - err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg) - require.NoError(t, err) - requireFileNotExists(t, sc, legacyCertBundlePath) - requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents) - issuerIds, err = sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, issuerIds) - keyIds, err = sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, keyIds) -} - -func TestDeletedIssuersPostMigration(t *testing.T) { - // We want to simulate the following scenario: - // - // 1.10.x: -> Create a CA. - // 1.11.0: -> Migrate to new issuer layout but version 1. - // -> Delete existing issuers, create new ones. - // (now): -> Migrate to version 2 layout, make sure we don't see - // re-migration. - - t.Parallel() - ctx := context.Background() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Reset the version the helper above set to 1. - b.pkiStorageVersion.Store(0) - require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.") - - // Create a legacy CA bundle and write it out. - bundle := genCertBundle(t, b, s) - json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle) - require.NoError(t, err) - err = s.Put(ctx, json) - require.NoError(t, err) - legacyContents := requireFileExists(t, sc, legacyCertBundlePath, nil) - - // Do a migration; this should provision an issuer and key. - initReq := &logical.InitializationRequest{Storage: s} - err = b.initialize(ctx, initReq) - require.NoError(t, err) - requireFileExists(t, sc, legacyCertBundlePath, legacyContents) - issuerIds, err := sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, issuerIds) - keyIds, err := sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, keyIds) - - // Hack: reset the version to 1, to simulate a pre-version-2 migration - // log. - info, err := getMigrationInfo(sc.Context, sc.Storage) - require.NoError(t, err, "failed to read migration info") - info.migrationLog.MigrationVersion = 1 - err = setLegacyBundleMigrationLog(sc.Context, sc.Storage, info.migrationLog) - require.NoError(t, err, "failed to write migration info") - - // Now delete all issuers and keys and create some new ones. - for _, issuerId := range issuerIds { - deleted, err := sc.deleteIssuer(issuerId) - require.True(t, deleted, "expected it to be deleted") - require.NoError(t, err, "error removing issuer") - } - for _, keyId := range keyIds { - deleted, err := sc.deleteKey(keyId) - require.True(t, deleted, "expected it to be deleted") - require.NoError(t, err, "error removing key") - } - emptyIssuers, err := sc.listIssuers() - require.NoError(t, err) - require.Empty(t, emptyIssuers) - emptyKeys, err := sc.listKeys() - require.NoError(t, err) - require.Empty(t, emptyKeys) - - // Create a new issuer + key. - bundle = genCertBundle(t, b, s) - _, _, err = sc.writeCaBundle(bundle, "", "") - require.NoError(t, err) - - // List which issuers + keys we currently have. - postDeletionIssuers, err := sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, postDeletionIssuers) - postDeletionKeys, err := sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, postDeletionKeys) - - // Now do another migration from 1->2. This should retain the newly - // created issuers+keys, but not revive any deleted ones. - err = b.initialize(ctx, initReq) - require.NoError(t, err) - requireFileExists(t, sc, legacyCertBundlePath, legacyContents) - postMigrationIssuers, err := sc.listIssuers() - require.NoError(t, err) - require.NotEmpty(t, postMigrationIssuers) - require.Equal(t, postMigrationIssuers, postDeletionIssuers, "regression failed: expected second migration from v1->v2 to not introduce new issuers") - postMigrationKeys, err := sc.listKeys() - require.NoError(t, err) - require.NotEmpty(t, postMigrationKeys) - require.Equal(t, postMigrationKeys, postDeletionKeys, "regression failed: expected second migration from v1->v2 to not introduce new keys") -} - -// requireFailInMigration validate that we fail the operation with the appropriate error message to the end-user -func requireFailInMigration(t *testing.T, b *backend, s logical.Storage, operation logical.Operation, path string) { - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: operation, - Path: path, - Storage: s, - MountPoint: "pki/", - }) - require.NoError(t, err, "error from op:%s path:%s", operation, path) - require.NotNil(t, resp, "got nil response from op:%s path:%s", operation, path) - require.True(t, resp.IsError(), "error flag was not set from op:%s path:%s resp: %#v", operation, path, resp) - require.Contains(t, resp.Error().Error(), "migration has completed", - "error message did not contain migration test for op:%s path:%s resp: %#v", operation, path, resp) -} - -func requireFileNotExists(t *testing.T, sc *storageContext, path string) { - t.Helper() - - entry, err := sc.Storage.Get(sc.Context, path) - require.NoError(t, err) - if entry != nil { - require.Empty(t, entry.Value) - } else { - require.Empty(t, entry) - } -} - -func requireFileExists(t *testing.T, sc *storageContext, path string, contents []byte) []byte { - t.Helper() - - entry, err := sc.Storage.Get(sc.Context, path) - require.NoError(t, err) - require.NotNil(t, entry) - require.NotEmpty(t, entry.Value) - if contents != nil { - require.Equal(t, entry.Value, contents) - } - return entry.Value -} - -// Keys to simulate an intermediate CA mount with also-imported root (parent). -const ( - migIntPrivKey = `-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAqu88Jcct/EyT8gDF+jdWuAwFplvanQ7KXAO5at58G6Y39UUz -fwnMS3P3VRBUoV5BDX+13wI2ldskbTKITsl6IXBPXUz0sKrdEKzXRVY4D6P2JR7W -YO1IUytfTgR+3F4sotFNQB++3ivT66AYLW7lOkoa+5lxsPM/oJ82DOlD2uGtDVTU -gQy1zugMBgPDlj+8tB562J9MTIdcKe9JpYrN0eO+aHzhbfvaSpScU4aZBgkS0kDv -8G4FxVfrBSDeD/JjCWaC48rLdgei1YrY0NFuw/8p/nPfA9vf2AtHMsWZRSwukQfq -I5HhQu+0OHQy3NWqXaBPzJNu3HnpKLykPHW7sQIDAQABAoIBAHNJy/2G66MhWx98 -Ggt7S4fyw9TCWx5XHXEWKfbEfFyBrXhF5kemqh2x5319+DamRaX/HwF8kqhcF6N2 -06ygAzmOcFjzUI3fkB5xFPh1AHa8FYZP2DOjloZR2IPcUFv9QInINRwszSU31kUz -w1rRUtYPqUdM5Pt99Mo219O5eMSlGtPKXm09uDAR8ZPuUx4jwGw90pSgeRB1Bg7X -Dt3YXx3X+OOs3Hbir1VDLSqCuy825l6Kn79h3eB8LAi+FUwCBvnTqyOEWyH2XjgP -z+tbz7lwnhGeKtxUl6Jb3m3SHtXpylot/4fwPisRV/9vaEDhVjKTmySH1WM+TRNR -CQLCJekCgYEA3b67DBhAYsFFdUd/4xh4QhHBanOcepV1CwaRln+UUjw1618ZEsTS -DKb9IS72C+ukUusGhQqxjFJlhOdXeMXpEbnEUY3PlREevWwm3bVAxtoAVRcmkQyK -PM4Oj9ODi2z8Cds0NvEXdX69uVutcbvm/JRZr/dsERWcLsfwdV/QqYcCgYEAxVce -d4ylsqORLm0/gcLnEyB9zhEPwmiJe1Yj5sH7LhGZ6JtLCqbOJO4jXmIzCrkbGyuf -BA/U7klc6jSprkBMgYhgOIuaULuFJvtKzJUzoATGFqX4r8WJm2ZycXgooAwZq6SZ -ySXOuQe9V7hlpI0fJfNhw+/HIjivL1jrnjBoXwcCgYEAtTv6LLx1g0Frv5scj0Ok -pntUlei/8ADPlJ9dxp+nXj8P4rvrBkgPVX/2S3TSbJO/znWA8qP20TVW+/UIrRE0 -mOQ37F/3VWKUuUT3zyUhOGVc+C7fupWBNolDpZG+ZepBZNzgJDeQcNuRvTmM3PQy -qiWl2AhlLuF2sVWA1q3lIWkCgYEAnuHWgNA3dE1nDWceE351hxvIzklEU/TQhAHF -o/uYHO5E6VdmoqvMG0W0KkCL8d046rZDMAUDHdrpOROvbcENF9lSBxS26LshqFH4 -ViDmULanOgLk57f2Y6ynBZ6Frt4vKNe8jYuoFacale67vzFz251JoHSD8pSKz2cb -ROCal68CgYA51hKqvki4r5rmS7W/Yvc3x3Wc0wpDEHTgLMoH+EV7AffJ8dy0/+po -AHK0nnRU63++1JmhQczBR0yTI6PUyeegEBk/d5CgFlY7UJQMTFPsMsiuM0Xw5nAv -KMPykK01D28UAkUxhwF7CqFrwwEv9GislgjewbdF5Za176+EuMEwIw== ------END RSA PRIVATE KEY----- -` - migIntCA = `-----BEGIN CERTIFICATE----- -MIIDHTCCAgWgAwIBAgIUfxlNBmrI7jsgH2Sdle1nVTqn5YQwDQYJKoZIhvcNAQEL -BQAwEjEQMA4GA1UEAxMHUm9vdCBYMTAeFw0yMjExMDIxMjI2MjhaFw0yMjEyMDQx -MjI2NThaMBoxGDAWBgNVBAMTD0ludGVybWVkaWF0ZSBSMTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAKrvPCXHLfxMk/IAxfo3VrgMBaZb2p0OylwDuWre -fBumN/VFM38JzEtz91UQVKFeQQ1/td8CNpXbJG0yiE7JeiFwT11M9LCq3RCs10VW -OA+j9iUe1mDtSFMrX04EftxeLKLRTUAfvt4r0+ugGC1u5TpKGvuZcbDzP6CfNgzp -Q9rhrQ1U1IEMtc7oDAYDw5Y/vLQeetifTEyHXCnvSaWKzdHjvmh84W372kqUnFOG -mQYJEtJA7/BuBcVX6wUg3g/yYwlmguPKy3YHotWK2NDRbsP/Kf5z3wPb39gLRzLF -mUUsLpEH6iOR4ULvtDh0MtzVql2gT8yTbtx56Si8pDx1u7ECAwEAAaNjMGEwDgYD -VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFusWj3piAiY -CR7tszR6uNYSMLe2MB8GA1UdIwQYMBaAFMNRNkLozstIhNhXCefi+WnaQApbMA0G -CSqGSIb3DQEBCwUAA4IBAQCmH852E/pDGBhf2VI1JAPZy9VYaRkKoqn4+5R1Gnoq -b90zhdCGueIm/usC1wAa0OOn7+xdQXFNfeI8UUB9w10q0QnM/A/G2v8UkdlLPPQP -zPjIYLalOOIOHf8hU2O5lwj0IA4JwjwDQ4xj69eX/N+x2LEI7SHyVVUZWAx0Y67a -QdyubpIJZlW/PI7kMwGyTx3tdkZxk1nTNtf/0nKvNuXKKcVzBCEMfvXyx4LFEM+U -nc2vdWN7PAoXcjUbxD3ZNGinr7mSBpQg82+nur/8yuSwu6iHomnfGxjUsEHic2GC -ja9siTbR+ONvVb4xUjugN/XmMSSaZnxig2vM9xcV8OMG ------END CERTIFICATE----- -` - migRootCA = `-----BEGIN CERTIFICATE----- -MIIDFTCCAf2gAwIBAgIURDTnXp8u78jWMe770Jj6Ac1paxkwDQYJKoZIhvcNAQEL -BQAwEjEQMA4GA1UEAxMHUm9vdCBYMTAeFw0yMjExMDIxMjI0NTVaFw0yMjEyMDQx -MjI1MjRaMBIxEDAOBgNVBAMTB1Jvb3QgWDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQC/+dh/o1qKTOua/OkHRMIvHiyBxjjoqrLqFSBYhjYKs+alA0qS -lLVzNqIKU8jm3fT73orx7yk/6acWaEYv/6owMaUn51xwS3gQhTHdFR/fLJwXnu2O -PZNqAs6tjAM3Q08aqR0qfxnjDvcgO7TOWSyOvVT2cTRK+uKYzxJEY52BDMUbp+iC -WJdXca9UwKRzi2wFqGliDycYsBBt/tr8tHSbTSZ5Qx6UpFrKpjZn+sT5KhKUlsdd -BYFmRegc0wXq4/kRjum0oEUigUMlHADIEhRasyXPEKa19sGP8nAZfo/hNOusGhj7 -z7UPA0Cbe2uclpYPxsKgvcqQmgKugqKLL305AgMBAAGjYzBhMA4GA1UdDwEB/wQE -AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTDUTZC6M7LSITYVwnn4vlp -2kAKWzAfBgNVHSMEGDAWgBTDUTZC6M7LSITYVwnn4vlp2kAKWzANBgkqhkiG9w0B -AQsFAAOCAQEAu7qdM1Li6V6iDCPpLg5zZReRtcxhUdwb5Xn4sDa8GJCy35f1voew -n0TQgM3Uph5x/djCR/Sj91MyAJ1/Q1PQQTyKGyUjSHvkcOBg628IAnLthn8Ua1fL -oQC/F/mlT1Yv+/W8eNPtD453/P0z8E0xMT5K3kpEDW/6K9RdHZlDJMW/z3UJ+4LN -6ONjIBmgffmLz9sVMpgCFyL7+w3W01bGP7w5AfKj2duoVG/Ekf2yUwmm6r9NgTQ1 -oke0ShbZuMocwO8anq7k0R42FoluH3ipv9Qzzhsy+KdK5/fW5oqy1tKFaZsc67Q6 -0UmD9DiDpCtn2Wod3nwxn0zW5HvDAWuDwg== ------END CERTIFICATE----- -` -) diff --git a/builtin/logical/pki/storage_test.go b/builtin/logical/pki/storage_test.go deleted file mode 100644 index 3955e4704..000000000 --- a/builtin/logical/pki/storage_test.go +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" -) - -var ctx = context.Background() - -func Test_ConfigsRoundTrip(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Create an empty key, issuer for testing. - key := keyEntry{ID: genKeyId()} - err := sc.writeKey(key) - require.NoError(t, err) - issuer := &issuerEntry{ID: genIssuerId()} - err = sc.writeIssuer(issuer) - require.NoError(t, err) - - // Verify we handle nothing stored properly - keyConfigEmpty, err := sc.getKeysConfig() - require.NoError(t, err) - require.Equal(t, &keyConfigEntry{}, keyConfigEmpty) - - issuerConfigEmpty, err := sc.getIssuersConfig() - require.NoError(t, err) - require.Equal(t, &issuerConfigEntry{}, issuerConfigEmpty) - - // Now attempt to store and reload properly - origKeyConfig := &keyConfigEntry{ - DefaultKeyId: key.ID, - } - origIssuerConfig := &issuerConfigEntry{ - DefaultIssuerId: issuer.ID, - } - - err = sc.setKeysConfig(origKeyConfig) - require.NoError(t, err) - err = sc.setIssuersConfig(origIssuerConfig) - require.NoError(t, err) - - keyConfig, err := sc.getKeysConfig() - require.NoError(t, err) - require.Equal(t, origKeyConfig, keyConfig) - - issuerConfig, err := sc.getIssuersConfig() - require.NoError(t, err) - require.Equal(t, origIssuerConfig.DefaultIssuerId, issuerConfig.DefaultIssuerId) -} - -func Test_IssuerRoundTrip(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - issuer1, key1 := genIssuerAndKey(t, b, s) - issuer2, key2 := genIssuerAndKey(t, b, s) - - // We get an error when issuer id not found - _, err := sc.fetchIssuerById(issuer1.ID) - require.Error(t, err) - - // We get an error when key id not found - _, err = sc.fetchKeyById(key1.ID) - require.Error(t, err) - - // Now write out our issuers and keys - err = sc.writeKey(key1) - require.NoError(t, err) - err = sc.writeIssuer(&issuer1) - require.NoError(t, err) - - err = sc.writeKey(key2) - require.NoError(t, err) - err = sc.writeIssuer(&issuer2) - require.NoError(t, err) - - fetchedKey1, err := sc.fetchKeyById(key1.ID) - require.NoError(t, err) - - fetchedIssuer1, err := sc.fetchIssuerById(issuer1.ID) - require.NoError(t, err) - - require.Equal(t, &key1, fetchedKey1) - require.Equal(t, &issuer1, fetchedIssuer1) - - keys, err := sc.listKeys() - require.NoError(t, err) - - require.ElementsMatch(t, []keyID{key1.ID, key2.ID}, keys) - - issuers, err := sc.listIssuers() - require.NoError(t, err) - - require.ElementsMatch(t, []issuerID{issuer1.ID, issuer2.ID}, issuers) -} - -func Test_KeysIssuerImport(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - issuer1, key1 := genIssuerAndKey(t, b, s) - issuer2, key2 := genIssuerAndKey(t, b, s) - - // Key 1 before Issuer 1; Issuer 2 before Key 2. - // Remove KeyIDs from non-written entities before beginning. - key1.ID = "" - issuer1.ID = "" - issuer1.KeyID = "" - - key1Ref1, existing, err := sc.importKey(key1.PrivateKey, "key1", key1.PrivateKeyType) - require.NoError(t, err) - require.False(t, existing) - require.Equal(t, strings.TrimSpace(key1.PrivateKey), strings.TrimSpace(key1Ref1.PrivateKey)) - - // Make sure if we attempt to re-import the same private key, no import/updates occur. - // So the existing flag should be set to true, and we do not update the existing Name field. - key1Ref2, existing, err := sc.importKey(key1.PrivateKey, "ignore-me", key1.PrivateKeyType) - require.NoError(t, err) - require.True(t, existing) - require.Equal(t, key1.PrivateKey, key1Ref1.PrivateKey) - require.Equal(t, key1Ref1.ID, key1Ref2.ID) - require.Equal(t, key1Ref1.Name, key1Ref2.Name) - - issuer1Ref1, existing, err := sc.importIssuer(issuer1.Certificate, "issuer1") - require.NoError(t, err) - require.False(t, existing) - require.Equal(t, strings.TrimSpace(issuer1.Certificate), strings.TrimSpace(issuer1Ref1.Certificate)) - require.Equal(t, key1Ref1.ID, issuer1Ref1.KeyID) - require.Equal(t, "issuer1", issuer1Ref1.Name) - - // Make sure if we attempt to re-import the same issuer, no import/updates occur. - // So the existing flag should be set to true, and we do not update the existing Name field. - issuer1Ref2, existing, err := sc.importIssuer(issuer1.Certificate, "ignore-me") - require.NoError(t, err) - require.True(t, existing) - require.Equal(t, strings.TrimSpace(issuer1.Certificate), strings.TrimSpace(issuer1Ref1.Certificate)) - require.Equal(t, issuer1Ref1.ID, issuer1Ref2.ID) - require.Equal(t, key1Ref1.ID, issuer1Ref2.KeyID) - require.Equal(t, issuer1Ref1.Name, issuer1Ref2.Name) - - err = sc.writeIssuer(&issuer2) - require.NoError(t, err) - - err = sc.writeKey(key2) - require.NoError(t, err) - - // Same double import tests as above, but make sure if the previous was created through writeIssuer not importIssuer. - issuer2Ref, existing, err := sc.importIssuer(issuer2.Certificate, "ignore-me") - require.NoError(t, err) - require.True(t, existing) - require.Equal(t, strings.TrimSpace(issuer2.Certificate), strings.TrimSpace(issuer2Ref.Certificate)) - require.Equal(t, issuer2.ID, issuer2Ref.ID) - require.Equal(t, "", issuer2Ref.Name) - require.Equal(t, issuer2.KeyID, issuer2Ref.KeyID) - - // Same double import tests as above, but make sure if the previous was created through writeKey not importKey. - key2Ref, existing, err := sc.importKey(key2.PrivateKey, "ignore-me", key2.PrivateKeyType) - require.NoError(t, err) - require.True(t, existing) - require.Equal(t, key2.PrivateKey, key2Ref.PrivateKey) - require.Equal(t, key2.ID, key2Ref.ID) - require.Equal(t, "", key2Ref.Name) -} - -func Test_IssuerUpgrade(t *testing.T) { - t.Parallel() - b, s := CreateBackendWithStorage(t) - sc := b.makeStorageContext(ctx, s) - - // Make sure that we add OCSP signing to v0 issuers if CRLSigning is enabled - issuer, _ := genIssuerAndKey(t, b, s) - issuer.Version = 0 - issuer.Usage.ToggleUsage(OCSPSigningUsage) - - err := sc.writeIssuer(&issuer) - require.NoError(t, err, "failed writing out issuer") - - newIssuer, err := sc.fetchIssuerById(issuer.ID) - require.NoError(t, err, "failed fetching issuer") - - require.Equal(t, uint(1), newIssuer.Version) - require.True(t, newIssuer.Usage.HasUsage(OCSPSigningUsage)) - - // If CRLSigning is not present on a v0, we should not have OCSP signing after upgrade. - issuer, _ = genIssuerAndKey(t, b, s) - issuer.Version = 0 - issuer.Usage.ToggleUsage(OCSPSigningUsage) - issuer.Usage.ToggleUsage(CRLSigningUsage) - - err = sc.writeIssuer(&issuer) - require.NoError(t, err, "failed writing out issuer") - - newIssuer, err = sc.fetchIssuerById(issuer.ID) - require.NoError(t, err, "failed fetching issuer") - - require.Equal(t, uint(1), newIssuer.Version) - require.False(t, newIssuer.Usage.HasUsage(OCSPSigningUsage)) -} - -func genIssuerAndKey(t *testing.T, b *backend, s logical.Storage) (issuerEntry, keyEntry) { - certBundle := genCertBundle(t, b, s) - - keyId := genKeyId() - - pkiKey := keyEntry{ - ID: keyId, - PrivateKeyType: certBundle.PrivateKeyType, - PrivateKey: strings.TrimSpace(certBundle.PrivateKey) + "\n", - } - - issuerId := genIssuerId() - - pkiIssuer := issuerEntry{ - ID: issuerId, - KeyID: keyId, - Certificate: strings.TrimSpace(certBundle.Certificate) + "\n", - CAChain: certBundle.CAChain, - SerialNumber: certBundle.SerialNumber, - Usage: AllIssuerUsages, - Version: latestIssuerVersion, - } - - return pkiIssuer, pkiKey -} - -func genCertBundle(t *testing.T, b *backend, s logical.Storage) *certutil.CertBundle { - // Pretty gross just to generate a cert bundle, but - fields := addCACommonFields(map[string]*framework.FieldSchema{}) - fields = addCAKeyGenerationFields(fields) - fields = addCAIssueFields(fields) - apiData := &framework.FieldData{ - Schema: fields, - Raw: map[string]interface{}{ - "exported": "internal", - "cn": "example.com", - "ttl": 3600, - }, - } - sc := b.makeStorageContext(ctx, s) - _, _, role, respErr := getGenerationParams(sc, apiData) - require.Nil(t, respErr) - - input := &inputBundle{ - req: &logical.Request{ - Operation: logical.UpdateOperation, - Path: "issue/testrole", - Storage: s, - }, - apiData: apiData, - role: role, - } - parsedCertBundle, _, err := generateCert(sc, input, nil, true, b.GetRandomReader()) - - require.NoError(t, err) - certBundle, err := parsedCertBundle.ToCertBundle() - require.NoError(t, err) - return certBundle -} - -func writeLegacyBundle(t *testing.T, b *backend, s logical.Storage, bundle *certutil.CertBundle) { - entry, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle) - require.NoError(t, err) - - err = s.Put(context.Background(), entry) - require.NoError(t, err) -} diff --git a/builtin/logical/pki/test_helpers.go b/builtin/logical/pki/test_helpers.go deleted file mode 100644 index 1cdc98795..000000000 --- a/builtin/logical/pki/test_helpers.go +++ /dev/null @@ -1,438 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pki - -import ( - "context" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/pem" - "fmt" - "io" - "math" - "math/big" - http2 "net/http" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/ocsp" -) - -// Setup helpers -func CreateBackendWithStorage(t testing.TB) (*backend, logical.Storage) { - t.Helper() - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - - var err error - b := Backend(config) - err = b.Setup(context.Background(), config) - if err != nil { - t.Fatal(err) - } - // Assume for our tests we have performed the migration already. - b.pkiStorageVersion.Store(1) - return b, config.StorageView -} - -func mountPKIEndpoint(t testing.TB, client *api.Client, path string) { - t.Helper() - - err := client.Sys().Mount(path, &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "32h", - }, - }) - require.NoError(t, err, "failed mounting pki endpoint") -} - -// Signing helpers -func requireSignedBy(t *testing.T, cert *x509.Certificate, signingCert *x509.Certificate) { - t.Helper() - - if err := cert.CheckSignatureFrom(signingCert); err != nil { - t.Fatalf("signature verification failed: %v", err) - } -} - -func requireSignedByAtPath(t *testing.T, client *api.Client, leaf *x509.Certificate, path string) { - t.Helper() - - resp, err := client.Logical().Read(path) - require.NoError(t, err, "got unexpected error fetching parent certificate") - require.NotNil(t, resp, "missing response when fetching parent certificate") - require.NotNil(t, resp.Data, "missing data from parent certificate response") - require.NotNil(t, resp.Data["certificate"], "missing certificate field on parent read response") - - parentCert := resp.Data["certificate"].(string) - parent := parseCert(t, parentCert) - - requireSignedBy(t, leaf, parent) -} - -// Certificate helper -func parseCert(t *testing.T, pemCert string) *x509.Certificate { - t.Helper() - - block, _ := pem.Decode([]byte(pemCert)) - require.NotNil(t, block, "failed to decode PEM block") - - cert, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) - return cert -} - -func requireMatchingPublicKeys(t *testing.T, cert *x509.Certificate, key crypto.PublicKey) { - t.Helper() - - certPubKey := cert.PublicKey - areEqual, err := certutil.ComparePublicKeysAndType(certPubKey, key) - require.NoError(t, err, "failed comparing public keys: %#v", err) - require.True(t, areEqual, "public keys mismatched: got: %v, expected: %v", certPubKey, key) -} - -func getSelfSigned(t *testing.T, subject, issuer *x509.Certificate, key *rsa.PrivateKey) (string, *x509.Certificate) { - t.Helper() - selfSigned, err := x509.CreateCertificate(rand.Reader, subject, issuer, key.Public(), key) - if err != nil { - t.Fatal(err) - } - cert, err := x509.ParseCertificate(selfSigned) - if err != nil { - t.Fatal(err) - } - pemSS := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: selfSigned, - }))) - return pemSS, cert -} - -// CRL related helpers -func getCrlCertificateList(t *testing.T, client *api.Client, mountPoint string) pkix.TBSCertificateList { - t.Helper() - - path := fmt.Sprintf("/v1/%s/crl", mountPoint) - return getParsedCrlAtPath(t, client, path).TBSCertList -} - -func parseCrlPemBytes(t *testing.T, crlPem []byte) pkix.TBSCertificateList { - t.Helper() - - certList, err := x509.ParseCRL(crlPem) - require.NoError(t, err) - return certList.TBSCertList -} - -func requireSerialNumberInCRL(t *testing.T, revokeList pkix.TBSCertificateList, serialNum string) bool { - if t != nil { - t.Helper() - } - - serialsInList := make([]string, 0, len(revokeList.RevokedCertificates)) - for _, revokeEntry := range revokeList.RevokedCertificates { - formattedSerial := certutil.GetHexFormatted(revokeEntry.SerialNumber.Bytes(), ":") - serialsInList = append(serialsInList, formattedSerial) - if formattedSerial == serialNum { - return true - } - } - - if t != nil { - t.Fatalf("the serial number %s, was not found in the CRL list containing: %v", serialNum, serialsInList) - } - - return false -} - -func getParsedCrl(t *testing.T, client *api.Client, mountPoint string) *pkix.CertificateList { - t.Helper() - - path := fmt.Sprintf("/v1/%s/crl", mountPoint) - return getParsedCrlAtPath(t, client, path) -} - -func getParsedCrlAtPath(t *testing.T, client *api.Client, path string) *pkix.CertificateList { - t.Helper() - - req := client.NewRequest("GET", path) - resp, err := client.RawRequest(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - - crlBytes, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("err: %s", err) - } - if len(crlBytes) == 0 { - t.Fatalf("expected CRL in response body") - } - - crl, err := x509.ParseDERCRL(crlBytes) - if err != nil { - t.Fatal(err) - } - return crl -} - -func getParsedCrlFromBackend(t *testing.T, b *backend, s logical.Storage, path string) *pkix.CertificateList { - t.Helper() - - resp, err := CBRead(b, s, path) - if err != nil { - t.Fatal(err) - } - - crl, err := x509.ParseDERCRL(resp.Data[logical.HTTPRawBody].([]byte)) - if err != nil { - t.Fatal(err) - } - return crl -} - -// Direct storage backend helpers (b, s := createBackendWithStorage(t)) which -// are mostly compatible with client.Logical() operations. The main difference -// is that the JSON round-tripping hasn't occurred, so values are as the -// backend returns them (e.g., []string instead of []interface{}). -func CBReq(b *backend, s logical.Storage, operation logical.Operation, path string, data map[string]interface{}) (*logical.Response, error) { - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: operation, - Path: path, - Data: data, - Storage: s, - MountPoint: "pki/", - }) - if err != nil || resp == nil { - return resp, err - } - - if msg, ok := resp.Data["error"]; ok && msg != nil && len(msg.(string)) > 0 { - return resp, fmt.Errorf("%s", msg) - } - - return resp, nil -} - -func CBHeader(b *backend, s logical.Storage, path string) (*logical.Response, error) { - return CBReq(b, s, logical.HeaderOperation, path, make(map[string]interface{})) -} - -func CBRead(b *backend, s logical.Storage, path string) (*logical.Response, error) { - return CBReq(b, s, logical.ReadOperation, path, make(map[string]interface{})) -} - -func CBWrite(b *backend, s logical.Storage, path string, data map[string]interface{}) (*logical.Response, error) { - return CBReq(b, s, logical.UpdateOperation, path, data) -} - -func CBPatch(b *backend, s logical.Storage, path string, data map[string]interface{}) (*logical.Response, error) { - return CBReq(b, s, logical.PatchOperation, path, data) -} - -func CBList(b *backend, s logical.Storage, path string) (*logical.Response, error) { - return CBReq(b, s, logical.ListOperation, path, make(map[string]interface{})) -} - -func CBDelete(b *backend, s logical.Storage, path string) (*logical.Response, error) { - return CBReq(b, s, logical.DeleteOperation, path, make(map[string]interface{})) -} - -func requireFieldsSetInResp(t *testing.T, resp *logical.Response, fields ...string) { - t.Helper() - - var missingFields []string - for _, field := range fields { - value, ok := resp.Data[field] - if !ok || value == nil { - missingFields = append(missingFields, field) - } - } - - require.Empty(t, missingFields, "The following fields were required but missing from response:\n%v", resp.Data) -} - -func requireSuccessNonNilResponse(t *testing.T, resp *logical.Response, err error, msgAndArgs ...interface{}) { - t.Helper() - - require.NoError(t, err, msgAndArgs...) - if resp.IsError() { - errContext := fmt.Sprintf("Expected successful response but got error: %v", resp.Error()) - require.Falsef(t, resp.IsError(), errContext, msgAndArgs...) - } - require.NotNil(t, resp, msgAndArgs...) -} - -func requireSuccessNilResponse(t *testing.T, resp *logical.Response, err error, msgAndArgs ...interface{}) { - t.Helper() - - require.NoError(t, err, msgAndArgs...) - if resp.IsError() { - errContext := fmt.Sprintf("Expected successful response but got error: %v", resp.Error()) - require.Falsef(t, resp.IsError(), errContext, msgAndArgs...) - } - if resp != nil { - msg := fmt.Sprintf("expected nil response but got: %v", resp) - require.Nilf(t, resp, msg, msgAndArgs...) - } -} - -func getCRLNumber(t *testing.T, crl pkix.TBSCertificateList) int { - t.Helper() - - for _, extension := range crl.Extensions { - if extension.Id.Equal(certutil.CRLNumberOID) { - bigInt := new(big.Int) - leftOver, err := asn1.Unmarshal(extension.Value, &bigInt) - require.NoError(t, err, "Failed unmarshalling crl number extension") - require.Empty(t, leftOver, "leftover bytes from unmarshalling crl number extension") - require.True(t, bigInt.IsInt64(), "parsed crl number integer is not an int64") - require.False(t, math.MaxInt <= bigInt.Int64(), "parsed crl number integer can not fit in an int") - return int(bigInt.Int64()) - } - } - - t.Fatalf("failed to find crl number extension") - return 0 -} - -func getCrlReferenceFromDelta(t *testing.T, crl pkix.TBSCertificateList) int { - t.Helper() - - for _, extension := range crl.Extensions { - if extension.Id.Equal(certutil.DeltaCRLIndicatorOID) { - bigInt := new(big.Int) - leftOver, err := asn1.Unmarshal(extension.Value, &bigInt) - require.NoError(t, err, "Failed unmarshalling delta crl indicator extension") - require.Empty(t, leftOver, "leftover bytes from unmarshalling delta crl indicator extension") - require.True(t, bigInt.IsInt64(), "parsed delta crl integer is not an int64") - require.False(t, math.MaxInt <= bigInt.Int64(), "parsed delta crl integer can not fit in an int") - return int(bigInt.Int64()) - } - } - - t.Fatalf("failed to find delta crl indicator extension") - return 0 -} - -// waitForUpdatedCrl will wait until the CRL at the provided path has been reloaded -// up for a maxWait duration and gives up if the timeout has been reached. If a negative -// value for lastSeenCRLNumber is provided, the method will load the current CRL and wait -// for a newer CRL be generated. -func waitForUpdatedCrl(t *testing.T, client *api.Client, crlPath string, lastSeenCRLNumber int, maxWait time.Duration) pkix.TBSCertificateList { - t.Helper() - - newCrl, didTimeOut := waitForUpdatedCrlUntil(t, client, crlPath, lastSeenCRLNumber, maxWait) - if didTimeOut { - t.Fatalf("Timed out waiting for new CRL rebuild on path %s", crlPath) - } - return newCrl.TBSCertList -} - -// waitForUpdatedCrlUntil is a helper method that will wait for a CRL to be updated up until maxWait duration -// or give up and return the last CRL it loaded. It will not fail, if it does not see a new CRL within the -// max duration unlike waitForUpdatedCrl. Returns the last loaded CRL at the provided path and a boolean -// indicating if we hit maxWait duration or not. -func waitForUpdatedCrlUntil(t *testing.T, client *api.Client, crlPath string, lastSeenCrlNumber int, maxWait time.Duration) (*pkix.CertificateList, bool) { - t.Helper() - - crl := getParsedCrlAtPath(t, client, crlPath) - initialCrlRevision := getCRLNumber(t, crl.TBSCertList) - newCrlRevision := initialCrlRevision - - // Short circuit the fetches if we have a version of the CRL we want - if lastSeenCrlNumber > 0 && getCRLNumber(t, crl.TBSCertList) > lastSeenCrlNumber { - return crl, false - } - - start := time.Now() - iteration := 0 - for { - iteration++ - - if time.Since(start) > maxWait { - t.Logf("Timed out waiting for new CRL on path %s after iteration %d, delay: %v", - crlPath, iteration, time.Now().Sub(start)) - return crl, true - } - - crl = getParsedCrlAtPath(t, client, crlPath) - newCrlRevision = getCRLNumber(t, crl.TBSCertList) - if newCrlRevision > initialCrlRevision { - t.Logf("Got new revision of CRL %s from %d to %d after iteration %d, delay %v", - crlPath, initialCrlRevision, newCrlRevision, iteration, time.Now().Sub(start)) - return crl, false - } - - time.Sleep(100 * time.Millisecond) - } -} - -// A quick CRL to string to provide better test error messages -func summarizeCrl(t *testing.T, crl pkix.TBSCertificateList) string { - version := getCRLNumber(t, crl) - serials := []string{} - for _, cert := range crl.RevokedCertificates { - serials = append(serials, normalizeSerialFromBigInt(cert.SerialNumber)) - } - return fmt.Sprintf("CRL Version: %d\n"+ - "This Update: %s\n"+ - "Next Update: %s\n"+ - "Revoked Serial Count: %d\n"+ - "Revoked Serials: %v", version, crl.ThisUpdate, crl.NextUpdate, len(serials), serials) -} - -// OCSP helpers -func generateRequest(t *testing.T, requestHash crypto.Hash, cert *x509.Certificate, issuer *x509.Certificate) []byte { - t.Helper() - - opts := &ocsp.RequestOptions{Hash: requestHash} - ocspRequestDer, err := ocsp.CreateRequest(cert, issuer, opts) - require.NoError(t, err, "Failed generating OCSP request") - return ocspRequestDer -} - -func requireOcspResponseSignedBy(t *testing.T, ocspResp *ocsp.Response, issuer *x509.Certificate) { - t.Helper() - - err := ocspResp.CheckSignatureFrom(issuer) - require.NoError(t, err, "Failed signature verification of ocsp response: %w", err) -} - -func performOcspPost(t *testing.T, cert *x509.Certificate, issuerCert *x509.Certificate, client *api.Client, ocspPath string) *ocsp.Response { - t.Helper() - - baseClient := client.WithNamespace("") - - ocspReq := generateRequest(t, crypto.SHA256, cert, issuerCert) - ocspPostReq := baseClient.NewRequest(http2.MethodPost, ocspPath) - ocspPostReq.Headers.Set("Content-Type", "application/ocsp-request") - ocspPostReq.BodyBytes = ocspReq - rawResp, err := baseClient.RawRequest(ocspPostReq) - require.NoError(t, err, "failed sending unified-ocsp post request") - - require.Equal(t, 200, rawResp.StatusCode) - require.Equal(t, ocspResponseContentType, rawResp.Header.Get("Content-Type")) - bodyReader := rawResp.Body - respDer, err := io.ReadAll(bodyReader) - bodyReader.Close() - require.NoError(t, err, "failed reading response body") - - ocspResp, err := ocsp.ParseResponse(respDer, issuerCert) - require.NoError(t, err, "parsing ocsp get response") - return ocspResp -} diff --git a/builtin/logical/pkiext/README.md b/builtin/logical/pkiext/README.md deleted file mode 100644 index 40364e085..000000000 --- a/builtin/logical/pkiext/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# What is `pkiext`? - -`pkiext` exists to split the Docker tests into a separate package from the -main PKI tests. Because the Docker tests execute in a smaller runner with -fewer resources, and we were hitting timeouts waiting for the entire PKI -test suite to run, we need to split the larger non-Docker PKI tests from -the smaller Docker tests, to ensure the former can execute. - -This package should lack any non-test related targets. diff --git a/builtin/logical/pkiext/nginx_test.go b/builtin/logical/pkiext/nginx_test.go deleted file mode 100644 index e7d3ab42e..000000000 --- a/builtin/logical/pkiext/nginx_test.go +++ /dev/null @@ -1,638 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pkiext - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "io" - "net" - "net/http" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/builtin/logical/pki" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/stretchr/testify/require" -) - -var ( - cwRunner *docker.Runner - builtNetwork string - buildClientContainerOnce sync.Once -) - -const ( - protectedFile = `dadgarcorp-internal-protected` - unprotectedFile = `hello-world` - failureIndicator = `THIS-TEST-SHOULD-FAIL` - uniqueHostname = `dadgarcorpvaultpkitestingnginxwgetcurlcontainersexample.com` - containerName = `vault_pki_nginx_integration` -) - -func buildNginxContainer(t *testing.T, root string, crl string, chain string, private string) (func(), string, int, string, string, int) { - containerfile := ` -FROM nginx:latest - -RUN mkdir /www /etc/nginx/ssl && rm /etc/nginx/conf.d/*.conf - -COPY testing.conf /etc/nginx/conf.d/ -COPY root.pem /etc/nginx/ssl/root.pem -COPY fullchain.pem /etc/nginx/ssl/fullchain.pem -COPY privkey.pem /etc/nginx/ssl/privkey.pem -COPY crl.pem /etc/nginx/ssl/crl.pem -COPY /data /www/data -` - - siteConfig := ` -server { - listen 80; - listen [::]:80; - - location / { - return 301 $request_uri; - } -} - -server { - listen 443 ssl; - listen [::]:443 ssl; - - ssl_certificate /etc/nginx/ssl/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/privkey.pem; - - ssl_client_certificate /etc/nginx/ssl/root.pem; - ssl_crl /etc/nginx/ssl/crl.pem; - ssl_verify_client optional; - - # Magic per: https://serverfault.com/questions/891603/nginx-reverse-proxy-with-optional-ssl-client-authentication - # Only necessary since we're too lazy to setup two different subdomains. - set $ssl_status 'open'; - if ($request_uri ~ protected) { - set $ssl_status 'closed'; - } - - if ($ssl_client_verify != SUCCESS) { - set $ssl_status "$ssl_status-fail"; - } - - if ($ssl_status = "closed-fail") { - return 403; - } - - location / { - root /www/data; - } -} -` - - bCtx := docker.NewBuildContext() - bCtx["testing.conf"] = docker.PathContentsFromString(siteConfig) - bCtx["root.pem"] = docker.PathContentsFromString(root) - bCtx["fullchain.pem"] = docker.PathContentsFromString(chain) - bCtx["privkey.pem"] = docker.PathContentsFromString(private) - bCtx["crl.pem"] = docker.PathContentsFromString(crl) - bCtx["/data/index.html"] = docker.PathContentsFromString(unprotectedFile) - bCtx["/data/protected.html"] = docker.PathContentsFromString(protectedFile) - - imageName := "vault_pki_nginx_integration" - suffix, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("error generating unique suffix: %v", err) - } - imageTag := suffix - - runner, err := docker.NewServiceRunner(docker.RunOptions{ - ImageRepo: imageName, - ImageTag: imageTag, - ContainerName: containerName, - Ports: []string{"443/tcp"}, - LogConsumer: func(s string) { - if t.Failed() { - t.Logf("container logs: %s", s) - } - }, - }) - if err != nil { - t.Fatalf("Could not provision docker service runner: %s", err) - } - - ctx := context.Background() - output, err := runner.BuildImage(ctx, containerfile, bCtx, - docker.BuildRemove(true), docker.BuildForceRemove(true), - docker.BuildPullParent(true), - docker.BuildTags([]string{imageName + ":" + imageTag})) - if err != nil { - t.Fatalf("Could not build new image: %v", err) - } - - t.Logf("Image build output: %v", string(output)) - - svc, err := runner.StartService(ctx, func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - // Nginx loads fast, we're too lazy to validate this properly. - time.Sleep(5 * time.Second) - return docker.NewServiceHostPort(host, port), nil - }) - if err != nil { - t.Fatalf("Could not start nginx container: %v", err) - } - - // We also need to find the network address of this node, and return - // the non-local address associated with it so that we can spawn the - // client command on the correct network/port. - networks, err := runner.GetNetworkAndAddresses(svc.Container.ID) - if err != nil { - t.Fatalf("Could not interrogate container for addresses: %v", err) - } - - var networkName string - var networkAddr string - for name, addr := range networks { - if addr == "" { - continue - } - - networkName = name - networkAddr = addr - break - } - - if networkName == "" || networkAddr == "" { - t.Fatalf("failed to get network info for containers: empty network address: %v", networks) - } - - pieces := strings.Split(svc.Config.Address(), ":") - port, _ := strconv.Atoi(pieces[1]) - return svc.Cleanup, pieces[0], port, networkName, networkAddr, 443 -} - -func buildWgetCurlContainer(t *testing.T, network string) { - containerfile := ` -FROM ubuntu:latest - -RUN apt update && DEBIAN_FRONTEND="noninteractive" apt install -y curl wget wget2 -` - - bCtx := docker.NewBuildContext() - - imageName := "vault_pki_wget_curl_integration" - imageTag := "latest" - - var err error - cwRunner, err = docker.NewServiceRunner(docker.RunOptions{ - ImageRepo: imageName, - ImageTag: imageTag, - ContainerName: "vault_pki_wget_curl", - NetworkID: network, - // We want to run sleep in the background so we're not stuck waiting - // for the default ubuntu container's shell to prompt for input. - Entrypoint: []string{"sleep", "45"}, - LogConsumer: func(s string) { - if t.Failed() { - t.Logf("container logs: %s", s) - } - }, - }) - if err != nil { - t.Fatalf("Could not provision docker service runner: %s", err) - } - - ctx := context.Background() - output, err := cwRunner.BuildImage(ctx, containerfile, bCtx, - docker.BuildRemove(true), docker.BuildForceRemove(true), - docker.BuildPullParent(true), - docker.BuildTags([]string{imageName + ":" + imageTag})) - if err != nil { - t.Fatalf("Could not build new image: %v", err) - } - - t.Logf("Image build output: %v", string(output)) -} - -func CheckWithClients(t *testing.T, network string, address string, url string, rootCert string, certificate string, privatekey string) { - // We assume the network doesn't change once assigned. - buildClientContainerOnce.Do(func() { - buildWgetCurlContainer(t, network) - builtNetwork = network - }) - - if builtNetwork != network { - t.Fatalf("failed assumption check: different built network (%v) vs run network (%v); must've changed while running tests", builtNetwork, network) - } - - // Start our service with a random name to not conflict with other - // threads. - ctx := context.Background() - result, err := cwRunner.Start(ctx, true, false) - if err != nil { - t.Fatalf("Could not start golang container for wget/curl checks: %s", err) - } - - // Commands to run after potentially writing the certificate. We - // might augment these if the certificate exists. - // - // We manually add the expected hostname to the local hosts file - // to avoid resolving it over the network and instead resolving it - // to this other container we just started (potentially in parallel - // with other containers). - hostPrimeCmd := []string{"sh", "-c", "echo '" + address + " " + uniqueHostname + "' >> /etc/hosts"} - wgetCmd := []string{"wget", "--verbose", "--ca-certificate=/root.pem", url} - curlCmd := []string{"curl", "--verbose", "--cacert", "/root.pem", url} - - certCtx := docker.NewBuildContext() - certCtx["root.pem"] = docker.PathContentsFromString(rootCert) - if certificate != "" { - // Copy the cert into the newly running container. - certCtx["client-cert.pem"] = docker.PathContentsFromString(certificate) - certCtx["client-privkey.pem"] = docker.PathContentsFromString(privatekey) - - wgetCmd = []string{"wget", "--verbose", "--ca-certificate=/root.pem", "--certificate=/client-cert.pem", "--private-key=/client-privkey.pem", url} - curlCmd = []string{"curl", "--verbose", "--cacert", "/root.pem", "--cert", "/client-cert.pem", "--key", "/client-privkey.pem", url} - } - if err := cwRunner.CopyTo(result.Container.ID, "/", certCtx); err != nil { - t.Fatalf("Could not copy certificate and key into container: %v", err) - } - - for _, cmd := range [][]string{hostPrimeCmd, wgetCmd, curlCmd} { - t.Logf("Running client connection command: %v", cmd) - - stdout, stderr, retcode, err := cwRunner.RunCmdWithOutput(ctx, result.Container.ID, cmd) - if err != nil { - t.Fatalf("Could not run command (%v) in container: %v", cmd, err) - } - - if len(stderr) != 0 { - t.Logf("Got stderr from command (%v):\n%v\n", cmd, string(stderr)) - } - - if retcode != 0 { - t.Logf("Got stdout from command (%v):\n%v\n", cmd, string(stdout)) - t.Fatalf("Got unexpected non-zero retcode from command (%v): %v\n", cmd, retcode) - } - } -} - -func CheckDeltaCRL(t *testing.T, network string, address string, url string, rootCert string, crls string) { - // We assume the network doesn't change once assigned. - buildClientContainerOnce.Do(func() { - buildWgetCurlContainer(t, network) - builtNetwork = network - }) - - if builtNetwork != network { - t.Fatalf("failed assumption check: different built network (%v) vs run network (%v); must've changed while running tests", builtNetwork, network) - } - - // Start our service with a random name to not conflict with other - // threads. - ctx := context.Background() - result, err := cwRunner.Start(ctx, true, false) - if err != nil { - t.Fatalf("Could not start golang container for wget2 delta CRL checks: %s", err) - } - - // Commands to run after potentially writing the certificate. We - // might augment these if the certificate exists. - // - // We manually add the expected hostname to the local hosts file - // to avoid resolving it over the network and instead resolving it - // to this other container we just started (potentially in parallel - // with other containers). - hostPrimeCmd := []string{"sh", "-c", "echo '" + address + " " + uniqueHostname + "' >> /etc/hosts"} - wgetCmd := []string{"wget2", "--verbose", "--ca-certificate=/root.pem", "--crl-file=/crls.pem", url} - - certCtx := docker.NewBuildContext() - certCtx["root.pem"] = docker.PathContentsFromString(rootCert) - certCtx["crls.pem"] = docker.PathContentsFromString(crls) - if err := cwRunner.CopyTo(result.Container.ID, "/", certCtx); err != nil { - t.Fatalf("Could not copy certificate and key into container: %v", err) - } - - for index, cmd := range [][]string{hostPrimeCmd, wgetCmd} { - t.Logf("Running client connection command: %v", cmd) - - stdout, stderr, retcode, err := cwRunner.RunCmdWithOutput(ctx, result.Container.ID, cmd) - if err != nil { - t.Fatalf("Could not run command (%v) in container: %v", cmd, err) - } - - if len(stderr) != 0 { - t.Logf("Got stderr from command (%v):\n%v\n", cmd, string(stderr)) - } - - if retcode != 0 && index == 0 { - t.Logf("Got stdout from command (%v):\n%v\n", cmd, string(stdout)) - t.Fatalf("Got unexpected non-zero retcode from command (%v): %v\n", cmd, retcode) - } - - if retcode == 0 && index == 1 { - t.Logf("Got stdout from command (%v):\n%v\n", cmd, string(stdout)) - t.Fatalf("Got unexpected zero retcode from command; wanted this to fail (%v): %v\n", cmd, retcode) - } - } -} - -func CheckWithGo(t *testing.T, rootCert string, clientCert string, clientChain []string, clientKey string, host string, port int, networkAddr string, networkPort int, url string, expected string, shouldFail bool) { - // Ensure we can connect with Go. - pool := x509.NewCertPool() - pool.AppendCertsFromPEM([]byte(rootCert)) - tlsConfig := &tls.Config{ - RootCAs: pool, - } - - if clientCert != "" { - var clientTLSCert tls.Certificate - clientTLSCert.Certificate = append(clientTLSCert.Certificate, parseCert(t, clientCert).Raw) - clientTLSCert.PrivateKey = parseKey(t, clientKey) - for _, cert := range clientChain { - clientTLSCert.Certificate = append(clientTLSCert.Certificate, parseCert(t, cert).Raw) - } - - tlsConfig.Certificates = append(tlsConfig.Certificates, clientTLSCert) - } - - dialer := &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - } - - transport := &http.Transport{ - TLSClientConfig: tlsConfig, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - if addr == host+":"+strconv.Itoa(port) { - // If we can't resolve our hostname, try - // accessing it via the docker protocol - // instead of via the returned service - // address. - if _, err := net.LookupHost(host); err != nil && strings.Contains(err.Error(), "no such host") { - addr = networkAddr + ":" + strconv.Itoa(networkPort) - } - } - return dialer.DialContext(ctx, network, addr) - }, - } - - client := &http.Client{Transport: transport} - clientResp, err := client.Get(url) - if err != nil { - if shouldFail { - return - } - - t.Fatalf("failed to fetch url (%v): %v", url, err) - } else if shouldFail { - if clientResp.StatusCode == 200 { - t.Fatalf("expected failure to fetch url (%v): got response: %v", url, clientResp) - } - - return - } - - defer clientResp.Body.Close() - body, err := io.ReadAll(clientResp.Body) - if err != nil { - t.Fatalf("failed to get read response body: %v", err) - } - if !strings.Contains(string(body), expected) { - t.Fatalf("expected body to contain (%v) but was:\n%v", expected, string(body)) - } -} - -func RunNginxRootTest(t *testing.T, caKeyType string, caKeyBits int, caUsePSS bool, roleKeyType string, roleKeyBits int, roleUsePSS bool) { - t.Skipf("flaky in CI") - - b, s := pki.CreateBackendWithStorage(t) - - testSuffix := fmt.Sprintf(" - %v %v %v - %v %v %v", caKeyType, caKeyType, caUsePSS, roleKeyType, roleKeyBits, roleUsePSS) - - // Configure our mount to use auto-rotate, even though we don't have - // a periodic func. - _, err := pki.CBWrite(b, s, "config/crl", map[string]interface{}{ - "auto_rebuild": true, - "enable_delta": true, - }) - - // Create a root and intermediate, setting the intermediate as default. - resp, err := pki.CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "Root X1" + testSuffix, - "country": "US", - "organization": "Dadgarcorp", - "ou": "QA", - "key_type": caKeyType, - "key_bits": caKeyBits, - "use_pss": caUsePSS, - "issuer_name": "root", - }) - requireSuccessNonNilResponse(t, resp, err, "failed to create root cert") - rootCert := resp.Data["certificate"].(string) - resp, err = pki.CBWrite(b, s, "intermediate/generate/internal", map[string]interface{}{ - "common_name": "Intermediate I1" + testSuffix, - "country": "US", - "organization": "Dadgarcorp", - "ou": "QA", - "key_type": caKeyType, - "key_bits": caKeyBits, - "use_pss": caUsePSS, - }) - requireSuccessNonNilResponse(t, resp, err, "failed to create intermediate csr") - resp, err = pki.CBWrite(b, s, "issuer/default/sign-intermediate", map[string]interface{}{ - "common_name": "Intermediate I1", - "country": "US", - "organization": "Dadgarcorp", - "ou": "QA", - "key_type": caKeyType, - "csr": resp.Data["csr"], - }) - requireSuccessNonNilResponse(t, resp, err, "failed to sign intermediate csr") - intCert := resp.Data["certificate"].(string) - resp, err = pki.CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ - "pem_bundle": intCert, - }) - requireSuccessNonNilResponse(t, resp, err, "failed to sign intermediate csr") - _, err = pki.CBWrite(b, s, "config/issuers", map[string]interface{}{ - "default": resp.Data["imported_issuers"].([]string)[0], - }) - - // Create a role+certificate valid for localhost only. - _, err = pki.CBWrite(b, s, "roles/testing", map[string]interface{}{ - "allow_any_name": true, - "key_type": roleKeyType, - "key_bits": roleKeyBits, - "use_pss": roleUsePSS, - "ttl": "60m", - }) - require.NoError(t, err) - resp, err = pki.CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": uniqueHostname, - "ip_sans": "127.0.0.1,::1", - "sans": uniqueHostname + ",localhost,localhost4,localhost6,localhost.localdomain", - }) - requireSuccessNonNilResponse(t, resp, err, "failed to create server leaf cert") - leafCert := resp.Data["certificate"].(string) - leafPrivateKey := resp.Data["private_key"].(string) + "\n" - fullChain := leafCert + "\n" - for _, cert := range resp.Data["ca_chain"].([]string) { - fullChain += cert + "\n" - } - - // Issue a client leaf certificate. - resp, err = pki.CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "testing.client.dadgarcorp.com", - }) - requireSuccessNonNilResponse(t, resp, err, "failed to create client leaf cert") - clientCert := resp.Data["certificate"].(string) - clientKey := resp.Data["private_key"].(string) + "\n" - clientWireChain := clientCert + "\n" + resp.Data["issuing_ca"].(string) + "\n" - clientTrustChain := resp.Data["issuing_ca"].(string) + "\n" + rootCert + "\n" - clientCAChain := resp.Data["ca_chain"].([]string) - - // Issue a client leaf cert and revoke it, placing it on the main CRL - // via rotation. - resp, err = pki.CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "revoked-crl.client.dadgarcorp.com", - }) - requireSuccessNonNilResponse(t, resp, err, "failed to create revoked client leaf cert") - revokedCert := resp.Data["certificate"].(string) - revokedKey := resp.Data["private_key"].(string) + "\n" - // revokedFullChain := revokedCert + "\n" + resp.Data["issuing_ca"].(string) + "\n" - // revokedTrustChain := resp.Data["issuing_ca"].(string) + "\n" + rootCert + "\n" - revokedCAChain := resp.Data["ca_chain"].([]string) - _, err = pki.CBWrite(b, s, "revoke", map[string]interface{}{ - "certificate": revokedCert, - }) - require.NoError(t, err) - _, err = pki.CBRead(b, s, "crl/rotate") - require.NoError(t, err) - - // Issue a client leaf cert and revoke it, placing it on the delta CRL - // via rotation. - /*resp, err = pki.CBWrite(b, s, "issue/testing", map[string]interface{}{ - "common_name": "revoked-delta-crl.client.dadgarcorp.com", - }) - requireSuccessNonNilResponse(t, resp, err, "failed to create delta CRL revoked client leaf cert") - deltaCert := resp.Data["certificate"].(string) - deltaKey := resp.Data["private_key"].(string) + "\n" - //deltaFullChain := deltaCert + "\n" + resp.Data["issuing_ca"].(string) + "\n" - //deltaTrustChain := resp.Data["issuing_ca"].(string) + "\n" + rootCert + "\n" - deltaCAChain := resp.Data["ca_chain"].([]string) - _, err = pki.CBWrite(b, s, "revoke", map[string]interface{}{ - "certificate": deltaCert, - }) - require.NoError(t, err) - _, err = pki.CBRead(b, s, "crl/rotate-delta") - require.NoError(t, err)*/ - - // Get the CRL and Delta CRLs. - resp, err = pki.CBRead(b, s, "issuer/root/crl") - require.NoError(t, err) - rootCRL := resp.Data["crl"].(string) + "\n" - resp, err = pki.CBRead(b, s, "issuer/default/crl") - require.NoError(t, err) - intCRL := resp.Data["crl"].(string) + "\n" - - // No need to fetch root Delta CRL as we've not revoked anything on it. - resp, err = pki.CBRead(b, s, "issuer/default/crl/delta") - require.NoError(t, err) - deltaCRL := resp.Data["crl"].(string) + "\n" - - crls := rootCRL + intCRL + deltaCRL - - cleanup, host, port, networkName, networkAddr, networkPort := buildNginxContainer(t, rootCert, crls, fullChain, leafPrivateKey) - defer cleanup() - - if host != "127.0.0.1" && host != "::1" && strings.HasPrefix(host, containerName) { - t.Logf("Assuming %v:%v is a container name rather than localhost reference.", host, port) - host = uniqueHostname - port = networkPort - } - - localBase := "https://" + host + ":" + strconv.Itoa(port) - localURL := localBase + "/index.html" - localProtectedURL := localBase + "/protected.html" - containerBase := "https://" + uniqueHostname + ":" + strconv.Itoa(networkPort) - containerURL := containerBase + "/index.html" - containerProtectedURL := containerBase + "/protected.html" - - t.Logf("Spawned nginx container:\nhost: %v\nport: %v\nnetworkName: %v\nnetworkAddr: %v\nnetworkPort: %v\nlocalURL: %v\ncontainerURL: %v\n", host, port, networkName, networkAddr, networkPort, localBase, containerBase) - - // Ensure we can connect with Go. We do our checks for revocation here, - // as this behavior is server-controlled and shouldn't matter based on - // client type. - CheckWithGo(t, rootCert, "", nil, "", host, port, networkAddr, networkPort, localURL, unprotectedFile, false) - CheckWithGo(t, rootCert, "", nil, "", host, port, networkAddr, networkPort, localProtectedURL, failureIndicator, true) - CheckWithGo(t, rootCert, clientCert, clientCAChain, clientKey, host, port, networkAddr, networkPort, localProtectedURL, protectedFile, false) - CheckWithGo(t, rootCert, revokedCert, revokedCAChain, revokedKey, host, port, networkAddr, networkPort, localProtectedURL, protectedFile, true) - // CheckWithGo(t, rootCert, deltaCert, deltaCAChain, deltaKey, host, port, networkAddr, networkPort, localProtectedURL, protectedFile, true) - - // Ensure we can connect with wget/curl. - CheckWithClients(t, networkName, networkAddr, containerURL, rootCert, "", "") - CheckWithClients(t, networkName, networkAddr, containerProtectedURL, clientTrustChain, clientWireChain, clientKey) - - // Ensure OpenSSL will validate the delta CRL by revoking our server leaf - // and then using it with wget2. This will land on the intermediate's - // Delta CRL. - _, err = pki.CBWrite(b, s, "revoke", map[string]interface{}{ - "certificate": leafCert, - }) - require.NoError(t, err) - _, err = pki.CBRead(b, s, "crl/rotate-delta") - require.NoError(t, err) - resp, err = pki.CBRead(b, s, "issuer/default/crl/delta") - require.NoError(t, err) - deltaCRL = resp.Data["crl"].(string) + "\n" - crls = rootCRL + intCRL + deltaCRL - - CheckDeltaCRL(t, networkName, networkAddr, containerURL, rootCert, crls) -} - -func Test_NginxRSAPure(t *testing.T) { - t.Parallel() - RunNginxRootTest(t, "rsa", 2048, false, "rsa", 2048, false) -} - -func Test_NginxRSAPurePSS(t *testing.T) { - t.Parallel() - RunNginxRootTest(t, "rsa", 2048, false, "rsa", 2048, true) -} - -func Test_NginxRSAPSSPure(t *testing.T) { - t.Parallel() - RunNginxRootTest(t, "rsa", 2048, true, "rsa", 2048, false) -} - -func Test_NginxRSAPSSPurePSS(t *testing.T) { - t.Parallel() - RunNginxRootTest(t, "rsa", 2048, true, "rsa", 2048, true) -} - -func Test_NginxECDSA256Pure(t *testing.T) { - t.Parallel() - RunNginxRootTest(t, "ec", 256, false, "ec", 256, false) -} - -func Test_NginxECDSAHybrid(t *testing.T) { - t.Parallel() - RunNginxRootTest(t, "ec", 256, false, "rsa", 2048, false) -} - -func Test_NginxECDSAHybridPSS(t *testing.T) { - t.Parallel() - RunNginxRootTest(t, "ec", 256, false, "rsa", 2048, true) -} - -func Test_NginxRSAHybrid(t *testing.T) { - t.Parallel() - RunNginxRootTest(t, "rsa", 2048, false, "ec", 256, false) -} - -func Test_NginxRSAPSSHybrid(t *testing.T) { - t.Parallel() - RunNginxRootTest(t, "rsa", 2048, true, "ec", 256, false) -} diff --git a/builtin/logical/pkiext/pkiext_binary/acme_test.go b/builtin/logical/pkiext/pkiext_binary/acme_test.go deleted file mode 100644 index e1a1d9a18..000000000 --- a/builtin/logical/pkiext/pkiext_binary/acme_test.go +++ /dev/null @@ -1,1102 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pkiext_binary - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - _ "embed" - "encoding/hex" - "errors" - "fmt" - "html/template" - "net" - "net/http" - "path" - "strings" - "testing" - "time" - - "golang.org/x/crypto/acme" - - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/builtin/logical/pkiext" - "github.com/hashicorp/vault/helper/testhelpers" - "github.com/hashicorp/vault/sdk/helper/certutil" - hDocker "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/stretchr/testify/require" -) - -//go:embed testdata/caddy_http.json -var caddyConfigTemplateHTTP string - -//go:embed testdata/caddy_http_eab.json -var caddyConfigTemplateHTTPEAB string - -//go:embed testdata/caddy_tls_alpn.json -var caddyConfigTemplateTLSALPN string - -// Test_ACME will start a Vault cluster using the docker based binary, and execute -// a bunch of sub-tests against that cluster. It is up to each sub-test to run/configure -// a new pki mount within the cluster to not interfere with each other. -func Test_ACME(t *testing.T) { - cluster := NewVaultPkiClusterWithDNS(t) - defer cluster.Cleanup() - - tc := map[string]func(t *testing.T, cluster *VaultPkiCluster){ - "caddy http": SubtestACMECaddy(caddyConfigTemplateHTTP, false), - "caddy http eab": SubtestACMECaddy(caddyConfigTemplateHTTPEAB, true), - "caddy tls-alpn": SubtestACMECaddy(caddyConfigTemplateTLSALPN, false), - "certbot": SubtestACMECertbot, - "certbot eab": SubtestACMECertbotEab, - "acme ip sans": SubtestACMEIPAndDNS, - "acme wildcard": SubtestACMEWildcardDNS, - "acme prevents ica": SubtestACMEPreventsICADNS, - } - - // Wrap the tests within an outer group, so that we run all tests - // in parallel, but still wait for all tests to finish before completing - // and running the cleanup of the Vault cluster. - t.Run("group", func(gt *testing.T) { - for testName := range tc { - // Trap the function to be embedded later in the run so it - // doesn't get clobbered on the next for iteration - testFunc := tc[testName] - - gt.Run(testName, func(st *testing.T) { - st.Parallel() - testFunc(st, cluster) - }) - } - }) - - // Do not run these tests in parallel. - t.Run("step down", func(gt *testing.T) { SubtestACMEStepDownNode(gt, cluster) }) -} - -// caddyConfig contains information used to render a Caddy configuration file from a template. -type caddyConfig struct { - Hostname string - Directory string - CACert string - EABID string - EABKey string -} - -// SubtestACMECaddy returns an ACME test for Caddy using the provided template. -func SubtestACMECaddy(configTemplate string, enableEAB bool) func(*testing.T, *VaultPkiCluster) { - return func(t *testing.T, cluster *VaultPkiCluster) { - ctx := context.Background() - - // Roll a random run ID for mount and hostname uniqueness. - runID, err := uuid.GenerateUUID() - require.NoError(t, err, "failed to generate a unique ID for test run") - runID = strings.Split(runID, "-")[0] - - // Create the PKI mount with ACME enabled - pki, err := cluster.CreateAcmeMount(runID) - require.NoError(t, err, "failed to set up ACME mount") - - // Conditionally enable EAB and retrieve the key. - var eabID, eabKey string - if enableEAB { - err = pki.UpdateAcmeConfig(true, map[string]interface{}{ - "eab_policy": "new-account-required", - }) - require.NoError(t, err, "failed to configure EAB policy in PKI mount") - - eabID, eabKey, err = pki.GetEabKey("acme/") - require.NoError(t, err, "failed to retrieve EAB key from PKI mount") - } - - directory := fmt.Sprintf("https://%s:8200/v1/%s/acme/directory", pki.GetActiveContainerIP(), runID) - vaultNetwork := pki.GetContainerNetworkName() - t.Logf("dir: %s", directory) - - logConsumer, logStdout, logStderr := getDockerLog(t) - - sleepTimer := "45" - - // Kick off Caddy container. - t.Logf("creating on network: %v", vaultNetwork) - caddyRunner, err := hDocker.NewServiceRunner(hDocker.RunOptions{ - ImageRepo: "docker.mirror.hashicorp.services/library/caddy", - ImageTag: "2.6.4", - ContainerName: fmt.Sprintf("caddy_test_%s", runID), - NetworkName: vaultNetwork, - Ports: []string{"80/tcp", "443/tcp", "443/udp"}, - Entrypoint: []string{"sleep", sleepTimer}, - LogConsumer: logConsumer, - LogStdout: logStdout, - LogStderr: logStderr, - }) - require.NoError(t, err, "failed creating caddy service runner") - - caddyResult, err := caddyRunner.Start(ctx, true, false) - require.NoError(t, err, "could not start Caddy container") - require.NotNil(t, caddyResult, "could not start Caddy container") - - defer caddyRunner.Stop(ctx, caddyResult.Container.ID) - - networks, err := caddyRunner.GetNetworkAndAddresses(caddyResult.Container.ID) - require.NoError(t, err, "could not read caddy container's IP address") - require.Contains(t, networks, vaultNetwork, "expected to contain vault network") - - ipAddr := networks[vaultNetwork] - hostname := fmt.Sprintf("%s.dadgarcorp.com", runID) - - err = pki.AddHostname(hostname, ipAddr) - require.NoError(t, err, "failed to update vault host files") - - // Render the Caddy configuration from the specified template. - tmpl, err := template.New("config").Parse(configTemplate) - require.NoError(t, err, "failed to parse Caddy config template") - var b strings.Builder - err = tmpl.Execute( - &b, - caddyConfig{ - Hostname: hostname, - Directory: directory, - CACert: "/tmp/vault_ca_cert.crt", - EABID: eabID, - EABKey: eabKey, - }, - ) - require.NoError(t, err, "failed to render Caddy config template") - - // Push the Caddy config and the cluster listener's CA certificate over to the docker container. - cpCtx := hDocker.NewBuildContext() - cpCtx["caddy_config.json"] = hDocker.PathContentsFromString(b.String()) - cpCtx["vault_ca_cert.crt"] = hDocker.PathContentsFromString(string(cluster.GetListenerCACertPEM())) - err = caddyRunner.CopyTo(caddyResult.Container.ID, "/tmp/", cpCtx) - require.NoError(t, err, "failed to copy Caddy config and Vault listener CA certificate to container") - - // Start the Caddy server. - caddyCmd := []string{ - "caddy", - "start", - "--config", "/tmp/caddy_config.json", - } - stdout, stderr, retcode, err := caddyRunner.RunCmdWithOutput(ctx, caddyResult.Container.ID, caddyCmd) - t.Logf("Caddy Start Command: %v\nstdout: %v\nstderr: %v\n", caddyCmd, string(stdout), string(stderr)) - require.NoError(t, err, "got error running Caddy start command") - require.Equal(t, 0, retcode, "expected zero retcode Caddy start command result") - - // Start a cURL container. - curlRunner, err := hDocker.NewServiceRunner(hDocker.RunOptions{ - ImageRepo: "docker.mirror.hashicorp.services/curlimages/curl", - ImageTag: "8.4.0", - ContainerName: fmt.Sprintf("curl_test_%s", runID), - NetworkName: vaultNetwork, - Entrypoint: []string{"sleep", sleepTimer}, - LogConsumer: logConsumer, - LogStdout: logStdout, - LogStderr: logStderr, - }) - require.NoError(t, err, "failed creating cURL service runner") - - curlResult, err := curlRunner.Start(ctx, true, false) - require.NoError(t, err, "could not start cURL container") - require.NotNil(t, curlResult, "could not start cURL container") - - // Retrieve the PKI mount CA cert and copy it over to the cURL container. - mountCACert, err := pki.GetCACertPEM() - require.NoError(t, err, "failed to retrieve PKI mount CA certificate") - - mountCACertCtx := hDocker.NewBuildContext() - mountCACertCtx["ca_cert.crt"] = hDocker.PathContentsFromString(mountCACert) - err = curlRunner.CopyTo(curlResult.Container.ID, "/tmp/", mountCACertCtx) - require.NoError(t, err, "failed to copy PKI mount CA certificate to cURL container") - - // Use cURL to hit the Caddy server and validate that a certificate was retrieved successfully. - curlCmd := []string{ - "curl", - "-L", - "--cacert", "/tmp/ca_cert.crt", - "--resolve", hostname + ":443:" + ipAddr, - "https://" + hostname + "/", - } - stdout, stderr, retcode, err = curlRunner.RunCmdWithOutput(ctx, curlResult.Container.ID, curlCmd) - t.Logf("cURL Command: %v\nstdout: %v\nstderr: %v\n", curlCmd, string(stdout), string(stderr)) - require.NoError(t, err, "got error running cURL command") - require.Equal(t, 0, retcode, "expected zero retcode cURL command result") - } -} - -func SubtestACMECertbot(t *testing.T, cluster *VaultPkiCluster) { - pki, err := cluster.CreateAcmeMount("pki") - require.NoError(t, err, "failed setting up acme mount") - - directory := "https://" + pki.GetActiveContainerIP() + ":8200/v1/pki/acme/directory" - vaultNetwork := pki.GetContainerNetworkName() - - logConsumer, logStdout, logStderr := getDockerLog(t) - - // Default to 45 second timeout, but bump to 120 when running locally or if nightly regression - // flag is provided. - sleepTimer := "45" - if testhelpers.IsLocalOrRegressionTests() { - sleepTimer = "120" - } - - t.Logf("creating on network: %v", vaultNetwork) - runner, err := hDocker.NewServiceRunner(hDocker.RunOptions{ - ImageRepo: "docker.mirror.hashicorp.services/certbot/certbot", - ImageTag: "latest", - ContainerName: "vault_pki_certbot_test", - NetworkName: vaultNetwork, - Entrypoint: []string{"sleep", sleepTimer}, - LogConsumer: logConsumer, - LogStdout: logStdout, - LogStderr: logStderr, - }) - require.NoError(t, err, "failed creating service runner") - - ctx := context.Background() - result, err := runner.Start(ctx, true, false) - require.NoError(t, err, "could not start container") - require.NotNil(t, result, "could not start container") - - defer runner.Stop(context.Background(), result.Container.ID) - - networks, err := runner.GetNetworkAndAddresses(result.Container.ID) - require.NoError(t, err, "could not read container's IP address") - require.Contains(t, networks, vaultNetwork, "expected to contain vault network") - - ipAddr := networks[vaultNetwork] - hostname := "certbot-acme-client.dadgarcorp.com" - - err = pki.AddHostname(hostname, ipAddr) - require.NoError(t, err, "failed to update vault host files") - - // Sinkhole a domain that's invalid just in case it's registered in the future. - cluster.Dns.AddDomain("armoncorp.com") - cluster.Dns.AddRecord("armoncorp.com", "A", "127.0.0.1") - - certbotCmd := []string{ - "certbot", - "certonly", - "--no-eff-email", - "--email", "certbot.client@dadgarcorp.com", - "--agree-tos", - "--no-verify-ssl", - "--standalone", - "--non-interactive", - "--server", directory, - "-d", hostname, - } - logCatCmd := []string{"cat", "/var/log/letsencrypt/letsencrypt.log"} - - stdout, stderr, retcode, err := runner.RunCmdWithOutput(ctx, result.Container.ID, certbotCmd) - t.Logf("Certbot Issue Command: %v\nstdout: %v\nstderr: %v\n", certbotCmd, string(stdout), string(stderr)) - if err != nil || retcode != 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - require.NoError(t, err, "got error running issue command") - require.Equal(t, 0, retcode, "expected zero retcode issue command result") - - // N.B. We're using the `certonly` subcommand here because it seems as though the `renew` command - // attempts to install the cert for you. This ends up hanging and getting killed by docker, but is - // also not desired behavior. The certbot docs suggest using `certonly` to renew as seen here: - // https://eff-certbot.readthedocs.io/en/stable/using.html#renewing-certificates - certbotRenewCmd := []string{ - "certbot", - "certonly", - "--no-eff-email", - "--email", "certbot.client@dadgarcorp.com", - "--agree-tos", - "--no-verify-ssl", - "--standalone", - "--non-interactive", - "--server", directory, - "-d", hostname, - "--cert-name", hostname, - "--force-renewal", - } - - stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotRenewCmd) - t.Logf("Certbot Renew Command: %v\nstdout: %v\nstderr: %v\n", certbotRenewCmd, string(stdout), string(stderr)) - if err != nil || retcode != 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - require.NoError(t, err, "got error running renew command") - require.Equal(t, 0, retcode, "expected zero retcode renew command result") - - certbotRevokeCmd := []string{ - "certbot", - "revoke", - "--no-eff-email", - "--email", "certbot.client@dadgarcorp.com", - "--agree-tos", - "--no-verify-ssl", - "--non-interactive", - "--no-delete-after-revoke", - "--cert-name", hostname, - } - - stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotRevokeCmd) - t.Logf("Certbot Revoke Command: %v\nstdout: %v\nstderr: %v\n", certbotRevokeCmd, string(stdout), string(stderr)) - if err != nil || retcode != 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - require.NoError(t, err, "got error running revoke command") - require.Equal(t, 0, retcode, "expected zero retcode revoke command result") - - // Revoking twice should fail. - stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotRevokeCmd) - t.Logf("Certbot Double Revoke Command: %v\nstdout: %v\nstderr: %v\n", certbotRevokeCmd, string(stdout), string(stderr)) - if err != nil || retcode == 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - - require.NoError(t, err, "got error running double revoke command") - require.NotEqual(t, 0, retcode, "expected non-zero retcode double revoke command result") - - // Attempt to issue against a domain that doesn't match the challenge. - // N.B. This test only runs locally or when the nightly regression env var is provided to CI. - if testhelpers.IsLocalOrRegressionTests() { - certbotInvalidIssueCmd := []string{ - "certbot", - "certonly", - "--no-eff-email", - "--email", "certbot.client@dadgarcorp.com", - "--agree-tos", - "--no-verify-ssl", - "--standalone", - "--non-interactive", - "--server", directory, - "-d", "armoncorp.com", - "--issuance-timeout", "10", - } - - stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotInvalidIssueCmd) - t.Logf("Certbot Invalid Issue Command: %v\nstdout: %v\nstderr: %v\n", certbotInvalidIssueCmd, string(stdout), string(stderr)) - if err != nil || retcode != 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - require.NoError(t, err, "got error running issue command") - require.NotEqual(t, 0, retcode, "expected non-zero retcode issue command result") - } - - // Attempt to close out our ACME account - certbotUnregisterCmd := []string{ - "certbot", - "unregister", - "--no-verify-ssl", - "--non-interactive", - "--server", directory, - } - - stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotUnregisterCmd) - t.Logf("Certbot Unregister Command: %v\nstdout: %v\nstderr: %v\n", certbotUnregisterCmd, string(stdout), string(stderr)) - if err != nil || retcode != 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - require.NoError(t, err, "got error running unregister command") - require.Equal(t, 0, retcode, "expected zero retcode unregister command result") - - // Attempting to close out our ACME account twice should fail - stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotUnregisterCmd) - t.Logf("Certbot double Unregister Command: %v\nstdout: %v\nstderr: %v\n", certbotUnregisterCmd, string(stdout), string(stderr)) - if err != nil || retcode != 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot double logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - require.NoError(t, err, "got error running double unregister command") - require.Equal(t, 1, retcode, "expected non-zero retcode double unregister command result") -} - -func SubtestACMECertbotEab(t *testing.T, cluster *VaultPkiCluster) { - mountName := "pki-certbot-eab" - pki, err := cluster.CreateAcmeMount(mountName) - require.NoError(t, err, "failed setting up acme mount") - - err = pki.UpdateAcmeConfig(true, map[string]interface{}{ - "eab_policy": "new-account-required", - }) - require.NoError(t, err) - - eabId, base64EabKey, err := pki.GetEabKey("acme/") - - directory := "https://" + pki.GetActiveContainerIP() + ":8200/v1/" + mountName + "/acme/directory" - vaultNetwork := pki.GetContainerNetworkName() - - logConsumer, logStdout, logStderr := getDockerLog(t) - - t.Logf("creating on network: %v", vaultNetwork) - runner, err := hDocker.NewServiceRunner(hDocker.RunOptions{ - ImageRepo: "docker.mirror.hashicorp.services/certbot/certbot", - ImageTag: "latest", - ContainerName: "vault_pki_certbot_eab_test", - NetworkName: vaultNetwork, - Entrypoint: []string{"sleep", "45"}, - LogConsumer: logConsumer, - LogStdout: logStdout, - LogStderr: logStderr, - }) - require.NoError(t, err, "failed creating service runner") - - ctx := context.Background() - result, err := runner.Start(ctx, true, false) - require.NoError(t, err, "could not start container") - require.NotNil(t, result, "could not start container") - - defer runner.Stop(context.Background(), result.Container.ID) - - networks, err := runner.GetNetworkAndAddresses(result.Container.ID) - require.NoError(t, err, "could not read container's IP address") - require.Contains(t, networks, vaultNetwork, "expected to contain vault network") - - ipAddr := networks[vaultNetwork] - hostname := "certbot-eab-acme-client.dadgarcorp.com" - - err = pki.AddHostname(hostname, ipAddr) - require.NoError(t, err, "failed to update vault host files") - - certbotCmd := []string{ - "certbot", - "certonly", - "--no-eff-email", - "--email", "certbot.client@dadgarcorp.com", - "--eab-kid", eabId, - "--eab-hmac-key='" + base64EabKey + "'", - "--agree-tos", - "--no-verify-ssl", - "--standalone", - "--non-interactive", - "--server", directory, - "-d", hostname, - } - logCatCmd := []string{"cat", "/var/log/letsencrypt/letsencrypt.log"} - - stdout, stderr, retcode, err := runner.RunCmdWithOutput(ctx, result.Container.ID, certbotCmd) - t.Logf("Certbot Issue Command: %v\nstdout: %v\nstderr: %v\n", certbotCmd, string(stdout), string(stderr)) - if err != nil || retcode != 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - require.NoError(t, err, "got error running issue command") - require.Equal(t, 0, retcode, "expected zero retcode issue command result") - - certbotRenewCmd := []string{ - "certbot", - "certonly", - "--no-eff-email", - "--email", "certbot.client@dadgarcorp.com", - "--agree-tos", - "--no-verify-ssl", - "--standalone", - "--non-interactive", - "--server", directory, - "-d", hostname, - "--cert-name", hostname, - "--force-renewal", - } - - stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotRenewCmd) - t.Logf("Certbot Renew Command: %v\nstdout: %v\nstderr: %v\n", certbotRenewCmd, string(stdout), string(stderr)) - if err != nil || retcode != 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - require.NoError(t, err, "got error running renew command") - require.Equal(t, 0, retcode, "expected zero retcode renew command result") - - certbotRevokeCmd := []string{ - "certbot", - "revoke", - "--no-eff-email", - "--email", "certbot.client@dadgarcorp.com", - "--agree-tos", - "--no-verify-ssl", - "--non-interactive", - "--no-delete-after-revoke", - "--cert-name", hostname, - } - - stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotRevokeCmd) - t.Logf("Certbot Revoke Command: %v\nstdout: %v\nstderr: %v\n", certbotRevokeCmd, string(stdout), string(stderr)) - if err != nil || retcode != 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - require.NoError(t, err, "got error running revoke command") - require.Equal(t, 0, retcode, "expected zero retcode revoke command result") - - // Revoking twice should fail. - stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotRevokeCmd) - t.Logf("Certbot Double Revoke Command: %v\nstdout: %v\nstderr: %v\n", certbotRevokeCmd, string(stdout), string(stderr)) - if err != nil || retcode == 0 { - logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd) - t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr)) - } - - require.NoError(t, err, "got error running double revoke command") - require.NotEqual(t, 0, retcode, "expected non-zero retcode double revoke command result") -} - -func SubtestACMEIPAndDNS(t *testing.T, cluster *VaultPkiCluster) { - pki, err := cluster.CreateAcmeMount("pki-ip-dns-sans") - require.NoError(t, err, "failed setting up acme mount") - - // Since we interact with ACME from outside the container network the ACME - // configuration needs to be updated to use the host port and not the internal - // docker ip. - basePath, err := pki.UpdateClusterConfigLocalAddr() - require.NoError(t, err, "failed updating cluster config") - - logConsumer, logStdout, logStderr := getDockerLog(t) - - // Setup an nginx container that we can have respond the queries for ips - runner, err := hDocker.NewServiceRunner(hDocker.RunOptions{ - ImageRepo: "docker.mirror.hashicorp.services/nginx", - ImageTag: "latest", - ContainerName: "vault_pki_ipsans_test", - NetworkName: pki.GetContainerNetworkName(), - LogConsumer: logConsumer, - LogStdout: logStdout, - LogStderr: logStderr, - }) - require.NoError(t, err, "failed creating service runner") - - ctx := context.Background() - result, err := runner.Start(ctx, true, false) - require.NoError(t, err, "could not start container") - require.NotNil(t, result, "could not start container") - - nginxContainerId := result.Container.ID - defer runner.Stop(context.Background(), nginxContainerId) - networks, err := runner.GetNetworkAndAddresses(nginxContainerId) - - challengeFolder := "/usr/share/nginx/html/.well-known/acme-challenge/" - createChallengeFolderCmd := []string{ - "sh", "-c", - "mkdir -p '" + challengeFolder + "'", - } - stdout, stderr, retcode, err := runner.RunCmdWithOutput(ctx, nginxContainerId, createChallengeFolderCmd) - require.NoError(t, err, "failed to create folder in nginx container") - t.Logf("Update host file command: %v\nstdout: %v\nstderr: %v", createChallengeFolderCmd, string(stdout), string(stderr)) - require.Equal(t, 0, retcode, "expected zero retcode from mkdir in nginx container") - - ipAddr := networks[pki.GetContainerNetworkName()] - hostname := "go-lang-acme-client.dadgarcorp.com" - - err = pki.AddHostname(hostname, ipAddr) - require.NoError(t, err, "failed to update vault host files") - - // Perform an ACME lifecycle with an order that contains both an IP and a DNS name identifier - err = pki.UpdateRole("ip-dns-sans", map[string]interface{}{ - "key_type": "any", - "allowed_domains": "dadgarcorp.com", - "allow_subdomains": true, - "allow_wildcard_certificates": false, - }) - require.NoError(t, err, "failed creating role ip-dns-sans") - - directoryUrl := basePath + "/roles/ip-dns-sans/acme/directory" - acmeOrderIdentifiers := []acme.AuthzID{ - {Type: "ip", Value: ipAddr}, - {Type: "dns", Value: hostname}, - } - cr := &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: hostname}, - DNSNames: []string{hostname}, - IPAddresses: []net.IP{net.ParseIP(ipAddr)}, - } - - provisioningFunc := func(acmeClient *acme.Client, auths []*acme.Authorization) []*acme.Challenge { - // For each http-01 challenge, generate the file to place underneath the nginx challenge folder - acmeCtx := hDocker.NewBuildContext() - var challengesToAccept []*acme.Challenge - for _, auth := range auths { - for _, challenge := range auth.Challenges { - if challenge.Status != acme.StatusPending { - t.Logf("ignoring challenge not in status pending: %v", challenge) - continue - } - - if challenge.Type == "http-01" { - challengeBody, err := acmeClient.HTTP01ChallengeResponse(challenge.Token) - require.NoError(t, err, "failed generating challenge response") - - challengePath := acmeClient.HTTP01ChallengePath(challenge.Token) - require.NoError(t, err, "failed generating challenge path") - - challengeFile := path.Base(challengePath) - - acmeCtx[challengeFile] = hDocker.PathContentsFromString(challengeBody) - - challengesToAccept = append(challengesToAccept, challenge) - } - } - } - - require.GreaterOrEqual(t, len(challengesToAccept), 1, "Need at least one challenge, got none") - - // Copy all challenges within the nginx container - err = runner.CopyTo(nginxContainerId, challengeFolder, acmeCtx) - require.NoError(t, err, "failed copying challenges to container") - - return challengesToAccept - } - - acmeCert := doAcmeValidationWithGoLibrary(t, directoryUrl, acmeOrderIdentifiers, cr, provisioningFunc, "") - - require.Len(t, acmeCert.IPAddresses, 1, "expected only a single ip address in cert") - require.Equal(t, ipAddr, acmeCert.IPAddresses[0].String()) - require.Equal(t, []string{hostname}, acmeCert.DNSNames) - require.Equal(t, hostname, acmeCert.Subject.CommonName) - - // Perform an ACME lifecycle with an order that contains just an IP identifier - err = pki.UpdateRole("ip-sans", map[string]interface{}{ - "key_type": "any", - "use_csr_common_name": false, - "require_cn": false, - "client_flag": false, - }) - require.NoError(t, err, "failed creating role ip-sans") - - directoryUrl = basePath + "/roles/ip-sans/acme/directory" - acmeOrderIdentifiers = []acme.AuthzID{ - {Type: "ip", Value: ipAddr}, - } - cr = &x509.CertificateRequest{ - IPAddresses: []net.IP{net.ParseIP(ipAddr)}, - } - - acmeCert = doAcmeValidationWithGoLibrary(t, directoryUrl, acmeOrderIdentifiers, cr, provisioningFunc, "") - - require.Len(t, acmeCert.IPAddresses, 1, "expected only a single ip address in cert") - require.Equal(t, ipAddr, acmeCert.IPAddresses[0].String()) - require.Empty(t, acmeCert.DNSNames, "acme cert dns name field should have been empty") - require.Equal(t, "", acmeCert.Subject.CommonName) -} - -type acmeGoValidatorProvisionerFunc func(acmeClient *acme.Client, auths []*acme.Authorization) []*acme.Challenge - -func doAcmeValidationWithGoLibrary(t *testing.T, directoryUrl string, acmeOrderIdentifiers []acme.AuthzID, cr *x509.CertificateRequest, provisioningFunc acmeGoValidatorProvisionerFunc, expectedFailure string) *x509.Certificate { - // Since we are contacting Vault through the host ip/port, the certificate will not validate properly - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - httpClient := &http.Client{Transport: tr} - - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa account key") - - t.Logf("Using the following url for the ACME directory: %s", directoryUrl) - acmeClient := &acme.Client{ - Key: accountKey, - HTTPClient: httpClient, - DirectoryURL: directoryUrl, - } - - testCtx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancelFunc() - - // Create new account - _, err = acmeClient.Register(testCtx, &acme.Account{Contact: []string{"mailto:ipsans@dadgarcorp.com"}}, - func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - - // Create an ACME order - order, err := acmeClient.AuthorizeOrder(testCtx, acmeOrderIdentifiers) - require.NoError(t, err, "failed creating ACME order") - - var auths []*acme.Authorization - for _, authUrl := range order.AuthzURLs { - authorization, err := acmeClient.GetAuthorization(testCtx, authUrl) - require.NoError(t, err, "failed to lookup authorization at url: %s", authUrl) - auths = append(auths, authorization) - } - - // Handle the validation using the external validation mechanism. - challengesToAccept := provisioningFunc(acmeClient, auths) - require.NotEmpty(t, challengesToAccept, "provisioning function failed to return any challenges to accept") - - // Tell the ACME server, that they can now validate those challenges. - for _, challenge := range challengesToAccept { - _, err = acmeClient.Accept(testCtx, challenge) - require.NoError(t, err, "failed to accept challenge: %v", challenge) - } - - // Wait for the order/challenges to be validated. - _, err = acmeClient.WaitOrder(testCtx, order.URI) - require.NoError(t, err, "failed waiting for order to be ready") - - // Create/sign the CSR and ask ACME server to sign it returning us the final certificate - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - csr, err := x509.CreateCertificateRequest(rand.Reader, cr, csrKey) - require.NoError(t, err, "failed generating csr") - - t.Logf("[TEST-LOG] Created CSR: %v", hex.EncodeToString(csr)) - - certs, _, err := acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, false) - if err != nil { - if expectedFailure != "" { - require.Contains(t, err.Error(), expectedFailure, "got a unexpected failure not matching expected value") - return nil - } - - require.NoError(t, err, "failed to get a certificate back from ACME") - } else if expectedFailure != "" { - t.Fatalf("expected failure containing: %s got none", expectedFailure) - } - - acmeCert, err := x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert bytes") - - return acmeCert -} - -func SubtestACMEWildcardDNS(t *testing.T, cluster *VaultPkiCluster) { - pki, err := cluster.CreateAcmeMount("pki-dns-wildcards") - require.NoError(t, err, "failed setting up acme mount") - - // Since we interact with ACME from outside the container network the ACME - // configuration needs to be updated to use the host port and not the internal - // docker ip. - basePath, err := pki.UpdateClusterConfigLocalAddr() - require.NoError(t, err, "failed updating cluster config") - - hostname := "go-lang-wildcard-client.dadgarcorp.com" - wildcard := "*." + hostname - - // Do validation without a role first. - directoryUrl := basePath + "/acme/directory" - acmeOrderIdentifiers := []acme.AuthzID{ - {Type: "dns", Value: hostname}, - {Type: "dns", Value: wildcard}, - } - cr := &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: wildcard}, - DNSNames: []string{hostname, wildcard}, - } - - provisioningFunc := func(acmeClient *acme.Client, auths []*acme.Authorization) []*acme.Challenge { - // For each dns-01 challenge, place the record in the associated DNS resolver. - var challengesToAccept []*acme.Challenge - for _, auth := range auths { - for _, challenge := range auth.Challenges { - if challenge.Status != acme.StatusPending { - t.Logf("ignoring challenge not in status pending: %v", challenge) - continue - } - - if challenge.Type == "dns-01" { - challengeBody, err := acmeClient.DNS01ChallengeRecord(challenge.Token) - require.NoError(t, err, "failed generating challenge response") - - err = pki.AddDNSRecord("_acme-challenge."+auth.Identifier.Value, "TXT", challengeBody) - require.NoError(t, err, "failed setting DNS record") - - challengesToAccept = append(challengesToAccept, challenge) - } - } - } - - require.GreaterOrEqual(t, len(challengesToAccept), 1, "Need at least one challenge, got none") - return challengesToAccept - } - - acmeCert := doAcmeValidationWithGoLibrary(t, directoryUrl, acmeOrderIdentifiers, cr, provisioningFunc, "") - require.Contains(t, acmeCert.DNSNames, hostname) - require.Contains(t, acmeCert.DNSNames, wildcard) - require.Equal(t, wildcard, acmeCert.Subject.CommonName) - pki.RemoveDNSRecordsForDomain(hostname) - - // Redo validation with a role this time. - err = pki.UpdateRole("wildcard", map[string]interface{}{ - "key_type": "any", - "allowed_domains": "go-lang-wildcard-client.dadgarcorp.com", - "allow_subdomains": true, - "allow_bare_domains": true, - "allow_wildcard_certificates": true, - "client_flag": false, - }) - require.NoError(t, err, "failed creating role wildcard") - directoryUrl = basePath + "/roles/wildcard/acme/directory" - - acmeCert = doAcmeValidationWithGoLibrary(t, directoryUrl, acmeOrderIdentifiers, cr, provisioningFunc, "") - require.Contains(t, acmeCert.DNSNames, hostname) - require.Contains(t, acmeCert.DNSNames, wildcard) - require.Equal(t, wildcard, acmeCert.Subject.CommonName) - pki.RemoveDNSRecordsForDomain(hostname) -} - -func SubtestACMEPreventsICADNS(t *testing.T, cluster *VaultPkiCluster) { - pki, err := cluster.CreateAcmeMount("pki-dns-ica") - require.NoError(t, err, "failed setting up acme mount") - - // Since we interact with ACME from outside the container network the ACME - // configuration needs to be updated to use the host port and not the internal - // docker ip. - basePath, err := pki.UpdateClusterConfigLocalAddr() - require.NoError(t, err, "failed updating cluster config") - - hostname := "go-lang-intermediate-ca-cert.dadgarcorp.com" - - // Do validation without a role first. - directoryUrl := basePath + "/acme/directory" - acmeOrderIdentifiers := []acme.AuthzID{ - {Type: "dns", Value: hostname}, - } - cr := &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: hostname}, - DNSNames: []string{hostname}, - ExtraExtensions: []pkix.Extension{ - // Basic Constraint with IsCA asserted to true. - { - Id: certutil.ExtensionBasicConstraintsOID, - Critical: true, - Value: []byte{0x30, 0x03, 0x01, 0x01, 0xFF}, - }, - }, - } - - provisioningFunc := func(acmeClient *acme.Client, auths []*acme.Authorization) []*acme.Challenge { - // For each dns-01 challenge, place the record in the associated DNS resolver. - var challengesToAccept []*acme.Challenge - for _, auth := range auths { - for _, challenge := range auth.Challenges { - if challenge.Status != acme.StatusPending { - t.Logf("ignoring challenge not in status pending: %v", challenge) - continue - } - - if challenge.Type == "dns-01" { - challengeBody, err := acmeClient.DNS01ChallengeRecord(challenge.Token) - require.NoError(t, err, "failed generating challenge response") - - err = pki.AddDNSRecord("_acme-challenge."+auth.Identifier.Value, "TXT", challengeBody) - require.NoError(t, err, "failed setting DNS record") - - challengesToAccept = append(challengesToAccept, challenge) - } - } - } - - require.GreaterOrEqual(t, len(challengesToAccept), 1, "Need at least one challenge, got none") - return challengesToAccept - } - - doAcmeValidationWithGoLibrary(t, directoryUrl, acmeOrderIdentifiers, cr, provisioningFunc, "refusing to accept CSR with Basic Constraints extension") - pki.RemoveDNSRecordsForDomain(hostname) - - // Redo validation with a role this time. - err = pki.UpdateRole("ica", map[string]interface{}{ - "key_type": "any", - "allowed_domains": "go-lang-intermediate-ca-cert.dadgarcorp.com", - "allow_subdomains": true, - "allow_bare_domains": true, - "allow_wildcard_certificates": true, - "client_flag": false, - }) - require.NoError(t, err, "failed creating role wildcard") - directoryUrl = basePath + "/roles/ica/acme/directory" - - doAcmeValidationWithGoLibrary(t, directoryUrl, acmeOrderIdentifiers, cr, provisioningFunc, "refusing to accept CSR with Basic Constraints extension") - pki.RemoveDNSRecordsForDomain(hostname) -} - -// SubtestACMEStepDownNode Verify that we can properly run an ACME session through a -// secondary node, and midway through the challenge verification process, seal the -// active node and make sure we can complete the ACME session on the new active node. -func SubtestACMEStepDownNode(t *testing.T, cluster *VaultPkiCluster) { - pki, err := cluster.CreateAcmeMount("stepdown-test") - require.NoError(t, err) - - // Since we interact with ACME from outside the container network the ACME - // configuration needs to be updated to use the host port and not the internal - // docker ip. We also grab the non-active node here on purpose to verify - // ACME related APIs are properly forwarded across standby hosts. - nonActiveNodes := pki.GetNonActiveNodes() - require.GreaterOrEqual(t, len(nonActiveNodes), 1, "Need at least one non-active node") - - nonActiveNode := nonActiveNodes[0] - - basePath := fmt.Sprintf("https://%s/v1/%s", nonActiveNode.HostPort, pki.mount) - err = pki.UpdateClusterConfig(map[string]interface{}{ - "path": basePath, - }) - - hostname := "go-lang-stepdown-client.dadgarcorp.com" - - acmeOrderIdentifiers := []acme.AuthzID{ - {Type: "dns", Value: hostname}, - } - cr := &x509.CertificateRequest{ - DNSNames: []string{hostname, hostname}, - } - - accountKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "failed creating rsa account key") - - acmeClient := &acme.Client{ - Key: accountKey, - HTTPClient: &http.Client{Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }}, - DirectoryURL: basePath + "/acme/directory", - } - - testCtx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancelFunc() - - // Create new account - _, err = acmeClient.Register(testCtx, &acme.Account{Contact: []string{"mailto:ipsans@dadgarcorp.com"}}, - func(tosURL string) bool { return true }) - require.NoError(t, err, "failed registering account") - - // Create an ACME order - order, err := acmeClient.AuthorizeOrder(testCtx, acmeOrderIdentifiers) - require.NoError(t, err, "failed creating ACME order") - - require.Len(t, order.AuthzURLs, 1, "expected a single authz url") - authUrl := order.AuthzURLs[0] - - authorization, err := acmeClient.GetAuthorization(testCtx, authUrl) - require.NoError(t, err, "failed to lookup authorization at url: %s", authUrl) - - dnsTxtRecordsToAdd := map[string]string{} - - var challengesToAccept []*acme.Challenge - for _, challenge := range authorization.Challenges { - if challenge.Status != acme.StatusPending { - t.Logf("ignoring challenge not in status pending: %v", challenge) - continue - } - - if challenge.Type == "dns-01" { - challengeBody, err := acmeClient.DNS01ChallengeRecord(challenge.Token) - require.NoError(t, err, "failed generating challenge response") - - // Collect the challenges for us to add the DNS records after step-down - dnsTxtRecordsToAdd["_acme-challenge."+authorization.Identifier.Value] = challengeBody - challengesToAccept = append(challengesToAccept, challenge) - } - } - - // Tell the ACME server, that they can now validate those challenges, this will cause challenge - // verification failures on the main node as the DNS records do not exist. - for _, challenge := range challengesToAccept { - _, err = acmeClient.Accept(testCtx, challenge) - require.NoError(t, err, "failed to accept challenge: %v", challenge) - } - - // Now wait till we start seeing the challenge engine start failing the lookups. - testhelpers.RetryUntil(t, 10*time.Second, func() error { - myAuth, err := acmeClient.GetAuthorization(testCtx, authUrl) - require.NoError(t, err, "failed to lookup authorization at url: %s", authUrl) - - for _, challenge := range myAuth.Challenges { - if challenge.Error != nil { - // The engine failed on one of the challenges, we are done waiting - return nil - } - } - - return fmt.Errorf("no challenges for auth %v contained any errors", myAuth.Identifier) - }) - - // Seal the active node now and wait for the next node to appear - previousActiveNode := pki.GetActiveClusterNode() - t.Logf("Stepping down node id: %s", previousActiveNode.NodeID) - - haStatus, _ := previousActiveNode.APIClient().Sys().HAStatus() - t.Logf("Node: %v HaStatus: %v\n", previousActiveNode.NodeID, haStatus) - - testhelpers.RetryUntil(t, 2*time.Minute, func() error { - state, err := previousActiveNode.APIClient().Sys().RaftAutopilotState() - if err != nil { - return err - } - - t.Logf("Node: %v Raft AutoPilotState: %v\n", previousActiveNode.NodeID, state) - - if !state.Healthy { - return fmt.Errorf("raft auto pilot state is not healthy") - } - - // Make sure that we have at least one node that can take over prior to sealing the current active node. - if state.FailureTolerance < 1 { - msg := fmt.Sprintf("there is no fault tolerance within raft state yet: %d", state.FailureTolerance) - t.Log(msg) - return errors.New(msg) - } - - return nil - }) - - t.Logf("Sealing active node") - err = previousActiveNode.APIClient().Sys().Seal() - require.NoError(t, err, "failed stepping down node") - - // Add our DNS records now - t.Logf("Adding DNS records") - for dnsHost, dnsValue := range dnsTxtRecordsToAdd { - err = pki.AddDNSRecord(dnsHost, "TXT", dnsValue) - require.NoError(t, err, "failed adding DNS record: %s:%s", dnsHost, dnsValue) - } - - // Wait for our new active node to come up - testhelpers.RetryUntil(t, 2*time.Minute, func() error { - newNode := pki.GetActiveClusterNode() - if newNode.NodeID == previousActiveNode.NodeID { - return fmt.Errorf("existing node is still the leader after stepdown: %s", newNode.NodeID) - } - - t.Logf("New active node has node id: %v", newNode.NodeID) - return nil - }) - - // Wait for the order/challenges to be validated. - _, err = acmeClient.WaitOrder(testCtx, order.URI) - if err != nil { - // We failed waiting for the order to become ready, lets print out current challenge statuses to help debugging - myAuth, authErr := acmeClient.GetAuthorization(testCtx, authUrl) - require.NoError(t, authErr, "failed to lookup authorization at url: %s and wait order failed with: %v", authUrl, err) - - t.Logf("Authorization Status: %s", myAuth.Status) - for _, challenge := range myAuth.Challenges { - // The engine failed on one of the challenges, we are done waiting - t.Logf("challenge: %v state: %v Error: %v", challenge.Type, challenge.Status, challenge.Error) - } - - require.NoError(t, err, "failed waiting for order to be ready") - } - - // Create/sign the CSR and ask ACME server to sign it returning us the final certificate - csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - csr, err := x509.CreateCertificateRequest(rand.Reader, cr, csrKey) - require.NoError(t, err, "failed generating csr") - - certs, _, err := acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, false) - require.NoError(t, err, "failed to get a certificate back from ACME") - - _, err = x509.ParseCertificate(certs[0]) - require.NoError(t, err, "failed parsing acme cert bytes") -} - -func getDockerLog(t *testing.T) (func(s string), *pkiext.LogConsumerWriter, *pkiext.LogConsumerWriter) { - logConsumer := func(s string) { - t.Logf(s) - } - - logStdout := &pkiext.LogConsumerWriter{logConsumer} - logStderr := &pkiext.LogConsumerWriter{logConsumer} - return logConsumer, logStdout, logStderr -} diff --git a/builtin/logical/pkiext/pkiext_binary/pki_cluster.go b/builtin/logical/pkiext/pkiext_binary/pki_cluster.go deleted file mode 100644 index 4462f5103..000000000 --- a/builtin/logical/pkiext/pkiext_binary/pki_cluster.go +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pkiext_binary - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/logical/pki/dnstest" - dockhelper "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/hashicorp/vault/sdk/helper/testcluster" - "github.com/hashicorp/vault/sdk/helper/testcluster/docker" -) - -type VaultPkiCluster struct { - cluster *docker.DockerCluster - Dns *dnstest.TestServer -} - -func NewVaultPkiCluster(t *testing.T) *VaultPkiCluster { - binary := os.Getenv("VAULT_BINARY") - if binary == "" { - t.Skip("only running docker test when $VAULT_BINARY present") - } - - opts := &docker.DockerClusterOptions{ - ImageRepo: "docker.mirror.hashicorp.services/hashicorp/vault", - // We're replacing the binary anyway, so we're not too particular about - // the docker image version tag. - ImageTag: "latest", - VaultBinary: binary, - ClusterOptions: testcluster.ClusterOptions{ - VaultNodeConfig: &testcluster.VaultNodeConfig{ - LogLevel: "TRACE", - }, - NumCores: 3, - }, - } - - cluster := docker.NewTestDockerCluster(t, opts) - - return &VaultPkiCluster{cluster: cluster} -} - -func NewVaultPkiClusterWithDNS(t *testing.T) *VaultPkiCluster { - cluster := NewVaultPkiCluster(t) - dns := dnstest.SetupResolverOnNetwork(t, "dadgarcorp.com", cluster.GetContainerNetworkName()) - cluster.Dns = dns - return cluster -} - -func (vpc *VaultPkiCluster) Cleanup() { - vpc.cluster.Cleanup() - if vpc.Dns != nil { - vpc.Dns.Cleanup() - } -} - -func (vpc *VaultPkiCluster) GetActiveClusterNode() *docker.DockerClusterNode { - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - node, err := testcluster.WaitForActiveNode(ctx, vpc.cluster) - if err != nil { - panic(fmt.Sprintf("no cluster node became active in timeout window: %v", err)) - } - - return vpc.cluster.ClusterNodes[node] -} - -func (vpc *VaultPkiCluster) GetNonActiveNodes() []*docker.DockerClusterNode { - nodes := []*docker.DockerClusterNode{} - for _, node := range vpc.cluster.ClusterNodes { - leader, err := node.APIClient().Sys().Leader() - if err != nil { - continue - } - - if !leader.IsSelf { - nodes = append(nodes, node) - } - } - - return nodes -} - -func (vpc *VaultPkiCluster) GetActiveContainerHostPort() string { - return vpc.GetActiveClusterNode().HostPort -} - -func (vpc *VaultPkiCluster) GetContainerNetworkName() string { - return vpc.cluster.ClusterNodes[0].ContainerNetworkName -} - -func (vpc *VaultPkiCluster) GetActiveContainerIP() string { - return vpc.GetActiveClusterNode().ContainerIPAddress -} - -func (vpc *VaultPkiCluster) GetActiveContainerID() string { - return vpc.GetActiveClusterNode().Container.ID -} - -func (vpc *VaultPkiCluster) GetActiveNode() *api.Client { - return vpc.GetActiveClusterNode().APIClient() -} - -// GetListenerCACertPEM returns the Vault cluster's PEM-encoded CA certificate. -func (vpc *VaultPkiCluster) GetListenerCACertPEM() []byte { - return vpc.cluster.CACertPEM -} - -func (vpc *VaultPkiCluster) AddHostname(hostname, ip string) error { - if vpc.Dns != nil { - vpc.Dns.AddRecord(hostname, "A", ip) - vpc.Dns.PushConfig() - return nil - } else { - return vpc.AddNameToHostFiles(hostname, ip) - } -} - -func (vpc *VaultPkiCluster) AddNameToHostFiles(hostname, ip string) error { - updateHostsCmd := []string{ - "sh", "-c", - "echo '" + ip + " " + hostname + "' >> /etc/hosts", - } - for _, node := range vpc.cluster.ClusterNodes { - containerID := node.Container.ID - _, _, retcode, err := dockhelper.RunCmdWithOutput(vpc.cluster.DockerAPI, context.Background(), containerID, updateHostsCmd) - if err != nil { - return fmt.Errorf("failed updating container %s host file: %w", containerID, err) - } - - if retcode != 0 { - return fmt.Errorf("expected zero retcode from updating vault host file in container %s got: %d", containerID, retcode) - } - } - - return nil -} - -func (vpc *VaultPkiCluster) AddDNSRecord(hostname, recordType, ip string) error { - if vpc.Dns == nil { - return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to provision custom records") - } - - vpc.Dns.AddRecord(hostname, recordType, ip) - vpc.Dns.PushConfig() - return nil -} - -func (vpc *VaultPkiCluster) RemoveDNSRecord(domain string, record string, value string) error { - if vpc.Dns == nil { - return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to remove specific record") - } - - vpc.Dns.RemoveRecord(domain, record, value) - return nil -} - -func (vpc *VaultPkiCluster) RemoveDNSRecordsOfTypeForDomain(domain string, record string) error { - if vpc.Dns == nil { - return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to remove all records of type") - } - - vpc.Dns.RemoveRecordsOfTypeForDomain(domain, record) - return nil -} - -func (vpc *VaultPkiCluster) RemoveDNSRecordsForDomain(domain string) error { - if vpc.Dns == nil { - return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to remove records for domain") - } - - vpc.Dns.RemoveRecordsForDomain(domain) - return nil -} - -func (vpc *VaultPkiCluster) RemoveAllDNSRecords() error { - if vpc.Dns == nil { - return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to remove all records") - } - - vpc.Dns.RemoveAllRecords() - return nil -} - -func (vpc *VaultPkiCluster) CreateMount(name string) (*VaultPkiMount, error) { - err := vpc.GetActiveNode().Sys().Mount(name, &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "32h", - AllowedResponseHeaders: []string{ - "Last-Modified", "Replay-Nonce", - "Link", "Location", - }, - }, - }) - if err != nil { - return nil, err - } - - return &VaultPkiMount{ - vpc, - name, - }, nil -} - -func (vpc *VaultPkiCluster) CreateAcmeMount(mountName string) (*VaultPkiMount, error) { - pki, err := vpc.CreateMount(mountName) - if err != nil { - return nil, fmt.Errorf("failed creating mount %s: %w", mountName, err) - } - - err = pki.UpdateClusterConfig(nil) - if err != nil { - return nil, fmt.Errorf("failed updating cluster config: %w", err) - } - - cfg := map[string]interface{}{ - "eab_policy": "not-required", - } - if vpc.Dns != nil { - cfg["dns_resolver"] = vpc.Dns.GetRemoteAddr() - } - - err = pki.UpdateAcmeConfig(true, cfg) - if err != nil { - return nil, fmt.Errorf("failed updating acme config: %w", err) - } - - // Setup root+intermediate CA hierarchy within this mount. - resp, err := pki.GenerateRootInternal(map[string]interface{}{ - "common_name": "Root X1", - "country": "US", - "organization": "Dadgarcorp", - "ou": "QA", - "key_type": "ec", - "key_bits": 256, - "use_pss": false, - "issuer_name": "root", - }) - if err != nil { - return nil, fmt.Errorf("failed generating root internal: %w", err) - } - if resp == nil || len(resp.Data) == 0 { - return nil, fmt.Errorf("failed generating root internal: nil or empty response but no error") - } - - resp, err = pki.GenerateIntermediateInternal(map[string]interface{}{ - "common_name": "Intermediate I1", - "country": "US", - "organization": "Dadgarcorp", - "ou": "QA", - "key_type": "ec", - "key_bits": 256, - "use_pss": false, - }) - if err != nil { - return nil, fmt.Errorf("failed generating int csr: %w", err) - } - if resp == nil || len(resp.Data) == 0 { - return nil, fmt.Errorf("failed generating int csr: nil or empty response but no error") - } - - resp, err = pki.SignIntermediary("default", resp.Data["csr"], map[string]interface{}{ - "common_name": "Intermediate I1", - "country": "US", - "organization": "Dadgarcorp", - "ou": "QA", - "key_type": "ec", - "csr": resp.Data["csr"], - }) - if err != nil { - return nil, fmt.Errorf("failed signing int csr: %w", err) - } - if resp == nil || len(resp.Data) == 0 { - return nil, fmt.Errorf("failed signing int csr: nil or empty response but no error") - } - intCert := resp.Data["certificate"].(string) - - resp, err = pki.ImportBundle(intCert, nil) - if err != nil { - return nil, fmt.Errorf("failed importing signed cert: %w", err) - } - if resp == nil || len(resp.Data) == 0 { - return nil, fmt.Errorf("failed importing signed cert: nil or empty response but no error") - } - - err = pki.UpdateDefaultIssuer(resp.Data["imported_issuers"].([]interface{})[0].(string), nil) - if err != nil { - return nil, fmt.Errorf("failed to set intermediate as default: %w", err) - } - - err = pki.UpdateIssuer("default", map[string]interface{}{ - "leaf_not_after_behavior": "truncate", - }) - if err != nil { - return nil, fmt.Errorf("failed to update intermediate ttl behavior: %w", err) - } - - err = pki.UpdateIssuer("root", map[string]interface{}{ - "leaf_not_after_behavior": "truncate", - }) - if err != nil { - return nil, fmt.Errorf("failed to update root ttl behavior: %w", err) - } - - return pki, nil -} diff --git a/builtin/logical/pkiext/pkiext_binary/pki_mount.go b/builtin/logical/pkiext/pkiext_binary/pki_mount.go deleted file mode 100644 index 15ce16b2a..000000000 --- a/builtin/logical/pkiext/pkiext_binary/pki_mount.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pkiext_binary - -import ( - "context" - "encoding/base64" - "fmt" - "path" - - "github.com/hashicorp/vault/api" -) - -type VaultPkiMount struct { - *VaultPkiCluster - mount string -} - -func (vpm *VaultPkiMount) UpdateClusterConfig(config map[string]interface{}) error { - defaultPath := "https://" + vpm.cluster.ClusterNodes[0].ContainerIPAddress + ":8200/v1/" + vpm.mount - defaults := map[string]interface{}{ - "path": defaultPath, - "aia_path": defaultPath, - } - - _, err := vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), - vpm.mount+"/config/cluster", mergeWithDefaults(config, defaults)) - return err -} - -func (vpm *VaultPkiMount) UpdateClusterConfigLocalAddr() (string, error) { - basePath := fmt.Sprintf("https://%s/v1/%s", vpm.GetActiveContainerHostPort(), vpm.mount) - return basePath, vpm.UpdateClusterConfig(map[string]interface{}{ - "path": basePath, - }) -} - -func (vpm *VaultPkiMount) UpdateAcmeConfig(enable bool, config map[string]interface{}) error { - defaults := map[string]interface{}{ - "enabled": enable, - } - - _, err := vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), - vpm.mount+"/config/acme", mergeWithDefaults(config, defaults)) - return err -} - -func (vpm *VaultPkiMount) GenerateRootInternal(props map[string]interface{}) (*api.Secret, error) { - defaults := map[string]interface{}{ - "common_name": "root-test.com", - "key_type": "ec", - "issuer_name": "root", - } - - return vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), - vpm.mount+"/root/generate/internal", mergeWithDefaults(props, defaults)) -} - -func (vpm *VaultPkiMount) GenerateIntermediateInternal(props map[string]interface{}) (*api.Secret, error) { - defaults := map[string]interface{}{ - "common_name": "intermediary-test.com", - "key_type": "ec", - "issuer_name": "intermediary", - } - - return vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), - vpm.mount+"/intermediate/generate/internal", mergeWithDefaults(props, defaults)) -} - -func (vpm *VaultPkiMount) SignIntermediary(signingIssuer string, csr interface{}, props map[string]interface{}) (*api.Secret, error) { - defaults := map[string]interface{}{ - "csr": csr, - } - - return vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), - vpm.mount+"/issuer/"+signingIssuer+"/sign-intermediate", - mergeWithDefaults(props, defaults)) -} - -func (vpm *VaultPkiMount) ImportBundle(pemBundle interface{}, props map[string]interface{}) (*api.Secret, error) { - defaults := map[string]interface{}{ - "pem_bundle": pemBundle, - } - - return vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), - vpm.mount+"/issuers/import/bundle", mergeWithDefaults(props, defaults)) -} - -func (vpm *VaultPkiMount) UpdateDefaultIssuer(issuerId string, props map[string]interface{}) error { - defaults := map[string]interface{}{ - "default": issuerId, - } - - _, err := vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), - vpm.mount+"/config/issuers", mergeWithDefaults(props, defaults)) - - return err -} - -func (vpm *VaultPkiMount) UpdateIssuer(issuerRef string, props map[string]interface{}) error { - defaults := map[string]interface{}{} - - _, err := vpm.GetActiveNode().Logical().JSONMergePatch(context.Background(), - vpm.mount+"/issuer/"+issuerRef, mergeWithDefaults(props, defaults)) - - return err -} - -func (vpm *VaultPkiMount) UpdateRole(roleName string, config map[string]interface{}) error { - defaults := map[string]interface{}{} - - _, err := vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), - vpm.mount+"/roles/"+roleName, mergeWithDefaults(config, defaults)) - - return err -} - -func (vpm *VaultPkiMount) GetEabKey(acmeDirectory string) (string, string, error) { - eabPath := path.Join(vpm.mount, acmeDirectory, "/new-eab") - resp, err := vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), eabPath, map[string]interface{}{}) - if err != nil { - return "", "", fmt.Errorf("failed fetching eab from %s: %w", eabPath, err) - } - eabId := resp.Data["id"].(string) - base64EabKey := resp.Data["key"].(string) - // just make sure we get something valid back from the server, we still want to pass back the base64 version - // to the caller... - _, err = base64.RawURLEncoding.DecodeString(base64EabKey) - if err != nil { - return "", "", fmt.Errorf("failed decoding key response field: %s: %w", base64EabKey, err) - } - return eabId, base64EabKey, nil -} - -// GetCACertPEM retrieves the PKI mount's PEM-encoded CA certificate. -func (vpm *VaultPkiMount) GetCACertPEM() (string, error) { - caCertPath := path.Join(vpm.mount, "/cert/ca") - resp, err := vpm.GetActiveNode().Logical().ReadWithContext(context.Background(), caCertPath) - if err != nil { - return "", err - } - return resp.Data["certificate"].(string), nil -} - -func mergeWithDefaults(config map[string]interface{}, defaults map[string]interface{}) map[string]interface{} { - myConfig := config - if myConfig == nil { - myConfig = map[string]interface{}{} - } - for key, value := range defaults { - if origVal, exists := config[key]; !exists { - myConfig[key] = value - } else { - myConfig[key] = origVal - } - } - - return myConfig -} diff --git a/builtin/logical/pkiext/pkiext_binary/testdata/caddy_http.json b/builtin/logical/pkiext/pkiext_binary/testdata/caddy_http.json deleted file mode 100644 index 272ecd102..000000000 --- a/builtin/logical/pkiext/pkiext_binary/testdata/caddy_http.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "apps": { - "http": { - "servers": { - "srv0": { - "listen": [ - ":80", - ":443" - ], - "routes": [ - { - "match": [ - { - "host": [ - "{{.Hostname}}" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "Hello!", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ] - } - } - }, - "tls": { - "automation": { - "policies": [ - { - "subjects": [ - "{{.Hostname}}" - ], - "issuers": [ - { - "ca": "{{.Directory}}", - "module": "acme", - "challenges": { - "tls-alpn": { - "disabled": true - } - }, - "trusted_roots_pem_files": [ - "{{.CACert}}" - ] - } - ] - } - ] - } - } - } -} diff --git a/builtin/logical/pkiext/pkiext_binary/testdata/caddy_http_eab.json b/builtin/logical/pkiext/pkiext_binary/testdata/caddy_http_eab.json deleted file mode 100644 index 61cab8894..000000000 --- a/builtin/logical/pkiext/pkiext_binary/testdata/caddy_http_eab.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "apps": { - "http": { - "servers": { - "srv0": { - "listen": [ - ":80", - ":443" - ], - "routes": [ - { - "match": [ - { - "host": [ - "{{.Hostname}}" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "Hello!", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ] - } - } - }, - "tls": { - "automation": { - "policies": [ - { - "subjects": [ - "{{.Hostname}}" - ], - "issuers": [ - { - "ca": "{{.Directory}}", - "module": "acme", - "external_account": { - "key_id": "{{.EABID}}", - "mac_key": "{{.EABKey}}" - }, - "challenges": { - "tls-alpn": { - "disabled": true - } - }, - "trusted_roots_pem_files": [ - "{{.CACert}}" - ] - } - ] - } - ] - } - } - } -} diff --git a/builtin/logical/pkiext/pkiext_binary/testdata/caddy_tls_alpn.json b/builtin/logical/pkiext/pkiext_binary/testdata/caddy_tls_alpn.json deleted file mode 100644 index 0bc0ea911..000000000 --- a/builtin/logical/pkiext/pkiext_binary/testdata/caddy_tls_alpn.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "apps": { - "http": { - "servers": { - "srv0": { - "listen": [ - ":80", - ":443" - ], - "routes": [ - { - "match": [ - { - "host": [ - "{{.Hostname}}" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "Hello!", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ] - } - } - }, - "tls": { - "automation": { - "policies": [ - { - "subjects": [ - "{{.Hostname}}" - ], - "issuers": [ - { - "ca": "{{.Directory}}", - "module": "acme", - "challenges": { - "http": { - "disabled": true - } - }, - "trusted_roots_pem_files": [ - "{{.CACert}}" - ] - } - ] - } - ] - } - } - } -} diff --git a/builtin/logical/pkiext/test_helpers.go b/builtin/logical/pkiext/test_helpers.go deleted file mode 100644 index 7f6abe36c..000000000 --- a/builtin/logical/pkiext/test_helpers.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pkiext - -import ( - "bufio" - "bytes" - "crypto" - "crypto/x509" - "encoding/pem" - "fmt" - "testing" - - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/hashicorp/vault/sdk/logical" - - "github.com/stretchr/testify/require" -) - -func requireFieldsSetInResp(t *testing.T, resp *logical.Response, fields ...string) { - var missingFields []string - for _, field := range fields { - value, ok := resp.Data[field] - if !ok || value == nil { - missingFields = append(missingFields, field) - } - } - - require.Empty(t, missingFields, "The following fields were required but missing from response:\n%v", resp.Data) -} - -func requireSuccessNonNilResponse(t *testing.T, resp *logical.Response, err error, msgAndArgs ...interface{}) { - require.NoError(t, err, msgAndArgs...) - if resp.IsError() { - errContext := fmt.Sprintf("Expected successful response but got error: %v", resp.Error()) - require.Falsef(t, resp.IsError(), errContext, msgAndArgs...) - } - require.NotNil(t, resp, msgAndArgs...) -} - -func requireSuccessNilResponse(t *testing.T, resp *logical.Response, err error, msgAndArgs ...interface{}) { - require.NoError(t, err, msgAndArgs...) - if resp.IsError() { - errContext := fmt.Sprintf("Expected successful response but got error: %v", resp.Error()) - require.Falsef(t, resp.IsError(), errContext, msgAndArgs...) - } - if resp != nil { - msg := fmt.Sprintf("expected nil response but got: %v", resp) - require.Nilf(t, resp, msg, msgAndArgs...) - } -} - -func parseCert(t *testing.T, pemCert string) *x509.Certificate { - block, _ := pem.Decode([]byte(pemCert)) - require.NotNil(t, block, "failed to decode PEM block") - - cert, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) - return cert -} - -func parseKey(t *testing.T, pemKey string) crypto.Signer { - block, _ := pem.Decode([]byte(pemKey)) - require.NotNil(t, block, "failed to decode PEM block") - - key, _, err := certutil.ParseDERKey(block.Bytes) - require.NoError(t, err) - return key -} - -type LogConsumerWriter struct { - Consumer func(string) -} - -func (l LogConsumerWriter) Write(p []byte) (n int, err error) { - // TODO this assumes that we're never passed partial log lines, which - // seems a safe assumption for now based on how docker looks to implement - // logging, but might change in the future. - scanner := bufio.NewScanner(bytes.NewReader(p)) - scanner.Buffer(make([]byte, 64*1024), bufio.MaxScanTokenSize) - for scanner.Scan() { - l.Consumer(scanner.Text()) - } - return len(p), nil -} diff --git a/builtin/logical/pkiext/zlint_test.go b/builtin/logical/pkiext/zlint_test.go deleted file mode 100644 index 206d23cb0..000000000 --- a/builtin/logical/pkiext/zlint_test.go +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pkiext - -import ( - "context" - "encoding/json" - "sync" - "testing" - - "github.com/hashicorp/vault/builtin/logical/pki" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/stretchr/testify/require" -) - -var ( - zRunner *docker.Runner - buildZLintOnce sync.Once -) - -func buildZLintContainer(t *testing.T) { - containerfile := ` -FROM docker.mirror.hashicorp.services/library/golang:latest - -RUN go install github.com/zmap/zlint/v3/cmd/zlint@latest -` - - bCtx := docker.NewBuildContext() - - imageName := "vault_pki_zlint_validator" - imageTag := "latest" - - var err error - zRunner, err = docker.NewServiceRunner(docker.RunOptions{ - ImageRepo: imageName, - ImageTag: imageTag, - ContainerName: "pki_zlint", - // We want to run sleep in the background so we're not stuck waiting - // for the default golang container's shell to prompt for input. - Entrypoint: []string{"sleep", "45"}, - LogConsumer: func(s string) { - if t.Failed() { - t.Logf("container logs: %s", s) - } - }, - }) - if err != nil { - t.Fatalf("Could not provision docker service runner: %s", err) - } - - ctx := context.Background() - output, err := zRunner.BuildImage(ctx, containerfile, bCtx, - docker.BuildRemove(true), docker.BuildForceRemove(true), - docker.BuildPullParent(true), - docker.BuildTags([]string{imageName + ":" + imageTag})) - if err != nil { - t.Fatalf("Could not build new image: %v", err) - } - - t.Logf("Image build output: %v", string(output)) -} - -func RunZLintContainer(t *testing.T, certificate string) []byte { - buildZLintOnce.Do(func() { - buildZLintContainer(t) - }) - - ctx := context.Background() - // We don't actually care about the address, we just want to start the - // container so we can run commands in it. We'd ideally like to skip this - // step and only build a new image, but the zlint output would be - // intermingled with container build stages, so its not that useful. - result, err := zRunner.Start(ctx, true, false) - if err != nil { - t.Fatalf("Could not start golang container for zlint: %s", err) - } - - // Copy the cert into the newly running container. - certCtx := docker.NewBuildContext() - certCtx["cert.pem"] = docker.PathContentsFromBytes([]byte(certificate)) - if err := zRunner.CopyTo(result.Container.ID, "/go/", certCtx); err != nil { - t.Fatalf("Could not copy certificate into container: %v", err) - } - - // Run the zlint command and save the output. - cmd := []string{"/go/bin/zlint", "/go/cert.pem"} - stdout, stderr, retcode, err := zRunner.RunCmdWithOutput(ctx, result.Container.ID, cmd) - if err != nil { - t.Fatalf("Could not run command in container: %v", err) - } - - if len(stderr) != 0 { - t.Logf("Got stderr from command:\n%v\n", string(stderr)) - } - - if retcode != 0 { - t.Logf("Got stdout from command:\n%v\n", string(stdout)) - t.Fatalf("Got unexpected non-zero retcode from zlint: %v\n", retcode) - } - - // Clean up after ourselves. - if err := zRunner.Stop(context.Background(), result.Container.ID); err != nil { - t.Fatalf("failed to stop container: %v", err) - } - - return stdout -} - -func RunZLintRootTest(t *testing.T, keyType string, keyBits int, usePSS bool, ignored []string) { - b, s := pki.CreateBackendWithStorage(t) - - resp, err := pki.CBWrite(b, s, "root/generate/internal", map[string]interface{}{ - "common_name": "Root X1", - "country": "US", - "organization": "Dadgarcorp", - "ou": "QA", - "key_type": keyType, - "key_bits": keyBits, - "use_pss": usePSS, - }) - require.NoError(t, err) - rootCert := resp.Data["certificate"].(string) - - var parsed map[string]interface{} - output := RunZLintContainer(t, rootCert) - - if err := json.Unmarshal(output, &parsed); err != nil { - t.Fatalf("failed to parse zlint output as JSON: %v\nOutput:\n%v\n\n", err, string(output)) - } - - for key, rawValue := range parsed { - value := rawValue.(map[string]interface{}) - result, ok := value["result"] - if !ok || result == "NA" { - continue - } - - if result == "error" { - skip := false - for _, allowedFailures := range ignored { - if allowedFailures == key { - skip = true - break - } - } - - if !skip { - t.Fatalf("got unexpected error from test %v: %v", key, value) - } - } - } -} - -func Test_ZLintRSA2048(t *testing.T) { - t.Parallel() - RunZLintRootTest(t, "rsa", 2048, false, nil) -} - -func Test_ZLintRSA2048PSS(t *testing.T) { - t.Parallel() - RunZLintRootTest(t, "rsa", 2048, true, nil) -} - -func Test_ZLintRSA3072(t *testing.T) { - t.Parallel() - RunZLintRootTest(t, "rsa", 3072, false, nil) -} - -func Test_ZLintRSA3072PSS(t *testing.T) { - t.Parallel() - RunZLintRootTest(t, "rsa", 3072, true, nil) -} - -func Test_ZLintECDSA256(t *testing.T) { - t.Parallel() - RunZLintRootTest(t, "ec", 256, false, nil) -} - -func Test_ZLintECDSA384(t *testing.T) { - t.Parallel() - RunZLintRootTest(t, "ec", 384, false, nil) -} - -func Test_ZLintECDSA521(t *testing.T) { - t.Parallel() - // Mozilla doesn't allow P-521 ECDSA keys. - RunZLintRootTest(t, "ec", 521, false, []string{ - "e_mp_ecdsa_pub_key_encoding_correct", - "e_mp_ecdsa_signature_encoding_correct", - }) -} diff --git a/builtin/logical/rabbitmq/backend_test.go b/builtin/logical/rabbitmq/backend_test.go deleted file mode 100644 index 21510b2f9..000000000 --- a/builtin/logical/rabbitmq/backend_test.go +++ /dev/null @@ -1,349 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package rabbitmq - -import ( - "context" - "fmt" - "log" - "os" - "testing" - - "github.com/hashicorp/go-secure-stdlib/base62" - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/logical" - rabbithole "github.com/michaelklishin/rabbit-hole/v2" - "github.com/mitchellh/mapstructure" -) - -const ( - envRabbitMQConnectionURI = "RABBITMQ_CONNECTION_URI" - envRabbitMQUsername = "RABBITMQ_USERNAME" - envRabbitMQPassword = "RABBITMQ_PASSWORD" -) - -const ( - testTags = "administrator" - testVHosts = `{"/": {"configure": ".*", "write": ".*", "read": ".*"}}` - testVHostTopics = `{"/": {"amq.topic": {"write": ".*", "read": ".*"}}}` - - roleName = "web" -) - -func prepareRabbitMQTestContainer(t *testing.T) (func(), string) { - if os.Getenv(envRabbitMQConnectionURI) != "" { - return func() {}, os.Getenv(envRabbitMQConnectionURI) - } - - runner, err := docker.NewServiceRunner(docker.RunOptions{ - ImageRepo: "docker.mirror.hashicorp.services/library/rabbitmq", - ImageTag: "3-management", - ContainerName: "rabbitmq", - Ports: []string{"15672/tcp"}, - }) - if err != nil { - t.Fatalf("could not start docker rabbitmq: %s", err) - } - - svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - connURL := fmt.Sprintf("http://%s:%d", host, port) - rmqc, err := rabbithole.NewClient(connURL, "guest", "guest") - if err != nil { - return nil, err - } - - _, err = rmqc.Overview() - if err != nil { - return nil, err - } - - return docker.NewServiceURLParse(connURL) - }) - if err != nil { - t.Fatalf("could not start docker rabbitmq: %s", err) - } - return svc.Cleanup, svc.Config.URL().String() -} - -func TestBackend_basic(t *testing.T) { - b, _ := Factory(context.Background(), logical.TestBackendConfig()) - - cleanup, uri := prepareRabbitMQTestContainer(t) - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - PreCheck: testAccPreCheckFunc(t, uri), - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, uri, ""), - testAccStepRole(t), - testAccStepReadCreds(t, b, uri, roleName), - }, - }) -} - -func TestBackend_returnsErrs(t *testing.T) { - b, _ := Factory(context.Background(), logical.TestBackendConfig()) - - cleanup, uri := prepareRabbitMQTestContainer(t) - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - PreCheck: testAccPreCheckFunc(t, uri), - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, uri, ""), - { - Operation: logical.CreateOperation, - Path: fmt.Sprintf("roles/%s", roleName), - Data: map[string]interface{}{ - "tags": testTags, - "vhosts": `{"invalid":{"write": ".*", "read": ".*"}}`, - "vhost_topics": testVHostTopics, - }, - }, - { - Operation: logical.ReadOperation, - Path: fmt.Sprintf("creds/%s", roleName), - ErrorOk: true, - }, - }, - }) -} - -func TestBackend_roleCrud(t *testing.T) { - b, _ := Factory(context.Background(), logical.TestBackendConfig()) - - cleanup, uri := prepareRabbitMQTestContainer(t) - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - PreCheck: testAccPreCheckFunc(t, uri), - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, uri, ""), - testAccStepRole(t), - testAccStepReadRole(t, roleName, testTags, testVHosts, testVHostTopics), - testAccStepDeleteRole(t, roleName), - testAccStepReadRole(t, roleName, "", "", ""), - }, - }) -} - -func TestBackend_roleWithPasswordPolicy(t *testing.T) { - if os.Getenv(logicaltest.TestEnvVar) == "" { - t.Skip(fmt.Sprintf("Acceptance tests skipped unless env %q set", logicaltest.TestEnvVar)) - return - } - - backendConfig := logical.TestBackendConfig() - passGen := func() (password string, err error) { - return base62.Random(30) - } - backendConfig.System.(*logical.StaticSystemView).SetPasswordPolicy("testpolicy", passGen) - b, _ := Factory(context.Background(), backendConfig) - - cleanup, uri := prepareRabbitMQTestContainer(t) - defer cleanup() - - logicaltest.Test(t, logicaltest.TestCase{ - PreCheck: testAccPreCheckFunc(t, uri), - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, uri, "testpolicy"), - testAccStepRole(t), - testAccStepReadCreds(t, b, uri, roleName), - }, - }) -} - -func testAccPreCheckFunc(t *testing.T, uri string) func() { - return func() { - if uri == "" { - t.Fatal("RabbitMQ URI must be set for acceptance tests") - } - } -} - -func testAccStepConfig(t *testing.T, uri string, passwordPolicy string) logicaltest.TestStep { - username := os.Getenv(envRabbitMQUsername) - if len(username) == 0 { - username = "guest" - } - password := os.Getenv(envRabbitMQPassword) - if len(password) == 0 { - password = "guest" - } - - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config/connection", - Data: map[string]interface{}{ - "connection_uri": uri, - "username": username, - "password": password, - "password_policy": passwordPolicy, - }, - } -} - -func testAccStepRole(t *testing.T) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("roles/%s", roleName), - Data: map[string]interface{}{ - "tags": testTags, - "vhosts": testVHosts, - "vhost_topics": testVHostTopics, - }, - } -} - -func testAccStepDeleteRole(t *testing.T, n string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "roles/" + n, - } -} - -func testAccStepReadCreds(t *testing.T, b logical.Backend, uri, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "creds/" + name, - Check: func(resp *logical.Response) error { - var d struct { - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - log.Printf("[WARN] Generated credentials: %v", d) - - client, err := rabbithole.NewClient(uri, d.Username, d.Password) - if err != nil { - t.Fatal(err) - } - - _, err = client.ListVhosts() - if err != nil { - t.Fatalf("unable to list vhosts with generated credentials: %s", err) - } - - resp, err = b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.RevokeOperation, - Secret: &logical.Secret{ - InternalData: map[string]interface{}{ - "secret_type": "creds", - "username": d.Username, - }, - }, - }) - if err != nil { - return err - } - if resp != nil { - if resp.IsError() { - return fmt.Errorf("error on resp: %#v", *resp) - } - } - - client, err = rabbithole.NewClient(uri, d.Username, d.Password) - if err != nil { - t.Fatal(err) - } - - _, err = client.ListVhosts() - if err == nil { - t.Fatalf("expected to fail listing vhosts: %s", err) - } - - return nil - }, - } -} - -func testAccStepReadRole(t *testing.T, name, tags, rawVHosts string, rawVHostTopics string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "roles/" + name, - Check: func(resp *logical.Response) error { - if resp == nil { - if tags == "" && rawVHosts == "" && rawVHostTopics == "" { - return nil - } - - return fmt.Errorf("bad: %#v", resp) - } - - var d struct { - Tags string `mapstructure:"tags"` - VHosts map[string]vhostPermission `mapstructure:"vhosts"` - VHostTopics map[string]map[string]vhostTopicPermission `mapstructure:"vhost_topics"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - if d.Tags != tags { - return fmt.Errorf("bad: %#v", resp) - } - - var vhosts map[string]vhostPermission - if err := jsonutil.DecodeJSON([]byte(rawVHosts), &vhosts); err != nil { - return fmt.Errorf("bad expected vhosts %#v: %s", vhosts, err) - } - - for host, permission := range vhosts { - actualPermission, ok := d.VHosts[host] - if !ok { - return fmt.Errorf("expected vhost: %s", host) - } - - if actualPermission.Configure != permission.Configure { - return fmt.Errorf("expected permission %s to be %s, got %s", "configure", permission.Configure, actualPermission.Configure) - } - - if actualPermission.Write != permission.Write { - return fmt.Errorf("expected permission %s to be %s, got %s", "write", permission.Write, actualPermission.Write) - } - - if actualPermission.Read != permission.Read { - return fmt.Errorf("expected permission %s to be %s, got %s", "read", permission.Read, actualPermission.Read) - } - } - - var vhostTopics map[string]map[string]vhostTopicPermission - if err := jsonutil.DecodeJSON([]byte(rawVHostTopics), &vhostTopics); err != nil { - return fmt.Errorf("bad expected vhostTopics %#v: %s", vhostTopics, err) - } - - for host, permissions := range vhostTopics { - for exchange, permission := range permissions { - actualPermissions, ok := d.VHostTopics[host] - if !ok { - return fmt.Errorf("expected vhost topics: %s", host) - } - - actualPermission, ok := actualPermissions[exchange] - if !ok { - return fmt.Errorf("expected vhost topic exchange: %s", exchange) - } - - if actualPermission.Write != permission.Write { - return fmt.Errorf("expected permission %s to be %s, got %s", "write", permission.Write, actualPermission.Write) - } - - if actualPermission.Read != permission.Read { - return fmt.Errorf("expected permission %s to be %s, got %s", "read", permission.Read, actualPermission.Read) - } - } - } - - return nil - }, - } -} diff --git a/builtin/logical/rabbitmq/path_config_connection_test.go b/builtin/logical/rabbitmq/path_config_connection_test.go deleted file mode 100644 index 8e7de881c..000000000 --- a/builtin/logical/rabbitmq/path_config_connection_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package rabbitmq - -import ( - "context" - "reflect" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestBackend_ConfigConnection_DefaultUsernameTemplate(t *testing.T) { - var resp *logical.Response - var err error - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b := Backend() - if err = b.Setup(context.Background(), config); err != nil { - t.Fatal(err) - } - - configData := map[string]interface{}{ - "connection_uri": "uri", - "username": "username", - "password": "password", - "verify_connection": "false", - } - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/connection", - Storage: config.StorageView, - Data: configData, - } - resp, err = b.HandleRequest(context.Background(), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp != nil { - t.Fatal("expected a nil response") - } - - actualConfig, err := readConfig(context.Background(), config.StorageView) - if err != nil { - t.Fatalf("unable to read configuration: %v", err) - } - - expectedConfig := connectionConfig{ - URI: "uri", - Username: "username", - Password: "password", - UsernameTemplate: "", - } - - if !reflect.DeepEqual(actualConfig, expectedConfig) { - t.Fatalf("Expected: %#v\nActual: %#v", expectedConfig, actualConfig) - } -} - -func TestBackend_ConfigConnection_CustomUsernameTemplate(t *testing.T) { - var resp *logical.Response - var err error - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b := Backend() - if err = b.Setup(context.Background(), config); err != nil { - t.Fatal(err) - } - - configData := map[string]interface{}{ - "connection_uri": "uri", - "username": "username", - "password": "password", - "verify_connection": "false", - "username_template": "{{ .DisplayName }}", - } - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/connection", - Storage: config.StorageView, - Data: configData, - } - resp, err = b.HandleRequest(context.Background(), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp != nil { - t.Fatal("expected a nil response") - } - - actualConfig, err := readConfig(context.Background(), config.StorageView) - if err != nil { - t.Fatalf("unable to read configuration: %v", err) - } - - expectedConfig := connectionConfig{ - URI: "uri", - Username: "username", - Password: "password", - UsernameTemplate: "{{ .DisplayName }}", - } - - if !reflect.DeepEqual(actualConfig, expectedConfig) { - t.Fatalf("Expected: %#v\nActual: %#v", expectedConfig, actualConfig) - } -} diff --git a/builtin/logical/rabbitmq/path_config_lease_test.go b/builtin/logical/rabbitmq/path_config_lease_test.go deleted file mode 100644 index 542a5d284..000000000 --- a/builtin/logical/rabbitmq/path_config_lease_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package rabbitmq - -import ( - "context" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestBackend_config_lease_RU(t *testing.T) { - var resp *logical.Response - var err error - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b := Backend() - if err = b.Setup(context.Background(), config); err != nil { - t.Fatal(err) - } - - configData := map[string]interface{}{ - "ttl": "10h", - "max_ttl": "20h", - } - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/lease", - Storage: config.StorageView, - Data: configData, - } - resp, err = b.HandleRequest(context.Background(), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp != nil { - t.Fatal("expected a nil response") - } - - configReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp == nil { - t.Fatal("expected a response") - } - - if resp.Data["ttl"].(time.Duration) != 36000 { - t.Fatalf("bad: ttl: expected:36000 actual:%d", resp.Data["ttl"].(time.Duration)) - } - if resp.Data["max_ttl"].(time.Duration) != 72000 { - t.Fatalf("bad: ttl: expected:72000 actual:%d", resp.Data["ttl"].(time.Duration)) - } -} diff --git a/builtin/logical/rabbitmq/path_role_create_test.go b/builtin/logical/rabbitmq/path_role_create_test.go deleted file mode 100644 index 0f2591caf..000000000 --- a/builtin/logical/rabbitmq/path_role_create_test.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package rabbitmq - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" -) - -func TestBackend_RoleCreate_DefaultUsernameTemplate(t *testing.T) { - cleanup, connectionURI := prepareRabbitMQTestContainer(t) - defer cleanup() - - var resp *logical.Response - var err error - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b := Backend() - if err = b.Setup(context.Background(), config); err != nil { - t.Fatal(err) - } - - configData := map[string]interface{}{ - "connection_uri": connectionURI, - "username": "guest", - "password": "guest", - "username_template": "", - } - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/connection", - Storage: config.StorageView, - Data: configData, - } - resp, err = b.HandleRequest(context.Background(), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp != nil { - t.Fatal("expected a nil response") - } - - roleData := map[string]interface{}{ - "name": "foo", - "tags": "bar", - } - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/foo", - Storage: config.StorageView, - Data: roleData, - } - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp != nil { - t.Fatal("expected a nil response") - } - - credsReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/foo", - Storage: config.StorageView, - DisplayName: "token", - } - resp, err = b.HandleRequest(context.Background(), credsReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp == nil { - t.Fatal("missing creds response") - } - if resp.Data == nil { - t.Fatalf("missing creds data") - } - - username, exists := resp.Data["username"] - if !exists { - t.Fatalf("missing username in response") - } - - require.Regexp(t, `^token-[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$`, username) -} - -func TestBackend_RoleCreate_CustomUsernameTemplate(t *testing.T) { - cleanup, connectionURI := prepareRabbitMQTestContainer(t) - defer cleanup() - - var resp *logical.Response - var err error - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b := Backend() - if err = b.Setup(context.Background(), config); err != nil { - t.Fatal(err) - } - - configData := map[string]interface{}{ - "connection_uri": connectionURI, - "username": "guest", - "password": "guest", - "username_template": "foo-{{ .DisplayName }}", - } - configReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/connection", - Storage: config.StorageView, - Data: configData, - } - resp, err = b.HandleRequest(context.Background(), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp != nil { - t.Fatal("expected a nil response") - } - - roleData := map[string]interface{}{ - "name": "foo", - "tags": "bar", - } - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/foo", - Storage: config.StorageView, - Data: roleData, - } - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp != nil { - t.Fatal("expected a nil response") - } - - credsReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "creds/foo", - Storage: config.StorageView, - DisplayName: "token", - } - resp, err = b.HandleRequest(context.Background(), credsReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: resp: %#v\nerr:%s", resp, err) - } - if resp == nil { - t.Fatal("missing creds response") - } - if resp.Data == nil { - t.Fatalf("missing creds data") - } - - username, exists := resp.Data["username"] - if !exists { - t.Fatalf("missing username in response") - } - - require.Regexp(t, `^foo-token$`, username) -} diff --git a/builtin/logical/ssh/backend_test.go b/builtin/logical/ssh/backend_test.go deleted file mode 100644 index 3d5bcaa7e..000000000 --- a/builtin/logical/ssh/backend_test.go +++ /dev/null @@ -1,2790 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package ssh - -import ( - "bytes" - "context" - "encoding/base64" - "errors" - "fmt" - "net" - "reflect" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/credential/userpass" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/mapstructure" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/ssh" -) - -const ( - testIP = "127.0.0.1" - testUserName = "vaultssh" - testMultiUserName = "vaultssh,otherssh" - testAdminUser = "vaultssh" - testCaKeyType = "ca" - testOTPKeyType = "otp" - testCIDRList = "127.0.0.1/32" - testAtRoleName = "test@RoleName" - testOTPRoleName = "testOTPRoleName" - // testKeyName is the name of the entry that will be written to SSHMOUNTPOINT/ssh/keys - testKeyName = "testKeyName" - // testSharedPrivateKey is the value of the entry that will be written to SSHMOUNTPOINT/ssh/keys - testSharedPrivateKey = ` ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAvYvoRcWRxqOim5VZnuM6wHCbLUeiND0yaM1tvOl+Fsrz55DG -A0OZp4RGAu1Fgr46E1mzxFz1+zY4UbcEExg+u21fpa8YH8sytSWW1FyuD8ICib0A -/l8slmDMw4BkkGOtSlEqgscpkpv/TWZD1NxJWkPcULk8z6c7TOETn2/H9mL+v2RE -mbE6NDEwJKfD3MvlpIqCP7idR+86rNBAODjGOGgyUbtFLT+K01XmDRALkV3V/nh+ -GltyjL4c6RU4zG2iRyV5RHlJtkml+UzUMkzr4IQnkCC32CC/wmtoo/IsAprpcHVe -nkBn3eFQ7uND70p5n6GhN/KOh2j519JFHJyokwIDAQABAoIBAHX7VOvBC3kCN9/x -+aPdup84OE7Z7MvpX6w+WlUhXVugnmsAAVDczhKoUc/WktLLx2huCGhsmKvyVuH+ -MioUiE+vx75gm3qGx5xbtmOfALVMRLopjCnJYf6EaFA0ZeQ+NwowNW7Lu0PHmAU8 -Z3JiX8IwxTz14DU82buDyewO7v+cEr97AnERe3PUcSTDoUXNaoNxjNpEJkKREY6h -4hAY676RT/GsRcQ8tqe/rnCqPHNd7JGqL+207FK4tJw7daoBjQyijWuB7K5chSal -oPInylM6b13ASXuOAOT/2uSUBWmFVCZPDCmnZxy2SdnJGbsJAMl7Ma3MUlaGvVI+ -Tfh1aQkCgYEA4JlNOabTb3z42wz6mz+Nz3JRwbawD+PJXOk5JsSnV7DtPtfgkK9y -6FTQdhnozGWShAvJvc+C4QAihs9AlHXoaBY5bEU7R/8UK/pSqwzam+MmxmhVDV7G -IMQPV0FteoXTaJSikhZ88mETTegI2mik+zleBpVxvfdhE5TR+lq8Br0CgYEA2AwJ -CUD5CYUSj09PluR0HHqamWOrJkKPFPwa+5eiTTCzfBBxImYZh7nXnWuoviXC0sg2 -AuvCW+uZ48ygv/D8gcz3j1JfbErKZJuV+TotK9rRtNIF5Ub7qysP7UjyI7zCssVM -kuDd9LfRXaB/qGAHNkcDA8NxmHW3gpln4CFdSY8CgYANs4xwfercHEWaJ1qKagAe -rZyrMpffAEhicJ/Z65lB0jtG4CiE6w8ZeUMWUVJQVcnwYD+4YpZbX4S7sJ0B8Ydy -AhkSr86D/92dKTIt2STk6aCN7gNyQ1vW198PtaAWH1/cO2UHgHOy3ZUt5X/Uwxl9 -cex4flln+1Viumts2GgsCQKBgCJH7psgSyPekK5auFdKEr5+Gc/jB8I/Z3K9+g4X -5nH3G1PBTCJYLw7hRzw8W/8oALzvddqKzEFHphiGXK94Lqjt/A4q1OdbCrhiE68D -My21P/dAKB1UYRSs9Y8CNyHCjuZM9jSMJ8vv6vG/SOJPsnVDWVAckAbQDvlTHC9t -O98zAoGAcbW6uFDkrv0XMCpB9Su3KaNXOR0wzag+WIFQRXCcoTvxVi9iYfUReQPi -oOyBJU/HMVvBfv4g+OVFLVgSwwm6owwsouZ0+D/LasbuHqYyqYqdyPJQYzWA2Y+F -+B6f4RoPdSXj24JHPg/ioRxjaj094UXJxua2yfkcecGNEuBQHSs= ------END RSA PRIVATE KEY----- -` - // Public half of `testCAPrivateKey`, identical to how it would be fed in from a file - testCAPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDArgK0ilRRfk8E7HIsjz5l3BuxmwpDd8DHRCVfOhbZ4gOSVxjEOOqBwWGjygdboBIZwFXmwDlU6sWX0hBJAgpQz0Cjvbjxtq/NjkvATrYPgnrXUhTaEn2eQO0PsqRNSFH46SK/oJfTp0q8/WgojxWJ2L7FUV8PO8uIk49DzqAqPV7WXU63vFsjx+3WQOX/ILeQvHCvaqs3dWjjzEoDudRWCOdUqcHEOshV9azIzPrXlQVzRV3QAKl6u7pC+/Secorpwt6IHpMKoVPGiR0tMMuNOVH8zrAKzIxPGfy2WmNDpJopbXMTvSOGAqNcp49O4SKOQl9Fzfq2HEevJamKLrMB dummy@example.com -` - publicKey2 = `AAAAB3NzaC1yc2EAAAADAQABAAABAQDArgK0ilRRfk8E7HIsjz5l3BuxmwpDd8DHRCVfOhbZ4gOSVxjEOOqBwWGjygdboBIZwFXmwDlU6sWX0hBJAgpQz0Cjvbjxtq/NjkvATrYPgnrXUhTaEn2eQO0PsqRNSFH46SK/oJfTp0q8/WgojxWJ2L7FUV8PO8uIk49DzqAqPV7WXU63vFsjx+3WQOX/ILeQvHCvaqs3dWjjzEoDudRWCOdUqcHEOshV9azIzPrXlQVzRV3QAKl6u7pC+/Secorpwt6IHpMKoVPGiR0tMMuNOVH8zrAKzIxPGfy2WmNDpJopbXMTvSOGAqNcp49O4SKOQl9Fzfq2HEevJamKLrMB -` - - publicKey3072 = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDlsMr3K1d0nzE1TjUULPRuVjEGETmOqHtWq4gVPq3HiuNVHE/e/BJnkXc40BoClQ2Z5ZZPJZ6izF9PnlzNDjpq8DrILUrn/6KrzCHvRwnkYMAXbfM/Br09z5QGptbOe1EMLeVe0b/udmUicbYAGPxMruZk+ljyr4vXkO+gOAIrxeSIQSdMVLU4g0pCPQuDCOx5IQpDYSlOB3091frpN8npfMueKPflNYzxnqqYgAVeDKAIqMCGOMOHUeIZJ7A7HuynEAVOsOkJwC9nesy9D6ppdWNduGl42IkzlwVdDMZtUAEznMUT/dnHNG1Krx9SuNZ/S9fGjxGVsT+jzUmizrWB9/6XIEHDxPBzcqlWFuwYTGz1OL8bfZ+HldOGPcnqZn9hKntWwjUc3whcvWt+NCmXpHSVLSxf+WN8pdmfEsCqn8mpvo2MXa+iJrtAVPX4i0u8AQUuqC3NuXHv4Cn0LNwtziBT544UjgbWkAZqzFZJREYA09OHscc3akEIrTnPehk= demo@example.com` - - publicKey4096 = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC54Oj4YCFDYxYv69Q9KfU6rWYtUB1eByQdUW0nXFi/vr98QUIV77sEeUVhaQzZcuCojAi/GrloW7ta0Z2DaEv5jOQMAnGpXBcqLJsz3KdrHbpvl93MPNdmNaGPU0GnUEsjBVuDVn9HdIUa8CNrxShvPu7/VqoaRHKLqphGgzFb37vi4qvnQ+5VYAO/TzyVYMD6qJX6I/9Pw8d74jCfEdOh2yGKkP7rXWOghreyIl8H2zTJKg9KoZuPq9F5M8nNt7Oi3rf+DwQiYvamzIqlDP4s5oFVTZW0E9lwWvYDpyiJnUrkQqksebBK/rcyfiFG3onb4qLo2WVWXeK3si8IhGik/TEzprScyAWIf9RviT8O+l5hTA2/c+ctn3MVCLRNfez2lKpdxCoprv1MbIcySGWblTJEcY6RA+aauVJpu7FMtRxHHtZKtMpep8cLu8GKbiP6Ifq2JXBtXtNxDeIgo2MkNoMh/NHAsACJniE/dqV/+u9HvhvgrTbJ69ell0nE4ivzA7O4kZgbR/4MHlLgLFvaqC8RrWRLY6BdFagPIMxghWha7Qw16zqoIjRnolvRzUWvSXanJVg8Z6ua1VxwgirNaAH1ivmJhUh2+4lNxCX6jmZyR3zjJsWY03gjJTairvI762opjjalF8fH6Xrs15mB14JiAlNbk6+5REQcvXlGqw== dummy@example.com` - - testCAPrivateKey = `-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAwK4CtIpUUX5PBOxyLI8+ZdwbsZsKQ3fAx0QlXzoW2eIDklcY -xDjqgcFho8oHW6ASGcBV5sA5VOrFl9IQSQIKUM9Ao7248bavzY5LwE62D4J611IU -2hJ9nkDtD7KkTUhR+Okiv6CX06dKvP1oKI8Vidi+xVFfDzvLiJOPQ86gKj1e1l1O -t7xbI8ft1kDl/yC3kLxwr2qrN3Vo48xKA7nUVgjnVKnBxDrIVfWsyMz615UFc0Vd -0ACperu6Qvv0nnKK6cLeiB6TCqFTxokdLTDLjTlR/M6wCsyMTxn8tlpjQ6SaKW1z -E70jhgKjXKePTuEijkJfRc36thxHryWpii6zAQIDAQABAoIBAA/DrPD8iF2KigiL -F+RRa/eFhLaJStOuTpV/G9eotwnolgY5Hguf5H/tRIHUG7oBZLm6pMyWWZp7AuOj -CjYO9q0Z5939vc349nVI+SWoyviF4msPiik1bhWulja8lPjFu/8zg+ZNy15Dx7ei -vAzleAupMiKOv8pNSB/KguQ3WZ9a9bcQcoFQ2Foru6mXpLJ03kghVRlkqvQ7t5cA -n11d2Hiipq9mleESr0c+MUPKLBX/neaWfGA4xgJTjIYjZi6avmYc/Ox3sQ9aLq2J -tH0D4HVUZvaU28hn+jhbs64rRFbu++qQMe3vNvi/Q/iqcYU4b6tgDNzm/JFRTS/W -njiz4mkCgYEA44CnQVmonN6qQ0AgNNlBY5+RX3wwBJZ1AaxpzwDRylAt2vlVUA0n -YY4RW4J4+RMRKwHwjxK5RRmHjsIJx+nrpqihW3fte3ev5F2A9Wha4dzzEHxBY6IL -362T/x2f+vYk6tV+uTZSUPHsuELH26mitbBVFNB/00nbMNdEc2bO5FMCgYEA2NCw -ubt+g2bRkkT/Qf8gIM8ZDpZbARt6onqxVcWkQFT16ZjbsBWUrH1Xi7alv9+lwYLJ -ckY/XDX4KeU19HabeAbpyy6G9Q2uBSWZlJbjl7QNhdLeuzV82U1/r8fy6Uu3gQnU -WSFx2GesRpSmZpqNKMs5ksqteZ9Yjg1EIgXdINsCgYBIn9REt1NtKGOf7kOZu1T1 -cYXdvm4xuLoHW7u3OiK+e9P3mCqU0G4m5UxDMyZdFKohWZAqjCaamWi9uNGYgOMa -I7DG20TzaiS7OOIm9TY17eul8pSJMrypnealxRZB7fug/6Bhjaa/cktIEwFr7P4l -E/JFH73+fBA9yipu0H3xQwKBgHmiwrLAZF6VrVcxDD9bQQwHA5iyc4Wwg+Fpkdl7 -0wUgZQHTdtRXlxwaCaZhJqX5c4WXuSo6DMvPn1TpuZZXgCsbPch2ZtJOBWXvzTSW -XkK6iaedQMWoYU2L8+mK9FU73EwxVodWgwcUSosiVCRV6oGLWdZnjGEiK00uVh38 -Si1nAoGBAL47wWinv1cDTnh5mm0mybz3oI2a6V9aIYCloQ/EFcvtahyR/gyB8qNF -lObH9Faf0WGdnACZvTz22U9gWhw79S0SpDV31tC5Kl8dXHFiZ09vYUKkYmSd/kms -SeKWrUkryx46LVf6NMhkyYmRqCEjBwfOozzezi5WbiJy6nn54GQt ------END RSA PRIVATE KEY----- -` - - testCAPublicKeyEd25519 = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO1S6g5Bib7vT8eoFnvTl3dZSjOQL/GkH1nkRcDS9++a ca -` - - testCAPrivateKeyEd25519 = `-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACDtUuoOQYm+70/HqBZ705d3WUozkC/xpB9Z5EXA0vfvmgAAAIhfRuszX0br -MwAAAAtzc2gtZWQyNTUxOQAAACDtUuoOQYm+70/HqBZ705d3WUozkC/xpB9Z5EXA0vfvmg -AAAEBQYa029SP/7AGPFQLmzwOc9eCoOZuwCq3iIf2C6fj9j+1S6g5Bib7vT8eoFnvTl3dZ -SjOQL/GkH1nkRcDS9++aAAAAAmNhAQID ------END OPENSSH PRIVATE KEY----- -` - - publicKeyECDSA256 = `ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJsfOouYIjJNI23QJqaDsFTGukm21fRAMeGvKZDB59i5jnX1EubMH1AEjjzz4fgySUlyWKo+TS31rxU8kX3DDM4= demo@example.com` - publicKeyECDSA521 = `ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAEg73ORD4J3FV2CrL01gLSKREO2EHrZPlJCOeDL5OKD3M1GCHv3q8O452RW49Aw+8zFFFU5u6d1Ys3Qsj05zdaQwQDt/D3ceWLGVkWiKyLPQStfn0GGOZh3YFKEw5XmeW9jh6xudEHlKs4Pfv2FrroaUKZvM2SlxR/feOK0tCQyq3MN/g== demo@example.com` - - // testPublicKeyInstall is the public key that is installed in the - // admin account's authorized_keys - testPublicKeyInstall = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9i+hFxZHGo6KblVme4zrAcJstR6I0PTJozW286X4WyvPnkMYDQ5mnhEYC7UWCvjoTWbPEXPX7NjhRtwQTGD67bV+lrxgfyzK1JZbUXK4PwgKJvQD+XyyWYMzDgGSQY61KUSqCxymSm/9NZkPU3ElaQ9xQuTzPpztM4ROfb8f2Yv6/ZESZsTo0MTAkp8Pcy+WkioI/uJ1H7zqs0EA4OMY4aDJRu0UtP4rTVeYNEAuRXdX+eH4aW3KMvhzpFTjMbaJHJXlEeUm2SaX5TNQyTOvghCeQILfYIL/Ca2ij8iwCmulwdV6eQGfd4VDu40PvSnmfoaE38o6HaPnX0kUcnKiT" - - dockerImageTagSupportsRSA1 = "8.1_p1-r0-ls20" - dockerImageTagSupportsNoRSA1 = "8.4_p1-r3-ls48" -) - -var ctx = context.Background() - -func prepareTestContainer(t *testing.T, tag, caPublicKeyPEM string) (func(), string) { - if tag == "" { - tag = dockerImageTagSupportsNoRSA1 - } - runner, err := docker.NewServiceRunner(docker.RunOptions{ - ContainerName: "openssh", - ImageRepo: "docker.mirror.hashicorp.services/linuxserver/openssh-server", - ImageTag: tag, - Env: []string{ - "DOCKER_MODS=linuxserver/mods:openssh-server-openssh-client", - "PUBLIC_KEY=" + testPublicKeyInstall, - "SUDO_ACCESS=true", - "USER_NAME=vaultssh", - }, - Ports: []string{"2222/tcp"}, - }) - if err != nil { - t.Fatalf("Could not start local ssh docker container: %s", err) - } - - svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - ipaddr, err := net.ResolveIPAddr("ip", host) - if err != nil { - return nil, err - } - sshAddress := fmt.Sprintf("%s:%d", ipaddr.String(), port) - - signer, err := ssh.ParsePrivateKey([]byte(testSharedPrivateKey)) - if err != nil { - return nil, err - } - - // Install util-linux for non-busybox flock that supports timeout option - err = testSSH("vaultssh", sshAddress, ssh.PublicKeys(signer), fmt.Sprintf(` - set -e; - sudo ln -s /config /home/vaultssh - sudo apk add util-linux; - echo "LogLevel DEBUG" | sudo tee -a /config/ssh_host_keys/sshd_config; - echo "TrustedUserCAKeys /config/ssh_host_keys/trusted-user-ca-keys.pem" | sudo tee -a /config/ssh_host_keys/sshd_config; - kill -HUP $(cat /config/sshd.pid) - echo "%s" | sudo tee /config/ssh_host_keys/trusted-user-ca-keys.pem - `, caPublicKeyPEM)) - if err != nil { - return nil, err - } - - return docker.NewServiceHostPort(ipaddr.String(), port), nil - }) - if err != nil { - t.Fatalf("Could not start docker ssh server: %s", err) - } - return svc.Cleanup, svc.Config.Address() -} - -func testSSH(user, host string, auth ssh.AuthMethod, command string) error { - client, err := ssh.Dial("tcp", host, &ssh.ClientConfig{ - User: user, - Auth: []ssh.AuthMethod{auth}, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - Timeout: 5 * time.Second, - }) - if err != nil { - return fmt.Errorf("unable to dial sshd to host %q: %v", host, err) - } - session, err := client.NewSession() - if err != nil { - return fmt.Errorf("unable to create sshd session to host %q: %v", host, err) - } - var stderr bytes.Buffer - session.Stderr = &stderr - defer session.Close() - err = session.Run(command) - if err != nil { - return fmt.Errorf("command %v failed, error: %v, stderr: %v", command, err, stderr.String()) - } - return nil -} - -func TestBackend_AllowedUsers(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - - b, err := Backend(config) - if err != nil { - t.Fatal(err) - } - err = b.Setup(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - roleData := map[string]interface{}{ - "key_type": "otp", - "default_user": "ubuntu", - "cidr_list": "52.207.235.245/16", - "allowed_users": "test", - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/role1", - Storage: config.StorageView, - Data: roleData, - } - - resp, err := b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) || resp != nil { - t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) - } - - credsData := map[string]interface{}{ - "ip": "52.207.235.245", - "username": "ubuntu", - } - credsReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: config.StorageView, - Path: "creds/role1", - Data: credsData, - } - - resp, err = b.HandleRequest(context.Background(), credsReq) - if err != nil || (resp != nil && resp.IsError()) || resp == nil { - t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) - } - if resp.Data["key"] == "" || - resp.Data["key_type"] != "otp" || - resp.Data["ip"] != "52.207.235.245" || - resp.Data["username"] != "ubuntu" { - t.Fatalf("failed to create credential: resp:%#v", resp) - } - - credsData["username"] = "test" - resp, err = b.HandleRequest(context.Background(), credsReq) - if err != nil || (resp != nil && resp.IsError()) || resp == nil { - t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) - } - if resp.Data["key"] == "" || - resp.Data["key_type"] != "otp" || - resp.Data["ip"] != "52.207.235.245" || - resp.Data["username"] != "test" { - t.Fatalf("failed to create credential: resp:%#v", resp) - } - - credsData["username"] = "random" - resp, err = b.HandleRequest(context.Background(), credsReq) - if err != nil || resp == nil || (resp != nil && !resp.IsError()) { - t.Fatalf("expected failure: resp:%#v err:%s", resp, err) - } - - delete(roleData, "allowed_users") - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) || resp != nil { - t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) - } - - credsData["username"] = "ubuntu" - resp, err = b.HandleRequest(context.Background(), credsReq) - if err != nil || (resp != nil && resp.IsError()) || resp == nil { - t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) - } - if resp.Data["key"] == "" || - resp.Data["key_type"] != "otp" || - resp.Data["ip"] != "52.207.235.245" || - resp.Data["username"] != "ubuntu" { - t.Fatalf("failed to create credential: resp:%#v", resp) - } - - credsData["username"] = "test" - resp, err = b.HandleRequest(context.Background(), credsReq) - if err != nil || resp == nil || (resp != nil && !resp.IsError()) { - t.Fatalf("expected failure: resp:%#v err:%s", resp, err) - } - - roleData["allowed_users"] = "*" - resp, err = b.HandleRequest(context.Background(), roleReq) - if err != nil || (resp != nil && resp.IsError()) || resp != nil { - t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) - } - - resp, err = b.HandleRequest(context.Background(), credsReq) - if err != nil || (resp != nil && resp.IsError()) || resp == nil { - t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) - } - if resp.Data["key"] == "" || - resp.Data["key_type"] != "otp" || - resp.Data["ip"] != "52.207.235.245" || - resp.Data["username"] != "test" { - t.Fatalf("failed to create credential: resp:%#v", resp) - } -} - -func TestBackend_AllowedDomainsTemplate(t *testing.T) { - testAllowedDomainsTemplate := "{{ identity.entity.metadata.ssh_username }}.example.com" - expectedValidPrincipal := "foo." + testUserName + ".example.com" - testAllowedPrincipalsTemplate( - t, testAllowedDomainsTemplate, - expectedValidPrincipal, - map[string]string{ - "ssh_username": testUserName, - }, - map[string]interface{}{ - "key_type": testCaKeyType, - "algorithm_signer": "rsa-sha2-256", - "allow_host_certificates": true, - "allow_subdomains": true, - "allowed_domains": testAllowedDomainsTemplate, - "allowed_domains_template": true, - }, - map[string]interface{}{ - "cert_type": "host", - "public_key": testCAPublicKey, - "valid_principals": expectedValidPrincipal, - }, - ) -} - -func TestBackend_AllowedUsersTemplate(t *testing.T) { - testAllowedUsersTemplate(t, - "{{ identity.entity.metadata.ssh_username }}", - testUserName, map[string]string{ - "ssh_username": testUserName, - }, - ) -} - -func TestBackend_MultipleAllowedUsersTemplate(t *testing.T) { - testAllowedUsersTemplate(t, - "{{ identity.entity.metadata.ssh_username }}", - testUserName, map[string]string{ - "ssh_username": testMultiUserName, - }, - ) -} - -func TestBackend_AllowedUsersTemplate_WithStaticPrefix(t *testing.T) { - testAllowedUsersTemplate(t, - "ssh-{{ identity.entity.metadata.ssh_username }}", - "ssh-"+testUserName, map[string]string{ - "ssh_username": testUserName, - }, - ) -} - -func TestBackend_DefaultUserTemplate(t *testing.T) { - testDefaultUserTemplate(t, - "{{ identity.entity.metadata.ssh_username }}", - testUserName, - map[string]string{ - "ssh_username": testUserName, - }, - ) -} - -func TestBackend_DefaultUserTemplate_WithStaticPrefix(t *testing.T) { - testDefaultUserTemplate(t, - "user-{{ identity.entity.metadata.ssh_username }}", - "user-"+testUserName, - map[string]string{ - "ssh_username": testUserName, - }, - ) -} - -func TestBackend_DefaultUserTemplateFalse_AllowedUsersTemplateTrue(t *testing.T) { - cluster, userpassToken := getSshCaTestCluster(t, testUserName) - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // set metadata "ssh_username" to userpass username - tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{ - "token": userpassToken, - }) - if err != nil { - t.Fatal(err) - } - entityID := tokenLookupResponse.Data["entity_id"].(string) - _, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{ - "metadata": map[string]string{ - "ssh_username": testUserName, - }, - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{ - "key_type": testCaKeyType, - "allow_user_certificates": true, - "default_user": "{{identity.entity.metadata.ssh_username}}", - // disable user templating but not allowed_user_template and the request should fail - "default_user_template": false, - "allowed_users": "{{identity.entity.metadata.ssh_username}}", - "allowed_users_template": true, - }) - if err != nil { - t.Fatal(err) - } - - // sign SSH key as userpass user - client.SetToken(userpassToken) - _, err = client.Logical().Write("ssh/sign/my-role", map[string]interface{}{ - "public_key": testCAPublicKey, - }) - if err == nil { - t.Errorf("signing request should fail when default_user is not in the allowed_users list, because allowed_users_template is true and default_user_template is not") - } - - expectedErrStr := "{{identity.entity.metadata.ssh_username}} is not a valid value for valid_principals" - if !strings.Contains(err.Error(), expectedErrStr) { - t.Errorf("expected error to include %q but it was: %q", expectedErrStr, err.Error()) - } -} - -func TestBackend_DefaultUserTemplateFalse_AllowedUsersTemplateFalse(t *testing.T) { - cluster, userpassToken := getSshCaTestCluster(t, testUserName) - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // set metadata "ssh_username" to userpass username - tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{ - "token": userpassToken, - }) - if err != nil { - t.Fatal(err) - } - entityID := tokenLookupResponse.Data["entity_id"].(string) - _, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{ - "metadata": map[string]string{ - "ssh_username": testUserName, - }, - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{ - "key_type": testCaKeyType, - "allow_user_certificates": true, - "default_user": "{{identity.entity.metadata.ssh_username}}", - "default_user_template": false, - "allowed_users": "{{identity.entity.metadata.ssh_username}}", - "allowed_users_template": false, - }) - if err != nil { - t.Fatal(err) - } - - // sign SSH key as userpass user - client.SetToken(userpassToken) - signResponse, err := client.Logical().Write("ssh/sign/my-role", map[string]interface{}{ - "public_key": testCAPublicKey, - }) - if err != nil { - t.Fatal(err) - } - - // check for the expected valid principals of certificate - signedKey := signResponse.Data["signed_key"].(string) - key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) - parsedKey, err := ssh.ParsePublicKey(key) - if err != nil { - t.Fatal(err) - } - actualPrincipals := parsedKey.(*ssh.Certificate).ValidPrincipals - if len(actualPrincipals) < 1 { - t.Fatal( - fmt.Sprintf("No ValidPrincipals returned: should have been %v", - []string{"{{identity.entity.metadata.ssh_username}}"}), - ) - } - if len(actualPrincipals) > 1 { - t.Error( - fmt.Sprintf("incorrect number ValidPrincipals, expected only 1: %v should be %v", - actualPrincipals, []string{"{{identity.entity.metadata.ssh_username}}"}), - ) - } - if actualPrincipals[0] != "{{identity.entity.metadata.ssh_username}}" { - t.Fatal( - fmt.Sprintf("incorrect ValidPrincipals: %v should be %v", - actualPrincipals, []string{"{{identity.entity.metadata.ssh_username}}"}), - ) - } -} - -func newTestingFactory(t *testing.T) func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { - return func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { - defaultLeaseTTLVal := 2 * time.Minute - maxLeaseTTLVal := 10 * time.Minute - return Factory(context.Background(), &logical.BackendConfig{ - Logger: corehelpers.NewTestLogger(t), - StorageView: &logical.InmemStorage{}, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: defaultLeaseTTLVal, - MaxLeaseTTLVal: maxLeaseTTLVal, - }, - }) - } -} - -func TestSSHBackend_Lookup(t *testing.T) { - testOTPRoleData := map[string]interface{}{ - "key_type": testOTPKeyType, - "default_user": testUserName, - "cidr_list": testCIDRList, - } - data := map[string]interface{}{ - "ip": testIP, - } - resp1 := []string(nil) - resp2 := []string{testOTPRoleName} - resp3 := []string{testAtRoleName} - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: newTestingFactory(t), - Steps: []logicaltest.TestStep{ - testLookupRead(t, data, resp1), - testRoleWrite(t, testOTPRoleName, testOTPRoleData), - testLookupRead(t, data, resp2), - testRoleDelete(t, testOTPRoleName), - testLookupRead(t, data, resp1), - testRoleWrite(t, testAtRoleName, testOTPRoleData), - testLookupRead(t, data, resp3), - testRoleDelete(t, testAtRoleName), - testLookupRead(t, data, resp1), - }, - }) -} - -func TestSSHBackend_RoleList(t *testing.T) { - testOTPRoleData := map[string]interface{}{ - "key_type": testOTPKeyType, - "default_user": testUserName, - "cidr_list": testCIDRList, - } - resp1 := map[string]interface{}{} - resp2 := map[string]interface{}{ - "keys": []string{testOTPRoleName}, - "key_info": map[string]interface{}{ - testOTPRoleName: map[string]interface{}{ - "key_type": testOTPKeyType, - }, - }, - } - resp3 := map[string]interface{}{ - "keys": []string{testAtRoleName, testOTPRoleName}, - "key_info": map[string]interface{}{ - testOTPRoleName: map[string]interface{}{ - "key_type": testOTPKeyType, - }, - testAtRoleName: map[string]interface{}{ - "key_type": testOTPKeyType, - }, - }, - } - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: newTestingFactory(t), - Steps: []logicaltest.TestStep{ - testRoleList(t, resp1), - testRoleWrite(t, testOTPRoleName, testOTPRoleData), - testRoleList(t, resp2), - testRoleWrite(t, testAtRoleName, testOTPRoleData), - testRoleList(t, resp3), - testRoleDelete(t, testAtRoleName), - testRoleList(t, resp2), - testRoleDelete(t, testOTPRoleName), - testRoleList(t, resp1), - }, - }) -} - -func TestSSHBackend_OTPRoleCrud(t *testing.T) { - testOTPRoleData := map[string]interface{}{ - "key_type": testOTPKeyType, - "default_user": testUserName, - "cidr_list": testCIDRList, - } - respOTPRoleData := map[string]interface{}{ - "key_type": testOTPKeyType, - "port": 22, - "default_user": testUserName, - "cidr_list": testCIDRList, - } - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: newTestingFactory(t), - Steps: []logicaltest.TestStep{ - testRoleWrite(t, testOTPRoleName, testOTPRoleData), - testRoleRead(t, testOTPRoleName, respOTPRoleData), - testRoleDelete(t, testOTPRoleName), - testRoleRead(t, testOTPRoleName, nil), - testRoleWrite(t, testAtRoleName, testOTPRoleData), - testRoleRead(t, testAtRoleName, respOTPRoleData), - testRoleDelete(t, testAtRoleName), - testRoleRead(t, testAtRoleName, nil), - }, - }) -} - -func TestSSHBackend_OTPCreate(t *testing.T) { - cleanup, sshAddress := prepareTestContainer(t, "", "") - defer func() { - if !t.Failed() { - cleanup() - } - }() - - host, port, err := net.SplitHostPort(sshAddress) - if err != nil { - t.Fatal(err) - } - - testOTPRoleData := map[string]interface{}{ - "key_type": testOTPKeyType, - "default_user": testUserName, - "cidr_list": host + "/32", - "port": port, - } - data := map[string]interface{}{ - "username": testUserName, - "ip": host, - } - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: newTestingFactory(t), - Steps: []logicaltest.TestStep{ - testRoleWrite(t, testOTPRoleName, testOTPRoleData), - testCredsWrite(t, testOTPRoleName, data, false, sshAddress), - }, - }) -} - -func TestSSHBackend_VerifyEcho(t *testing.T) { - verifyData := map[string]interface{}{ - "otp": api.VerifyEchoRequest, - } - expectedData := map[string]interface{}{ - "message": api.VerifyEchoResponse, - } - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: newTestingFactory(t), - Steps: []logicaltest.TestStep{ - testVerifyWrite(t, verifyData, expectedData), - }, - }) -} - -func TestSSHBackend_ConfigZeroAddressCRUD(t *testing.T) { - testOTPRoleData := map[string]interface{}{ - "key_type": testOTPKeyType, - "default_user": testUserName, - "cidr_list": testCIDRList, - } - req1 := map[string]interface{}{ - "roles": testOTPRoleName, - } - resp1 := map[string]interface{}{ - "roles": []string{testOTPRoleName}, - } - resp2 := map[string]interface{}{ - "roles": []string{testOTPRoleName}, - } - resp3 := map[string]interface{}{ - "roles": []string{}, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: newTestingFactory(t), - Steps: []logicaltest.TestStep{ - testRoleWrite(t, testOTPRoleName, testOTPRoleData), - testConfigZeroAddressWrite(t, req1), - testConfigZeroAddressRead(t, resp1), - testConfigZeroAddressRead(t, resp2), - testConfigZeroAddressRead(t, resp1), - testRoleDelete(t, testOTPRoleName), - testConfigZeroAddressRead(t, resp3), - testConfigZeroAddressDelete(t), - }, - }) -} - -func TestSSHBackend_CredsForZeroAddressRoles_otp(t *testing.T) { - otpRoleData := map[string]interface{}{ - "key_type": testOTPKeyType, - "default_user": testUserName, - } - data := map[string]interface{}{ - "username": testUserName, - "ip": testIP, - } - req1 := map[string]interface{}{ - "roles": testOTPRoleName, - } - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: newTestingFactory(t), - Steps: []logicaltest.TestStep{ - testRoleWrite(t, testOTPRoleName, otpRoleData), - testCredsWrite(t, testOTPRoleName, data, true, ""), - testConfigZeroAddressWrite(t, req1), - testCredsWrite(t, testOTPRoleName, data, false, ""), - testConfigZeroAddressDelete(t), - testCredsWrite(t, testOTPRoleName, data, true, ""), - }, - }) -} - -func TestSSHBackend_CA(t *testing.T) { - testCases := []struct { - name string - tag string - caPublicKey string - caPrivateKey string - algoSigner string - expectError bool - }{ - { - "RSAKey_EmptyAlgoSigner_ImageSupportsRSA1", - dockerImageTagSupportsRSA1, - testCAPublicKey, - testCAPrivateKey, - "", - false, - }, - { - "RSAKey_EmptyAlgoSigner_ImageSupportsNoRSA1", - dockerImageTagSupportsNoRSA1, - testCAPublicKey, - testCAPrivateKey, - "", - false, - }, - { - "RSAKey_DefaultAlgoSigner_ImageSupportsRSA1", - dockerImageTagSupportsRSA1, - testCAPublicKey, - testCAPrivateKey, - "default", - false, - }, - { - "RSAKey_DefaultAlgoSigner_ImageSupportsNoRSA1", - dockerImageTagSupportsNoRSA1, - testCAPublicKey, - testCAPrivateKey, - "default", - false, - }, - { - "RSAKey_RSA1AlgoSigner_ImageSupportsRSA1", - dockerImageTagSupportsRSA1, - testCAPublicKey, - testCAPrivateKey, - ssh.SigAlgoRSA, - false, - }, - { - "RSAKey_RSA1AlgoSigner_ImageSupportsNoRSA1", - dockerImageTagSupportsNoRSA1, - testCAPublicKey, - testCAPrivateKey, - ssh.SigAlgoRSA, - true, - }, - { - "RSAKey_RSASHA2256AlgoSigner_ImageSupportsRSA1", - dockerImageTagSupportsRSA1, - testCAPublicKey, - testCAPrivateKey, - ssh.SigAlgoRSASHA2256, - false, - }, - { - "RSAKey_RSASHA2256AlgoSigner_ImageSupportsNoRSA1", - dockerImageTagSupportsNoRSA1, - testCAPublicKey, - testCAPrivateKey, - ssh.SigAlgoRSASHA2256, - false, - }, - { - "ed25519Key_EmptyAlgoSigner_ImageSupportsRSA1", - dockerImageTagSupportsRSA1, - testCAPublicKeyEd25519, - testCAPrivateKeyEd25519, - "", - false, - }, - { - "ed25519Key_EmptyAlgoSigner_ImageSupportsNoRSA1", - dockerImageTagSupportsNoRSA1, - testCAPublicKeyEd25519, - testCAPrivateKeyEd25519, - "", - false, - }, - { - "ed25519Key_RSA1AlgoSigner_ImageSupportsRSA1", - dockerImageTagSupportsRSA1, - testCAPublicKeyEd25519, - testCAPrivateKeyEd25519, - ssh.SigAlgoRSA, - true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - testSSHBackend_CA(t, tc.tag, tc.caPublicKey, tc.caPrivateKey, tc.algoSigner, tc.expectError) - }) - } -} - -func testSSHBackend_CA(t *testing.T, dockerImageTag, caPublicKey, caPrivateKey, algorithmSigner string, expectError bool) { - cleanup, sshAddress := prepareTestContainer(t, dockerImageTag, caPublicKey) - defer cleanup() - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - testKeyToSignPrivate := `-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAQEAwn1V2xd/EgJXIY53fBTtc20k/ajekqQngvkpFSwNHW63XNEQK8Ll -FOCyGXoje9DUGxnYs3F/ohfsBBWkLNfU7fiENdSJL1pbkAgJ+2uhV9sLZjvYhikrXWoyJX -LDKfY12LjpcBS2HeLMT04laZ/xSJrOBEJHGzHyr2wUO0NUQUQPUODAFhnHKgvvA4Uu79UY -gcdThF4w83+EAnE4JzBZMKPMjzy4u1C0R/LoD8DuapHwX6NGWdEUvUZZ+XRcIWeCOvR0ne -qGBRH35k1Mv7k65d7kkE0uvM5Z36erw3tdoszxPYf7AKnO1DpeU2uwMcym6xNwfwynKjhL -qL/Mgi4uRwAAA8iAsY0zgLGNMwAAAAdzc2gtcnNhAAABAQDCfVXbF38SAlchjnd8FO1zbS -T9qN6SpCeC+SkVLA0dbrdc0RArwuUU4LIZeiN70NQbGdizcX+iF+wEFaQs19Tt+IQ11Ikv -WluQCAn7a6FX2wtmO9iGKStdajIlcsMp9jXYuOlwFLYd4sxPTiVpn/FIms4EQkcbMfKvbB -Q7Q1RBRA9Q4MAWGccqC+8DhS7v1RiBx1OEXjDzf4QCcTgnMFkwo8yPPLi7ULRH8ugPwO5q -kfBfo0ZZ0RS9Rln5dFwhZ4I69HSd6oYFEffmTUy/uTrl3uSQTS68zlnfp6vDe12izPE9h/ -sAqc7UOl5Ta7AxzKbrE3B/DKcqOEuov8yCLi5HAAAAAwEAAQAAAQABns2yT5XNbpuPOgKg -1APObGBchKWmDxwNKUpAVOefEScR7OP3mV4TOHQDZlMZWvoJZ8O4av+nOA/NUOjXPs0VVn -azhBvIezY8EvUSVSk49Cg6J9F7/KfR1WqpiTU7CkQUlCXNuz5xLUyKdJo3MQ/vjOqeenbh -MR9Wes4IWF1BVe4VOD6lxRsjwuIieIgmScW28FFh2rgsEfO2spzZ3AWOGExw+ih757hFz5 -4A2fhsQXP8m3r8m7iiqcjTLWXdxTUk4zot2kZEjbI4Avk0BL+wVeFq6f/y+G+g5edqSo7j -uuSgzbUQtA9PMnGxhrhU2Ob7n3VGdya7WbGZkaKP8zJhAAAAgQC3bJurmOSLIi3KVhp7lD -/FfxwXHwVBFALCgq7EyNlkTz6RDoMFM4eOTRMDvsgWxT+bSB8R8eg1sfgY8rkHOuvTAVI5 -3oEYco3H7NWE9X8Zt0lyhO1uaE49EENNSQ8hY7R3UIw5becyI+7ZZxs9HkBgCQCZzSjzA+ -SIyAoMKM261AAAAIEA+PCkcDRp3J0PaoiuetXSlWZ5WjP3CtwT2xrvEX9x+ZsDgXCDYQ5T -osxvEKOGSfIrHUUhzZbFGvqWyfrziPe9ypJrtCM7RJT/fApBXnbWFcDZzWamkQvohst+0w -XHYCmNoJ6/Y+roLv3pzyFUmqRNcrQaohex7TZmsvHJT513UakAAACBAMgBXxH8DyNYdniX -mIXEto4GqMh4rXdNwCghfpyWdJE6vCyDt7g7bYMq7AQ2ynSKRtQDT/ZgQNfSbilUq3iXz7 -xNZn5U9ndwFs90VmEpBup/PmhfX+Gwt5hQZLbkKZcgQ9XrhSKdMxVm1yy/fk0U457enlz5 -cKumubUxOfFdy1ZvAAAAEm5jY0BtYnAudWJudC5sb2NhbA== ------END OPENSSH PRIVATE KEY----- -` - testKeyToSignPublic := `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCfVXbF38SAlchjnd8FO1zbST9qN6SpCeC+SkVLA0dbrdc0RArwuUU4LIZeiN70NQbGdizcX+iF+wEFaQs19Tt+IQ11IkvWluQCAn7a6FX2wtmO9iGKStdajIlcsMp9jXYuOlwFLYd4sxPTiVpn/FIms4EQkcbMfKvbBQ7Q1RBRA9Q4MAWGccqC+8DhS7v1RiBx1OEXjDzf4QCcTgnMFkwo8yPPLi7ULRH8ugPwO5qkfBfo0ZZ0RS9Rln5dFwhZ4I69HSd6oYFEffmTUy/uTrl3uSQTS68zlnfp6vDe12izPE9h/sAqc7UOl5Ta7AxzKbrE3B/DKcqOEuov8yCLi5H ` - - roleOptions := map[string]interface{}{ - "allow_user_certificates": true, - "allowed_users": "*", - "default_extensions": []map[string]string{ - { - "permit-pty": "", - }, - }, - "key_type": "ca", - "default_user": testUserName, - "ttl": "30m0s", - } - if algorithmSigner != "" { - roleOptions["algorithm_signer"] = algorithmSigner - } - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(caPublicKey, caPrivateKey), - testRoleWrite(t, "testcarole", roleOptions), - { - Operation: logical.UpdateOperation, - Path: "sign/testcarole", - ErrorOk: expectError, - Data: map[string]interface{}{ - "public_key": testKeyToSignPublic, - "valid_principals": testUserName, - }, - - Check: func(resp *logical.Response) error { - // Tolerate nil response if an error was expected - if expectError && resp == nil { - return nil - } - - signedKey := strings.TrimSpace(resp.Data["signed_key"].(string)) - if signedKey == "" { - return errors.New("no signed key in response") - } - - privKey, err := ssh.ParsePrivateKey([]byte(testKeyToSignPrivate)) - if err != nil { - return fmt.Errorf("error parsing private key: %v", err) - } - - parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(signedKey)) - if err != nil { - return fmt.Errorf("error parsing signed key: %v", err) - } - certSigner, err := ssh.NewCertSigner(parsedKey.(*ssh.Certificate), privKey) - if err != nil { - return err - } - - err = testSSH(testUserName, sshAddress, ssh.PublicKeys(certSigner), "date") - if expectError && err == nil { - return fmt.Errorf("expected error but got none") - } - if !expectError && err != nil { - return err - } - - return nil - }, - }, - }, - } - - logicaltest.Test(t, testCase) -} - -func TestSSHBackend_CAUpgradeAlgorithmSigner(t *testing.T) { - cleanup, sshAddress := prepareTestContainer(t, dockerImageTagSupportsRSA1, testCAPublicKey) - defer cleanup() - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - testKeyToSignPrivate := `-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAQEAwn1V2xd/EgJXIY53fBTtc20k/ajekqQngvkpFSwNHW63XNEQK8Ll -FOCyGXoje9DUGxnYs3F/ohfsBBWkLNfU7fiENdSJL1pbkAgJ+2uhV9sLZjvYhikrXWoyJX -LDKfY12LjpcBS2HeLMT04laZ/xSJrOBEJHGzHyr2wUO0NUQUQPUODAFhnHKgvvA4Uu79UY -gcdThF4w83+EAnE4JzBZMKPMjzy4u1C0R/LoD8DuapHwX6NGWdEUvUZZ+XRcIWeCOvR0ne -qGBRH35k1Mv7k65d7kkE0uvM5Z36erw3tdoszxPYf7AKnO1DpeU2uwMcym6xNwfwynKjhL -qL/Mgi4uRwAAA8iAsY0zgLGNMwAAAAdzc2gtcnNhAAABAQDCfVXbF38SAlchjnd8FO1zbS -T9qN6SpCeC+SkVLA0dbrdc0RArwuUU4LIZeiN70NQbGdizcX+iF+wEFaQs19Tt+IQ11Ikv -WluQCAn7a6FX2wtmO9iGKStdajIlcsMp9jXYuOlwFLYd4sxPTiVpn/FIms4EQkcbMfKvbB -Q7Q1RBRA9Q4MAWGccqC+8DhS7v1RiBx1OEXjDzf4QCcTgnMFkwo8yPPLi7ULRH8ugPwO5q -kfBfo0ZZ0RS9Rln5dFwhZ4I69HSd6oYFEffmTUy/uTrl3uSQTS68zlnfp6vDe12izPE9h/ -sAqc7UOl5Ta7AxzKbrE3B/DKcqOEuov8yCLi5HAAAAAwEAAQAAAQABns2yT5XNbpuPOgKg -1APObGBchKWmDxwNKUpAVOefEScR7OP3mV4TOHQDZlMZWvoJZ8O4av+nOA/NUOjXPs0VVn -azhBvIezY8EvUSVSk49Cg6J9F7/KfR1WqpiTU7CkQUlCXNuz5xLUyKdJo3MQ/vjOqeenbh -MR9Wes4IWF1BVe4VOD6lxRsjwuIieIgmScW28FFh2rgsEfO2spzZ3AWOGExw+ih757hFz5 -4A2fhsQXP8m3r8m7iiqcjTLWXdxTUk4zot2kZEjbI4Avk0BL+wVeFq6f/y+G+g5edqSo7j -uuSgzbUQtA9PMnGxhrhU2Ob7n3VGdya7WbGZkaKP8zJhAAAAgQC3bJurmOSLIi3KVhp7lD -/FfxwXHwVBFALCgq7EyNlkTz6RDoMFM4eOTRMDvsgWxT+bSB8R8eg1sfgY8rkHOuvTAVI5 -3oEYco3H7NWE9X8Zt0lyhO1uaE49EENNSQ8hY7R3UIw5becyI+7ZZxs9HkBgCQCZzSjzA+ -SIyAoMKM261AAAAIEA+PCkcDRp3J0PaoiuetXSlWZ5WjP3CtwT2xrvEX9x+ZsDgXCDYQ5T -osxvEKOGSfIrHUUhzZbFGvqWyfrziPe9ypJrtCM7RJT/fApBXnbWFcDZzWamkQvohst+0w -XHYCmNoJ6/Y+roLv3pzyFUmqRNcrQaohex7TZmsvHJT513UakAAACBAMgBXxH8DyNYdniX -mIXEto4GqMh4rXdNwCghfpyWdJE6vCyDt7g7bYMq7AQ2ynSKRtQDT/ZgQNfSbilUq3iXz7 -xNZn5U9ndwFs90VmEpBup/PmhfX+Gwt5hQZLbkKZcgQ9XrhSKdMxVm1yy/fk0U457enlz5 -cKumubUxOfFdy1ZvAAAAEm5jY0BtYnAudWJudC5sb2NhbA== ------END OPENSSH PRIVATE KEY----- -` - testKeyToSignPublic := `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCfVXbF38SAlchjnd8FO1zbST9qN6SpCeC+SkVLA0dbrdc0RArwuUU4LIZeiN70NQbGdizcX+iF+wEFaQs19Tt+IQ11IkvWluQCAn7a6FX2wtmO9iGKStdajIlcsMp9jXYuOlwFLYd4sxPTiVpn/FIms4EQkcbMfKvbBQ7Q1RBRA9Q4MAWGccqC+8DhS7v1RiBx1OEXjDzf4QCcTgnMFkwo8yPPLi7ULRH8ugPwO5qkfBfo0ZZ0RS9Rln5dFwhZ4I69HSd6oYFEffmTUy/uTrl3uSQTS68zlnfp6vDe12izPE9h/sAqc7UOl5Ta7AxzKbrE3B/DKcqOEuov8yCLi5H ` - - // Old role entries between 1.4.3 and 1.5.2 had algorithm_signer default to - // ssh-rsa if not provided. - roleOptionsOldEntry := map[string]interface{}{ - "allow_user_certificates": true, - "allowed_users": "*", - "default_extensions": []map[string]string{ - { - "permit-pty": "", - }, - }, - "key_type": "ca", - "default_user": testUserName, - "ttl": "30m0s", - "algorithm_signer": ssh.SigAlgoRSA, - } - - // Upgrade entry by overwriting algorithm_signer with an empty value - roleOptionsUpgradedEntry := map[string]interface{}{ - "allow_user_certificates": true, - "allowed_users": "*", - "default_extensions": []map[string]string{ - { - "permit-pty": "", - }, - }, - "key_type": "ca", - "default_user": testUserName, - "ttl": "30m0s", - "algorithm_signer": "", - } - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(testCAPublicKey, testCAPrivateKey), - testRoleWrite(t, "testcarole", roleOptionsOldEntry), - testRoleWrite(t, "testcarole", roleOptionsUpgradedEntry), - { - Operation: logical.UpdateOperation, - Path: "sign/testcarole", - ErrorOk: false, - Data: map[string]interface{}{ - "public_key": testKeyToSignPublic, - "valid_principals": testUserName, - }, - - Check: func(resp *logical.Response) error { - signedKey := strings.TrimSpace(resp.Data["signed_key"].(string)) - if signedKey == "" { - return errors.New("no signed key in response") - } - - privKey, err := ssh.ParsePrivateKey([]byte(testKeyToSignPrivate)) - if err != nil { - return fmt.Errorf("error parsing private key: %v", err) - } - - parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(signedKey)) - if err != nil { - return fmt.Errorf("error parsing signed key: %v", err) - } - certSigner, err := ssh.NewCertSigner(parsedKey.(*ssh.Certificate), privKey) - if err != nil { - return err - } - - err = testSSH(testUserName, sshAddress, ssh.PublicKeys(certSigner), "date") - if err != nil { - return err - } - - return nil - }, - }, - }, - } - - logicaltest.Test(t, testCase) -} - -func TestBackend_AbleToRetrievePublicKey(t *testing.T) { - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(testCAPublicKey, testCAPrivateKey), - - { - Operation: logical.ReadOperation, - Path: "public_key", - Unauthenticated: true, - - Check: func(resp *logical.Response) error { - key := string(resp.Data["http_raw_body"].([]byte)) - - if key != testCAPublicKey { - return fmt.Errorf("public_key incorrect. Expected %v, actual %v", testCAPublicKey, key) - } - - return nil - }, - }, - }, - } - - logicaltest.Test(t, testCase) -} - -func TestBackend_AbleToAutoGenerateSigningKeys(t *testing.T) { - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - var expectedPublicKey string - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - { - Operation: logical.UpdateOperation, - Path: "config/ca", - Check: func(resp *logical.Response) error { - if resp.Data["public_key"].(string) == "" { - return fmt.Errorf("public_key empty") - } - expectedPublicKey = resp.Data["public_key"].(string) - return nil - }, - }, - - { - Operation: logical.ReadOperation, - Path: "public_key", - Unauthenticated: true, - - Check: func(resp *logical.Response) error { - key := string(resp.Data["http_raw_body"].([]byte)) - - if key == "" { - return fmt.Errorf("public_key empty. Expected not empty, actual %s", key) - } - if key != expectedPublicKey { - return fmt.Errorf("public_key mismatch. Expected %s, actual %s", expectedPublicKey, key) - } - - return nil - }, - }, - }, - } - - logicaltest.Test(t, testCase) -} - -func TestBackend_ValidPrincipalsValidatedForHostCertificates(t *testing.T) { - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(testCAPublicKey, testCAPrivateKey), - - createRoleStep("testing", map[string]interface{}{ - "key_type": "ca", - "allow_host_certificates": true, - "allowed_domains": "example.com,example.org", - "allow_subdomains": true, - "default_critical_options": map[string]interface{}{ - "option": "value", - }, - "default_extensions": map[string]interface{}{ - "extension": "extended", - }, - }), - - signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.HostCert, []string{"dummy.example.org", "second.example.com"}, map[string]string{ - "option": "value", - }, map[string]string{ - "extension": "extended", - }, - 2*time.Hour, map[string]interface{}{ - "public_key": publicKey2, - "ttl": "2h", - "cert_type": "host", - "valid_principals": "dummy.example.org,second.example.com", - }), - }, - } - - logicaltest.Test(t, testCase) -} - -func TestBackend_OptionsOverrideDefaults(t *testing.T) { - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(testCAPublicKey, testCAPrivateKey), - - createRoleStep("testing", map[string]interface{}{ - "key_type": "ca", - "allowed_users": "tuber", - "default_user": "tuber", - "allow_user_certificates": true, - "allowed_critical_options": "option,secondary", - "allowed_extensions": "extension,additional", - "default_critical_options": map[string]interface{}{ - "option": "value", - }, - "default_extensions": map[string]interface{}{ - "extension": "extended", - }, - }), - - signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.UserCert, []string{"tuber"}, map[string]string{ - "secondary": "value", - }, map[string]string{ - "additional": "value", - }, 2*time.Hour, map[string]interface{}{ - "public_key": publicKey2, - "ttl": "2h", - "critical_options": map[string]interface{}{ - "secondary": "value", - }, - "extensions": map[string]interface{}{ - "additional": "value", - }, - }), - }, - } - - logicaltest.Test(t, testCase) -} - -func TestBackend_AllowedUserKeyLengths(t *testing.T) { - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(testCAPublicKey, testCAPrivateKey), - createRoleStep("weakkey", map[string]interface{}{ - "key_type": "ca", - "allow_user_certificates": true, - "allowed_user_key_lengths": map[string]interface{}{ - "rsa": 4096, - }, - }), - { - Operation: logical.UpdateOperation, - Path: "sign/weakkey", - Data: map[string]interface{}{ - "public_key": testCAPublicKey, - }, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp.Data["error"] != "public_key failed to meet the key requirements: key is of an invalid size: 2048" { - return errors.New("a smaller key (2048) was allowed, when the minimum was set for 4096") - } - return nil - }, - }, - createRoleStep("stdkey", map[string]interface{}{ - "key_type": "ca", - "allow_user_certificates": true, - "allowed_user_key_lengths": map[string]interface{}{ - "rsa": 2048, - }, - }), - // Pass with 2048 key - { - Operation: logical.UpdateOperation, - Path: "sign/stdkey", - Data: map[string]interface{}{ - "public_key": testCAPublicKey, - }, - }, - // Fail with 4096 key - { - Operation: logical.UpdateOperation, - Path: "sign/stdkey", - Data: map[string]interface{}{ - "public_key": publicKey4096, - }, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp.Data["error"] != "public_key failed to meet the key requirements: key is of an invalid size: 4096" { - return errors.New("a larger key (4096) was allowed, when the size was set for 2048") - } - return nil - }, - }, - createRoleStep("multikey", map[string]interface{}{ - "key_type": "ca", - "allow_user_certificates": true, - "allowed_user_key_lengths": map[string]interface{}{ - "rsa": []int{2048, 4096}, - }, - }), - // Pass with 2048-bit key - { - Operation: logical.UpdateOperation, - Path: "sign/multikey", - Data: map[string]interface{}{ - "public_key": testCAPublicKey, - }, - }, - // Pass with 4096-bit key - { - Operation: logical.UpdateOperation, - Path: "sign/multikey", - Data: map[string]interface{}{ - "public_key": publicKey4096, - }, - }, - // Fail with 3072-bit key - { - Operation: logical.UpdateOperation, - Path: "sign/multikey", - Data: map[string]interface{}{ - "public_key": publicKey3072, - }, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp.Data["error"] != "public_key failed to meet the key requirements: key is of an invalid size: 3072" { - return errors.New("a larger key (3072) was allowed, when the size was set for 2048") - } - return nil - }, - }, - // Fail with ECDSA key - { - Operation: logical.UpdateOperation, - Path: "sign/multikey", - Data: map[string]interface{}{ - "public_key": publicKeyECDSA256, - }, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp.Data["error"] != "public_key failed to meet the key requirements: key of type ecdsa is not allowed" { - return errors.New("an ECDSA key was allowed under RSA-only policy") - } - return nil - }, - }, - createRoleStep("ectypes", map[string]interface{}{ - "key_type": "ca", - "allow_user_certificates": true, - "allowed_user_key_lengths": map[string]interface{}{ - "ec": []int{256}, - "ecdsa-sha2-nistp521": 0, - }, - }), - // Pass with ECDSA P-256 - { - Operation: logical.UpdateOperation, - Path: "sign/ectypes", - Data: map[string]interface{}{ - "public_key": publicKeyECDSA256, - }, - }, - // Pass with ECDSA P-521 - { - Operation: logical.UpdateOperation, - Path: "sign/ectypes", - Data: map[string]interface{}{ - "public_key": publicKeyECDSA521, - }, - }, - // Fail with RSA key - { - Operation: logical.UpdateOperation, - Path: "sign/ectypes", - Data: map[string]interface{}{ - "public_key": publicKey3072, - }, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp.Data["error"] != "public_key failed to meet the key requirements: key of type rsa is not allowed" { - return errors.New("an RSA key was allowed under ECDSA-only policy") - } - return nil - }, - }, - }, - } - - logicaltest.Test(t, testCase) -} - -func TestBackend_CustomKeyIDFormat(t *testing.T) { - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(testCAPublicKey, testCAPrivateKey), - - createRoleStep("customrole", map[string]interface{}{ - "key_type": "ca", - "key_id_format": "{{role_name}}-{{token_display_name}}-{{public_key_hash}}", - "allowed_users": "tuber", - "default_user": "tuber", - "allow_user_certificates": true, - "allowed_critical_options": "option,secondary", - "allowed_extensions": "extension,additional", - "default_critical_options": map[string]interface{}{ - "option": "value", - }, - "default_extensions": map[string]interface{}{ - "extension": "extended", - }, - }), - - signCertificateStep("customrole", "customrole-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.UserCert, []string{"tuber"}, map[string]string{ - "secondary": "value", - }, map[string]string{ - "additional": "value", - }, 2*time.Hour, map[string]interface{}{ - "public_key": publicKey2, - "ttl": "2h", - "critical_options": map[string]interface{}{ - "secondary": "value", - }, - "extensions": map[string]interface{}{ - "additional": "value", - }, - }), - }, - } - - logicaltest.Test(t, testCase) -} - -func TestBackend_DisallowUserProvidedKeyIDs(t *testing.T) { - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(testCAPublicKey, testCAPrivateKey), - - createRoleStep("testing", map[string]interface{}{ - "key_type": "ca", - "allow_user_key_ids": false, - "allow_user_certificates": true, - }), - { - Operation: logical.UpdateOperation, - Path: "sign/testing", - Data: map[string]interface{}{ - "public_key": publicKey2, - "key_id": "override", - }, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp.Data["error"] != "setting key_id is not allowed by role" { - return errors.New("custom user key id was allowed even when 'allow_user_key_ids' is false") - } - return nil - }, - }, - }, - } - - logicaltest.Test(t, testCase) -} - -func TestBackend_DefExtTemplatingEnabled(t *testing.T) { - cluster, userpassToken := getSshCaTestCluster(t, testUserName) - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Get auth accessor for identity template. - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - userpassAccessor := auths["userpass/"].Accessor - - // Write SSH role. - _, err = client.Logical().Write("ssh/roles/test", map[string]interface{}{ - "key_type": "ca", - "allowed_extensions": "login@zipzap.com", - "allow_user_certificates": true, - "allowed_users": "tuber", - "default_user": "tuber", - "default_extensions_template": true, - "default_extensions": map[string]interface{}{ - "login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}", - "login@foobar2.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}, " + - "{{identity.entity.aliases." + userpassAccessor + ".name}}_foobar", - }, - }) - if err != nil { - t.Fatal(err) - } - - sshKeyID := "vault-userpass-" + testUserName + "-9bd0f01b7dfc50a13aa5e5cd11aea19276968755c8f1f9c98965d04147f30ed0" - - // Issue SSH certificate with default extensions templating enabled, and no user-provided extensions - client.SetToken(userpassToken) - resp, err := client.Logical().Write("ssh/sign/test", map[string]interface{}{ - "public_key": publicKey4096, - }) - if err != nil { - t.Fatal(err) - } - signedKey := resp.Data["signed_key"].(string) - key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) - - parsedKey, err := ssh.ParsePublicKey(key) - if err != nil { - t.Fatal(err) - } - - defaultExtensionPermissions := map[string]string{ - "login@foobar.com": testUserName, - "login@foobar2.com": fmt.Sprintf("%s, %s_foobar", testUserName, testUserName), - } - - err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, defaultExtensionPermissions, 16*time.Hour) - if err != nil { - t.Fatal(err) - } - - // Issue SSH certificate with default extensions templating enabled, and user-provided extensions - // The certificate should only have the user-provided extensions, and no templated extensions - userProvidedExtensionPermissions := map[string]string{ - "login@zipzap.com": "some_other_user_name", - } - resp, err = client.Logical().Write("ssh/sign/test", map[string]interface{}{ - "public_key": publicKey4096, - "extensions": userProvidedExtensionPermissions, - }) - if err != nil { - t.Fatal(err) - } - signedKey = resp.Data["signed_key"].(string) - key, _ = base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) - - parsedKey, err = ssh.ParsePublicKey(key) - if err != nil { - t.Fatal(err) - } - - err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, userProvidedExtensionPermissions, 16*time.Hour) - if err != nil { - t.Fatal(err) - } - - // Issue SSH certificate with default extensions templating enabled, and invalid user-provided extensions - it should fail - invalidUserProvidedExtensionPermissions := map[string]string{ - "login@foobar.com": "{{identity.entity.metadata}}", - } - resp, err = client.Logical().Write("ssh/sign/test", map[string]interface{}{ - "public_key": publicKey4096, - "extensions": invalidUserProvidedExtensionPermissions, - }) - if err == nil { - t.Fatal("expected an error while attempting to sign a key with invalid permissions") - } -} - -func TestBackend_EmptyAllowedExtensionFailsClosed(t *testing.T) { - cluster, userpassToken := getSshCaTestCluster(t, testUserName) - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Get auth accessor for identity template. - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - userpassAccessor := auths["userpass/"].Accessor - - // Write SSH role to test with no allowed extension. We also provide a templated default extension, - // to verify that it's not actually being evaluated - _, err = client.Logical().Write("ssh/roles/test_allow_all_extensions", map[string]interface{}{ - "key_type": "ca", - "allow_user_certificates": true, - "allowed_users": "tuber", - "default_user": "tuber", - "allowed_extensions": "", - "default_extensions_template": false, - "default_extensions": map[string]interface{}{ - "login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Issue SSH certificate with default extensions templating disabled, and user-provided extensions - client.SetToken(userpassToken) - userProvidedAnyExtensionPermissions := map[string]string{ - "login@foobar.com": "not_userpassname", - } - _, err = client.Logical().Write("ssh/sign/test_allow_all_extensions", map[string]interface{}{ - "public_key": publicKey4096, - "extensions": userProvidedAnyExtensionPermissions, - }) - if err == nil { - t.Fatal("Expected failure we should not have allowed specifying custom extensions") - } - - if !strings.Contains(err.Error(), "are not on allowed list") { - t.Fatalf("Expected failure to contain 'are not on allowed list' but was %s", err) - } -} - -func TestBackend_DefExtTemplatingDisabled(t *testing.T) { - cluster, userpassToken := getSshCaTestCluster(t, testUserName) - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // Get auth accessor for identity template. - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - userpassAccessor := auths["userpass/"].Accessor - - // Write SSH role to test with any extension. We also provide a templated default extension, - // to verify that it's not actually being evaluated - _, err = client.Logical().Write("ssh/roles/test_allow_all_extensions", map[string]interface{}{ - "key_type": "ca", - "allow_user_certificates": true, - "allowed_users": "tuber", - "default_user": "tuber", - "allowed_extensions": "*", - "default_extensions_template": false, - "default_extensions": map[string]interface{}{ - "login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}", - }, - }) - if err != nil { - t.Fatal(err) - } - - sshKeyID := "vault-userpass-" + testUserName + "-9bd0f01b7dfc50a13aa5e5cd11aea19276968755c8f1f9c98965d04147f30ed0" - - // Issue SSH certificate with default extensions templating disabled, and no user-provided extensions - client.SetToken(userpassToken) - defaultExtensionPermissions := map[string]string{ - "login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}", - "login@zipzap.com": "some_other_user_name", - } - resp, err := client.Logical().Write("ssh/sign/test_allow_all_extensions", map[string]interface{}{ - "public_key": publicKey4096, - "extensions": defaultExtensionPermissions, - }) - if err != nil { - t.Fatal(err) - } - signedKey := resp.Data["signed_key"].(string) - key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) - - parsedKey, err := ssh.ParsePublicKey(key) - if err != nil { - t.Fatal(err) - } - - err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, defaultExtensionPermissions, 16*time.Hour) - if err != nil { - t.Fatal(err) - } - - // Issue SSH certificate with default extensions templating disabled, and user-provided extensions - client.SetToken(userpassToken) - userProvidedAnyExtensionPermissions := map[string]string{ - "login@foobar.com": "not_userpassname", - "login@zipzap.com": "some_other_user_name", - } - resp, err = client.Logical().Write("ssh/sign/test_allow_all_extensions", map[string]interface{}{ - "public_key": publicKey4096, - "extensions": userProvidedAnyExtensionPermissions, - }) - if err != nil { - t.Fatal(err) - } - signedKey = resp.Data["signed_key"].(string) - key, _ = base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) - - parsedKey, err = ssh.ParsePublicKey(key) - if err != nil { - t.Fatal(err) - } - - err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, userProvidedAnyExtensionPermissions, 16*time.Hour) - if err != nil { - t.Fatal(err) - } -} - -func TestSSHBackend_ValidateNotBeforeDuration(t *testing.T) { - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(testCAPublicKey, testCAPrivateKey), - - createRoleStep("testing", map[string]interface{}{ - "key_type": "ca", - "allow_host_certificates": true, - "allowed_domains": "example.com,example.org", - "allow_subdomains": true, - "default_critical_options": map[string]interface{}{ - "option": "value", - }, - "default_extensions": map[string]interface{}{ - "extension": "extended", - }, - "not_before_duration": "300s", - }), - - signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.HostCert, []string{"dummy.example.org", "second.example.com"}, map[string]string{ - "option": "value", - }, map[string]string{ - "extension": "extended", - }, - 2*time.Hour+5*time.Minute-30*time.Second, map[string]interface{}{ - "public_key": publicKey2, - "ttl": "2h", - "cert_type": "host", - "valid_principals": "dummy.example.org,second.example.com", - }), - - createRoleStep("testing", map[string]interface{}{ - "key_type": "ca", - "allow_host_certificates": true, - "allowed_domains": "example.com,example.org", - "allow_subdomains": true, - "default_critical_options": map[string]interface{}{ - "option": "value", - }, - "default_extensions": map[string]interface{}{ - "extension": "extended", - }, - "not_before_duration": "2h", - }), - - signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.HostCert, []string{"dummy.example.org", "second.example.com"}, map[string]string{ - "option": "value", - }, map[string]string{ - "extension": "extended", - }, - 4*time.Hour-30*time.Second, map[string]interface{}{ - "public_key": publicKey2, - "ttl": "2h", - "cert_type": "host", - "valid_principals": "dummy.example.org,second.example.com", - }), - createRoleStep("testing", map[string]interface{}{ - "key_type": "ca", - "allow_host_certificates": true, - "allowed_domains": "example.com,example.org", - "allow_subdomains": true, - "default_critical_options": map[string]interface{}{ - "option": "value", - }, - "default_extensions": map[string]interface{}{ - "extension": "extended", - }, - "not_before_duration": "30s", - }), - - signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.HostCert, []string{"dummy.example.org", "second.example.com"}, map[string]string{ - "option": "value", - }, map[string]string{ - "extension": "extended", - }, - 2*time.Hour, map[string]interface{}{ - "public_key": publicKey2, - "ttl": "2h", - "cert_type": "host", - "valid_principals": "dummy.example.org,second.example.com", - }), - }, - } - - logicaltest.Test(t, testCase) -} - -func TestSSHBackend_IssueSign(t *testing.T) { - config := logical.TestBackendConfig() - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - testCase := logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - configCaStep(testCAPublicKey, testCAPrivateKey), - - createRoleStep("testing", map[string]interface{}{ - "key_type": "otp", - "default_user": "user", - }), - // Key pair not issued with invalid role key type - issueSSHKeyPairStep("testing", "rsa", 0, true, "role key type 'otp' not allowed to issue key pairs"), - - createRoleStep("testing", map[string]interface{}{ - "key_type": "ca", - "allow_user_key_ids": false, - "allow_user_certificates": true, - "allowed_user_key_lengths": map[string]interface{}{ - "ssh-rsa": []int{2048, 3072, 4096}, - "ecdsa-sha2-nistp521": 0, - "ed25519": 0, - }, - }), - // Key_type not in allowed_user_key_types_lengths - issueSSHKeyPairStep("testing", "ec", 256, true, "provided key_type value not in allowed_user_key_types"), - // Key_bits not in allowed_user_key_types_lengths for provided key_type - issueSSHKeyPairStep("testing", "rsa", 2560, true, "provided key_bits value not in list of role's allowed_user_key_types"), - // key_type `rsa` and key_bits `2048` successfully created - issueSSHKeyPairStep("testing", "rsa", 2048, false, ""), - // key_type `ed22519` and key_bits `0` successfully created - issueSSHKeyPairStep("testing", "ed25519", 0, false, ""), - }, - } - - logicaltest.Test(t, testCase) -} - -func getSshCaTestCluster(t *testing.T, userIdentity string) (*vault.TestCluster, string) { - coreConfig := &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": userpass.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "ssh": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - client := cluster.Cores[0].Client - - // Write test policy for userpass auth method. - err := client.Sys().PutPolicy("test", ` - path "ssh/*" { - capabilities = ["update"] - }`) - if err != nil { - t.Fatal(err) - } - - // Enable userpass auth method. - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - - // Configure test role for userpass. - if _, err := client.Logical().Write("auth/userpass/users/"+userIdentity, map[string]interface{}{ - "password": "test", - "policies": "test", - }); err != nil { - t.Fatal(err) - } - - // Login userpass for test role and keep client token. - secret, err := client.Logical().Write("auth/userpass/login/"+userIdentity, map[string]interface{}{ - "password": "test", - }) - if err != nil || secret == nil { - t.Fatal(err) - } - userpassToken := secret.Auth.ClientToken - - // Mount SSH. - err = client.Sys().Mount("ssh", &api.MountInput{ - Type: "ssh", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Configure SSH CA. - _, err = client.Logical().Write("ssh/config/ca", map[string]interface{}{ - "public_key": testCAPublicKey, - "private_key": testCAPrivateKey, - }) - if err != nil { - t.Fatal(err) - } - - return cluster, userpassToken -} - -func testDefaultUserTemplate(t *testing.T, testDefaultUserTemplate string, - expectedValidPrincipal string, testEntityMetadata map[string]string, -) { - cluster, userpassToken := getSshCaTestCluster(t, testUserName) - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // set metadata "ssh_username" to userpass username - tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{ - "token": userpassToken, - }) - if err != nil { - t.Fatal(err) - } - entityID := tokenLookupResponse.Data["entity_id"].(string) - _, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{ - "metadata": testEntityMetadata, - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{ - "key_type": testCaKeyType, - "allow_user_certificates": true, - "default_user": testDefaultUserTemplate, - "default_user_template": true, - "allowed_users": testDefaultUserTemplate, - "allowed_users_template": true, - }) - if err != nil { - t.Fatal(err) - } - - // sign SSH key as userpass user - client.SetToken(userpassToken) - signResponse, err := client.Logical().Write("ssh/sign/my-role", map[string]interface{}{ - "public_key": testCAPublicKey, - }) - if err != nil { - t.Fatal(err) - } - - // check for the expected valid principals of certificate - signedKey := signResponse.Data["signed_key"].(string) - key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) - parsedKey, err := ssh.ParsePublicKey(key) - if err != nil { - t.Fatal(err) - } - actualPrincipals := parsedKey.(*ssh.Certificate).ValidPrincipals - if actualPrincipals[0] != expectedValidPrincipal { - t.Fatal( - fmt.Sprintf("incorrect ValidPrincipals: %v should be %v", - actualPrincipals, []string{expectedValidPrincipal}), - ) - } -} - -func testAllowedPrincipalsTemplate(t *testing.T, testAllowedDomainsTemplate string, - expectedValidPrincipal string, testEntityMetadata map[string]string, - roleConfigPayload map[string]interface{}, signingPayload map[string]interface{}, -) { - cluster, userpassToken := getSshCaTestCluster(t, testUserName) - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - // set metadata "ssh_username" to userpass username - tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{ - "token": userpassToken, - }) - if err != nil { - t.Fatal(err) - } - entityID := tokenLookupResponse.Data["entity_id"].(string) - _, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{ - "metadata": testEntityMetadata, - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("ssh/roles/my-role", roleConfigPayload) - if err != nil { - t.Fatal(err) - } - - // sign SSH key as userpass user - client.SetToken(userpassToken) - signResponse, err := client.Logical().Write("ssh/sign/my-role", signingPayload) - if err != nil { - t.Fatal(err) - } - - // check for the expected valid principals of certificate - signedKey := signResponse.Data["signed_key"].(string) - key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) - parsedKey, err := ssh.ParsePublicKey(key) - if err != nil { - t.Fatal(err) - } - actualPrincipals := parsedKey.(*ssh.Certificate).ValidPrincipals - if actualPrincipals[0] != expectedValidPrincipal { - t.Fatal( - fmt.Sprintf("incorrect ValidPrincipals: %v should be %v", - actualPrincipals, []string{expectedValidPrincipal}), - ) - } -} - -func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string, - expectedValidPrincipal string, testEntityMetadata map[string]string, -) { - testAllowedPrincipalsTemplate( - t, testAllowedUsersTemplate, - expectedValidPrincipal, testEntityMetadata, - map[string]interface{}{ - "key_type": testCaKeyType, - "allow_user_certificates": true, - "allowed_users": testAllowedUsersTemplate, - "allowed_users_template": true, - }, - map[string]interface{}{ - "public_key": testCAPublicKey, - "valid_principals": expectedValidPrincipal, - }, - ) -} - -func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config/ca", - Data: map[string]interface{}{ - "public_key": caPublicKey, - "private_key": caPrivateKey, - }, - } -} - -func createRoleStep(name string, parameters map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.CreateOperation, - Path: "roles/" + name, - Data: parameters, - } -} - -func signCertificateStep( - role, keyID string, certType int, validPrincipals []string, - criticalOptionPermissions, extensionPermissions map[string]string, - ttl time.Duration, - requestParameters map[string]interface{}, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "sign/" + role, - Data: requestParameters, - - Check: func(resp *logical.Response) error { - serialNumber := resp.Data["serial_number"].(string) - if serialNumber == "" { - return errors.New("no serial number in response") - } - - signedKey := strings.TrimSpace(resp.Data["signed_key"].(string)) - if signedKey == "" { - return errors.New("no signed key in response") - } - - key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) - - parsedKey, err := ssh.ParsePublicKey(key) - if err != nil { - return err - } - - return validateSSHCertificate(parsedKey.(*ssh.Certificate), keyID, certType, validPrincipals, criticalOptionPermissions, extensionPermissions, ttl) - }, - } -} - -func issueSSHKeyPairStep(role, keyType string, keyBits int, expectError bool, errorMsg string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "issue/" + role, - Data: map[string]interface{}{ - "key_type": keyType, - "key_bits": keyBits, - }, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if expectError { - var err error - if resp.Data["error"] != errorMsg { - err = fmt.Errorf("actual error message \"%s\" different from expected error message \"%s\"", resp.Data["error"], errorMsg) - } - - return err - } - - if resp.IsError() { - return fmt.Errorf("unexpected error response returned: %v", resp.Error()) - } - - if resp.Data["private_key_type"] != keyType { - return fmt.Errorf("response private_key_type (%s) does not match the provided key_type (%s)", resp.Data["private_key_type"], keyType) - } - - if resp.Data["signed_key"] == "" { - return errors.New("certificate/signed_key should not be empty") - } - - return nil - }, - } -} - -func validateSSHCertificate(cert *ssh.Certificate, keyID string, certType int, validPrincipals []string, criticalOptionPermissions, extensionPermissions map[string]string, - ttl time.Duration, -) error { - if cert.KeyId != keyID { - return fmt.Errorf("incorrect KeyId: %v, wanted %v", cert.KeyId, keyID) - } - - if cert.CertType != uint32(certType) { - return fmt.Errorf("incorrect CertType: %v", cert.CertType) - } - - if time.Unix(int64(cert.ValidAfter), 0).After(time.Now()) { - return fmt.Errorf("incorrect ValidAfter: %v", cert.ValidAfter) - } - - if time.Unix(int64(cert.ValidBefore), 0).Before(time.Now()) { - return fmt.Errorf("incorrect ValidBefore: %v", cert.ValidBefore) - } - - actualTTL := time.Unix(int64(cert.ValidBefore), 0).Add(-30 * time.Second).Sub(time.Unix(int64(cert.ValidAfter), 0)) - if actualTTL != ttl { - return fmt.Errorf("incorrect ttl: expected: %v, actual %v", ttl, actualTTL) - } - - if !reflect.DeepEqual(cert.ValidPrincipals, validPrincipals) { - return fmt.Errorf("incorrect ValidPrincipals: expected: %#v actual: %#v", validPrincipals, cert.ValidPrincipals) - } - - publicSigningKey, err := getSigningPublicKey() - if err != nil { - return err - } - if !reflect.DeepEqual(cert.SignatureKey, publicSigningKey) { - return fmt.Errorf("incorrect SignatureKey: %v", cert.SignatureKey) - } - - if cert.Signature == nil { - return fmt.Errorf("incorrect Signature: %v", cert.Signature) - } - - if !reflect.DeepEqual(cert.Permissions.Extensions, extensionPermissions) { - return fmt.Errorf("incorrect Permissions.Extensions: Expected: %v, Actual: %v", extensionPermissions, cert.Permissions.Extensions) - } - - if !reflect.DeepEqual(cert.Permissions.CriticalOptions, criticalOptionPermissions) { - return fmt.Errorf("incorrect Permissions.CriticalOptions: %v", cert.Permissions.CriticalOptions) - } - - return nil -} - -func getSigningPublicKey() (ssh.PublicKey, error) { - key, err := base64.StdEncoding.DecodeString(strings.Split(testCAPublicKey, " ")[1]) - if err != nil { - return nil, err - } - - parsedKey, err := ssh.ParsePublicKey(key) - if err != nil { - return nil, err - } - - return parsedKey, nil -} - -func testConfigZeroAddressDelete(t *testing.T) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "config/zeroaddress", - } -} - -func testConfigZeroAddressWrite(t *testing.T, data map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config/zeroaddress", - Data: data, - } -} - -func testConfigZeroAddressRead(t *testing.T, expected map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "config/zeroaddress", - Check: func(resp *logical.Response) error { - var d zeroAddressRoles - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - var ex zeroAddressRoles - if err := mapstructure.Decode(expected, &ex); err != nil { - return err - } - - if !reflect.DeepEqual(d, ex) { - return fmt.Errorf("Response mismatch:\nActual:%#v\nExpected:%#v", d, ex) - } - - return nil - }, - } -} - -func testVerifyWrite(t *testing.T, data map[string]interface{}, expected map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("verify"), - Data: data, - Check: func(resp *logical.Response) error { - var ac api.SSHVerifyResponse - if err := mapstructure.Decode(resp.Data, &ac); err != nil { - return err - } - var ex api.SSHVerifyResponse - if err := mapstructure.Decode(expected, &ex); err != nil { - return err - } - - if !reflect.DeepEqual(ac, ex) { - return fmt.Errorf("invalid response") - } - return nil - }, - } -} - -func testLookupRead(t *testing.T, data map[string]interface{}, expected []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "lookup", - Data: data, - Check: func(resp *logical.Response) error { - if resp.Data == nil || resp.Data["roles"] == nil { - return fmt.Errorf("missing roles information") - } - if !reflect.DeepEqual(resp.Data["roles"].([]string), expected) { - return fmt.Errorf("Invalid response: \nactual:%#v\nexpected:%#v", resp.Data["roles"].([]string), expected) - } - return nil - }, - } -} - -func testRoleWrite(t *testing.T, name string, data map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "roles/" + name, - Data: data, - } -} - -func testRoleList(t *testing.T, expected map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "roles", - Check: func(resp *logical.Response) error { - if resp == nil { - return fmt.Errorf("nil response") - } - if resp.Data == nil { - return fmt.Errorf("nil data") - } - if !reflect.DeepEqual(resp.Data, expected) { - return fmt.Errorf("Invalid response:\nactual:%#v\nexpected is %#v", resp.Data, expected) - } - return nil - }, - } -} - -func testRoleRead(t *testing.T, roleName string, expected map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "roles/" + roleName, - Check: func(resp *logical.Response) error { - if resp == nil { - if expected == nil { - return nil - } - return fmt.Errorf("bad: %#v", resp) - } - var d sshRole - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return fmt.Errorf("error decoding response:%s", err) - } - switch d.KeyType { - case "otp": - if d.KeyType != expected["key_type"] || d.DefaultUser != expected["default_user"] || d.CIDRList != expected["cidr_list"] { - return fmt.Errorf("data mismatch. bad: %#v", resp) - } - default: - return fmt.Errorf("unknown key type. bad: %#v", resp) - } - return nil - }, - } -} - -func testRoleDelete(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "roles/" + name, - } -} - -func testCredsWrite(t *testing.T, roleName string, data map[string]interface{}, expectError bool, address string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("creds/%s", roleName), - Data: data, - ErrorOk: expectError, - Check: func(resp *logical.Response) error { - if resp == nil { - return fmt.Errorf("response is nil") - } - if resp.Data == nil { - return fmt.Errorf("data is nil") - } - if expectError { - var e struct { - Error string `mapstructure:"error"` - } - if err := mapstructure.Decode(resp.Data, &e); err != nil { - return err - } - if len(e.Error) == 0 { - return fmt.Errorf("expected error, but write succeeded") - } - return nil - } - if roleName == testAtRoleName { - var d struct { - Key string `mapstructure:"key"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if d.Key == "" { - return fmt.Errorf("generated key is an empty string") - } - // Checking only for a parsable key - privKey, err := ssh.ParsePrivateKey([]byte(d.Key)) - if err != nil { - return fmt.Errorf("generated key is invalid") - } - if err := testSSH(data["username"].(string), address, ssh.PublicKeys(privKey), "date"); err != nil { - return fmt.Errorf("unable to SSH with new key (%s): %w", d.Key, err) - } - } else { - if resp.Data["key_type"] != KeyTypeOTP { - return fmt.Errorf("incorrect key_type") - } - if resp.Data["key"] == nil { - return fmt.Errorf("invalid key") - } - } - return nil - }, - } -} - -func TestBackend_CleanupDynamicHostKeys(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - - b, err := Backend(config) - if err != nil { - t.Fatal(err) - } - err = b.Setup(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Running on a clean mount shouldn't do anything. - cleanRequest := &logical.Request{ - Operation: logical.DeleteOperation, - Path: "tidy/dynamic-keys", - Storage: config.StorageView, - } - - resp, err := b.HandleRequest(context.Background(), cleanRequest) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotNil(t, resp.Data["message"]) - require.Contains(t, resp.Data["message"], "0 of 0") - - // Write a bunch of bogus entries. - for i := 0; i < 15; i++ { - data := map[string]interface{}{ - "host": "localhost", - "key": "nothing-to-see-here", - } - entry, err := logical.StorageEntryJSON(fmt.Sprintf("%vexample-%v", keysStoragePrefix, i), &data) - require.NoError(t, err) - err = config.StorageView.Put(context.Background(), entry) - require.NoError(t, err) - } - - // Should now have 15 - resp, err = b.HandleRequest(context.Background(), cleanRequest) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotNil(t, resp.Data["message"]) - require.Contains(t, resp.Data["message"], "15 of 15") - - // Should have none left. - resp, err = b.HandleRequest(context.Background(), cleanRequest) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - require.NotNil(t, resp.Data["message"]) - require.Contains(t, resp.Data["message"], "0 of 0") -} - -type pathAuthCheckerFunc func(t *testing.T, client *api.Client, path string, token string) - -func isPermDenied(err error) bool { - return strings.Contains(err.Error(), "permission denied") -} - -func isUnsupportedPathOperation(err error) bool { - return strings.Contains(err.Error(), "unsupported path") || strings.Contains(err.Error(), "unsupported operation") -} - -func isDeniedOp(err error) bool { - return isPermDenied(err) || isUnsupportedPathOperation(err) -} - -func pathShouldBeAuthed(t *testing.T, client *api.Client, path string, token string) { - client.SetToken("") - resp, err := client.Logical().ReadWithContext(ctx, path) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to read %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().ListWithContext(ctx, path) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to list %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to write %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to delete %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if err == nil || !isPermDenied(err) { - t.Fatalf("expected failure to patch %v while unauthed: %v / %v", path, err, resp) - } -} - -func pathShouldBeUnauthedReadList(t *testing.T, client *api.Client, path string, token string) { - // Should be able to read both with and without a token. - client.SetToken("") - resp, err := client.Logical().ReadWithContext(ctx, path) - if err != nil && isPermDenied(err) { - // Read will sometimes return permission denied, when the handler - // does not support the given operation. Retry with the token. - client.SetToken(token) - resp2, err2 := client.Logical().ReadWithContext(ctx, path) - if err2 != nil && !isUnsupportedPathOperation(err2) { - t.Fatalf("unexpected failure to read %v while unauthed: %v / %v\nWhile authed: %v / %v", path, err, resp, err2, resp2) - } - client.SetToken("") - } - resp, err = client.Logical().ListWithContext(ctx, path) - if err != nil && isPermDenied(err) { - // List will sometimes return permission denied, when the handler - // does not support the given operation. Retry with the token. - client.SetToken(token) - resp2, err2 := client.Logical().ListWithContext(ctx, path) - if err2 != nil && !isUnsupportedPathOperation(err2) { - t.Fatalf("unexpected failure to list %v while unauthed: %v / %v\nWhile authed: %v / %v", path, err, resp, err2, resp2) - } - client.SetToken("") - } - - // These should all be denied. - resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during write on read-only path %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during delete on read-only path %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during patch on read-only path %v while unauthed: %v / %v", path, err, resp) - } - - // Retrying with token should allow read/list, but not modification still. - client.SetToken(token) - resp, err = client.Logical().ReadWithContext(ctx, path) - if err != nil && isPermDenied(err) { - t.Fatalf("unexpected failure to read %v while authed: %v / %v", path, err, resp) - } - resp, err = client.Logical().ListWithContext(ctx, path) - if err != nil && isPermDenied(err) { - t.Fatalf("unexpected failure to list %v while authed: %v / %v", path, err, resp) - } - - // Should all be denied. - resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during write on read-only path %v while authed: %v / %v", path, err, resp) - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during delete on read-only path %v while authed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during patch on read-only path %v while authed: %v / %v", path, err, resp) - } -} - -func pathShouldBeUnauthedWriteOnly(t *testing.T, client *api.Client, path string, token string) { - client.SetToken("") - resp, err := client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err != nil && isPermDenied(err) { - t.Fatalf("unexpected failure to write %v while unauthed: %v / %v", path, err, resp) - } - - // These should all be denied. - resp, err = client.Logical().ReadWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during read on write-only path %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().ListWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during list on write-only path %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during delete on write-only path %v while unauthed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during patch on write-only path %v while unauthed: %v / %v", path, err, resp) - } - - // Retrying with token should allow writing, but nothing else. - client.SetToken(token) - resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) - if err != nil && isPermDenied(err) { - t.Fatalf("unexpected failure to write %v while unauthed: %v / %v", path, err, resp) - } - - // These should all be denied. - resp, err = client.Logical().ReadWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during read on write-only path %v while authed: %v / %v", path, err, resp) - } - resp, err = client.Logical().ListWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - if resp != nil || err != nil { - t.Fatalf("unexpected failure during list on write-only path %v while authed: %v / %v", path, err, resp) - } - } - resp, err = client.Logical().DeleteWithContext(ctx, path) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during delete on write-only path %v while authed: %v / %v", path, err, resp) - } - resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) - if err == nil || !isDeniedOp(err) { - t.Fatalf("unexpected failure during patch on write-only path %v while authed: %v / %v", path, err, resp) - } -} - -type pathAuthChecker int - -const ( - shouldBeAuthed pathAuthChecker = iota - shouldBeUnauthedReadList - shouldBeUnauthedWriteOnly -) - -var pathAuthChckerMap = map[pathAuthChecker]pathAuthCheckerFunc{ - shouldBeAuthed: pathShouldBeAuthed, - shouldBeUnauthedReadList: pathShouldBeUnauthedReadList, - shouldBeUnauthedWriteOnly: pathShouldBeUnauthedWriteOnly, -} - -func TestProperAuthing(t *testing.T) { - t.Parallel() - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "ssh": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - token := client.Token() - - // Mount SSH. - err := client.Sys().MountWithContext(ctx, "ssh", &api.MountInput{ - Type: "ssh", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "60h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Setup basic configuration. - _, err = client.Logical().WriteWithContext(ctx, "ssh/config/ca", map[string]interface{}{ - "generate_signing_key": true, - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().WriteWithContext(ctx, "ssh/roles/test-ca", map[string]interface{}{ - "key_type": "ca", - "allow_user_certificates": true, - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().WriteWithContext(ctx, "ssh/issue/test-ca", map[string]interface{}{ - "username": "toor", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().WriteWithContext(ctx, "ssh/roles/test-otp", map[string]interface{}{ - "key_type": "otp", - "default_user": "toor", - "cidr_list": "127.0.0.0/24", - }) - if err != nil { - t.Fatal(err) - } - - resp, err := client.Logical().WriteWithContext(ctx, "ssh/creds/test-otp", map[string]interface{}{ - "username": "toor", - "ip": "127.0.0.1", - }) - if err != nil || resp == nil { - t.Fatal(err) - } - // key := resp.Data["key"].(string) - - paths := map[string]pathAuthChecker{ - "config/ca": shouldBeAuthed, - "config/zeroaddress": shouldBeAuthed, - "creds/test-otp": shouldBeAuthed, - "issue/test-ca": shouldBeAuthed, - "lookup": shouldBeAuthed, - "public_key": shouldBeUnauthedReadList, - "roles/test-ca": shouldBeAuthed, - "roles/test-otp": shouldBeAuthed, - "roles": shouldBeAuthed, - "sign/test-ca": shouldBeAuthed, - "tidy/dynamic-keys": shouldBeAuthed, - "verify": shouldBeUnauthedWriteOnly, - } - for path, checkerType := range paths { - checker := pathAuthChckerMap[checkerType] - checker(t, client, "ssh/"+path, token) - } - - client.SetToken(token) - openAPIResp, err := client.Logical().ReadWithContext(ctx, "sys/internal/specs/openapi") - if err != nil { - t.Fatalf("failed to get openapi data: %v", err) - } - - if len(openAPIResp.Data["paths"].(map[string]interface{})) == 0 { - t.Fatalf("expected to get response from OpenAPI; got empty path list") - } - - validatedPath := false - for openapi_path, raw_data := range openAPIResp.Data["paths"].(map[string]interface{}) { - if !strings.HasPrefix(openapi_path, "/ssh/") { - t.Logf("Skipping path: %v", openapi_path) - continue - } - - t.Logf("Validating path: %v", openapi_path) - validatedPath = true - - // Substitute values in from our testing map. - raw_path := openapi_path[5:] - if strings.Contains(raw_path, "{role}") && strings.Contains(raw_path, "roles/") { - raw_path = strings.ReplaceAll(raw_path, "{role}", "test-ca") - } - if strings.Contains(raw_path, "{role}") && (strings.Contains(raw_path, "sign/") || strings.Contains(raw_path, "issue/")) { - raw_path = strings.ReplaceAll(raw_path, "{role}", "test-ca") - } - if strings.Contains(raw_path, "{role}") && strings.Contains(raw_path, "creds") { - raw_path = strings.ReplaceAll(raw_path, "{role}", "test-otp") - } - - handler, present := paths[raw_path] - if !present { - t.Fatalf("OpenAPI reports SSH mount contains %v->%v but was not tested to be authed or authed.", openapi_path, raw_path) - } - - openapi_data := raw_data.(map[string]interface{}) - hasList := false - rawGetData, hasGet := openapi_data["get"] - if hasGet { - getData := rawGetData.(map[string]interface{}) - getParams, paramsPresent := getData["parameters"].(map[string]interface{}) - if getParams != nil && paramsPresent { - if _, hasList = getParams["list"]; hasList { - // LIST is exclusive from GET on the same endpoint usually. - hasGet = false - } - } - } - _, hasPost := openapi_data["post"] - _, hasDelete := openapi_data["delete"] - - if handler == shouldBeUnauthedReadList { - if hasPost || hasDelete { - t.Fatalf("Unauthed read-only endpoints should not have POST/DELETE capabilities") - } - } - } - - if !validatedPath { - t.Fatalf("Expected to have validated at least one path.") - } -} diff --git a/builtin/logical/ssh/path_config_ca_test.go b/builtin/logical/ssh/path_config_ca_test.go deleted file mode 100644 index a096073c6..000000000 --- a/builtin/logical/ssh/path_config_ca_test.go +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package ssh - -import ( - "context" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestSSH_ConfigCAStorageUpgrade(t *testing.T) { - var err error - - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - - b, err := Backend(config) - if err != nil { - t.Fatal(err) - } - - err = b.Setup(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Store at an older path - err = config.StorageView.Put(context.Background(), &logical.StorageEntry{ - Key: caPrivateKeyStoragePathDeprecated, - Value: []byte(testCAPrivateKey), - }) - if err != nil { - t.Fatal(err) - } - - // Reading it should return the key as well as upgrade the storage path - privateKeyEntry, err := caKey(context.Background(), config.StorageView, caPrivateKey) - if err != nil { - t.Fatal(err) - } - if privateKeyEntry == nil || privateKeyEntry.Key == "" { - t.Fatalf("failed to read the stored private key") - } - - entry, err := config.StorageView.Get(context.Background(), caPrivateKeyStoragePathDeprecated) - if err != nil { - t.Fatal(err) - } - if entry != nil { - t.Fatalf("bad: expected a nil entry after upgrade") - } - - entry, err = config.StorageView.Get(context.Background(), caPrivateKeyStoragePath) - if err != nil { - t.Fatal(err) - } - if entry == nil { - t.Fatalf("bad: expected a non-nil entry after upgrade") - } - - // Store at an older path - err = config.StorageView.Put(context.Background(), &logical.StorageEntry{ - Key: caPublicKeyStoragePathDeprecated, - Value: []byte(testCAPublicKey), - }) - if err != nil { - t.Fatal(err) - } - - // Reading it should return the key as well as upgrade the storage path - publicKeyEntry, err := caKey(context.Background(), config.StorageView, caPublicKey) - if err != nil { - t.Fatal(err) - } - if publicKeyEntry == nil || publicKeyEntry.Key == "" { - t.Fatalf("failed to read the stored public key") - } - - entry, err = config.StorageView.Get(context.Background(), caPublicKeyStoragePathDeprecated) - if err != nil { - t.Fatal(err) - } - if entry != nil { - t.Fatalf("bad: expected a nil entry after upgrade") - } - - entry, err = config.StorageView.Get(context.Background(), caPublicKeyStoragePath) - if err != nil { - t.Fatal(err) - } - if entry == nil { - t.Fatalf("bad: expected a non-nil entry after upgrade") - } -} - -func TestSSH_ConfigCAUpdateDelete(t *testing.T) { - var resp *logical.Response - var err error - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - caReq := &logical.Request{ - Path: "config/ca", - Operation: logical.UpdateOperation, - Storage: config.StorageView, - } - - // Auto-generate the keys - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v, resp:%v", err, resp) - } - - // Fail to overwrite it - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil { - t.Fatal(err) - } - if !resp.IsError() { - t.Fatalf("expected an error, got %#v", *resp) - } - - caReq.Operation = logical.DeleteOperation - // Delete the configured keys - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v, resp:%v", err, resp) - } - - caReq.Operation = logical.UpdateOperation - caReq.Data = map[string]interface{}{ - "public_key": testCAPublicKey, - "private_key": testCAPrivateKey, - } - - // Successfully create a new one - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v, resp:%v", err, resp) - } - - // Fail to overwrite it - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil { - t.Fatal(err) - } - if !resp.IsError() { - t.Fatalf("expected an error, got %#v", *resp) - } - - caReq.Operation = logical.DeleteOperation - // Delete the configured keys - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v, resp:%v", err, resp) - } - - caReq.Operation = logical.UpdateOperation - caReq.Data = nil - - // Successfully create a new one - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v, resp:%v", err, resp) - } - - // Delete the configured keys - caReq.Operation = logical.DeleteOperation - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v, resp:%v", err, resp) - } -} - -func createDeleteHelper(t *testing.T, b logical.Backend, config *logical.BackendConfig, index int, keyType string, keyBits int) { - // Check that we can create a new key of the specified type - caReq := &logical.Request{ - Path: "config/ca", - Operation: logical.UpdateOperation, - Storage: config.StorageView, - } - caReq.Data = map[string]interface{}{ - "generate_signing_key": true, - "key_type": keyType, - "key_bits": keyBits, - } - resp, err := b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad case %v: err: %v, resp: %v", index, err, resp) - } - if !strings.Contains(resp.Data["public_key"].(string), caReq.Data["key_type"].(string)) { - t.Fatalf("bad case %v: expected public key of type %v but was %v", index, caReq.Data["key_type"], resp.Data["public_key"]) - } - - issueOptions := map[string]interface{}{ - "public_key": testCAPublicKeyEd25519, - } - issueReq := &logical.Request{ - Path: "sign/ca-issuance", - Operation: logical.UpdateOperation, - Storage: config.StorageView, - Data: issueOptions, - } - resp, err = b.HandleRequest(context.Background(), issueReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad case %v: err: %v, resp: %v", index, err, resp) - } - - // Delete the configured keys - caReq.Operation = logical.DeleteOperation - resp, err = b.HandleRequest(context.Background(), caReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad case %v: err: %v, resp: %v", index, err, resp) - } -} - -func TestSSH_ConfigCAKeyTypes(t *testing.T) { - var err error - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatalf("Cannot create backend: %s", err) - } - - cases := []struct { - keyType string - keyBits int - }{ - {"ssh-rsa", 2048}, - {"ssh-rsa", 4096}, - {"ssh-rsa", 0}, - {"rsa", 2048}, - {"rsa", 4096}, - {"ecdsa-sha2-nistp256", 0}, - {"ecdsa-sha2-nistp384", 0}, - {"ecdsa-sha2-nistp521", 0}, - {"ec", 256}, - {"ec", 384}, - {"ec", 521}, - {"ec", 0}, - {"ssh-ed25519", 0}, - {"ed25519", 0}, - } - - // Create a role for ssh signing. - roleOptions := map[string]interface{}{ - "allow_user_certificates": true, - "allowed_users": "*", - "key_type": "ca", - "ttl": "30s", - "not_before_duration": "2h", - } - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "roles/ca-issuance", - Data: roleOptions, - Storage: config.StorageView, - } - _, err = b.HandleRequest(context.Background(), roleReq) - if err != nil { - t.Fatalf("Cannot create role to issue against: %s", err) - } - - for index, scenario := range cases { - createDeleteHelper(t, b, config, index, scenario.keyType, scenario.keyBits) - } -} diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go deleted file mode 100644 index 1d3ba4d4f..000000000 --- a/builtin/logical/totp/backend_test.go +++ /dev/null @@ -1,1243 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package totp - -import ( - "context" - "fmt" - "log" - "net/url" - "path" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/helper/namespace" - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/mapstructure" - otplib "github.com/pquerna/otp" - totplib "github.com/pquerna/otp/totp" -) - -func createKey() (string, error) { - keyUrl, err := totplib.Generate(totplib.GenerateOpts{ - Issuer: "Vault", - AccountName: "Test", - }) - - key := keyUrl.Secret() - - return strings.ToLower(key), err -} - -func generateCode(key string, period uint, digits otplib.Digits, algorithm otplib.Algorithm) (string, error) { - // Generate password using totp library - totpToken, err := totplib.GenerateCodeCustom(key, time.Now(), totplib.ValidateOpts{ - Period: period, - Digits: digits, - Algorithm: algorithm, - }) - - return totpToken, err -} - -func TestBackend_KeyName(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - tests := []struct { - Name string - KeyName string - Fail bool - }{ - { - "without @", - "sample", - false, - }, - { - "with @ in the beginning", - "@sample.com", - true, - }, - { - "with @ in the end", - "sample.com@", - true, - }, - { - "with @ in between", - "sample@sample.com", - false, - }, - { - "with multiple @", - "sample@sample@@sample.com", - false, - }, - } - var resp *logical.Response - for _, tc := range tests { - resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Path: "keys/" + tc.KeyName, - Operation: logical.UpdateOperation, - Storage: config.StorageView, - Data: map[string]interface{}{ - "generate": true, - "account_name": "vault", - "issuer": "hashicorp", - }, - }) - if tc.Fail { - if err == nil { - t.Fatalf("expected an error for test %q", tc.Name) - } - continue - } else if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: test name: %q\nresp: %#v\nerr: %v", tc.Name, resp, err) - } - resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{ - Path: "code/" + tc.KeyName, - Operation: logical.ReadOperation, - Storage: config.StorageView, - }) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: test name: %q\nresp: %#v\nerr: %v", tc.Name, resp, err) - } - if resp.Data["code"].(string) == "" { - t.Fatalf("failed to generate code for test %q", tc.Name) - } - } -} - -func TestBackend_readCredentialsDefaultValues(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - keyData := map[string]interface{}{ - "key": key, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "", - "account_name": "", - "digits": otplib.DigitsSix, - "period": 30, - "algorithm": otplib.AlgorithmSHA1, - "key": key, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_readCredentialsEightDigitsThirtySecondPeriod(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, - "digits": 8, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "digits": otplib.DigitsEight, - "period": 30, - "algorithm": otplib.AlgorithmSHA1, - "key": key, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_readCredentialsSixDigitsNinetySecondPeriod(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, - "period": 90, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "digits": otplib.DigitsSix, - "period": 90, - "algorithm": otplib.AlgorithmSHA1, - "key": key, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_readCredentialsSHA256(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, - "algorithm": "SHA256", - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "digits": otplib.DigitsSix, - "period": 30, - "algorithm": otplib.AlgorithmSHA256, - "key": key, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_readCredentialsSHA512(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, - "algorithm": "SHA512", - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "digits": otplib.DigitsSix, - "period": 30, - "algorithm": otplib.AlgorithmSHA512, - "key": key, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_keyCrudDefaultValues(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - key, _ := createKey() - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "digits": otplib.DigitsSix, - "period": 30, - "algorithm": otplib.AlgorithmSHA1, - "key": key, - } - - code, _ := generateCode(key, 30, otplib.DigitsSix, otplib.AlgorithmSHA1) - invalidCode := "12345678" - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepValidateCode(t, "test", code, true, false), - // Next step should fail because it should be in the used cache - testAccStepValidateCode(t, "test", code, false, true), - testAccStepValidateCode(t, "test", invalidCode, false, false), - testAccStepDeleteKey(t, "test"), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_createKeyMissingKeyValue(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "generate": false, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_createKeyInvalidKeyValue(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": "1", - "generate": false, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_createKeyInvalidAlgorithm(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, - "algorithm": "BADALGORITHM", - "generate": false, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_createKeyInvalidPeriod(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, - "period": -1, - "generate": false, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_createKeyInvalidDigits(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, - "digits": 20, - "generate": false, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_generatedKeyDefaultValues(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "generate": true, - "key_size": 20, - "exported": true, - "qr_size": 200, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "digits": otplib.DigitsSix, - "period": 30, - "algorithm": otplib.AlgorithmSHA1, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - }, - }) -} - -func TestBackend_generatedKeyDefaultValuesNoQR(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "generate": true, - "key_size": 20, - "exported": true, - "qr_size": 0, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - }, - }) -} - -func TestBackend_generatedKeyNonDefaultKeySize(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "generate": true, - "key_size": 10, - "exported": true, - "qr_size": 200, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "digits": otplib.DigitsSix, - "period": 30, - "algorithm": otplib.AlgorithmSHA1, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - }, - }) -} - -func TestBackend_urlPassedNonGeneratedKeyInvalidPeriod(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - urlString := "otpauth://totp/Vault:test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=AZ" - - keyData := map[string]interface{}{ - "url": urlString, - "generate": false, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_urlPassedNonGeneratedKeyInvalidDigits(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - urlString := "otpauth://totp/Vault:test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=Q&period=60" - - keyData := map[string]interface{}{ - "url": urlString, - "generate": false, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_urlPassedNonGeneratedKeyIssuerInFirstPosition(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - urlString := "otpauth://totp/Vault:test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60" - - keyData := map[string]interface{}{ - "url": urlString, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "test@email.com", - "digits": otplib.DigitsSix, - "period": 60, - "algorithm": otplib.AlgorithmSHA512, - "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_urlPassedNonGeneratedKeyIssuerInQueryString(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - urlString := "otpauth://totp/test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60&issuer=Vault" - - keyData := map[string]interface{}{ - "url": urlString, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "test@email.com", - "digits": otplib.DigitsSix, - "period": 60, - "algorithm": otplib.AlgorithmSHA512, - "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_urlPassedNonGeneratedKeyMissingIssuer(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - urlString := "otpauth://totp/test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60" - - keyData := map[string]interface{}{ - "url": urlString, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "", - "account_name": "test@email.com", - "digits": otplib.DigitsSix, - "period": 60, - "algorithm": otplib.AlgorithmSHA512, - "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_urlPassedNonGeneratedKeyMissingAccountName(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - urlString := "otpauth://totp/Vault:?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60" - - keyData := map[string]interface{}{ - "url": urlString, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "", - "digits": otplib.DigitsSix, - "period": 60, - "algorithm": otplib.AlgorithmSHA512, - "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_urlPassedNonGeneratedKeyMissingAccountNameandIssuer(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - urlString := "otpauth://totp/?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60" - - keyData := map[string]interface{}{ - "url": urlString, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "", - "account_name": "", - "digits": otplib.DigitsSix, - "period": 60, - "algorithm": otplib.AlgorithmSHA512, - "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_urlPassedNonGeneratedKeyMissingAccountNameandIssuerandPadding(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - urlString := "otpauth://totp/?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZAU&algorithm=SHA512&digits=6&period=60" - - keyData := map[string]interface{}{ - "url": urlString, - "generate": false, - } - - expected := map[string]interface{}{ - "issuer": "", - "account_name": "", - "digits": otplib.DigitsSix, - "period": 60, - "algorithm": otplib.AlgorithmSHA512, - "key": "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZAU===", - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - -func TestBackend_generatedKeyInvalidSkew(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "skew": "2", - "generate": true, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_generatedKeyInvalidQRSize(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "qr_size": "-100", - "generate": true, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_generatedKeyInvalidKeySize(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key_size": "-100", - "generate": true, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_generatedKeyMissingAccountName(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "generate": true, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_generatedKeyMissingIssuer(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "account_name": "test@email.com", - "generate": true, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_invalidURLValue(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "url": "notaurl", - "generate": false, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_urlAndGenerateTrue(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "url": "otpauth://totp/Vault:test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60", - "generate": true, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_keyAndGenerateTrue(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", - "generate": true, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, true), - testAccStepReadKey(t, "test", nil), - }, - }) -} - -func TestBackend_generatedKeyExportedFalse(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - keyData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "test@email.com", - "generate": true, - "exported": false, - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "test@email.com", - "digits": otplib.DigitsSix, - "period": 30, - "algorithm": otplib.AlgorithmSHA1, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateKey(t, "test", keyData, false), - testAccStepReadKey(t, "test", expected), - }, - }) -} - -func testAccStepCreateKey(t *testing.T, name string, keyData map[string]interface{}, expectFail bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: path.Join("keys", name), - Data: keyData, - ErrorOk: expectFail, - Check: func(resp *logical.Response) error { - // Skip this if the key is not generated by vault or if the test is expected to fail - if !keyData["generate"].(bool) || expectFail { - return nil - } - - // Check to see if barcode and url were returned if exported is false - if !keyData["exported"].(bool) { - if resp != nil { - t.Fatalf("data was returned when exported was set to false") - } - return nil - } - - // Check to see if a barcode was returned when qr_size is zero - if keyData["qr_size"].(int) == 0 { - if _, exists := resp.Data["barcode"]; exists { - t.Fatalf("a barcode was returned when qr_size was set to zero") - } - return nil - } - - var d struct { - Url string `mapstructure:"url"` - Barcode string `mapstructure:"barcode"` - } - - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - // Check to see if barcode and url are returned - if d.Barcode == "" { - t.Fatalf("a barcode was not returned for a generated key") - } - - if d.Url == "" { - t.Fatalf("a url was not returned for a generated key") - } - - // Parse url - urlObject, err := url.Parse(d.Url) - if err != nil { - t.Fatal("an error occurred while parsing url string") - } - - // Set up query object - urlQuery := urlObject.Query() - - // Read secret - urlSecret := urlQuery.Get("secret") - - // Check key length - keySize := keyData["key_size"].(int) - correctSecretStringSize := (keySize / 5) * 8 - actualSecretStringSize := len(urlSecret) - - if actualSecretStringSize != correctSecretStringSize { - t.Fatal("incorrect key string length") - } - - return nil - }, - } -} - -func testAccStepDeleteKey(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: path.Join("keys", name), - } -} - -func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, name string, validation map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: path.Join("code", name), - Check: func(resp *logical.Response) error { - var d struct { - Code string `mapstructure:"code"` - } - - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - log.Printf("[TRACE] Generated credentials: %v", d) - - period := validation["period"].(int) - key := validation["key"].(string) - algorithm := validation["algorithm"].(otplib.Algorithm) - digits := validation["digits"].(otplib.Digits) - - valid, _ := totplib.ValidateCustom(d.Code, key, time.Now(), totplib.ValidateOpts{ - Period: uint(period), - Skew: 1, - Digits: digits, - Algorithm: algorithm, - }) - - if !valid { - t.Fatalf("generated code isn't valid") - } - - return nil - }, - } -} - -func testAccStepReadKey(t *testing.T, name string, expected map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "keys/" + name, - Check: func(resp *logical.Response) error { - if resp == nil { - if expected == nil { - return nil - } - return fmt.Errorf("bad: %#v", resp) - } - - var d struct { - Issuer string `mapstructure:"issuer"` - AccountName string `mapstructure:"account_name"` - Period uint `mapstructure:"period"` - Algorithm string `mapstructure:"algorithm"` - Digits otplib.Digits `mapstructure:"digits"` - } - - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - var keyAlgorithm otplib.Algorithm - switch d.Algorithm { - case "SHA1": - keyAlgorithm = otplib.AlgorithmSHA1 - case "SHA256": - keyAlgorithm = otplib.AlgorithmSHA256 - case "SHA512": - keyAlgorithm = otplib.AlgorithmSHA512 - } - - period := expected["period"].(int) - - switch { - case d.Issuer != expected["issuer"]: - return fmt.Errorf("issuer should equal: %s", expected["issuer"]) - case d.AccountName != expected["account_name"]: - return fmt.Errorf("account_name should equal: %s", expected["account_name"]) - case d.Period != uint(period): - return fmt.Errorf("period should equal: %d", expected["period"]) - case keyAlgorithm != expected["algorithm"]: - return fmt.Errorf("algorithm should equal: %s", expected["algorithm"]) - case d.Digits != expected["digits"]: - return fmt.Errorf("digits should equal: %d", expected["digits"]) - } - return nil - }, - } -} - -func testAccStepValidateCode(t *testing.T, name string, code string, valid, expectError bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "code/" + name, - Data: map[string]interface{}{ - "code": code, - }, - ErrorOk: expectError, - Check: func(resp *logical.Response) error { - if resp == nil { - return fmt.Errorf("bad: %#v", resp) - } - - var d struct { - Valid bool `mapstructure:"valid"` - } - - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - switch valid { - case true: - if d.Valid != true { - return fmt.Errorf("code was not valid: %s", code) - } - - default: - if d.Valid != false { - return fmt.Errorf("code was incorrectly validated: %s", code) - } - } - return nil - }, - } -} diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go deleted file mode 100644 index b7d5cc8b4..000000000 --- a/builtin/logical/transit/backend_test.go +++ /dev/null @@ -1,2305 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "crypto" - "crypto/ed25519" - cryptoRand "crypto/rand" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "fmt" - "io" - "math/rand" - "os" - "path" - "reflect" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/logical/pki" - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/keysutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - - uuid "github.com/hashicorp/go-uuid" - "github.com/mitchellh/mapstructure" - - "github.com/stretchr/testify/require" -) - -const ( - testPlaintext = "The quick brown fox" -) - -func createBackendWithStorage(t testing.TB) (*backend, logical.Storage) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - - b, _ := Backend(context.Background(), config) - if b == nil { - t.Fatalf("failed to create backend") - } - err := b.Backend.Setup(context.Background(), config) - if err != nil { - t.Fatal(err) - } - return b, config.StorageView -} - -func createBackendWithSysView(t testing.TB) (*backend, logical.Storage) { - sysView := logical.TestSystemView() - storage := &logical.InmemStorage{} - - conf := &logical.BackendConfig{ - StorageView: storage, - System: sysView, - } - - b, _ := Backend(context.Background(), conf) - if b == nil { - t.Fatal("failed to create backend") - } - - err := b.Backend.Setup(context.Background(), conf) - if err != nil { - t.Fatal(err) - } - - return b, storage -} - -func createBackendWithSysViewWithStorage(t testing.TB, s logical.Storage) *backend { - sysView := logical.TestSystemView() - - conf := &logical.BackendConfig{ - StorageView: s, - System: sysView, - } - - b, _ := Backend(context.Background(), conf) - if b == nil { - t.Fatal("failed to create backend") - } - - err := b.Backend.Setup(context.Background(), conf) - if err != nil { - t.Fatal(err) - } - - return b -} - -func createBackendWithForceNoCacheWithSysViewWithStorage(t testing.TB, s logical.Storage) *backend { - sysView := logical.TestSystemView() - sysView.CachingDisabledVal = true - - conf := &logical.BackendConfig{ - StorageView: s, - System: sysView, - } - - b, _ := Backend(context.Background(), conf) - if b == nil { - t.Fatal("failed to create backend") - } - - err := b.Backend.Setup(context.Background(), conf) - if err != nil { - t.Fatal(err) - } - - return b -} - -func TestTransit_RSA(t *testing.T) { - testTransit_RSA(t, "rsa-2048") - testTransit_RSA(t, "rsa-3072") - testTransit_RSA(t, "rsa-4096") -} - -func testTransit_RSA(t *testing.T, keyType string) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - keyReq := &logical.Request{ - Path: "keys/rsa", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "type": keyType, - }, - Storage: storage, - } - - resp, err = b.HandleRequest(context.Background(), keyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox" - - encryptReq := &logical.Request{ - Path: "encrypt/rsa", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "plaintext": plaintext, - }, - } - - resp, err = b.HandleRequest(context.Background(), encryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - - ciphertext1 := resp.Data["ciphertext"].(string) - - decryptReq := &logical.Request{ - Path: "decrypt/rsa", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "ciphertext": ciphertext1, - }, - } - - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - - decryptedPlaintext := resp.Data["plaintext"] - - if plaintext != decryptedPlaintext { - t.Fatalf("bad: plaintext; expected: %q\nactual: %q", plaintext, decryptedPlaintext) - } - - // Rotate the key - rotateReq := &logical.Request{ - Path: "keys/rsa/rotate", - Operation: logical.UpdateOperation, - Storage: storage, - } - resp, err = b.HandleRequest(context.Background(), rotateReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - - // Encrypt again - resp, err = b.HandleRequest(context.Background(), encryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - ciphertext2 := resp.Data["ciphertext"].(string) - - if ciphertext1 == ciphertext2 { - t.Fatalf("expected different ciphertexts") - } - - // See if the older ciphertext can still be decrypted - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - if resp.Data["plaintext"].(string) != plaintext { - t.Fatal("failed to decrypt old ciphertext after rotating the key") - } - - // Decrypt the new ciphertext - decryptReq.Data = map[string]interface{}{ - "ciphertext": ciphertext2, - } - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - if resp.Data["plaintext"].(string) != plaintext { - t.Fatal("failed to decrypt ciphertext after rotating the key") - } - - signReq := &logical.Request{ - Path: "sign/rsa", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "input": plaintext, - }, - } - resp, err = b.HandleRequest(context.Background(), signReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - signature := resp.Data["signature"].(string) - - verifyReq := &logical.Request{ - Path: "verify/rsa", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "input": plaintext, - "signature": signature, - }, - } - - resp, err = b.HandleRequest(context.Background(), verifyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - if !resp.Data["valid"].(bool) { - t.Fatalf("failed to verify the RSA signature") - } - - signReq.Data = map[string]interface{}{ - "input": plaintext, - "hash_algorithm": "invalid", - } - resp, err = b.HandleRequest(context.Background(), signReq) - if err == nil { - t.Fatal(err) - } - - signReq.Data = map[string]interface{}{ - "input": plaintext, - "hash_algorithm": "sha2-512", - } - resp, err = b.HandleRequest(context.Background(), signReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - signature = resp.Data["signature"].(string) - - verifyReq.Data = map[string]interface{}{ - "input": plaintext, - "signature": signature, - } - resp, err = b.HandleRequest(context.Background(), verifyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - if resp.Data["valid"].(bool) { - t.Fatalf("expected validation to fail") - } - - verifyReq.Data = map[string]interface{}{ - "input": plaintext, - "signature": signature, - "hash_algorithm": "sha2-512", - } - resp, err = b.HandleRequest(context.Background(), verifyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - if !resp.Data["valid"].(bool) { - t.Fatalf("failed to verify the RSA signature") - } - - // Take a random hash and sign it using PKCSv1_5_NoOID. - hash := "P8m2iUWdc4+MiKOkiqnjNUIBa3pAUuABqqU2/KdIE8s=" - signReq.Data = map[string]interface{}{ - "input": hash, - "hash_algorithm": "none", - "signature_algorithm": "pkcs1v15", - "prehashed": true, - } - resp, err = b.HandleRequest(context.Background(), signReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - signature = resp.Data["signature"].(string) - - verifyReq.Data = map[string]interface{}{ - "input": hash, - "signature": signature, - "hash_algorithm": "none", - "signature_algorithm": "pkcs1v15", - "prehashed": true, - } - resp, err = b.HandleRequest(context.Background(), verifyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - if !resp.Data["valid"].(bool) { - t.Fatalf("failed to verify the RSA signature") - } -} - -func TestBackend_basic(t *testing.T) { - decryptData := make(map[string]interface{}) - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: Factory, - Steps: []logicaltest.TestStep{ - testAccStepListPolicy(t, "test", true), - testAccStepWritePolicy(t, "test", false), - testAccStepListPolicy(t, "test", false), - testAccStepReadPolicy(t, "test", false, false), - testAccStepEncrypt(t, "test", testPlaintext, decryptData), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepEncrypt(t, "test", "", decryptData), - testAccStepDecrypt(t, "test", "", decryptData), - testAccStepDeleteNotDisabledPolicy(t, "test"), - testAccStepEnableDeletion(t, "test"), - testAccStepDeletePolicy(t, "test"), - testAccStepWritePolicy(t, "test", false), - testAccStepEnableDeletion(t, "test"), - testAccStepDisableDeletion(t, "test"), - testAccStepDeleteNotDisabledPolicy(t, "test"), - testAccStepEnableDeletion(t, "test"), - testAccStepDeletePolicy(t, "test"), - testAccStepReadPolicy(t, "test", true, false), - }, - }) -} - -func TestBackend_upsert(t *testing.T) { - decryptData := make(map[string]interface{}) - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: Factory, - Steps: []logicaltest.TestStep{ - testAccStepReadPolicy(t, "test", true, false), - testAccStepListPolicy(t, "test", true), - testAccStepEncryptUpsert(t, "test", testPlaintext, decryptData), - testAccStepListPolicy(t, "test", false), - testAccStepReadPolicy(t, "test", false, false), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - }, - }) -} - -func TestBackend_datakey(t *testing.T) { - dataKeyInfo := make(map[string]interface{}) - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: Factory, - Steps: []logicaltest.TestStep{ - testAccStepListPolicy(t, "test", true), - testAccStepWritePolicy(t, "test", false), - testAccStepListPolicy(t, "test", false), - testAccStepReadPolicy(t, "test", false, false), - testAccStepWriteDatakey(t, "test", false, 256, dataKeyInfo), - testAccStepDecryptDatakey(t, "test", dataKeyInfo), - testAccStepWriteDatakey(t, "test", true, 128, dataKeyInfo), - }, - }) -} - -func TestBackend_rotation(t *testing.T) { - defer os.Setenv("TRANSIT_ACC_KEY_TYPE", "") - testBackendRotation(t) - os.Setenv("TRANSIT_ACC_KEY_TYPE", "CHACHA") - testBackendRotation(t) -} - -func testBackendRotation(t *testing.T) { - decryptData := make(map[string]interface{}) - encryptHistory := make(map[int]map[string]interface{}) - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: Factory, - Steps: []logicaltest.TestStep{ - testAccStepListPolicy(t, "test", true), - testAccStepWritePolicy(t, "test", false), - testAccStepListPolicy(t, "test", false), - testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 0, encryptHistory), - testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 1, encryptHistory), - testAccStepRotate(t, "test"), // now v2 - testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 2, encryptHistory), - testAccStepRotate(t, "test"), // now v3 - testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 3, encryptHistory), - testAccStepRotate(t, "test"), // now v4 - testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 4, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepEncryptVX(t, "test", testPlaintext, decryptData, 99, encryptHistory), - testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 0, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 1, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 2, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 3, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 99, encryptHistory), - testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 4, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepDeleteNotDisabledPolicy(t, "test"), - testAccStepAdjustPolicyMinDecryption(t, "test", 3), - testAccStepAdjustPolicyMinEncryption(t, "test", 4), - testAccStepReadPolicyWithVersions(t, "test", false, false, 3, 4), - testAccStepLoadVX(t, "test", decryptData, 0, encryptHistory), - testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 1, encryptHistory), - testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 2, encryptHistory), - testAccStepDecryptExpectFailure(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 3, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 4, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepAdjustPolicyMinDecryption(t, "test", 1), - testAccStepReadPolicyWithVersions(t, "test", false, false, 1, 4), - testAccStepLoadVX(t, "test", decryptData, 0, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 1, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepLoadVX(t, "test", decryptData, 2, encryptHistory), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepRewrap(t, "test", decryptData, 4), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepEnableDeletion(t, "test"), - testAccStepDeletePolicy(t, "test"), - testAccStepReadPolicy(t, "test", true, false), - testAccStepListPolicy(t, "test", true), - }, - }) -} - -func TestBackend_basic_derived(t *testing.T) { - decryptData := make(map[string]interface{}) - logicaltest.Test(t, logicaltest.TestCase{ - LogicalFactory: Factory, - Steps: []logicaltest.TestStep{ - testAccStepListPolicy(t, "test", true), - testAccStepWritePolicy(t, "test", true), - testAccStepListPolicy(t, "test", false), - testAccStepReadPolicy(t, "test", false, true), - testAccStepEncryptContext(t, "test", testPlaintext, "my-cool-context", decryptData), - testAccStepDecrypt(t, "test", testPlaintext, decryptData), - testAccStepEnableDeletion(t, "test"), - testAccStepDeletePolicy(t, "test"), - testAccStepReadPolicy(t, "test", true, true), - }, - }) -} - -func testAccStepWritePolicy(t *testing.T, name string, derived bool) logicaltest.TestStep { - ts := logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "keys/" + name, - Data: map[string]interface{}{ - "derived": derived, - }, - } - if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" { - ts.Data["type"] = "chacha20-poly1305" - } - return ts -} - -func testAccStepListPolicy(t *testing.T, name string, expectNone bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "keys", - Check: func(resp *logical.Response) error { - if resp == nil { - return fmt.Errorf("missing response") - } - if expectNone { - keysRaw, ok := resp.Data["keys"] - if ok || keysRaw != nil { - return fmt.Errorf("response data when expecting none") - } - return nil - } - if len(resp.Data) == 0 { - return fmt.Errorf("no data returned") - } - - var d struct { - Keys []string `mapstructure:"keys"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if len(d.Keys) > 0 && d.Keys[0] != name { - return fmt.Errorf("bad name: %#v", d) - } - if len(d.Keys) != 1 { - return fmt.Errorf("only 1 key expected, %d returned", len(d.Keys)) - } - return nil - }, - } -} - -func testAccStepAdjustPolicyMinDecryption(t *testing.T, name string, minVer int) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "keys/" + name + "/config", - Data: map[string]interface{}{ - "min_decryption_version": minVer, - }, - } -} - -func testAccStepAdjustPolicyMinEncryption(t *testing.T, name string, minVer int) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "keys/" + name + "/config", - Data: map[string]interface{}{ - "min_encryption_version": minVer, - }, - } -} - -func testAccStepDisableDeletion(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "keys/" + name + "/config", - Data: map[string]interface{}{ - "deletion_allowed": false, - }, - } -} - -func testAccStepEnableDeletion(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "keys/" + name + "/config", - Data: map[string]interface{}{ - "deletion_allowed": true, - }, - } -} - -func testAccStepDeletePolicy(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "keys/" + name, - } -} - -func testAccStepDeleteNotDisabledPolicy(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "keys/" + name, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp == nil { - return fmt.Errorf("got nil response instead of error") - } - if resp.IsError() { - return nil - } - return fmt.Errorf("expected error but did not get one") - }, - } -} - -func testAccStepReadPolicy(t *testing.T, name string, expectNone, derived bool) logicaltest.TestStep { - return testAccStepReadPolicyWithVersions(t, name, expectNone, derived, 1, 0) -} - -func testAccStepReadPolicyWithVersions(t *testing.T, name string, expectNone, derived bool, minDecryptionVersion int, minEncryptionVersion int) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "keys/" + name, - Check: func(resp *logical.Response) error { - if resp == nil && !expectNone { - return fmt.Errorf("missing response") - } else if expectNone { - if resp != nil { - return fmt.Errorf("response when expecting none") - } - return nil - } - var d struct { - Name string `mapstructure:"name"` - Key []byte `mapstructure:"key"` - Keys map[string]int64 `mapstructure:"keys"` - Type string `mapstructure:"type"` - Derived bool `mapstructure:"derived"` - KDF string `mapstructure:"kdf"` - DeletionAllowed bool `mapstructure:"deletion_allowed"` - ConvergentEncryption bool `mapstructure:"convergent_encryption"` - MinDecryptionVersion int `mapstructure:"min_decryption_version"` - MinEncryptionVersion int `mapstructure:"min_encryption_version"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - if d.Name != name { - return fmt.Errorf("bad name: %#v", d) - } - if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" { - if d.Type != keysutil.KeyType(keysutil.KeyType_ChaCha20_Poly1305).String() { - return fmt.Errorf("bad key type: %#v", d) - } - } else if d.Type != keysutil.KeyType(keysutil.KeyType_AES256_GCM96).String() { - return fmt.Errorf("bad key type: %#v", d) - } - // Should NOT get a key back - if d.Key != nil { - return fmt.Errorf("bad: %#v", d) - } - if d.Keys == nil { - return fmt.Errorf("bad: %#v", d) - } - if d.MinDecryptionVersion != minDecryptionVersion { - return fmt.Errorf("bad: %#v", d) - } - if d.MinEncryptionVersion != minEncryptionVersion { - return fmt.Errorf("bad: %#v", d) - } - if d.DeletionAllowed { - return fmt.Errorf("bad: %#v", d) - } - if d.Derived != derived { - return fmt.Errorf("bad: %#v", d) - } - if derived && d.KDF != "hkdf_sha256" { - return fmt.Errorf("bad: %#v", d) - } - return nil - }, - } -} - -func testAccStepEncrypt( - t *testing.T, name, plaintext string, decryptData map[string]interface{}, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "encrypt/" + name, - Data: map[string]interface{}{ - "plaintext": base64.StdEncoding.EncodeToString([]byte(plaintext)), - }, - Check: func(resp *logical.Response) error { - var d struct { - Ciphertext string `mapstructure:"ciphertext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if d.Ciphertext == "" { - return fmt.Errorf("missing ciphertext") - } - decryptData["ciphertext"] = d.Ciphertext - return nil - }, - } -} - -func testAccStepEncryptUpsert( - t *testing.T, name, plaintext string, decryptData map[string]interface{}, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.CreateOperation, - Path: "encrypt/" + name, - Data: map[string]interface{}{ - "plaintext": base64.StdEncoding.EncodeToString([]byte(plaintext)), - }, - Check: func(resp *logical.Response) error { - var d struct { - Ciphertext string `mapstructure:"ciphertext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if d.Ciphertext == "" { - return fmt.Errorf("missing ciphertext") - } - decryptData["ciphertext"] = d.Ciphertext - return nil - }, - } -} - -func testAccStepEncryptContext( - t *testing.T, name, plaintext, context string, decryptData map[string]interface{}, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "encrypt/" + name, - Data: map[string]interface{}{ - "plaintext": base64.StdEncoding.EncodeToString([]byte(plaintext)), - "context": base64.StdEncoding.EncodeToString([]byte(context)), - }, - Check: func(resp *logical.Response) error { - var d struct { - Ciphertext string `mapstructure:"ciphertext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if d.Ciphertext == "" { - return fmt.Errorf("missing ciphertext") - } - decryptData["ciphertext"] = d.Ciphertext - decryptData["context"] = base64.StdEncoding.EncodeToString([]byte(context)) - return nil - }, - } -} - -func testAccStepDecrypt( - t *testing.T, name, plaintext string, decryptData map[string]interface{}, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "decrypt/" + name, - Data: decryptData, - Check: func(resp *logical.Response) error { - var d struct { - Plaintext string `mapstructure:"plaintext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - // Decode the base64 - plainRaw, err := base64.StdEncoding.DecodeString(d.Plaintext) - if err != nil { - return err - } - - if string(plainRaw) != plaintext { - return fmt.Errorf("plaintext mismatch: %s expect: %s, decryptData was %#v", plainRaw, plaintext, decryptData) - } - return nil - }, - } -} - -func testAccStepRewrap( - t *testing.T, name string, decryptData map[string]interface{}, expectedVer int, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "rewrap/" + name, - Data: decryptData, - Check: func(resp *logical.Response) error { - var d struct { - Ciphertext string `mapstructure:"ciphertext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if d.Ciphertext == "" { - return fmt.Errorf("missing ciphertext") - } - splitStrings := strings.Split(d.Ciphertext, ":") - verString := splitStrings[1][1:] - ver, err := strconv.Atoi(verString) - if err != nil { - return fmt.Errorf("error pulling out version from verString %q, ciphertext was %s", verString, d.Ciphertext) - } - if ver != expectedVer { - return fmt.Errorf("did not get expected version") - } - decryptData["ciphertext"] = d.Ciphertext - return nil - }, - } -} - -func testAccStepEncryptVX( - t *testing.T, name, plaintext string, decryptData map[string]interface{}, - ver int, encryptHistory map[int]map[string]interface{}, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "encrypt/" + name, - Data: map[string]interface{}{ - "plaintext": base64.StdEncoding.EncodeToString([]byte(plaintext)), - }, - Check: func(resp *logical.Response) error { - var d struct { - Ciphertext string `mapstructure:"ciphertext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if d.Ciphertext == "" { - return fmt.Errorf("missing ciphertext") - } - splitStrings := strings.Split(d.Ciphertext, ":") - splitStrings[1] = "v" + strconv.Itoa(ver) - ciphertext := strings.Join(splitStrings, ":") - decryptData["ciphertext"] = ciphertext - encryptHistory[ver] = map[string]interface{}{ - "ciphertext": ciphertext, - } - return nil - }, - } -} - -func testAccStepLoadVX( - t *testing.T, name string, decryptData map[string]interface{}, - ver int, encryptHistory map[int]map[string]interface{}, -) logicaltest.TestStep { - // This is really a no-op to allow us to do data manip in the check function - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "keys/" + name, - Check: func(resp *logical.Response) error { - decryptData["ciphertext"] = encryptHistory[ver]["ciphertext"].(string) - return nil - }, - } -} - -func testAccStepDecryptExpectFailure( - t *testing.T, name, plaintext string, decryptData map[string]interface{}, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "decrypt/" + name, - Data: decryptData, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if !resp.IsError() { - return fmt.Errorf("expected error") - } - return nil - }, - } -} - -func testAccStepRotate(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "keys/" + name + "/rotate", - } -} - -func testAccStepWriteDatakey(t *testing.T, name string, - noPlaintext bool, bits int, - dataKeyInfo map[string]interface{}, -) logicaltest.TestStep { - data := map[string]interface{}{} - subPath := "plaintext" - if noPlaintext { - subPath = "wrapped" - } - if bits != 256 { - data["bits"] = bits - } - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "datakey/" + subPath + "/" + name, - Data: data, - Check: func(resp *logical.Response) error { - var d struct { - Plaintext string `mapstructure:"plaintext"` - Ciphertext string `mapstructure:"ciphertext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if noPlaintext && len(d.Plaintext) != 0 { - return fmt.Errorf("received plaintxt when we disabled it") - } - if !noPlaintext { - if len(d.Plaintext) == 0 { - return fmt.Errorf("did not get plaintext when we expected it") - } - dataKeyInfo["plaintext"] = d.Plaintext - plainBytes, err := base64.StdEncoding.DecodeString(d.Plaintext) - if err != nil { - return fmt.Errorf("could not base64 decode plaintext string %q", d.Plaintext) - } - if len(plainBytes)*8 != bits { - return fmt.Errorf("returned key does not have correct bit length") - } - } - dataKeyInfo["ciphertext"] = d.Ciphertext - return nil - }, - } -} - -func testAccStepDecryptDatakey(t *testing.T, name string, - dataKeyInfo map[string]interface{}, -) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "decrypt/" + name, - Data: dataKeyInfo, - Check: func(resp *logical.Response) error { - var d struct { - Plaintext string `mapstructure:"plaintext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - if d.Plaintext != dataKeyInfo["plaintext"].(string) { - return fmt.Errorf("plaintext mismatch: got %q, expected %q, decryptData was %#v", d.Plaintext, dataKeyInfo["plaintext"].(string), resp.Data) - } - return nil - }, - } -} - -func TestKeyUpgrade(t *testing.T) { - key, _ := uuid.GenerateRandomBytes(32) - p := &keysutil.Policy{ - Name: "test", - Key: key, - Type: keysutil.KeyType_AES256_GCM96, - } - - p.MigrateKeyToKeysMap() - - if p.Key != nil || - p.Keys == nil || - len(p.Keys) != 1 || - !reflect.DeepEqual(p.Keys[strconv.Itoa(1)].Key, key) { - t.Errorf("bad key migration, result is %#v", p.Keys) - } -} - -func TestDerivedKeyUpgrade(t *testing.T) { - testDerivedKeyUpgrade(t, keysutil.KeyType_AES256_GCM96) - testDerivedKeyUpgrade(t, keysutil.KeyType_ChaCha20_Poly1305) -} - -func testDerivedKeyUpgrade(t *testing.T, keyType keysutil.KeyType) { - storage := &logical.InmemStorage{} - key, _ := uuid.GenerateRandomBytes(32) - keyContext, _ := uuid.GenerateRandomBytes(32) - - p := &keysutil.Policy{ - Name: "test", - Key: key, - Type: keyType, - Derived: true, - } - - p.MigrateKeyToKeysMap() - p.Upgrade(context.Background(), storage, cryptoRand.Reader) // Need to run the upgrade code to make the migration stick - - if p.KDF != keysutil.Kdf_hmac_sha256_counter { - t.Fatalf("bad KDF value by default; counter val is %d, KDF val is %d, policy is %#v", keysutil.Kdf_hmac_sha256_counter, p.KDF, *p) - } - - derBytesOld, err := p.GetKey(keyContext, 1, 0) - if err != nil { - t.Fatal(err) - } - - derBytesOld2, err := p.GetKey(keyContext, 1, 0) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(derBytesOld, derBytesOld2) { - t.Fatal("mismatch of same context alg") - } - - p.KDF = keysutil.Kdf_hkdf_sha256 - if p.NeedsUpgrade() { - t.Fatal("expected no upgrade needed") - } - - derBytesNew, err := p.GetKey(keyContext, 1, 64) - if err != nil { - t.Fatal(err) - } - - derBytesNew2, err := p.GetKey(keyContext, 1, 64) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(derBytesNew, derBytesNew2) { - t.Fatal("mismatch of same context alg") - } - - if reflect.DeepEqual(derBytesOld, derBytesNew) { - t.Fatal("match of different context alg") - } -} - -func TestConvergentEncryption(t *testing.T) { - testConvergentEncryptionCommon(t, 0, keysutil.KeyType_AES256_GCM96) - testConvergentEncryptionCommon(t, 2, keysutil.KeyType_AES128_GCM96) - testConvergentEncryptionCommon(t, 2, keysutil.KeyType_AES256_GCM96) - testConvergentEncryptionCommon(t, 2, keysutil.KeyType_ChaCha20_Poly1305) - testConvergentEncryptionCommon(t, 3, keysutil.KeyType_AES128_GCM96) - testConvergentEncryptionCommon(t, 3, keysutil.KeyType_AES256_GCM96) - testConvergentEncryptionCommon(t, 3, keysutil.KeyType_ChaCha20_Poly1305) -} - -func testConvergentEncryptionCommon(t *testing.T, ver int, keyType keysutil.KeyType) { - b, storage := createBackendWithSysView(t) - - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/testkeynonderived", - Data: map[string]interface{}{ - "derived": false, - "convergent_encryption": true, - "type": keyType.String(), - }, - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if !resp.IsError() { - t.Fatalf("bad: expected error response, got %#v", *resp) - } - - req = &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/testkey", - Data: map[string]interface{}{ - "derived": true, - "convergent_encryption": true, - "type": keyType.String(), - }, - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - require.NotNil(t, resp, "expected populated request") - - p, err := keysutil.LoadPolicy(context.Background(), storage, path.Join("policy", "testkey")) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("got nil policy") - } - - if ver > 2 { - p.ConvergentVersion = -1 - } else { - p.ConvergentVersion = ver - } - err = p.Persist(context.Background(), storage) - if err != nil { - t.Fatal(err) - } - b.invalidate(context.Background(), "policy/testkey") - - if ver < 3 { - // There will be an embedded key version of 3, so specifically clear it - key := p.Keys[strconv.Itoa(p.LatestVersion)] - key.ConvergentVersion = 0 - p.Keys[strconv.Itoa(p.LatestVersion)] = key - err = p.Persist(context.Background(), storage) - if err != nil { - t.Fatal(err) - } - b.invalidate(context.Background(), "policy/testkey") - - // Verify it - p, err = keysutil.LoadPolicy(context.Background(), storage, path.Join(p.StoragePrefix, "policy", "testkey")) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("got nil policy") - } - if p.ConvergentVersion != ver { - t.Fatalf("bad convergent version %d", p.ConvergentVersion) - } - key = p.Keys[strconv.Itoa(p.LatestVersion)] - if key.ConvergentVersion != 0 { - t.Fatalf("bad convergent key version %d", key.ConvergentVersion) - } - } - - // First, test using an invalid length of nonce -- this is only used for v1 convergent - req.Path = "encrypt/testkey" - if ver < 2 { - req.Data = map[string]interface{}{ - "plaintext": "emlwIHphcA==", // "zip zap" - "nonce": "Zm9vIGJhcg==", // "foo bar" - "context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S", - } - resp, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatalf("expected error, got nil, version is %d", ver) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if !resp.IsError() { - t.Fatalf("expected error response, got %#v", *resp) - } - - // Ensure we fail if we do not provide a nonce - req.Data = map[string]interface{}{ - "plaintext": "emlwIHphcA==", // "zip zap" - "context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S", - } - resp, err = b.HandleRequest(context.Background(), req) - if err == nil && (resp == nil || !resp.IsError()) { - t.Fatal("expected error response") - } - } - - // Now test encrypting the same value twice - req.Data = map[string]interface{}{ - "plaintext": "emlwIHphcA==", // "zip zap" - "context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S", - } - if ver == 0 { - req.Data["nonce"] = "b25ldHdvdGhyZWVl" // "onetwothreee" - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext1 := resp.Data["ciphertext"].(string) - - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext2 := resp.Data["ciphertext"].(string) - - if ciphertext1 != ciphertext2 { - t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext1, ciphertext2) - } - - // For sanity, also check a different nonce value... - req.Data = map[string]interface{}{ - "plaintext": "emlwIHphcA==", // "zip zap" - "context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S", - } - if ver == 0 { - req.Data["nonce"] = "dHdvdGhyZWVmb3Vy" // "twothreefour" - } else { - req.Data["context"] = "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOldandSdd7S" - } - - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext3 := resp.Data["ciphertext"].(string) - - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext4 := resp.Data["ciphertext"].(string) - - if ciphertext3 != ciphertext4 { - t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext3, ciphertext4) - } - if ciphertext1 == ciphertext3 { - t.Fatalf("expected different ciphertexts") - } - - // ...and a different context value - req.Data = map[string]interface{}{ - "plaintext": "emlwIHphcA==", // "zip zap" - "context": "qV4h9iQyvn+raODOer4JNAsOhkXBwdT4HZ677Ql4KLqXSU+Jk4C/fXBWbv6xkSYT", - } - if ver == 0 { - req.Data["nonce"] = "dHdvdGhyZWVmb3Vy" // "twothreefour" - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext5 := resp.Data["ciphertext"].(string) - - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext6 := resp.Data["ciphertext"].(string) - - if ciphertext5 != ciphertext6 { - t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext5, ciphertext6) - } - if ciphertext1 == ciphertext5 { - t.Fatalf("expected different ciphertexts") - } - if ciphertext3 == ciphertext5 { - t.Fatalf("expected different ciphertexts") - } - - // If running version 2, check upgrade handling - if ver == 2 { - curr, err := keysutil.LoadPolicy(context.Background(), storage, path.Join(p.StoragePrefix, "policy", "testkey")) - if err != nil { - t.Fatal(err) - } - if curr == nil { - t.Fatal("got nil policy") - } - if curr.ConvergentVersion != 2 { - t.Fatalf("bad convergent version %d", curr.ConvergentVersion) - } - key := curr.Keys[strconv.Itoa(curr.LatestVersion)] - if key.ConvergentVersion != 0 { - t.Fatalf("bad convergent key version %d", key.ConvergentVersion) - } - - curr.ConvergentVersion = 3 - err = curr.Persist(context.Background(), storage) - if err != nil { - t.Fatal(err) - } - b.invalidate(context.Background(), "policy/testkey") - - // Different algorithm, should be different value - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext7 := resp.Data["ciphertext"].(string) - - // Now do it via key-specified version - if len(curr.Keys) != 1 { - t.Fatalf("unexpected length of keys %d", len(curr.Keys)) - } - key = curr.Keys[strconv.Itoa(curr.LatestVersion)] - key.ConvergentVersion = 3 - curr.Keys[strconv.Itoa(curr.LatestVersion)] = key - curr.ConvergentVersion = 2 - err = curr.Persist(context.Background(), storage) - if err != nil { - t.Fatal(err) - } - b.invalidate(context.Background(), "policy/testkey") - - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext8 := resp.Data["ciphertext"].(string) - - if ciphertext7 != ciphertext8 { - t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext7, ciphertext8) - } - if ciphertext6 == ciphertext7 { - t.Fatalf("expected different ciphertexts") - } - if ciphertext3 == ciphertext7 { - t.Fatalf("expected different ciphertexts") - } - } - - // Finally, check operations on empty values - // First, check without setting a plaintext at all - req.Data = map[string]interface{}{ - "context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S", - } - if ver == 0 { - req.Data["nonce"] = "dHdvdGhyZWVmb3Vy" // "twothreefour" - } - resp, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("expected error, got nil") - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if !resp.IsError() { - t.Fatalf("expected error response, got: %#v", *resp) - } - - // Now set plaintext to empty - req.Data = map[string]interface{}{ - "plaintext": "", - "context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S", - } - if ver == 0 { - req.Data["nonce"] = "dHdvdGhyZWVmb3Vy" // "twothreefour" - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext7 := resp.Data["ciphertext"].(string) - - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("got error response: %#v", *resp) - } - ciphertext8 := resp.Data["ciphertext"].(string) - - if ciphertext7 != ciphertext8 { - t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext7, ciphertext8) - } -} - -func TestPolicyFuzzing(t *testing.T) { - var be *backend - sysView := logical.TestSystemView() - sysView.CachingDisabledVal = true - conf := &logical.BackendConfig{ - System: sysView, - } - - be, _ = Backend(context.Background(), conf) - be.Setup(context.Background(), conf) - testPolicyFuzzingCommon(t, be) - - sysView.CachingDisabledVal = true - be, _ = Backend(context.Background(), conf) - be.Setup(context.Background(), conf) - testPolicyFuzzingCommon(t, be) -} - -func testPolicyFuzzingCommon(t *testing.T, be *backend) { - storage := &logical.InmemStorage{} - wg := sync.WaitGroup{} - - funcs := []string{"encrypt", "decrypt", "rotate", "change_min_version"} - // keys := []string{"test1", "test2", "test3", "test4", "test5"} - keys := []string{"test1", "test2", "test3"} - - // This is the goroutine loop - doFuzzy := func(id int) { - // Check for panics, otherwise notify we're done - defer func() { - wg.Done() - }() - - // Holds the latest encrypted value for each key - latestEncryptedText := map[string]string{} - - startTime := time.Now() - req := &logical.Request{ - Storage: storage, - Data: map[string]interface{}{}, - } - fd := &framework.FieldData{} - - var chosenFunc, chosenKey string - - // t.Errorf("Starting %d", id) - for { - // Stop after 10 seconds - if time.Now().Sub(startTime) > 10*time.Second { - return - } - - // Pick a function and a key - chosenFunc = funcs[rand.Int()%len(funcs)] - chosenKey = keys[rand.Int()%len(keys)] - - fd.Raw = map[string]interface{}{ - "name": chosenKey, - } - fd.Schema = be.pathKeys().Fields - - // Try to write the key to make sure it exists - _, err := be.pathPolicyWrite(context.Background(), req, fd) - if err != nil { - t.Errorf("got an error: %v", err) - } - - switch chosenFunc { - // Encrypt our plaintext and store the result - case "encrypt": - // t.Errorf("%s, %s, %d", chosenFunc, chosenKey, id) - fd.Raw["plaintext"] = base64.StdEncoding.EncodeToString([]byte(testPlaintext)) - fd.Schema = be.pathEncrypt().Fields - resp, err := be.pathEncryptWrite(context.Background(), req, fd) - if err != nil { - t.Errorf("got an error: %v, resp is %#v", err, *resp) - } - latestEncryptedText[chosenKey] = resp.Data["ciphertext"].(string) - - // Rotate to a new key version - case "rotate": - // t.Errorf("%s, %s, %d", chosenFunc, chosenKey, id) - fd.Schema = be.pathRotate().Fields - resp, err := be.pathRotateWrite(context.Background(), req, fd) - if err != nil { - t.Errorf("got an error: %v, resp is %#v, chosenKey is %s", err, *resp, chosenKey) - } - - // Decrypt the ciphertext and compare the result - case "decrypt": - // t.Errorf("%s, %s, %d", chosenFunc, chosenKey, id) - ct := latestEncryptedText[chosenKey] - if ct == "" { - continue - } - - fd.Raw["ciphertext"] = ct - fd.Schema = be.pathDecrypt().Fields - resp, err := be.pathDecryptWrite(context.Background(), req, fd) - if err != nil { - // This could well happen since the min version is jumping around - if resp.Data["error"].(string) == keysutil.ErrTooOld { - continue - } - t.Errorf("got an error: %v, resp is %#v, ciphertext was %s, chosenKey is %s, id is %d", err, *resp, ct, chosenKey, id) - } - ptb64, ok := resp.Data["plaintext"].(string) - if !ok { - t.Errorf("no plaintext found, response was %#v", *resp) - return - } - pt, err := base64.StdEncoding.DecodeString(ptb64) - if err != nil { - t.Errorf("got an error decoding base64 plaintext: %v", err) - return - } - if string(pt) != testPlaintext { - t.Errorf("got bad plaintext back: %s", pt) - } - - // Change the min version, which also tests the archive functionality - case "change_min_version": - // t.Errorf("%s, %s, %d", chosenFunc, chosenKey, id) - resp, err := be.pathPolicyRead(context.Background(), req, fd) - if err != nil { - t.Errorf("got an error reading policy %s: %v", chosenKey, err) - } - latestVersion := resp.Data["latest_version"].(int) - - // keys start at version 1 so we want [1, latestVersion] not [0, latestVersion) - setVersion := (rand.Int() % latestVersion) + 1 - fd.Raw["min_decryption_version"] = setVersion - fd.Schema = be.pathKeysConfig().Fields - resp, err = be.pathKeysConfigWrite(context.Background(), req, fd) - if err != nil { - t.Errorf("got an error setting min decryption version: %v", err) - } - } - } - } - - // Spawn 1000 of these workers for 10 seconds - for i := 0; i < 1000; i++ { - wg.Add(1) - go doFuzzy(i) - } - - // Wait for them all to finish - wg.Wait() -} - -func TestBadInput(t *testing.T) { - b, storage := createBackendWithSysView(t) - - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/test", - } - - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - require.NotNil(t, resp, "expected populated request") - - req.Path = "decrypt/test" - req.Data = map[string]interface{}{ - "ciphertext": "vault:v1:abcd", - } - - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("expected error") - } -} - -func TestTransit_AutoRotateKeys(t *testing.T) { - tests := map[string]struct { - isDRSecondary bool - isPerfSecondary bool - isStandby bool - isLocal bool - shouldRotate bool - }{ - "primary, no local mount": { - shouldRotate: true, - }, - "DR secondary, no local mount": { - isDRSecondary: true, - shouldRotate: false, - }, - "perf standby, no local mount": { - isStandby: true, - shouldRotate: false, - }, - "perf secondary, no local mount": { - isPerfSecondary: true, - shouldRotate: false, - }, - "perf secondary, local mount": { - isPerfSecondary: true, - isLocal: true, - shouldRotate: true, - }, - } - - for name, test := range tests { - t.Run( - name, - func(t *testing.T) { - var repState consts.ReplicationState - if test.isDRSecondary { - repState.AddState(consts.ReplicationDRSecondary) - } - if test.isPerfSecondary { - repState.AddState(consts.ReplicationPerformanceSecondary) - } - if test.isStandby { - repState.AddState(consts.ReplicationPerformanceStandby) - } - - sysView := logical.TestSystemView() - sysView.ReplicationStateVal = repState - sysView.LocalMountVal = test.isLocal - - storage := &logical.InmemStorage{} - - conf := &logical.BackendConfig{ - StorageView: storage, - System: sysView, - } - - b, _ := Backend(context.Background(), conf) - if b == nil { - t.Fatal("failed to create backend") - } - - err := b.Backend.Setup(context.Background(), conf) - if err != nil { - t.Fatal(err) - } - - // Write a key with the default auto rotate value (0/disabled) - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/test1", - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - require.NotNil(t, resp, "expected populated request") - - // Write a key with an auto rotate value one day in the future - req = &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/test2", - Data: map[string]interface{}{ - "auto_rotate_period": 24 * time.Hour, - }, - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - require.NotNil(t, resp, "expected populated request") - - // Run the rotation check and ensure none of the keys have rotated - b.checkAutoRotateAfter = time.Now() - if err = b.autoRotateKeys(context.Background(), &logical.Request{Storage: storage}); err != nil { - t.Fatal(err) - } - req = &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "keys/test1", - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.Data["latest_version"] != 1 { - t.Fatalf("incorrect latest_version found, got: %d, want: %d", resp.Data["latest_version"], 1) - } - - req.Path = "keys/test2" - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.Data["latest_version"] != 1 { - t.Fatalf("incorrect latest_version found, got: %d, want: %d", resp.Data["latest_version"], 1) - } - - // Update auto rotate period on one key to be one nanosecond - p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ - Storage: storage, - Name: "test2", - }, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("expected non-nil policy") - } - p.AutoRotatePeriod = time.Nanosecond - err = p.Persist(context.Background(), storage) - if err != nil { - t.Fatal(err) - } - - // Run the rotation check and validate the state of key rotations - b.checkAutoRotateAfter = time.Now() - if err = b.autoRotateKeys(context.Background(), &logical.Request{Storage: storage}); err != nil { - t.Fatal(err) - } - req = &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "keys/test1", - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.Data["latest_version"] != 1 { - t.Fatalf("incorrect latest_version found, got: %d, want: %d", resp.Data["latest_version"], 1) - } - req.Path = "keys/test2" - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - expectedVersion := 1 - if test.shouldRotate { - expectedVersion = 2 - } - if resp.Data["latest_version"] != expectedVersion { - t.Fatalf("incorrect latest_version found, got: %d, want: %d", resp.Data["latest_version"], expectedVersion) - } - }, - ) - } -} - -func TestTransit_AEAD(t *testing.T) { - testTransit_AEAD(t, "aes128-gcm96") - testTransit_AEAD(t, "aes256-gcm96") - testTransit_AEAD(t, "chacha20-poly1305") -} - -func testTransit_AEAD(t *testing.T, keyType string) { - var resp *logical.Response - var err error - b, storage := createBackendWithStorage(t) - - keyReq := &logical.Request{ - Path: "keys/aead", - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "type": keyType, - }, - Storage: storage, - } - - resp, err = b.HandleRequest(context.Background(), keyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox" - associated := "U3BoaW54IG9mIGJsYWNrIHF1YXJ0eiwganVkZ2UgbXkgdm93Lgo=" // "Sphinx of black quartz, judge my vow." - - // Basic encrypt/decrypt should work. - encryptReq := &logical.Request{ - Path: "encrypt/aead", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "plaintext": plaintext, - }, - } - - resp, err = b.HandleRequest(context.Background(), encryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - - ciphertext1 := resp.Data["ciphertext"].(string) - - decryptReq := &logical.Request{ - Path: "decrypt/aead", - Operation: logical.UpdateOperation, - Storage: storage, - Data: map[string]interface{}{ - "ciphertext": ciphertext1, - }, - } - - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - - decryptedPlaintext := resp.Data["plaintext"] - - if plaintext != decryptedPlaintext { - t.Fatalf("bad: plaintext; expected: %q\nactual: %q", plaintext, decryptedPlaintext) - } - - // Using associated as ciphertext should fail. - decryptReq.Data["ciphertext"] = associated - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err == nil || (resp != nil && !resp.IsError()) { - t.Fatalf("bad expected error: err: %v\nresp: %#v", err, resp) - } - - // Redoing the above with additional data should work. - encryptReq.Data["associated_data"] = associated - resp, err = b.HandleRequest(context.Background(), encryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - - ciphertext2 := resp.Data["ciphertext"].(string) - decryptReq.Data["ciphertext"] = ciphertext2 - decryptReq.Data["associated_data"] = associated - - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: err: %v\nresp: %#v", err, resp) - } - - decryptedPlaintext = resp.Data["plaintext"] - if plaintext != decryptedPlaintext { - t.Fatalf("bad: plaintext; expected: %q\nactual: %q", plaintext, decryptedPlaintext) - } - - // Removing the associated_data should break the decryption. - decryptReq.Data = map[string]interface{}{ - "ciphertext": ciphertext2, - } - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err == nil || (resp != nil && !resp.IsError()) { - t.Fatalf("bad expected error: err: %v\nresp: %#v", err, resp) - } - - // Using a valid ciphertext with associated_data should also break the - // decryption. - decryptReq.Data["ciphertext"] = ciphertext1 - decryptReq.Data["associated_data"] = associated - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err == nil || (resp != nil && !resp.IsError()) { - t.Fatalf("bad expected error: err: %v\nresp: %#v", err, resp) - } -} - -// Hack: use Transit as a signer. -type transitKey struct { - public any - mount string - name string - t *testing.T - client *api.Client -} - -func (k *transitKey) Public() crypto.PublicKey { - return k.public -} - -func (k *transitKey) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { - hash := opts.(crypto.Hash) - if hash.String() != "SHA-256" { - return nil, fmt.Errorf("unknown hash algorithm: %v", opts) - } - - resp, err := k.client.Logical().Write(k.mount+"/sign/"+k.name, map[string]interface{}{ - "hash_algorithm": "sha2-256", - "input": base64.StdEncoding.EncodeToString(digest), - "prehashed": true, - "signature_algorithm": "pkcs1v15", - }) - if err != nil { - return nil, fmt.Errorf("failed to sign data: %w", err) - } - require.NotNil(k.t, resp) - require.NotNil(k.t, resp.Data) - require.NotNil(k.t, resp.Data["signature"]) - rawSig := resp.Data["signature"].(string) - sigParts := strings.Split(rawSig, ":") - - decoded, err := base64.StdEncoding.DecodeString(sigParts[2]) - if err != nil { - return nil, fmt.Errorf("failed to decode signature (%v): %w", rawSig, err) - } - - return decoded, nil -} - -func TestTransitPKICSR(t *testing.T) { - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": Factory, - "pki": pki.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - client := cores[0].Client - - // Mount transit, write a key. - err := client.Sys().Mount("transit", &api.MountInput{ - Type: "transit", - }) - require.NoError(t, err) - - _, err = client.Logical().Write("transit/keys/leaf", map[string]interface{}{ - "type": "rsa-2048", - }) - require.NoError(t, err) - - resp, err := client.Logical().Read("transit/keys/leaf") - require.NoError(t, err) - require.NotNil(t, resp) - - keys := resp.Data["keys"].(map[string]interface{}) - require.NotNil(t, keys) - keyData := keys["1"].(map[string]interface{}) - require.NotNil(t, keyData) - keyPublic := keyData["public_key"].(string) - require.NotEmpty(t, keyPublic) - - pemBlock, _ := pem.Decode([]byte(keyPublic)) - require.NotNil(t, pemBlock) - pubKey, err := x509.ParsePKIXPublicKey(pemBlock.Bytes) - require.NoError(t, err) - require.NotNil(t, pubKey) - - // Setup a new CSR... - var reqTemplate x509.CertificateRequest - reqTemplate.PublicKey = pubKey - reqTemplate.PublicKeyAlgorithm = x509.RSA - reqTemplate.Subject.CommonName = "dadgarcorp.com" - - var k transitKey - k.public = pubKey - k.mount = "transit" - k.name = "leaf" - k.t = t - k.client = client - - req, err := x509.CreateCertificateRequest(cryptoRand.Reader, &reqTemplate, &k) - require.NoError(t, err) - require.NotNil(t, req) - - reqPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: req, - }) - t.Logf("csr: %v", string(reqPEM)) - - // Mount PKI, generate a root, sign this CSR. - err = client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - }) - require.NoError(t, err) - - resp, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "common_name": "PKI Root X1", - }) - require.NoError(t, err) - require.NotNil(t, resp) - rootCertPEM := resp.Data["certificate"].(string) - - pemBlock, _ = pem.Decode([]byte(rootCertPEM)) - require.NotNil(t, pemBlock) - - rootCert, err := x509.ParseCertificate(pemBlock.Bytes) - require.NoError(t, err) - - resp, err = client.Logical().Write("pki/issuer/default/sign-verbatim", map[string]interface{}{ - "csr": string(reqPEM), - "ttl": "10m", - }) - require.NoError(t, err) - require.NotNil(t, resp) - - leafCertPEM := resp.Data["certificate"].(string) - pemBlock, _ = pem.Decode([]byte(leafCertPEM)) - require.NotNil(t, pemBlock) - - leafCert, err := x509.ParseCertificate(pemBlock.Bytes) - require.NoError(t, err) - require.NoError(t, leafCert.CheckSignatureFrom(rootCert)) - t.Logf("root: %v", rootCertPEM) - t.Logf("leaf: %v", leafCertPEM) -} - -func TestTransit_ReadPublicKeyImported(t *testing.T) { - testTransit_ReadPublicKeyImported(t, "rsa-2048") - testTransit_ReadPublicKeyImported(t, "ecdsa-p256") - testTransit_ReadPublicKeyImported(t, "ed25519") -} - -func testTransit_ReadPublicKeyImported(t *testing.T, keyType string) { - generateKeys(t) - b, s := createBackendWithStorage(t) - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get key - privateKey := getKey(t, keyType) - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatalf("failed to extract the public key: %s", err) - } - - // Import key - importReq := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - "type": keyType, - }, - } - importResp, err := b.HandleRequest(context.Background(), importReq) - if err != nil || (importResp != nil && importResp.IsError()) { - t.Fatalf("failed to import public key. err: %s\nresp: %#v", err, importResp) - } - - // Read key - readReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "keys/" + keyID, - Storage: s, - } - - readResp, err := b.HandleRequest(context.Background(), readReq) - if err != nil || (readResp != nil && readResp.IsError()) { - t.Fatalf("failed to read key. err: %s\nresp: %#v", err, readResp) - } -} - -func TestTransit_SignWithImportedPublicKey(t *testing.T) { - testTransit_SignWithImportedPublicKey(t, "rsa-2048") - testTransit_SignWithImportedPublicKey(t, "ecdsa-p256") - testTransit_SignWithImportedPublicKey(t, "ed25519") -} - -func testTransit_SignWithImportedPublicKey(t *testing.T, keyType string) { - generateKeys(t) - b, s := createBackendWithStorage(t) - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get key - privateKey := getKey(t, keyType) - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatalf("failed to extract the public key: %s", err) - } - - // Import key - importReq := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - "type": keyType, - }, - } - importResp, err := b.HandleRequest(context.Background(), importReq) - if err != nil || (importResp != nil && importResp.IsError()) { - t.Fatalf("failed to import public key. err: %s\nresp: %#v", err, importResp) - } - - // Sign text - signReq := &logical.Request{ - Path: "sign/" + keyID, - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "plaintext": base64.StdEncoding.EncodeToString([]byte(testPlaintext)), - }, - } - - _, err = b.HandleRequest(context.Background(), signReq) - if err == nil { - t.Fatalf("expected error, should have failed to sign input") - } -} - -func TestTransit_VerifyWithImportedPublicKey(t *testing.T) { - generateKeys(t) - keyType := "rsa-2048" - b, s := createBackendWithStorage(t) - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get key - privateKey := getKey(t, keyType) - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatal(err) - } - - // Retrieve public wrapping key - wrappingKey, err := b.getWrappingKey(context.Background(), s) - if err != nil || wrappingKey == nil { - t.Fatalf("failed to retrieve public wrapping key: %s", err) - } - - privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey - pubWrappingKey := &privWrappingKey.PublicKey - - // generate ciphertext - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, privateKey, keyType, "SHA256") - - // Import private key - importReq := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - "type": keyType, - }, - } - importResp, err := b.HandleRequest(context.Background(), importReq) - if err != nil || (importResp != nil && importResp.IsError()) { - t.Fatalf("failed to import key. err: %s\nresp: %#v", err, importResp) - } - - // Sign text - signReq := &logical.Request{ - Storage: s, - Path: "sign/" + keyID, - Operation: logical.UpdateOperation, - Data: map[string]interface{}{ - "plaintext": base64.StdEncoding.EncodeToString([]byte(testPlaintext)), - }, - } - - signResp, err := b.HandleRequest(context.Background(), signReq) - if err != nil || (signResp != nil && signResp.IsError()) { - t.Fatalf("failed to sign plaintext. err: %s\nresp: %#v", err, signResp) - } - - // Get signature - signature := signResp.Data["signature"].(string) - - // Import new key as public key - importPubReq := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", "public-key-rsa"), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - "type": keyType, - }, - } - importPubResp, err := b.HandleRequest(context.Background(), importPubReq) - if err != nil || (importPubResp != nil && importPubResp.IsError()) { - t.Fatalf("failed to import public key. err: %s\nresp: %#v", err, importPubResp) - } - - // Verify signed text - verifyReq := &logical.Request{ - Path: "verify/public-key-rsa", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "input": base64.StdEncoding.EncodeToString([]byte(testPlaintext)), - "signature": signature, - }, - } - - verifyResp, err := b.HandleRequest(context.Background(), verifyReq) - if err != nil || (importResp != nil && verifyResp.IsError()) { - t.Fatalf("failed to verify signed data. err: %s\nresp: %#v", err, importResp) - } -} - -func TestTransit_ExportPublicKeyImported(t *testing.T) { - testTransit_ExportPublicKeyImported(t, "rsa-2048") - testTransit_ExportPublicKeyImported(t, "ecdsa-p256") - testTransit_ExportPublicKeyImported(t, "ed25519") -} - -func testTransit_ExportPublicKeyImported(t *testing.T, keyType string) { - generateKeys(t) - b, s := createBackendWithStorage(t) - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get key - privateKey := getKey(t, keyType) - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatalf("failed to extract the public key: %s", err) - } - - t.Logf("generated key: %v", string(publicKeyBytes)) - - // Import key - importReq := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - "type": keyType, - "exportable": true, - }, - } - importResp, err := b.HandleRequest(context.Background(), importReq) - if err != nil || (importResp != nil && importResp.IsError()) { - t.Fatalf("failed to import public key. err: %s\nresp: %#v", err, importResp) - } - - t.Logf("importing key: %v", importResp) - - // Export key - exportReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: fmt.Sprintf("export/public-key/%s/latest", keyID), - Storage: s, - } - - exportResp, err := b.HandleRequest(context.Background(), exportReq) - if err != nil || (exportResp != nil && exportResp.IsError()) { - t.Fatalf("failed to export key. err: %v\nresp: %#v", err, exportResp) - } - - t.Logf("exporting key: %v", exportResp) - - responseKeys, exist := exportResp.Data["keys"] - if !exist { - t.Fatal("expected response data to hold a 'keys' field") - } - - exportedKeyBytes := responseKeys.(map[string]string)["1"] - - if keyType != "ed25519" { - exportedKeyBlock, _ := pem.Decode([]byte(exportedKeyBytes)) - publicKeyBlock, _ := pem.Decode(publicKeyBytes) - - if !reflect.DeepEqual(publicKeyBlock.Bytes, exportedKeyBlock.Bytes) { - t.Fatalf("exported key bytes should have matched with imported key for key type: %v\nexported: %v\nimported: %v", keyType, exportedKeyBlock.Bytes, publicKeyBlock.Bytes) - } - } else { - exportedKey, err := base64.StdEncoding.DecodeString(exportedKeyBytes) - if err != nil { - t.Fatalf("error decoding exported key bytes (%v) to base64 for key type %v: %v", exportedKeyBytes, keyType, err) - } - - publicKeyBlock, _ := pem.Decode(publicKeyBytes) - publicKeyParsed, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes) - if err != nil { - t.Fatalf("error decoding source key bytes (%v) from PKIX marshaling for key type %v: %v", publicKeyBlock.Bytes, keyType, err) - } - - if !reflect.DeepEqual([]byte(publicKeyParsed.(ed25519.PublicKey)), exportedKey) { - t.Fatalf("exported key bytes should have matched with imported key for key type: %v\nexported: %v\nimported: %v", keyType, exportedKey, publicKeyParsed) - } - } -} diff --git a/builtin/logical/transit/path_backup_test.go b/builtin/logical/transit/path_backup_test.go deleted file mode 100644 index 05d7a10f6..000000000 --- a/builtin/logical/transit/path_backup_test.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTransit_BackupRestore(t *testing.T) { - // Test encryption/decryption after a restore for supported keys - testBackupRestore(t, "aes128-gcm96", "encrypt-decrypt") - testBackupRestore(t, "aes256-gcm96", "encrypt-decrypt") - testBackupRestore(t, "chacha20-poly1305", "encrypt-decrypt") - testBackupRestore(t, "rsa-2048", "encrypt-decrypt") - testBackupRestore(t, "rsa-3072", "encrypt-decrypt") - testBackupRestore(t, "rsa-4096", "encrypt-decrypt") - - // Test signing/verification after a restore for supported keys - testBackupRestore(t, "ecdsa-p256", "sign-verify") - testBackupRestore(t, "ecdsa-p384", "sign-verify") - testBackupRestore(t, "ecdsa-p521", "sign-verify") - testBackupRestore(t, "ed25519", "sign-verify") - testBackupRestore(t, "rsa-2048", "sign-verify") - testBackupRestore(t, "rsa-3072", "sign-verify") - testBackupRestore(t, "rsa-4096", "sign-verify") - - // Test HMAC/verification after a restore for all key types - testBackupRestore(t, "aes128-gcm96", "hmac-verify") - testBackupRestore(t, "aes256-gcm96", "hmac-verify") - testBackupRestore(t, "chacha20-poly1305", "hmac-verify") - testBackupRestore(t, "ecdsa-p256", "hmac-verify") - testBackupRestore(t, "ecdsa-p384", "hmac-verify") - testBackupRestore(t, "ecdsa-p521", "hmac-verify") - testBackupRestore(t, "ed25519", "hmac-verify") - testBackupRestore(t, "rsa-2048", "hmac-verify") - testBackupRestore(t, "rsa-3072", "hmac-verify") - testBackupRestore(t, "rsa-4096", "hmac-verify") - testBackupRestore(t, "hmac", "hmac-verify") -} - -func testBackupRestore(t *testing.T, keyType, feature string) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - // Create a key - keyReq := &logical.Request{ - Path: "keys/test", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "type": keyType, - "exportable": true, - }, - } - if keyType == "hmac" { - keyReq.Data["key_size"] = 32 - } - resp, err = b.HandleRequest(context.Background(), keyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Configure the key to allow its deletion - configReq := &logical.Request{ - Path: "keys/test/config", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "deletion_allowed": true, - "allow_plaintext_backup": true, - }, - } - resp, err = b.HandleRequest(context.Background(), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Take a backup of the key - backupReq := &logical.Request{ - Path: "backup/test", - Operation: logical.ReadOperation, - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), backupReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - backup := resp.Data["backup"] - - // Try to restore the key without deleting it. Expect error due to - // conflicting key names. - restoreReq := &logical.Request{ - Path: "restore", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "backup": backup, - }, - } - resp, err = b.HandleRequest(context.Background(), restoreReq) - if resp != nil && resp.IsError() { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - if err == nil { - t.Fatalf("expected an error") - } - - plaintextB64 := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox" - - // Perform encryption, signing or hmac-ing based on the set 'feature' - var encryptReq, signReq, hmacReq *logical.Request - var ciphertext, signature, hmac string - switch feature { - case "encrypt-decrypt": - encryptReq = &logical.Request{ - Path: "encrypt/test", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "plaintext": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), encryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - ciphertext = resp.Data["ciphertext"].(string) - - case "sign-verify": - signReq = &logical.Request{ - Path: "sign/test", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "input": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), signReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - signature = resp.Data["signature"].(string) - - case "hmac-verify": - hmacReq = &logical.Request{ - Path: "hmac/test", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "input": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), hmacReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - hmac = resp.Data["hmac"].(string) - } - - // Delete the key - keyReq.Operation = logical.DeleteOperation - resp, err = b.HandleRequest(context.Background(), keyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Restore the key from the backup - resp, err = b.HandleRequest(context.Background(), restoreReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // validationFunc verifies the ciphertext, signature or hmac based on the - // set 'feature' - validationFunc := func(keyName string) { - var decryptReq *logical.Request - var verifyReq *logical.Request - switch feature { - case "encrypt-decrypt": - decryptReq = &logical.Request{ - Path: "decrypt/" + keyName, - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "ciphertext": ciphertext, - }, - } - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - if resp.Data["plaintext"].(string) != plaintextB64 { - t.Fatalf("bad: plaintext; expected: %q, actual: %q", plaintextB64, resp.Data["plaintext"].(string)) - } - case "sign-verify": - verifyReq = &logical.Request{ - Path: "verify/" + keyName, - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "signature": signature, - "input": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), verifyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - if resp.Data["valid"].(bool) != true { - t.Fatalf("bad: signature verification failed for key type %q", keyType) - } - - case "hmac-verify": - verifyReq = &logical.Request{ - Path: "verify/" + keyName, - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "hmac": hmac, - "input": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), verifyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - if resp.Data["valid"].(bool) != true { - t.Fatalf("bad: HMAC verification failed for key type %q", keyType) - } - } - } - - // Ensure that the restored key is functional - validationFunc("test") - - // Delete the key again - resp, err = b.HandleRequest(context.Background(), keyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Restore the key under a different name - restoreReq.Path = "restore/test1" - resp, err = b.HandleRequest(context.Background(), restoreReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Ensure that the restored key is functional - validationFunc("test1") -} diff --git a/builtin/logical/transit/path_byok_test.go b/builtin/logical/transit/path_byok_test.go deleted file mode 100644 index 44dbafa18..000000000 --- a/builtin/logical/transit/path_byok_test.go +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTransit_BYOKExportImport(t *testing.T) { - // Test encryption/decryption after a restore for supported keys - testBYOKExportImport(t, "aes128-gcm96", "encrypt-decrypt") - testBYOKExportImport(t, "aes256-gcm96", "encrypt-decrypt") - testBYOKExportImport(t, "chacha20-poly1305", "encrypt-decrypt") - testBYOKExportImport(t, "rsa-2048", "encrypt-decrypt") - testBYOKExportImport(t, "rsa-3072", "encrypt-decrypt") - testBYOKExportImport(t, "rsa-4096", "encrypt-decrypt") - - // Test signing/verification after a restore for supported keys - testBYOKExportImport(t, "ecdsa-p256", "sign-verify") - testBYOKExportImport(t, "ecdsa-p384", "sign-verify") - testBYOKExportImport(t, "ecdsa-p521", "sign-verify") - testBYOKExportImport(t, "ed25519", "sign-verify") - testBYOKExportImport(t, "rsa-2048", "sign-verify") - testBYOKExportImport(t, "rsa-3072", "sign-verify") - testBYOKExportImport(t, "rsa-4096", "sign-verify") - - // Test HMAC sign/verify after a restore for supported keys. - testBYOKExportImport(t, "hmac", "hmac-verify") -} - -func testBYOKExportImport(t *testing.T, keyType, feature string) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - // Create a key - keyReq := &logical.Request{ - Path: "keys/test-source", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "type": keyType, - "exportable": true, - }, - } - if keyType == "hmac" { - keyReq.Data["key_size"] = 32 - } - resp, err = b.HandleRequest(context.Background(), keyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Read the wrapping key. - wrapKeyReq := &logical.Request{ - Path: "wrapping_key", - Operation: logical.ReadOperation, - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), wrapKeyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Import the wrapping key. - wrapKeyImportReq := &logical.Request{ - Path: "keys/wrapper/import", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "public_key": resp.Data["public_key"], - "type": "rsa-4096", - }, - } - resp, err = b.HandleRequest(context.Background(), wrapKeyImportReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Export the key - backupReq := &logical.Request{ - Path: "byok-export/wrapper/test-source", - Operation: logical.ReadOperation, - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), backupReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - keys := resp.Data["keys"].(map[string]string) - - // Import the key to a new name. - restoreReq := &logical.Request{ - Path: "keys/test/import", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "ciphertext": keys["1"], - "type": keyType, - }, - } - resp, err = b.HandleRequest(context.Background(), restoreReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - plaintextB64 := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox" - // Perform encryption, signing or hmac-ing based on the set 'feature' - var encryptReq, signReq, hmacReq *logical.Request - var ciphertext, signature, hmac string - switch feature { - case "encrypt-decrypt": - encryptReq = &logical.Request{ - Path: "encrypt/test-source", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "plaintext": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), encryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - ciphertext = resp.Data["ciphertext"].(string) - - case "sign-verify": - signReq = &logical.Request{ - Path: "sign/test-source", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "input": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), signReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - signature = resp.Data["signature"].(string) - - case "hmac-verify": - hmacReq = &logical.Request{ - Path: "hmac/test-source", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "input": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), hmacReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - hmac = resp.Data["hmac"].(string) - } - - // validationFunc verifies the ciphertext, signature or hmac based on the - // set 'feature' - validationFunc := func(keyName string) { - var decryptReq *logical.Request - var verifyReq *logical.Request - switch feature { - case "encrypt-decrypt": - decryptReq = &logical.Request{ - Path: "decrypt/" + keyName, - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "ciphertext": ciphertext, - }, - } - resp, err = b.HandleRequest(context.Background(), decryptReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - if resp.Data["plaintext"].(string) != plaintextB64 { - t.Fatalf("bad: plaintext; expected: %q, actual: %q", plaintextB64, resp.Data["plaintext"].(string)) - } - case "sign-verify": - verifyReq = &logical.Request{ - Path: "verify/" + keyName, - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "signature": signature, - "input": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), verifyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - if resp.Data["valid"].(bool) != true { - t.Fatalf("bad: signature verification failed for key type %q", keyType) - } - - case "hmac-verify": - verifyReq = &logical.Request{ - Path: "verify/" + keyName, - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "hmac": hmac, - "input": plaintextB64, - }, - } - resp, err = b.HandleRequest(context.Background(), verifyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - if resp.Data["valid"].(bool) != true { - t.Fatalf("bad: HMAC verification failed for key type %q", keyType) - } - } - } - - // Ensure that the restored key is functional - validationFunc("test") - - // Ensure the original key is functional - validationFunc("test-source") -} diff --git a/builtin/logical/transit/path_cache_config_test.go b/builtin/logical/transit/path_cache_config_test.go deleted file mode 100644 index 0141f6a32..000000000 --- a/builtin/logical/transit/path_cache_config_test.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -const ( - targetCacheSize = 12345 - smallCacheSize = 3 -) - -func TestTransit_CacheConfig(t *testing.T) { - b1, storage := createBackendWithSysView(t) - - doReq := func(b *backend, req *logical.Request) *logical.Response { - resp, err := b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("got err:\n%#v\nreq:\n%#v\n", err, *req) - } - return resp - } - - doErrReq := func(b *backend, req *logical.Request) { - resp, err := b.HandleRequest(context.Background(), req) - if err == nil { - if resp == nil || !resp.IsError() { - t.Fatalf("expected error; req:\n%#v\n", *req) - } - } - } - - validateResponse := func(resp *logical.Response, expectedCacheSize int, expectedWarning bool) { - actualCacheSize, ok := resp.Data["size"].(int) - if !ok { - t.Fatalf("No size returned") - } - if expectedCacheSize != actualCacheSize { - t.Fatalf("testAccReadCacheConfig expected: %d got: %d", expectedCacheSize, actualCacheSize) - } - // check for the presence/absence of warnings - warnings are expected if a cache size has been - // configured but not yet applied by reloading the plugin - warningCheckPass := expectedWarning == (len(resp.Warnings) > 0) - if !warningCheckPass { - t.Fatalf( - "testAccSteporeadCacheConfig warnings error.\n"+ - "expect warnings: %t but number of warnings was: %d", - expectedWarning, len(resp.Warnings), - ) - } - } - - writeReq := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "cache-config", - Data: map[string]interface{}{ - "size": targetCacheSize, - }, - } - - writeSmallCacheSizeReq := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "cache-config", - Data: map[string]interface{}{ - "size": smallCacheSize, - }, - } - - readReq := &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "cache-config", - } - - polReq := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/aes256", - Data: map[string]interface{}{ - "derived": true, - }, - } - - // test steps - // b1 should spin up with an unlimited cache - validateResponse(doReq(b1, readReq), 0, false) - - // Change cache size to targetCacheSize 12345 and validate that cache size is updated - doReq(b1, writeReq) - validateResponse(doReq(b1, readReq), targetCacheSize, false) - b1.invalidate(context.Background(), "cache-config/") - - // Change the cache size to 1000 to mock the scenario where - // current cache size and stored cache size are different and - // a cache update is needed - b1.lm.InitCache(1000) - - // Write a new policy which in its code path detects that cache size has changed - // and refreshes the cache to 12345 - doReq(b1, polReq) - - // Validate that cache size is updated to 12345 - validateResponse(doReq(b1, readReq), targetCacheSize, false) - - // b2 should spin up with a configured cache - b2 := createBackendWithSysViewWithStorage(t, storage) - validateResponse(doReq(b2, readReq), targetCacheSize, false) - - // b3 enables transit without a cache, trying to read it should error - b3 := createBackendWithForceNoCacheWithSysViewWithStorage(t, storage) - doErrReq(b3, readReq) - - // b4 should spin up with a size less than minimum cache size (10) - b4, storage := createBackendWithSysView(t) - doErrReq(b4, writeSmallCacheSizeReq) -} diff --git a/builtin/logical/transit/path_config_keys_test.go b/builtin/logical/transit/path_config_keys_test.go deleted file mode 100644 index d5aa12b9c..000000000 --- a/builtin/logical/transit/path_config_keys_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTransit_ConfigKeys(t *testing.T) { - b, s := createBackendWithSysView(t) - - doReq := func(req *logical.Request) *logical.Response { - resp, err := b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("got err:\n%#v\nreq:\n%#v\n", err, *req) - } - return resp - } - doErrReq := func(req *logical.Request) { - resp, err := b.HandleRequest(context.Background(), req) - if err == nil { - if resp == nil || !resp.IsError() { - t.Fatalf("expected error; req:\n%#v\n", *req) - } - } - } - - // First read the global config - req := &logical.Request{ - Storage: s, - Operation: logical.ReadOperation, - Path: "config/keys", - } - resp := doReq(req) - if resp.Data["disable_upsert"].(bool) != false { - t.Fatalf("expected disable_upsert to be false; got: %v", resp) - } - - // Ensure we can upsert. - req.Operation = logical.CreateOperation - req.Path = "encrypt/upsert-1" - req.Data = map[string]interface{}{ - "plaintext": "aGVsbG8K", - } - doReq(req) - - // Disable upserting. - req.Operation = logical.UpdateOperation - req.Path = "config/keys" - req.Data = map[string]interface{}{ - "disable_upsert": true, - } - doReq(req) - - // Attempt upserting again, it should fail. - req.Operation = logical.CreateOperation - req.Path = "encrypt/upsert-2" - req.Data = map[string]interface{}{ - "plaintext": "aGVsbG8K", - } - doErrReq(req) - - // Redoing this with the first key should succeed. - req.Path = "encrypt/upsert-1" - doReq(req) -} diff --git a/builtin/logical/transit/path_decrypt_bench_test.go b/builtin/logical/transit/path_decrypt_bench_test.go deleted file mode 100644 index d0816fdb6..000000000 --- a/builtin/logical/transit/path_decrypt_bench_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func BenchmarkTransit_BatchDecryption1(b *testing.B) { - BTransit_BatchDecryption(b, 1) -} - -func BenchmarkTransit_BatchDecryption10(b *testing.B) { - BTransit_BatchDecryption(b, 10) -} - -func BenchmarkTransit_BatchDecryption50(b *testing.B) { - BTransit_BatchDecryption(b, 50) -} - -func BenchmarkTransit_BatchDecryption100(b *testing.B) { - BTransit_BatchDecryption(b, 100) -} - -func BenchmarkTransit_BatchDecryption1000(b *testing.B) { - BTransit_BatchDecryption(b, 1_000) -} - -func BenchmarkTransit_BatchDecryption10000(b *testing.B) { - BTransit_BatchDecryption(b, 10_000) -} - -func BTransit_BatchDecryption(b *testing.B, bsize int) { - b.StopTimer() - - var resp *logical.Response - var err error - - backend, s := createBackendWithStorage(b) - - batchEncryptionInput := make([]interface{}, 0, bsize) - for i := 0; i < bsize; i++ { - batchEncryptionInput = append( - batchEncryptionInput, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - ) - } - - batchEncryptionData := map[string]interface{}{ - "batch_input": batchEncryptionInput, - } - - batchEncryptionReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchEncryptionData, - } - resp, err = backend.HandleRequest(context.Background(), batchEncryptionReq) - if err != nil || (resp != nil && resp.IsError()) { - b.Fatalf("err:%v resp:%#v", err, resp) - } - - batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) - batchDecryptionInput := make([]interface{}, len(batchResponseItems)) - for i, item := range batchResponseItems { - batchDecryptionInput[i] = map[string]interface{}{"ciphertext": item.Ciphertext} - } - batchDecryptionData := map[string]interface{}{ - "batch_input": batchDecryptionInput, - } - - batchDecryptionReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/upserted_key", - Storage: s, - Data: batchDecryptionData, - } - - b.StartTimer() - for i := 0; i < b.N; i++ { - resp, err = backend.HandleRequest(context.Background(), batchDecryptionReq) - if err != nil || (resp != nil && resp.IsError()) { - b.Fatalf("err:%v resp:%#v", err, resp) - } - } -} diff --git a/builtin/logical/transit/path_decrypt_test.go b/builtin/logical/transit/path_decrypt_test.go deleted file mode 100644 index a61d85ddc..000000000 --- a/builtin/logical/transit/path_decrypt_test.go +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "encoding/json" - "net/http" - "reflect" - "testing" - - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/mapstructure" -) - -func TestTransit_BatchDecryption(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - batchEncryptionInput := []interface{}{ - map[string]interface{}{"plaintext": "", "reference": "foo"}, // empty string - map[string]interface{}{"plaintext": "Cg==", "reference": "bar"}, // newline - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "baz"}, - } - batchEncryptionData := map[string]interface{}{ - "batch_input": batchEncryptionInput, - } - - batchEncryptionReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchEncryptionData, - } - resp, err = b.HandleRequest(context.Background(), batchEncryptionReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) - batchDecryptionInput := make([]interface{}, len(batchResponseItems)) - for i, item := range batchResponseItems { - batchDecryptionInput[i] = map[string]interface{}{"ciphertext": item.Ciphertext, "reference": item.Reference} - } - batchDecryptionData := map[string]interface{}{ - "batch_input": batchDecryptionInput, - } - - batchDecryptionReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/upserted_key", - Storage: s, - Data: batchDecryptionData, - } - resp, err = b.HandleRequest(context.Background(), batchDecryptionReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchDecryptionResponseItems := resp.Data["batch_results"].([]DecryptBatchResponseItem) - // This seems fragile - expectedResult := "[{\"plaintext\":\"\",\"reference\":\"foo\"},{\"plaintext\":\"Cg==\",\"reference\":\"bar\"},{\"plaintext\":\"dGhlIHF1aWNrIGJyb3duIGZveA==\",\"reference\":\"baz\"}]" - - jsonResponse, err := json.Marshal(batchDecryptionResponseItems) - if err != nil || err == nil && string(jsonResponse) != expectedResult { - t.Fatalf("bad: expected json response [%s]", jsonResponse) - } -} - -func TestTransit_BatchDecryption_DerivedKey(t *testing.T) { - var req *logical.Request - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - // Create a derived key. - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/existing_key", - Storage: s, - Data: map[string]interface{}{ - "derived": true, - }, - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Encrypt some values for use in test cases. - plaintextItems := []struct { - plaintext, context string - }{ - {plaintext: "dGhlIHF1aWNrIGJyb3duIGZveA==", context: "dGVzdGNvbnRleHQ="}, - {plaintext: "anVtcGVkIG92ZXIgdGhlIGxhenkgZG9n", context: "dGVzdGNvbnRleHQy"}, - } - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "encrypt/existing_key", - Storage: s, - Data: map[string]interface{}{ - "batch_input": []interface{}{ - map[string]interface{}{"plaintext": plaintextItems[0].plaintext, "context": plaintextItems[0].context}, - map[string]interface{}{"plaintext": plaintextItems[1].plaintext, "context": plaintextItems[1].context}, - }, - }, - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - encryptedItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) - - tests := []struct { - name string - in []interface{} - want []DecryptBatchResponseItem - shouldErr bool - wantHTTPStatus int - params map[string]interface{} - }{ - { - name: "nil-input", - in: nil, - shouldErr: true, - }, - { - name: "empty-input", - in: []interface{}{}, - shouldErr: true, - }, - { - name: "single-item-success", - in: []interface{}{ - map[string]interface{}{"ciphertext": encryptedItems[0].Ciphertext, "context": plaintextItems[0].context}, - }, - want: []DecryptBatchResponseItem{ - {Plaintext: plaintextItems[0].plaintext}, - }, - }, - { - name: "single-item-invalid-ciphertext", - in: []interface{}{ - map[string]interface{}{"ciphertext": "xxx", "context": plaintextItems[0].context}, - }, - want: []DecryptBatchResponseItem{ - {Error: "invalid ciphertext: no prefix"}, - }, - wantHTTPStatus: http.StatusBadRequest, - }, - { - name: "single-item-wrong-context", - in: []interface{}{ - map[string]interface{}{"ciphertext": encryptedItems[0].Ciphertext, "context": plaintextItems[1].context}, - }, - want: []DecryptBatchResponseItem{ - {Error: "cipher: message authentication failed"}, - }, - wantHTTPStatus: http.StatusBadRequest, - }, - { - name: "batch-full-success", - in: []interface{}{ - map[string]interface{}{"ciphertext": encryptedItems[0].Ciphertext, "context": plaintextItems[0].context}, - map[string]interface{}{"ciphertext": encryptedItems[1].Ciphertext, "context": plaintextItems[1].context}, - }, - want: []DecryptBatchResponseItem{ - {Plaintext: plaintextItems[0].plaintext}, - {Plaintext: plaintextItems[1].plaintext}, - }, - }, - { - name: "batch-partial-success", - in: []interface{}{ - map[string]interface{}{"ciphertext": encryptedItems[0].Ciphertext, "context": plaintextItems[1].context}, - map[string]interface{}{"ciphertext": encryptedItems[1].Ciphertext, "context": plaintextItems[1].context}, - }, - want: []DecryptBatchResponseItem{ - {Error: "cipher: message authentication failed"}, - {Plaintext: plaintextItems[1].plaintext}, - }, - wantHTTPStatus: http.StatusBadRequest, - }, - { - name: "batch-partial-success-overridden-response", - in: []interface{}{ - map[string]interface{}{"ciphertext": encryptedItems[0].Ciphertext, "context": plaintextItems[1].context}, - map[string]interface{}{"ciphertext": encryptedItems[1].Ciphertext, "context": plaintextItems[1].context}, - }, - want: []DecryptBatchResponseItem{ - {Error: "cipher: message authentication failed"}, - {Plaintext: plaintextItems[1].plaintext}, - }, - params: map[string]interface{}{"partial_failure_response_code": http.StatusAccepted}, - wantHTTPStatus: http.StatusAccepted, - }, - { - name: "batch-full-failure", - in: []interface{}{ - map[string]interface{}{"ciphertext": encryptedItems[0].Ciphertext, "context": plaintextItems[1].context}, - map[string]interface{}{"ciphertext": encryptedItems[1].Ciphertext, "context": plaintextItems[0].context}, - }, - want: []DecryptBatchResponseItem{ - {Error: "cipher: message authentication failed"}, - {Error: "cipher: message authentication failed"}, - }, - wantHTTPStatus: http.StatusBadRequest, - }, - { - name: "batch-full-failure-overridden-response", - in: []interface{}{ - map[string]interface{}{"ciphertext": encryptedItems[0].Ciphertext, "context": plaintextItems[1].context}, - map[string]interface{}{"ciphertext": encryptedItems[1].Ciphertext, "context": plaintextItems[0].context}, - }, - want: []DecryptBatchResponseItem{ - {Error: "cipher: message authentication failed"}, - {Error: "cipher: message authentication failed"}, - }, - params: map[string]interface{}{"partial_failure_response_code": http.StatusAccepted}, - // Full failure, shouldn't affect status code - wantHTTPStatus: http.StatusBadRequest, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/existing_key", - Storage: s, - Data: map[string]interface{}{ - "batch_input": tt.in, - }, - } - for k, v := range tt.params { - req.Data[k] = v - } - resp, err = b.HandleRequest(context.Background(), req) - - didErr := err != nil || (resp != nil && resp.IsError()) - if didErr { - if !tt.shouldErr { - t.Fatalf("unexpected error err:%v, resp:%#v", err, resp) - } - } else { - if tt.shouldErr { - t.Fatal("expected error, but none occurred") - } - - if rawRespBody, ok := resp.Data[logical.HTTPRawBody]; ok { - httpResp := &logical.HTTPResponse{} - err = jsonutil.DecodeJSON([]byte(rawRespBody.(string)), httpResp) - if err != nil { - t.Fatalf("failed to unmarshal nested response: err:%v, resp:%#v", err, resp) - } - - if respStatus, ok := resp.Data[logical.HTTPStatusCode]; !ok || respStatus != tt.wantHTTPStatus { - t.Fatalf("HTTP response status code mismatch, want:%d, got:%d", tt.wantHTTPStatus, respStatus) - } - - resp = logical.HTTPResponseToLogicalResponse(httpResp) - } - - var respItems []DecryptBatchResponseItem - err = mapstructure.Decode(resp.Data["batch_results"], &respItems) - if err != nil { - t.Fatalf("problem decoding response items: err:%v, resp:%#v", err, resp) - } - if !reflect.DeepEqual(tt.want, respItems) { - t.Fatalf("response items mismatch, want:%#v, got:%#v", tt.want, respItems) - } - } - }) - } -} diff --git a/builtin/logical/transit/path_encrypt_bench_test.go b/builtin/logical/transit/path_encrypt_bench_test.go deleted file mode 100644 index a57c90fa7..000000000 --- a/builtin/logical/transit/path_encrypt_bench_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func BenchmarkTransit_BatchEncryption1(b *testing.B) { - BTransit_BatchEncryption(b, 1) -} - -func BenchmarkTransit_BatchEncryption10(b *testing.B) { - BTransit_BatchEncryption(b, 10) -} - -func BenchmarkTransit_BatchEncryption50(b *testing.B) { - BTransit_BatchEncryption(b, 50) -} - -func BenchmarkTransit_BatchEncryption100(b *testing.B) { - BTransit_BatchEncryption(b, 100) -} - -func BenchmarkTransit_BatchEncryption1000(b *testing.B) { - BTransit_BatchEncryption(b, 1_000) -} - -func BenchmarkTransit_BatchEncryption10000(b *testing.B) { - BTransit_BatchEncryption(b, 10_000) -} - -func BTransit_BatchEncryption(b *testing.B, bsize int) { - b.StopTimer() - - var resp *logical.Response - var err error - - backend, s := createBackendWithStorage(b) - - batchEncryptionInput := make([]interface{}, 0, bsize) - for i := 0; i < bsize; i++ { - batchEncryptionInput = append( - batchEncryptionInput, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - ) - } - - batchEncryptionData := map[string]interface{}{ - "batch_input": batchEncryptionInput, - } - - batchEncryptionReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchEncryptionData, - } - - b.StartTimer() - for i := 0; i < b.N; i++ { - resp, err = backend.HandleRequest(context.Background(), batchEncryptionReq) - if err != nil || (resp != nil && resp.IsError()) { - b.Fatalf("err:%v resp:%#v", err, resp) - } - } -} diff --git a/builtin/logical/transit/path_encrypt_test.go b/builtin/logical/transit/path_encrypt_test.go deleted file mode 100644 index 4f5088e8e..000000000 --- a/builtin/logical/transit/path_encrypt_test.go +++ /dev/null @@ -1,1068 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "reflect" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/helper/keysutil" - - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/mapstructure" -) - -func TestTransit_MissingPlaintext(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - // Create the policy - policyReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/existing_key", - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - encReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "encrypt/existing_key", - Storage: s, - Data: map[string]interface{}{}, - } - resp, err = b.HandleRequest(context.Background(), encReq) - if resp == nil || !resp.IsError() { - t.Fatalf("expected error due to missing plaintext in request, err:%v resp:%#v", err, resp) - } -} - -func TestTransit_MissingPlaintextInBatchInput(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - // Create the policy - policyReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/existing_key", - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchInput := []interface{}{ - map[string]interface{}{}, // Note that there is no map entry for plaintext - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err == nil { - t.Fatalf("expected error due to missing plaintext in request, err:%v resp:%#v", err, resp) - } -} - -// Case1: Ensure that batch encryption did not affect the normal flow of -// encrypting the plaintext with a pre-existing key. -func TestTransit_BatchEncryptionCase1(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - // Create the policy - policyReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/existing_key", - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox" - - encData := map[string]interface{}{ - "plaintext": plaintext, - } - - encReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "encrypt/existing_key", - Storage: s, - Data: encData, - } - resp, err = b.HandleRequest(context.Background(), encReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - keyVersion := resp.Data["key_version"].(int) - if keyVersion != 1 { - t.Fatalf("unexpected key version; got: %d, expected: %d", keyVersion, 1) - } - - ciphertext := resp.Data["ciphertext"] - - decData := map[string]interface{}{ - "ciphertext": ciphertext, - } - decReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/existing_key", - Storage: s, - Data: decData, - } - resp, err = b.HandleRequest(context.Background(), decReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["plaintext"] != plaintext { - t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"]) - } -} - -// Case2: Ensure that batch encryption did not affect the normal flow of -// encrypting the plaintext with the key upserted. -func TestTransit_BatchEncryptionCase2(t *testing.T) { - var resp *logical.Response - var err error - b, s := createBackendWithStorage(t) - - // Upsert the key and encrypt the data - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" - - encData := map[string]interface{}{ - "plaintext": plaintext, - } - - encReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: encData, - } - resp, err = b.HandleRequest(context.Background(), encReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - keyVersion := resp.Data["key_version"].(int) - if keyVersion != 1 { - t.Fatalf("unexpected key version; got: %d, expected: %d", keyVersion, 1) - } - - ciphertext := resp.Data["ciphertext"] - decData := map[string]interface{}{ - "ciphertext": ciphertext, - } - - policyReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "keys/upserted_key", - Storage: s, - } - - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - decReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/upserted_key", - Storage: s, - Data: decData, - } - resp, err = b.HandleRequest(context.Background(), decReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["plaintext"] != plaintext { - t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"]) - } -} - -// Case3: If batch encryption input is not base64 encoded, it should fail. -func TestTransit_BatchEncryptionCase3(t *testing.T) { - var err error - - b, s := createBackendWithStorage(t) - - batchInput := `[{"plaintext":"dGhlIHF1aWNrIGJyb3duIGZveA=="}]` - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchData, - } - _, err = b.HandleRequest(context.Background(), batchReq) - if err == nil { - t.Fatal("expected an error") - } -} - -// Case4: Test batch encryption with an existing key (and test references) -func TestTransit_BatchEncryptionCase4(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - policyReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/existing_key", - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchInput := []interface{}{ - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "b"}, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "a"}, - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - batchReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "encrypt/existing_key", - Storage: s, - Data: batchData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) - - decReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/existing_key", - Storage: s, - } - - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" - - for i, item := range batchResponseItems { - if item.KeyVersion != 1 { - t.Fatalf("unexpected key version; got: %d, expected: %d", item.KeyVersion, 1) - } - - decReq.Data = map[string]interface{}{ - "ciphertext": item.Ciphertext, - } - resp, err = b.HandleRequest(context.Background(), decReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["plaintext"] != plaintext { - t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"]) - } - inputItem := batchInput[i].(map[string]interface{}) - if item.Reference != inputItem["reference"] { - t.Fatalf("reference mismatch. Expected %s, Actual: %s", inputItem["reference"], item.Reference) - } - } -} - -// Case5: Test batch encryption with an existing derived key -func TestTransit_BatchEncryptionCase5(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - policyData := map[string]interface{}{ - "derived": true, - } - - policyReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/existing_key", - Storage: s, - Data: policyData, - } - - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchInput := []interface{}{ - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="}, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="}, - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - - batchReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "encrypt/existing_key", - Storage: s, - Data: batchData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) - - decReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/existing_key", - Storage: s, - } - - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" - - for _, item := range batchResponseItems { - if item.KeyVersion != 1 { - t.Fatalf("unexpected key version; got: %d, expected: %d", item.KeyVersion, 1) - } - - decReq.Data = map[string]interface{}{ - "ciphertext": item.Ciphertext, - "context": "dmlzaGFsCg==", - } - resp, err = b.HandleRequest(context.Background(), decReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["plaintext"] != plaintext { - t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"]) - } - } -} - -// Case6: Test batch encryption with an upserted non-derived key -func TestTransit_BatchEncryptionCase6(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - batchInput := []interface{}{ - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) - - decReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/upserted_key", - Storage: s, - } - - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" - - for _, responseItem := range batchResponseItems { - var item EncryptBatchResponseItem - if err := mapstructure.Decode(responseItem, &item); err != nil { - t.Fatal(err) - } - - if item.KeyVersion != 1 { - t.Fatalf("unexpected key version; got: %d, expected: %d", item.KeyVersion, 1) - } - - decReq.Data = map[string]interface{}{ - "ciphertext": item.Ciphertext, - } - resp, err = b.HandleRequest(context.Background(), decReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["plaintext"] != plaintext { - t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"]) - } - } -} - -// Case7: Test batch encryption with an upserted derived key -func TestTransit_BatchEncryptionCase7(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - batchInput := []interface{}{ - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="}, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="}, - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) - - decReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/upserted_key", - Storage: s, - } - - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" - - for _, item := range batchResponseItems { - if item.KeyVersion != 1 { - t.Fatalf("unexpected key version; got: %d, expected: %d", item.KeyVersion, 1) - } - - decReq.Data = map[string]interface{}{ - "ciphertext": item.Ciphertext, - "context": "dmlzaGFsCg==", - } - resp, err = b.HandleRequest(context.Background(), decReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["plaintext"] != plaintext { - t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"]) - } - } -} - -// Case8: If plaintext is not base64 encoded, encryption should fail -func TestTransit_BatchEncryptionCase8(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - // Create the policy - policyReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/existing_key", - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchInput := []interface{}{ - map[string]interface{}{"plaintext": "simple_plaintext"}, - } - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - batchReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "encrypt/existing_key", - Storage: s, - Data: batchData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - plaintext := "simple plaintext" - - encData := map[string]interface{}{ - "plaintext": plaintext, - } - - encReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "encrypt/existing_key", - Storage: s, - Data: encData, - } - resp, err = b.HandleRequest(context.Background(), encReq) - if err == nil { - t.Fatal("expected an error") - } -} - -// Case9: If both plaintext and batch inputs are supplied, plaintext should be -// ignored. -func TestTransit_BatchEncryptionCase9(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - batchInput := []interface{}{ - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - } - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" - batchData := map[string]interface{}{ - "batch_input": batchInput, - "plaintext": plaintext, - } - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - _, ok := resp.Data["ciphertext"] - if ok { - t.Fatal("ciphertext field should not be set") - } -} - -// Case10: Inconsistent presence of 'context' in batch input should be caught -func TestTransit_BatchEncryptionCase10(t *testing.T) { - var err error - - b, s := createBackendWithStorage(t) - - batchInput := []interface{}{ - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="}, - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchData, - } - _, err = b.HandleRequest(context.Background(), batchReq) - if err == nil { - t.Fatalf("expected an error") - } -} - -// Case11: Incorrect inputs for context and nonce should not fail the operation -func TestTransit_BatchEncryptionCase11(t *testing.T) { - var err error - - b, s := createBackendWithStorage(t) - - batchInput := []interface{}{ - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="}, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "not-encoded"}, - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchData, - } - _, err = b.HandleRequest(context.Background(), batchReq) - if err != nil { - t.Fatal(err) - } -} - -// Case12: Invalid batch input -func TestTransit_BatchEncryptionCase12(t *testing.T) { - var err error - b, s := createBackendWithStorage(t) - - batchInput := []interface{}{ - map[string]interface{}{}, - "unexpected_interface", - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchData, - } - _, err = b.HandleRequest(context.Background(), batchReq) - if err == nil { - t.Fatalf("expected an error") - } -} - -// Case13: Incorrect input for nonce when we aren't in convergent encryption should fail the operation -func TestTransit_EncryptionCase13(t *testing.T) { - var err error - - b, s := createBackendWithStorage(t) - - // Non-batch first - data := map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "nonce": "R80hr9eNUIuFV52e"} - req := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/my-key", - Storage: s, - Data: data, - } - resp, err := b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("expected invalid request") - } - - batchInput := []interface{}{ - map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "nonce": "R80hr9eNUIuFV52e"}, - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/my-key", - Storage: s, - Data: batchData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err != nil { - t.Fatal(err) - } - - if v, ok := resp.Data["http_status_code"]; !ok || v.(int) != http.StatusBadRequest { - t.Fatal("expected request error") - } -} - -// Case14: Incorrect input for nonce when we are in convergent version 3 should fail -func TestTransit_EncryptionCase14(t *testing.T) { - var err error - - b, s := createBackendWithStorage(t) - - cReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/my-key", - Storage: s, - Data: map[string]interface{}{ - "convergent_encryption": "true", - "derived": "true", - }, - } - resp, err := b.HandleRequest(context.Background(), cReq) - if err != nil { - t.Fatal(err) - } - - // Non-batch first - data := map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "context": "SGVsbG8sIFdvcmxkCg==", "nonce": "R80hr9eNUIuFV52e"} - req := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/my-key", - Storage: s, - Data: data, - } - - resp, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("expected invalid request") - } - - batchInput := []interface{}{ - data, - } - - batchData := map[string]interface{}{ - "batch_input": batchInput, - } - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/my-key", - Storage: s, - Data: batchData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err != nil { - t.Fatal(err) - } - - if v, ok := resp.Data["http_status_code"]; !ok || v.(int) != http.StatusBadRequest { - t.Fatal("expected request error") - } -} - -// Test that the fast path function decodeBatchRequestItems behave like mapstructure.Decode() to decode []BatchRequestItem. -func TestTransit_decodeBatchRequestItems(t *testing.T) { - tests := []struct { - name string - src interface{} - requirePlaintext bool - requireCiphertext bool - dest []BatchRequestItem - wantErrContains string - }{ - // basic edge cases of nil values - {name: "nil-nil", src: nil, dest: nil}, - {name: "nil-empty", src: nil, dest: []BatchRequestItem{}}, - {name: "empty-nil", src: []interface{}{}, dest: nil}, - { - name: "src-nil", - src: []interface{}{map[string]interface{}{}}, - dest: nil, - }, - // empty src & dest - { - name: "src-dest", - src: []interface{}{map[string]interface{}{}}, - dest: []BatchRequestItem{}, - }, - // empty src but with already populated dest, mapstructure discard pre-populated data. - { - name: "src-dest_pre_filled", - src: []interface{}{map[string]interface{}{}}, - dest: []BatchRequestItem{{}}, - }, - // two test per properties to test valid and invalid input - { - name: "src_plaintext-dest", - src: []interface{}{map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}}, - dest: []BatchRequestItem{}, - }, - { - name: "src_plaintext_invalid-dest", - src: []interface{}{map[string]interface{}{"plaintext": 666}}, - dest: []BatchRequestItem{}, - wantErrContains: "expected type 'string', got unconvertible type 'int'", - }, - { - name: "src_ciphertext-dest", - src: []interface{}{map[string]interface{}{"ciphertext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}}, - dest: []BatchRequestItem{}, - }, - { - name: "src_ciphertext_invalid-dest", - src: []interface{}{map[string]interface{}{"ciphertext": 666}}, - dest: []BatchRequestItem{}, - wantErrContains: "expected type 'string', got unconvertible type 'int'", - }, - { - name: "src_key_version-dest", - src: []interface{}{map[string]interface{}{"key_version": 1}}, - dest: []BatchRequestItem{}, - }, - { - name: "src_key_version_invalid-dest", - src: []interface{}{map[string]interface{}{"key_version": "666"}}, - dest: []BatchRequestItem{}, - wantErrContains: "expected type 'int', got unconvertible type 'string'", - }, - { - name: "src_key_version_invalid-number-dest", - src: []interface{}{map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "key_version": json.Number("1.1")}}, - dest: []BatchRequestItem{}, - wantErrContains: "error decoding json.Number into [0].key_version", - }, - { - name: "src_nonce-dest", - src: []interface{}{map[string]interface{}{"nonce": "dGVzdGNvbnRleHQ="}}, - dest: []BatchRequestItem{}, - }, - { - name: "src_nonce_invalid-dest", - src: []interface{}{map[string]interface{}{"nonce": 666}}, - dest: []BatchRequestItem{}, - wantErrContains: "expected type 'string', got unconvertible type 'int'", - }, - { - name: "src_context-dest", - src: []interface{}{map[string]interface{}{"context": "dGVzdGNvbnRleHQ="}}, - dest: []BatchRequestItem{}, - }, - { - name: "src_context_invalid-dest", - src: []interface{}{map[string]interface{}{"context": 666}}, - dest: []BatchRequestItem{}, - wantErrContains: "expected type 'string', got unconvertible type 'int'", - }, - { - name: "src_multi_order-dest", - src: []interface{}{ - map[string]interface{}{"context": "1"}, - map[string]interface{}{"context": "2"}, - map[string]interface{}{"context": "3"}, - }, - dest: []BatchRequestItem{}, - }, - { - name: "src_multi_with_invalid-dest", - src: []interface{}{ - map[string]interface{}{"context": "1"}, - map[string]interface{}{"context": "2", "key_version": "666"}, - map[string]interface{}{"context": "3"}, - }, - dest: []BatchRequestItem{}, - wantErrContains: "expected type 'int', got unconvertible type 'string'", - }, - { - name: "src_multi_with_multi_invalid-dest", - src: []interface{}{ - map[string]interface{}{"context": "1"}, - map[string]interface{}{"context": "2", "key_version": "666"}, - map[string]interface{}{"context": "3", "key_version": "1337"}, - }, - dest: []BatchRequestItem{}, - wantErrContains: "expected type 'int', got unconvertible type 'string'", - }, - { - name: "src_plaintext-nil-nonce", - src: []interface{}{map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "nonce": "null"}}, - dest: []BatchRequestItem{}, - }, - // required fields - { - name: "required_plaintext_present", - src: []interface{}{map[string]interface{}{"plaintext": ""}}, - requirePlaintext: true, - dest: []BatchRequestItem{}, - }, - { - name: "required_plaintext_missing", - src: []interface{}{map[string]interface{}{}}, - requirePlaintext: true, - dest: []BatchRequestItem{}, - wantErrContains: "missing plaintext", - }, - { - name: "required_ciphertext_present", - src: []interface{}{map[string]interface{}{"ciphertext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}}, - requireCiphertext: true, - dest: []BatchRequestItem{}, - }, - { - name: "required_ciphertext_missing", - src: []interface{}{map[string]interface{}{}}, - requireCiphertext: true, - dest: []BatchRequestItem{}, - wantErrContains: "missing ciphertext", - }, - { - name: "required_plaintext_and_ciphertext_missing", - src: []interface{}{map[string]interface{}{}}, - requirePlaintext: true, - requireCiphertext: true, - dest: []BatchRequestItem{}, - wantErrContains: "missing ciphertext", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - expectedDest := append(tt.dest[:0:0], tt.dest...) // copy of the dest state - expectedErr := mapstructure.Decode(tt.src, &expectedDest) != nil || tt.wantErrContains != "" - - gotErr := decodeBatchRequestItems(tt.src, tt.requirePlaintext, tt.requireCiphertext, &tt.dest) - gotDest := tt.dest - - if expectedErr { - if gotErr == nil { - t.Fatal("decodeBatchRequestItems unexpected error value; expected error but got none") - } - if tt.wantErrContains == "" { - t.Fatal("missing error condition") - } - if !strings.Contains(gotErr.Error(), tt.wantErrContains) { - t.Errorf("decodeBatchRequestItems unexpected error value, want err contains: '%v', got: '%v'", tt.wantErrContains, gotErr) - } - } - - if !reflect.DeepEqual(expectedDest, gotDest) { - t.Errorf("decodeBatchRequestItems unexpected dest value, want: '%v', got: '%v'", expectedDest, gotDest) - } - }) - } -} - -func TestShouldWarnAboutNonceUsage(t *testing.T) { - tests := []struct { - name string - keyTypes []keysutil.KeyType - nonce []byte - convergentEncryption bool - convergentVersion int - expected bool - }{ - { - name: "-NoConvergent-WithNonce", - keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305}, - nonce: []byte("testnonce"), - convergentEncryption: false, - convergentVersion: -1, - expected: true, - }, - { - name: "-NoConvergent-NoNonce", - keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305}, - nonce: []byte{}, - convergentEncryption: false, - convergentVersion: -1, - expected: false, - }, - { - name: "-Convergentv1-WithNonce", - keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305}, - nonce: []byte("testnonce"), - convergentEncryption: true, - convergentVersion: 1, - expected: true, - }, - { - name: "-Convergentv2-WithNonce", - keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305}, - nonce: []byte("testnonce"), - convergentEncryption: true, - convergentVersion: 2, - expected: false, - }, - { - name: "-Convergentv3-WithNonce", - keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305}, - nonce: []byte("testnonce"), - convergentEncryption: true, - convergentVersion: 3, - expected: false, - }, - { - name: "-NoConvergent-WithNonce", - keyTypes: []keysutil.KeyType{keysutil.KeyType_RSA2048, keysutil.KeyType_RSA4096}, - nonce: []byte("testnonce"), - convergentEncryption: false, - convergentVersion: -1, - expected: false, - }, - } - - for _, tt := range tests { - for _, keyType := range tt.keyTypes { - testName := keyType.String() + tt.name - t.Run(testName, func(t *testing.T) { - p := keysutil.Policy{ - ConvergentEncryption: tt.convergentEncryption, - ConvergentVersion: tt.convergentVersion, - Type: keyType, - } - - actual := shouldWarnAboutNonceUsage(&p, tt.nonce) - - if actual != tt.expected { - t.Errorf("Expected actual '%v' but got '%v'", tt.expected, actual) - } - }) - } - } -} - -func TestTransit_EncryptWithRSAPublicKey(t *testing.T) { - generateKeys(t) - b, s := createBackendWithStorage(t) - keyType := "rsa-2048" - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get key - privateKey := getKey(t, keyType) - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatal(err) - } - - // Import key - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - "type": keyType, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import public key: %s", err) - } - - req = &logical.Request{ - Operation: logical.CreateOperation, - Path: fmt.Sprintf("encrypt/%s", keyID), - Storage: s, - Data: map[string]interface{}{ - "plaintext": "bXkgc2VjcmV0IGRhdGE=", - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } -} diff --git a/builtin/logical/transit/path_export_test.go b/builtin/logical/transit/path_export_test.go deleted file mode 100644 index 5eb6eeaf1..000000000 --- a/builtin/logical/transit/path_export_test.go +++ /dev/null @@ -1,489 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "fmt" - "reflect" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTransit_Export_Unknown_ExportType(t *testing.T) { - t.Parallel() - - b, storage := createBackendWithSysView(t) - keyType := "ed25519" - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - Data: map[string]interface{}{ - "exportable": true, - "type": keyType, - }, - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed creating key %s: %v", keyType, err) - } - - req = &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "export/bad-export-type/foo", - } - rsp, err := b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatalf("did not error on bad export type got: %v", rsp) - } - if rsp == nil || !rsp.IsError() { - t.Fatalf("response did not contain an error on bad export type got: %v", rsp) - } - if !strings.Contains(rsp.Error().Error(), "invalid export type") { - t.Fatalf("failed with unexpected error: %v", err) - } -} - -func TestTransit_Export_KeyVersion_ExportsCorrectVersion(t *testing.T) { - t.Parallel() - - verifyExportsCorrectVersion(t, "encryption-key", "aes128-gcm96") - verifyExportsCorrectVersion(t, "encryption-key", "aes256-gcm96") - verifyExportsCorrectVersion(t, "encryption-key", "chacha20-poly1305") - verifyExportsCorrectVersion(t, "encryption-key", "rsa-2048") - verifyExportsCorrectVersion(t, "encryption-key", "rsa-3072") - verifyExportsCorrectVersion(t, "encryption-key", "rsa-4096") - verifyExportsCorrectVersion(t, "signing-key", "ecdsa-p256") - verifyExportsCorrectVersion(t, "signing-key", "ecdsa-p384") - verifyExportsCorrectVersion(t, "signing-key", "ecdsa-p521") - verifyExportsCorrectVersion(t, "signing-key", "ed25519") - verifyExportsCorrectVersion(t, "signing-key", "rsa-2048") - verifyExportsCorrectVersion(t, "signing-key", "rsa-3072") - verifyExportsCorrectVersion(t, "signing-key", "rsa-4096") - verifyExportsCorrectVersion(t, "hmac-key", "aes128-gcm96") - verifyExportsCorrectVersion(t, "hmac-key", "aes256-gcm96") - verifyExportsCorrectVersion(t, "hmac-key", "chacha20-poly1305") - verifyExportsCorrectVersion(t, "hmac-key", "ecdsa-p256") - verifyExportsCorrectVersion(t, "hmac-key", "ecdsa-p384") - verifyExportsCorrectVersion(t, "hmac-key", "ecdsa-p521") - verifyExportsCorrectVersion(t, "hmac-key", "ed25519") - verifyExportsCorrectVersion(t, "hmac-key", "hmac") - verifyExportsCorrectVersion(t, "public-key", "rsa-2048") - verifyExportsCorrectVersion(t, "public-key", "rsa-3072") - verifyExportsCorrectVersion(t, "public-key", "rsa-4096") - verifyExportsCorrectVersion(t, "public-key", "ecdsa-p256") - verifyExportsCorrectVersion(t, "public-key", "ecdsa-p384") - verifyExportsCorrectVersion(t, "public-key", "ecdsa-p521") - verifyExportsCorrectVersion(t, "public-key", "ed25519") -} - -func verifyExportsCorrectVersion(t *testing.T, exportType, keyType string) { - b, storage := createBackendWithSysView(t) - - // First create a key, v1 - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - } - req.Data = map[string]interface{}{ - "exportable": true, - "type": keyType, - } - if keyType == "hmac" { - req.Data["key_size"] = 32 - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - verifyVersion := func(versionRequest string, expectedVersion int) { - req := &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: fmt.Sprintf("export/%s/foo/%s", exportType, versionRequest), - } - rsp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - typRaw, ok := rsp.Data["type"] - if !ok { - t.Fatal("no type returned from export") - } - typ, ok := typRaw.(string) - if !ok { - t.Fatalf("could not find key type, resp data is %#v", rsp.Data) - } - if typ != keyType { - t.Fatalf("key type mismatch; %q vs %q", typ, keyType) - } - - keysRaw, ok := rsp.Data["keys"] - if !ok { - t.Fatal("could not find keys value") - } - keys, ok := keysRaw.(map[string]string) - if !ok { - t.Fatal("could not cast to keys map") - } - if len(keys) != 1 { - t.Fatal("unexpected number of keys found") - } - - for k := range keys { - if k != strconv.Itoa(expectedVersion) { - t.Fatalf("expected version %q, received version %q", strconv.Itoa(expectedVersion), k) - } - } - } - - verifyVersion("v1", 1) - verifyVersion("1", 1) - verifyVersion("latest", 1) - - req.Path = "keys/foo/rotate" - // v2 - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - verifyVersion("v1", 1) - verifyVersion("1", 1) - verifyVersion("v2", 2) - verifyVersion("2", 2) - verifyVersion("latest", 2) - - // v3 - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - verifyVersion("v1", 1) - verifyVersion("1", 1) - verifyVersion("v3", 3) - verifyVersion("3", 3) - verifyVersion("latest", 3) -} - -func TestTransit_Export_ValidVersionsOnly(t *testing.T) { - t.Parallel() - - b, storage := createBackendWithSysView(t) - - // First create a key, v1 - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - } - req.Data = map[string]interface{}{ - "exportable": true, - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Path = "keys/foo/rotate" - // v2 - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - // v3 - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - verifyExport := func(validVersions []int) { - req = &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "export/encryption-key/foo", - } - rsp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if _, ok := rsp.Data["keys"]; !ok { - t.Error("no keys returned from export") - } - - keys, ok := rsp.Data["keys"].(map[string]string) - if !ok { - t.Error("could not cast to keys object") - } - if len(keys) != len(validVersions) { - t.Errorf("expected %d key count, received %d", len(validVersions), len(keys)) - } - for _, version := range validVersions { - if _, ok := keys[strconv.Itoa(version)]; !ok { - t.Errorf("expecting to find key version %d, not found", version) - } - } - } - - verifyExport([]int{1, 2, 3}) - - req = &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo/config", - } - req.Data = map[string]interface{}{ - "min_decryption_version": 3, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - verifyExport([]int{3}) - - req = &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo/config", - } - req.Data = map[string]interface{}{ - "min_decryption_version": 2, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - verifyExport([]int{2, 3}) - - req = &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo/rotate", - } - // v4 - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - verifyExport([]int{2, 3, 4}) -} - -func TestTransit_Export_KeysNotMarkedExportable_ReturnsError(t *testing.T) { - t.Parallel() - - b, storage := createBackendWithSysView(t) - - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - } - req.Data = map[string]interface{}{ - "exportable": false, - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req = &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "export/encryption-key/foo", - } - rsp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if !rsp.IsError() { - t.Fatal("Key not marked as exportable but was exported.") - } -} - -func TestTransit_Export_SigningDoesNotSupportSigning_ReturnsError(t *testing.T) { - t.Parallel() - - b, storage := createBackendWithSysView(t) - - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - } - req.Data = map[string]interface{}{ - "exportable": true, - "type": "aes256-gcm96", - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req = &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "export/signing-key/foo", - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("Key does not support signing but was exported without error.") - } -} - -func TestTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t *testing.T) { - t.Parallel() - - testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t, "ecdsa-p256") - testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t, "ecdsa-p384") - testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t, "ecdsa-p521") - testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t, "ed25519") -} - -func testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t *testing.T, keyType string) { - b, storage := createBackendWithSysView(t) - - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - } - req.Data = map[string]interface{}{ - "exportable": true, - "type": keyType, - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req = &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "export/encryption-key/foo", - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatalf("Key %s does not support encryption but was exported without error.", keyType) - } -} - -func TestTransit_Export_PublicKeyDoesNotSupportEncryption_ReturnsError(t *testing.T) { - t.Parallel() - - testTransit_Export_PublicKeyNotSupported_ReturnsError(t, "chacha20-poly1305") - testTransit_Export_PublicKeyNotSupported_ReturnsError(t, "aes128-gcm96") - testTransit_Export_PublicKeyNotSupported_ReturnsError(t, "aes256-gcm96") - testTransit_Export_PublicKeyNotSupported_ReturnsError(t, "hmac") -} - -func testTransit_Export_PublicKeyNotSupported_ReturnsError(t *testing.T, keyType string) { - b, storage := createBackendWithSysView(t) - - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - Data: map[string]interface{}{ - "type": keyType, - }, - } - if keyType == "hmac" { - req.Data["key_size"] = 32 - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed creating key %s: %v", keyType, err) - } - - req = &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "export/public-key/foo", - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatalf("Key %s does not support public key exporting but was exported without error.", keyType) - } - if !strings.Contains(err.Error(), fmt.Sprintf("unknown key type %s for export type public-key", keyType)) { - t.Fatalf("unexpected error value for key type: %s: %v", keyType, err) - } -} - -func TestTransit_Export_KeysDoesNotExist_ReturnsNotFound(t *testing.T) { - t.Parallel() - - b, storage := createBackendWithSysView(t) - - req := &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "export/encryption-key/foo", - } - rsp, err := b.HandleRequest(context.Background(), req) - - if !(rsp == nil && err == nil) { - t.Fatal("Key does not exist but does not return not found") - } -} - -func TestTransit_Export_EncryptionKey_DoesNotExportHMACKey(t *testing.T) { - t.Parallel() - - b, storage := createBackendWithSysView(t) - - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - } - req.Data = map[string]interface{}{ - "exportable": true, - "type": "aes256-gcm96", - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req = &logical.Request{ - Storage: storage, - Operation: logical.ReadOperation, - Path: "export/encryption-key/foo", - } - encryptionKeyRsp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - req.Path = "export/hmac-key/foo" - hmacKeyRsp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - encryptionKeys, ok := encryptionKeyRsp.Data["keys"].(map[string]string) - if !ok { - t.Error("could not cast to keys object") - } - hmacKeys, ok := hmacKeyRsp.Data["keys"].(map[string]string) - if !ok { - t.Error("could not cast to keys object") - } - if len(hmacKeys) != len(encryptionKeys) { - t.Errorf("hmac (%d) and encryption (%d) key count don't match", - len(hmacKeys), len(encryptionKeys)) - } - - if reflect.DeepEqual(encryptionKeyRsp.Data, hmacKeyRsp.Data) { - t.Fatal("Encryption key data matched hmac key data") - } -} diff --git a/builtin/logical/transit/path_hash_test.go b/builtin/logical/transit/path_hash_test.go deleted file mode 100644 index 9ded6721a..000000000 --- a/builtin/logical/transit/path_hash_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTransit_Hash(t *testing.T) { - b, storage := createBackendWithSysView(t) - - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "hash", - Data: map[string]interface{}{ - "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", - }, - } - - doRequest := func(req *logical.Request, errExpected bool, expected string) { - resp, err := b.HandleRequest(context.Background(), req) - if err != nil && !errExpected { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if errExpected { - if !resp.IsError() { - t.Fatalf("bad: did not get error response: %#v", *resp) - } - return - } - if resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - sum, ok := resp.Data["sum"] - if !ok { - t.Fatal("no sum key found in returned data") - } - if sum.(string) != expected { - t.Fatal("mismatched hashes") - } - } - - // Test defaults -- sha2-256 - doRequest(req, false, "9ecb36561341d18eb65484e833efea61edc74b84cf5e6ae1b81c63533e25fc8f") - - // Test algorithm selection in the path - req.Path = "hash/sha2-224" - doRequest(req, false, "ea074a96cabc5a61f8298a2c470f019074642631a49e1c5e2f560865") - - // Reset and test algorithm selection in the data - req.Path = "hash" - req.Data["algorithm"] = "sha2-224" - doRequest(req, false, "ea074a96cabc5a61f8298a2c470f019074642631a49e1c5e2f560865") - - req.Data["algorithm"] = "sha2-384" - doRequest(req, false, "15af9ec8be783f25c583626e9491dbf129dd6dd620466fdf05b3a1d0bb8381d30f4d3ec29f923ff1e09a0f6b337365a6") - - req.Data["algorithm"] = "sha2-512" - doRequest(req, false, "d9d380f29b97ad6a1d92e987d83fa5a02653301e1006dd2bcd51afa59a9147e9caedaf89521abc0f0b682adcd47fb512b8343c834a32f326fe9bef00542ce887") - - // Test returning as base64 - req.Data["format"] = "base64" - doRequest(req, false, "2dOA8puXrWodkumH2D+loCZTMB4QBt0rzVGvpZqRR+nK7a+JUhq8DwtoKtzUf7USuDQ8g0oy8yb+m+8AVCzohw==") - - // Test SHA3 - req.Data["format"] = "hex" - req.Data["algorithm"] = "sha3-224" - doRequest(req, false, "ced91e69d89c837e87cff960bd64fd9b9f92325fb9add8988d33d007") - - req.Data["algorithm"] = "sha3-256" - doRequest(req, false, "e4bd866ec3fa52df3b7842aa97b448bc859a7606cefcdad1715847f4b82a6c93") - - req.Data["algorithm"] = "sha3-384" - doRequest(req, false, "715cd38cbf8d0bab426b6a084d649760be555dd64b34de6db148a3fbf2cd2aa5d8b03eb6eda73a3e9a8769c00b4c2113") - - req.Data["algorithm"] = "sha3-512" - doRequest(req, false, "f7cac5ad830422a5408b36a60a60620687be180765a3e2895bc3bdbd857c9e08246c83064d4e3612f0cb927f3ead208413ab98624bf7b0617af0f03f62080976") - - // Test returning SHA3 as base64 - req.Data["format"] = "base64" - doRequest(req, false, "98rFrYMEIqVAizamCmBiBoe+GAdlo+KJW8O9vYV8nggkbIMGTU42EvDLkn8+rSCEE6uYYkv3sGF68PA/YggJdg==") - - // Test bad input/format/algorithm - delete(req.Data, "input") - doRequest(req, true, "") - - req.Data["input"] = "dGhlIHF1aWNrIGJyb3duIGZveA==" - req.Data["format"] = "base92" - doRequest(req, true, "") - - req.Data["format"] = "hex" - req.Data["algorithm"] = "foobar" - doRequest(req, true, "") - - req.Data["algorithm"] = "sha2-256" - req.Data["input"] = "foobar" - doRequest(req, true, "") -} diff --git a/builtin/logical/transit/path_hmac_test.go b/builtin/logical/transit/path_hmac_test.go deleted file mode 100644 index 4fa0fbce3..000000000 --- a/builtin/logical/transit/path_hmac_test.go +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "fmt" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/helper/keysutil" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTransit_HMAC(t *testing.T) { - b, storage := createBackendWithSysView(t) - - cases := []struct { - name string - typ string - }{ - { - name: "foo", - typ: "", - }, - { - name: "dedicated", - typ: "hmac", - }, - } - - for _, c := range cases { - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/" + c.name, - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Now, change the key value to something we control - p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ - Storage: storage, - Name: c.name, - }, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - // We don't care as we're the only one using this - latestVersion := strconv.Itoa(p.LatestVersion) - keyEntry := p.Keys[latestVersion] - keyEntry.HMACKey = []byte("01234567890123456789012345678901") - keyEntry.Key = []byte("01234567890123456789012345678901") - p.Keys[latestVersion] = keyEntry - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - req.Path = "hmac/" + c.name - req.Data = map[string]interface{}{ - "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", - } - - doRequest := func(req *logical.Request, errExpected bool, expected string) { - path := req.Path - defer func() { req.Path = path }() - - resp, err := b.HandleRequest(context.Background(), req) - if err != nil && !errExpected { - panic(fmt.Sprintf("%v", err)) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if errExpected { - if !resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - return - } - if resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - value, ok := resp.Data["hmac"] - if !ok { - t.Fatalf("no hmac key found in returned data, got resp data %#v", resp.Data) - } - if value.(string) != expected { - panic(fmt.Sprintf("mismatched hashes; expected %s, got resp data %#v", expected, resp.Data)) - } - - // Now verify - req.Path = strings.ReplaceAll(req.Path, "hmac", "verify") - req.Data["hmac"] = value.(string) - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("%v: %v", err, resp) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.Data["valid"].(bool) == false { - panic(fmt.Sprintf("error validating hmac;\nreq:\n%#v\nresp:\n%#v", *req, *resp)) - } - } - - // Comparisons are against values generated via openssl - - // Test defaults -- sha2-256 - doRequest(req, false, "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=") - - // Test algorithm selection in the path - req.Path = "hmac/" + c.name + "/sha2-224" - doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==") - - // Reset and test algorithm selection in the data - req.Path = "hmac/" + c.name - req.Data["algorithm"] = "sha2-224" - doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==") - - req.Data["algorithm"] = "sha2-384" - doRequest(req, false, "vault:v1:jDB9YXdPjpmr29b1JCIEJO93IydlKVfD9mA2EO9OmJtJQg3QAV5tcRRRb7IQGW9p") - - req.Data["algorithm"] = "sha2-512" - doRequest(req, false, "vault:v1:PSXLXvkvKF4CpU65e2bK1tGBZQpcpCEM32fq2iUoiTyQQCfBcGJJItQ+60tMwWXAPQrC290AzTrNJucGrr4GFA==") - - // Test returning as base64 - req.Data["format"] = "base64" - doRequest(req, false, "vault:v1:PSXLXvkvKF4CpU65e2bK1tGBZQpcpCEM32fq2iUoiTyQQCfBcGJJItQ+60tMwWXAPQrC290AzTrNJucGrr4GFA==") - - // Test SHA3 - req.Path = "hmac/" + c.name - req.Data["algorithm"] = "sha3-224" - doRequest(req, false, "vault:v1:TGipmKH8LR/BkMolYpDYy0BJCIhTtGPDhV2VkQ==") - - req.Data["algorithm"] = "sha3-256" - doRequest(req, false, "vault:v1:+px9V/7QYLfdK808zPESC2T/L33uFf4Blzsn9Jy838o=") - - req.Data["algorithm"] = "sha3-384" - doRequest(req, false, "vault:v1:YGoRwN4UdTRYZeOER86jsQOB8piWenzLDzJ2wJQK/Jq59rAsY8lh7SCdqqCyFg70") - - req.Data["algorithm"] = "sha3-512" - doRequest(req, false, "vault:v1:GrNA8sU88naMPEQ7UZGj9EJl7YJhl03AFHfxcEURFrtvnobdea9ZlZHePpxAx/oCaC7R2HkrAO+Tu3uXPIl3lg==") - - // Test returning SHA3 as base64 - req.Data["format"] = "base64" - doRequest(req, false, "vault:v1:GrNA8sU88naMPEQ7UZGj9EJl7YJhl03AFHfxcEURFrtvnobdea9ZlZHePpxAx/oCaC7R2HkrAO+Tu3uXPIl3lg==") - - req.Data["algorithm"] = "foobar" - doRequest(req, true, "") - - req.Data["algorithm"] = "sha2-256" - req.Data["input"] = "foobar" - doRequest(req, true, "") - req.Data["input"] = "dGhlIHF1aWNrIGJyb3duIGZveA==" - - // Rotate - err = p.Rotate(context.Background(), storage, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - keyEntry = p.Keys["2"] - // Set to another value we control - keyEntry.HMACKey = []byte("12345678901234567890123456789012") - p.Keys["2"] = keyEntry - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - doRequest(req, false, "vault:v2:Dt+mO/B93kuWUbGMMobwUNX5Wodr6dL3JH4DMfpQ0kw=") - - // Verify a previous version - req.Path = "verify/" + c.name - - req.Data["hmac"] = "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("%v: %v", err, resp) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.Data["valid"].(bool) == false { - t.Fatalf("error validating hmac\nreq\n%#v\nresp\n%#v", *req, *resp) - } - - // Try a bad value - req.Data["hmac"] = "vault:v1:UcBvm4VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("%v: %v", err, resp) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.Data["valid"].(bool) { - t.Fatalf("expected error validating hmac") - } - - // Set min decryption version, attempt to verify - p.MinDecryptionVersion = 2 - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - req.Data["hmac"] = "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" - resp, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatalf("expected an error, got response %#v", resp) - } - if err != logical.ErrInvalidRequest { - t.Fatalf("expected invalid request error, got %v", err) - } - } -} - -func TestTransit_batchHMAC(t *testing.T) { - b, storage := createBackendWithSysView(t) - - // First create a key - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Now, change the key value to something we control - p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ - Storage: storage, - Name: "foo", - }, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - // We don't care as we're the only one using this - latestVersion := strconv.Itoa(p.LatestVersion) - keyEntry := p.Keys[latestVersion] - keyEntry.HMACKey = []byte("01234567890123456789012345678901") - p.Keys[latestVersion] = keyEntry - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - req.Path = "hmac/foo" - batchInput := []batchRequestHMACItem{ - {"input": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "one"}, - {"input": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "two"}, - {"input": "", "reference": "three"}, - {"input": ":;.?", "reference": "four"}, - {}, - } - - expected := []batchResponseHMACItem{ - {HMAC: "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=", Reference: "one"}, - {HMAC: "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=", Reference: "two"}, - {HMAC: "vault:v1:BCfVv6rlnRsIKpjCZCxWvh5iYwSSabRXpX9XJniuNgc=", Reference: "three"}, - {Error: "unable to decode input as base64: illegal base64 data at input byte 0", Reference: "four"}, - {Error: "missing input for HMAC"}, - } - - req.Data = map[string]interface{}{ - "batch_input": batchInput, - } - - resp, err := b.HandleRequest(context.Background(), req) - - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchResponseItems := resp.Data["batch_results"].([]batchResponseHMACItem) - - if len(batchResponseItems) != len(batchInput) { - t.Fatalf("Expected %d items in response. Got %d", len(batchInput), len(batchResponseItems)) - } - - for i, m := range batchResponseItems { - if expected[i].Error == "" && expected[i].HMAC != m.HMAC { - t.Fatalf("Expected HMAC %s got %s in result %d", expected[i].HMAC, m.HMAC, i) - } - if expected[i].Error != "" && expected[i].Error != m.Error { - t.Fatalf("Expected Error %q got %q in result %d", expected[i].Error, m.Error, i) - } - if expected[i].Reference != m.Reference { - t.Fatalf("Expected references to match, Got %s, Expected %s", m.Reference, expected[i].Reference) - } - } - - // Verify a previous version - req.Path = "verify/foo" - good_hmac := "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" - bad_hmac := "vault:v1:UcBvm4VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" - verifyBatch := []batchRequestHMACItem{ - {"input": "dGhlIHF1aWNrIGJyb3duIGZveA==", "hmac": good_hmac}, - } - - req.Data = map[string]interface{}{ - "batch_input": verifyBatch, - } - - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("%v: %v", err, resp) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - - batchHMACVerifyResponseItems := resp.Data["batch_results"].([]batchResponseHMACItem) - - if !batchHMACVerifyResponseItems[0].Valid { - t.Fatalf("error validating hmac\nreq\n%#v\nresp\n%#v", *req, *resp) - } - - // Try a bad value - verifyBatch[0]["hmac"] = bad_hmac - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("%v: %v", err, resp) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - - batchHMACVerifyResponseItems = resp.Data["batch_results"].([]batchResponseHMACItem) - - if batchHMACVerifyResponseItems[0].Valid { - t.Fatalf("expected error validating hmac\nreq\n%#v\nresp\n%#v", *req, *resp) - } - - // Rotate - err = p.Rotate(context.Background(), storage, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - keyEntry = p.Keys["2"] - // Set to another value we control - keyEntry.HMACKey = []byte("12345678901234567890123456789012") - p.Keys["2"] = keyEntry - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - // Set min decryption version, attempt to verify - p.MinDecryptionVersion = 2 - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - // supply a good hmac, but with expired key version - verifyBatch[0]["hmac"] = good_hmac - - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("%v: %v", err, resp) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - - batchHMACVerifyResponseItems = resp.Data["batch_results"].([]batchResponseHMACItem) - - if batchHMACVerifyResponseItems[0].Valid { - t.Fatalf("expected error validating hmac\nreq\n%#v\nresp\n%#v", *req, *resp) - } -} diff --git a/builtin/logical/transit/path_import_test.go b/builtin/logical/transit/path_import_test.go deleted file mode 100644 index ab471f0ff..000000000 --- a/builtin/logical/transit/path_import_test.go +++ /dev/null @@ -1,1075 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "fmt" - "strconv" - "sync" - "testing" - - "github.com/google/tink/go/kwp/subtle" - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/sdk/logical" -) - -var keyTypes = []string{ - "aes256-gcm96", - "aes128-gcm96", - "chacha20-poly1305", - "ed25519", - "ecdsa-p256", - "ecdsa-p384", - "ecdsa-p521", - "rsa-2048", - "rsa-3072", - "rsa-4096", - "hmac", -} - -var hashFns = []string{ - "SHA256", - "SHA1", - "SHA224", - "SHA384", - "SHA512", -} - -var ( - keysLock sync.RWMutex - keys = map[string]interface{}{} -) - -const ( - nssFormattedEd25519Key = "MGcCAQAwFAYHKoZIzj0CAQYJKwYBBAHaRw8BBEwwSgIBAQQgfJm5R+LK4FMwGzOpemTBXksimEVOVCE8QeC+XBBfNU+hIwMhADaif7IhYx46IHcRTy1z8LeyhABep+UB8Da6olMZGx0i" - rsaPSSFormattedKey = "MIIEvAIBADALBgkqhkiG9w0BAQoEggSoMIIEpAIBAAKCAQEAiFXSBaicB534+2qMZTVzQHMjuhb4NM9hi5H4EAFiYHEBuvm2BAk58NdBK3wiMq/p7Ewu5NQI0gJ7GlcV1MBU94U6MEmWNd0ztmlz37esEDuaCDhmLEBHKRzs8Om0bY9vczcNwcnRIYusP2KMxon3Gv2C86M2Jahig70AIq0E9C7esfrlYxFnoxUfO09XyYfiHlZY59+/dhyULp/RDIvaQ0/DqSSnYmXw8vRQ1gp6DqIzxx3j8ikUrpE7MK6348keFQj1eb83Z5w8qgIdceHHH4wbIAW7qWCPJ/vIJp8Pe1NEanlef61pDut2YcljvN79ccjX/QyqwqYv6xX2uzSlpQIDAQABAoIBACtpBCAoIVJtkv9e3EhHniR55PjWYn7SP5GEz3MtNalWokHqS/H6DBhrOcWCV5NDHx1N3qqe9xYDkzX+X6Wn/gX4RmBkte79uX8OEca8wY1DpRaT+riBWQc2vh0xlPFDuC177KX1QGFJi3V9SCzZdjSCXyV7pPyVopSm4/mmlMq5ANfN8bcHAtcArP7vPzEdckJqurjwHyzsUZJa9sk3OL3rBkKy5bmoPebE1ZQ7C+9eA4u9MKSy95WpTiqMe3rRhvr6zj4bzEvzS9M4r2EdwgAn4FyDwtGdOqtfbtSLTikb73f4MSINnWbt3YPBfRC4PGjWXIN2sMG5XYC3KH+RKbsCgYEAu0HOFInH8OtWiUY0aqRKZuo7lrBczNa5gnce3ZYnNkfrPlu1Xp0SjUkEWukznBLO0N9lvG9j3ksUDTQlPoKarJb9uf/1H0tYHhHm6mP8mH87yfVn2bLb3VPeIQYb+MXnDrwNVCAtxhuHlpnXJPldeuVKeRigHUNIEs76UMiiLqMCgYEAumJxm5NrKk0LXUQmeZolLh0lM/shg8zW7Vi3Ksz5Pe4Pcmg+hTbHjZuJwK6HesljEA0JDNkS0+5hkqiS5UDnj94XfDbi08/kKbPYA12GPVSRNTJxL8q70rFnEUZuMBeL0SKMPhEfR2z5TDDZUBoO6HBUUwgJAij1EsXrBAb0BxcCgYBKS3eKKohLi/PPjy0oynpCjtiJlvuawe7kVoLGg9aW8L3jBdvV6Bf+OmQh9bhmSggIUzo4IzHKdptECdZlEMhxhY6xh14nxmr1s0Cc6oLDtmdwX4+OjioxjB7rl1Ltxwc/j1jycbn3ieCn3e3AW7e9FNARb7XHJnSoEbq65n+CZQKBgQChLPozYAL/HIrkR0fCRmM6gmemkNeFo0CFFP+oWoJ6ZIAlHjJafmmIcmVoI0TzEG3C9pLJ8nmOnYjxCyekakEUryi9+LSkGBWlXmlBV8H7DUNYrlskyfssEs8fKDmnCuWUn3yJO8NBv+HBWkjCNRaJOIIjH0KzBHoRludJnz2tVwKBgQCsQF5lvcXefNfQojbhF+9NfyhvAc7EsMTXQhP9HEj0wVqTuuqyGyu8meXEkcQPRl6yD/yZKuMREDNNck4KV2fdGekBsh8zBgpxdHQ2DcbfxZfNgv3yoX3f0grb/ApQNJb3DVW9FVRigue8XPzFOFX/demJmkUnTg3zGFnXLXjgxg==" -) - -func generateKeys(t *testing.T) { - t.Helper() - - keysLock.Lock() - defer keysLock.Unlock() - - if len(keys) > 0 { - return - } - - for _, keyType := range keyTypes { - key, err := generateKey(keyType) - if err != nil { - t.Fatalf("failed to generate %s key: %s", keyType, err) - } - keys[keyType] = key - } -} - -func getKey(t *testing.T, keyType string) interface{} { - t.Helper() - - keysLock.RLock() - defer keysLock.RUnlock() - - key, ok := keys[keyType] - if !ok { - t.Fatalf("no pre-generated key of type: %s", keyType) - } - - return key -} - -func TestTransit_ImportNSSEd25519Key(t *testing.T) { - generateKeys(t) - b, s := createBackendWithStorage(t) - - wrappingKey, err := b.getWrappingKey(context.Background(), s) - if err != nil || wrappingKey == nil { - t.Fatalf("failed to retrieve public wrapping key: %s", err) - } - privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey - pubWrappingKey := &privWrappingKey.PublicKey - - rawPKCS8, err := base64.StdEncoding.DecodeString(nssFormattedEd25519Key) - if err != nil { - t.Fatalf("failed to parse nss base64: %v", err) - } - - blob := wrapTargetPKCS8ForImport(t, pubWrappingKey, rawPKCS8, "SHA256") - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: "keys/nss-ed25519/import", - Data: map[string]interface{}{ - "ciphertext": blob, - "type": "ed25519", - }, - } - - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import NSS-formatted Ed25519 key: %v", err) - } -} - -func TestTransit_ImportRSAPSS(t *testing.T) { - generateKeys(t) - b, s := createBackendWithStorage(t) - - wrappingKey, err := b.getWrappingKey(context.Background(), s) - if err != nil || wrappingKey == nil { - t.Fatalf("failed to retrieve public wrapping key: %s", err) - } - privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey - pubWrappingKey := &privWrappingKey.PublicKey - - rawPKCS8, err := base64.StdEncoding.DecodeString(rsaPSSFormattedKey) - if err != nil { - t.Fatalf("failed to parse rsa-pss base64: %v", err) - } - - blob := wrapTargetPKCS8ForImport(t, pubWrappingKey, rawPKCS8, "SHA256") - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: "keys/rsa-pss/import", - Data: map[string]interface{}{ - "ciphertext": blob, - "type": "rsa-2048", - }, - } - - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import RSA-PSS private key: %v", err) - } -} - -func TestTransit_Import(t *testing.T) { - generateKeys(t) - b, s := createBackendWithStorage(t) - - t.Run( - "import into a key fails before wrapping key is read", - func(t *testing.T) { - fakeWrappingKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - t.Fatalf("failed to generate fake wrapping key: %s", err) - } - // Roll an AES256 key and import - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - targetKey := getKey(t, "aes256-gcm96") - importBlob := wrapTargetKeyForImport(t, &fakeWrappingKey.PublicKey, targetKey, "aes256-gcm96", "SHA256") - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("import prior to wrapping key generation incorrectly succeeded") - } - }, - ) - - // Retrieve public wrapping key - wrappingKey, err := b.getWrappingKey(context.Background(), s) - if err != nil || wrappingKey == nil { - t.Fatalf("failed to retrieve public wrapping key: %s", err) - } - privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey - pubWrappingKey := &privWrappingKey.PublicKey - - t.Run( - "import into an existing key fails", - func(t *testing.T) { - // Generate a key ID - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate a key ID: %s", err) - } - - // Create an AES256 key within Transit - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s", keyID), - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("unexpected error creating key: %s", err) - } - - targetKey := getKey(t, "aes256-gcm96") - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("import into an existing key incorrectly succeeded") - } - }, - ) - - for _, keyType := range keyTypes { - priv := getKey(t, keyType) - for _, hashFn := range hashFns { - t.Run( - fmt.Sprintf("%s/%s", keyType, hashFn), - func(t *testing.T) { - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, priv, keyType, hashFn) - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "type": keyType, - "hash_function": hashFn, - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import valid key: %s", err) - } - }, - ) - - // Shouldn't need to test every combination of key and hash function - if keyType != "aes256-gcm96" { - break - } - } - } - - failures := []struct { - name string - ciphertext interface{} - keyType interface{} - hashFn interface{} - }{ - { - name: "nil ciphertext", - }, - { - name: "empty string ciphertext", - ciphertext: "", - }, - { - name: "ciphertext not base64", - ciphertext: "this isn't correct", - }, - { - name: "ciphertext too short", - ciphertext: "ZmFrZSBjaXBoZXJ0ZXh0Cg", - }, - { - name: "invalid key type", - keyType: "fake-key-type", - }, - { - name: "invalid hash function", - hashFn: "fake-hash-fn", - }, - } - for _, tt := range failures { - t.Run( - tt.name, - func(t *testing.T) { - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{}, - } - if tt.ciphertext != nil { - req.Data["ciphertext"] = tt.ciphertext - } - if tt.keyType != nil { - req.Data["type"] = tt.keyType - } - if tt.hashFn != nil { - req.Data["hash_function"] = tt.hashFn - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("invalid import request incorrectly succeeded") - } - }, - ) - } - - t.Run( - "disallow import of convergent keys", - func(t *testing.T) { - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - targetKey := getKey(t, "aes256-gcm96") - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "convergent_encryption": true, - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("import of convergent key incorrectly succeeded") - } - }, - ) - - t.Run( - "allow_rotation=true enables rotation within vault", - func(t *testing.T) { - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - targetKey := getKey(t, "aes256-gcm96") - - // Import key - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "allow_rotation": true, - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import key: %s", err) - } - - // Rotate key - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/rotate", keyID), - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to rotate key: %s", err) - } - }, - ) - - t.Run( - "allow_rotation=false disables rotation within vault", - func(t *testing.T) { - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - targetKey := getKey(t, "aes256-gcm96") - - // Import key - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "allow_rotation": false, - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import key: %s", err) - } - - // Rotate key - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/rotate", keyID), - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("rotation of key with allow_rotation incorrectly succeeded") - } - }, - ) - - t.Run( - "import public key ed25519", - func(t *testing.T) { - keyType := "ed25519" - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get keys - privateKey := getKey(t, keyType) - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatal(err) - } - - // Import key - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - "type": keyType, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import ed25519 key: %v", err) - } - }) - - t.Run( - "import public key ecdsa", - func(t *testing.T) { - keyType := "ecdsa-p256" - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get keys - privateKey := getKey(t, keyType) - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatal(err) - } - - // Import key - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - "type": keyType, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import public key: %s", err) - } - }) -} - -func TestTransit_ImportVersion(t *testing.T) { - generateKeys(t) - b, s := createBackendWithStorage(t) - - t.Run( - "import into a key version fails before wrapping key is read", - func(t *testing.T) { - fakeWrappingKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - t.Fatalf("failed to generate fake wrapping key: %s", err) - } - // Roll an AES256 key and import - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - targetKey := getKey(t, "aes256-gcm96") - importBlob := wrapTargetKeyForImport(t, &fakeWrappingKey.PublicKey, targetKey, "aes256-gcm96", "SHA256") - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("import_version prior to wrapping key generation incorrectly succeeded") - } - }, - ) - - // Retrieve public wrapping key - wrappingKey, err := b.getWrappingKey(context.Background(), s) - if err != nil || wrappingKey == nil { - t.Fatalf("failed to retrieve public wrapping key: %s", err) - } - privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey - pubWrappingKey := &privWrappingKey.PublicKey - - t.Run( - "import into a non-existent key fails", - func(t *testing.T) { - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - targetKey := getKey(t, "aes256-gcm96") - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("import_version into a non-existent key incorrectly succeeded") - } - }, - ) - - t.Run( - "import into an internally-generated key fails", - func(t *testing.T) { - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Roll a key within Transit - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s", keyID), - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to generate a key within transit: %s", err) - } - - // Attempt to import into newly generated key - targetKey := getKey(t, "aes256-gcm96") - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("import_version into an internally-generated key incorrectly succeeded") - } - }, - ) - - t.Run( - "imported key version type must match existing key type", - func(t *testing.T) { - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Import an RSA key - targetKey := getKey(t, "rsa-2048") - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "rsa-2048", "SHA256") - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - "type": "rsa-2048", - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to generate a key within transit: %s", err) - } - - // Attempt to import an AES key version into existing RSA key - targetKey = getKey(t, "aes256-gcm96") - importBlob = wrapTargetKeyForImport(t, pubWrappingKey, targetKey, "aes256-gcm96", "SHA256") - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatal("import_version into a key of a different type incorrectly succeeded") - } - }, - ) - - t.Run( - "import rsa public key and update version with private counterpart", - func(t *testing.T) { - keyType := "rsa-2048" - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get keys - privateKey := getKey(t, keyType) - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, privateKey, keyType, "SHA256") - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatal(err) - } - - // Import RSA public key - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - "type": keyType, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import public key: %s", err) - } - - // Update version - import RSA private key - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to update key: %s", err) - } - }, - ) -} - -func TestTransit_ImportVersionWithPublicKeys(t *testing.T) { - generateKeys(t) - b, s := createBackendWithStorage(t) - - // Retrieve public wrapping key - wrappingKey, err := b.getWrappingKey(context.Background(), s) - if err != nil || wrappingKey == nil { - t.Fatalf("failed to retrieve public wrapping key: %s", err) - } - privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey - pubWrappingKey := &privWrappingKey.PublicKey - - // Import a public key then import private should give us one key - t.Run( - "import rsa public key and update version with private counterpart", - func(t *testing.T) { - keyType := "ecdsa-p256" - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get keys - privateKey := getKey(t, keyType) - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, privateKey, keyType, "SHA256") - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatal(err) - } - - // Import EC public key - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - "type": keyType, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import public key: %s", err) - } - - // Update version - import EC private key - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to update key: %s", err) - } - - // We should have one key on export - req = &logical.Request{ - Storage: s, - Operation: logical.ReadOperation, - Path: fmt.Sprintf("export/public-key/%s", keyID), - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to export key: %s", err) - } - - if len(resp.Data["keys"].(map[string]string)) != 1 { - t.Fatalf("expected 1 key but got %v: %v", len(resp.Data["keys"].(map[string]string)), resp) - } - }, - ) - - // Import a private and then public should give us two keys - t.Run( - "import ec private key and then its public counterpart", - func(t *testing.T) { - keyType := "ecdsa-p256" - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get keys - privateKey := getKey(t, keyType) - importBlob := wrapTargetKeyForImport(t, pubWrappingKey, privateKey, keyType, "SHA256") - publicKeyBytes, err := getPublicKey(privateKey, keyType) - if err != nil { - t.Fatal(err) - } - - // Import EC private key - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob, - "type": keyType, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to update key: %s", err) - } - - // Update version - Import EC public key - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import public key: %s", err) - } - - // We should have two keys on export - req = &logical.Request{ - Storage: s, - Operation: logical.ReadOperation, - Path: fmt.Sprintf("export/public-key/%s", keyID), - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to export key: %s", err) - } - - if len(resp.Data["keys"].(map[string]string)) != 2 { - t.Fatalf("expected 2 key but got %v: %v", len(resp.Data["keys"].(map[string]string)), resp) - } - }, - ) - - // Import a public and another public should allow us to insert two private key. - t.Run( - "import two public keys and two private keys in reverse order", - func(t *testing.T) { - keyType := "ecdsa-p256" - keyID, err := uuid.GenerateUUID() - if err != nil { - t.Fatalf("failed to generate key ID: %s", err) - } - - // Get keys - privateKey1 := getKey(t, keyType) - importBlob1 := wrapTargetKeyForImport(t, pubWrappingKey, privateKey1, keyType, "SHA256") - publicKeyBytes1, err := getPublicKey(privateKey1, keyType) - if err != nil { - t.Fatal(err) - } - - privateKey2, err := generateKey(keyType) - if err != nil { - t.Fatal(err) - } - importBlob2 := wrapTargetKeyForImport(t, pubWrappingKey, privateKey2, keyType, "SHA256") - publicKeyBytes2, err := getPublicKey(privateKey2, keyType) - if err != nil { - t.Fatal(err) - } - - // Import EC public key - req := &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes1, - "type": keyType, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to update key: %s", err) - } - - // Update version - Import second EC public key - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "public_key": publicKeyBytes2, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import public key: %s", err) - } - - // We should have two keys on export - req = &logical.Request{ - Storage: s, - Operation: logical.ReadOperation, - Path: fmt.Sprintf("export/public-key/%s", keyID), - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to export key: %s", err) - } - - if len(resp.Data["keys"].(map[string]string)) != 2 { - t.Fatalf("expected 2 key but got %v: %v", len(resp.Data["keys"].(map[string]string)), resp) - } - - // Import second private key first, with no options. - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob2, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import private key: %s", err) - } - - // Import first private key second, with a version - req = &logical.Request{ - Storage: s, - Operation: logical.UpdateOperation, - Path: fmt.Sprintf("keys/%s/import_version", keyID), - Data: map[string]interface{}{ - "ciphertext": importBlob1, - "version": 1, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to import private key: %s", err) - } - - // We should still have two keys on export - req = &logical.Request{ - Storage: s, - Operation: logical.ReadOperation, - Path: fmt.Sprintf("export/public-key/%s", keyID), - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("failed to export key: %s", err) - } - - if len(resp.Data["keys"].(map[string]string)) != 2 { - t.Fatalf("expected 2 key but got %v: %v", len(resp.Data["keys"].(map[string]string)), resp) - } - }, - ) -} - -func wrapTargetKeyForImport(t *testing.T, wrappingKey *rsa.PublicKey, targetKey interface{}, targetKeyType string, hashFnName string) string { - t.Helper() - - // Format target key for wrapping - var preppedTargetKey []byte - var ok bool - var err error - switch targetKeyType { - case "aes128-gcm96", "aes256-gcm96", "chacha20-poly1305", "hmac": - preppedTargetKey, ok = targetKey.([]byte) - if !ok { - t.Fatal("failed to wrap target key for import: symmetric key not provided in byte format") - } - default: - preppedTargetKey, err = x509.MarshalPKCS8PrivateKey(targetKey) - if err != nil { - t.Fatalf("failed to wrap target key for import: %s", err) - } - } - - return wrapTargetPKCS8ForImport(t, wrappingKey, preppedTargetKey, hashFnName) -} - -func wrapTargetPKCS8ForImport(t *testing.T, wrappingKey *rsa.PublicKey, preppedTargetKey []byte, hashFnName string) string { - t.Helper() - - // Generate an ephemeral AES-256 key - ephKey, err := uuid.GenerateRandomBytes(32) - if err != nil { - t.Fatalf("failed to wrap target key for import: %s", err) - } - - // Parse the hash function name into an actual function - hashFn, err := parseHashFn(hashFnName) - if err != nil { - t.Fatalf("failed to wrap target key for import: %s", err) - } - - // Wrap ephemeral AES key with public wrapping key - ephKeyWrapped, err := rsa.EncryptOAEP(hashFn, rand.Reader, wrappingKey, ephKey, []byte{}) - if err != nil { - t.Fatalf("failed to wrap target key for import: %s", err) - } - - // Create KWP instance for wrapping target key - kwp, err := subtle.NewKWP(ephKey) - if err != nil { - t.Fatalf("failed to wrap target key for import: %s", err) - } - - // Wrap target key with KWP - targetKeyWrapped, err := kwp.Wrap(preppedTargetKey) - if err != nil { - t.Fatalf("failed to wrap target key for import: %s", err) - } - - // Combined wrapped keys into a single blob and base64 encode - wrappedKeys := append(ephKeyWrapped, targetKeyWrapped...) - return base64.StdEncoding.EncodeToString(wrappedKeys) -} - -func generateKey(keyType string) (interface{}, error) { - switch keyType { - case "aes128-gcm96": - return uuid.GenerateRandomBytes(16) - case "aes256-gcm96", "hmac": - return uuid.GenerateRandomBytes(32) - case "chacha20-poly1305": - return uuid.GenerateRandomBytes(32) - case "ed25519": - _, priv, err := ed25519.GenerateKey(rand.Reader) - return priv, err - case "ecdsa-p256": - return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - case "ecdsa-p384": - return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case "ecdsa-p521": - return ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - case "rsa-2048": - return rsa.GenerateKey(rand.Reader, 2048) - case "rsa-3072": - return rsa.GenerateKey(rand.Reader, 3072) - case "rsa-4096": - return rsa.GenerateKey(rand.Reader, 4096) - default: - return nil, fmt.Errorf("failed to generate unsupported key type: %s", keyType) - } -} - -func getPublicKey(privateKey crypto.PrivateKey, keyType string) ([]byte, error) { - var publicKey crypto.PublicKey - var publicKeyBytes []byte - switch keyType { - case "rsa-2048", "rsa-3072", "rsa-4096": - publicKey = privateKey.(*rsa.PrivateKey).Public() - case "ecdsa-p256", "ecdsa-p384", "ecdsa-p521": - publicKey = privateKey.(*ecdsa.PrivateKey).Public() - case "ed25519": - publicKey = privateKey.(ed25519.PrivateKey).Public() - default: - return publicKeyBytes, fmt.Errorf("failed to get public key from %s key", keyType) - } - - publicKeyBytes, err := publicKeyToBytes(publicKey) - if err != nil { - return publicKeyBytes, err - } - - return publicKeyBytes, nil -} - -func publicKeyToBytes(publicKey crypto.PublicKey) ([]byte, error) { - var publicKeyBytesPem []byte - publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) - if err != nil { - return publicKeyBytesPem, fmt.Errorf("failed to marshal public key: %s", err) - } - - pemBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: publicKeyBytes, - } - - return pem.EncodeToMemory(pemBlock), nil -} diff --git a/builtin/logical/transit/path_keys_config_test.go b/builtin/logical/transit/path_keys_config_test.go deleted file mode 100644 index 98bcbe448..000000000 --- a/builtin/logical/transit/path_keys_config_test.go +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "encoding/hex" - "encoding/json" - "fmt" - "strconv" - "strings" - "testing" - "time" - - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/api" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestTransit_ConfigSettings(t *testing.T) { - b, storage := createBackendWithSysView(t) - - doReq := func(req *logical.Request) *logical.Response { - resp, err := b.HandleRequest(context.Background(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("got err:\n%#v\nreq:\n%#v\n", err, *req) - } - return resp - } - doErrReq := func(req *logical.Request) { - resp, err := b.HandleRequest(context.Background(), req) - if err == nil { - if resp == nil || !resp.IsError() { - t.Fatalf("expected error; req:\n%#v\n", *req) - } - } - } - - // First create a key - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/aes256", - Data: map[string]interface{}{ - "derived": true, - }, - } - doReq(req) - - req.Path = "keys/aes128" - req.Data["type"] = "aes128-gcm96" - doReq(req) - - req.Path = "keys/ed" - req.Data["type"] = "ed25519" - doReq(req) - - delete(req.Data, "derived") - - req.Path = "keys/p256" - req.Data["type"] = "ecdsa-p256" - doReq(req) - - req.Path = "keys/p384" - req.Data["type"] = "ecdsa-p384" - doReq(req) - - req.Path = "keys/p521" - req.Data["type"] = "ecdsa-p521" - doReq(req) - - delete(req.Data, "type") - - req.Path = "keys/aes128/rotate" - doReq(req) - doReq(req) - doReq(req) - doReq(req) - - req.Path = "keys/aes256/rotate" - doReq(req) - doReq(req) - doReq(req) - doReq(req) - - req.Path = "keys/ed/rotate" - doReq(req) - doReq(req) - doReq(req) - doReq(req) - - req.Path = "keys/p256/rotate" - doReq(req) - doReq(req) - doReq(req) - doReq(req) - - req.Path = "keys/p384/rotate" - doReq(req) - doReq(req) - doReq(req) - doReq(req) - - req.Path = "keys/p521/rotate" - doReq(req) - doReq(req) - doReq(req) - doReq(req) - - req.Path = "keys/aes256/config" - // Too high - req.Data["min_decryption_version"] = 7 - doErrReq(req) - // Too low - req.Data["min_decryption_version"] = -1 - doErrReq(req) - - delete(req.Data, "min_decryption_version") - // Too high - req.Data["min_encryption_version"] = 7 - doErrReq(req) - // Too low - req.Data["min_encryption_version"] = 7 - doErrReq(req) - - // Not allowed, cannot decrypt - req.Data["min_decryption_version"] = 3 - req.Data["min_encryption_version"] = 2 - doErrReq(req) - - // Allowed - req.Data["min_decryption_version"] = 2 - req.Data["min_encryption_version"] = 3 - doReq(req) - req.Path = "keys/aes128/config" - doReq(req) - req.Path = "keys/ed/config" - doReq(req) - req.Path = "keys/p256/config" - doReq(req) - req.Path = "keys/p384/config" - doReq(req) - - req.Path = "keys/p521/config" - doReq(req) - - req.Data = map[string]interface{}{ - "plaintext": "abcd", - "input": "abcd", - "context": "abcd", - } - - maxKeyVersion := 5 - key := "aes256" - - testHMAC := func(ver int, valid bool) { - req.Path = "hmac/" + key - delete(req.Data, "hmac") - if ver == maxKeyVersion { - delete(req.Data, "key_version") - } else { - req.Data["key_version"] = ver - } - - if !valid { - doErrReq(req) - return - } - - resp := doReq(req) - ct := resp.Data["hmac"].(string) - if strings.Split(ct, ":")[1] != "v"+strconv.Itoa(ver) { - t.Fatal("wrong hmac version") - } - - req.Path = "verify/" + key - delete(req.Data, "key_version") - req.Data["hmac"] = resp.Data["hmac"] - doReq(req) - } - - testEncryptDecrypt := func(ver int, valid bool) { - req.Path = "encrypt/" + key - delete(req.Data, "ciphertext") - if ver == maxKeyVersion { - delete(req.Data, "key_version") - } else { - req.Data["key_version"] = ver - } - - if !valid { - doErrReq(req) - return - } - - resp := doReq(req) - ct := resp.Data["ciphertext"].(string) - if strings.Split(ct, ":")[1] != "v"+strconv.Itoa(ver) { - t.Fatal("wrong encryption version") - } - - req.Path = "decrypt/" + key - delete(req.Data, "key_version") - req.Data["ciphertext"] = resp.Data["ciphertext"] - doReq(req) - } - testEncryptDecrypt(5, true) - testEncryptDecrypt(4, true) - testEncryptDecrypt(3, true) - testEncryptDecrypt(2, false) - testHMAC(5, true) - testHMAC(4, true) - testHMAC(3, true) - testHMAC(2, false) - - key = "aes128" - testEncryptDecrypt(5, true) - testEncryptDecrypt(4, true) - testEncryptDecrypt(3, true) - testEncryptDecrypt(2, false) - testHMAC(5, true) - testHMAC(4, true) - testHMAC(3, true) - testHMAC(2, false) - - delete(req.Data, "plaintext") - req.Data["input"] = "abcd" - key = "ed" - testSignVerify := func(ver int, valid bool) { - req.Path = "sign/" + key - delete(req.Data, "signature") - if ver == maxKeyVersion { - delete(req.Data, "key_version") - } else { - req.Data["key_version"] = ver - } - - if !valid { - doErrReq(req) - return - } - - resp := doReq(req) - ct := resp.Data["signature"].(string) - if strings.Split(ct, ":")[1] != "v"+strconv.Itoa(ver) { - t.Fatal("wrong signature version") - } - - req.Path = "verify/" + key - delete(req.Data, "key_version") - req.Data["signature"] = resp.Data["signature"] - doReq(req) - } - testSignVerify(5, true) - testSignVerify(4, true) - testSignVerify(3, true) - testSignVerify(2, false) - testHMAC(5, true) - testHMAC(4, true) - testHMAC(3, true) - testHMAC(2, false) - - delete(req.Data, "context") - key = "p256" - testSignVerify(5, true) - testSignVerify(4, true) - testSignVerify(3, true) - testSignVerify(2, false) - testHMAC(5, true) - testHMAC(4, true) - testHMAC(3, true) - testHMAC(2, false) - - key = "p384" - testSignVerify(5, true) - testSignVerify(4, true) - testSignVerify(3, true) - testSignVerify(2, false) - testHMAC(5, true) - testHMAC(4, true) - testHMAC(3, true) - testHMAC(2, false) - - key = "p521" - testSignVerify(5, true) - testSignVerify(4, true) - testSignVerify(3, true) - testSignVerify(2, false) - testHMAC(5, true) - testHMAC(4, true) - testHMAC(3, true) - testHMAC(2, false) -} - -func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) { - tests := map[string]struct { - initialAutoRotatePeriod interface{} - newAutoRotatePeriod interface{} - shouldError bool - expectedValue time.Duration - }{ - "default (no value)": { - initialAutoRotatePeriod: "5h", - shouldError: false, - expectedValue: 5 * time.Hour, - }, - "0 (int)": { - initialAutoRotatePeriod: "5h", - newAutoRotatePeriod: 0, - shouldError: false, - expectedValue: 0, - }, - "0 (string)": { - initialAutoRotatePeriod: "5h", - newAutoRotatePeriod: 0, - shouldError: false, - expectedValue: 0, - }, - "5 seconds": { - newAutoRotatePeriod: "5s", - shouldError: true, - }, - "5 hours": { - newAutoRotatePeriod: "5h", - shouldError: false, - expectedValue: 5 * time.Hour, - }, - "negative value": { - newAutoRotatePeriod: "-1800s", - shouldError: true, - }, - "invalid string": { - newAutoRotatePeriod: "this shouldn't work", - shouldError: true, - }, - } - - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - client := cores[0].Client - err := client.Sys().Mount("transit", &api.MountInput{ - Type: "transit", - }) - if err != nil { - t.Fatal(err) - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - keyNameBytes, err := uuid.GenerateRandomBytes(16) - if err != nil { - t.Fatal(err) - } - keyName := hex.EncodeToString(keyNameBytes) - - _, err = client.Logical().Write(fmt.Sprintf("transit/keys/%s", keyName), map[string]interface{}{ - "auto_rotate_period": test.initialAutoRotatePeriod, - }) - if err != nil { - t.Fatal(err) - } - resp, err := client.Logical().Write(fmt.Sprintf("transit/keys/%s/config", keyName), map[string]interface{}{ - "auto_rotate_period": test.newAutoRotatePeriod, - }) - switch { - case test.shouldError && err == nil: - t.Fatal("expected non-nil error") - case !test.shouldError && err != nil: - t.Fatal(err) - } - - if !test.shouldError { - resp, err = client.Logical().Read(fmt.Sprintf("transit/keys/%s", keyName)) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - gotRaw, ok := resp.Data["auto_rotate_period"].(json.Number) - if !ok { - t.Fatal("returned value is of unexpected type") - } - got, err := gotRaw.Int64() - if err != nil { - t.Fatal(err) - } - want := int64(test.expectedValue.Seconds()) - if got != want { - t.Fatalf("incorrect auto_rotate_period returned, got: %d, want: %d", got, want) - } - } - }) - } -} diff --git a/builtin/logical/transit/path_keys_test.go b/builtin/logical/transit/path_keys_test.go deleted file mode 100644 index 3a0abfeb0..000000000 --- a/builtin/logical/transit/path_keys_test.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit_test - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "testing" - "time" - - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/audit" - "github.com/hashicorp/vault/builtin/audit/file" - "github.com/hashicorp/vault/builtin/logical/transit" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestTransit_Issue_2958(t *testing.T) { - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": transit.Factory, - }, - AuditBackends: map[string]audit.Factory{ - "file": file.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - client := cores[0].Client - - err := client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{ - Type: "file", - Options: map[string]string{ - "file_path": "/dev/null", - }, - }) - if err != nil { - t.Fatal(err) - } - - err = client.Sys().Mount("transit", &api.MountInput{ - Type: "transit", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("transit/keys/foo", map[string]interface{}{ - "type": "ecdsa-p256", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("transit/keys/foobar", map[string]interface{}{ - "type": "ecdsa-p384", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("transit/keys/bar", map[string]interface{}{ - "type": "ed25519", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Read("transit/keys/foo") - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Read("transit/keys/foobar") - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Read("transit/keys/bar") - if err != nil { - t.Fatal(err) - } -} - -func TestTransit_CreateKeyWithAutorotation(t *testing.T) { - tests := map[string]struct { - autoRotatePeriod interface{} - shouldError bool - expectedValue time.Duration - }{ - "default (no value)": { - shouldError: false, - }, - "0 (int)": { - autoRotatePeriod: 0, - shouldError: false, - expectedValue: 0, - }, - "0 (string)": { - autoRotatePeriod: "0", - shouldError: false, - expectedValue: 0, - }, - "5 seconds": { - autoRotatePeriod: "5s", - shouldError: true, - }, - "5 hours": { - autoRotatePeriod: "5h", - shouldError: false, - expectedValue: 5 * time.Hour, - }, - "negative value": { - autoRotatePeriod: "-1800s", - shouldError: true, - }, - "invalid string": { - autoRotatePeriod: "this shouldn't work", - shouldError: true, - }, - } - - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": transit.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - client := cores[0].Client - err := client.Sys().Mount("transit", &api.MountInput{ - Type: "transit", - }) - if err != nil { - t.Fatal(err) - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - keyNameBytes, err := uuid.GenerateRandomBytes(16) - if err != nil { - t.Fatal(err) - } - keyName := hex.EncodeToString(keyNameBytes) - - _, err = client.Logical().Write(fmt.Sprintf("transit/keys/%s", keyName), map[string]interface{}{ - "auto_rotate_period": test.autoRotatePeriod, - }) - switch { - case test.shouldError && err == nil: - t.Fatal("expected non-nil error") - case !test.shouldError && err != nil: - t.Fatal(err) - } - - if !test.shouldError { - resp, err := client.Logical().Read(fmt.Sprintf("transit/keys/%s", keyName)) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - gotRaw, ok := resp.Data["auto_rotate_period"].(json.Number) - if !ok { - t.Fatal("returned value is of unexpected type") - } - got, err := gotRaw.Int64() - if err != nil { - t.Fatal(err) - } - want := int64(test.expectedValue.Seconds()) - if got != want { - t.Fatalf("incorrect auto_rotate_period returned, got: %d, want: %d", got, want) - } - } - }) - } -} diff --git a/builtin/logical/transit/path_random_test.go b/builtin/logical/transit/path_random_test.go deleted file mode 100644 index a58820b98..000000000 --- a/builtin/logical/transit/path_random_test.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "encoding/base64" - "encoding/hex" - "fmt" - "reflect" - "testing" - - "github.com/hashicorp/vault/helper/random" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTransit_Random(t *testing.T) { - var b *backend - sysView := logical.TestSystemView() - storage := &logical.InmemStorage{} - sysView.CachingDisabledVal = true - - b, _ = Backend(context.Background(), &logical.BackendConfig{ - StorageView: storage, - System: sysView, - }) - - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "random", - Data: map[string]interface{}{}, - } - - doRequest := func(req *logical.Request, errExpected bool, format string, numBytes int) { - getResponse := func() []byte { - resp, err := b.HandleRequest(context.Background(), req) - if err != nil && !errExpected { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if errExpected { - if !resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - return nil - } - if resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - if _, ok := resp.Data["random_bytes"]; !ok { - t.Fatal("no random_bytes found in response") - } - - outputStr := resp.Data["random_bytes"].(string) - var outputBytes []byte - switch format { - case "base64": - outputBytes, err = base64.StdEncoding.DecodeString(outputStr) - case "hex": - outputBytes, err = hex.DecodeString(outputStr) - default: - t.Fatal("unknown format") - } - if err != nil { - t.Fatal(err) - } - - return outputBytes - } - - rand1 := getResponse() - // Expected error - if rand1 == nil { - return - } - rand2 := getResponse() - if len(rand1) != numBytes || len(rand2) != numBytes { - t.Fatal("length of output random bytes not what is expected") - } - if reflect.DeepEqual(rand1, rand2) { - t.Fatal("found identical ouputs") - } - } - - for _, source := range []string{"", "platform", "seal", "all"} { - req.Data["source"] = source - req.Data["bytes"] = 32 - req.Data["format"] = "base64" - req.Path = "random" - // Test defaults - doRequest(req, false, "base64", 32) - - // Test size selection in the path - req.Path = "random/24" - req.Data["format"] = "hex" - doRequest(req, false, "hex", 24) - - if source != "" { - // Test source selection in the path - req.Path = fmt.Sprintf("random/%s", source) - req.Data["format"] = "hex" - doRequest(req, false, "hex", 32) - - req.Path = fmt.Sprintf("random/%s/24", source) - req.Data["format"] = "hex" - doRequest(req, false, "hex", 24) - } - - // Test bad input/format - req.Path = "random" - req.Data["format"] = "base92" - doRequest(req, true, "", 0) - - req.Data["format"] = "hex" - req.Data["bytes"] = -1 - doRequest(req, true, "", 0) - - req.Data["format"] = "hex" - req.Data["bytes"] = random.APIMaxBytes + 1 - - doRequest(req, true, "", 0) - } -} diff --git a/builtin/logical/transit/path_restore_test.go b/builtin/logical/transit/path_restore_test.go deleted file mode 100644 index 1cd0dcd61..000000000 --- a/builtin/logical/transit/path_restore_test.go +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "fmt" - "testing" - - "github.com/hashicorp/vault/helper/testhelpers" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTransit_Restore(t *testing.T) { - // Test setup: - // - Create a key - // - Configure it to be exportable, allowing deletion, and backups - // - Capture backup - // - Delete key - // - Run test cases - // - // Each test case should start with no key present. If the 'Seed' parameter is - // in the struct, we'll start by restoring it (without force) to run that test - // as if the key already existed - - keyType := "aes256-gcm96" - b, s := createBackendWithStorage(t) - keyName := testhelpers.RandomWithPrefix("my-key") - - // Create a key - keyReq := &logical.Request{ - Path: "keys/" + keyName, - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "type": keyType, - "exportable": true, - }, - } - resp, err := b.HandleRequest(context.Background(), keyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Configure the key to allow its deletion and backup - configReq := &logical.Request{ - Path: fmt.Sprintf("keys/%s/config", keyName), - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "deletion_allowed": true, - "allow_plaintext_backup": true, - }, - } - resp, err = b.HandleRequest(context.Background(), configReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // Take a backup of the key - backupReq := &logical.Request{ - Path: "backup/" + keyName, - Operation: logical.ReadOperation, - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), backupReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - backupKey := resp.Data["backup"].(string) - if backupKey == "" { - t.Fatal("failed to get a backup") - } - - // Delete the key to start test cases with clean slate - keyReq.Operation = logical.DeleteOperation - resp, err = b.HandleRequest(context.Background(), keyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // helper func to get a pointer value for a boolean - boolPtr := func(b bool) *bool { - return &b - } - - keyExitsError := fmt.Errorf("key %q already exists", keyName) - - testCases := []struct { - Name string - // Seed dermines if we start the test by restoring the initial backup we - // took, to test a restore operation based on the key existing or not - Seed bool - // Force is a pointer to differentiate between default false and given false - Force *bool - // The error we expect, if any - ExpectedErr error - - // RestoreName is used to restore the key to a differnt name - RestoreName string - }{ - { - // key does not already exist - Name: "Default restore", - }, - { - // key already exists - Name: "Restore-without-force", - Seed: true, - ExpectedErr: keyExitsError, - }, - { - // key already exists, use force to force a restore - Name: "Restore-with-force", - Seed: true, - Force: boolPtr(true), - }, - { - // using force shouldn't matter if the key doesn't exist - Name: "Restore-with-force-no-seed", - Force: boolPtr(true), - }, - { - // key already exists, restore to new name - Name: "Restore-new-name", - Seed: true, - RestoreName: "new-key", - }, - { - // key already exists, restore to bad path, should error - Name: "Restore-new-name-bad-path", - Seed: true, - RestoreName: "sub/path/new-key", - ExpectedErr: ErrInvalidKeyName, - }, - { - // using force shouldn't matter if the restore key name is different - Name: "Restore-with-force-seed-new-name", - Seed: true, - Force: boolPtr(true), - RestoreName: "other-key", - }, - { - // using force shouldn't matter if the restore key name is different - Name: "Restore-with-out-force-seed-new-name", - Seed: true, - Force: boolPtr(false), - RestoreName: "other-key", - }, - { - // using force shouldn't matter if the key doesn't exist - Name: "Restore-force-false", - Force: boolPtr(false), - }, - { - // using false force should still error - Name: "Restore-force-false", - Seed: true, - Force: boolPtr(false), - ExpectedErr: keyExitsError, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - var resp *logical.Response - var err error - if tc.Seed { - // restore our key to test a pre-existing key - seedRestoreReq := &logical.Request{ - Path: "restore", - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "backup": backupKey, - }, - } - - resp, err := b.HandleRequest(context.Background(), seedRestoreReq) - if resp != nil && resp.IsError() { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - if err != nil && tc.ExpectedErr == nil { - t.Fatalf("did not expect an error in SeedKey restore: %s", err) - } - } - - restorePath := "restore" - if tc.RestoreName != "" { - restorePath = fmt.Sprintf("%s/%s", restorePath, tc.RestoreName) - } - - restoreReq := &logical.Request{ - Path: restorePath, - Operation: logical.UpdateOperation, - Storage: s, - Data: map[string]interface{}{ - "backup": backupKey, - }, - } - - if tc.Force != nil { - restoreReq.Data["force"] = *tc.Force - } - - resp, err = b.HandleRequest(context.Background(), restoreReq) - if resp != nil && resp.IsError() { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - if err == nil && tc.ExpectedErr != nil { - t.Fatalf("expected an error, but got none") - } - if err != nil && tc.ExpectedErr == nil { - t.Fatalf("unexpected error:%s", err) - } - - if err != nil && tc.ExpectedErr != nil { - if err.Error() != tc.ExpectedErr.Error() { - t.Fatalf("expected error: (%s), got: (%s)", tc.ExpectedErr.Error(), err.Error()) - } - } - - readKeyName := keyName - if tc.RestoreName != "" { - readKeyName = tc.RestoreName - } - - // read the key and make sure it's there - readReq := &logical.Request{ - Path: "keys/" + readKeyName, - Operation: logical.ReadOperation, - Storage: s, - } - - resp, _ = b.HandleRequest(context.Background(), readReq) - if resp != nil && resp.IsError() { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - if tc.ExpectedErr == nil && resp == nil { - t.Fatal("expected to find a key, but got none") - } - - // cleanup / delete key after each run - keyReq.Operation = logical.DeleteOperation - resp, err = b.HandleRequest(context.Background(), keyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - - // cleanup / delete restore key after each run, if it was created - if tc.RestoreName != "" && tc.ExpectedErr == nil { - readReq.Operation = logical.DeleteOperation - resp, err = b.HandleRequest(context.Background(), readReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v\nerr: %v", resp, err) - } - } - }) - } -} diff --git a/builtin/logical/transit/path_rewrap_test.go b/builtin/logical/transit/path_rewrap_test.go deleted file mode 100644 index 55f288746..000000000 --- a/builtin/logical/transit/path_rewrap_test.go +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -// Check the normal flow of rewrap -func TestTransit_BatchRewrapCase1(t *testing.T) { - var resp *logical.Response - var err error - b, s := createBackendWithStorage(t) - - // Upsert the key and encrypt the data - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" - - encData := map[string]interface{}{ - "plaintext": plaintext, - } - - // Create a key and encrypt a plaintext - encReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: encData, - } - resp, err = b.HandleRequest(context.Background(), encReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Cache the ciphertext - ciphertext := resp.Data["ciphertext"] - if !strings.HasPrefix(ciphertext.(string), "vault:v1") { - t.Fatalf("bad: ciphertext version: expected: 'vault:v1', actual: %s", ciphertext) - } - - keyVersion := resp.Data["key_version"].(int) - if keyVersion != 1 { - t.Fatalf("unexpected key version; got: %d, expected: %d", keyVersion, 1) - } - - rewrapData := map[string]interface{}{ - "ciphertext": ciphertext, - } - - // Read the policy and check if the latest version is 1 - policyReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "keys/upserted_key", - Storage: s, - } - - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["latest_version"] != 1 { - t.Fatalf("bad: latest_version: expected: 1, actual: %d", resp.Data["latest_version"]) - } - - rotateReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/upserted_key/rotate", - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), rotateReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Read the policy again and the latest version is 2 - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["latest_version"] != 2 { - t.Fatalf("bad: latest_version: expected: 2, actual: %d", resp.Data["latest_version"]) - } - - // Rewrap the ciphertext and check that they are different - rewrapReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "rewrap/upserted_key", - Storage: s, - Data: rewrapData, - } - - resp, err = b.HandleRequest(context.Background(), rewrapReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if ciphertext.(string) == resp.Data["ciphertext"].(string) { - t.Fatalf("bad: ciphertexts are same before and after rewrap") - } - - if !strings.HasPrefix(resp.Data["ciphertext"].(string), "vault:v2") { - t.Fatalf("bad: ciphertext version: expected: 'vault:v2', actual: %s", resp.Data["ciphertext"].(string)) - } - - keyVersion = resp.Data["key_version"].(int) - if keyVersion != 2 { - t.Fatalf("unexpected key version; got: %d, expected: %d", keyVersion, 2) - } -} - -// Check the normal flow of rewrap with upserted key -func TestTransit_BatchRewrapCase2(t *testing.T) { - var resp *logical.Response - var err error - b, s := createBackendWithStorage(t) - - // Upsert the key and encrypt the data - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" - - encData := map[string]interface{}{ - "plaintext": plaintext, - "context": "dmlzaGFsCg==", - } - - // Create a key and encrypt a plaintext - encReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: encData, - } - resp, err = b.HandleRequest(context.Background(), encReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Cache the ciphertext - ciphertext := resp.Data["ciphertext"] - if !strings.HasPrefix(ciphertext.(string), "vault:v1") { - t.Fatalf("bad: ciphertext version: expected: 'vault:v1', actual: %s", ciphertext) - } - - keyVersion := resp.Data["key_version"].(int) - if keyVersion != 1 { - t.Fatalf("unexpected key version; got: %d, expected: %d", keyVersion, 1) - } - - rewrapData := map[string]interface{}{ - "ciphertext": ciphertext, - "context": "dmlzaGFsCg==", - } - - // Read the policy and check if the latest version is 1 - policyReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "keys/upserted_key", - Storage: s, - } - - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["latest_version"] != 1 { - t.Fatalf("bad: latest_version: expected: 1, actual: %d", resp.Data["latest_version"]) - } - - rotateReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/upserted_key/rotate", - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), rotateReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - // Read the policy again and the latest version is 2 - resp, err = b.HandleRequest(context.Background(), policyReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if resp.Data["latest_version"] != 2 { - t.Fatalf("bad: latest_version: expected: 2, actual: %d", resp.Data["latest_version"]) - } - - // Rewrap the ciphertext and check that they are different - rewrapReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "rewrap/upserted_key", - Storage: s, - Data: rewrapData, - } - - resp, err = b.HandleRequest(context.Background(), rewrapReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - if ciphertext.(string) == resp.Data["ciphertext"].(string) { - t.Fatalf("bad: ciphertexts are same before and after rewrap") - } - - if !strings.HasPrefix(resp.Data["ciphertext"].(string), "vault:v2") { - t.Fatalf("bad: ciphertext version: expected: 'vault:v2', actual: %s", resp.Data["ciphertext"].(string)) - } - - keyVersion = resp.Data["key_version"].(int) - if keyVersion != 2 { - t.Fatalf("unexpected key version; got: %d, expected: %d", keyVersion, 2) - } -} - -// Batch encrypt plaintexts, rotate the keys and rewrap all the ciphertexts -func TestTransit_BatchRewrapCase3(t *testing.T) { - var resp *logical.Response - var err error - - b, s := createBackendWithStorage(t) - - batchEncryptionInput := []interface{}{ - map[string]interface{}{"plaintext": "dmlzaGFsCg==", "reference": "ek"}, - map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "do"}, - } - batchEncryptionData := map[string]interface{}{ - "batch_input": batchEncryptionInput, - } - batchReq := &logical.Request{ - Operation: logical.CreateOperation, - Path: "encrypt/upserted_key", - Storage: s, - Data: batchEncryptionData, - } - resp, err = b.HandleRequest(context.Background(), batchReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchEncryptionResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) - - batchRewrapInput := make([]interface{}, len(batchEncryptionResponseItems)) - for i, item := range batchEncryptionResponseItems { - batchRewrapInput[i] = map[string]interface{}{"ciphertext": item.Ciphertext, "reference": item.Reference} - } - - batchRewrapData := map[string]interface{}{ - "batch_input": batchRewrapInput, - } - - rotateReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "keys/upserted_key/rotate", - Storage: s, - } - resp, err = b.HandleRequest(context.Background(), rotateReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - rewrapReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "rewrap/upserted_key", - Storage: s, - Data: batchRewrapData, - } - - resp, err = b.HandleRequest(context.Background(), rewrapReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - batchRewrapResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem) - - if len(batchRewrapResponseItems) != len(batchEncryptionResponseItems) { - t.Fatalf("bad: length of input and output or rewrap are not matching; expected: %d, actual: %d", len(batchEncryptionResponseItems), len(batchRewrapResponseItems)) - } - - decReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "decrypt/upserted_key", - Storage: s, - } - - for i, eItem := range batchEncryptionResponseItems { - rItem := batchRewrapResponseItems[i] - - inputRef := batchEncryptionInput[i].(map[string]interface{})["reference"] - if eItem.Reference != inputRef { - t.Fatalf("bad: reference mismatch. Expected %s, Actual: %s", inputRef, eItem.Reference) - } - - if eItem.Ciphertext == rItem.Ciphertext { - t.Fatalf("bad: rewrap input and output are the same") - } - - if !strings.HasPrefix(rItem.Ciphertext, "vault:v2") { - t.Fatalf("bad: invalid version of ciphertext in rewrap response; expected: 'vault:v2', actual: %s", rItem.Ciphertext) - } - - if rItem.KeyVersion != 2 { - t.Fatalf("unexpected key version; got: %d, expected: %d", rItem.KeyVersion, 2) - } - - decReq.Data = map[string]interface{}{ - "ciphertext": rItem.Ciphertext, - } - - resp, err = b.HandleRequest(context.Background(), decReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%v resp:%#v", err, resp) - } - - plaintext1 := "dGhlIHF1aWNrIGJyb3duIGZveA==" - plaintext2 := "dmlzaGFsCg==" - if resp.Data["plaintext"] != plaintext1 && resp.Data["plaintext"] != plaintext2 { - t.Fatalf("bad: plaintext. Expected: %q or %q, Actual: %q", plaintext1, plaintext2, resp.Data["plaintext"]) - } - - } -} diff --git a/builtin/logical/transit/path_sign_verify_test.go b/builtin/logical/transit/path_sign_verify_test.go deleted file mode 100644 index a7abf6be4..000000000 --- a/builtin/logical/transit/path_sign_verify_test.go +++ /dev/null @@ -1,981 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "encoding/base64" - "fmt" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/vault/helper/constants" - - "golang.org/x/crypto/ed25519" - - "github.com/hashicorp/vault/sdk/helper/keysutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/mapstructure" -) - -// The outcome of processing a request includes -// the possibility that the request is incomplete or incorrect, -// or that the request is well-formed but the signature (for verification) -// is invalid, or that the signature is valid, but the key is not. -type signOutcome struct { - requestOk bool - valid bool - keyValid bool - reference string -} - -func TestTransit_SignVerify_ECDSA(t *testing.T) { - t.Run("256", func(t *testing.T) { - testTransit_SignVerify_ECDSA(t, 256) - }) - t.Run("384", func(t *testing.T) { - testTransit_SignVerify_ECDSA(t, 384) - }) - t.Run("521", func(t *testing.T) { - testTransit_SignVerify_ECDSA(t, 521) - }) -} - -func testTransit_SignVerify_ECDSA(t *testing.T, bits int) { - b, storage := createBackendWithSysView(t) - - // First create a key - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - Data: map[string]interface{}{ - "type": fmt.Sprintf("ecdsa-p%d", bits), - }, - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Now, change the key value to something we control - p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ - Storage: storage, - Name: "foo", - }, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - - // Useful code to output a key for openssl verification - /* - if bits == 384 { - var curve elliptic.Curve - switch bits { - case 521: - curve = elliptic.P521() - case 384: - curve = elliptic.P384() - default: - curve = elliptic.P256() - } - key := p.Keys[strconv.Itoa(p.LatestVersion)] - keyBytes, _ := x509.MarshalECPrivateKey(&ecdsa.PrivateKey{ - PublicKey: ecdsa.PublicKey{ - Curve: curve, - X: key.EC_X, - Y: key.EC_Y, - }, - D: key.EC_D, - }) - pemBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: keyBytes, - } - pemBytes := pem.EncodeToMemory(pemBlock) - t.Fatalf("X: %s, Y: %s, D: %s, marshaled: %s", key.EC_X.Text(16), key.EC_Y.Text(16), key.EC_D.Text(16), string(pemBytes)) - } - */ - - var xString, yString, dString string - switch bits { - case 384: - xString = "703457a84e48bfcb037cfb509f1870d2aa5b74c109c2f24624ab21444492575229f8711453e5c656dab596b4e26db30e" - yString = "411c5b7092a893dc8b7af39de3d21d1c26f45b27616baeac4c479ef3c9f21c194b5ac501dee47ba2b2cb243a54256524" - dString = "3de3e4fd2ecbc490e956f41f5003a1e57a84763cec7b722fa3427cf461a1148ea4d5206023bcce0422289f6633730759" - /* - -----BEGIN EC PRIVATE KEY----- - MIGkAgEBBDA94+T9LsvEkOlW9B9QA6HleoR2POx7ci+jQnz0YaEUjqTVIGAjvM4E - IiifZjNzB1mgBwYFK4EEACKhZANiAARwNFeoTki/ywN8+1CfGHDSqlt0wQnC8kYk - qyFERJJXUin4cRRT5cZW2rWWtOJtsw5BHFtwkqiT3It6853j0h0cJvRbJ2FrrqxM - R57zyfIcGUtaxQHe5HuissskOlQlZSQ= - -----END EC PRIVATE KEY----- - */ - case 521: - xString = "1913f75fc044fe5d1f871c2629a377462fd819b174a41d3ec7d04ebd5ae35475ff8de544f4e19a9aa6b16a8f67af479be6884e00ca3147dc24d5924d66ac395e04b" - yString = "4919406b90d8323fdb5c9c4f48259c56ebcea37b40ad1a82bbbfad62a9b9c2dce515772274b84725471c7d0b7c62e10c23296b1a9d2b2586ada67735ff5d9fffc4" - dString = "1867d0fcd9bac4c5821b70a6b13117499438f8c274579c0aba254fbd85fa98892c3608576197d5534366a9aab0f904155bec46d800d23a57f7f053d91526568b09" - /* - -----BEGIN EC PRIVATE KEY----- - MIHcAgEBBEIAGGfQ/Nm6xMWCG3CmsTEXSZQ4+MJ0V5wKuiVPvYX6mIksNghXYZfV - U0Nmqaqw+QQVW+xG2ADSOlf38FPZFSZWiwmgBwYFK4EEACOhgYkDgYYABAGRP3X8 - BE/l0fhxwmKaN3Ri/YGbF0pB0+x9BOvVrjVHX/jeVE9OGamqaxao9nr0eb5ohOAM - oxR9wk1ZJNZqw5XgSwBJGUBrkNgyP9tcnE9IJZxW686je0CtGoK7v61iqbnC3OUV - dyJ0uEclRxx9C3xi4QwjKWsanSslhq2mdzX/XZ//xA== - -----END EC PRIVATE KEY----- - */ - default: - xString = "7336010a6da5935113d26d9ea4bb61b3b8d102c9a8083ed432f9b58fd7e80686" - yString = "4040aa31864691a8a9e7e3ec9250e85425b797ad7be34ba8df62bfbad45ebb0e" - dString = "99e5569be8683a2691dfc560ca9dfa71e887867a3af60635a08a3e3655aba3ef" - } - - keyEntry := p.Keys[strconv.Itoa(p.LatestVersion)] - _, ok := keyEntry.EC_X.SetString(xString, 16) - if !ok { - t.Fatal("could not set X") - } - _, ok = keyEntry.EC_Y.SetString(yString, 16) - if !ok { - t.Fatal("could not set Y") - } - _, ok = keyEntry.EC_D.SetString(dString, 16) - if !ok { - t.Fatal("could not set D") - } - p.Keys[strconv.Itoa(p.LatestVersion)] = keyEntry - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - req.Data = map[string]interface{}{ - "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", - } - - signRequest := func(req *logical.Request, errExpected bool, postpath string) string { - t.Helper() - req.Path = "sign/foo" + postpath - resp, err := b.HandleRequest(context.Background(), req) - if err != nil && !errExpected { - t.Fatalf("request: %v\nerror: %v", req, err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if errExpected { - if !resp.IsError() { - t.Fatalf("bad: should have gotten error response: %#v", *resp) - } - return "" - } - if resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - value, ok := resp.Data["signature"] - if !ok { - t.Fatalf("no signature key found in returned data, got resp data %#v", resp.Data) - } - return value.(string) - } - - verifyRequest := func(req *logical.Request, errExpected bool, postpath, sig string) { - t.Helper() - req.Path = "verify/foo" + postpath - req.Data["signature"] = sig - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - if errExpected { - return - } - t.Fatalf("got error: %v, sig was %v", err, sig) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - if errExpected { - return - } - t.Fatalf("bad: got error response: %#v", *resp) - } - value, ok := resp.Data["valid"] - if !ok { - t.Fatalf("no valid key found in returned data, got resp data %#v", resp.Data) - } - if !value.(bool) && !errExpected { - t.Fatalf("verification failed; req was %#v, resp is %#v", *req, *resp) - } else if value.(bool) && errExpected { - t.Fatalf("expected error and didn't get one; req was %#v, resp is %#v", *req, *resp) - } - } - - // Comparisons are against values generated via openssl - - // Test defaults -- sha2-256 - sig := signRequest(req, false, "") - verifyRequest(req, false, "", sig) - - // Test a bad signature - verifyRequest(req, true, "", sig[0:len(sig)-2]) - - // Test a signature generated with the same key by openssl - switch bits { - case 384: - sig = `vault:v1:MGUCMHHZLRN/3ehWuWACfSCMLtFtNEAdx6Rkwon2Lx6FWCyXCXqH6A8Pz8er0Qkgvm2ElQIxAO922LmUeYzHmDSfC5is/TjFu3b4Fb+1XtoBXncc2u4t2vSuTAxEv7WMh2D2YDdxeA==` - case 521: - sig = `vault:v1:MIGIAkIBYhspOgSs/K/NUWtlBN+CfYe1IVFpUbQNSqdjT7s+QKcr6GKmdGLIQAXw0q6K0elBgzi1wgLjxwdscwMeW7tm/QQCQgDzdITGlUEd9Z7DOfLCnDP4X8pGsfO60Tvsh/BN44drZsHLtXYBXLczB/XZfIWAsPMuI5F7ExwVNbmQP0FBVri/QQ==` - default: - sig = `vault:v1:MEUCIAgnEl9V8P305EBAlz68Nq4jZng5fE8k6MactcnlUw9dAiEAvJVePg3dazW6MaW7lRAVtEz82QJDVmR98tXCl8Pc7DA=` - } - verifyRequest(req, false, "", sig) - - // Test algorithm selection in the path - sig = signRequest(req, false, "/sha2-224") - verifyRequest(req, false, "/sha2-224", sig) - - // Reset and test algorithm selection in the data - req.Data["hash_algorithm"] = "sha2-224" - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - - req.Data["hash_algorithm"] = "sha2-384" - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - - req.Data["hash_algorithm"] = "sha2-512" - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - - req.Data["hash_algorithm"] = "sha3-224" - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - - req.Data["hash_algorithm"] = "sha3-256" - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - - req.Data["hash_algorithm"] = "sha3-384" - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - - req.Data["hash_algorithm"] = "sha3-512" - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - - req.Data["prehashed"] = true - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - delete(req.Data, "prehashed") - - // Test marshaling selection - // Bad value - req.Data["marshaling_algorithm"] = "asn2" - sig = signRequest(req, true, "") - // Use the default, verify we can't validate with jws - req.Data["marshaling_algorithm"] = "asn1" - sig = signRequest(req, false, "") - req.Data["marshaling_algorithm"] = "jws" - verifyRequest(req, true, "", sig) - // Sign with jws, verify we can validate - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - // If we change marshaling back to asn1 we shouldn't be able to verify - delete(req.Data, "marshaling_algorithm") - verifyRequest(req, true, "", sig) - - // Test 512 and save sig for later to ensure we can't validate once min - // decryption version is set - req.Data["hash_algorithm"] = "sha2-512" - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - - v1sig := sig - - // Test bad algorithm - req.Data["hash_algorithm"] = "foobar" - signRequest(req, true, "") - - // Test bad input - req.Data["hash_algorithm"] = "sha2-256" - req.Data["input"] = "foobar" - signRequest(req, true, "") - - // Rotate and set min decryption version - err = p.Rotate(context.Background(), storage, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - err = p.Rotate(context.Background(), storage, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - - p.MinDecryptionVersion = 2 - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - req.Data["input"] = "dGhlIHF1aWNrIGJyb3duIGZveA==" - req.Data["hash_algorithm"] = "sha2-256" - // Make sure signing still works fine - sig = signRequest(req, false, "") - verifyRequest(req, false, "", sig) - // Now try the v1 - verifyRequest(req, true, "", v1sig) -} - -func validatePublicKey(t *testing.T, in string, sig string, pubKeyRaw []byte, expectValid bool, postpath string, b *backend) { - t.Helper() - input, _ := base64.StdEncoding.DecodeString(in) - splitSig := strings.Split(sig, ":") - signature, _ := base64.StdEncoding.DecodeString(splitSig[2]) - valid := ed25519.Verify(ed25519.PublicKey(pubKeyRaw), input, signature) - if valid != expectValid { - t.Fatalf("status of signature: expected %v. Got %v", valid, expectValid) - } - if !valid { - return - } - - keyReadReq := &logical.Request{ - Operation: logical.ReadOperation, - Path: "keys/" + postpath, - } - keyReadResp, err := b.HandleRequest(context.Background(), keyReadReq) - if err != nil { - t.Fatal(err) - } - val := keyReadResp.Data["keys"].(map[string]map[string]interface{})[strings.TrimPrefix(splitSig[1], "v")] - var ak asymKey - if err := mapstructure.Decode(val, &ak); err != nil { - t.Fatal(err) - } - if ak.PublicKey != "" { - t.Fatal("got non-empty public key") - } - keyReadReq.Data = map[string]interface{}{ - "context": "abcd", - } - keyReadResp, err = b.HandleRequest(context.Background(), keyReadReq) - if err != nil { - t.Fatal(err) - } - val = keyReadResp.Data["keys"].(map[string]map[string]interface{})[strings.TrimPrefix(splitSig[1], "v")] - if err := mapstructure.Decode(val, &ak); err != nil { - t.Fatal(err) - } - if ak.PublicKey != base64.StdEncoding.EncodeToString(pubKeyRaw) { - t.Fatalf("got incorrect public key; got %q, expected %q\nasymKey struct is\n%#v", ak.PublicKey, pubKeyRaw, ak) - } -} - -func TestTransit_SignVerify_ED25519(t *testing.T) { - b, storage := createBackendWithSysView(t) - - // First create a key - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - Data: map[string]interface{}{ - "type": "ed25519", - }, - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Now create a derived key" - req = &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/bar", - Data: map[string]interface{}{ - "type": "ed25519", - "derived": true, - }, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Get the keys for later - fooP, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ - Storage: storage, - Name: "foo", - }, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - - barP, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ - Storage: storage, - Name: "bar", - }, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - - signRequest := func(req *logical.Request, errExpected bool, postpath string) []string { - t.Helper() - // Delete any key that exists in the request - delete(req.Data, "public_key") - req.Path = "sign/" + postpath - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - if !errExpected { - t.Fatal(err) - } - return nil - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if errExpected { - if resp.IsError() { - return nil - } - t.Fatalf("bad: expected error response, got: %#v", *resp) - } - if resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - // memoize any pubic key - if key, ok := resp.Data["public_key"]; ok { - req.Data["public_key"] = key - } - // batch_input supplied - if _, ok := req.Data["batch_input"]; ok { - batchRequestItems := req.Data["batch_input"].([]batchRequestSignItem) - - batchResults, ok := resp.Data["batch_results"] - if !ok { - t.Fatalf("no batch_results in returned data, got resp data %#v", resp.Data) - } - batchResponseItems := batchResults.([]batchResponseSignItem) - if len(batchResponseItems) != len(batchRequestItems) { - t.Fatalf("Expected %d items in response. Got %d: %#v", len(batchRequestItems), len(batchResponseItems), resp) - } - if len(batchRequestItems) == 0 { - return nil - } - ret := make([]string, len(batchRequestItems)) - for i, v := range batchResponseItems { - ret[i] = v.Signature - } - return ret - } - - // input supplied - value, ok := resp.Data["signature"] - if !ok { - t.Fatalf("no signature key found in returned data, got resp data %#v", resp.Data) - } - return []string{value.(string)} - } - - verifyRequest := func(req *logical.Request, errExpected bool, outcome []signOutcome, postpath string, sig []string, attachSig bool) { - t.Helper() - req.Path = "verify/" + postpath - if _, ok := req.Data["batch_input"]; ok && attachSig { - batchRequestItems := req.Data["batch_input"].([]batchRequestSignItem) - if len(batchRequestItems) != len(sig) { - t.Fatalf("number of requests in batch(%d) != number of signatures(%d)", len(batchRequestItems), len(sig)) - } - for i, v := range sig { - batchRequestItems[i]["signature"] = v - batchRequestItems[i]["reference"] = outcome[i].reference - } - } else if attachSig { - req.Data["signature"] = sig[0] - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil && !errExpected { - t.Fatalf("got error: %v, sig was %v", err, sig) - } - if errExpected { - if resp != nil && !resp.IsError() { - t.Fatalf("bad: expected error response, got: %#v\n%#v", *resp, req) - } - return - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - - // batch_input field supplied - if _, ok := req.Data["batch_input"]; ok { - batchRequestItems := req.Data["batch_input"].([]batchRequestSignItem) - - batchResults, ok := resp.Data["batch_results"] - if !ok { - t.Fatalf("no batch_results in returned data, got resp data %#v", resp.Data) - } - batchResponseItems := batchResults.([]batchResponseVerifyItem) - if len(batchResponseItems) != len(batchRequestItems) { - t.Fatalf("Expected %d items in response. Got %d: %#v", len(batchRequestItems), len(batchResponseItems), resp) - } - if len(batchRequestItems) == 0 { - return - } - for i, v := range batchResponseItems { - if v.Error != "" && outcome[i].requestOk { - t.Fatalf("verification failed; req was %#v, resp is %#v", *req, *resp) - } - if v.Error != "" { - continue - } - if v.Valid != outcome[i].valid { - t.Fatalf("verification failed; req was %#v, resp is %#v", *req, *resp) - } - if !v.Valid { - continue - } - if pubKeyRaw, ok := req.Data["public_key"]; ok { - validatePublicKey(t, batchRequestItems[i]["input"], sig[i], pubKeyRaw.([]byte), outcome[i].keyValid, postpath, b) - } - if v.Reference != outcome[i].reference { - t.Fatalf("verification failed, mismatched references %s vs %s", v.Reference, outcome[i].reference) - } - } - return - } - - // input field supplied - value, ok := resp.Data["valid"] - if !ok { - t.Fatalf("no valid key found in returned data, got resp data %#v", resp.Data) - } - valid := value.(bool) - if valid != outcome[0].valid { - t.Fatalf("verification failed; req was %#v, resp is %#v", *req, *resp) - } - if !valid { - return - } - - if pubKeyRaw, ok := req.Data["public_key"]; ok { - validatePublicKey(t, req.Data["input"].(string), sig[0], pubKeyRaw.([]byte), outcome[0].keyValid, postpath, b) - } - } - - req.Data = map[string]interface{}{ - "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", - "context": "abcd", - } - - outcome := []signOutcome{{requestOk: true, valid: true, keyValid: true}} - // Test defaults - sig := signRequest(req, false, "foo") - verifyRequest(req, false, outcome, "foo", sig, true) - - sig = signRequest(req, false, "bar") - verifyRequest(req, false, outcome, "bar", sig, true) - - // Verify with incorrect key - outcome[0].valid = false - verifyRequest(req, false, outcome, "foo", sig, true) - - // Verify with missing signatures - delete(req.Data, "signature") - verifyRequest(req, true, outcome, "foo", sig, false) - - // Test a bad signature - badsig := sig[0] - badsig = badsig[:len(badsig)-2] - verifyRequest(req, true, outcome, "bar", []string{badsig}, true) - - v1sig := sig - - // Test a missing context - delete(req.Data, "context") - sig = signRequest(req, true, "bar") - - // Rotate and set min decryption version - err = fooP.Rotate(context.Background(), storage, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - err = fooP.Rotate(context.Background(), storage, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - fooP.MinDecryptionVersion = 2 - if err = fooP.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - err = barP.Rotate(context.Background(), storage, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - err = barP.Rotate(context.Background(), storage, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - barP.MinDecryptionVersion = 2 - if err = barP.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - req.Data = map[string]interface{}{ - "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", - "context": "abcd", - } - - // Make sure signing still works fine - sig = signRequest(req, false, "foo") - outcome[0].valid = true - verifyRequest(req, false, outcome, "foo", sig, true) - // Now try the v1 - verifyRequest(req, true, outcome, "foo", v1sig, true) - - // Repeat with the other key - sig = signRequest(req, false, "bar") - verifyRequest(req, false, outcome, "bar", sig, true) - verifyRequest(req, true, outcome, "bar", v1sig, true) - - // Test Batch Signing - batchInput := []batchRequestSignItem{ - {"context": "abcd", "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "uno"}, - {"context": "efgh", "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "dos"}, - } - - req.Data = map[string]interface{}{ - "batch_input": batchInput, - } - - outcome = []signOutcome{ - {requestOk: true, valid: true, keyValid: true, reference: "uno"}, - {requestOk: true, valid: true, keyValid: true, reference: "dos"}, - } - - sig = signRequest(req, false, "foo") - verifyRequest(req, false, outcome, "foo", sig, true) - - goodsig := signRequest(req, false, "bar") - verifyRequest(req, false, outcome, "bar", goodsig, true) - - // key doesn't match signatures - outcome[0].valid = false - outcome[1].valid = false - verifyRequest(req, false, outcome, "foo", goodsig, true) - - // Test a bad signature - badsig = sig[0] - badsig = badsig[:len(badsig)-2] - // matching key, but first signature is corrupted - outcome[0].requestOk = false - outcome[1].valid = true - verifyRequest(req, false, outcome, "bar", []string{badsig, goodsig[1]}, true) - - // Verify with missing signatures - outcome[0].valid = false - outcome[1].valid = false - delete(batchInput[0], "signature") - delete(batchInput[1], "signature") - verifyRequest(req, true, outcome, "foo", sig, false) - - // Test missing context - batchInput = []batchRequestSignItem{ - {"context": "abcd", "input": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - {"input": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - } - - req.Data = map[string]interface{}{ - "batch_input": batchInput, - } - - sig = signRequest(req, false, "bar") - - outcome[0].requestOk = true - outcome[0].valid = true - outcome[1].requestOk = false - verifyRequest(req, false, outcome, "bar", goodsig, true) - - // Test incorrect context - batchInput = []batchRequestSignItem{ - {"context": "abca", "input": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - {"context": "efga", "input": "dGhlIHF1aWNrIGJyb3duIGZveA=="}, - } - req.Data = map[string]interface{}{ - "batch_input": batchInput, - } - - outcome[0].requestOk = true - outcome[0].valid = false - outcome[1].requestOk = true - outcome[1].valid = false - verifyRequest(req, false, outcome, "bar", goodsig, true) -} - -func TestTransit_SignVerify_RSA_PSS(t *testing.T) { - t.Run("2048", func(t *testing.T) { - testTransit_SignVerify_RSA_PSS(t, 2048) - }) - t.Run("3072", func(t *testing.T) { - testTransit_SignVerify_RSA_PSS(t, 3072) - }) - t.Run("4096", func(t *testing.T) { - testTransit_SignVerify_RSA_PSS(t, 4096) - }) -} - -func testTransit_SignVerify_RSA_PSS(t *testing.T, bits int) { - b, storage := createBackendWithSysView(t) - - // First create a key - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - Data: map[string]interface{}{ - "type": fmt.Sprintf("rsa-%d", bits), - }, - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - signRequest := func(errExpected bool, postpath string) string { - t.Helper() - req.Path = "sign/foo" + postpath - resp, err := b.HandleRequest(context.Background(), req) - if err != nil && !errExpected { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if errExpected { - if !resp.IsError() { - t.Fatalf("bad: should have gotten error response: %#v", *resp) - } - return "" - } - if resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - // Since we are reusing the same request, let's clear the salt length each time. - delete(req.Data, "salt_length") - - value, ok := resp.Data["signature"] - if !ok { - t.Fatalf("no signature key found in returned data, got resp data %#v", resp.Data) - } - return value.(string) - } - - verifyRequest := func(errExpected bool, postpath, sig string) { - t.Helper() - req.Path = "verify/foo" + postpath - req.Data["signature"] = sig - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - if errExpected { - return - } - t.Fatalf("got error: %v, sig was %v", err, sig) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.IsError() { - if errExpected { - return - } - t.Fatalf("bad: got error response: %#v", *resp) - } - value, ok := resp.Data["valid"] - if !ok { - t.Fatalf("no valid key found in returned data, got resp data %#v", resp.Data) - } - if !value.(bool) && !errExpected { - t.Fatalf("verification failed; req was %#v, resp is %#v", *req, *resp) - } else if value.(bool) && errExpected { - t.Fatalf("expected error and didn't get one; req was %#v, resp is %#v", *req, *resp) - } - // Since we are reusing the same request, let's clear the signature each time. - delete(req.Data, "signature") - } - - newReqData := func(hashAlgorithm string, marshalingName string) map[string]interface{} { - return map[string]interface{}{ - "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", - "signature_algorithm": "pss", - "hash_algorithm": hashAlgorithm, - "marshaling_algorithm": marshalingName, - } - } - - signAndVerifyRequest := func(hashAlgorithm string, marshalingName string, signSaltLength string, signErrExpected bool, verifySaltLength string, verifyErrExpected bool) { - t.Log("\t\t\t", signSaltLength, "/", verifySaltLength) - req.Data = newReqData(hashAlgorithm, marshalingName) - - req.Data["salt_length"] = signSaltLength - t.Log("\t\t\t\t", "sign req data:", req.Data) - sig := signRequest(signErrExpected, "") - - req.Data["salt_length"] = verifySaltLength - t.Log("\t\t\t\t", "verify req data:", req.Data) - verifyRequest(verifyErrExpected, "", sig) - } - - invalidSaltLengths := []string{"bar", "-2"} - t.Log("invalidSaltLengths:", invalidSaltLengths) - - autoSaltLengths := []string{"auto", "0"} - t.Log("autoSaltLengths:", autoSaltLengths) - - hashSaltLengths := []string{"hash", "-1"} - t.Log("hashSaltLengths:", hashSaltLengths) - - positiveSaltLengths := []string{"1"} - t.Log("positiveSaltLengths:", positiveSaltLengths) - - nonAutoSaltLengths := append(hashSaltLengths, positiveSaltLengths...) - t.Log("nonAutoSaltLengths:", nonAutoSaltLengths) - - validSaltLengths := append(autoSaltLengths, nonAutoSaltLengths...) - t.Log("validSaltLengths:", validSaltLengths) - - testCombinatorics := func(t *testing.T, hashAlgorithm string, marshalingName string) { - t.Log("\t\t", "valid", "/", "invalid salt lengths") - for _, validSaltLength := range validSaltLengths { - for _, invalidSaltLength := range invalidSaltLengths { - signAndVerifyRequest(hashAlgorithm, marshalingName, validSaltLength, false, invalidSaltLength, true) - } - } - - t.Log("\t\t", "invalid", "/", "invalid salt lengths") - for _, invalidSaltLength1 := range invalidSaltLengths { - for _, invalidSaltLength2 := range invalidSaltLengths { - signAndVerifyRequest(hashAlgorithm, marshalingName, invalidSaltLength1, true, invalidSaltLength2, true) - } - } - - t.Log("\t\t", "invalid", "/", "valid salt lengths") - for _, invalidSaltLength := range invalidSaltLengths { - for _, validSaltLength := range validSaltLengths { - signAndVerifyRequest(hashAlgorithm, marshalingName, invalidSaltLength, true, validSaltLength, true) - } - } - - t.Log("\t\t", "valid", "/", "valid salt lengths") - for _, validSaltLength := range validSaltLengths { - signAndVerifyRequest(hashAlgorithm, marshalingName, validSaltLength, false, validSaltLength, false) - } - - t.Log("\t\t", "hash", "/", "hash salt lengths") - for _, hashSaltLength1 := range hashSaltLengths { - for _, hashSaltLength2 := range hashSaltLengths { - if hashSaltLength1 != hashSaltLength2 { - signAndVerifyRequest(hashAlgorithm, marshalingName, hashSaltLength1, false, hashSaltLength2, false) - } - } - } - - t.Log("\t\t", "hash", "/", "positive salt lengths") - for _, hashSaltLength := range hashSaltLengths { - for _, positiveSaltLength := range positiveSaltLengths { - signAndVerifyRequest(hashAlgorithm, marshalingName, hashSaltLength, false, positiveSaltLength, true) - } - } - - t.Log("\t\t", "positive", "/", "hash salt lengths") - for _, positiveSaltLength := range positiveSaltLengths { - for _, hashSaltLength := range hashSaltLengths { - signAndVerifyRequest(hashAlgorithm, marshalingName, positiveSaltLength, false, hashSaltLength, true) - } - } - - t.Log("\t\t", "auto", "/", "auto salt lengths") - for _, autoSaltLength1 := range autoSaltLengths { - for _, autoSaltLength2 := range autoSaltLengths { - if autoSaltLength1 != autoSaltLength2 { - signAndVerifyRequest(hashAlgorithm, marshalingName, autoSaltLength1, false, autoSaltLength2, false) - } - } - } - - t.Log("\t\t", "auto", "/", "non-auto salt lengths") - for _, autoSaltLength := range autoSaltLengths { - for _, nonAutoSaltLength := range nonAutoSaltLengths { - signAndVerifyRequest(hashAlgorithm, marshalingName, autoSaltLength, false, nonAutoSaltLength, true) - } - } - - t.Log("\t\t", "non-auto", "/", "auto salt lengths") - for _, nonAutoSaltLength := range nonAutoSaltLengths { - for _, autoSaltLength := range autoSaltLengths { - signAndVerifyRequest(hashAlgorithm, marshalingName, nonAutoSaltLength, false, autoSaltLength, false) - } - } - } - - testAutoSignAndVerify := func(t *testing.T, hashAlgorithm string, marshalingName string) { - t.Log("\t\t", "Make a signature with an implicit, automatic salt length") - req.Data = newReqData(hashAlgorithm, marshalingName) - t.Log("\t\t\t", "sign req data:", req.Data) - sig := signRequest(false, "") - - t.Log("\t\t", "Verify it with an implicit, automatic salt length") - t.Log("\t\t\t", "verify req data:", req.Data) - verifyRequest(false, "", sig) - - t.Log("\t\t", "Verify it with an explicit, automatic salt length") - for _, autoSaltLength := range autoSaltLengths { - t.Log("\t\t\t", "auto", "/", autoSaltLength) - req.Data["salt_length"] = autoSaltLength - t.Log("\t\t\t\t", "verify req data:", req.Data) - verifyRequest(false, "", sig) - } - - t.Log("\t\t", "Try to verify it with an explicit, incorrect salt length") - for _, nonAutoSaltLength := range nonAutoSaltLengths { - t.Log("\t\t\t", "auto", "/", nonAutoSaltLength) - req.Data["salt_length"] = nonAutoSaltLength - t.Log("\t\t\t\t", "verify req data:", req.Data) - verifyRequest(true, "", sig) - } - - t.Log("\t\t", "Make a signature with an explicit, valid salt length & and verify it with an implicit, automatic salt length") - for _, validSaltLength := range validSaltLengths { - t.Log("\t\t\t", validSaltLength, "/", "auto") - - req.Data = newReqData(hashAlgorithm, marshalingName) - req.Data["salt_length"] = validSaltLength - t.Log("\t\t\t", "sign req data:", req.Data) - sig := signRequest(false, "") - - t.Log("\t\t\t", "verify req data:", req.Data) - verifyRequest(false, "", sig) - } - } - - for hashAlgorithm := range keysutil.HashTypeMap { - t.Log("Hash algorithm:", hashAlgorithm) - if hashAlgorithm == "none" { - continue - } - - for marshalingName := range keysutil.MarshalingTypeMap { - t.Log("\t", "Marshaling type:", marshalingName) - testName := fmt.Sprintf("%s-%s", hashAlgorithm, marshalingName) - t.Run(testName, func(t *testing.T) { - if constants.IsFIPS() && strings.HasPrefix(hashAlgorithm, "sha3-") { - t.Skip("\t", "Skipping hashing algo on fips:", hashAlgorithm) - } - - testCombinatorics(t, hashAlgorithm, marshalingName) - testAutoSignAndVerify(t, hashAlgorithm, marshalingName) - }) - } - } -} diff --git a/builtin/logical/transit/path_trim_test.go b/builtin/logical/transit/path_trim_test.go deleted file mode 100644 index 448d0fba3..000000000 --- a/builtin/logical/transit/path_trim_test.go +++ /dev/null @@ -1,273 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "testing" - - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/sdk/helper/keysutil" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTransit_Trim(t *testing.T) { - b, storage := createBackendWithSysView(t) - - doReq := func(t *testing.T, req *logical.Request) *logical.Response { - t.Helper() - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("got err:\n%#v\nresp:\n%#v\n", err, resp) - } - return resp - } - doErrReq := func(t *testing.T, req *logical.Request) { - t.Helper() - resp, err := b.HandleRequest(namespace.RootContext(nil), req) - if err == nil && (resp == nil || !resp.IsError()) { - t.Fatalf("expected error; resp:\n%#v\n", resp) - } - } - - // Create a key - req := &logical.Request{ - Path: "keys/aes", - Storage: storage, - Operation: logical.UpdateOperation, - } - doReq(t, req) - - // Get the policy and check that the archive has correct number of keys - p, _, err := b.GetPolicy(namespace.RootContext(nil), keysutil.PolicyRequest{ - Storage: storage, - Name: "aes", - }, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - - // Archive: 0, 1 - archive, err := p.LoadArchive(namespace.RootContext(nil), storage) - if err != nil { - t.Fatal(err) - } - // Index "0" in the archive is unused. Hence the length of the archived - // keys will always be 1 more than the actual number of keys. - if len(archive.Keys) != 2 { - t.Fatalf("bad: len of archived keys; expected: 2, actual: %d", len(archive.Keys)) - } - - // Ensure that there are 5 key versions, by rotating the key 4 times - for i := 0; i < 4; i++ { - req.Path = "keys/aes/rotate" - req.Data = nil - doReq(t, req) - } - - // Archive: 0, 1, 2, 3, 4, 5 - archive, err = p.LoadArchive(namespace.RootContext(nil), storage) - if err != nil { - t.Fatal(err) - } - if len(archive.Keys) != 6 { - t.Fatalf("bad: len of archived keys; expected: 6, actual: %d", len(archive.Keys)) - } - - // Min available version should not be set when min_encryption_version is not - // set - req.Path = "keys/aes/trim" - req.Data = map[string]interface{}{ - "min_available_version": 1, - } - doErrReq(t, req) - - // Set min_encryption_version to 0 - req.Path = "keys/aes/config" - req.Data = map[string]interface{}{ - "min_encryption_version": 0, - } - doReq(t, req) - - // Min available version should not be converted to 0 for nil values - req.Path = "keys/aes/trim" - req.Data = map[string]interface{}{ - "min_available_version": nil, - } - doErrReq(t, req) - - // Set min_encryption_version to 4 - req.Path = "keys/aes/config" - req.Data = map[string]interface{}{ - "min_encryption_version": 4, - } - doReq(t, req) - - // Set min_decryption_version to 3 - req.Data = map[string]interface{}{ - "min_decryption_version": 3, - } - doReq(t, req) - - // Min available version cannot be greater than min encryption version - req.Path = "keys/aes/trim" - req.Data = map[string]interface{}{ - "min_available_version": 5, - } - doErrReq(t, req) - - // Min available version cannot be greater than min decryption version - req.Data["min_available_version"] = 4 - doErrReq(t, req) - - // Min available version cannot be negative - req.Data["min_available_version"] = -1 - doErrReq(t, req) - - // Min available version should be positive - req.Data["min_available_version"] = 0 - doErrReq(t, req) - - // Trim all keys before version 3. Index 0 and index 1 will be deleted from - // archived keys. - req.Data["min_available_version"] = 3 - doReq(t, req) - - // Archive: 3, 4, 5 - archive, err = p.LoadArchive(namespace.RootContext(nil), storage) - if err != nil { - t.Fatal(err) - } - if len(archive.Keys) != 3 { - t.Fatalf("bad: len of archived keys; expected: 3, actual: %d", len(archive.Keys)) - } - - // Min decryption version should not be less than min available version - req.Path = "keys/aes/config" - req.Data = map[string]interface{}{ - "min_decryption_version": 1, - } - doErrReq(t, req) - - // Min encryption version should not be less than min available version - req.Data = map[string]interface{}{ - "min_encryption_version": 2, - } - doErrReq(t, req) - - // Rotate 5 more times - for i := 0; i < 5; i++ { - doReq(t, &logical.Request{ - Path: "keys/aes/rotate", - Storage: storage, - Operation: logical.UpdateOperation, - }) - } - - // Archive: 3, 4, 5, 6, 7, 8, 9, 10 - archive, err = p.LoadArchive(namespace.RootContext(nil), storage) - if err != nil { - t.Fatal(err) - } - if len(archive.Keys) != 8 { - t.Fatalf("bad: len of archived keys; expected: 8, actual: %d", len(archive.Keys)) - } - - // Set min encryption version to 7 - req.Data = map[string]interface{}{ - "min_encryption_version": 7, - } - doReq(t, req) - - // Set min decryption version to 7 - req.Data = map[string]interface{}{ - "min_decryption_version": 7, - } - doReq(t, req) - - // Trim all versions before 7 - req.Path = "keys/aes/trim" - req.Data = map[string]interface{}{ - "min_available_version": 7, - } - doReq(t, req) - - // Archive: 7, 8, 9, 10 - archive, err = p.LoadArchive(namespace.RootContext(nil), storage) - if err != nil { - t.Fatal(err) - } - if len(archive.Keys) != 4 { - t.Fatalf("bad: len of archived keys; expected: 4, actual: %d", len(archive.Keys)) - } - - // Read the key - req.Path = "keys/aes" - req.Operation = logical.ReadOperation - resp := doReq(t, req) - keys := resp.Data["keys"].(map[string]int64) - if len(keys) != 4 { - t.Fatalf("bad: number of keys; expected: 4, actual: %d", len(keys)) - } - - // Test if moving the min_encryption_version and min_decryption_versions - // are working fine - - // Set min encryption version to 10 - req.Path = "keys/aes/config" - req.Operation = logical.UpdateOperation - req.Data = map[string]interface{}{ - "min_encryption_version": 10, - } - doReq(t, req) - if p.MinEncryptionVersion != 10 { - t.Fatalf("failed to set min encryption version") - } - - // Set min decryption version to 9 - req.Data = map[string]interface{}{ - "min_decryption_version": 9, - } - doReq(t, req) - if p.MinDecryptionVersion != 9 { - t.Fatalf("failed to set min encryption version") - } - - // Reduce the min decryption version to 8 - req.Data = map[string]interface{}{ - "min_decryption_version": 8, - } - doReq(t, req) - if p.MinDecryptionVersion != 8 { - t.Fatalf("failed to set min encryption version") - } - - // Reduce the min encryption version to 8 - req.Data = map[string]interface{}{ - "min_encryption_version": 8, - } - doReq(t, req) - if p.MinDecryptionVersion != 8 { - t.Fatalf("failed to set min decryption version") - } - - // Read the key to ensure that the keys are properly copied from the - // archive into the policy - req.Path = "keys/aes" - req.Operation = logical.ReadOperation - resp = doReq(t, req) - keys = resp.Data["keys"].(map[string]int64) - if len(keys) != 3 { - t.Fatalf("bad: number of keys; expected: 3, actual: %d", len(keys)) - } - - // Ensure that archive has remained unchanged - // Archive: 7, 8, 9, 10 - archive, err = p.LoadArchive(namespace.RootContext(nil), storage) - if err != nil { - t.Fatal(err) - } - if len(archive.Keys) != 4 { - t.Fatalf("bad: len of archived keys; expected: 4, actual: %d", len(archive.Keys)) - } -} diff --git a/builtin/logical/transit/path_wrapping_key_test.go b/builtin/logical/transit/path_wrapping_key_test.go deleted file mode 100644 index 9ed58e45c..000000000 --- a/builtin/logical/transit/path_wrapping_key_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "context" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -const ( - storagePath = "policy/import/" + WrappingKeyName -) - -func TestTransit_WrappingKey(t *testing.T) { - // Set up shared backend for subtests - b, s := createBackendWithStorage(t) - - // Ensure the key does not exist before requesting it. - keyEntry, err := s.Get(context.Background(), storagePath) - if err != nil { - t.Fatalf("error retrieving wrapping key from storage: %s", err) - } - if keyEntry != nil { - t.Fatal("wrapping key unexpectedly exists") - } - - // Generate the key pair by requesting the public key. - req := &logical.Request{ - Storage: s, - Operation: logical.ReadOperation, - Path: "wrapping_key", - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("unexpected request error: %s", err) - } - if resp == nil || resp.Data == nil || resp.Data["public_key"] == nil { - t.Fatal("expected non-nil response") - } - pubKeyPEM := resp.Data["public_key"] - - // Ensure the returned key is a 4096-bit RSA key. - pubKeyBlock, _ := pem.Decode([]byte(pubKeyPEM.(string))) - rawPubKey, err := x509.ParsePKIXPublicKey(pubKeyBlock.Bytes) - if err != nil { - t.Fatalf("failed to parse public wrapping key: %s", err) - } - wrappingKey, ok := rawPubKey.(*rsa.PublicKey) - if !ok || wrappingKey.Size() != 512 { - t.Fatal("public wrapping key is not a 4096-bit RSA key") - } - - // Request the wrapping key again to ensure it isn't regenerated. - req = &logical.Request{ - Storage: s, - Operation: logical.ReadOperation, - Path: "wrapping_key", - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("unexpected request error: %s", err) - } - if resp == nil || resp.Data == nil || resp.Data["public_key"] == nil { - t.Fatal("expected non-nil response") - } - if resp.Data["public_key"] != pubKeyPEM { - t.Fatal("wrapping key public component changed between requests") - } -} diff --git a/builtin/logical/transit/stepwise_test.go b/builtin/logical/transit/stepwise_test.go deleted file mode 100644 index 77cf093b9..000000000 --- a/builtin/logical/transit/stepwise_test.go +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package transit - -import ( - "encoding/base64" - "fmt" - "os" - "testing" - - stepwise "github.com/hashicorp/vault-testing-stepwise" - dockerEnvironment "github.com/hashicorp/vault-testing-stepwise/environments/docker" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/helper/keysutil" - "github.com/mitchellh/mapstructure" -) - -// TestBackend_basic_docker is an example test using the Docker Environment -func TestAccBackend_basic_docker(t *testing.T) { - decryptData := make(map[string]interface{}) - envOptions := stepwise.MountOptions{ - RegistryName: "updatedtransit", - PluginType: api.PluginTypeSecrets, - PluginName: "transit", - MountPathPrefix: "transit_temp", - } - stepwise.Run(t, stepwise.Case{ - Environment: dockerEnvironment.NewEnvironment("updatedtransit", &envOptions), - Steps: []stepwise.Step{ - testAccStepwiseListPolicy(t, "test", true), - testAccStepwiseWritePolicy(t, "test", true), - testAccStepwiseListPolicy(t, "test", false), - testAccStepwiseReadPolicy(t, "test", false, true), - testAccStepwiseEncryptContext(t, "test", testPlaintext, "my-cool-context", decryptData), - testAccStepwiseDecrypt(t, "test", testPlaintext, decryptData), - testAccStepwiseEnableDeletion(t, "test"), - testAccStepwiseDeletePolicy(t, "test"), - testAccStepwiseReadPolicy(t, "test", true, true), - }, - }) -} - -func testAccStepwiseWritePolicy(t *testing.T, name string, derived bool) stepwise.Step { - ts := stepwise.Step{ - Operation: stepwise.WriteOperation, - Path: "keys/" + name, - Data: map[string]interface{}{ - "derived": derived, - }, - } - if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" { - ts.Data["type"] = "chacha20-poly1305" - } - return ts -} - -func testAccStepwiseListPolicy(t *testing.T, name string, expectNone bool) stepwise.Step { - return stepwise.Step{ - Operation: stepwise.ListOperation, - Path: "keys", - Assert: func(resp *api.Secret, err error) error { - if (resp == nil || len(resp.Data) == 0) && !expectNone { - return fmt.Errorf("missing response") - } - if expectNone && resp != nil { - return fmt.Errorf("response data when expecting none") - } - - if expectNone && resp == nil { - return nil - } - - var d struct { - Keys []string `mapstructure:"keys"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if len(d.Keys) == 0 { - return fmt.Errorf("missing keys") - } - if len(d.Keys) > 1 { - return fmt.Errorf("only 1 key expected, %d returned", len(d.Keys)) - } - if d.Keys[0] != name { - return fmt.Errorf("Actual key: %s\nExpected key: %s", d.Keys[0], name) - } - return nil - }, - } -} - -func testAccStepwiseReadPolicy(t *testing.T, name string, expectNone, derived bool) stepwise.Step { - t.Helper() - return testAccStepwiseReadPolicyWithVersions(t, name, expectNone, derived, 1, 0) -} - -func testAccStepwiseReadPolicyWithVersions(t *testing.T, name string, expectNone, derived bool, minDecryptionVersion int, minEncryptionVersion int) stepwise.Step { - t.Helper() - return stepwise.Step{ - Operation: stepwise.ReadOperation, - Path: "keys/" + name, - Assert: func(resp *api.Secret, err error) error { - t.Helper() - if resp == nil && !expectNone { - return fmt.Errorf("missing response") - } else if expectNone { - if resp != nil { - return fmt.Errorf("response when expecting none") - } - return nil - } - var d struct { - Name string `mapstructure:"name"` - Key []byte `mapstructure:"key"` - Keys map[string]int64 `mapstructure:"keys"` - Type string `mapstructure:"type"` - Derived bool `mapstructure:"derived"` - KDF string `mapstructure:"kdf"` - DeletionAllowed bool `mapstructure:"deletion_allowed"` - ConvergentEncryption bool `mapstructure:"convergent_encryption"` - MinDecryptionVersion int `mapstructure:"min_decryption_version"` - MinEncryptionVersion int `mapstructure:"min_encryption_version"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - if d.Name != name { - return fmt.Errorf("bad name: %#v", d) - } - if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" { - if d.Type != keysutil.KeyType(keysutil.KeyType_ChaCha20_Poly1305).String() { - return fmt.Errorf("bad key type: %#v", d) - } - } else if d.Type != keysutil.KeyType(keysutil.KeyType_AES256_GCM96).String() { - return fmt.Errorf("bad key type: %#v", d) - } - // Should NOT get a key back - if d.Key != nil { - return fmt.Errorf("unexpected key found") - } - if d.Keys == nil { - return fmt.Errorf("no keys found") - } - if d.MinDecryptionVersion != minDecryptionVersion { - return fmt.Errorf("minimum decryption version mismatch, expected (%#v), found (%#v)", minEncryptionVersion, d.MinDecryptionVersion) - } - if d.MinEncryptionVersion != minEncryptionVersion { - return fmt.Errorf("minimum encryption version mismatch, expected (%#v), found (%#v)", minEncryptionVersion, d.MinDecryptionVersion) - } - if d.DeletionAllowed { - return fmt.Errorf("expected DeletionAllowed to be false, but got true") - } - if d.Derived != derived { - return fmt.Errorf("derived mismatch, expected (%t), got (%t)", derived, d.Derived) - } - if derived && d.KDF != "hkdf_sha256" { - return fmt.Errorf("expected KDF to be hkdf_sha256, but got (%s)", d.KDF) - } - return nil - }, - } -} - -func testAccStepwiseEncryptContext( - t *testing.T, name, plaintext, context string, decryptData map[string]interface{}, -) stepwise.Step { - return stepwise.Step{ - Operation: stepwise.UpdateOperation, - Path: "encrypt/" + name, - Data: map[string]interface{}{ - "plaintext": base64.StdEncoding.EncodeToString([]byte(plaintext)), - "context": base64.StdEncoding.EncodeToString([]byte(context)), - }, - Assert: func(resp *api.Secret, err error) error { - var d struct { - Ciphertext string `mapstructure:"ciphertext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - if d.Ciphertext == "" { - return fmt.Errorf("missing ciphertext") - } - decryptData["ciphertext"] = d.Ciphertext - decryptData["context"] = base64.StdEncoding.EncodeToString([]byte(context)) - return nil - }, - } -} - -func testAccStepwiseDecrypt( - t *testing.T, name, plaintext string, decryptData map[string]interface{}, -) stepwise.Step { - return stepwise.Step{ - Operation: stepwise.UpdateOperation, - Path: "decrypt/" + name, - Data: decryptData, - Assert: func(resp *api.Secret, err error) error { - var d struct { - Plaintext string `mapstructure:"plaintext"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - - // Decode the base64 - plainRaw, err := base64.StdEncoding.DecodeString(d.Plaintext) - if err != nil { - return err - } - - if string(plainRaw) != plaintext { - return fmt.Errorf("plaintext mismatch: %s expect: %s, decryptData was %#v", plainRaw, plaintext, decryptData) - } - return nil - }, - } -} - -func testAccStepwiseEnableDeletion(t *testing.T, name string) stepwise.Step { - return stepwise.Step{ - Operation: stepwise.UpdateOperation, - Path: "keys/" + name + "/config", - Data: map[string]interface{}{ - "deletion_allowed": true, - }, - } -} - -func testAccStepwiseDeletePolicy(t *testing.T, name string) stepwise.Step { - return stepwise.Step{ - Operation: stepwise.DeleteOperation, - Path: "keys/" + name, - } -} diff --git a/builtin/plugin/backend_lazyLoad_test.go b/builtin/plugin/backend_lazyLoad_test.go deleted file mode 100644 index 094047c03..000000000 --- a/builtin/plugin/backend_lazyLoad_test.go +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package plugin - -import ( - "context" - "errors" - "testing" - - "github.com/hashicorp/vault/sdk/helper/logging" - - "github.com/hashicorp/vault/sdk/helper/pluginutil" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/plugin" -) - -func TestBackend_lazyLoad(t *testing.T) { - // normal load - var invocations int - b := testLazyLoad(t, func() error { - invocations++ - return nil - }) - if invocations != 1 { - t.Fatalf("expected 1 invocation") - } - if b.canary != "" { - t.Fatalf("expected empty canary") - } - - // load with plugin shutdown - invocations = 0 - b = testLazyLoad(t, func() error { - invocations++ - if invocations == 1 { - return plugin.ErrPluginShutdown - } - return nil - }) - if invocations != 2 { - t.Fatalf("expected 2 invocations") - } - if b.canary == "" { - t.Fatalf("expected canary") - } -} - -func testLazyLoad(t *testing.T, methodWrapper func() error) *PluginBackend { - sysView := newTestSystemView() - - ctx := context.Background() - config := &logical.BackendConfig{ - Logger: logging.NewVaultLogger(hclog.Trace), - System: sysView, - Config: map[string]string{ - "plugin_name": "test-plugin", - "plugin_type": "secret", - }, - } - - // this is a dummy plugin that hasn't really been loaded yet - orig, err := plugin.NewBackend(ctx, "test-plugin", consts.PluginTypeSecrets, sysView, config, true) - if err != nil { - t.Fatal(err) - } - - b := &PluginBackend{ - Backend: orig, - config: config, - } - - // lazy load - err = b.lazyLoadBackend(ctx, &logical.InmemStorage{}, methodWrapper) - if err != nil { - t.Fatal(err) - } - if !b.loaded { - t.Fatalf("not loaded") - } - - // make sure dummy plugin was handled properly - ob := orig.(*testBackend) - if !ob.cleaned { - t.Fatalf("not cleaned") - } - if ob.setup { - t.Fatalf("setup") - } - if ob.initialized { - t.Fatalf("initialized") - } - - // make sure our newly initialized plugin was handled properly - nb := b.Backend.(*testBackend) - if nb.cleaned { - t.Fatalf("cleaned") - } - if !nb.setup { - t.Fatalf("not setup") - } - if !nb.initialized { - t.Fatalf("not initialized") - } - - return b -} - -//------------------------------------------------------------------ - -type testBackend struct { - cleaned bool - setup bool - initialized bool -} - -var _ logical.Backend = (*testBackend)(nil) - -func (b *testBackend) Cleanup(context.Context) { - b.cleaned = true -} - -func (b *testBackend) Setup(context.Context, *logical.BackendConfig) error { - b.setup = true - return nil -} - -func (b *testBackend) Initialize(context.Context, *logical.InitializationRequest) error { - b.initialized = true - return nil -} - -func (b *testBackend) Type() logical.BackendType { - return logical.TypeLogical -} - -func (b *testBackend) SpecialPaths() *logical.Paths { - return &logical.Paths{ - Root: []string{"test-root"}, - } -} - -func (b *testBackend) Logger() hclog.Logger { - return logging.NewVaultLogger(hclog.Trace) -} - -func (b *testBackend) HandleRequest(context.Context, *logical.Request) (*logical.Response, error) { - panic("not needed") -} - -func (b *testBackend) System() logical.SystemView { - panic("not needed") -} - -func (b *testBackend) HandleExistenceCheck(context.Context, *logical.Request) (bool, bool, error) { - panic("not needed") -} - -func (b *testBackend) InvalidateKey(context.Context, string) { - panic("not needed") -} - -//------------------------------------------------------------------ - -type testSystemView struct { - logical.StaticSystemView - factory logical.Factory -} - -func newTestSystemView() testSystemView { - return testSystemView{ - factory: func(_ context.Context, _ *logical.BackendConfig) (logical.Backend, error) { - return &testBackend{}, nil - }, - } -} - -func (v testSystemView) LookupPlugin(context.Context, string, consts.PluginType) (*pluginutil.PluginRunner, error) { - return &pluginutil.PluginRunner{ - Name: "test-plugin-runner", - Builtin: true, - BuiltinFactory: func() (interface{}, error) { - return v.factory, nil - }, - }, nil -} - -func (v testSystemView) LookupPluginVersion(context.Context, string, consts.PluginType, string) (*pluginutil.PluginRunner, error) { - return &pluginutil.PluginRunner{ - Name: "test-plugin-runner", - Builtin: true, - BuiltinFactory: func() (interface{}, error) { - return v.factory, nil - }, - }, nil -} - -func (v testSystemView) ListVersionedPlugins(_ context.Context, _ consts.PluginType) ([]pluginutil.VersionedPlugin, error) { - return nil, errors.New("ListVersionedPlugins not implemented for testSystemView") -} diff --git a/builtin/plugin/backend_test.go b/builtin/plugin/backend_test.go deleted file mode 100644 index 2d06c2b31..000000000 --- a/builtin/plugin/backend_test.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package plugin_test - -import ( - "context" - "fmt" - "os" - "testing" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/plugin" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/helper/pluginutil" - "github.com/hashicorp/vault/sdk/logical" - logicalPlugin "github.com/hashicorp/vault/sdk/plugin" - "github.com/hashicorp/vault/sdk/plugin/mock" - "github.com/hashicorp/vault/vault" -) - -func TestBackend_impl(t *testing.T) { - var _ logical.Backend = &plugin.PluginBackend{} -} - -func TestBackend(t *testing.T) { - pluginCmds := []string{"TestBackend_PluginMain", "TestBackend_PluginMain_Multiplexed"} - - for _, pluginCmd := range pluginCmds { - t.Run(pluginCmd, func(t *testing.T) { - config, cleanup := testConfig(t, pluginCmd) - defer cleanup() - - _, err := plugin.Backend(context.Background(), config) - if err != nil { - t.Fatal(err) - } - }) - } -} - -func TestBackend_Factory(t *testing.T) { - pluginCmds := []string{"TestBackend_PluginMain", "TestBackend_PluginMain_Multiplexed"} - - for _, pluginCmd := range pluginCmds { - t.Run(pluginCmd, func(t *testing.T) { - config, cleanup := testConfig(t, pluginCmd) - defer cleanup() - - _, err := plugin.Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - }) - } -} - -func TestBackend_PluginMain(t *testing.T) { - args := []string{} - if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" && os.Getenv(pluginutil.PluginMetadataModeEnv) != "true" { - return - } - - caPEM := os.Getenv(pluginutil.PluginCACertPEMEnv) - if caPEM == "" { - t.Fatal("CA cert not passed in") - } - - args = append(args, fmt.Sprintf("--ca-cert=%s", caPEM)) - - apiClientMeta := &api.PluginAPIClientMeta{} - flags := apiClientMeta.FlagSet() - flags.Parse(args) - tlsConfig := apiClientMeta.GetTLSConfig() - tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) - - err := logicalPlugin.Serve(&logicalPlugin.ServeOpts{ - BackendFactoryFunc: mock.Factory, - TLSProviderFunc: tlsProviderFunc, - }) - if err != nil { - t.Fatal(err) - } -} - -func TestBackend_PluginMain_Multiplexed(t *testing.T) { - args := []string{} - if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" && os.Getenv(pluginutil.PluginMetadataModeEnv) != "true" { - return - } - - caPEM := os.Getenv(pluginutil.PluginCACertPEMEnv) - if caPEM == "" { - t.Fatal("CA cert not passed in") - } - - args = append(args, fmt.Sprintf("--ca-cert=%s", caPEM)) - - apiClientMeta := &api.PluginAPIClientMeta{} - flags := apiClientMeta.FlagSet() - flags.Parse(args) - tlsConfig := apiClientMeta.GetTLSConfig() - tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) - - err := logicalPlugin.ServeMultiplex(&logicalPlugin.ServeOpts{ - BackendFactoryFunc: mock.Factory, - TLSProviderFunc: tlsProviderFunc, - }) - if err != nil { - t.Fatal(err) - } -} - -func testConfig(t *testing.T, pluginCmd string) (*logical.BackendConfig, func()) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - cores := cluster.Cores - - core := cores[0] - - sys := vault.TestDynamicSystemView(core.Core, nil) - - config := &logical.BackendConfig{ - Logger: logging.NewVaultLogger(log.Debug), - System: sys, - Config: map[string]string{ - "plugin_name": "mock-plugin", - "plugin_type": "secret", - "plugin_version": "v0.0.0+mock", - }, - } - - os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) - - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", pluginCmd, []string{}, "") - - return config, func() { - cluster.Cleanup() - } -} diff --git a/builtin/plugin/mock_plugin_test.go b/builtin/plugin/mock_plugin_test.go deleted file mode 100644 index 6c189a846..000000000 --- a/builtin/plugin/mock_plugin_test.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package plugin - -import ( - _ "github.com/hashicorp/vault-plugin-mock" -) - -// This file exists to force an import of vault-plugin-mock (which itself does nothing), -// for purposes of CI and GitHub actions testing between plugin repos and Vault. diff --git a/command/agent/approle_end_to_end_test.go b/command/agent/approle_end_to_end_test.go deleted file mode 100644 index aca68de9c..000000000 --- a/command/agent/approle_end_to_end_test.go +++ /dev/null @@ -1,812 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package agent - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "strings" - "testing" - "time" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - "github.com/hashicorp/vault/command/agentproxyshared/auth" - agentapprole "github.com/hashicorp/vault/command/agentproxyshared/auth/approle" - "github.com/hashicorp/vault/command/agentproxyshared/sink" - "github.com/hashicorp/vault/command/agentproxyshared/sink/file" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestAppRoleEndToEnd(t *testing.T) { - t.Parallel() - - testCases := []struct { - removeSecretIDFile bool - bindSecretID bool - secretIDLess bool - expectToken bool - }{ - // default behaviour => token expected - {false, true, false, true}, - {true, true, false, true}, - - //bindSecretID=false, wrong secret provided => token expected - //(vault ignores the supplied secret_id if bind_secret_id=false) - {false, false, false, true}, - {true, false, false, true}, - - // bindSecretID=false, secret not provided => token expected - {false, false, true, true}, - {true, false, true, true}, - - // bindSecretID=true, secret not provided => token not expected - {false, true, true, false}, - {true, true, true, false}, - } - - for _, tc := range testCases { - secretFileAction := "preserve" - if tc.removeSecretIDFile { - secretFileAction = "remove" - } - tc := tc // capture range variable - t.Run(fmt.Sprintf("%s_secret_id_file bindSecretID=%v secretIDLess=%v expectToken=%v", secretFileAction, tc.bindSecretID, tc.secretIDLess, tc.expectToken), func(t *testing.T) { - t.Parallel() - testAppRoleEndToEnd(t, tc.removeSecretIDFile, tc.bindSecretID, tc.secretIDLess, tc.expectToken) - }) - } -} - -func testAppRoleEndToEnd(t *testing.T, removeSecretIDFile bool, bindSecretID bool, secretIDLess bool, expectToken bool) { - var err error - logger := logging.NewVaultLogger(log.Trace) - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: log.NewNullLogger(), - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - client := cores[0].Client - - err = client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{ - Type: "approle", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/approle/role/test1", addConstraints(!bindSecretID, map[string]interface{}{ - "bind_secret_id": bindSecretID, - "token_ttl": "6s", - "token_max_ttl": "10s", - })) - - logger.Trace("vault configured with", "bind_secret_id", bindSecretID) - - if err != nil { - t.Fatal(err) - } - - secret := "" - secretID1 := "" - secretID2 := "" - if bindSecretID { - resp, err := client.Logical().Write("auth/approle/role/test1/secret-id", nil) - if err != nil { - t.Fatal(err) - } - secretID1 = resp.Data["secret_id"].(string) - } else { - logger.Trace("skipped write to auth/approle/role/test1/secret-id") - } - resp, err := client.Logical().Read("auth/approle/role/test1/role-id") - if err != nil { - t.Fatal(err) - } - roleID1 := resp.Data["role_id"].(string) - - _, err = client.Logical().Write("auth/approle/role/test2", addConstraints(!bindSecretID, map[string]interface{}{ - "bind_secret_id": bindSecretID, - "token_ttl": "6s", - "token_max_ttl": "10s", - })) - if err != nil { - t.Fatal(err) - } - if bindSecretID { - resp, err = client.Logical().Write("auth/approle/role/test2/secret-id", nil) - if err != nil { - t.Fatal(err) - } - secretID2 = resp.Data["secret_id"].(string) - } else { - logger.Trace("skipped write to auth/approle/role/test2/secret-id") - } - resp, err = client.Logical().Read("auth/approle/role/test2/role-id") - if err != nil { - t.Fatal(err) - } - roleID2 := resp.Data["role_id"].(string) - - rolef, err := ioutil.TempFile("", "auth.role-id.test.") - if err != nil { - t.Fatal(err) - } - role := rolef.Name() - rolef.Close() // WriteFile doesn't need it open - defer os.Remove(role) - t.Logf("input role_id_file_path: %s", role) - if bindSecretID { - secretf, err := ioutil.TempFile("", "auth.secret-id.test.") - if err != nil { - t.Fatal(err) - } - secret = secretf.Name() - secretf.Close() - defer os.Remove(secret) - t.Logf("input secret_id_file_path: %s", secret) - } else { - logger.Trace("skipped writing tempfile auth.secret-id.test.") - } - // We close these right away because we're just basically testing - // permissions and finding a usable file name - ouf, err := ioutil.TempFile("", "auth.tokensink.test.") - if err != nil { - t.Fatal(err) - } - out := ouf.Name() - ouf.Close() - os.Remove(out) - t.Logf("output: %s", out) - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - - secretFromAgent := secret - if secretIDLess { - secretFromAgent = "" - } - if !bindSecretID && !secretIDLess { - logger.Trace("agent is providing an invalid secret that should be ignored") - secretf, err := ioutil.TempFile("", "auth.secret-id.test.") - if err != nil { - t.Fatal(err) - } - secretFromAgent = secretf.Name() - secretf.Close() - defer os.Remove(secretFromAgent) - // if the token is empty, auth.approle would fail reporting the error - if err := ioutil.WriteFile(secretFromAgent, []byte("wrong-secret"), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote secret_id_file_path with wrong-secret", "path", secretFromAgent) - } - } - conf := map[string]interface{}{ - "role_id_file_path": role, - "secret_id_file_path": secretFromAgent, - } - logger.Trace("agent configured with", "conf", conf) - if !removeSecretIDFile { - conf["remove_secret_id_file_after_reading"] = removeSecretIDFile - } - am, err := agentapprole.NewApproleAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.approle"), - MountPath: "auth/approle", - Config: conf, - }) - if err != nil { - t.Fatal(err) - } - ahConfig := &auth.AuthHandlerConfig{ - Logger: logger.Named("auth.handler"), - Client: client, - } - ah := auth.NewAuthHandler(ahConfig) - errCh := make(chan error) - go func() { - errCh <- ah.Run(ctx, am) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - config := &sink.SinkConfig{ - Logger: logger.Named("sink.file"), - Config: map[string]interface{}{ - "path": out, - }, - } - fs, err := file.NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = fs - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: logger.Named("sink.server"), - Client: client, - }) - - go func() { - errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config}) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // This has to be after the other defers so it happens first. It allows - // successful test runs to immediately cancel all of the runner goroutines - // and unblock any of the blocking defer calls by the runner's DoneCh that - // comes before this and avoid successful tests from taking the entire - // timeout duration. - defer cancel() - - // Check that no sink file exists - _, err = os.Lstat(out) - if err == nil { - t.Fatal("expected err") - } - if !os.IsNotExist(err) { - t.Fatal("expected notexist err") - } - - if err := ioutil.WriteFile(role, []byte(roleID1), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test role 1", "path", role) - } - - if bindSecretID { - if err := ioutil.WriteFile(secret, []byte(secretID1), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test secret 1", "path", secret) - } - } else { - logger.Trace("skipped writing test secret 1") - } - - checkToken := func() string { - timeout := time.Now().Add(10 * time.Second) - for { - if time.Now().After(timeout) { - if expectToken { - t.Fatal("did not find a written token after timeout") - } - return "" - } - val, err := ioutil.ReadFile(out) - if err == nil { - os.Remove(out) - if len(val) == 0 { - t.Fatal("written token was empty") - } - if !secretIDLess { - _, err = os.Stat(secretFromAgent) - switch { - case removeSecretIDFile && err == nil: - t.Fatal("secret file exists but was supposed to be removed") - case !removeSecretIDFile && err != nil: - t.Fatal("secret ID file does not exist but was not supposed to be removed") - } - } - client.SetToken(string(val)) - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - return secret.Data["entity_id"].(string) - } - time.Sleep(250 * time.Millisecond) - } - } - origEntity := checkToken() - if !expectToken && origEntity != "" { - t.Fatal("did not expect a token to be written: " + origEntity) - } - if !expectToken && origEntity == "" { - logger.Trace("skipping entities comparison as we are not expecting tokens to be written") - return - } - - // Make sure it gets renewed - timeout := time.Now().Add(4 * time.Second) - for { - if time.Now().After(timeout) { - break - } - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - ttl, err := secret.Data["ttl"].(json.Number).Int64() - if err != nil { - t.Fatal(err) - } - if ttl > 6 { - t.Fatalf("unexpected ttl: %v", secret.Data["ttl"]) - } - } - - // Write new values - if err := ioutil.WriteFile(role, []byte(roleID2), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test role 2", "path", role) - } - - if bindSecretID { - if err := ioutil.WriteFile(secret, []byte(secretID2), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test secret 2", "path", secret) - } - } else { - logger.Trace("skipped writing test secret 2") - } - - newEntity := checkToken() - if newEntity == origEntity { - t.Fatal("found same entity") - } - - timeout = time.Now().Add(4 * time.Second) - for { - if time.Now().After(timeout) { - break - } - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - ttl, err := secret.Data["ttl"].(json.Number).Int64() - if err != nil { - t.Fatal(err) - } - if ttl > 6 { - t.Fatalf("unexpected ttl: %v", secret.Data["ttl"]) - } - } -} - -// TestAppRoleLongRoleName tests that the creation of an approle is a maximum of 4096 bytes -// Prior to VAULT-8518 being fixed, you were unable to delete an approle value longer than 1024 bytes -// due to a restriction put into place by PR #14746, to prevent unbounded HMAC creation. -func TestAppRoleLongRoleName(t *testing.T) { - approleName := strings.Repeat("a", 5000) - - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: log.NewNullLogger(), - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - client := cores[0].Client - - err := client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{ - Type: "approle", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write(fmt.Sprintf("auth/approle/role/%s", approleName), map[string]interface{}{ - "token_ttl": "6s", - "token_max_ttl": "10s", - }) - if err != nil { - if !strings.Contains(err.Error(), "role_name is longer than maximum") { - t.Fatal(err) - } - } -} - -func TestAppRoleWithWrapping(t *testing.T) { - testCases := []struct { - bindSecretID bool - secretIDLess bool - expectToken bool - }{ - // default behaviour => token expected - {true, false, true}, - - //bindSecretID=false, wrong secret provided, wrapping_path provided => token not expected - //(wrapping token is not valid or does not exist) - {false, false, false}, - - // bindSecretID=false, no secret provided, wrapping_path provided but ignored => token expected - {false, true, true}, - } - for _, tc := range testCases { - t.Run(fmt.Sprintf("bindSecretID=%v secretIDLess=%v expectToken=%v", tc.bindSecretID, tc.secretIDLess, tc.expectToken), func(t *testing.T) { - testAppRoleWithWrapping(t, tc.bindSecretID, tc.secretIDLess, tc.expectToken) - }) - } -} - -func testAppRoleWithWrapping(t *testing.T, bindSecretID bool, secretIDLess bool, expectToken bool) { - var err error - logger := logging.NewVaultLogger(log.Trace) - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: log.NewNullLogger(), - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - client := cores[0].Client - origToken := client.Token() - - err = client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{ - Type: "approle", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/approle/role/test1", addConstraints(!bindSecretID, map[string]interface{}{ - "bind_secret_id": bindSecretID, - "token_ttl": "6s", - "token_max_ttl": "10s", - })) - if err != nil { - t.Fatal(err) - } - - client.SetWrappingLookupFunc(func(operation, path string) string { - if path == "auth/approle/role/test1/secret-id" { - return "10s" - } - return "" - }) - - secret := "" - secretID1 := "" - if bindSecretID { - resp, err := client.Logical().Write("auth/approle/role/test1/secret-id", nil) - if err != nil { - t.Fatal(err) - } - secretID1 = resp.WrapInfo.Token - } else { - logger.Trace("skipped write to auth/approle/role/test1/secret-id") - } - resp, err := client.Logical().Read("auth/approle/role/test1/role-id") - if err != nil { - t.Fatal(err) - } - roleID1 := resp.Data["role_id"].(string) - - rolef, err := ioutil.TempFile("", "auth.role-id.test.") - if err != nil { - t.Fatal(err) - } - role := rolef.Name() - rolef.Close() // WriteFile doesn't need it open - defer os.Remove(role) - t.Logf("input role_id_file_path: %s", role) - - if bindSecretID { - secretf, err := ioutil.TempFile("", "auth.secret-id.test.") - if err != nil { - t.Fatal(err) - } - secret = secretf.Name() - secretf.Close() - defer os.Remove(secret) - t.Logf("input secret_id_file_path: %s", secret) - } else { - logger.Trace("skipped writing tempfile auth.secret-id.test.") - } - - // We close these right away because we're just basically testing - // permissions and finding a usable file name - ouf, err := ioutil.TempFile("", "auth.tokensink.test.") - if err != nil { - t.Fatal(err) - } - out := ouf.Name() - ouf.Close() - os.Remove(out) - t.Logf("output: %s", out) - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - - secretFromAgent := secret - if secretIDLess { - secretFromAgent = "" - } - if !bindSecretID && !secretIDLess { - logger.Trace("agent is providing an invalid secret that should be ignored") - secretf, err := ioutil.TempFile("", "auth.secret-id.test.") - if err != nil { - t.Fatal(err) - } - secretFromAgent = secretf.Name() - secretf.Close() - defer os.Remove(secretFromAgent) - // if the token is empty, auth.approle would fail reporting the error - if err := ioutil.WriteFile(secretFromAgent, []byte("wrong-secret"), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote secret_id_file_path with wrong-secret", "path", secretFromAgent) - } - } - conf := map[string]interface{}{ - "role_id_file_path": role, - "secret_id_file_path": secretFromAgent, - "secret_id_response_wrapping_path": "auth/approle/role/test1/secret-id", - "remove_secret_id_file_after_reading": true, - } - logger.Trace("agent configured with", "conf", conf) - - am, err := agentapprole.NewApproleAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.approle"), - MountPath: "auth/approle", - Config: conf, - }) - if err != nil { - t.Fatal(err) - } - ahConfig := &auth.AuthHandlerConfig{ - Logger: logger.Named("auth.handler"), - Client: client, - } - ah := auth.NewAuthHandler(ahConfig) - errCh := make(chan error) - go func() { - errCh <- ah.Run(ctx, am) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - config := &sink.SinkConfig{ - Logger: logger.Named("sink.file"), - Config: map[string]interface{}{ - "path": out, - }, - } - fs, err := file.NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = fs - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: logger.Named("sink.server"), - Client: client, - }) - go func() { - errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config}) - }() - - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // This has to be after the other defers so it happens first. It allows - // successful test runs to immediately cancel all of the runner goroutines - // and unblock any of the blocking defer calls by the runner's DoneCh that - // comes before this and avoid successful tests from taking the entire - // timeout duration. - defer cancel() - - // Check that no sink file exists - _, err = os.Lstat(out) - if err == nil { - t.Fatal("expected err") - } - if !os.IsNotExist(err) { - t.Fatal("expected notexist err") - } - - if err := ioutil.WriteFile(role, []byte(roleID1), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test role 1", "path", role) - } - - if bindSecretID { - logger.Trace("WRITING TO auth.secret-id.test.", "secret", secret, "secretID1", secretID1) - - if err := ioutil.WriteFile(secret, []byte(secretID1), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test secret 1", "path", secret) - } - } else { - logger.Trace("skipped writing test secret 1") - } - - checkToken := func() string { - timeout := time.Now().Add(10 * time.Second) - for { - if time.Now().After(timeout) { - if expectToken { - t.Fatal("did not find a written token after timeout") - } - return "" - } - val, err := ioutil.ReadFile(out) - if err == nil { - os.Remove(out) - if len(val) == 0 { - t.Fatal("written token was empty") - } - if !secretIDLess { - if _, err := os.Stat(secret); err == nil { - t.Fatal("secret ID file does not exist but was not supposed to be removed") - } - } - - client.SetToken(string(val)) - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - return secret.Data["entity_id"].(string) - } - time.Sleep(250 * time.Millisecond) - } - } - origEntity := checkToken() - logger.Trace("cheking token", "origEntity", origEntity) - - if !expectToken && origEntity != "" { - t.Fatal("did not expect a token to be written: " + origEntity) - } - if !expectToken && origEntity == "" { - logger.Trace("skipping entities comparison as we are not expecting tokens to be written") - return - } - - // Make sure it gets renewed - timeout := time.Now().Add(4 * time.Second) - for { - if time.Now().After(timeout) { - break - } - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - ttl, err := secret.Data["ttl"].(json.Number).Int64() - if err != nil { - t.Fatal(err) - } - if ttl > 6 { - t.Fatalf("unexpected ttl: %v", secret.Data["ttl"]) - } - } - - // Write new values - client.SetToken(origToken) - logger.Trace("origToken set into client", "origToken", origToken) - - if bindSecretID { - resp, err = client.Logical().Write("auth/approle/role/test1/secret-id", nil) - if err != nil { - t.Fatal(err) - } - secretID2 := resp.WrapInfo.Token - if err := ioutil.WriteFile(secret, []byte(secretID2), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test secret 2", "path", secret) - } - } else { - logger.Trace("skipped writing test secret 2") - } - - newEntity := checkToken() - if newEntity != origEntity { - t.Fatal("did not find same entity") - } - - timeout = time.Now().Add(4 * time.Second) - for { - if time.Now().After(timeout) { - break - } - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - ttl, err := secret.Data["ttl"].(json.Number).Int64() - if err != nil { - t.Fatal(err) - } - if ttl > 6 { - t.Fatalf("unexpected ttl: %v", secret.Data["ttl"]) - } - } -} - -func addConstraints(add bool, cfg map[string]interface{}) map[string]interface{} { - if add { - // extraConstraints to add when bind_secret_id=false (otherwise Vault would fail with: "at least one constraint should be enabled on the role") - extraConstraints := map[string]interface{}{ - "secret_id_bound_cidrs": "127.0.0.1/32", - "token_bound_cidrs": "127.0.0.1/32", - } - for k, v := range extraConstraints { - cfg[k] = v - } - } - return cfg -} diff --git a/command/agent/auto_auth_preload_token_end_to_end_test.go b/command/agent/auto_auth_preload_token_end_to_end_test.go deleted file mode 100644 index 5630d9cb9..000000000 --- a/command/agent/auto_auth_preload_token_end_to_end_test.go +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package agent - -import ( - "context" - "io/ioutil" - "os" - "testing" - "time" - - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - "github.com/hashicorp/vault/command/agentproxyshared/auth" - agentAppRole "github.com/hashicorp/vault/command/agentproxyshared/auth/approle" - "github.com/hashicorp/vault/command/agentproxyshared/sink" - "github.com/hashicorp/vault/command/agentproxyshared/sink/file" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestTokenPreload_UsingAutoAuth(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - coreConfig := &vault.CoreConfig{ - Logger: logger, - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - client := cluster.Cores[0].Client - - // Setup Vault - if err := client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{ - Type: "approle", - }); err != nil { - t.Fatal(err) - } - - // Setup Approle - _, err := client.Logical().Write("auth/approle/role/test1", map[string]interface{}{ - "bind_secret_id": "true", - "token_ttl": "3s", - "token_max_ttl": "10s", - "policies": []string{"test-autoauth"}, - }) - if err != nil { - t.Fatal(err) - } - - resp, err := client.Logical().Write("auth/approle/role/test1/secret-id", nil) - if err != nil { - t.Fatal(err) - } - secretID1 := resp.Data["secret_id"].(string) - - resp, err = client.Logical().Read("auth/approle/role/test1/role-id") - if err != nil { - t.Fatal(err) - } - roleID1 := resp.Data["role_id"].(string) - - rolef, err := ioutil.TempFile("", "auth.role-id.test.") - if err != nil { - t.Fatal(err) - } - role := rolef.Name() - rolef.Close() // WriteFile doesn't need it open - defer os.Remove(role) - t.Logf("input role_id_file_path: %s", role) - - secretf, err := ioutil.TempFile("", "auth.secret-id.test.") - if err != nil { - t.Fatal(err) - } - secret := secretf.Name() - secretf.Close() - defer os.Remove(secret) - t.Logf("input secret_id_file_path: %s", secret) - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - - conf := map[string]interface{}{ - "role_id_file_path": role, - "secret_id_file_path": secret, - } - - if err := ioutil.WriteFile(role, []byte(roleID1), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test role 1", "path", role) - } - - if err := ioutil.WriteFile(secret, []byte(secretID1), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test secret 1", "path", secret) - } - - // Setup Preload Token - tokenRespRaw, err := client.Logical().Write("auth/token/create", map[string]interface{}{ - "ttl": "10s", - "explicit-max-ttl": "15s", - "policies": []string{""}, - }) - if err != nil { - t.Fatal(err) - } - - if tokenRespRaw.Auth == nil || tokenRespRaw.Auth.ClientToken == "" { - t.Fatal("expected token but got none") - } - token := tokenRespRaw.Auth.ClientToken - - am, err := agentAppRole.NewApproleAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.approle"), - MountPath: "auth/approle", - Config: conf, - }) - if err != nil { - t.Fatal(err) - } - - ahConfig := &auth.AuthHandlerConfig{ - Logger: logger.Named("auth.handler"), - Client: client, - Token: token, - } - - ah := auth.NewAuthHandler(ahConfig) - - tmpFile, err := ioutil.TempFile("", "auth.tokensink.test.") - if err != nil { - t.Fatal(err) - } - tokenSinkFileName := tmpFile.Name() - tmpFile.Close() - os.Remove(tokenSinkFileName) - t.Logf("output: %s", tokenSinkFileName) - - config := &sink.SinkConfig{ - Logger: logger.Named("sink.file"), - Config: map[string]interface{}{ - "path": tokenSinkFileName, - }, - WrapTTL: 10 * time.Second, - } - - fs, err := file.NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = fs - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: logger.Named("sink.server"), - Client: client, - }) - - errCh := make(chan error) - go func() { - errCh <- ah.Run(ctx, am) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - go func() { - errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config}) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // This has to be after the other defers so it happens first. It allows - // successful test runs to immediately cancel all of the runner goroutines - // and unblock any of the blocking defer calls by the runner's DoneCh that - // comes before this and avoid successful tests from taking the entire - // timeout duration. - defer cancel() - - if stat, err := os.Lstat(tokenSinkFileName); err == nil { - t.Fatalf("expected err but got %s", stat) - } else if !os.IsNotExist(err) { - t.Fatal("expected notexist err") - } - - // Wait 2 seconds for the env variables to be detected and an auth to be generated. - time.Sleep(time.Second * 2) - - authToken, err := readToken(tokenSinkFileName) - if err != nil { - t.Fatal(err) - } - - if authToken.Token == "" { - t.Fatal("expected token but didn't receive it") - } - - wrappedToken := map[string]interface{}{ - "token": authToken.Token, - } - unwrapResp, err := client.Logical().Write("sys/wrapping/unwrap", wrappedToken) - if err != nil { - t.Fatalf("error unwrapping token: %s", err) - } - - sinkToken, ok := unwrapResp.Data["token"].(string) - if !ok { - t.Fatal("expected token but didn't receive it") - } - - if sinkToken != token { - t.Fatalf("auth token and preload token should be the same: expected: %s, actual: %s", token, sinkToken) - } -} diff --git a/command/agent/cache_end_to_end_test.go b/command/agent/cache_end_to_end_test.go deleted file mode 100644 index e1425e5c6..000000000 --- a/command/agent/cache_end_to_end_test.go +++ /dev/null @@ -1,422 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package agent - -import ( - "context" - "fmt" - "io/ioutil" - "net" - "net/http" - "os" - "testing" - "time" - - hclog "github.com/hashicorp/go-hclog" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - "github.com/hashicorp/vault/command/agentproxyshared/auth" - agentapprole "github.com/hashicorp/vault/command/agentproxyshared/auth/approle" - cache "github.com/hashicorp/vault/command/agentproxyshared/cache" - "github.com/hashicorp/vault/command/agentproxyshared/sink" - "github.com/hashicorp/vault/command/agentproxyshared/sink/file" - "github.com/hashicorp/vault/command/agentproxyshared/sink/inmem" - "github.com/hashicorp/vault/helper/useragent" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -const policyAutoAuthAppRole = ` -path "/kv/*" { - capabilities = ["sudo", "create", "read", "update", "delete", "list"] -} - -path "/auth/token/create" { - capabilities = ["create", "update"] -} -` - -func TestCache_UsingAutoAuthToken(t *testing.T) { - var err error - logger := logging.NewVaultLogger(log.Trace) - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: log.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - client := cores[0].Client - - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Setenv(api.EnvVaultAddress, client.Address()) - - defer os.Setenv(api.EnvVaultCACert, os.Getenv(api.EnvVaultCACert)) - os.Setenv(api.EnvVaultCACert, fmt.Sprintf("%s/ca_cert.pem", cluster.TempDir)) - - err = client.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - // Create a secret in the backend - _, err = client.Logical().Write("kv/foo", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - // Add an kv-admin policy - if err := client.Sys().PutPolicy("test-autoauth", policyAutoAuthAppRole); err != nil { - t.Fatal(err) - } - - // Enable approle - err = client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{ - Type: "approle", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/approle/role/test1", map[string]interface{}{ - "bind_secret_id": "true", - "token_ttl": "3s", - "token_max_ttl": "10s", - "policies": []string{"test-autoauth"}, - }) - if err != nil { - t.Fatal(err) - } - - resp, err := client.Logical().Write("auth/approle/role/test1/secret-id", nil) - if err != nil { - t.Fatal(err) - } - secretID1 := resp.Data["secret_id"].(string) - - resp, err = client.Logical().Read("auth/approle/role/test1/role-id") - if err != nil { - t.Fatal(err) - } - roleID1 := resp.Data["role_id"].(string) - - rolef, err := ioutil.TempFile("", "auth.role-id.test.") - if err != nil { - t.Fatal(err) - } - role := rolef.Name() - rolef.Close() // WriteFile doesn't need it open - defer os.Remove(role) - t.Logf("input role_id_file_path: %s", role) - - secretf, err := ioutil.TempFile("", "auth.secret-id.test.") - if err != nil { - t.Fatal(err) - } - secret := secretf.Name() - secretf.Close() - defer os.Remove(secret) - t.Logf("input secret_id_file_path: %s", secret) - - // We close these right away because we're just basically testing - // permissions and finding a usable file name - ouf, err := ioutil.TempFile("", "auth.tokensink.test.") - if err != nil { - t.Fatal(err) - } - out := ouf.Name() - ouf.Close() - os.Remove(out) - t.Logf("output: %s", out) - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - - conf := map[string]interface{}{ - "role_id_file_path": role, - "secret_id_file_path": secret, - "remove_secret_id_file_after_reading": true, - } - - cacheLogger := logging.NewVaultLogger(hclog.Trace).Named("cache") - - // Create the API proxier - apiProxy, err := cache.NewAPIProxy(&cache.APIProxyConfig{ - Client: client, - Logger: cacheLogger.Named("apiproxy"), - UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, - UserAgentString: useragent.ProxyAPIProxyString(), - }) - if err != nil { - t.Fatal(err) - } - - // Create the lease cache proxier and set its underlying proxier to - // the API proxier. - leaseCache, err := cache.NewLeaseCache(&cache.LeaseCacheConfig{ - Client: client, - BaseContext: ctx, - Proxier: apiProxy, - Logger: cacheLogger.Named("leasecache"), - }) - if err != nil { - t.Fatal(err) - } - - am, err := agentapprole.NewApproleAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.approle"), - MountPath: "auth/approle", - Config: conf, - }) - if err != nil { - t.Fatal(err) - } - ahConfig := &auth.AuthHandlerConfig{ - Logger: logger.Named("auth.handler"), - Client: client, - } - ah := auth.NewAuthHandler(ahConfig) - errCh := make(chan error) - go func() { - errCh <- ah.Run(ctx, am) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - config := &sink.SinkConfig{ - Logger: logger.Named("sink.file"), - Config: map[string]interface{}{ - "path": out, - }, - } - fs, err := file.NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = fs - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: logger.Named("sink.server"), - Client: client, - }) - - inmemSinkConfig := &sink.SinkConfig{ - Logger: logger.Named("sink.inmem"), - } - - inmemSink, err := inmem.New(inmemSinkConfig, leaseCache) - if err != nil { - t.Fatal(err) - } - inmemSinkConfig.Sink = inmemSink - - go func() { - errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config, inmemSinkConfig}) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // This has to be after the other defers so it happens first. It allows - // successful test runs to immediately cancel all of the runner goroutines - // and unblock any of the blocking defer calls by the runner's DoneCh that - // comes before this and avoid successful tests from taking the entire - // timeout duration. - defer cancel() - - // Check that no sink file exists - _, err = os.Lstat(out) - if err == nil { - t.Fatal("expected err") - } - if !os.IsNotExist(err) { - t.Fatal("expected notexist err") - } - - if err := ioutil.WriteFile(role, []byte(roleID1), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test role 1", "path", role) - } - - if err := ioutil.WriteFile(secret, []byte(secretID1), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test secret 1", "path", secret) - } - - getToken := func() string { - timeout := time.Now().Add(10 * time.Second) - for { - if time.Now().After(timeout) { - t.Fatal("did not find a written token after timeout") - } - val, err := ioutil.ReadFile(out) - if err == nil { - os.Remove(out) - if len(val) == 0 { - t.Fatal("written token was empty") - } - - _, err = os.Stat(secret) - if err == nil { - t.Fatal("secret file exists but was supposed to be removed") - } - - return string(val) - } - time.Sleep(250 * time.Millisecond) - } - } - - t.Logf("auto-auth token: %q", getToken()) - - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - - defer listener.Close() - - // Create a muxer and add paths relevant for the lease cache layer - mux := http.NewServeMux() - mux.Handle(consts.AgentPathCacheClear, leaseCache.HandleCacheClear(ctx)) - - // Passing a non-nil inmemsink tells the agent to use the auto-auth token - mux.Handle("/", cache.ProxyHandler(ctx, cacheLogger, leaseCache, inmemSink, true)) - server := &http.Server{ - Handler: mux, - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 30 * time.Second, - IdleTimeout: 5 * time.Minute, - ErrorLog: cacheLogger.StandardLogger(nil), - } - go server.Serve(listener) - - testClient, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - - if err := testClient.SetAddress("http://" + listener.Addr().String()); err != nil { - t.Fatal(err) - } - - // Wait for listeners to come up - time.Sleep(2 * time.Second) - - // This block tests that no token on the client is detected by the agent - // and the auto-auth token is used - { - // Empty the token in the client to ensure that auto-auth token is used - testClient.SetToken("") - - resp, err = testClient.Logical().Read("auth/token/lookup-self") - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatalf("failed to use the auto-auth token to perform lookup-self") - } - } - - // This block tests lease creation caching using the auto-auth token. - { - resp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - - origReqID := resp.RequestID - - resp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - - // Sleep for a bit to allow renewer logic to kick in - time.Sleep(20 * time.Millisecond) - - cacheReqID := resp.RequestID - - if origReqID != cacheReqID { - t.Fatalf("request ID mismatch, expected second request to be a cached response: %s != %s", origReqID, cacheReqID) - } - } - - // This block tests auth token creation caching (child, non-orphan tokens) - // using the auto-auth token. - { - resp, err = testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - origReqID := resp.RequestID - - // Sleep for a bit to allow renewer logic to kick in - time.Sleep(20 * time.Millisecond) - - resp, err = testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - cacheReqID := resp.RequestID - - if origReqID != cacheReqID { - t.Fatalf("request ID mismatch, expected second request to be a cached response: %s != %s", origReqID, cacheReqID) - } - } - - // This blocks tests that despite being allowed to use auto-auth token, the - // token on the request will be prioritized. - { - // Empty the token in the client to ensure that auto-auth token is used - testClient.SetToken(client.Token()) - - resp, err = testClient.Logical().Read("auth/token/lookup-self") - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Data["id"] != client.Token() { - t.Fatalf("failed to use the cluster client token to perform lookup-self") - } - } -} diff --git a/command/agent/cert_end_to_end_test.go b/command/agent/cert_end_to_end_test.go deleted file mode 100644 index 3afb11454..000000000 --- a/command/agent/cert_end_to_end_test.go +++ /dev/null @@ -1,583 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package agent - -import ( - "context" - "encoding/pem" - "fmt" - "io/ioutil" - "os" - "testing" - "time" - - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - vaultcert "github.com/hashicorp/vault/builtin/credential/cert" - "github.com/hashicorp/vault/builtin/logical/pki" - "github.com/hashicorp/vault/command/agentproxyshared/auth" - agentcert "github.com/hashicorp/vault/command/agentproxyshared/auth/cert" - "github.com/hashicorp/vault/command/agentproxyshared/sink" - "github.com/hashicorp/vault/command/agentproxyshared/sink/file" - "github.com/hashicorp/vault/helper/dhutil" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestCertEndToEnd(t *testing.T) { - cases := []struct { - name string - withCertRoleName bool - ahWrapping bool - }{ - { - "with name with wrapping", - true, - true, - }, - { - "with name without wrapping", - true, - false, - }, - { - "without name with wrapping", - false, - true, - }, - { - "without name without wrapping", - false, - false, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - testCertEndToEnd(t, tc.withCertRoleName, tc.ahWrapping) - }) - } -} - -func testCertEndToEnd(t *testing.T, withCertRoleName, ahWrapping bool) { - logger := logging.NewVaultLogger(hclog.Trace) - coreConfig := &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "cert": vaultcert.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - client := cluster.Cores[0].Client - - // Setup Vault - err := client.Sys().EnableAuthWithOptions("cert", &api.EnableAuthOptions{ - Type: "cert", - }) - if err != nil { - t.Fatal(err) - } - - certificatePEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cluster.CACert.Raw}) - - certRoleName := "test" - _, err = client.Logical().Write(fmt.Sprintf("auth/cert/certs/%s", certRoleName), map[string]interface{}{ - "certificate": string(certificatePEM), - "policies": "default", - }) - if err != nil { - t.Fatal(err) - } - - // Generate encryption params - pub, pri, err := dhutil.GeneratePublicPrivateKey() - if err != nil { - t.Fatal(err) - } - - ouf, err := ioutil.TempFile("", "auth.tokensink.test.") - if err != nil { - t.Fatal(err) - } - out := ouf.Name() - ouf.Close() - os.Remove(out) - t.Logf("output: %s", out) - - dhpathf, err := ioutil.TempFile("", "auth.dhpath.test.") - if err != nil { - t.Fatal(err) - } - dhpath := dhpathf.Name() - dhpathf.Close() - os.Remove(dhpath) - - // Write DH public key to file - mPubKey, err := jsonutil.EncodeJSON(&dhutil.PublicKeyInfo{ - Curve25519PublicKey: pub, - }) - if err != nil { - t.Fatal(err) - } - if err := ioutil.WriteFile(dhpath, mPubKey, 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote dh param file", "path", dhpath) - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - - aaConfig := map[string]interface{}{} - - if withCertRoleName { - aaConfig["name"] = certRoleName - } - - am, err := agentcert.NewCertAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.cert"), - MountPath: "auth/cert", - Config: aaConfig, - }) - if err != nil { - t.Fatal(err) - } - - ahConfig := &auth.AuthHandlerConfig{ - Logger: logger.Named("auth.handler"), - Client: client, - EnableReauthOnNewCredentials: true, - } - if ahWrapping { - ahConfig.WrapTTL = 10 * time.Second - } - ah := auth.NewAuthHandler(ahConfig) - errCh := make(chan error) - go func() { - errCh <- ah.Run(ctx, am) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - config := &sink.SinkConfig{ - Logger: logger.Named("sink.file"), - AAD: "foobar", - DHType: "curve25519", - DHPath: dhpath, - DeriveKey: true, - Config: map[string]interface{}{ - "path": out, - }, - } - if !ahWrapping { - config.WrapTTL = 10 * time.Second - } - fs, err := file.NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = fs - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: logger.Named("sink.server"), - Client: client, - }) - go func() { - errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config}) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // This has to be after the other defers so it happens first. It allows - // successful test runs to immediately cancel all of the runner goroutines - // and unblock any of the blocking defer calls by the runner's DoneCh that - // comes before this and avoid successful tests from taking the entire - // timeout duration. - defer cancel() - - cloned, err := client.Clone() - if err != nil { - t.Fatal(err) - } - - checkToken := func() string { - timeout := time.Now().Add(5 * time.Second) - for { - if time.Now().After(timeout) { - t.Fatal("did not find a written token after timeout") - } - val, err := ioutil.ReadFile(out) - if err == nil { - os.Remove(out) - if len(val) == 0 { - t.Fatal("written token was empty") - } - - // First decrypt it - resp := new(dhutil.Envelope) - if err := jsonutil.DecodeJSON(val, resp); err != nil { - continue - } - - shared, err := dhutil.GenerateSharedSecret(pri, resp.Curve25519PublicKey) - if err != nil { - t.Fatal(err) - } - aesKey, err := dhutil.DeriveSharedKey(shared, pub, resp.Curve25519PublicKey) - if err != nil { - t.Fatal(err) - } - if len(aesKey) == 0 { - t.Fatal("got empty aes key") - } - - val, err = dhutil.DecryptAES(aesKey, resp.EncryptedPayload, resp.Nonce, []byte("foobar")) - if err != nil { - t.Fatalf("error: %v\nresp: %v", err, string(val)) - } - - // Now unwrap it - wrapInfo := new(api.SecretWrapInfo) - if err := jsonutil.DecodeJSON(val, wrapInfo); err != nil { - t.Fatal(err) - } - switch { - case wrapInfo.TTL != 10: - t.Fatalf("bad wrap info: %v", wrapInfo.TTL) - case !ahWrapping && wrapInfo.CreationPath != "sys/wrapping/wrap": - t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath) - case ahWrapping && wrapInfo.CreationPath != "auth/cert/login": - t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath) - case wrapInfo.Token == "": - t.Fatal("wrap token is empty") - } - cloned.SetToken(wrapInfo.Token) - secret, err := cloned.Logical().Unwrap("") - if err != nil { - t.Fatal(err) - } - if ahWrapping { - switch { - case secret.Auth == nil: - t.Fatal("unwrap secret auth is nil") - case secret.Auth.ClientToken == "": - t.Fatal("unwrap token is nil") - } - return secret.Auth.ClientToken - } else { - switch { - case secret.Data == nil: - t.Fatal("unwrap secret data is nil") - case secret.Data["token"] == nil: - t.Fatal("unwrap token is nil") - } - return secret.Data["token"].(string) - } - } - time.Sleep(250 * time.Millisecond) - } - } - checkToken() -} - -func TestCertEndToEnd_CertsInConfig(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - coreConfig := &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "cert": vaultcert.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "pki": pki.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - client := cluster.Cores[0].Client - - // ///////////// - // PKI setup - // ///////////// - - // Mount /pki as a root CA - err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "32h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Set the cluster's certificate as the root CA in /pki - pemBundleRootCA := string(cluster.CACertPEM) + string(cluster.CAKeyPEM) - _, err = client.Logical().Write("pki/config/ca", map[string]interface{}{ - "pem_bundle": pemBundleRootCA, - }) - if err != nil { - t.Fatal(err) - } - - // Mount /pki2 to operate as an intermediate CA - err = client.Sys().Mount("pki2", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - DefaultLeaseTTL: "16h", - MaxLeaseTTL: "32h", - }, - }) - if err != nil { - t.Fatal(err) - } - - // Create a CSR for the intermediate CA - secret, err := client.Logical().Write("pki2/intermediate/generate/internal", nil) - if err != nil { - t.Fatal(err) - } - intermediateCSR := secret.Data["csr"].(string) - - // Sign the intermediate CSR using /pki - secret, err = client.Logical().Write("pki/root/sign-intermediate", map[string]interface{}{ - "permitted_dns_domains": ".myvault.com", - "csr": intermediateCSR, - }) - if err != nil { - t.Fatal(err) - } - intermediateCertPEM := secret.Data["certificate"].(string) - - // Configure the intermediate cert as the CA in /pki2 - _, err = client.Logical().Write("pki2/intermediate/set-signed", map[string]interface{}{ - "certificate": intermediateCertPEM, - }) - if err != nil { - t.Fatal(err) - } - - // Create a role on the intermediate CA mount - _, err = client.Logical().Write("pki2/roles/myvault-dot-com", map[string]interface{}{ - "allowed_domains": "myvault.com", - "allow_subdomains": "true", - "max_ttl": "5m", - }) - if err != nil { - t.Fatal(err) - } - - // Issue a leaf cert using the intermediate CA - secret, err = client.Logical().Write("pki2/issue/myvault-dot-com", map[string]interface{}{ - "common_name": "cert.myvault.com", - "format": "pem", - "ip_sans": "127.0.0.1", - }) - if err != nil { - t.Fatal(err) - } - leafCertPEM := secret.Data["certificate"].(string) - leafCertKeyPEM := secret.Data["private_key"].(string) - - // Create temporary files for CA cert, client cert and client cert key. - // This is used to configure TLS in the api client. - caCertFile, err := ioutil.TempFile("", "caCert") - if err != nil { - t.Fatal(err) - } - defer os.Remove(caCertFile.Name()) - if _, err := caCertFile.Write([]byte(cluster.CACertPEM)); err != nil { - t.Fatal(err) - } - if err := caCertFile.Close(); err != nil { - t.Fatal(err) - } - - leafCertFile, err := ioutil.TempFile("", "leafCert") - if err != nil { - t.Fatal(err) - } - defer os.Remove(leafCertFile.Name()) - if _, err := leafCertFile.Write([]byte(leafCertPEM)); err != nil { - t.Fatal(err) - } - if err := leafCertFile.Close(); err != nil { - t.Fatal(err) - } - - leafCertKeyFile, err := ioutil.TempFile("", "leafCertKey") - if err != nil { - t.Fatal(err) - } - defer os.Remove(leafCertKeyFile.Name()) - if _, err := leafCertKeyFile.Write([]byte(leafCertKeyPEM)); err != nil { - t.Fatal(err) - } - if err := leafCertKeyFile.Close(); err != nil { - t.Fatal(err) - } - - // ///////////// - // Cert auth setup - // ///////////// - - // Enable the cert auth method - err = client.Sys().EnableAuthWithOptions("cert", &api.EnableAuthOptions{ - Type: "cert", - }) - if err != nil { - t.Fatal(err) - } - - // Set the intermediate CA cert as a trusted certificate in the backend - _, err = client.Logical().Write("auth/cert/certs/myvault-dot-com", map[string]interface{}{ - "display_name": "myvault.com", - "policies": "default", - "certificate": intermediateCertPEM, - }) - if err != nil { - t.Fatal(err) - } - - // ///////////// - // Auth handler (auto-auth) setup - // ///////////// - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - - am, err := agentcert.NewCertAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.cert"), - MountPath: "auth/cert", - Config: map[string]interface{}{ - "ca_cert": caCertFile.Name(), - "client_cert": leafCertFile.Name(), - "client_key": leafCertKeyFile.Name(), - }, - }) - if err != nil { - t.Fatal(err) - } - - ahConfig := &auth.AuthHandlerConfig{ - Logger: logger.Named("auth.handler"), - Client: client, - EnableReauthOnNewCredentials: true, - } - - ah := auth.NewAuthHandler(ahConfig) - errCh := make(chan error) - go func() { - errCh <- ah.Run(ctx, am) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // ///////////// - // Sink setup - // ///////////// - - // Use TempFile to get us a generated file name to use for the sink. - ouf, err := ioutil.TempFile("", "auth.tokensink.test.") - if err != nil { - t.Fatal(err) - } - ouf.Close() - out := ouf.Name() - os.Remove(out) - t.Logf("output: %s", out) - - config := &sink.SinkConfig{ - Logger: logger.Named("sink.file"), - Config: map[string]interface{}{ - "path": out, - }, - } - fs, err := file.NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = fs - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: logger.Named("sink.server"), - Client: client, - }) - go func() { - errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config}) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // This has to be after the other defers so it happens first. It allows - // successful test runs to immediately cancel all of the runner goroutines - // and unblock any of the blocking defer calls by the runner's DoneCh that - // comes before this and avoid successful tests from taking the entire - // timeout duration. - defer cancel() - - // Read the token from the sink - timeout := time.Now().Add(5 * time.Second) - for { - if time.Now().After(timeout) { - t.Fatal("did not find a written token after timeout") - } - - // Attempt to read the sink file until we get a token or the timeout is - // reached. - val, err := ioutil.ReadFile(out) - if err == nil { - os.Remove(out) - if len(val) == 0 { - t.Fatal("written token was empty") - } - - t.Logf("sink token: %s", val) - - break - } - - time.Sleep(250 * time.Millisecond) - } -} diff --git a/command/agent/config/config_test.go b/command/agent/config/config_test.go deleted file mode 100644 index 4b1ad8756..000000000 --- a/command/agent/config/config_test.go +++ /dev/null @@ -1,2330 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package config - -import ( - "os" - "syscall" - "testing" - "time" - - "github.com/go-test/deep" - ctconfig "github.com/hashicorp/consul-template/config" - "golang.org/x/exp/slices" - - "github.com/hashicorp/vault/command/agentproxyshared" - "github.com/hashicorp/vault/internalshared/configutil" - "github.com/hashicorp/vault/sdk/helper/pointerutil" -) - -func TestLoadConfigFile_AgentCache(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-cache.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - Listeners: []*configutil.Listener{ - { - Type: "unix", - Address: "/path/to/socket", - TLSDisable: true, - SocketMode: "configmode", - SocketUser: "configuser", - SocketGroup: "configgroup", - }, - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - { - Type: "tcp", - Address: "127.0.0.1:3000", - Role: "metrics_only", - TLSDisable: true, - }, - { - Type: "tcp", - Role: "default", - Address: "127.0.0.1:8400", - TLSKeyFile: "/path/to/cakey.pem", - TLSCertFile: "/path/to/cacert.pem", - }, - }, - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - APIProxy: &APIProxy{ - UseAutoAuthToken: true, - ForceAutoAuthToken: false, - }, - Cache: &Cache{ - UseAutoAuthToken: true, - UseAutoAuthTokenRaw: true, - ForceAutoAuthToken: false, - Persist: &agentproxyshared.PersistConfig{ - Type: "kubernetes", - Path: "/vault/agent-cache/", - KeepAfterImport: true, - ExitOnErr: true, - ServiceAccountTokenFile: "/tmp/serviceaccount/token", - }, - }, - Vault: &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", - Retry: &Retry{ - NumRetries: 12, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - - config, err = LoadConfigFile("./test-fixtures/config-cache-embedded-type.hcl") - if err != nil { - t.Fatal(err) - } - expected.Vault.TLSSkipVerifyRaw = interface{}(true) - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigDir_AgentCache(t *testing.T) { - config, err := LoadConfig("./test-fixtures/config-dir-cache/") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - Listeners: []*configutil.Listener{ - { - Type: "unix", - Address: "/path/to/socket", - TLSDisable: true, - SocketMode: "configmode", - SocketUser: "configuser", - SocketGroup: "configgroup", - }, - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - { - Type: "tcp", - Address: "127.0.0.1:3000", - Role: "metrics_only", - TLSDisable: true, - }, - { - Type: "tcp", - Role: "default", - Address: "127.0.0.1:8400", - TLSKeyFile: "/path/to/cakey.pem", - TLSCertFile: "/path/to/cacert.pem", - }, - }, - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - APIProxy: &APIProxy{ - UseAutoAuthToken: true, - ForceAutoAuthToken: false, - }, - Cache: &Cache{ - UseAutoAuthToken: true, - UseAutoAuthTokenRaw: true, - ForceAutoAuthToken: false, - Persist: &agentproxyshared.PersistConfig{ - Type: "kubernetes", - Path: "/vault/agent-cache/", - KeepAfterImport: true, - ExitOnErr: true, - ServiceAccountTokenFile: "/tmp/serviceaccount/token", - }, - }, - Vault: &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", - Retry: &Retry{ - NumRetries: 12, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - - config, err = LoadConfigFile("./test-fixtures/config-dir-cache/config-cache1.hcl") - if err != nil { - t.Fatal(err) - } - config2, err := LoadConfigFile("./test-fixtures/config-dir-cache/config-cache2.hcl") - - mergedConfig := config.Merge(config2) - - mergedConfig.Prune() - if diff := deep.Equal(mergedConfig, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigDir_AutoAuthAndListener(t *testing.T) { - config, err := LoadConfig("./test-fixtures/config-dir-auto-auth-and-listener/") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - - config, err = LoadConfigFile("./test-fixtures/config-dir-auto-auth-and-listener/config1.hcl") - if err != nil { - t.Fatal(err) - } - config2, err := LoadConfigFile("./test-fixtures/config-dir-auto-auth-and-listener/config2.hcl") - - mergedConfig := config.Merge(config2) - - mergedConfig.Prune() - if diff := deep.Equal(mergedConfig, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigDir_VaultBlock(t *testing.T) { - config, err := LoadConfig("./test-fixtures/config-dir-vault-block/") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - Vault: &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", - Retry: &Retry{ - NumRetries: 12, - }, - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - - config, err = LoadConfigFile("./test-fixtures/config-dir-vault-block/config1.hcl") - if err != nil { - t.Fatal(err) - } - config2, err := LoadConfigFile("./test-fixtures/config-dir-vault-block/config2.hcl") - - mergedConfig := config.Merge(config2) - - mergedConfig.Prune() - if diff := deep.Equal(mergedConfig, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_AgentCache_NoListeners(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-cache-no-listeners.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - APIProxy: &APIProxy{ - UseAutoAuthToken: true, - ForceAutoAuthToken: false, - }, - Cache: &Cache{ - UseAutoAuthToken: true, - UseAutoAuthTokenRaw: true, - ForceAutoAuthToken: false, - Persist: &agentproxyshared.PersistConfig{ - Type: "kubernetes", - Path: "/vault/agent-cache/", - KeepAfterImport: true, - ExitOnErr: true, - ServiceAccountTokenFile: "/tmp/serviceaccount/token", - }, - }, - Vault: &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", - Retry: &Retry{ - NumRetries: 12, - }, - }, - Templates: []*ctconfig.TemplateConfig{ - { - Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile(t *testing.T) { - if err := os.Setenv("TEST_AAD_ENV", "aad"); err != nil { - t.Fatal(err) - } - defer func() { - if err := os.Unsetenv("TEST_AAD_ENV"); err != nil { - t.Fatal(err) - } - }() - - config, err := LoadConfigFile("./test-fixtures/config.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - LogFile: "/var/log/vault/vault-agent.log", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - MaxBackoff: 0, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - { - Type: "file", - WrapTTL: 5 * time.Minute, - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath2", - AAD: "aad", - DeriveKey: true, - Config: map[string]interface{}{ - "path": "/tmp/file-bar", - }, - }, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - - config, err = LoadConfigFile("./test-fixtures/config-embedded-type.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Method_Wrapping(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-method-wrapping.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - ExitOnError: false, - WrapTTL: 5 * time.Minute, - MaxBackoff: 2 * time.Minute, - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Method_InitialBackoff(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-method-initial-backoff.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - ExitOnError: false, - WrapTTL: 5 * time.Minute, - MinBackoff: 5 * time.Second, - MaxBackoff: 2 * time.Minute, - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Method_ExitOnErr(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-method-exit-on-err.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - ExitOnError: true, - WrapTTL: 5 * time.Minute, - MinBackoff: 5 * time.Second, - MaxBackoff: 2 * time.Minute, - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_AgentCache_NoAutoAuth(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-cache-no-auto_auth.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - Cache: &Cache{}, - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Bad_AgentCache_InconsisentAutoAuth(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/bad-config-cache-inconsistent-auto_auth.hcl") - if err != nil { - t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err) - } - if config == nil { - t.Fatal("config was nil") - } - err = config.ValidateConfig() - if err == nil { - t.Fatal("ValidateConfig should return an error when use_auto_auth_token=true and no auto_auth section present") - } -} - -func TestLoadConfigFile_Bad_AgentCache_ForceAutoAuthNoMethod(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/bad-config-cache-force-token-no-auth-method.hcl") - if err != nil { - t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err) - } - if config == nil { - t.Fatal("config was nil") - } - err = config.ValidateConfig() - if err == nil { - t.Fatal("ValidateConfig should return an error when use_auto_auth_token=force and no auto_auth section present") - } -} - -func TestLoadConfigFile_Bad_AgentCache_NoListeners(t *testing.T) { - _, err := LoadConfigFile("./test-fixtures/bad-config-cache-no-listeners.hcl") - if err != nil { - t.Fatalf("LoadConfigFile should return an error for this config") - } -} - -func TestLoadConfigFile_Bad_AutoAuth_Wrapped_Multiple_Sinks(t *testing.T) { - _, err := LoadConfigFile("./test-fixtures/bad-config-auto_auth-wrapped-multiple-sinks.hcl") - if err == nil { - t.Fatalf("LoadConfigFile should return an error for this config, err: %v", err) - } -} - -func TestLoadConfigFile_Bad_AutoAuth_Nosinks_Nocache_Notemplates(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/bad-config-auto_auth-nosinks-nocache-notemplates.hcl") - if err != nil { - t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err) - } - if config == nil { - t.Fatal("config was nil") - } - err = config.ValidateConfig() - if err == nil { - t.Fatal("ValidateConfig should return an error when auto_auth configured and there are no sinks, caches or templates") - } -} - -func TestLoadConfigFile_Bad_AutoAuth_Both_Wrapping_Types(t *testing.T) { - _, err := LoadConfigFile("./test-fixtures/bad-config-method-wrapping-and-sink-wrapping.hcl") - if err == nil { - t.Fatalf("LoadConfigFile should return an error for this config") - } -} - -func TestLoadConfigFile_Bad_AgentCache_AutoAuth_Method_wrapping(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/bad-config-cache-auto_auth-method-wrapping.hcl") - if err != nil { - t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err) - } - if config == nil { - t.Fatal("config was nil") - } - err = config.ValidateConfig() - if err == nil { - t.Fatal("ValidateConfig should return an error when auth_auth.method.wrap_ttl nonzero and cache.use_auto_auth_token=true") - } -} - -func TestLoadConfigFile_Bad_APIProxy_And_Cache_Same_Config(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/bad-config-api_proxy-cache.hcl") - if err != nil { - t.Fatalf("LoadConfigFile should not return an error for this config, err: %v", err) - } - if config == nil { - t.Fatal("config was nil") - } - err = config.ValidateConfig() - if err == nil { - t.Fatal("ValidateConfig should return an error when cache and api_proxy try and configure the same value") - } -} - -func TestLoadConfigFile_AgentCache_AutoAuth_NoSink(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-cache-auto_auth-no-sink.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - }, - APIProxy: &APIProxy{ - UseAutoAuthToken: true, - ForceAutoAuthToken: false, - }, - Cache: &Cache{ - UseAutoAuthToken: true, - UseAutoAuthTokenRaw: true, - ForceAutoAuthToken: false, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_AgentCache_AutoAuth_Force(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-cache-auto_auth-force.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - }, - APIProxy: &APIProxy{ - UseAutoAuthToken: true, - ForceAutoAuthToken: true, - }, - Cache: &Cache{ - UseAutoAuthToken: true, - UseAutoAuthTokenRaw: "force", - ForceAutoAuthToken: true, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_AgentCache_AutoAuth_True(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-cache-auto_auth-true.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - }, - APIProxy: &APIProxy{ - UseAutoAuthToken: true, - ForceAutoAuthToken: false, - }, - Cache: &Cache{ - UseAutoAuthToken: true, - UseAutoAuthTokenRaw: "true", - ForceAutoAuthToken: false, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Agent_AutoAuth_APIProxyAllConfig(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-api_proxy-auto_auth-all-api_proxy-config.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - }, - APIProxy: &APIProxy{ - UseAutoAuthToken: true, - UseAutoAuthTokenRaw: "force", - ForceAutoAuthToken: true, - EnforceConsistency: "always", - WhenInconsistent: "forward", - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_AgentCache_AutoAuth_False(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-cache-auto_auth-false.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Cache: &Cache{ - UseAutoAuthToken: false, - UseAutoAuthTokenRaw: "false", - ForceAutoAuthToken: false, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_AgentCache_Persist(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-cache-persist-false.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - Cache: &Cache{ - Persist: &agentproxyshared.PersistConfig{ - Type: "kubernetes", - Path: "/vault/agent-cache/", - KeepAfterImport: false, - ExitOnErr: false, - ServiceAccountTokenFile: "", - }, - }, - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_AgentCache_PersistMissingType(t *testing.T) { - _, err := LoadConfigFile("./test-fixtures/config-cache-persist-empty-type.hcl") - if err == nil || os.IsNotExist(err) { - t.Fatal("expected error or file is missing") - } -} - -func TestLoadConfigFile_TemplateConfig(t *testing.T) { - testCases := map[string]struct { - fixturePath string - expectedTemplateConfig TemplateConfig - }{ - "set-true": { - "./test-fixtures/config-template_config.hcl", - TemplateConfig{ - ExitOnRetryFailure: true, - StaticSecretRenderInt: 1 * time.Minute, - }, - }, - "empty": { - "./test-fixtures/config-template_config-empty.hcl", - TemplateConfig{ - ExitOnRetryFailure: false, - }, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - config, err := LoadConfigFile(tc.fixturePath) - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{}, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - NumRetries: 5, - }, - }, - TemplateConfig: &tc.expectedTemplateConfig, - Templates: []*ctconfig.TemplateConfig{ - { - Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - }) - } -} - -// TestLoadConfigFile_Template tests template definitions in Vault Agent -func TestLoadConfigFile_Template(t *testing.T) { - testCases := map[string]struct { - fixturePath string - expectedTemplates []*ctconfig.TemplateConfig - }{ - "min": { - fixturePath: "./test-fixtures/config-template-min.hcl", - expectedTemplates: []*ctconfig.TemplateConfig{ - { - Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), - }, - }, - }, - "full": { - fixturePath: "./test-fixtures/config-template-full.hcl", - expectedTemplates: []*ctconfig.TemplateConfig{ - { - Backup: pointerutil.BoolPtr(true), - Command: []string{"restart service foo"}, - CommandTimeout: pointerutil.TimeDurationPtr("60s"), - Contents: pointerutil.StringPtr("{{ keyOrDefault \"service/redis/maxconns@east-aws\" \"5\" }}"), - CreateDestDirs: pointerutil.BoolPtr(true), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), - ErrMissingKey: pointerutil.BoolPtr(true), - LeftDelim: pointerutil.StringPtr("<<"), - Perms: pointerutil.FileModePtr(0o655), - RightDelim: pointerutil.StringPtr(">>"), - SandboxPath: pointerutil.StringPtr("/path/on/disk/where"), - Exec: &ctconfig.ExecConfig{ - Command: []string{"foo"}, - Timeout: pointerutil.TimeDurationPtr("10s"), - }, - - Wait: &ctconfig.WaitConfig{ - Min: pointerutil.TimeDurationPtr("10s"), - Max: pointerutil.TimeDurationPtr("40s"), - }, - }, - }, - }, - "many": { - fixturePath: "./test-fixtures/config-template-many.hcl", - expectedTemplates: []*ctconfig.TemplateConfig{ - { - Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), - ErrMissingKey: pointerutil.BoolPtr(false), - CreateDestDirs: pointerutil.BoolPtr(true), - Command: []string{"restart service foo"}, - Perms: pointerutil.FileModePtr(0o600), - }, - { - Source: pointerutil.StringPtr("/path/on/disk/to/template2.ctmpl"), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render2.txt"), - Backup: pointerutil.BoolPtr(true), - Perms: pointerutil.FileModePtr(0o755), - Wait: &ctconfig.WaitConfig{ - Min: pointerutil.TimeDurationPtr("2s"), - Max: pointerutil.TimeDurationPtr("10s"), - }, - }, - }, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - config, err := LoadConfigFile(tc.fixturePath) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Templates: tc.expectedTemplates, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - }) - } -} - -// TestLoadConfigFile_Template_NoSinks tests template definitions without sinks in Vault Agent -func TestLoadConfigFile_Template_NoSinks(t *testing.T) { - testCases := map[string]struct { - fixturePath string - expectedTemplates []*ctconfig.TemplateConfig - }{ - "min": { - fixturePath: "./test-fixtures/config-template-min-nosink.hcl", - expectedTemplates: []*ctconfig.TemplateConfig{ - { - Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), - }, - }, - }, - "full": { - fixturePath: "./test-fixtures/config-template-full-nosink.hcl", - expectedTemplates: []*ctconfig.TemplateConfig{ - { - Backup: pointerutil.BoolPtr(true), - Command: []string{"restart service foo"}, - CommandTimeout: pointerutil.TimeDurationPtr("60s"), - Contents: pointerutil.StringPtr("{{ keyOrDefault \"service/redis/maxconns@east-aws\" \"5\" }}"), - CreateDestDirs: pointerutil.BoolPtr(true), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), - ErrMissingKey: pointerutil.BoolPtr(true), - LeftDelim: pointerutil.StringPtr("<<"), - Perms: pointerutil.FileModePtr(0o655), - RightDelim: pointerutil.StringPtr(">>"), - SandboxPath: pointerutil.StringPtr("/path/on/disk/where"), - - Wait: &ctconfig.WaitConfig{ - Min: pointerutil.TimeDurationPtr("10s"), - Max: pointerutil.TimeDurationPtr("40s"), - }, - }, - }, - }, - "many": { - fixturePath: "./test-fixtures/config-template-many-nosink.hcl", - expectedTemplates: []*ctconfig.TemplateConfig{ - { - Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), - ErrMissingKey: pointerutil.BoolPtr(false), - CreateDestDirs: pointerutil.BoolPtr(true), - Command: []string{"restart service foo"}, - Perms: pointerutil.FileModePtr(0o600), - }, - { - Source: pointerutil.StringPtr("/path/on/disk/to/template2.ctmpl"), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render2.txt"), - Backup: pointerutil.BoolPtr(true), - Perms: pointerutil.FileModePtr(0o755), - Wait: &ctconfig.WaitConfig{ - Min: pointerutil.TimeDurationPtr("2s"), - Max: pointerutil.TimeDurationPtr("10s"), - }, - }, - }, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - config, err := LoadConfigFile(tc.fixturePath) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: nil, - }, - Templates: tc.expectedTemplates, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - }) - } -} - -// TestLoadConfigFile_Template_WithCache tests ensures that cache {} stanza is -// permitted in vault agent configuration with template(s) -func TestLoadConfigFile_Template_WithCache(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-template-with-cache.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - }, - Cache: &Cache{}, - Templates: []*ctconfig.TemplateConfig{ - { - Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"), - Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Vault_Retry(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-vault-retry.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - NumRetries: 5, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Vault_Retry_Empty(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-vault-retry-empty.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_EnforceConsistency(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-consistency.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - PidFile: "", - }, - Cache: &Cache{ - EnforceConsistency: "always", - WhenInconsistent: "retry", - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_EnforceConsistency_APIProxy(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-consistency-apiproxy.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - }, - PidFile: "", - }, - APIProxy: &APIProxy{ - EnforceConsistency: "always", - WhenInconsistent: "retry", - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Idle_Conns_All(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-all.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableIdleConns: []string{"auto-auth", "caching", "templating", "proxying"}, - DisableIdleConnsAPIProxy: true, - DisableIdleConnsAutoAuth: true, - DisableIdleConnsTemplating: true, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Idle_Conns_Auto_Auth(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-auto-auth.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableIdleConns: []string{"auto-auth"}, - DisableIdleConnsAPIProxy: false, - DisableIdleConnsAutoAuth: true, - DisableIdleConnsTemplating: false, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Idle_Conns_Templating(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-templating.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableIdleConns: []string{"templating"}, - DisableIdleConnsAPIProxy: false, - DisableIdleConnsAutoAuth: false, - DisableIdleConnsTemplating: true, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Idle_Conns_Caching(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-caching.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableIdleConns: []string{"caching"}, - DisableIdleConnsAPIProxy: true, - DisableIdleConnsAutoAuth: false, - DisableIdleConnsTemplating: false, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Idle_Conns_Proxying(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-proxying.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableIdleConns: []string{"proxying"}, - DisableIdleConnsAPIProxy: true, - DisableIdleConnsAutoAuth: false, - DisableIdleConnsTemplating: false, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Idle_Conns_Empty(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-empty.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableIdleConns: []string{}, - DisableIdleConnsAPIProxy: false, - DisableIdleConnsAutoAuth: false, - DisableIdleConnsTemplating: false, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Idle_Conns_Env(t *testing.T) { - err := os.Setenv(DisableIdleConnsEnv, "auto-auth,caching,templating") - defer os.Unsetenv(DisableIdleConnsEnv) - - if err != nil { - t.Fatal(err) - } - config, err := LoadConfigFile("./test-fixtures/config-disable-idle-connections-empty.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableIdleConns: []string{"auto-auth", "caching", "templating"}, - DisableIdleConnsAPIProxy: true, - DisableIdleConnsAutoAuth: true, - DisableIdleConnsTemplating: true, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Bad_Value_Disable_Idle_Conns(t *testing.T) { - _, err := LoadConfigFile("./test-fixtures/bad-config-disable-idle-connections.hcl") - if err == nil { - t.Fatal("should have error, it didn't") - } -} - -func TestLoadConfigFile_Disable_Keep_Alives_All(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-all.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableKeepAlives: []string{"auto-auth", "caching", "templating", "proxying"}, - DisableKeepAlivesAPIProxy: true, - DisableKeepAlivesAutoAuth: true, - DisableKeepAlivesTemplating: true, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Keep_Alives_Auto_Auth(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-auto-auth.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableKeepAlives: []string{"auto-auth"}, - DisableKeepAlivesAPIProxy: false, - DisableKeepAlivesAutoAuth: true, - DisableKeepAlivesTemplating: false, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Keep_Alives_Templating(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-templating.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableKeepAlives: []string{"templating"}, - DisableKeepAlivesAPIProxy: false, - DisableKeepAlivesAutoAuth: false, - DisableKeepAlivesTemplating: true, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Keep_Alives_Caching(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-caching.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableKeepAlives: []string{"caching"}, - DisableKeepAlivesAPIProxy: true, - DisableKeepAlivesAutoAuth: false, - DisableKeepAlivesTemplating: false, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Keep_Alives_Proxying(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-proxying.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableKeepAlives: []string{"proxying"}, - DisableKeepAlivesAPIProxy: true, - DisableKeepAlivesAutoAuth: false, - DisableKeepAlivesTemplating: false, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Keep_Alives_Empty(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-empty.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableKeepAlives: []string{}, - DisableKeepAlivesAPIProxy: false, - DisableKeepAlivesAutoAuth: false, - DisableKeepAlivesTemplating: false, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Disable_Keep_Alives_Env(t *testing.T) { - err := os.Setenv(DisableKeepAlivesEnv, "auto-auth,caching,templating") - defer os.Unsetenv(DisableKeepAlivesEnv) - - if err != nil { - t.Fatal(err) - } - config, err := LoadConfigFile("./test-fixtures/config-disable-keep-alives-empty.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - }, - DisableKeepAlives: []string{"auto-auth", "caching", "templating"}, - DisableKeepAlivesAPIProxy: true, - DisableKeepAlivesAutoAuth: true, - DisableKeepAlivesTemplating: true, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Namespace: "my-namespace/", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - Vault: &Vault{ - Address: "http://127.0.0.1:1111", - Retry: &Retry{ - ctconfig.DefaultRetryAttempts, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestLoadConfigFile_Bad_Value_Disable_Keep_Alives(t *testing.T) { - _, err := LoadConfigFile("./test-fixtures/bad-config-disable-keep-alives.hcl") - if err == nil { - t.Fatal("should have error, it didn't") - } -} - -// TestLoadConfigFile_EnvTemplates_Simple loads and validates an env_template config -func TestLoadConfigFile_EnvTemplates_Simple(t *testing.T) { - cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-simple.hcl") - if err != nil { - t.Fatalf("error loading config file: %s", err) - } - - if err := cfg.ValidateConfig(); err != nil { - t.Fatalf("validation error: %s", err) - } - - expectedKey := "MY_DATABASE_USER" - found := false - for _, envTemplate := range cfg.EnvTemplates { - if *envTemplate.MapToEnvironmentVariable == expectedKey { - found = true - } - } - if !found { - t.Fatalf("expected environment variable name to be populated") - } -} - -// TestLoadConfigFile_EnvTemplates_Complex loads and validates an env_template config -func TestLoadConfigFile_EnvTemplates_Complex(t *testing.T) { - cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-complex.hcl") - if err != nil { - t.Fatalf("error loading config file: %s", err) - } - - if err := cfg.ValidateConfig(); err != nil { - t.Fatalf("validation error: %s", err) - } - - expectedKeys := []string{ - "FOO_PASSWORD", - "FOO_USER", - } - - envExists := func(key string) bool { - for _, envTmpl := range cfg.EnvTemplates { - if *envTmpl.MapToEnvironmentVariable == key { - return true - } - } - return false - } - - for _, expected := range expectedKeys { - if !envExists(expected) { - t.Fatalf("expected environment variable %s", expected) - } - } -} - -// TestLoadConfigFile_EnvTemplates_WithSource loads and validates an -// env_template config with "source" instead of "contents" -func TestLoadConfigFile_EnvTemplates_WithSource(t *testing.T) { - cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-with-source.hcl") - if err != nil { - t.Fatalf("error loading config file: %s", err) - } - - if err := cfg.ValidateConfig(); err != nil { - t.Fatalf("validation error: %s", err) - } -} - -// TestLoadConfigFile_EnvTemplates_NoName ensures that env_template with no name triggers an error -func TestLoadConfigFile_EnvTemplates_NoName(t *testing.T) { - _, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-no-name.hcl") - if err == nil { - t.Fatalf("expected error") - } -} - -// TestLoadConfigFile_EnvTemplates_ExecInvalidSignal ensures that an invalid signal triggers an error -func TestLoadConfigFile_EnvTemplates_ExecInvalidSignal(t *testing.T) { - _, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-invalid-signal.hcl") - if err == nil { - t.Fatalf("expected error") - } -} - -// TestLoadConfigFile_EnvTemplates_ExecSimple validates the exec section with default parameters -func TestLoadConfigFile_EnvTemplates_ExecSimple(t *testing.T) { - cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-simple.hcl") - if err != nil { - t.Fatalf("error loading config file: %s", err) - } - - if err := cfg.ValidateConfig(); err != nil { - t.Fatalf("validation error: %s", err) - } - - expectedCmd := []string{"/path/to/my/app", "arg1", "arg2"} - if !slices.Equal(cfg.Exec.Command, expectedCmd) { - t.Fatal("exec.command does not have expected value") - } - - // check defaults - if cfg.Exec.RestartOnSecretChanges != "always" { - t.Fatalf("expected cfg.Exec.RestartOnSecretChanges to be 'always', got '%s'", cfg.Exec.RestartOnSecretChanges) - } - - if cfg.Exec.RestartStopSignal != syscall.SIGTERM { - t.Fatalf("expected cfg.Exec.RestartStopSignal to be 'syscall.SIGTERM', got '%s'", cfg.Exec.RestartStopSignal) - } -} - -// TestLoadConfigFile_EnvTemplates_ExecComplex validates the exec section with non-default parameters -func TestLoadConfigFile_EnvTemplates_ExecComplex(t *testing.T) { - cfg, err := LoadConfigFile("./test-fixtures/config-env-templates-complex.hcl") - if err != nil { - t.Fatalf("error loading config file: %s", err) - } - - if err := cfg.ValidateConfig(); err != nil { - t.Fatalf("validation error: %s", err) - } - - if !slices.Equal(cfg.Exec.Command, []string{"env"}) { - t.Fatal("exec.command does not have expected value") - } - - if cfg.Exec.RestartOnSecretChanges != "never" { - t.Fatalf("expected cfg.Exec.RestartOnSecretChanges to be 'never', got %q", cfg.Exec.RestartOnSecretChanges) - } - - if cfg.Exec.RestartStopSignal != syscall.SIGINT { - t.Fatalf("expected cfg.Exec.RestartStopSignal to be 'syscall.SIGINT', got %q", cfg.Exec.RestartStopSignal) - } -} - -// TestLoadConfigFile_Bad_EnvTemplates_MissingExec ensures that ValidateConfig -// errors when "env_template" stanza(s) are specified but "exec" is missing -func TestLoadConfigFile_Bad_EnvTemplates_MissingExec(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-missing-exec.hcl") - if err != nil { - t.Fatalf("error loading config file: %s", err) - } - - if err := config.ValidateConfig(); err == nil { - t.Fatal("expected an error from ValidateConfig: exec section is missing") - } -} - -// TestLoadConfigFile_Bad_EnvTemplates_WithProxy ensures that ValidateConfig -// errors when both env_template and api_proxy stanzas are present -func TestLoadConfigFile_Bad_EnvTemplates_WithProxy(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-with-proxy.hcl") - if err != nil { - t.Fatalf("error loading config file: %s", err) - } - - if err := config.ValidateConfig(); err == nil { - t.Fatal("expected an error from ValidateConfig: listener / api_proxy are not compatible with env_template") - } -} - -// TestLoadConfigFile_Bad_EnvTemplates_WithFileTemplates ensures that -// ValidateConfig errors when both env_template and template stanzas are present -func TestLoadConfigFile_Bad_EnvTemplates_WithFileTemplates(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-with-file-templates.hcl") - if err != nil { - t.Fatalf("error loading config file: %s", err) - } - - if err := config.ValidateConfig(); err == nil { - t.Fatal("expected an error from ValidateConfig: file template stanza is not compatible with env_template") - } -} - -// TestLoadConfigFile_Bad_EnvTemplates_DisalowedFields ensure that -// ValidateConfig errors for disalowed env_template fields -func TestLoadConfigFile_Bad_EnvTemplates_DisalowedFields(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/bad-config-env-templates-disalowed-fields.hcl") - if err != nil { - t.Fatalf("error loading config file: %s", err) - } - - if err := config.ValidateConfig(); err == nil { - t.Fatal("expected an error from ValidateConfig: disallowed fields specified in env_template") - } -} diff --git a/command/agent/config/test-fixtures/bad-config-api_proxy-cache.hcl b/command/agent/config/test-fixtures/bad-config-api_proxy-cache.hcl deleted file mode 100644 index b35a06f28..000000000 --- a/command/agent/config/test-fixtures/bad-config-api_proxy-cache.hcl +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } -} - -cache { - use_auto_auth_token = true -} - -api_proxy { - use_auto_auth_token = "force" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} diff --git a/command/agent/config/test-fixtures/bad-config-auto_auth-nosinks-nocache-notemplates.hcl b/command/agent/config/test-fixtures/bad-config-auto_auth-nosinks-nocache-notemplates.hcl deleted file mode 100644 index 1d618befa..000000000 --- a/command/agent/config/test-fixtures/bad-config-auto_auth-nosinks-nocache-notemplates.hcl +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method "aws" { - mount_path = "auth/aws" - config = { - role = "foobar" - } - } -} diff --git a/command/agent/config/test-fixtures/bad-config-auto_auth-wrapped-multiple-sinks.hcl b/command/agent/config/test-fixtures/bad-config-auto_auth-wrapped-multiple-sinks.hcl deleted file mode 100644 index eea90860c..000000000 --- a/command/agent/config/test-fixtures/bad-config-auto_auth-wrapped-multiple-sinks.hcl +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method "aws" { - mount_path = "auth/aws" - wrap_ttl = 300 - config = { - role = "foobar" - } - } - - sink "file" { - config = { - path = "/tmp/file-foo" - } - } - - sink "file" { - config = { - path = "/tmp/file-bar" - } - } -} diff --git a/command/agent/config/test-fixtures/bad-config-cache-auto_auth-method-wrapping.hcl b/command/agent/config/test-fixtures/bad-config-cache-auto_auth-method-wrapping.hcl deleted file mode 100644 index e90ba98bb..000000000 --- a/command/agent/config/test-fixtures/bad-config-cache-auto_auth-method-wrapping.hcl +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - wrap_ttl = 300 - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - } -} - -cache { - use_auto_auth_token = true -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} - - diff --git a/command/agent/config/test-fixtures/bad-config-cache-force-token-no-auth-method.hcl b/command/agent/config/test-fixtures/bad-config-cache-force-token-no-auth-method.hcl deleted file mode 100644 index 39f2bc740..000000000 --- a/command/agent/config/test-fixtures/bad-config-cache-force-token-no-auth-method.hcl +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -cache { - use_auto_auth_token = "force" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} diff --git a/command/agent/config/test-fixtures/bad-config-cache-inconsistent-auto_auth.hcl b/command/agent/config/test-fixtures/bad-config-cache-inconsistent-auto_auth.hcl deleted file mode 100644 index f5d39af89..000000000 --- a/command/agent/config/test-fixtures/bad-config-cache-inconsistent-auto_auth.hcl +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -cache { - use_auto_auth_token = true -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} - - diff --git a/command/agent/config/test-fixtures/bad-config-cache-no-listeners.hcl b/command/agent/config/test-fixtures/bad-config-cache-no-listeners.hcl deleted file mode 100644 index 46f280268..000000000 --- a/command/agent/config/test-fixtures/bad-config-cache-no-listeners.hcl +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -cache { -} - diff --git a/command/agent/config/test-fixtures/bad-config-disable-idle-connections.hcl b/command/agent/config/test-fixtures/bad-config-disable-idle-connections.hcl deleted file mode 100644 index 10b7e54a7..000000000 --- a/command/agent/config/test-fixtures/bad-config-disable-idle-connections.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_idle_connections = ["foo","caching","templating"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/bad-config-disable-keep-alives.hcl b/command/agent/config/test-fixtures/bad-config-disable-keep-alives.hcl deleted file mode 100644 index 47f1eb2e5..000000000 --- a/command/agent/config/test-fixtures/bad-config-disable-keep-alives.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_keep_alives = ["foo","caching","templating"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/bad-config-env-templates-disalowed-fields.hcl b/command/agent/config/test-fixtures/bad-config-env-templates-disalowed-fields.hcl deleted file mode 100644 index 4355fd078..000000000 --- a/command/agent/config/test-fixtures/bad-config-env-templates-disalowed-fields.hcl +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -auto_auth { - - method { - type = "token_file" - - config { - token_file_path = "/Users/avean/.vault-token" - } - } -} - -template_config { - static_secret_render_interval = "5m" - exit_on_retry_failure = true -} - -vault { - address = "http://localhost:8200" -} - -env_template "FOO_PASSWORD" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.password }}{{ end }}" - - # Error: destination and create_dest_dirs are not allowed in env_template - destination = "/path/on/disk/where/template/will/render.txt" - create_dest_dirs = true -} - -exec { - command = ["./my-app", "arg1", "arg2"] - restart_on_secret_changes = "always" - restart_stop_signal = "SIGTERM" -} diff --git a/command/agent/config/test-fixtures/bad-config-env-templates-invalid-signal.hcl b/command/agent/config/test-fixtures/bad-config-env-templates-invalid-signal.hcl deleted file mode 100644 index 7cbbc0931..000000000 --- a/command/agent/config/test-fixtures/bad-config-env-templates-invalid-signal.hcl +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -auto_auth { - - method { - type = "token_file" - - config { - token_file_path = "/home/username/.vault-token" - } - } -} - -vault { - address = "http://localhost:8200" -} - -env_template "FOO" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.lock }}{{ end }}" - error_on_missing_key = false -} - - -exec { - command = ["env"] - restart_on_secret_changes = "never" - restart_stop_signal = "notasignal" -} diff --git a/command/agent/config/test-fixtures/bad-config-env-templates-missing-exec.hcl b/command/agent/config/test-fixtures/bad-config-env-templates-missing-exec.hcl deleted file mode 100644 index 8fbbd83ba..000000000 --- a/command/agent/config/test-fixtures/bad-config-env-templates-missing-exec.hcl +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -auto_auth { - - method { - type = "token_file" - - config { - token_file_path = "/Users/avean/.vault-token" - } - } -} - -template_config { - static_secret_render_interval = "5m" - exit_on_retry_failure = true -} - -vault { - address = "http://localhost:8200" -} - -env_template "FOO_PASSWORD" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.password }}{{ end }}" - error_on_missing_key = false -} -env_template "FOO_USER" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.user }}{{ end }}" - error_on_missing_key = false -} - -# Error: missing a required "exec" section! diff --git a/command/agent/config/test-fixtures/bad-config-env-templates-no-name.hcl b/command/agent/config/test-fixtures/bad-config-env-templates-no-name.hcl deleted file mode 100644 index 7c7363a46..000000000 --- a/command/agent/config/test-fixtures/bad-config-env-templates-no-name.hcl +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -auto_auth { - - method { - type = "token_file" - - config { - token_file_path = "/home/username/.vault-token" - } - } -} - -vault { - address = "http://localhost:8200" -} - -env_template { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.lock }}{{ end }}" - error_on_missing_key = false -} - - -exec { - command = ["env"] - restart_on_secret_changes = "never" - restart_stop_signal = "SIGTERM" -} diff --git a/command/agent/config/test-fixtures/bad-config-env-templates-with-file-templates.hcl b/command/agent/config/test-fixtures/bad-config-env-templates-with-file-templates.hcl deleted file mode 100644 index ace9410bd..000000000 --- a/command/agent/config/test-fixtures/bad-config-env-templates-with-file-templates.hcl +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -auto_auth { - - method { - type = "token_file" - - config { - token_file_path = "/Users/avean/.vault-token" - } - } -} - -template_config { - static_secret_render_interval = "5m" - exit_on_retry_failure = true -} - -vault { - address = "http://localhost:8200" -} - -# Error: template is incompatible with env_template! -template { - source = "/path/on/disk/to/template.ctmpl" - destination = "/path/on/disk/where/template/will/render.txt" -} - -env_template "FOO_PASSWORD" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.password }}{{ end }}" - error_on_missing_key = false -} -env_template "FOO_USER" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.user }}{{ end }}" - error_on_missing_key = false -} - -exec { - command = ["./my-app", "arg1", "arg2"] - restart_on_secret_changes = "always" - restart_stop_signal = "SIGTERM" -} diff --git a/command/agent/config/test-fixtures/bad-config-env-templates-with-proxy.hcl b/command/agent/config/test-fixtures/bad-config-env-templates-with-proxy.hcl deleted file mode 100644 index ac0824441..000000000 --- a/command/agent/config/test-fixtures/bad-config-env-templates-with-proxy.hcl +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -auto_auth { - - method { - type = "token_file" - - config { - token_file_path = "/Users/avean/.vault-token" - } - } -} - -template_config { - static_secret_render_interval = "5m" - exit_on_retry_failure = true -} - -vault { - address = "http://localhost:8200" -} - -env_template "FOO_PASSWORD" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.password }}{{ end }}" - error_on_missing_key = false -} -env_template "FOO_USER" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.user }}{{ end }}" - error_on_missing_key = false -} - -exec { - command = ["./my-app", "arg1", "arg2"] - restart_on_secret_changes = "always" - restart_stop_signal = "SIGTERM" -} - -# Error: api_proxy is incompatible with env_template -api_proxy { - use_auto_auth_token = "force" - enforce_consistency = "always" - when_inconsistent = "forward" -} - -# Error: listener is incompatible with env_template -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} diff --git a/command/agent/config/test-fixtures/bad-config-method-wrapping-and-sink-wrapping.hcl b/command/agent/config/test-fixtures/bad-config-method-wrapping-and-sink-wrapping.hcl deleted file mode 100644 index 89c766d5c..000000000 --- a/command/agent/config/test-fixtures/bad-config-method-wrapping-and-sink-wrapping.hcl +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - wrap_ttl = 300 - config = { - role = "foobar" - } - } - - sink { - type = "file" - wrap_ttl = 300 - config = { - path = "/tmp/file-foo" - } - } -} diff --git a/command/agent/config/test-fixtures/config-api_proxy-auto_auth-all-api_proxy-config.hcl b/command/agent/config/test-fixtures/config-api_proxy-auto_auth-all-api_proxy-config.hcl deleted file mode 100644 index 79b200963..000000000 --- a/command/agent/config/test-fixtures/config-api_proxy-auto_auth-all-api_proxy-config.hcl +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } -} - -api_proxy { - use_auto_auth_token = "force" - enforce_consistency = "always" - when_inconsistent = "forward" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} diff --git a/command/agent/config/test-fixtures/config-cache-auto_auth-false.hcl b/command/agent/config/test-fixtures/config-cache-auto_auth-false.hcl deleted file mode 100644 index 7fbaa7418..000000000 --- a/command/agent/config/test-fixtures/config-cache-auto_auth-false.hcl +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -cache { - use_auto_auth_token = "false" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} - diff --git a/command/agent/config/test-fixtures/config-cache-auto_auth-force.hcl b/command/agent/config/test-fixtures/config-cache-auto_auth-force.hcl deleted file mode 100644 index 5d280bd20..000000000 --- a/command/agent/config/test-fixtures/config-cache-auto_auth-force.hcl +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } -} - -cache { - use_auto_auth_token = "force" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} - diff --git a/command/agent/config/test-fixtures/config-cache-auto_auth-no-sink.hcl b/command/agent/config/test-fixtures/config-cache-auto_auth-no-sink.hcl deleted file mode 100644 index e95142743..000000000 --- a/command/agent/config/test-fixtures/config-cache-auto_auth-no-sink.hcl +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } -} - -cache { - use_auto_auth_token = true -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} - diff --git a/command/agent/config/test-fixtures/config-cache-auto_auth-true.hcl b/command/agent/config/test-fixtures/config-cache-auto_auth-true.hcl deleted file mode 100644 index bbc945cca..000000000 --- a/command/agent/config/test-fixtures/config-cache-auto_auth-true.hcl +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } -} - -cache { - use_auto_auth_token = "true" - force_auto_auth_token = false -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} diff --git a/command/agent/config/test-fixtures/config-cache-embedded-type.hcl b/command/agent/config/test-fixtures/config-cache-embedded-type.hcl deleted file mode 100644 index 6661966c2..000000000 --- a/command/agent/config/test-fixtures/config-cache-embedded-type.hcl +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -cache { - use_auto_auth_token = true - persist "kubernetes" { - path = "/vault/agent-cache/" - keep_after_import = true - exit_on_err = true - service_account_token_file = "/tmp/serviceaccount/token" - } -} - -listener { - type = "unix" - address = "/path/to/socket" - tls_disable = true - socket_mode = "configmode" - socket_user = "configuser" - socket_group = "configgroup" -} - -listener { - type = "tcp" - address = "127.0.0.1:8300" - tls_disable = true -} - -listener { - type = "tcp" - address = "127.0.0.1:3000" - tls_disable = true - role = "metrics_only" -} - -listener { - type = "tcp" - role = "default" - address = "127.0.0.1:8400" - tls_key_file = "/path/to/cakey.pem" - tls_cert_file = "/path/to/cacert.pem" -} - -vault { - address = "http://127.0.0.1:1111" - ca_cert = "config_ca_cert" - ca_path = "config_ca_path" - tls_skip_verify = true - client_cert = "config_client_cert" - client_key = "config_client_key" -} diff --git a/command/agent/config/test-fixtures/config-cache-no-auto_auth.hcl b/command/agent/config/test-fixtures/config-cache-no-auto_auth.hcl deleted file mode 100644 index b654e202d..000000000 --- a/command/agent/config/test-fixtures/config-cache-no-auto_auth.hcl +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -cache { -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} - - diff --git a/command/agent/config/test-fixtures/config-cache-no-listeners.hcl b/command/agent/config/test-fixtures/config-cache-no-listeners.hcl deleted file mode 100644 index 0ad5203fd..000000000 --- a/command/agent/config/test-fixtures/config-cache-no-listeners.hcl +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -cache { - use_auto_auth_token = true - persist = { - type = "kubernetes" - path = "/vault/agent-cache/" - keep_after_import = true - exit_on_err = true - service_account_token_file = "/tmp/serviceaccount/token" - } -} - -vault { - address = "http://127.0.0.1:1111" - ca_cert = "config_ca_cert" - ca_path = "config_ca_path" - tls_skip_verify = "true" - client_cert = "config_client_cert" - client_key = "config_client_key" -} - -template { - source = "/path/on/disk/to/template.ctmpl" - destination = "/path/on/disk/where/template/will/render.txt" -} diff --git a/command/agent/config/test-fixtures/config-cache-persist-empty-type.hcl b/command/agent/config/test-fixtures/config-cache-persist-empty-type.hcl deleted file mode 100644 index f85799bb9..000000000 --- a/command/agent/config/test-fixtures/config-cache-persist-empty-type.hcl +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -cache { - persist = { - path = "/vault/agent-cache/" - } -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} diff --git a/command/agent/config/test-fixtures/config-cache-persist-false.hcl b/command/agent/config/test-fixtures/config-cache-persist-false.hcl deleted file mode 100644 index f48dfd857..000000000 --- a/command/agent/config/test-fixtures/config-cache-persist-false.hcl +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -cache { - persist "kubernetes" { - exit_on_err = false - keep_after_import = false - path = "/vault/agent-cache/" - } -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} diff --git a/command/agent/config/test-fixtures/config-cache.hcl b/command/agent/config/test-fixtures/config-cache.hcl deleted file mode 100644 index 148ef6e7c..000000000 --- a/command/agent/config/test-fixtures/config-cache.hcl +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -cache { - use_auto_auth_token = true - persist = { - type = "kubernetes" - path = "/vault/agent-cache/" - keep_after_import = true - exit_on_err = true - service_account_token_file = "/tmp/serviceaccount/token" - } -} - -listener "unix" { - address = "/path/to/socket" - tls_disable = true - socket_mode = "configmode" - socket_user = "configuser" - socket_group = "configgroup" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} - -listener { - type = "tcp" - address = "127.0.0.1:3000" - tls_disable = true - role = "metrics_only" -} - -listener "tcp" { - role = "default" - address = "127.0.0.1:8400" - tls_key_file = "/path/to/cakey.pem" - tls_cert_file = "/path/to/cacert.pem" -} - -vault { - address = "http://127.0.0.1:1111" - ca_cert = "config_ca_cert" - ca_path = "config_ca_path" - tls_skip_verify = "true" - client_cert = "config_client_cert" - client_key = "config_client_key" -} diff --git a/command/agent/config/test-fixtures/config-consistency-apiproxy.hcl b/command/agent/config/test-fixtures/config-consistency-apiproxy.hcl deleted file mode 100644 index 318c36973..000000000 --- a/command/agent/config/test-fixtures/config-consistency-apiproxy.hcl +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -api_proxy { - enforce_consistency = "always" - when_inconsistent = "retry" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} diff --git a/command/agent/config/test-fixtures/config-consistency.hcl b/command/agent/config/test-fixtures/config-consistency.hcl deleted file mode 100644 index 7ed752297..000000000 --- a/command/agent/config/test-fixtures/config-consistency.hcl +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -cache { - enforce_consistency = "always" - when_inconsistent = "retry" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} diff --git a/command/agent/config/test-fixtures/config-dir-auto-auth-and-listener/config1.hcl b/command/agent/config/test-fixtures/config-dir-auto-auth-and-listener/config1.hcl deleted file mode 100644 index 1bd5e93c7..000000000 --- a/command/agent/config/test-fixtures/config-dir-auto-auth-and-listener/config1.hcl +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} \ No newline at end of file diff --git a/command/agent/config/test-fixtures/config-dir-auto-auth-and-listener/config2.hcl b/command/agent/config/test-fixtures/config-dir-auto-auth-and-listener/config2.hcl deleted file mode 100644 index b5d7425eb..000000000 --- a/command/agent/config/test-fixtures/config-dir-auto-auth-and-listener/config2.hcl +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} \ No newline at end of file diff --git a/command/agent/config/test-fixtures/config-dir-cache/config-cache1.hcl b/command/agent/config/test-fixtures/config-dir-cache/config-cache1.hcl deleted file mode 100644 index f65b4c6dd..000000000 --- a/command/agent/config/test-fixtures/config-dir-cache/config-cache1.hcl +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -listener "unix" { - address = "/path/to/socket" - tls_disable = true - socket_mode = "configmode" - socket_user = "configuser" - socket_group = "configgroup" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} - -listener { - type = "tcp" - address = "127.0.0.1:3000" - tls_disable = true - role = "metrics_only" -} - -listener "tcp" { - role = "default" - address = "127.0.0.1:8400" - tls_key_file = "/path/to/cakey.pem" - tls_cert_file = "/path/to/cacert.pem" -} \ No newline at end of file diff --git a/command/agent/config/test-fixtures/config-dir-cache/config-cache2.hcl b/command/agent/config/test-fixtures/config-dir-cache/config-cache2.hcl deleted file mode 100644 index 57929cd55..000000000 --- a/command/agent/config/test-fixtures/config-dir-cache/config-cache2.hcl +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -cache { - use_auto_auth_token = true - persist = { - type = "kubernetes" - path = "/vault/agent-cache/" - keep_after_import = true - exit_on_err = true - service_account_token_file = "/tmp/serviceaccount/token" - } -} - -vault { - address = "http://127.0.0.1:1111" - ca_cert = "config_ca_cert" - ca_path = "config_ca_path" - tls_skip_verify = "true" - client_cert = "config_client_cert" - client_key = "config_client_key" -} diff --git a/command/agent/config/test-fixtures/config-dir-vault-block/config1.hcl b/command/agent/config/test-fixtures/config-dir-vault-block/config1.hcl deleted file mode 100644 index b99ee93f9..000000000 --- a/command/agent/config/test-fixtures/config-dir-vault-block/config1.hcl +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -vault { - address = "http://127.0.0.1:1111" - ca_cert = "config_ca_cert" - ca_path = "config_ca_path" - tls_skip_verify = "true" - client_cert = "config_client_cert" - client_key = "config_client_key" -} diff --git a/command/agent/config/test-fixtures/config-dir-vault-block/config2.hcl b/command/agent/config/test-fixtures/config-dir-vault-block/config2.hcl deleted file mode 100644 index 1bd5e93c7..000000000 --- a/command/agent/config/test-fixtures/config-dir-vault-block/config2.hcl +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} \ No newline at end of file diff --git a/command/agent/config/test-fixtures/config-disable-idle-connections-all.hcl b/command/agent/config/test-fixtures/config-disable-idle-connections-all.hcl deleted file mode 100644 index b6869a200..000000000 --- a/command/agent/config/test-fixtures/config-disable-idle-connections-all.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_idle_connections = ["auto-auth","caching","templating","proxying"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-idle-connections-auto-auth.hcl b/command/agent/config/test-fixtures/config-disable-idle-connections-auto-auth.hcl deleted file mode 100644 index 02bda0b0a..000000000 --- a/command/agent/config/test-fixtures/config-disable-idle-connections-auto-auth.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_idle_connections = ["auto-auth"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-idle-connections-caching.hcl b/command/agent/config/test-fixtures/config-disable-idle-connections-caching.hcl deleted file mode 100644 index 624d1bd1c..000000000 --- a/command/agent/config/test-fixtures/config-disable-idle-connections-caching.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_idle_connections = ["caching"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-idle-connections-empty.hcl b/command/agent/config/test-fixtures/config-disable-idle-connections-empty.hcl deleted file mode 100644 index 6b7ac26df..000000000 --- a/command/agent/config/test-fixtures/config-disable-idle-connections-empty.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_idle_connections = [] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-idle-connections-proxying.hcl b/command/agent/config/test-fixtures/config-disable-idle-connections-proxying.hcl deleted file mode 100644 index 2219b84eb..000000000 --- a/command/agent/config/test-fixtures/config-disable-idle-connections-proxying.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_idle_connections = ["proxying"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-idle-connections-templating.hcl b/command/agent/config/test-fixtures/config-disable-idle-connections-templating.hcl deleted file mode 100644 index 4f819c7a4..000000000 --- a/command/agent/config/test-fixtures/config-disable-idle-connections-templating.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_idle_connections = ["templating"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-keep-alives-all.hcl b/command/agent/config/test-fixtures/config-disable-keep-alives-all.hcl deleted file mode 100644 index 356c79ff5..000000000 --- a/command/agent/config/test-fixtures/config-disable-keep-alives-all.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_keep_alives = ["auto-auth","caching","templating","proxying"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-keep-alives-auto-auth.hcl b/command/agent/config/test-fixtures/config-disable-keep-alives-auto-auth.hcl deleted file mode 100644 index a7648c480..000000000 --- a/command/agent/config/test-fixtures/config-disable-keep-alives-auto-auth.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_keep_alives = ["auto-auth"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-keep-alives-caching.hcl b/command/agent/config/test-fixtures/config-disable-keep-alives-caching.hcl deleted file mode 100644 index 4f93218ee..000000000 --- a/command/agent/config/test-fixtures/config-disable-keep-alives-caching.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_keep_alives = ["caching"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-keep-alives-empty.hcl b/command/agent/config/test-fixtures/config-disable-keep-alives-empty.hcl deleted file mode 100644 index b0969776f..000000000 --- a/command/agent/config/test-fixtures/config-disable-keep-alives-empty.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_keep_alives = [] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-keep-alives-proxying.hcl b/command/agent/config/test-fixtures/config-disable-keep-alives-proxying.hcl deleted file mode 100644 index 138254652..000000000 --- a/command/agent/config/test-fixtures/config-disable-keep-alives-proxying.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_keep_alives = ["proxying"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-disable-keep-alives-templating.hcl b/command/agent/config/test-fixtures/config-disable-keep-alives-templating.hcl deleted file mode 100644 index 9e154a9ce..000000000 --- a/command/agent/config/test-fixtures/config-disable-keep-alives-templating.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -disable_keep_alives = ["templating"] - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" -} diff --git a/command/agent/config/test-fixtures/config-embedded-type.hcl b/command/agent/config/test-fixtures/config-embedded-type.hcl deleted file mode 100644 index cf3c182a8..000000000 --- a/command/agent/config/test-fixtures/config-embedded-type.hcl +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -log_file = "/var/log/vault/vault-agent.log" - -auto_auth { - method "aws" { - mount_path = "auth/aws" - namespace = "my-namespace" - config = { - role = "foobar" - } - } - - sink "file" { - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } - - sink "file" { - wrap_ttl = "5m" - aad_env_var = "TEST_AAD_ENV" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath2" - derive_key = true - config = { - path = "/tmp/file-bar" - } - } -} diff --git a/command/agent/config/test-fixtures/config-env-templates-complex.hcl b/command/agent/config/test-fixtures/config-env-templates-complex.hcl deleted file mode 100644 index adcd4b0dc..000000000 --- a/command/agent/config/test-fixtures/config-env-templates-complex.hcl +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -auto_auth { - - method { - type = "token_file" - - config { - token_file_path = "/home/username/.vault-token" - } - } -} - -cache {} - -template_config { - static_secret_render_interval = "5m" - exit_on_retry_failure = true -} - -vault { - address = "http://localhost:8200" -} - -env_template "FOO_PASSWORD" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.password }}{{ end }}" - error_on_missing_key = false -} -env_template "FOO_USER" { - contents = "{{ with secret \"secret/data/foo\" }}{{ .Data.data.user }}{{ end }}" - error_on_missing_key = false -} - -exec { - command = ["env"] - restart_on_secret_changes = "never" - restart_stop_signal = "SIGINT" -} diff --git a/command/agent/config/test-fixtures/config-env-templates-simple.hcl b/command/agent/config/test-fixtures/config-env-templates-simple.hcl deleted file mode 100644 index 3ca1a1909..000000000 --- a/command/agent/config/test-fixtures/config-env-templates-simple.hcl +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -auto_auth { - - method { - type = "token_file" - - config { - token_file_path = "/Users/avean/.vault-token" - } - } -} - -env_template "MY_DATABASE_USER" { - contents = "{{ with secret \"secret/db-secret\" }}{{ .Data.data.user }}{{ end }}" -} - -exec { - command = ["/path/to/my/app", "arg1", "arg2"] -} diff --git a/command/agent/config/test-fixtures/config-env-templates-with-source.hcl b/command/agent/config/test-fixtures/config-env-templates-with-source.hcl deleted file mode 100644 index 7643ff28d..000000000 --- a/command/agent/config/test-fixtures/config-env-templates-with-source.hcl +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -auto_auth { - method { - type = "token_file" - config { - token_file_path = "/home/username/.vault-token" - } - } -} - -env_template "MY_PASSWORD" { - source = "/path/on/disk/to/template.ctmpl" -} - -exec { - command = ["/path/to/my/app", "arg1", "arg2"] -} diff --git a/command/agent/config/test-fixtures/config-method-exit-on-err.hcl b/command/agent/config/test-fixtures/config-method-exit-on-err.hcl deleted file mode 100644 index d6b32c70d..000000000 --- a/command/agent/config/test-fixtures/config-method-exit-on-err.hcl +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - wrap_ttl = 300 - exit_on_err = true - config = { - role = "foobar" - } - max_backoff = "2m" - min_backoff = "5s" - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - } -} diff --git a/command/agent/config/test-fixtures/config-method-initial-backoff.hcl b/command/agent/config/test-fixtures/config-method-initial-backoff.hcl deleted file mode 100644 index a7fbccd4b..000000000 --- a/command/agent/config/test-fixtures/config-method-initial-backoff.hcl +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - wrap_ttl = 300 - config = { - role = "foobar" - } - max_backoff = "2m" - min_backoff = "5s" - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - } -} diff --git a/command/agent/config/test-fixtures/config-method-wrapping.hcl b/command/agent/config/test-fixtures/config-method-wrapping.hcl deleted file mode 100644 index 0012bb570..000000000 --- a/command/agent/config/test-fixtures/config-method-wrapping.hcl +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - wrap_ttl = 300 - config = { - role = "foobar" - } - max_backoff = "2m" - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - } -} diff --git a/command/agent/config/test-fixtures/config-template-full-nosink.hcl b/command/agent/config/test-fixtures/config-template-full-nosink.hcl deleted file mode 100644 index cda6d020c..000000000 --- a/command/agent/config/test-fixtures/config-template-full-nosink.hcl +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - namespace = "/my-namespace" - - config = { - role = "foobar" - } - } -} - -template { - destination = "/path/on/disk/where/template/will/render.txt" - create_dest_dirs = true - contents = "{{ keyOrDefault \"service/redis/maxconns@east-aws\" \"5\" }}" - - command = "restart service foo" - command_timeout = "60s" - - error_on_missing_key = true - perms = 0655 - backup = true - left_delimiter = "<<" - right_delimiter = ">>" - - sandbox_path = "/path/on/disk/where" - wait { - min = "5s" - max = "30s" - } - wait { - min = "10s" - max = "40s" - } -} diff --git a/command/agent/config/test-fixtures/config-template-full.hcl b/command/agent/config/test-fixtures/config-template-full.hcl deleted file mode 100644 index 649510d16..000000000 --- a/command/agent/config/test-fixtures/config-template-full.hcl +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - namespace = "/my-namespace" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - - config = { - path = "/tmp/file-foo" - } - - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -template { - destination = "/path/on/disk/where/template/will/render.txt" - create_dest_dirs = true - contents = "{{ keyOrDefault \"service/redis/maxconns@east-aws\" \"5\" }}" - - command = "restart service foo" - command_timeout = "60s" - - error_on_missing_key = true - perms = 0655 - backup = true - left_delimiter = "<<" - right_delimiter = ">>" - - sandbox_path = "/path/on/disk/where" - wait { - min = "5s" - max = "30s" - } - wait { - min = "10s" - max = "40s" - } - - exec { - command = ["foo"] - timeout = "10s" - } -} \ No newline at end of file diff --git a/command/agent/config/test-fixtures/config-template-many-nosink.hcl b/command/agent/config/test-fixtures/config-template-many-nosink.hcl deleted file mode 100644 index 2882d76de..000000000 --- a/command/agent/config/test-fixtures/config-template-many-nosink.hcl +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - namespace = "/my-namespace" - - config = { - role = "foobar" - } - } -} - -template { - source = "/path/on/disk/to/template.ctmpl" - destination = "/path/on/disk/where/template/will/render.txt" - - create_dest_dirs = true - - command = "restart service foo" - - error_on_missing_key = false - perms = 0600 -} - -template { - source = "/path/on/disk/to/template2.ctmpl" - destination = "/path/on/disk/where/template/will/render2.txt" - - perms = 0755 - - backup = true - - wait { - min = "2s" - max = "10s" - } -} diff --git a/command/agent/config/test-fixtures/config-template-many.hcl b/command/agent/config/test-fixtures/config-template-many.hcl deleted file mode 100644 index 992381704..000000000 --- a/command/agent/config/test-fixtures/config-template-many.hcl +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - namespace = "/my-namespace" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - - config = { - path = "/tmp/file-foo" - } - - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -template { - source = "/path/on/disk/to/template.ctmpl" - destination = "/path/on/disk/where/template/will/render.txt" - - create_dest_dirs = true - - command = "restart service foo" - - error_on_missing_key = false - perms = 0600 -} - -template { - source = "/path/on/disk/to/template2.ctmpl" - destination = "/path/on/disk/where/template/will/render2.txt" - - perms = 0755 - - backup = true - - wait { - min = "2s" - max = "10s" - } -} diff --git a/command/agent/config/test-fixtures/config-template-min-nosink.hcl b/command/agent/config/test-fixtures/config-template-min-nosink.hcl deleted file mode 100644 index 395be10e3..000000000 --- a/command/agent/config/test-fixtures/config-template-min-nosink.hcl +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - namespace = "/my-namespace" - - config = { - role = "foobar" - } - } -} - -template { - source = "/path/on/disk/to/template.ctmpl" - destination = "/path/on/disk/where/template/will/render.txt" -} diff --git a/command/agent/config/test-fixtures/config-template-min.hcl b/command/agent/config/test-fixtures/config-template-min.hcl deleted file mode 100644 index 523a81e46..000000000 --- a/command/agent/config/test-fixtures/config-template-min.hcl +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - namespace = "/my-namespace" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - - config = { - path = "/tmp/file-foo" - } - - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -template { - source = "/path/on/disk/to/template.ctmpl" - destination = "/path/on/disk/where/template/will/render.txt" -} diff --git a/command/agent/config/test-fixtures/config-template-with-cache.hcl b/command/agent/config/test-fixtures/config-template-with-cache.hcl deleted file mode 100644 index 14e8ab119..000000000 --- a/command/agent/config/test-fixtures/config-template-with-cache.hcl +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - namespace = "/my-namespace" - - config = { - role = "foobar" - } - } -} - -cache {} - -template { - source = "/path/on/disk/to/template.ctmpl" - destination = "/path/on/disk/where/template/will/render.txt" -} diff --git a/command/agent/config/test-fixtures/config-template_config-empty.hcl b/command/agent/config/test-fixtures/config-template_config-empty.hcl deleted file mode 100644 index b497032a7..000000000 --- a/command/agent/config/test-fixtures/config-template_config-empty.hcl +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -vault { - address = "http://127.0.0.1:1111" - retry { - num_retries = 5 - } -} - -template_config {} - -template { - source = "/path/on/disk/to/template.ctmpl" - destination = "/path/on/disk/where/template/will/render.txt" -} \ No newline at end of file diff --git a/command/agent/config/test-fixtures/config-template_config.hcl b/command/agent/config/test-fixtures/config-template_config.hcl deleted file mode 100644 index 41759904e..000000000 --- a/command/agent/config/test-fixtures/config-template_config.hcl +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -vault { - address = "http://127.0.0.1:1111" - retry { - num_retries = 5 - } -} - -template_config { - exit_on_retry_failure = true - static_secret_render_interval = 60 -} - -template { - source = "/path/on/disk/to/template.ctmpl" - destination = "/path/on/disk/where/template/will/render.txt" -} diff --git a/command/agent/config/test-fixtures/config-vault-retry-empty.hcl b/command/agent/config/test-fixtures/config-vault-retry-empty.hcl deleted file mode 100644 index b6bf1abe8..000000000 --- a/command/agent/config/test-fixtures/config-vault-retry-empty.hcl +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" - retry {} -} - diff --git a/command/agent/config/test-fixtures/config-vault-retry.hcl b/command/agent/config/test-fixtures/config-vault-retry.hcl deleted file mode 100644 index aedbfdc52..000000000 --- a/command/agent/config/test-fixtures/config-vault-retry.hcl +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - namespace = "my-namespace/" - - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -vault { - address = "http://127.0.0.1:1111" - retry { - num_retries = 5 - } -} diff --git a/command/agent/config/test-fixtures/config.hcl b/command/agent/config/test-fixtures/config.hcl deleted file mode 100644 index f6ca0e684..000000000 --- a/command/agent/config/test-fixtures/config.hcl +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" -log_file = "/var/log/vault/vault-agent.log" - -auto_auth { - method { - type = "aws" - namespace = "/my-namespace" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } - - sink { - type = "file" - wrap_ttl = "5m" - aad_env_var = "TEST_AAD_ENV" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath2" - derive_key = true - config = { - path = "/tmp/file-bar" - } - } -} diff --git a/command/agent/exec/exec_test.go b/command/agent/exec/exec_test.go deleted file mode 100644 index d5f9eed6b..000000000 --- a/command/agent/exec/exec_test.go +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package exec - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "os" - "os/exec" - "path/filepath" - "strconv" - "syscall" - "testing" - "time" - - ctconfig "github.com/hashicorp/consul-template/config" - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-retryablehttp" - - "github.com/hashicorp/vault/command/agent/config" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/helper/pointerutil" -) - -func fakeVaultServer(t *testing.T) *httptest.Server { - t.Helper() - - firstRequest := true - - mux := http.NewServeMux() - mux.HandleFunc("/v1/kv/my-app/creds", func(w http.ResponseWriter, r *http.Request) { - // change the password on the second request to re-render the template - var password string - - if firstRequest { - password = "s3cr3t" - } else { - password = "s3cr3t-two" - } - - firstRequest = false - - fmt.Fprintf(w, `{ - "request_id": "8af096e9-518c-7351-eff5-5ba20554b21f", - "lease_id": "", - "renewable": false, - "lease_duration": 0, - "data": { - "data": { - "password": "%s", - "user": "app-user" - }, - "metadata": { - "created_time": "2019-10-07T22:18:44.233247Z", - "deletion_time": "", - "destroyed": false, - "version": 3 - } - }, - "wrap_info": null, - "warnings": null, - "auth": null - }`, - password, - ) - }) - - return httptest.NewServer(mux) -} - -// TestExecServer_Run tests various scenarios of using vault agent as a process -// supervisor. At its core is a sample application referred to as 'test app', -// compiled from ./test-app/main.go. Each test case verifies that the test app -// is started and/or stopped correctly by exec.Server.Run(). There are 3 -// high-level scenarios we want to test for: -// -// 1. test app is started and is injected with environment variables -// 2. test app exits early (either with zero or non-zero extit code) -// 3. test app needs to be stopped (and restarted) by exec.Server -func TestExecServer_Run(t *testing.T) { - // we must build a test-app binary since 'go run' does not propagate signals correctly - goBinary, err := exec.LookPath("go") - if err != nil { - t.Fatalf("could not find go binary on path: %s", err) - } - - testAppBinary := filepath.Join(os.TempDir(), "test-app") - - if err := exec.Command(goBinary, "build", "-o", testAppBinary, "./test-app").Run(); err != nil { - t.Fatalf("could not build the test application: %s", err) - } - defer func() { - if err := os.Remove(testAppBinary); err != nil { - t.Fatalf("could not remove %q test application: %s", testAppBinary, err) - } - }() - - testCases := map[string]struct { - // skip this test case - skip bool - skipReason string - - // inputs to the exec server - envTemplates []*ctconfig.TemplateConfig - staticSecretRenderInterval time.Duration - - // test app parameters - testAppArgs []string - testAppStopSignal os.Signal - testAppPort int - - // simulate a shutdown of agent, which, in turn stops the test app - simulateShutdown bool - simulateShutdownWaitDuration time.Duration - - // expected results - expected map[string]string - expectedTestDuration time.Duration - expectedError error - }{ - "ensure_environment_variables_are_injected": { - skip: true, - envTemplates: []*ctconfig.TemplateConfig{{ - Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`), - MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"), - }, { - Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.password }}{{ end }}`), - MapToEnvironmentVariable: pointerutil.StringPtr("MY_PASSWORD"), - }}, - testAppArgs: []string{"--stop-after", "10s"}, - testAppStopSignal: syscall.SIGTERM, - testAppPort: 34001, - expected: map[string]string{ - "MY_USER": "app-user", - "MY_PASSWORD": "s3cr3t", - }, - expectedTestDuration: 15 * time.Second, - expectedError: nil, - }, - - "password_changes_test_app_should_restart": { - envTemplates: []*ctconfig.TemplateConfig{{ - Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`), - MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"), - }, { - Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.password }}{{ end }}`), - MapToEnvironmentVariable: pointerutil.StringPtr("MY_PASSWORD"), - }}, - staticSecretRenderInterval: 5 * time.Second, - testAppArgs: []string{"--stop-after", "15s", "--sleep-after-stop-signal", "0s"}, - testAppStopSignal: syscall.SIGTERM, - testAppPort: 34002, - expected: map[string]string{ - "MY_USER": "app-user", - "MY_PASSWORD": "s3cr3t-two", - }, - expectedTestDuration: 15 * time.Second, - expectedError: nil, - }, - - "test_app_exits_early": { - skip: true, - envTemplates: []*ctconfig.TemplateConfig{{ - Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`), - MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"), - }}, - testAppArgs: []string{"--stop-after", "1s"}, - testAppStopSignal: syscall.SIGTERM, - testAppPort: 34003, - expectedTestDuration: 15 * time.Second, - expectedError: &ProcessExitError{0}, - }, - - "test_app_exits_early_non_zero": { - skip: true, - envTemplates: []*ctconfig.TemplateConfig{{ - Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`), - MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"), - }}, - testAppArgs: []string{"--stop-after", "1s", "--exit-code", "5"}, - testAppStopSignal: syscall.SIGTERM, - testAppPort: 34004, - expectedTestDuration: 15 * time.Second, - expectedError: &ProcessExitError{5}, - }, - - "send_sigterm_expect_test_app_exit": { - skip: true, - envTemplates: []*ctconfig.TemplateConfig{{ - Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`), - MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"), - }}, - testAppArgs: []string{"--stop-after", "30s", "--sleep-after-stop-signal", "1s"}, - testAppStopSignal: syscall.SIGTERM, - testAppPort: 34005, - simulateShutdown: true, - simulateShutdownWaitDuration: 3 * time.Second, - expectedTestDuration: 15 * time.Second, - expectedError: nil, - }, - - "send_sigusr1_expect_test_app_exit": { - skip: true, - envTemplates: []*ctconfig.TemplateConfig{{ - Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`), - MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"), - }}, - testAppArgs: []string{"--stop-after", "30s", "--sleep-after-stop-signal", "1s", "--use-sigusr1"}, - testAppStopSignal: syscall.SIGUSR1, - testAppPort: 34006, - simulateShutdown: true, - simulateShutdownWaitDuration: 3 * time.Second, - expectedTestDuration: 15 * time.Second, - expectedError: nil, - }, - - "test_app_ignores_stop_signal": { - skip: true, - skipReason: "This test currently fails with 'go test -race' (see hashicorp/consul-template/issues/1753).", - envTemplates: []*ctconfig.TemplateConfig{{ - Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`), - MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"), - }}, - testAppArgs: []string{"--stop-after", "60s", "--sleep-after-stop-signal", "60s"}, - testAppStopSignal: syscall.SIGTERM, - testAppPort: 34007, - simulateShutdown: true, - simulateShutdownWaitDuration: 32 * time.Second, // the test app should be stopped immediately after 30s - expectedTestDuration: 45 * time.Second, - expectedError: nil, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - if testCase.skip { - t.Skip(testCase.skipReason) - } - - t.Logf("test case %s: begin", name) - defer t.Logf("test case %s: end", name) - - fakeVault := fakeVaultServer(t) - defer fakeVault.Close() - - ctx, cancelContextFunc := context.WithTimeout(context.Background(), testCase.expectedTestDuration) - defer cancelContextFunc() - - testAppCommand := []string{ - testAppBinary, - "--port", - strconv.Itoa(testCase.testAppPort), - } - - execServer := NewServer(&ServerConfig{ - Logger: logging.NewVaultLogger(hclog.Trace), - AgentConfig: &config.Config{ - Vault: &config.Vault{ - Address: fakeVault.URL, - Retry: &config.Retry{ - NumRetries: 3, - }, - }, - Exec: &config.ExecConfig{ - RestartOnSecretChanges: "always", - Command: append(testAppCommand, testCase.testAppArgs...), - RestartStopSignal: testCase.testAppStopSignal, - }, - EnvTemplates: testCase.envTemplates, - TemplateConfig: &config.TemplateConfig{ - ExitOnRetryFailure: true, - StaticSecretRenderInt: testCase.staticSecretRenderInterval, - }, - }, - LogLevel: hclog.Trace, - LogWriter: hclog.DefaultOutput, - }) - - // start the exec server - var ( - execServerErrCh = make(chan error) - execServerTokenCh = make(chan string, 1) - ) - go func() { - execServerErrCh <- execServer.Run(ctx, execServerTokenCh) - }() - - // send a dummy token to kick off the server - execServerTokenCh <- "my-token" - - // ensure the test app is running after 3 seconds - var ( - testAppAddr = fmt.Sprintf("http://localhost:%d", testCase.testAppPort) - testAppStartedCh = make(chan error) - ) - if testCase.expectedError == nil { - time.AfterFunc(500*time.Millisecond, func() { - _, err := retryablehttp.Head(testAppAddr) - testAppStartedCh <- err - }) - } - - select { - case <-ctx.Done(): - t.Fatal("timeout reached before templates were rendered") - - case err := <-execServerErrCh: - if testCase.expectedError == nil && err != nil { - t.Fatalf("exec server did not expect an error, got: %v", err) - } - - if errors.Is(err, testCase.expectedError) { - t.Fatalf("exec server expected error %v; got %v", testCase.expectedError, err) - } - - t.Log("exec server exited without an error") - - return - - case err := <-testAppStartedCh: - if testCase.expectedError == nil && err != nil { - t.Fatalf("test app could not be started") - } - - t.Log("test app started successfully") - } - - // expect the test app to restart after staticSecretRenderInterval + debounce timer due to a password change - if testCase.staticSecretRenderInterval != 0 { - t.Logf("sleeping for %v to wait for application restart", testCase.staticSecretRenderInterval+5*time.Second) - time.Sleep(testCase.staticSecretRenderInterval + 5*time.Second) - } - - // simulate a shutdown of agent, which, in turn stops the test app - if testCase.simulateShutdown { - cancelContextFunc() - - time.Sleep(testCase.simulateShutdownWaitDuration) - - // check if the test app is still alive - if _, err := http.Head(testAppAddr); err == nil { - t.Fatalf("the test app is still alive %v after a simulated shutdown!", testCase.simulateShutdownWaitDuration) - } - - return - } - - // verify the environment variables - t.Logf("verifying test-app's environment variables") - - resp, err := retryablehttp.Get(testAppAddr) - if err != nil { - t.Fatalf("error making request to the test app: %s", err) - } - defer resp.Body.Close() - - decoder := json.NewDecoder(resp.Body) - var response struct { - EnvironmentVariables map[string]string `json:"environment_variables"` - ProcessID int `json:"process_id"` - } - if err := decoder.Decode(&response); err != nil { - t.Fatalf("unable to parse response from test app: %s", err) - } - - for key, expectedValue := range testCase.expected { - actualValue, ok := response.EnvironmentVariables[key] - if !ok { - t.Fatalf("expected the test app to return %q environment variable", key) - } - if expectedValue != actualValue { - t.Fatalf("expected environment variable %s to have a value of %q but it has a value of %q", key, expectedValue, actualValue) - } - } - }) - } -} diff --git a/command/agent/exec/test-app/main.go b/command/agent/exec/test-app/main.go deleted file mode 100644 index 5c42cf71c..000000000 --- a/command/agent/exec/test-app/main.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package main - -// This is a test application that is used by TestExecServer_Run to verify -// the behavior of vault agent running as a process supervisor. -// -// The app will automatically exit after 1 minute or the --stop-after interval, -// whichever comes first. It also can serve its loaded environment variables on -// the given --port. This app will also return the given --exit-code and -// terminate on SIGTERM unless --use-sigusr1 is specified. - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "log" - "net/http" - "os" - "os/signal" - "strings" - "syscall" - "time" -) - -var ( - port uint - ignoreStopSignal bool - sleepAfterStopSignal time.Duration - useSigusr1StopSignal bool - stopAfter time.Duration - exitCode int -) - -func init() { - flag.UintVar(&port, "port", 34000, "port to run the test app on") - flag.DurationVar(&sleepAfterStopSignal, "sleep-after-stop-signal", 1*time.Second, "time to sleep after getting the signal before exiting") - flag.BoolVar(&useSigusr1StopSignal, "use-sigusr1", false, "use SIGUSR1 as the stop signal, instead of the default SIGTERM") - flag.DurationVar(&stopAfter, "stop-after", 0, "stop the process after duration (overrides all other flags if set)") - flag.IntVar(&exitCode, "exit-code", 0, "exit code to return when this script exits") -} - -type Response struct { - EnvironmentVariables map[string]string `json:"environment_variables"` - ProcessID int `json:"process_id"` -} - -func newResponse() Response { - respEnv := make(map[string]string, len(os.Environ())) - for _, envVar := range os.Environ() { - tokens := strings.Split(envVar, "=") - respEnv[tokens[0]] = tokens[1] - } - - return Response{ - EnvironmentVariables: respEnv, - ProcessID: os.Getpid(), - } -} - -func handler(w http.ResponseWriter, r *http.Request) { - var buf bytes.Buffer - encoder := json.NewEncoder(&buf) - if r.URL.Query().Get("pretty") == "1" { - encoder.SetIndent("", " ") - } - if err := encoder.Encode(newResponse()); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(buf.Bytes()) -} - -func main() { - logger := log.New(os.Stderr, "test-app: ", log.LstdFlags) - - if err := run(logger); err != nil { - log.Fatalf("error: %v\n", err) - } - - logger.Printf("exit code: %d\n", exitCode) - - os.Exit(exitCode) -} - -func run(logger *log.Logger) error { - /* */ logger.Println("run: started") - defer logger.Println("run: done") - - ctx, cancelContextFunc := context.WithTimeout(context.Background(), 60*time.Second) - defer cancelContextFunc() - - flag.Parse() - - server := http.Server{ - Addr: fmt.Sprintf(":%d", port), - Handler: http.HandlerFunc(handler), - ReadTimeout: 20 * time.Second, - WriteTimeout: 20 * time.Second, - IdleTimeout: 20 * time.Second, - } - - doneCh := make(chan struct{}) - - go func() { - defer close(doneCh) - - stopSignal := make(chan os.Signal, 1) - if useSigusr1StopSignal { - signal.Notify(stopSignal, syscall.SIGUSR1) - } else { - signal.Notify(stopSignal, syscall.SIGTERM) - } - - select { - case <-ctx.Done(): - logger.Println("context done: exiting") - - case s := <-stopSignal: - logger.Printf("signal %q: received\n", s) - - if sleepAfterStopSignal > 0 { - logger.Printf("signal %q: sleeping for %v simulate cleanup\n", s, sleepAfterStopSignal) - time.Sleep(sleepAfterStopSignal) - } - - case <-time.After(stopAfter): - logger.Printf("stopping after: %v\n", stopAfter) - } - - if err := server.Shutdown(context.Background()); err != nil { - log.Printf("server shutdown error: %v", err) - } - }() - - logger.Printf("server %s: started\n", server.Addr) - - if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { - return fmt.Errorf("could not start the server: %v", err) - } - - logger.Printf("server %s: done\n", server.Addr) - - <-doneCh - - return nil -} diff --git a/command/agent/jwt_end_to_end_test.go b/command/agent/jwt_end_to_end_test.go deleted file mode 100644 index 2fb058ea1..000000000 --- a/command/agent/jwt_end_to_end_test.go +++ /dev/null @@ -1,434 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package agent - -import ( - "context" - "encoding/json" - "fmt" - "os" - "testing" - "time" - - hclog "github.com/hashicorp/go-hclog" - vaultjwt "github.com/hashicorp/vault-plugin-auth-jwt" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agentproxyshared/auth" - agentjwt "github.com/hashicorp/vault/command/agentproxyshared/auth/jwt" - "github.com/hashicorp/vault/command/agentproxyshared/sink" - "github.com/hashicorp/vault/command/agentproxyshared/sink/file" - "github.com/hashicorp/vault/helper/dhutil" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestJWTEndToEnd(t *testing.T) { - t.Parallel() - testCases := []struct { - ahWrapping bool - useSymlink bool - removeJWTAfterReading bool - }{ - {false, false, false}, - {true, false, false}, - {false, true, false}, - {true, true, false}, - {false, false, true}, - {true, false, true}, - {false, true, true}, - {true, true, true}, - } - - for _, tc := range testCases { - tc := tc // capture range variable - t.Run(fmt.Sprintf("ahWrapping=%v, useSymlink=%v, removeJWTAfterReading=%v", tc.ahWrapping, tc.useSymlink, tc.removeJWTAfterReading), func(t *testing.T) { - t.Parallel() - testJWTEndToEnd(t, tc.ahWrapping, tc.useSymlink, tc.removeJWTAfterReading) - }) - } -} - -func testJWTEndToEnd(t *testing.T, ahWrapping, useSymlink, removeJWTAfterReading bool) { - logger := logging.NewVaultLogger(hclog.Trace) - coreConfig := &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "jwt": vaultjwt.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - client := cluster.Cores[0].Client - - // Setup Vault - err := client.Sys().EnableAuthWithOptions("jwt", &api.EnableAuthOptions{ - Type: "jwt", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/jwt/config", map[string]interface{}{ - "bound_issuer": "https://team-vault.auth0.com/", - "jwt_validation_pubkeys": TestECDSAPubKey, - "jwt_supported_algs": "ES256", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/jwt/role/test", map[string]interface{}{ - "role_type": "jwt", - "bound_subject": "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", - "bound_audiences": "https://vault.plugin.auth.jwt.test", - "user_claim": "https://vault/user", - "groups_claim": "https://vault/groups", - "policies": "test", - "period": "3s", - }) - if err != nil { - t.Fatal(err) - } - - // Generate encryption params - pub, pri, err := dhutil.GeneratePublicPrivateKey() - if err != nil { - t.Fatal(err) - } - - // We close these right away because we're just basically testing - // permissions and finding a usable file name - inf, err := os.CreateTemp("", "auth.jwt.test.") - if err != nil { - t.Fatal(err) - } - in := inf.Name() - inf.Close() - os.Remove(in) - symlink, err := os.CreateTemp("", "auth.jwt.symlink.test.") - if err != nil { - t.Fatal(err) - } - symlinkName := symlink.Name() - symlink.Close() - os.Remove(symlinkName) - os.Symlink(in, symlinkName) - t.Logf("input: %s", in) - - ouf, err := os.CreateTemp("", "auth.tokensink.test.") - if err != nil { - t.Fatal(err) - } - out := ouf.Name() - ouf.Close() - os.Remove(out) - t.Logf("output: %s", out) - - dhpathf, err := os.CreateTemp("", "auth.dhpath.test.") - if err != nil { - t.Fatal(err) - } - dhpath := dhpathf.Name() - dhpathf.Close() - os.Remove(dhpath) - - // Write DH public key to file - mPubKey, err := jsonutil.EncodeJSON(&dhutil.PublicKeyInfo{ - Curve25519PublicKey: pub, - }) - if err != nil { - t.Fatal(err) - } - if err := os.WriteFile(dhpath, mPubKey, 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote dh param file", "path", dhpath) - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - - var fileNameToUseAsPath string - if useSymlink { - fileNameToUseAsPath = symlinkName - } else { - fileNameToUseAsPath = in - } - am, err := agentjwt.NewJWTAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.jwt"), - MountPath: "auth/jwt", - Config: map[string]interface{}{ - "path": fileNameToUseAsPath, - "role": "test", - "remove_jwt_after_reading": removeJWTAfterReading, - "remove_jwt_follows_symlinks": true, - "jwt_read_period": "0.5s", - }, - }) - if err != nil { - t.Fatal(err) - } - - ahConfig := &auth.AuthHandlerConfig{ - Logger: logger.Named("auth.handler"), - Client: client, - EnableReauthOnNewCredentials: true, - } - if ahWrapping { - ahConfig.WrapTTL = 10 * time.Second - } - ah := auth.NewAuthHandler(ahConfig) - errCh := make(chan error) - go func() { - errCh <- ah.Run(ctx, am) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - config := &sink.SinkConfig{ - Logger: logger.Named("sink.file"), - AAD: "foobar", - DHType: "curve25519", - DHPath: dhpath, - DeriveKey: true, - Config: map[string]interface{}{ - "path": out, - }, - } - if !ahWrapping { - config.WrapTTL = 10 * time.Second - } - fs, err := file.NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = fs - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: logger.Named("sink.server"), - Client: client, - }) - go func() { - errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config}) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // This has to be after the other defers so it happens first. It allows - // successful test runs to immediately cancel all of the runner goroutines - // and unblock any of the blocking defer calls by the runner's DoneCh that - // comes before this and avoid successful tests from taking the entire - // timeout duration. - defer cancel() - - // Check that no jwt file exists - _, err = os.Lstat(in) - if err == nil { - t.Fatal("expected err") - } - if !os.IsNotExist(err) { - t.Fatal("expected notexist err") - } - _, err = os.Lstat(out) - if err == nil { - t.Fatal("expected err") - } - if !os.IsNotExist(err) { - t.Fatal("expected notexist err") - } - - cloned, err := client.Clone() - if err != nil { - t.Fatal(err) - } - - // Get a token - jwtToken, _ := GetTestJWT(t) - - if err := os.WriteFile(in, []byte(jwtToken), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test jwt", "path", in) - } - - checkToken := func() string { - timeout := time.Now().Add(5 * time.Second) - for { - if time.Now().After(timeout) { - t.Fatal("did not find a written token after timeout") - } - val, err := os.ReadFile(out) - if err == nil { - os.Remove(out) - if len(val) == 0 { - t.Fatal("written token was empty") - } - - // First, ensure JWT has been removed - if removeJWTAfterReading { - _, err = os.Stat(in) - if err == nil { - t.Fatal("no error returned from stat, indicating the jwt is still present") - } - if !os.IsNotExist(err) { - t.Fatalf("unexpected error: %v", err) - } - } else { - _, err := os.Stat(in) - if err != nil { - t.Fatal("JWT file removed despite removeJWTAfterReading being set to false") - } - } - - // First decrypt it - resp := new(dhutil.Envelope) - if err := jsonutil.DecodeJSON(val, resp); err != nil { - continue - } - - shared, err := dhutil.GenerateSharedSecret(pri, resp.Curve25519PublicKey) - if err != nil { - t.Fatal(err) - } - aesKey, err := dhutil.DeriveSharedKey(shared, pub, resp.Curve25519PublicKey) - if err != nil { - t.Fatal(err) - } - if len(aesKey) == 0 { - t.Fatal("got empty aes key") - } - - val, err = dhutil.DecryptAES(aesKey, resp.EncryptedPayload, resp.Nonce, []byte("foobar")) - if err != nil { - t.Fatalf("error: %v\nresp: %v", err, string(val)) - } - - // Now unwrap it - wrapInfo := new(api.SecretWrapInfo) - if err := jsonutil.DecodeJSON(val, wrapInfo); err != nil { - t.Fatal(err) - } - switch { - case wrapInfo.TTL != 10: - t.Fatalf("bad wrap info: %v", wrapInfo.TTL) - case !ahWrapping && wrapInfo.CreationPath != "sys/wrapping/wrap": - t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath) - case ahWrapping && wrapInfo.CreationPath != "auth/jwt/login": - t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath) - case wrapInfo.Token == "": - t.Fatal("wrap token is empty") - } - cloned.SetToken(wrapInfo.Token) - secret, err := cloned.Logical().Unwrap("") - if err != nil { - t.Fatal(err) - } - if ahWrapping { - switch { - case secret.Auth == nil: - t.Fatal("unwrap secret auth is nil") - case secret.Auth.ClientToken == "": - t.Fatal("unwrap token is nil") - } - return secret.Auth.ClientToken - } else { - switch { - case secret.Data == nil: - t.Fatal("unwrap secret data is nil") - case secret.Data["token"] == nil: - t.Fatal("unwrap token is nil") - } - return secret.Data["token"].(string) - } - } - time.Sleep(250 * time.Millisecond) - } - } - origToken := checkToken() - - // We only check this if the renewer is actually renewing for us - if !ahWrapping { - // Period of 3 seconds, so should still be alive after 7 - timeout := time.Now().Add(7 * time.Second) - cloned.SetToken(origToken) - for { - if time.Now().After(timeout) { - break - } - secret, err := cloned.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - ttl, err := secret.Data["ttl"].(json.Number).Int64() - if err != nil { - t.Fatal(err) - } - if ttl > 3 { - t.Fatalf("unexpected ttl: %v", secret.Data["ttl"]) - } - } - } - - // Get another token to test the backend pushing the need to authenticate - // to the handler - jwtToken, _ = GetTestJWT(t) - if err := os.WriteFile(in, []byte(jwtToken), 0o600); err != nil { - t.Fatal(err) - } - - newToken := checkToken() - if newToken == origToken { - t.Fatal("found same token written") - } - - if !ahWrapping { - // Repeat the period test. At the end the old token should have expired and - // the new token should still be alive after 7 - timeout := time.Now().Add(7 * time.Second) - cloned.SetToken(newToken) - for { - if time.Now().After(timeout) { - break - } - secret, err := cloned.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - ttl, err := secret.Data["ttl"].(json.Number).Int64() - if err != nil { - t.Fatal(err) - } - if ttl > 3 { - t.Fatalf("unexpected ttl: %v", secret.Data["ttl"]) - } - } - - cloned.SetToken(origToken) - _, err = cloned.Auth().Token().LookupSelf() - if err == nil { - t.Fatal("expected error") - } - } -} diff --git a/command/agent/template/template_test.go b/command/agent/template/template_test.go deleted file mode 100644 index e03460ec8..000000000 --- a/command/agent/template/template_test.go +++ /dev/null @@ -1,584 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -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 }} -` diff --git a/command/agent/test-fixtures/reload/reload_bar.key b/command/agent/test-fixtures/reload/reload_bar.key deleted file mode 100644 index 10849fbe1..000000000 --- a/command/agent/test-fixtures/reload/reload_bar.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAwF7sRAyUiLcd6es6VeaTRUBOusFFGkmKJ5lU351waCJqXFju -Z6i/SQYNAAnnRgotXSTE1fIPjE2kZNH1hvqE5IpTGgAwy50xpjJrrBBI6e9lyKqj -7T8gLVNBvtC0cpQi+pGrszEI0ckDQCSZHqi/PAzcpmLUgh2KMrgagT+YlN35KHtl -/bQ/Fsn+kqykVqNw69n/CDKNKdDHn1qPwiX9q/fTMj3EG6g+3ntKrUOh8V/gHKPz -q8QGP/wIud2K+tTSorVXr/4zx7xgzlbJkCakzcQQiP6K+paPnDRlE8fK+1gRRyR7 -XCzyp0irUl8G1NjYAR/tVWxiUhlk/jZutb8PpwIDAQABAoIBAEOzJELuindyujxQ -ZD9G3h1I/GwNCFyv9Mbq10u7BIwhUH0fbwdcA7WXQ4v38ERd4IkfH4aLoZ0m1ewF -V/sgvxQO+h/0YTfHImny5KGxOXfaoF92bipYROKuojydBmQsbgLwsRRm9UufCl3Q -g3KewG5JuH112oPQEYq379v8nZ4FxC3Ano1OFBTm9UhHIAX1Dn22kcHOIIw8jCsQ -zp7TZOW+nwtkS41cBwhvV4VIeL6yse2UgbOfRVRwI7B0OtswS5VgW3wysO2mTDKt -V/WCmeht1il/6ZogEHgi/mvDCKpj20wQ1EzGnPdFLdiFJFylf0oufQD/7N/uezbC -is0qJEECgYEA3AE7SeLpe3SZApj2RmE2lcD9/Saj1Y30PznxB7M7hK0sZ1yXEbtS -Qf894iDDD/Cn3ufA4xk/K52CXgAcqvH/h2geG4pWLYsT1mdWhGftprtOMCIvJvzU -8uWJzKdOGVMG7R59wNgEpPDZDpBISjexwQsFo3aw1L/H1/Sa8cdY3a0CgYEA39hB -1oLmGRyE32Q4GF/srG4FqKL1EsbISGDUEYTnaYg2XiM43gu3tC/ikfclk27Jwc2L -m7cA5FxxaEyfoOgfAizfU/uWTAbx9GoXgWsO0hWSN9+YNq61gc5WKoHyrJ/rfrti -y5d7k0OCeBxckLqGDuJqICQ0myiz0El6FU8h5SMCgYEAuhigmiNC9JbwRu40g9v/ -XDVfox9oPmBRVpogdC78DYKeqN/9OZaGQiUxp3GnDni2xyqqUm8srCwT9oeJuF/z -kgpUTV96/hNCuH25BU8UC5Es1jJUSFpdlwjqwx5SRcGhfjnojZMseojwUg1h2MW7 -qls0bc0cTxnaZaYW2qWRWhECgYBrT0cwyQv6GdvxJCBoPwQ9HXmFAKowWC+H0zOX -Onmd8/jsZEJM4J0uuo4Jn8vZxBDg4eL9wVuiHlcXwzP7dYv4BP8DSechh2rS21Ft -b59pQ4IXWw+jl1nYYsyYEDgAXaIN3VNder95N7ICVsZhc6n01MI/qlu1zmt1fOQT -9x2utQKBgHI9SbsfWfbGiu6oLS3+9V1t4dORhj8D8b7z3trvECrD6tPhxoZqtfrH -4apKr3OKRSXk3K+1K6pkMHJHunspucnA1ChXLhzfNF08BSRJkQDGYuaRLS6VGgab -JZTl54bGvO1GkszEBE/9QFcqNVtWGMWXnUPwNNv8t//yJT5rvQil ------END RSA PRIVATE KEY----- diff --git a/command/agent/test-fixtures/reload/reload_bar.pem b/command/agent/test-fixtures/reload/reload_bar.pem deleted file mode 100644 index a8217be5c..000000000 --- a/command/agent/test-fixtures/reload/reload_bar.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDQzCCAiugAwIBAgIULLCz3mZKmg2xy3rWCud0f1zcmBwwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMzEwMDIzNjQ0WhcNMzYw -MzA1MDEzNzE0WjAaMRgwFgYDVQQDEw9iYXIuZXhhbXBsZS5jb20wggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAXuxEDJSItx3p6zpV5pNFQE66wUUaSYon -mVTfnXBoImpcWO5nqL9JBg0ACedGCi1dJMTV8g+MTaRk0fWG+oTkilMaADDLnTGm -MmusEEjp72XIqqPtPyAtU0G+0LRylCL6kauzMQjRyQNAJJkeqL88DNymYtSCHYoy -uBqBP5iU3fkoe2X9tD8Wyf6SrKRWo3Dr2f8IMo0p0MefWo/CJf2r99MyPcQbqD7e -e0qtQ6HxX+Aco/OrxAY//Ai53Yr61NKitVev/jPHvGDOVsmQJqTNxBCI/or6lo+c -NGUTx8r7WBFHJHtcLPKnSKtSXwbU2NgBH+1VbGJSGWT+Nm61vw+nAgMBAAGjgYQw -gYEwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSVoF8F -7qbzSryIFrldurAG78LvSjAfBgNVHSMEGDAWgBRzDNvqF/Tq21OgWs13B5YydZjl -vzAgBgNVHREEGTAXgg9iYXIuZXhhbXBsZS5jb22HBH8AAAEwDQYJKoZIhvcNAQEL -BQADggEBAGmz2N282iT2IaEZvOmzIE4znHGkvoxZmrr/2byq5PskBg9ysyCHfUvw -SFA8U7jWjezKTnGRUu5blB+yZdjrMtB4AePWyEqtkJwVsZ2SPeP+9V2gNYK4iktP -UF3aIgBbAbw8rNuGIIB0T4D+6Zyo9Y3MCygs6/N4bRPZgLhewWn1ilklfnl3eqaC -a+JY1NBuTgCMa28NuC+Hy3mCveqhI8tFNiOthlLdgAEbuQaOuNutAG73utZ2aq6Q -W4pajFm3lEf5zt7Lo6ZCFtY/Q8jjURJ9e4O7VjXcqIhBM5bSMI6+fgQyOH0SLboj -RNanJ2bcyF1iPVyPBGzV3dF0ngYzxEY= ------END CERTIFICATE----- diff --git a/command/agent/test-fixtures/reload/reload_ca.pem b/command/agent/test-fixtures/reload/reload_ca.pem deleted file mode 100644 index 72a74440c..000000000 --- a/command/agent/test-fixtures/reload/reload_ca.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDNTCCAh2gAwIBAgIUBeVo+Ce2BrdRT1cogKvJLtdOky8wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMzEwMDIzNTM4WhcNMzYw -MzA1MDIzNjA4WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAPTQGWPRIOECGeJB6tR/ftvvtioC9f84fY2QdJ5k -JBupXjPAGYKgS4MGzyT5bz9yY400tCtmh6h7p9tZwHl/TElTugtLQ/8ilMbJTiOM -SiyaMDPHiMJJYKTjm9bu6bKeU1qPZ0Cryes4rygbqs7w2XPgA2RxNmDh7JdX7/h+ -VB5onBmv8g4WFSayowGyDcJWWCbu5yv6ZdH1bqQjgRzQ5xp17WXNmvlzdp2vate/ -9UqPdA8sdJzW/91Gvmros0o/FnG7c2pULhk22wFqO8t2HRjKb3nuxALEJvqoPvad -KjpDTaq1L1ZzxcB7wvWyhy/lNLZL7jiNWy0mN1YB0UpSWdECAwEAAaN7MHkwDgYD -VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHMM2+oX9Orb -U6BazXcHljJ1mOW/MB8GA1UdIwQYMBaAFHMM2+oX9OrbU6BazXcHljJ1mOW/MBYG -A1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAp17XsOaT9 -hculRqrFptn3+zkH3HrIckHm+28R5xYT8ASFXFcLFugGizJAXVL5lvsRVRIwCoOX -Nhi8XSNEFP640VbHcEl81I84bbRIIDS+Yheu6JDZGemTaDYLv1J3D5SHwgoM+nyf -oTRgotUCIXcwJHmTpWEUkZFKuqBxsoTGzk0jO8wOP6xoJkzxVVG5PvNxs924rxY8 -Y8iaLdDfMeT7Pi0XIliBa/aSp/iqSW8XKyJl5R5vXg9+DOgZUrVzIxObaF5RBl/a -mJOeklJBdNVzQm5+iMpO42lu0TA9eWtpP+YiUEXU17XDvFeQWOocFbQ1Peo0W895 -XRz2GCwCNyvW ------END CERTIFICATE----- diff --git a/command/agent/test-fixtures/reload/reload_foo.key b/command/agent/test-fixtures/reload/reload_foo.key deleted file mode 100644 index 86e6cce63..000000000 --- a/command/agent/test-fixtures/reload/reload_foo.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpgIBAAKCAQEAzNyVieSti9XBb5/celB5u8YKRJv3mQS9A4/X0mqY1ePznt1i -ilG7OmG0yM2VAk0ceIAQac3Bsn74jxn2cDlrrVniPXcNgYtMtW0kRqNEo4doo4EX -xZguS9vNBu29useHhif1TGX/pA3dgvaVycUCjzTEVk6qI8UEehMK6gEGZb7nOr0A -A9nipSqoeHpDLe3a4KVqj1vtlJKUvD2i1MuBuQ130cB1K9rufLCShGu7mEgzEosc -gr+K3Bf03IejbeVRyIfLtgj1zuvV1katec75UqRA/bsvt5G9JfJqiZ9mwFN0vp3g -Cr7pdQBSBQ2q4yf9s8CuY5c5w9fl3F8f5QFQoQIDAQABAoIBAQCbCb1qNFRa5ZSV -I8i6ELlwMDqJHfhOJ9XcIjpVljLAfNlcu3Ld92jYkCU/asaAjVckotbJG9yhd5Io -yp9E40/oS4P6vGTOS1vsWgMAKoPBtrKsOwCAm+E9q8UIn1fdSS/5ibgM74x+3bds -a62Em8KKGocUQkhk9a+jq1GxMsFisbHRxEHvClLmDMgGnW3FyGmWwT6yZLPSC0ey -szmmjt3ouP8cLAOmSjzcQBMmEZpQMCgR6Qckg6nrLQAGzZyTdCd875wbGA57DpWX -Lssn95+A5EFvr/6b7DkXeIFCrYBFFa+UQN3PWGEQ6Zjmiw4VgV2vO8yX2kCLlUhU -02bL393ZAoGBAPXPD/0yWINbKUPcRlx/WfWQxfz0bu50ytwIXzVK+pRoAMuNqehK -BJ6kNzTTBq40u+IZ4f5jbLDulymR+4zSkirLE7CyWFJOLNI/8K4Pf5DJUgNdrZjJ -LCtP9XRdxiPatQF0NGfdgHlSJh+/CiRJP4AgB17AnB/4z9/M0ZlJGVrzAoGBANVa -69P3Rp/WPBQv0wx6f0tWppJolWekAHKcDIdQ5HdOZE5CPAYSlTrTUW3uJuqMwU2L -M0Er2gIPKWIR5X+9r7Fvu9hQW6l2v3xLlcrGPiapp3STJvuMxzhRAmXmu3bZfVn1 -Vn7Vf1jPULHtTFSlNFEvYG5UJmygK9BeyyVO5KMbAoGBAMCyAibLQPg4jrDUDZSV -gUAwrgUO2ae1hxHWvkxY6vdMUNNByuB+pgB3W4/dnm8Sh/dHsxJpftt1Lqs39ar/ -p/ZEHLt4FCTxg9GOrm7FV4t5RwG8fko36phJpnIC0UFqQltRbYO+8OgqrhhU+u5X -PaCDe0OcWsf1lYAsYGN6GpZhAoGBAMJ5Ksa9+YEODRs1cIFKUyd/5ztC2xRqOAI/ -3WemQ2nAacuvsfizDZVeMzYpww0+maAuBt0btI719PmwaGmkpDXvK+EDdlmkpOwO -FY6MXvBs6fdnfjwCWUErDi2GQFAX9Jt/9oSL5JU1+08DhvUM1QA/V/2Y9KFE6kr3 -bOIn5F4LAoGBAKQzH/AThDGhT3hwr4ktmReF3qKxBgxzjVa8veXtkY5VWwyN09iT -jnTTt6N1CchZoK5WCETjdzNYP7cuBTcV4d3bPNRiJmxXaNVvx3Tlrk98OiffT8Qa -5DO/Wfb43rNHYXBjU6l0n2zWcQ4PUSSbu0P0bM2JTQPRCqSthXvSHw2P ------END RSA PRIVATE KEY----- diff --git a/command/agent/test-fixtures/reload/reload_foo.pem b/command/agent/test-fixtures/reload/reload_foo.pem deleted file mode 100644 index c8b868bcd..000000000 --- a/command/agent/test-fixtures/reload/reload_foo.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDQzCCAiugAwIBAgIUFVW6i/M+yJUsDrXWgRKO/Dnb+L4wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMzEwMDIzNjA1WhcNMzYw -MzA1MDEzNjM1WjAaMRgwFgYDVQQDEw9mb28uZXhhbXBsZS5jb20wggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM3JWJ5K2L1cFvn9x6UHm7xgpEm/eZBL0D -j9fSapjV4/Oe3WKKUbs6YbTIzZUCTRx4gBBpzcGyfviPGfZwOWutWeI9dw2Bi0y1 -bSRGo0Sjh2ijgRfFmC5L280G7b26x4eGJ/VMZf+kDd2C9pXJxQKPNMRWTqojxQR6 -EwrqAQZlvuc6vQAD2eKlKqh4ekMt7drgpWqPW+2UkpS8PaLUy4G5DXfRwHUr2u58 -sJKEa7uYSDMSixyCv4rcF/Tch6Nt5VHIh8u2CPXO69XWRq15zvlSpED9uy+3kb0l -8mqJn2bAU3S+neAKvul1AFIFDarjJ/2zwK5jlznD1+XcXx/lAVChAgMBAAGjgYQw -gYEwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRNJoOJ -dnazDiuqLhV6truQ4cRe9jAfBgNVHSMEGDAWgBRzDNvqF/Tq21OgWs13B5YydZjl -vzAgBgNVHREEGTAXgg9mb28uZXhhbXBsZS5jb22HBH8AAAEwDQYJKoZIhvcNAQEL -BQADggEBAHzv67mtbxMWcuMsxCFBN1PJNAyUDZVCB+1gWhk59EySbVg81hWJDCBy -fl3TKjz3i7wBGAv+C2iTxmwsSJbda22v8JQbuscXIfLFbNALsPzF+J0vxAgJs5Gc -sDbfJ7EQOIIOVKQhHLYnQoLnigSSPc1kd0JjYyHEBjgIaSuXgRRTBAeqLiBMx0yh -RKL1lQ+WoBU/9SXUZZkwokqWt5G7khi5qZkNxVXZCm8VGPg0iywf6gGyhI1SU5S2 -oR219S6kA4JY/stw1qne85/EmHmoImHGt08xex3GoU72jKAjsIpqRWopcD/+uene -Tc9nn3fTQW/Z9fsoJ5iF5OdJnDEswqE= ------END CERTIFICATE----- diff --git a/command/agent/testing.go b/command/agent/testing.go deleted file mode 100644 index f3260c148..000000000 --- a/command/agent/testing.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package agent - -import ( - "bytes" - "crypto/ecdsa" - "crypto/x509" - "encoding/json" - "encoding/pem" - "os" - "testing" - "time" - - "github.com/go-jose/go-jose/v3" - "github.com/go-jose/go-jose/v3/jwt" - - "github.com/hashicorp/vault/sdk/logical" -) - -const envVarRunAccTests = "VAULT_ACC" - -var runAcceptanceTests = os.Getenv(envVarRunAccTests) == "1" - -func GetTestJWT(t *testing.T) (string, *ecdsa.PrivateKey) { - t.Helper() - cl := jwt.Claims{ - Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", - Issuer: "https://team-vault.auth0.com/", - NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), - Audience: jwt.Audience{"https://vault.plugin.auth.jwt.test"}, - } - - privateCl := struct { - User string `json:"https://vault/user"` - Groups []string `json:"https://vault/groups"` - }{ - "jeff", - []string{"foo", "bar"}, - } - - var key *ecdsa.PrivateKey - block, _ := pem.Decode([]byte(TestECDSAPrivKey)) - if block != nil { - var err error - key, err = x509.ParseECPrivateKey(block.Bytes) - if err != nil { - t.Fatal(err) - } - } - - sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: key}, (&jose.SignerOptions{}).WithType("JWT")) - if err != nil { - t.Fatal(err) - } - - raw, err := jwt.Signed(sig).Claims(cl).Claims(privateCl).CompactSerialize() - if err != nil { - t.Fatal(err) - } - - return raw, key -} - -func readToken(fileName string) (*logical.HTTPWrapInfo, error) { - b, err := os.ReadFile(fileName) - if err != nil { - return nil, err - } - wrapper := &logical.HTTPWrapInfo{} - if err := json.NewDecoder(bytes.NewReader(b)).Decode(wrapper); err != nil { - return nil, err - } - return wrapper, nil -} - -const ( - TestECDSAPrivKey string = `-----BEGIN EC PRIVATE KEY----- -MHcCAQEEIKfldwWLPYsHjRL9EVTsjSbzTtcGRu6icohNfIqcb6A+oAoGCCqGSM49 -AwEHoUQDQgAE4+SFvPwOy0miy/FiTT05HnwjpEbSq+7+1q9BFxAkzjgKnlkXk5qx -hzXQvRmS4w9ZsskoTZtuUI+XX7conJhzCQ== ------END EC PRIVATE KEY-----` - - TestECDSAPubKey string = `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4+SFvPwOy0miy/FiTT05HnwjpEbS -q+7+1q9BFxAkzjgKnlkXk5qxhzXQvRmS4w9ZsskoTZtuUI+XX7conJhzCQ== ------END PUBLIC KEY-----` -) diff --git a/command/agent/token_file_end_to_end_test.go b/command/agent/token_file_end_to_end_test.go deleted file mode 100644 index 1784d8cf9..000000000 --- a/command/agent/token_file_end_to_end_test.go +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package agent - -import ( - "context" - "os" - "path/filepath" - "testing" - "time" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agentproxyshared/auth" - token_file "github.com/hashicorp/vault/command/agentproxyshared/auth/token-file" - "github.com/hashicorp/vault/command/agentproxyshared/sink" - "github.com/hashicorp/vault/command/agentproxyshared/sink/file" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/vault" -) - -func TestTokenFileEndToEnd(t *testing.T) { - var err error - logger := logging.NewVaultLogger(log.Trace) - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: log.NewNullLogger(), - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - client := cores[0].Client - - secret, err := client.Auth().Token().Create(nil) - if err != nil || secret == nil { - t.Fatal(err) - } - - tokenFile, err := os.Create(filepath.Join(t.TempDir(), "token_file")) - if err != nil { - t.Fatal(err) - } - tokenFileName := tokenFile.Name() - tokenFile.Close() // WriteFile doesn't need it open - os.WriteFile(tokenFileName, []byte(secret.Auth.ClientToken), 0o666) - defer os.Remove(tokenFileName) - - ahConfig := &auth.AuthHandlerConfig{ - Logger: logger.Named("auth.handler"), - Client: client, - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - - am, err := token_file.NewTokenFileAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.method"), - Config: map[string]interface{}{ - "token_file_path": tokenFileName, - }, - }) - if err != nil { - t.Fatal(err) - } - - ah := auth.NewAuthHandler(ahConfig) - errCh := make(chan error) - go func() { - errCh <- ah.Run(ctx, am) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // We close these right away because we're just basically testing - // permissions and finding a usable file name - sinkFile, err := os.Create(filepath.Join(t.TempDir(), "auth.tokensink.test.")) - if err != nil { - t.Fatal(err) - } - tokenSinkFileName := sinkFile.Name() - sinkFile.Close() - os.Remove(tokenSinkFileName) - t.Logf("output: %s", tokenSinkFileName) - - config := &sink.SinkConfig{ - Logger: logger.Named("sink.file"), - Config: map[string]interface{}{ - "path": tokenSinkFileName, - }, - WrapTTL: 10 * time.Second, - } - - fs, err := file.NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = fs - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: logger.Named("sink.server"), - Client: client, - }) - go func() { - errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config}) - }() - defer func() { - select { - case <-ctx.Done(): - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - }() - - // This has to be after the other defers, so it happens first. It allows - // successful test runs to immediately cancel all of the runner goroutines - // and unblock any of the blocking defer calls by the runner's DoneCh that - // comes before this and avoid successful tests from taking the entire - // timeout duration. - defer cancel() - - if stat, err := os.Lstat(tokenSinkFileName); err == nil { - t.Fatalf("expected err but got %s", stat) - } else if !os.IsNotExist(err) { - t.Fatal("expected notexist err") - } - - // Wait 2 seconds for the env variables to be detected and an auth to be generated. - time.Sleep(time.Second * 2) - - token, err := readToken(tokenSinkFileName) - if err != nil { - t.Fatal(err) - } - - if token.Token == "" { - t.Fatal("expected token but didn't receive it") - } - - _, err = os.Stat(tokenFileName) - if err != nil { - t.Fatal("Token file removed") - } -} diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go deleted file mode 100644 index 6e7df6ec3..000000000 --- a/command/agent_generate_config_test.go +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "context" - "reflect" - "regexp" - "testing" - "time" -) - -// TestConstructTemplates tests the construcTemplates helper function -func TestConstructTemplates(t *testing.T) { - ctx, cancelContextFunc := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelContextFunc() - - client, closer := testVaultServerWithSecrets(ctx, t) - defer closer() - - cases := map[string]struct { - paths []string - expected []generatedConfigEnvTemplate - expectedError bool - }{ - "kv-v1-simple": { - paths: []string{"kv-v1/foo"}, - expected: []generatedConfigEnvTemplate{ - {Contents: `{{ with secret "kv-v1/foo" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"}, - {Contents: `{{ with secret "kv-v1/foo" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"}, - }, - expectedError: false, - }, - - "kv-v2-simple": { - paths: []string{"kv-v2/foo"}, - expected: []generatedConfigEnvTemplate{ - {Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"}, - {Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"}, - }, - expectedError: false, - }, - - "kv-v2-data-in-path": { - paths: []string{"kv-v2/data/foo"}, - expected: []generatedConfigEnvTemplate{ - {Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"}, - {Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"}, - }, - expectedError: false, - }, - - "kv-v1-nested": { - paths: []string{"kv-v1/app-1/*"}, - expected: []generatedConfigEnvTemplate{ - {Contents: `{{ with secret "kv-v1/app-1/bar" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_PASSWORD"}, - {Contents: `{{ with secret "kv-v1/app-1/bar" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_USER"}, - {Contents: `{{ with secret "kv-v1/app-1/foo" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"}, - {Contents: `{{ with secret "kv-v1/app-1/foo" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"}, - {Contents: `{{ with secret "kv-v1/app-1/nested/baz" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAZ_PASSWORD"}, - {Contents: `{{ with secret "kv-v1/app-1/nested/baz" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAZ_USER"}, - }, - expectedError: false, - }, - - "kv-v2-nested": { - paths: []string{"kv-v2/app-1/*"}, - expected: []generatedConfigEnvTemplate{ - {Contents: `{{ with secret "kv-v2/data/app-1/bar" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_PASSWORD"}, - {Contents: `{{ with secret "kv-v2/data/app-1/bar" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_USER"}, - {Contents: `{{ with secret "kv-v2/data/app-1/foo" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"}, - {Contents: `{{ with secret "kv-v2/data/app-1/foo" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"}, - {Contents: `{{ with secret "kv-v2/data/app-1/nested/baz" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAZ_PASSWORD"}, - {Contents: `{{ with secret "kv-v2/data/app-1/nested/baz" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAZ_USER"}, - }, - expectedError: false, - }, - - "kv-v1-multi-path": { - paths: []string{"kv-v1/foo", "kv-v1/app-1/bar"}, - expected: []generatedConfigEnvTemplate{ - {Contents: `{{ with secret "kv-v1/foo" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"}, - {Contents: `{{ with secret "kv-v1/foo" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"}, - {Contents: `{{ with secret "kv-v1/app-1/bar" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_PASSWORD"}, - {Contents: `{{ with secret "kv-v1/app-1/bar" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_USER"}, - }, - expectedError: false, - }, - - "kv-v2-multi-path": { - paths: []string{"kv-v2/foo", "kv-v2/app-1/bar"}, - expected: []generatedConfigEnvTemplate{ - {Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"}, - {Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"}, - {Contents: `{{ with secret "kv-v2/data/app-1/bar" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_PASSWORD"}, - {Contents: `{{ with secret "kv-v2/data/app-1/bar" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_USER"}, - }, - expectedError: false, - }, - - "kv-v1-path-not-found": { - paths: []string{"kv-v1/does/not/exist"}, - expected: nil, - expectedError: true, - }, - - "kv-v2-path-not-found": { - paths: []string{"kv-v2/does/not/exist"}, - expected: nil, - expectedError: true, - }, - - "kv-v1-early-wildcard": { - paths: []string{"kv-v1/*/foo"}, - expected: nil, - expectedError: true, - }, - - "kv-v2-early-wildcard": { - paths: []string{"kv-v2/*/foo"}, - expected: nil, - expectedError: true, - }, - } - - for name, tc := range cases { - name, tc := name, tc - - t.Run(name, func(t *testing.T) { - templates, err := constructTemplates(ctx, client, tc.paths) - - if tc.expectedError { - if err == nil { - t.Fatal("an error was expected but the test succeeded") - } - } else { - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(tc.expected, templates) { - t.Fatalf("unexpected output; want: %v, got: %v", tc.expected, templates) - } - } - }) - } -} - -// TestGenerateConfiguration tests the generateConfiguration helper function -func TestGenerateConfiguration(t *testing.T) { - ctx, cancelContextFunc := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelContextFunc() - - client, closer := testVaultServerWithSecrets(ctx, t) - defer closer() - - cases := map[string]struct { - flagExec string - flagPaths []string - expected *regexp.Regexp - expectedError bool - }{ - "kv-v1-simple": { - flagExec: "./my-app arg1 arg2", - flagPaths: []string{"kv-v1/foo"}, - expected: regexp.MustCompile(` -auto_auth \{ - - method \{ - type = "token_file" - - config \{ - token_file_path = ".*/.vault-token" - } - } -} - -template_config \{ - static_secret_render_interval = "5m" - exit_on_retry_failure = true -} - -vault \{ - address = "https://127.0.0.1:[0-9]{5}" -} - -env_template "FOO_PASSWORD" \{ - contents = "\{\{ with secret \\"kv-v1/foo\\" }}\{\{ .Data.password }}\{\{ end }}" - error_on_missing_key = true -} -env_template "FOO_USER" \{ - contents = "\{\{ with secret \\"kv-v1/foo\\" }}\{\{ .Data.user }}\{\{ end }}" - error_on_missing_key = true -} - -exec \{ - command = \["./my-app", "arg1", "arg2"\] - restart_on_secret_changes = "always" - restart_stop_signal = "SIGTERM" -} -`), - expectedError: false, - }, - - "kv-v2-default-exec": { - flagExec: "", - flagPaths: []string{"kv-v2/foo"}, - expected: regexp.MustCompile(` -auto_auth \{ - - method \{ - type = "token_file" - - config \{ - token_file_path = ".*/.vault-token" - } - } -} - -template_config \{ - static_secret_render_interval = "5m" - exit_on_retry_failure = true -} - -vault \{ - address = "https://127.0.0.1:[0-9]{5}" -} - -env_template "FOO_PASSWORD" \{ - contents = "\{\{ with secret \\"kv-v2/data/foo\\" }}\{\{ .Data.data.password }}\{\{ end }}" - error_on_missing_key = true -} -env_template "FOO_USER" \{ - contents = "\{\{ with secret \\"kv-v2/data/foo\\" }}\{\{ .Data.data.user }}\{\{ end }}" - error_on_missing_key = true -} - -exec \{ - command = \["env"\] - restart_on_secret_changes = "always" - restart_stop_signal = "SIGTERM" -} -`), - expectedError: false, - }, - } - - for name, tc := range cases { - name, tc := name, tc - - t.Run(name, func(t *testing.T) { - var config bytes.Buffer - - c, err := generateConfiguration(ctx, client, tc.flagExec, tc.flagPaths) - c.WriteTo(&config) - - if tc.expectedError { - if err == nil { - t.Fatal("an error was expected but the test succeeded") - } - } else { - if err != nil { - t.Fatal(err) - } - - if !tc.expected.MatchString(config.String()) { - t.Fatalf("unexpected output; want: %v, got: %v", tc.expected.String(), config.String()) - } - } - }) - } -} diff --git a/command/agent_test.go b/command/agent_test.go deleted file mode 100644 index c49f9db9e..000000000 --- a/command/agent_test.go +++ /dev/null @@ -1,3225 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bufio" - "crypto/tls" - "crypto/x509" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "os" - "path/filepath" - "reflect" - "strings" - "sync" - "testing" - "time" - - "github.com/hashicorp/go-hclog" - vaultjwt "github.com/hashicorp/vault-plugin-auth-jwt" - logicalKv "github.com/hashicorp/vault-plugin-secrets-kv" - "github.com/hashicorp/vault/api" - credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - "github.com/hashicorp/vault/command/agent" - agentConfig "github.com/hashicorp/vault/command/agent/config" - "github.com/hashicorp/vault/helper/useragent" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/helper/pointerutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/cli" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - BasicHclConfig = ` -log_file = "TMPDIR/juan.log" -log_level="warn" -log_rotate_max_files=2 -log_rotate_bytes=1048576 -vault { - address = "http://127.0.0.1:8200" - retry { - num_retries = 5 - } -} - -listener "tcp" { - address = "127.0.0.1:8100" - tls_disable = false - tls_cert_file = "TMPDIR/reload_cert.pem" - tls_key_file = "TMPDIR/reload_key.pem" -}` - BasicHclConfig2 = ` -log_file = "TMPDIR/juan.log" -log_level="debug" -log_rotate_max_files=-1 -log_rotate_bytes=1048576 -vault { - address = "http://127.0.0.1:8200" - retry { - num_retries = 5 - } -} - -listener "tcp" { - address = "127.0.0.1:8100" - tls_disable = false - tls_cert_file = "TMPDIR/reload_cert.pem" - tls_key_file = "TMPDIR/reload_key.pem" -}` -) - -func testAgentCommand(tb testing.TB, logger hclog.Logger) (*cli.MockUi, *AgentCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AgentCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - ShutdownCh: MakeShutdownCh(), - SighupCh: MakeSighupCh(), - logger: logger, - startedCh: make(chan struct{}, 5), - reloadedCh: make(chan struct{}, 5), - } -} - -func TestAgent_ExitAfterAuth(t *testing.T) { - t.Run("via_config", func(t *testing.T) { - testAgentExitAfterAuth(t, false) - }) - - t.Run("via_flag", func(t *testing.T) { - testAgentExitAfterAuth(t, true) - }) -} - -func testAgentExitAfterAuth(t *testing.T, viaFlag bool) { - logger := logging.NewVaultLogger(hclog.Trace) - coreConfig := &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "jwt": vaultjwt.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - client := cluster.Cores[0].Client - - // Setup Vault - err := client.Sys().EnableAuthWithOptions("jwt", &api.EnableAuthOptions{ - Type: "jwt", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/jwt/config", map[string]interface{}{ - "bound_issuer": "https://team-vault.auth0.com/", - "jwt_validation_pubkeys": agent.TestECDSAPubKey, - "jwt_supported_algs": "ES256", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/jwt/role/test", map[string]interface{}{ - "role_type": "jwt", - "bound_subject": "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", - "bound_audiences": "https://vault.plugin.auth.jwt.test", - "user_claim": "https://vault/user", - "groups_claim": "https://vault/groups", - "policies": "test", - "period": "3s", - }) - if err != nil { - t.Fatal(err) - } - - inf, err := os.CreateTemp("", "auth.jwt.test.") - if err != nil { - t.Fatal(err) - } - in := inf.Name() - inf.Close() - os.Remove(in) - t.Logf("input: %s", in) - - sink1f, err := os.CreateTemp("", "sink1.jwt.test.") - if err != nil { - t.Fatal(err) - } - sink1 := sink1f.Name() - sink1f.Close() - os.Remove(sink1) - t.Logf("sink1: %s", sink1) - - sink2f, err := os.CreateTemp("", "sink2.jwt.test.") - if err != nil { - t.Fatal(err) - } - sink2 := sink2f.Name() - sink2f.Close() - os.Remove(sink2) - t.Logf("sink2: %s", sink2) - - conff, err := os.CreateTemp("", "conf.jwt.test.") - if err != nil { - t.Fatal(err) - } - conf := conff.Name() - conff.Close() - os.Remove(conf) - t.Logf("config: %s", conf) - - jwtToken, _ := agent.GetTestJWT(t) - if err := os.WriteFile(in, []byte(jwtToken), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test jwt", "path", in) - } - - exitAfterAuthTemplText := "exit_after_auth = true" - if viaFlag { - exitAfterAuthTemplText = "" - } - - config := ` -%s - -auto_auth { - method { - type = "jwt" - config = { - role = "test" - path = "%s" - } - } - - sink { - type = "file" - config = { - path = "%s" - } - } - - sink "file" { - config = { - path = "%s" - } - } -} -` - - config = fmt.Sprintf(config, exitAfterAuthTemplText, in, sink1, sink2) - if err := os.WriteFile(conf, []byte(config), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test config", "path", conf) - } - - doneCh := make(chan struct{}) - go func() { - ui, cmd := testAgentCommand(t, logger) - cmd.client = client - - args := []string{"-config", conf} - if viaFlag { - args = append(args, "-exit-after-auth") - } - - code := cmd.Run(args) - if code != 0 { - t.Errorf("expected %d to be %d", code, 0) - t.Logf("output from agent:\n%s", ui.OutputWriter.String()) - t.Logf("error from agent:\n%s", ui.ErrorWriter.String()) - } - close(doneCh) - }() - - select { - case <-doneCh: - break - case <-time.After(1 * time.Minute): - t.Fatal("timeout reached while waiting for agent to exit") - } - - sink1Bytes, err := os.ReadFile(sink1) - if err != nil { - t.Fatal(err) - } - if len(sink1Bytes) == 0 { - t.Fatal("got no output from sink 1") - } - - sink2Bytes, err := os.ReadFile(sink2) - if err != nil { - t.Fatal(err) - } - if len(sink2Bytes) == 0 { - t.Fatal("got no output from sink 2") - } - - if string(sink1Bytes) != string(sink2Bytes) { - t.Fatal("sink 1/2 values don't match") - } -} - -func TestAgent_RequireRequestHeader(t *testing.T) { - // newApiClient creates an *api.Client. - newApiClient := func(addr string, includeVaultRequestHeader bool) *api.Client { - conf := api.DefaultConfig() - conf.Address = addr - cli, err := api.NewClient(conf) - if err != nil { - t.Fatalf("err: %s", err) - } - - h := cli.Headers() - val, ok := h[consts.RequestHeaderName] - if !ok || !reflect.DeepEqual(val, []string{"true"}) { - t.Fatalf("invalid %s header", consts.RequestHeaderName) - } - if !includeVaultRequestHeader { - delete(h, consts.RequestHeaderName) - cli.SetHeaders(h) - } - - return cli - } - - //---------------------------------------------------- - // Start the server and agent - //---------------------------------------------------- - - // Start a vault server - logger := logging.NewVaultLogger(hclog.Trace) - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - }, - &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Enable the approle auth method - roleIDPath, secretIDPath := setupAppRole(t, serverClient) - - // Create a config file - config := ` -auto_auth { - method "approle" { - mount_path = "auth/approle" - config = { - role_id_file_path = "%s" - secret_id_file_path = "%s" - } - } -} - -cache { - use_auto_auth_token = true -} - -listener "tcp" { - address = "%s" - tls_disable = true -} -listener "tcp" { - address = "%s" - tls_disable = true - require_request_header = false -} -listener "tcp" { - address = "%s" - tls_disable = true - require_request_header = true -} -` - listenAddr1 := generateListenerAddress(t) - listenAddr2 := generateListenerAddress(t) - listenAddr3 := generateListenerAddress(t) - config = fmt.Sprintf( - config, - roleIDPath, - secretIDPath, - listenAddr1, - listenAddr2, - listenAddr3, - ) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - ui, cmd := testAgentCommand(t, logger) - cmd.client = serverClient - cmd.startedCh = make(chan struct{}) - - var output string - var code int - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - code = cmd.Run([]string{"-config", configPath}) - if code != 0 { - output = ui.ErrorWriter.String() + ui.OutputWriter.String() - } - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - // defer agent shutdown - defer func() { - cmd.ShutdownCh <- struct{}{} - wg.Wait() - if code != 0 { - t.Fatalf("got a non-zero exit status: %d, stdout/stderr: %s", code, output) - } - }() - - //---------------------------------------------------- - // Perform the tests - //---------------------------------------------------- - - // Test against a listener configuration that omits - // 'require_request_header', with the header missing from the request. - agentClient := newApiClient("http://"+listenAddr1, false) - req := agentClient.NewRequest("GET", "/v1/sys/health") - request(t, agentClient, req, 200) - - // Test against a listener configuration that sets 'require_request_header' - // to 'false', with the header missing from the request. - agentClient = newApiClient("http://"+listenAddr2, false) - req = agentClient.NewRequest("GET", "/v1/sys/health") - request(t, agentClient, req, 200) - - // Test against a listener configuration that sets 'require_request_header' - // to 'true', with the header missing from the request. - agentClient = newApiClient("http://"+listenAddr3, false) - req = agentClient.NewRequest("GET", "/v1/sys/health") - resp, err := agentClient.RawRequest(req) - if err == nil { - t.Fatalf("expected error") - } - if resp.StatusCode != http.StatusPreconditionFailed { - t.Fatalf("expected status code %d, not %d", http.StatusPreconditionFailed, resp.StatusCode) - } - - // Test against a listener configuration that sets 'require_request_header' - // to 'true', with an invalid header present in the request. - agentClient = newApiClient("http://"+listenAddr3, false) - h := agentClient.Headers() - h[consts.RequestHeaderName] = []string{"bogus"} - agentClient.SetHeaders(h) - req = agentClient.NewRequest("GET", "/v1/sys/health") - resp, err = agentClient.RawRequest(req) - if err == nil { - t.Fatalf("expected error") - } - if resp.StatusCode != http.StatusPreconditionFailed { - t.Fatalf("expected status code %d, not %d", http.StatusPreconditionFailed, resp.StatusCode) - } - - // Test against a listener configuration that sets 'require_request_header' - // to 'true', with the proper header present in the request. - agentClient = newApiClient("http://"+listenAddr3, true) - req = agentClient.NewRequest("GET", "/v1/sys/health") - request(t, agentClient, req, 200) -} - -// TestAgent_RequireAutoAuthWithForce ensures that the client exits with a -// non-zero code if configured to force the use of an auto-auth token without -// configuring the auto_auth block -func TestAgent_RequireAutoAuthWithForce(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - // Create a config file - config := fmt.Sprintf(` -cache { - use_auto_auth_token = "force" -} - -listener "tcp" { - address = "%s" - tls_disable = true -} -`, generateListenerAddress(t)) - - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - ui, cmd := testAgentCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - code := cmd.Run([]string{"-config", configPath}) - if code == 0 { - t.Errorf("expected error code, but got 0: %d", code) - t.Logf("STDOUT from agent:\n%s", ui.OutputWriter.String()) - t.Logf("STDERR from agent:\n%s", ui.ErrorWriter.String()) - } -} - -// TestAgent_Template_UserAgent Validates that the User-Agent sent to Vault -// as part of Templating requests is correct. Uses the custom handler -// userAgentHandler struct defined in this test package, so that Vault validates the -// User-Agent on requests sent by Agent. -func TestAgent_Template_UserAgent(t *testing.T) { - //---------------------------------------------------- - // Start the server and agent - //---------------------------------------------------- - logger := logging.NewVaultLogger(hclog.Trace) - var h userAgentHandler - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "kv": logicalKv.Factory, - }, - }, - &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc( - func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.userAgentToCheckFor = useragent.AgentTemplatingString() - h.pathToCheck = "/v1/secret/data" - h.requestMethodToCheck = "GET" - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Setenv(api.EnvVaultAddress, serverClient.Address()) - - roleIDPath, secretIDPath := setupAppRoleAndKVMounts(t, serverClient) - - // make a temp directory to hold renders. Each test will create a temp dir - // inside this one - tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDirRoot) - // create temp dir for this test run - tmpDir, err := os.MkdirTemp(tmpDirRoot, "TestAgent_Template_UserAgent") - if err != nil { - t.Fatal(err) - } - - // make some template files - var templatePaths []string - fileName := filepath.Join(tmpDir, "render_0.tmpl") - if err := os.WriteFile(fileName, []byte(templateContents(0)), 0o600); err != nil { - t.Fatal(err) - } - templatePaths = append(templatePaths, fileName) - - // build up the template config to be added to the Agent config.hcl file - var templateConfigStrings []string - for i, t := range templatePaths { - index := fmt.Sprintf("render_%d.json", i) - s := fmt.Sprintf(templateConfigString, t, tmpDir, index) - templateConfigStrings = append(templateConfigStrings, s) - } - - // Create a config file - config := ` -vault { - address = "%s" - tls_skip_verify = true -} - -auto_auth { - method "approle" { - mount_path = "auth/approle" - config = { - role_id_file_path = "%s" - secret_id_file_path = "%s" - remove_secret_id_file_after_reading = false - } - } -} - -%s -` - - // flatten the template configs - templateConfig := strings.Join(templateConfigStrings, " ") - - config = fmt.Sprintf(config, serverClient.Address(), roleIDPath, secretIDPath, templateConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - ui, cmd := testAgentCommand(t, logger) - cmd.client = serverClient - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - code := cmd.Run([]string{"-config", configPath}) - if code != 0 { - t.Errorf("non-zero return code when running agent: %d", code) - t.Logf("STDOUT from agent:\n%s", ui.OutputWriter.String()) - t.Logf("STDERR from agent:\n%s", ui.ErrorWriter.String()) - } - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - // We need to shut down the Agent command - defer func() { - cmd.ShutdownCh <- struct{}{} - wg.Wait() - }() - - verify := func(suffix string) { - t.Helper() - // We need to poll for a bit to give Agent time to render the - // templates. Without this, the test will attempt to read - // the temp dir before Agent has had time to render and will - // likely fail the test - tick := time.Tick(1 * time.Second) - timeout := time.After(10 * time.Second) - var err error - for { - select { - case <-timeout: - t.Fatalf("timed out waiting for templates to render, last error: %v", err) - case <-tick: - } - // Check for files rendered in the directory and break - // early for shutdown if we do have all the files - // rendered - - //---------------------------------------------------- - // Perform the tests - //---------------------------------------------------- - - if numFiles := testListFiles(t, tmpDir, ".json"); numFiles != len(templatePaths) { - err = fmt.Errorf("expected (%d) templates, got (%d)", len(templatePaths), numFiles) - continue - } - - for i := range templatePaths { - fileName := filepath.Join(tmpDir, fmt.Sprintf("render_%d.json", i)) - var c []byte - c, err = os.ReadFile(fileName) - if err != nil { - continue - } - if string(c) != templateRendered(i)+suffix { - err = fmt.Errorf("expected=%q, got=%q", templateRendered(i)+suffix, string(c)) - continue - } - } - return - } - } - - verify("") - - fileName = filepath.Join(tmpDir, "render_0.tmpl") - if err := os.WriteFile(fileName, []byte(templateContents(0)+"{}"), 0o600); err != nil { - t.Fatal(err) - } - - verify("{}") -} - -// TestAgent_Template tests rendering templates -func TestAgent_Template_Basic(t *testing.T) { - //---------------------------------------------------- - // Start the server and agent - //---------------------------------------------------- - logger := logging.NewVaultLogger(hclog.Trace) - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "kv": logicalKv.Factory, - }, - }, - &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Setenv(api.EnvVaultAddress, serverClient.Address()) - - roleIDPath, secretIDPath := setupAppRoleAndKVMounts(t, serverClient) - - // make a temp directory to hold renders. Each test will create a temp dir - // inside this one - tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDirRoot) - - // start test cases here - testCases := map[string]struct { - templateCount int - exitAfterAuth bool - }{ - "one": { - templateCount: 1, - }, - "one_with_exit": { - templateCount: 1, - exitAfterAuth: true, - }, - "many": { - templateCount: 15, - }, - "many_with_exit": { - templateCount: 13, - exitAfterAuth: true, - }, - } - - for tcname, tc := range testCases { - t.Run(tcname, func(t *testing.T) { - // create temp dir for this test run - tmpDir, err := os.MkdirTemp(tmpDirRoot, tcname) - if err != nil { - t.Fatal(err) - } - - // make some template files - var templatePaths []string - for i := 0; i < tc.templateCount; i++ { - fileName := filepath.Join(tmpDir, fmt.Sprintf("render_%d.tmpl", i)) - if err := os.WriteFile(fileName, []byte(templateContents(i)), 0o600); err != nil { - t.Fatal(err) - } - templatePaths = append(templatePaths, fileName) - } - - // build up the template config to be added to the Agent config.hcl file - var templateConfigStrings []string - for i, t := range templatePaths { - index := fmt.Sprintf("render_%d.json", i) - s := fmt.Sprintf(templateConfigString, t, tmpDir, index) - templateConfigStrings = append(templateConfigStrings, s) - } - - // Create a config file - config := ` -vault { - address = "%s" - tls_skip_verify = true -} - -auto_auth { - method "approle" { - mount_path = "auth/approle" - config = { - role_id_file_path = "%s" - secret_id_file_path = "%s" - remove_secret_id_file_after_reading = false - } - } -} - -%s - -%s -` - - // conditionally set the exit_after_auth flag - exitAfterAuth := "" - if tc.exitAfterAuth { - exitAfterAuth = "exit_after_auth = true" - } - - // flatten the template configs - templateConfig := strings.Join(templateConfigStrings, " ") - - config = fmt.Sprintf(config, serverClient.Address(), roleIDPath, secretIDPath, templateConfig, exitAfterAuth) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - ui, cmd := testAgentCommand(t, logger) - cmd.client = serverClient - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - code := cmd.Run([]string{"-config", configPath}) - if code != 0 { - t.Errorf("non-zero return code when running agent: %d", code) - t.Logf("STDOUT from agent:\n%s", ui.OutputWriter.String()) - t.Logf("STDERR from agent:\n%s", ui.ErrorWriter.String()) - } - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - // if using exit_after_auth, then the command will have returned at the - // end and no longer be running. If we are not using exit_after_auth, then - // we need to shut down the command - if !tc.exitAfterAuth { - defer func() { - cmd.ShutdownCh <- struct{}{} - wg.Wait() - }() - } - - verify := func(suffix string) { - t.Helper() - // We need to poll for a bit to give Agent time to render the - // templates. Without this, the test will attempt to read - // the temp dir before Agent has had time to render and will - // likely fail the test - tick := time.Tick(1 * time.Second) - timeout := time.After(10 * time.Second) - var err error - for { - select { - case <-timeout: - t.Fatalf("timed out waiting for templates to render, last error: %v", err) - case <-tick: - } - // Check for files rendered in the directory and break - // early for shutdown if we do have all the files - // rendered - - //---------------------------------------------------- - // Perform the tests - //---------------------------------------------------- - - if numFiles := testListFiles(t, tmpDir, ".json"); numFiles != len(templatePaths) { - err = fmt.Errorf("expected (%d) templates, got (%d)", len(templatePaths), numFiles) - continue - } - - for i := range templatePaths { - fileName := filepath.Join(tmpDir, fmt.Sprintf("render_%d.json", i)) - var c []byte - c, err = os.ReadFile(fileName) - if err != nil { - continue - } - if string(c) != templateRendered(i)+suffix { - err = fmt.Errorf("expected=%q, got=%q", templateRendered(i)+suffix, string(c)) - continue - } - } - return - } - } - - verify("") - - for i := 0; i < tc.templateCount; i++ { - fileName := filepath.Join(tmpDir, fmt.Sprintf("render_%d.tmpl", i)) - if err := os.WriteFile(fileName, []byte(templateContents(i)+"{}"), 0o600); err != nil { - t.Fatal(err) - } - } - - verify("{}") - }) - } -} - -func setupAppRole(t *testing.T, serverClient *api.Client) (string, string) { - t.Helper() - // Enable the approle auth method - req := serverClient.NewRequest("POST", "/v1/sys/auth/approle") - req.BodyBytes = []byte(`{ - "type": "approle" - }`) - request(t, serverClient, req, 204) - - // Create a named role - req = serverClient.NewRequest("PUT", "/v1/auth/approle/role/test-role") - req.BodyBytes = []byte(`{ - "token_ttl": "5m", - "token_policies":"default,myapp-read", - "policies":"default,myapp-read" - }`) - request(t, serverClient, req, 204) - - // Fetch the RoleID of the named role - req = serverClient.NewRequest("GET", "/v1/auth/approle/role/test-role/role-id") - body := request(t, serverClient, req, 200) - data := body["data"].(map[string]interface{}) - roleID := data["role_id"].(string) - - // Get a SecretID issued against the named role - req = serverClient.NewRequest("PUT", "/v1/auth/approle/role/test-role/secret-id") - body = request(t, serverClient, req, 200) - data = body["data"].(map[string]interface{}) - secretID := data["secret_id"].(string) - - // Write the RoleID and SecretID to temp files - roleIDPath := makeTempFile(t, "role_id.txt", roleID+"\n") - secretIDPath := makeTempFile(t, "secret_id.txt", secretID+"\n") - t.Cleanup(func() { - os.Remove(roleIDPath) - os.Remove(secretIDPath) - }) - - return roleIDPath, secretIDPath -} - -func setupAppRoleAndKVMounts(t *testing.T, serverClient *api.Client) (string, string) { - roleIDPath, secretIDPath := setupAppRole(t, serverClient) - - // give test-role permissions to read the kv secret - req := serverClient.NewRequest("PUT", "/v1/sys/policy/myapp-read") - req.BodyBytes = []byte(`{ - "policy": "path \"secret/*\" { capabilities = [\"read\", \"list\"] }" - }`) - request(t, serverClient, req, 204) - - // setup the kv secrets - req = serverClient.NewRequest("POST", "/v1/sys/mounts/secret/tune") - req.BodyBytes = []byte(`{ - "options": {"version": "2"} - }`) - request(t, serverClient, req, 200) - - // Secret: myapp - req = serverClient.NewRequest("POST", "/v1/secret/data/myapp") - req.BodyBytes = []byte(`{ - "data": { - "username": "bar", - "password": "zap" - } - }`) - request(t, serverClient, req, 200) - - // Secret: myapp2 - req = serverClient.NewRequest("POST", "/v1/secret/data/myapp2") - req.BodyBytes = []byte(`{ - "data": { - "username": "barstuff", - "password": "zap" - } - }`) - request(t, serverClient, req, 200) - - // Secret: otherapp - req = serverClient.NewRequest("POST", "/v1/secret/data/otherapp") - req.BodyBytes = []byte(`{ - "data": { - "username": "barstuff", - "password": "zap", - "cert": "something" - } - }`) - request(t, serverClient, req, 200) - - return roleIDPath, secretIDPath -} - -// TestAgent_Template_VaultClientFromEnv tests that Vault Agent can read in its -// required `vault` client details from environment variables instead of config. -func TestAgent_Template_VaultClientFromEnv(t *testing.T) { - //---------------------------------------------------- - // Start the server and agent - //---------------------------------------------------- - logger := logging.NewVaultLogger(hclog.Trace) - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "kv": logicalKv.Factory, - }, - }, - &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - roleIDPath, secretIDPath := setupAppRoleAndKVMounts(t, serverClient) - - // make a temp directory to hold renders. Each test will create a temp dir - // inside this one - tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDirRoot) - - vaultAddr := "https://" + cluster.Cores[0].Listeners[0].Address.String() - testCases := map[string]struct { - env map[string]string - }{ - "VAULT_ADDR and VAULT_CACERT": { - env: map[string]string{ - api.EnvVaultAddress: vaultAddr, - api.EnvVaultCACert: cluster.CACertPEMFile, - }, - }, - "VAULT_ADDR and VAULT_CACERT_BYTES": { - env: map[string]string{ - api.EnvVaultAddress: vaultAddr, - api.EnvVaultCACertBytes: string(cluster.CACertPEM), - }, - }, - } - - for tcname, tc := range testCases { - t.Run(tcname, func(t *testing.T) { - for k, v := range tc.env { - t.Setenv(k, v) - } - tmpDir := t.TempDir() - - // Make a template. - templateFile := filepath.Join(tmpDir, "render.tmpl") - if err := os.WriteFile(templateFile, []byte(templateContents(0)), 0o600); err != nil { - t.Fatal(err) - } - - // build up the template config to be added to the Agent config.hcl file - targetFile := filepath.Join(tmpDir, "render.json") - templateConfig := fmt.Sprintf(` -template { - source = "%s" - destination = "%s" -} - `, templateFile, targetFile) - - // Create a config file - config := ` -auto_auth { - method "approle" { - mount_path = "auth/approle" - config = { - role_id_file_path = "%s" - secret_id_file_path = "%s" - remove_secret_id_file_after_reading = false - } - } -} - -%s -` - - config = fmt.Sprintf(config, roleIDPath, secretIDPath, templateConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - ui, cmd := testAgentCommand(t, logger) - cmd.client = serverClient - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - code := cmd.Run([]string{"-config", configPath}) - if code != 0 { - t.Errorf("non-zero return code when running agent: %d", code) - t.Logf("STDOUT from agent:\n%s", ui.OutputWriter.String()) - t.Logf("STDERR from agent:\n%s", ui.ErrorWriter.String()) - } - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - defer func() { - cmd.ShutdownCh <- struct{}{} - wg.Wait() - }() - - // We need to poll for a bit to give Agent time to render the - // templates. Without this this, the test will attempt to read - // the temp dir before Agent has had time to render and will - // likely fail the test - tick := time.Tick(1 * time.Second) - timeout := time.After(10 * time.Second) - for { - select { - case <-timeout: - t.Fatalf("timed out waiting for templates to render, last error: %v", err) - case <-tick: - } - - contents, err := os.ReadFile(targetFile) - if err != nil { - // If the file simply doesn't exist, continue waiting for - // the template rendering to complete. - if os.IsNotExist(err) { - continue - } - t.Fatal(err) - } - - if string(contents) != templateRendered(0) { - t.Fatalf("expected=%q, got=%q", templateRendered(0), string(contents)) - } - - // Success! Break out of the retry loop. - break - } - }) - } -} - -func testListFiles(t *testing.T, dir, extension string) int { - t.Helper() - - files, err := os.ReadDir(dir) - if err != nil { - t.Fatal(err) - } - var count int - for _, f := range files { - if filepath.Ext(f.Name()) == extension { - count++ - } - } - - return count -} - -// TestAgent_Template_ExitCounter tests that Vault Agent correctly renders all -// templates before exiting when the configuration uses exit_after_auth. This is -// similar to TestAgent_Template_Basic, but differs by using a consistent number -// of secrets from multiple sources, where as the basic test could possibly -// generate a random number of secrets, but all using the same source. This test -// reproduces https://github.com/hashicorp/vault/issues/7883 -func TestAgent_Template_ExitCounter(t *testing.T) { - //---------------------------------------------------- - // Start the server and agent - //---------------------------------------------------- - logger := logging.NewVaultLogger(hclog.Trace) - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "kv": logicalKv.Factory, - }, - }, - &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Setenv(api.EnvVaultAddress, serverClient.Address()) - - roleIDPath, secretIDPath := setupAppRoleAndKVMounts(t, serverClient) - - // make a temp directory to hold renders. Each test will create a temp dir - // inside this one - tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDirRoot) - - // create temp dir for this test run - tmpDir, err := os.MkdirTemp(tmpDirRoot, "agent-test") - if err != nil { - t.Fatal(err) - } - - // Create a config file - config := ` -vault { - address = "%s" - tls_skip_verify = true -} - -auto_auth { - method "approle" { - mount_path = "auth/approle" - config = { - role_id_file_path = "%s" - secret_id_file_path = "%s" - remove_secret_id_file_after_reading = false - } - } -} - -template { - contents = "{{ with secret \"secret/myapp\" }}{{ range $k, $v := .Data.data }}{{ $v }}{{ end }}{{ end }}" - destination = "%s/render-pass.txt" -} - -template { - contents = "{{ with secret \"secret/myapp2\" }}{{ .Data.data.username}}{{ end }}" - destination = "%s/render-user.txt" -} - -template { - contents = < 0 { - h.failCount-- - h.t.Logf("%s failing GET request on %s, failures left: %d", time.Now(), req.URL.Path, h.failCount) - resp.WriteHeader(500) - return - } - h.t.Logf("passing GET request on %s", req.URL.Path) - } - vaulthttp.Handler.Handler(h.props).ServeHTTP(resp, req) -} - -// userAgentHandler makes it easy to test the User-Agent header received -// by Vault -type userAgentHandler struct { - props *vault.HandlerProperties - failCount int - userAgentToCheckFor string - pathToCheck string - requestMethodToCheck string - t *testing.T -} - -func (h *userAgentHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if req.Method == h.requestMethodToCheck && strings.Contains(req.RequestURI, h.pathToCheck) { - userAgent := req.UserAgent() - if !(userAgent == h.userAgentToCheckFor) { - h.t.Fatalf("User-Agent string not as expected. Expected to find %s, got %s", h.userAgentToCheckFor, userAgent) - } - } - vaulthttp.Handler.Handler(h.props).ServeHTTP(w, req) -} - -// TestAgent_Template_Retry verifies that the template server retries requests -// based on retry configuration. -func TestAgent_Template_Retry(t *testing.T) { - //---------------------------------------------------- - // Start the server and agent - //---------------------------------------------------- - logger := logging.NewVaultLogger(hclog.Trace) - var h handler - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "kv": logicalKv.Factory, - }, - }, - &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc( - func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - methodConf, cleanup := prepAgentApproleKV(t, serverClient) - defer cleanup() - - err := serverClient.Sys().TuneMount("secret", api.MountConfigInput{ - Options: map[string]string{ - "version": "2", - }, - }) - if err != nil { - t.Fatal(err) - } - - _, err = serverClient.Logical().Write("secret/data/otherapp", map[string]interface{}{ - "data": map[string]interface{}{ - "username": "barstuff", - "password": "zap", - "cert": "something", - }, - }) - if err != nil { - t.Fatal(err) - } - - // make a temp directory to hold renders. Each test will create a temp dir - // inside this one - tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDirRoot) - - intRef := func(i int) *int { - return &i - } - // start test cases here - testCases := map[string]struct { - retries *int - expectError bool - }{ - "none": { - retries: intRef(-1), - expectError: true, - }, - "one": { - retries: intRef(1), - expectError: true, - }, - "two": { - retries: intRef(2), - expectError: false, - }, - "missing": { - retries: nil, - expectError: false, - }, - "default": { - retries: intRef(0), - expectError: false, - }, - } - - for tcname, tc := range testCases { - t.Run(tcname, func(t *testing.T) { - // We fail the first 6 times. The consul-template code creates - // a Vault client with MaxRetries=2, so for every consul-template - // retry configured, it will in practice make up to 3 requests. - // Thus if consul-template is configured with "one" retry, it will - // fail given our failCount, but if configured with "two" retries, - // they will consume our 6th failure, and on the "third (from its - // perspective) attempt, it will succeed. - h.failCount = 6 - - // create temp dir for this test run - tmpDir, err := os.MkdirTemp(tmpDirRoot, tcname) - if err != nil { - t.Fatal(err) - } - - // make some template files - templatePath := filepath.Join(tmpDir, "render_0.tmpl") - if err := os.WriteFile(templatePath, []byte(templateContents(0)), 0o600); err != nil { - t.Fatal(err) - } - templateConfig := fmt.Sprintf(templateConfigString, templatePath, tmpDir, "render_0.json") - - var retryConf string - if tc.retries != nil { - retryConf = fmt.Sprintf("retry { num_retries = %d }", *tc.retries) - } - - config := fmt.Sprintf(` -%s -vault { - address = "%s" - %s - tls_skip_verify = true -} -%s -template_config { - exit_on_retry_failure = true -} -`, methodConf, serverClient.Address(), retryConf, templateConfig) - - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - _, cmd := testAgentCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - var code int - go func() { - code = cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - verify := func() error { - t.Helper() - // We need to poll for a bit to give Agent time to render the - // templates. Without this this, the test will attempt to read - // the temp dir before Agent has had time to render and will - // likely fail the test - tick := time.Tick(1 * time.Second) - timeout := time.After(15 * time.Second) - var err error - for { - select { - case <-timeout: - return fmt.Errorf("timed out waiting for templates to render, last error: %v", err) - case <-tick: - } - // Check for files rendered in the directory and break - // early for shutdown if we do have all the files - // rendered - - //---------------------------------------------------- - // Perform the tests - //---------------------------------------------------- - - if numFiles := testListFiles(t, tmpDir, ".json"); numFiles != 1 { - err = fmt.Errorf("expected 1 template, got (%d)", numFiles) - continue - } - - fileName := filepath.Join(tmpDir, "render_0.json") - var c []byte - c, err = os.ReadFile(fileName) - if err != nil { - continue - } - if string(c) != templateRendered(0) { - err = fmt.Errorf("expected=%q, got=%q", templateRendered(0), string(c)) - continue - } - return nil - } - } - - err = verify() - close(cmd.ShutdownCh) - wg.Wait() - - switch { - case (code != 0 || err != nil) && tc.expectError: - case code == 0 && err == nil && !tc.expectError: - default: - t.Fatalf("%s expectError=%v error=%v code=%d", tcname, tc.expectError, err, code) - } - }) - } -} - -// prepAgentApproleKV configures a Vault instance for approle authentication, -// such that the resulting token will have global permissions across /kv -// and /secret mounts. Returns the auto_auth config stanza to setup an Agent -// to connect using approle. -func prepAgentApproleKV(t *testing.T, client *api.Client) (string, func()) { - t.Helper() - - policyAutoAuthAppRole := ` -path "/kv/*" { - capabilities = ["create", "read", "update", "delete", "list"] -} -path "/secret/*" { - capabilities = ["create", "read", "update", "delete", "list"] -} -` - // Add an kv-admin policy - if err := client.Sys().PutPolicy("test-autoauth", policyAutoAuthAppRole); err != nil { - t.Fatal(err) - } - - // Enable approle - err := client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{ - Type: "approle", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/approle/role/test1", map[string]interface{}{ - "bind_secret_id": "true", - "token_ttl": "1h", - "token_max_ttl": "2h", - "policies": []string{"test-autoauth"}, - }) - if err != nil { - t.Fatal(err) - } - - resp, err := client.Logical().Write("auth/approle/role/test1/secret-id", nil) - if err != nil { - t.Fatal(err) - } - secretID := resp.Data["secret_id"].(string) - secretIDFile := makeTempFile(t, "secret_id.txt", secretID+"\n") - - resp, err = client.Logical().Read("auth/approle/role/test1/role-id") - if err != nil { - t.Fatal(err) - } - roleID := resp.Data["role_id"].(string) - roleIDFile := makeTempFile(t, "role_id.txt", roleID+"\n") - - config := fmt.Sprintf(` -auto_auth { - method "approle" { - mount_path = "auth/approle" - config = { - role_id_file_path = "%s" - secret_id_file_path = "%s" - remove_secret_id_file_after_reading = false - } - } -} -`, roleIDFile, secretIDFile) - - cleanup := func() { - _ = os.Remove(roleIDFile) - _ = os.Remove(secretIDFile) - } - return config, cleanup -} - -// TestAgent_AutoAuth_UserAgent tests that the User-Agent sent -// to Vault by Vault Agent is correct when performing Auto-Auth. -// Uses the custom handler userAgentHandler (defined above) so -// that Vault validates the User-Agent on requests sent by Agent. -func TestAgent_AutoAuth_UserAgent(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - var h userAgentHandler - cluster := vault.NewTestCluster(t, &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - }, &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc( - func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.userAgentToCheckFor = useragent.AgentAutoAuthString() - h.requestMethodToCheck = "PUT" - h.pathToCheck = "auth/approle/login" - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - serverClient := cluster.Cores[0].Client - - // Enable the approle auth method - roleIDPath, secretIDPath := setupAppRole(t, serverClient) - - sinkf, err := os.CreateTemp("", "sink.test.") - if err != nil { - t.Fatal(err) - } - sink := sinkf.Name() - sinkf.Close() - os.Remove(sink) - - autoAuthConfig := fmt.Sprintf(` -auto_auth { - method "approle" { - mount_path = "auth/approle" - config = { - role_id_file_path = "%s" - secret_id_file_path = "%s" - } - } - - sink "file" { - config = { - path = "%s" - } - } -}`, roleIDPath, secretIDPath, sink) - - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - config := fmt.Sprintf(` -vault { - address = "%s" - tls_skip_verify = true -} -api_proxy { - use_auto_auth_token = true -} -%s -%s -`, serverClient.Address(), listenConfig, autoAuthConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - // Start the agent - _, cmd := testAgentCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - // Validate that the auto-auth token has been correctly attained - // and works for LookupSelf - conf := api.DefaultConfig() - conf.Address = "http://" + listenAddr - agentClient, err := api.NewClient(conf) - if err != nil { - t.Fatalf("err: %s", err) - } - - agentClient.SetToken("") - err = agentClient.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - - // Wait for the token to be sent to syncs and be available to be used - time.Sleep(5 * time.Second) - - req := agentClient.NewRequest("GET", "/v1/auth/token/lookup-self") - request(t, agentClient, req, 200) - - close(cmd.ShutdownCh) - wg.Wait() -} - -// TestAgent_APIProxyWithoutCache_UserAgent tests that the User-Agent sent -// to Vault by Vault Agent is correct using the API proxy without -// the cache configured. Uses the custom handler -// userAgentHandler struct defined in this test package, so that Vault validates the -// User-Agent on requests sent by Agent. -func TestAgent_APIProxyWithoutCache_UserAgent(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - userAgentForProxiedClient := "proxied-client" - var h userAgentHandler - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc( - func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.userAgentToCheckFor = useragent.AgentProxyStringWithProxiedUserAgent(userAgentForProxiedClient) - h.pathToCheck = "/v1/auth/token/lookup-self" - h.requestMethodToCheck = "GET" - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - config := fmt.Sprintf(` -vault { - address = "%s" - tls_skip_verify = true -} -%s -`, serverClient.Address(), listenConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - _, cmd := testAgentCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - agentClient, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - agentClient.AddHeader("User-Agent", userAgentForProxiedClient) - agentClient.SetToken(serverClient.Token()) - agentClient.SetMaxRetries(0) - err = agentClient.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - - _, err = agentClient.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - - close(cmd.ShutdownCh) - wg.Wait() -} - -// TestAgent_APIProxyWithCache_UserAgent tests that the User-Agent sent -// to Vault by Vault Agent is correct using the API proxy with -// the cache configured. Uses the custom handler -// userAgentHandler struct defined in this test package, so that Vault validates the -// User-Agent on requests sent by Agent. -func TestAgent_APIProxyWithCache_UserAgent(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - userAgentForProxiedClient := "proxied-client" - var h userAgentHandler - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc( - func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.userAgentToCheckFor = useragent.AgentProxyStringWithProxiedUserAgent(userAgentForProxiedClient) - h.pathToCheck = "/v1/auth/token/lookup-self" - h.requestMethodToCheck = "GET" - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - cacheConfig := ` -cache { -}` - - config := fmt.Sprintf(` -vault { - address = "%s" - tls_skip_verify = true -} -%s -%s -`, serverClient.Address(), listenConfig, cacheConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - _, cmd := testAgentCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - agentClient, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - agentClient.AddHeader("User-Agent", userAgentForProxiedClient) - agentClient.SetToken(serverClient.Token()) - agentClient.SetMaxRetries(0) - err = agentClient.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - - _, err = agentClient.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - - close(cmd.ShutdownCh) - wg.Wait() -} - -func TestAgent_Cache_DynamicSecret(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - cacheConfig := ` -cache { -} -` - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - config := fmt.Sprintf(` -vault { - address = "%s" - tls_skip_verify = true -} -%s -%s -`, serverClient.Address(), cacheConfig, listenConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - _, cmd := testAgentCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - agentClient, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - agentClient.SetToken(serverClient.Token()) - agentClient.SetMaxRetries(0) - err = agentClient.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - - renewable := true - tokenCreateRequest := &api.TokenCreateRequest{ - Policies: []string{"default"}, - TTL: "30m", - Renewable: &renewable, - } - - // This was the simplest test I could find to trigger the caching behaviour, - // i.e. the most concise I could make the test that I can tell - // creating an orphan token returns Auth, is renewable, and isn't a token - // that's managed elsewhere (since it's an orphan) - secret, err := agentClient.Auth().Token().CreateOrphan(tokenCreateRequest) - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Auth == nil { - t.Fatalf("secret not as expected: %v", secret) - } - - token := secret.Auth.ClientToken - - secret, err = agentClient.Auth().Token().CreateOrphan(tokenCreateRequest) - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Auth == nil { - t.Fatalf("secret not as expected: %v", secret) - } - - token2 := secret.Auth.ClientToken - - if token != token2 { - t.Fatalf("token create response not cached when it should have been, as tokens differ") - } - - close(cmd.ShutdownCh) - wg.Wait() -} - -func TestAgent_ApiProxy_Retry(t *testing.T) { - //---------------------------------------------------- - // Start the server and agent - //---------------------------------------------------- - logger := logging.NewVaultLogger(hclog.Trace) - var h handler - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "kv": logicalKv.Factory, - }, - }, - &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc(func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - _, err := serverClient.Logical().Write("secret/foo", map[string]interface{}{ - "bar": "baz", - }) - if err != nil { - t.Fatal(err) - } - - intRef := func(i int) *int { - return &i - } - // start test cases here - testCases := map[string]struct { - retries *int - expectError bool - }{ - "none": { - retries: intRef(-1), - expectError: true, - }, - "one": { - retries: intRef(1), - expectError: true, - }, - "two": { - retries: intRef(2), - expectError: false, - }, - "missing": { - retries: nil, - expectError: false, - }, - "default": { - retries: intRef(0), - expectError: false, - }, - } - - for tcname, tc := range testCases { - t.Run(tcname, func(t *testing.T) { - h.failCount = 2 - - cacheConfig := ` -cache { -} -` - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - var retryConf string - if tc.retries != nil { - retryConf = fmt.Sprintf("retry { num_retries = %d }", *tc.retries) - } - - config := fmt.Sprintf(` -vault { - address = "%s" - %s - tls_skip_verify = true -} -%s -%s -`, serverClient.Address(), retryConf, cacheConfig, listenConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the agent - _, cmd := testAgentCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - client, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - client.SetToken(serverClient.Token()) - client.SetMaxRetries(0) - err = client.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - secret, err := client.Logical().Read("secret/foo") - switch { - case (err != nil || secret == nil) && tc.expectError: - case (err == nil || secret != nil) && !tc.expectError: - default: - t.Fatalf("%s expectError=%v error=%v secret=%v", tcname, tc.expectError, err, secret) - } - if secret != nil && secret.Data["foo"] != nil { - val := secret.Data["foo"].(map[string]interface{}) - if !reflect.DeepEqual(val, map[string]interface{}{"bar": "baz"}) { - t.Fatalf("expected key 'foo' to yield bar=baz, got: %v", val) - } - } - time.Sleep(time.Second) - - close(cmd.ShutdownCh) - wg.Wait() - }) - } -} - -func TestAgent_TemplateConfig_ExitOnRetryFailure(t *testing.T) { - //---------------------------------------------------- - // Start the server and agent - //---------------------------------------------------- - logger := logging.NewVaultLogger(hclog.Trace) - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - // Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "kv": logicalKv.Factory, - }, - }, - &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that agent picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - autoAuthConfig, cleanup := prepAgentApproleKV(t, serverClient) - defer cleanup() - - err := serverClient.Sys().TuneMount("secret", api.MountConfigInput{ - Options: map[string]string{ - "version": "2", - }, - }) - if err != nil { - t.Fatal(err) - } - - _, err = serverClient.Logical().Write("secret/data/otherapp", map[string]interface{}{ - "data": map[string]interface{}{ - "username": "barstuff", - "password": "zap", - "cert": "something", - }, - }) - if err != nil { - t.Fatal(err) - } - - // make a temp directory to hold renders. Each test will create a temp dir - // inside this one - tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDirRoot) - - // Note that missing key is different from a non-existent secret. A missing - // key (2xx response with missing keys in the response map) can still yield - // a successful render unless error_on_missing_key is specified, whereas a - // missing secret (4xx response) always results in an error. - missingKeyTemplateContent := `{{- with secret "secret/otherapp"}}{"secret": "other", -{{- if .Data.data.foo}}"foo":"{{ .Data.data.foo}}"{{- end }}} -{{- end }}` - missingKeyTemplateRender := `{"secret": "other",}` - - badTemplateContent := `{{- with secret "secret/non-existent"}}{"secret": "other", -{{- if .Data.data.foo}}"foo":"{{ .Data.data.foo}}"{{- end }}} -{{- end }}` - - testCases := map[string]struct { - exitOnRetryFailure *bool - templateContents string - expectTemplateRender string - templateErrorOnMissingKey bool - expectError bool - expectExitFromError bool - }{ - "true, no template error": { - exitOnRetryFailure: pointerutil.BoolPtr(true), - templateContents: templateContents(0), - expectTemplateRender: templateRendered(0), - templateErrorOnMissingKey: false, - expectError: false, - expectExitFromError: false, - }, - "true, with non-existent secret": { - exitOnRetryFailure: pointerutil.BoolPtr(true), - templateContents: badTemplateContent, - expectTemplateRender: "", - templateErrorOnMissingKey: false, - expectError: true, - expectExitFromError: true, - }, - "true, with missing key": { - exitOnRetryFailure: pointerutil.BoolPtr(true), - templateContents: missingKeyTemplateContent, - expectTemplateRender: missingKeyTemplateRender, - templateErrorOnMissingKey: false, - expectError: false, - expectExitFromError: false, - }, - "true, with missing key, with error_on_missing_key": { - exitOnRetryFailure: pointerutil.BoolPtr(true), - templateContents: missingKeyTemplateContent, - expectTemplateRender: "", - templateErrorOnMissingKey: true, - expectError: true, - expectExitFromError: true, - }, - "false, no template error": { - exitOnRetryFailure: pointerutil.BoolPtr(false), - templateContents: templateContents(0), - expectTemplateRender: templateRendered(0), - templateErrorOnMissingKey: false, - expectError: false, - expectExitFromError: false, - }, - "false, with non-existent secret": { - exitOnRetryFailure: pointerutil.BoolPtr(false), - templateContents: badTemplateContent, - expectTemplateRender: "", - templateErrorOnMissingKey: false, - expectError: true, - expectExitFromError: false, - }, - "false, with missing key": { - exitOnRetryFailure: pointerutil.BoolPtr(false), - templateContents: missingKeyTemplateContent, - expectTemplateRender: missingKeyTemplateRender, - templateErrorOnMissingKey: false, - expectError: false, - expectExitFromError: false, - }, - "false, with missing key, with error_on_missing_key": { - exitOnRetryFailure: pointerutil.BoolPtr(false), - templateContents: missingKeyTemplateContent, - expectTemplateRender: missingKeyTemplateRender, - templateErrorOnMissingKey: true, - expectError: true, - expectExitFromError: false, - }, - "missing": { - exitOnRetryFailure: nil, - templateContents: templateContents(0), - expectTemplateRender: templateRendered(0), - templateErrorOnMissingKey: false, - expectError: false, - expectExitFromError: false, - }, - } - - for tcName, tc := range testCases { - t.Run(tcName, func(t *testing.T) { - // create temp dir for this test run - tmpDir, err := os.MkdirTemp(tmpDirRoot, tcName) - if err != nil { - t.Fatal(err) - } - - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - var exitOnRetryFailure string - if tc.exitOnRetryFailure != nil { - exitOnRetryFailure = fmt.Sprintf("exit_on_retry_failure = %t", *tc.exitOnRetryFailure) - } - templateConfig := fmt.Sprintf(` -template_config = { - %s -} -`, exitOnRetryFailure) - - template := fmt.Sprintf(` -template { - contents = < 0, "no files were found") - - for _, p := range m { - f, err := os.Open(p) - require.NoError(t, err) - - fs := bufio.NewScanner(f) - fs.Split(bufio.ScanLines) - - for fs.Scan() { - s := fs.Text() - entry := make(map[string]string) - err := json.Unmarshal([]byte(s), &entry) - require.NoError(t, err) - v, ok := entry["@message"] - if !ok { - continue - } - if v == runnerLogMessage { - found = true - break - } - } - } - - require.Truef(t, found, "unable to find consul-template partial message in logs: %s", runnerLogMessage) -} - -// Get a randomly assigned port and then free it again before returning it. -// There is still a race when trying to use it, but should work better -// than a static port. -func generateListenerAddress(t *testing.T) string { - t.Helper() - - ln1, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - listenAddr := ln1.Addr().String() - ln1.Close() - return listenAddr -} diff --git a/command/agentproxyshared/auth/auth_test.go b/command/agentproxyshared/auth/auth_test.go deleted file mode 100644 index b4be3c480..000000000 --- a/command/agentproxyshared/auth/auth_test.go +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package auth - -import ( - "context" - "net/http" - "testing" - "time" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/credential/userpass" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -type userpassTestMethod struct{} - -func newUserpassTestMethod(t *testing.T, client *api.Client) AuthMethod { - err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", - Config: api.AuthConfigInput{ - DefaultLeaseTTL: "1s", - MaxLeaseTTL: "3s", - }, - }) - if err != nil { - t.Fatal(err) - } - - return &userpassTestMethod{} -} - -func (u *userpassTestMethod) Authenticate(_ context.Context, client *api.Client) (string, http.Header, map[string]interface{}, error) { - _, err := client.Logical().Write("auth/userpass/users/foo", map[string]interface{}{ - "password": "bar", - }) - if err != nil { - return "", nil, nil, err - } - return "auth/userpass/login/foo", nil, map[string]interface{}{ - "password": "bar", - }, nil -} - -func (u *userpassTestMethod) NewCreds() chan struct{} { - return nil -} - -func (u *userpassTestMethod) CredSuccess() { -} - -func (u *userpassTestMethod) Shutdown() { -} - -func TestAuthHandler(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - coreConfig := &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "userpass": userpass.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - client := cluster.Cores[0].Client - - ctx, cancelFunc := context.WithCancel(context.Background()) - - ah := NewAuthHandler(&AuthHandlerConfig{ - Logger: logger.Named("auth.handler"), - Client: client, - }) - - am := newUserpassTestMethod(t, client) - errCh := make(chan error) - go func() { - errCh <- ah.Run(ctx, am) - }() - - // Consume tokens so we don't block - stopTime := time.Now().Add(5 * time.Second) - closed := false -consumption: - for { - select { - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - break consumption - case <-ah.OutputCh: - case <-ah.TemplateTokenCh: - // Nothing - case <-time.After(stopTime.Sub(time.Now())): - if !closed { - cancelFunc() - closed = true - } - } - } -} - -func TestAgentBackoff(t *testing.T) { - max := 1024 * time.Second - backoff := newAutoAuthBackoff(defaultMinBackoff, max, false) - - // Test initial value - if backoff.current != defaultMinBackoff { - t.Fatalf("expected 1s initial backoff, got: %v", backoff.current) - } - - // Test that backoff values are in expected range (75-100% of 2*previous) - for i := 0; i < 9; i++ { - old := backoff.current - backoff.next() - - expMax := 2 * old - expMin := 3 * expMax / 4 - - if backoff.current < expMin || backoff.current > expMax { - t.Fatalf("expected backoff in range %v to %v, got: %v", expMin, expMax, backoff) - } - } - - // Test that backoff is capped - for i := 0; i < 100; i++ { - backoff.next() - if backoff.current > max { - t.Fatalf("backoff exceeded max of 100s: %v", backoff) - } - } - - // Test reset - backoff.reset() - if backoff.current != defaultMinBackoff { - t.Fatalf("expected 1s backoff after reset, got: %v", backoff.current) - } -} - -func TestAgentMinBackoffCustom(t *testing.T) { - type test struct { - minBackoff time.Duration - want time.Duration - } - - tests := []test{ - {minBackoff: 0 * time.Second, want: 1 * time.Second}, - {minBackoff: 1 * time.Second, want: 1 * time.Second}, - {minBackoff: 5 * time.Second, want: 5 * time.Second}, - {minBackoff: 10 * time.Second, want: 10 * time.Second}, - } - - for _, test := range tests { - max := 1024 * time.Second - backoff := newAutoAuthBackoff(test.minBackoff, max, false) - - // Test initial value - if backoff.current != test.want { - t.Fatalf("expected %d initial backoff, got: %v", test.want, backoff.current) - } - - // Test that backoff values are in expected range (75-100% of 2*previous) - for i := 0; i < 5; i++ { - old := backoff.current - backoff.next() - - expMax := 2 * old - expMin := 3 * expMax / 4 - - if backoff.current < expMin || backoff.current > expMax { - t.Fatalf("expected backoff in range %v to %v, got: %v", expMin, expMax, backoff) - } - } - - // Test that backoff is capped - for i := 0; i < 100; i++ { - backoff.next() - if backoff.current > max { - t.Fatalf("backoff exceeded max of 100s: %v", backoff) - } - } - - // Test reset - backoff.reset() - if backoff.current != test.want { - t.Fatalf("expected %d backoff after reset, got: %v", test.want, backoff.current) - } - } -} diff --git a/command/agentproxyshared/auth/cert/cert_test.go b/command/agentproxyshared/auth/cert/cert_test.go deleted file mode 100644 index 6a7e4f779..000000000 --- a/command/agentproxyshared/auth/cert/cert_test.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cert - -import ( - "context" - "os" - "path" - "reflect" - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agentproxyshared/auth" -) - -func TestCertAuthMethod_Authenticate(t *testing.T) { - config := &auth.AuthConfig{ - Logger: hclog.NewNullLogger(), - MountPath: "cert-test", - Config: map[string]interface{}{ - "name": "foo", - }, - } - - method, err := NewCertAuthMethod(config) - if err != nil { - t.Fatal(err) - } - - client, err := api.NewClient(nil) - if err != nil { - t.Fatal(err) - } - - loginPath, _, authMap, err := method.Authenticate(context.Background(), client) - if err != nil { - t.Fatal(err) - } - - expectedLoginPath := path.Join(config.MountPath, "/login") - if loginPath != expectedLoginPath { - t.Fatalf("mismatch on login path: got: %s, expected: %s", loginPath, expectedLoginPath) - } - - expectedAuthMap := map[string]interface{}{ - "name": config.Config["name"], - } - if !reflect.DeepEqual(authMap, expectedAuthMap) { - t.Fatalf("mismatch on login path:\ngot:\n\t%v\nexpected:\n\t%v", authMap, expectedAuthMap) - } -} - -func TestCertAuthMethod_AuthClient_withoutCerts(t *testing.T) { - config := &auth.AuthConfig{ - Logger: hclog.NewNullLogger(), - MountPath: "cert-test", - Config: map[string]interface{}{ - "name": "without-certs", - }, - } - - method, err := NewCertAuthMethod(config) - if err != nil { - t.Fatal(err) - } - - client, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - - clientToUse, err := method.(auth.AuthMethodWithClient).AuthClient(client) - if err != nil { - t.Fatal(err) - } - - if client != clientToUse { - t.Fatal("error: expected AuthClient to return back original client") - } -} - -func TestCertAuthMethod_AuthClient_withCerts(t *testing.T) { - clientCert, err := os.Open("./test-fixtures/keys/cert.pem") - if err != nil { - t.Fatal(err) - } - defer clientCert.Close() - - clientKey, err := os.Open("./test-fixtures/keys/key.pem") - if err != nil { - t.Fatal(err) - } - defer clientKey.Close() - - config := &auth.AuthConfig{ - Logger: hclog.NewNullLogger(), - MountPath: "cert-test", - Config: map[string]interface{}{ - "name": "with-certs", - "client_cert": clientCert.Name(), - "client_key": clientKey.Name(), - }, - } - - method, err := NewCertAuthMethod(config) - if err != nil { - t.Fatal(err) - } - - client, err := api.NewClient(nil) - if err != nil { - t.Fatal(err) - } - - clientToUse, err := method.(auth.AuthMethodWithClient).AuthClient(client) - if err != nil { - t.Fatal(err) - } - - if client == clientToUse { - t.Fatal("expected client from AuthClient to be different from original client") - } - - // Call AuthClient again to get back the cached client - cachedClient, err := method.(auth.AuthMethodWithClient).AuthClient(client) - if err != nil { - t.Fatal(err) - } - - if cachedClient != clientToUse { - t.Fatal("expected client from AuthClient to return back a cached client") - } -} - -func TestCertAuthMethod_AuthClient_withCertsReload(t *testing.T) { - clientCert, err := os.Open("./test-fixtures/keys/cert.pem") - if err != nil { - t.Fatal(err) - } - - defer clientCert.Close() - - clientKey, err := os.Open("./test-fixtures/keys/key.pem") - if err != nil { - t.Fatal(err) - } - - defer clientKey.Close() - - config := &auth.AuthConfig{ - Logger: hclog.NewNullLogger(), - MountPath: "cert-test", - Config: map[string]interface{}{ - "name": "with-certs-reloaded", - "client_cert": clientCert.Name(), - "client_key": clientKey.Name(), - "reload": true, - }, - } - - method, err := NewCertAuthMethod(config) - if err != nil { - t.Fatal(err) - } - - client, err := api.NewClient(nil) - if err != nil { - t.Fatal(err) - } - - clientToUse, err := method.(auth.AuthMethodWithClient).AuthClient(client) - if err != nil { - t.Fatal(err) - } - - if client == clientToUse { - t.Fatal("expected client from AuthClient to be different from original client") - } - - // Call AuthClient again to get back a new client with reloaded certificates - reloadedClient, err := method.(auth.AuthMethodWithClient).AuthClient(client) - if err != nil { - t.Fatal(err) - } - - if reloadedClient == clientToUse { - t.Fatal("expected client from AuthClient to return back a new client") - } -} diff --git a/command/agentproxyshared/auth/cert/test-fixtures/keys/cert.pem b/command/agentproxyshared/auth/cert/test-fixtures/keys/cert.pem deleted file mode 100644 index 67ef67dd8..000000000 --- a/command/agentproxyshared/auth/cert/test-fixtures/keys/cert.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIUf+jhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw -MTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS -TRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGn -SgMld6ZWRhNheZhA6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmi -YYMiIWplidMmMO5NTRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5 -donyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVG -B+5+AAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABo4H1 -MIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm++e -HpyM3p708bgZJuRYEdX1o+UwHwYDVR0jBBgwFoAUncSzT/6HMexyuiU9/7EgHu+o -k5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x -OjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A -AAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br -aS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy+SgMIrwfs -X1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4 -aYqNKFWrRaBRAaaYZ/O1ApRTOrXqRx9Eqr0H1BXLsoAq+mWassL8sf6siae+CpwA -KqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU+idkuqfV2h1BQKgSEhFDABjFdTCN -QDAHsEHsi2M4/jRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj -xqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc/J9DIQM+Xmk= ------END CERTIFICATE----- \ No newline at end of file diff --git a/command/agentproxyshared/auth/cert/test-fixtures/keys/key.pem b/command/agentproxyshared/auth/cert/test-fixtures/keys/key.pem deleted file mode 100644 index add982002..000000000 --- a/command/agentproxyshared/auth/cert/test-fixtures/keys/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftetT8pu -HflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNheZhA -6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMmMO5N -TRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJGuUd -y54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuHC3N2 -DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABAoIBAHR7fFV0eAGaopsX -9OD0TUGlsephBXb43g0GYHfJ/1Ew18w9oaxszJEqkl+PB4W3xZ3yG3e8ZomxDOhF -RreF2WgG5xOfhDogMwu6NodbArfgnAvoC6JnW3qha8HMP4F500RFVyCRcd6A3Frd -rFtaZn/UyCsBAN8/zkwPeYHayo7xX6d9kzgRl9HluEX5PXI5+3uiBDUiM085gkLI -5Cmadh9fMdjfhDXI4x2JYmILpp/9Nlc/krB15s5n1MPNtn3yL0TI0tWp0WlwDCV7 -oUm1SfIM0F1fXGFyFDcqwoIr6JCQgXk6XtTg31YhH1xgUIclUVdtHqmAwAbLdIhQ -GAiHn2kCgYEAwD4pZ8HfpiOG/EHNoWsMATc/5yC7O8F9WbvcHZQIymLY4v/7HKZb -VyOR6UQ5/O2cztSGIuKSF6+OK1C34lOyCuTSOTFrjlgEYtLIXjdGLfFdtOO8GRQR -akVXdwuzNAjTBaH5eXbG+NKcjmCvZL48dQVlfDTVulzFGbcsVTHIMQUCgYEA7IQI -FVsKnY3KqpyGqXq92LMcsT3XgW6X1BIIV+YhJ5AFUFkFrjrbXs94/8XyLfi0xBQy -efK+8g5sMs7koF8LyZEcAXWZJQduaKB71hoLlRaU4VQkL/dl2B6VFmAII/CsRCYh -r9RmDN2PF/mp98Ih9dpC1VqcCDRGoTYsd7jLalMCgYAMgH5k1wDaZxkSMp1S0AlZ -0uP+/evvOOgT+9mWutfPgZolOQx1koQCKLgGeX9j6Xf3I28NubpSfAI84uTyfQrp -FnRtb79U5Hh0jMynA+U2e6niZ6UF5H41cQj9Hu+qhKBkj2IP+h96cwfnYnZFkPGR -kqZE65KyqfHPeFATwkcImQKBgCdrfhlpGiTWXCABhKQ8s+WpPLAB2ahV8XJEKyXT -UlVQuMIChGLcpnFv7P/cUxf8asx/fUY8Aj0/0CLLvulHziQjTmKj4gl86pb/oIQ3 -xRRtNhU0O+/OsSfLORgIm3K6C0w0esregL/GMbJSR1TnA1gBr7/1oSnw5JC8Ab9W -injHAoGAJT1MGAiQrhlt9GCGe6Ajw4omdbY0wS9NXefnFhf7EwL0es52ezZ28zpU -2LXqSFbtann5CHgpSLxiMYPDIf+er4xgg9Bz34tz1if1rDfP2Qrxdrpr4jDnrGT3 -gYC2qCpvVD9RRUMKFfnJTfl5gMQdBW/LINkHtJ82snAeLl3gjQ4= ------END RSA PRIVATE KEY----- diff --git a/command/agentproxyshared/auth/cert/test-fixtures/keys/pkioutput b/command/agentproxyshared/auth/cert/test-fixtures/keys/pkioutput deleted file mode 100644 index 526ff0316..000000000 --- a/command/agentproxyshared/auth/cert/test-fixtures/keys/pkioutput +++ /dev/null @@ -1,74 +0,0 @@ -Key Value -lease_id pki/issue/example-dot-com/d8214077-9976-8c68-9c07-6610da30aea4 -lease_duration 279359999 -lease_renewable false -certificate -----BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIUf+jhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw -MTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS -TRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGn -SgMld6ZWRhNheZhA6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmi -YYMiIWplidMmMO5NTRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5 -donyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVG -B+5+AAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABo4H1 -MIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm++e -HpyM3p708bgZJuRYEdX1o+UwHwYDVR0jBBgwFoAUncSzT/6HMexyuiU9/7EgHu+o -k5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x -OjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A -AAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br -aS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy+SgMIrwfs -X1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4 -aYqNKFWrRaBRAaaYZ/O1ApRTOrXqRx9Eqr0H1BXLsoAq+mWassL8sf6siae+CpwA -KqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU+idkuqfV2h1BQKgSEhFDABjFdTCN -QDAHsEHsi2M4/jRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj -xqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc/J9DIQM+Xmk= ------END CERTIFICATE----- -issuing_ca -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -private_key -----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftetT8pu -HflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNheZhA -6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMmMO5N -TRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJGuUd -y54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuHC3N2 -DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABAoIBAHR7fFV0eAGaopsX -9OD0TUGlsephBXb43g0GYHfJ/1Ew18w9oaxszJEqkl+PB4W3xZ3yG3e8ZomxDOhF -RreF2WgG5xOfhDogMwu6NodbArfgnAvoC6JnW3qha8HMP4F500RFVyCRcd6A3Frd -rFtaZn/UyCsBAN8/zkwPeYHayo7xX6d9kzgRl9HluEX5PXI5+3uiBDUiM085gkLI -5Cmadh9fMdjfhDXI4x2JYmILpp/9Nlc/krB15s5n1MPNtn3yL0TI0tWp0WlwDCV7 -oUm1SfIM0F1fXGFyFDcqwoIr6JCQgXk6XtTg31YhH1xgUIclUVdtHqmAwAbLdIhQ -GAiHn2kCgYEAwD4pZ8HfpiOG/EHNoWsMATc/5yC7O8F9WbvcHZQIymLY4v/7HKZb -VyOR6UQ5/O2cztSGIuKSF6+OK1C34lOyCuTSOTFrjlgEYtLIXjdGLfFdtOO8GRQR -akVXdwuzNAjTBaH5eXbG+NKcjmCvZL48dQVlfDTVulzFGbcsVTHIMQUCgYEA7IQI -FVsKnY3KqpyGqXq92LMcsT3XgW6X1BIIV+YhJ5AFUFkFrjrbXs94/8XyLfi0xBQy -efK+8g5sMs7koF8LyZEcAXWZJQduaKB71hoLlRaU4VQkL/dl2B6VFmAII/CsRCYh -r9RmDN2PF/mp98Ih9dpC1VqcCDRGoTYsd7jLalMCgYAMgH5k1wDaZxkSMp1S0AlZ -0uP+/evvOOgT+9mWutfPgZolOQx1koQCKLgGeX9j6Xf3I28NubpSfAI84uTyfQrp -FnRtb79U5Hh0jMynA+U2e6niZ6UF5H41cQj9Hu+qhKBkj2IP+h96cwfnYnZFkPGR -kqZE65KyqfHPeFATwkcImQKBgCdrfhlpGiTWXCABhKQ8s+WpPLAB2ahV8XJEKyXT -UlVQuMIChGLcpnFv7P/cUxf8asx/fUY8Aj0/0CLLvulHziQjTmKj4gl86pb/oIQ3 -xRRtNhU0O+/OsSfLORgIm3K6C0w0esregL/GMbJSR1TnA1gBr7/1oSnw5JC8Ab9W -injHAoGAJT1MGAiQrhlt9GCGe6Ajw4omdbY0wS9NXefnFhf7EwL0es52ezZ28zpU -2LXqSFbtann5CHgpSLxiMYPDIf+er4xgg9Bz34tz1if1rDfP2Qrxdrpr4jDnrGT3 -gYC2qCpvVD9RRUMKFfnJTfl5gMQdBW/LINkHtJ82snAeLl3gjQ4= ------END RSA PRIVATE KEY----- -private_key_type rsa diff --git a/command/agentproxyshared/auth/cert/test-fixtures/root/pkioutput b/command/agentproxyshared/auth/cert/test-fixtures/root/pkioutput deleted file mode 100644 index 312ae18de..000000000 --- a/command/agentproxyshared/auth/cert/test-fixtures/root/pkioutput +++ /dev/null @@ -1,74 +0,0 @@ -Key Value -lease_id pki/root/generate/exported/7bf99d76-dd3e-2c5b-04ce-5253062ad586 -lease_duration 315359999 -lease_renewable false -certificate -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -expiration 1.772072879e+09 -issuing_ca -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -private_key -----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p -t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 -BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w -/2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv -0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi -18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb -ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn -8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f -nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 -2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t -grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc -bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 -0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN -ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf -lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 -lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj -AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG -ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib -thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU -4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb -iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO -tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y -LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc -4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX -OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= ------END RSA PRIVATE KEY----- -private_key_type rsa -serial_number 6f:98:9d:f8:67:1a:31:e3:27:60:1b:f7:32:f7:53:19:68:a0:c8:9d diff --git a/command/agentproxyshared/auth/cert/test-fixtures/root/root.crl b/command/agentproxyshared/auth/cert/test-fixtures/root/root.crl deleted file mode 100644 index a80c9e411..000000000 --- a/command/agentproxyshared/auth/cert/test-fixtures/root/root.crl +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN X509 CRL----- -MIIBrjCBlzANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbRcN -MTYwMjI5MDIyOTE3WhcNMjUwMTA1MTAyOTE3WjArMCkCFG+YnfhnGjHjJ2Ab9zL3 -UxlooMidFxExNjAyMjgyMTI5MTctMDUwMKAjMCEwHwYDVR0jBBgwFoAUncSzT/6H -MexyuiU9/7EgHu+ok5swDQYJKoZIhvcNAQELBQADggEBAG9YDXpNe4LJroKZmVCn -HqMhW8eyzyaPak2nPPGCVUnc6vt8rlBYQU+xlBizD6xatZQDMPgrT8sBl9W3ysXk -RUlliHsT/SHddMz5dAZsBPRMJ7pYWLTx8jI4w2WRfbSyI4bY/6qTRNkEBUv+Fk8J -xvwB89+EM0ENcVMhv9ghsUA8h7kOg673HKwRstLDAzxS/uLmEzFjj8SV2m5DbV2Y -UUCKRSV20/kxJMIC9x2KikZhwOSyv1UE1otD+RQvbfAoZPUDmvp2FR/E0NGjBBOg -1TtCPRrl63cjqU3s8KQ4uah9Vj+Cwcu9n/yIKKtNQq4NKHvagv8GlUsoJ4BdAxCw -IA0= ------END X509 CRL----- diff --git a/command/agentproxyshared/auth/cert/test-fixtures/root/rootcacert.pem b/command/agentproxyshared/auth/cert/test-fixtures/root/rootcacert.pem deleted file mode 100644 index dcb307a14..000000000 --- a/command/agentproxyshared/auth/cert/test-fixtures/root/rootcacert.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- diff --git a/command/agentproxyshared/auth/cert/test-fixtures/root/rootcakey.pem b/command/agentproxyshared/auth/cert/test-fixtures/root/rootcakey.pem deleted file mode 100644 index e950da5ba..000000000 --- a/command/agentproxyshared/auth/cert/test-fixtures/root/rootcakey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p -t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 -BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w -/2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv -0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi -18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb -ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn -8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f -nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 -2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t -grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc -bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 -0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN -ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf -lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 -lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj -AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG -ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib -thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU -4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb -iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO -tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y -LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc -4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX -OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= ------END RSA PRIVATE KEY----- diff --git a/command/agentproxyshared/auth/jwt/jwt_test.go b/command/agentproxyshared/auth/jwt/jwt_test.go deleted file mode 100644 index 62fbc24e8..000000000 --- a/command/agentproxyshared/auth/jwt/jwt_test.go +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package jwt - -import ( - "bytes" - "os" - "path" - "strings" - "sync/atomic" - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agentproxyshared/auth" -) - -func TestIngressToken(t *testing.T) { - const ( - dir = "dir" - file = "file" - empty = "empty" - missing = "missing" - symlinked = "symlinked" - ) - - rootDir, err := os.MkdirTemp("", "vault-agent-jwt-auth-test") - if err != nil { - t.Fatalf("failed to create temp dir: %s", err) - } - defer os.RemoveAll(rootDir) - - setupTestDir := func() string { - testDir, err := os.MkdirTemp(rootDir, "") - if err != nil { - t.Fatal(err) - } - err = os.WriteFile(path.Join(testDir, file), []byte("test"), 0o644) - if err != nil { - t.Fatal(err) - } - _, err = os.Create(path.Join(testDir, empty)) - if err != nil { - t.Fatal(err) - } - err = os.Mkdir(path.Join(testDir, dir), 0o755) - if err != nil { - t.Fatal(err) - } - err = os.Symlink(path.Join(testDir, file), path.Join(testDir, symlinked)) - if err != nil { - t.Fatal(err) - } - - return testDir - } - - for _, tc := range []struct { - name string - path string - errString string - }{ - { - "happy path", - file, - "", - }, - { - "path is directory", - dir, - "[ERROR] jwt file is not a regular file or symlink", - }, - { - "path is symlink", - symlinked, - "", - }, - { - "path is missing (implies nothing for ingressToken to do)", - missing, - "", - }, - { - "path is empty file", - empty, - "[WARN] empty jwt file read", - }, - } { - testDir := setupTestDir() - logBuffer := bytes.Buffer{} - jwtAuth := &jwtMethod{ - logger: hclog.New(&hclog.LoggerOptions{ - Output: &logBuffer, - }), - latestToken: new(atomic.Value), - path: path.Join(testDir, tc.path), - } - - jwtAuth.ingressToken() - - if tc.errString != "" { - if !strings.Contains(logBuffer.String(), tc.errString) { - t.Fatal("logs did no contain expected error", tc.errString, logBuffer.String()) - } - } else { - if strings.Contains(logBuffer.String(), "[ERROR]") || strings.Contains(logBuffer.String(), "[WARN]") { - t.Fatal("logs contained unexpected error", logBuffer.String()) - } - } - } -} - -func TestDeleteAfterReading(t *testing.T) { - for _, tc := range map[string]struct { - configValue string - shouldDelete bool - }{ - "default": { - "", - true, - }, - "explicit true": { - "true", - true, - }, - "false": { - "false", - false, - }, - } { - rootDir, err := os.MkdirTemp("", "vault-agent-jwt-auth-test") - if err != nil { - t.Fatalf("failed to create temp dir: %s", err) - } - defer os.RemoveAll(rootDir) - tokenPath := path.Join(rootDir, "token") - err = os.WriteFile(tokenPath, []byte("test"), 0o644) - if err != nil { - t.Fatal(err) - } - - config := &auth.AuthConfig{ - Config: map[string]interface{}{ - "path": tokenPath, - "role": "unusedrole", - }, - Logger: hclog.Default(), - } - if tc.configValue != "" { - config.Config["remove_jwt_after_reading"] = tc.configValue - } - - jwtAuth, err := NewJWTAuthMethod(config) - if err != nil { - t.Fatal(err) - } - - jwtAuth.(*jwtMethod).ingressToken() - - if _, err := os.Lstat(tokenPath); tc.shouldDelete { - if err == nil || !os.IsNotExist(err) { - t.Fatal(err) - } - } else { - if err != nil { - t.Fatal(err) - } - } - } -} - -func TestDeleteAfterReadingSymlink(t *testing.T) { - for _, tc := range map[string]struct { - configValue string - shouldDelete bool - removeJWTFollowsSymlinks bool - }{ - "default": { - "", - true, - false, - }, - "explicit true": { - "true", - true, - false, - }, - "false": { - "false", - false, - false, - }, - "default + removeJWTFollowsSymlinks": { - "", - true, - true, - }, - "explicit true + removeJWTFollowsSymlinks": { - "true", - true, - true, - }, - "false + removeJWTFollowsSymlinks": { - "false", - false, - true, - }, - } { - rootDir, err := os.MkdirTemp("", "vault-agent-jwt-auth-test") - if err != nil { - t.Fatalf("failed to create temp dir: %s", err) - } - defer os.RemoveAll(rootDir) - tokenPath := path.Join(rootDir, "token") - err = os.WriteFile(tokenPath, []byte("test"), 0o644) - if err != nil { - t.Fatal(err) - } - - symlink, err := os.CreateTemp("", "auth.jwt.symlink.test.") - if err != nil { - t.Fatal(err) - } - symlinkName := symlink.Name() - symlink.Close() - os.Remove(symlinkName) - os.Symlink(tokenPath, symlinkName) - - config := &auth.AuthConfig{ - Config: map[string]interface{}{ - "path": symlinkName, - "role": "unusedrole", - }, - Logger: hclog.Default(), - } - if tc.configValue != "" { - config.Config["remove_jwt_after_reading"] = tc.configValue - } - config.Config["remove_jwt_follows_symlinks"] = tc.removeJWTFollowsSymlinks - - jwtAuth, err := NewJWTAuthMethod(config) - if err != nil { - t.Fatal(err) - } - - jwtAuth.(*jwtMethod).ingressToken() - - pathToCheck := symlinkName - if tc.removeJWTFollowsSymlinks { - pathToCheck = tokenPath - } - if _, err := os.Lstat(pathToCheck); tc.shouldDelete { - if err == nil || !os.IsNotExist(err) { - t.Fatal(err) - } - } else { - if err != nil { - t.Fatal(err) - } - } - } -} diff --git a/command/agentproxyshared/auth/kerberos/integtest/integrationtest.sh b/command/agentproxyshared/auth/kerberos/integtest/integrationtest.sh deleted file mode 100755 index 6b8a6925d..000000000 --- a/command/agentproxyshared/auth/kerberos/integtest/integrationtest.sh +++ /dev/null @@ -1,173 +0,0 @@ -#!/bin/bash -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -# Instructions -# This integration test is for the Vault Kerberos agent. -# Before running, execute: -# pip install --quiet requests-kerberos -# Then run this test from Vault's home directory. -# ./command/agent/auth/kerberos/integtest/integrationtest.sh - -if [[ "$OSTYPE" == "darwin"* ]]; then - base64cmd="base64 -D" -else - base64cmd="base64 -d" -fi - -VAULT_PORT=8200 -SAMBA_VER=4.8.12 - -export VAULT_TOKEN=${VAULT_TOKEN:-myroot} -DOMAIN_ADMIN_PASS=Pa55word! -DOMAIN_VAULT_ACCOUNT=vault_svc -DOMAIN_VAULT_PASS=vaultPa55word! -DOMAIN_USER_ACCOUNT=grace -DOMAIN_USER_PASS=gracePa55word! - -SAMBA_CONF_FILE=/srv/etc/smb.conf -DOMAIN_NAME=matrix -DNS_NAME=host -REALM_NAME=MATRIX.LAN -DOMAIN_DN=DC=MATRIX,DC=LAN -TESTS_DIR=/tmp/vault_plugin_tests - -function add_user() { - - username="${1}" - password="${2}" - - if [[ $(check_user ${username}) -eq 0 ]] - then - echo "add user '${username}'" - - docker exec $SAMBA_CONTAINER \ - /usr/bin/samba-tool user create \ - ${username} \ - ${password}\ - --configfile=${SAMBA_CONF_FILE} - fi -} - -function check_user() { - - username="${1}" - - docker exec $SAMBA_CONTAINER \ - /usr/bin/samba-tool user list \ - --configfile=${SAMBA_CONF_FILE} \ - | grep -c ${username} -} - -function create_keytab() { - - username="${1}" - password="${2}" - - user_kvno=$(docker exec $SAMBA_CONTAINER \ - bash -c "ldapsearch -H ldaps://localhost -D \"Administrator@${REALM_NAME}\" -w \"${DOMAIN_ADMIN_PASS}\" -b \"CN=Users,${DOMAIN_DN}\" -LLL \"(&(objectClass=user)(sAMAccountName=${username}))\" msDS-KeyVersionNumber | sed -n 's/^[ \t]*msDS-KeyVersionNumber:[ \t]*\(.*\)/\1/p'") - - docker exec $SAMBA_CONTAINER \ - bash -c "printf \"%b\" \"addent -password -p \"${username}@${REALM_NAME}\" -k ${user_kvno} -e rc4-hmac\n${password}\nwrite_kt ${username}.keytab\" | ktutil" - - docker exec $SAMBA_CONTAINER \ - bash -c "printf \"%b\" \"read_kt ${username}.keytab\nlist\" | ktutil" - - docker exec $SAMBA_CONTAINER \ - base64 ${username}.keytab > ${TESTS_DIR}/integration/${username}.keytab.base64 - - docker cp $SAMBA_CONTAINER:/${username}.keytab ${TESTS_DIR}/integration/ -} - -function main() { - # make and start vault - make dev - vault server -dev -dev-root-token-id=root & - - # start our domain controller - SAMBA_CONTAINER=$(docker run --net=${DNS_NAME} -d -ti --privileged -e "SAMBA_DC_ADMIN_PASSWD=${DOMAIN_ADMIN_PASS}" -e "KERBEROS_PASSWORD=${DOMAIN_ADMIN_PASS}" -e SAMBA_DC_DOMAIN=${DOMAIN_NAME} -e SAMBA_DC_REALM=${REALM_NAME} "bodsch/docker-samba4:${SAMBA_VER}") - sleep 15 - - # set up users - add_user $DOMAIN_VAULT_ACCOUNT $DOMAIN_VAULT_PASS - create_keytab $DOMAIN_VAULT_ACCOUNT $DOMAIN_VAULT_PASS - - add_user $DOMAIN_USER_ACCOUNT $DOMAIN_USER_PASS - create_keytab $DOMAIN_USER_ACCOUNT $DOMAIN_USER_PASS - - # add the service principals we'll need - docker exec $SAMBA_CONTAINER \ - samba-tool spn add HTTP/localhost ${DOMAIN_VAULT_ACCOUNT} --configfile=${SAMBA_CONF_FILE} - docker exec $SAMBA_CONTAINER \ - samba-tool spn add HTTP/localhost:${VAULT_PORT} ${DOMAIN_VAULT_ACCOUNT} --configfile=${SAMBA_CONF_FILE} - docker exec $SAMBA_CONTAINER \ - samba-tool spn add HTTP/localhost.${DNS_NAME} ${DOMAIN_VAULT_ACCOUNT} --configfile=${SAMBA_CONF_FILE} - docker exec $SAMBA_CONTAINER \ - samba-tool spn add HTTP/localhost.${DNS_NAME}:${VAULT_PORT} ${DOMAIN_VAULT_ACCOUNT} --configfile=${SAMBA_CONF_FILE} - - # enable and configure the kerberos plugin in Vault - vault auth enable -passthrough-request-headers=Authorization -allowed-response-headers=www-authenticate kerberos - vault write auth/kerberos/config keytab=@${TESTS_DIR}/integration/vault_svc.keytab.base64 service_account="vault_svc" - vault write auth/kerberos/config/ldap binddn=${DOMAIN_VAULT_ACCOUNT}@${REALM_NAME} bindpass=${DOMAIN_VAULT_PASS} groupattr=sAMAccountName groupdn="${DOMAIN_DN}" groupfilter="(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))" insecure_tls=true starttls=true userdn="CN=Users,${DOMAIN_DN}" userattr=sAMAccountName upndomain=${REALM_NAME} url=ldaps://localhost:636 - - mkdir -p ${TESTS_DIR}/integration - - echo " -[libdefaults] - default_realm = ${REALM_NAME} - dns_lookup_realm = false - dns_lookup_kdc = true - ticket_lifetime = 24h - renew_lifetime = 7d - forwardable = true - rdns = false - preferred_preauth_types = 23 -[realms] - ${REALM_NAME} = { - kdc = localhost - admin_server = localhost - master_kdc = localhost - default_domain = localhost - } -" > ${TESTS_DIR}/integration/krb5.conf - - echo " -auto_auth { - method \"kerberos\" { - mount_path = \"auth/kerberos\" - config = { - username = \"$DOMAIN_USER_ACCOUNT\" - service = \"HTTP/localhost:8200\" - realm = \"$REALM_NAME\" - keytab_path = \"$TESTS_DIR/integration/grace.keytab\" - krb5conf_path = \"$TESTS_DIR/integration/krb5.conf\" - } - } - sink \"file\" { - config = { - path = \"$TESTS_DIR/integration/agent-token.txt\" - } - } -} -" > ${TESTS_DIR}/integration/agent.conf - - vault agent -config=${TESTS_DIR}/integration/agent.conf & - sleep 10 - token=$(cat $TESTS_DIR/integration/agent-token.txt) - - # clean up: kill vault and stop the docker container we started - kill -9 $(ps aux | grep vault | awk '{print $2}' | head -1) # kill vault server - kill -9 $(ps aux | grep vault | awk '{print $2}' | head -1) # kill vault agent - docker rm -f ${SAMBA_CONTAINER} - - # a valid Vault token starts with "s.", check for that - if [[ $token != s.* ]]; then - echo "received invalid token: $token" - return 1 - fi - - echo "vault kerberos agent obtained auth token: $token" - echo "exiting successfully!" - return 0 -} -main diff --git a/command/agentproxyshared/auth/kerberos/kerberos_test.go b/command/agentproxyshared/auth/kerberos/kerberos_test.go deleted file mode 100644 index 819cb7dff..000000000 --- a/command/agentproxyshared/auth/kerberos/kerberos_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package kerberos - -import ( - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agentproxyshared/auth" -) - -func TestNewKerberosAuthMethod(t *testing.T) { - if _, err := NewKerberosAuthMethod(nil); err == nil { - t.Fatal("err should be returned for nil input") - } - if _, err := NewKerberosAuthMethod(&auth.AuthConfig{}); err == nil { - t.Fatal("err should be returned for nil config map") - } - - authConfig := simpleAuthConfig() - delete(authConfig.Config, "username") - if _, err := NewKerberosAuthMethod(authConfig); err == nil { - t.Fatal("err should be returned for missing username") - } - - authConfig = simpleAuthConfig() - delete(authConfig.Config, "service") - if _, err := NewKerberosAuthMethod(authConfig); err == nil { - t.Fatal("err should be returned for missing service") - } - - authConfig = simpleAuthConfig() - delete(authConfig.Config, "realm") - if _, err := NewKerberosAuthMethod(authConfig); err == nil { - t.Fatal("err should be returned for missing realm") - } - - authConfig = simpleAuthConfig() - delete(authConfig.Config, "keytab_path") - if _, err := NewKerberosAuthMethod(authConfig); err == nil { - t.Fatal("err should be returned for missing keytab_path") - } - - authConfig = simpleAuthConfig() - delete(authConfig.Config, "krb5conf_path") - if _, err := NewKerberosAuthMethod(authConfig); err == nil { - t.Fatal("err should be returned for missing krb5conf_path") - } - - authConfig = simpleAuthConfig() - authMethod, err := NewKerberosAuthMethod(authConfig) - if err != nil { - t.Fatal(err) - } - - // False by default - if actual := authMethod.(*kerberosMethod).loginCfg.DisableFASTNegotiation; actual { - t.Fatalf("disable_fast_negotation should be false, it wasn't: %t", actual) - } - - authConfig.Config["disable_fast_negotiation"] = "true" - authMethod, err = NewKerberosAuthMethod(authConfig) - if err != nil { - t.Fatal(err) - } - - // True from override - if actual := authMethod.(*kerberosMethod).loginCfg.DisableFASTNegotiation; !actual { - t.Fatalf("disable_fast_negotation should be true, it wasn't: %t", actual) - } -} - -func simpleAuthConfig() *auth.AuthConfig { - return &auth.AuthConfig{ - Logger: hclog.NewNullLogger(), - MountPath: "kerberos", - WrapTTL: 20, - Config: map[string]interface{}{ - "username": "grace", - "service": "HTTP/05a65fad28ef.matrix.lan:8200", - "realm": "MATRIX.LAN", - "keytab_path": "grace.keytab", - "krb5conf_path": "krb5.conf", - }, - } -} diff --git a/command/agentproxyshared/auth/kubernetes/kubernetes_test.go b/command/agentproxyshared/auth/kubernetes/kubernetes_test.go deleted file mode 100644 index 93b348c7f..000000000 --- a/command/agentproxyshared/auth/kubernetes/kubernetes_test.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package kubernetes - -import ( - "bytes" - "context" - "errors" - "io" - "testing" - - "github.com/hashicorp/errwrap" - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agentproxyshared/auth" - "github.com/hashicorp/vault/sdk/helper/logging" -) - -func TestKubernetesAuth_basic(t *testing.T) { - testCases := map[string]struct { - tokenPath string - data *mockJWTFile - e error - }{ - "normal": { - data: newMockJWTFile(jwtData), - }, - "projected": { - tokenPath: "/some/other/path", - data: newMockJWTFile(jwtProjectedData), - }, - "not_found": { - e: errors.New("open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory"), - }, - "projected_not_found": { - tokenPath: "/some/other/path", - e: errors.New("open /some/other/path: no such file or directory"), - }, - } - - for k, tc := range testCases { - t.Run(k, func(t *testing.T) { - authCfg := auth.AuthConfig{ - Logger: logging.NewVaultLogger(hclog.Trace), - MountPath: "kubernetes", - Config: map[string]interface{}{ - "role": "plugin-test", - }, - } - - if tc.tokenPath != "" { - authCfg.Config["token_path"] = tc.tokenPath - } - - a, err := NewKubernetesAuthMethod(&authCfg) - if err != nil { - t.Fatal(err) - } - - // Type assert to set the kubernetesMethod jwtData, to mock out reading - // files from the pod. - k := a.(*kubernetesMethod) - if tc.data != nil { - k.jwtData = tc.data - } - - _, _, data, err := k.Authenticate(context.Background(), nil) - if err != nil && tc.e == nil { - t.Fatal(err) - } - - if err != nil && !errwrap.Contains(err, tc.e.Error()) { - t.Fatalf("expected \"no such file\" error, got: (%s)", err) - } - - if err == nil && tc.e != nil { - t.Fatal("expected error, but got none") - } - - if tc.e == nil { - authJWTraw, ok := data["jwt"] - if !ok { - t.Fatal("expected to find jwt data") - } - - authJWT := authJWTraw.(string) - token := jwtData - if tc.tokenPath != "" { - token = jwtProjectedData - } - if authJWT != token { - t.Fatalf("error with auth tokens, expected (%s) got (%s)", token, authJWT) - } - } - }) - } -} - -// jwt for default service account -var jwtData = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InZhdWx0LWF1dGgtdG9rZW4tdDVwY24iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoidmF1bHQtYXV0aCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImQ3N2Y4OWJjLTkwNTUtMTFlNy1hMDY4LTA4MDAyNzZkOTliZiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0LWF1dGgifQ.HKUcqgrvan5ZC_mnpaMEx4RW3KrhfyH_u8G_IA2vUfkLK8tH3T7fJuJaPr7W6K_BqCrbeM5y3owszOzb4NR0Lvw6GBt2cFcen2x1Ua4Wokr0bJjTT7xQOIOw7UvUDyVS17wAurlfUnmWMwMMMOebpqj5K1t6GnyqghH1wPdHYRGX-q5a6C323dBCgM5t6JY_zTTaBgM6EkFq0poBaifmSMiJRPrdUN_-IgyK8fgQRiFYYkgS6DMIU4k4nUOb_sUFf5xb8vMs3SMteKiuWFAIt4iszXTj5IyBUNqe0cXA3zSY3QiNCV6bJ2CWW0Qf9WDtniT79VAqcR4GYaTC_gxjNA" - -// jwt for projected service account -var jwtProjectedData = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJhdWQiOlsia3ViZXJuZXRlcy5kZWZhdWx0LnN2YyJdLCJleHAiOjE2MDMwNTM1NjMsImlhdCI6MTUzOTk4MTU2MywiaXNzIjoia3ViZXJuZXRlcy9zZXJ2aWNlYWNjb3VudCIsImt1YmVybmV0ZXMuaW8iOnsibmFtZXNwYWNlIjoiZGVmYXVsdCIsInBvZCI6eyJuYW1lIjoidmF1bHQiLCJ1aWQiOiIxMDA2YTA2Yy1kM2RmLTExZTgtOGZlMi0wODAwMjdlNTVlYTgifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJiMzg5YjNiMi1kMzAyLTExZTgtYjE0Yy0wODAwMjdlNTVlYTgifX0sIm5iZiI6MTUzOTk4MTU2Mywic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6ZGVmYXVsdCJ9.byu3BpCbs0tzQvEBCRTayXF3-kV1Ey7YvStBcCwovfSl6evBze43FFaDps78HtdDAMszjE_yn55_1BMN87EzOZYsF3GBoPLWxkofxhPIy88wmPTpurBsSx-nCKdjf4ayXhTpqGG9gy0xlkUc_xL4pM3Q8XZiqYqwq_T0PHXOpSfdzVy1oabFSZXr5QTZ377v8bvrMgAVWJF_4vZsSMG3XVCK8KBWNRw4_wt6yOelVKE5OGLPJvNu1CFjEKh4HBFBcQnB_Sgpe1nPlnm5utp-1-OVfd7zopOGDAp_Pk_Apu8OPDdPSafn6HpzIeuhMtWXcv1K8ZhZYDLC1wLywZPNyw" - -// mockJWTFile provides a mock ReadCloser struct to inject into -// kubernetesMethod.jwtData -type mockJWTFile struct { - b *bytes.Buffer -} - -var _ io.ReadCloser = &mockJWTFile{} - -func (j *mockJWTFile) Read(p []byte) (n int, err error) { - return j.b.Read(p) -} - -func (j *mockJWTFile) Close() error { return nil } - -func newMockJWTFile(s string) *mockJWTFile { - return &mockJWTFile{ - b: bytes.NewBufferString(s), - } -} diff --git a/command/agentproxyshared/auth/token-file/token_file_test.go b/command/agentproxyshared/auth/token-file/token_file_test.go deleted file mode 100644 index 7e6e8982b..000000000 --- a/command/agentproxyshared/auth/token-file/token_file_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package token_file - -import ( - "os" - "path/filepath" - "testing" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agentproxyshared/auth" - "github.com/hashicorp/vault/sdk/helper/logging" -) - -func TestNewTokenFileAuthMethodEmptyConfig(t *testing.T) { - logger := logging.NewVaultLogger(log.Trace) - _, err := NewTokenFileAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.method"), - Config: map[string]interface{}{}, - }) - if err == nil { - t.Fatal("Expected error due to empty config") - } -} - -func TestNewTokenFileEmptyFilePath(t *testing.T) { - logger := logging.NewVaultLogger(log.Trace) - _, err := NewTokenFileAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.method"), - Config: map[string]interface{}{ - "token_file_path": "", - }, - }) - if err == nil { - t.Fatalf("Expected error when giving empty file path") - } -} - -func TestNewTokenFileAuthenticate(t *testing.T) { - tokenFile, err := os.Create(filepath.Join(t.TempDir(), "token_file")) - tokenFileContents := "super-secret-token" - if err != nil { - t.Fatal(err) - } - tokenFileName := tokenFile.Name() - tokenFile.Close() // WriteFile doesn't need it open - os.WriteFile(tokenFileName, []byte(tokenFileContents), 0o666) - defer os.Remove(tokenFileName) - - logger := logging.NewVaultLogger(log.Trace) - am, err := NewTokenFileAuthMethod(&auth.AuthConfig{ - Logger: logger.Named("auth.method"), - Config: map[string]interface{}{ - "token_file_path": tokenFileName, - }, - }) - if err != nil { - t.Fatal(err) - } - - path, headers, data, err := am.Authenticate(nil, nil) - if err != nil { - t.Fatal(err) - } - if path != "auth/token/lookup-self" { - t.Fatalf("Incorrect path, was %s", path) - } - if headers != nil { - t.Fatalf("Expected no headers, instead got %v", headers) - } - if data == nil { - t.Fatal("Data was nil") - } - tokenDataFromAuthMethod := data["token"].(string) - if tokenDataFromAuthMethod != tokenFileContents { - t.Fatalf("Incorrect token file contents return by auth method, expected %s, got %s", tokenFileContents, tokenDataFromAuthMethod) - } - - _, err = os.Stat(tokenFileName) - if err != nil { - t.Fatal("Token file removed") - } -} diff --git a/command/agentproxyshared/cache/api_proxy_test.go b/command/agentproxyshared/cache/api_proxy_test.go deleted file mode 100644 index fa53c27d1..000000000 --- a/command/agentproxyshared/cache/api_proxy_test.go +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cache - -import ( - "context" - "fmt" - "net" - "net/http" - "os" - "testing" - "time" - - "github.com/hashicorp/vault/helper/useragent" - - "github.com/hashicorp/vault/builtin/credential/userpass" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/helper/logging" -) - -const policyAdmin = ` -path "*" { - capabilities = ["sudo", "create", "read", "update", "delete", "list"] -} -` - -func TestAPIProxy(t *testing.T) { - cleanup, client, _, _ := setupClusterAndAgent(namespace.RootContext(nil), t, nil) - defer cleanup() - - proxier, err := NewAPIProxy(&APIProxyConfig{ - Client: client, - Logger: logging.NewVaultLogger(hclog.Trace), - UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, - UserAgentString: useragent.ProxyAPIProxyString(), - }) - if err != nil { - t.Fatal(err) - } - - r := client.NewRequest("GET", "/v1/sys/health") - req, err := r.ToHTTP() - if err != nil { - t.Fatal(err) - } - - resp, err := proxier.Send(namespace.RootContext(nil), &SendRequest{ - Request: req, - }) - if err != nil { - t.Fatal(err) - } - - var result api.HealthResponse - err = jsonutil.DecodeJSONFromReader(resp.Response.Body, &result) - if err != nil { - t.Fatal(err) - } - - if !result.Initialized || result.Sealed || result.Standby { - t.Fatalf("bad sys/health response: %#v", result) - } -} - -func TestAPIProxyNoCache(t *testing.T) { - cleanup, client, _, _ := setupClusterAndAgentNoCache(namespace.RootContext(nil), t, nil) - defer cleanup() - - proxier, err := NewAPIProxy(&APIProxyConfig{ - Client: client, - Logger: logging.NewVaultLogger(hclog.Trace), - UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, - UserAgentString: useragent.ProxyAPIProxyString(), - }) - if err != nil { - t.Fatal(err) - } - - r := client.NewRequest("GET", "/v1/sys/health") - req, err := r.ToHTTP() - if err != nil { - t.Fatal(err) - } - - resp, err := proxier.Send(namespace.RootContext(nil), &SendRequest{ - Request: req, - }) - if err != nil { - t.Fatal(err) - } - - var result api.HealthResponse - err = jsonutil.DecodeJSONFromReader(resp.Response.Body, &result) - if err != nil { - t.Fatal(err) - } - - if !result.Initialized || result.Sealed || result.Standby { - t.Fatalf("bad sys/health response: %#v", result) - } -} - -func TestAPIProxy_queryParams(t *testing.T) { - // Set up an agent that points to a standby node for this particular test - // since it needs to proxy a /sys/health?standbyok=true request to a standby - cleanup, client, _, _ := setupClusterAndAgentOnStandby(namespace.RootContext(nil), t, nil) - defer cleanup() - - proxier, err := NewAPIProxy(&APIProxyConfig{ - Client: client, - Logger: logging.NewVaultLogger(hclog.Trace), - UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, - UserAgentString: useragent.ProxyAPIProxyString(), - }) - if err != nil { - t.Fatal(err) - } - - r := client.NewRequest("GET", "/v1/sys/health") - req, err := r.ToHTTP() - if err != nil { - t.Fatal(err) - } - - // Add a query parameter for testing - q := req.URL.Query() - q.Add("standbyok", "true") - req.URL.RawQuery = q.Encode() - - resp, err := proxier.Send(namespace.RootContext(nil), &SendRequest{ - Request: req, - }) - if err != nil { - t.Fatal(err) - } - - var result api.HealthResponse - err = jsonutil.DecodeJSONFromReader(resp.Response.Body, &result) - if err != nil { - t.Fatal(err) - } - - if !result.Initialized || result.Sealed || !result.Standby { - t.Fatalf("bad sys/health response: %#v", result) - } - - if resp.Response.StatusCode != http.StatusOK { - t.Fatalf("exptected standby to return 200, got: %v", resp.Response.StatusCode) - } -} - -// setupClusterAndAgent is a helper func used to set up a test cluster and -// caching agent against the active node. It returns a cleanup func that should -// be deferred immediately along with two clients, one for direct cluster -// communication and another to talk to the caching agent. -func setupClusterAndAgent(ctx context.Context, t *testing.T, coreConfig *vault.CoreConfig) (func(), *api.Client, *api.Client, *LeaseCache) { - return setupClusterAndAgentCommon(ctx, t, coreConfig, false, true) -} - -// setupClusterAndAgentNoCache is a helper func used to set up a test cluster and -// proxying agent against the active node. It returns a cleanup func that should -// be deferred immediately along with two clients, one for direct cluster -// communication and another to talk to the caching agent. -func setupClusterAndAgentNoCache(ctx context.Context, t *testing.T, coreConfig *vault.CoreConfig) (func(), *api.Client, *api.Client, *LeaseCache) { - return setupClusterAndAgentCommon(ctx, t, coreConfig, false, false) -} - -// setupClusterAndAgentOnStandby is a helper func used to set up a test cluster -// and caching agent against a standby node. It returns a cleanup func that -// should be deferred immediately along with two clients, one for direct cluster -// communication and another to talk to the caching agent. -func setupClusterAndAgentOnStandby(ctx context.Context, t *testing.T, coreConfig *vault.CoreConfig) (func(), *api.Client, *api.Client, *LeaseCache) { - return setupClusterAndAgentCommon(ctx, t, coreConfig, true, true) -} - -func setupClusterAndAgentCommon(ctx context.Context, t *testing.T, coreConfig *vault.CoreConfig, onStandby bool, useCache bool) (func(), *api.Client, *api.Client, *LeaseCache) { - t.Helper() - - if ctx == nil { - ctx = context.Background() - } - - // Handle sane defaults - if coreConfig == nil { - coreConfig = &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: logging.NewVaultLogger(hclog.Trace), - } - } - - // Always set up the userpass backend since we use that to generate an admin - // token for the client that will make proxied requests to through the agent. - if coreConfig.CredentialBackends == nil || coreConfig.CredentialBackends["userpass"] == nil { - coreConfig.CredentialBackends = map[string]logical.Factory{ - "userpass": userpass.Factory, - } - } - - // Init new test cluster - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - - activeClient := cores[0].Client - standbyClient := cores[1].Client - - // clienToUse is the client for the agent to point to. - clienToUse := activeClient - if onStandby { - clienToUse = standbyClient - } - - // Add an admin policy - if err := activeClient.Sys().PutPolicy("admin", policyAdmin); err != nil { - t.Fatal(err) - } - - // Set up the userpass auth backend and an admin user. Used for getting a token - // for the agent later down in this func. - err := activeClient.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", - }) - if err != nil { - t.Fatal(err) - } - - _, err = activeClient.Logical().Write("auth/userpass/users/foo", map[string]interface{}{ - "password": "bar", - "policies": []string{"admin"}, - }) - if err != nil { - t.Fatal(err) - } - - // Set up env vars for agent consumption - origEnvVaultAddress := os.Getenv(api.EnvVaultAddress) - os.Setenv(api.EnvVaultAddress, clienToUse.Address()) - - origEnvVaultCACert := os.Getenv(api.EnvVaultCACert) - os.Setenv(api.EnvVaultCACert, fmt.Sprintf("%s/ca_cert.pem", cluster.TempDir)) - - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - - apiProxyLogger := logging.NewVaultLogger(hclog.Trace).Named("apiproxy") - - // Create the API proxier - apiProxy, err := NewAPIProxy(&APIProxyConfig{ - Client: clienToUse, - Logger: apiProxyLogger, - UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, - UserAgentString: useragent.ProxyAPIProxyString(), - }) - if err != nil { - t.Fatal(err) - } - - // Create a muxer and add paths relevant for the lease cache layer and API proxy layer - mux := http.NewServeMux() - - var leaseCache *LeaseCache - if useCache { - cacheLogger := logging.NewVaultLogger(hclog.Trace).Named("cache") - - // Create the lease cache proxier and set its underlying proxier to - // the API proxier. - leaseCache, err = NewLeaseCache(&LeaseCacheConfig{ - Client: clienToUse, - BaseContext: ctx, - Proxier: apiProxy, - Logger: cacheLogger.Named("leasecache"), - }) - if err != nil { - t.Fatal(err) - } - - mux.Handle("/agent/v1/cache-clear", leaseCache.HandleCacheClear(ctx)) - - mux.Handle("/", ProxyHandler(ctx, cacheLogger, leaseCache, nil, true)) - } else { - mux.Handle("/", ProxyHandler(ctx, apiProxyLogger, apiProxy, nil, true)) - } - - server := &http.Server{ - Handler: mux, - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 30 * time.Second, - IdleTimeout: 5 * time.Minute, - ErrorLog: apiProxyLogger.StandardLogger(nil), - } - go server.Serve(listener) - - // testClient is the client that is used to talk to the agent for proxying/caching behavior. - testClient, err := activeClient.Clone() - if err != nil { - t.Fatal(err) - } - - if err := testClient.SetAddress("http://" + listener.Addr().String()); err != nil { - t.Fatal(err) - } - - // Login via userpass method to derive a managed token. Set that token as the - // testClient's token - resp, err := testClient.Logical().Write("auth/userpass/login/foo", map[string]interface{}{ - "password": "bar", - }) - if err != nil { - t.Fatal(err) - } - testClient.SetToken(resp.Auth.ClientToken) - - cleanup := func() { - // We wait for a tiny bit for things such as agent renewal to exit properly - time.Sleep(50 * time.Millisecond) - - cluster.Cleanup() - os.Setenv(api.EnvVaultAddress, origEnvVaultAddress) - os.Setenv(api.EnvVaultCACert, origEnvVaultCACert) - listener.Close() - } - - return cleanup, clienToUse, testClient, leaseCache -} diff --git a/command/agentproxyshared/cache/cache_test.go b/command/agentproxyshared/cache/cache_test.go deleted file mode 100644 index 0ec9f2205..000000000 --- a/command/agentproxyshared/cache/cache_test.go +++ /dev/null @@ -1,1242 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cache - -import ( - "context" - "encoding/json" - "fmt" - "io" - "math/rand" - "net" - "net/http" - "sync" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/hashicorp/go-hclog" - kv "github.com/hashicorp/vault-plugin-secrets-kv" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agentproxyshared/cache/cachememdb" - "github.com/hashicorp/vault/command/agentproxyshared/sink/mock" - "github.com/hashicorp/vault/helper/namespace" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func tokenRevocationValidation(t *testing.T, sampleSpace map[string]string, expected map[string]string, leaseCache *LeaseCache) { - t.Helper() - for val, valType := range sampleSpace { - index, err := leaseCache.db.Get(valType, val) - if err != nil { - t.Fatal(err) - } - if expected[val] == "" && index != nil { - t.Fatalf("failed to evict index from the cache: type: %q, value: %q", valType, val) - } - if expected[val] != "" && index == nil { - t.Fatalf("evicted an undesired index from cache: type: %q, value: %q", valType, val) - } - } -} - -func TestCache_AutoAuthTokenStripping(t *testing.T) { - response1 := `{"data": {"id": "testid", "accessor": "testaccessor", "request": "lookup-self"}}` - response2 := `{"data": {"id": "testid", "accessor": "testaccessor", "request": "lookup"}}` - response3 := `{"auth": {"client_token": "testid", "accessor": "testaccessor"}}` - response4 := `{"auth": {"client_token": "testid", "accessor": "testaccessor"}}` - responses := []*SendResponse{ - newTestSendResponse(http.StatusOK, response1), - newTestSendResponse(http.StatusOK, response2), - newTestSendResponse(http.StatusOK, response3), - newTestSendResponse(http.StatusOK, response4), - } - - leaseCache := testNewLeaseCache(t, responses) - - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - client := cores[0].Client - - cacheLogger := logging.NewVaultLogger(hclog.Trace).Named("cache") - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - - ctx := namespace.RootContext(nil) - - // Create a muxer and add paths relevant for the lease cache layer - mux := http.NewServeMux() - mux.Handle(consts.AgentPathCacheClear, leaseCache.HandleCacheClear(ctx)) - - mux.Handle("/", ProxyHandler(ctx, cacheLogger, leaseCache, mock.NewSink("testid"), true)) - server := &http.Server{ - Handler: mux, - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 30 * time.Second, - IdleTimeout: 5 * time.Minute, - ErrorLog: cacheLogger.StandardLogger(nil), - } - go server.Serve(listener) - - testClient, err := client.Clone() - if err != nil { - t.Fatal(err) - } - - if err := testClient.SetAddress("http://" + listener.Addr().String()); err != nil { - t.Fatal(err) - } - - // Empty the token in the client. Auto-auth token should be put to use. - testClient.SetToken("") - secret, err := testClient.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - if secret.Data["id"] != nil || secret.Data["accessor"] != nil || secret.Data["request"].(string) != "lookup-self" { - t.Fatalf("failed to strip off auto-auth token on lookup-self") - } - - secret, err = testClient.Auth().Token().Lookup("") - if err != nil { - t.Fatal(err) - } - if secret.Data["id"] != nil || secret.Data["accessor"] != nil || secret.Data["request"].(string) != "lookup" { - t.Fatalf("failed to strip off auto-auth token on lookup") - } - - secret, err = testClient.Auth().Token().RenewSelf(1) - if err != nil { - t.Fatal(err) - } - if secret.Auth == nil { - secretJson, _ := json.Marshal(secret) - t.Fatalf("Expected secret to have Auth but was %s", secretJson) - } - if secret.Auth.ClientToken != "" || secret.Auth.Accessor != "" { - t.Fatalf("failed to strip off auto-auth token on renew-self") - } - - secret, err = testClient.Auth().Token().Renew("testid", 1) - if err != nil { - t.Fatal(err) - } - if secret.Auth == nil { - secretJson, _ := json.Marshal(secret) - t.Fatalf("Expected secret to have Auth but was %s", secretJson) - } - if secret.Auth.ClientToken != "" || secret.Auth.Accessor != "" { - t.Fatalf("failed to strip off auto-auth token on renew") - } -} - -func TestCache_AutoAuthClientTokenProxyStripping(t *testing.T) { - leaseCache := &mockTokenVerifierProxier{} - dummyToken := "DUMMY" - realToken := "testid" - - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - vault.TestWaitActive(t, cores[0].Core) - client := cores[0].Client - - cacheLogger := logging.NewVaultLogger(hclog.Trace).Named("cache") - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - - ctx := namespace.RootContext(nil) - - // Create a muxer and add paths relevant for the lease cache layer - mux := http.NewServeMux() - // mux.Handle(consts.AgentPathCacheClear, leaseCache.HandleCacheClear(ctx)) - - mux.Handle("/", ProxyHandler(ctx, cacheLogger, leaseCache, mock.NewSink(realToken), false)) - server := &http.Server{ - Handler: mux, - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 30 * time.Second, - IdleTimeout: 5 * time.Minute, - ErrorLog: cacheLogger.StandardLogger(nil), - } - go server.Serve(listener) - - testClient, err := client.Clone() - if err != nil { - t.Fatal(err) - } - - if err := testClient.SetAddress("http://" + listener.Addr().String()); err != nil { - t.Fatal(err) - } - - // Empty the token in the client. Auto-auth token should be put to use. - testClient.SetToken(dummyToken) - _, err = testClient.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - if leaseCache.currentToken != realToken { - t.Fatalf("failed to use real token from auto-auth") - } -} - -func TestCache_ConcurrentRequests(t *testing.T) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - } - - cleanup, _, testClient, _ := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) - defer cleanup() - - err := testClient.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - wg := &sync.WaitGroup{} - for i := 0; i < 100; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - key := fmt.Sprintf("kv/foo/%d_%d", i, rand.Int()) - _, err := testClient.Logical().Write(key, map[string]interface{}{ - "key": key, - }) - if err != nil { - t.Fatal(err) - } - secret, err := testClient.Logical().Read(key) - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data["key"].(string) != key { - t.Fatal(fmt.Sprintf("failed to read value for key: %q", key)) - } - }(i) - - } - wg.Wait() -} - -func TestCache_TokenRevocations_RevokeOrphan(t *testing.T) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - } - - sampleSpace := make(map[string]string) - - cleanup, _, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) - defer cleanup() - - token1 := testClient.Token() - sampleSpace[token1] = "token" - - // Mount the kv backend - err := testClient.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - // Create a secret in the backend - _, err = testClient.Logical().Write("kv/foo", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - // Read the secret and create a lease - leaseResp, err := testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease1 := leaseResp.LeaseID - sampleSpace[lease1] = "lease" - - resp, err := testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token2 := resp.Auth.ClientToken - sampleSpace[token2] = "token" - - testClient.SetToken(token2) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease2 := leaseResp.LeaseID - sampleSpace[lease2] = "lease" - - resp, err = testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token3 := resp.Auth.ClientToken - sampleSpace[token3] = "token" - - testClient.SetToken(token3) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease3 := leaseResp.LeaseID - sampleSpace[lease3] = "lease" - - expected := make(map[string]string) - for k, v := range sampleSpace { - expected[k] = v - } - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) - - // Revoke-orphan the intermediate token. This should result in its own - // eviction and evictions of the revoked token's leases. All other things - // including the child tokens and leases of the child tokens should be - // untouched. - testClient.SetToken(token2) - err = testClient.Auth().Token().RevokeOrphan(token2) - if err != nil { - t.Fatal(err) - } - time.Sleep(1 * time.Second) - - expected = map[string]string{ - token1: "token", - lease1: "lease", - token3: "token", - lease3: "lease", - } - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) -} - -func TestCache_TokenRevocations_LeafLevelToken(t *testing.T) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - } - - sampleSpace := make(map[string]string) - - cleanup, _, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) - defer cleanup() - - token1 := testClient.Token() - sampleSpace[token1] = "token" - - // Mount the kv backend - err := testClient.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - // Create a secret in the backend - _, err = testClient.Logical().Write("kv/foo", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - // Read the secret and create a lease - leaseResp, err := testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease1 := leaseResp.LeaseID - sampleSpace[lease1] = "lease" - - resp, err := testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token2 := resp.Auth.ClientToken - sampleSpace[token2] = "token" - - testClient.SetToken(token2) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease2 := leaseResp.LeaseID - sampleSpace[lease2] = "lease" - - resp, err = testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token3 := resp.Auth.ClientToken - sampleSpace[token3] = "token" - - testClient.SetToken(token3) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease3 := leaseResp.LeaseID - sampleSpace[lease3] = "lease" - - expected := make(map[string]string) - for k, v := range sampleSpace { - expected[k] = v - } - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) - - // Revoke the lef token. This should evict all the leases belonging to this - // token, evict entries for all the child tokens and their respective - // leases. - testClient.SetToken(token3) - err = testClient.Auth().Token().RevokeSelf("") - if err != nil { - t.Fatal(err) - } - time.Sleep(1 * time.Second) - - expected = map[string]string{ - token1: "token", - lease1: "lease", - token2: "token", - lease2: "lease", - } - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) -} - -func TestCache_TokenRevocations_IntermediateLevelToken(t *testing.T) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - } - - sampleSpace := make(map[string]string) - - cleanup, _, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) - defer cleanup() - - token1 := testClient.Token() - sampleSpace[token1] = "token" - - // Mount the kv backend - err := testClient.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - // Create a secret in the backend - _, err = testClient.Logical().Write("kv/foo", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - // Read the secret and create a lease - leaseResp, err := testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease1 := leaseResp.LeaseID - sampleSpace[lease1] = "lease" - - resp, err := testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token2 := resp.Auth.ClientToken - sampleSpace[token2] = "token" - - testClient.SetToken(token2) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease2 := leaseResp.LeaseID - sampleSpace[lease2] = "lease" - - resp, err = testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token3 := resp.Auth.ClientToken - sampleSpace[token3] = "token" - - testClient.SetToken(token3) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease3 := leaseResp.LeaseID - sampleSpace[lease3] = "lease" - - expected := make(map[string]string) - for k, v := range sampleSpace { - expected[k] = v - } - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) - - // Revoke the second level token. This should evict all the leases - // belonging to this token, evict entries for all the child tokens and - // their respective leases. - testClient.SetToken(token2) - err = testClient.Auth().Token().RevokeSelf("") - if err != nil { - t.Fatal(err) - } - time.Sleep(1 * time.Second) - - expected = map[string]string{ - token1: "token", - lease1: "lease", - } - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) -} - -func TestCache_TokenRevocations_TopLevelToken(t *testing.T) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - } - - sampleSpace := make(map[string]string) - - cleanup, _, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) - defer cleanup() - - token1 := testClient.Token() - sampleSpace[token1] = "token" - - // Mount the kv backend - err := testClient.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - // Create a secret in the backend - _, err = testClient.Logical().Write("kv/foo", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - // Read the secret and create a lease - leaseResp, err := testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease1 := leaseResp.LeaseID - sampleSpace[lease1] = "lease" - - resp, err := testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token2 := resp.Auth.ClientToken - sampleSpace[token2] = "token" - - testClient.SetToken(token2) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease2 := leaseResp.LeaseID - sampleSpace[lease2] = "lease" - - resp, err = testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token3 := resp.Auth.ClientToken - sampleSpace[token3] = "token" - - testClient.SetToken(token3) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease3 := leaseResp.LeaseID - sampleSpace[lease3] = "lease" - - expected := make(map[string]string) - for k, v := range sampleSpace { - expected[k] = v - } - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) - - // Revoke the top level token. This should evict all the leases belonging - // to this token, evict entries for all the child tokens and their - // respective leases. - testClient.SetToken(token1) - err = testClient.Auth().Token().RevokeSelf("") - if err != nil { - t.Fatal(err) - } - time.Sleep(1 * time.Second) - - expected = make(map[string]string) - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) -} - -func TestCache_TokenRevocations_Shutdown(t *testing.T) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - } - - sampleSpace := make(map[string]string) - - ctx, rootCancelFunc := context.WithCancel(namespace.RootContext(nil)) - cleanup, _, testClient, leaseCache := setupClusterAndAgent(ctx, t, coreConfig) - defer cleanup() - - token1 := testClient.Token() - sampleSpace[token1] = "token" - - // Mount the kv backend - err := testClient.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - // Create a secret in the backend - _, err = testClient.Logical().Write("kv/foo", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - // Read the secret and create a lease - leaseResp, err := testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease1 := leaseResp.LeaseID - sampleSpace[lease1] = "lease" - - resp, err := testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token2 := resp.Auth.ClientToken - sampleSpace[token2] = "token" - - testClient.SetToken(token2) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease2 := leaseResp.LeaseID - sampleSpace[lease2] = "lease" - - resp, err = testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token3 := resp.Auth.ClientToken - sampleSpace[token3] = "token" - - testClient.SetToken(token3) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease3 := leaseResp.LeaseID - sampleSpace[lease3] = "lease" - - expected := make(map[string]string) - for k, v := range sampleSpace { - expected[k] = v - } - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) - - rootCancelFunc() - time.Sleep(1 * time.Second) - - // Ensure that all the entries are now gone - expected = make(map[string]string) - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) -} - -func TestCache_TokenRevocations_BaseContextCancellation(t *testing.T) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - } - - sampleSpace := make(map[string]string) - - cleanup, _, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) - defer cleanup() - - token1 := testClient.Token() - sampleSpace[token1] = "token" - - // Mount the kv backend - err := testClient.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - // Create a secret in the backend - _, err = testClient.Logical().Write("kv/foo", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - // Read the secret and create a lease - leaseResp, err := testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease1 := leaseResp.LeaseID - sampleSpace[lease1] = "lease" - - resp, err := testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token2 := resp.Auth.ClientToken - sampleSpace[token2] = "token" - - testClient.SetToken(token2) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease2 := leaseResp.LeaseID - sampleSpace[lease2] = "lease" - - resp, err = testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token3 := resp.Auth.ClientToken - sampleSpace[token3] = "token" - - testClient.SetToken(token3) - - leaseResp, err = testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - lease3 := leaseResp.LeaseID - sampleSpace[lease3] = "lease" - - expected := make(map[string]string) - for k, v := range sampleSpace { - expected[k] = v - } - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) - - // Cancel the base context of the lease cache. This should trigger - // evictions of all the entries from the cache. - leaseCache.baseCtxInfo.CancelFunc() - time.Sleep(1 * time.Second) - - // Ensure that all the entries are now gone - expected = make(map[string]string) - tokenRevocationValidation(t, sampleSpace, expected, leaseCache) -} - -func TestCache_NonCacheable(t *testing.T) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": kv.Factory, - }, - } - - cleanup, _, testClient, _ := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) - defer cleanup() - - // Query mounts first - origMounts, err := testClient.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - // Mount a kv backend - if err := testClient.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - Options: map[string]string{ - "version": "2", - }, - }); err != nil { - t.Fatal(err) - } - - // Query mounts again - newMounts, err := testClient.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - if diff := deep.Equal(origMounts, newMounts); diff == nil { - t.Logf("response #1: %#v", origMounts) - t.Logf("response #2: %#v", newMounts) - t.Fatal("expected requests to be not cached") - } - - // Query a non-existing mount, expect an error from api.Response - ctx, cancelFunc := context.WithCancel(context.Background()) - defer cancelFunc() - r := testClient.NewRequest("GET", "/v1/kv-invalid") - - apiResp, err := testClient.RawRequestWithContext(ctx, r) - if apiResp != nil { - defer apiResp.Body.Close() - } - if apiResp.Error() == nil || (apiResp != nil && apiResp.StatusCode != 404) { - t.Fatalf("expected an error response and a 404 from requesting an invalid path, got: %#v", apiResp) - } - if err == nil { - t.Fatal("expected an error from requesting an invalid path") - } -} - -func TestCache_Caching_AuthResponse(t *testing.T) { - cleanup, _, testClient, _ := setupClusterAndAgent(namespace.RootContext(nil), t, nil) - defer cleanup() - - resp, err := testClient.Logical().Write("auth/token/create", nil) - if err != nil { - t.Fatal(err) - } - token := resp.Auth.ClientToken - testClient.SetToken(token) - - authTokeCreateReq := func(t *testing.T, policies map[string]interface{}) *api.Secret { - resp, err := testClient.Logical().Write("auth/token/create", policies) - if err != nil { - t.Fatal(err) - } - if resp.Auth == nil || resp.Auth.ClientToken == "" { - t.Fatalf("expected a valid client token in the response, got = %#v", resp) - } - - return resp - } - - // Test on auth response by creating a child token - { - proxiedResp := authTokeCreateReq(t, map[string]interface{}{ - "policies": "default", - }) - - cachedResp := authTokeCreateReq(t, map[string]interface{}{ - "policies": "default", - }) - - if diff := deep.Equal(proxiedResp.Auth.ClientToken, cachedResp.Auth.ClientToken); diff != nil { - t.Fatal(diff) - } - } - - // Test on *non-renewable* auth response by creating a child root token - { - proxiedResp := authTokeCreateReq(t, nil) - - cachedResp := authTokeCreateReq(t, nil) - - if diff := deep.Equal(proxiedResp.Auth.ClientToken, cachedResp.Auth.ClientToken); diff != nil { - t.Fatal(diff) - } - } -} - -func TestCache_Caching_LeaseResponse(t *testing.T) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - } - - cleanup, client, testClient, _ := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) - defer cleanup() - - err := client.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - // Test proxy by issuing two different requests - { - // Write data to the lease-kv backend - _, err := testClient.Logical().Write("kv/foo", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - _, err = testClient.Logical().Write("kv/foobar", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - firstResp, err := testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - - secondResp, err := testClient.Logical().Read("kv/foobar") - if err != nil { - t.Fatal(err) - } - - if diff := deep.Equal(firstResp, secondResp); diff == nil { - t.Logf("response: %#v", firstResp) - t.Fatal("expected proxied responses, got cached response on second request") - } - } - - // Test caching behavior by issue the same request twice - { - _, err := testClient.Logical().Write("kv/baz", map[string]interface{}{ - "value": "foo", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - proxiedResp, err := testClient.Logical().Read("kv/baz") - if err != nil { - t.Fatal(err) - } - - cachedResp, err := testClient.Logical().Read("kv/baz") - if err != nil { - t.Fatal(err) - } - - if diff := deep.Equal(proxiedResp, cachedResp); diff != nil { - t.Fatal(diff) - } - } -} - -func TestCache_Caching_CacheClear(t *testing.T) { - t.Run("request_path", func(t *testing.T) { - testCachingCacheClearCommon(t, "request_path") - }) - - t.Run("lease", func(t *testing.T) { - testCachingCacheClearCommon(t, "lease") - }) - - t.Run("token", func(t *testing.T) { - testCachingCacheClearCommon(t, "token") - }) - - t.Run("token_accessor", func(t *testing.T) { - testCachingCacheClearCommon(t, "token_accessor") - }) - - t.Run("all", func(t *testing.T) { - testCachingCacheClearCommon(t, "all") - }) -} - -func testCachingCacheClearCommon(t *testing.T, clearType string) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: hclog.NewNullLogger(), - LogicalBackends: map[string]logical.Factory{ - "kv": vault.LeasedPassthroughBackendFactory, - }, - } - - cleanup, client, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) - defer cleanup() - - err := client.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - }) - if err != nil { - t.Fatal(err) - } - - // Write data to the lease-kv backend - _, err = testClient.Logical().Write("kv/foo", map[string]interface{}{ - "value": "bar", - "ttl": "1h", - }) - if err != nil { - t.Fatal(err) - } - - // Proxy this request, agent should cache the response - resp, err := testClient.Logical().Read("kv/foo") - if err != nil { - t.Fatal(err) - } - gotLeaseID := resp.LeaseID - - // Verify the entry exists - idx, err := leaseCache.db.Get(cachememdb.IndexNameLease, gotLeaseID) - if err != nil { - t.Fatal(err) - } - - if idx == nil { - t.Fatalf("expected cached entry, got: %v", idx) - } - - data := map[string]interface{}{ - "type": clearType, - } - - // We need to set the value here depending on what we're trying to test. - // Some values are be static, but others are dynamically generated at runtime. - switch clearType { - case "request_path": - data["value"] = "/v1/kv/foo" - case "lease": - data["value"] = resp.LeaseID - case "token": - data["value"] = testClient.Token() - case "token_accessor": - lookupResp, err := client.Auth().Token().Lookup(testClient.Token()) - if err != nil { - t.Fatal(err) - } - data["value"] = lookupResp.Data["accessor"] - case "all": - default: - t.Fatalf("invalid type provided: %v", clearType) - } - - r := testClient.NewRequest("PUT", consts.AgentPathCacheClear) - if err := r.SetJSONBody(data); err != nil { - t.Fatal(err) - } - - ctx, cancelFunc := context.WithCancel(context.Background()) - defer cancelFunc() - apiResp, err := testClient.RawRequestWithContext(ctx, r) - if apiResp != nil { - defer apiResp.Body.Close() - } - if apiResp != nil && apiResp.StatusCode == 404 { - _, parseErr := api.ParseSecret(apiResp.Body) - switch parseErr { - case nil: - case io.EOF: - default: - t.Fatal(err) - } - } - if err != nil { - t.Fatal(err) - } - - time.Sleep(100 * time.Millisecond) - - // Verify the entry is cleared - idx, err = leaseCache.db.Get(cachememdb.IndexNameLease, gotLeaseID) - if err != nil { - t.Fatal(err) - } - - if idx != nil { - t.Fatalf("expected entry to be nil, got: %v", idx) - } -} - -func TestCache_AuthTokenCreateOrphan(t *testing.T) { - t.Run("create", func(t *testing.T) { - t.Run("managed", func(t *testing.T) { - cleanup, _, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, nil) - defer cleanup() - - reqOpts := &api.TokenCreateRequest{ - Policies: []string{"default"}, - NoParent: true, - } - resp, err := testClient.Auth().Token().Create(reqOpts) - if err != nil { - t.Fatal(err) - } - token := resp.Auth.ClientToken - - idx, err := leaseCache.db.Get(cachememdb.IndexNameToken, token) - if err != nil { - t.Fatal(err) - } - if idx == nil { - t.Fatalf("expected entry to be non-nil, got: %#v", idx) - } - }) - - t.Run("non-managed", func(t *testing.T) { - cleanup, clusterClient, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, nil) - defer cleanup() - - reqOpts := &api.TokenCreateRequest{ - Policies: []string{"default"}, - NoParent: true, - } - - // Use the test client but set the token to one that's not managed by agent - testClient.SetToken(clusterClient.Token()) - - resp, err := testClient.Auth().Token().Create(reqOpts) - if err != nil { - t.Fatal(err) - } - token := resp.Auth.ClientToken - - idx, err := leaseCache.db.Get(cachememdb.IndexNameToken, token) - if err != nil { - t.Fatal(err) - } - if idx == nil { - t.Fatalf("expected entry to be non-nil, got: %#v", idx) - } - }) - }) - - t.Run("create-orphan", func(t *testing.T) { - t.Run("managed", func(t *testing.T) { - cleanup, _, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, nil) - defer cleanup() - - reqOpts := &api.TokenCreateRequest{ - Policies: []string{"default"}, - } - resp, err := testClient.Auth().Token().CreateOrphan(reqOpts) - if err != nil { - t.Fatal(err) - } - token := resp.Auth.ClientToken - - idx, err := leaseCache.db.Get(cachememdb.IndexNameToken, token) - if err != nil { - t.Fatal(err) - } - if idx == nil { - t.Fatalf("expected entry to be non-nil, got: %#v", idx) - } - }) - - t.Run("non-managed", func(t *testing.T) { - cleanup, clusterClient, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, nil) - defer cleanup() - - reqOpts := &api.TokenCreateRequest{ - Policies: []string{"default"}, - } - - // Use the test client but set the token to one that's not managed by agent - testClient.SetToken(clusterClient.Token()) - - resp, err := testClient.Auth().Token().CreateOrphan(reqOpts) - if err != nil { - t.Fatal(err) - } - token := resp.Auth.ClientToken - - idx, err := leaseCache.db.Get(cachememdb.IndexNameToken, token) - if err != nil { - t.Fatal(err) - } - if idx == nil { - t.Fatalf("expected entry to be non-nil, got: %#v", idx) - } - }) - }) -} diff --git a/command/agentproxyshared/cache/cacheboltdb/bolt_test.go b/command/agentproxyshared/cache/cacheboltdb/bolt_test.go deleted file mode 100644 index 6d4c7a051..000000000 --- a/command/agentproxyshared/cache/cacheboltdb/bolt_test.go +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cacheboltdb - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/golang/protobuf/proto" - bolt "github.com/hashicorp-forge/bbolt" - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agentproxyshared/cache/keymanager" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func getTestKeyManager(t *testing.T) keymanager.KeyManager { - t.Helper() - - km, err := keymanager.NewPassthroughKeyManager(context.Background(), nil) - require.NoError(t, err) - - return km -} - -func TestBolt_SetGet(t *testing.T) { - ctx := context.Background() - - path, err := ioutil.TempDir("", "bolt-test") - require.NoError(t, err) - defer os.RemoveAll(path) - - b, err := NewBoltStorage(&BoltStorageConfig{ - Path: path, - Logger: hclog.Default(), - Wrapper: getTestKeyManager(t).Wrapper(), - }) - require.NoError(t, err) - - secrets, err := b.GetByType(ctx, LeaseType) - assert.NoError(t, err) - require.Len(t, secrets, 0) - - err = b.Set(ctx, "test1", []byte("hello"), LeaseType) - assert.NoError(t, err) - secrets, err = b.GetByType(ctx, LeaseType) - assert.NoError(t, err) - require.Len(t, secrets, 1) - assert.Equal(t, []byte("hello"), secrets[0]) -} - -func TestBoltDelete(t *testing.T) { - ctx := context.Background() - - path, err := ioutil.TempDir("", "bolt-test") - require.NoError(t, err) - defer os.RemoveAll(path) - - b, err := NewBoltStorage(&BoltStorageConfig{ - Path: path, - Logger: hclog.Default(), - Wrapper: getTestKeyManager(t).Wrapper(), - }) - require.NoError(t, err) - - err = b.Set(ctx, "secret-test1", []byte("hello1"), LeaseType) - require.NoError(t, err) - err = b.Set(ctx, "secret-test2", []byte("hello2"), LeaseType) - require.NoError(t, err) - - secrets, err := b.GetByType(ctx, LeaseType) - require.NoError(t, err) - assert.Len(t, secrets, 2) - assert.ElementsMatch(t, [][]byte{[]byte("hello1"), []byte("hello2")}, secrets) - - err = b.Delete("secret-test1", LeaseType) - require.NoError(t, err) - secrets, err = b.GetByType(ctx, LeaseType) - require.NoError(t, err) - require.Len(t, secrets, 1) - assert.Equal(t, []byte("hello2"), secrets[0]) -} - -func TestBoltClear(t *testing.T) { - ctx := context.Background() - - path, err := ioutil.TempDir("", "bolt-test") - require.NoError(t, err) - defer os.RemoveAll(path) - - b, err := NewBoltStorage(&BoltStorageConfig{ - Path: path, - Logger: hclog.Default(), - Wrapper: getTestKeyManager(t).Wrapper(), - }) - require.NoError(t, err) - - // Populate the bolt db - err = b.Set(ctx, "secret-test1", []byte("hello1"), LeaseType) - require.NoError(t, err) - secrets, err := b.GetByType(ctx, LeaseType) - require.NoError(t, err) - require.Len(t, secrets, 1) - assert.Equal(t, []byte("hello1"), secrets[0]) - - err = b.Set(ctx, "auth-test1", []byte("hello2"), LeaseType) - require.NoError(t, err) - auths, err := b.GetByType(ctx, LeaseType) - require.NoError(t, err) - require.Len(t, auths, 2) - assert.Equal(t, []byte("hello1"), auths[0]) - assert.Equal(t, []byte("hello2"), auths[1]) - - err = b.Set(ctx, "token-test1", []byte("hello"), TokenType) - require.NoError(t, err) - tokens, err := b.GetByType(ctx, TokenType) - require.NoError(t, err) - require.Len(t, tokens, 1) - assert.Equal(t, []byte("hello"), tokens[0]) - - // Clear the bolt db, and check that it's indeed clear - err = b.Clear() - require.NoError(t, err) - auths, err = b.GetByType(ctx, LeaseType) - require.NoError(t, err) - assert.Len(t, auths, 0) - tokens, err = b.GetByType(ctx, TokenType) - require.NoError(t, err) - assert.Len(t, tokens, 0) -} - -func TestBoltSetAutoAuthToken(t *testing.T) { - ctx := context.Background() - - path, err := ioutil.TempDir("", "bolt-test") - require.NoError(t, err) - defer os.RemoveAll(path) - - b, err := NewBoltStorage(&BoltStorageConfig{ - Path: path, - Logger: hclog.Default(), - Wrapper: getTestKeyManager(t).Wrapper(), - }) - require.NoError(t, err) - - token, err := b.GetAutoAuthToken(ctx) - assert.NoError(t, err) - assert.Nil(t, token) - - // set first token - err = b.Set(ctx, "token-test1", []byte("hello 1"), TokenType) - require.NoError(t, err) - secrets, err := b.GetByType(ctx, TokenType) - require.NoError(t, err) - require.Len(t, secrets, 1) - assert.Equal(t, []byte("hello 1"), secrets[0]) - token, err = b.GetAutoAuthToken(ctx) - assert.NoError(t, err) - assert.Equal(t, []byte("hello 1"), token) - - // set second token - err = b.Set(ctx, "token-test2", []byte("hello 2"), TokenType) - require.NoError(t, err) - secrets, err = b.GetByType(ctx, TokenType) - require.NoError(t, err) - require.Len(t, secrets, 2) - assert.ElementsMatch(t, [][]byte{[]byte("hello 1"), []byte("hello 2")}, secrets) - token, err = b.GetAutoAuthToken(ctx) - assert.NoError(t, err) - assert.Equal(t, []byte("hello 2"), token) -} - -func TestDBFileExists(t *testing.T) { - testCases := []struct { - name string - mkDir bool - createFile bool - expectExist bool - }{ - { - name: "all exists", - mkDir: true, - createFile: true, - expectExist: true, - }, - { - name: "dir exist, file missing", - mkDir: true, - createFile: false, - expectExist: false, - }, - { - name: "all missing", - mkDir: false, - createFile: false, - expectExist: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - var tmpPath string - var err error - if tc.mkDir { - tmpPath, err = ioutil.TempDir("", "test-db-path") - require.NoError(t, err) - } - if tc.createFile { - err = ioutil.WriteFile(path.Join(tmpPath, DatabaseFileName), []byte("test-db-path"), 0o600) - require.NoError(t, err) - } - exists, err := DBFileExists(tmpPath) - assert.NoError(t, err) - assert.Equal(t, tc.expectExist, exists) - }) - } -} - -func Test_SetGetRetrievalToken(t *testing.T) { - testCases := []struct { - name string - tokenToSet []byte - expectedToken []byte - }{ - { - name: "normal set and get", - tokenToSet: []byte("test token"), - expectedToken: []byte("test token"), - }, - { - name: "no token set", - tokenToSet: nil, - expectedToken: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - path, err := ioutil.TempDir("", "bolt-test") - require.NoError(t, err) - defer os.RemoveAll(path) - - b, err := NewBoltStorage(&BoltStorageConfig{ - Path: path, - Logger: hclog.Default(), - Wrapper: getTestKeyManager(t).Wrapper(), - }) - require.NoError(t, err) - defer b.Close() - - if tc.tokenToSet != nil { - err := b.StoreRetrievalToken(tc.tokenToSet) - require.NoError(t, err) - } - gotKey, err := b.GetRetrievalToken() - assert.NoError(t, err) - assert.Equal(t, tc.expectedToken, gotKey) - }) - } -} - -func TestBolt_MigrateFromV1ToV2Schema(t *testing.T) { - ctx := context.Background() - - path, err := ioutil.TempDir("", "bolt-test") - require.NoError(t, err) - defer os.RemoveAll(path) - - dbPath := filepath.Join(path, DatabaseFileName) - db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: 1 * time.Second}) - require.NoError(t, err) - err = db.Update(func(tx *bolt.Tx) error { - return createBoltSchema(tx, "1") - }) - require.NoError(t, err) - b := &BoltStorage{ - db: db, - logger: hclog.Default(), - wrapper: getTestKeyManager(t).Wrapper(), - } - - // Manually insert some items into the v1 schema. - err = db.Update(func(tx *bolt.Tx) error { - blob, err := b.wrapper.Encrypt(ctx, []byte("ignored-contents")) - if err != nil { - return fmt.Errorf("error encrypting contents: %w", err) - } - protoBlob, err := proto.Marshal(blob) - if err != nil { - return err - } - - if err := tx.Bucket([]byte(authLeaseType)).Put([]byte("test-auth-id-1"), protoBlob); err != nil { - return err - } - if err := tx.Bucket([]byte(authLeaseType)).Put([]byte("test-auth-id-2"), protoBlob); err != nil { - return err - } - if err := tx.Bucket([]byte(secretLeaseType)).Put([]byte("test-secret-id-1"), protoBlob); err != nil { - return err - } - - return nil - }) - require.NoError(t, err) - - // Check we have the contents we would expect for the v1 schema. - leases, err := b.GetByType(ctx, authLeaseType) - require.NoError(t, err) - assert.Len(t, leases, 2) - leases, err = b.GetByType(ctx, secretLeaseType) - require.NoError(t, err) - assert.Len(t, leases, 1) - leases, err = b.GetByType(ctx, LeaseType) - require.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "not found")) - - // Now migrate to the v2 schema. - err = db.Update(migrateFromV1ToV2Schema) - require.NoError(t, err) - - // Check all the leases have been migrated into one bucket. - leases, err = b.GetByType(ctx, authLeaseType) - require.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "not found")) - leases, err = b.GetByType(ctx, secretLeaseType) - require.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "not found")) - leases, err = b.GetByType(ctx, LeaseType) - require.NoError(t, err) - assert.Len(t, leases, 3) -} - -func TestBolt_MigrateFromInvalidToV2Schema(t *testing.T) { - ctx := context.Background() - - path, err := ioutil.TempDir("", "bolt-test") - require.NoError(t, err) - defer os.RemoveAll(path) - - dbPath := filepath.Join(path, DatabaseFileName) - db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: 1 * time.Second}) - require.NoError(t, err) - b := &BoltStorage{ - db: db, - logger: hclog.Default(), - wrapper: getTestKeyManager(t).Wrapper(), - } - - // All GetByType calls should fail as there's no schema - for _, bucket := range []string{authLeaseType, secretLeaseType, LeaseType} { - _, err = b.GetByType(ctx, bucket) - require.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "not found")) - } - - // Now migrate to the v2 schema. - err = db.Update(migrateFromV1ToV2Schema) - require.NoError(t, err) - - // Deprecated auth and secret lease buckets still shouldn't exist - // All GetByType calls should fail as there's no schema - for _, bucket := range []string{authLeaseType, secretLeaseType} { - _, err = b.GetByType(ctx, bucket) - require.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "not found")) - } - - // GetByType for LeaseType should now return an empty result - leases, err := b.GetByType(ctx, LeaseType) - require.NoError(t, err) - require.Len(t, leases, 0) -} diff --git a/command/agentproxyshared/cache/cachememdb/cache_memdb_test.go b/command/agentproxyshared/cache/cachememdb/cache_memdb_test.go deleted file mode 100644 index 0a08e9a70..000000000 --- a/command/agentproxyshared/cache/cachememdb/cache_memdb_test.go +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cachememdb - -import ( - "context" - "testing" - - "github.com/go-test/deep" -) - -func testContextInfo() *ContextInfo { - ctx, cancelFunc := context.WithCancel(context.Background()) - - return &ContextInfo{ - Ctx: ctx, - CancelFunc: cancelFunc, - } -} - -func TestNew(t *testing.T) { - _, err := New() - if err != nil { - t.Fatal(err) - } -} - -func TestCacheMemDB_Get(t *testing.T) { - cache, err := New() - if err != nil { - t.Fatal(err) - } - - // Test invalid index name - _, err = cache.Get("foo", "bar") - if err == nil { - t.Fatal("expected error") - } - - // Test on empty cache - index, err := cache.Get(IndexNameID, "foo") - if err != nil { - t.Fatal(err) - } - if index != nil { - t.Fatalf("expected nil index, got: %v", index) - } - - // Populate cache - in := &Index{ - ID: "test_id", - Namespace: "test_ns/", - RequestPath: "/v1/request/path", - Token: "test_token", - TokenAccessor: "test_accessor", - Lease: "test_lease", - Response: []byte("hello world"), - } - - if err := cache.Set(in); err != nil { - t.Fatal(err) - } - - testCases := []struct { - name string - indexName string - indexValues []interface{} - }{ - { - "by_index_id", - "id", - []interface{}{in.ID}, - }, - { - "by_request_path", - "request_path", - []interface{}{in.Namespace, in.RequestPath}, - }, - { - "by_lease", - "lease", - []interface{}{in.Lease}, - }, - { - "by_token", - "token", - []interface{}{in.Token}, - }, - { - "by_token_accessor", - "token_accessor", - []interface{}{in.TokenAccessor}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - out, err := cache.Get(tc.indexName, tc.indexValues...) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(in, out); diff != nil { - t.Fatal(diff) - } - }) - } -} - -func TestCacheMemDB_GetByPrefix(t *testing.T) { - cache, err := New() - if err != nil { - t.Fatal(err) - } - - // Test invalid index name - _, err = cache.GetByPrefix("foo", "bar", "baz") - if err == nil { - t.Fatal("expected error") - } - - // Test on empty cache - index, err := cache.GetByPrefix(IndexNameRequestPath, "foo", "bar") - if err != nil { - t.Fatal(err) - } - if index != nil { - t.Fatalf("expected nil index, got: %v", index) - } - - // Populate cache - in := &Index{ - ID: "test_id", - Namespace: "test_ns/", - RequestPath: "/v1/request/path/1", - Token: "test_token", - TokenParent: "test_token_parent", - TokenAccessor: "test_accessor", - Lease: "path/to/test_lease/1", - LeaseToken: "test_lease_token", - Response: []byte("hello world"), - } - - if err := cache.Set(in); err != nil { - t.Fatal(err) - } - - // Populate cache - in2 := &Index{ - ID: "test_id_2", - Namespace: "test_ns/", - RequestPath: "/v1/request/path/2", - Token: "test_token2", - TokenParent: "test_token_parent", - TokenAccessor: "test_accessor2", - Lease: "path/to/test_lease/2", - LeaseToken: "test_lease_token", - Response: []byte("hello world"), - } - - if err := cache.Set(in2); err != nil { - t.Fatal(err) - } - - testCases := []struct { - name string - indexName string - indexValues []interface{} - }{ - { - "by_request_path", - "request_path", - []interface{}{"test_ns/", "/v1/request/path"}, - }, - { - "by_lease", - "lease", - []interface{}{"path/to/test_lease"}, - }, - { - "by_token_parent", - "token_parent", - []interface{}{"test_token_parent"}, - }, - { - "by_lease_token", - "lease_token", - []interface{}{"test_lease_token"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - out, err := cache.GetByPrefix(tc.indexName, tc.indexValues...) - if err != nil { - t.Fatal(err) - } - - if diff := deep.Equal([]*Index{in, in2}, out); diff != nil { - t.Fatal(diff) - } - }) - } -} - -func TestCacheMemDB_Set(t *testing.T) { - cache, err := New() - if err != nil { - t.Fatal(err) - } - - testCases := []struct { - name string - index *Index - wantErr bool - }{ - { - "nil", - nil, - true, - }, - { - "empty_fields", - &Index{}, - true, - }, - { - "missing_required_fields", - &Index{ - Lease: "foo", - }, - true, - }, - { - "all_fields", - &Index{ - ID: "test_id", - Namespace: "test_ns/", - RequestPath: "/v1/request/path", - Token: "test_token", - TokenAccessor: "test_accessor", - Lease: "test_lease", - RenewCtxInfo: testContextInfo(), - }, - false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if err := cache.Set(tc.index); (err != nil) != tc.wantErr { - t.Fatalf("CacheMemDB.Set() error = %v, wantErr = %v", err, tc.wantErr) - } - }) - } -} - -func TestCacheMemDB_Evict(t *testing.T) { - cache, err := New() - if err != nil { - t.Fatal(err) - } - - // Test on empty cache - if err := cache.Evict(IndexNameID, "foo"); err != nil { - t.Fatal(err) - } - - testIndex := &Index{ - ID: "test_id", - Namespace: "test_ns/", - RequestPath: "/v1/request/path", - Token: "test_token", - TokenAccessor: "test_token_accessor", - Lease: "test_lease", - RenewCtxInfo: testContextInfo(), - } - - testCases := []struct { - name string - indexName string - indexValues []interface{} - insertIndex *Index - wantErr bool - }{ - { - "empty_params", - "", - []interface{}{""}, - nil, - true, - }, - { - "invalid_params", - "foo", - []interface{}{"bar"}, - nil, - true, - }, - { - "by_id", - "id", - []interface{}{"test_id"}, - testIndex, - false, - }, - { - "by_request_path", - "request_path", - []interface{}{"test_ns/", "/v1/request/path"}, - testIndex, - false, - }, - { - "by_token", - "token", - []interface{}{"test_token"}, - testIndex, - false, - }, - { - "by_token_accessor", - "token_accessor", - []interface{}{"test_accessor"}, - testIndex, - false, - }, - { - "by_lease", - "lease", - []interface{}{"test_lease"}, - testIndex, - false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if tc.insertIndex != nil { - if err := cache.Set(tc.insertIndex); err != nil { - t.Fatal(err) - } - } - - if err := cache.Evict(tc.indexName, tc.indexValues...); (err != nil) != tc.wantErr { - t.Fatal(err) - } - - // Verify that the cache doesn't contain the entry any more - index, err := cache.Get(tc.indexName, tc.indexValues...) - if (err != nil) != tc.wantErr { - t.Fatal(err) - } - - if index != nil { - t.Fatalf("expected nil entry, got = %#v", index) - } - }) - } -} - -func TestCacheMemDB_Flush(t *testing.T) { - cache, err := New() - if err != nil { - t.Fatal(err) - } - - // Populate cache - in := &Index{ - ID: "test_id", - Token: "test_token", - Lease: "test_lease", - Namespace: "test_ns/", - RequestPath: "/v1/request/path", - Response: []byte("hello world"), - } - - if err := cache.Set(in); err != nil { - t.Fatal(err) - } - - // Reset the cache - if err := cache.Flush(); err != nil { - t.Fatal(err) - } - - // Check the cache doesn't contain inserted index - out, err := cache.Get(IndexNameID, "test_id") - if err != nil { - t.Fatal(err) - } - if out != nil { - t.Fatalf("expected cache to be empty, got = %v", out) - } -} diff --git a/command/agentproxyshared/cache/cachememdb/index_test.go b/command/agentproxyshared/cache/cachememdb/index_test.go deleted file mode 100644 index 871ac023e..000000000 --- a/command/agentproxyshared/cache/cachememdb/index_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cachememdb - -import ( - "context" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSerializeDeserialize(t *testing.T) { - testIndex := &Index{ - ID: "testid", - Token: "testtoken", - TokenParent: "parent token", - TokenAccessor: "test accessor", - Namespace: "test namespace", - RequestPath: "/test/path", - Lease: "lease id", - LeaseToken: "lease token id", - Response: []byte(`{"something": "here"}`), - RenewCtxInfo: NewContextInfo(context.Background()), - RequestMethod: "GET", - RequestToken: "request token", - RequestHeader: http.Header{ - "X-Test": []string{"vault", "agent"}, - }, - LastRenewed: time.Now().UTC(), - } - indexBytes, err := testIndex.Serialize() - require.NoError(t, err) - assert.True(t, len(indexBytes) > 0) - assert.NotNil(t, testIndex.RenewCtxInfo, "Serialize should not modify original Index object") - - restoredIndex, err := Deserialize(indexBytes) - require.NoError(t, err) - - testIndex.RenewCtxInfo = nil - assert.Equal(t, testIndex, restoredIndex, "They should be equal without RenewCtxInfo set on the original") -} diff --git a/command/agentproxyshared/cache/keymanager/passthrough_test.go b/command/agentproxyshared/cache/keymanager/passthrough_test.go deleted file mode 100644 index b3dc9b725..000000000 --- a/command/agentproxyshared/cache/keymanager/passthrough_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package keymanager - -import ( - "bytes" - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestKeyManager_PassthrougKeyManager(t *testing.T) { - tests := []struct { - name string - key []byte - wantErr bool - }{ - { - "new key", - nil, - false, - }, - { - "existing valid key", - []byte("e679e2f3d8d0e489d408bc617c6890d6"), - false, - }, - { - "invalid key length", - []byte("foobar"), - true, - }, - } - - ctx := context.Background() - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - m, err := NewPassthroughKeyManager(ctx, tc.key) - if tc.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - if w := m.Wrapper(); w == nil { - t.Fatalf("expected non-nil wrapper from the key manager") - } - - token, err := m.RetrievalToken(ctx) - if err != nil { - t.Fatalf("unable to retrieve token: %s", err) - } - - if len(tc.key) != 0 && !bytes.Equal(tc.key, token) { - t.Fatalf("expected key bytes: %x, got: %x", tc.key, token) - } - }) - } -} diff --git a/command/agentproxyshared/cache/lease_cache_test.go b/command/agentproxyshared/cache/lease_cache_test.go deleted file mode 100644 index cbd46bf92..000000000 --- a/command/agentproxyshared/cache/lease_cache_test.go +++ /dev/null @@ -1,1232 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cache - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "os" - "reflect" - "strings" - "sync" - "testing" - "time" - - "github.com/go-test/deep" - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agentproxyshared/cache/cacheboltdb" - "github.com/hashicorp/vault/command/agentproxyshared/cache/cachememdb" - "github.com/hashicorp/vault/command/agentproxyshared/cache/keymanager" - "github.com/hashicorp/vault/helper/useragent" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/atomic" -) - -func testNewLeaseCache(t *testing.T, responses []*SendResponse) *LeaseCache { - t.Helper() - - client, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - lc, err := NewLeaseCache(&LeaseCacheConfig{ - Client: client, - BaseContext: context.Background(), - Proxier: NewMockProxier(responses), - Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"), - }) - if err != nil { - t.Fatal(err) - } - return lc -} - -func testNewLeaseCacheWithDelay(t *testing.T, cacheable bool, delay int) *LeaseCache { - t.Helper() - - client, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - - lc, err := NewLeaseCache(&LeaseCacheConfig{ - Client: client, - BaseContext: context.Background(), - Proxier: &mockDelayProxier{cacheable, delay}, - Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"), - }) - if err != nil { - t.Fatal(err) - } - - return lc -} - -func testNewLeaseCacheWithPersistence(t *testing.T, responses []*SendResponse, storage *cacheboltdb.BoltStorage) *LeaseCache { - t.Helper() - - client, err := api.NewClient(api.DefaultConfig()) - require.NoError(t, err) - - lc, err := NewLeaseCache(&LeaseCacheConfig{ - Client: client, - BaseContext: context.Background(), - Proxier: NewMockProxier(responses), - Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"), - Storage: storage, - }) - require.NoError(t, err) - - return lc -} - -func TestCache_ComputeIndexID(t *testing.T) { - type args struct { - req *http.Request - } - tests := []struct { - name string - req *SendRequest - want string - wantErr bool - }{ - { - "basic", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "test", - }, - }, - }, - "7b5db388f211fd9edca8c6c254831fb01ad4e6fe624dbb62711f256b5e803717", - false, - }, - { - "ignore consistency headers", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "test", - }, - Header: http.Header{ - vaulthttp.VaultIndexHeaderName: []string{"foo"}, - vaulthttp.VaultInconsistentHeaderName: []string{"foo"}, - vaulthttp.VaultForwardHeaderName: []string{"foo"}, - }, - }, - }, - "7b5db388f211fd9edca8c6c254831fb01ad4e6fe624dbb62711f256b5e803717", - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := computeIndexID(tt.req) - if (err != nil) != tt.wantErr { - t.Errorf("actual_error: %v, expected_error: %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, string(tt.want)) { - t.Errorf("bad: index id; actual: %q, expected: %q", got, string(tt.want)) - } - }) - } -} - -func TestLeaseCache_EmptyToken(t *testing.T) { - responses := []*SendResponse{ - newTestSendResponse(http.StatusCreated, `{"value": "invalid", "auth": {"client_token": "testtoken"}}`), - } - lc := testNewLeaseCache(t, responses) - - // Even if the send request doesn't have a token on it, a successful - // cacheable response should result in the index properly getting populated - // with a token and memdb shouldn't complain while inserting the index. - urlPath := "http://example.com/v1/sample/api" - sendReq := &SendRequest{ - Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)), - } - resp, err := lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatalf("expected a non empty response") - } -} - -func TestLeaseCache_SendCacheable(t *testing.T) { - // Emulate 2 responses from the api proxy. One returns a new token and the - // other returns a lease. - responses := []*SendResponse{ - newTestSendResponse(http.StatusCreated, `{"auth": {"client_token": "testtoken", "renewable": true}}`), - newTestSendResponse(http.StatusOK, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}}`), - } - - lc := testNewLeaseCache(t, responses) - // Register an token so that the token and lease requests are cached - require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken")) - - // Make a request. A response with a new token is returned to the lease - // cache and that will be cached. - urlPath := "http://example.com/v1/sample/api" - sendReq := &SendRequest{ - Token: "autoauthtoken", - Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)), - } - resp, err := lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response.StatusCode, responses[0].Response.StatusCode); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } - - // Send the same request again to get the cached response - sendReq = &SendRequest{ - Token: "autoauthtoken", - Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)), - } - resp, err = lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response.StatusCode, responses[0].Response.StatusCode); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } - - // Check TokenParent - cachedItem, err := lc.db.Get(cachememdb.IndexNameToken, "testtoken") - if err != nil { - t.Fatal(err) - } - if cachedItem == nil { - t.Fatalf("expected token entry from cache") - } - if cachedItem.TokenParent != "autoauthtoken" { - t.Fatalf("unexpected value for tokenparent: %s", cachedItem.TokenParent) - } - - // Modify the request a little bit to ensure the second response is - // returned to the lease cache. - sendReq = &SendRequest{ - Token: "autoauthtoken", - Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input_changed"}`)), - } - resp, err = lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response.StatusCode, responses[1].Response.StatusCode); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } - - // Make the same request again and ensure that the same response is returned - // again. - sendReq = &SendRequest{ - Token: "autoauthtoken", - Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input_changed"}`)), - } - resp, err = lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response.StatusCode, responses[1].Response.StatusCode); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } -} - -func TestLeaseCache_SendNonCacheable(t *testing.T) { - responses := []*SendResponse{ - newTestSendResponse(http.StatusOK, `{"value": "output"}`), - newTestSendResponse(http.StatusNotFound, `{"value": "invalid"}`), - newTestSendResponse(http.StatusOK, `Hello`), - newTestSendResponse(http.StatusTemporaryRedirect, ""), - } - - lc := testNewLeaseCache(t, responses) - - // Send a request through the lease cache which is not cacheable (there is - // no lease information or auth information in the response) - sendReq := &SendRequest{ - Request: httptest.NewRequest("GET", "http://example.com", strings.NewReader(`{"value": "input"}`)), - } - resp, err := lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response, responses[0].Response); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } - - // Since the response is non-cacheable, the second response will be - // returned. - sendReq = &SendRequest{ - Token: "foo", - Request: httptest.NewRequest("GET", "http://example.com", strings.NewReader(`{"value": "input"}`)), - } - resp, err = lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response, responses[1].Response); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } - - // Since the response is non-cacheable, the third response will be - // returned. - sendReq = &SendRequest{ - Token: "foo", - Request: httptest.NewRequest("GET", "http://example.com", nil), - } - resp, err = lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response, responses[2].Response); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } - - // Since the response is non-cacheable, the fourth response will be - // returned. - sendReq = &SendRequest{ - Token: "foo", - Request: httptest.NewRequest("GET", "http://example.com", nil), - } - resp, err = lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response, responses[3].Response); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } -} - -func TestLeaseCache_SendNonCacheableNonTokenLease(t *testing.T) { - // Create the cache - responses := []*SendResponse{ - newTestSendResponse(http.StatusOK, `{"value": "output", "lease_id": "foo"}`), - newTestSendResponse(http.StatusCreated, `{"value": "invalid", "auth": {"client_token": "testtoken"}}`), - } - lc := testNewLeaseCache(t, responses) - - // Send a request through lease cache which returns a response containing - // lease_id. Response will not be cached because it doesn't belong to a - // token that is managed by the lease cache. - urlPath := "http://example.com/v1/sample/api" - sendReq := &SendRequest{ - Token: "foo", - Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)), - } - resp, err := lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response, responses[0].Response); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } - - idx, err := lc.db.Get(cachememdb.IndexNameRequestPath, "root/", urlPath) - if err != nil { - t.Fatal(err) - } - if idx != nil { - t.Fatalf("expected nil entry, got: %#v", idx) - } - - // Verify that the response is not cached by sending the same request and - // by expecting a different response. - sendReq = &SendRequest{ - Token: "foo", - Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)), - } - resp, err = lc.Send(context.Background(), sendReq) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(resp.Response, responses[1].Response); diff != nil { - t.Fatalf("expected getting proxied response: got %v", diff) - } - - idx, err = lc.db.Get(cachememdb.IndexNameRequestPath, "root/", urlPath) - if err != nil { - t.Fatal(err) - } - if idx != nil { - t.Fatalf("expected nil entry, got: %#v", idx) - } -} - -func TestLeaseCache_HandleCacheClear(t *testing.T) { - lc := testNewLeaseCache(t, nil) - - handler := lc.HandleCacheClear(context.Background()) - ts := httptest.NewServer(handler) - defer ts.Close() - - // Test missing body, should return 400 - resp, err := http.Post(ts.URL, "application/json", nil) - if err != nil { - t.Fatal() - } - if resp.StatusCode != http.StatusBadRequest { - t.Fatalf("status code mismatch: expected = %v, got = %v", http.StatusBadRequest, resp.StatusCode) - } - - testCases := []struct { - name string - reqType string - reqValue string - expectedStatusCode int - }{ - { - "invalid_type", - "foo", - "", - http.StatusBadRequest, - }, - { - "invalid_value", - "", - "bar", - http.StatusBadRequest, - }, - { - "all", - "all", - "", - http.StatusOK, - }, - { - "by_request_path", - "request_path", - "foo", - http.StatusOK, - }, - { - "by_token", - "token", - "foo", - http.StatusOK, - }, - { - "by_lease", - "lease", - "foo", - http.StatusOK, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - reqBody := fmt.Sprintf("{\"type\": \"%s\", \"value\": \"%s\"}", tc.reqType, tc.reqValue) - resp, err := http.Post(ts.URL, "application/json", strings.NewReader(reqBody)) - if err != nil { - t.Fatal(err) - } - if tc.expectedStatusCode != resp.StatusCode { - t.Fatalf("status code mismatch: expected = %v, got = %v", tc.expectedStatusCode, resp.StatusCode) - } - }) - } -} - -func TestCache_DeriveNamespaceAndRevocationPath(t *testing.T) { - tests := []struct { - name string - req *SendRequest - wantNamespace string - wantRelativePath string - }{ - { - "non_revocation_full_path", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "/v1/ns1/sys/mounts", - }, - }, - }, - "root/", - "/v1/ns1/sys/mounts", - }, - { - "non_revocation_relative_path", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "/v1/sys/mounts", - }, - Header: http.Header{ - consts.NamespaceHeaderName: []string{"ns1/"}, - }, - }, - }, - "ns1/", - "/v1/sys/mounts", - }, - { - "non_revocation_relative_path", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "/v1/ns2/sys/mounts", - }, - Header: http.Header{ - consts.NamespaceHeaderName: []string{"ns1/"}, - }, - }, - }, - "ns1/", - "/v1/ns2/sys/mounts", - }, - { - "revocation_full_path", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "/v1/ns1/sys/leases/revoke", - }, - }, - }, - "ns1/", - "/v1/sys/leases/revoke", - }, - { - "revocation_relative_path", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "/v1/sys/leases/revoke", - }, - Header: http.Header{ - consts.NamespaceHeaderName: []string{"ns1/"}, - }, - }, - }, - "ns1/", - "/v1/sys/leases/revoke", - }, - { - "revocation_relative_partial_ns", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "/v1/ns2/sys/leases/revoke", - }, - Header: http.Header{ - consts.NamespaceHeaderName: []string{"ns1/"}, - }, - }, - }, - "ns1/ns2/", - "/v1/sys/leases/revoke", - }, - { - "revocation_prefix_full_path", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "/v1/ns1/sys/leases/revoke-prefix/foo", - }, - }, - }, - "ns1/", - "/v1/sys/leases/revoke-prefix/foo", - }, - { - "revocation_prefix_relative_path", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "/v1/sys/leases/revoke-prefix/foo", - }, - Header: http.Header{ - consts.NamespaceHeaderName: []string{"ns1/"}, - }, - }, - }, - "ns1/", - "/v1/sys/leases/revoke-prefix/foo", - }, - { - "revocation_prefix_partial_ns", - &SendRequest{ - Request: &http.Request{ - URL: &url.URL{ - Path: "/v1/ns2/sys/leases/revoke-prefix/foo", - }, - Header: http.Header{ - consts.NamespaceHeaderName: []string{"ns1/"}, - }, - }, - }, - "ns1/ns2/", - "/v1/sys/leases/revoke-prefix/foo", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotNamespace, gotRelativePath := deriveNamespaceAndRevocationPath(tt.req) - if gotNamespace != tt.wantNamespace { - t.Errorf("deriveNamespaceAndRevocationPath() gotNamespace = %v, want %v", gotNamespace, tt.wantNamespace) - } - if gotRelativePath != tt.wantRelativePath { - t.Errorf("deriveNamespaceAndRevocationPath() gotRelativePath = %v, want %v", gotRelativePath, tt.wantRelativePath) - } - }) - } -} - -func TestLeaseCache_Concurrent_NonCacheable(t *testing.T) { - lc := testNewLeaseCacheWithDelay(t, false, 50) - - // We are going to send 100 requests, each taking 50ms to process. If these - // requests are processed serially, it will take ~5seconds to finish. we - // use a ContextWithTimeout to tell us if this is the case by giving ample - // time for it process them concurrently but time out if they get processed - // serially. - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - wgDoneCh := make(chan struct{}) - errCh := make(chan error) - - go func() { - var wg sync.WaitGroup - // 100 concurrent requests - for i := 0; i < 100; i++ { - wg.Add(1) - - go func() { - defer wg.Done() - - // Send a request through the lease cache which is not cacheable (there is - // no lease information or auth information in the response) - sendReq := &SendRequest{ - Request: httptest.NewRequest("GET", "http://example.com", nil), - } - - _, err := lc.Send(ctx, sendReq) - if err != nil { - errCh <- err - } - }() - } - - wg.Wait() - close(wgDoneCh) - }() - - select { - case <-ctx.Done(): - t.Fatalf("request timed out: %s", ctx.Err()) - case <-wgDoneCh: - case err := <-errCh: - t.Fatal(err) - } -} - -func TestLeaseCache_Concurrent_Cacheable(t *testing.T) { - lc := testNewLeaseCacheWithDelay(t, true, 50) - - if err := lc.RegisterAutoAuthToken("autoauthtoken"); err != nil { - t.Fatal(err) - } - - // We are going to send 100 requests, each taking 50ms to process. If these - // requests are processed serially, it will take ~5seconds to finish, so we - // use a ContextWithTimeout to tell us if this is the case. - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - var cacheCount atomic.Uint32 - wgDoneCh := make(chan struct{}) - errCh := make(chan error) - - go func() { - var wg sync.WaitGroup - // Start 100 concurrent requests - for i := 0; i < 100; i++ { - wg.Add(1) - - go func() { - defer wg.Done() - - sendReq := &SendRequest{ - Token: "autoauthtoken", - Request: httptest.NewRequest("GET", "http://example.com/v1/sample/api", nil), - } - - resp, err := lc.Send(ctx, sendReq) - if err != nil { - errCh <- err - } - - if resp.CacheMeta != nil && resp.CacheMeta.Hit { - cacheCount.Inc() - } - }() - } - - wg.Wait() - close(wgDoneCh) - }() - - select { - case <-ctx.Done(): - t.Fatalf("request timed out: %s", ctx.Err()) - case <-wgDoneCh: - case err := <-errCh: - t.Fatal(err) - } - - // Ensure that all but one request got proxied. The other 99 should be - // returned from the cache. - if cacheCount.Load() != 99 { - t.Fatalf("Should have returned a cached response 99 times, got %d", cacheCount.Load()) - } -} - -func setupBoltStorage(t *testing.T) (tempCacheDir string, boltStorage *cacheboltdb.BoltStorage) { - t.Helper() - - km, err := keymanager.NewPassthroughKeyManager(context.Background(), nil) - require.NoError(t, err) - - tempCacheDir, err = ioutil.TempDir("", "agent-cache-test") - require.NoError(t, err) - boltStorage, err = cacheboltdb.NewBoltStorage(&cacheboltdb.BoltStorageConfig{ - Path: tempCacheDir, - Logger: hclog.Default(), - Wrapper: km.Wrapper(), - }) - require.NoError(t, err) - require.NotNil(t, boltStorage) - // The calling function should `defer boltStorage.Close()` and `defer os.RemoveAll(tempCacheDir)` - return tempCacheDir, boltStorage -} - -func compareBeforeAndAfter(t *testing.T, before, after *LeaseCache, beforeLen, afterLen int) { - beforeDB, err := before.db.GetByPrefix(cachememdb.IndexNameID) - require.NoError(t, err) - assert.Len(t, beforeDB, beforeLen) - afterDB, err := after.db.GetByPrefix(cachememdb.IndexNameID) - require.NoError(t, err) - assert.Len(t, afterDB, afterLen) - for _, cachedItem := range beforeDB { - if strings.Contains(cachedItem.RequestPath, "expect-missing") { - continue - } - restoredItem, err := after.db.Get(cachememdb.IndexNameID, cachedItem.ID) - require.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, cachedItem.ID, restoredItem.ID) - assert.Equal(t, cachedItem.Lease, restoredItem.Lease) - assert.Equal(t, cachedItem.LeaseToken, restoredItem.LeaseToken) - assert.Equal(t, cachedItem.Namespace, restoredItem.Namespace) - assert.EqualValues(t, cachedItem.RequestHeader, restoredItem.RequestHeader) - assert.Equal(t, cachedItem.RequestMethod, restoredItem.RequestMethod) - assert.Equal(t, cachedItem.RequestPath, restoredItem.RequestPath) - assert.Equal(t, cachedItem.RequestToken, restoredItem.RequestToken) - assert.Equal(t, cachedItem.Response, restoredItem.Response) - assert.Equal(t, cachedItem.Token, restoredItem.Token) - assert.Equal(t, cachedItem.TokenAccessor, restoredItem.TokenAccessor) - assert.Equal(t, cachedItem.TokenParent, restoredItem.TokenParent) - - // check what we can in the renewal context - assert.NotEmpty(t, restoredItem.RenewCtxInfo.CancelFunc) - assert.NotZero(t, restoredItem.RenewCtxInfo.DoneCh) - require.NotEmpty(t, restoredItem.RenewCtxInfo.Ctx) - assert.Equal(t, - cachedItem.RenewCtxInfo.Ctx.Value(contextIndexID), - restoredItem.RenewCtxInfo.Ctx.Value(contextIndexID), - ) - } -} - -func TestLeaseCache_PersistAndRestore(t *testing.T) { - // Emulate responses from the api proxy. The first two use the auto-auth - // token, and the others use another token. - // The test re-sends each request to ensure that the response is cached - // so the number of responses and cacheTests specified should always be equal. - responses := []*SendResponse{ - newTestSendResponse(200, `{"auth": {"client_token": "testtoken", "renewable": true, "lease_duration": 600}}`), - newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}, "lease_duration": 600}`), - // The auth token will get manually deleted from the bolt DB storage, causing both of the following two responses - // to be missing from the cache after a restore, because the lease is a child of the auth token. - newTestSendResponse(202, `{"auth": {"client_token": "testtoken2", "renewable": true, "orphan": true, "lease_duration": 600}}`), - newTestSendResponse(203, `{"lease_id": "secret2-lease", "renewable": true, "data": {"number": "two"}, "lease_duration": 600}`), - // 204 No content gets special handling - avoid. - newTestSendResponse(250, `{"auth": {"client_token": "testtoken3", "renewable": true, "orphan": true, "lease_duration": 600}}`), - newTestSendResponse(251, `{"lease_id": "secret3-lease", "renewable": true, "data": {"number": "three"}, "lease_duration": 600}`), - } - - tempDir, boltStorage := setupBoltStorage(t) - defer os.RemoveAll(tempDir) - defer boltStorage.Close() - lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage) - - // Register an auto-auth token so that the token and lease requests are cached - err := lc.RegisterAutoAuthToken("autoauthtoken") - require.NoError(t, err) - - cacheTests := []struct { - token string - method string - urlPath string - body string - deleteFromPersistentStore bool // If true, will be deleted from bolt DB to induce an error on restore - expectMissingAfterRestore bool // If true, the response is not expected to be present in the restored cache - }{ - { - // Make a request. A response with a new token is returned to the - // lease cache and that will be cached. - token: "autoauthtoken", - method: "GET", - urlPath: "http://example.com/v1/sample/api", - body: `{"value": "input"}`, - }, - { - // Modify the request a little bit to ensure the second response is - // returned to the lease cache. - token: "autoauthtoken", - method: "GET", - urlPath: "http://example.com/v1/sample/api", - body: `{"value": "input_changed"}`, - }, - { - // Simulate an approle login to get another token - method: "PUT", - urlPath: "http://example.com/v1/auth/approle-expect-missing/login", - body: `{"role_id": "my role", "secret_id": "my secret"}`, - deleteFromPersistentStore: true, - expectMissingAfterRestore: true, - }, - { - // Test caching with the token acquired from the approle login - token: "testtoken2", - method: "GET", - urlPath: "http://example.com/v1/sample-expect-missing/api", - body: `{"second": "input"}`, - // This will be missing from the restored cache because its parent token was deleted - expectMissingAfterRestore: true, - }, - { - // Simulate another approle login to get another token - method: "PUT", - urlPath: "http://example.com/v1/auth/approle/login", - body: `{"role_id": "my role", "secret_id": "my secret"}`, - }, - { - // Test caching with the token acquired from the latest approle login - token: "testtoken3", - method: "GET", - urlPath: "http://example.com/v1/sample3/api", - body: `{"third": "input"}`, - }, - } - - var deleteIDs []string - for i, ct := range cacheTests { - // Send once to cache - req := httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body)) - req.Header.Set("User-Agent", useragent.AgentProxyString()) - - sendReq := &SendRequest{ - Token: ct.token, - Request: req, - } - if ct.deleteFromPersistentStore { - deleteID, err := computeIndexID(sendReq) - require.NoError(t, err) - deleteIDs = append(deleteIDs, deleteID) - // Now reset the body after calculating the index - req = httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body)) - req.Header.Set("User-Agent", useragent.AgentProxyString()) - sendReq.Request = req - } - resp, err := lc.Send(context.Background(), sendReq) - require.NoError(t, err) - assert.Equal(t, responses[i].Response.StatusCode, resp.Response.StatusCode, "expected proxied response") - assert.Nil(t, resp.CacheMeta) - - // Send again to test cache. If this isn't cached, the response returned - // will be the next in the list and the status code will not match. - req = httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body)) - req.Header.Set("User-Agent", useragent.AgentProxyString()) - sendCacheReq := &SendRequest{ - Token: ct.token, - Request: req, - } - respCached, err := lc.Send(context.Background(), sendCacheReq) - require.NoError(t, err, "failed to send request %+v", ct) - assert.Equal(t, responses[i].Response.StatusCode, respCached.Response.StatusCode, "expected proxied response") - require.NotNil(t, respCached.CacheMeta) - assert.True(t, respCached.CacheMeta.Hit) - } - - require.NotEmpty(t, deleteIDs) - for _, deleteID := range deleteIDs { - err = boltStorage.Delete(deleteID, cacheboltdb.LeaseType) - require.NoError(t, err) - } - - // Now we know the cache is working, so try restoring from the persisted - // cache's storage. Responses 3 and 4 have been cleared from the cache, so - // re-send those. - restoredCache := testNewLeaseCache(t, responses[2:4]) - - err = restoredCache.Restore(context.Background(), boltStorage) - errors, ok := err.(*multierror.Error) - require.True(t, ok) - assert.Len(t, errors.Errors, 1) - assert.Contains(t, errors.Error(), "could not find parent Token testtoken2") - - // Now compare the cache contents before and after - compareBeforeAndAfter(t, lc, restoredCache, 7, 5) - - // And finally send the cache requests once to make sure they're all being - // served from the restoredCache unless they were intended to be missing after restore. - for i, ct := range cacheTests { - req := httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body)) - req.Header.Set("User-Agent", useragent.AgentProxyString()) - sendCacheReq := &SendRequest{ - Token: ct.token, - Request: req, - } - respCached, err := restoredCache.Send(context.Background(), sendCacheReq) - require.NoError(t, err, "failed to send request %+v", ct) - assert.Equal(t, responses[i].Response.StatusCode, respCached.Response.StatusCode, "expected proxied response") - if ct.expectMissingAfterRestore { - require.Nil(t, respCached.CacheMeta) - } else { - require.NotNil(t, respCached.CacheMeta) - assert.True(t, respCached.CacheMeta.Hit) - } - } -} - -func TestLeaseCache_PersistAndRestore_WithManyDependencies(t *testing.T) { - tempDir, boltStorage := setupBoltStorage(t) - defer os.RemoveAll(tempDir) - defer boltStorage.Close() - - var requests []*SendRequest - var responses []*SendResponse - var orderedRequestPaths []string - - // helper func to generate new auth leases with a child secret lease attached - authAndSecretLease := func(id int, parentToken, newToken string) { - t.Helper() - path := fmt.Sprintf("/v1/auth/approle-%d/login", id) - orderedRequestPaths = append(orderedRequestPaths, path) - requests = append(requests, &SendRequest{ - Token: parentToken, - Request: httptest.NewRequest("PUT", "http://example.com"+path, strings.NewReader("")), - }) - responses = append(responses, newTestSendResponse(200, fmt.Sprintf(`{"auth": {"client_token": "%s", "renewable": true, "lease_duration": 600}}`, newToken))) - - // Fetch a leased secret using the new token - path = fmt.Sprintf("/v1/kv/%d", id) - orderedRequestPaths = append(orderedRequestPaths, path) - requests = append(requests, &SendRequest{ - Token: newToken, - Request: httptest.NewRequest("GET", "http://example.com"+path, strings.NewReader("")), - }) - responses = append(responses, newTestSendResponse(200, fmt.Sprintf(`{"lease_id": "secret-%d-lease", "renewable": true, "data": {"number": %d}, "lease_duration": 600}`, id, id))) - } - - // Pathological case: a long chain of child tokens - authAndSecretLease(0, "autoauthtoken", "many-ancestors-token;0") - for i := 1; i <= 50; i++ { - // Create a new generation of child token - authAndSecretLease(i, fmt.Sprintf("many-ancestors-token;%d", i-1), fmt.Sprintf("many-ancestors-token;%d", i)) - } - - // Lots of sibling tokens with auto auth token as their parent - for i := 51; i <= 100; i++ { - authAndSecretLease(i, "autoauthtoken", fmt.Sprintf("many-siblings-token;%d", i)) - } - - // Also create some extra siblings for an auth token further down the chain - for i := 101; i <= 110; i++ { - authAndSecretLease(i, "many-ancestors-token;25", fmt.Sprintf("many-siblings-for-ancestor-token;%d", i)) - } - - lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage) - - // Register an auto-auth token so that the token and lease requests are cached - err := lc.RegisterAutoAuthToken("autoauthtoken") - require.NoError(t, err) - - for _, req := range requests { - // Send once to cache - resp, err := lc.Send(context.Background(), req) - require.NoError(t, err) - assert.Equal(t, 200, resp.Response.StatusCode, "expected success") - assert.Nil(t, resp.CacheMeta) - } - - // Ensure leases are retrieved in the correct order - var processed int - - leases, err := boltStorage.GetByType(context.Background(), cacheboltdb.LeaseType) - require.NoError(t, err) - for _, lease := range leases { - index, err := cachememdb.Deserialize(lease) - require.NoError(t, err) - require.Equal(t, orderedRequestPaths[processed], index.RequestPath) - processed++ - } - - assert.Equal(t, len(orderedRequestPaths), processed) - - restoredCache := testNewLeaseCache(t, nil) - err = restoredCache.Restore(context.Background(), boltStorage) - require.NoError(t, err) - - // Now compare the cache contents before and after - compareBeforeAndAfter(t, lc, restoredCache, 223, 223) -} - -func TestEvictPersistent(t *testing.T) { - ctx := context.Background() - - responses := []*SendResponse{ - newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}}`), - } - - tempDir, boltStorage := setupBoltStorage(t) - defer os.RemoveAll(tempDir) - defer boltStorage.Close() - lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage) - - require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken")) - - // populate cache by sending request through - sendReq := &SendRequest{ - Token: "autoauthtoken", - Request: httptest.NewRequest("GET", "http://example.com/v1/sample/api", strings.NewReader(`{"value": "some_input"}`)), - } - resp, err := lc.Send(context.Background(), sendReq) - require.NoError(t, err) - assert.Equal(t, resp.Response.StatusCode, 201, "expected proxied response") - assert.Nil(t, resp.CacheMeta) - - // Check bolt for the cached lease - secrets, err := lc.ps.GetByType(ctx, cacheboltdb.LeaseType) - require.NoError(t, err) - assert.Len(t, secrets, 1) - - // Call clear for the request path - err = lc.handleCacheClear(context.Background(), &cacheClearInput{ - Type: "request_path", - RequestPath: "/v1/sample/api", - }) - require.NoError(t, err) - - time.Sleep(2 * time.Second) - - // Check that cached item is gone - secrets, err = lc.ps.GetByType(ctx, cacheboltdb.LeaseType) - require.NoError(t, err) - assert.Len(t, secrets, 0) -} - -func TestRegisterAutoAuth_sameToken(t *testing.T) { - // If the auto-auth token already exists in the cache, it should not be - // stored again in a new index. - lc := testNewLeaseCache(t, nil) - err := lc.RegisterAutoAuthToken("autoauthtoken") - assert.NoError(t, err) - - oldTokenIndex, err := lc.db.Get(cachememdb.IndexNameToken, "autoauthtoken") - assert.NoError(t, err) - oldTokenID := oldTokenIndex.ID - - // register the same token again - err = lc.RegisterAutoAuthToken("autoauthtoken") - assert.NoError(t, err) - - // check that there's only one index for autoauthtoken - entries, err := lc.db.GetByPrefix(cachememdb.IndexNameToken, "autoauthtoken") - assert.NoError(t, err) - assert.Len(t, entries, 1) - - newTokenIndex, err := lc.db.Get(cachememdb.IndexNameToken, "autoauthtoken") - assert.NoError(t, err) - - // compare the ID's since those are randomly generated when an index for a - // token is added to the cache, so if a new token was added, the id's will - // not match. - assert.Equal(t, oldTokenID, newTokenIndex.ID) -} - -func Test_hasExpired(t *testing.T) { - responses := []*SendResponse{ - newTestSendResponse(200, `{"auth": {"client_token": "testtoken", "renewable": true, "lease_duration": 60}}`), - newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}, "lease_duration": 60}`), - } - lc := testNewLeaseCache(t, responses) - require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken")) - - cacheTests := []struct { - token string - urlPath string - leaseType string - wantStatusCode int - }{ - { - // auth lease - token: "autoauthtoken", - urlPath: "/v1/sample/auth", - leaseType: cacheboltdb.LeaseType, - wantStatusCode: responses[0].Response.StatusCode, - }, - { - // secret lease - token: "autoauthtoken", - urlPath: "/v1/sample/secret", - leaseType: cacheboltdb.LeaseType, - wantStatusCode: responses[1].Response.StatusCode, - }, - } - - for _, ct := range cacheTests { - // Send once to cache - urlPath := "http://example.com" + ct.urlPath - sendReq := &SendRequest{ - Token: ct.token, - Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)), - } - resp, err := lc.Send(context.Background(), sendReq) - require.NoError(t, err) - assert.Equal(t, resp.Response.StatusCode, ct.wantStatusCode, "expected proxied response") - assert.Nil(t, resp.CacheMeta) - - // get the Index out of the mem cache - index, err := lc.db.Get(cachememdb.IndexNameRequestPath, "root/", ct.urlPath) - require.NoError(t, err) - assert.Equal(t, ct.leaseType, index.Type) - - // The lease duration is 60 seconds, so time.Now() should be within that - notExpired, err := lc.hasExpired(time.Now().UTC(), index) - require.NoError(t, err) - assert.False(t, notExpired) - - // In 90 seconds the index should be "expired" - futureTime := time.Now().UTC().Add(time.Second * 90) - expired, err := lc.hasExpired(futureTime, index) - require.NoError(t, err) - assert.True(t, expired) - } -} - -func TestLeaseCache_hasExpired_wrong_type(t *testing.T) { - index := &cachememdb.Index{ - Type: cacheboltdb.TokenType, - Response: []byte(`HTTP/0.0 200 OK -Content-Type: application/json -Date: Tue, 02 Mar 2021 17:54:16 GMT - -{}`), - } - - lc := testNewLeaseCache(t, nil) - expired, err := lc.hasExpired(time.Now().UTC(), index) - assert.False(t, expired) - assert.EqualError(t, err, `secret without lease encountered in expiration check`) -} - -func TestLeaseCacheRestore_expired(t *testing.T) { - // Emulate 2 responses from the api proxy, both expired - responses := []*SendResponse{ - newTestSendResponse(200, `{"auth": {"client_token": "testtoken", "renewable": true, "lease_duration": -600}}`), - newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}, "lease_duration": -600}`), - } - - tempDir, boltStorage := setupBoltStorage(t) - defer os.RemoveAll(tempDir) - defer boltStorage.Close() - lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage) - - // Register an auto-auth token so that the token and lease requests are cached in mem - require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken")) - - cacheTests := []struct { - token string - method string - urlPath string - body string - wantStatusCode int - }{ - { - // Make a request. A response with a new token is returned to the - // lease cache and that will be cached. - token: "autoauthtoken", - method: "GET", - urlPath: "http://example.com/v1/sample/api", - body: `{"value": "input"}`, - wantStatusCode: responses[0].Response.StatusCode, - }, - { - // Modify the request a little bit to ensure the second response is - // returned to the lease cache. - token: "autoauthtoken", - method: "GET", - urlPath: "http://example.com/v1/sample/api", - body: `{"value": "input_changed"}`, - wantStatusCode: responses[1].Response.StatusCode, - }, - } - - for _, ct := range cacheTests { - // Send once to cache - sendReq := &SendRequest{ - Token: ct.token, - Request: httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body)), - } - resp, err := lc.Send(context.Background(), sendReq) - require.NoError(t, err) - assert.Equal(t, resp.Response.StatusCode, ct.wantStatusCode, "expected proxied response") - assert.Nil(t, resp.CacheMeta) - } - - // Restore from the persisted cache's storage - restoredCache := testNewLeaseCache(t, nil) - - err := restoredCache.Restore(context.Background(), boltStorage) - assert.NoError(t, err) - - // The original mem cache should have all three items - beforeDB, err := lc.db.GetByPrefix(cachememdb.IndexNameID) - require.NoError(t, err) - assert.Len(t, beforeDB, 3) - - // There should only be one item in the restored cache: the autoauth token - afterDB, err := restoredCache.db.GetByPrefix(cachememdb.IndexNameID) - require.NoError(t, err) - assert.Len(t, afterDB, 1) - - // Just verify that the one item in the restored mem cache matches one in the original mem cache, and that it's the auto-auth token - beforeItem, err := lc.db.Get(cachememdb.IndexNameID, afterDB[0].ID) - require.NoError(t, err) - assert.NotNil(t, beforeItem) - - assert.Equal(t, "autoauthtoken", afterDB[0].Token) - assert.Equal(t, cacheboltdb.TokenType, afterDB[0].Type) -} diff --git a/command/agentproxyshared/cache/testing.go b/command/agentproxyshared/cache/testing.go deleted file mode 100644 index 8bc2239cd..000000000 --- a/command/agentproxyshared/cache/testing.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cache - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "math/rand" - "net/http" - "strings" - "time" - - "github.com/hashicorp/vault/helper/useragent" - - "github.com/hashicorp/vault/api" -) - -// mockProxier is a mock implementation of the Proxier interface, used for testing purposes. -// The mock will return the provided responses every time it reaches its Send method, up to -// the last provided response. This lets tests control what the next/underlying Proxier layer -// might expect to return. -type mockProxier struct { - proxiedResponses []*SendResponse - responseIndex int -} - -func NewMockProxier(responses []*SendResponse) *mockProxier { - return &mockProxier{ - proxiedResponses: responses, - } -} - -func (p *mockProxier) Send(ctx context.Context, req *SendRequest) (*SendResponse, error) { - if p.responseIndex >= len(p.proxiedResponses) { - return nil, fmt.Errorf("index out of bounds: responseIndex = %d, responses = %d", p.responseIndex, len(p.proxiedResponses)) - } - resp := p.proxiedResponses[p.responseIndex] - - p.responseIndex++ - - return resp, nil -} - -func (p *mockProxier) ResponseIndex() int { - return p.responseIndex -} - -func newTestSendResponse(status int, body string) *SendResponse { - headers := make(http.Header) - headers.Add("User-Agent", useragent.AgentProxyString()) - resp := &SendResponse{ - Response: &api.Response{ - Response: &http.Response{ - StatusCode: status, - Header: headers, - }, - }, - } - resp.Response.Header.Set("Date", time.Now().Format(http.TimeFormat)) - - if body != "" { - resp.Response.Body = ioutil.NopCloser(strings.NewReader(body)) - resp.ResponseBody = []byte(body) - } - - if json.Valid([]byte(body)) { - resp.Response.Header.Set("content-type", "application/json") - } - - return resp -} - -type mockTokenVerifierProxier struct { - currentToken string -} - -func (p *mockTokenVerifierProxier) Send(ctx context.Context, req *SendRequest) (*SendResponse, error) { - p.currentToken = req.Token - resp := newTestSendResponse(http.StatusOK, - `{"data": {"id": "`+p.currentToken+`"}}`) - - return resp, nil -} - -func (p *mockTokenVerifierProxier) GetCurrentRequestToken() string { - return p.currentToken -} - -type mockDelayProxier struct { - cacheableResp bool - delay int -} - -func (p *mockDelayProxier) Send(ctx context.Context, req *SendRequest) (*SendResponse, error) { - if p.delay > 0 { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-time.After(time.Duration(p.delay) * time.Millisecond): - } - } - - // If this is a cacheable response, we return a unique response every time - if p.cacheableResp { - rand.Seed(time.Now().Unix()) - s := fmt.Sprintf(`{"lease_id": "%d", "renewable": true, "data": {"foo": "bar"}}`, rand.Int()) - return newTestSendResponse(http.StatusOK, s), nil - } - - return newTestSendResponse(http.StatusOK, `{"value": "output"}`), nil -} diff --git a/command/agentproxyshared/helpers_test.go b/command/agentproxyshared/helpers_test.go deleted file mode 100644 index 2e5244f52..000000000 --- a/command/agentproxyshared/helpers_test.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package agentproxyshared - -import ( - "context" - "os" - "testing" - - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agentproxyshared/cache" - "github.com/hashicorp/vault/sdk/helper/logging" -) - -func testNewLeaseCache(t *testing.T, responses []*cache.SendResponse) *cache.LeaseCache { - t.Helper() - - client, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - lc, err := cache.NewLeaseCache(&cache.LeaseCacheConfig{ - Client: client, - BaseContext: context.Background(), - Proxier: cache.NewMockProxier(responses), - Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"), - }) - if err != nil { - t.Fatal(err) - } - return lc -} - -func populateTempFile(t *testing.T, name, contents string) *os.File { - t.Helper() - - file, err := os.CreateTemp(t.TempDir(), name) - if err != nil { - t.Fatal(err) - } - - _, err = file.WriteString(contents) - if err != nil { - t.Fatal(err) - } - - err = file.Close() - if err != nil { - t.Fatal(err) - } - - return file -} - -// Test_AddPersistentStorageToLeaseCache Tests that AddPersistentStorageToLeaseCache() correctly -// adds persistent storage to a lease cache -func Test_AddPersistentStorageToLeaseCache(t *testing.T) { - tempDir := t.TempDir() - serviceAccountTokenFile := populateTempFile(t, "proxy-config.hcl", "token") - - persistConfig := &PersistConfig{ - Type: "kubernetes", - Path: tempDir, - KeepAfterImport: false, - ExitOnErr: false, - ServiceAccountTokenFile: serviceAccountTokenFile.Name(), - } - - leaseCache := testNewLeaseCache(t, nil) - if leaseCache.PersistentStorage() != nil { - t.Fatal("persistent storage was available before ours was added") - } - - deferFunc, token, err := AddPersistentStorageToLeaseCache(context.Background(), leaseCache, persistConfig, logging.NewVaultLogger(hclog.Info)) - if err != nil { - t.Fatal(err) - } - - if leaseCache.PersistentStorage() == nil { - t.Fatal("persistent storage was not added") - } - - if token != "" { - t.Fatal("expected token to be empty") - } - - if deferFunc == nil { - t.Fatal("expected deferFunc to not be nil") - } -} diff --git a/command/agentproxyshared/sink/file/file_sink_test.go b/command/agentproxyshared/sink/file/file_sink_test.go deleted file mode 100644 index e603c6a32..000000000 --- a/command/agentproxyshared/sink/file/file_sink_test.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package file - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "testing" - - hclog "github.com/hashicorp/go-hclog" - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/command/agentproxyshared/sink" - "github.com/hashicorp/vault/sdk/helper/logging" -) - -const ( - fileServerTestDir = "vault-agent-file-test" -) - -func testFileSink(t *testing.T, log hclog.Logger) (*sink.SinkConfig, string) { - tmpDir, err := ioutil.TempDir("", fmt.Sprintf("%s.", fileServerTestDir)) - if err != nil { - t.Fatal(err) - } - - path := filepath.Join(tmpDir, "token") - - config := &sink.SinkConfig{ - Logger: log.Named("sink.file"), - Config: map[string]interface{}{ - "path": path, - }, - } - - s, err := NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = s - - return config, tmpDir -} - -func TestFileSink(t *testing.T) { - log := logging.NewVaultLogger(hclog.Trace) - - fs, tmpDir := testFileSink(t, log) - defer os.RemoveAll(tmpDir) - - path := filepath.Join(tmpDir, "token") - - uuidStr, _ := uuid.GenerateUUID() - if err := fs.WriteToken(uuidStr); err != nil { - t.Fatal(err) - } - - file, err := os.Open(path) - if err != nil { - t.Fatal(err) - } - - fi, err := file.Stat() - if err != nil { - t.Fatal(err) - } - if fi.Mode() != os.FileMode(0o640) { - t.Fatalf("wrong file mode was detected at %s", path) - } - err = file.Close() - if err != nil { - t.Fatal(err) - } - - fileBytes, err := ioutil.ReadFile(path) - if err != nil { - t.Fatal(err) - } - - if string(fileBytes) != uuidStr { - t.Fatalf("expected %s, got %s", uuidStr, string(fileBytes)) - } -} - -func testFileSinkMode(t *testing.T, log hclog.Logger) (*sink.SinkConfig, string) { - tmpDir, err := ioutil.TempDir("", fmt.Sprintf("%s.", fileServerTestDir)) - if err != nil { - t.Fatal(err) - } - - path := filepath.Join(tmpDir, "token") - - config := &sink.SinkConfig{ - Logger: log.Named("sink.file"), - Config: map[string]interface{}{ - "path": path, - "mode": 0o644, - }, - } - - s, err := NewFileSink(config) - if err != nil { - t.Fatal(err) - } - config.Sink = s - - return config, tmpDir -} - -func TestFileSinkMode(t *testing.T) { - log := logging.NewVaultLogger(hclog.Trace) - - fs, tmpDir := testFileSinkMode(t, log) - defer os.RemoveAll(tmpDir) - - path := filepath.Join(tmpDir, "token") - - uuidStr, _ := uuid.GenerateUUID() - if err := fs.WriteToken(uuidStr); err != nil { - t.Fatal(err) - } - - file, err := os.Open(path) - if err != nil { - t.Fatal(err) - } - defer file.Close() - - fi, err := file.Stat() - if err != nil { - t.Fatal(err) - } - if fi.Mode() != os.FileMode(0o644) { - t.Fatalf("wrong file mode was detected at %s", path) - } - - fileBytes, err := ioutil.ReadFile(path) - if err != nil { - t.Fatal(err) - } - - if string(fileBytes) != uuidStr { - t.Fatalf("expected %s, got %s", uuidStr, string(fileBytes)) - } -} diff --git a/command/agentproxyshared/sink/file/sink_test.go b/command/agentproxyshared/sink/file/sink_test.go deleted file mode 100644 index d061342af..000000000 --- a/command/agentproxyshared/sink/file/sink_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package file - -import ( - "context" - "errors" - "fmt" - "io/ioutil" - "os" - "sync/atomic" - "testing" - "time" - - hclog "github.com/hashicorp/go-hclog" - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/command/agentproxyshared/sink" - "github.com/hashicorp/vault/sdk/helper/logging" -) - -func TestSinkServer(t *testing.T) { - log := logging.NewVaultLogger(hclog.Trace) - - fs1, path1 := testFileSink(t, log) - defer os.RemoveAll(path1) - fs2, path2 := testFileSink(t, log) - defer os.RemoveAll(path2) - - ctx, cancelFunc := context.WithCancel(context.Background()) - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: log.Named("sink.server"), - }) - - uuidStr, _ := uuid.GenerateUUID() - in := make(chan string) - sinks := []*sink.SinkConfig{fs1, fs2} - errCh := make(chan error) - go func() { - errCh <- ss.Run(ctx, in, sinks) - }() - - // Seed a token - in <- uuidStr - - // Tell it to shut down and give it time to do so - timer := time.AfterFunc(3*time.Second, func() { - cancelFunc() - }) - defer timer.Stop() - - select { - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } - - for _, path := range []string{path1, path2} { - fileBytes, err := ioutil.ReadFile(fmt.Sprintf("%s/token", path)) - if err != nil { - t.Fatal(err) - } - - if string(fileBytes) != uuidStr { - t.Fatalf("expected %s, got %s", uuidStr, string(fileBytes)) - } - } -} - -type badSink struct { - tryCount uint32 - logger hclog.Logger -} - -func (b *badSink) WriteToken(token string) error { - switch token { - case "bad": - atomic.AddUint32(&b.tryCount, 1) - b.logger.Info("got bad") - return errors.New("bad") - case "good": - atomic.StoreUint32(&b.tryCount, 0) - b.logger.Info("got good") - return nil - default: - return errors.New("unknown case") - } -} - -func TestSinkServerRetry(t *testing.T) { - log := logging.NewVaultLogger(hclog.Trace) - - b1 := &badSink{logger: log.Named("b1")} - b2 := &badSink{logger: log.Named("b2")} - - ctx, cancelFunc := context.WithCancel(context.Background()) - - ss := sink.NewSinkServer(&sink.SinkServerConfig{ - Logger: log.Named("sink.server"), - }) - - in := make(chan string) - sinks := []*sink.SinkConfig{{Sink: b1}, {Sink: b2}} - errCh := make(chan error) - go func() { - errCh <- ss.Run(ctx, in, sinks) - }() - - // Seed a token - in <- "bad" - - // During this time we should see it retry multiple times - time.Sleep(10 * time.Second) - if atomic.LoadUint32(&b1.tryCount) < 2 { - t.Fatal("bad try count") - } - if atomic.LoadUint32(&b2.tryCount) < 2 { - t.Fatal("bad try count") - } - - in <- "good" - - time.Sleep(2 * time.Second) - if atomic.LoadUint32(&b1.tryCount) != 0 { - t.Fatal("bad try count") - } - if atomic.LoadUint32(&b2.tryCount) != 0 { - t.Fatal("bad try count") - } - - // Tell it to shut down and give it time to do so - cancelFunc() - select { - case err := <-errCh: - if err != nil { - t.Fatal(err) - } - } -} diff --git a/command/approle_concurrency_integ_test.go b/command/approle_concurrency_integ_test.go deleted file mode 100644 index 1686701cc..000000000 --- a/command/approle_concurrency_integ_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "context" - "sync" - "testing" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - auth "github.com/hashicorp/vault/api/auth/approle" - credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestAppRole_Integ_ConcurrentLogins(t *testing.T) { - var err error - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: log.NewNullLogger(), - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - client := cores[0].Client - - err = client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{ - Type: "approle", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/approle/role/role1", map[string]interface{}{ - "bind_secret_id": "true", - "period": "300", - }) - if err != nil { - t.Fatal(err) - } - - secret, err := client.Logical().Write("auth/approle/role/role1/secret-id", nil) - if err != nil { - t.Fatal(err) - } - secretID := secret.Data["secret_id"].(string) - - secret, err = client.Logical().Read("auth/approle/role/role1/role-id") - if err != nil { - t.Fatal(err) - } - roleID := secret.Data["role_id"].(string) - - wg := &sync.WaitGroup{} - - for i := 0; i < 100; i++ { - wg.Add(1) - go func() { - defer wg.Done() - appRoleAuth, err := auth.NewAppRoleAuth(roleID, &auth.SecretID{FromString: secretID}) - if err != nil { - t.Error(err) - return - } - secret, err := client.Auth().Login(context.TODO(), appRoleAuth) - if err != nil { - t.Error(err) - return - } - if secret.Auth.ClientToken == "" { - t.Error("expected a successful login") - return - } - }() - - } - wg.Wait() -} diff --git a/command/audit_disable_test.go b/command/audit_disable_test.go deleted file mode 100644 index ec28f70dd..000000000 --- a/command/audit_disable_test.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testAuditDisableCommand(tb testing.TB) (*cli.MockUi, *AuditDisableCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuditDisableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestAuditDisableCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - nil, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar", "baz"}, - "Too many arguments", - 1, - }, - { - "not_real", - []string{"not_real"}, - "Success! Disabled audit device (if it was enabled) at: not_real/", - 0, - }, - { - "default", - []string{"file"}, - "Success! Disabled audit device (if it was enabled) at: file/", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{ - Type: "file", - Options: map[string]string{ - "file_path": "discard", - }, - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testAuditDisableCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuditWithOptions("integration_audit_disable", &api.EnableAuditOptions{ - Type: "file", - Options: map[string]string{ - "file_path": "discard", - }, - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testAuditDisableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "integration_audit_disable/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Disabled audit device (if it was enabled) at: integration_audit_disable/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - if _, ok := mounts["integration_audit_disable"]; ok { - t.Errorf("expected mount to not exist: %#v", mounts) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuditDisableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "file", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error disabling audit device: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuditDisableCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/audit_enable_test.go b/command/audit_enable_test.go deleted file mode 100644 index 58dca872e..000000000 --- a/command/audit_enable_test.go +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testAuditEnableCommand(tb testing.TB) (*cli.MockUi, *AuditEnableCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuditEnableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestAuditEnableCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "empty", - nil, - "Error enabling audit device: audit type missing. Valid types include 'file', 'socket' and 'syslog'.", - 1, - }, - { - "not_a_valid_type", - []string{"nope_definitely_not_a_valid_type_like_ever"}, - "", - 2, - }, - { - "enable", - []string{"file", "file_path=discard"}, - "Success! Enabled the file audit device at: file/", - 0, - }, - { - "enable_path", - []string{ - "-path", "audit_path", - "file", - "file_path=discard", - }, - "Success! Enabled the file audit device at: audit_path/", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuditEnableCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuditEnableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-path", "audit_enable_integration/", - "-description", "The best kind of test", - "file", - "file_path=discard", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Enabled the file audit device at: audit_enable_integration/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - audits, err := client.Sys().ListAudit() - if err != nil { - t.Fatal(err) - } - - auditInfo, ok := audits["audit_enable_integration/"] - if !ok { - t.Fatalf("expected audit to exist") - } - if exp := "file"; auditInfo.Type != exp { - t.Errorf("expected %q to be %q", auditInfo.Type, exp) - } - if exp := "The best kind of test"; auditInfo.Description != exp { - t.Errorf("expected %q to be %q", auditInfo.Description, exp) - } - - filePath, ok := auditInfo.Options["file_path"] - if !ok || filePath != "discard" { - t.Errorf("missing some options: %#v", auditInfo) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuditEnableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "pki", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error enabling audit device: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuditEnableCommand(t) - assertNoTabs(t, cmd) - }) - - t.Run("mount_all", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerAllBackends(t) - defer closer() - - files, err := ioutil.ReadDir("../builtin/audit") - if err != nil { - t.Fatal(err) - } - - var backends []string - for _, f := range files { - if f.IsDir() { - backends = append(backends, f.Name()) - } - } - - for _, b := range backends { - ui, cmd := testAuditEnableCommand(t) - cmd.client = client - - args := []string{ - b, - } - switch b { - case "file": - args = append(args, "file_path=discard") - case "socket": - args = append(args, "address=127.0.0.1:8888", - "skip_test=true") - case "syslog": - if _, exists := os.LookupEnv("WSLENV"); exists { - t.Log("skipping syslog test on WSL") - continue - } - if os.Getenv("CIRCLECI") == "true" { - // TODO install syslog in docker image we run our tests in - t.Log("skipping syslog test on CircleCI") - continue - } - } - code := cmd.Run(args) - if exp := 0; code != exp { - t.Errorf("type %s, expected %d to be %d - %s", b, code, exp, ui.OutputWriter.String()+ui.ErrorWriter.String()) - } - } - }) -} diff --git a/command/audit_list_test.go b/command/audit_list_test.go deleted file mode 100644 index bc41c13d6..000000000 --- a/command/audit_list_test.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testAuditListCommand(tb testing.TB) (*cli.MockUi, *AuditListCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuditListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestAuditListCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo"}, - "Too many arguments", - 1, - }, - { - "lists", - nil, - "Path", - 0, - }, - { - "detailed", - []string{"-detailed"}, - "Options", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{ - Type: "file", - Options: map[string]string{ - "file_path": "discard", - }, - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testAuditListCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuditListCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error listing audits: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuditListCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/auth_disable_test.go b/command/auth_disable_test.go deleted file mode 100644 index 1cf429876..000000000 --- a/command/auth_disable_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testAuthDisableCommand(tb testing.TB) (*cli.MockUi, *AuthDisableCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuthDisableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestAuthDisableCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - nil, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthDisableCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("my-auth", "userpass", ""); err != nil { - t.Fatal(err) - } - - ui, cmd := testAuthDisableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-auth", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Disabled the auth method" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - - if auth, ok := auths["my-auth/"]; ok { - t.Errorf("expected auth to be disabled: %#v", auth) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuthDisableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-auth", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error disabling auth method at my-auth/: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuthDisableCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/auth_enable_test.go b/command/auth_enable_test.go deleted file mode 100644 index 442658528..000000000 --- a/command/auth_enable_test.go +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "io/ioutil" - "strings" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/helper/builtinplugins" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/mitchellh/cli" -) - -func testAuthEnableCommand(tb testing.TB) (*cli.MockUi, *AuthEnableCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuthEnableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestAuthEnableCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - nil, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - { - "not_a_valid_auth", - []string{"nope_definitely_not_a_valid_mount_like_ever"}, - "", - 2, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthEnableCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected command return code to be %d, got %d", tc.code, code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q in response\n got: %+v", tc.out, combined) - } - }) - } - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthEnableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-path", "auth_integration/", - "-description", "The best kind of test", - "-audit-non-hmac-request-keys", "foo,bar", - "-audit-non-hmac-response-keys", "foo,bar", - "-passthrough-request-headers", "authorization,authentication", - "-passthrough-request-headers", "www-authentication", - "-allowed-response-headers", "authorization", - "-listing-visibility", "unauth", - "userpass", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Enabled userpass auth method at:" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - - authInfo, ok := auths["auth_integration/"] - if !ok { - t.Fatalf("expected mount to exist") - } - if exp := "userpass"; authInfo.Type != exp { - t.Errorf("expected %q to be %q", authInfo.Type, exp) - } - if exp := "The best kind of test"; authInfo.Description != exp { - t.Errorf("expected %q to be %q", authInfo.Description, exp) - } - if diff := deep.Equal([]string{"authorization,authentication", "www-authentication"}, authInfo.Config.PassthroughRequestHeaders); len(diff) > 0 { - t.Errorf("Failed to find expected values in PassthroughRequestHeaders. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"authorization"}, authInfo.Config.AllowedResponseHeaders); len(diff) > 0 { - t.Errorf("Failed to find expected values in AllowedResponseHeaders. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"foo,bar"}, authInfo.Config.AuditNonHMACRequestKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AuditNonHMACRequestKeys. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"foo,bar"}, authInfo.Config.AuditNonHMACResponseKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AuditNonHMACResponseKeys. Difference is: %v", diff) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuthEnableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "userpass", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error enabling userpass auth: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuthEnableCommand(t) - assertNoTabs(t, cmd) - }) - - t.Run("mount_all", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerAllBackends(t) - defer closer() - - files, err := ioutil.ReadDir("../builtin/credential") - if err != nil { - t.Fatal(err) - } - - var backends []string - for _, f := range files { - if f.IsDir() { - backends = append(backends, f.Name()) - } - } - - modFile, err := ioutil.ReadFile("../go.mod") - if err != nil { - t.Fatal(err) - } - modLines := strings.Split(string(modFile), "\n") - for _, p := range modLines { - splitLine := strings.Split(strings.TrimSpace(p), " ") - if len(splitLine) == 0 { - continue - } - potPlug := strings.TrimPrefix(splitLine[0], "github.com/hashicorp/") - if strings.HasPrefix(potPlug, "vault-plugin-auth-") { - backends = append(backends, strings.TrimPrefix(potPlug, "vault-plugin-auth-")) - } - } - // Since "pcf" plugin in the Vault registry is also pointed at the "vault-plugin-auth-cf" - // repository, we need to manually append it here so it'll tie out with our expected number - // of credential backends. - backends = append(backends, "pcf") - - // Add 1 to account for the "token" backend, which is visible when you walk the filesystem but - // is treated as special and excluded from the registry. - // Subtract 1 to account for "oidc" which is an alias of "jwt" and not a separate plugin. - expected := len(builtinplugins.Registry.Keys(consts.PluginTypeCredential)) - if len(backends) != expected { - t.Fatalf("expected %d credential backends, got %d", expected, len(backends)) - } - - for _, b := range backends { - var expectedResult int = 0 - - // Not a builtin - if b == "token" { - continue - } - - ui, cmd := testAuthEnableCommand(t) - cmd.client = client - - actualResult := cmd.Run([]string{ - b, - }) - - // Need to handle deprecated builtins specially - status, _ := builtinplugins.Registry.DeprecationStatus(b, consts.PluginTypeCredential) - if status == consts.PendingRemoval || status == consts.Removed { - expectedResult = 2 - } - - if actualResult != expectedResult { - t.Errorf("type: %s - got: %d, expected: %d - %s", b, actualResult, expectedResult, ui.OutputWriter.String()+ui.ErrorWriter.String()) - } - } - }) -} diff --git a/command/auth_help_test.go b/command/auth_help_test.go deleted file mode 100644 index d87832c89..000000000 --- a/command/auth_help_test.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" - - credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" -) - -func testAuthHelpCommand(tb testing.TB) (*cli.MockUi, *AuthHelpCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuthHelpCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - Handlers: map[string]LoginHandler{ - "userpass": &credUserpass.CLIHandler{ - DefaultMount: "userpass", - }, - }, - } -} - -func TestAuthHelpCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - { - "not_enough_args", - nil, - "Not enough arguments", - 1, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthHelpCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("path", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("foo", "userpass", ""); err != nil { - t.Fatal(err) - } - - ui, cmd := testAuthHelpCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "foo/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Usage: vault login -method=userpass" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("type", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - // No mounted auth methods - - ui, cmd := testAuthHelpCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "userpass", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Usage: vault login -method=userpass" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuthHelpCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "sys/mounts", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error listing auth methods: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuthHelpCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/auth_list_test.go b/command/auth_list_test.go deleted file mode 100644 index 8e019f1b8..000000000 --- a/command/auth_list_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testAuthListCommand(tb testing.TB) (*cli.MockUi, *AuthListCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuthListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestAuthListCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo"}, - "Too many arguments", - 1, - }, - { - "lists", - nil, - "Path", - 0, - }, - { - "detailed", - []string{"-detailed"}, - "Default TTL", - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthListCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuthListCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error listing enabled authentications: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuthListCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/auth_move_test.go b/command/auth_move_test.go deleted file mode 100644 index 095c3ae9d..000000000 --- a/command/auth_move_test.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testAuthMoveCommand(tb testing.TB) (*cli.MockUi, *AuthMoveCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuthMoveCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestAuthMoveCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar", "baz"}, - "Too many arguments", - 1, - }, - { - "non_existent", - []string{"not_real", "over_here"}, - "Error moving auth method not_real/ to over_here/", - 2, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthMoveCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthMoveCommand(t) - cmd.client = client - - if err := client.Sys().EnableAuthWithOptions("my-auth", &api.EnableAuthOptions{ - Type: "userpass", - }); err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "auth/my-auth/", "auth/my-auth-2/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Finished moving auth method auth/my-auth/ to auth/my-auth-2/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - mounts, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - - if _, ok := mounts["my-auth-2/"]; !ok { - t.Errorf("expected mount at my-auth-2/: %#v", mounts) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuthMoveCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "auth/my-auth/", "auth/my-auth-2/", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error moving auth method auth/my-auth/ to auth/my-auth-2/:" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuthMoveCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/auth_test.go b/command/auth_test.go deleted file mode 100644 index 4eb7d4ee3..000000000 --- a/command/auth_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "testing" - - "github.com/mitchellh/cli" - - "github.com/hashicorp/vault/command/token" -) - -func testAuthCommand(tb testing.TB) (*cli.MockUi, *AuthCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuthCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - - // Override to our own token helper - tokenHelper: token.NewTestingTokenHelper(), - }, - } -} - -func TestAuthCommand_Run(t *testing.T) { - t.Parallel() - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuthCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/auth_tune_test.go b/command/auth_tune_test.go deleted file mode 100644 index ea0449b9d..000000000 --- a/command/auth_tune_test.go +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/mitchellh/cli" -) - -func testAuthTuneCommand(tb testing.TB) (*cli.MockUi, *AuthTuneCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &AuthTuneCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestAuthTuneCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthTuneCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Run("flags_all", func(t *testing.T) { - t.Parallel() - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - ui, cmd := testAuthTuneCommand(t) - cmd.client = client - - // Mount - if err := client.Sys().EnableAuthWithOptions("my-auth", &api.EnableAuthOptions{ - Type: "userpass", - }); err != nil { - t.Fatal(err) - } - - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - mountInfo, ok := auths["my-auth/"] - if !ok { - t.Fatalf("expected mount to exist: %#v", auths) - } - - if exp := ""; mountInfo.PluginVersion != exp { - t.Errorf("expected %q to be %q", mountInfo.PluginVersion, exp) - } - - _, _, version := testPluginCreateAndRegisterVersioned(t, client, pluginDir, "userpass", api.PluginTypeCredential) - - code := cmd.Run([]string{ - "-description", "new description", - "-default-lease-ttl", "30m", - "-max-lease-ttl", "1h", - "-audit-non-hmac-request-keys", "foo,bar", - "-audit-non-hmac-response-keys", "foo,bar", - "-passthrough-request-headers", "authorization", - "-passthrough-request-headers", "www-authentication", - "-allowed-response-headers", "authorization,www-authentication", - "-listing-visibility", "unauth", - "-plugin-version", version, - "my-auth/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Tuned the auth method at: my-auth/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - auths, err = client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - - mountInfo, ok = auths["my-auth/"] - if !ok { - t.Fatalf("expected auth to exist") - } - if exp := "new description"; mountInfo.Description != exp { - t.Errorf("expected %q to be %q", mountInfo.Description, exp) - } - if exp := "userpass"; mountInfo.Type != exp { - t.Errorf("expected %q to be %q", mountInfo.Type, exp) - } - if exp := version; mountInfo.PluginVersion != exp { - t.Errorf("expected %q to be %q", mountInfo.PluginVersion, exp) - } - if exp := 1800; mountInfo.Config.DefaultLeaseTTL != exp { - t.Errorf("expected %d to be %d", mountInfo.Config.DefaultLeaseTTL, exp) - } - if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp { - t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp) - } - if diff := deep.Equal([]string{"authorization", "www-authentication"}, mountInfo.Config.PassthroughRequestHeaders); len(diff) > 0 { - t.Errorf("Failed to find expected values in PassthroughRequestHeaders. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"authorization,www-authentication"}, mountInfo.Config.AllowedResponseHeaders); len(diff) > 0 { - t.Errorf("Failed to find expected values in AllowedResponseHeaders. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"foo,bar"}, mountInfo.Config.AuditNonHMACRequestKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AuditNonHMACRequestKeys. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"foo,bar"}, mountInfo.Config.AuditNonHMACResponseKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AuditNonHMACResponseKeys. Difference is: %v", diff) - } - }) - - t.Run("flags_description", func(t *testing.T) { - t.Parallel() - t.Run("not_provided", func(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthTuneCommand(t) - cmd.client = client - - // Mount - if err := client.Sys().EnableAuthWithOptions("my-auth", &api.EnableAuthOptions{ - Type: "userpass", - Description: "initial description", - }); err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "-default-lease-ttl", "30m", - "my-auth/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Tuned the auth method at: my-auth/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - - mountInfo, ok := auths["my-auth/"] - if !ok { - t.Fatalf("expected auth to exist") - } - if exp := "initial description"; mountInfo.Description != exp { - t.Errorf("expected %q to be %q", mountInfo.Description, exp) - } - }) - - t.Run("provided_empty", func(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthTuneCommand(t) - cmd.client = client - - // Mount - if err := client.Sys().EnableAuthWithOptions("my-auth", &api.EnableAuthOptions{ - Type: "userpass", - Description: "initial description", - }); err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "-description", "", - "my-auth/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Tuned the auth method at: my-auth/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - - mountInfo, ok := auths["my-auth/"] - if !ok { - t.Fatalf("expected auth to exist") - } - if exp := ""; mountInfo.Description != exp { - t.Errorf("expected %q to be %q", mountInfo.Description, exp) - } - }) - }) - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuthTuneCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "userpass/", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error tuning auth method userpass/: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testAuthTuneCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/base_flags_test.go b/command/base_flags_test.go deleted file mode 100644 index ea3561ef8..000000000 --- a/command/base_flags_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func Test_BoolPtr(t *testing.T) { - var boolPtr BoolPtr - value := newBoolPtrValue(nil, &boolPtr, false) - - require.False(t, boolPtr.IsSet()) - require.False(t, boolPtr.Get()) - - err := value.Set("false") - require.NoError(t, err) - - require.True(t, boolPtr.IsSet()) - require.False(t, boolPtr.Get()) - - err = value.Set("true") - require.NoError(t, err) - - require.True(t, boolPtr.IsSet()) - require.True(t, boolPtr.Get()) - - var boolPtrFalseDefault BoolPtr - _ = newBoolPtrValue(new(bool), &boolPtrFalseDefault, false) - - require.True(t, boolPtrFalseDefault.IsSet()) - require.False(t, boolPtrFalseDefault.Get()) - - var boolPtrTrueDefault BoolPtr - defTrue := true - _ = newBoolPtrValue(&defTrue, &boolPtrTrueDefault, false) - - require.True(t, boolPtrTrueDefault.IsSet()) - require.True(t, boolPtrTrueDefault.Get()) - - var boolPtrHidden BoolPtr - value = newBoolPtrValue(nil, &boolPtrHidden, true) - require.Equal(t, true, value.Hidden()) -} - -func Test_TimeParsing(t *testing.T) { - var zeroTime time.Time - - testCases := []struct { - Input string - Formats TimeFormat - Valid bool - Expected time.Time - }{ - { - "2020-08-24", - TimeVar_TimeOrDay, - true, - time.Date(2020, 8, 24, 0, 0, 0, 0, time.UTC), - }, - { - "2099-09", - TimeVar_TimeOrDay, - false, - zeroTime, - }, - { - "2099-09", - TimeVar_TimeOrDay | TimeVar_Month, - true, - time.Date(2099, 9, 1, 0, 0, 0, 0, time.UTC), - }, - { - "2021-01-02T03:04:05-02:00", - TimeVar_TimeOrDay, - true, - time.Date(2021, 1, 2, 5, 4, 5, 0, time.UTC), - }, - { - "2021-01-02T03:04:05", - TimeVar_TimeOrDay, - false, // Missing timezone not supported - time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC), - }, - { - "2021-01-02T03:04:05+02:00", - TimeVar_TimeOrDay, - true, - time.Date(2021, 1, 2, 1, 4, 5, 0, time.UTC), - }, - { - "1598313593", - TimeVar_TimeOrDay, - true, - time.Date(2020, 8, 24, 23, 59, 53, 0, time.UTC), - }, - { - "2037", - TimeVar_TimeOrDay, - false, - zeroTime, - }, - { - "20201212", - TimeVar_TimeOrDay, - false, - zeroTime, - }, - { - "9999999999999999999999999999999999999999999999", - TimeVar_TimeOrDay, - false, - zeroTime, - }, - { - "2021-13-02T03:04:05-02:00", - TimeVar_TimeOrDay, - false, - zeroTime, - }, - { - "2021-12-02T24:04:05+00:00", - TimeVar_TimeOrDay, - false, - zeroTime, - }, - { - "2021-01-02T03:04:05.234567890Z", - TimeVar_TimeOrDay, - true, - time.Date(2021, 1, 2, 3, 4, 5, 234567890, time.UTC), - }, - } - - for _, tc := range testCases { - var result time.Time - timeVal := newTimeValue(zeroTime, &result, false, tc.Formats) - err := timeVal.Set(tc.Input) - if err == nil && !tc.Valid { - t.Errorf("Time %q parsed without error as %v, but is not valid", tc.Input, result) - continue - } - if err != nil { - if tc.Valid { - t.Errorf("Time %q parsed as error, but is valid", tc.Input) - } - continue - } - if !tc.Expected.Equal(result) { - t.Errorf("Time %q parsed incorrectly, expected %v but got %v", tc.Input, tc.Expected.UTC(), result.UTC()) - } - } -} diff --git a/command/base_helpers_test.go b/command/base_helpers_test.go deleted file mode 100644 index 6fdbc08f4..000000000 --- a/command/base_helpers_test.go +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "strings" - "testing" - "time" -) - -func TestParseArgsData(t *testing.T) { - t.Parallel() - - t.Run("stdin_full", func(t *testing.T) { - t.Parallel() - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(`{"foo":"bar"}`)) - stdinW.Close() - }() - - m, err := parseArgsData(stdinR, []string{"-"}) - if err != nil { - t.Fatal(err) - } - - if v, ok := m["foo"]; !ok || v != "bar" { - t.Errorf("expected %q to be %q", v, "bar") - } - }) - - t.Run("stdin_value", func(t *testing.T) { - t.Parallel() - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(`bar`)) - stdinW.Close() - }() - - m, err := parseArgsData(stdinR, []string{"foo=-"}) - if err != nil { - t.Fatal(err) - } - - if v, ok := m["foo"]; !ok || v != "bar" { - t.Errorf("expected %q to be %q", v, "bar") - } - }) - - t.Run("file_full", func(t *testing.T) { - t.Parallel() - - f, err := ioutil.TempFile("", "vault") - if err != nil { - t.Fatal(err) - } - f.Write([]byte(`{"foo":"bar"}`)) - f.Close() - defer os.Remove(f.Name()) - - m, err := parseArgsData(os.Stdin, []string{"@" + f.Name()}) - if err != nil { - t.Fatal(err) - } - - if v, ok := m["foo"]; !ok || v != "bar" { - t.Errorf("expected %q to be %q", v, "bar") - } - }) - - t.Run("file_value", func(t *testing.T) { - t.Parallel() - - f, err := ioutil.TempFile("", "vault") - if err != nil { - t.Fatal(err) - } - f.Write([]byte(`bar`)) - f.Close() - defer os.Remove(f.Name()) - - m, err := parseArgsData(os.Stdin, []string{"foo=@" + f.Name()}) - if err != nil { - t.Fatal(err) - } - - if v, ok := m["foo"]; !ok || v != "bar" { - t.Errorf("expected %q to be %q", v, "bar") - } - }) - - t.Run("file_value_escaped", func(t *testing.T) { - t.Parallel() - - m, err := parseArgsData(os.Stdin, []string{`foo=\@`}) - if err != nil { - t.Fatal(err) - } - - if v, ok := m["foo"]; !ok || v != "@" { - t.Errorf("expected %q to be %q", v, "@") - } - }) -} - -func TestTruncateToSeconds(t *testing.T) { - t.Parallel() - - cases := []struct { - d time.Duration - exp int - }{ - { - 10 * time.Nanosecond, - 0, - }, - { - 10 * time.Microsecond, - 0, - }, - { - 10 * time.Millisecond, - 0, - }, - { - 1 * time.Second, - 1, - }, - { - 10 * time.Second, - 10, - }, - { - 100 * time.Second, - 100, - }, - { - 3 * time.Minute, - 180, - }, - { - 3 * time.Hour, - 10800, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.d.String(), func(t *testing.T) { - t.Parallel() - - act := truncateToSeconds(tc.d) - if act != tc.exp { - t.Errorf("expected %d to be %d", act, tc.exp) - } - }) - } -} - -func TestParseFlagFile(t *testing.T) { - t.Parallel() - - content := "some raw content" - tmpFile, err := ioutil.TempFile(os.TempDir(), "TestParseFlagFile") - if err != nil { - t.Fatalf("failed to create temporary file: %v", err) - } - - defer os.Remove(tmpFile.Name()) - - if _, err := tmpFile.WriteString(content); err != nil { - t.Fatalf("failed to write to temporary file: %v", err) - } - - cases := []struct { - value string - exp string - }{ - { - "", - "", - }, - { - content, - content, - }, - { - fmt.Sprintf("@%s", tmpFile.Name()), - content, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.value, func(t *testing.T) { - content, err := parseFlagFile(tc.value) - if err != nil { - t.Fatalf("unexpected error parsing flag value: %v", err) - } - - if content != tc.exp { - t.Fatalf("expected %s to be %s", content, tc.exp) - } - }) - } -} - -func TestArgWarnings(t *testing.T) { - t.Parallel() - - cases := []struct { - args []string - expected string - }{ - { - []string{"a", "b", "c"}, - "", - }, - { - []string{"a", "-b"}, - "-b", - }, - { - []string{"a", "--b"}, - "--b", - }, - { - []string{"a-b", "-c"}, - "-c", - }, - { - []string{"a", "-b-c"}, - "-b-c", - }, - { - []string{"-a", "b"}, - "-a", - }, - { - []string{globalFlagDetailed}, - "", - }, - { - []string{"-" + globalFlagOutputCurlString + "=true"}, - "", - }, - { - []string{"--" + globalFlagFormat + "=false"}, - "", - }, - { - []string{"-x" + globalFlagDetailed}, - "-x" + globalFlagDetailed, - }, - { - []string{"--x=" + globalFlagDetailed}, - "--x=" + globalFlagDetailed, - }, - { - []string{"policy", "write", "my-policy", "-"}, - "", - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.expected, func(t *testing.T) { - warnings := generateFlagWarnings(tc.args) - if !strings.Contains(warnings, tc.expected) { - t.Fatalf("expected %s to contain %s", warnings, tc.expected) - } - }) - } -} diff --git a/command/base_predict_test.go b/command/base_predict_test.go deleted file mode 100644 index 2d752ed63..000000000 --- a/command/base_predict_test.go +++ /dev/null @@ -1,743 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "reflect" - "testing" - - "github.com/hashicorp/go-secure-stdlib/strutil" - "github.com/hashicorp/vault/api" - "github.com/posener/complete" -) - -func TestPredictVaultPaths(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - data := map[string]interface{}{"a": "b"} - if _, err := client.Logical().Write("secret/bar", data); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("secret/foo", data); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("secret/zip/zap", data); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("secret/zip/zonk", data); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("secret/zip/twoot", data); err != nil { - t.Fatal(err) - } - if err := client.Sys().Mount("level1a/level2a/level3a", &api.MountInput{Type: "kv"}); err != nil { - t.Fatal(err) - } - if err := client.Sys().Mount("level1a/level2a/level3b", &api.MountInput{Type: "kv"}); err != nil { - t.Fatal(err) - } - - cases := []struct { - name string - args complete.Args - includeFiles bool - exp []string - }{ - { - "has_args", - complete.Args{ - All: []string{"read", "secret/foo", "a=b"}, - Last: "a=b", - }, - true, - nil, - }, - { - "has_args_no_files", - complete.Args{ - All: []string{"read", "secret/foo", "a=b"}, - Last: "a=b", - }, - false, - nil, - }, - { - "part_mount", - complete.Args{ - All: []string{"read", "s"}, - Last: "s", - }, - true, - []string{"secret/", "sys/"}, - }, - { - "part_mount_no_files", - complete.Args{ - All: []string{"read", "s"}, - Last: "s", - }, - false, - []string{"secret/", "sys/"}, - }, - { - "only_mount", - complete.Args{ - All: []string{"read", "sec"}, - Last: "sec", - }, - true, - []string{"secret/bar", "secret/foo", "secret/zip/"}, - }, - { - "only_mount_no_files", - complete.Args{ - All: []string{"read", "sec"}, - Last: "sec", - }, - false, - []string{"secret/zip/"}, - }, - { - "full_mount", - complete.Args{ - All: []string{"read", "secret"}, - Last: "secret", - }, - true, - []string{"secret/bar", "secret/foo", "secret/zip/"}, - }, - { - "full_mount_no_files", - complete.Args{ - All: []string{"read", "secret"}, - Last: "secret", - }, - false, - []string{"secret/zip/"}, - }, - { - "full_mount_slash", - complete.Args{ - All: []string{"read", "secret/"}, - Last: "secret/", - }, - true, - []string{"secret/bar", "secret/foo", "secret/zip/"}, - }, - { - "full_mount_slash_no_files", - complete.Args{ - All: []string{"read", "secret/"}, - Last: "secret/", - }, - false, - []string{"secret/zip/"}, - }, - { - "path_partial", - complete.Args{ - All: []string{"read", "secret/z"}, - Last: "secret/z", - }, - true, - []string{"secret/zip/twoot", "secret/zip/zap", "secret/zip/zonk"}, - }, - { - "path_partial_no_files", - complete.Args{ - All: []string{"read", "secret/z"}, - Last: "secret/z", - }, - false, - []string{"secret/zip/"}, - }, - { - "subpath_partial_z", - complete.Args{ - All: []string{"read", "secret/zip/z"}, - Last: "secret/zip/z", - }, - true, - []string{"secret/zip/zap", "secret/zip/zonk"}, - }, - { - "subpath_partial_z_no_files", - complete.Args{ - All: []string{"read", "secret/zip/z"}, - Last: "secret/zip/z", - }, - false, - []string{"secret/zip/z"}, - }, - { - "subpath_partial_t", - complete.Args{ - All: []string{"read", "secret/zip/t"}, - Last: "secret/zip/t", - }, - true, - []string{"secret/zip/twoot"}, - }, - { - "subpath_partial_t_no_files", - complete.Args{ - All: []string{"read", "secret/zip/t"}, - Last: "secret/zip/t", - }, - false, - []string{"secret/zip/t"}, - }, - { - "multi_nested", - complete.Args{ - All: []string{"read", "level1a/level2a"}, - Last: "level1a/level2a", - }, - false, - []string{ - "level1a/level2a/level3a/", - "level1a/level2a/level3b/", - }, - }, - } - - t.Run("group", func(t *testing.T) { - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - p.client = client - - f := p.vaultPaths(tc.includeFiles) - act := f(tc.args) - if !reflect.DeepEqual(act, tc.exp) { - t.Errorf("expected %q to be %q", act, tc.exp) - } - }) - } - }) -} - -func TestPredict_Audits(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - badClient, badCloser := testVaultServerBad(t) - defer badCloser() - - if err := client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{ - Type: "file", - Options: map[string]string{ - "file_path": "discard", - }, - }); err != nil { - t.Fatal(err) - } - - cases := []struct { - name string - client *api.Client - exp []string - }{ - { - "not_connected_client", - badClient, - nil, - }, - { - "good_path", - client, - []string{"file/"}, - }, - } - - t.Run("group", func(t *testing.T) { - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - p.client = tc.client - - act := p.audits() - if !reflect.DeepEqual(act, tc.exp) { - t.Errorf("expected %q to be %q", act, tc.exp) - } - }) - } - }) -} - -func TestPredict_Mounts(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - badClient, badCloser := testVaultServerBad(t) - defer badCloser() - - cases := []struct { - name string - client *api.Client - exp []string - }{ - { - "not_connected_client", - badClient, - defaultPredictVaultMounts, - }, - { - "good_path", - client, - []string{"cubbyhole/", "identity/", "secret/", "sys/"}, - }, - } - - t.Run("group", func(t *testing.T) { - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - p.client = tc.client - - act := p.mounts() - if !reflect.DeepEqual(act, tc.exp) { - t.Errorf("expected %q to be %q", act, tc.exp) - } - }) - } - }) -} - -func TestPredict_Plugins(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - badClient, badCloser := testVaultServerBad(t) - defer badCloser() - - cases := []struct { - name string - client *api.Client - exp []string - }{ - { - "not_connected_client", - badClient, - nil, - }, - { - "good_path", - client, - []string{ - "ad", - "alicloud", - "approle", - "aws", - "azure", - "cassandra-database-plugin", - "centrify", - "cert", - "cf", - "consul", - "couchbase-database-plugin", - "elasticsearch-database-plugin", - "gcp", - "gcpkms", - "github", - "hana-database-plugin", - "influxdb-database-plugin", - "jwt", - "kerberos", - "keymgmt", - "kmip", - "kubernetes", - "kv", - "ldap", - "mongodb-database-plugin", - "mongodbatlas", - "mongodbatlas-database-plugin", - "mssql-database-plugin", - "mysql-aurora-database-plugin", - "mysql-database-plugin", - "mysql-legacy-database-plugin", - "mysql-rds-database-plugin", - "nomad", - "oci", - "oidc", - "okta", - "openldap", - "pcf", // Deprecated. - "pki", - "postgresql-database-plugin", - "rabbitmq", - "radius", - "redis-database-plugin", - "redis-elasticache-database-plugin", - "redshift-database-plugin", - "snowflake-database-plugin", - "ssh", - "terraform", - "totp", - "transform", - "transit", - "userpass", - }, - }, - } - - t.Run("group", func(t *testing.T) { - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - p.client = tc.client - - act := p.plugins() - - if !strutil.StrListContains(act, "keymgmt") { - for i, v := range tc.exp { - if v == "keymgmt" { - tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) - break - } - } - } - if !strutil.StrListContains(act, "kmip") { - for i, v := range tc.exp { - if v == "kmip" { - tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) - break - } - } - } - if !strutil.StrListContains(act, "transform") { - for i, v := range tc.exp { - if v == "transform" { - tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) - break - } - } - } - if !reflect.DeepEqual(act, tc.exp) { - t.Errorf("expected: %q, got: %q, diff: %v", tc.exp, act, strutil.Difference(act, tc.exp, true)) - } - }) - } - }) -} - -func TestPredict_Policies(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - badClient, badCloser := testVaultServerBad(t) - defer badCloser() - - cases := []struct { - name string - client *api.Client - exp []string - }{ - { - "not_connected_client", - badClient, - nil, - }, - { - "good_path", - client, - []string{"default", "root"}, - }, - } - - t.Run("group", func(t *testing.T) { - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - p.client = tc.client - - act := p.policies() - if !reflect.DeepEqual(act, tc.exp) { - t.Errorf("expected %q to be %q", act, tc.exp) - } - }) - } - }) -} - -func TestPredict_Paths(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - data := map[string]interface{}{"a": "b"} - if _, err := client.Logical().Write("secret/bar", data); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("secret/foo", data); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("secret/zip/zap", data); err != nil { - t.Fatal(err) - } - - cases := []struct { - name string - path string - includeFiles bool - exp []string - }{ - { - "bad_path", - "nope/not/a/real/path/ever", - true, - []string{"nope/not/a/real/path/ever"}, - }, - { - "good_path", - "secret/", - true, - []string{"secret/bar", "secret/foo", "secret/zip/"}, - }, - { - "good_path_no_files", - "secret/", - false, - []string{"secret/zip/"}, - }, - { - "partial_match", - "secret/z", - true, - []string{"secret/zip/"}, - }, - { - "partial_match_no_files", - "secret/z", - false, - []string{"secret/zip/"}, - }, - } - - t.Run("group", func(t *testing.T) { - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - p.client = client - - act := p.paths("kv", "1", tc.path, tc.includeFiles) - if !reflect.DeepEqual(act, tc.exp) { - t.Errorf("expected %q to be %q", act, tc.exp) - } - }) - } - }) -} - -func TestPredict_PathsKVv2(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerWithKVVersion(t, "2") - defer closer() - - data := map[string]interface{}{"data": map[string]interface{}{"a": "b"}} - if _, err := client.Logical().Write("secret/data/bar", data); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("secret/data/foo", data); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("secret/data/zip/zap", data); err != nil { - t.Fatal(err) - } - - cases := []struct { - name string - path string - includeFiles bool - exp []string - }{ - { - "bad_path", - "nope/not/a/real/path/ever", - true, - []string{"nope/not/a/real/path/ever"}, - }, - { - "good_path", - "secret/", - true, - []string{"secret/bar", "secret/foo", "secret/zip/"}, - }, - { - "good_path_no_files", - "secret/", - false, - []string{"secret/zip/"}, - }, - { - "partial_match", - "secret/z", - true, - []string{"secret/zip/"}, - }, - { - "partial_match_no_files", - "secret/z", - false, - []string{"secret/zip/"}, - }, - } - - t.Run("group", func(t *testing.T) { - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - p.client = client - - act := p.paths("kv", "2", tc.path, tc.includeFiles) - if !reflect.DeepEqual(act, tc.exp) { - t.Errorf("expected %q to be %q", act, tc.exp) - } - }) - } - }) -} - -func TestPredict_ListPaths(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - badClient, badCloser := testVaultServerBad(t) - defer badCloser() - - data := map[string]interface{}{"a": "b"} - if _, err := client.Logical().Write("secret/bar", data); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("secret/foo", data); err != nil { - t.Fatal(err) - } - - cases := []struct { - name string - client *api.Client - path string - exp []string - }{ - { - "bad_path", - client, - "nope/not/a/real/path/ever", - nil, - }, - { - "good_path", - client, - "secret/", - []string{"bar", "foo"}, - }, - { - "not_connected_client", - badClient, - "secret/", - nil, - }, - } - - t.Run("group", func(t *testing.T) { - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - p.client = tc.client - - act := p.listPaths(tc.path) - if !reflect.DeepEqual(act, tc.exp) { - t.Errorf("expected %q to be %q", act, tc.exp) - } - }) - } - }) -} - -func TestPredict_HasPathArg(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - exp bool - }{ - { - "nil", - nil, - false, - }, - { - "empty", - []string{}, - false, - }, - { - "empty_string", - []string{""}, - false, - }, - { - "single", - []string{"foo"}, - false, - }, - { - "multiple", - []string{"foo", "bar", "baz"}, - true, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - if act := p.hasPathArg(tc.args); act != tc.exp { - t.Errorf("expected %t to be %t", act, tc.exp) - } - }) - } -} diff --git a/command/base_test.go b/command/base_test.go deleted file mode 100644 index 4d53310d8..000000000 --- a/command/base_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "net/http" - "reflect" - "testing" -) - -func getDefaultCliHeaders(t *testing.T) http.Header { - bc := &BaseCommand{} - cli, err := bc.Client() - if err != nil { - t.Fatal(err) - } - return cli.Headers() -} - -func TestClient_FlagHeader(t *testing.T) { - defaultHeaders := getDefaultCliHeaders(t) - - cases := []struct { - Input map[string]string - Valid bool - }{ - { - map[string]string{}, - true, - }, - { - map[string]string{"foo": "bar", "header2": "value2"}, - true, - }, - { - map[string]string{"X-Vault-foo": "bar", "header2": "value2"}, - false, - }, - } - - for _, tc := range cases { - expectedHeaders := defaultHeaders.Clone() - for key, val := range tc.Input { - expectedHeaders.Add(key, val) - } - - bc := &BaseCommand{flagHeader: tc.Input} - cli, err := bc.Client() - - if err == nil && !tc.Valid { - t.Errorf("No error for input[%#v], but not valid", tc.Input) - continue - } - - if err != nil { - if tc.Valid { - t.Errorf("Error[%v] with input[%#v], but valid", err, tc.Input) - } - continue - } - - if cli == nil { - t.Error("client should not be nil") - } - - actualHeaders := cli.Headers() - if !reflect.DeepEqual(expectedHeaders, actualHeaders) { - t.Errorf("expected [%#v] but got [%#v]", expectedHeaders, actualHeaders) - } - } -} diff --git a/command/command_test.go b/command/command_test.go deleted file mode 100644 index edc750913..000000000 --- a/command/command_test.go +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "context" - "encoding/base64" - "net" - "net/http" - "strings" - "testing" - "time" - - log "github.com/hashicorp/go-hclog" - kv "github.com/hashicorp/vault-plugin-secrets-kv" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/audit" - "github.com/hashicorp/vault/builtin/logical/pki" - "github.com/hashicorp/vault/builtin/logical/ssh" - "github.com/hashicorp/vault/builtin/logical/transit" - "github.com/hashicorp/vault/helper/benchhelpers" - "github.com/hashicorp/vault/helper/builtinplugins" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/physical/inmem" - "github.com/hashicorp/vault/vault" - "github.com/hashicorp/vault/vault/seal" - "github.com/mitchellh/cli" - - auditFile "github.com/hashicorp/vault/builtin/audit/file" - credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" - vaulthttp "github.com/hashicorp/vault/http" -) - -var ( - defaultVaultLogger = log.NewNullLogger() - - defaultVaultCredentialBackends = map[string]logical.Factory{ - "userpass": credUserpass.Factory, - } - - defaultVaultAuditBackends = map[string]audit.Factory{ - "file": auditFile.Factory, - } - - defaultVaultLogicalBackends = map[string]logical.Factory{ - "generic-leased": vault.LeasedPassthroughBackendFactory, - "pki": pki.Factory, - "ssh": ssh.Factory, - "transit": transit.Factory, - "kv": kv.Factory, - } -) - -// assertNoTabs asserts the CLI help has no tab characters. -func assertNoTabs(tb testing.TB, c cli.Command) { - tb.Helper() - - if strings.ContainsRune(c.Help(), '\t') { - tb.Errorf("%#v help output contains tabs", c) - } -} - -// testVaultServer creates a test vault cluster and returns a configured API -// client and closer function. -func testVaultServer(tb testing.TB) (*api.Client, func()) { - tb.Helper() - - client, _, closer := testVaultServerUnseal(tb) - return client, closer -} - -func testVaultServerWithSecrets(ctx context.Context, tb testing.TB) (*api.Client, func()) { - tb.Helper() - - client, _, closer := testVaultServerUnseal(tb) - - // enable kv-v1 backend - if err := client.Sys().Mount("kv-v1/", &api.MountInput{ - Type: "kv-v1", - }); err != nil { - tb.Fatal(err) - } - - // enable kv-v2 backend - if err := client.Sys().Mount("kv-v2/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - tb.Fatal(err) - } - - // populate dummy secrets - for _, path := range []string{ - "foo", - "app-1/foo", - "app-1/bar", - "app-1/nested/baz", - } { - if err := client.KVv1("kv-v1").Put(ctx, path, map[string]interface{}{ - "user": "test", - "password": "Hashi123", - }); err != nil { - tb.Fatal(err) - } - - if _, err := client.KVv2("kv-v2").Put(ctx, path, map[string]interface{}{ - "user": "test", - "password": "Hashi123", - }); err != nil { - tb.Fatal(err) - } - } - - return client, closer -} - -func testVaultServerWithKVVersion(tb testing.TB, kvVersion string) (*api.Client, func()) { - tb.Helper() - - client, _, closer := testVaultServerUnsealWithKVVersionWithSeal(tb, kvVersion, nil) - return client, closer -} - -func testVaultServerAllBackends(tb testing.TB) (*api.Client, func()) { - tb.Helper() - - client, _, closer := testVaultServerCoreConfig(tb, &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: defaultVaultLogger, - CredentialBackends: credentialBackends, - AuditBackends: auditBackends, - LogicalBackends: logicalBackends, - BuiltinRegistry: builtinplugins.Registry, - }) - return client, closer -} - -// testVaultServerAutoUnseal creates a test vault cluster and sets it up with auto unseal -// the function returns a client, the recovery keys, and a closer function -func testVaultServerAutoUnseal(tb testing.TB) (*api.Client, []string, func()) { - testSeal, _ := seal.NewTestSeal(nil) - autoSeal, err := vault.NewAutoSeal(testSeal) - if err != nil { - tb.Fatal("unable to create autoseal", err) - } - return testVaultServerUnsealWithKVVersionWithSeal(tb, "1", autoSeal) -} - -// testVaultServerUnseal creates a test vault cluster and returns a configured -// API client, list of unseal keys (as strings), and a closer function. -func testVaultServerUnseal(tb testing.TB) (*api.Client, []string, func()) { - return testVaultServerUnsealWithKVVersionWithSeal(tb, "1", nil) -} - -func testVaultServerUnsealWithKVVersionWithSeal(tb testing.TB, kvVersion string, seal vault.Seal) (*api.Client, []string, func()) { - tb.Helper() - logger := log.NewInterceptLogger(&log.LoggerOptions{ - Output: log.DefaultOutput, - Level: log.Debug, - JSONFormat: logging.ParseEnvLogFormat() == logging.JSONFormat, - }) - - return testVaultServerCoreConfigWithOpts(tb, &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: logger, - CredentialBackends: defaultVaultCredentialBackends, - AuditBackends: defaultVaultAuditBackends, - LogicalBackends: defaultVaultLogicalBackends, - BuiltinRegistry: builtinplugins.Registry, - Seal: seal, - }, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - NumCores: 1, - KVVersion: kvVersion, - }) -} - -// testVaultServerUnseal creates a test vault cluster and returns a configured -// API client, list of unseal keys (as strings), and a closer function -// configured with the given plugin directory. -func testVaultServerPluginDir(tb testing.TB, pluginDir string) (*api.Client, []string, func()) { - tb.Helper() - - return testVaultServerCoreConfig(tb, &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: defaultVaultLogger, - CredentialBackends: defaultVaultCredentialBackends, - AuditBackends: defaultVaultAuditBackends, - LogicalBackends: defaultVaultLogicalBackends, - PluginDirectory: pluginDir, - BuiltinRegistry: builtinplugins.Registry, - }) -} - -func testVaultServerCoreConfig(tb testing.TB, coreConfig *vault.CoreConfig) (*api.Client, []string, func()) { - return testVaultServerCoreConfigWithOpts(tb, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - NumCores: 1, // Default is 3, but we don't need that many - }) -} - -// testVaultServerCoreConfig creates a new vault cluster with the given core -// configuration. This is a lower-level test helper. If the seal config supports recovery keys, then -// recovery keys are returned. Otherwise, unseal keys are returned -func testVaultServerCoreConfigWithOpts(tb testing.TB, coreConfig *vault.CoreConfig, opts *vault.TestClusterOptions) (*api.Client, []string, func()) { - tb.Helper() - - cluster := vault.NewTestCluster(benchhelpers.TBtoT(tb), coreConfig, opts) - cluster.Start() - - // Make it easy to get access to the active - core := cluster.Cores[0].Core - vault.TestWaitActive(benchhelpers.TBtoT(tb), core) - - // Get the client already setup for us! - client := cluster.Cores[0].Client - client.SetToken(cluster.RootToken) - - var keys [][]byte - if coreConfig.Seal != nil && coreConfig.Seal.RecoveryKeySupported() { - keys = cluster.RecoveryKeys - } else { - keys = cluster.BarrierKeys - } - - return client, encodeKeys(keys), cluster.Cleanup -} - -// Convert the unseal keys to base64 encoded, since these are how the user -// will get them. -func encodeKeys(rawKeys [][]byte) []string { - keys := make([]string, len(rawKeys)) - for i := range rawKeys { - keys[i] = base64.StdEncoding.EncodeToString(rawKeys[i]) - } - return keys -} - -// testVaultServerUninit creates an uninitialized server. -func testVaultServerUninit(tb testing.TB) (*api.Client, func()) { - tb.Helper() - - inm, err := inmem.NewInmem(nil, defaultVaultLogger) - if err != nil { - tb.Fatal(err) - } - - core, err := vault.NewCore(&vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: defaultVaultLogger, - Physical: inm, - CredentialBackends: defaultVaultCredentialBackends, - AuditBackends: defaultVaultAuditBackends, - LogicalBackends: defaultVaultLogicalBackends, - BuiltinRegistry: builtinplugins.Registry, - }) - if err != nil { - tb.Fatal(err) - } - - ln, addr := vaulthttp.TestServer(tb, core) - - client, err := api.NewClient(&api.Config{ - Address: addr, - }) - if err != nil { - tb.Fatal(err) - } - - closer := func() { - core.Shutdown() - ln.Close() - } - - return client, closer -} - -// testVaultServerBad creates an http server that returns a 500 on each request -// to simulate failures. -func testVaultServerBad(tb testing.TB) (*api.Client, func()) { - tb.Helper() - - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - tb.Fatal(err) - } - - server := &http.Server{ - Addr: "127.0.0.1:0", - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "500 internal server error", http.StatusInternalServerError) - }), - ReadTimeout: 1 * time.Second, - ReadHeaderTimeout: 1 * time.Second, - WriteTimeout: 1 * time.Second, - IdleTimeout: 1 * time.Second, - } - - go func() { - if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { - tb.Fatal(err) - } - }() - - client, err := api.NewClient(&api.Config{ - Address: "http://" + listener.Addr().String(), - }) - if err != nil { - tb.Fatal(err) - } - - return client, func() { - ctx, done := context.WithTimeout(context.Background(), 5*time.Second) - defer done() - - server.Shutdown(ctx) - } -} - -// testTokenAndAccessor creates a new authentication token capable of being renewed with -// the default policy attached. It returns the token and it's accessor. -func testTokenAndAccessor(tb testing.TB, client *api.Client) (string, string) { - tb.Helper() - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"default"}, - TTL: "30m", - }) - if err != nil { - tb.Fatal(err) - } - if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { - tb.Fatalf("missing auth data: %#v", secret) - } - return secret.Auth.ClientToken, secret.Auth.Accessor -} - -func testClient(tb testing.TB, addr string, token string) *api.Client { - tb.Helper() - config := api.DefaultConfig() - config.Address = addr - client, err := api.NewClient(config) - if err != nil { - tb.Fatal(err) - } - client.SetToken(token) - - return client -} diff --git a/command/config/config_test.go b/command/config/config_test.go deleted file mode 100644 index 04cb1be98..000000000 --- a/command/config/config_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package config - -import ( - "path/filepath" - "reflect" - "strings" - "testing" -) - -const FixturePath = "../test-fixtures" - -func TestLoadConfig(t *testing.T) { - config, err := LoadConfig(filepath.Join(FixturePath, "config.hcl")) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &DefaultConfig{ - TokenHelper: "foo", - } - if !reflect.DeepEqual(expected, config) { - t.Fatalf("bad: %#v", config) - } -} - -func TestLoadConfig_noExist(t *testing.T) { - config, err := LoadConfig("nope/not-once/.never") - if err != nil { - t.Fatal(err) - } - - if config.TokenHelper != "" { - t.Errorf("expected %q to be %q", config.TokenHelper, "") - } -} - -func TestParseConfig_badKeys(t *testing.T) { - _, err := ParseConfig(` -token_helper = "/token" -nope = "true" -`) - if err == nil { - t.Fatal("expected error") - } - - if !strings.Contains(err.Error(), `invalid key "nope" on line 3`) { - t.Errorf("bad error: %s", err.Error()) - } -} diff --git a/command/config_test.go b/command/config_test.go deleted file mode 100644 index 187d4ce8b..000000000 --- a/command/config_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "path/filepath" - "reflect" - "strings" - "testing" -) - -const FixturePath = "./test-fixtures" - -func TestLoadConfig(t *testing.T) { - config, err := LoadConfig(filepath.Join(FixturePath, "config.hcl")) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &DefaultConfig{ - TokenHelper: "foo", - } - if !reflect.DeepEqual(expected, config) { - t.Fatalf("bad: %#v", config) - } -} - -func TestLoadConfig_noExist(t *testing.T) { - config, err := LoadConfig("nope/not-once/.never") - if err != nil { - t.Fatal(err) - } - - if config.TokenHelper != "" { - t.Errorf("expected %q to be %q", config.TokenHelper, "") - } -} - -func TestParseConfig_badKeys(t *testing.T) { - _, err := ParseConfig(` -token_helper = "/token" -nope = "true" -`) - if err == nil { - t.Fatal("expected error") - } - - if !strings.Contains(err.Error(), `invalid key "nope" on line 3`) { - t.Errorf("bad error: %s", err.Error()) - } -} diff --git a/command/debug_test.go b/command/debug_test.go deleted file mode 100644 index 80576311d..000000000 --- a/command/debug_test.go +++ /dev/null @@ -1,846 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "archive/tar" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - "syscall" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/mholt/archiver/v3" - "github.com/mitchellh/cli" -) - -func testDebugCommand(tb testing.TB) (*cli.MockUi, *DebugCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &DebugCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestDebugCommand_Run(t *testing.T) { - t.Parallel() - - testDir, err := ioutil.TempDir("", "vault-debug") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "valid", - []string{ - "-duration=1s", - fmt.Sprintf("-output=%s/valid", testDir), - }, - "", - 0, - }, - { - "too_many_args", - []string{ - "-duration=1s", - fmt.Sprintf("-output=%s/too_many_args", testDir), - "foo", - }, - "Too many arguments", - 1, - }, - { - "invalid_target", - []string{ - "-duration=1s", - fmt.Sprintf("-output=%s/invalid_target", testDir), - "-target=foo", - }, - "Ignoring invalid targets: foo", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testDebugCommand(t) - cmd.client = client - cmd.skipTimingChecks = true - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Fatalf("expected %q to contain %q", combined, tc.out) - } - }) - } -} - -func TestDebugCommand_Archive(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - ext string - expectError bool - }{ - { - "no-ext", - "", - false, - }, - { - "with-ext-tar-gz", - ".tar.gz", - false, - }, - { - "with-ext-tgz", - ".tgz", - false, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - // Create temp dirs for each test case since os.Stat and tgz.Walk - // (called down below) exhibits raciness otherwise. - testDir, err := ioutil.TempDir("", "vault-debug") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testDebugCommand(t) - cmd.client = client - cmd.skipTimingChecks = true - - // We use tc.name as the base path and apply the extension per - // test case. - basePath := tc.name - outputPath := filepath.Join(testDir, basePath+tc.ext) - args := []string{ - "-duration=1s", - fmt.Sprintf("-output=%s", outputPath), - "-target=server-status", - } - - code := cmd.Run(args) - if exp := 0; code != exp { - t.Log(ui.OutputWriter.String()) - t.Log(ui.ErrorWriter.String()) - t.Fatalf("expected %d to be %d", code, exp) - } - // If we expect an error we're done here - if tc.expectError { - return - } - - expectedExt := tc.ext - if expectedExt == "" { - expectedExt = debugCompressionExt - } - - bundlePath := filepath.Join(testDir, basePath+expectedExt) - _, err = os.Stat(bundlePath) - if os.IsNotExist(err) { - t.Log(ui.OutputWriter.String()) - t.Fatal(err) - } - - tgz := archiver.NewTarGz() - err = tgz.Walk(bundlePath, func(f archiver.File) error { - fh, ok := f.Header.(*tar.Header) - if !ok { - return fmt.Errorf("invalid file header: %#v", f.Header) - } - - // Ignore base directory and index file - if fh.Name == basePath+"/" || fh.Name == filepath.Join(basePath, "index.json") { - return nil - } - - if fh.Name != filepath.Join(basePath, "server_status.json") { - return fmt.Errorf("unexpected file: %s", fh.Name) - } - return nil - }) - if err != nil { - t.Fatal(err) - } - }) - } -} - -func TestDebugCommand_CaptureTargets(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - targets []string - expectedFiles []string - }{ - { - "config", - []string{"config"}, - []string{"config.json"}, - }, - { - "host-info", - []string{"host"}, - []string{"host_info.json"}, - }, - { - "metrics", - []string{"metrics"}, - []string{"metrics.json"}, - }, - { - "replication-status", - []string{"replication-status"}, - []string{"replication_status.json"}, - }, - { - "server-status", - []string{"server-status"}, - []string{"server_status.json"}, - }, - { - "in-flight-req", - []string{"requests"}, - []string{"requests.json"}, - }, - { - "all-minus-pprof", - []string{"config", "host", "metrics", "replication-status", "server-status"}, - []string{"config.json", "host_info.json", "metrics.json", "replication_status.json", "server_status.json"}, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - testDir, err := ioutil.TempDir("", "vault-debug") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testDebugCommand(t) - cmd.client = client - cmd.skipTimingChecks = true - - basePath := tc.name - args := []string{ - "-duration=1s", - fmt.Sprintf("-output=%s/%s", testDir, basePath), - } - for _, target := range tc.targets { - args = append(args, fmt.Sprintf("-target=%s", target)) - } - - code := cmd.Run(args) - if exp := 0; code != exp { - t.Log(ui.ErrorWriter.String()) - t.Fatalf("expected %d to be %d", code, exp) - } - - bundlePath := filepath.Join(testDir, basePath+debugCompressionExt) - _, err = os.Open(bundlePath) - if err != nil { - t.Fatalf("failed to open archive: %s", err) - } - - tgz := archiver.NewTarGz() - err = tgz.Walk(bundlePath, func(f archiver.File) error { - fh, ok := f.Header.(*tar.Header) - if !ok { - t.Fatalf("invalid file header: %#v", f.Header) - } - - // Ignore base directory and index file - if fh.Name == basePath+"/" || fh.Name == filepath.Join(basePath, "index.json") { - return nil - } - - for _, fileName := range tc.expectedFiles { - if fh.Name == filepath.Join(basePath, fileName) { - return nil - } - } - - // If we reach here, it means that this is an unexpected file - return fmt.Errorf("unexpected file: %s", fh.Name) - }) - if err != nil { - t.Fatal(err) - } - }) - } -} - -func TestDebugCommand_Pprof(t *testing.T) { - testDir, err := ioutil.TempDir("", "vault-debug") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testDebugCommand(t) - cmd.client = client - cmd.skipTimingChecks = true - - basePath := "pprof" - outputPath := filepath.Join(testDir, basePath) - // pprof requires a minimum interval of 1s, we set it to 2 to ensure it - // runs through and reduce flakiness on slower systems. - args := []string{ - "-compress=false", - "-duration=2s", - "-interval=2s", - fmt.Sprintf("-output=%s", outputPath), - "-target=pprof", - } - - code := cmd.Run(args) - if exp := 0; code != exp { - t.Log(ui.ErrorWriter.String()) - t.Fatalf("expected %d to be %d", code, exp) - } - - profiles := []string{"heap.prof", "goroutine.prof"} - pollingProfiles := []string{"profile.prof", "trace.out"} - - // These are captures on the first (0th) and last (1st) frame - for _, v := range profiles { - files, _ := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, v)) - if len(files) != 2 { - t.Errorf("2 output files should exist for %s: got: %v", v, files) - } - } - - // Since profile and trace are polling outputs, these only get captured - // on the first (0th) frame. - for _, v := range pollingProfiles { - files, _ := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, v)) - if len(files) != 1 { - t.Errorf("1 output file should exist for %s: got: %v", v, files) - } - } - - t.Log(ui.OutputWriter.String()) - t.Log(ui.ErrorWriter.String()) -} - -func TestDebugCommand_IndexFile(t *testing.T) { - t.Parallel() - - testDir, err := ioutil.TempDir("", "vault-debug") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testDebugCommand(t) - cmd.client = client - cmd.skipTimingChecks = true - - basePath := "index-test" - outputPath := filepath.Join(testDir, basePath) - // pprof requires a minimum interval of 1s - args := []string{ - "-compress=false", - "-duration=1s", - "-interval=1s", - "-metrics-interval=1s", - fmt.Sprintf("-output=%s", outputPath), - } - - code := cmd.Run(args) - if exp := 0; code != exp { - t.Log(ui.ErrorWriter.String()) - t.Fatalf("expected %d to be %d", code, exp) - } - - content, err := ioutil.ReadFile(filepath.Join(outputPath, "index.json")) - if err != nil { - t.Fatal(err) - } - - index := &debugIndex{} - if err := json.Unmarshal(content, index); err != nil { - t.Fatal(err) - } - if len(index.Output) == 0 { - t.Fatalf("expected valid index file: got: %v", index) - } -} - -func TestDebugCommand_TimingChecks(t *testing.T) { - t.Parallel() - - testDir, err := ioutil.TempDir("", "vault-debug") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - cases := []struct { - name string - duration string - interval string - metricsInterval string - }{ - { - "short-values-all", - "10ms", - "10ms", - "10ms", - }, - { - "short-duration", - "10ms", - "", - "", - }, - { - "short-interval", - debugMinInterval.String(), - "10ms", - "", - }, - { - "short-metrics-interval", - debugMinInterval.String(), - "", - "10ms", - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - // If we are past the minimum duration + some grace, trigger shutdown - // to prevent hanging - grace := 10 * time.Second - shutdownCh := make(chan struct{}) - go func() { - time.AfterFunc(grace, func() { - close(shutdownCh) - }) - }() - - ui, cmd := testDebugCommand(t) - cmd.client = client - cmd.ShutdownCh = shutdownCh - - basePath := tc.name - outputPath := filepath.Join(testDir, basePath) - // pprof requires a minimum interval of 1s - args := []string{ - "-target=server-status", - fmt.Sprintf("-output=%s", outputPath), - } - if tc.duration != "" { - args = append(args, fmt.Sprintf("-duration=%s", tc.duration)) - } - if tc.interval != "" { - args = append(args, fmt.Sprintf("-interval=%s", tc.interval)) - } - if tc.metricsInterval != "" { - args = append(args, fmt.Sprintf("-metrics-interval=%s", tc.metricsInterval)) - } - - code := cmd.Run(args) - if exp := 0; code != exp { - t.Log(ui.ErrorWriter.String()) - t.Fatalf("expected %d to be %d", code, exp) - } - - if !strings.Contains(ui.OutputWriter.String(), "Duration: 5s") { - t.Fatal("expected minimum duration value") - } - - if tc.interval != "" { - if !strings.Contains(ui.OutputWriter.String(), " Interval: 5s") { - t.Fatal("expected minimum interval value") - } - } - - if tc.metricsInterval != "" { - if !strings.Contains(ui.OutputWriter.String(), "Metrics Interval: 5s") { - t.Fatal("expected minimum metrics interval value") - } - } - }) - } -} - -func TestDebugCommand_NoConnection(t *testing.T) { - t.Parallel() - - client, err := api.NewClient(nil) - if err != nil { - t.Fatal(err) - } - - if err := client.SetAddress(""); err != nil { - t.Fatal(err) - } - - _, cmd := testDebugCommand(t) - cmd.client = client - cmd.skipTimingChecks = true - - args := []string{ - "-duration=1s", - "-target=server-status", - } - - code := cmd.Run(args) - if exp := 1; code != exp { - t.Fatalf("expected %d to be %d", code, exp) - } -} - -func TestDebugCommand_OutputExists(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - compress bool - outputFile string - expectedError string - }{ - { - "no-compress", - false, - "output-exists", - "output directory already exists", - }, - { - "compress", - true, - "output-exist.tar.gz", - "output file already exists", - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - testDir, err := ioutil.TempDir("", "vault-debug") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testDebugCommand(t) - cmd.client = client - cmd.skipTimingChecks = true - - outputPath := filepath.Join(testDir, tc.outputFile) - - // Create a conflicting file/directory - if tc.compress { - _, err = os.Create(outputPath) - if err != nil { - t.Fatal(err) - } - } else { - err = os.Mkdir(outputPath, 0o700) - if err != nil { - t.Fatal(err) - } - } - - args := []string{ - fmt.Sprintf("-compress=%t", tc.compress), - "-duration=1s", - "-interval=1s", - "-metrics-interval=1s", - fmt.Sprintf("-output=%s", outputPath), - } - - code := cmd.Run(args) - if exp := 1; code != exp { - t.Log(ui.OutputWriter.String()) - t.Log(ui.ErrorWriter.String()) - t.Errorf("expected %d to be %d", code, exp) - } - - output := ui.ErrorWriter.String() + ui.OutputWriter.String() - if !strings.Contains(output, tc.expectedError) { - t.Fatalf("expected %s, got: %s", tc.expectedError, output) - } - }) - } -} - -func TestDebugCommand_PartialPermissions(t *testing.T) { - t.Parallel() - - testDir, err := ioutil.TempDir("", "vault-debug") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - client, closer := testVaultServer(t) - defer closer() - - // Create a new token with default policy - resp, err := client.Logical().Write("auth/token/create", map[string]interface{}{ - "policies": "default", - }) - if err != nil { - t.Fatal(err) - } - - client.SetToken(resp.Auth.ClientToken) - - ui, cmd := testDebugCommand(t) - cmd.client = client - cmd.skipTimingChecks = true - - basePath := "with-default-policy-token" - args := []string{ - "-duration=1s", - fmt.Sprintf("-output=%s/%s", testDir, basePath), - } - - code := cmd.Run(args) - if exp := 0; code != exp { - t.Log(ui.ErrorWriter.String()) - t.Fatalf("expected %d to be %d", code, exp) - } - - bundlePath := filepath.Join(testDir, basePath+debugCompressionExt) - _, err = os.Open(bundlePath) - if err != nil { - t.Fatalf("failed to open archive: %s", err) - } - - tgz := archiver.NewTarGz() - err = tgz.Walk(bundlePath, func(f archiver.File) error { - fh, ok := f.Header.(*tar.Header) - if !ok { - t.Fatalf("invalid file header: %#v", f.Header) - } - - // Ignore base directory and index file - if fh.Name == basePath+"/" { - return nil - } - - // Ignore directories, which still get created by pprof but should - // otherwise be empty. - if fh.FileInfo().IsDir() { - return nil - } - - switch { - case fh.Name == filepath.Join(basePath, "index.json"): - case fh.Name == filepath.Join(basePath, "replication_status.json"): - case fh.Name == filepath.Join(basePath, "server_status.json"): - case fh.Name == filepath.Join(basePath, "vault.log"): - default: - return fmt.Errorf("unexpected file: %s", fh.Name) - } - - return nil - }) - if err != nil { - t.Fatal(err) - } -} - -// set insecure umask to see if the files and directories get created with right permissions -func TestDebugCommand_InsecureUmask(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("test does not work in windows environment") - } - t.Parallel() - - cases := []struct { - name string - compress bool - outputFile string - expectError bool - }{ - { - "with-compress", - true, - "with-compress.tar.gz", - false, - }, - { - "no-compress", - false, - "no-compress", - false, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - // set insecure umask - defer syscall.Umask(syscall.Umask(0)) - - testDir, err := ioutil.TempDir("", "vault-debug") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testDebugCommand(t) - cmd.client = client - cmd.skipTimingChecks = true - - outputPath := filepath.Join(testDir, tc.outputFile) - - args := []string{ - fmt.Sprintf("-compress=%t", tc.compress), - "-duration=1s", - "-interval=1s", - "-metrics-interval=1s", - fmt.Sprintf("-output=%s", outputPath), - } - - code := cmd.Run(args) - if exp := 0; code != exp { - t.Log(ui.ErrorWriter.String()) - t.Fatalf("expected %d to be %d", code, exp) - } - // If we expect an error we're done here - if tc.expectError { - return - } - - bundlePath := filepath.Join(testDir, tc.outputFile) - fs, err := os.Stat(bundlePath) - if os.IsNotExist(err) { - t.Log(ui.OutputWriter.String()) - t.Fatal(err) - } - // check permissions of the parent debug directory - err = isValidFilePermissions(fs) - if err != nil { - t.Fatalf(err.Error()) - } - - // check permissions of the files within the parent directory - switch tc.compress { - case true: - tgz := archiver.NewTarGz() - - err = tgz.Walk(bundlePath, func(f archiver.File) error { - fh, ok := f.Header.(*tar.Header) - if !ok { - return fmt.Errorf("invalid file header: %#v", f.Header) - } - err = isValidFilePermissions(fh.FileInfo()) - if err != nil { - t.Fatalf(err.Error()) - } - return nil - }) - - case false: - err = filepath.Walk(bundlePath, func(path string, info os.FileInfo, err error) error { - err = isValidFilePermissions(info) - if err != nil { - t.Fatalf(err.Error()) - } - return nil - }) - } - - if err != nil { - t.Fatal(err) - } - }) - } -} - -func isValidFilePermissions(info os.FileInfo) (err error) { - mode := info.Mode() - // check group permissions - for i := 4; i < 7; i++ { - if string(mode.String()[i]) != "-" { - return fmt.Errorf("expected no permissions for group but got %s permissions for file %s", string(mode.String()[i]), info.Name()) - } - } - - // check others permissions - for i := 7; i < 10; i++ { - if string(mode.String()[i]) != "-" { - return fmt.Errorf("expected no permissions for others but got %s permissions for file %s", string(mode.String()[i]), info.Name()) - } - } - return err -} diff --git a/command/delete_test.go b/command/delete_test.go deleted file mode 100644 index 3c08bc685..000000000 --- a/command/delete_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testDeleteCommand(tb testing.TB) (*cli.MockUi, *DeleteCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &DeleteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestDeleteCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "default", - []string{"secret/foo"}, - "", - 0, - }, - { - "optional_args", - []string{"secret/foo", "bar=baz"}, - "", - 0, - }, - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testDeleteCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if _, err := client.Logical().Write("secret/delete/foo", map[string]interface{}{ - "foo": "bar", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testDeleteCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/delete/foo", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Data deleted (if it existed) at: secret/delete/foo" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - secret, _ := client.Logical().Read("secret/delete/foo") - if secret != nil { - t.Errorf("expected deletion: %#v", secret) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testDeleteCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/delete/foo", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error deleting secret/delete/foo: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testDeleteCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/events_test.go b/command/events_test.go deleted file mode 100644 index 336dc0a34..000000000 --- a/command/events_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testEventsSubscribeCommand(tb testing.TB) (*cli.MockUi, *EventsSubscribeCommands) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &EventsSubscribeCommands{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -// TestEventsSubscribeCommand_Run tests that the command argument parsing is working as expected. -func TestEventsSubscribeCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testEventsSubscribeCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } -} diff --git a/command/format_test.go b/command/format_test.go deleted file mode 100644 index 94db7414a..000000000 --- a/command/format_test.go +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/ghodss/yaml" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/helper/jsonutil" -) - -type mockUi struct { - t *testing.T - SampleData string - outputData *string -} - -func (m mockUi) Ask(_ string) (string, error) { - m.t.FailNow() - return "", nil -} - -func (m mockUi) AskSecret(_ string) (string, error) { - m.t.FailNow() - return "", nil -} -func (m mockUi) Output(s string) { *m.outputData = s } -func (m mockUi) Info(s string) { m.t.Log(s) } -func (m mockUi) Error(s string) { m.t.Log(s) } -func (m mockUi) Warn(s string) { m.t.Log(s) } - -func TestJsonFormatter(t *testing.T) { - os.Setenv(EnvVaultFormat, "json") - var output string - ui := mockUi{t: t, SampleData: "something", outputData: &output} - if err := outputWithFormat(ui, nil, ui); err != 0 { - t.Fatal(err) - } - var newUi mockUi - if err := jsonutil.DecodeJSON([]byte(output), &newUi); err != nil { - t.Fatal(err) - } - if newUi.SampleData != ui.SampleData { - t.Fatalf(`values not equal ("%s" != "%s")`, - newUi.SampleData, - ui.SampleData) - } -} - -func TestYamlFormatter(t *testing.T) { - os.Setenv(EnvVaultFormat, "yaml") - var output string - ui := mockUi{t: t, SampleData: "something", outputData: &output} - if err := outputWithFormat(ui, nil, ui); err != 0 { - t.Fatal(err) - } - var newUi mockUi - err := yaml.Unmarshal([]byte(output), &newUi) - if err != nil { - t.Fatal(err) - } - if newUi.SampleData != ui.SampleData { - t.Fatalf(`values not equal ("%s" != "%s")`, - newUi.SampleData, - ui.SampleData) - } -} - -func TestTableFormatter(t *testing.T) { - os.Setenv(EnvVaultFormat, "table") - var output string - ui := mockUi{t: t, outputData: &output} - - // Testing secret formatting - s := api.Secret{Data: map[string]interface{}{"k": "something"}} - if err := outputWithFormat(ui, &s, &s); err != 0 { - t.Fatal(err) - } - if !strings.Contains(output, "something") { - t.Fatal("did not find 'something'") - } -} - -// TestStatusFormat tests to verify that the embedded struct -// SealStatusOutput ignores omitEmpty fields and prints out -// fields in the embedded struct explicitly. It also checks the spacing, -// indentation, and delimiters of table formatting explicitly. -func TestStatusFormat(t *testing.T) { - var output string - ui := mockUi{t: t, outputData: &output} - os.Setenv(EnvVaultFormat, "table") - - statusHA := getMockStatusData(false) - statusOmitEmpty := getMockStatusData(true) - - // Testing that HA fields are formatted properly for table. - // All fields (including new HA fields) are expected - if err := outputWithFormat(ui, nil, statusHA); err != 0 { - t.Fatal(err) - } - - expectedOutputString := `Key Value ---- ----- -Recovery Seal Type type -Initialized true -Sealed true -Total Recovery Shares 2 -Threshold 1 -Unseal Progress 3/1 -Unseal Nonce nonce -Seal Migration in Progress true -Version version -Build Date build date -Storage Type storage type -Cluster Name cluster name -Cluster ID cluster id -HA Enabled true -Raft Committed Index 3 -Raft Applied Index 4 -Last WAL 2 -Warnings [warning]` - - if expectedOutputString != output { - fmt.Printf("%s\n%+v\n %s\n%+v\n", "output found was: ", output, "versus", expectedOutputString) - t.Fatal("format output for status does not match expected format. Check print statements above.") - } - - // Testing that omitEmpty fields are omitted from status - // no HA fields are expected, except HA Enabled - if err := outputWithFormat(ui, nil, statusOmitEmpty); err != 0 { - t.Fatal(err) - } - - expectedOutputString = `Key Value ---- ----- -Recovery Seal Type type -Initialized true -Sealed true -Total Recovery Shares 2 -Threshold 1 -Unseal Progress 3/1 -Unseal Nonce nonce -Seal Migration in Progress true -Version version -Build Date build date -Storage Type n/a -HA Enabled false` - - if expectedOutputString != output { - fmt.Printf("%s\n%+v\n %s\n%+v\n", "output found was: ", output, "versus", expectedOutputString) - t.Fatal("format output for status does not match expected format. Check print statements above.") - } -} - -// getMockStatusData outputs a SealStatusOutput struct from format.go to be used -// for testing. The emptyfields parameter specifies whether the struct will be -// initialized with all the omitempty fields as empty or not. -func getMockStatusData(emptyFields bool) SealStatusOutput { - var status SealStatusOutput - var sealStatusResponseMock api.SealStatusResponse - if !emptyFields { - sealStatusResponseMock = api.SealStatusResponse{ - Type: "type", - Initialized: true, - Sealed: true, - T: 1, - N: 2, - Progress: 3, - Nonce: "nonce", - Version: "version", - BuildDate: "build date", - Migration: true, - ClusterName: "cluster name", - ClusterID: "cluster id", - RecoverySeal: true, - StorageType: "storage type", - Warnings: []string{"warning"}, - } - - // must initialize this struct without explicit field names due to embedding - status = SealStatusOutput{ - sealStatusResponseMock, - true, // HAEnabled - true, // IsSelf - time.Time{}.UTC(), // ActiveTime - "leader address", // LeaderAddress - "leader cluster address", // LeaderClusterAddress - true, // PerfStandby - 1, // PerfStandbyLastRemoteWAL - 2, // LastWAL - 3, // RaftCommittedIndex - 4, // RaftAppliedIndex - } - } else { - sealStatusResponseMock = api.SealStatusResponse{ - Type: "type", - Initialized: true, - Sealed: true, - T: 1, - N: 2, - Progress: 3, - Nonce: "nonce", - Version: "version", - BuildDate: "build date", - Migration: true, - ClusterName: "", - ClusterID: "", - RecoverySeal: true, - StorageType: "", - } - - // must initialize this struct without explicit field names due to embedding - status = SealStatusOutput{ - sealStatusResponseMock, - false, // HAEnabled - false, // IsSelf - time.Time{}.UTC(), // ActiveTime - "", // LeaderAddress - "", // LeaderClusterAddress - false, // PerfStandby - 0, // PerfStandbyLastRemoteWAL - 0, // LastWAL - 0, // RaftCommittedIndex - 0, // RaftAppliedIndex - } - } - return status -} - -func Test_Format_Parsing(t *testing.T) { - defer func() { - os.Setenv(EnvVaultCLINoColor, "") - os.Setenv(EnvVaultFormat, "") - }() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "format", - []string{"token", "renew", "-format", "json"}, - "{", - 0, - }, - { - "format_bad", - []string{"token", "renew", "-format", "nope-not-real"}, - "Invalid output format", - 1, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - runOpts := &RunOptions{ - Stdout: stdout, - Stderr: stderr, - Client: client, - } - - // Login with the token so we can renew-self. - token, _ := testTokenAndAccessor(t, client) - client.SetToken(token) - - code := RunCustom(tc.args, runOpts) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := stdout.String() + stderr.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } -} diff --git a/command/kv_helpers_test.go b/command/kv_helpers_test.go deleted file mode 100644 index 06a1bb8ee..000000000 --- a/command/kv_helpers_test.go +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/hashicorp/vault/api" -) - -// TestAddPrefixToKVPath tests the addPrefixToKVPath helper function -func TestAddPrefixToKVPath(t *testing.T) { - cases := map[string]struct { - path string - mountPath string - apiPrefix string - skipIfExists bool - expected string - }{ - "simple": { - path: "kv-v2/foo", - mountPath: "kv-v2/", - apiPrefix: "data", - skipIfExists: false, - expected: "kv-v2/data/foo", - }, - - "multi-part": { - path: "my/kv-v2/mount/path/foo/bar/baz", - mountPath: "my/kv-v2/mount/path", - apiPrefix: "metadata", - skipIfExists: false, - expected: "my/kv-v2/mount/path/metadata/foo/bar/baz", - }, - - "with-namespace": { - path: "my/kv-v2/mount/path/foo/bar/baz", - mountPath: "my/ns1/my/kv-v2/mount/path", - apiPrefix: "metadata", - skipIfExists: false, - expected: "my/kv-v2/mount/path/metadata/foo/bar/baz", - }, - - "skip-if-exists-true": { - path: "kv-v2/data/foo", - mountPath: "kv-v2/", - apiPrefix: "data", - skipIfExists: true, - expected: "kv-v2/data/foo", - }, - - "skip-if-exists-false": { - path: "kv-v2/data/foo", - mountPath: "kv-v2", - apiPrefix: "data", - skipIfExists: false, - expected: "kv-v2/data/data/foo", - }, - - "skip-if-exists-with-namespace": { - path: "my/kv-v2/mount/path/metadata/foo/bar/baz", - mountPath: "my/ns1/my/kv-v2/mount/path", - apiPrefix: "metadata", - skipIfExists: true, - expected: "my/kv-v2/mount/path/metadata/foo/bar/baz", - }, - } - - for name, tc := range cases { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - actual := addPrefixToKVPath( - tc.path, - tc.mountPath, - tc.apiPrefix, - tc.skipIfExists, - ) - - if tc.expected != actual { - t.Fatalf("unexpected output; want: %v, got: %v", tc.expected, actual) - } - }) - } -} - -// TestWalkSecretsTree tests the walkSecretsTree helper function -func TestWalkSecretsTree(t *testing.T) { - // test setup - client, closer := testVaultServer(t) - defer closer() - - // enable kv-v1 backend - if err := client.Sys().Mount("kv-v1/", &api.MountInput{ - Type: "kv-v1", - }); err != nil { - t.Fatal(err) - } - time.Sleep(time.Second) - - // enable kv-v2 backend - if err := client.Sys().Mount("kv-v2/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatal(err) - } - time.Sleep(time.Second) - - ctx, cancelContextFunc := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelContextFunc() - - // populate secrets - for _, path := range []string{ - "foo", - "app-1/foo", - "app-1/bar", - "app-1/nested/x/y/z", - "app-1/nested/x/y", - "app-1/nested/bar", - } { - if err := client.KVv1("kv-v1").Put(ctx, path, map[string]interface{}{ - "password": "Hashi123", - }); err != nil { - t.Fatal(err) - } - - if _, err := client.KVv2("kv-v2").Put(ctx, path, map[string]interface{}{ - "password": "Hashi123", - }); err != nil { - t.Fatal(err) - } - } - - type treePath struct { - path string - directory bool - } - - cases := map[string]struct { - path string - expected []treePath - expectedError bool - }{ - "kv-v1-simple": { - path: "kv-v1/app-1/nested/x/y", - expected: []treePath{ - {path: "kv-v1/app-1/nested/x/y/z", directory: false}, - }, - expectedError: false, - }, - - "kv-v2-simple": { - path: "kv-v2/metadata/app-1/nested/x/y", - expected: []treePath{ - {path: "kv-v2/metadata/app-1/nested/x/y/z", directory: false}, - }, - expectedError: false, - }, - - "kv-v1-nested": { - path: "kv-v1/app-1/nested/", - expected: []treePath{ - {path: "kv-v1/app-1/nested/bar", directory: false}, - {path: "kv-v1/app-1/nested/x", directory: true}, - {path: "kv-v1/app-1/nested/x/y", directory: false}, - {path: "kv-v1/app-1/nested/x/y", directory: true}, - {path: "kv-v1/app-1/nested/x/y/z", directory: false}, - }, - expectedError: false, - }, - - "kv-v2-nested": { - path: "kv-v2/metadata/app-1/nested/", - expected: []treePath{ - {path: "kv-v2/metadata/app-1/nested/bar", directory: false}, - {path: "kv-v2/metadata/app-1/nested/x", directory: true}, - {path: "kv-v2/metadata/app-1/nested/x/y", directory: false}, - {path: "kv-v2/metadata/app-1/nested/x/y", directory: true}, - {path: "kv-v2/metadata/app-1/nested/x/y/z", directory: false}, - }, - expectedError: false, - }, - - "kv-v1-all": { - path: "kv-v1", - expected: []treePath{ - {path: "kv-v1/app-1", directory: true}, - {path: "kv-v1/app-1/bar", directory: false}, - {path: "kv-v1/app-1/foo", directory: false}, - {path: "kv-v1/app-1/nested", directory: true}, - {path: "kv-v1/app-1/nested/bar", directory: false}, - {path: "kv-v1/app-1/nested/x", directory: true}, - {path: "kv-v1/app-1/nested/x/y", directory: false}, - {path: "kv-v1/app-1/nested/x/y", directory: true}, - {path: "kv-v1/app-1/nested/x/y/z", directory: false}, - {path: "kv-v1/foo", directory: false}, - }, - expectedError: false, - }, - - "kv-v2-all": { - path: "kv-v2/metadata", - expected: []treePath{ - {path: "kv-v2/metadata/app-1", directory: true}, - {path: "kv-v2/metadata/app-1/bar", directory: false}, - {path: "kv-v2/metadata/app-1/foo", directory: false}, - {path: "kv-v2/metadata/app-1/nested", directory: true}, - {path: "kv-v2/metadata/app-1/nested/bar", directory: false}, - {path: "kv-v2/metadata/app-1/nested/x", directory: true}, - {path: "kv-v2/metadata/app-1/nested/x/y", directory: false}, - {path: "kv-v2/metadata/app-1/nested/x/y", directory: true}, - {path: "kv-v2/metadata/app-1/nested/x/y/z", directory: false}, - {path: "kv-v2/metadata/foo", directory: false}, - }, - expectedError: false, - }, - - "kv-v1-not-found": { - path: "kv-v1/does/not/exist", - expected: nil, - expectedError: true, - }, - - "kv-v2-not-found": { - path: "kv-v2/metadata/does/not/exist", - expected: nil, - expectedError: true, - }, - - "kv-v1-not-listable-leaf-node": { - path: "kv-v1/foo", - expected: nil, - expectedError: true, - }, - - "kv-v2-not-listable-leaf-node": { - path: "kv-v2/metadata/foo", - expected: nil, - expectedError: true, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - var descendants []treePath - - err := walkSecretsTree(ctx, client, tc.path, func(path string, directory bool) error { - descendants = append(descendants, treePath{ - path: path, - directory: directory, - }) - return nil - }) - - if tc.expectedError { - if err == nil { - t.Fatal("an error was expected but the test succeeded") - } - } else { - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(tc.expected, descendants) { - t.Fatalf("unexpected list output; want: %v, got: %v", tc.expected, descendants) - } - } - }) - } -} diff --git a/command/kv_metadata_patch_test.go b/command/kv_metadata_patch_test.go deleted file mode 100644 index a8dc58378..000000000 --- a/command/kv_metadata_patch_test.go +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "encoding/json" - "io" - "strings" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testKVMetadataPatchCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPatchCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &KVMetadataPatchCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func kvMetadataPatchWithRetry(t *testing.T, client *api.Client, args []string, stdin *io.PipeReader) (int, string) { - t.Helper() - - return retryKVCommand(t, func() (int, string) { - ui, cmd := testKVMetadataPatchCommand(t) - cmd.client = client - - if stdin != nil { - cmd.testStdin = stdin - } - - code := cmd.Run(args) - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - - return code, combined - }) -} - -func kvMetadataPutWithRetry(t *testing.T, client *api.Client, args []string, stdin *io.PipeReader) (int, string) { - t.Helper() - - return retryKVCommand(t, func() (int, string) { - ui, cmd := testKVMetadataPutCommand(t) - cmd.client = client - - if stdin != nil { - cmd.testStdin = stdin - } - - code := cmd.Run(args) - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - - return code, combined - }) -} - -func TestKvMetadataPatchCommand_EmptyArgs(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount error: %#v", err) - } - - args := make([]string, 0) - code, combined := kvMetadataPatchWithRetry(t, client, args, nil) - - expectedCode := 1 - expectedOutput := "Not enough arguments" - - if code != expectedCode { - t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v", expectedCode, code, args) - } - - if !strings.Contains(combined, expectedOutput) { - t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", expectedOutput, combined, args) - } -} - -func TestKvMetadataPatchCommand_Flags(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - expectedUpdates map[string]interface{} - }{ - { - "cas_required_success", - []string{"-cas-required=true"}, - "Success!", - 0, - map[string]interface{}{ - "cas_required": true, - }, - }, - { - "cas_required_invalid", - []string{"-cas-required=12345"}, - "invalid boolean value", - 1, - map[string]interface{}{}, - }, - { - "custom_metadata_success", - []string{"-custom-metadata=baz=ghi"}, - "Success!", - 0, - map[string]interface{}{ - "custom_metadata": map[string]interface{}{ - "foo": "abc", - "bar": "def", - "baz": "ghi", - }, - }, - }, - { - "remove-custom_metadata", - []string{"-custom-metadata=baz=ghi", "-remove-custom-metadata=foo"}, - "Success!", - 0, - map[string]interface{}{ - "custom_metadata": map[string]interface{}{ - "bar": "def", - "baz": "ghi", - }, - }, - }, - { - "remove-custom_metadata-multiple", - []string{"-custom-metadata=baz=ghi", "-remove-custom-metadata=foo", "-remove-custom-metadata=bar"}, - "Success!", - 0, - map[string]interface{}{ - "custom_metadata": map[string]interface{}{ - "baz": "ghi", - }, - }, - }, - { - "delete_version_after_success", - []string{"-delete-version-after=5s"}, - "Success!", - 0, - map[string]interface{}{ - "delete_version_after": "5s", - }, - }, - { - "delete_version_after_invalid", - []string{"-delete-version-after=false"}, - "invalid duration", - 1, - map[string]interface{}{}, - }, - { - "max_versions_success", - []string{"-max-versions=10"}, - "Success!", - 0, - map[string]interface{}{ - "max_versions": json.Number("10"), - }, - }, - { - "max_versions_invalid", - []string{"-max-versions=false"}, - "invalid syntax", - 1, - map[string]interface{}{}, - }, - { - "multiple_flags_success", - []string{"-max-versions=20", "-custom-metadata=baz=123"}, - "Success!", - 0, - map[string]interface{}{ - "max_versions": json.Number("20"), - "custom_metadata": map[string]interface{}{ - "foo": "abc", - "bar": "def", - "baz": "123", - }, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - basePath := t.Name() + "/" - secretPath := basePath + "my-secret" - metadataPath := basePath + "metadata/" + "my-secret" - - if err := client.Sys().Mount(basePath, &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount error: %#v", err) - } - - putArgs := []string{"-cas-required=true", "-custom-metadata=foo=abc", "-custom-metadata=bar=def", secretPath} - code, combined := kvMetadataPutWithRetry(t, client, putArgs, nil) - - if code != 0 { - t.Fatalf("initial metadata put failed, code: %d, output: %s", code, combined) - } - - initialMetadata, err := client.Logical().Read(metadataPath) - if err != nil { - t.Fatalf("metadata read failed, err: %#v", err) - } - - patchArgs := append(tc.args, secretPath) - - code, combined = kvMetadataPatchWithRetry(t, client, patchArgs, nil) - - if !strings.Contains(combined, tc.out) { - t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", tc.out, combined, patchArgs) - } - if code != tc.code { - t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v", tc.code, code, patchArgs) - } - - patchedMetadata, err := client.Logical().Read(metadataPath) - if err != nil { - t.Fatalf("metadata read failed, err: %#v", err) - } - - for k, v := range patchedMetadata.Data { - var expectedVal interface{} - - if inputVal, ok := tc.expectedUpdates[k]; ok { - expectedVal = inputVal - } else { - expectedVal = initialMetadata.Data[k] - } - - if diff := deep.Equal(expectedVal, v); len(diff) > 0 { - t.Fatalf("patched %q mismatch, diff: %#v", k, diff) - } - } - }) - } -} - -func TestKvMetadataPatchCommand_CasWarning(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - basePath := "kv/" - if err := client.Sys().Mount(basePath, &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount error: %#v", err) - } - - secretPath := basePath + "my-secret" - - args := []string{"-cas-required=true", secretPath} - code, combined := kvMetadataPutWithRetry(t, client, args, nil) - - if code != 0 { - t.Fatalf("metadata put failed, code: %d, output: %s", code, combined) - } - - casConfig := map[string]interface{}{ - "cas_required": true, - } - - _, err := client.Logical().Write(basePath+"config", casConfig) - if err != nil { - t.Fatalf("config write failed, err: #%v", err) - } - - args = []string{"-cas-required=false", secretPath} - code, combined = kvMetadataPatchWithRetry(t, client, args, nil) - - if code != 0 { - t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v", code, args) - } - - expectedOutput := "\"cas_required\" set to false, but is mandated by backend config" - if !strings.Contains(combined, expectedOutput) { - t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", expectedOutput, combined, args) - } -} diff --git a/command/kv_metadata_put_test.go b/command/kv_metadata_put_test.go deleted file mode 100644 index e39541559..000000000 --- a/command/kv_metadata_put_test.go +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "encoding/json" - "strings" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testKVMetadataPutCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPutCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &KVMetadataPutCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestKvMetadataPutCommand_DeleteVersionAfter(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - basePath := t.Name() + "/" - if err := client.Sys().Mount(basePath, &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testKVMetadataPutCommand(t) - cmd.client = client - - // Set a limit of 1s first. - code := cmd.Run([]string{"-delete-version-after=1s", basePath + "secret/my-secret"}) - if code != 0 { - t.Fatalf("expected %d but received %d", 0, code) - } - - metaFullPath := basePath + "metadata/secret/my-secret" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - success := "Success! Data written to: " + metaFullPath - if !strings.Contains(combined, success) { - t.Fatalf("expected %q but received %q", success, combined) - } - - secret, err := client.Logical().Read(metaFullPath) - if err != nil { - t.Fatal(err) - } - if secret.Data["delete_version_after"] != "1s" { - t.Fatalf("expected 1s but received %q", secret.Data["delete_version_after"]) - } - - // Now verify that we can return it to 0s. - ui, cmd = testKVMetadataPutCommand(t) - cmd.client = client - - // Set a limit of 1s first. - code = cmd.Run([]string{"-delete-version-after=0", basePath + "secret/my-secret"}) - if code != 0 { - t.Errorf("expected %d but received %d", 0, code) - } - - combined = ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, success) { - t.Errorf("expected %q but received %q", success, combined) - } - - secret, err = client.Logical().Read(metaFullPath) - if err != nil { - t.Fatal(err) - } - if secret.Data["delete_version_after"] != "0s" { - t.Fatalf("expected 0s but received %q", secret.Data["delete_version_after"]) - } -} - -func TestKvMetadataPutCommand_CustomMetadata(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - basePath := t.Name() + "/" - secretPath := basePath + "secret/my-secret" - - if err := client.Sys().Mount(basePath, &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount error: %#v", err) - } - - ui, cmd := testKVMetadataPutCommand(t) - cmd.client = client - - exitStatus := cmd.Run([]string{"-custom-metadata=foo=abc", "-custom-metadata=bar=123", secretPath}) - - if exitStatus != 0 { - t.Fatalf("Expected 0 exit status but received %d", exitStatus) - } - - metaFullPath := basePath + "metadata/secret/my-secret" - commandOutput := ui.OutputWriter.String() + ui.ErrorWriter.String() - expectedOutput := "Success! Data written to: " + metaFullPath - - if !strings.Contains(commandOutput, expectedOutput) { - t.Fatalf("Expected command output %q but received %q", expectedOutput, commandOutput) - } - - metadata, err := client.Logical().Read(metaFullPath) - if err != nil { - t.Fatalf("Metadata read error: %#v", err) - } - - // JSON output from read decoded into map[string]interface{} - expectedCustomMetadata := map[string]interface{}{ - "foo": "abc", - "bar": "123", - } - - if diff := deep.Equal(metadata.Data["custom_metadata"], expectedCustomMetadata); len(diff) > 0 { - t.Fatal(diff) - } - - ui, cmd = testKVMetadataPutCommand(t) - cmd.client = client - - // Overwrite entire custom metadata with a single key - exitStatus = cmd.Run([]string{"-custom-metadata=baz=abc123", secretPath}) - - if exitStatus != 0 { - t.Fatalf("Expected 0 exit status but received %d", exitStatus) - } - - commandOutput = ui.OutputWriter.String() + ui.ErrorWriter.String() - - if !strings.Contains(commandOutput, expectedOutput) { - t.Fatalf("Expected command output %q but received %q", expectedOutput, commandOutput) - } - - metadata, err = client.Logical().Read(metaFullPath) - if err != nil { - t.Fatalf("Metadata read error: %#v", err) - } - - expectedCustomMetadata = map[string]interface{}{ - "baz": "abc123", - } - - if diff := deep.Equal(metadata.Data["custom_metadata"], expectedCustomMetadata); len(diff) > 0 { - t.Fatal(diff) - } -} - -func TestKvMetadataPutCommand_UnprovidedFlags(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - basePath := t.Name() + "/" - secretPath := basePath + "my-secret" - - if err := client.Sys().Mount(basePath, &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount error: %#v", err) - } - - _, cmd := testKVMetadataPutCommand(t) - cmd.client = client - - args := []string{"-cas-required=true", "-max-versions=10", secretPath} - code, _ := kvMetadataPutWithRetry(t, client, args, nil) - - if code != 0 { - t.Fatalf("expected 0 exit status but received %d", code) - } - - args = []string{"-custom-metadata=foo=bar", secretPath} - code, _ = kvMetadataPutWithRetry(t, client, args, nil) - - if code != 0 { - t.Fatalf("expected 0 exit status but received %d", code) - } - - secret, err := client.Logical().Read(basePath + "metadata/" + "my-secret") - if err != nil { - t.Fatal(err) - } - - if secret.Data["cas_required"] != true { - t.Fatalf("expected cas_required to be true but received %#v", secret.Data["cas_required"]) - } - - if secret.Data["max_versions"] != json.Number("10") { - t.Fatalf("expected max_versions to be 10 but received %#v", secret.Data["max_versions"]) - } -} diff --git a/command/kv_test.go b/command/kv_test.go deleted file mode 100644 index 1471b009a..000000000 --- a/command/kv_test.go +++ /dev/null @@ -1,1546 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "context" - "fmt" - "io" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testKVPutCommand(tb testing.TB) (*cli.MockUi, *KVPutCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &KVPutCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func retryKVCommand(t *testing.T, cmdFunc func() (int, string)) (int, string) { - t.Helper() - - var code int - var combined string - - // Loop until return message does not indicate upgrade, or timeout. - timeout := time.After(20 * time.Second) - ticker := time.Tick(time.Second) - - for { - select { - case <-timeout: - t.Errorf("timeout expired waiting for upgrade: %q", combined) - return code, combined - case <-ticker: - code, combined = cmdFunc() - - // This is an error if a v1 mount, but test case doesn't - // currently contain the information to know the difference. - if !strings.Contains(combined, "Upgrading from non-versioned to versioned") { - return code, combined - } - } - } -} - -func kvPutWithRetry(t *testing.T, client *api.Client, args []string) (int, string) { - t.Helper() - - return retryKVCommand(t, func() (int, string) { - ui, cmd := testKVPutCommand(t) - cmd.client = client - - code := cmd.Run(args) - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - - return code, combined - }) -} - -func kvPatchWithRetry(t *testing.T, client *api.Client, args []string, stdin *io.PipeReader) (int, string) { - t.Helper() - - return retryKVCommand(t, func() (int, string) { - ui, cmd := testKVPatchCommand(t) - cmd.client = client - - if stdin != nil { - cmd.testStdin = stdin - } - - code := cmd.Run(args) - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - - return code, combined - }) -} - -func TestKVPutCommand(t *testing.T) { - t.Parallel() - - v2ExpectedFields := []string{"created_time", "custom_metadata", "deletion_time", "deletion_time", "version"} - - cases := []struct { - name string - args []string - outStrings []string - code int - }{ - { - "not_enough_args", - []string{}, - []string{"Not enough arguments"}, - 1, - }, - { - "empty_kvs", - []string{"secret/write/foo"}, - []string{"Must supply data"}, - 1, - }, - { - "kvs_no_value", - []string{"secret/write/foo", "foo"}, - []string{"Failed to parse K=V data"}, - 1, - }, - { - "single_value", - []string{"secret/write/foo", "foo=bar"}, - []string{"Success!"}, - 0, - }, - { - "multi_value", - []string{"secret/write/foo", "foo=bar", "zip=zap"}, - []string{"Success!"}, - 0, - }, - { - "v1_mount_flag_syntax", - []string{"-mount", "secret", "write/foo", "foo=bar"}, - []string{"Success!"}, - 0, - }, - { - "v1_mount_flag_syntax_key_same_as_mount", - []string{"-mount", "secret", "secret", "foo=bar"}, - []string{"Success!"}, - 0, - }, - { - "v2_single_value", - []string{"kv/write/foo", "foo=bar"}, - v2ExpectedFields, - 0, - }, - { - "v2_multi_value", - []string{"kv/write/foo", "foo=bar", "zip=zap"}, - v2ExpectedFields, - 0, - }, - { - "v2_secret_path", - []string{"kv/write/foo", "foo=bar"}, - []string{"== Secret Path ==", "kv/data/write/foo"}, - 0, - }, - { - "v2_mount_flag_syntax", - []string{"-mount", "kv", "write/foo", "foo=bar"}, - v2ExpectedFields, - 0, - }, - { - "v2_mount_flag_syntax_key_same_as_mount", - []string{"-mount", "kv", "kv", "foo=bar"}, - v2ExpectedFields, - 0, - }, - { - "v2_single_value_backslash", - []string{"kv/write/foo", "foo=\\"}, - []string{"== Secret Path ==", "kv/data/write/foo"}, - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatal(err) - } - - code, combined := kvPutWithRetry(t, client, tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - for _, str := range tc.outStrings { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - }) - } - - t.Run("v2_cas", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatal(err) - } - - // Only have to potentially retry the first time. - code, combined := kvPutWithRetry(t, client, []string{ - "-cas", "0", "kv/write/cas", "bar=baz", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - - for _, str := range v2ExpectedFields { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - - ui, cmd := testKVPutCommand(t) - cmd.client = client - code = cmd.Run([]string{ - "-cas", "1", "kv/write/cas", "bar=baz", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - combined = ui.OutputWriter.String() + ui.ErrorWriter.String() - - for _, str := range v2ExpectedFields { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - - ui, cmd = testKVPutCommand(t) - cmd.client = client - code = cmd.Run([]string{ - "-cas", "1", "kv/write/cas", "bar=baz", - }) - if code != 2 { - t.Fatalf("expected 2 to be %d", code) - } - combined = ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, "check-and-set parameter did not match the current version") { - t.Errorf("expected %q to contain %q", combined, "check-and-set parameter did not match the current version") - } - }) - - t.Run("v1_data", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testKVPutCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/write/data", "bar=baz", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, "Success!") { - t.Errorf("expected %q to contain %q", combined, "created_time") - } - - ui, rcmd := testReadCommand(t) - rcmd.client = client - code = rcmd.Run([]string{ - "secret/write/data", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - combined = ui.OutputWriter.String() + ui.ErrorWriter.String() - if strings.Contains(combined, "data") { - t.Errorf("expected %q not to contain %q", combined, "data") - } - }) - - t.Run("stdin_full", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(`{"foo":"bar"}`)) - stdinW.Close() - }() - - _, cmd := testKVPutCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "secret/write/stdin_full", "-", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - - secret, err := client.Logical().Read("secret/write/stdin_full") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - if exp, act := "bar", secret.Data["foo"].(string); exp != act { - t.Errorf("expected %q to be %q", act, exp) - } - }) - - t.Run("stdin_value", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte("bar")) - stdinW.Close() - }() - - _, cmd := testKVPutCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "secret/write/stdin_value", "foo=-", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - - secret, err := client.Logical().Read("secret/write/stdin_value") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - if exp, act := "bar", secret.Data["foo"].(string); exp != act { - t.Errorf("expected %q to be %q", act, exp) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - _, cmd := testKVPutCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/write/integration", "foo=bar", "zip=zap", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - - secret, err := client.Logical().Read("secret/write/integration") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - if exp, act := "bar", secret.Data["foo"].(string); exp != act { - t.Errorf("expected %q to be %q", act, exp) - } - if exp, act := "zap", secret.Data["zip"].(string); exp != act { - t.Errorf("expected %q to be %q", act, exp) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testKVPutCommand(t) - assertNoTabs(t, cmd) - }) -} - -func testKVGetCommand(tb testing.TB) (*cli.MockUi, *KVGetCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &KVGetCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestKVGetCommand(t *testing.T) { - t.Parallel() - - baseV2ExpectedFields := []string{"created_time", "custom_metadata", "deletion_time", "deletion_time", "version"} - - cases := []struct { - name string - args []string - outStrings []string - code int - }{ - { - "not_enough_args", - []string{}, - []string{"Not enough arguments"}, - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - []string{"Too many arguments"}, - 1, - }, - { - "not_found", - []string{"secret/nope/not/once/never"}, - []string{"No value found at secret/nope/not/once/never"}, - 2, - }, - { - "default", - []string{"secret/read/foo"}, - []string{"foo"}, - 0, - }, - { - "v1_field", - []string{"-field", "foo", "secret/read/foo"}, - []string{"bar"}, - 0, - }, - { - "v1_mount_flag_syntax", - []string{"-mount", "secret", "read/foo"}, - []string{"foo"}, - 0, - }, - { - "v2_field", - []string{"-field", "foo", "kv/read/foo"}, - []string{"bar"}, - 0, - }, - { - "v2_mount_flag_syntax", - []string{"-mount", "kv", "read/foo"}, - append(baseV2ExpectedFields, "foo"), - 0, - }, - { - "v2_mount_flag_syntax_leading_slash", - []string{"-mount", "kv", "/read/foo"}, - append(baseV2ExpectedFields, "foo"), - 0, - }, - { - "v1_mount_flag_syntax_key_same_as_mount", - []string{"-mount", "kv", "kv"}, - append(baseV2ExpectedFields, "foo"), - 0, - }, - { - "v2_mount_flag_syntax_key_same_as_mount", - []string{"-mount", "kv", "kv"}, - append(baseV2ExpectedFields, "foo"), - 0, - }, - { - "v2_not_found", - []string{"kv/nope/not/once/never"}, - []string{"No value found at kv/data/nope/not/once/never"}, - 2, - }, - { - "v2_read", - []string{"kv/read/foo"}, - append(baseV2ExpectedFields, "foo"), - 0, - }, - { - "v2_read_leading_slash", - []string{"/kv/read/foo"}, - append(baseV2ExpectedFields, "foo"), - 0, - }, - { - "v2_read_version", - []string{"--version", "1", "kv/read/foo"}, - append(baseV2ExpectedFields, "foo"), - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatal(err) - } - - // Give time for the upgrade code to run/finish - time.Sleep(time.Second) - - if _, err := client.Logical().Write("secret/read/foo", map[string]interface{}{ - "foo": "bar", - }); err != nil { - t.Fatal(err) - } - - if _, err := client.Logical().Write("kv/data/read/foo", map[string]interface{}{ - "data": map[string]interface{}{ - "foo": "bar", - }, - }); err != nil { - t.Fatal(err) - } - - // create KV entries to test -mount flag where secret key is same as mount path - if _, err := client.Logical().Write("secret/secret", map[string]interface{}{ - "foo": "bar", - }); err != nil { - t.Fatal(err) - } - - if _, err := client.Logical().Write("kv/data/kv", map[string]interface{}{ - "data": map[string]interface{}{ - "foo": "bar", - }, - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testKVGetCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - - for _, str := range tc.outStrings { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - }) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testKVGetCommand(t) - assertNoTabs(t, cmd) - }) -} - -func testKVListCommand(tb testing.TB) (*cli.MockUi, *KVListCommand) { - tb.Helper() - ui := cli.NewMockUi() - cmd := &KVListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } - - return ui, cmd -} - -// TestKVListCommand runs tests for `vault kv list` -func TestKVListCommand(t *testing.T) { - testCases := []struct { - name string - args []string - outStrings []string - code int - }{ - { - name: "default", - args: []string{"kv/my-prefix"}, - outStrings: []string{"secret-0", "secret-1", "secret-2"}, - code: 0, - }, - { - name: "not_enough_args", - args: []string{}, - outStrings: []string{"Not enough arguments"}, - code: 1, - }, - { - name: "v2_default_with_mount", - args: []string{"-mount", "kv", "my-prefix"}, - outStrings: []string{"secret-0", "secret-1", "secret-2"}, - code: 0, - }, - { - name: "v1_default_with_mount", - args: []string{"kv/my-prefix"}, - outStrings: []string{"secret-0", "secret-1", "secret-2"}, - code: 0, - }, - { - name: "v2_not_found", - args: []string{"kv/nope/not/once/never"}, - outStrings: []string{"No value found at kv/metadata/nope/not/once/never"}, - code: 2, - }, - { - name: "v1_mount_only", - args: []string{"kv"}, - outStrings: []string{"my-prefix"}, - code: 0, - }, - { - name: "v2_mount_only", - args: []string{"-mount", "kv"}, - outStrings: []string{"my-prefix"}, - code: 0, - }, - { - // this is behavior that should be tested - // `kv` here is an explicit mount - // `my-prefix` is not - // the current kv code will ignore `my-prefix` - name: "ignore_multi_part_mounts", - args: []string{"-mount", "kv/my-prefix"}, - outStrings: []string{"my-prefix"}, - code: 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, testCase := range testCases { - testCase := testCase - - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - // test setup - client, closer := testVaultServer(t) - defer closer() - - // enable kv-v2 backend - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatal(err) - } - time.Sleep(time.Second) - - ctx := context.Background() - for i := 0; i < 3; i++ { - path := fmt.Sprintf("my-prefix/secret-%d", i) - _, err := client.KVv2("kv/").Put(ctx, path, map[string]interface{}{ - "foo": "bar", - }) - if err != nil { - t.Fatal(err) - } - } - - ui, cmd := testKVListCommand(t) - cmd.client = client - - code := cmd.Run(testCase.args) - if code != testCase.code { - t.Errorf("expected %d to be %d", code, testCase.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - for _, str := range testCase.outStrings { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - }) - } - }) -} - -func testKVMetadataGetCommand(tb testing.TB) (*cli.MockUi, *KVMetadataGetCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &KVMetadataGetCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestKVMetadataGetCommand(t *testing.T) { - t.Parallel() - - expectedTopLevelFields := []string{ - "cas_required", - "created_time", - "current_version", - "custom_metadata", - "delete_version_after", - "max_versions", - "oldest_version", - "updated_time", - } - - expectedVersionFields := []string{ - "created_time", // field is redundant - "deletion_time", - "destroyed", - } - - cases := []struct { - name string - args []string - outStrings []string - code int - }{ - { - "v1", - []string{"secret/foo"}, - []string{"Metadata not supported on KV Version 1"}, - 1, - }, - { - "metadata_exists", - []string{"kv/foo"}, - expectedTopLevelFields, - 0, - }, - // ensure that all top-level and version-level fields are output along with version num - { - "versions_exist", - []string{"kv/foo"}, - append(expectedTopLevelFields, expectedVersionFields[:]...), - 0, - }, - { - "mount_flag_syntax", - []string{"-mount", "kv", "foo"}, - expectedTopLevelFields, - 0, - }, - { - "mount_flag_syntax_key_same_as_mount", - []string{"-mount", "kv", "kv"}, - expectedTopLevelFields, - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatal(err) - } - - // Give time for the upgrade code to run/finish - time.Sleep(time.Second) - - if _, err := client.Logical().Write("kv/data/foo", map[string]interface{}{ - "data": map[string]interface{}{ - "foo": "bar", - }, - }); err != nil { - t.Fatal(err) - } - - // create KV entry to test -mount flag where secret key is same as mount path - if _, err := client.Logical().Write("kv/data/kv", map[string]interface{}{ - "data": map[string]interface{}{ - "foo": "bar", - }, - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testKVMetadataGetCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - for _, str := range tc.outStrings { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - }) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testKVMetadataGetCommand(t) - assertNoTabs(t, cmd) - }) -} - -func testKVPatchCommand(tb testing.TB) (*cli.MockUi, *KVPatchCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &KVPatchCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestKVPatchCommand_ArgValidation(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "empty_kvs", - []string{"kv/patch/foo"}, - "Must supply data", - 1, - }, - { - "kvs_no_value", - []string{"kv/patch/foo", "foo"}, - "Failed to parse K=V data", - 1, - }, - { - "mount_flag_syntax", - []string{"-mount", "kv"}, - "Not enough arguments", - 1, - }, - } - - for _, tc := range cases { - tc := tc // capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - code, combined := kvPatchWithRetry(t, client, tc.args, nil) - - if code != tc.code { - t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v\n", tc.code, code, tc.args) - } - - if !strings.Contains(combined, tc.out) { - t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v\n", tc.out, combined, tc.args) - } - }) - } -} - -// expectedPatchFields produces a deterministic slice of -// expected fields for patch command output since const -// slices are not supported -func expectedPatchFields() []string { - return []string{ - "created_time", - "custom_metadata", - "deletion_time", - "destroyed", - "version", - } -} - -func TestKVPatchCommand_StdinFull(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - if _, err := client.Logical().Write("kv/data/patch/foo", map[string]interface{}{ - "data": map[string]interface{}{ - "foo": "a", - }, - }); err != nil { - t.Fatalf("write failed, err: %#v\n", err) - } - - cases := [][]string{ - {"kv/patch/foo", "-"}, - {"-mount", "kv", "patch/foo", "-"}, - } - for i, args := range cases { - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(fmt.Sprintf(`{"foo%d":"bar%d"}`, i, i))) - stdinW.Close() - }() - code, combined := kvPatchWithRetry(t, client, args, stdinR) - - for _, str := range expectedPatchFields() { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - - if code != 0 { - t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args) - } - - secret, err := client.Logical().ReadWithContext(context.Background(), "kv/data/patch/foo") - if err != nil { - t.Fatalf("read failed, err: %#v\n", err) - } - - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - - secretDataRaw, ok := secret.Data["data"] - - if !ok { - t.Fatalf("expected secret to have nested data key, data: %#v", secret.Data) - } - - secretData := secretDataRaw.(map[string]interface{}) - foo, ok := secretData[fmt.Sprintf("foo%d", i)].(string) - if !ok { - t.Fatal("expected foo to be a string but it wasn't") - } - - if exp, act := fmt.Sprintf("bar%d", i), foo; exp != act { - t.Fatalf("expected %q to be %q, data: %#v\n", act, exp, secret.Data) - } - } -} - -func TestKVPatchCommand_StdinValue(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - if _, err := client.Logical().Write("kv/data/patch/foo", map[string]interface{}{ - "data": map[string]interface{}{ - "foo": "a", - }, - }); err != nil { - t.Fatalf("write failed, err: %#v\n", err) - } - - cases := [][]string{ - {"kv/patch/foo", "foo=-"}, - {"-mount", "kv", "patch/foo", "foo=-"}, - } - - for i, args := range cases { - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(fmt.Sprintf("bar%d", i))) - stdinW.Close() - }() - - code, combined := kvPatchWithRetry(t, client, args, stdinR) - if code != 0 { - t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args) - } - - for _, str := range expectedPatchFields() { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - - secret, err := client.Logical().ReadWithContext(context.Background(), "kv/data/patch/foo") - if err != nil { - t.Fatalf("read failed, err: %#v\n", err) - } - - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - - secretDataRaw, ok := secret.Data["data"] - - if !ok { - t.Fatalf("expected secret to have nested data key, data: %#v\n", secret.Data) - } - - secretData := secretDataRaw.(map[string]interface{}) - - if exp, act := fmt.Sprintf("bar%d", i), secretData["foo"].(string); exp != act { - t.Fatalf("expected %q to be %q, data: %#v\n", act, exp, secret.Data) - } - } -} - -func TestKVPatchCommand_RWMethodNotExists(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - cases := [][]string{ - {"-method", "rw", "kv/patch/foo", "foo=a"}, - {"-method", "rw", "-mount", "kv", "patch/foo", "foo=a"}, - } - - for _, args := range cases { - code, combined := kvPatchWithRetry(t, client, args, nil) - - if code != 2 { - t.Fatalf("expected code to be 2 but was %d for patch cmd with args %#v\n", code, args) - } - - expectedOutputSubstr := "No value found" - if !strings.Contains(combined, expectedOutputSubstr) { - t.Fatalf("expected output %q to contain %q for patch cmd with args %#v\n", combined, expectedOutputSubstr, args) - } - } -} - -func TestKVPatchCommand_RWMethodSucceeds(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - if _, err := client.Logical().Write("kv/data/patch/foo", map[string]interface{}{ - "data": map[string]interface{}{ - "foo": "a", - "bar": "b", - }, - }); err != nil { - t.Fatalf("write failed, err: %#v\n", err) - } - - // Test single value - args := []string{"-method", "rw", "kv/patch/foo", "foo=aa"} - code, combined := kvPatchWithRetry(t, client, args, nil) - - if code != 0 { - t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args) - } - - for _, str := range expectedPatchFields() { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - - // Test that full path was output - for _, str := range []string{"== Secret Path ==", "kv/data/patch/foo"} { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - - // Test multi value - args = []string{"-method", "rw", "kv/patch/foo", "foo=aaa", "bar=bbb"} - code, combined = kvPatchWithRetry(t, client, args, nil) - - if code != 0 { - t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args) - } - - for _, str := range expectedPatchFields() { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } -} - -func TestKVPatchCommand_CAS(t *testing.T) { - cases := []struct { - name string - key string - args []string - expected string - outStrings []string - code int - }{ - { - "right version", - "foo", - []string{"-cas", "1", "kv/foo", "bar=quux"}, - "quux", - expectedPatchFields(), - 0, - }, - { - "wrong version", - "foo", - []string{"-cas", "2", "kv/foo", "bar=wibble"}, - "baz", - []string{"check-and-set parameter did not match the current version"}, - 2, - }, - { - "mount_flag_syntax", - "foo", - []string{"-mount", "kv", "-cas", "1", "foo", "bar=quux"}, - "quux", - expectedPatchFields(), - 0, - }, - { - "v2_mount_flag_syntax_key_same_as_mount", - "kv", - []string{"-mount", "kv", "-cas", "1", "kv", "bar=quux"}, - "quux", - expectedPatchFields(), - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - // create a policy with patch capability - policy := `path "kv/*" { capabilities = ["create", "update", "read", "patch"] }` - secretAuth, err := createTokenForPolicy(t, client, policy) - if err != nil { - t.Fatalf("policy/token creation failed for policy %s, err: %#v\n", policy, err) - } - - kvClient, err := client.Clone() - if err != nil { - t.Fatal(err) - } - - kvClient.SetToken(secretAuth.ClientToken) - - data := map[string]interface{}{ - "bar": "baz", - } - - _, err = kvClient.Logical().Write("kv/data/"+tc.key, map[string]interface{}{"data": data}) - if err != nil { - t.Fatal(err) - } - - code, combined := kvPatchWithRetry(t, kvClient, tc.args, nil) - - if code != tc.code { - t.Fatalf("expected code to be %d but was %d", tc.code, code) - } - - for _, str := range tc.outStrings { - if !strings.Contains(combined, str) { - t.Errorf("expected %q to contain %q", combined, str) - } - } - - secret, err := kvClient.Logical().ReadWithContext(context.Background(), "kv/data/"+tc.key) - if err != nil { - t.Fatal(err) - } - bar := secret.Data["data"].(map[string]interface{})["bar"] - if bar != tc.expected { - t.Fatalf("expected bar to be %q but it was %q", tc.expected, bar) - } - }) - } -} - -func TestKVPatchCommand_Methods(t *testing.T) { - cases := []struct { - name string - args []string - expected string - code int - }{ - { - "rw", - []string{"-method", "rw", "kv/foo", "bar=quux"}, - "quux", - 0, - }, - { - "patch", - []string{"-method", "patch", "kv/foo", "bar=wibble"}, - "wibble", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - // create a policy with patch capability - policy := `path "kv/*" { capabilities = ["create", "update", "read", "patch"] }` - secretAuth, err := createTokenForPolicy(t, client, policy) - if err != nil { - t.Fatalf("policy/token creation failed for policy %s, err: %#v\n", policy, err) - } - - kvClient, err := client.Clone() - if err != nil { - t.Fatal(err) - } - - kvClient.SetToken(secretAuth.ClientToken) - - _, err = kvClient.Logical().Write("kv/data/foo", map[string]interface{}{"data": map[string]interface{}{"bar": "baz"}}) - if err != nil { - t.Fatal(err) - } - - code, _ := kvPatchWithRetry(t, kvClient, tc.args, nil) - - if code != tc.code { - t.Fatalf("expected code to be %d but was %d", tc.code, code) - } - - secret, err := kvClient.Logical().ReadWithContext(context.Background(), "kv/data/foo") - if err != nil { - t.Fatal(err) - } - bar := secret.Data["data"].(map[string]interface{})["bar"] - if bar != tc.expected { - t.Fatalf("expected bar to be %q but it was %q", tc.expected, bar) - } - }) - } -} - -func TestKVPatchCommand_403Fallback(t *testing.T) { - cases := []struct { - name string - args []string - expected string - code int - }{ - // if no -method is specified, and patch fails, it should fall back to rw and succeed - { - "unspecified", - []string{"kv/foo", "bar=quux"}, - `add the "patch" capability to your ACL policy`, - 0, - }, - // if -method=patch is specified, and patch fails, it should not fall back, and just error - { - "specifying patch", - []string{"-method", "patch", "kv/foo", "bar=quux"}, - "permission denied", - 2, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - // create a policy without patch capability - policy := `path "kv/*" { capabilities = ["create", "update", "read"] }` - secretAuth, err := createTokenForPolicy(t, client, policy) - if err != nil { - t.Fatalf("policy/token creation failed for policy %s, err: %#v\n", policy, err) - } - - kvClient, err := client.Clone() - if err != nil { - t.Fatal(err) - } - - kvClient.SetToken(secretAuth.ClientToken) - - // Write a value then attempt to patch it - _, err = kvClient.Logical().Write("kv/data/foo", map[string]interface{}{"data": map[string]interface{}{"bar": "baz"}}) - if err != nil { - t.Fatal(err) - } - - code, combined := kvPatchWithRetry(t, kvClient, tc.args, nil) - - if code != tc.code { - t.Fatalf("expected code to be %d but was %d", tc.code, code) - } - - if !strings.Contains(combined, tc.expected) { - t.Errorf("expected %q to contain %q", combined, tc.expected) - } - }) - } -} - -func TestKVPatchCommand_RWMethodPolicyVariations(t *testing.T) { - cases := []struct { - name string - args []string - policy string - expected string - code int - }{ - // if the policy doesn't have read capability and -method=rw is specified, it fails - { - "no read", - []string{"-method", "rw", "kv/foo", "bar=quux"}, - `path "kv/*" { capabilities = ["create", "update"] }`, - "permission denied", - 2, - }, - // if the policy doesn't have update capability and -method=rw is specified, it fails - { - "no update", - []string{"-method", "rw", "kv/foo", "bar=quux"}, - `path "kv/*" { capabilities = ["create", "read"] }`, - "permission denied", - 2, - }, - // if the policy has both read and update and -method=rw is specified, it succeeds - { - "read and update", - []string{"-method", "rw", "kv/foo", "bar=quux"}, - `path "kv/*" { capabilities = ["create", "read", "update"] }`, - "", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - secretAuth, err := createTokenForPolicy(t, client, tc.policy) - if err != nil { - t.Fatalf("policy/token creation failed for policy %s, err: %#v\n", tc.policy, err) - } - - client.SetToken(secretAuth.ClientToken) - - putArgs := []string{"kv/foo", "foo=bar", "bar=baz"} - code, combined := kvPutWithRetry(t, client, putArgs) - if code != 0 { - t.Errorf("write failed, expected %d to be 0, output: %s", code, combined) - } - - code, combined = kvPatchWithRetry(t, client, tc.args, nil) - if code != tc.code { - t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v\n", tc.code, code, tc.args) - } - - if code != 0 { - if !strings.Contains(combined, tc.expected) { - t.Fatalf("expected output %q to contain %q for patch cmd with args %#v\n", combined, tc.expected, tc.args) - } - } - }) - } -} - -func TestPadEqualSigns(t *testing.T) { - t.Parallel() - - header := "Test Header" - - cases := []struct { - name string - totalPathLen int - expectedCount int - }{ - { - name: "path with even length", - totalPathLen: 20, - expectedCount: 4, - }, - { - name: "path with odd length", - totalPathLen: 19, - expectedCount: 3, - }, - { - name: "smallest possible path", - totalPathLen: 8, - expectedCount: 2, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - padded := padEqualSigns(header, tc.totalPathLen) - - signs := strings.Split(padded, fmt.Sprintf(" %s ", header)) - if len(signs[0]) != len(signs[1]) { - t.Fatalf("expected an equal number of equal signs on both sides") - } - for _, sign := range signs { - count := strings.Count(sign, "=") - if count != tc.expectedCount { - t.Fatalf("expected %d equal signs but there were %d", tc.expectedCount, count) - } - } - }) - } -} - -func createTokenForPolicy(t *testing.T, client *api.Client, policy string) (*api.SecretAuth, error) { - t.Helper() - - if err := client.Sys().PutPolicy("policy", policy); err != nil { - return nil, err - } - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"policy"}, - TTL: "30m", - }) - if err != nil { - return nil, err - } - - if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { - return nil, fmt.Errorf("missing auth data: %#v", secret) - } - - return secret.Auth, err -} diff --git a/command/lease_lookup_test.go b/command/lease_lookup_test.go deleted file mode 100644 index bc0019428..000000000 --- a/command/lease_lookup_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testLeaseLookupCommand(tb testing.TB) (*cli.MockUi, *LeaseLookupCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &LeaseLookupCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -// testLeaseLookupCommandMountAndLease mounts a leased secret backend and returns -// the leaseID of an item. -func testLeaseLookupCommandMountAndLease(tb testing.TB, client *api.Client) string { - if err := client.Sys().Mount("testing", &api.MountInput{ - Type: "generic-leased", - }); err != nil { - tb.Fatal(err) - } - - if _, err := client.Logical().Write("testing/foo", map[string]interface{}{ - "key": "value", - "lease": "5m", - }); err != nil { - tb.Fatal(err) - } - - // Read the secret back to get the leaseID - secret, err := client.Logical().Read("testing/foo") - if err != nil { - tb.Fatal(err) - } - if secret == nil || secret.LeaseID == "" { - tb.Fatalf("missing secret or lease: %#v", secret) - } - - return secret.LeaseID -} - -// TestLeaseLookupCommand_Run tests basic lookup -func TestLeaseLookupCommand_Run(t *testing.T) { - t.Parallel() - - t.Run("empty", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - _ = testLeaseLookupCommandMountAndLease(t, client) - - ui, cmd := testLeaseLookupCommand(t) - cmd.client = client - - code := cmd.Run(nil) - if exp := 1; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - expectedMsg := "Missing ID!" - if !strings.Contains(combined, expectedMsg) { - t.Errorf("expected %q to contain %q", combined, expectedMsg) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - leaseID := testLeaseLookupCommandMountAndLease(t, client) - - _, cmd := testLeaseLookupCommand(t) - cmd.client = client - - code := cmd.Run([]string{leaseID}) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testLeaseLookupCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/lease_renew_test.go b/command/lease_renew_test.go deleted file mode 100644 index 40719d1f0..000000000 --- a/command/lease_renew_test.go +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testLeaseRenewCommand(tb testing.TB) (*cli.MockUi, *LeaseRenewCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &LeaseRenewCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -// testLeaseRenewCommandMountAndLease mounts a leased secret backend and returns -// the leaseID of an item. -func testLeaseRenewCommandMountAndLease(tb testing.TB, client *api.Client) string { - if err := client.Sys().Mount("testing", &api.MountInput{ - Type: "generic-leased", - }); err != nil { - tb.Fatal(err) - } - - if _, err := client.Logical().Write("testing/foo", map[string]interface{}{ - "key": "value", - "lease": "5m", - }); err != nil { - tb.Fatal(err) - } - - // Read the secret back to get the leaseID - secret, err := client.Logical().Read("testing/foo") - if err != nil { - tb.Fatal(err) - } - if secret == nil || secret.LeaseID == "" { - tb.Fatalf("missing secret or lease: %#v", secret) - } - - return secret.LeaseID -} - -func TestLeaseRenewCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "empty", - nil, - "Missing ID!", - 1, - }, - { - "increment", - []string{"-increment", "60s"}, - "foo", - 0, - }, - { - "increment_no_suffix", - []string{"-increment", "60"}, - "foo", - 0, - }, - } - - t.Run("group", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - leaseID := testLeaseRenewCommandMountAndLease(t, client) - - ui, cmd := testLeaseRenewCommand(t) - cmd.client = client - - if tc.args != nil { - tc.args = append(tc.args, leaseID) - } - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - leaseID := testLeaseRenewCommandMountAndLease(t, client) - - _, cmd := testLeaseRenewCommand(t) - cmd.client = client - - code := cmd.Run([]string{leaseID}) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testLeaseRenewCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "foo/bar", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error renewing foo/bar: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testLeaseRenewCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/lease_revoke_test.go b/command/lease_revoke_test.go deleted file mode 100644 index ece8d31f0..000000000 --- a/command/lease_revoke_test.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testLeaseRevokeCommand(tb testing.TB) (*cli.MockUi, *LeaseRevokeCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &LeaseRevokeCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestLeaseRevokeCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "force_without_prefix", - []string{"-force"}, - "requires also specifying -prefix", - 1, - }, - { - "single", - nil, - "All revocation operations queued successfully", - 0, - }, - { - "single_sync", - []string{"-sync"}, - "Success", - 0, - }, - { - "force_prefix", - []string{"-force", "-prefix"}, - "Success", - 0, - }, - { - "prefix", - []string{"-prefix"}, - "All revocation operations queued successfully", - 0, - }, - { - "prefix_sync", - []string{"-prefix", "-sync"}, - "Success", - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("secret-leased", &api.MountInput{ - Type: "generic-leased", - }); err != nil { - t.Fatal(err) - } - - path := "secret-leased/revoke/" + tc.name - data := map[string]interface{}{ - "key": "value", - "lease": "1m", - } - if _, err := client.Logical().Write(path, data); err != nil { - t.Fatal(err) - } - secret, err := client.Logical().Read(path) - if err != nil { - t.Fatal(err) - } - - ui, cmd := testLeaseRevokeCommand(t) - cmd.client = client - - tc.args = append(tc.args, secret.LeaseID) - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testLeaseRevokeCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "foo/bar", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error revoking lease foo/bar: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testLeaseRevokeCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/list_test.go b/command/list_test.go deleted file mode 100644 index 60467647a..000000000 --- a/command/list_test.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testListCommand(tb testing.TB) (*cli.MockUi, *ListCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &ListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestListCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - { - "not_found", - []string{"nope/not/once/never"}, - "", - 2, - }, - { - "default", - []string{"secret/list"}, - "bar\nbaz\nfoo", - 0, - }, - { - "default_slash", - []string{"secret/list/"}, - "bar\nbaz\nfoo", - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - keys := []string{ - "secret/list/foo", - "secret/list/bar", - "secret/list/baz", - } - for _, k := range keys { - if _, err := client.Logical().Write(k, map[string]interface{}{ - "foo": "bar", - }); err != nil { - t.Fatal(err) - } - } - - ui, cmd := testListCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testListCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/list", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error listing secret/list: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testListCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/log_flags_test.go b/command/log_flags_test.go deleted file mode 100644 index 1e54397f8..000000000 --- a/command/log_flags_test.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "flag" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLogFlags_ValuesProvider(t *testing.T) { - cases := map[string]struct { - flagKey string - envVarKey string - wantValue string - wantFound bool - }{ - "flag-missing": { - flagKey: "invalid", - envVarKey: "valid-env-var", - wantValue: "envVarValue", - wantFound: true, - }, - "envVar-missing": { - flagKey: "valid-flag", - envVarKey: "invalid", - wantValue: "flagValue", - wantFound: true, - }, - "all-present": { - flagKey: "valid-flag", - envVarKey: "valid-env-var", - wantValue: "flagValue", - wantFound: true, - }, - "all-missing": { - flagKey: "invalid", - envVarKey: "invalid", - wantValue: "", - wantFound: false, - }, - } - - // Sneaky little fake providers - flagFaker := func(key string) (flag.Value, bool) { - var result fakeFlag - var found bool - - if key == "valid-flag" { - result.Set("flagValue") - found = true - } - - return &result, found - } - - envFaker := func(key string) (string, bool) { - var found bool - var result string - - if key == "valid-env-var" { - result = "envVarValue" - found = true - } - - return result, found - } - - vp := valuesProvider{ - flagProvider: flagFaker, - envVarProvider: envFaker, - } - - for name, tc := range cases { - val, found := vp.overrideValue(tc.flagKey, tc.envVarKey) - assert.Equal(t, tc.wantFound, found, name) - assert.Equal(t, tc.wantValue, val, name) - } -} - -type fakeFlag struct { - value string -} - -func (v *fakeFlag) String() string { - return v.value -} - -func (v *fakeFlag) Set(raw string) error { - v.value = raw - return nil -} diff --git a/command/login_test.go b/command/login_test.go deleted file mode 100644 index deb96be18..000000000 --- a/command/login_test.go +++ /dev/null @@ -1,616 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "context" - "regexp" - "strings" - "testing" - "time" - - "github.com/mitchellh/cli" - - "github.com/hashicorp/vault/api" - credToken "github.com/hashicorp/vault/builtin/credential/token" - credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" - "github.com/hashicorp/vault/command/token" - "github.com/hashicorp/vault/helper/testhelpers" - "github.com/hashicorp/vault/vault" -) - -// minTokenLengthExternal is the minimum size of SSC -// tokens we are currently handing out to end users, without any -// namespace information -const minTokenLengthExternal = 91 - -func testLoginCommand(tb testing.TB) (*cli.MockUi, *LoginCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &LoginCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - - // Override to our own token helper - tokenHelper: token.NewTestingTokenHelper(), - }, - Handlers: map[string]LoginHandler{ - "token": &credToken.CLIHandler{}, - "userpass": &credUserpass.CLIHandler{}, - }, - } -} - -func TestCustomPath(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("my-auth", "userpass", ""); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("auth/my-auth/users/test", map[string]interface{}{ - "password": "test", - "policies": "default", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testLoginCommand(t) - cmd.client = client - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - - // Emulate an unknown token format present in ~/.vault-token, for example - client.SetToken("a.a") - - code := cmd.Run([]string{ - "-method", "userpass", - "-path", "my-auth", - "username=test", - "password=test", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! You are now authenticated." - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to be %q", combined, expected) - } - - storedToken, err := tokenHelper.Get() - if err != nil { - t.Fatal(err) - } - - if l, exp := len(storedToken), minTokenLengthExternal+vault.TokenPrefixLength; l < exp { - t.Errorf("expected token to be %d characters, was %d: %q", exp, l, storedToken) - } -} - -// Do not persist the token to the token helper -func TestNoStore(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"default"}, - TTL: "30m", - }) - if err != nil { - t.Fatal(err) - } - token := secret.Auth.ClientToken - - _, cmd := testLoginCommand(t) - cmd.client = client - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - - // Ensure we have no token to start - if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { - t.Errorf("expected token helper to be empty: %s: %q", err, storedToken) - } - - code := cmd.Run([]string{ - "-no-store", - token, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - storedToken, err := tokenHelper.Get() - if err != nil { - t.Fatal(err) - } - - if exp := ""; storedToken != exp { - t.Errorf("expected %q to be %q", storedToken, exp) - } -} - -func TestStores(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"default"}, - TTL: "30m", - }) - if err != nil { - t.Fatal(err) - } - token := secret.Auth.ClientToken - - _, cmd := testLoginCommand(t) - cmd.client = client - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - token, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - storedToken, err := tokenHelper.Get() - if err != nil { - t.Fatal(err) - } - - if storedToken != token { - t.Errorf("expected %q to be %q", storedToken, token) - } -} - -func TestTokenOnly(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ - "password": "test", - "policies": "default", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testLoginCommand(t) - cmd.client = client - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "-token-only", - "-method", "userpass", - "username=test", - "password=test", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - // Verify only the token was printed - token := ui.OutputWriter.String() - if l, exp := len(token), minTokenLengthExternal+vault.TokenPrefixLength; l != exp { - t.Errorf("expected token to be %d characters, was %d: %q", exp, l, token) - } - - // Verify the token was not stored - if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { - t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) - } -} - -func TestFailureNoStore(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testLoginCommand(t) - cmd.client = client - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "not-a-real-token", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error authenticating: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { - t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) - } -} - -func TestWrapAutoUnwrap(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ - "password": "test", - "policies": "default", - }); err != nil { - t.Fatal(err) - } - - _, cmd := testLoginCommand(t) - cmd.client = client - - // Set the wrapping ttl to 5s. We can't set this via the flag because we - // override the client object before that particular flag is parsed. - client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) - - code := cmd.Run([]string{ - "-method", "userpass", - "username=test", - "password=test", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - // Unset the wrapping - client.SetWrappingLookupFunc(func(string, string) string { return "" }) - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - token, err := tokenHelper.Get() - if err != nil || token == "" { - t.Fatalf("expected token from helper: %s: %q", err, token) - } - client.SetToken(token) - - // Ensure the resulting token is unwrapped - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Error(err) - } - if secret == nil { - t.Fatal("secret was nil") - } - - if secret.WrapInfo != nil { - t.Errorf("expected to be unwrapped: %#v", secret) - } -} - -func TestWrapTokenOnly(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ - "password": "test", - "policies": "default", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testLoginCommand(t) - cmd.client = client - - // Set the wrapping ttl to 5s. We can't set this via the flag because we - // override the client object before that particular flag is parsed. - client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) - - code := cmd.Run([]string{ - "-token-only", - "-method", "userpass", - "username=test", - "password=test", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - // Unset the wrapping - client.SetWrappingLookupFunc(func(string, string) string { return "" }) - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - storedToken, err := tokenHelper.Get() - if err != nil || storedToken != "" { - t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) - } - - token := strings.TrimSpace(ui.OutputWriter.String()) - if token == "" { - t.Errorf("expected %q to not be %q", token, "") - } - - // Ensure the resulting token is, in fact, still wrapped. - client.SetToken(token) - secret, err := client.Logical().Unwrap("") - if err != nil { - t.Error(err) - } - if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { - t.Fatalf("expected secret to have auth: %#v", secret) - } -} - -func TestWrapNoStore(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ - "password": "test", - "policies": "default", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testLoginCommand(t) - cmd.client = client - - // Set the wrapping ttl to 5s. We can't set this via the flag because we - // override the client object before that particular flag is parsed. - client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) - - code := cmd.Run([]string{ - "-no-store", - "-method", "userpass", - "username=test", - "password=test", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - // Unset the wrapping - client.SetWrappingLookupFunc(func(string, string) string { return "" }) - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - storedToken, err := tokenHelper.Get() - if err != nil || storedToken != "" { - t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) - } - - expected := "wrapping_token" - output := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(output, expected) { - t.Errorf("expected %q to contain %q", output, expected) - } -} - -func TestCommunicationFailure(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testLoginCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "token", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error authenticating: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } -} - -func TestNoTabs(t *testing.T) { - t.Parallel() - - _, cmd := testLoginCommand(t) - assertNoTabs(t, cmd) -} - -func TestLoginMFASinglePhase(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - methodName := "foo" - waitPeriod := 5 - userClient, entityID, methodID := testhelpers.SetupLoginMFATOTP(t, client, methodName, waitPeriod) - enginePath := testhelpers.RegisterEntityInTOTPEngine(t, client, entityID, methodID) - - runCommand := func(methodIdentifier string) { - // the time required for the totp engine to generate a new code - time.Sleep(time.Duration(waitPeriod) * time.Second) - totpCode := testhelpers.GetTOTPCodeFromEngine(t, client, enginePath) - ui, cmd := testLoginCommand(t) - cmd.client = userClient - // login command bails early for test clients, so we have to explicitly set this - cmd.client.SetMFACreds([]string{methodIdentifier + ":" + totpCode}) - code := cmd.Run([]string{ - "-method", "userpass", - "username=testuser1", - "password=testpassword", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - storedToken, err := tokenHelper.Get() - if err != nil { - t.Fatal(err) - } - if storedToken == "" { - t.Fatal("expected non-empty stored token") - } - output := ui.OutputWriter.String() - if !strings.Contains(output, storedToken) { - t.Fatalf("expected stored token: %q, got: %q", storedToken, output) - } - } - runCommand(methodID) - runCommand(methodName) -} - -func TestLoginMFATwoPhase(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testLoginCommand(t) - - userclient, entityID, methodID := testhelpers.SetupLoginMFATOTP(t, client, "", 5) - cmd.client = userclient - - _ = testhelpers.RegisterEntityInTOTPEngine(t, client, entityID, methodID) - - // clear the MFA creds just to be sure - cmd.client.SetMFACreds([]string{}) - - code := cmd.Run([]string{ - "-method", "userpass", - "username=testuser1", - "password=testpassword", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := methodID - output := ui.OutputWriter.String() - if !strings.Contains(output, expected) { - t.Fatalf("expected stored token: %q, got: %q", expected, output) - } - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - storedToken, err := tokenHelper.Get() - if storedToken != "" { - t.Fatal("expected empty stored token") - } - if err != nil { - t.Fatal(err) - } -} - -func TestLoginMFATwoPhaseNonInteractiveMethodName(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testLoginCommand(t) - - methodName := "foo" - waitPeriod := 5 - userclient, entityID, methodID := testhelpers.SetupLoginMFATOTP(t, client, methodName, waitPeriod) - cmd.client = userclient - - engineName := testhelpers.RegisterEntityInTOTPEngine(t, client, entityID, methodID) - - // clear the MFA creds just to be sure - cmd.client.SetMFACreds([]string{}) - - code := cmd.Run([]string{ - "-method", "userpass", - "-non-interactive", - "username=testuser1", - "password=testpassword", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - output := ui.OutputWriter.String() - - reqIdReg := regexp.MustCompile(`mfa_request_id\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s+mfa_constraint`) - reqIDRaw := reqIdReg.FindAllStringSubmatch(output, -1) - if len(reqIDRaw) == 0 || len(reqIDRaw[0]) < 2 { - t.Fatal("failed to MFA request ID from output") - } - mfaReqID := reqIDRaw[0][1] - - validateFunc := func(methodIdentifier string) { - // the time required for the totp engine to generate a new code - time.Sleep(time.Duration(waitPeriod) * time.Second) - totpPasscode1 := "passcode=" + testhelpers.GetTOTPCodeFromEngine(t, client, engineName) - - secret, err := cmd.client.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ - "mfa_request_id": mfaReqID, - "mfa_payload": map[string][]string{ - methodIdentifier: {totpPasscode1}, - }, - }) - if err != nil { - t.Fatalf("mfa validation failed: %v", err) - } - - if secret.Auth == nil || secret.Auth.ClientToken == "" { - t.Fatalf("mfa validation did not return a client token") - } - } - - validateFunc(methodName) -} diff --git a/command/monitor_test.go b/command/monitor_test.go deleted file mode 100644 index 8c2c288d8..000000000 --- a/command/monitor_test.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/mitchellh/cli" -) - -func testMonitorCommand(tb testing.TB) (*cli.MockUi, *MonitorCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &MonitorCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestMonitorCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int64 - }{ - { - "valid", - []string{ - "-log-level=debug", - }, - "", - 0, - }, - { - "too_many_args", - []string{ - "-log-level=debug", - "foo", - }, - "Too many arguments", - 1, - }, - { - "unknown_log_level", - []string{ - "-log-level=haha", - }, - "haha is an unknown log level", - 1, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - client, closer := testVaultServer(t) - defer closer() - - var code int64 - shutdownCh := make(chan struct{}) - - ui, cmd := testMonitorCommand(t) - cmd.client = client - cmd.ShutdownCh = shutdownCh - - go func() { - atomic.StoreInt64(&code, int64(cmd.Run(tc.args))) - }() - - <-time.After(3 * time.Second) - close(shutdownCh) - - if atomic.LoadInt64(&code) != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Fatalf("expected %q to contain %q", combined, tc.out) - } - }) - } -} diff --git a/command/operator_diagnose_test.go b/command/operator_diagnose_test.go deleted file mode 100644 index 11b00b0f1..000000000 --- a/command/operator_diagnose_test.go +++ /dev/null @@ -1,560 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -//go:build !race - -package command - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/hashicorp/vault/vault/diagnose" - "github.com/mitchellh/cli" -) - -func testOperatorDiagnoseCommand(tb testing.TB) *OperatorDiagnoseCommand { - tb.Helper() - - ui := cli.NewMockUi() - return &OperatorDiagnoseCommand{ - diagnose: diagnose.New(ioutil.Discard), - BaseCommand: &BaseCommand{ - UI: ui, - }, - skipEndEnd: true, - } -} - -func TestOperatorDiagnoseCommand_Run(t *testing.T) { - t.Parallel() - cases := []struct { - name string - args []string - expected []*diagnose.Result - }{ - { - "diagnose_ok", - []string{ - "-config", "./server/test-fixtures/config_diagnose_ok.hcl", - }, - []*diagnose.Result{ - { - Name: "Parse Configuration", - Status: diagnose.OkStatus, - }, - { - Name: "Start Listeners", - Status: diagnose.WarningStatus, - Children: []*diagnose.Result{ - { - Name: "Create Listeners", - Status: diagnose.OkStatus, - }, - { - Name: "Check Listener TLS", - Status: diagnose.WarningStatus, - Warnings: []string{ - "TLS is disabled in a listener config stanza.", - }, - }, - }, - }, - { - Name: "Check Storage", - Status: diagnose.OkStatus, - Children: []*diagnose.Result{ - { - Name: "Create Storage Backend", - Status: diagnose.OkStatus, - }, - { - Name: "Check Consul TLS", - Status: diagnose.SkippedStatus, - }, - { - Name: "Check Consul Direct Storage Access", - Status: diagnose.OkStatus, - }, - }, - }, - { - Name: "Check Service Discovery", - Status: diagnose.OkStatus, - Children: []*diagnose.Result{ - { - Name: "Check Consul Service Discovery TLS", - Status: diagnose.SkippedStatus, - }, - { - Name: "Check Consul Direct Service Discovery", - Status: diagnose.OkStatus, - }, - }, - }, - { - Name: "Create Vault Server Configuration Seals", - Status: diagnose.OkStatus, - }, - { - Name: "Create Core Configuration", - Status: diagnose.OkStatus, - Children: []*diagnose.Result{ - { - Name: "Initialize Randomness for Core", - Status: diagnose.OkStatus, - }, - }, - }, - { - Name: "HA Storage", - Status: diagnose.OkStatus, - Children: []*diagnose.Result{ - { - Name: "Create HA Storage Backend", - Status: diagnose.OkStatus, - }, - { - Name: "Check HA Consul Direct Storage Access", - Status: diagnose.OkStatus, - }, - { - Name: "Check Consul TLS", - Status: diagnose.SkippedStatus, - }, - }, - }, - { - Name: "Determine Redirect Address", - Status: diagnose.OkStatus, - }, - { - Name: "Check Cluster Address", - Status: diagnose.OkStatus, - }, - { - Name: "Check Core Creation", - Status: diagnose.OkStatus, - }, - { - Name: "Start Listeners", - Status: diagnose.WarningStatus, - Children: []*diagnose.Result{ - { - Name: "Create Listeners", - Status: diagnose.OkStatus, - }, - { - Name: "Check Listener TLS", - Status: diagnose.WarningStatus, - Warnings: []string{ - "TLS is disabled in a listener config stanza.", - }, - }, - }, - }, - { - Name: "Check Autounseal Encryption", - Status: diagnose.SkippedStatus, - Message: "Skipping barrier encryption", - }, - { - Name: "Check Server Before Runtime", - Status: diagnose.OkStatus, - }, - { - Name: "Finalize Shamir Seal", - Status: diagnose.OkStatus, - }, - }, - }, - { - "diagnose_raft_problems", - []string{ - "-config", "./server/test-fixtures/config_raft.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Storage", - Status: diagnose.WarningStatus, - Children: []*diagnose.Result{ - { - Name: "Create Storage Backend", - Status: diagnose.OkStatus, - }, - { - Name: "Check Raft Folder Permissions", - Status: diagnose.WarningStatus, - Message: "too many permissions", - }, - { - Name: "Check For Raft Quorum", - Status: diagnose.WarningStatus, - Message: "0 voters found", - }, - }, - }, - }, - }, - { - "diagnose_invalid_storage", - []string{ - "-config", "./server/test-fixtures/nostore_config.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Storage", - Status: diagnose.ErrorStatus, - Message: "No storage stanza in Vault server configuration.", - }, - }, - }, - { - "diagnose_listener_config_ok", - []string{ - "-config", "./server/test-fixtures/tls_config_ok.hcl", - }, - []*diagnose.Result{ - { - Name: "Start Listeners", - Status: diagnose.OkStatus, - Children: []*diagnose.Result{ - { - Name: "Create Listeners", - Status: diagnose.OkStatus, - }, - { - Name: "Check Listener TLS", - Status: diagnose.OkStatus, - }, - }, - }, - }, - }, - { - "diagnose_invalid_https_storage", - []string{ - "-config", "./server/test-fixtures/config_bad_https_storage.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Storage", - Status: diagnose.ErrorStatus, - Children: []*diagnose.Result{ - { - Name: "Create Storage Backend", - Status: diagnose.OkStatus, - }, - { - Name: "Check Consul TLS", - Status: diagnose.ErrorStatus, - Message: "certificate has expired or is not yet valid", - Warnings: []string{ - "expired or near expiry", - }, - }, - { - Name: "Check Consul Direct Storage Access", - Status: diagnose.OkStatus, - }, - }, - }, - }, - }, - { - "diagnose_invalid_https_hastorage", - []string{ - "-config", "./server/test-fixtures/config_diagnose_hastorage_bad_https.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Storage", - Status: diagnose.WarningStatus, - Children: []*diagnose.Result{ - { - Name: "Create Storage Backend", - Status: diagnose.OkStatus, - }, - { - Name: "Check Consul TLS", - Status: diagnose.SkippedStatus, - }, - { - Name: "Check Consul Direct Storage Access", - Status: diagnose.WarningStatus, - Advice: "We recommend connecting to a local agent.", - Warnings: []string{ - "Vault storage is directly connected to a Consul server.", - }, - }, - }, - }, - { - Name: "HA Storage", - Status: diagnose.ErrorStatus, - Children: []*diagnose.Result{ - { - Name: "Create HA Storage Backend", - Status: diagnose.OkStatus, - }, - { - Name: "Check HA Consul Direct Storage Access", - Status: diagnose.WarningStatus, - Advice: "We recommend connecting to a local agent.", - Warnings: []string{ - "Vault storage is directly connected to a Consul server.", - }, - }, - { - Name: "Check Consul TLS", - Status: diagnose.ErrorStatus, - Message: "certificate has expired or is not yet valid", - Warnings: []string{ - "expired or near expiry", - }, - }, - }, - }, - { - Name: "Check Cluster Address", - Status: diagnose.ErrorStatus, - }, - }, - }, - { - "diagnose_seal_transit_tls_check_fail", - []string{ - "-config", "./server/test-fixtures/diagnose_seal_transit_tls_check.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Transit Seal TLS", - Status: diagnose.WarningStatus, - Warnings: []string{ - "Found at least one intermediate certificate in the CA certificate file.", - }, - }, - }, - }, - { - "diagnose_invalid_https_sr", - []string{ - "-config", "./server/test-fixtures/diagnose_bad_https_consul_sr.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Service Discovery", - Status: diagnose.ErrorStatus, - Children: []*diagnose.Result{ - { - Name: "Check Consul Service Discovery TLS", - Status: diagnose.ErrorStatus, - Message: "certificate has expired or is not yet valid", - Warnings: []string{ - "expired or near expiry", - }, - }, - { - Name: "Check Consul Direct Service Discovery", - Status: diagnose.WarningStatus, - Warnings: []string{ - diagnose.DirAccessErr, - }, - }, - }, - }, - }, - }, - { - "diagnose_direct_storage_access", - []string{ - "-config", "./server/test-fixtures/diagnose_ok_storage_direct_access.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Storage", - Status: diagnose.WarningStatus, - Children: []*diagnose.Result{ - { - Name: "Create Storage Backend", - Status: diagnose.OkStatus, - }, - { - Name: "Check Consul TLS", - Status: diagnose.SkippedStatus, - }, - { - Name: "Check Consul Direct Storage Access", - Status: diagnose.WarningStatus, - Warnings: []string{ - diagnose.DirAccessErr, - }, - }, - }, - }, - }, - }, - { - "diagnose_raft_no_folder_backend", - []string{ - "-config", "./server/test-fixtures/diagnose_raft_no_bolt_folder.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Storage", - Status: diagnose.ErrorStatus, - Message: "Diagnose could not initialize storage backend.", - Children: []*diagnose.Result{ - { - Name: "Create Storage Backend", - Status: diagnose.ErrorStatus, - Message: "no such file or directory", - }, - }, - }, - }, - }, - { - "diagnose_telemetry_partial_circonus", - []string{ - "-config", "./server/test-fixtures/diagnose_bad_telemetry1.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Telemetry", - Status: diagnose.ErrorStatus, - Message: "incomplete Circonus telemetry configuration, missing circonus_api_url", - }, - }, - }, - { - "diagnose_telemetry_partial_dogstats", - []string{ - "-config", "./server/test-fixtures/diagnose_bad_telemetry2.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Telemetry", - Status: diagnose.ErrorStatus, - Message: "incomplete DogStatsD telemetry configuration, missing dogstatsd_addr, while dogstatsd_tags specified", - }, - }, - }, - { - "diagnose_telemetry_partial_stackdriver", - []string{ - "-config", "./server/test-fixtures/diagnose_bad_telemetry3.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Telemetry", - Status: diagnose.ErrorStatus, - Message: "incomplete Stackdriver telemetry configuration, missing stackdriver_project_id", - }, - }, - }, - { - "diagnose_telemetry_default", - []string{ - "-config", "./server/test-fixtures/config4.hcl", - }, - []*diagnose.Result{ - { - Name: "Check Telemetry", - Status: diagnose.WarningStatus, - Warnings: []string{"Telemetry is using default configuration"}, - }, - }, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - client, closer := testVaultServer(t) - defer closer() - cmd := testOperatorDiagnoseCommand(t) - cmd.client = client - - cmd.Run(tc.args) - result := cmd.diagnose.Finalize(context.Background()) - - if err := compareResults(tc.expected, result.Children); err != nil { - t.Fatalf("Did not find expected test results: %v", err) - } - }) - } - }) -} - -func compareResults(expected []*diagnose.Result, actual []*diagnose.Result) error { - for _, exp := range expected { - found := false - // Check them all so we don't have to be order specific - for _, act := range actual { - fmt.Printf("%+v", act) - if exp.Name == act.Name { - found = true - if err := compareResult(exp, act); err != nil { - return err - } - break - } - } - if !found { - return fmt.Errorf("could not find expected test result: %s", exp.Name) - } - } - return nil -} - -func compareResult(exp *diagnose.Result, act *diagnose.Result) error { - if exp.Name != act.Name { - return fmt.Errorf("names mismatch: %s vs %s", exp.Name, act.Name) - } - if exp.Status != act.Status { - if act.Status != diagnose.OkStatus { - return fmt.Errorf("section %s, status mismatch: %s vs %s, got error %s", exp.Name, exp.Status, act.Status, act.Message) - } - return fmt.Errorf("section %s, status mismatch: %s vs %s", exp.Name, exp.Status, act.Status) - } - if exp.Message != "" && exp.Message != act.Message && !strings.Contains(act.Message, exp.Message) { - return fmt.Errorf("section %s, message not found: %s in %s", exp.Name, exp.Message, act.Message) - } - if exp.Advice != "" && exp.Advice != act.Advice && !strings.Contains(act.Advice, exp.Advice) { - return fmt.Errorf("section %s, advice not found: %s in %s", exp.Name, exp.Advice, act.Advice) - } - if len(exp.Warnings) != len(act.Warnings) { - return fmt.Errorf("section %s, warning count mismatch: %d vs %d", exp.Name, len(exp.Warnings), len(act.Warnings)) - } - for j := range exp.Warnings { - if !strings.Contains(act.Warnings[j], exp.Warnings[j]) { - return fmt.Errorf("section %s, warning message not found: %s in %s", exp.Name, exp.Warnings[j], act.Warnings[j]) - } - } - if len(exp.Children) > len(act.Children) { - errStrings := []string{} - for _, c := range act.Children { - errStrings = append(errStrings, fmt.Sprintf("%+v", c)) - } - return fmt.Errorf(strings.Join(errStrings, ",")) - } - - if len(exp.Children) > 0 { - return compareResults(exp.Children, act.Children) - } - - // Remove raft file if it exists - os.Remove("./server/test-fixtures/vault.db") - os.RemoveAll("./server/test-fixtures/raft") - - return nil -} diff --git a/command/operator_generate_root_test.go b/command/operator_generate_root_test.go deleted file mode 100644 index 4db2262ca..000000000 --- a/command/operator_generate_root_test.go +++ /dev/null @@ -1,561 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -//go:build !race - -package command - -import ( - "encoding/base64" - "io" - "os" - "regexp" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/helper/xor" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/cli" -) - -func testOperatorGenerateRootCommand(tb testing.TB) (*cli.MockUi, *OperatorGenerateRootCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &OperatorGenerateRootCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestOperatorGenerateRootCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "init_invalid_otp", - []string{ - "-init", - "-otp", "not-a-valid-otp", - }, - "OTP string is wrong length", - 2, - }, - { - "init_pgp_multi", - []string{ - "-init", - "-pgp-key", "keybase:hashicorp", - "-pgp-key", "keybase:jefferai", - }, - "can only be specified once", - 1, - }, - { - "init_pgp_multi_inline", - []string{ - "-init", - "-pgp-key", "keybase:hashicorp,keybase:jefferai", - }, - "can only specify one pgp key", - 1, - }, - { - "init_pgp_otp", - []string{ - "-init", - "-pgp-key", "keybase:hashicorp", - "-otp", "abcd1234", - }, - "cannot specify both -otp and -pgp-key", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("%s: expected %d to be %d", tc.name, code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("%s: expected %q to contain %q", tc.name, combined, tc.out) - } - }) - } - }) - - t.Run("generate_otp", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - _, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-generate-otp", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - }) - - t.Run("decode", func(t *testing.T) { - t.Parallel() - - encoded := "Bxg9JQQqOCNKBRICNwMIRzo2J3cWCBRi" - otp := "3JhHkONiyiaNYj14nnD9xZQS" - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - // Simulate piped output to print raw output - old := os.Stdout - _, w, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - os.Stdout = w - - code := cmd.Run([]string{ - "-decode", encoded, - "-otp", otp, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - w.Close() - os.Stdout = old - - expected := "4RUmoevJ3lsLni9sTXcNnRE1" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if combined != expected { - t.Errorf("expected %q to be %q", combined, expected) - } - }) - - t.Run("decode_from_stdin", func(t *testing.T) { - t.Parallel() - - encoded := "Bxg9JQQqOCNKBRICNwMIRzo2J3cWCBRi" - otp := "3JhHkONiyiaNYj14nnD9xZQS" - - client, closer := testVaultServer(t) - defer closer() - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(encoded)) - stdinW.Close() - }() - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - // Simulate piped output to print raw output - old := os.Stdout - _, w, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - os.Stdout = w - - code := cmd.Run([]string{ - "-decode", "-", // read from stdin - "-otp", otp, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - w.Close() - os.Stdout = old - - expected := "4RUmoevJ3lsLni9sTXcNnRE1" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if combined != expected { - t.Errorf("expected %q to be %q", combined, expected) - } - }) - - t.Run("decode_from_stdin_empty", func(t *testing.T) { - t.Parallel() - - encoded := "" - otp := "3JhHkONiyiaNYj14nnD9xZQS" - - client, closer := testVaultServer(t) - defer closer() - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(encoded)) - stdinW.Close() - }() - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - // Simulate piped output to print raw output - old := os.Stdout - _, w, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - os.Stdout = w - - code := cmd.Run([]string{ - "-decode", "-", // read from stdin - "-otp", otp, - }) - if exp := 1; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - w.Close() - os.Stdout = old - - expected := "Missing encoded value" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("cancel", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - // Initialize a generation - if _, err := client.Sys().GenerateRootInit("", ""); err != nil { - t.Fatal(err) - } - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-cancel", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Root token generation canceled" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - status, err := client.Sys().GenerateRootStatus() - if err != nil { - t.Fatal(err) - } - - if status.Started { - t.Errorf("expected status to be canceled: %#v", status) - } - }) - - t.Run("init_otp", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-init", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Nonce" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - status, err := client.Sys().GenerateRootStatus() - if err != nil { - t.Fatal(err) - } - - if !status.Started { - t.Errorf("expected status to be started: %#v", status) - } - }) - - t.Run("init_pgp", func(t *testing.T) { - t.Parallel() - - pgpKey := "keybase:hashicorp" - pgpFingerprint := "c874011f0ab405110d02105534365d9472d7468f" - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-init", - "-pgp-key", pgpKey, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Nonce" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - status, err := client.Sys().GenerateRootStatus() - if err != nil { - t.Fatal(err) - } - - if !status.Started { - t.Errorf("expected status to be started: %#v", status) - } - if status.PGPFingerprint != pgpFingerprint { - t.Errorf("expected %q to be %q", status.PGPFingerprint, pgpFingerprint) - } - }) - - t.Run("status", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-status", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Nonce" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("provide_arg", func(t *testing.T) { - t.Parallel() - - client, keys, closer := testVaultServerUnseal(t) - defer closer() - - // Initialize a generation - status, err := client.Sys().GenerateRootInit("", "") - if err != nil { - t.Fatal(err) - } - nonce := status.Nonce - otp := status.OTP - - // Supply the first n-1 unseal keys - for _, key := range keys[:len(keys)-1] { - _, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-nonce", nonce, - key, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - } - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-nonce", nonce, - keys[len(keys)-1], // the last unseal key - }) - if exp := 0; code != exp { - t.Fatalf("expected %d to be %d, out=%q, err=%q", code, exp, ui.OutputWriter, ui.ErrorWriter) - } - - reToken := regexp.MustCompile(`Encoded Token\s+(.+)`) - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - match := reToken.FindAllStringSubmatch(combined, -1) - if len(match) < 1 || len(match[0]) < 2 { - t.Fatalf("no match: %#v", match) - } - - tokenBytes, err := base64.RawStdEncoding.DecodeString(match[0][1]) - if err != nil { - t.Fatal(err) - } - - token, err := xor.XORBytes(tokenBytes, []byte(otp)) - if err != nil { - t.Fatal(err) - } - - if l, exp := len(token), vault.TokenLength+vault.TokenPrefixLength; l != exp { - t.Errorf("expected %d to be %d: %s", l, exp, token) - } - }) - - t.Run("provide_stdin", func(t *testing.T) { - t.Parallel() - - client, keys, closer := testVaultServerUnseal(t) - defer closer() - - // Initialize a generation - status, err := client.Sys().GenerateRootInit("", "") - if err != nil { - t.Fatal(err) - } - nonce := status.Nonce - otp := status.OTP - - // Supply the first n-1 unseal keys - for _, key := range keys[:len(keys)-1] { - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(key)) - stdinW.Close() - }() - - _, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "-nonce", nonce, - "-", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - } - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(keys[len(keys)-1])) // the last unseal key - stdinW.Close() - }() - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "-nonce", nonce, - "-", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - reToken := regexp.MustCompile(`Encoded Token\s+(.+)`) - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - match := reToken.FindAllStringSubmatch(combined, -1) - if len(match) < 1 || len(match[0]) < 2 { - t.Fatalf("no match: %#v", match) - } - - // encodedOTP := base64.RawStdEncoding.EncodeToString([]byte(otp)) - - // tokenBytes, err := xor.XORBase64(match[0][1], encodedOTP) - // if err != nil { - // t.Fatal(err) - // } - // token, err := uuid.FormatUUID(tokenBytes) - // if err != nil { - // t.Fatal(err) - // } - - tokenBytes, err := base64.RawStdEncoding.DecodeString(match[0][1]) - if err != nil { - t.Fatal(err) - } - - token, err := xor.XORBytes(tokenBytes, []byte(otp)) - if err != nil { - t.Fatal(err) - } - - if l, exp := len(token), vault.TokenLength+vault.TokenPrefixLength; l != exp { - t.Errorf("expected %d to be %d: %s", l, exp, token) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testOperatorGenerateRootCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/foo", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error getting root generation status: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testOperatorGenerateRootCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/operator_init_test.go b/command/operator_init_test.go deleted file mode 100644 index 917686543..000000000 --- a/command/operator_init_test.go +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -//go:build !race - -package command - -import ( - "fmt" - "os" - "regexp" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/cli" -) - -func testOperatorInitCommand(tb testing.TB) (*cli.MockUi, *OperatorInitCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &OperatorInitCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestOperatorInitCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo"}, - "Too many arguments", - 1, - }, - { - "pgp_keys_multi", - []string{ - "-pgp-keys", "keybase:hashicorp", - "-pgp-keys", "keybase:jefferai", - }, - "can only be specified once", - 1, - }, - { - "root_token_pgp_key_multi", - []string{ - "-root-token-pgp-key", "keybase:hashicorp", - "-root-token-pgp-key", "keybase:jefferai", - }, - "can only be specified once", - 1, - }, - { - "root_token_pgp_key_multi_inline", - []string{ - "-root-token-pgp-key", "keybase:hashicorp,keybase:jefferai", - }, - "can only specify one pgp key", - 1, - }, - { - "recovery_pgp_keys_multi", - []string{ - "-recovery-pgp-keys", "keybase:hashicorp", - "-recovery-pgp-keys", "keybase:jefferai", - }, - "can only be specified once", - 1, - }, - { - "key_shares_pgp_less", - []string{ - "-key-shares", "10", - "-pgp-keys", "keybase:jefferai,keybase:sethvargo", - }, - "incorrect number", - 2, - }, - { - "key_shares_pgp_more", - []string{ - "-key-shares", "1", - "-pgp-keys", "keybase:jefferai,keybase:sethvargo", - }, - "incorrect number", - 2, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorInitCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("status", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerUninit(t) - defer closer() - - ui, cmd := testOperatorInitCommand(t) - cmd.client = client - - // Verify the non-init response code - code := cmd.Run([]string{ - "-status", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - // Now init to verify the init response code - if _, err := client.Sys().Init(&api.InitRequest{ - SecretShares: 1, - SecretThreshold: 1, - }); err != nil { - t.Fatal(err) - } - - // Verify the init response code - ui, cmd = testOperatorInitCommand(t) - cmd.client = client - code = cmd.Run([]string{ - "-status", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - }) - - t.Run("default", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerUninit(t) - defer closer() - - ui, cmd := testOperatorInitCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - init, err := client.Sys().InitStatus() - if err != nil { - t.Fatal(err) - } - if !init { - t.Error("expected initialized") - } - - re := regexp.MustCompile(`Unseal Key \d+: (.+)`) - output := ui.OutputWriter.String() - match := re.FindAllStringSubmatch(output, -1) - if len(match) < 5 || len(match[0]) < 2 { - t.Fatalf("no match: %#v", match) - } - - keys := make([]string, len(match)) - for i := range match { - keys[i] = match[i][1] - } - - // Try unsealing with those keys - only use 3, which is the default - // threshold. - for i, key := range keys[:3] { - resp, err := client.Sys().Unseal(key) - if err != nil { - t.Fatal(err) - } - - exp := (i + 1) % 3 // 1, 2, 0 - if resp.Progress != exp { - t.Errorf("expected %d to be %d", resp.Progress, exp) - } - } - - status, err := client.Sys().SealStatus() - if err != nil { - t.Fatal(err) - } - if status.Sealed { - t.Errorf("expected vault to be unsealed: %#v", status) - } - }) - - t.Run("custom_shares_threshold", func(t *testing.T) { - t.Parallel() - - keyShares, keyThreshold := 20, 15 - - client, closer := testVaultServerUninit(t) - defer closer() - - ui, cmd := testOperatorInitCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-key-shares", strconv.Itoa(keyShares), - "-key-threshold", strconv.Itoa(keyThreshold), - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - init, err := client.Sys().InitStatus() - if err != nil { - t.Fatal(err) - } - if !init { - t.Error("expected initialized") - } - - re := regexp.MustCompile(`Unseal Key \d+: (.+)`) - output := ui.OutputWriter.String() - match := re.FindAllStringSubmatch(output, -1) - if len(match) < keyShares || len(match[0]) < 2 { - t.Fatalf("no match: %#v", match) - } - - keys := make([]string, len(match)) - for i := range match { - keys[i] = match[i][1] - } - - // Try unsealing with those keys - only use 3, which is the default - // threshold. - for i, key := range keys[:keyThreshold] { - resp, err := client.Sys().Unseal(key) - if err != nil { - t.Fatal(err) - } - - exp := (i + 1) % keyThreshold - if resp.Progress != exp { - t.Errorf("expected %d to be %d", resp.Progress, exp) - } - } - - status, err := client.Sys().SealStatus() - if err != nil { - t.Fatal(err) - } - if status.Sealed { - t.Errorf("expected vault to be unsealed: %#v", status) - } - }) - - t.Run("pgp", func(t *testing.T) { - t.Parallel() - - tempDir, pubFiles, err := getPubKeyFiles(t) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - client, closer := testVaultServerUninit(t) - defer closer() - - ui, cmd := testOperatorInitCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-key-shares", "4", - "-key-threshold", "2", - "-pgp-keys", fmt.Sprintf("%s,@%s, %s, %s ", - pubFiles[0], pubFiles[1], pubFiles[2], pubFiles[3]), - "-root-token-pgp-key", pubFiles[0], - }) - if exp := 0; code != exp { - t.Fatalf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - re := regexp.MustCompile(`Unseal Key \d+: (.+)`) - output := ui.OutputWriter.String() - match := re.FindAllStringSubmatch(output, -1) - if len(match) < 4 || len(match[0]) < 2 { - t.Fatalf("no match: %#v", match) - } - - keys := make([]string, len(match)) - for i := range match { - keys[i] = match[i][1] - } - - // Try unsealing with one key - decryptedKey := testPGPDecrypt(t, pgpkeys.TestPrivKey1, keys[0]) - if _, err := client.Sys().Unseal(decryptedKey); err != nil { - t.Fatal(err) - } - - // Decrypt the root token - reToken := regexp.MustCompile(`Root Token: (.+)`) - match = reToken.FindAllStringSubmatch(output, -1) - if len(match) < 1 || len(match[0]) < 2 { - t.Fatalf("no match") - } - root := match[0][1] - decryptedRoot := testPGPDecrypt(t, pgpkeys.TestPrivKey1, root) - - if l, exp := len(decryptedRoot), vault.TokenLength+vault.TokenPrefixLength; l != exp { - t.Errorf("expected %d to be %d", l, exp) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testOperatorInitCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-key-shares=1", - "-key-threshold=1", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error making API request" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testOperatorInitCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/operator_key_status_test.go b/command/operator_key_status_test.go deleted file mode 100644 index db92eb9e9..000000000 --- a/command/operator_key_status_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testOperatorKeyStatusCommand(tb testing.TB) (*cli.MockUi, *OperatorKeyStatusCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &OperatorKeyStatusCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestOperatorKeyStatusCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorKeyStatusCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorKeyStatusCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Key Term" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testOperatorKeyStatusCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error reading key status: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testOperatorKeyStatusCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/operator_migrate_test.go b/command/operator_migrate_test.go deleted file mode 100644 index 9a6c27196..000000000 --- a/command/operator_migrate_test.go +++ /dev/null @@ -1,412 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "context" - "fmt" - "math/rand" - "os" - "path/filepath" - "reflect" - "sort" - "strings" - "sync" - "testing" - "time" - - "github.com/go-test/deep" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-secure-stdlib/base62" - "github.com/hashicorp/vault/command/server" - "github.com/hashicorp/vault/sdk/physical" - "github.com/hashicorp/vault/vault" -) - -const trailing_slash_key = "trailing_slash/" - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func TestMigration(t *testing.T) { - t.Run("Default", func(t *testing.T) { - data := generateData() - - fromFactory := physicalBackends["file"] - - folder := t.TempDir() - - confFrom := map[string]string{ - "path": folder, - } - - from, err := fromFactory(confFrom, nil) - if err != nil { - t.Fatal(err) - } - if err := storeData(from, data); err != nil { - t.Fatal(err) - } - - toFactory := physicalBackends["inmem"] - confTo := map[string]string{} - to, err := toFactory(confTo, nil) - if err != nil { - t.Fatal(err) - } - cmd := OperatorMigrateCommand{ - logger: log.NewNullLogger(), - } - if err := cmd.migrateAll(context.Background(), from, to, 1); err != nil { - t.Fatal(err) - } - - if err := compareStoredData(to, data, ""); err != nil { - t.Fatal(err) - } - }) - - t.Run("Concurrent migration", func(t *testing.T) { - data := generateData() - - fromFactory := physicalBackends["file"] - - folder := t.TempDir() - - confFrom := map[string]string{ - "path": folder, - } - - from, err := fromFactory(confFrom, nil) - if err != nil { - t.Fatal(err) - } - if err := storeData(from, data); err != nil { - t.Fatal(err) - } - - toFactory := physicalBackends["inmem"] - confTo := map[string]string{} - to, err := toFactory(confTo, nil) - if err != nil { - t.Fatal(err) - } - - cmd := OperatorMigrateCommand{ - logger: log.NewNullLogger(), - } - - if err := cmd.migrateAll(context.Background(), from, to, 10); err != nil { - t.Fatal(err) - } - if err := compareStoredData(to, data, ""); err != nil { - t.Fatal(err) - } - }) - - t.Run("Start option", func(t *testing.T) { - data := generateData() - - fromFactory := physicalBackends["inmem"] - confFrom := map[string]string{} - from, err := fromFactory(confFrom, nil) - if err != nil { - t.Fatal(err) - } - if err := storeData(from, data); err != nil { - t.Fatal(err) - } - - toFactory := physicalBackends["file"] - folder := t.TempDir() - confTo := map[string]string{ - "path": folder, - } - - to, err := toFactory(confTo, nil) - if err != nil { - t.Fatal(err) - } - - const start = "m" - - cmd := OperatorMigrateCommand{ - logger: log.NewNullLogger(), - flagStart: start, - } - if err := cmd.migrateAll(context.Background(), from, to, 1); err != nil { - t.Fatal(err) - } - - if err := compareStoredData(to, data, start); err != nil { - t.Fatal(err) - } - }) - - t.Run("Start option (parallel)", func(t *testing.T) { - data := generateData() - - fromFactory := physicalBackends["inmem"] - confFrom := map[string]string{} - from, err := fromFactory(confFrom, nil) - if err != nil { - t.Fatal(err) - } - if err := storeData(from, data); err != nil { - t.Fatal(err) - } - - toFactory := physicalBackends["file"] - folder := t.TempDir() - confTo := map[string]string{ - "path": folder, - } - - to, err := toFactory(confTo, nil) - if err != nil { - t.Fatal(err) - } - - const start = "m" - - cmd := OperatorMigrateCommand{ - logger: log.NewNullLogger(), - flagStart: start, - } - if err := cmd.migrateAll(context.Background(), from, to, 10); err != nil { - t.Fatal(err) - } - - if err := compareStoredData(to, data, start); err != nil { - t.Fatal(err) - } - }) - - t.Run("Config parsing", func(t *testing.T) { - cmd := new(OperatorMigrateCommand) - cfgName := filepath.Join(t.TempDir(), "migrator") - os.WriteFile(cfgName, []byte(` -storage_source "src_type" { - path = "src_path" -} - -storage_destination "dest_type" { - path = "dest_path" -}`), 0o644) - - expCfg := &migratorConfig{ - StorageSource: &server.Storage{ - Type: "src_type", - Config: map[string]string{ - "path": "src_path", - }, - }, - StorageDestination: &server.Storage{ - Type: "dest_type", - Config: map[string]string{ - "path": "dest_path", - }, - }, - } - cfg, err := cmd.loadMigratorConfig(cfgName) - if err != nil { - t.Fatal(cfg) - } - if diff := deep.Equal(cfg, expCfg); diff != nil { - t.Fatal(diff) - } - - verifyBad := func(cfg string) { - os.WriteFile(cfgName, []byte(cfg), 0o644) - _, err := cmd.loadMigratorConfig(cfgName) - if err == nil { - t.Fatalf("expected error but none received from: %v", cfg) - } - } - - // missing source - verifyBad(` -storage_destination "dest_type" { - path = "dest_path" -}`) - - // missing destination - verifyBad(` -storage_source "src_type" { - path = "src_path" -}`) - - // duplicate source - verifyBad(` -storage_source "src_type" { - path = "src_path" -} - -storage_source "src_type2" { - path = "src_path" -} - -storage_destination "dest_type" { - path = "dest_path" -}`) - - // duplicate destination - verifyBad(` -storage_source "src_type" { - path = "src_path" -} - -storage_destination "dest_type" { - path = "dest_path" -} - -storage_destination "dest_type2" { - path = "dest_path" -}`) - }) - - t.Run("DFS Scan", func(t *testing.T) { - s, _ := physicalBackends["inmem"](map[string]string{}, nil) - - data := generateData() - data["cc"] = []byte{} - data["c/d/e/f"] = []byte{} - data["c/d/e/g"] = []byte{} - data["c"] = []byte{} - storeData(s, data) - - l := randomLister{s} - - type SafeAppend struct { - out []string - lock sync.Mutex - } - outKeys := SafeAppend{} - dfsScan(context.Background(), l, 10, func(ctx context.Context, path string) error { - outKeys.lock.Lock() - defer outKeys.lock.Unlock() - - outKeys.out = append(outKeys.out, path) - return nil - }) - - delete(data, trailing_slash_key) - delete(data, "") - - var keys []string - for key := range data { - keys = append(keys, key) - } - sort.Strings(keys) - outKeys.lock.Lock() - sort.Strings(outKeys.out) - outKeys.lock.Unlock() - if !reflect.DeepEqual(keys, outKeys.out) { - t.Fatalf("expected equal: %v, %v", keys, outKeys.out) - } - }) -} - -// randomLister wraps a physical backend, providing a List method -// that returns results in a random order. -type randomLister struct { - b physical.Backend -} - -func (l randomLister) List(ctx context.Context, path string) ([]string, error) { - result, err := l.b.List(ctx, path) - if err != nil { - return nil, err - } - rand.Shuffle(len(result), func(i, j int) { - result[i], result[j] = result[j], result[i] - }) - return result, err -} - -func (l randomLister) Get(ctx context.Context, path string) (*physical.Entry, error) { - return l.b.Get(ctx, path) -} - -func (l randomLister) Put(ctx context.Context, entry *physical.Entry) error { - return l.b.Put(ctx, entry) -} - -func (l randomLister) Delete(ctx context.Context, path string) error { - return l.b.Delete(ctx, path) -} - -// generateData creates a map of 500 random keys and values -func generateData() map[string][]byte { - result := make(map[string][]byte) - for i := 0; i < 500; i++ { - segments := make([]string, rand.Intn(8)+1) - for j := 0; j < len(segments); j++ { - s, _ := base62.Random(6) - segments[j] = s - } - data := make([]byte, 100) - rand.Read(data) - result[strings.Join(segments, "/")] = data - } - - // Add special keys that should be excluded from migration - result[storageMigrationLock] = []byte{} - result[vault.CoreLockPath] = []byte{} - - // Empty keys are now prevented in Vault, but older data sets - // might contain them. - result[""] = []byte{} - result[trailing_slash_key] = []byte{} - - return result -} - -func storeData(s physical.Backend, ref map[string][]byte) error { - for k, v := range ref { - entry := physical.Entry{ - Key: k, - Value: v, - } - - err := s.Put(context.Background(), &entry) - if err != nil { - return err - } - } - return nil -} - -func compareStoredData(s physical.Backend, ref map[string][]byte, start string) error { - for k, v := range ref { - entry, err := s.Get(context.Background(), k) - if err != nil { - return err - } - - if k == storageMigrationLock || k == vault.CoreLockPath || k == "" || strings.HasSuffix(k, "/") { - if entry == nil { - continue - } - return fmt.Errorf("key found that should have been excluded: %s", k) - } - - if k >= start { - if entry == nil { - return fmt.Errorf("key not found: %s", k) - } - if !bytes.Equal(v, entry.Value) { - return fmt.Errorf("values differ for key: %s", k) - } - } else { - if entry != nil { - return fmt.Errorf("found key the should have been skipped by start option: %s", k) - } - } - } - - return nil -} diff --git a/command/operator_raft_snapshot_inspect_test.go b/command/operator_raft_snapshot_inspect_test.go deleted file mode 100644 index de306595e..000000000 --- a/command/operator_raft_snapshot_inspect_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "context" - "fmt" - "os" - "strings" - "testing" - - "github.com/hashicorp/vault/physical/raft" - "github.com/hashicorp/vault/sdk/physical" - "github.com/mitchellh/cli" -) - -func testOperatorRaftSnapshotInspectCommand(tb testing.TB) (*cli.MockUi, *OperatorRaftSnapshotInspectCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &OperatorRaftSnapshotInspectCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func createSnapshot(tb testing.TB) (*os.File, func(), error) { - // Create new raft backend - r, raftDir := raft.GetRaft(tb, true, false) - defer os.RemoveAll(raftDir) - - // Write some data - for i := 0; i < 100; i++ { - err := r.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - return nil, nil, fmt.Errorf("Error adding data to snapshot %s", err) - } - } - - // Create temporary file to save snapshot to - snap, err := os.CreateTemp("", "temp_snapshot.snap") - if err != nil { - return nil, nil, fmt.Errorf("Error creating temporary file %s", err) - } - - cleanup := func() { - err := os.RemoveAll(snap.Name()) - if err != nil { - tb.Errorf("Error deleting temporary snapshot %s", err) - } - } - - // Save snapshot - err = r.Snapshot(snap, nil) - if err != nil { - return nil, nil, fmt.Errorf("Error saving raft snapshot %s", err) - } - - return snap, cleanup, nil -} - -func TestOperatorRaftSnapshotInspectCommand_Run(t *testing.T) { - t.Parallel() - - file1, cleanup1, err := createSnapshot(t) - if err != nil { - t.Fatalf("Error creating snapshot %s", err) - } - - file2, cleanup2, err := createSnapshot(t) - if err != nil { - t.Fatalf("Error creating snapshot %s", err) - } - - cases := []struct { - name string - args []string - out string - code int - cleanup func() - }{ - { - "too_many_args", - []string{"test.snap", "test"}, - "Too many arguments", - 1, - nil, - }, - { - "default", - []string{file1.Name()}, - "ID bolt-snapshot", - 0, - cleanup1, - }, - { - "all_flags", - []string{"-details", "-depth", "10", "-filter", "key", file2.Name()}, - "Key Name", - 0, - cleanup2, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorRaftSnapshotInspectCommand(t) - - cmd.client = client - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - - if tc.cleanup != nil { - tc.cleanup() - } - }) - } - }) -} diff --git a/command/operator_rekey_test.go b/command/operator_rekey_test.go deleted file mode 100644 index f06ac5613..000000000 --- a/command/operator_rekey_test.go +++ /dev/null @@ -1,687 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -//go:build !race - -package command - -import ( - "io" - "reflect" - "regexp" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/helper/roottoken" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testOperatorRekeyCommand(tb testing.TB) (*cli.MockUi, *OperatorRekeyCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &OperatorRekeyCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestOperatorRekeyCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "pgp_keys_multi", - []string{ - "-init", - "-pgp-keys", "keybase:hashicorp", - "-pgp-keys", "keybase:jefferai", - }, - "can only be specified once", - 1, - }, - { - "key_shares_pgp_less", - []string{ - "-init", - "-key-shares", "10", - "-pgp-keys", "keybase:jefferai,keybase:sethvargo", - }, - "incorrect number", - 2, - }, - { - "key_shares_pgp_more", - []string{ - "-init", - "-key-shares", "1", - "-pgp-keys", "keybase:jefferai,keybase:sethvargo", - }, - "incorrect number", - 2, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("status", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - // Verify the non-init response - code := cmd.Run([]string{ - "-status", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - expected := "Nonce" - combined := ui.OutputWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - // Now init to verify the init response - if _, err := client.Sys().RekeyInit(&api.RekeyInitRequest{ - SecretShares: 1, - SecretThreshold: 1, - }); err != nil { - t.Fatal(err) - } - - // Verify the init response - ui, cmd = testOperatorRekeyCommand(t) - cmd.client = client - code = cmd.Run([]string{ - "-status", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - expected = "Progress" - combined = ui.OutputWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("cancel", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - // Initialize a rekey - if _, err := client.Sys().RekeyInit(&api.RekeyInitRequest{ - SecretShares: 1, - SecretThreshold: 1, - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-cancel", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Canceled rekeying" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - status, err := client.Sys().GenerateRootStatus() - if err != nil { - t.Fatal(err) - } - - if status.Started { - t.Errorf("expected status to be canceled: %#v", status) - } - }) - - t.Run("init", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-init", - "-key-shares", "1", - "-key-threshold", "1", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - expected := "Nonce" - combined := ui.OutputWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - status, err := client.Sys().RekeyStatus() - if err != nil { - t.Fatal(err) - } - if !status.Started { - t.Errorf("expected status to be started: %#v", status) - } - }) - - t.Run("init_pgp", func(t *testing.T) { - t.Parallel() - - pgpKey := "keybase:hashicorp" - pgpFingerprints := []string{"c874011f0ab405110d02105534365d9472d7468f"} - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-init", - "-key-shares", "1", - "-key-threshold", "1", - "-pgp-keys", pgpKey, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - expected := "Nonce" - combined := ui.OutputWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - status, err := client.Sys().RekeyStatus() - if err != nil { - t.Fatal(err) - } - if !status.Started { - t.Errorf("expected status to be started: %#v", status) - } - if !reflect.DeepEqual(status.PGPFingerprints, pgpFingerprints) { - t.Errorf("expected %#v to be %#v", status.PGPFingerprints, pgpFingerprints) - } - }) - - t.Run("provide_arg_recovery_keys", func(t *testing.T) { - t.Parallel() - - client, keys, closer := testVaultServerAutoUnseal(t) - defer closer() - - // Initialize a rekey - status, err := client.Sys().RekeyRecoveryKeyInit(&api.RekeyInitRequest{ - SecretShares: 1, - SecretThreshold: 1, - }) - if err != nil { - t.Fatal(err) - } - nonce := status.Nonce - - // Supply the first n-1 recovery keys - for _, key := range keys[:len(keys)-1] { - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-nonce", nonce, - "-target", "recovery", - key, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - } - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-nonce", nonce, - "-target", "recovery", - keys[len(keys)-1], // the last recovery key - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - re := regexp.MustCompile(`Key 1: (.+)`) - output := ui.OutputWriter.String() - match := re.FindAllStringSubmatch(output, -1) - if len(match) < 1 || len(match[0]) < 2 { - t.Fatalf("bad match: %#v", match) - } - recoveryKey := match[0][1] - - if strings.Contains(strings.ToLower(output), "unseal key") { - t.Fatalf(`output %s shouldn't contain "unseal key"`, output) - } - - // verify that we can perform operations with the recovery key - // below we generate a root token using the recovery key - rootStatus, err := client.Sys().GenerateRootStatus() - if err != nil { - t.Fatal(err) - } - otp, err := roottoken.GenerateOTP(rootStatus.OTPLength) - if err != nil { - t.Fatal(err) - } - genRoot, err := client.Sys().GenerateRootInit(otp, "") - if err != nil { - t.Fatal(err) - } - r, err := client.Sys().GenerateRootUpdate(recoveryKey, genRoot.Nonce) - if err != nil { - t.Fatal(err) - } - if !r.Complete { - t.Fatal("expected root update to be complete") - } - }) - t.Run("provide_arg", func(t *testing.T) { - t.Parallel() - - client, keys, closer := testVaultServerUnseal(t) - defer closer() - - // Initialize a rekey - status, err := client.Sys().RekeyInit(&api.RekeyInitRequest{ - SecretShares: 1, - SecretThreshold: 1, - }) - if err != nil { - t.Fatal(err) - } - nonce := status.Nonce - - // Supply the first n-1 unseal keys - for _, key := range keys[:len(keys)-1] { - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-nonce", nonce, - key, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - } - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-nonce", nonce, - keys[len(keys)-1], // the last unseal key - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - re := regexp.MustCompile(`Key 1: (.+)`) - output := ui.OutputWriter.String() - match := re.FindAllStringSubmatch(output, -1) - if len(match) < 1 || len(match[0]) < 2 { - t.Fatalf("bad match: %#v", match) - } - - // Grab the unseal key and try to unseal - unsealKey := match[0][1] - if err := client.Sys().Seal(); err != nil { - t.Fatal(err) - } - sealStatus, err := client.Sys().Unseal(unsealKey) - if err != nil { - t.Fatal(err) - } - if sealStatus.Sealed { - t.Errorf("expected vault to be unsealed: %#v", sealStatus) - } - }) - - t.Run("provide_stdin", func(t *testing.T) { - t.Parallel() - - client, keys, closer := testVaultServerUnseal(t) - defer closer() - - // Initialize a rekey - status, err := client.Sys().RekeyInit(&api.RekeyInitRequest{ - SecretShares: 1, - SecretThreshold: 1, - }) - if err != nil { - t.Fatal(err) - } - nonce := status.Nonce - - // Supply the first n-1 unseal keys - for _, key := range keys[:len(keys)-1] { - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(key)) - stdinW.Close() - }() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "-nonce", nonce, - "-", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - } - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(keys[len(keys)-1])) // the last unseal key - stdinW.Close() - }() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "-nonce", nonce, - "-", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - re := regexp.MustCompile(`Key 1: (.+)`) - output := ui.OutputWriter.String() - match := re.FindAllStringSubmatch(output, -1) - if len(match) < 1 || len(match[0]) < 2 { - t.Fatalf("bad match: %#v", match) - } - - // Grab the unseal key and try to unseal - unsealKey := match[0][1] - if err := client.Sys().Seal(); err != nil { - t.Fatal(err) - } - sealStatus, err := client.Sys().Unseal(unsealKey) - if err != nil { - t.Fatal(err) - } - if sealStatus.Sealed { - t.Errorf("expected vault to be unsealed: %#v", sealStatus) - } - }) - - t.Run("provide_stdin_recovery_keys", func(t *testing.T) { - t.Parallel() - - client, keys, closer := testVaultServerAutoUnseal(t) - defer closer() - - // Initialize a rekey - status, err := client.Sys().RekeyRecoveryKeyInit(&api.RekeyInitRequest{ - SecretShares: 1, - SecretThreshold: 1, - }) - if err != nil { - t.Fatal(err) - } - nonce := status.Nonce - for _, key := range keys[:len(keys)-1] { - stdinR, stdinW := io.Pipe() - go func() { - _, _ = stdinW.Write([]byte(key)) - _ = stdinW.Close() - }() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "-target", "recovery", - "-nonce", nonce, - "-", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - } - - stdinR, stdinW := io.Pipe() - go func() { - _, _ = stdinW.Write([]byte(keys[len(keys)-1])) // the last recovery key - _ = stdinW.Close() - }() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "-nonce", nonce, - "-target", "recovery", - "-", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - re := regexp.MustCompile(`Key 1: (.+)`) - output := ui.OutputWriter.String() - match := re.FindAllStringSubmatch(output, -1) - if len(match) < 1 || len(match[0]) < 2 { - t.Fatalf("bad match: %#v", match) - } - recoveryKey := match[0][1] - - if strings.Contains(strings.ToLower(output), "unseal key") { - t.Fatalf(`output %s shouldn't contain "unseal key"`, output) - } - // verify that we can perform operations with the recovery key - // below we generate a root token using the recovery key - rootStatus, err := client.Sys().GenerateRootStatus() - if err != nil { - t.Fatal(err) - } - otp, err := roottoken.GenerateOTP(rootStatus.OTPLength) - if err != nil { - t.Fatal(err) - } - genRoot, err := client.Sys().GenerateRootInit(otp, "") - if err != nil { - t.Fatal(err) - } - r, err := client.Sys().GenerateRootUpdate(recoveryKey, genRoot.Nonce) - if err != nil { - t.Fatal(err) - } - if !r.Complete { - t.Fatal("expected root update to be complete") - } - }) - t.Run("backup", func(t *testing.T) { - t.Parallel() - - pgpKey := "keybase:hashicorp" - // pgpFingerprints := []string{"c874011f0ab405110d02105534365d9472d7468f"} - - client, keys, closer := testVaultServerUnseal(t) - defer closer() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-init", - "-key-shares", "1", - "-key-threshold", "1", - "-pgp-keys", pgpKey, - "-backup", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - // Get the status for the nonce - status, err := client.Sys().RekeyStatus() - if err != nil { - t.Fatal(err) - } - nonce := status.Nonce - - var combined string - // Supply the unseal keys - for _, key := range keys { - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-nonce", nonce, - key, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - // Append to our output string - combined += ui.OutputWriter.String() - } - - re := regexp.MustCompile(`Key 1 fingerprint: (.+); value: (.+)`) - match := re.FindAllStringSubmatch(combined, -1) - if len(match) < 1 || len(match[0]) < 3 { - t.Fatalf("bad match: %#v", match) - } - - // Grab the output fingerprint and encrypted key - fingerprint, encryptedKey := match[0][1], match[0][2] - - // Get the backup - ui, cmd = testOperatorRekeyCommand(t) - cmd.client = client - - code = cmd.Run([]string{ - "-backup-retrieve", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - output := ui.OutputWriter.String() - if !strings.Contains(output, fingerprint) { - t.Errorf("expected %q to contain %q", output, fingerprint) - } - if !strings.Contains(output, encryptedKey) { - t.Errorf("expected %q to contain %q", output, encryptedKey) - } - - // Delete the backup - ui, cmd = testOperatorRekeyCommand(t) - cmd.client = client - - code = cmd.Run([]string{ - "-backup-delete", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - - secret, err := client.Sys().RekeyRetrieveBackup() - if err == nil { - t.Errorf("expected error: %#v", secret) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testOperatorRekeyCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/foo", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error getting rekey status: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testOperatorRekeyCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/operator_seal_test.go b/command/operator_seal_test.go deleted file mode 100644 index 7a8e5c35e..000000000 --- a/command/operator_seal_test.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testOperatorSealCommand(tb testing.TB) (*cli.MockUi, *OperatorSealCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &OperatorSealCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestOperatorSealCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "args", - []string{"foo"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorSealCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorSealCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Vault is sealed." - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - sealStatus, err := client.Sys().SealStatus() - if err != nil { - t.Fatal(err) - } - if !sealStatus.Sealed { - t.Errorf("expected to be sealed") - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testOperatorSealCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error sealing: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testOperatorSealCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/operator_step_down_test.go b/command/operator_step_down_test.go deleted file mode 100644 index 376f4ef2e..000000000 --- a/command/operator_step_down_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testOperatorStepDownCommand(tb testing.TB) (*cli.MockUi, *OperatorStepDownCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &OperatorStepDownCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestOperatorStepDownCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo"}, - "Too many arguments", - 1, - }, - { - "default", - nil, - "Success! Stepped down: ", - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorStepDownCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testOperatorStepDownCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error stepping down: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testOperatorStepDownCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/operator_unseal_test.go b/command/operator_unseal_test.go deleted file mode 100644 index 8e45978c0..000000000 --- a/command/operator_unseal_test.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testOperatorUnsealCommand(tb testing.TB) (*cli.MockUi, *OperatorUnsealCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &OperatorUnsealCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestOperatorUnsealCommand_Run(t *testing.T) { - t.Parallel() - - t.Run("error_non_terminal", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorUnsealCommand(t) - cmd.client = client - cmd.testOutput = ioutil.Discard - - code := cmd.Run(nil) - if exp := 1; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "is not a terminal" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("reset", func(t *testing.T) { - t.Parallel() - - client, keys, closer := testVaultServerUnseal(t) - defer closer() - - // Seal so we can unseal - if err := client.Sys().Seal(); err != nil { - t.Fatal(err) - } - - // Enter an unseal key - if _, err := client.Sys().Unseal(keys[0]); err != nil { - t.Fatal(err) - } - - ui, cmd := testOperatorUnsealCommand(t) - cmd.client = client - cmd.testOutput = ioutil.Discard - - // Reset and check output - code := cmd.Run([]string{ - "-reset", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - expected := "0/3" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("full", func(t *testing.T) { - t.Parallel() - - client, keys, closer := testVaultServerUnseal(t) - defer closer() - - // Seal so we can unseal - if err := client.Sys().Seal(); err != nil { - t.Fatal(err) - } - - for _, key := range keys { - ui, cmd := testOperatorUnsealCommand(t) - cmd.client = client - cmd.testOutput = ioutil.Discard - - // Reset and check output - code := cmd.Run([]string{ - key, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) - } - } - - status, err := client.Sys().SealStatus() - if err != nil { - t.Fatal(err) - } - if status.Sealed { - t.Error("expected unsealed") - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testOperatorUnsealCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "abcd", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error unsealing: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testOperatorUnsealCommand(t) - assertNoTabs(t, cmd) - }) -} - -func TestOperatorUnsealCommand_Format(t *testing.T) { - defer func() { - os.Setenv(EnvVaultCLINoColor, "") - }() - - client, keys, closer := testVaultServerUnseal(t) - defer closer() - - // Seal so we can unseal - if err := client.Sys().Seal(); err != nil { - t.Fatal(err) - } - - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - runOpts := &RunOptions{ - Stdout: stdout, - Stderr: stderr, - Client: client, - } - - args, format, _, _, _ := setupEnv([]string{"operator", "unseal", "-format", "json"}) - if format != "json" { - t.Fatalf("expected %q, got %q", "json", format) - } - - // Unseal with one key - code := RunCustom(append(args, []string{ - keys[0], - }...), runOpts) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, stderr.String()) - } - - if !json.Valid(stdout.Bytes()) { - t.Error("expected output to be valid JSON") - } -} diff --git a/command/patch_test.go b/command/patch_test.go deleted file mode 100644 index a8476722e..000000000 --- a/command/patch_test.go +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "io" - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testPatchCommand(tb testing.TB) (*cli.MockUi, *PatchCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PatchCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPatchCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "empty_kvs", - []string{"secret/write/foo"}, - "Must supply data or use -force", - 1, - }, - { - "force_kvs", - []string{"-force", "pki/roles/example"}, - "allow_localhost", - 0, - }, - { - "force_f_kvs", - []string{"-f", "pki/roles/example"}, - "allow_localhost", - 0, - }, - { - "kvs_no_value", - []string{"pki/roles/example", "foo"}, - "Failed to parse K=V data", - 1, - }, - { - "single_value", - []string{"pki/roles/example", "allow_localhost=true"}, - "allow_localhost", - 0, - }, - { - "multi_value", - []string{"pki/roles/example", "allow_localhost=true", "allowed_domains=true"}, - "allow_localhost", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if _, err := client.Logical().Write("pki/roles/example", nil); err != nil { - t.Fatalf("failed to prime role: %v", err) - } - - if _, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X1", - }); err != nil { - t.Fatalf("failed to prime CA: %v", err) - } - - ui, cmd := testPatchCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("stdin_full", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if _, err := client.Logical().Write("pki/roles/example", nil); err != nil { - t.Fatalf("failed to prime role: %v", err) - } - - if _, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X1", - }); err != nil { - t.Fatalf("failed to prime CA: %v", err) - } - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(`{"allow_localhost":"false","allow_wildcard_certificates":"false"}`)) - stdinW.Close() - }() - - ui, cmd := testPatchCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "pki/roles/example", "-", - }) - if code != 0 { - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - t.Fatalf("expected retcode=%d to be 0\nOutput:\n%v", code, combined) - } - - secret, err := client.Logical().Read("pki/roles/example") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - if exp, act := false, secret.Data["allow_localhost"].(bool); exp != act { - t.Errorf("expected allowed_localhost=%v to be %v", act, exp) - } - if exp, act := false, secret.Data["allow_wildcard_certificates"].(bool); exp != act { - t.Errorf("expected allow_wildcard_certificates=%v to be %v", act, exp) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPatchCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "foo/bar", "a=b", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error writing data to foo/bar: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPatchCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/path_help_test.go b/command/path_help_test.go deleted file mode 100644 index 0bce2c788..000000000 --- a/command/path_help_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testPathHelpCommand(tb testing.TB) (*cli.MockUi, *PathHelpCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PathHelpCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPathHelpCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - { - "not_found", - []string{"nope/not/once/never"}, - "", - 2, - }, - { - "kv", - []string{"secret/"}, - "The kv backend", - 0, - }, - { - "sys", - []string{"sys/mounts"}, - "currently mounted backends", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPathHelpCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPathHelpCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "sys/mounts", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error retrieving help: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPathHelpCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/pgp_test.go b/command/pgp_test.go deleted file mode 100644 index 2211cbed5..000000000 --- a/command/pgp_test.go +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "encoding/base64" - "encoding/hex" - "io/ioutil" - "reflect" - "regexp" - "sort" - "testing" - - "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/vault" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" -) - -func getPubKeyFiles(t *testing.T) (string, []string, error) { - tempDir, err := ioutil.TempDir("", "vault-test") - if err != nil { - t.Fatalf("Error creating temporary directory: %s", err) - } - - pubFiles := []string{ - tempDir + "/pubkey1", - tempDir + "/pubkey2", - tempDir + "/pubkey3", - tempDir + "/aapubkey1", - } - decoder := base64.StdEncoding - pub1Bytes, err := decoder.DecodeString(pgpkeys.TestPubKey1) - if err != nil { - t.Fatalf("Error decoding bytes for public key 1: %s", err) - } - err = ioutil.WriteFile(pubFiles[0], pub1Bytes, 0o755) - if err != nil { - t.Fatalf("Error writing pub key 1 to temp file: %s", err) - } - pub2Bytes, err := decoder.DecodeString(pgpkeys.TestPubKey2) - if err != nil { - t.Fatalf("Error decoding bytes for public key 2: %s", err) - } - err = ioutil.WriteFile(pubFiles[1], pub2Bytes, 0o755) - if err != nil { - t.Fatalf("Error writing pub key 2 to temp file: %s", err) - } - pub3Bytes, err := decoder.DecodeString(pgpkeys.TestPubKey3) - if err != nil { - t.Fatalf("Error decoding bytes for public key 3: %s", err) - } - err = ioutil.WriteFile(pubFiles[2], pub3Bytes, 0o755) - if err != nil { - t.Fatalf("Error writing pub key 3 to temp file: %s", err) - } - err = ioutil.WriteFile(pubFiles[3], []byte(pgpkeys.TestAAPubKey1), 0o755) - if err != nil { - t.Fatalf("Error writing aa pub key 1 to temp file: %s", err) - } - - return tempDir, pubFiles, nil -} - -func testPGPDecrypt(tb testing.TB, privKey, enc string) string { - tb.Helper() - - privKeyBytes, err := base64.StdEncoding.DecodeString(privKey) - if err != nil { - tb.Fatal(err) - } - - ptBuf := bytes.NewBuffer(nil) - entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes))) - if err != nil { - tb.Fatal(err) - } - - var rootBytes []byte - rootBytes, err = base64.StdEncoding.DecodeString(enc) - if err != nil { - tb.Fatal(err) - } - - entityList := &openpgp.EntityList{entity} - md, err := openpgp.ReadMessage(bytes.NewBuffer(rootBytes), entityList, nil, nil) - if err != nil { - tb.Fatal(err) - } - ptBuf.ReadFrom(md.UnverifiedBody) - return ptBuf.String() -} - -func parseDecryptAndTestUnsealKeys(t *testing.T, - input, rootToken string, - fingerprints bool, - backupKeys map[string][]string, - backupKeysB64 map[string][]string, - core *vault.Core, -) { - decoder := base64.StdEncoding - priv1Bytes, err := decoder.DecodeString(pgpkeys.TestPrivKey1) - if err != nil { - t.Fatalf("Error decoding bytes for private key 1: %s", err) - } - priv2Bytes, err := decoder.DecodeString(pgpkeys.TestPrivKey2) - if err != nil { - t.Fatalf("Error decoding bytes for private key 2: %s", err) - } - priv3Bytes, err := decoder.DecodeString(pgpkeys.TestPrivKey3) - if err != nil { - t.Fatalf("Error decoding bytes for private key 3: %s", err) - } - - privBytes := [][]byte{ - priv1Bytes, - priv2Bytes, - priv3Bytes, - } - - testFunc := func(bkeys map[string][]string) { - var re *regexp.Regexp - if fingerprints { - re, err = regexp.Compile(`\s*Key\s+\d+\s+fingerprint:\s+([0-9a-fA-F]+);\s+value:\s+(.*)`) - } else { - re, err = regexp.Compile(`\s*Key\s+\d+:\s+(.*)`) - } - if err != nil { - t.Fatalf("Error compiling regex: %s", err) - } - matches := re.FindAllStringSubmatch(input, -1) - if len(matches) != 4 { - t.Fatalf("Unexpected number of keys returned, got %d, matches was \n\n%#v\n\n, input was \n\n%s\n\n", len(matches), matches, input) - } - - encodedKeys := []string{} - matchedFingerprints := []string{} - for _, tuple := range matches { - if fingerprints { - if len(tuple) != 3 { - t.Fatalf("Key not found: %#v", tuple) - } - matchedFingerprints = append(matchedFingerprints, tuple[1]) - encodedKeys = append(encodedKeys, tuple[2]) - } else { - if len(tuple) != 2 { - t.Fatalf("Key not found: %#v", tuple) - } - encodedKeys = append(encodedKeys, tuple[1]) - } - } - - if bkeys != nil && len(matchedFingerprints) != 0 { - testMap := map[string][]string{} - for i, v := range matchedFingerprints { - testMap[v] = append(testMap[v], encodedKeys[i]) - sort.Strings(testMap[v]) - } - if !reflect.DeepEqual(testMap, bkeys) { - t.Fatalf("test map and backup map do not match, test map is\n%#v\nbackup map is\n%#v", testMap, bkeys) - } - } - - unsealKeys := []string{} - ptBuf := bytes.NewBuffer(nil) - for i, privKeyBytes := range privBytes { - if i > 2 { - break - } - ptBuf.Reset() - entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes))) - if err != nil { - t.Fatalf("Error parsing private key %d: %s", i, err) - } - var keyBytes []byte - keyBytes, err = base64.StdEncoding.DecodeString(encodedKeys[i]) - if err != nil { - t.Fatalf("Error decoding key %d: %s", i, err) - } - entityList := &openpgp.EntityList{entity} - md, err := openpgp.ReadMessage(bytes.NewBuffer(keyBytes), entityList, nil, nil) - if err != nil { - t.Fatalf("Error decrypting with key %d (%s): %s", i, encodedKeys[i], err) - } - ptBuf.ReadFrom(md.UnverifiedBody) - unsealKeys = append(unsealKeys, ptBuf.String()) - } - - err = core.Seal(rootToken) - if err != nil { - t.Fatalf("Error sealing vault with provided root token: %s", err) - } - - for i, unsealKey := range unsealKeys { - unsealBytes, err := hex.DecodeString(unsealKey) - if err != nil { - t.Fatalf("Error hex decoding unseal key %s: %s", unsealKey, err) - } - unsealed, err := core.Unseal(unsealBytes) - if err != nil { - t.Fatalf("Error using unseal key %s: %s", unsealKey, err) - } - if i >= 2 && !unsealed { - t.Fatalf("Error: Provided two unseal keys but core is not unsealed") - } - } - } - - testFunc(backupKeysB64) -} diff --git a/command/pki_health_check_test.go b/command/pki_health_check_test.go deleted file mode 100644 index b3b2dfb8c..000000000 --- a/command/pki_health_check_test.go +++ /dev/null @@ -1,694 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "encoding/json" - "fmt" - "net/url" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/healthcheck" - - "github.com/mitchellh/cli" - "github.com/stretchr/testify/require" -) - -func TestPKIHC_AllGood(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - AuditNonHMACRequestKeys: healthcheck.VisibleReqParams, - AuditNonHMACResponseKeys: healthcheck.VisibleRespParams, - PassthroughRequestHeaders: []string{"If-Modified-Since"}, - AllowedResponseHeaders: []string{"Last-Modified", "Replay-Nonce", "Link", "Location"}, - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X1", - "ttl": "3650d", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - if _, err := client.Logical().Read("pki/crl/rotate"); err != nil { - t.Fatalf("failed to rotate CRLs: %v", err) - } - - if _, err := client.Logical().Write("pki/roles/testing", map[string]interface{}{ - "allow_any_name": true, - "no_store": true, - }); err != nil { - t.Fatalf("failed to write role: %v", err) - } - - if _, err := client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ - "enabled": true, - "tidy_cert_store": true, - }); err != nil { - t.Fatalf("failed to write auto-tidy config: %v", err) - } - - if _, err := client.Logical().Write("pki/tidy", map[string]interface{}{ - "tidy_cert_store": true, - }); err != nil { - t.Fatalf("failed to run tidy: %v", err) - } - - path, err := url.Parse(client.Address()) - require.NoError(t, err, "failed parsing client address") - - if _, err := client.Logical().Write("pki/config/cluster", map[string]interface{}{ - "path": path.JoinPath("/v1/", "pki/").String(), - }); err != nil { - t.Fatalf("failed to update local cluster: %v", err) - } - - if _, err := client.Logical().Write("pki/config/acme", map[string]interface{}{ - "enabled": "true", - }); err != nil { - t.Fatalf("failed to update acme config: %v", err) - } - - _, _, results := execPKIHC(t, client, true) - - validateExpectedPKIHC(t, expectedAllGood, results) -} - -func TestPKIHC_AllBad(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X1", - "ttl": "35d", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - if _, err := client.Logical().Write("pki/config/crl", map[string]interface{}{ - "expiry": "5s", - }); err != nil { - t.Fatalf("failed to issue leaf cert: %v", err) - } - - if _, err := client.Logical().Read("pki/crl/rotate"); err != nil { - t.Fatalf("failed to rotate CRLs: %v", err) - } - - time.Sleep(5 * time.Second) - - if _, err := client.Logical().Write("pki/roles/testing", map[string]interface{}{ - "allow_localhost": true, - "allowed_domains": "*.example.com", - "allow_glob_domains": true, - "allow_wildcard_certificates": true, - "no_store": false, - "key_type": "ec", - "ttl": "30d", - }); err != nil { - t.Fatalf("failed to write role: %v", err) - } - - if _, err := client.Logical().Write("pki/issue/testing", map[string]interface{}{ - "common_name": "something.example.com", - }); err != nil { - t.Fatalf("failed to issue leaf cert: %v", err) - } - - if _, err := client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ - "enabled": false, - "tidy_cert_store": false, - }); err != nil { - t.Fatalf("failed to write auto-tidy config: %v", err) - } - - _, _, results := execPKIHC(t, client, true) - - validateExpectedPKIHC(t, expectedAllBad, results) -} - -func TestPKIHC_OnlyIssuer(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X1", - "ttl": "35d", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - _, _, results := execPKIHC(t, client, true) - validateExpectedPKIHC(t, expectedEmptyWithIssuer, results) -} - -func TestPKIHC_NoMount(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - code, message, _ := execPKIHC(t, client, false) - if code != 1 { - t.Fatalf("Expected return code 1 from invocation on non-existent mount, got %v\nOutput: %v", code, message) - } - - if !strings.Contains(message, "route entry not found") { - t.Fatalf("Expected failure to talk about missing route entry, got exit code %v\nOutput: %v", code, message) - } -} - -func TestPKIHC_ExpectedEmptyMount(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - code, message, _ := execPKIHC(t, client, false) - if code != 1 { - t.Fatalf("Expected return code 1 from invocation on empty mount, got %v\nOutput: %v", code, message) - } - - if !strings.Contains(message, "lacks any configured issuers,") { - t.Fatalf("Expected failure to talk about no issuers, got exit code %v\nOutput: %v", code, message) - } -} - -func TestPKIHC_NoPerm(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("pki", &api.MountInput{ - Type: "pki", - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X1", - "ttl": "35d", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - if _, err := client.Logical().Write("pki/config/crl", map[string]interface{}{ - "expiry": "5s", - }); err != nil { - t.Fatalf("failed to issue leaf cert: %v", err) - } - - if _, err := client.Logical().Read("pki/crl/rotate"); err != nil { - t.Fatalf("failed to rotate CRLs: %v", err) - } - - time.Sleep(5 * time.Second) - - if _, err := client.Logical().Write("pki/roles/testing", map[string]interface{}{ - "allow_localhost": true, - "allowed_domains": "*.example.com", - "allow_glob_domains": true, - "allow_wildcard_certificates": true, - "no_store": false, - "key_type": "ec", - "ttl": "30d", - }); err != nil { - t.Fatalf("failed to write role: %v", err) - } - - if _, err := client.Logical().Write("pki/issue/testing", map[string]interface{}{ - "common_name": "something.example.com", - }); err != nil { - t.Fatalf("failed to issue leaf cert: %v", err) - } - - if _, err := client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ - "enabled": false, - "tidy_cert_store": false, - }); err != nil { - t.Fatalf("failed to write auto-tidy config: %v", err) - } - - // Remove client token. - client.ClearToken() - - _, _, results := execPKIHC(t, client, true) - validateExpectedPKIHC(t, expectedNoPerm, results) -} - -func testPKIHealthCheckCommand(tb testing.TB) (*cli.MockUi, *PKIHealthCheckCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PKIHealthCheckCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func execPKIHC(t *testing.T, client *api.Client, ok bool) (int, string, map[string][]map[string]interface{}) { - t.Helper() - - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - runOpts := &RunOptions{ - Stdout: stdout, - Stderr: stderr, - Client: client, - } - - code := RunCustom([]string{"pki", "health-check", "-format=json", "pki"}, runOpts) - combined := stdout.String() + stderr.String() - - var results map[string][]map[string]interface{} - if err := json.Unmarshal([]byte(combined), &results); err != nil { - if ok { - t.Fatalf("failed to decode json (ret %v): %v\njson:\n%v", code, err, combined) - } - } - - t.Log(combined) - - return code, combined, results -} - -func validateExpectedPKIHC(t *testing.T, expected, results map[string][]map[string]interface{}) { - t.Helper() - - for test, subtest := range expected { - actual, ok := results[test] - require.True(t, ok, fmt.Sprintf("expected top-level test %v to be present", test)) - - if subtest == nil { - continue - } - - require.NotNil(t, actual, fmt.Sprintf("expected top-level test %v to be non-empty; wanted wireframe format %v", test, subtest)) - require.Equal(t, len(subtest), len(actual), fmt.Sprintf("top-level test %v has different number of results %v in wireframe, %v in test output\nwireframe: %v\noutput: %v\n", test, len(subtest), len(actual), subtest, actual)) - - for index, subset := range subtest { - for key, value := range subset { - a_value, present := actual[index][key] - require.True(t, present) - if value != nil { - require.Equal(t, value, a_value, fmt.Sprintf("in test: %v / result %v - when validating key %v\nWanted: %v\nGot: %v", test, index, key, subset, actual[index])) - } - } - } - } - - for name := range results { - if _, present := expected[name]; !present { - t.Fatalf("got unexpected health check: %v\n%v", name, results[name]) - } - } -} - -var expectedAllGood = map[string][]map[string]interface{}{ - "ca_validity_period": { - { - "status": "ok", - }, - }, - "crl_validity_period": { - { - "status": "ok", - }, - { - "status": "ok", - }, - }, - "allow_acme_headers": { - { - "status": "ok", - }, - }, - "allow_if_modified_since": { - { - "status": "ok", - }, - }, - "audit_visibility": { - { - "status": "ok", - }, - }, - "enable_acme_issuance": { - { - "status": "ok", - }, - }, - "enable_auto_tidy": { - { - "status": "ok", - }, - }, - "role_allows_glob_wildcards": { - { - "status": "ok", - }, - }, - "role_allows_localhost": { - { - "status": "ok", - }, - }, - "role_no_store_false": { - { - "status": "ok", - }, - }, - "root_issued_leaves": { - { - "status": "ok", - }, - }, - "tidy_last_run": { - { - "status": "ok", - }, - }, - "too_many_certs": { - { - "status": "ok", - }, - }, -} - -var expectedAllBad = map[string][]map[string]interface{}{ - "ca_validity_period": { - { - "status": "critical", - }, - }, - "crl_validity_period": { - { - "status": "critical", - }, - { - "status": "critical", - }, - }, - "allow_acme_headers": { - { - "status": "not_applicable", - }, - }, - "allow_if_modified_since": { - { - "status": "informational", - }, - }, - "audit_visibility": { - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - { - "status": "informational", - }, - }, - "enable_acme_issuance": { - { - "status": "not_applicable", - }, - }, - "enable_auto_tidy": { - { - "status": "informational", - }, - }, - "role_allows_glob_wildcards": { - { - "status": "warning", - }, - }, - "role_allows_localhost": { - { - "status": "warning", - }, - }, - "role_no_store_false": { - { - "status": "warning", - }, - }, - "root_issued_leaves": { - { - "status": "warning", - }, - }, - "tidy_last_run": { - { - "status": "critical", - }, - }, - "too_many_certs": { - { - "status": "ok", - }, - }, -} - -var expectedEmptyWithIssuer = map[string][]map[string]interface{}{ - "ca_validity_period": { - { - "status": "critical", - }, - }, - "crl_validity_period": { - { - "status": "ok", - }, - { - "status": "ok", - }, - }, - "allow_acme_headers": { - { - "status": "not_applicable", - }, - }, - "allow_if_modified_since": nil, - "audit_visibility": nil, - "enable_acme_issuance": { - { - "status": "not_applicable", - }, - }, - "enable_auto_tidy": { - { - "status": "informational", - }, - }, - "role_allows_glob_wildcards": nil, - "role_allows_localhost": nil, - "role_no_store_false": nil, - "root_issued_leaves": { - { - "status": "ok", - }, - }, - "tidy_last_run": { - { - "status": "critical", - }, - }, - "too_many_certs": { - { - "status": "ok", - }, - }, -} - -var expectedNoPerm = map[string][]map[string]interface{}{ - "ca_validity_period": { - { - "status": "critical", - }, - }, - "crl_validity_period": { - { - "status": "insufficient_permissions", - }, - { - "status": "critical", - }, - { - "status": "critical", - }, - }, - "allow_acme_headers": { - { - "status": "insufficient_permissions", - }, - }, - "allow_if_modified_since": nil, - "audit_visibility": nil, - "enable_acme_issuance": { - { - "status": "insufficient_permissions", - }, - }, - "enable_auto_tidy": { - { - "status": "insufficient_permissions", - }, - }, - "role_allows_glob_wildcards": { - { - "status": "insufficient_permissions", - }, - }, - "role_allows_localhost": { - { - "status": "insufficient_permissions", - }, - }, - "role_no_store_false": { - { - "status": "insufficient_permissions", - }, - }, - "root_issued_leaves": { - { - "status": "insufficient_permissions", - }, - }, - "tidy_last_run": { - { - "status": "insufficient_permissions", - }, - }, - "too_many_certs": { - { - "status": "insufficient_permissions", - }, - }, -} diff --git a/command/pki_issue_intermediate_test.go b/command/pki_issue_intermediate_test.go deleted file mode 100644 index cb66d45e7..000000000 --- a/command/pki_issue_intermediate_test.go +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "encoding/json" - "testing" - - "github.com/hashicorp/vault/api" -) - -func TestPKIIssueIntermediate(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - // Relationship Map to Create - // pki-root | pki-newroot | pki-empty - // RootX1 RootX2 RootX4 RootX3 - // | | - // ---------------------------------------------- - // v v - // IntX1 IntX2 pki-int - // | | - // v v - // IntX3 (-----------------------) IntX3 - // - // Here X1,X2 have the same name (same mount) - // RootX4 uses the same key as RootX1 (but a different common_name/subject) - // RootX3 has the same name, and is on a different mount - // RootX1 has issued IntX1; RootX3 has issued IntX2 - createComplicatedIssuerSetUpWithIssueIntermediate(t, client) - - runPkiVerifySignTests(t, client) - - runPkiListIntermediateTests(t, client) -} - -func createComplicatedIssuerSetUpWithIssueIntermediate(t *testing.T, client *api.Client) { - // Relationship Map to Create - // pki-root | pki-newroot | pki-empty - // RootX1 RootX2 RootX4 RootX3 - // | | - // ---------------------------------------------- - // v v - // IntX1 IntX2 pki-int - // | | - // v v - // IntX3 (-----------------------) IntX3 - // - // Here X1,X2 have the same name (same mount) - // RootX4 uses the same key as RootX1 (but a different common_name/subject) - // RootX3 has the same name, and is on a different mount - // RootX1 has issued IntX1; RootX3 has issued IntX2 - - if err := client.Sys().Mount("pki-root", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if err := client.Sys().Mount("pki-newroot", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if err := client.Sys().Mount("pki-int", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - // Used to check handling empty list responses: Not Used for Any Issuers / Certificates - if err := client.Sys().Mount("pki-empty", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{}, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - resp, err := client.Logical().Write("pki-root/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X", - "ttl": "3650d", - "issuer_name": "rootX1", - "key_name": "rootX1", - }) - if err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - resp, err = client.Logical().Write("pki-root/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X", - "ttl": "3650d", - "issuer_name": "rootX2", - }) - if err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - if resp, err := client.Logical().Write("pki-newroot/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X", - "ttl": "3650d", - "issuer_name": "rootX3", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - if resp, err := client.Logical().Write("pki-root/root/generate/existing", map[string]interface{}{ - "common_name": "Root X4", - "ttl": "3650d", - "issuer_name": "rootX4", - "key_ref": "rootX1", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - // Next we create the Intermediates Using the Issue Intermediate Command - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - runOpts := &RunOptions{ - Stdout: stdout, - Stderr: stderr, - Client: client, - } - - // Intermediate X1 - intX1CallArgs := []string{ - "pki", "issue", "-format=json", "-issuer_name=intX1", - "pki-root/issuer/rootX1", - "pki-int/", - "key_type=rsa", - "common_name=Int X1", - "ttl=3650d", - } - codeOut := RunCustom(intX1CallArgs, runOpts) - if codeOut != 0 { - t.Fatalf("error issuing intermediate X1, code: %d \n stdout: %v \n stderr: %v", codeOut, stdout, stderr) - } - - // Intermediate X2 - intX2CallArgs := []string{ - "pki", "issue", "-format=json", "-issuer_name=intX2", - "pki-newroot/issuer/rootX3", - "pki-int/", - "key_type=ec", - "common_name=Int X2", - "ttl=3650d", - } - codeOut = RunCustom(intX2CallArgs, runOpts) - if codeOut != 0 { - t.Fatalf("error issuing intermediate X2, code: %d \n stdout: %v \n stderr: %v", codeOut, stdout, stderr) - } - - // Intermediate X3 - // Clear Buffers so that we can unmarshall json of just this call - stdout = bytes.NewBuffer(nil) - stderr = bytes.NewBuffer(nil) - runOpts = &RunOptions{ - Stdout: stdout, - Stderr: stderr, - Client: client, - } - intX3OriginalCallArgs := []string{ - "pki", "issue", "-format=json", "-issuer_name=intX3", - "pki-int/issuer/intX1", - "pki-int/", - "key_type=rsa", - "common_name=Int X3", - "ttl=3650d", - } - codeOut = RunCustom(intX3OriginalCallArgs, runOpts) - if codeOut != 0 { - t.Fatalf("error issuing intermediate X3, code: %d \n stdout: %v \n stderr: %v", codeOut, stdout, stderr) - } - var intX3Resp map[string]interface{} - json.Unmarshal(stdout.Bytes(), &intX3Resp) - intX3Data := intX3Resp["data"].(map[string]interface{}) - keyId := intX3Data["key_id"].(string) - - intX3AdaptedCallArgs := []string{ - "pki", "issue", "-format=json", "-issuer_name=intX3also", "-type=existing", - "pki-int/issuer/intX2", - "pki-int/", - "key_ref=" + keyId, - "common_name=Int X3", - "ttl=3650d", - } - codeOut = RunCustom(intX3AdaptedCallArgs, runOpts) - if codeOut != 0 { - t.Fatalf("error issuing intermediate X3also, code: %d \n stdout: %v \n stderr: %v", codeOut, stdout, stderr) - } -} diff --git a/command/pki_list_intermediate_test.go b/command/pki_list_intermediate_test.go deleted file mode 100644 index 5abfabd55..000000000 --- a/command/pki_list_intermediate_test.go +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" -) - -func TestPKIListIntermediate(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - // Relationship Map to Create - // pki-root | pki-newroot | pki-empty - // RootX1 RootX2 RootX4 RootX3 - // | | - // ---------------------------------------------- - // v v - // IntX1 IntX2 pki-int - // | | - // v v - // IntX3 (-----------------------) IntX3(also) - // - // Here X1,X2 have the same name (same mount) - // RootX4 uses the same key as RootX1 (but a different common_name/subject) - // RootX3 has the same name, and is on a different mount - // RootX1 has issued IntX1; RootX3 has issued IntX2 - createComplicatedIssuerSetUp(t, client) - - runPkiListIntermediateTests(t, client) -} - -func runPkiListIntermediateTests(t *testing.T, client *api.Client) { - cases := []struct { - name string - args []string - expectedMatches map[string]bool - jsonOut bool - shouldError bool - expectErrorCont string - expectErrorNotCont string - nonJsonOutputCont string - }{ - { - "rootX1-match-everything-no-constraints", - []string{ - "pki", "list-intermediates", "-format=json", "-use_names=true", - "-subject_match=false", "-key_id_match=false", "-direct_sign=false", "-indirect_sign=false", "-path_match=false", - "pki-root/issuer/rootX1", - }, - map[string]bool{ - "pki-root/issuer/rootX1": true, - "pki-root/issuer/rootX2": true, - "pki-newroot/issuer/rootX3": true, - "pki-root/issuer/rootX4": true, - "pki-int/issuer/intX1": true, - "pki-int/issuer/intX2": true, - "pki-int/issuer/intX3": true, - "pki-int/issuer/intX3also": true, - "pki-int/issuer/rootX1": true, - "pki-int/issuer/rootX3": true, - }, - true, - false, - "", - "", - "", - }, - { - "rootX1-default-children", - []string{"pki", "list-intermediates", "-format=json", "-use_names=true", "pki-root/issuer/rootX1"}, - map[string]bool{ - "pki-root/issuer/rootX1": true, - "pki-root/issuer/rootX2": false, - "pki-newroot/issuer/rootX3": false, - "pki-root/issuer/rootX4": false, - "pki-int/issuer/intX1": true, - "pki-int/issuer/intX2": false, - "pki-int/issuer/intX3": false, - "pki-int/issuer/intX3also": false, - "pki-int/issuer/rootX1": true, - "pki-int/issuer/rootX3": false, - }, - true, - false, - "", - "", - "", - }, - { - "rootX1-subject-match-only", - []string{ - "pki", "list-intermediates", "-format=json", "-use_names=true", - "-key_id_match=false", "-direct_sign=false", "-indirect_sign=false", - "pki-root/issuer/rootX1", - }, - map[string]bool{ - "pki-root/issuer/rootX1": true, - "pki-root/issuer/rootX2": true, - "pki-newroot/issuer/rootX3": true, - "pki-root/issuer/rootX4": false, - "pki-int/issuer/intX1": true, - "pki-int/issuer/intX2": true, - "pki-int/issuer/intX3": false, - "pki-int/issuer/intX3also": false, - "pki-int/issuer/rootX1": true, - "pki-int/issuer/rootX3": true, - }, - true, - false, - "", - "", - "", - }, - { - "rootX1-in-path", - []string{ - "pki", "list-intermediates", "-format=json", "-use_names=true", - "-subject_match=false", "-key_id_match=false", "-direct_sign=false", "-indirect_sign=false", "-path_match=true", - "pki-root/issuer/rootX1", - }, - map[string]bool{ - "pki-root/issuer/rootX1": true, - "pki-root/issuer/rootX2": false, - "pki-newroot/issuer/rootX3": false, - "pki-root/issuer/rootX4": false, - "pki-int/issuer/intX1": true, - "pki-int/issuer/intX2": false, - "pki-int/issuer/intX3": true, - "pki-int/issuer/intX3also": false, - "pki-int/issuer/rootX1": true, - "pki-int/issuer/rootX3": false, - }, - true, - false, - "", - "", - "", - }, - { - "rootX1-only-int-mount", - []string{ - "pki", "list-intermediates", "-format=json", "-use_names=true", - "-subject_match=false", "-key_id_match=false", "-direct_sign=false", "-indirect_sign=false", "-path_match=true", - "pki-root/issuer/rootX1", "pki-int/", - }, - map[string]bool{ - "pki-int/issuer/intX1": true, - "pki-int/issuer/intX2": false, - "pki-int/issuer/intX3": true, - "pki-int/issuer/intX3also": false, - "pki-int/issuer/rootX1": true, - "pki-int/issuer/rootX3": false, - }, - true, - false, - "", - "", - "", - }, - { - "rootX1-subject-match-root-mounts-only", - []string{ - "pki", "list-intermediates", "-format=json", "-use_names=true", - "-key_id_match=false", "-direct_sign=false", "-indirect_sign=false", - "pki-root/issuer/rootX1", "pki-root/", "pki-newroot", "pki-empty", - }, - map[string]bool{ - "pki-root/issuer/rootX1": true, - "pki-root/issuer/rootX2": true, - "pki-newroot/issuer/rootX3": true, - "pki-root/issuer/rootX4": false, - }, - true, - false, - "", - "", - "", - }, - { - "rootX1-subject-match-these-certs-only", - []string{ - "pki", "list-intermediates", "-format=json", "-use_names=true", - "-key_id_match=false", "-direct_sign=false", "-indirect_sign=false", - "pki-root/issuer/rootX1", "pki-root/issuer/rootX2", "pki-newroot/issuer/rootX3", "pki-root/issuer/rootX4", - }, - map[string]bool{ - "pki-root/issuer/rootX2": true, - "pki-newroot/issuer/rootX3": true, - "pki-root/issuer/rootX4": false, - }, - true, - false, - "", - "", - "", - }, - } - for _, testCase := range cases { - var errString string - var results map[string]interface{} - var stdOut string - - if testCase.jsonOut { - results, errString = execPKIVerifyJson(t, client, false, testCase.shouldError, testCase.args) - } else { - stdOut, errString = execPKIVerifyNonJson(t, client, testCase.shouldError, testCase.args) - } - - // Verify Error Behavior - if testCase.shouldError { - if errString == "" { - t.Fatalf("Expected error in Testcase %s : no error produced, got results %s", testCase.name, results) - } - if testCase.expectErrorCont != "" && !strings.Contains(errString, testCase.expectErrorCont) { - t.Fatalf("Expected error in Testcase %s to contain %s, but got error %s", testCase.name, testCase.expectErrorCont, errString) - } - if testCase.expectErrorNotCont != "" && strings.Contains(errString, testCase.expectErrorNotCont) { - t.Fatalf("Expected error in Testcase %s to not contain %s, but got error %s", testCase.name, testCase.expectErrorNotCont, errString) - } - } else { - if errString != "" { - t.Fatalf("Error in Testcase %s : no error expected, but got error: %s", testCase.name, errString) - } - } - - // Verify Output - if testCase.jsonOut { - isMatch, errString := verifyExpectedJson(testCase.expectedMatches, results) - if !isMatch { - t.Fatalf("Expected Results for Testcase %s, do not match returned results %s", testCase.name, errString) - } - } else { - if !strings.Contains(stdOut, testCase.nonJsonOutputCont) { - t.Fatalf("Expected standard output for Testcase %s to contain %s, but got %s", testCase.name, testCase.nonJsonOutputCont, stdOut) - } - } - - } -} diff --git a/command/pki_reissue_intermediate_test.go b/command/pki_reissue_intermediate_test.go deleted file mode 100644 index 45657fe11..000000000 --- a/command/pki_reissue_intermediate_test.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "testing" - - "github.com/hashicorp/vault/api" -) - -// TestPKIReIssueIntermediate tests that the pki reissue command line tool accurately copies information from the -// template certificate to the newly issued certificate, by issuing and reissuing several certificates and seeing how -// they related to each other. -func TestPKIReIssueIntermediate(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - // Relationship Map to Create - // pki-root | pki-newroot | pki-empty - // RootX1 RootX2 RootX4 RootX3 - // | | - // ---------------------------------------------- - // v v - // IntX1 IntX2 pki-int - // | | - // v v - // IntX3 (-----------------------) IntX3 - // - // Here X1,X2 have the same name (same mount) - // RootX4 uses the same key as RootX1 (but a different common_name/subject) - // RootX3 has the same name, and is on a different mount - // RootX1 has issued IntX1; RootX3 has issued IntX2 - createComplicatedIssuerSetUpWithReIssueIntermediate(t, client) - - runPkiVerifySignTests(t, client) - - runPkiListIntermediateTests(t, client) -} - -func createComplicatedIssuerSetUpWithReIssueIntermediate(t *testing.T, client *api.Client) { - // Relationship Map to Create - // pki-root | pki-newroot | pki-empty - // RootX1 RootX2 RootX4 RootX3 - // | | - // ---------------------------------------------- - // v v - // IntX1 IntX2 pki-int - // | | - // v v - // IntX3 (-----------------------) IntX3 - // - // Here X1,X2 have the same name (same mount) - // RootX4 uses the same key as RootX1 (but a different common_name/subject) - // RootX3 has the same name, and is on a different mount - // RootX1 has issued IntX1; RootX3 has issued IntX2 - - if err := client.Sys().Mount("pki-root", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if err := client.Sys().Mount("pki-newroot", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if err := client.Sys().Mount("pki-int", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - // Used to check handling empty list responses: Not Used for Any Issuers / Certificates - if err := client.Sys().Mount("pki-empty", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{}, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - resp, err := client.Logical().Write("pki-root/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X", - "ttl": "3650d", - "issuer_name": "rootX1", - "key_name": "rootX1", - }) - if err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - resp, err = client.Logical().Write("pki-root/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X", - "ttl": "3650d", - "issuer_name": "rootX2", - }) - if err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - if resp, err := client.Logical().Write("pki-newroot/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X", - "ttl": "3650d", - "issuer_name": "rootX3", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - if resp, err := client.Logical().Write("pki-root/root/generate/existing", map[string]interface{}{ - "common_name": "Root X4", - "ttl": "3650d", - "issuer_name": "rootX4", - "key_ref": "rootX1", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - runOpts := &RunOptions{ - Stdout: stdout, - Stderr: stderr, - Client: client, - } - - // Intermediate X1 - intX1CallArgs := []string{ - "pki", "issue", "-format=json", "-issuer_name=intX1", - "pki-root/issuer/rootX1", - "pki-int/", - "key_type=rsa", - "common_name=Int X1", - "ou=thing", - "ttl=3650d", - } - codeOut := RunCustom(intX1CallArgs, runOpts) - if codeOut != 0 { - t.Fatalf("error issuing intermediate X1, code: %d \n stdout: %v \n stderr: %v", codeOut, stdout, stderr) - } - - // Intermediate X2 - using ReIssue - intX2CallArgs := []string{ - "pki", "reissue", "-format=json", "-issuer_name=intX2", - "pki-newroot/issuer/rootX3", - "pki-int/issuer/intX1", - "pki-int/", - "key_type=ec", - "common_name=Int X2", - } - codeOut = RunCustom(intX2CallArgs, runOpts) - if codeOut != 0 { - t.Fatalf("error issuing intermediate X2, code: %d \n stdout: %v \n stderr: %v", codeOut, stdout, stderr) - } - - // Intermediate X3 - intX3OriginalCallArgs := []string{ - "pki", "issue", "-format=json", "-issuer_name=intX3", - "pki-int/issuer/intX1", - "pki-int/", - "key_type=ec", - "use_pss=true", // This is meaningful because rootX1 is an RSA key - "signature_bits=512", - "common_name=Int X3", - "ttl=3650d", - } - codeOut = RunCustom(intX3OriginalCallArgs, runOpts) - if codeOut != 0 { - t.Fatalf("error issuing intermediate X3, code: %d \n stdout: %v \n stderr: %v", codeOut, stdout, stderr) - } - - intX3AdaptedCallArgs := []string{ - "pki", "reissue", "-format=json", "-issuer_name=intX3also", "-type=existing", - "pki-int/issuer/intX2", // This is a EC key - "pki-int/issuer/intX3", // This template includes use_pss = true which can't be accomodated - "pki-int/", - } - codeOut = RunCustom(intX3AdaptedCallArgs, runOpts) - if codeOut != 0 { - t.Fatalf("error issuing intermediate X3also, code: %d \n stdout: %v \n stderr: %v", codeOut, stdout, stderr) - } -} diff --git a/command/pki_verify_sign_test.go b/command/pki_verify_sign_test.go deleted file mode 100644 index 4001aadbc..000000000 --- a/command/pki_verify_sign_test.go +++ /dev/null @@ -1,468 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/vault/api" -) - -func TestPKIVerifySign(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - // Relationship Map to Create - // pki-root | pki-newroot | pki-empty - // RootX1 RootX2 RootX4 RootX3 - // | | - // ---------------------------------------------- - // v v - // IntX1 IntX2 pki-int - // | | - // v v - // IntX3 (-----------------------) IntX3 - // - // Here X1,X2 have the same name (same mount) - // RootX4 uses the same key as RootX1 (but a different common_name/subject) - // RootX3 has the same name, and is on a different mount - // RootX1 has issued IntX1; RootX3 has issued IntX2 - createComplicatedIssuerSetUp(t, client) - - runPkiVerifySignTests(t, client) -} - -func runPkiVerifySignTests(t *testing.T, client *api.Client) { - cases := []struct { - name string - args []string - expectedMatches map[string]bool - jsonOut bool - shouldError bool - expectErrorCont string - expectErrorNotCont string - nonJsonOutputCont string - }{ - { - "rootX1-matches-rootX1", - []string{"pki", "verify-sign", "-format=json", "pki-root/issuer/rootX1", "pki-root/issuer/rootX1"}, - map[string]bool{ - "key_id_match": true, - "path_match": true, - "signature_match": true, - "subject_match": true, - "trust_match": true, - }, - true, - false, - "", - "", - "", - }, - { - "rootX1-on-rootX2-onlySameName", - []string{"pki", "verify-sign", "-format=json", "pki-root/issuer/rootX1", "pki-root/issuer/rootX2"}, - map[string]bool{ - "key_id_match": false, - "path_match": false, - "signature_match": false, - "subject_match": true, - "trust_match": false, - }, - true, - false, - "", - "", - "", - }, - } - for _, testCase := range cases { - var errString string - var results map[string]interface{} - var stdOut string - - if testCase.jsonOut { - results, errString = execPKIVerifyJson(t, client, false, testCase.shouldError, testCase.args) - } else { - stdOut, errString = execPKIVerifyNonJson(t, client, testCase.shouldError, testCase.args) - } - - // Verify Error Behavior - if testCase.shouldError { - if errString == "" { - t.Fatalf("Expected error in Testcase %s : no error produced, got results %s", testCase.name, results) - } - if testCase.expectErrorCont != "" && !strings.Contains(errString, testCase.expectErrorCont) { - t.Fatalf("Expected error in Testcase %s to contain %s, but got error %s", testCase.name, testCase.expectErrorCont, errString) - } - if testCase.expectErrorNotCont != "" && strings.Contains(errString, testCase.expectErrorNotCont) { - t.Fatalf("Expected error in Testcase %s to not contain %s, but got error %s", testCase.name, testCase.expectErrorNotCont, errString) - } - } else { - if errString != "" { - t.Fatalf("Error in Testcase %s : no error expected, but got error: %s", testCase.name, errString) - } - } - - // Verify Output - if testCase.jsonOut { - isMatch, errString := verifyExpectedJson(testCase.expectedMatches, results) - if !isMatch { - t.Fatalf("Expected Results for Testcase %s, do not match returned results %s", testCase.name, errString) - } - } else { - if !strings.Contains(stdOut, testCase.nonJsonOutputCont) { - t.Fatalf("Expected standard output for Testcase %s to contain %s, but got %s", testCase.name, testCase.nonJsonOutputCont, stdOut) - } - } - - } -} - -func execPKIVerifyJson(t *testing.T, client *api.Client, expectErrorUnmarshalling bool, expectErrorOut bool, callArgs []string) (map[string]interface{}, string) { - stdout, stderr := execPKIVerifyNonJson(t, client, expectErrorOut, callArgs) - - var results map[string]interface{} - if err := json.Unmarshal([]byte(stdout), &results); err != nil && !expectErrorUnmarshalling { - t.Fatalf("failed to decode json response : %v \n json: \n%v", err, stdout) - } - - return results, stderr -} - -func execPKIVerifyNonJson(t *testing.T, client *api.Client, expectErrorOut bool, callArgs []string) (string, string) { - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - runOpts := &RunOptions{ - Stdout: stdout, - Stderr: stderr, - Client: client, - } - - code := RunCustom(callArgs, runOpts) - if !expectErrorOut && code != 0 { - t.Fatalf("running command `%v` unsuccessful (ret %v)\nerr: %v", strings.Join(callArgs, " "), code, stderr.String()) - } - - t.Log(stdout.String() + stderr.String()) - - return stdout.String(), stderr.String() -} - -func convertListOfInterfaceToString(list []interface{}, sep string) string { - newList := make([]string, len(list)) - for i, interfa := range list { - newList[i] = interfa.(string) - } - return strings.Join(newList, sep) -} - -func createComplicatedIssuerSetUp(t *testing.T, client *api.Client) { - // Relationship Map to Create - // pki-root | pki-newroot | pki-empty - // RootX1 RootX2 RootX4 RootX3 - // | | - // ---------------------------------------------- - // v v - // IntX1 IntX2 pki-int - // | | - // v v - // IntX3 (-----------------------) IntX3 - // - // Here X1,X2 have the same name (same mount) - // RootX4 uses the same key as RootX1 (but a different common_name/subject) - // RootX3 has the same name, and is on a different mount - // RootX1 has issued IntX1; RootX3 has issued IntX2 - - if err := client.Sys().Mount("pki-root", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if err := client.Sys().Mount("pki-newroot", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - if err := client.Sys().Mount("pki-int", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{ - MaxLeaseTTL: "36500d", - }, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - // Used to check handling empty list responses: Not Used for Any Issuers / Certificates - if err := client.Sys().Mount("pki-empty", &api.MountInput{ - Type: "pki", - Config: api.MountConfigInput{}, - }); err != nil { - t.Fatalf("pki mount error: %#v", err) - } - - resp, err := client.Logical().Write("pki-root/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X", - "ttl": "3650d", - "issuer_name": "rootX1", - "key_name": "rootX1", - }) - if err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - resp, err = client.Logical().Write("pki-root/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X", - "ttl": "3650d", - "issuer_name": "rootX2", - }) - if err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - if resp, err := client.Logical().Write("pki-newroot/root/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Root X", - "ttl": "3650d", - "issuer_name": "rootX3", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - if resp, err := client.Logical().Write("pki-root/root/generate/existing", map[string]interface{}{ - "common_name": "Root X4", - "ttl": "3650d", - "issuer_name": "rootX4", - "key_ref": "rootX1", - }); err != nil || resp == nil { - t.Fatalf("failed to prime CA: %v", err) - } - - // Intermediate X1 - int1CsrResp, err := client.Logical().Write("pki-int/intermediate/generate/internal", map[string]interface{}{ - "key_type": "rsa", - "common_name": "Int X1", - "ttl": "3650d", - }) - if err != nil || int1CsrResp == nil { - t.Fatalf("failed to generate CSR: %v", err) - } - int1KeyId, ok := int1CsrResp.Data["key_id"] - if !ok { - t.Fatalf("no key_id produced when generating csr, response %v", int1CsrResp.Data) - } - int1CsrRaw, ok := int1CsrResp.Data["csr"] - if !ok { - t.Fatalf("no csr produced when generating intermediate, resp: %v", int1CsrResp) - } - int1Csr := int1CsrRaw.(string) - int1CertResp, err := client.Logical().Write("pki-root/issuer/rootX1/sign-intermediate", map[string]interface{}{ - "csr": int1Csr, - }) - if err != nil || int1CertResp == nil { - t.Fatalf("failed to sign CSR: %v", err) - } - int1CertChainRaw, ok := int1CertResp.Data["ca_chain"] - if !ok { - t.Fatalf("no ca_chain produced when signing intermediate, resp: %v", int1CertResp) - } - int1CertChain := convertListOfInterfaceToString(int1CertChainRaw.([]interface{}), "\n") - importInt1Resp, err := client.Logical().Write("pki-int/issuers/import/cert", map[string]interface{}{ - "pem_bundle": int1CertChain, - }) - if err != nil || importInt1Resp == nil { - t.Fatalf("failed to import certificate: %v", err) - } - importIssuerIdMap, ok := importInt1Resp.Data["mapping"] - if !ok { - t.Fatalf("no mapping data returned on issuer import: %v", importInt1Resp) - } - for key, value := range importIssuerIdMap.(map[string]interface{}) { - if value != nil && len(value.(string)) > 0 { - if value != int1KeyId { - t.Fatalf("Expected exactly one key_match to %v, got multiple: %v", int1KeyId, importIssuerIdMap) - } - if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ - "issuer_name": "intX1", - }); err != nil || resp == nil { - t.Fatalf("error naming issuer %v", err) - } - } else { - if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ - "issuer_name": "rootX1", - }); err != nil || resp == nil { - t.Fatalf("error naming issuer parent %v", err) - } - } - } - - // Intermediate X2 - int2CsrResp, err := client.Logical().Write("pki-int/intermediate/generate/internal", map[string]interface{}{ - "key_type": "ec", - "common_name": "Int X2", - "ttl": "3650d", - }) - if err != nil || int2CsrResp == nil { - t.Fatalf("failed to generate CSR: %v", err) - } - int2KeyId, ok := int2CsrResp.Data["key_id"] - if !ok { - t.Fatalf("no key material returned from producing csr, resp: %v", int2CsrResp) - } - int2CsrRaw, ok := int2CsrResp.Data["csr"] - if !ok { - t.Fatalf("no csr produced when generating intermediate, resp: %v", int2CsrResp) - } - int2Csr := int2CsrRaw.(string) - int2CertResp, err := client.Logical().Write("pki-newroot/issuer/rootX3/sign-intermediate", map[string]interface{}{ - "csr": int2Csr, - }) - if err != nil || int2CertResp == nil { - t.Fatalf("failed to sign CSR: %v", err) - } - int2CertChainRaw, ok := int2CertResp.Data["ca_chain"] - if !ok { - t.Fatalf("no ca_chain produced when signing intermediate, resp: %v", int2CertResp) - } - int2CertChain := convertListOfInterfaceToString(int2CertChainRaw.([]interface{}), "\n") - importInt2Resp, err := client.Logical().Write("pki-int/issuers/import/cert", map[string]interface{}{ - "pem_bundle": int2CertChain, - }) - if err != nil || importInt2Resp == nil { - t.Fatalf("failed to import certificate: %v", err) - } - importIssuer2IdMap, ok := importInt2Resp.Data["mapping"] - if !ok { - t.Fatalf("no mapping data returned on issuer import: %v", importInt2Resp) - } - for key, value := range importIssuer2IdMap.(map[string]interface{}) { - if value != nil && len(value.(string)) > 0 { - if value != int2KeyId { - t.Fatalf("unexpected key_match with ca_chain, expected only %v, got %v", int2KeyId, importIssuer2IdMap) - } - if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ - "issuer_name": "intX2", - }); err != nil || resp == nil { - t.Fatalf("error naming issuer %v", err) - } - } else { - if resp, err := client.Logical().Write("pki-int/issuer/"+key, map[string]interface{}{ - "issuer_name": "rootX3", - }); err != nil || resp == nil { - t.Fatalf("error naming parent issuer %v", err) - } - } - } - - // Intermediate X3 - int3CsrResp, err := client.Logical().Write("pki-int/intermediate/generate/internal", map[string]interface{}{ - "key_type": "rsa", - "common_name": "Int X3", - "ttl": "3650d", - }) - if err != nil || int3CsrResp == nil { - t.Fatalf("failed to generate CSR: %v", err) - } - int3KeyId, ok := int3CsrResp.Data["key_id"] - int3CsrRaw, ok := int3CsrResp.Data["csr"] - if !ok { - t.Fatalf("no csr produced when generating intermediate, resp: %v", int3CsrResp) - } - int3Csr := int3CsrRaw.(string) - // sign by intX1 and import - int3CertResp1, err := client.Logical().Write("pki-int/issuer/intX1/sign-intermediate", map[string]interface{}{ - "csr": int3Csr, - }) - if err != nil || int3CertResp1 == nil { - t.Fatalf("failed to sign CSR: %v", err) - } - int3CertChainRaw1, ok := int3CertResp1.Data["ca_chain"] - if !ok { - t.Fatalf("no ca_chain produced when signing intermediate, resp: %v", int3CertResp1) - } - int3CertChain1 := convertListOfInterfaceToString(int3CertChainRaw1.([]interface{}), "\n") - importInt3Resp1, err := client.Logical().Write("pki-int/issuers/import/cert", map[string]interface{}{ - "pem_bundle": int3CertChain1, - }) - if err != nil || importInt3Resp1 == nil { - t.Fatalf("failed to import certificate: %v", err) - } - importIssuer3IdMap1, ok := importInt3Resp1.Data["mapping"] - if !ok { - t.Fatalf("no mapping data returned on issuer import: %v", importInt2Resp) - } - for key, value := range importIssuer3IdMap1.(map[string]interface{}) { - if value != nil && len(value.(string)) > 0 && value == int3KeyId { - if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ - "issuer_name": "intX3", - }); err != nil || resp == nil { - t.Fatalf("error naming issuer %v", err) - } - break - } - } - - // sign by intX2 and import - int3CertResp2, err := client.Logical().Write("pki-int/issuer/intX2/sign-intermediate", map[string]interface{}{ - "csr": int3Csr, - }) - if err != nil || int3CertResp2 == nil { - t.Fatalf("failed to sign CSR: %v", err) - } - int3CertChainRaw2, ok := int3CertResp2.Data["ca_chain"] - if !ok { - t.Fatalf("no ca_chain produced when signing intermediate, resp: %v", int3CertResp2) - } - int3CertChain2 := convertListOfInterfaceToString(int3CertChainRaw2.([]interface{}), "\n") - importInt3Resp2, err := client.Logical().Write("pki-int/issuers/import/cert", map[string]interface{}{ - "pem_bundle": int3CertChain2, - }) - if err != nil || importInt3Resp2 == nil { - t.Fatalf("failed to import certificate: %v", err) - } - importIssuer3IdMap2, ok := importInt3Resp2.Data["mapping"] - if !ok { - t.Fatalf("no mapping data returned on issuer import: %v", importInt2Resp) - } - for key, value := range importIssuer3IdMap2.(map[string]interface{}) { - if value != nil && len(value.(string)) > 0 && value == int3KeyId { - if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ - "issuer_name": "intX3also", - }); err != nil || resp == nil { - t.Fatalf("error naming issuer %v", err) - } - break // Parent Certs Already Named - } - } -} - -func verifyExpectedJson(expectedResults map[string]bool, results map[string]interface{}) (isMatch bool, error string) { - if len(expectedResults) != len(results) { - return false, fmt.Sprintf("Different Number of Keys in Expected Results (%d), than results (%d)", - len(expectedResults), len(results)) - } - for key, value := range expectedResults { - if results[key].(bool) != value { - return false, fmt.Sprintf("Different value for key %s : expected %t got %s", key, value, results[key]) - } - } - return true, "" -} diff --git a/command/plugin_deregister_test.go b/command/plugin_deregister_test.go deleted file mode 100644 index f23c8b6c4..000000000 --- a/command/plugin_deregister_test.go +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/mitchellh/cli" -) - -func testPluginDeregisterCommand(tb testing.TB) (*cli.MockUi, *PluginDeregisterCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PluginDeregisterCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPluginDeregisterCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - nil, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar", "fizz"}, - "Too many arguments", - 1, - }, - { - "not_a_plugin", - []string{consts.PluginTypeCredential.String(), "nope_definitely_never_a_plugin_nope"}, - "", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPluginDeregisterCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - pluginName := "my-plugin" - _, sha256Sum := testPluginCreateAndRegister(t, client, pluginDir, pluginName, api.PluginTypeCredential, "") - - ui, cmd := testPluginDeregisterCommand(t) - cmd.client = client - - if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{ - Name: pluginName, - Type: api.PluginTypeCredential, - Command: pluginName, - SHA256: sha256Sum, - }); err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - consts.PluginTypeCredential.String(), - pluginName, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Deregistered plugin (if it was registered): " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{ - Type: api.PluginTypeCredential, - }) - if err != nil { - t.Fatal(err) - } - - found := false - for _, plugins := range resp.PluginsByType { - for _, p := range plugins { - if p == pluginName { - found = true - } - } - } - if found { - t.Errorf("expected %q to not be in %q", pluginName, resp.PluginsByType) - } - }) - - t.Run("integration with version", func(t *testing.T) { - t.Parallel() - - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - pluginName := "my-plugin" - _, _, version := testPluginCreateAndRegisterVersioned(t, client, pluginDir, pluginName, api.PluginTypeCredential) - - ui, cmd := testPluginDeregisterCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-version=" + version, - consts.PluginTypeCredential.String(), - pluginName, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Deregistered plugin (if it was registered): " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{ - Type: api.PluginTypeUnknown, - }) - if err != nil { - t.Fatal(err) - } - - found := false - for _, p := range resp.Details { - if p.Name == pluginName { - found = true - } - } - if found { - t.Errorf("expected %q to not be in %#v", pluginName, resp.Details) - } - }) - - t.Run("integration with missing version", func(t *testing.T) { - t.Parallel() - - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - pluginName := "my-plugin" - testPluginCreateAndRegisterVersioned(t, client, pluginDir, pluginName, api.PluginTypeCredential) - - ui, cmd := testPluginDeregisterCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - consts.PluginTypeCredential.String(), - pluginName, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Deregistered plugin (if it was registered): " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{ - Type: api.PluginTypeUnknown, - }) - if err != nil { - t.Fatal(err) - } - - found := false - for _, p := range resp.Details { - if p.Name == pluginName { - found = true - } - } - if !found { - t.Errorf("expected %q to be in %#v", pluginName, resp.Details) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPluginDeregisterCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - consts.PluginTypeCredential.String(), - "my-plugin", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error deregistering plugin named my-plugin: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPluginDeregisterCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/plugin_info_test.go b/command/plugin_info_test.go deleted file mode 100644 index 367124301..000000000 --- a/command/plugin_info_test.go +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/hashicorp/vault/helper/versions" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/mitchellh/cli" -) - -func testPluginInfoCommand(tb testing.TB) (*cli.MockUi, *PluginInfoCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PluginInfoCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPluginInfoCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo", "bar", "fizz"}, - "Too many arguments", - 1, - }, - { - "no_plugin_exist", - []string{api.PluginTypeCredential.String(), "not-a-real-plugin-like-ever"}, - "Error reading plugin", - 2, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPluginInfoCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("default", func(t *testing.T) { - t.Parallel() - - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - pluginName := "my-plugin" - _, sha256Sum := testPluginCreateAndRegister(t, client, pluginDir, pluginName, api.PluginTypeCredential, "") - - ui, cmd := testPluginInfoCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - api.PluginTypeCredential.String(), pluginName, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, pluginName) { - t.Errorf("expected %q to contain %q", combined, pluginName) - } - if !strings.Contains(combined, sha256Sum) { - t.Errorf("expected %q to contain %q", combined, sha256Sum) - } - }) - - t.Run("version flag", func(t *testing.T) { - t.Parallel() - - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - const pluginName = "azure" - _, sha256Sum := testPluginCreateAndRegister(t, client, pluginDir, pluginName, api.PluginTypeCredential, "v1.0.0") - - for name, tc := range map[string]struct { - version string - expectedSHA string - }{ - "versioned": {"v1.0.0", sha256Sum}, - "builtin version": {versions.GetBuiltinVersion(consts.PluginTypeSecrets, pluginName), ""}, - } { - t.Run(name, func(t *testing.T) { - ui, cmd := testPluginInfoCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-version=" + tc.version, - api.PluginTypeCredential.String(), pluginName, - }) - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, combined) - } - - if !strings.Contains(combined, pluginName) { - t.Errorf("expected %q to contain %q", combined, pluginName) - } - if !strings.Contains(combined, tc.expectedSHA) { - t.Errorf("expected %q to contain %q", combined, tc.expectedSHA) - } - if !strings.Contains(combined, tc.version) { - t.Errorf("expected %q to contain %q", combined, tc.version) - } - }) - } - }) - - t.Run("field", func(t *testing.T) { - t.Parallel() - - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - pluginName := "my-plugin" - testPluginCreateAndRegister(t, client, pluginDir, pluginName, api.PluginTypeCredential, "") - - ui, cmd := testPluginInfoCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-field", "builtin", - api.PluginTypeCredential.String(), pluginName, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if exp := "false"; combined != exp { - t.Errorf("expected %q to be %q", combined, exp) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPluginInfoCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - api.PluginTypeCredential.String(), "my-plugin", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error reading plugin named my-plugin: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPluginInfoCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/plugin_list_test.go b/command/plugin_list_test.go deleted file mode 100644 index 395167e45..000000000 --- a/command/plugin_list_test.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "regexp" - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testPluginListCommand(tb testing.TB) (*cli.MockUi, *PluginListCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PluginListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPluginListCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo", "fizz"}, - "Too many arguments", - 1, - }, - { - "lists", - nil, - "Name\\s+Type\\s+Version", - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPluginListCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - matcher := regexp.MustCompile(tc.out) - if !matcher.MatchString(combined) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPluginListCommand(t) - cmd.client = client - - code := cmd.Run([]string{"database"}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error listing available plugins: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPluginListCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/plugin_register_test.go b/command/plugin_register_test.go deleted file mode 100644 index e585839cb..000000000 --- a/command/plugin_register_test.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "reflect" - "sort" - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/mitchellh/cli" -) - -func testPluginRegisterCommand(tb testing.TB) (*cli.MockUi, *PluginRegisterCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PluginRegisterCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPluginRegisterCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - nil, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar", "fizz"}, - "Too many arguments", - 1, - }, - { - "not_a_plugin", - []string{consts.PluginTypeCredential.String(), "nope_definitely_never_a_plugin_nope"}, - "", - 2, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPluginRegisterCommand(t) - cmd.client = client - - args := append([]string{"-sha256", "abcd1234"}, tc.args...) - code := cmd.Run(args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - pluginName := "my-plugin" - _, sha256Sum := testPluginCreate(t, pluginDir, pluginName) - - ui, cmd := testPluginRegisterCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-sha256", sha256Sum, - consts.PluginTypeCredential.String(), pluginName, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Registered plugin: my-plugin" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{ - Type: api.PluginTypeCredential, - }) - if err != nil { - t.Fatal(err) - } - - found := false - for _, plugins := range resp.PluginsByType { - for _, p := range plugins { - if p == pluginName { - found = true - } - } - } - if !found { - t.Errorf("expected %q to be in %q", pluginName, resp.PluginsByType) - } - }) - - t.Run("integration with version", func(t *testing.T) { - t.Parallel() - - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - const pluginName = "my-plugin" - versions := []string{"v1.0.0", "v2.0.1"} - _, sha256Sum := testPluginCreate(t, pluginDir, pluginName) - types := []api.PluginType{api.PluginTypeCredential, api.PluginTypeDatabase, api.PluginTypeSecrets} - - for _, typ := range types { - for _, version := range versions { - ui, cmd := testPluginRegisterCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-version=" + version, - "-sha256=" + sha256Sum, - typ.String(), - pluginName, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Registered plugin: my-plugin" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - } - } - - resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{ - Type: api.PluginTypeUnknown, - }) - if err != nil { - t.Fatal(err) - } - - found := make(map[api.PluginType]int) - versionsFound := make(map[api.PluginType][]string) - for _, p := range resp.Details { - if p.Name == pluginName { - typ, err := api.ParsePluginType(p.Type) - if err != nil { - t.Fatal(err) - } - found[typ]++ - versionsFound[typ] = append(versionsFound[typ], p.Version) - } - } - - for _, typ := range types { - if found[typ] != 2 { - t.Fatalf("expected %q to be found 2 times, but found it %d times for %s type in %#v", pluginName, found[typ], typ.String(), resp.Details) - } - sort.Strings(versions) - sort.Strings(versionsFound[typ]) - if !reflect.DeepEqual(versions, versionsFound[typ]) { - t.Fatalf("expected %v versions but got %v", versions, versionsFound[typ]) - } - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPluginRegisterCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-sha256", "abcd1234", - consts.PluginTypeCredential.String(), "my-plugin", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error registering plugin my-plugin:" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPluginRegisterCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/plugin_reload_test.go b/command/plugin_reload_test.go deleted file mode 100644 index cf9f2d149..000000000 --- a/command/plugin_reload_test.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/mitchellh/cli" -) - -func testPluginReloadCommand(tb testing.TB) (*cli.MockUi, *PluginReloadCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PluginReloadCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func testPluginReloadStatusCommand(tb testing.TB) (*cli.MockUi, *PluginReloadStatusCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PluginReloadStatusCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPluginReloadCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - nil, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"-plugin", "foo", "-mounts", "bar"}, - "Too many arguments", - 1, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPluginReloadCommand(t) - cmd.client = client - - args := append([]string{}, tc.args...) - code := cmd.Run(args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - pluginName := "my-plugin" - _, sha256Sum := testPluginCreateAndRegister(t, client, pluginDir, pluginName, api.PluginTypeCredential, "") - - ui, cmd := testPluginReloadCommand(t) - cmd.client = client - - if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{ - Name: pluginName, - Type: api.PluginTypeCredential, - Command: pluginName, - SHA256: sha256Sum, - }); err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "-plugin", pluginName, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Reloaded plugin: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) -} - -func TestPluginReloadStatusCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - nil, - "Not enough arguments", - 1, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPluginReloadCommand(t) - cmd.client = client - - args := append([]string{}, tc.args...) - code := cmd.Run(args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } -} diff --git a/command/plugin_test.go b/command/plugin_test.go deleted file mode 100644 index 2e72bb7c1..000000000 --- a/command/plugin_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "crypto/sha256" - "fmt" - "io" - "io/ioutil" - "os" - "testing" - - "github.com/hashicorp/vault/api" -) - -// testPluginCreate creates a sample plugin in a tempdir and returns the shasum -// and filepath to the plugin. -func testPluginCreate(tb testing.TB, dir, name string) (string, string) { - tb.Helper() - - pth := dir + "/" + name - if err := ioutil.WriteFile(pth, nil, 0o755); err != nil { - tb.Fatal(err) - } - - f, err := os.Open(pth) - if err != nil { - tb.Fatal(err) - } - defer f.Close() - - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - tb.Fatal(err) - } - sha256Sum := fmt.Sprintf("%x", h.Sum(nil)) - - return pth, sha256Sum -} - -// testPluginCreateAndRegister creates a plugin and registers it in the catalog. -func testPluginCreateAndRegister(tb testing.TB, client *api.Client, dir, name string, pluginType api.PluginType, version string) (string, string) { - tb.Helper() - - pth, sha256Sum := testPluginCreate(tb, dir, name) - - if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{ - Name: name, - Type: pluginType, - Command: name, - SHA256: sha256Sum, - Version: version, - }); err != nil { - tb.Fatal(err) - } - - return pth, sha256Sum -} - -// testPluginCreateAndRegisterVersioned creates a versioned plugin and registers it in the catalog. -func testPluginCreateAndRegisterVersioned(tb testing.TB, client *api.Client, dir, name string, pluginType api.PluginType) (string, string, string) { - tb.Helper() - - pth, sha256Sum := testPluginCreate(tb, dir, name) - - if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{ - Name: name, - Type: pluginType, - Command: name, - SHA256: sha256Sum, - Version: "v1.0.0", - }); err != nil { - tb.Fatal(err) - } - - return pth, sha256Sum, "v1.0.0" -} diff --git a/command/policy_delete_test.go b/command/policy_delete_test.go deleted file mode 100644 index 7c96d2415..000000000 --- a/command/policy_delete_test.go +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "reflect" - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testPolicyDeleteCommand(tb testing.TB) (*cli.MockUi, *PolicyDeleteCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PolicyDeleteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPolicyDeleteCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - nil, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyDeleteCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - policy := `path "secret/" {}` - if err := client.Sys().PutPolicy("my-policy", policy); err != nil { - t.Fatal(err) - } - - ui, cmd := testPolicyDeleteCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-policy", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Deleted policy: my-policy" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - policies, err := client.Sys().ListPolicies() - if err != nil { - t.Fatal(err) - } - - list := []string{"default", "root"} - if !reflect.DeepEqual(policies, list) { - t.Errorf("expected %q to be %q", policies, list) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPolicyDeleteCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-policy", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error deleting my-policy: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPolicyDeleteCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/policy_fmt_test.go b/command/policy_fmt_test.go deleted file mode 100644 index 25a1565df..000000000 --- a/command/policy_fmt_test.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testPolicyFmtCommand(tb testing.TB) (*cli.MockUi, *PolicyFmtCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PolicyFmtCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPolicyFmtCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyFmtCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("default", func(t *testing.T) { - t.Parallel() - - policy := strings.TrimSpace(` -path "secret" { - capabilities = ["create", "update","delete"] - -} -`) - - f, err := ioutil.TempFile("", "") - if err != nil { - t.Fatal(err) - } - defer os.Remove(f.Name()) - if _, err := f.Write([]byte(policy)); err != nil { - t.Fatal(err) - } - f.Close() - - client, closer := testVaultServer(t) - defer closer() - - _, cmd := testPolicyFmtCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - f.Name(), - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := strings.TrimSpace(` -path "secret" { - capabilities = ["create", "update", "delete"] -} -`) + "\n" - - contents, err := ioutil.ReadFile(f.Name()) - if err != nil { - t.Fatal(err) - } - if string(contents) != expected { - t.Errorf("expected %q to be %q", string(contents), expected) - } - }) - - t.Run("bad_hcl", func(t *testing.T) { - t.Parallel() - - policy := `dafdaf` - - f, err := ioutil.TempFile("", "") - if err != nil { - t.Fatal(err) - } - defer os.Remove(f.Name()) - if _, err := f.Write([]byte(policy)); err != nil { - t.Fatal(err) - } - f.Close() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyFmtCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - f.Name(), - }) - if exp := 1; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - stderr := ui.ErrorWriter.String() - expected := "failed to parse policy" - if !strings.Contains(stderr, expected) { - t.Errorf("expected %q to include %q", stderr, expected) - } - }) - - t.Run("bad_policy", func(t *testing.T) { - t.Parallel() - - policy := `banana "foo" {}` - - f, err := ioutil.TempFile("", "") - if err != nil { - t.Fatal(err) - } - defer os.Remove(f.Name()) - if _, err := f.Write([]byte(policy)); err != nil { - t.Fatal(err) - } - f.Close() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyFmtCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - f.Name(), - }) - if exp := 1; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - stderr := ui.ErrorWriter.String() - expected := "failed to parse policy" - if !strings.Contains(stderr, expected) { - t.Errorf("expected %q to include %q", stderr, expected) - } - }) - - t.Run("bad_policy", func(t *testing.T) { - t.Parallel() - - policy := `path "secret/" { capabilities = ["bogus"] }` - - f, err := ioutil.TempFile("", "") - if err != nil { - t.Fatal(err) - } - defer os.Remove(f.Name()) - if _, err := f.Write([]byte(policy)); err != nil { - t.Fatal(err) - } - f.Close() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyFmtCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - f.Name(), - }) - if exp := 1; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - stderr := ui.ErrorWriter.String() - expected := "failed to parse policy" - if !strings.Contains(stderr, expected) { - t.Errorf("expected %q to include %q", stderr, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPolicyFmtCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/policy_list_test.go b/command/policy_list_test.go deleted file mode 100644 index 0626f3a1b..000000000 --- a/command/policy_list_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testPolicyListCommand(tb testing.TB) (*cli.MockUi, *PolicyListCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PolicyListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPolicyListCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyListCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("default", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyListCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "default\nroot" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPolicyListCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error listing policies: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPolicyListCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/policy_read_test.go b/command/policy_read_test.go deleted file mode 100644 index 84ec9f9cf..000000000 --- a/command/policy_read_test.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testPolicyReadCommand(tb testing.TB) (*cli.MockUi, *PolicyReadCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PolicyReadCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestPolicyReadCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - { - "no_policy_exists", - []string{"not-a-real-policy"}, - "No policy named", - 2, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyReadCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("default", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - policy := `path "secret/" {}` - if err := client.Sys().PutPolicy("my-policy", policy); err != nil { - t.Fatal(err) - } - - ui, cmd := testPolicyReadCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-policy", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, policy) { - t.Errorf("expected %q to contain %q", combined, policy) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPolicyReadCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-policy", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error reading policy named my-policy: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPolicyReadCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/policy_write_test.go b/command/policy_write_test.go deleted file mode 100644 index 57ace717d..000000000 --- a/command/policy_write_test.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "io" - "io/ioutil" - "os" - "reflect" - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testPolicyWriteCommand(tb testing.TB) (*cli.MockUi, *PolicyWriteCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &PolicyWriteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func testPolicyWritePolicyContents(tb testing.TB) []byte { - return bytes.TrimSpace([]byte(` -path "secret/" { - capabilities = ["read"] -} - `)) -} - -func TestPolicyWriteCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo", "bar", "baz"}, - "Too many arguments", - 1, - }, - { - "not_enough_args", - []string{"foo"}, - "Not enough arguments", - 1, - }, - { - "bad_file", - []string{"my-policy", "/not/a/real/path.hcl"}, - "Error opening policy file", - 2, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyWriteCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("file", func(t *testing.T) { - t.Parallel() - - policy := testPolicyWritePolicyContents(t) - f, err := ioutil.TempFile("", "vault-policy-write") - if err != nil { - t.Fatal(err) - } - if _, err := f.Write(policy); err != nil { - t.Fatal(err) - } - if err := f.Close(); err != nil { - t.Fatal(err) - } - defer os.Remove(f.Name()) - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyWriteCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-policy", f.Name(), - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Uploaded policy: my-policy" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - policies, err := client.Sys().ListPolicies() - if err != nil { - t.Fatal(err) - } - - list := []string{"default", "my-policy", "root"} - if !reflect.DeepEqual(policies, list) { - t.Errorf("expected %q to be %q", policies, list) - } - }) - - t.Run("stdin", func(t *testing.T) { - t.Parallel() - - stdinR, stdinW := io.Pipe() - go func() { - policy := testPolicyWritePolicyContents(t) - stdinW.Write(policy) - stdinW.Close() - }() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testPolicyWriteCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "my-policy", "-", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Uploaded policy: my-policy" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - policies, err := client.Sys().ListPolicies() - if err != nil { - t.Fatal(err) - } - - list := []string{"default", "my-policy", "root"} - if !reflect.DeepEqual(policies, list) { - t.Errorf("expected %q to be %q", policies, list) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testPolicyWriteCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-policy", "-", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error uploading policy: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testPolicyWriteCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/proxy/config/config_test.go b/command/proxy/config/config_test.go deleted file mode 100644 index c6b631df2..000000000 --- a/command/proxy/config/config_test.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package config - -import ( - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/command/agentproxyshared" - "github.com/hashicorp/vault/internalshared/configutil" -) - -// TestLoadConfigFile_ProxyCache tests loading a config file containing a cache -// as well as a valid proxy config. -func TestLoadConfigFile_ProxyCache(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config-cache.hcl") - if err != nil { - t.Fatal(err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - PidFile: "./pidfile", - Listeners: []*configutil.Listener{ - { - Type: "unix", - Address: "/path/to/socket", - TLSDisable: true, - SocketMode: "configmode", - SocketUser: "configuser", - SocketGroup: "configgroup", - }, - { - Type: "tcp", - Address: "127.0.0.1:8300", - TLSDisable: true, - }, - { - Type: "tcp", - Address: "127.0.0.1:3000", - Role: "metrics_only", - TLSDisable: true, - }, - { - Type: "tcp", - Role: "default", - Address: "127.0.0.1:8400", - TLSKeyFile: "/path/to/cakey.pem", - TLSCertFile: "/path/to/cacert.pem", - }, - }, - }, - AutoAuth: &AutoAuth{ - Method: &Method{ - Type: "aws", - MountPath: "auth/aws", - Config: map[string]interface{}{ - "role": "foobar", - }, - }, - Sinks: []*Sink{ - { - Type: "file", - DHType: "curve25519", - DHPath: "/tmp/file-foo-dhpath", - AAD: "foobar", - Config: map[string]interface{}{ - "path": "/tmp/file-foo", - }, - }, - }, - }, - APIProxy: &APIProxy{ - EnforceConsistency: "always", - WhenInconsistent: "retry", - UseAutoAuthTokenRaw: true, - UseAutoAuthToken: true, - ForceAutoAuthToken: false, - }, - Cache: &Cache{ - Persist: &agentproxyshared.PersistConfig{ - Type: "kubernetes", - Path: "/vault/agent-cache/", - KeepAfterImport: true, - ExitOnErr: true, - ServiceAccountTokenFile: "/tmp/serviceaccount/token", - }, - }, - Vault: &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", - Retry: &Retry{ - NumRetries: 12, - }, - }, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } - - config, err = LoadConfigFile("./test-fixtures/config-cache-embedded-type.hcl") - if err != nil { - t.Fatal(err) - } - expected.Vault.TLSSkipVerifyRaw = interface{}(true) - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} diff --git a/command/proxy/config/test-fixtures/config-cache-embedded-type.hcl b/command/proxy/config/test-fixtures/config-cache-embedded-type.hcl deleted file mode 100644 index cd953e7f3..000000000 --- a/command/proxy/config/test-fixtures/config-cache-embedded-type.hcl +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -api_proxy { - use_auto_auth_token = true - enforce_consistency = "always" - when_inconsistent = "retry" -} - -cache { - persist "kubernetes" { - path = "/vault/agent-cache/" - keep_after_import = true - exit_on_err = true - service_account_token_file = "/tmp/serviceaccount/token" - } -} - -listener { - type = "unix" - address = "/path/to/socket" - tls_disable = true - socket_mode = "configmode" - socket_user = "configuser" - socket_group = "configgroup" -} - -listener { - type = "tcp" - address = "127.0.0.1:8300" - tls_disable = true -} - -listener { - type = "tcp" - address = "127.0.0.1:3000" - tls_disable = true - role = "metrics_only" -} - -listener { - type = "tcp" - role = "default" - address = "127.0.0.1:8400" - tls_key_file = "/path/to/cakey.pem" - tls_cert_file = "/path/to/cacert.pem" -} - -vault { - address = "http://127.0.0.1:1111" - ca_cert = "config_ca_cert" - ca_path = "config_ca_path" - tls_skip_verify = true - client_cert = "config_client_cert" - client_key = "config_client_key" -} diff --git a/command/proxy/config/test-fixtures/config-cache.hcl b/command/proxy/config/test-fixtures/config-cache.hcl deleted file mode 100644 index caf153479..000000000 --- a/command/proxy/config/test-fixtures/config-cache.hcl +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -pid_file = "./pidfile" - -auto_auth { - method { - type = "aws" - config = { - role = "foobar" - } - } - - sink { - type = "file" - config = { - path = "/tmp/file-foo" - } - aad = "foobar" - dh_type = "curve25519" - dh_path = "/tmp/file-foo-dhpath" - } -} - -api_proxy { - use_auto_auth_token = true - enforce_consistency = "always" - when_inconsistent = "retry" -} - -cache { - persist = { - type = "kubernetes" - path = "/vault/agent-cache/" - keep_after_import = true - exit_on_err = true - service_account_token_file = "/tmp/serviceaccount/token" - } -} - -listener "unix" { - address = "/path/to/socket" - tls_disable = true - socket_mode = "configmode" - socket_user = "configuser" - socket_group = "configgroup" -} - -listener "tcp" { - address = "127.0.0.1:8300" - tls_disable = true -} - -listener { - type = "tcp" - address = "127.0.0.1:3000" - tls_disable = true - role = "metrics_only" -} - -listener "tcp" { - role = "default" - address = "127.0.0.1:8400" - tls_key_file = "/path/to/cakey.pem" - tls_cert_file = "/path/to/cacert.pem" -} - -vault { - address = "http://127.0.0.1:1111" - ca_cert = "config_ca_cert" - ca_path = "config_ca_path" - tls_skip_verify = "true" - client_cert = "config_client_cert" - client_key = "config_client_key" -} diff --git a/command/proxy/test-fixtures/reload/reload_bar.key b/command/proxy/test-fixtures/reload/reload_bar.key deleted file mode 100644 index 10849fbe1..000000000 --- a/command/proxy/test-fixtures/reload/reload_bar.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAwF7sRAyUiLcd6es6VeaTRUBOusFFGkmKJ5lU351waCJqXFju -Z6i/SQYNAAnnRgotXSTE1fIPjE2kZNH1hvqE5IpTGgAwy50xpjJrrBBI6e9lyKqj -7T8gLVNBvtC0cpQi+pGrszEI0ckDQCSZHqi/PAzcpmLUgh2KMrgagT+YlN35KHtl -/bQ/Fsn+kqykVqNw69n/CDKNKdDHn1qPwiX9q/fTMj3EG6g+3ntKrUOh8V/gHKPz -q8QGP/wIud2K+tTSorVXr/4zx7xgzlbJkCakzcQQiP6K+paPnDRlE8fK+1gRRyR7 -XCzyp0irUl8G1NjYAR/tVWxiUhlk/jZutb8PpwIDAQABAoIBAEOzJELuindyujxQ -ZD9G3h1I/GwNCFyv9Mbq10u7BIwhUH0fbwdcA7WXQ4v38ERd4IkfH4aLoZ0m1ewF -V/sgvxQO+h/0YTfHImny5KGxOXfaoF92bipYROKuojydBmQsbgLwsRRm9UufCl3Q -g3KewG5JuH112oPQEYq379v8nZ4FxC3Ano1OFBTm9UhHIAX1Dn22kcHOIIw8jCsQ -zp7TZOW+nwtkS41cBwhvV4VIeL6yse2UgbOfRVRwI7B0OtswS5VgW3wysO2mTDKt -V/WCmeht1il/6ZogEHgi/mvDCKpj20wQ1EzGnPdFLdiFJFylf0oufQD/7N/uezbC -is0qJEECgYEA3AE7SeLpe3SZApj2RmE2lcD9/Saj1Y30PznxB7M7hK0sZ1yXEbtS -Qf894iDDD/Cn3ufA4xk/K52CXgAcqvH/h2geG4pWLYsT1mdWhGftprtOMCIvJvzU -8uWJzKdOGVMG7R59wNgEpPDZDpBISjexwQsFo3aw1L/H1/Sa8cdY3a0CgYEA39hB -1oLmGRyE32Q4GF/srG4FqKL1EsbISGDUEYTnaYg2XiM43gu3tC/ikfclk27Jwc2L -m7cA5FxxaEyfoOgfAizfU/uWTAbx9GoXgWsO0hWSN9+YNq61gc5WKoHyrJ/rfrti -y5d7k0OCeBxckLqGDuJqICQ0myiz0El6FU8h5SMCgYEAuhigmiNC9JbwRu40g9v/ -XDVfox9oPmBRVpogdC78DYKeqN/9OZaGQiUxp3GnDni2xyqqUm8srCwT9oeJuF/z -kgpUTV96/hNCuH25BU8UC5Es1jJUSFpdlwjqwx5SRcGhfjnojZMseojwUg1h2MW7 -qls0bc0cTxnaZaYW2qWRWhECgYBrT0cwyQv6GdvxJCBoPwQ9HXmFAKowWC+H0zOX -Onmd8/jsZEJM4J0uuo4Jn8vZxBDg4eL9wVuiHlcXwzP7dYv4BP8DSechh2rS21Ft -b59pQ4IXWw+jl1nYYsyYEDgAXaIN3VNder95N7ICVsZhc6n01MI/qlu1zmt1fOQT -9x2utQKBgHI9SbsfWfbGiu6oLS3+9V1t4dORhj8D8b7z3trvECrD6tPhxoZqtfrH -4apKr3OKRSXk3K+1K6pkMHJHunspucnA1ChXLhzfNF08BSRJkQDGYuaRLS6VGgab -JZTl54bGvO1GkszEBE/9QFcqNVtWGMWXnUPwNNv8t//yJT5rvQil ------END RSA PRIVATE KEY----- diff --git a/command/proxy/test-fixtures/reload/reload_bar.pem b/command/proxy/test-fixtures/reload/reload_bar.pem deleted file mode 100644 index a8217be5c..000000000 --- a/command/proxy/test-fixtures/reload/reload_bar.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDQzCCAiugAwIBAgIULLCz3mZKmg2xy3rWCud0f1zcmBwwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMzEwMDIzNjQ0WhcNMzYw -MzA1MDEzNzE0WjAaMRgwFgYDVQQDEw9iYXIuZXhhbXBsZS5jb20wggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAXuxEDJSItx3p6zpV5pNFQE66wUUaSYon -mVTfnXBoImpcWO5nqL9JBg0ACedGCi1dJMTV8g+MTaRk0fWG+oTkilMaADDLnTGm -MmusEEjp72XIqqPtPyAtU0G+0LRylCL6kauzMQjRyQNAJJkeqL88DNymYtSCHYoy -uBqBP5iU3fkoe2X9tD8Wyf6SrKRWo3Dr2f8IMo0p0MefWo/CJf2r99MyPcQbqD7e -e0qtQ6HxX+Aco/OrxAY//Ai53Yr61NKitVev/jPHvGDOVsmQJqTNxBCI/or6lo+c -NGUTx8r7WBFHJHtcLPKnSKtSXwbU2NgBH+1VbGJSGWT+Nm61vw+nAgMBAAGjgYQw -gYEwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSVoF8F -7qbzSryIFrldurAG78LvSjAfBgNVHSMEGDAWgBRzDNvqF/Tq21OgWs13B5YydZjl -vzAgBgNVHREEGTAXgg9iYXIuZXhhbXBsZS5jb22HBH8AAAEwDQYJKoZIhvcNAQEL -BQADggEBAGmz2N282iT2IaEZvOmzIE4znHGkvoxZmrr/2byq5PskBg9ysyCHfUvw -SFA8U7jWjezKTnGRUu5blB+yZdjrMtB4AePWyEqtkJwVsZ2SPeP+9V2gNYK4iktP -UF3aIgBbAbw8rNuGIIB0T4D+6Zyo9Y3MCygs6/N4bRPZgLhewWn1ilklfnl3eqaC -a+JY1NBuTgCMa28NuC+Hy3mCveqhI8tFNiOthlLdgAEbuQaOuNutAG73utZ2aq6Q -W4pajFm3lEf5zt7Lo6ZCFtY/Q8jjURJ9e4O7VjXcqIhBM5bSMI6+fgQyOH0SLboj -RNanJ2bcyF1iPVyPBGzV3dF0ngYzxEY= ------END CERTIFICATE----- diff --git a/command/proxy/test-fixtures/reload/reload_ca.pem b/command/proxy/test-fixtures/reload/reload_ca.pem deleted file mode 100644 index 72a74440c..000000000 --- a/command/proxy/test-fixtures/reload/reload_ca.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDNTCCAh2gAwIBAgIUBeVo+Ce2BrdRT1cogKvJLtdOky8wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMzEwMDIzNTM4WhcNMzYw -MzA1MDIzNjA4WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAPTQGWPRIOECGeJB6tR/ftvvtioC9f84fY2QdJ5k -JBupXjPAGYKgS4MGzyT5bz9yY400tCtmh6h7p9tZwHl/TElTugtLQ/8ilMbJTiOM -SiyaMDPHiMJJYKTjm9bu6bKeU1qPZ0Cryes4rygbqs7w2XPgA2RxNmDh7JdX7/h+ -VB5onBmv8g4WFSayowGyDcJWWCbu5yv6ZdH1bqQjgRzQ5xp17WXNmvlzdp2vate/ -9UqPdA8sdJzW/91Gvmros0o/FnG7c2pULhk22wFqO8t2HRjKb3nuxALEJvqoPvad -KjpDTaq1L1ZzxcB7wvWyhy/lNLZL7jiNWy0mN1YB0UpSWdECAwEAAaN7MHkwDgYD -VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHMM2+oX9Orb -U6BazXcHljJ1mOW/MB8GA1UdIwQYMBaAFHMM2+oX9OrbU6BazXcHljJ1mOW/MBYG -A1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAp17XsOaT9 -hculRqrFptn3+zkH3HrIckHm+28R5xYT8ASFXFcLFugGizJAXVL5lvsRVRIwCoOX -Nhi8XSNEFP640VbHcEl81I84bbRIIDS+Yheu6JDZGemTaDYLv1J3D5SHwgoM+nyf -oTRgotUCIXcwJHmTpWEUkZFKuqBxsoTGzk0jO8wOP6xoJkzxVVG5PvNxs924rxY8 -Y8iaLdDfMeT7Pi0XIliBa/aSp/iqSW8XKyJl5R5vXg9+DOgZUrVzIxObaF5RBl/a -mJOeklJBdNVzQm5+iMpO42lu0TA9eWtpP+YiUEXU17XDvFeQWOocFbQ1Peo0W895 -XRz2GCwCNyvW ------END CERTIFICATE----- diff --git a/command/proxy/test-fixtures/reload/reload_foo.key b/command/proxy/test-fixtures/reload/reload_foo.key deleted file mode 100644 index 86e6cce63..000000000 --- a/command/proxy/test-fixtures/reload/reload_foo.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpgIBAAKCAQEAzNyVieSti9XBb5/celB5u8YKRJv3mQS9A4/X0mqY1ePznt1i -ilG7OmG0yM2VAk0ceIAQac3Bsn74jxn2cDlrrVniPXcNgYtMtW0kRqNEo4doo4EX -xZguS9vNBu29useHhif1TGX/pA3dgvaVycUCjzTEVk6qI8UEehMK6gEGZb7nOr0A -A9nipSqoeHpDLe3a4KVqj1vtlJKUvD2i1MuBuQ130cB1K9rufLCShGu7mEgzEosc -gr+K3Bf03IejbeVRyIfLtgj1zuvV1katec75UqRA/bsvt5G9JfJqiZ9mwFN0vp3g -Cr7pdQBSBQ2q4yf9s8CuY5c5w9fl3F8f5QFQoQIDAQABAoIBAQCbCb1qNFRa5ZSV -I8i6ELlwMDqJHfhOJ9XcIjpVljLAfNlcu3Ld92jYkCU/asaAjVckotbJG9yhd5Io -yp9E40/oS4P6vGTOS1vsWgMAKoPBtrKsOwCAm+E9q8UIn1fdSS/5ibgM74x+3bds -a62Em8KKGocUQkhk9a+jq1GxMsFisbHRxEHvClLmDMgGnW3FyGmWwT6yZLPSC0ey -szmmjt3ouP8cLAOmSjzcQBMmEZpQMCgR6Qckg6nrLQAGzZyTdCd875wbGA57DpWX -Lssn95+A5EFvr/6b7DkXeIFCrYBFFa+UQN3PWGEQ6Zjmiw4VgV2vO8yX2kCLlUhU -02bL393ZAoGBAPXPD/0yWINbKUPcRlx/WfWQxfz0bu50ytwIXzVK+pRoAMuNqehK -BJ6kNzTTBq40u+IZ4f5jbLDulymR+4zSkirLE7CyWFJOLNI/8K4Pf5DJUgNdrZjJ -LCtP9XRdxiPatQF0NGfdgHlSJh+/CiRJP4AgB17AnB/4z9/M0ZlJGVrzAoGBANVa -69P3Rp/WPBQv0wx6f0tWppJolWekAHKcDIdQ5HdOZE5CPAYSlTrTUW3uJuqMwU2L -M0Er2gIPKWIR5X+9r7Fvu9hQW6l2v3xLlcrGPiapp3STJvuMxzhRAmXmu3bZfVn1 -Vn7Vf1jPULHtTFSlNFEvYG5UJmygK9BeyyVO5KMbAoGBAMCyAibLQPg4jrDUDZSV -gUAwrgUO2ae1hxHWvkxY6vdMUNNByuB+pgB3W4/dnm8Sh/dHsxJpftt1Lqs39ar/ -p/ZEHLt4FCTxg9GOrm7FV4t5RwG8fko36phJpnIC0UFqQltRbYO+8OgqrhhU+u5X -PaCDe0OcWsf1lYAsYGN6GpZhAoGBAMJ5Ksa9+YEODRs1cIFKUyd/5ztC2xRqOAI/ -3WemQ2nAacuvsfizDZVeMzYpww0+maAuBt0btI719PmwaGmkpDXvK+EDdlmkpOwO -FY6MXvBs6fdnfjwCWUErDi2GQFAX9Jt/9oSL5JU1+08DhvUM1QA/V/2Y9KFE6kr3 -bOIn5F4LAoGBAKQzH/AThDGhT3hwr4ktmReF3qKxBgxzjVa8veXtkY5VWwyN09iT -jnTTt6N1CchZoK5WCETjdzNYP7cuBTcV4d3bPNRiJmxXaNVvx3Tlrk98OiffT8Qa -5DO/Wfb43rNHYXBjU6l0n2zWcQ4PUSSbu0P0bM2JTQPRCqSthXvSHw2P ------END RSA PRIVATE KEY----- diff --git a/command/proxy/test-fixtures/reload/reload_foo.pem b/command/proxy/test-fixtures/reload/reload_foo.pem deleted file mode 100644 index c8b868bcd..000000000 --- a/command/proxy/test-fixtures/reload/reload_foo.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDQzCCAiugAwIBAgIUFVW6i/M+yJUsDrXWgRKO/Dnb+L4wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMzEwMDIzNjA1WhcNMzYw -MzA1MDEzNjM1WjAaMRgwFgYDVQQDEw9mb28uZXhhbXBsZS5jb20wggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM3JWJ5K2L1cFvn9x6UHm7xgpEm/eZBL0D -j9fSapjV4/Oe3WKKUbs6YbTIzZUCTRx4gBBpzcGyfviPGfZwOWutWeI9dw2Bi0y1 -bSRGo0Sjh2ijgRfFmC5L280G7b26x4eGJ/VMZf+kDd2C9pXJxQKPNMRWTqojxQR6 -EwrqAQZlvuc6vQAD2eKlKqh4ekMt7drgpWqPW+2UkpS8PaLUy4G5DXfRwHUr2u58 -sJKEa7uYSDMSixyCv4rcF/Tch6Nt5VHIh8u2CPXO69XWRq15zvlSpED9uy+3kb0l -8mqJn2bAU3S+neAKvul1AFIFDarjJ/2zwK5jlznD1+XcXx/lAVChAgMBAAGjgYQw -gYEwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRNJoOJ -dnazDiuqLhV6truQ4cRe9jAfBgNVHSMEGDAWgBRzDNvqF/Tq21OgWs13B5YydZjl -vzAgBgNVHREEGTAXgg9mb28uZXhhbXBsZS5jb22HBH8AAAEwDQYJKoZIhvcNAQEL -BQADggEBAHzv67mtbxMWcuMsxCFBN1PJNAyUDZVCB+1gWhk59EySbVg81hWJDCBy -fl3TKjz3i7wBGAv+C2iTxmwsSJbda22v8JQbuscXIfLFbNALsPzF+J0vxAgJs5Gc -sDbfJ7EQOIIOVKQhHLYnQoLnigSSPc1kd0JjYyHEBjgIaSuXgRRTBAeqLiBMx0yh -RKL1lQ+WoBU/9SXUZZkwokqWt5G7khi5qZkNxVXZCm8VGPg0iywf6gGyhI1SU5S2 -oR219S6kA4JY/stw1qne85/EmHmoImHGt08xex3GoU72jKAjsIpqRWopcD/+uene -Tc9nn3fTQW/Z9fsoJ5iF5OdJnDEswqE= ------END CERTIFICATE----- diff --git a/command/proxy_test.go b/command/proxy_test.go deleted file mode 100644 index 83103ce63..000000000 --- a/command/proxy_test.go +++ /dev/null @@ -1,1274 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "net/http" - "os" - "path/filepath" - "reflect" - "strings" - "sync" - "testing" - "time" - - "github.com/hashicorp/go-hclog" - vaultjwt "github.com/hashicorp/vault-plugin-auth-jwt" - logicalKv "github.com/hashicorp/vault-plugin-secrets-kv" - "github.com/hashicorp/vault/api" - credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - "github.com/hashicorp/vault/command/agent" - proxyConfig "github.com/hashicorp/vault/command/proxy/config" - "github.com/hashicorp/vault/helper/useragent" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/cli" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func testProxyCommand(tb testing.TB, logger hclog.Logger) (*cli.MockUi, *ProxyCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &ProxyCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - ShutdownCh: MakeShutdownCh(), - SighupCh: MakeSighupCh(), - logger: logger, - startedCh: make(chan struct{}, 5), - reloadedCh: make(chan struct{}, 5), - } -} - -// TestProxy_ExitAfterAuth tests the exit_after_auth flag, provided both -// as config and via -exit-after-auth. -func TestProxy_ExitAfterAuth(t *testing.T) { - t.Run("via_config", func(t *testing.T) { - testProxyExitAfterAuth(t, false) - }) - - t.Run("via_flag", func(t *testing.T) { - testProxyExitAfterAuth(t, true) - }) -} - -func testProxyExitAfterAuth(t *testing.T, viaFlag bool) { - logger := logging.NewVaultLogger(hclog.Trace) - coreConfig := &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "jwt": vaultjwt.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - client := cluster.Cores[0].Client - - // Setup Vault - err := client.Sys().EnableAuthWithOptions("jwt", &api.EnableAuthOptions{ - Type: "jwt", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/jwt/config", map[string]interface{}{ - "bound_issuer": "https://team-vault.auth0.com/", - "jwt_validation_pubkeys": agent.TestECDSAPubKey, - "jwt_supported_algs": "ES256", - }) - if err != nil { - t.Fatal(err) - } - - _, err = client.Logical().Write("auth/jwt/role/test", map[string]interface{}{ - "role_type": "jwt", - "bound_subject": "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", - "bound_audiences": "https://vault.plugin.auth.jwt.test", - "user_claim": "https://vault/user", - "groups_claim": "https://vault/groups", - "policies": "test", - "period": "3s", - }) - if err != nil { - t.Fatal(err) - } - - dir := t.TempDir() - inf, err := os.CreateTemp(dir, "auth.jwt.test.") - if err != nil { - t.Fatal(err) - } - in := inf.Name() - inf.Close() - // We remove these files in this test since we don't need the files, we just need - // a non-conflicting file name for the config. - os.Remove(in) - t.Logf("input: %s", in) - - sink1f, err := os.CreateTemp(dir, "sink1.jwt.test.") - if err != nil { - t.Fatal(err) - } - sink1 := sink1f.Name() - sink1f.Close() - os.Remove(sink1) - t.Logf("sink1: %s", sink1) - - sink2f, err := os.CreateTemp(dir, "sink2.jwt.test.") - if err != nil { - t.Fatal(err) - } - sink2 := sink2f.Name() - sink2f.Close() - os.Remove(sink2) - t.Logf("sink2: %s", sink2) - - conff, err := os.CreateTemp(dir, "conf.jwt.test.") - if err != nil { - t.Fatal(err) - } - conf := conff.Name() - conff.Close() - os.Remove(conf) - t.Logf("config: %s", conf) - - jwtToken, _ := agent.GetTestJWT(t) - if err := os.WriteFile(in, []byte(jwtToken), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test jwt", "path", in) - } - - exitAfterAuthTemplText := "exit_after_auth = true" - if viaFlag { - exitAfterAuthTemplText = "" - } - - config := ` -%s - -auto_auth { - method { - type = "jwt" - config = { - role = "test" - path = "%s" - } - } - - sink { - type = "file" - config = { - path = "%s" - } - } - - sink "file" { - config = { - path = "%s" - } - } -} -` - - config = fmt.Sprintf(config, exitAfterAuthTemplText, in, sink1, sink2) - if err := os.WriteFile(conf, []byte(config), 0o600); err != nil { - t.Fatal(err) - } else { - logger.Trace("wrote test config", "path", conf) - } - - doneCh := make(chan struct{}) - go func() { - ui, cmd := testProxyCommand(t, logger) - cmd.client = client - - args := []string{"-config", conf} - if viaFlag { - args = append(args, "-exit-after-auth") - } - - code := cmd.Run(args) - if code != 0 { - t.Errorf("expected %d to be %d", code, 0) - t.Logf("output from proxy:\n%s", ui.OutputWriter.String()) - t.Logf("error from proxy:\n%s", ui.ErrorWriter.String()) - } - close(doneCh) - }() - - select { - case <-doneCh: - break - case <-time.After(1 * time.Minute): - t.Fatal("timeout reached while waiting for proxy to exit") - } - - sink1Bytes, err := os.ReadFile(sink1) - if err != nil { - t.Fatal(err) - } - if len(sink1Bytes) == 0 { - t.Fatal("got no output from sink 1") - } - - sink2Bytes, err := os.ReadFile(sink2) - if err != nil { - t.Fatal(err) - } - if len(sink2Bytes) == 0 { - t.Fatal("got no output from sink 2") - } - - if string(sink1Bytes) != string(sink2Bytes) { - t.Fatal("sink 1/2 values don't match") - } -} - -// TestProxy_AutoAuth_UserAgent tests that the User-Agent sent -// to Vault by Vault Proxy is correct when performing Auto-Auth. -// Uses the custom handler userAgentHandler (defined above) so -// that Vault validates the User-Agent on requests sent by Proxy. -func TestProxy_AutoAuth_UserAgent(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - var h userAgentHandler - cluster := vault.NewTestCluster(t, &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - }, &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc( - func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.userAgentToCheckFor = useragent.ProxyAutoAuthString() - h.requestMethodToCheck = "PUT" - h.pathToCheck = "auth/approle/login" - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - serverClient := cluster.Cores[0].Client - - // Enable the approle auth method - req := serverClient.NewRequest("POST", "/v1/sys/auth/approle") - req.BodyBytes = []byte(`{ - "type": "approle" - }`) - request(t, serverClient, req, 204) - - // Create a named role - req = serverClient.NewRequest("PUT", "/v1/auth/approle/role/test-role") - req.BodyBytes = []byte(`{ - "secret_id_num_uses": "10", - "secret_id_ttl": "1m", - "token_max_ttl": "1m", - "token_num_uses": "10", - "token_ttl": "1m", - "policies": "default" - }`) - request(t, serverClient, req, 204) - - // Fetch the RoleID of the named role - req = serverClient.NewRequest("GET", "/v1/auth/approle/role/test-role/role-id") - body := request(t, serverClient, req, 200) - data := body["data"].(map[string]interface{}) - roleID := data["role_id"].(string) - - // Get a SecretID issued against the named role - req = serverClient.NewRequest("PUT", "/v1/auth/approle/role/test-role/secret-id") - body = request(t, serverClient, req, 200) - data = body["data"].(map[string]interface{}) - secretID := data["secret_id"].(string) - - // Write the RoleID and SecretID to temp files - roleIDPath := makeTempFile(t, "role_id.txt", roleID+"\n") - secretIDPath := makeTempFile(t, "secret_id.txt", secretID+"\n") - defer os.Remove(roleIDPath) - defer os.Remove(secretIDPath) - - sinkf, err := os.CreateTemp("", "sink.test.") - if err != nil { - t.Fatal(err) - } - sink := sinkf.Name() - sinkf.Close() - os.Remove(sink) - - autoAuthConfig := fmt.Sprintf(` -auto_auth { - method "approle" { - mount_path = "auth/approle" - config = { - role_id_file_path = "%s" - secret_id_file_path = "%s" - } - } - - sink "file" { - config = { - path = "%s" - } - } -}`, roleIDPath, secretIDPath, sink) - - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - config := fmt.Sprintf(` -vault { - address = "%s" - tls_skip_verify = true -} -api_proxy { - use_auto_auth_token = true -} -%s -%s -`, serverClient.Address(), listenConfig, autoAuthConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Unset the environment variable so that proxy picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - // Start proxy - _, cmd := testProxyCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - // Validate that the auto-auth token has been correctly attained - // and works for LookupSelf - conf := api.DefaultConfig() - conf.Address = "http://" + listenAddr - proxyClient, err := api.NewClient(conf) - if err != nil { - t.Fatalf("err: %s", err) - } - - proxyClient.SetToken("") - err = proxyClient.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - - // Wait for the token to be sent to syncs and be available to be used - time.Sleep(5 * time.Second) - - req = proxyClient.NewRequest("GET", "/v1/auth/token/lookup-self") - body = request(t, proxyClient, req, 200) - - close(cmd.ShutdownCh) - wg.Wait() -} - -// TestProxy_APIProxyWithoutCache_UserAgent tests that the User-Agent sent -// to Vault by Vault Proxy is correct using the API proxy without -// the cache configured. Uses the custom handler -// userAgentHandler struct defined in this test package, so that Vault validates the -// User-Agent on requests sent by Proxy. -func TestProxy_APIProxyWithoutCache_UserAgent(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - userAgentForProxiedClient := "proxied-client" - var h userAgentHandler - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc( - func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.userAgentToCheckFor = useragent.ProxyStringWithProxiedUserAgent(userAgentForProxiedClient) - h.pathToCheck = "/v1/auth/token/lookup-self" - h.requestMethodToCheck = "GET" - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that proxy picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - config := fmt.Sprintf(` -vault { - address = "%s" - tls_skip_verify = true -} -%s -`, serverClient.Address(), listenConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the proxy - _, cmd := testProxyCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - proxyClient, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - proxyClient.AddHeader("User-Agent", userAgentForProxiedClient) - proxyClient.SetToken(serverClient.Token()) - proxyClient.SetMaxRetries(0) - err = proxyClient.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - - _, err = proxyClient.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - - close(cmd.ShutdownCh) - wg.Wait() -} - -// TestProxy_APIProxyWithCache_UserAgent tests that the User-Agent sent -// to Vault by Vault Proxy is correct using the API proxy with -// the cache configured. Uses the custom handler -// userAgentHandler struct defined in this test package, so that Vault validates the -// User-Agent on requests sent by Proxy. -func TestProxy_APIProxyWithCache_UserAgent(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - userAgentForProxiedClient := "proxied-client" - var h userAgentHandler - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc( - func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.userAgentToCheckFor = useragent.ProxyStringWithProxiedUserAgent(userAgentForProxiedClient) - h.pathToCheck = "/v1/auth/token/lookup-self" - h.requestMethodToCheck = "GET" - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that proxy picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - cacheConfig := ` -cache { -}` - - config := fmt.Sprintf(` -vault { - address = "%s" - tls_skip_verify = true -} -%s -%s -`, serverClient.Address(), listenConfig, cacheConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start the proxy - _, cmd := testProxyCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - proxyClient, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - proxyClient.AddHeader("User-Agent", userAgentForProxiedClient) - proxyClient.SetToken(serverClient.Token()) - proxyClient.SetMaxRetries(0) - err = proxyClient.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - - _, err = proxyClient.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - - close(cmd.ShutdownCh) - wg.Wait() -} - -// TestProxy_Cache_DynamicSecret Tests that the cache successfully caches a dynamic secret -// going through the Proxy, -func TestProxy_Cache_DynamicSecret(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Trace) - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that proxy picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - cacheConfig := ` -cache { -} -` - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - config := fmt.Sprintf(` -vault { - address = "%s" - tls_skip_verify = true -} -%s -%s -`, serverClient.Address(), cacheConfig, listenConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - // Start proxy - _, cmd := testProxyCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - proxyClient, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - proxyClient.SetToken(serverClient.Token()) - proxyClient.SetMaxRetries(0) - err = proxyClient.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - - renewable := true - tokenCreateRequest := &api.TokenCreateRequest{ - Policies: []string{"default"}, - TTL: "30m", - Renewable: &renewable, - } - - // This was the simplest test I could find to trigger the caching behaviour, - // i.e. the most concise I could make the test that I can tell - // creating an orphan token returns Auth, is renewable, and isn't a token - // that's managed elsewhere (since it's an orphan) - secret, err := proxyClient.Auth().Token().CreateOrphan(tokenCreateRequest) - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Auth == nil { - t.Fatalf("secret not as expected: %v", secret) - } - - token := secret.Auth.ClientToken - - secret, err = proxyClient.Auth().Token().CreateOrphan(tokenCreateRequest) - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Auth == nil { - t.Fatalf("secret not as expected: %v", secret) - } - - token2 := secret.Auth.ClientToken - - if token != token2 { - t.Fatalf("token create response not cached when it should have been, as tokens differ") - } - - close(cmd.ShutdownCh) - wg.Wait() -} - -// TestProxy_ApiProxy_Retry Tests the retry functionalities of Vault Proxy's API Proxy -func TestProxy_ApiProxy_Retry(t *testing.T) { - //---------------------------------------------------- - // Start the server and proxy - //---------------------------------------------------- - logger := logging.NewVaultLogger(hclog.Trace) - var h handler - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "kv": logicalKv.Factory, - }, - }, - &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: vaulthttp.HandlerFunc(func(properties *vault.HandlerProperties) http.Handler { - h.props = properties - h.t = t - return &h - }), - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that proxy picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - os.Unsetenv(api.EnvVaultAddress) - - _, err := serverClient.Logical().Write("secret/foo", map[string]interface{}{ - "bar": "baz", - }) - if err != nil { - t.Fatal(err) - } - - intRef := func(i int) *int { - return &i - } - // start test cases here - testCases := map[string]struct { - retries *int - expectError bool - }{ - "none": { - retries: intRef(-1), - expectError: true, - }, - "one": { - retries: intRef(1), - expectError: true, - }, - "two": { - retries: intRef(2), - expectError: false, - }, - "missing": { - retries: nil, - expectError: false, - }, - "default": { - retries: intRef(0), - expectError: false, - }, - } - - for tcname, tc := range testCases { - t.Run(tcname, func(t *testing.T) { - h.failCount = 2 - - cacheConfig := ` -cache { -} -` - listenAddr := generateListenerAddress(t) - listenConfig := fmt.Sprintf(` -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - - var retryConf string - if tc.retries != nil { - retryConf = fmt.Sprintf("retry { num_retries = %d }", *tc.retries) - } - - config := fmt.Sprintf(` -vault { - address = "%s" - %s - tls_skip_verify = true -} -%s -%s -`, serverClient.Address(), retryConf, cacheConfig, listenConfig) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - _, cmd := testProxyCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - client, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - client.SetToken(serverClient.Token()) - client.SetMaxRetries(0) - err = client.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - secret, err := client.Logical().Read("secret/foo") - switch { - case (err != nil || secret == nil) && tc.expectError: - case (err == nil || secret != nil) && !tc.expectError: - default: - t.Fatalf("%s expectError=%v error=%v secret=%v", tcname, tc.expectError, err, secret) - } - if secret != nil && secret.Data["foo"] != nil { - val := secret.Data["foo"].(map[string]interface{}) - if !reflect.DeepEqual(val, map[string]interface{}{"bar": "baz"}) { - t.Fatalf("expected key 'foo' to yield bar=baz, got: %v", val) - } - } - time.Sleep(time.Second) - - close(cmd.ShutdownCh) - wg.Wait() - }) - } -} - -// TestProxy_Metrics tests that metrics are being properly reported. -func TestProxy_Metrics(t *testing.T) { - // Start a vault server - logger := logging.NewVaultLogger(hclog.Trace) - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - Logger: logger, - }, - &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Create a config file - listenAddr := generateListenerAddress(t) - config := fmt.Sprintf(` -cache {} - -listener "tcp" { - address = "%s" - tls_disable = true -} -`, listenAddr) - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - ui, cmd := testProxyCommand(t, logger) - cmd.client = serverClient - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - code := cmd.Run([]string{"-config", configPath}) - if code != 0 { - t.Errorf("non-zero return code when running proxy: %d", code) - t.Logf("STDOUT from proxy:\n%s", ui.OutputWriter.String()) - t.Logf("STDERR from proxy:\n%s", ui.ErrorWriter.String()) - } - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - // defer proxy shutdown - defer func() { - cmd.ShutdownCh <- struct{}{} - wg.Wait() - }() - - conf := api.DefaultConfig() - conf.Address = "http://" + listenAddr - proxyClient, err := api.NewClient(conf) - if err != nil { - t.Fatalf("err: %s", err) - } - - req := proxyClient.NewRequest("GET", "/proxy/v1/metrics") - body := request(t, proxyClient, req, 200) - keys := []string{} - for k := range body { - keys = append(keys, k) - } - require.ElementsMatch(t, keys, []string{ - "Counters", - "Samples", - "Timestamp", - "Gauges", - "Points", - }) -} - -// TestProxy_QuitAPI Tests the /proxy/v1/quit API that can be enabled for the proxy. -func TestProxy_QuitAPI(t *testing.T) { - logger := logging.NewVaultLogger(hclog.Error) - cluster := vault.NewTestCluster(t, - &vault.CoreConfig{ - Logger: logger, - CredentialBackends: map[string]logical.Factory{ - "approle": credAppRole.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "kv": logicalKv.Factory, - }, - }, - &vault.TestClusterOptions{ - NumCores: 1, - }) - cluster.Start() - defer cluster.Cleanup() - - vault.TestWaitActive(t, cluster.Cores[0].Core) - serverClient := cluster.Cores[0].Client - - // Unset the environment variable so that proxy picks up the right test - // cluster address - defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) - err := os.Unsetenv(api.EnvVaultAddress) - if err != nil { - t.Fatal(err) - } - - listenAddr := generateListenerAddress(t) - listenAddr2 := generateListenerAddress(t) - config := fmt.Sprintf(` -vault { - address = "%s" - tls_skip_verify = true -} - -listener "tcp" { - address = "%s" - tls_disable = true -} - -listener "tcp" { - address = "%s" - tls_disable = true - proxy_api { - enable_quit = true - } -} - -cache {} -`, serverClient.Address(), listenAddr, listenAddr2) - - configPath := makeTempFile(t, "config.hcl", config) - defer os.Remove(configPath) - - _, cmd := testProxyCommand(t, logger) - cmd.startedCh = make(chan struct{}) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - cmd.Run([]string{"-config", configPath}) - wg.Done() - }() - - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - client, err := api.NewClient(api.DefaultConfig()) - if err != nil { - t.Fatal(err) - } - client.SetToken(serverClient.Token()) - client.SetMaxRetries(0) - err = client.SetAddress("http://" + listenAddr) - if err != nil { - t.Fatal(err) - } - - // First try on listener 1 where the API should be disabled. - resp, err := client.RawRequest(client.NewRequest(http.MethodPost, "/proxy/v1/quit")) - if err == nil { - t.Fatalf("expected error") - } - if resp != nil && resp.StatusCode != http.StatusNotFound { - t.Fatalf("expected %d but got: %d", http.StatusNotFound, resp.StatusCode) - } - - // Now try on listener 2 where the quit API should be enabled. - err = client.SetAddress("http://" + listenAddr2) - if err != nil { - t.Fatal(err) - } - - _, err = client.RawRequest(client.NewRequest(http.MethodPost, "/proxy/v1/quit")) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - select { - case <-cmd.ShutdownCh: - case <-time.After(5 * time.Second): - t.Errorf("timeout") - } - - wg.Wait() -} - -// TestProxy_LogFile_CliOverridesConfig tests that the CLI values -// override the config for log files -func TestProxy_LogFile_CliOverridesConfig(t *testing.T) { - // Create basic config - configFile := populateTempFile(t, "proxy-config.hcl", BasicHclConfig) - cfg, err := proxyConfig.LoadConfigFile(configFile.Name()) - if err != nil { - t.Fatal("Cannot load config to test update/merge", err) - } - - // Sanity check that the config value is the current value - assert.Equal(t, "TMPDIR/juan.log", cfg.LogFile) - - // Initialize the command and parse any flags - cmd := &ProxyCommand{BaseCommand: &BaseCommand{}} - f := cmd.Flags() - // Simulate the flag being specified - err = f.Parse([]string{"-log-file=/foo/bar/test.log"}) - if err != nil { - t.Fatal(err) - } - - // Update the config based on the inputs. - cmd.applyConfigOverrides(f, cfg) - - assert.NotEqual(t, "TMPDIR/juan.log", cfg.LogFile) - assert.NotEqual(t, "/squiggle/logs.txt", cfg.LogFile) - assert.Equal(t, "/foo/bar/test.log", cfg.LogFile) -} - -// TestProxy_LogFile_Config tests log file config when loaded from config -func TestProxy_LogFile_Config(t *testing.T) { - configFile := populateTempFile(t, "proxy-config.hcl", BasicHclConfig) - - cfg, err := proxyConfig.LoadConfigFile(configFile.Name()) - if err != nil { - t.Fatal("Cannot load config to test update/merge", err) - } - - // Sanity check that the config value is the current value - assert.Equal(t, "TMPDIR/juan.log", cfg.LogFile, "sanity check on log config failed") - assert.Equal(t, 2, cfg.LogRotateMaxFiles) - assert.Equal(t, 1048576, cfg.LogRotateBytes) - - // Parse the cli flags (but we pass in an empty slice) - cmd := &ProxyCommand{BaseCommand: &BaseCommand{}} - f := cmd.Flags() - err = f.Parse([]string{}) - if err != nil { - t.Fatal(err) - } - - // Should change nothing... - cmd.applyConfigOverrides(f, cfg) - - assert.Equal(t, "TMPDIR/juan.log", cfg.LogFile, "actual config check") - assert.Equal(t, 2, cfg.LogRotateMaxFiles) - assert.Equal(t, 1048576, cfg.LogRotateBytes) -} - -// TestProxy_Config_NewLogger_Default Tests defaults for log level and -// specifically cmd.newLogger() -func TestProxy_Config_NewLogger_Default(t *testing.T) { - cmd := &ProxyCommand{BaseCommand: &BaseCommand{}} - cmd.config = proxyConfig.NewConfig() - logger, err := cmd.newLogger() - - assert.NoError(t, err) - assert.NotNil(t, logger) - assert.Equal(t, hclog.Info.String(), logger.GetLevel().String()) -} - -// TestProxy_Config_ReloadLogLevel Tests reloading updates the log -// level as expected. -func TestProxy_Config_ReloadLogLevel(t *testing.T) { - cmd := &ProxyCommand{BaseCommand: &BaseCommand{}} - var err error - tempDir := t.TempDir() - - // Load an initial config - hcl := strings.ReplaceAll(BasicHclConfig, "TMPDIR", tempDir) - configFile := populateTempFile(t, "proxy-config.hcl", hcl) - cmd.config, err = proxyConfig.LoadConfigFile(configFile.Name()) - if err != nil { - t.Fatal("Cannot load config to test update/merge", err) - } - - // Tweak the loaded config to make sure we can put log files into a temp dir - // and systemd log attempts work fine, this would usually happen during Run. - cmd.logWriter = os.Stdout - cmd.logger, err = cmd.newLogger() - if err != nil { - t.Fatal("logger required for systemd log messages", err) - } - - // Sanity check - assert.Equal(t, "warn", cmd.config.LogLevel) - - // Load a new config - hcl = strings.ReplaceAll(BasicHclConfig2, "TMPDIR", tempDir) - configFile = populateTempFile(t, "proxy-config.hcl", hcl) - err = cmd.reloadConfig([]string{configFile.Name()}) - assert.NoError(t, err) - assert.Equal(t, "debug", cmd.config.LogLevel) -} - -// TestProxy_Config_ReloadTls Tests that the TLS certs for the listener are -// correctly reloaded. -func TestProxy_Config_ReloadTls(t *testing.T) { - var wg sync.WaitGroup - wd, err := os.Getwd() - if err != nil { - t.Fatal("unable to get current working directory") - } - workingDir := filepath.Join(wd, "/proxy/test-fixtures/reload") - fooCert := "reload_foo.pem" - fooKey := "reload_foo.key" - - barCert := "reload_bar.pem" - barKey := "reload_bar.key" - - reloadCert := "reload_cert.pem" - reloadKey := "reload_key.pem" - caPem := "reload_ca.pem" - - tempDir := t.TempDir() - - // Set up initial 'foo' certs - inBytes, err := os.ReadFile(filepath.Join(workingDir, fooCert)) - if err != nil { - t.Fatal("unable to read cert required for test", fooCert, err) - } - err = os.WriteFile(filepath.Join(tempDir, reloadCert), inBytes, 0o777) - if err != nil { - t.Fatal("unable to write temp cert required for test", reloadCert, err) - } - - inBytes, err = os.ReadFile(filepath.Join(workingDir, fooKey)) - if err != nil { - t.Fatal("unable to read cert key required for test", fooKey, err) - } - err = os.WriteFile(filepath.Join(tempDir, reloadKey), inBytes, 0o777) - if err != nil { - t.Fatal("unable to write temp cert key required for test", reloadKey, err) - } - - inBytes, err = os.ReadFile(filepath.Join(workingDir, caPem)) - if err != nil { - t.Fatal("unable to read CA pem required for test", caPem, err) - } - certPool := x509.NewCertPool() - ok := certPool.AppendCertsFromPEM(inBytes) - if !ok { - t.Fatal("not ok when appending CA cert") - } - - replacedHcl := strings.ReplaceAll(BasicHclConfig, "TMPDIR", tempDir) - configFile := populateTempFile(t, "proxy-config.hcl", replacedHcl) - - // Set up Proxy - logger := logging.NewVaultLogger(hclog.Trace) - ui, cmd := testProxyCommand(t, logger) - - var output string - var code int - wg.Add(1) - args := []string{"-config", configFile.Name()} - go func() { - if code = cmd.Run(args); code != 0 { - output = ui.ErrorWriter.String() + ui.OutputWriter.String() - } - wg.Done() - }() - - testCertificateName := func(cn string) error { - conn, err := tls.Dial("tcp", "127.0.0.1:8100", &tls.Config{ - RootCAs: certPool, - }) - if err != nil { - return err - } - defer conn.Close() - if err = conn.Handshake(); err != nil { - return err - } - servName := conn.ConnectionState().PeerCertificates[0].Subject.CommonName - if servName != cn { - return fmt.Errorf("expected %s, got %s", cn, servName) - } - return nil - } - - // Start - select { - case <-cmd.startedCh: - case <-time.After(5 * time.Second): - t.Fatalf("timeout") - } - - if err := testCertificateName("foo.example.com"); err != nil { - t.Fatalf("certificate name didn't check out: %s", err) - } - - // Swap out certs - inBytes, err = os.ReadFile(filepath.Join(workingDir, barCert)) - if err != nil { - t.Fatal("unable to read cert required for test", barCert, err) - } - err = os.WriteFile(filepath.Join(tempDir, reloadCert), inBytes, 0o777) - if err != nil { - t.Fatal("unable to write temp cert required for test", reloadCert, err) - } - - inBytes, err = os.ReadFile(filepath.Join(workingDir, barKey)) - if err != nil { - t.Fatal("unable to read cert key required for test", barKey, err) - } - err = os.WriteFile(filepath.Join(tempDir, reloadKey), inBytes, 0o777) - if err != nil { - t.Fatal("unable to write temp cert key required for test", reloadKey, err) - } - - // Reload - cmd.SighupCh <- struct{}{} - select { - case <-cmd.reloadedCh: - case <-time.After(5 * time.Second): - t.Fatalf("timeout") - } - - if err := testCertificateName("bar.example.com"); err != nil { - t.Fatalf("certificate name didn't check out: %s", err) - } - - // Shut down - cmd.ShutdownCh <- struct{}{} - wg.Wait() - - if code != 0 { - t.Fatalf("got a non-zero exit status: %d, stdout/stderr: %s", code, output) - } -} diff --git a/command/read_test.go b/command/read_test.go deleted file mode 100644 index 6f33a1ef2..000000000 --- a/command/read_test.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testReadCommand(tb testing.TB) (*cli.MockUi, *ReadCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &ReadCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestReadCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "proper_args", - []string{"foo", "bar=baz"}, - "No value found at foo\n", - 2, - }, - { - "not_found", - []string{"nope/not/once/never"}, - "", - 2, - }, - { - "default", - []string{"secret/read/foo"}, - "foo", - 0, - }, - { - "field", - []string{ - "-field", "foo", - "secret/read/foo", - }, - "bar", - 0, - }, - { - "field_not_found", - []string{ - "-field", "not-a-real-field", - "secret/read/foo", - }, - "not present in secret", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if _, err := client.Logical().Write("secret/read/foo", map[string]interface{}{ - "foo": "bar", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testReadCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("%s: expected %q to contain %q", tc.name, combined, tc.out) - } - }) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testReadCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/foo", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error reading secret/foo: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_data_object_from_api_response", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testReadCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "sys/health", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - expected := []string{ - "cluster_id", "cluster_name", "initialized", "performance_standby", "replication_dr_mode", "replication_performance_mode", "sealed", - "server_time_utc", "standby", "version", - } - for _, expectedField := range expected { - if !strings.Contains(combined, expectedField) { - t.Errorf("expected %q to contain %q", combined, expected) - } - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testReadCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/rotate_test.go b/command/rotate_test.go deleted file mode 100644 index b591b5f73..000000000 --- a/command/rotate_test.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testOperatorRotateCommand(tb testing.TB) (*cli.MockUi, *OperatorRotateCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &OperatorRotateCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestOperatorRotateCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"abcd1234"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorRotateCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("default", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testOperatorRotateCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Rotated key" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - status, err := client.Sys().KeyStatus() - if err != nil { - t.Fatal(err) - } - if exp := 1; status.Term < exp { - t.Errorf("expected %d to be less than %d", status.Term, exp) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testOperatorRotateCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error rotating key: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testOperatorRotateCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/secrets_disable_test.go b/command/secrets_disable_test.go deleted file mode 100644 index 40058ec2f..000000000 --- a/command/secrets_disable_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testSecretsDisableCommand(tb testing.TB) (*cli.MockUi, *SecretsDisableCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &SecretsDisableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestSecretsDisableCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - { - "not_real", - []string{"not_real"}, - "Success! Disabled the secrets engine (if it existed) at: not_real/", - 0, - }, - { - "default", - []string{"secret"}, - "Success! Disabled the secrets engine (if it existed) at: secret/", - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsDisableCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("my-secret/", &api.MountInput{ - Type: "generic", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testSecretsDisableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-secret/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Disabled the secrets engine (if it existed) at: my-secret/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - if _, ok := mounts["integration_unmount"]; ok { - t.Errorf("expected mount to not exist: %#v", mounts) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testSecretsDisableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "pki/", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error disabling secrets engine at pki/: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testSecretsDisableCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/secrets_enable_test.go b/command/secrets_enable_test.go deleted file mode 100644 index 99c857377..000000000 --- a/command/secrets_enable_test.go +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "errors" - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/helper/builtinplugins" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/mitchellh/cli" -) - -// logicalBackendAdjustmentFactor is set to plus 1 for the database backend -// which is a plugin but not found in go.mod files, and minus 1 for the ldap -// and openldap secret backends which have the same underlying plugin. -var logicalBackendAdjustmentFactor = 1 - 1 - -func testSecretsEnableCommand(tb testing.TB) (*cli.MockUi, *SecretsEnableCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &SecretsEnableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestSecretsEnableCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - { - "not_a_valid_mount", - []string{"nope_definitely_not_a_valid_mount_like_ever"}, - "", - 2, - }, - { - "mount", - []string{"transit"}, - "Success! Enabled the transit secrets engine at: transit/", - 0, - }, - { - "mount_path", - []string{ - "-path", "transit_mount_point", - "transit", - }, - "Success! Enabled the transit secrets engine at: transit_mount_point/", - 0, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsEnableCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsEnableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-path", "mount_integration/", - "-description", "The best kind of test", - "-default-lease-ttl", "30m", - "-max-lease-ttl", "1h", - "-audit-non-hmac-request-keys", "foo,bar", - "-audit-non-hmac-response-keys", "foo,bar", - "-passthrough-request-headers", "authorization,authentication", - "-passthrough-request-headers", "www-authentication", - "-allowed-response-headers", "authorization", - "-allowed-managed-keys", "key1,key2", - "-force-no-cache", - "pki", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Enabled the pki secrets engine at: mount_integration/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - mountInfo, ok := mounts["mount_integration/"] - if !ok { - t.Fatalf("expected mount to exist") - } - if exp := "pki"; mountInfo.Type != exp { - t.Errorf("expected %q to be %q", mountInfo.Type, exp) - } - if exp := "The best kind of test"; mountInfo.Description != exp { - t.Errorf("expected %q to be %q", mountInfo.Description, exp) - } - if exp := 1800; mountInfo.Config.DefaultLeaseTTL != exp { - t.Errorf("expected %d to be %d", mountInfo.Config.DefaultLeaseTTL, exp) - } - if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp { - t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp) - } - if exp := true; mountInfo.Config.ForceNoCache != exp { - t.Errorf("expected %t to be %t", mountInfo.Config.ForceNoCache, exp) - } - if diff := deep.Equal([]string{"authorization,authentication", "www-authentication"}, mountInfo.Config.PassthroughRequestHeaders); len(diff) > 0 { - t.Errorf("Failed to find expected values in PassthroughRequestHeaders. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"authorization"}, mountInfo.Config.AllowedResponseHeaders); len(diff) > 0 { - t.Errorf("Failed to find expected values in AllowedResponseHeaders. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"foo,bar"}, mountInfo.Config.AuditNonHMACRequestKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AuditNonHMACRequestKeys. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"foo,bar"}, mountInfo.Config.AuditNonHMACResponseKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AuditNonHMACResponseKeys. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"key1,key2"}, mountInfo.Config.AllowedManagedKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AllowedManagedKeys. Difference is: %v", diff) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testSecretsEnableCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "pki", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error enabling: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testSecretsEnableCommand(t) - assertNoTabs(t, cmd) - }) - - t.Run("mount_all", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerAllBackends(t) - defer closer() - - files, err := ioutil.ReadDir("../builtin/logical") - if err != nil { - t.Fatal(err) - } - - var backends []string - for _, f := range files { - if f.IsDir() { - if f.Name() == "plugin" { - continue - } - if _, err := os.Stat("../builtin/logical/" + f.Name() + "/backend.go"); errors.Is(err, os.ErrNotExist) { - // Skip ext test packages (fake plugins without backends). - continue - } - backends = append(backends, f.Name()) - } - } - - modFile, err := ioutil.ReadFile("../go.mod") - if err != nil { - t.Fatal(err) - } - modLines := strings.Split(string(modFile), "\n") - for _, p := range modLines { - splitLine := strings.Split(strings.TrimSpace(p), " ") - if len(splitLine) == 0 { - continue - } - potPlug := strings.TrimPrefix(splitLine[0], "github.com/hashicorp/") - if strings.HasPrefix(potPlug, "vault-plugin-secrets-") { - backends = append(backends, strings.TrimPrefix(potPlug, "vault-plugin-secrets-")) - } - } - - // backends are found by walking the directory, which includes the database backend, - // however, the plugins registry omits that one - if len(backends) != len(builtinplugins.Registry.Keys(consts.PluginTypeSecrets))+logicalBackendAdjustmentFactor { - t.Fatalf("expected %d logical backends, got %d", len(builtinplugins.Registry.Keys(consts.PluginTypeSecrets))+logicalBackendAdjustmentFactor, len(backends)) - } - - for _, b := range backends { - expectedResult := 0 - - ui, cmd := testSecretsEnableCommand(t) - cmd.client = client - - actualResult := cmd.Run([]string{ - b, - }) - - // Need to handle deprecated builtins specially - status, _ := builtinplugins.Registry.DeprecationStatus(b, consts.PluginTypeSecrets) - if status == consts.PendingRemoval || status == consts.Removed { - expectedResult = 2 - } - - if actualResult != expectedResult { - t.Errorf("type: %s - got: %d, expected: %d - %s", b, actualResult, expectedResult, ui.OutputWriter.String()+ui.ErrorWriter.String()) - } - } - }) -} diff --git a/command/secrets_list_test.go b/command/secrets_list_test.go deleted file mode 100644 index 544e7196c..000000000 --- a/command/secrets_list_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testSecretsListCommand(tb testing.TB) (*cli.MockUi, *SecretsListCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &SecretsListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestSecretsListCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo"}, - "Too many arguments", - 1, - }, - { - "lists", - nil, - "Path", - 0, - }, - { - "detailed", - []string{"-detailed"}, - "Deprecation Status", - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsListCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testSecretsListCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error listing secrets engines: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testSecretsListCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/secrets_move_test.go b/command/secrets_move_test.go deleted file mode 100644 index 6e7a0d796..000000000 --- a/command/secrets_move_test.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testSecretsMoveCommand(tb testing.TB) (*cli.MockUi, *SecretsMoveCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &SecretsMoveCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestSecretsMoveCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar", "baz"}, - "Too many arguments", - 1, - }, - { - "non_existent", - []string{"not_real", "over_here"}, - "Error moving secrets engine not_real/ to over_here/", - 2, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsMoveCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsMoveCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/", "generic/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Finished moving secrets engine secret/ to generic/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - if _, ok := mounts["generic/"]; !ok { - t.Errorf("expected mount at generic/: %#v", mounts) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testSecretsMoveCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/", "generic/", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error moving secrets engine secret/ to generic/:" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testSecretsMoveCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/secrets_tune_test.go b/command/secrets_tune_test.go deleted file mode 100644 index 8146dec92..000000000 --- a/command/secrets_tune_test.go +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/mitchellh/cli" -) - -func testSecretsTuneCommand(tb testing.TB) (*cli.MockUi, *SecretsTuneCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &SecretsTuneCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestSecretsTuneCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsTuneCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("protect_downgrade", func(t *testing.T) { - t.Parallel() - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsTuneCommand(t) - cmd.client = client - - // Mount - if err := client.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - Options: map[string]string{ - "version": "2", - }, - }); err != nil { - t.Fatal(err) - } - - // confirm default max_versions - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - mountInfo, ok := mounts["kv/"] - if !ok { - t.Fatalf("expected mount to exist") - } - if exp := "kv"; mountInfo.Type != exp { - t.Errorf("expected %q to be %q", mountInfo.Type, exp) - } - if exp := "2"; mountInfo.Options["version"] != exp { - t.Errorf("expected %q to be %q", mountInfo.Options["version"], exp) - } - - if exp := ""; mountInfo.Options["max_versions"] != exp { - t.Errorf("expected %s to be empty", mountInfo.Options["max_versions"]) - } - - // omitting the version should not cause a downgrade - code := cmd.Run([]string{ - "-options", "max_versions=2", - "kv/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Tuned the secrets engine at: kv/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - mounts, err = client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - mountInfo, ok = mounts["kv/"] - if !ok { - t.Fatalf("expected mount to exist") - } - if exp := "2"; mountInfo.Options["version"] != exp { - t.Errorf("expected %q to be %q", mountInfo.Options["version"], exp) - } - if exp := "kv"; mountInfo.Type != exp { - t.Errorf("expected %q to be %q", mountInfo.Type, exp) - } - if exp := "2"; mountInfo.Options["max_versions"] != exp { - t.Errorf("expected %s to be %s", mountInfo.Options["max_versions"], exp) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Run("flags_all", func(t *testing.T) { - t.Parallel() - pluginDir, cleanup := corehelpers.MakeTestPluginDir(t) - defer cleanup(t) - - client, _, closer := testVaultServerPluginDir(t, pluginDir) - defer closer() - - ui, cmd := testSecretsTuneCommand(t) - cmd.client = client - - // Mount - if err := client.Sys().Mount("mount_tune_integration", &api.MountInput{ - Type: "pki", - }); err != nil { - t.Fatal(err) - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - mountInfo, ok := mounts["mount_tune_integration/"] - if !ok { - t.Fatalf("expected mount to exist") - } - - if exp := ""; mountInfo.PluginVersion != exp { - t.Errorf("expected %q to be %q", mountInfo.PluginVersion, exp) - } - - _, _, version := testPluginCreateAndRegisterVersioned(t, client, pluginDir, "pki", api.PluginTypeSecrets) - - code := cmd.Run([]string{ - "-description", "new description", - "-default-lease-ttl", "30m", - "-max-lease-ttl", "1h", - "-audit-non-hmac-request-keys", "foo,bar", - "-audit-non-hmac-response-keys", "foo,bar", - "-passthrough-request-headers", "authorization", - "-passthrough-request-headers", "www-authentication", - "-allowed-response-headers", "authorization,www-authentication", - "-allowed-managed-keys", "key1,key2", - "-listing-visibility", "unauth", - "-plugin-version", version, - "mount_tune_integration/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Tuned the secrets engine at: mount_tune_integration/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - mounts, err = client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - mountInfo, ok = mounts["mount_tune_integration/"] - if !ok { - t.Fatalf("expected mount to exist") - } - if exp := "new description"; mountInfo.Description != exp { - t.Errorf("expected %q to be %q", mountInfo.Description, exp) - } - if exp := "pki"; mountInfo.Type != exp { - t.Errorf("expected %q to be %q", mountInfo.Type, exp) - } - if exp := version; mountInfo.PluginVersion != exp { - t.Errorf("expected %q to be %q", mountInfo.PluginVersion, exp) - } - if exp := 1800; mountInfo.Config.DefaultLeaseTTL != exp { - t.Errorf("expected %d to be %d", mountInfo.Config.DefaultLeaseTTL, exp) - } - if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp { - t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp) - } - if diff := deep.Equal([]string{"authorization", "www-authentication"}, mountInfo.Config.PassthroughRequestHeaders); len(diff) > 0 { - t.Errorf("Failed to find expected values for PassthroughRequestHeaders. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"authorization,www-authentication"}, mountInfo.Config.AllowedResponseHeaders); len(diff) > 0 { - t.Errorf("Failed to find expected values in AllowedResponseHeaders. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"foo,bar"}, mountInfo.Config.AuditNonHMACRequestKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AuditNonHMACRequestKeys. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"foo,bar"}, mountInfo.Config.AuditNonHMACResponseKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AuditNonHMACResponseKeys. Difference is: %v", diff) - } - if diff := deep.Equal([]string{"key1,key2"}, mountInfo.Config.AllowedManagedKeys); len(diff) > 0 { - t.Errorf("Failed to find expected values in AllowedManagedKeys. Difference is: %v", diff) - } - }) - - t.Run("flags_description", func(t *testing.T) { - t.Parallel() - t.Run("not_provided", func(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsTuneCommand(t) - cmd.client = client - - // Mount - if err := client.Sys().Mount("mount_tune_integration", &api.MountInput{ - Type: "pki", - Description: "initial description", - }); err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "-default-lease-ttl", "30m", - "mount_tune_integration/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Tuned the secrets engine at: mount_tune_integration/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - mountInfo, ok := mounts["mount_tune_integration/"] - if !ok { - t.Fatalf("expected mount to exist") - } - if exp := "initial description"; mountInfo.Description != exp { - t.Errorf("expected %q to be %q", mountInfo.Description, exp) - } - }) - - t.Run("provided_empty", func(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testSecretsTuneCommand(t) - cmd.client = client - - // Mount - if err := client.Sys().Mount("mount_tune_integration", &api.MountInput{ - Type: "pki", - Description: "initial description", - }); err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "-description", "", - "mount_tune_integration/", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Tuned the secrets engine at: mount_tune_integration/" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatal(err) - } - - mountInfo, ok := mounts["mount_tune_integration/"] - if !ok { - t.Fatalf("expected mount to exist") - } - if exp := ""; mountInfo.Description != exp { - t.Errorf("expected %q to be %q", mountInfo.Description, exp) - } - }) - }) - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testSecretsTuneCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "pki/", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error tuning secrets engine pki/: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testSecretsTuneCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/server.go b/command/server.go index c9a104502..faf38c7d1 100644 --- a/command/server.go +++ b/command/server.go @@ -10,7 +10,6 @@ import ( "encoding/hex" "fmt" "io" - "io/ioutil" "net" "net/http" "net/url" @@ -43,7 +42,6 @@ import ( loghelper "github.com/hashicorp/vault/helper/logging" "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/helper/testhelpers/teststorage" "github.com/hashicorp/vault/helper/useragent" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/internalshared/configutil" @@ -51,7 +49,6 @@ import ( "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/helper/strutil" - "github.com/hashicorp/vault/sdk/helper/testcluster" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/physical" sr "github.com/hashicorp/vault/serviceregistration" @@ -60,7 +57,6 @@ import ( vaultseal "github.com/hashicorp/vault/vault/seal" "github.com/hashicorp/vault/version" "github.com/mitchellh/cli" - "github.com/mitchellh/go-testing-interface" "github.com/pkg/errors" "github.com/posener/complete" "github.com/sasha-s/go-deadlock" @@ -76,11 +72,6 @@ var ( var memProfilerEnabled = false -var enableFourClusterDev = func(c *ServerCommand, base *vault.CoreConfig, info map[string]string, infoKeys []string, devListenAddress, tempDir string) int { - c.logger.Error("-dev-four-cluster only supported in enterprise Vault") - return 1 -} - const ( storageMigrationLock = "core/migration" @@ -135,13 +126,8 @@ type ServerCommand struct { flagDevLatency int flagDevLatencyJitter int flagDevLeasedKV bool - flagDevKVV1 bool flagDevSkipInit bool - flagDevThreeNode bool - flagDevFourCluster bool flagDevTransactional bool - flagDevAutoSeal bool - flagDevClusterJson string flagTestVerifyOnly bool flagTestServerConfig bool flagExitOnCoreShutdown bool @@ -330,20 +316,6 @@ func (c *ServerCommand) Flags() *FlagSets { Hidden: true, }) - f.BoolVar(&BoolVar{ - Name: "dev-kv-v1", - Target: &c.flagDevKVV1, - Default: false, - Hidden: true, - }) - - f.BoolVar(&BoolVar{ - Name: "dev-auto-seal", - Target: &c.flagDevAutoSeal, - Default: false, - Hidden: true, - }) - f.BoolVar(&BoolVar{ Name: "dev-skip-init", Target: &c.flagDevSkipInit, @@ -351,26 +323,6 @@ func (c *ServerCommand) Flags() *FlagSets { Hidden: true, }) - f.BoolVar(&BoolVar{ - Name: "dev-three-node", - Target: &c.flagDevThreeNode, - Default: false, - Hidden: true, - }) - - f.BoolVar(&BoolVar{ - Name: "dev-four-cluster", - Target: &c.flagDevFourCluster, - Default: false, - Hidden: true, - }) - - f.StringVar(&StringVar{ - Name: "dev-cluster-json", - Target: &c.flagDevClusterJson, - Usage: "File to write cluster definition to", - }) - // TODO: should the below flags be public? f.BoolVar(&BoolVar{ Name: "test-verify-only", @@ -996,7 +948,7 @@ func (c *ServerCommand) Run(args []string) int { } // Automatically enable dev mode if other dev flags are provided. - if c.flagDevHA || c.flagDevTransactional || c.flagDevLeasedKV || c.flagDevThreeNode || c.flagDevFourCluster || c.flagDevAutoSeal || c.flagDevKVV1 || c.flagDevTLS { + if c.flagDevHA || c.flagDevTransactional || c.flagDevLeasedKV || c.flagDevTLS { c.flagDev = true } @@ -1060,11 +1012,6 @@ func (c *ServerCommand) Run(args []string) int { f.applyLogConfigOverrides(config.SharedConfig) - // Set 'trace' log level for the following 'dev' clusters - if c.flagDevThreeNode || c.flagDevFourCluster { - config.LogLevel = "trace" - } - l, err := c.configureLogging(config) if err != nil { c.UI.Error(err.Error()) @@ -1237,13 +1184,6 @@ func (c *ServerCommand) Run(args []string) int { } coreConfig := createCoreConfig(c, config, backend, configSR, barrierSeal, unwrapSeal, metricsHelper, metricSink, secureRandomReader) - if c.flagDevThreeNode { - return c.enableThreeNodeDevCluster(&coreConfig, info, infoKeys, c.flagDevListenAddr, os.Getenv("VAULT_DEV_TEMP_DIR")) - } - - if c.flagDevFourCluster { - return enableFourClusterDev(c, &coreConfig, info, infoKeys, c.flagDevListenAddr, os.Getenv("VAULT_DEV_TEMP_DIR")) - } if allowPendingRemoval := os.Getenv(consts.EnvVaultAllowPendingRemovalMounts); allowPendingRemoval != "" { var err error @@ -1475,8 +1415,7 @@ func (c *ServerCommand) Run(args []string) int { } // If we're in Dev mode, then initialize the core - clusterJson := &testcluster.ClusterJson{} - err = initDevCore(c, &coreConfig, config, core, certDir, clusterJson) + err = initDevCore(c, &coreConfig, config, core, certDir) if err != nil { c.UI.Error(err.Error()) return 1 @@ -1536,34 +1475,6 @@ func (c *ServerCommand) Run(args []string) int { // Notify systemd that the server is ready (if applicable) c.notifySystemd(systemd.SdNotifyReady) - if c.flagDev { - protocol := "http://" - if c.flagDevTLS { - protocol = "https://" - } - clusterJson.Nodes = []testcluster.ClusterNode{ - { - APIAddress: protocol + config.Listeners[0].Address, - }, - } - if c.flagDevTLS { - clusterJson.CACertPath = fmt.Sprintf("%s/%s", certDir, server.VaultDevCAFilename) - } - - if c.flagDevClusterJson != "" && !c.flagDevThreeNode { - b, err := jsonutil.EncodeJSON(clusterJson) - if err != nil { - c.UI.Error(fmt.Sprintf("Error encoding cluster.json: %s", err)) - return 1 - } - err = os.WriteFile(c.flagDevClusterJson, b, 0o600) - if err != nil { - c.UI.Error(fmt.Sprintf("Error writing cluster.json %q: %s", c.flagDevClusterJson, err)) - return 1 - } - } - } - defer func() { if err := c.removePidFile(config.PidFile); err != nil { c.UI.Error(fmt.Sprintf("Error deleting the PID file: %s", err)) @@ -1970,7 +1881,7 @@ func (c *ServerCommand) enableDev(core *vault.Core, coreConfig *vault.CoreConfig } kvVer := "2" - if c.flagDevKVV1 || c.flagDevLeasedKV { + if c.flagDevLeasedKV { kvVer = "1" } req := &logical.Request{ @@ -1997,244 +1908,6 @@ func (c *ServerCommand) enableDev(core *vault.Core, coreConfig *vault.CoreConfig return init, nil } -func (c *ServerCommand) enableThreeNodeDevCluster(base *vault.CoreConfig, info map[string]string, infoKeys []string, devListenAddress, tempDir string) int { - conf, opts := teststorage.ClusterSetup(base, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - BaseListenAddress: c.flagDevListenAddr, - Logger: c.logger, - TempDir: tempDir, - DefaultHandlerProperties: vault.HandlerProperties{ - ListenerConfig: &configutil.Listener{ - Profiling: configutil.ListenerProfiling{ - UnauthenticatedPProfAccess: true, - }, - Telemetry: configutil.ListenerTelemetry{ - UnauthenticatedMetricsAccess: true, - }, - }, - }, - }, nil) - testCluster := vault.NewTestCluster(&testing.RuntimeT{}, conf, opts) - defer c.cleanupGuard.Do(testCluster.Cleanup) - - if constants.IsEnterprise { - err := testcluster.WaitForActiveNodeAndPerfStandbys(context.Background(), testCluster) - if err != nil { - c.UI.Error(fmt.Sprintf("perf standbys didn't become ready: %v", err)) - return 1 - } - } - - info["cluster parameters path"] = testCluster.TempDir - infoKeys = append(infoKeys, "cluster parameters path") - - for i, core := range testCluster.Cores { - info[fmt.Sprintf("node %d api address", i)] = fmt.Sprintf("https://%s", core.Listeners[0].Address.String()) - infoKeys = append(infoKeys, fmt.Sprintf("node %d api address", i)) - } - - infoKeys = append(infoKeys, "version") - verInfo := version.GetVersion() - info["version"] = verInfo.FullVersionNumber(false) - if verInfo.Revision != "" { - info["version sha"] = strings.Trim(verInfo.Revision, "'") - infoKeys = append(infoKeys, "version sha") - } - - infoKeys = append(infoKeys, "cgo") - info["cgo"] = "disabled" - if version.CgoEnabled { - info["cgo"] = "enabled" - } - - infoKeys = append(infoKeys, "go version") - info["go version"] = runtime.Version() - - fipsStatus := getFIPSInfoKey() - if fipsStatus != "" { - infoKeys = append(infoKeys, "fips") - info["fips"] = fipsStatus - } - - // Server configuration output - padding := 24 - - sort.Strings(infoKeys) - c.UI.Output("==> Vault server configuration:\n") - - for _, k := range infoKeys { - c.UI.Output(fmt.Sprintf( - "%s%s: %s", - strings.Repeat(" ", padding-len(k)), - strings.Title(k), - info[k])) - } - - c.UI.Output("") - - for _, core := range testCluster.Cores { - core.Server.Handler = vaulthttp.Handler.Handler(&vault.HandlerProperties{ - Core: core.Core, - }) - core.SetClusterHandler(core.Server.Handler) - } - - testCluster.Start() - - ctx := namespace.ContextWithNamespace(context.Background(), namespace.RootNamespace) - - if base.DevToken != "" { - req := &logical.Request{ - ID: "dev-gen-root", - Operation: logical.UpdateOperation, - ClientToken: testCluster.RootToken, - Path: "auth/token/create", - Data: map[string]interface{}{ - "id": base.DevToken, - "policies": []string{"root"}, - "no_parent": true, - "no_default_policy": true, - }, - } - resp, err := testCluster.Cores[0].HandleRequest(ctx, req) - if err != nil { - c.UI.Error(fmt.Sprintf("failed to create root token with ID %s: %s", base.DevToken, err)) - return 1 - } - if resp == nil { - c.UI.Error(fmt.Sprintf("nil response when creating root token with ID %s", base.DevToken)) - return 1 - } - if resp.Auth == nil { - c.UI.Error(fmt.Sprintf("nil auth when creating root token with ID %s", base.DevToken)) - return 1 - } - - testCluster.RootToken = resp.Auth.ClientToken - - req.ID = "dev-revoke-init-root" - req.Path = "auth/token/revoke-self" - req.Data = nil - _, err = testCluster.Cores[0].HandleRequest(ctx, req) - if err != nil { - c.UI.Output(fmt.Sprintf("failed to revoke initial root token: %s", err)) - return 1 - } - } - - // Set the token - tokenHelper, err := c.TokenHelper() - if err != nil { - c.UI.Error(fmt.Sprintf("Error getting token helper: %s", err)) - return 1 - } - if err := tokenHelper.Store(testCluster.RootToken); err != nil { - c.UI.Error(fmt.Sprintf("Error storing in token helper: %s", err)) - return 1 - } - - if err := ioutil.WriteFile(filepath.Join(testCluster.TempDir, "root_token"), []byte(testCluster.RootToken), 0o600); err != nil { - c.UI.Error(fmt.Sprintf("Error writing token to tempfile: %s", err)) - return 1 - } - - c.UI.Output(fmt.Sprintf( - "==> Three node dev mode is enabled\n\n" + - "The unseal key and root token are reproduced below in case you\n" + - "want to seal/unseal the Vault or play with authentication.\n", - )) - - for i, key := range testCluster.BarrierKeys { - c.UI.Output(fmt.Sprintf( - "Unseal Key %d: %s", - i+1, base64.StdEncoding.EncodeToString(key), - )) - } - - c.UI.Output(fmt.Sprintf( - "\nRoot Token: %s\n", testCluster.RootToken, - )) - - c.UI.Output(fmt.Sprintf( - "\nUseful env vars:\n"+ - "VAULT_TOKEN=%s\n"+ - "VAULT_ADDR=%s\n"+ - "VAULT_CACERT=%s/ca_cert.pem\n", - testCluster.RootToken, - testCluster.Cores[0].Client.Address(), - testCluster.TempDir, - )) - - if c.flagDevClusterJson != "" { - clusterJson := testcluster.ClusterJson{ - Nodes: []testcluster.ClusterNode{}, - CACertPath: filepath.Join(testCluster.TempDir, "ca_cert.pem"), - RootToken: testCluster.RootToken, - } - for _, core := range testCluster.Cores { - clusterJson.Nodes = append(clusterJson.Nodes, testcluster.ClusterNode{ - APIAddress: core.Client.Address(), - }) - } - b, err := jsonutil.EncodeJSON(clusterJson) - if err != nil { - c.UI.Error(fmt.Sprintf("Error encoding cluster.json: %s", err)) - return 1 - } - err = os.WriteFile(c.flagDevClusterJson, b, 0o600) - if err != nil { - c.UI.Error(fmt.Sprintf("Error writing cluster.json %q: %s", c.flagDevClusterJson, err)) - return 1 - } - } - - // Output the header that the server has started - c.UI.Output("==> Vault server started! Log data will stream in below:\n") - - // Inform any tests that the server is ready - select { - case c.startedCh <- struct{}{}: - default: - } - - // Release the log gate. - c.flushLog() - - // Wait for shutdown - shutdownTriggered := false - - for !shutdownTriggered { - select { - case <-c.ShutdownCh: - c.UI.Output("==> Vault shutdown triggered") - - // Stop the listeners so that we don't process further client requests. - c.cleanupGuard.Do(testCluster.Cleanup) - - // Finalize will wait until after Vault is sealed, which means the - // request forwarding listeners will also be closed (and also - // waited for). - for _, core := range testCluster.Cores { - if err := core.Shutdown(); err != nil { - c.UI.Error(fmt.Sprintf("Error with core shutdown: %s", err)) - } - } - - shutdownTriggered = true - - case <-c.SighupCh: - c.UI.Output("==> Vault reload triggered") - for _, core := range testCluster.Cores { - if err := c.Reload(core.ReloadFuncsLock, core.ReloadFuncs, nil, core.Core); err != nil { - c.UI.Error(fmt.Sprintf("Error(s) were encountered during reload: %s", err)) - } - } - } - } - - return 0 -} - // addPlugin adds any plugins to the catalog func (c *ServerCommand) addPlugin(path, token string, core *vault.Core) error { // Get the sha256 of the file at the given path. @@ -2482,15 +2155,6 @@ func setSeal(c *ServerCommand, config *server.Config, infoKeys []string, info ma var sealConfigError error var wrapper wrapping.Wrapper var barrierWrapper wrapping.Wrapper - if c.flagDevAutoSeal { - var err error - access, _ := vaultseal.NewTestSeal(nil) - barrierSeal, err = vault.NewAutoSeal(access) - if err != nil { - return nil, nil, nil, nil, nil, err - } - return barrierSeal, nil, nil, nil, nil, nil - } // Handle the case where no seal is provided switch len(config.Seals) { @@ -2821,7 +2485,7 @@ func runListeners(c *ServerCommand, coreConfig *vault.CoreConfig, config *server return nil } -func initDevCore(c *ServerCommand, coreConfig *vault.CoreConfig, config *server.Config, core *vault.Core, certDir string, clusterJSON *testcluster.ClusterJson) error { +func initDevCore(c *ServerCommand, coreConfig *vault.CoreConfig, config *server.Config, core *vault.Core, certDir string) error { if c.flagDev && !c.flagDevSkipInit { init, err := c.enableDev(core, coreConfig) @@ -2829,10 +2493,6 @@ func initDevCore(c *ServerCommand, coreConfig *vault.CoreConfig, config *server. return fmt.Errorf("Error initializing Dev mode: %s", err) } - if clusterJSON != nil { - clusterJSON.RootToken = init.RootToken - } - var plugins, pluginsNotLoaded []string if c.flagDevPluginDir != "" && c.flagDevPluginInit { diff --git a/command/server/config.go b/command/server/config.go index 625eb6f08..e9e8fd1ad 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -24,8 +24,6 @@ import ( "github.com/hashicorp/vault/internalshared/configutil" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/strutil" - "github.com/hashicorp/vault/sdk/helper/testcluster" - "github.com/mitchellh/mapstructure" ) const ( @@ -1223,12 +1221,3 @@ func (c *Config) found(s, k string) { delete(c.UnusedKeys, s) c.FoundKeys = append(c.FoundKeys, k) } - -func (c *Config) ToVaultNodeConfig() (*testcluster.VaultNodeConfig, error) { - var vnc testcluster.VaultNodeConfig - err := mapstructure.Decode(c, &vnc) - if err != nil { - return nil, err - } - return &vnc, nil -} diff --git a/command/server/config_custom_response_headers_test.go b/command/server/config_custom_response_headers_test.go deleted file mode 100644 index 8db646bfd..000000000 --- a/command/server/config_custom_response_headers_test.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package server - -import ( - "fmt" - "testing" - - "github.com/go-test/deep" -) - -var defaultCustomHeaders = map[string]string{ - "Strict-Transport-Security": "max-age=1; domains", - "Content-Security-Policy": "default-src 'others'", - "X-Vault-Ignored": "ignored", - "X-Custom-Header": "Custom header value default", -} - -var customHeaders307 = map[string]string{ - "X-Custom-Header": "Custom header value 307", -} - -var customHeader3xx = map[string]string{ - "X-Vault-Ignored-3xx": "Ignored 3xx", - "X-Custom-Header": "Custom header value 3xx", -} - -var customHeaders200 = map[string]string{ - "Someheader-200": "200", - "X-Custom-Header": "Custom header value 200", -} - -var customHeader2xx = map[string]string{ - "X-Custom-Header": "Custom header value 2xx", -} - -var customHeader400 = map[string]string{ - "Someheader-400": "400", -} - -var defaultCustomHeadersMultiListener = map[string]string{ - "Strict-Transport-Security": "max-age=31536000; includeSubDomains", - "Content-Security-Policy": "default-src 'others'", - "X-Vault-Ignored": "ignored", - "X-Custom-Header": "Custom header value default", -} - -var defaultSTS = map[string]string{ - "Strict-Transport-Security": "max-age=31536000; includeSubDomains", -} - -func TestCustomResponseHeadersConfigs(t *testing.T) { - expectedCustomResponseHeader := map[string]map[string]string{ - "default": defaultCustomHeaders, - "307": customHeaders307, - "3xx": customHeader3xx, - "200": customHeaders200, - "2xx": customHeader2xx, - "400": customHeader400, - } - - config, err := LoadConfigFile("./test-fixtures/config_custom_response_headers_1.hcl") - if err != nil { - t.Fatalf("Error encountered when loading config %+v", err) - } - if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[0].CustomResponseHeaders); diff != nil { - t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff)) - } -} - -func TestCustomResponseHeadersConfigsMultipleListeners(t *testing.T) { - expectedCustomResponseHeader := map[string]map[string]string{ - "default": defaultCustomHeadersMultiListener, - "307": customHeaders307, - "3xx": customHeader3xx, - "200": customHeaders200, - "2xx": customHeader2xx, - "400": customHeader400, - } - - config, err := LoadConfigFile("./test-fixtures/config_custom_response_headers_multiple_listeners.hcl") - if err != nil { - t.Fatalf("Error encountered when loading config %+v", err) - } - if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[0].CustomResponseHeaders); diff != nil { - t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff)) - } - - if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[1].CustomResponseHeaders); diff == nil { - t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff)) - } - if diff := deep.Equal(expectedCustomResponseHeader["default"], config.Listeners[1].CustomResponseHeaders["default"]); diff != nil { - t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff)) - } - - if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[2].CustomResponseHeaders); diff == nil { - t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff)) - } - - if diff := deep.Equal(defaultSTS, config.Listeners[2].CustomResponseHeaders["default"]); diff != nil { - t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff)) - } - - if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[3].CustomResponseHeaders); diff == nil { - t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff)) - } - - if diff := deep.Equal(defaultSTS, config.Listeners[3].CustomResponseHeaders["default"]); diff != nil { - t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff)) - } -} diff --git a/command/server/config_oss_test.go b/command/server/config_oss_test.go deleted file mode 100644 index 20e97c1cf..000000000 --- a/command/server/config_oss_test.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -//go:build !enterprise - -package server - -import ( - "testing" -) - -func TestLoadConfigFile_topLevel(t *testing.T) { - testLoadConfigFile_topLevel(t, nil) -} - -func TestLoadConfigFile_json2(t *testing.T) { - testLoadConfigFile_json2(t, nil) -} - -func TestParseEntropy(t *testing.T) { - testParseEntropy(t, true) -} diff --git a/command/server/config_telemetry_test.go b/command/server/config_telemetry_test.go deleted file mode 100644 index 18870558c..000000000 --- a/command/server/config_telemetry_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package server - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestMetricFilterConfigs(t *testing.T) { - t.Parallel() - cases := []struct { - configFile string - expectedFilterDefault *bool - expectedPrefixFilter []string - }{ - { - "./test-fixtures/telemetry/valid_prefix_filter.hcl", - nil, - []string{"-vault.expire", "-vault.audit", "+vault.expire.num_irrevocable_leases"}, - }, - { - "./test-fixtures/telemetry/filter_default_override.hcl", - boolPointer(false), - []string(nil), - }, - } - t.Run("validate metric filter configs", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - config, err := LoadConfigFile(tc.configFile) - if err != nil { - t.Fatalf("Error encountered when loading config %+v", err) - } - - assert.Equal(t, tc.expectedFilterDefault, config.SharedConfig.Telemetry.FilterDefault) - assert.Equal(t, tc.expectedPrefixFilter, config.SharedConfig.Telemetry.PrefixFilter) - } - }) -} diff --git a/command/server/config_test.go b/command/server/config_test.go deleted file mode 100644 index 1afb083d0..000000000 --- a/command/server/config_test.go +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package server - -import ( - "fmt" - "reflect" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestLoadConfigFile(t *testing.T) { - testLoadConfigFile(t) -} - -func TestLoadConfigFile_json(t *testing.T) { - testLoadConfigFile_json(t) -} - -func TestLoadConfigFileIntegerAndBooleanValues(t *testing.T) { - testLoadConfigFileIntegerAndBooleanValues(t) -} - -func TestLoadConfigFileIntegerAndBooleanValuesJson(t *testing.T) { - testLoadConfigFileIntegerAndBooleanValuesJson(t) -} - -func TestLoadConfigFileWithLeaseMetricTelemetry(t *testing.T) { - testLoadConfigFileLeaseMetrics(t) -} - -func TestLoadConfigDir(t *testing.T) { - testLoadConfigDir(t) -} - -func TestConfig_Sanitized(t *testing.T) { - testConfig_Sanitized(t) -} - -func TestParseListeners(t *testing.T) { - testParseListeners(t) -} - -func TestParseUserLockouts(t *testing.T) { - testParseUserLockouts(t) -} - -func TestParseSockaddrTemplate(t *testing.T) { - testParseSockaddrTemplate(t) -} - -func TestConfigRaftRetryJoin(t *testing.T) { - testConfigRaftRetryJoin(t) -} - -func TestParseSeals(t *testing.T) { - testParseSeals(t) -} - -func TestParseStorage(t *testing.T) { - testParseStorageTemplate(t) -} - -// TestConfigWithAdministrativeNamespace tests that .hcl and .json configurations are correctly parsed when the administrative_namespace_path is present. -func TestConfigWithAdministrativeNamespace(t *testing.T) { - testConfigWithAdministrativeNamespaceHcl(t) - testConfigWithAdministrativeNamespaceJson(t) -} - -func TestUnknownFieldValidation(t *testing.T) { - testUnknownFieldValidation(t) -} - -func TestUnknownFieldValidationJson(t *testing.T) { - testUnknownFieldValidationJson(t) -} - -func TestUnknownFieldValidationHcl(t *testing.T) { - testUnknownFieldValidationHcl(t) -} - -func TestUnknownFieldValidationListenerAndStorage(t *testing.T) { - testUnknownFieldValidationStorageAndListener(t) -} - -func TestExperimentsConfigParsing(t *testing.T) { - const envKey = "VAULT_EXPERIMENTS" - originalValue := validExperiments - validExperiments = []string{"foo", "bar", "baz"} - t.Cleanup(func() { - validExperiments = originalValue - }) - - for name, tc := range map[string]struct { - fromConfig []string - fromEnv []string - fromCLI []string - expected []string - expectedError string - }{ - // Multiple sources. - "duplication": {[]string{"foo"}, []string{"foo"}, []string{"foo"}, []string{"foo"}, ""}, - "disjoint set": {[]string{"foo"}, []string{"bar"}, []string{"baz"}, []string{"foo", "bar", "baz"}, ""}, - - // Single source. - "config only": {[]string{"foo"}, nil, nil, []string{"foo"}, ""}, - "env only": {nil, []string{"foo"}, nil, []string{"foo"}, ""}, - "CLI only": {nil, nil, []string{"foo"}, []string{"foo"}, ""}, - - // Validation errors. - "config invalid": {[]string{"invalid"}, nil, nil, nil, "from config"}, - "env invalid": {nil, []string{"invalid"}, nil, nil, "from environment variable"}, - "CLI invalid": {nil, nil, []string{"invalid"}, nil, "from command line flag"}, - } { - t.Run(name, func(t *testing.T) { - var configString string - t.Setenv(envKey, strings.Join(tc.fromEnv, ",")) - if len(tc.fromConfig) != 0 { - configString = fmt.Sprintf("experiments = [\"%s\"]", strings.Join(tc.fromConfig, "\", \"")) - } - config, err := ParseConfig(configString, "") - if err == nil { - err = ExperimentsFromEnvAndCLI(config, envKey, tc.fromCLI) - } - - switch tc.expectedError { - case "": - if err != nil { - t.Fatal(err) - } - - default: - if err == nil || !strings.Contains(err.Error(), tc.expectedError) { - t.Fatalf("Expected error to contain %q, but got: %s", tc.expectedError, err) - } - } - }) - } -} - -func TestValidate(t *testing.T) { - originalValue := validExperiments - for name, tc := range map[string]struct { - validSet []string - input []string - expectError bool - }{ - // Valid cases - "minimal valid": {[]string{"foo"}, []string{"foo"}, false}, - "valid subset": {[]string{"foo", "bar"}, []string{"bar"}, false}, - "repeated": {[]string{"foo"}, []string{"foo", "foo"}, false}, - - // Error cases - "partially valid": {[]string{"foo", "bar"}, []string{"foo", "baz"}, true}, - "empty": {[]string{"foo"}, []string{""}, true}, - "no valid experiments": {[]string{}, []string{"foo"}, true}, - } { - t.Run(name, func(t *testing.T) { - t.Cleanup(func() { - validExperiments = originalValue - }) - - validExperiments = tc.validSet - err := validateExperiments(tc.input) - if tc.expectError && err == nil { - t.Fatal("Expected error but got none") - } - if !tc.expectError && err != nil { - t.Fatal("Did not expect error but got", err) - } - }) - } -} - -func TestMerge(t *testing.T) { - for name, tc := range map[string]struct { - left []string - right []string - expected []string - }{ - "disjoint": {[]string{"foo"}, []string{"bar"}, []string{"foo", "bar"}}, - "empty left": {[]string{}, []string{"foo"}, []string{"foo"}}, - "empty right": {[]string{"foo"}, []string{}, []string{"foo"}}, - "overlapping": {[]string{"foo", "bar"}, []string{"foo", "baz"}, []string{"foo", "bar", "baz"}}, - } { - t.Run(name, func(t *testing.T) { - result := mergeExperiments(tc.left, tc.right) - if !reflect.DeepEqual(tc.expected, result) { - t.Fatalf("Expected %v but got %v", tc.expected, result) - } - }) - } -} - -// Test_parseDevTLSConfig verifies that both Windows and Unix directories are correctly escaped when creating a dev TLS -// configuration in HCL -func Test_parseDevTLSConfig(t *testing.T) { - tests := []struct { - name string - certDirectory string - }{ - { - name: "windows path", - certDirectory: `C:\Users\ADMINI~1\AppData\Local\Temp\2\vault-tls4169358130`, - }, - { - name: "unix path", - certDirectory: "/tmp/vault-tls4169358130", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg, err := parseDevTLSConfig("file", tt.certDirectory) - require.NoError(t, err) - require.Equal(t, fmt.Sprintf("%s/%s", tt.certDirectory, VaultDevCertFilename), cfg.Listeners[0].TLSCertFile) - require.Equal(t, fmt.Sprintf("%s/%s", tt.certDirectory, VaultDevKeyFilename), cfg.Listeners[0].TLSKeyFile) - }) - } -} diff --git a/command/server/config_test_helpers.go b/command/server/config_test_helpers.go deleted file mode 100644 index be6113aae..000000000 --- a/command/server/config_test_helpers.go +++ /dev/null @@ -1,1257 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package server - -import ( - "fmt" - "reflect" - "sort" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/go-test/deep" - "github.com/hashicorp/hcl" - "github.com/hashicorp/hcl/hcl/ast" - "github.com/hashicorp/hcl/hcl/token" - "github.com/hashicorp/vault/internalshared/configutil" -) - -var DefaultCustomHeaders = map[string]map[string]string{ - "default": { - "Strict-Transport-Security": configutil.StrictTransportSecurity, - }, -} - -func boolPointer(x bool) *bool { - return &x -} - -func testConfigRaftRetryJoin(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/raft_retry_join.hcl") - if err != nil { - t.Fatal(err) - } - retryJoinConfig := `[{"leader_api_addr":"http://127.0.0.1:8200"},{"leader_api_addr":"http://127.0.0.2:8200"},{"leader_api_addr":"http://127.0.0.3:8200"}]` - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8200", - CustomResponseHeaders: DefaultCustomHeaders, - }, - }, - DisableMlock: true, - }, - - Storage: &Storage{ - Type: "raft", - Config: map[string]string{ - "path": "/storage/path/raft", - "node_id": "raft1", - "retry_join": retryJoinConfig, - }, - }, - } - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func testLoadConfigFile_topLevel(t *testing.T, entropy *configutil.Entropy) { - config, err := LoadConfigFile("./test-fixtures/config2.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:443", - CustomResponseHeaders: DefaultCustomHeaders, - }, - }, - - Telemetry: &configutil.Telemetry{ - StatsdAddr: "bar", - StatsiteAddr: "foo", - DisableHostname: false, - DogStatsDAddr: "127.0.0.1:7254", - DogStatsDTags: []string{"tag_1:val_1", "tag_2:val_2"}, - PrometheusRetentionTime: 30 * time.Second, - UsageGaugePeriod: 5 * time.Minute, - MaximumGaugeCardinality: 125, - LeaseMetricsEpsilon: time.Hour, - NumLeaseMetricsTimeBuckets: 168, - LeaseMetricsNameSpaceLabels: false, - }, - - DisableMlock: true, - - PidFile: "./pidfile", - - ClusterName: "testcluster", - - Seals: []*configutil.KMS{ - { - Type: "nopurpose", - }, - { - Type: "stringpurpose", - Purpose: []string{"foo"}, - }, - { - Type: "commastringpurpose", - Purpose: []string{"foo", "bar"}, - }, - { - Type: "slicepurpose", - Purpose: []string{"zip", "zap"}, - }, - }, - }, - - Storage: &Storage{ - Type: "consul", - RedirectAddr: "top_level_api_addr", - ClusterAddr: "top_level_cluster_addr", - Config: map[string]string{ - "foo": "bar", - }, - }, - - HAStorage: &Storage{ - Type: "consul", - RedirectAddr: "top_level_api_addr", - ClusterAddr: "top_level_cluster_addr", - Config: map[string]string{ - "bar": "baz", - }, - DisableClustering: true, - }, - - ServiceRegistration: &ServiceRegistration{ - Type: "consul", - Config: map[string]string{ - "foo": "bar", - }, - }, - - DisableCache: true, - DisableCacheRaw: true, - EnableUI: true, - EnableUIRaw: true, - - EnableRawEndpoint: true, - EnableRawEndpointRaw: true, - - DisableSealWrap: true, - DisableSealWrapRaw: true, - - MaxLeaseTTL: 10 * time.Hour, - MaxLeaseTTLRaw: "10h", - DefaultLeaseTTL: 10 * time.Hour, - DefaultLeaseTTLRaw: "10h", - - APIAddr: "top_level_api_addr", - ClusterAddr: "top_level_cluster_addr", - } - addExpectedEntConfig(expected, []string{}) - - if entropy != nil { - expected.Entropy = entropy - } - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func testLoadConfigFile_json2(t *testing.T, entropy *configutil.Entropy) { - config, err := LoadConfigFile("./test-fixtures/config2.hcl.json") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:443", - CustomResponseHeaders: DefaultCustomHeaders, - }, - { - Type: "tcp", - Address: "127.0.0.1:444", - CustomResponseHeaders: DefaultCustomHeaders, - }, - }, - - Telemetry: &configutil.Telemetry{ - StatsiteAddr: "foo", - StatsdAddr: "bar", - DisableHostname: true, - UsageGaugePeriod: 5 * time.Minute, - MaximumGaugeCardinality: 125, - CirconusAPIToken: "0", - CirconusAPIApp: "vault", - CirconusAPIURL: "http://api.circonus.com/v2", - CirconusSubmissionInterval: "10s", - CirconusCheckSubmissionURL: "https://someplace.com/metrics", - CirconusCheckID: "0", - CirconusCheckForceMetricActivation: "true", - CirconusCheckInstanceID: "node1:vault", - CirconusCheckSearchTag: "service:vault", - CirconusCheckDisplayName: "node1:vault", - CirconusCheckTags: "cat1:tag1,cat2:tag2", - CirconusBrokerID: "0", - CirconusBrokerSelectTag: "dc:sfo", - PrometheusRetentionTime: 30 * time.Second, - LeaseMetricsEpsilon: time.Hour, - NumLeaseMetricsTimeBuckets: 168, - LeaseMetricsNameSpaceLabels: false, - }, - }, - - Storage: &Storage{ - Type: "consul", - Config: map[string]string{ - "foo": "bar", - }, - }, - - HAStorage: &Storage{ - Type: "consul", - Config: map[string]string{ - "bar": "baz", - }, - DisableClustering: true, - }, - - ServiceRegistration: &ServiceRegistration{ - Type: "consul", - Config: map[string]string{ - "foo": "bar", - }, - }, - - CacheSize: 45678, - - EnableUI: true, - EnableUIRaw: true, - - EnableRawEndpoint: true, - EnableRawEndpointRaw: true, - - DisableSealWrap: true, - DisableSealWrapRaw: true, - } - addExpectedEntConfig(expected, []string{"http"}) - - if entropy != nil { - expected.Entropy = entropy - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func testParseEntropy(t *testing.T, oss bool) { - tests := []struct { - inConfig string - outErr error - outEntropy configutil.Entropy - }{ - { - inConfig: `entropy "seal" { - mode = "augmentation" - }`, - outErr: nil, - outEntropy: configutil.Entropy{Mode: configutil.EntropyAugmentation}, - }, - { - inConfig: `entropy "seal" { - mode = "a_mode_that_is_not_supported" - }`, - outErr: fmt.Errorf("the specified entropy mode %q is not supported", "a_mode_that_is_not_supported"), - }, - { - inConfig: `entropy "device_that_is_not_supported" { - mode = "augmentation" - }`, - outErr: fmt.Errorf("only the %q type of external entropy is supported", "seal"), - }, - { - inConfig: `entropy "seal" { - mode = "augmentation" - } - entropy "seal" { - mode = "augmentation" - }`, - outErr: fmt.Errorf("only one %q block is permitted", "entropy"), - }, - } - - config := Config{ - SharedConfig: &configutil.SharedConfig{}, - } - - for _, test := range tests { - obj, _ := hcl.Parse(strings.TrimSpace(test.inConfig)) - list, _ := obj.Node.(*ast.ObjectList) - objList := list.Filter("entropy") - err := configutil.ParseEntropy(config.SharedConfig, objList, "entropy") - // validate the error, both should be nil or have the same Error() - switch { - case oss: - if config.Entropy != nil { - t.Fatalf("parsing Entropy should not be possible in oss but got a non-nil config.Entropy: %#v", config.Entropy) - } - case err != nil && test.outErr != nil: - if err.Error() != test.outErr.Error() { - t.Fatalf("error mismatch: expected %#v got %#v", err, test.outErr) - } - case err != test.outErr: - t.Fatalf("error mismatch: expected %#v got %#v", err, test.outErr) - case err == nil && config.Entropy != nil && *config.Entropy != test.outEntropy: - t.Fatalf("entropy config mismatch: expected %#v got %#v", test.outEntropy, *config.Entropy) - } - } -} - -func testLoadConfigFileIntegerAndBooleanValues(t *testing.T) { - testLoadConfigFileIntegerAndBooleanValuesCommon(t, "./test-fixtures/config4.hcl") -} - -func testLoadConfigFileIntegerAndBooleanValuesJson(t *testing.T) { - testLoadConfigFileIntegerAndBooleanValuesCommon(t, "./test-fixtures/config4.hcl.json") -} - -func testLoadConfigFileIntegerAndBooleanValuesCommon(t *testing.T, path string) { - config, err := LoadConfigFile(path) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:8200", - CustomResponseHeaders: DefaultCustomHeaders, - }, - }, - DisableMlock: true, - }, - - Storage: &Storage{ - Type: "raft", - Config: map[string]string{ - "path": "/storage/path/raft", - "node_id": "raft1", - "performance_multiplier": "1", - "foo": "bar", - "baz": "true", - }, - ClusterAddr: "127.0.0.1:8201", - }, - - ClusterAddr: "127.0.0.1:8201", - - DisableCache: true, - DisableCacheRaw: true, - EnableUI: true, - EnableUIRaw: true, - } - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func testLoadConfigFile(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:443", - CustomResponseHeaders: DefaultCustomHeaders, - }, - }, - - Telemetry: &configutil.Telemetry{ - StatsdAddr: "bar", - StatsiteAddr: "foo", - DisableHostname: false, - UsageGaugePeriod: 5 * time.Minute, - MaximumGaugeCardinality: 100, - DogStatsDAddr: "127.0.0.1:7254", - DogStatsDTags: []string{"tag_1:val_1", "tag_2:val_2"}, - PrometheusRetentionTime: configutil.PrometheusDefaultRetentionTime, - MetricsPrefix: "myprefix", - LeaseMetricsEpsilon: time.Hour, - NumLeaseMetricsTimeBuckets: 168, - LeaseMetricsNameSpaceLabels: false, - }, - - DisableMlock: true, - - Entropy: nil, - - PidFile: "./pidfile", - - ClusterName: "testcluster", - }, - - Storage: &Storage{ - Type: "consul", - RedirectAddr: "foo", - Config: map[string]string{ - "foo": "bar", - }, - }, - - HAStorage: &Storage{ - Type: "consul", - RedirectAddr: "snafu", - Config: map[string]string{ - "bar": "baz", - }, - DisableClustering: true, - }, - - ServiceRegistration: &ServiceRegistration{ - Type: "consul", - Config: map[string]string{ - "foo": "bar", - }, - }, - - DisableCache: true, - DisableCacheRaw: true, - DisablePrintableCheckRaw: true, - DisablePrintableCheck: true, - EnableUI: true, - EnableUIRaw: true, - - EnableRawEndpoint: true, - EnableRawEndpointRaw: true, - - EnableIntrospectionEndpoint: true, - EnableIntrospectionEndpointRaw: true, - - DisableSealWrap: true, - DisableSealWrapRaw: true, - - MaxLeaseTTL: 10 * time.Hour, - MaxLeaseTTLRaw: "10h", - DefaultLeaseTTL: 10 * time.Hour, - DefaultLeaseTTLRaw: "10h", - - EnableResponseHeaderHostname: true, - EnableResponseHeaderHostnameRaw: true, - EnableResponseHeaderRaftNodeID: true, - EnableResponseHeaderRaftNodeIDRaw: true, - - LicensePath: "/path/to/license", - } - - addExpectedEntConfig(expected, []string{}) - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func testUnknownFieldValidationStorageAndListener(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/storage-listener-config.json") - if err != nil { - t.Fatalf("err: %s", err) - } - if len(config.UnusedKeys) != 0 { - t.Fatalf("unused keys for valid config are %+v\n", config.UnusedKeys) - } -} - -func testUnknownFieldValidation(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := []configutil.ConfigError{ - { - Problem: "unknown or unsupported field bad_value found in configuration", - Position: token.Pos{ - Filename: "./test-fixtures/config.hcl", - Offset: 652, - Line: 37, - Column: 5, - }, - }, - } - errors := config.Validate("./test-fixtures/config.hcl") - - for _, er1 := range errors { - found := false - if strings.Contains(er1.String(), "sentinel") { - // This happens on OSS, and is fine - continue - } - for _, ex := range expected { - // TODO: Only test the string, pos may change - if ex.Problem == er1.Problem && reflect.DeepEqual(ex.Position, er1.Position) { - found = true - break - } - } - if !found { - t.Fatalf("found unexpected error: %v", er1.String()) - } - } - for _, ex := range expected { - found := false - for _, er1 := range errors { - if ex.Problem == er1.Problem && reflect.DeepEqual(ex.Position, er1.Position) { - found = true - } - } - if !found { - t.Fatalf("could not find expected error: %v", ex.String()) - } - } -} - -// testUnknownFieldValidationJson tests that this valid json config does not result in -// errors. Prior to VAULT-8519, it reported errors even with a valid config that was -// parsed properly. -func testUnknownFieldValidationJson(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config_small.json") - if err != nil { - t.Fatalf("err: %s", err) - } - - errors := config.Validate("./test-fixtures/config_small.json") - if errors != nil { - t.Fatal(errors) - } -} - -// testUnknownFieldValidationHcl tests that this valid hcl config does not result in -// errors. Prior to VAULT-8519, the json version of this config reported errors even -// with a valid config that was parsed properly. -// In short, this ensures the same for HCL as we test in testUnknownFieldValidationJson -func testUnknownFieldValidationHcl(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config_small.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - - errors := config.Validate("./test-fixtures/config_small.hcl") - if errors != nil { - t.Fatal(errors) - } -} - -// testConfigWithAdministrativeNamespaceJson tests that a config with a valid administrative namespace path is correctly validated and loaded. -func testConfigWithAdministrativeNamespaceJson(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config_with_valid_admin_ns.json") - require.NoError(t, err) - - configErrors := config.Validate("./test-fixtures/config_with_valid_admin_ns.json") - require.Empty(t, configErrors) - - require.NotEmpty(t, config.AdministrativeNamespacePath) -} - -// testConfigWithAdministrativeNamespaceHcl tests that a config with a valid administrative namespace path is correctly validated and loaded. -func testConfigWithAdministrativeNamespaceHcl(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config_with_valid_admin_ns.hcl") - require.NoError(t, err) - - configErrors := config.Validate("./test-fixtures/config_with_valid_admin_ns.hcl") - require.Empty(t, configErrors) - - require.NotEmpty(t, config.AdministrativeNamespacePath) -} - -func testLoadConfigFile_json(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config.hcl.json") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:443", - CustomResponseHeaders: DefaultCustomHeaders, - }, - }, - - Telemetry: &configutil.Telemetry{ - StatsiteAddr: "baz", - StatsdAddr: "", - DisableHostname: false, - UsageGaugePeriod: 5 * time.Minute, - MaximumGaugeCardinality: 100, - CirconusAPIToken: "", - CirconusAPIApp: "", - CirconusAPIURL: "", - CirconusSubmissionInterval: "", - CirconusCheckSubmissionURL: "", - CirconusCheckID: "", - CirconusCheckForceMetricActivation: "", - CirconusCheckInstanceID: "", - CirconusCheckSearchTag: "", - CirconusCheckDisplayName: "", - CirconusCheckTags: "", - CirconusBrokerID: "", - CirconusBrokerSelectTag: "", - PrometheusRetentionTime: configutil.PrometheusDefaultRetentionTime, - LeaseMetricsEpsilon: time.Hour, - NumLeaseMetricsTimeBuckets: 168, - LeaseMetricsNameSpaceLabels: false, - }, - - PidFile: "./pidfile", - Entropy: nil, - ClusterName: "testcluster", - }, - - Storage: &Storage{ - Type: "consul", - Config: map[string]string{ - "foo": "bar", - }, - DisableClustering: true, - }, - - ServiceRegistration: &ServiceRegistration{ - Type: "consul", - Config: map[string]string{ - "foo": "bar", - }, - }, - - ClusterCipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", - - MaxLeaseTTL: 10 * time.Hour, - MaxLeaseTTLRaw: "10h", - DefaultLeaseTTL: 10 * time.Hour, - DefaultLeaseTTLRaw: "10h", - DisableCacheRaw: interface{}(nil), - EnableUI: true, - EnableUIRaw: true, - EnableRawEndpoint: true, - EnableRawEndpointRaw: true, - DisableSealWrap: true, - DisableSealWrapRaw: true, - } - - addExpectedEntConfig(expected, []string{}) - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func testLoadConfigDir(t *testing.T) { - config, err := LoadConfigDir("./test-fixtures/config-dir") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - DisableMlock: true, - - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:443", - CustomResponseHeaders: DefaultCustomHeaders, - }, - }, - - Telemetry: &configutil.Telemetry{ - StatsiteAddr: "qux", - StatsdAddr: "baz", - DisableHostname: true, - UsageGaugePeriod: 5 * time.Minute, - MaximumGaugeCardinality: 100, - PrometheusRetentionTime: configutil.PrometheusDefaultRetentionTime, - LeaseMetricsEpsilon: time.Hour, - NumLeaseMetricsTimeBuckets: 168, - LeaseMetricsNameSpaceLabels: false, - }, - ClusterName: "testcluster", - }, - - DisableCache: true, - DisableClustering: false, - DisableClusteringRaw: false, - - APIAddr: "https://vault.local", - ClusterAddr: "https://127.0.0.1:444", - - Storage: &Storage{ - Type: "consul", - Config: map[string]string{ - "foo": "bar", - }, - RedirectAddr: "https://vault.local", - ClusterAddr: "https://127.0.0.1:444", - DisableClustering: false, - }, - - EnableUI: true, - - EnableRawEndpoint: true, - - MaxLeaseTTL: 10 * time.Hour, - DefaultLeaseTTL: 10 * time.Hour, - } - - addExpectedEntConfig(expected, []string{"http"}) - - config.Prune() - if diff := deep.Equal(config, expected); diff != nil { - t.Fatal(diff) - } -} - -func testConfig_Sanitized(t *testing.T) { - config, err := LoadConfigFile("./test-fixtures/config3.hcl") - if err != nil { - t.Fatalf("err: %s", err) - } - sanitizedConfig := config.Sanitized() - - expected := map[string]interface{}{ - "api_addr": "top_level_api_addr", - "cache_size": 0, - "cluster_addr": "top_level_cluster_addr", - "cluster_cipher_suites": "", - "cluster_name": "testcluster", - "default_lease_ttl": (365 * 24 * time.Hour) / time.Second, - "default_max_request_duration": 0 * time.Second, - "disable_cache": true, - "disable_clustering": false, - "disable_indexing": false, - "disable_mlock": true, - "disable_performance_standby": false, - "experiments": []string(nil), - "plugin_file_uid": 0, - "plugin_file_permissions": 0, - "disable_printable_check": false, - "disable_sealwrap": true, - "raw_storage_endpoint": true, - "introspection_endpoint": false, - "disable_sentinel_trace": true, - "detect_deadlocks": "", - "enable_ui": true, - "enable_response_header_hostname": false, - "enable_response_header_raft_node_id": false, - "log_requests_level": "basic", - "ha_storage": map[string]interface{}{ - "cluster_addr": "top_level_cluster_addr", - "disable_clustering": true, - "redirect_addr": "top_level_api_addr", - "type": "consul", - }, - "listeners": []interface{}{ - map[string]interface{}{ - "config": map[string]interface{}{ - "address": "127.0.0.1:443", - }, - "type": "tcp", - }, - }, - "log_format": "", - "log_level": "", - "max_lease_ttl": (30 * 24 * time.Hour) / time.Second, - "pid_file": "./pidfile", - "plugin_directory": "", - "seals": []interface{}{ - map[string]interface{}{ - "disabled": false, - "type": "awskms", - }, - }, - "storage": map[string]interface{}{ - "cluster_addr": "top_level_cluster_addr", - "disable_clustering": false, - "redirect_addr": "top_level_api_addr", - "type": "consul", - }, - "service_registration": map[string]interface{}{ - "type": "consul", - }, - "telemetry": map[string]interface{}{ - "usage_gauge_period": 5 * time.Minute, - "maximum_gauge_cardinality": 100, - "circonus_api_app": "", - "circonus_api_token": "", - "circonus_api_url": "", - "circonus_broker_id": "", - "circonus_broker_select_tag": "", - "circonus_check_display_name": "", - "circonus_check_force_metric_activation": "", - "circonus_check_id": "", - "circonus_check_instance_id": "", - "circonus_check_search_tag": "", - "circonus_submission_url": "", - "circonus_check_tags": "", - "circonus_submission_interval": "", - "disable_hostname": false, - "metrics_prefix": "pfx", - "dogstatsd_addr": "", - "dogstatsd_tags": []string(nil), - "prometheus_retention_time": 24 * time.Hour, - "statsd_address": "bar", - "statsite_address": "", - "lease_metrics_epsilon": time.Hour, - "num_lease_metrics_buckets": 168, - "add_lease_metrics_namespace_labels": false, - }, - "administrative_namespace_path": "admin/", - "imprecise_lease_role_tracking": false, - } - - addExpectedEntSanitizedConfig(expected, []string{"http"}) - - config.Prune() - if diff := deep.Equal(sanitizedConfig, expected); len(diff) > 0 { - t.Fatalf("bad, diff: %#v", diff) - } -} - -func testParseListeners(t *testing.T) { - obj, _ := hcl.Parse(strings.TrimSpace(` -listener "tcp" { - address = "127.0.0.1:443" - cluster_address = "127.0.0.1:8201" - tls_disable = false - tls_cert_file = "./certs/server.crt" - tls_key_file = "./certs/server.key" - tls_client_ca_file = "./certs/rootca.crt" - tls_min_version = "tls12" - tls_max_version = "tls13" - tls_require_and_verify_client_cert = true - tls_disable_client_certs = true - telemetry { - unauthenticated_metrics_access = true - } - profiling { - unauthenticated_pprof_access = true - } - agent_api { - enable_quit = true - } - proxy_api { - enable_quit = true - } -}`)) - - config := Config{ - SharedConfig: &configutil.SharedConfig{}, - } - list, _ := obj.Node.(*ast.ObjectList) - objList := list.Filter("listener") - configutil.ParseListeners(config.SharedConfig, objList) - listeners := config.Listeners - if len(listeners) == 0 { - t.Fatalf("expected at least one listener in the config") - } - listener := listeners[0] - if listener.Type != "tcp" { - t.Fatalf("expected tcp listener in the config") - } - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1:443", - ClusterAddress: "127.0.0.1:8201", - TLSCertFile: "./certs/server.crt", - TLSKeyFile: "./certs/server.key", - TLSClientCAFile: "./certs/rootca.crt", - TLSMinVersion: "tls12", - TLSMaxVersion: "tls13", - TLSRequireAndVerifyClientCert: true, - TLSDisableClientCerts: true, - Telemetry: configutil.ListenerTelemetry{ - UnauthenticatedMetricsAccess: true, - }, - Profiling: configutil.ListenerProfiling{ - UnauthenticatedPProfAccess: true, - }, - AgentAPI: &configutil.AgentAPI{ - EnableQuit: true, - }, - ProxyAPI: &configutil.ProxyAPI{ - EnableQuit: true, - }, - CustomResponseHeaders: DefaultCustomHeaders, - }, - }, - }, - } - config.Prune() - if diff := deep.Equal(config, *expected); diff != nil { - t.Fatal(diff) - } -} - -func testParseUserLockouts(t *testing.T) { - obj, _ := hcl.Parse(strings.TrimSpace(` - user_lockout "all" { - lockout_duration = "40m" - lockout_counter_reset = "45m" - disable_lockout = "false" - } - user_lockout "userpass" { - lockout_threshold = "100" - lockout_duration = "20m" - } - user_lockout "ldap" { - disable_lockout = "true" - }`)) - - config := Config{ - SharedConfig: &configutil.SharedConfig{}, - } - list, _ := obj.Node.(*ast.ObjectList) - objList := list.Filter("user_lockout") - configutil.ParseUserLockouts(config.SharedConfig, objList) - - sort.Slice(config.SharedConfig.UserLockouts[:], func(i, j int) bool { - return config.SharedConfig.UserLockouts[i].Type < config.SharedConfig.UserLockouts[j].Type - }) - - expected := &Config{ - SharedConfig: &configutil.SharedConfig{ - UserLockouts: []*configutil.UserLockout{ - { - Type: "all", - LockoutThreshold: 5, - LockoutDuration: 2400000000000, - LockoutCounterReset: 2700000000000, - DisableLockout: false, - }, - { - Type: "userpass", - LockoutThreshold: 100, - LockoutDuration: 1200000000000, - LockoutCounterReset: 2700000000000, - DisableLockout: false, - }, - { - Type: "ldap", - LockoutThreshold: 5, - LockoutDuration: 2400000000000, - LockoutCounterReset: 2700000000000, - DisableLockout: true, - }, - }, - }, - } - - sort.Slice(expected.SharedConfig.UserLockouts[:], func(i, j int) bool { - return expected.SharedConfig.UserLockouts[i].Type < expected.SharedConfig.UserLockouts[j].Type - }) - config.Prune() - require.Equal(t, config, *expected) -} - -func testParseSockaddrTemplate(t *testing.T) { - config, err := ParseConfig(` -api_addr = < 0, "test description %s", testcase.TestDescription) - require.Equal(t, testcase.TLSDisable, cfg.Listeners[0].TLSDisable, "test description %s", testcase.TestDescription) - } - require.Equal(t, testcase.CertPathEmpty, len(certPath) == 0, "test description %s", testcase.TestDescription) - require.Equal(t, testcase.ErrNotNil, (err != nil), "test description %s", testcase.TestDescription) - } -} diff --git a/command/ssh_test.go b/command/ssh_test.go deleted file mode 100644 index 32f16f79e..000000000 --- a/command/ssh_test.go +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testSSHCommand(tb testing.TB) (*cli.MockUi, *SSHCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &SSHCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestSSHCommand_Run(t *testing.T) { - t.Parallel() - t.Skip("Need a way to setup target infrastructure") -} - -func TestParseSSHCommand(t *testing.T) { - t.Parallel() - - _, cmd := testSSHCommand(t) - tests := []struct { - name string - args []string - hostname string - username string - port string - err error - }{ - { - "Parse just a hostname", - []string{ - "hostname", - }, - "hostname", - "", - "", - nil, - }, - { - "Parse the standard username@hostname", - []string{ - "username@hostname", - }, - "hostname", - "username", - "", - nil, - }, - { - "Parse the username out of -o User=username", - []string{ - "-o", "User=username", - "hostname", - }, - "hostname", - "username", - "", - nil, - }, - { - "If the username is specified with -o User=username and realname@hostname prefer realname@", - []string{ - "-o", "User=username", - "realname@hostname", - }, - "hostname", - "realname", - "", - nil, - }, - { - "Parse the port out of -o Port=2222", - []string{ - "-o", "Port=2222", - "hostname", - }, - "hostname", - "", - "2222", - nil, - }, - { - "Parse the port out of -p 2222", - []string{ - "-p", "2222", - "hostname", - }, - "hostname", - "", - "2222", - nil, - }, - { - "If port is defined with -o Port=2222 and -p 2244 prefer -p", - []string{ - "-p", "2244", - "-o", "Port=2222", - "hostname", - }, - "hostname", - "", - "2244", - nil, - }, - { - "Ssh args with a command", - []string{ - "hostname", - "command", - }, - "hostname", - "", - "", - nil, - }, - { - "Flags after the ssh command are not passed because they are part of the command", - []string{ - "username@hostname", - "command", - "-p 22", - }, - "hostname", - "username", - "", - nil, - }, - { - "Allow single args which don't have a value", - []string{ - "-v", - "hostname", - }, - "hostname", - "", - "", - nil, - }, - { - "Allow single args before and after the hostname and command", - []string{ - "-v", - "hostname", - "-v", - "command", - "-v", - }, - "hostname", - "", - "", - nil, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - hostname, username, port, err := cmd.parseSSHCommand(test.args) - if err != test.err { - t.Errorf("got error: %q want %q", err, test.err) - } - if hostname != test.hostname { - t.Errorf("got hostname: %q want %q", hostname, test.hostname) - } - if username != test.username { - t.Errorf("got username: %q want %q", username, test.username) - } - if port != test.port { - t.Errorf("got port: %q want %q", port, test.port) - } - }) - } -} - -func TestIsSingleSSHArg(t *testing.T) { - t.Parallel() - - _, cmd := testSSHCommand(t) - tests := []struct { - name string - arg string - want bool - }{ - { - "-v is a single ssh arg", - "-v", - true, - }, - { - "-o is NOT a single ssh arg", - "-o", - false, - }, - { - "Repeated args like -vvv is still a single ssh arg", - "-vvv", - true, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got := cmd.isSingleSSHArg(test.arg) - if got != test.want { - t.Errorf("arg %q got %v want %v", test.arg, got, test.want) - } - }) - } -} - -// TestSSHCommandOmitFlagWarning checks if flags warning messages are printed -// in the output of the CLI command or not. If so, it will fail. -func TestSSHCommandOmitFlagWarning(t *testing.T) { - t.Parallel() - - ui, cmd := testSSHCommand(t) - - _ = cmd.Run([]string{"-mode", "ca", "-role", "otp_key_role", "user@1.2.3.4", "-extraFlag", "bug"}) - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if strings.Contains(combined, "Command flags must be provided before positional arguments. The following arguments will not be parsed as flags") { - t.Fatalf("ssh command displayed flag warnings") - } -} diff --git a/command/status_test.go b/command/status_test.go deleted file mode 100644 index b423edb68..000000000 --- a/command/status_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testStatusCommand(tb testing.TB) (*cli.MockUi, *StatusCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &StatusCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestStatusCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - sealed bool - out string - code int - }{ - { - "unsealed", - nil, - false, - "Sealed false", - 0, - }, - { - "sealed", - nil, - true, - "Sealed true", - 2, - }, - { - "args", - []string{"foo"}, - false, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if tc.sealed { - if err := client.Sys().Seal(); err != nil { - t.Fatal(err) - } - } - - ui, cmd := testStatusCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testStatusCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 1; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error checking seal status: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testStatusCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/test-backend/main.go b/command/test-backend/main.go deleted file mode 100644 index 69a6fcd0a..000000000 --- a/command/test-backend/main.go +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package test_backend diff --git a/command/test-fixtures/config.hcl b/command/test-fixtures/config.hcl deleted file mode 100644 index 9161fff45..000000000 --- a/command/test-fixtures/config.hcl +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -token_helper = "foo" diff --git a/command/test-fixtures/policy.hcl b/command/test-fixtures/policy.hcl deleted file mode 100644 index 6160bf780..000000000 --- a/command/test-fixtures/policy.hcl +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -path "secret/foo" { - policy = "write" -} - -path "secret/bar/*" { - capabilities = ["create", "read", "update"] -} diff --git a/command/token/helper_external_test.go b/command/token/helper_external_test.go deleted file mode 100644 index d7b032360..000000000 --- a/command/token/helper_external_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package token - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "runtime" - "strings" - "testing" -) - -func TestExternalTokenHelperPath(t *testing.T) { - cases := map[string]string{} - - unixCases := map[string]string{ - "/foo": "/foo", - } - windowsCases := map[string]string{ - "C:/foo": "C:/foo", - `C:\Program Files`: `C:\Program Files`, - } - - var runtimeCases map[string]string - if runtime.GOOS == "windows" { - runtimeCases = windowsCases - } else { - runtimeCases = unixCases - } - - for k, v := range runtimeCases { - cases[k] = v - } - - // We don't expect those to actually exist, so we expect an error. For now, - // I'm commenting out the rest of this code as we don't have real external - // helpers to test with and the os.Stat will fail with our fake test cases. - /* - for k, v := range cases { - actual, err := ExternalTokenHelperPath(k) - if err != nil { - t.Fatalf("error getting external helper path: %v", err) - } - if actual != v { - t.Fatalf( - "input: %s, expected: %s, got: %s", - k, v, actual) - } - } - */ -} - -func TestExternalTokenHelper(t *testing.T) { - Test(t, testExternalTokenHelper(t)) -} - -func testExternalTokenHelper(t *testing.T) *ExternalTokenHelper { - return &ExternalTokenHelper{BinaryPath: helperPath("helper"), Env: helperEnv()} -} - -func helperPath(s ...string) string { - cs := []string{"-test.run=TestExternalTokenHelperProcess", "--"} - cs = append(cs, s...) - return fmt.Sprintf( - "%s %s", - os.Args[0], - strings.Join(cs, " ")) -} - -func helperEnv() []string { - var env []string - - tf, err := ioutil.TempFile("", "vault") - if err != nil { - panic(err) - } - tf.Close() - - env = append(env, "GO_HELPER_PATH="+tf.Name(), "GO_WANT_HELPER_PROCESS=1") - return env -} - -// This is not a real test. This is just a helper process kicked off by tests. -func TestExternalTokenHelperProcess(*testing.T) { - if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { - return - } - - defer os.Exit(0) - - args := os.Args - for len(args) > 0 { - if args[0] == "--" { - args = args[1:] - break - } - - args = args[1:] - } - - if len(args) == 0 { - fmt.Fprintf(os.Stderr, "No command\n") - os.Exit(2) - } - - cmd, args := args[0], args[1:] - switch cmd { - case "helper": - path := os.Getenv("GO_HELPER_PATH") - - switch args[0] { - case "erase": - os.Remove(path) - case "get": - f, err := os.Open(path) - if os.IsNotExist(err) { - return - } - if err != nil { - fmt.Fprintf(os.Stderr, "Err: %s\n", err) - os.Exit(1) - } - defer f.Close() - io.Copy(os.Stdout, f) - case "store": - f, err := os.Create(path) - if err != nil { - fmt.Fprintf(os.Stderr, "Err: %s\n", err) - os.Exit(1) - } - defer f.Close() - io.Copy(f, os.Stdin) - } - default: - fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd) - os.Exit(2) - } -} diff --git a/command/token/helper_internal_test.go b/command/token/helper_internal_test.go deleted file mode 100644 index 10a7a0cc9..000000000 --- a/command/token/helper_internal_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package token - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -// TestCommand re-uses the existing Test function to ensure proper behavior of -// the internal token helper -func TestCommand(t *testing.T) { - helper, err := NewInternalTokenHelper() - if err != nil { - t.Fatal(err) - } - Test(t, helper) -} - -func TestInternalHelperFilePerms(t *testing.T) { - tmpDir, err := ioutil.TempDir("", t.Name()) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - helper, err := NewInternalTokenHelper() - if err != nil { - t.Fatal(err) - } - helper.homeDir = tmpDir - - tmpFile := filepath.Join(tmpDir, ".vault-token") - f, err := os.Create(tmpFile) - if err != nil { - t.Fatal(err) - } - defer f.Close() - - fi, err := os.Stat(tmpFile) - if err != nil { - t.Fatal(err) - } - - if fi.Mode().Perm()&0o04 != 0o04 { - t.Fatalf("expected world-readable/writable permission bits, got: %o", fi.Mode().Perm()) - } - - err = helper.Store("bogus_token") - if err != nil { - t.Fatal(err) - } - - fi, err = os.Stat(tmpFile) - if err != nil { - t.Fatal(err) - } - - if fi.Mode().Perm()&0o04 != 0 { - t.Fatalf("expected no world-readable/writable permission bits, got: %o", fi.Mode().Perm()) - } -} diff --git a/command/token/helper_testing.go b/command/token/helper_testing.go deleted file mode 100644 index e948092f4..000000000 --- a/command/token/helper_testing.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package token - -import ( - "sync" -) - -var _ TokenHelper = (*TestingTokenHelper)(nil) - -// TestingTokenHelper implements token.TokenHelper which runs entirely -// in-memory. This should not be used outside of testing. -type TestingTokenHelper struct { - lock sync.RWMutex - token string -} - -func NewTestingTokenHelper() *TestingTokenHelper { - return &TestingTokenHelper{} -} - -func (t *TestingTokenHelper) Erase() error { - t.lock.Lock() - defer t.lock.Unlock() - t.token = "" - return nil -} - -func (t *TestingTokenHelper) Get() (string, error) { - t.lock.RLock() - defer t.lock.RUnlock() - return t.token, nil -} - -func (t *TestingTokenHelper) Path() string { - return "" -} - -func (t *TestingTokenHelper) Store(token string) error { - t.lock.Lock() - defer t.lock.Unlock() - t.token = token - return nil -} diff --git a/command/token/testing.go b/command/token/testing.go deleted file mode 100644 index acfc84338..000000000 --- a/command/token/testing.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package token - -import ( - "fmt" - "os" - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -// Test is a public function that can be used in other tests to -// test that a helper is functioning properly. -func Test(t *testing.T, h TokenHelper) { - if err := h.Store("foo"); err != nil { - t.Fatalf("err: %s", err) - } - - v, err := h.Get() - if err != nil { - t.Fatalf("err: %s", err) - } - - if v != "foo" { - t.Fatalf("bad: %#v", v) - } - - if err := h.Erase(); err != nil { - t.Fatalf("err: %s", err) - } - - v, err = h.Get() - if err != nil { - t.Fatalf("err: %s", err) - } - - if v != "" { - t.Fatalf("bad: %#v", v) - } -} - -// TestProcess is used to re-execute this test in order to use it as the -// helper process. For this to work, the TestExternalTokenHelperProcess function must -// exist. -func TestProcess(t *testing.T, s ...string) { - h := &ExternalTokenHelper{BinaryPath: TestProcessPath(t, s...)} - Test(t, h) -} - -// TestProcessPath returns the path to the test process. -func TestProcessPath(t *testing.T, s ...string) string { - cs := []string{"-test.run=TestExternalTokenHelperProcess", "--", "GO_WANT_HELPER_PROCESS"} - cs = append(cs, s...) - return fmt.Sprintf( - "%s %s", - os.Args[0], - strings.Join(cs, " ")) -} - -// TestExternalTokenHelperProcessCLI can be called to implement TestExternalTokenHelperProcess -// for TestProcess that just executes a CLI command. -func TestExternalTokenHelperProcessCLI(t *testing.T, cmd cli.Command) { - args := os.Args - for len(args) > 0 { - if args[0] == "--" { - args = args[1:] - break - } - - args = args[1:] - } - if len(args) == 0 || args[0] != "GO_WANT_HELPER_PROCESS" { - return - } - args = args[1:] - - os.Exit(cmd.Run(args)) -} diff --git a/command/token_capabilities_test.go b/command/token_capabilities_test.go deleted file mode 100644 index 6dffae53f..000000000 --- a/command/token_capabilities_test.go +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testTokenCapabilitiesCommand(tb testing.TB) (*cli.MockUi, *TokenCapabilitiesCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &TokenCapabilitiesCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestTokenCapabilitiesCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo", "bar", "zip"}, - "Too many arguments", - 1, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testTokenCapabilitiesCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("token", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - policy := `path "secret/foo" { capabilities = ["read"] }` - if err := client.Sys().PutPolicy("policy", policy); err != nil { - t.Error(err) - } - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"policy"}, - TTL: "30m", - }) - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { - t.Fatalf("missing auth data: %#v", secret) - } - token := secret.Auth.ClientToken - - ui, cmd := testTokenCapabilitiesCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - token, "secret/foo", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "read" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("local", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - policy := `path "secret/foo" { capabilities = ["read"] }` - if err := client.Sys().PutPolicy("policy", policy); err != nil { - t.Error(err) - } - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"policy"}, - TTL: "30m", - }) - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { - t.Fatalf("missing auth data: %#v", secret) - } - token := secret.Auth.ClientToken - - client.SetToken(token) - - ui, cmd := testTokenCapabilitiesCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/foo", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "read" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testTokenCapabilitiesCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "foo", "bar", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error listing capabilities: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("multiple_paths", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - _, cmd := testTokenCapabilitiesCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/foo,secret/bar", - }) - if exp := 1; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testTokenCapabilitiesCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/token_create_test.go b/command/token_create_test.go deleted file mode 100644 index 9991afb36..000000000 --- a/command/token_create_test.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "reflect" - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testTokenCreateCommand(tb testing.TB) (*cli.MockUi, *TokenCreateCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &TokenCreateCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestTokenCreateCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"abcd1234"}, - "Too many arguments", - 1, - }, - { - "default", - nil, - "token", - 0, - }, - { - "metadata", - []string{"-metadata", "foo=bar", "-metadata", "zip=zap"}, - "token", - 0, - }, - { - "policies", - []string{"-policy", "foo", "-policy", "bar"}, - "token", - 0, - }, - { - "field", - []string{ - "-field", "token_renewable", - }, - "false", - 0, - }, - { - "field_not_found", - []string{ - "-field", "not-a-real-field", - }, - "not present in secret", - 1, - }, - { - "ttl", - []string{"-ttl", "1d", "-explicit-max-ttl", "2d"}, - "token", - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testTokenCreateCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("default", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testTokenCreateCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-field", "token", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - token := strings.TrimSpace(ui.OutputWriter.String()) - secret, err := client.Auth().Token().Lookup(token) - if secret == nil || err != nil { - t.Fatal(err) - } - }) - - t.Run("metadata", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testTokenCreateCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-metadata", "foo=bar", - "-metadata", "zip=zap", - "-field", "token", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - token := strings.TrimSpace(ui.OutputWriter.String()) - secret, err := client.Auth().Token().Lookup(token) - if secret == nil || err != nil { - t.Fatal(err) - } - - meta, ok := secret.Data["meta"].(map[string]interface{}) - if !ok { - t.Fatalf("missing meta: %#v", secret) - } - if _, ok := meta["foo"]; !ok { - t.Errorf("missing meta.foo: %#v", meta) - } - if _, ok := meta["zip"]; !ok { - t.Errorf("missing meta.bar: %#v", meta) - } - }) - - t.Run("policies", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testTokenCreateCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-policy", "foo", - "-policy", "bar", - "-field", "token", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - token := strings.TrimSpace(ui.OutputWriter.String()) - secret, err := client.Auth().Token().Lookup(token) - if secret == nil || err != nil { - t.Fatal(err) - } - - raw, ok := secret.Data["policies"].([]interface{}) - if !ok { - t.Fatalf("missing policies: %#v", secret) - } - - policies := make([]string, len(raw)) - for i := range raw { - policies[i] = raw[i].(string) - } - - expected := []string{"bar", "default", "foo"} - if !reflect.DeepEqual(policies, expected) { - t.Errorf("expected %q to be %q", policies, expected) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testTokenCreateCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error creating token: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testTokenCreateCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/token_lookup_test.go b/command/token_lookup_test.go deleted file mode 100644 index db6807300..000000000 --- a/command/token_lookup_test.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testTokenLookupCommand(tb testing.TB) (*cli.MockUi, *TokenLookupCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &TokenLookupCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestTokenLookupCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "accessor_no_args", - []string{"-accessor"}, - "Not enough arguments", - 1, - }, - { - "accessor_too_many_args", - []string{"-accessor", "abcd1234", "efgh5678"}, - "Too many arguments", - 1, - }, - { - "too_many_args", - []string{"abcd1234", "efgh5678"}, - "Too many arguments", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testTokenLookupCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("token", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - token, _ := testTokenAndAccessor(t, client) - - ui, cmd := testTokenLookupCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - token, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := token - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("self", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testTokenLookupCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "display_name" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("accessor", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - _, accessor := testTokenAndAccessor(t, client) - - ui, cmd := testTokenLookupCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-accessor", - accessor, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := accessor - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testTokenLookupCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error looking up token: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testTokenLookupCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/token_renew_test.go b/command/token_renew_test.go deleted file mode 100644 index 70ec46e99..000000000 --- a/command/token_renew_test.go +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "encoding/json" - "strconv" - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testTokenRenewCommand(tb testing.TB) (*cli.MockUi, *TokenRenewCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &TokenRenewCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestTokenRenewCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo", "bar", "baz"}, - "Too many arguments", - 1, - }, - { - "default", - nil, - "", - 0, - }, - { - "increment", - []string{"-increment", "60s"}, - "", - 0, - }, - { - "increment_no_suffix", - []string{"-increment", "60"}, - "", - 0, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - // Login with the token so we can renew-self. - token, _ := testTokenAndAccessor(t, client) - client.SetToken(token) - - ui, cmd := testTokenRenewCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("token", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - token, _ := testTokenAndAccessor(t, client) - - _, cmd := testTokenRenewCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-increment", "30m", - token, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - secret, err := client.Auth().Token().Lookup(token) - if err != nil { - t.Fatal(err) - } - - str := string(secret.Data["ttl"].(json.Number)) - ttl, err := strconv.ParseInt(str, 10, 64) - if err != nil { - t.Fatalf("bad ttl: %#v", secret.Data["ttl"]) - } - if exp := int64(1800); ttl > exp { - t.Errorf("expected %d to be <= to %d", ttl, exp) - } - }) - - t.Run("accessor", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - token, accessor := testTokenAndAccessor(t, client) - - _, cmd := testTokenRenewCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-increment", "30m", - "-accessor", - accessor, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - secret, err := client.Auth().Token().Lookup(token) - if err != nil { - t.Fatal(err) - } - - str := string(secret.Data["ttl"].(json.Number)) - ttl, err := strconv.ParseInt(str, 10, 64) - if err != nil { - t.Fatalf("bad ttl: %#v", secret.Data["ttl"]) - } - if exp := int64(1800); ttl > exp { - t.Errorf("expected %d to be <= to %d", ttl, exp) - } - }) - - t.Run("self", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - token, _ := testTokenAndAccessor(t, client) - - // Get the old token and login as the new token. We need the old token - // to query after the lookup, but we need the new token on the client. - oldToken := client.Token() - client.SetToken(token) - - _, cmd := testTokenRenewCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-increment", "30m", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - client.SetToken(oldToken) - secret, err := client.Auth().Token().Lookup(token) - if err != nil { - t.Fatal(err) - } - - str := string(secret.Data["ttl"].(json.Number)) - ttl, err := strconv.ParseInt(str, 10, 64) - if err != nil { - t.Fatalf("bad ttl: %#v", secret.Data["ttl"]) - } - if exp := int64(1800); ttl > exp { - t.Errorf("expected %d to be <= to %d", ttl, exp) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testTokenRenewCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "foo/bar", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error renewing token: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testTokenRenewCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/token_revoke_test.go b/command/token_revoke_test.go deleted file mode 100644 index 70aa8a08c..000000000 --- a/command/token_revoke_test.go +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func testTokenRevokeCommand(tb testing.TB) (*cli.MockUi, *TokenRevokeCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &TokenRevokeCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestTokenRevokeCommand_Run(t *testing.T) { - t.Parallel() - - validations := []struct { - name string - args []string - out string - code int - }{ - { - "bad_mode", - []string{"-mode=banana"}, - "Invalid mode", - 1, - }, - { - "empty", - nil, - "Not enough arguments", - 1, - }, - { - "args_with_self", - []string{"-self", "abcd1234"}, - "Too many arguments", - 1, - }, - { - "too_many_args", - []string{"abcd1234", "efgh5678"}, - "Too many arguments", - 1, - }, - { - "self_and_accessor", - []string{"-self", "-accessor"}, - "Cannot use -self with -accessor", - 1, - }, - { - "self_and_mode", - []string{"-self", "-mode=orphan"}, - "Cannot use -self with -mode", - 1, - }, - { - "accessor_and_mode_orphan", - []string{"-accessor", "-mode=orphan", "abcd1234"}, - "Cannot use -accessor with -mode=orphan", - 1, - }, - { - "accessor_and_mode_path", - []string{"-accessor", "-mode=path", "abcd1234"}, - "Cannot use -accessor with -mode=path", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range validations { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testTokenRevokeCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("token", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - token, _ := testTokenAndAccessor(t, client) - - ui, cmd := testTokenRevokeCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - token, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Revoked token" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - secret, err := client.Auth().Token().Lookup(token) - if secret != nil || err == nil { - t.Errorf("expected token to be revoked: %#v", secret) - } - }) - - t.Run("self", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testTokenRevokeCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-self", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Revoked token" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - secret, err := client.Auth().Token().LookupSelf() - if secret != nil || err == nil { - t.Errorf("expected token to be revoked: %#v", secret) - } - }) - - t.Run("accessor", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - token, accessor := testTokenAndAccessor(t, client) - - ui, cmd := testTokenRevokeCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-accessor", - accessor, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Success! Revoked token" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - secret, err := client.Auth().Token().Lookup(token) - if secret != nil || err == nil { - t.Errorf("expected token to be revoked: %#v", secret) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testTokenRevokeCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "abcd1234", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error revoking token: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testTokenRevokeCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/transit_import_key_test.go b/command/transit_import_key_test.go deleted file mode 100644 index 6bcf0237d..000000000 --- a/command/transit_import_key_test.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "testing" - - "github.com/hashicorp/vault/api" - - "github.com/stretchr/testify/require" -) - -// Validate the `vault transit import` command works. -func TestTransitImport(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("transit", &api.MountInput{ - Type: "transit", - }); err != nil { - t.Fatalf("transit mount error: %#v", err) - } - - rsa1, rsa2, aes128, aes256 := generateKeys(t) - - type testCase struct { - variant string - path string - key []byte - args []string - shouldFail bool - } - tests := []testCase{ - { - "import", - "transit/keys/rsa1", - rsa1, - []string{"type=rsa-2048"}, - false, /* first import */ - }, - { - "import", - "transit/keys/rsa1", - rsa2, - []string{"type=rsa-2048"}, - true, /* already exists */ - }, - { - "import-version", - "transit/keys/rsa1", - rsa2, - []string{"type=rsa-2048"}, - false, /* new version */ - }, - { - "import", - "transit/keys/rsa2", - rsa2, - []string{"type=rsa-4096"}, - true, /* wrong type */ - }, - { - "import", - "transit/keys/rsa2", - rsa2, - []string{"type=rsa-2048"}, - false, /* new name */ - }, - { - "import", - "transit/keys/aes1", - aes128, - []string{"type=aes128-gcm96"}, - false, /* first import */ - }, - { - "import", - "transit/keys/aes1", - aes256, - []string{"type=aes256-gcm96"}, - true, /* already exists */ - }, - { - "import-version", - "transit/keys/aes1", - aes256, - []string{"type=aes256-gcm96"}, - true, /* new version, different type */ - }, - { - "import-version", - "transit/keys/aes1", - aes128, - []string{"type=aes128-gcm96"}, - false, /* new version */ - }, - { - "import", - "transit/keys/aes2", - aes256, - []string{"type=aes128-gcm96"}, - true, /* wrong type */ - }, - { - "import", - "transit/keys/aes2", - aes256, - []string{"type=aes256-gcm96"}, - false, /* new name */ - }, - } - - for index, tc := range tests { - t.Logf("Running test case %d: %v", index, tc) - execTransitImport(t, client, tc.variant, tc.path, tc.key, tc.args, tc.shouldFail) - } -} - -func execTransitImport(t *testing.T, client *api.Client, method string, path string, key []byte, data []string, expectFailure bool) { - t.Helper() - - keyBase64 := base64.StdEncoding.EncodeToString(key) - - var args []string - args = append(args, "transit") - args = append(args, method) - args = append(args, path) - args = append(args, keyBase64) - args = append(args, data...) - - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - runOpts := &RunOptions{ - Stdout: stdout, - Stderr: stderr, - Client: client, - } - - code := RunCustom(args, runOpts) - combined := stdout.String() + stderr.String() - - if code != 0 { - if !expectFailure { - t.Fatalf("Got unexpected failure from test (ret %d): %v", code, combined) - } - } else { - if expectFailure { - t.Fatalf("Expected failure, got success from test (ret %d): %v", code, combined) - } - } -} - -func generateKeys(t *testing.T) (rsa1 []byte, rsa2 []byte, aes128 []byte, aes256 []byte) { - t.Helper() - - priv1, err := rsa.GenerateKey(rand.Reader, 2048) - require.NotNil(t, priv1, "failed generating RSA 1 key") - require.NoError(t, err, "failed generating RSA 1 key") - - rsa1, err = x509.MarshalPKCS8PrivateKey(priv1) - require.NotNil(t, rsa1, "failed marshaling RSA 1 key") - require.NoError(t, err, "failed marshaling RSA 1 key") - - priv2, err := rsa.GenerateKey(rand.Reader, 2048) - require.NotNil(t, priv2, "failed generating RSA 2 key") - require.NoError(t, err, "failed generating RSA 2 key") - - rsa2, err = x509.MarshalPKCS8PrivateKey(priv2) - require.NotNil(t, rsa2, "failed marshaling RSA 2 key") - require.NoError(t, err, "failed marshaling RSA 2 key") - - aes128 = make([]byte, 128/8) - _, err = rand.Read(aes128) - require.NoError(t, err, "failed generating AES 128 key") - - aes256 = make([]byte, 256/8) - _, err = rand.Read(aes256) - require.NoError(t, err, "failed generating AES 256 key") - - return -} diff --git a/command/unwrap_test.go b/command/unwrap_test.go deleted file mode 100644 index 41b03095a..000000000 --- a/command/unwrap_test.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testUnwrapCommand(tb testing.TB) (*cli.MockUi, *UnwrapCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &UnwrapCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func testUnwrapWrappedToken(tb testing.TB, client *api.Client, data map[string]interface{}) string { - tb.Helper() - - wrapped, err := client.Logical().Write("sys/wrapping/wrap", data) - if err != nil { - tb.Fatal(err) - } - if wrapped == nil || wrapped.WrapInfo == nil || wrapped.WrapInfo.Token == "" { - tb.Fatalf("missing wrap info: %v", wrapped) - } - return wrapped.WrapInfo.Token -} - -func TestUnwrapCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "too_many_args", - []string{"foo", "bar"}, - "Too many arguments", - 1, - }, - { - "default", - nil, // Token comes in the test func - "bar", - 0, - }, - { - "field", - []string{"-field", "foo"}, - "bar", - 0, - }, - { - "field_not_found", - []string{"-field", "not-a-real-field"}, - "not present in secret", - 1, - }, - } - - t.Run("validations", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - wrappedToken := testUnwrapWrappedToken(t, client, map[string]interface{}{ - "foo": "bar", - }) - - ui, cmd := testUnwrapCommand(t) - cmd.client = client - - tc.args = append(tc.args, wrappedToken) - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testUnwrapCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "foo", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error unwrapping: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - // This test needs its own client and server because it modifies the client - // to the wrapping token - t.Run("local_token", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - wrappedToken := testUnwrapWrappedToken(t, client, map[string]interface{}{ - "foo": "bar", - }) - - ui, cmd := testUnwrapCommand(t) - cmd.client = client - cmd.client.SetToken(wrappedToken) - - // Intentionally don't pass the token here - it should use the local token - code := cmd.Run([]string{}) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, "bar") { - t.Errorf("expected %q to contain %q", combined, "bar") - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testUnwrapCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/version_history_test.go b/command/version_history_test.go deleted file mode 100644 index b6467b8fc..000000000 --- a/command/version_history_test.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/hashicorp/vault/version" - "github.com/mitchellh/cli" -) - -func testVersionHistoryCommand(tb testing.TB) (*cli.MockUi, *VersionHistoryCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &VersionHistoryCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestVersionHistoryCommand_TableOutput(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testVersionHistoryCommand(t) - cmd.client = client - - code := cmd.Run([]string{}) - - if expectedCode := 0; code != expectedCode { - t.Fatalf("expected %d to be %d: %s", code, expectedCode, ui.ErrorWriter.String()) - } - - if errorString := ui.ErrorWriter.String(); !strings.Contains(errorString, versionTrackingWarning) { - t.Errorf("expected %q to contain %q", errorString, versionTrackingWarning) - } - - output := ui.OutputWriter.String() - - if !strings.Contains(output, version.Version) { - t.Errorf("expected %q to contain version %q", output, version.Version) - } -} - -func TestVersionHistoryCommand_JsonOutput(t *testing.T) { - client, closer := testVaultServer(t) - defer closer() - - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - runOpts := &RunOptions{ - Stdout: stdout, - Stderr: stderr, - Client: client, - } - - args, format, _, _, _ := setupEnv([]string{"version-history", "-format", "json"}) - if format != "json" { - t.Fatalf("expected format to be %q, actual %q", "json", format) - } - - code := RunCustom(args, runOpts) - - if expectedCode := 0; code != expectedCode { - t.Fatalf("expected %d to be %d: %s", code, expectedCode, stderr.String()) - } - - if stderrString := stderr.String(); !strings.Contains(stderrString, versionTrackingWarning) { - t.Errorf("expected %q to contain %q", stderrString, versionTrackingWarning) - } - - stdoutBytes := stdout.Bytes() - - if !json.Valid(stdoutBytes) { - t.Fatalf("expected output %q to be valid JSON", stdoutBytes) - } - - var versionHistoryResp map[string]interface{} - err := json.Unmarshal(stdoutBytes, &versionHistoryResp) - if err != nil { - t.Fatalf("failed to unmarshal json from STDOUT, err: %s", err.Error()) - } - - var respData map[string]interface{} - var ok bool - var keys []interface{} - var keyInfo map[string]interface{} - - if respData, ok = versionHistoryResp["data"].(map[string]interface{}); !ok { - t.Fatalf("expected data key to be map, actual: %#v", versionHistoryResp["data"]) - } - - if keys, ok = respData["keys"].([]interface{}); !ok { - t.Fatalf("expected keys to be array, actual: %#v", respData["keys"]) - } - - if keyInfo, ok = respData["key_info"].(map[string]interface{}); !ok { - t.Fatalf("expected key_info to be map, actual: %#v", respData["key_info"]) - } - - if len(keys) != 1 { - t.Fatalf("expected single version history entry for %q", version.Version) - } - - if keyInfo[version.Version] == nil { - t.Fatalf("expected version %s to be present in key_info, actual: %#v", version.Version, keyInfo) - } -} diff --git a/command/version_test.go b/command/version_test.go deleted file mode 100644 index be40377c1..000000000 --- a/command/version_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/version" - "github.com/mitchellh/cli" -) - -func testVersionCommand(tb testing.TB) (*cli.MockUi, *VersionCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &VersionCommand{ - VersionInfo: &version.VersionInfo{}, - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestVersionCommand_Run(t *testing.T) { - t.Parallel() - - t.Run("output", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testVersionCommand(t) - cmd.client = client - - code := cmd.Run(nil) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Vault" - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to equal %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testVersionCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/command/write_test.go b/command/write_test.go deleted file mode 100644 index c7c6f68da..000000000 --- a/command/write_test.go +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package command - -import ( - "io" - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" -) - -func testWriteCommand(tb testing.TB) (*cli.MockUi, *WriteCommand) { - tb.Helper() - - ui := cli.NewMockUi() - return ui, &WriteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - } -} - -func TestWriteCommand_Run(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - args []string - out string - code int - }{ - { - "not_enough_args", - []string{}, - "Not enough arguments", - 1, - }, - { - "empty_kvs", - []string{"secret/write/foo"}, - "Must supply data or use -force", - 1, - }, - { - "force_kvs", - []string{"-force", "auth/token/create"}, - "token", - 0, - }, - { - "force_f_kvs", - []string{"-f", "auth/token/create"}, - "token", - 0, - }, - { - "kvs_no_value", - []string{"secret/write/foo", "foo"}, - "Failed to parse K=V data", - 1, - }, - { - "single_value", - []string{"secret/write/foo", "foo=bar"}, - "Success!", - 0, - }, - { - "multi_value", - []string{"secret/write/foo", "foo=bar", "zip=zap"}, - "Success!", - 0, - }, - { - "field", - []string{ - "-field", "token_renewable", - "auth/token/create", "display_name=foo", - }, - "false", - 0, - }, - { - "field_not_found", - []string{ - "-field", "not-a-real-field", - "auth/token/create", "display_name=foo", - }, - "not present in secret", - 1, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testWriteCommand(t) - cmd.client = client - - code := cmd.Run(tc.args) - if code != tc.code { - t.Errorf("expected %d to be %d", code, tc.code) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, tc.out) { - t.Errorf("expected %q to contain %q", combined, tc.out) - } - }) - } - - t.Run("force", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().Mount("transit/", &api.MountInput{ - Type: "transit", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testWriteCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-force", - "transit/keys/my-key", - }) - if exp := 0; code != exp { - t.Fatalf("expected %d to be %d: %q", code, exp, ui.ErrorWriter.String()) - } - - secret, err := client.Logical().Read("transit/keys/my-key") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - }) - - t.Run("stdin_full", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte(`{"foo":"bar"}`)) - stdinW.Close() - }() - - _, cmd := testWriteCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "secret/write/stdin_full", "-", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - - secret, err := client.Logical().Read("secret/write/stdin_full") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - if exp, act := "bar", secret.Data["foo"].(string); exp != act { - t.Errorf("expected %q to be %q", act, exp) - } - }) - - t.Run("stdin_value", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - stdinR, stdinW := io.Pipe() - go func() { - stdinW.Write([]byte("bar")) - stdinW.Close() - }() - - _, cmd := testWriteCommand(t) - cmd.client = client - cmd.testStdin = stdinR - - code := cmd.Run([]string{ - "secret/write/stdin_value", "foo=-", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - - secret, err := client.Logical().Read("secret/write/stdin_value") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - if exp, act := "bar", secret.Data["foo"].(string); exp != act { - t.Errorf("expected %q to be %q", act, exp) - } - }) - - t.Run("integration", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - _, cmd := testWriteCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "secret/write/integration", "foo=bar", "zip=zap", - }) - if code != 0 { - t.Fatalf("expected 0 to be %d", code) - } - - secret, err := client.Logical().Read("secret/write/integration") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("expected secret to have data") - } - if exp, act := "bar", secret.Data["foo"].(string); exp != act { - t.Errorf("expected %q to be %q", act, exp) - } - if exp, act := "zap", secret.Data["zip"].(string); exp != act { - t.Errorf("expected %q to be %q", act, exp) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testWriteCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "foo/bar", "a=b", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error writing data to foo/bar: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - }) - - t.Run("no_tabs", func(t *testing.T) { - t.Parallel() - - _, cmd := testWriteCommand(t) - assertNoTabs(t, cmd) - }) -} diff --git a/go.mod b/go.mod index 641bca766..d678b4c1c 100644 --- a/go.mod +++ b/go.mod @@ -30,24 +30,18 @@ require ( github.com/armon/go-radix v1.0.0 github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef github.com/axiomhq/hyperloglog v0.0.0-20220105174342-98591331716a - github.com/cenkalti/backoff/v3 v3.2.2 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.16.0 github.com/fatih/structs v1.1.0 github.com/gammazero/workerpool v1.1.3 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 - github.com/go-errors/errors v1.4.2 github.com/go-jose/go-jose/v3 v3.0.3 - github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-sql-driver/mysql v1.6.0 - github.com/go-test/deep v1.1.0 github.com/gocql/gocql v1.0.0 github.com/golang/protobuf v1.5.3 - github.com/google/go-cmp v0.6.0 github.com/google/tink/go v1.7.0 github.com/hashicorp-forge/bbolt v1.3.8-hc3 - github.com/hashicorp/cap v0.3.0 github.com/hashicorp/consul-template v0.33.0 github.com/hashicorp/consul/api v1.23.0 github.com/hashicorp/errwrap v1.1.0 @@ -66,7 +60,6 @@ require ( github.com/hashicorp/go-plugin v1.5.2 github.com/hashicorp/go-raftchunking v0.6.3-0.20191002164813-7e9e8525653a github.com/hashicorp/go-retryablehttp v0.7.2 - github.com/hashicorp/go-rootcerts v1.0.2 github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 github.com/hashicorp/go-secure-stdlib/gatedwriter v0.1.1 github.com/hashicorp/go-secure-stdlib/kv-builder v0.1.2 @@ -96,16 +89,12 @@ require ( github.com/hashicorp/vault-plugin-auth-jwt v0.16.1 github.com/hashicorp/vault-plugin-auth-kerberos v0.10.0 github.com/hashicorp/vault-plugin-auth-kubernetes v0.16.0 - github.com/hashicorp/vault-plugin-mock v0.16.1 github.com/hashicorp/vault-plugin-secrets-ad v0.16.0 github.com/hashicorp/vault-plugin-secrets-kubernetes v0.5.0 github.com/hashicorp/vault-plugin-secrets-kv v0.15.0 github.com/hashicorp/vault-plugin-secrets-openldap v0.11.1 github.com/hashicorp/vault-plugin-secrets-terraform v0.7.1 - github.com/hashicorp/vault-testing-stepwise v0.1.3 github.com/hashicorp/vault/api v1.9.2 - github.com/hashicorp/vault/api/auth/approle v0.1.0 - github.com/hashicorp/vault/api/auth/userpass v0.1.0 github.com/hashicorp/vault/sdk v0.10.2 github.com/hashicorp/vault/vault/hcp_link/proto v0.0.0-20230201201504-b741fa893d77 github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab @@ -124,13 +113,11 @@ require ( github.com/mitchellh/cli v1.1.5 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/go-testing-interface v1.14.1 github.com/mitchellh/go-wordwrap v1.0.0 github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/reflectwalk v1.0.2 github.com/natefinch/atomic v0.0.0-20150920032501-a62ce929ffcc github.com/oklog/run v1.1.0 - github.com/ory/dockertest v3.3.5+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pires/go-proxyproto v0.6.1 github.com/pkg/errors v0.9.1 @@ -144,13 +131,11 @@ require ( github.com/sasha-s/go-deadlock v0.2.0 github.com/sethvargo/go-limiter v0.7.1 github.com/shirou/gopsutil/v3 v3.22.6 - github.com/stretchr/testify v1.8.4 go.etcd.io/bbolt v1.3.7 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 go.opentelemetry.io/otel/trace v1.22.0 go.uber.org/atomic v1.11.0 - go.uber.org/goleak v1.2.1 golang.org/x/crypto v0.23.0 golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 golang.org/x/net v0.25.0 @@ -170,7 +155,6 @@ require ( cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/Azure/azure-sdk-for-go v67.2.0+incompatible // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // 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.22 // indirect @@ -188,8 +172,6 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect @@ -197,32 +179,27 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/boombuler/barcode v1.0.1 // indirect - github.com/cenkalti/backoff v2.2.1+incompatible // indirect + github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect github.com/circonus-labs/circonusllhist v0.1.3 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/containerd v1.7.12 // indirect - github.com/containerd/continuity v0.4.2 // indirect - github.com/containerd/log v0.1.0 // indirect github.com/coreos/go-oidc/v3 v3.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba // indirect github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect github.com/digitalocean/godo v1.7.5 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/distribution/reference v0.6.0 // indirect github.com/dnaeon/go-vcr v1.2.0 // indirect github.com/docker/cli v25.0.1+incompatible // indirect - github.com/docker/docker v25.0.5+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/emicklei/go-restful/v3 v3.10.1 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gammazero/deque v0.2.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -244,6 +221,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.7 // indirect @@ -253,9 +231,11 @@ require ( github.com/gophercloud/gophercloud v0.1.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/hashicorp/cap v0.3.0 // indirect github.com/hashicorp/cronexpr v1.1.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-msgpack/v2 v2.0.0 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/awsutil v0.2.3 // indirect github.com/hashicorp/go-secure-stdlib/fileutil v0.1.0 // indirect github.com/hashicorp/go-slug v0.11.1 // indirect @@ -295,18 +275,14 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/miekg/dns v1.1.43 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/hashstructure v1.1.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect - github.com/moby/patternmatcher v0.5.0 // indirect - github.com/moby/sys/sequential v0.5.0 // indirect - github.com/moby/sys/user v0.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2 // indirect github.com/nwaples/rardecode v1.1.2 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect github.com/opencontainers/runc v1.2.0-rc.1 // indirect github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect @@ -325,6 +301,7 @@ require ( github.com/spf13/cast v1.5.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.8.4 // indirect github.com/tencentcloud/tencentcloud-sdk-go v1.0.162 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/numcpus v0.4.0 // indirect diff --git a/go.sum b/go.sum index 547772abd..1ef457515 100644 --- a/go.sum +++ b/go.sum @@ -802,7 +802,6 @@ gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zum git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/AdaLogics/go-fuzz-headers v0.0.0-20221206110420-d395f97c4830/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -911,7 +910,6 @@ github.com/Microsoft/hcsshim v0.9.3/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfy github.com/Microsoft/hcsshim v0.9.4/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/Microsoft/hcsshim v0.9.6/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/Microsoft/hcsshim v0.9.10/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= -github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= @@ -1023,8 +1021,6 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytecodealliance/wasmtime-go v0.36.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= @@ -1810,8 +1806,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI= -github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -1819,11 +1813,9 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= @@ -2006,8 +1998,6 @@ github.com/hashicorp/vault-plugin-auth-kerberos v0.10.0 h1:YH2x9kIV0jKXk22tVkpyd github.com/hashicorp/vault-plugin-auth-kerberos v0.10.0/go.mod h1:I6ulXug4oxx77DFYjqI1kVl+72TgXEo3Oju4tTOVfU4= github.com/hashicorp/vault-plugin-auth-kubernetes v0.16.0 h1:vuXNJvtMyoqQ01Sfwf2TNcJNkGcxP1vD3C7gpvuVkCU= github.com/hashicorp/vault-plugin-auth-kubernetes v0.16.0/go.mod h1:onx9W/rDwENQkN+1yEnJvS51PVkkGAPOBXasne7lnnk= -github.com/hashicorp/vault-plugin-mock v0.16.1 h1:5QQvSUHxDjEEbrd2REOeacqyJnCLPD51IQzy71hx8P0= -github.com/hashicorp/vault-plugin-mock v0.16.1/go.mod h1:83G4JKlOwUtxVourn5euQfze3ZWyXcUiLj2wqrKSDIM= github.com/hashicorp/vault-plugin-secrets-ad v0.16.0 h1:6RCpd2PbBvmi5xmxXhggE0Xv+/Gag896/NNZeMKH+8A= github.com/hashicorp/vault-plugin-secrets-ad v0.16.0/go.mod h1:6IeXly3xi+dVodzFSx6aVZjdhd3syboPyhxr1/WMcyo= github.com/hashicorp/vault-plugin-secrets-kubernetes v0.5.0 h1:g0W1ybHjO945jDtuDEFcqTINyW/s06wxZarE/7aLumc= @@ -2104,6 +2094,7 @@ github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da h1:FjHUJJ7oBW4G/9j1KzlHaXL09LyMVM9rupS39lncbXk= github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= @@ -2367,7 +2358,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= @@ -2486,8 +2476,6 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh github.com/opencontainers/selinux v1.10.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= -github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c h1:vwpFWvAO8DeIZfFeqASzZfsxuWPno9ncAebBEP0N3uE= @@ -2889,7 +2877,6 @@ go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9 go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= -go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0/go.mod h1:M1hVZHNxcbkAlcvrOMlpQ4YOO3Awf+4N2dxkZL3xm04= @@ -2898,7 +2885,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1/go.mod h1:Kv8liBeVNFkkk go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0/go.mod h1:ceUgdyfNv4h4gLxHR0WNfDiiVmZFodZhZSbOLhpxqXE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0/go.mod h1:Krqnjl22jUJ0HgMzw5eveuCvFDXY4nSYb4F8t5gdrag= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1/go.mod h1:xOvWoTOrQjxjW61xtOmD/WKGRYb/P4NzRo3bs65U6Rk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= @@ -2906,7 +2892,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0/go.mod h1 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0/go.mod h1:OfUCyyIiDvNXHWpcWgbF+MWvqPZiNa3YDEnivcnYsV0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= go.opentelemetry.io/otel/metric v0.30.0/go.mod h1:/ShZ7+TS4dHzDFmfi1kSXMhMVubNoP0oIaBp70J6UXU= @@ -2940,7 +2925,6 @@ go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.16.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -3971,13 +3955,11 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/helper/benchhelpers/benchhelpers.go b/helper/benchhelpers/benchhelpers.go deleted file mode 100644 index afb0d4a88..000000000 --- a/helper/benchhelpers/benchhelpers.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package benchhelpers - -import ( - "testing" - - testinginterface "github.com/mitchellh/go-testing-interface" -) - -type tbWrapper struct { - testing.TB -} - -func (b tbWrapper) Parallel() { - // no-op -} - -func TBtoT(tb testing.TB) testinginterface.T { - return tbWrapper{tb} -} diff --git a/helper/builtinplugins/registry_test.go b/helper/builtinplugins/registry_test.go deleted file mode 100644 index 77012886e..000000000 --- a/helper/builtinplugins/registry_test.go +++ /dev/null @@ -1,334 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package builtinplugins - -import ( - "bufio" - "fmt" - "os" - "reflect" - "regexp" - "testing" - - credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" - dbMysql "github.com/hashicorp/vault/plugins/database/mysql" - "github.com/hashicorp/vault/sdk/helper/consts" - - "golang.org/x/exp/slices" -) - -// Test_RegistryGet exercises the (registry).Get functionality by comparing -// factory types and ok response. -func Test_RegistryGet(t *testing.T) { - tests := []struct { - name string - builtin string - pluginType consts.PluginType - want BuiltinFactory - wantOk bool - }{ - { - name: "non-existent builtin", - builtin: "foo", - pluginType: consts.PluginTypeCredential, - want: nil, - wantOk: false, - }, - { - name: "bad plugin type", - builtin: "app-id", - pluginType: 9000, - want: nil, - wantOk: false, - }, - { - name: "known builtin lookup", - builtin: "userpass", - pluginType: consts.PluginTypeCredential, - want: toFunc(credUserpass.Factory), - wantOk: true, - }, - { - name: "removed builtin lookup", - builtin: "app-id", - pluginType: consts.PluginTypeCredential, - want: nil, - wantOk: true, - }, - { - name: "known builtin lookup", - builtin: "mysql-database-plugin", - pluginType: consts.PluginTypeDatabase, - want: dbMysql.New(dbMysql.DefaultUserNameTemplate), - wantOk: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var got BuiltinFactory - got, ok := Registry.Get(tt.builtin, tt.pluginType) - if ok { - if reflect.TypeOf(got) != reflect.TypeOf(tt.want) { - t.Fatalf("got type: %T, want type: %T", got, tt.want) - } - } - if tt.wantOk != ok { - t.Fatalf("error: got %v, want %v", ok, tt.wantOk) - } - }) - } -} - -// Test_RegistryKeyCounts is a light unit test used to check the builtin -// registry lists for each plugin type and make sure they match in length. -func Test_RegistryKeyCounts(t *testing.T) { - tests := []struct { - name string - pluginType consts.PluginType - want int // use slice length as test condition - wantOk bool - }{ - { - name: "bad plugin type", - pluginType: 9001, - want: 0, - }, - { - name: "number of auth plugins", - pluginType: consts.PluginTypeCredential, - want: 19, - }, - { - name: "number of database plugins", - pluginType: consts.PluginTypeDatabase, - want: 17, - }, - { - name: "number of secrets plugins", - pluginType: consts.PluginTypeSecrets, - want: 19, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - keys := Registry.Keys(tt.pluginType) - if len(keys) != tt.want { - t.Fatalf("got size: %d, want size: %d", len(keys), tt.want) - } - }) - } -} - -// Test_RegistryContains exercises the (registry).Contains functionality. -func Test_RegistryContains(t *testing.T) { - tests := []struct { - name string - builtin string - pluginType consts.PluginType - want bool - }{ - { - name: "non-existent builtin", - builtin: "foo", - pluginType: consts.PluginTypeCredential, - want: false, - }, - { - name: "bad plugin type", - builtin: "app-id", - pluginType: 9001, - want: false, - }, - { - name: "known builtin lookup", - builtin: "approle", - pluginType: consts.PluginTypeCredential, - want: true, - }, - { - name: "removed builtin lookup", - builtin: "app-id", - pluginType: consts.PluginTypeCredential, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := Registry.Contains(tt.builtin, tt.pluginType) - if got != tt.want { - t.Fatalf("error: got %v, wanted %v", got, tt.want) - } - }) - } -} - -// Test_RegistryStatus exercises the (registry).Status functionality. -func Test_RegistryStatus(t *testing.T) { - tests := []struct { - name string - builtin string - pluginType consts.PluginType - want consts.DeprecationStatus - wantOk bool - }{ - { - name: "non-existent builtin and valid type", - builtin: "foo", - pluginType: consts.PluginTypeCredential, - want: consts.Unknown, - wantOk: false, - }, - { - name: "mismatch builtin and plugin type", - builtin: "app-id", - pluginType: consts.PluginTypeSecrets, - want: consts.Unknown, - wantOk: false, - }, - { - name: "existing builtin and invalid plugin type", - builtin: "app-id", - pluginType: 9000, - want: consts.Unknown, - wantOk: false, - }, - { - name: "supported builtin lookup", - builtin: "approle", - pluginType: consts.PluginTypeCredential, - want: consts.Supported, - wantOk: true, - }, - { - name: "deprecated builtin lookup", - builtin: "pcf", - pluginType: consts.PluginTypeCredential, - want: consts.Deprecated, - wantOk: true, - }, - { - name: "removed builtin lookup", - builtin: "app-id", - pluginType: consts.PluginTypeCredential, - want: consts.Removed, - wantOk: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, ok := Registry.DeprecationStatus(tt.builtin, tt.pluginType) - if got != tt.want { - t.Fatalf("got %+v, wanted %+v", got, tt.want) - } - if ok != tt.wantOk { - t.Fatalf("got ok: %t, want ok: %t", ok, tt.wantOk) - } - }) - } -} - -// Test_RegistryMatchesGenOpenapi ensures that the plugins mounted in gen_openapi.sh match registry.go -func Test_RegistryMatchesGenOpenapi(t *testing.T) { - const scriptPath = "../../scripts/gen_openapi.sh" - - // parseScript fetches the contents of gen_openapi.sh script & extract the relevant lines - parseScript := func(path string) ([]string, []string, error) { - f, err := os.Open(scriptPath) - if err != nil { - return nil, nil, fmt.Errorf("could not open gen_openapi.sh script: %w", err) - } - defer f.Close() - - var ( - credentialBackends []string - credentialBackendsRe = regexp.MustCompile(`^vault auth enable (?:-.+ )*(?:"([a-zA-Z]+)"|([a-zA-Z]+))$`) - - secretsBackends []string - secretsBackendsRe = regexp.MustCompile(`^vault secrets enable (?:-.+ )*(?:"([a-zA-Z]+)"|([a-zA-Z]+))$`) - ) - - scanner := bufio.NewScanner(f) - - for scanner.Scan() { - line := scanner.Text() - - if m := credentialBackendsRe.FindStringSubmatch(line); m != nil { - credentialBackends = append(credentialBackends, m[1]) - } - if m := secretsBackendsRe.FindStringSubmatch(line); m != nil { - secretsBackends = append(secretsBackends, m[1]) - } - } - - if err := scanner.Err(); err != nil { - return nil, nil, fmt.Errorf("error scanning gen_openapi.sh: %v", err) - } - - return credentialBackends, secretsBackends, nil - } - - // ensureInRegistry ensures that the given plugin is in registry and marked as "supported" - ensureInRegistry := func(t *testing.T, name string, pluginType consts.PluginType) { - t.Helper() - - // "database" will not be present in registry, it is represented as - // a list of database plugins instead - if name == "database" && pluginType == consts.PluginTypeSecrets { - return - } - - deprecationStatus, ok := Registry.DeprecationStatus(name, pluginType) - if !ok { - t.Fatalf("%q %s backend is missing from registry.go; please remove it from gen_openapi.sh", name, pluginType) - } - - if deprecationStatus == consts.Removed { - t.Fatalf("%q %s backend is marked 'removed' in registry.go; please remove it from gen_openapi.sh", name, pluginType) - } - } - - // ensureInScript ensures that the given plugin name in in gen_openapi.sh script - ensureInScript := func(t *testing.T, scriptBackends []string, name string) { - t.Helper() - - for _, excluded := range []string{ - "oidc", // alias for "jwt" - "openldap", // alias for "ldap" - } { - if name == excluded { - return - } - } - - if !slices.Contains(scriptBackends, name) { - t.Fatalf("%q backend could not be found in gen_openapi.sh, please add it there", name) - } - } - - // test starts here - scriptCredentialBackends, scriptSecretsBackends, err := parseScript(scriptPath) - if err != nil { - t.Fatal(err) - } - - for _, name := range scriptCredentialBackends { - ensureInRegistry(t, name, consts.PluginTypeCredential) - } - - for _, name := range scriptSecretsBackends { - ensureInRegistry(t, name, consts.PluginTypeSecrets) - } - - for name, backend := range Registry.credentialBackends { - if backend.DeprecationStatus == consts.Supported { - ensureInScript(t, scriptCredentialBackends, name) - } - } - - for name, backend := range Registry.logicalBackends { - if backend.DeprecationStatus == consts.Supported { - ensureInScript(t, scriptSecretsBackends, name) - } - } -} diff --git a/helper/dhutil/dhutil_test.go b/helper/dhutil/dhutil_test.go deleted file mode 100644 index 18cd2c064..000000000 --- a/helper/dhutil/dhutil_test.go +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package dhutil diff --git a/helper/fairshare/fairshare_testing_util.go b/helper/fairshare/fairshare_testing_util.go deleted file mode 100644 index 5aae025f5..000000000 --- a/helper/fairshare/fairshare_testing_util.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package fairshare - -import ( - "fmt" - "testing" - - log "github.com/hashicorp/go-hclog" - uuid "github.com/hashicorp/go-uuid" -) - -type testJob struct { - id string - ex func(id string) error - onFail func(error) -} - -func (t *testJob) Execute() error { - return t.ex(t.id) -} - -func (t *testJob) OnFailure(err error) { - t.onFail(err) -} - -func newTestJob(t *testing.T, id string, ex func(string) error, onFail func(error)) testJob { - t.Helper() - if ex == nil { - t.Errorf("ex cannot be nil") - } - if onFail == nil { - t.Errorf("onFail cannot be nil") - } - - return testJob{ - id: id, - ex: ex, - onFail: onFail, - } -} - -func newDefaultTestJob(t *testing.T, id string) testJob { - ex := func(_ string) error { return nil } - onFail := func(_ error) {} - return newTestJob(t, id, ex, onFail) -} - -func newTestLogger(name string) log.Logger { - guid, err := uuid.GenerateUUID() - if err != nil { - guid = "no-guid" - } - return log.New(&log.LoggerOptions{ - Name: fmt.Sprintf("%s-%s", name, guid), - Level: log.LevelFromString("TRACE"), - }) -} - -func GetNumWorkers(j *JobManager) int { - return j.workerPool.numWorkers -} diff --git a/helper/fairshare/jobmanager_test.go b/helper/fairshare/jobmanager_test.go deleted file mode 100644 index 3d903d6ae..000000000 --- a/helper/fairshare/jobmanager_test.go +++ /dev/null @@ -1,749 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package fairshare - -import ( - "fmt" - "reflect" - "sync" - "testing" - "time" -) - -func TestJobManager_NewJobManager(t *testing.T) { - testCases := []struct { - name string - numWorkers int - expectedNumWorkers int - }{ - { - name: "", - numWorkers: 0, - expectedNumWorkers: 1, - }, - { - name: "", - numWorkers: 5, - expectedNumWorkers: 5, - }, - { - name: "", - numWorkers: 5, - expectedNumWorkers: 5, - }, - { - name: "", - numWorkers: 5, - expectedNumWorkers: 5, - }, - { - name: "", - numWorkers: 5, - expectedNumWorkers: 5, - }, - } - - l := newTestLogger("jobmanager-test") - for tcNum, tc := range testCases { - j := NewJobManager(tc.name, tc.numWorkers, l, nil) - - if tc.name != "" && tc.name != j.name { - t.Errorf("tc %d: expected name %s, got %s", tcNum, tc.name, j.name) - } - if j.queues == nil { - t.Errorf("tc %d: queues not set up properly", tcNum) - } - if j.queuesIndex == nil { - t.Errorf("tc %d: queues index not set up properly", tcNum) - } - if j.quit == nil { - t.Errorf("tc %d: quit channel not set up properly", tcNum) - } - if j.workerPool.numWorkers != tc.expectedNumWorkers { - t.Errorf("tc %d: expected %d workers, got %d", tcNum, tc.expectedNumWorkers, j.workerPool.numWorkers) - } - if j.logger == nil { - t.Errorf("tc %d: logger not set up properly", tcNum) - } - } -} - -func TestJobManager_Start(t *testing.T) { - numJobs := 10 - j := NewJobManager("job-mgr-test", 3, newTestLogger("jobmanager-test"), nil) - - var wg sync.WaitGroup - wg.Add(numJobs) - j.Start() - defer j.Stop() - - doneCh := make(chan struct{}) - timeout := time.After(5 * time.Second) - go func() { - wg.Wait() - doneCh <- struct{}{} - }() - - ex := func(_ string) error { - wg.Done() - return nil - } - onFail := func(_ error) {} - - for i := 0; i < numJobs; i++ { - // distribute jobs between 3 queues in the job manager - job := newTestJob(t, fmt.Sprintf("test-job-%d", i), ex, onFail) - j.AddJob(&job, fmt.Sprintf("queue-%d", i%3)) - } - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } -} - -func TestJobManager_StartAndPause(t *testing.T) { - numJobs := 10 - j := NewJobManager("job-mgr-test", 3, newTestLogger("jobmanager-test"), nil) - - var wg sync.WaitGroup - wg.Add(numJobs) - j.Start() - defer j.Stop() - - doneCh := make(chan struct{}) - timeout := time.After(5 * time.Second) - go func() { - wg.Wait() - doneCh <- struct{}{} - }() - - ex := func(_ string) error { - wg.Done() - return nil - } - onFail := func(_ error) {} - - for i := 0; i < numJobs; i++ { - // distribute jobs between 3 queues in the job manager - job := newTestJob(t, fmt.Sprintf("test-job-%d", i), ex, onFail) - j.AddJob(&job, fmt.Sprintf("queue-%d", i%3)) - } - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } - - // now that the work queue is empty, let's add more jobs and make sure - // we pick up where we left off - - for i := 0; i < 5; i++ { - numAdditionalJobs := 5 - wg.Add(numAdditionalJobs) - - timeout = time.After(5 * time.Second) - go func() { - wg.Wait() - doneCh <- struct{}{} - }() - - for i := numJobs; i < numJobs+numAdditionalJobs; i++ { - // distribute jobs between 3 queues in the job manager - job := newTestJob(t, fmt.Sprintf("test-job-%d", i), ex, onFail) - j.AddJob(&job, fmt.Sprintf("queue-%d", i%3)) - } - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } - - numJobs += numAdditionalJobs - } -} - -func TestJobManager_Stop(t *testing.T) { - j := NewJobManager("job-mgr-test", 5, newTestLogger("jobmanager-test"), nil) - - j.Start() - - doneCh := make(chan struct{}) - timeout := time.After(5 * time.Second) - go func() { - j.Stop() - j.wg.Wait() - doneCh <- struct{}{} - }() - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } -} - -func TestFairshare_StopMultiple(t *testing.T) { - j := NewJobManager("job-mgr-test", 5, newTestLogger("jobmanager-test"), nil) - - j.Start() - - doneCh := make(chan struct{}) - timeout := time.After(5 * time.Second) - go func() { - j.Stop() - j.wg.Wait() - doneCh <- struct{}{} - }() - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } - - // essentially, we don't want to panic here - var r interface{} - go func() { - t.Helper() - - defer func() { - r = recover() - doneCh <- struct{}{} - }() - - j.Stop() - j.wg.Wait() - }() - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } - - if r != nil { - t.Fatalf("panic during second stop: %v", r) - } -} - -func TestJobManager_AddJob(t *testing.T) { - testCases := []struct { - name string - queueID string - }{ - { - name: "test1", - queueID: "q1", - }, - { - name: "test2", - queueID: "q1", - }, - { - name: "test3", - queueID: "q1", - }, - { - name: "test4", - queueID: "q2", - }, - { - name: "test5", - queueID: "q3", - }, - } - - j := NewJobManager("job-mgr-test", 3, newTestLogger("jobmanager-test"), nil) - - expectedCount := make(map[string]int) - for _, tc := range testCases { - if _, ok := expectedCount[tc.queueID]; !ok { - expectedCount[tc.queueID] = 1 - } else { - expectedCount[tc.queueID]++ - } - - job := newDefaultTestJob(t, tc.name) - j.AddJob(&job, tc.queueID) - } - - if len(expectedCount) != len(j.queues) { - t.Fatalf("expected %d queues, got %d", len(expectedCount), len(j.queues)) - } - - for k, v := range j.queues { - if v.Len() != expectedCount[k] { - t.Fatalf("queue %s has bad count. expected %d, got %d", k, expectedCount[k], v.Len()) - } - } -} - -func TestJobManager_GetPendingJobCount(t *testing.T) { - numJobs := 15 - j := NewJobManager("test-job-mgr", 3, newTestLogger("jobmanager-test"), nil) - - for i := 0; i < numJobs; i++ { - job := newDefaultTestJob(t, fmt.Sprintf("job-%d", i)) - j.AddJob(&job, fmt.Sprintf("queue-%d", i%4)) - } - - pendingJobs := j.GetPendingJobCount() - if pendingJobs != numJobs { - t.Errorf("expected %d jobs, got %d", numJobs, pendingJobs) - } -} - -func TestJobManager_GetWorkQueueLengths(t *testing.T) { - j := NewJobManager("test-job-mgr", 3, newTestLogger("jobmanager-test"), nil) - - expected := make(map[string]int) - for i := 0; i < 25; i++ { - queueID := fmt.Sprintf("queue-%d", i%4) - job := newDefaultTestJob(t, fmt.Sprintf("job-%d", i)) - - j.AddJob(&job, queueID) - - if _, ok := expected[queueID]; !ok { - expected[queueID] = 0 - } - - expected[queueID]++ - } - - pendingJobs := j.GetWorkQueueLengths() - if !reflect.DeepEqual(pendingJobs, expected) { - t.Errorf("expected %v job count, got %v", expected, pendingJobs) - } -} - -func TestJobManager_removeLastQueueAccessed(t *testing.T) { - j := NewJobManager("job-mgr-test", 1, newTestLogger("jobmanager-test"), nil) - - testCases := []struct { - lastQueueAccessed int - updatedLastQueueAccessed int - len int - expectedQueues []string - }{ - { - // remove with bad index (too low) - lastQueueAccessed: -1, - updatedLastQueueAccessed: -1, - len: 3, - expectedQueues: []string{"queue-0", "queue-1", "queue-2"}, - }, - { - // remove with bad index (too high) - lastQueueAccessed: 3, - updatedLastQueueAccessed: 3, - len: 3, - expectedQueues: []string{"queue-0", "queue-1", "queue-2"}, - }, - { - // remove queue-1 (index 1) - lastQueueAccessed: 1, - updatedLastQueueAccessed: 0, - len: 2, - expectedQueues: []string{"queue-0", "queue-2"}, - }, - { - // remove queue-0 (index 0) - lastQueueAccessed: 0, - updatedLastQueueAccessed: 0, - len: 1, - expectedQueues: []string{"queue-2"}, - }, - { - // remove queue-1 (index 1) - lastQueueAccessed: 0, - updatedLastQueueAccessed: -1, - len: 0, - expectedQueues: []string{}, - }, - } - - j.l.Lock() - defer j.l.Unlock() - - j.addQueue("queue-0") - j.addQueue("queue-1") - j.addQueue("queue-2") - - for _, tc := range testCases { - j.lastQueueAccessed = tc.lastQueueAccessed - j.removeLastQueueAccessed() - - if j.lastQueueAccessed != tc.updatedLastQueueAccessed { - t.Errorf("last queue access update failed. expected %d, got %d", tc.updatedLastQueueAccessed, j.lastQueueAccessed) - } - if len(j.queuesIndex) != tc.len { - t.Fatalf("queue index update failed. expected %d elements, found %v", tc.len, j.queues) - } - if len(j.queues) != len(tc.expectedQueues) { - t.Fatalf("bad amount of queues. expected %d, found %v", len(tc.expectedQueues), j.queues) - } - - for _, q := range tc.expectedQueues { - if _, ok := j.queues[q]; !ok { - t.Errorf("bad queue. expected %s in %v", q, j.queues) - } - } - } -} - -func TestJobManager_EndToEnd(t *testing.T) { - testCases := []struct { - name string - queueID string - }{ - { - name: "job-1", - queueID: "queue-1", - }, - { - name: "job-2", - queueID: "queue-2", - }, - { - name: "job-3", - queueID: "queue-1", - }, - { - name: "job-4", - queueID: "queue-3", - }, - { - name: "job-5", - queueID: "queue-3", - }, - } - - // we add the jobs before starting the workers, so we'd expect the round - // robin to pick the least-recently-added job from each queue, and cycle - // through queues in a round-robin fashion. jobs would appear on the queues - // as illustrated below, and we expect to round robin as: - // queue-1 -> queue-2 -> queue-3 -> queue-1 ... - // - // queue-1 [job-3, job-1] - // queue-2 [job-2] - // queue-3 [job-5, job-4] - - // ... where jobs are pushed to the left side and popped from the right side - - expectedOrder := []string{"job-1", "job-2", "job-4", "job-3", "job-5"} - - resultsCh := make(chan string) - defer close(resultsCh) - - var mu sync.Mutex - order := make([]string, 0) - - go func() { - for { - select { - case res, ok := <-resultsCh: - if !ok { - return - } - - mu.Lock() - order = append(order, res) - mu.Unlock() - } - } - }() - - var wg sync.WaitGroup - ex := func(name string) error { - resultsCh <- name - time.Sleep(50 * time.Millisecond) - wg.Done() - return nil - } - onFail := func(_ error) {} - - // use one worker to guarantee ordering - j := NewJobManager("test-job-mgr", 1, newTestLogger("jobmanager-test"), nil) - for _, tc := range testCases { - wg.Add(1) - job := newTestJob(t, tc.name, ex, onFail) - j.AddJob(&job, tc.queueID) - } - - j.Start() - defer j.Stop() - - doneCh := make(chan struct{}) - go func() { - wg.Wait() - doneCh <- struct{}{} - }() - - timeout := time.After(5 * time.Second) - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } - - mu.Lock() - defer mu.Unlock() - if !reflect.DeepEqual(order, expectedOrder) { - t.Fatalf("results out of order. \nexpected: %v\ngot: %v", expectedOrder, order) - } -} - -func TestFairshare_StressTest(t *testing.T) { - var wg sync.WaitGroup - ex := func(name string) error { - wg.Done() - return nil - } - onFail := func(_ error) {} - - j := NewJobManager("test-job-mgr", 15, nil, nil) - j.Start() - defer j.Stop() - - for i := 0; i < 3000; i++ { - wg.Add(1) - job := newTestJob(t, fmt.Sprintf("a-job-%d", i), ex, onFail) - j.AddJob(&job, "a") - } - for i := 0; i < 4000; i++ { - wg.Add(1) - job := newTestJob(t, fmt.Sprintf("b-job-%d", i), ex, onFail) - j.AddJob(&job, "b") - } - for i := 0; i < 3000; i++ { - wg.Add(1) - job := newTestJob(t, fmt.Sprintf("c-job-%d", i), ex, onFail) - j.AddJob(&job, "c") - } - - doneCh := make(chan struct{}) - go func() { - wg.Wait() - doneCh <- struct{}{} - }() - - timeout := time.After(5 * time.Second) - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } -} - -func TestFairshare_nilLoggerJobManager(t *testing.T) { - j := NewJobManager("test-job-mgr", 1, nil, nil) - if j.logger == nil { - t.Error("logger not set up properly") - } -} - -func TestFairshare_getNextQueue(t *testing.T) { - j := NewJobManager("test-job-mgr", 18, nil, nil) - - for i := 0; i < 10; i++ { - job := newDefaultTestJob(t, fmt.Sprintf("job-%d", i)) - j.AddJob(&job, "a") - j.AddJob(&job, "b") - j.AddJob(&job, "c") - } - - j.l.Lock() - defer j.l.Unlock() - - // fake out some number of workers with various remaining work scenario - // no queue can be assigned more than 6 workers - j.workerCount["a"] = 1 - j.workerCount["b"] = 2 - j.workerCount["c"] = 5 - - expectedOrder := []string{"a", "b", "c", "a", "b", "a", "b", "a", "b", "a"} - - for _, expectedQueueID := range expectedOrder { - queueID, canAssignWorker := j.getNextQueue() - - if !canAssignWorker { - t.Fatalf("expected have work true, got false for queue %q", queueID) - } - if queueID != expectedQueueID { - t.Errorf("expected queueID %q, got %q", expectedQueueID, queueID) - } - - // simulate a worker being added to that queue - j.workerCount[queueID]++ - } - - // queues are saturated with work, we shouldn't be able to find a queue - // eligible for a worker (and last accessed queue shouldn't update) - expectedLastQueueAccessed := j.lastQueueAccessed - queueID, canAssignWork := j.getNextQueue() - if canAssignWork { - t.Error("should not be able to assign work with all queues saturated") - } - if queueID != "" { - t.Errorf("expected no queueID, got %s", queueID) - } - if j.lastQueueAccessed != expectedLastQueueAccessed { - t.Errorf("expected no last queue accessed update. had %d, got %d", expectedLastQueueAccessed, j.lastQueueAccessed) - } -} - -func TestJobManager_pruneEmptyQueues(t *testing.T) { - j := NewJobManager("test-job-mgr", 18, nil, nil) - - // add a few jobs to test out queue pruning - // for test simplicity, we'll keep the number of workers per queue at 0 - testJob := newDefaultTestJob(t, "job-0") - j.AddJob(&testJob, "a") - j.AddJob(&testJob, "a") - j.AddJob(&testJob, "b") - - job, queueID := j.getNextJob() - if queueID != "a" || job == nil { - t.Fatalf("bad next job: queueID %s, job: %#v", queueID, job) - } - - j.l.RLock() - if _, ok := j.queues["a"]; !ok { - t.Error("expected queue 'a' to exist") - } - if _, ok := j.queues["b"]; !ok { - t.Error("expected queue 'b' to exist") - } - j.l.RUnlock() - - job, queueID = j.getNextJob() - if queueID != "b" || job == nil { - t.Fatalf("bad next job: queueID %s, job: %#v", queueID, job) - } - - j.l.RLock() - if _, ok := j.queues["a"]; !ok { - t.Error("expected queue 'a' to exist") - } - if _, ok := j.queues["b"]; ok { - t.Error("expected queue 'b' to be pruned") - } - j.l.RUnlock() - - job, queueID = j.getNextJob() - if queueID != "a" || job == nil { - t.Fatalf("bad next job: queueID %s, job: %#v", queueID, job) - } - - j.l.RLock() - if _, ok := j.queues["a"]; ok { - t.Error("expected queue 'a' to be pruned") - } - if _, ok := j.queues["b"]; ok { - t.Error("expected queue 'b' to be pruned") - } - j.l.RUnlock() - - job, queueID = j.getNextJob() - if job != nil { - t.Errorf("expected no more jobs (out of queues). queueID: %s, job: %#v", queueID, job) - } -} - -func TestFairshare_WorkerCount_IncrementAndDecrement(t *testing.T) { - j := NewJobManager("test-job-mgr", 18, nil, nil) - - job := newDefaultTestJob(t, "job-0") - j.AddJob(&job, "a") - j.AddJob(&job, "b") - j.AddJob(&job, "c") - - // test to make sure increment works - j.incrementWorkerCount("a") - workerCounts := j.GetWorkerCounts() - if workerCounts["a"] != 1 { - t.Fatalf("expected 1 worker on 'a', got %d", workerCounts["a"]) - } - if workerCounts["b"] != 0 { - t.Fatalf("expected 0 workers on 'b', got %d", workerCounts["b"]) - } - if workerCounts["c"] != 0 { - t.Fatalf("expected 0 workers on 'c', got %d", workerCounts["c"]) - } - - // test to make sure decrement works (when there is still work for the queue) - j.decrementWorkerCount("a") - workerCounts = j.GetWorkerCounts() - if workerCounts["a"] != 0 { - t.Fatalf("expected 0 workers on 'a', got %d", workerCounts["a"]) - } - - // add a worker to queue "a" and remove all work to ensure worker count gets - // cleared out for "a" - j.incrementWorkerCount("a") - j.l.Lock() - delete(j.queues, "a") - j.l.Unlock() - - j.decrementWorkerCount("a") - workerCounts = j.GetWorkerCounts() - if _, ok := workerCounts["a"]; ok { - t.Fatalf("expected no worker count for 'a', got %#v", workerCounts) - } -} - -func TestFairshare_queueWorkersSaturated(t *testing.T) { - j := NewJobManager("test-job-mgr", 20, nil, nil) - - job := newDefaultTestJob(t, "job-0") - j.AddJob(&job, "a") - j.AddJob(&job, "b") - - // no more than 9 workers can be assigned to a single queue in this example - for i := 0; i < 8; i++ { - j.incrementWorkerCount("a") - j.incrementWorkerCount("b") - - j.l.RLock() - if j.queueWorkersSaturated("a") { - j.l.RUnlock() - t.Fatalf("queue 'a' falsely saturated: %#v", j.GetWorkerCounts()) - } - if j.queueWorkersSaturated("b") { - j.l.RUnlock() - t.Fatalf("queue 'b' falsely saturated: %#v", j.GetWorkerCounts()) - } - j.l.RUnlock() - } - - // adding the 9th and 10th workers should saturate the number of workers we - // can have per queue - for i := 8; i < 10; i++ { - j.incrementWorkerCount("a") - j.incrementWorkerCount("b") - - j.l.RLock() - if !j.queueWorkersSaturated("a") { - j.l.RUnlock() - t.Fatalf("queue 'a' falsely unsaturated: %#v", j.GetWorkerCounts()) - } - if !j.queueWorkersSaturated("b") { - j.l.RUnlock() - t.Fatalf("queue 'b' falsely unsaturated: %#v", j.GetWorkerCounts()) - } - j.l.RUnlock() - } -} diff --git a/helper/fairshare/workerpool_test.go b/helper/fairshare/workerpool_test.go deleted file mode 100644 index d347c6734..000000000 --- a/helper/fairshare/workerpool_test.go +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package fairshare - -import ( - "fmt" - "reflect" - "sync" - "testing" - "time" -) - -func TestFairshare_newDispatcher(t *testing.T) { - testCases := []struct { - name string - numWorkers int - expectedNumWorkers int - }{ - { - name: "", - numWorkers: 0, - expectedNumWorkers: 1, - }, - { - name: "", - numWorkers: 10, - expectedNumWorkers: 10, - }, - { - name: "test-dispatcher", - numWorkers: 10, - expectedNumWorkers: 10, - }, - } - - l := newTestLogger("workerpool-test") - for tcNum, tc := range testCases { - d := newDispatcher(tc.name, tc.numWorkers, l) - - if tc.name != "" && d.name != tc.name { - t.Errorf("tc %d: expected name %s, got %s", tcNum, tc.name, d.name) - } - if len(d.workers) != tc.expectedNumWorkers { - t.Errorf("tc %d: expected %d workers, got %d", tcNum, tc.expectedNumWorkers, len(d.workers)) - } - if d.jobCh == nil { - t.Errorf("tc %d: work channel not set up properly", tcNum) - } - } -} - -func TestFairshare_createDispatcher(t *testing.T) { - testCases := []struct { - name string - numWorkers int - expectedNumWorkers int - }{ - { - name: "", - numWorkers: -1, - expectedNumWorkers: 1, - }, - { - name: "", - numWorkers: 0, - expectedNumWorkers: 1, - }, - { - name: "", - numWorkers: 10, - expectedNumWorkers: 10, - }, - { - name: "", - numWorkers: 10, - expectedNumWorkers: 10, - }, - { - name: "test-dispatcher", - numWorkers: 10, - expectedNumWorkers: 10, - }, - } - - l := newTestLogger("workerpool-test") - for tcNum, tc := range testCases { - d := createDispatcher(tc.name, tc.numWorkers, l) - if d == nil { - t.Fatalf("tc %d: expected non-nil object", tcNum) - } - - if tc.name != "" && d.name != tc.name { - t.Errorf("tc %d: expected name %s, got %s", tcNum, tc.name, d.name) - } - if len(d.name) == 0 { - t.Errorf("tc %d: expected name to be set", tcNum) - } - if d.numWorkers != tc.expectedNumWorkers { - t.Errorf("tc %d: expected %d workers, got %d", tcNum, tc.expectedNumWorkers, d.numWorkers) - } - if d.workers == nil { - t.Errorf("tc %d: expected non-nil workers", tcNum) - } - if d.jobCh == nil { - t.Errorf("tc %d: work channel not set up properly", tcNum) - } - if d.quit == nil { - t.Errorf("tc %d: expected non-nil quit channel", tcNum) - } - if d.logger == nil { - t.Errorf("tc %d: expected non-nil logger", tcNum) - } - } -} - -func TestFairshare_initDispatcher(t *testing.T) { - testCases := []struct { - numWorkers int - }{ - { - numWorkers: 1, - }, - { - numWorkers: 10, - }, - { - numWorkers: 100, - }, - { - numWorkers: 1000, - }, - } - - l := newTestLogger("workerpool-test") - for tcNum, tc := range testCases { - d := createDispatcher("", tc.numWorkers, l) - - d.init() - if len(d.workers) != tc.numWorkers { - t.Fatalf("tc %d: expected %d workers, got %d", tcNum, tc.numWorkers, len(d.workers)) - } - } -} - -func TestFairshare_initializeWorker(t *testing.T) { - numWorkers := 3 - - d := createDispatcher("", numWorkers, newTestLogger("workerpool-test")) - - for workerNum := 0; workerNum < numWorkers; workerNum++ { - d.initializeWorker() - - w := d.workers[workerNum] - expectedName := fmt.Sprint("worker-", workerNum) - if w.name != expectedName { - t.Errorf("tc %d: expected name %s, got %s", workerNum, expectedName, w.name) - } - if w.jobCh != d.jobCh { - t.Errorf("tc %d: work channel not set up properly", workerNum) - } - if w.quit == nil || w.quit != d.quit { - t.Errorf("tc %d: quit channel not set up properly", workerNum) - } - if w.logger == nil || w.logger != d.logger { - t.Errorf("tc %d: logger not set up properly", workerNum) - } - } -} - -func TestFairshare_startWorker(t *testing.T) { - d := newDispatcher("", 1, newTestLogger("workerpool-test")) - - d.workers[0].start() - defer d.stop() - - var wg sync.WaitGroup - ex := func(_ string) error { - wg.Done() - return nil - } - onFail := func(_ error) {} - - job := newTestJob(t, "test job", ex, onFail) - - doneCh := make(chan struct{}) - timeout := time.After(5 * time.Second) - - wg.Add(1) - d.dispatch(&job, nil, nil) - go func() { - wg.Wait() - doneCh <- struct{}{} - }() - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } -} - -func TestFairshare_start(t *testing.T) { - numJobs := 10 - var wg sync.WaitGroup - ex := func(_ string) error { - wg.Done() - return nil - } - onFail := func(_ error) {} - - wg.Add(numJobs) - d := newDispatcher("", 3, newTestLogger("workerpool-test")) - - d.start() - defer d.stop() - - doneCh := make(chan struct{}) - timeout := time.After(5 * time.Second) - go func() { - wg.Wait() - doneCh <- struct{}{} - }() - - for i := 0; i < numJobs; i++ { - job := newTestJob(t, fmt.Sprintf("job-%d", i), ex, onFail) - d.dispatch(&job, nil, nil) - } - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } -} - -func TestFairshare_stop(t *testing.T) { - d := newDispatcher("", 5, newTestLogger("workerpool-test")) - - d.start() - - doneCh := make(chan struct{}) - timeout := time.After(5 * time.Second) - - go func() { - d.stop() - d.wg.Wait() - doneCh <- struct{}{} - }() - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } -} - -func TestFairshare_stopMultiple(t *testing.T) { - d := newDispatcher("", 5, newTestLogger("workerpool-test")) - - d.start() - - doneCh := make(chan struct{}) - timeout := time.After(5 * time.Second) - - go func() { - d.stop() - d.wg.Wait() - doneCh <- struct{}{} - }() - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } - - // essentially, we don't want to panic here - var r interface{} - go func() { - t.Helper() - - defer func() { - r = recover() - doneCh <- struct{}{} - }() - - d.stop() - d.wg.Wait() - }() - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } - - if r != nil { - t.Fatalf("panic during second stop: %v", r) - } -} - -func TestFairshare_dispatch(t *testing.T) { - d := newDispatcher("", 1, newTestLogger("workerpool-test")) - - var wg sync.WaitGroup - accumulatedIDs := make([]string, 0) - ex := func(id string) error { - accumulatedIDs = append(accumulatedIDs, id) - wg.Done() - return nil - } - onFail := func(_ error) {} - - expectedIDs := []string{"job-1", "job-2", "job-3", "job-4"} - go func() { - for _, id := range expectedIDs { - job := newTestJob(t, id, ex, onFail) - d.dispatch(&job, nil, nil) - } - }() - - wg.Add(len(expectedIDs)) - d.start() - defer d.stop() - - doneCh := make(chan struct{}) - go func() { - wg.Wait() - doneCh <- struct{}{} - }() - - timeout := time.After(5 * time.Second) - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } - - if !reflect.DeepEqual(accumulatedIDs, expectedIDs) { - t.Fatalf("bad job ids. expected %v, got %v", expectedIDs, accumulatedIDs) - } -} - -func TestFairshare_jobFailure(t *testing.T) { - numJobs := 10 - testErr := fmt.Errorf("test error") - var wg sync.WaitGroup - - ex := func(_ string) error { - return testErr - } - onFail := func(err error) { - if err != testErr { - t.Errorf("got unexpected error. expected %v, got %v", testErr, err) - } - - wg.Done() - } - - wg.Add(numJobs) - d := newDispatcher("", 3, newTestLogger("workerpool-test")) - - d.start() - defer d.stop() - - doneCh := make(chan struct{}) - timeout := time.After(5 * time.Second) - go func() { - wg.Wait() - doneCh <- struct{}{} - }() - - for i := 0; i < numJobs; i++ { - job := newTestJob(t, fmt.Sprintf("job-%d", i), ex, onFail) - d.dispatch(&job, nil, nil) - } - - select { - case <-doneCh: - break - case <-timeout: - t.Fatal("timed out") - } -} - -func TestFairshare_nilLoggerDispatcher(t *testing.T) { - d := newDispatcher("test-job-mgr", 1, nil) - if d.logger == nil { - t.Error("logger not set up properly") - } -} diff --git a/helper/flag-kv/flag_test.go b/helper/flag-kv/flag_test.go deleted file mode 100644 index 91a344403..000000000 --- a/helper/flag-kv/flag_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package kvFlag - -import ( - "flag" - "reflect" - "testing" -) - -func TestFlag_impl(t *testing.T) { - var _ flag.Value = new(Flag) -} - -func TestFlag(t *testing.T) { - cases := []struct { - Input string - Output map[string]string - Error bool - }{ - { - "key=value", - map[string]string{"key": "value"}, - false, - }, - - { - "key=", - map[string]string{"key": ""}, - false, - }, - - { - "key=foo=bar", - map[string]string{"key": "foo=bar"}, - false, - }, - - { - "key", - nil, - true, - }, - } - - for _, tc := range cases { - f := new(Flag) - err := f.Set(tc.Input) - if (err != nil) != tc.Error { - t.Fatalf("bad error. Input: %#v", tc.Input) - } - - actual := map[string]string(*f) - if !reflect.DeepEqual(actual, tc.Output) { - t.Fatalf("bad: %#v", actual) - } - } -} diff --git a/helper/flag-slice/flag_test.go b/helper/flag-slice/flag_test.go deleted file mode 100644 index 6662446df..000000000 --- a/helper/flag-slice/flag_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package sliceflag - -import ( - "flag" - "reflect" - "testing" -) - -func TestStringFlag_implements(t *testing.T) { - var raw interface{} - raw = new(StringFlag) - if _, ok := raw.(flag.Value); !ok { - t.Fatalf("StringFlag should be a Value") - } -} - -func TestStringFlagSet(t *testing.T) { - sv := new(StringFlag) - err := sv.Set("foo") - if err != nil { - t.Fatalf("err: %s", err) - } - - err = sv.Set("bar") - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := []string{"foo", "bar"} - if !reflect.DeepEqual([]string(*sv), expected) { - t.Fatalf("Bad: %#v", sv) - } -} diff --git a/helper/forwarding/util_test.go b/helper/forwarding/util_test.go deleted file mode 100644 index 0bf4be769..000000000 --- a/helper/forwarding/util_test.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package forwarding - -import ( - "bufio" - "bytes" - "net/http" - "os" - "reflect" - "testing" -) - -func Test_ForwardedRequest_GenerateParse(t *testing.T) { - testForwardedRequestGenerateParse(t) -} - -func Benchmark_ForwardedRequest_GenerateParse_JSON(b *testing.B) { - os.Setenv("VAULT_MESSAGE_TYPE", "json") - var totalSize int64 - var numRuns int64 - for i := 0; i < b.N; i++ { - totalSize += testForwardedRequestGenerateParse(b) - numRuns++ - } - b.Logf("message size per op: %d", totalSize/numRuns) -} - -func Benchmark_ForwardedRequest_GenerateParse_JSON_Compressed(b *testing.B) { - os.Setenv("VAULT_MESSAGE_TYPE", "json_compress") - var totalSize int64 - var numRuns int64 - for i := 0; i < b.N; i++ { - totalSize += testForwardedRequestGenerateParse(b) - numRuns++ - } - b.Logf("message size per op: %d", totalSize/numRuns) -} - -func Benchmark_ForwardedRequest_GenerateParse_Proto3(b *testing.B) { - os.Setenv("VAULT_MESSAGE_TYPE", "proto3") - var totalSize int64 - var numRuns int64 - for i := 0; i < b.N; i++ { - totalSize += testForwardedRequestGenerateParse(b) - numRuns++ - } - b.Logf("message size per op: %d", totalSize/numRuns) -} - -func testForwardedRequestGenerateParse(t testing.TB) int64 { - bodBuf := bytes.NewReader([]byte(`{ "foo": "bar", "zip": { "argle": "bargle", neet: 0 } }`)) - req, err := http.NewRequest("FOOBAR", "https://pushit.real.good:9281/snicketysnack?furbleburble=bloopetybloop", bodBuf) - if err != nil { - t.Fatal(err) - } - - // We want to get the fields we would expect from an incoming request, so - // we write it out and then read it again - buf1 := bytes.NewBuffer(nil) - err = req.Write(buf1) - if err != nil { - t.Fatal(err) - } - - // Read it back in, parsing like a server - bufr1 := bufio.NewReader(buf1) - initialReq, err := http.ReadRequest(bufr1) - if err != nil { - t.Fatal(err) - } - - // Generate the request with the forwarded request in the body - req, err = GenerateForwardedHTTPRequest(initialReq, "https://bloopety.bloop:8201") - if err != nil { - t.Fatal(err) - } - - // Perform another "round trip" - buf2 := bytes.NewBuffer(nil) - err = req.Write(buf2) - if err != nil { - t.Fatal(err) - } - size := int64(buf2.Len()) - bufr2 := bufio.NewReader(buf2) - intreq, err := http.ReadRequest(bufr2) - if err != nil { - t.Fatal(err) - } - - // Now extract the forwarded request to generate a final request for processing - finalReq, err := ParseForwardedHTTPRequest(intreq) - if err != nil { - t.Fatal(err) - } - - switch { - case initialReq.Method != finalReq.Method: - t.Fatalf("bad method:\ninitialReq:\n%#v\nfinalReq:\n%#v\n", *initialReq, *finalReq) - case initialReq.RemoteAddr != finalReq.RemoteAddr: - t.Fatalf("bad remoteaddr:\ninitialReq:\n%#v\nfinalReq:\n%#v\n", *initialReq, *finalReq) - case initialReq.Host != finalReq.Host: - t.Fatalf("bad host:\ninitialReq:\n%#v\nfinalReq:\n%#v\n", *initialReq, *finalReq) - case !reflect.DeepEqual(initialReq.URL, finalReq.URL): - t.Fatalf("bad url:\ninitialReq:\n%#v\nfinalReq:\n%#v\n", *initialReq.URL, *finalReq.URL) - case !reflect.DeepEqual(initialReq.Header, finalReq.Header): - t.Fatalf("bad header:\ninitialReq:\n%#v\nfinalReq:\n%#v\n", *initialReq, *finalReq) - default: - // Compare bodies - bodBuf.Seek(0, 0) - initBuf := bytes.NewBuffer(nil) - _, err = initBuf.ReadFrom(bodBuf) - if err != nil { - t.Fatal(err) - } - finBuf := bytes.NewBuffer(nil) - _, err = finBuf.ReadFrom(finalReq.Body) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(initBuf.Bytes(), finBuf.Bytes()) { - t.Fatalf("badbody :\ninitialReq:\n%#v\nfinalReq:\n%#v\n", initBuf.Bytes(), finBuf.Bytes()) - } - } - - return size -} diff --git a/helper/hostutil/hostinfo_test.go b/helper/hostutil/hostinfo_test.go deleted file mode 100644 index 6862cacf7..000000000 --- a/helper/hostutil/hostinfo_test.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package hostutil - -import ( - "context" - "strings" - "testing" - - "github.com/hashicorp/errwrap" -) - -func TestCollectHostInfo(t *testing.T) { - info, err := CollectHostInfo(context.Background()) - if err != nil && !errwrap.ContainsType(err, new(HostInfoError)) { - t.Fatal(err) - } - - // Get all the possible HostInfoError errors and check for the resulting - // stat if the package is able to fetch it for the platform we're testing - // on. - errs := errwrap.GetAllType(err, new(HostInfoError)) - - if info.Timestamp.IsZero() { - t.Fatal("expected non-zero Timestamp") - } - if !checkErrTypeExists(errs, "cpu") && info.CPU == nil { - t.Fatal("expected non-nil CPU value") - } - if !checkErrTypeExists(errs, "cpu_times") && info.CPUTimes == nil { - t.Fatal("expected non-nil CPUTimes value") - } - if !checkErrTypeExists(errs, "disk") && info.Disk == nil { - t.Fatal("expected non-nil Disk value") - } - if !checkErrTypeExists(errs, "host") && info.Host == nil { - t.Fatal("expected non-nil Host value") - } - if !checkErrTypeExists(errs, "memory") && info.Memory == nil { - t.Fatal("expected non-nil Memory value") - } -} - -// checkErrTypeExists is a helper that checks whether an particular -// HostInfoError.Type exists within a set of errors. -func checkErrTypeExists(errs []error, errType string) bool { - for _, e := range errs { - err, ok := e.(*HostInfoError) - if !ok { - return false - } - - // This is mainly for disk error since the type string can contain an - // index for the disk. - parts := strings.SplitN(err.Type, ".", 2) - - if parts[0] == errType { - return true - } - } - return false -} diff --git a/helper/logging/logfile_test.go b/helper/logging/logfile_test.go deleted file mode 100644 index 8cb66693d..000000000 --- a/helper/logging/logfile_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package logging - -import ( - "os" - "path/filepath" - "sort" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLogFile_openNew(t *testing.T) { - logFile := &LogFile{ - fileName: "vault.log", - logPath: t.TempDir(), - duration: defaultRotateDuration, - } - - err := logFile.openNew() - require.NoError(t, err) - - msg := "[INFO] Something" - _, err = logFile.Write([]byte(msg)) - require.NoError(t, err) - - content, err := os.ReadFile(logFile.fileInfo.Name()) - require.NoError(t, err) - require.Contains(t, string(content), msg) -} - -func TestLogFile_Rotation_MaxDuration(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") - } - - tempDir := t.TempDir() - logFile := LogFile{ - fileName: "vault.log", - logPath: tempDir, - duration: 50 * time.Millisecond, - } - - _, err := logFile.Write([]byte("Hello World")) - assert.NoError(t, err, "error writing rotation max duration part 1") - - time.Sleep(3 * logFile.duration) - - _, err = logFile.Write([]byte("Second File")) - assert.NoError(t, err, "error writing rotation max duration part 2") - - require.Len(t, listDir(t, tempDir), 2) -} - -func TestLogFile_Rotation_MaxBytes(t *testing.T) { - tempDir := t.TempDir() - logFile := LogFile{ - fileName: "somefile.log", - logPath: tempDir, - maxBytes: 10, - duration: defaultRotateDuration, - } - _, err := logFile.Write([]byte("Hello World")) - assert.NoError(t, err, "error writing rotation max bytes part 1") - - _, err = logFile.Write([]byte("Second File")) - assert.NoError(t, err, "error writing rotation max bytes part 2") - - require.Len(t, listDir(t, tempDir), 2) -} - -func TestLogFile_PruneFiles(t *testing.T) { - tempDir := t.TempDir() - logFile := LogFile{ - fileName: "vault.log", - logPath: tempDir, - maxBytes: 10, - duration: defaultRotateDuration, - maxArchivedFiles: 1, - } - _, err := logFile.Write([]byte("[INFO] Hello World")) - assert.NoError(t, err, "error writing during prune files test part 1") - - _, err = logFile.Write([]byte("[INFO] Second File")) - assert.NoError(t, err, "error writing during prune files test part 1") - - _, err = logFile.Write([]byte("[INFO] Third File")) - assert.NoError(t, err, "error writing during prune files test part 1") - - logFiles := listDir(t, tempDir) - sort.Strings(logFiles) - require.Len(t, logFiles, 2) - - content, err := os.ReadFile(filepath.Join(tempDir, logFiles[0])) - require.NoError(t, err) - require.Contains(t, string(content), "Second File") - - content, err = os.ReadFile(filepath.Join(tempDir, logFiles[1])) - require.NoError(t, err) - require.Contains(t, string(content), "Third File") -} - -func TestLogFile_PruneFiles_Disabled(t *testing.T) { - tempDir := t.TempDir() - logFile := LogFile{ - fileName: "somename.log", - logPath: tempDir, - maxBytes: 10, - duration: defaultRotateDuration, - maxArchivedFiles: 0, - } - - _, err := logFile.Write([]byte("[INFO] Hello World")) - assert.NoError(t, err, "error writing during prune files - disabled test part 1") - - _, err = logFile.Write([]byte("[INFO] Second File")) - assert.NoError(t, err, "error writing during prune files - disabled test part 2") - - _, err = logFile.Write([]byte("[INFO] Third File")) - assert.NoError(t, err, "error writing during prune files - disabled test part 3") - - require.Len(t, listDir(t, tempDir), 3) -} - -func TestLogFile_FileRotation_Disabled(t *testing.T) { - tempDir := t.TempDir() - logFile := LogFile{ - fileName: "vault.log", - logPath: tempDir, - maxBytes: 10, - maxArchivedFiles: -1, - } - - _, err := logFile.Write([]byte("[INFO] Hello World")) - assert.NoError(t, err, "error writing during rotation disabled test part 1") - - _, err = logFile.Write([]byte("[INFO] Second File")) - assert.NoError(t, err, "error writing during rotation disabled test part 2") - - _, err = logFile.Write([]byte("[INFO] Third File")) - assert.NoError(t, err, "error writing during rotation disabled test part 3") - - require.Len(t, listDir(t, tempDir), 1) -} - -func listDir(t *testing.T, name string) []string { - t.Helper() - fh, err := os.Open(name) - require.NoError(t, err) - files, err := fh.Readdirnames(100) - require.NoError(t, err) - return files -} diff --git a/helper/logging/logger_test.go b/helper/logging/logger_test.go deleted file mode 100644 index c5f7ec50d..000000000 --- a/helper/logging/logger_test.go +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package logging - -import ( - "bytes" - "encoding/json" - "errors" - "os" - "path/filepath" - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLogger_SetupBasic(t *testing.T) { - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Info - - logger, err := Setup(cfg, nil) - require.NoError(t, err) - require.NotNil(t, logger) - require.Equal(t, logger.Name(), "test-system") - require.True(t, logger.IsInfo()) -} - -func TestLogger_SetupInvalidLogLevel(t *testing.T) { - cfg := newTestLogConfig(t) - - _, err := Setup(cfg, nil) - assert.Containsf(t, err.Error(), "invalid log level", "expected error %s", err) -} - -func TestLogger_SetupLoggerErrorLevel(t *testing.T) { - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Error - - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.NoError(t, err) - require.NotNil(t, logger) - - logger.Error("test error msg") - logger.Info("test info msg") - - output := buf.String() - - require.Contains(t, output, "[ERROR] test-system: test error msg") - require.NotContains(t, output, "[INFO] test-system: test info msg") -} - -func TestLogger_SetupLoggerDebugLevel(t *testing.T) { - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Debug - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.NoError(t, err) - require.NotNil(t, logger) - - logger.Info("test info msg") - logger.Debug("test debug msg") - - output := buf.String() - - require.Contains(t, output, "[INFO] test-system: test info msg") - require.Contains(t, output, "[DEBUG] test-system: test debug msg") -} - -func TestLogger_SetupLoggerWithoutName(t *testing.T) { - cfg := newTestLogConfig(t) - cfg.Name = "" - cfg.LogLevel = hclog.Info - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.NoError(t, err) - require.NotNil(t, logger) - - logger.Warn("test warn msg") - - require.Contains(t, buf.String(), "[WARN] test warn msg") -} - -func TestLogger_SetupLoggerWithJSON(t *testing.T) { - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Debug - cfg.LogFormat = JSONFormat - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.NoError(t, err) - require.NotNil(t, logger) - - logger.Warn("test warn msg") - - var jsonOutput map[string]string - err = json.Unmarshal(buf.Bytes(), &jsonOutput) - require.NoError(t, err) - require.Contains(t, jsonOutput, "@level") - require.Equal(t, jsonOutput["@level"], "warn") - require.Contains(t, jsonOutput, "@message") - require.Equal(t, jsonOutput["@message"], "test warn msg") -} - -func TestLogger_SetupLoggerWithValidLogPathMissingFileName(t *testing.T) { - tmpDir := t.TempDir() - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Info - cfg.LogFilePath = tmpDir + "/" // add the trailing slash to the temp dir - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.NoError(t, err) - require.NotNil(t, logger) - - logger.Info("juan?") - - m, err := filepath.Glob(cfg.LogFilePath + "*") - require.NoError(t, err) - require.Truef(t, len(m) == 1, "no files were found") -} - -func TestLogger_SetupLoggerWithValidLogPathFileName(t *testing.T) { - tmpDir := t.TempDir() - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Info - cfg.LogFilePath = filepath.Join(tmpDir, "juan.log") - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.NoError(t, err) - require.NotNil(t, logger) - - logger.Info("juan?") - f, err := os.Stat(cfg.LogFilePath) - require.NoError(t, err) - require.NotNil(t, f) -} - -func TestLogger_SetupLoggerWithValidLogPathFileNameRotate(t *testing.T) { - tmpDir := t.TempDir() - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Info - cfg.LogFilePath = filepath.Join(tmpDir, "juan.log") - cfg.LogRotateBytes = 1 // set a tiny number of bytes to force rotation - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.NoError(t, err) - require.NotNil(t, logger) - - logger.Info("juan?") - logger.Info("john?") - f, err := os.Stat(cfg.LogFilePath) - require.NoError(t, err) - require.NotNil(t, f) - m, err := filepath.Glob(tmpDir + "/juan-*") // look for juan-{timestamp}.log - require.NoError(t, err) - require.Truef(t, len(m) == 1, "no files were found") -} - -func TestLogger_SetupLoggerWithValidLogPath(t *testing.T) { - tmpDir := t.TempDir() - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Info - cfg.LogFilePath = tmpDir + "/" // add the trailing slash to the temp dir - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.NoError(t, err) - require.NotNil(t, logger) -} - -func TestLogger_SetupLoggerWithInValidLogPath(t *testing.T) { - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Info - cfg.LogLevel = hclog.Info - cfg.LogFilePath = "nonexistentdir/" - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.Error(t, err) - require.True(t, errors.Is(err, os.ErrNotExist)) - require.Nil(t, logger) -} - -func TestLogger_SetupLoggerWithInValidLogPathPermission(t *testing.T) { - tmpDir := "/tmp/" + t.Name() - - err := os.Mkdir(tmpDir, 0o000) - assert.NoError(t, err, "unexpected error testing with invalid log path permission") - defer os.RemoveAll(tmpDir) - - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Info - cfg.LogFilePath = tmpDir + "/" - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.Error(t, err) - require.True(t, errors.Is(err, os.ErrPermission)) - require.Nil(t, logger) -} - -func TestLogger_SetupLoggerWithInvalidLogFilePath(t *testing.T) { - cases := map[string]struct { - path string - message string - }{ - "file name *": { - path: "/this/isnt/ok/juan*.log", - message: "file name contains globbing character", - }, - "file name ?": { - path: "/this/isnt/ok/juan?.log", - message: "file name contains globbing character", - }, - "file name [": { - path: "/this/isnt/ok/[juan].log", - message: "file name contains globbing character", - }, - "directory path *": { - path: "/this/isnt/ok/*/qwerty.log", - message: "directory contains glob character", - }, - "directory path ?": { - path: "/this/isnt/ok/?/qwerty.log", - message: "directory contains glob character", - }, - "directory path [": { - path: "/this/isnt/ok/[foo]/qwerty.log", - message: "directory contains glob character", - }, - } - - for name, tc := range cases { - name := name - tc := tc - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Info - cfg.LogFilePath = tc.path - - _, err := Setup(cfg, &bytes.Buffer{}) - assert.Error(t, err, "%s: expected error due to *", name) - assert.Contains(t, err.Error(), tc.message, "%s: error message does not match: %s", name, err.Error()) - } -} - -func TestLogger_ChangeLogLevels(t *testing.T) { - cfg := newTestLogConfig(t) - cfg.LogLevel = hclog.Debug - var buf bytes.Buffer - - logger, err := Setup(cfg, &buf) - require.NoError(t, err) - require.NotNil(t, logger) - - assert.Equal(t, hclog.Debug, logger.GetLevel()) - - // Create new named loggers from the base logger and change the levels - logger2 := logger.Named("test2") - logger3 := logger.Named("test3") - - logger2.SetLevel(hclog.Info) - logger3.SetLevel(hclog.Error) - - assert.Equal(t, hclog.Debug, logger.GetLevel()) - assert.Equal(t, hclog.Info, logger2.GetLevel()) - assert.Equal(t, hclog.Error, logger3.GetLevel()) -} - -func newTestLogConfig(t *testing.T) *LogConfig { - t.Helper() - - cfg, err := NewLogConfig("test") - require.NoError(t, err) - cfg.Name = "test-system" - - return cfg -} diff --git a/helper/metricsutil/bucket_test.go b/helper/metricsutil/bucket_test.go deleted file mode 100644 index 19b6636ed..000000000 --- a/helper/metricsutil/bucket_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package metricsutil - -import ( - "testing" - "time" -) - -func TestTTLBucket_Lookup(t *testing.T) { - testCases := []struct { - Input time.Duration - Expected string - }{ - {30 * time.Second, "1m"}, - {0 * time.Second, "1m"}, - {2 * time.Hour, "2h"}, - {2*time.Hour - time.Second, "2h"}, - {2*time.Hour + time.Second, "1d"}, - {30 * 24 * time.Hour, "30d"}, - {31 * 24 * time.Hour, "+Inf"}, - } - - for _, tc := range testCases { - bucket := TTLBucket(tc.Input) - if bucket != tc.Expected { - t.Errorf("Expected %q, got %q for duration %v.", tc.Expected, bucket, tc.Input) - } - } -} diff --git a/helper/metricsutil/gauge_process_test.go b/helper/metricsutil/gauge_process_test.go deleted file mode 100644 index e5e1c6145..000000000 --- a/helper/metricsutil/gauge_process_test.go +++ /dev/null @@ -1,579 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package metricsutil - -import ( - "context" - "errors" - "fmt" - "math/rand" - "reflect" - "sync/atomic" - "testing" - "time" - - "github.com/armon/go-metrics" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/helper/timeutil" -) - -// SimulatedTime maintains a virtual clock so the test isn't -// dependent upon real time. -// Unfortunately there is no way to run these tests in parallel -// since they rely on the same global timeNow function. -type SimulatedTime struct { - now time.Time - tickerBarrier chan *SimulatedTicker - timeutil.DefaultClock -} - -var _ timeutil.Clock = &SimulatedTime{} - -type SimulatedTicker struct { - ticker *time.Ticker - duration time.Duration - sender chan time.Time -} - -func (s *SimulatedTime) Now() time.Time { - return s.now -} - -func (s *SimulatedTime) NewTicker(d time.Duration) *time.Ticker { - // Create a real ticker, but set its duration to an amount that will never fire for real. - // We'll inject times into the channel directly. - replacementChannel := make(chan time.Time) - t := time.NewTicker(1000 * time.Hour) - t.C = replacementChannel - s.tickerBarrier <- &SimulatedTicker{t, d, replacementChannel} - return t -} - -func (s *SimulatedTime) waitForTicker(t *testing.T) *SimulatedTicker { - t.Helper() - // System under test should create a ticker within 100ms, - // wait for it to show up or else fail the test. - timeout := time.After(100 * time.Millisecond) - select { - case <-timeout: - t.Fatal("Timeout waiting for ticker creation.") - return nil - case t := <-s.tickerBarrier: - return t - } -} - -func (s *SimulatedTime) allowTickers(n int) { - s.tickerBarrier = make(chan *SimulatedTicker, n) -} - -func startSimulatedTime() *SimulatedTime { - s := &SimulatedTime{ - now: time.Now(), - tickerBarrier: make(chan *SimulatedTicker, 1), - } - return s -} - -type SimulatedCollector struct { - numCalls uint32 - callBarrier chan uint32 -} - -func newSimulatedCollector() *SimulatedCollector { - return &SimulatedCollector{ - numCalls: 0, - callBarrier: make(chan uint32, 1), - } -} - -func (s *SimulatedCollector) waitForCall(t *testing.T) { - timeout := time.After(100 * time.Millisecond) - select { - case <-timeout: - t.Fatal("Timeout waiting for call to collection function.") - return - case <-s.callBarrier: - return - } -} - -func (s *SimulatedCollector) EmptyCollectionFunction(ctx context.Context) ([]GaugeLabelValues, error) { - atomic.AddUint32(&s.numCalls, 1) - s.callBarrier <- s.numCalls - return []GaugeLabelValues{}, nil -} - -func TestGauge_Creation(t *testing.T) { - c := newSimulatedCollector() - sink := BlackholeSink() - sink.GaugeInterval = 33 * time.Minute - - key := []string{"example", "count"} - labels := []Label{{"gauge", "test"}} - - p, err := sink.NewGaugeCollectionProcess( - key, - labels, - c.EmptyCollectionFunction, - log.Default(), - ) - if err != nil { - t.Fatalf("Error creating collection process: %v", err) - } - - if _, ok := p.clock.(timeutil.DefaultClock); !ok { - t.Error("Default clock not installed.") - } - - if !reflect.DeepEqual(p.key, key) { - t.Errorf("Key not initialized, got %v but expected %v", - p.key, key) - } - - if !reflect.DeepEqual(p.labels, labels) { - t.Errorf("Labels not initialized, got %v but expected %v", - p.key, key) - } - - if p.originalInterval != sink.GaugeInterval || p.currentInterval != sink.GaugeInterval { - t.Errorf("Intervals not initialized, got %v and %v, expected %v", - p.originalInterval, p.currentInterval, sink.GaugeInterval) - } -} - -func TestGauge_StartDelay(t *testing.T) { - // Work through an entire startup sequence, up to collecting - // the first batch of gauges. - s := startSimulatedTime() - c := newSimulatedCollector() - - sink := BlackholeSink() - sink.GaugeInterval = 2 * time.Hour - - p, err := newGaugeCollectionProcessWithClock( - []string{"example", "count"}, - []Label{{"gauge", "test"}}, - c.EmptyCollectionFunction, - sink, - sink.GaugeInterval, - sink.MaxGaugeCardinality, - log.Default(), - s, - ) - if err != nil { - t.Fatalf("Error creating collection process: %v", err) - } - go p.Run() - - delayTicker := s.waitForTicker(t) - if delayTicker.duration > sink.GaugeInterval { - t.Errorf("Delayed start %v is more than interval %v.", - delayTicker.duration, sink.GaugeInterval) - } - if c.numCalls > 0 { - t.Error("Collection function has been called") - } - - // Signal the end of delay, then another ticker should start - delayTicker.sender <- time.Now() - - intervalTicker := s.waitForTicker(t) - if intervalTicker.duration != sink.GaugeInterval { - t.Errorf("Ticker duration is %v, expected %v", - intervalTicker.duration, sink.GaugeInterval) - } - if c.numCalls > 0 { - t.Error("Collection function has been called") - } - - // Time's up, ensure the collection function is executed. - intervalTicker.sender <- time.Now() - c.waitForCall(t) - if c.numCalls != 1 { - t.Errorf("Collection function called %v times, expected %v.", c.numCalls, 1) - } - - p.Stop() -} - -func waitForStopped(t *testing.T, p *GaugeCollectionProcess) { - t.Helper() - timeout := time.After(100 * time.Millisecond) - select { - case <-timeout: - t.Fatal("Timeout waiting for process to stop.") - case <-p.stopped: - return - } -} - -func TestGauge_StoppedDuringInitialDelay(t *testing.T) { - // Stop the process before it gets into its main loop - s := startSimulatedTime() - c := newSimulatedCollector() - - sink := BlackholeSink() - sink.GaugeInterval = 2 * time.Hour - - p, err := newGaugeCollectionProcessWithClock( - []string{"example", "count"}, - []Label{{"gauge", "test"}}, - c.EmptyCollectionFunction, - sink, - sink.GaugeInterval, - sink.MaxGaugeCardinality, - log.Default(), - s, - ) - if err != nil { - t.Fatalf("Error creating collection process: %v", err) - } - go p.Run() - - // Stop during the initial delay, check that goroutine exits - s.waitForTicker(t) - p.Stop() - waitForStopped(t, p) -} - -func TestGauge_StoppedAfterInitialDelay(t *testing.T) { - // Stop the process during its main loop - s := startSimulatedTime() - c := newSimulatedCollector() - - sink := BlackholeSink() - sink.GaugeInterval = 2 * time.Hour - - p, err := newGaugeCollectionProcessWithClock( - []string{"example", "count"}, - []Label{{"gauge", "test"}}, - c.EmptyCollectionFunction, - sink, - sink.GaugeInterval, - sink.MaxGaugeCardinality, - log.Default(), - s, - ) - if err != nil { - t.Fatalf("Error creating collection process: %v", err) - } - go p.Run() - - // Get through initial delay, wait for interval ticker - delayTicker := s.waitForTicker(t) - delayTicker.sender <- time.Now() - - s.waitForTicker(t) - p.Stop() - waitForStopped(t, p) -} - -func TestGauge_Backoff(t *testing.T) { - s := startSimulatedTime() - s.allowTickers(100) - - c := newSimulatedCollector() - - sink := BlackholeSink() - sink.GaugeInterval = 2 * time.Hour - - threshold := sink.GaugeInterval / 100 - f := func(ctx context.Context) ([]GaugeLabelValues, error) { - atomic.AddUint32(&c.numCalls, 1) - // Move time forward by more than 1% of the gauge interval - s.now = s.now.Add(threshold).Add(time.Second) - c.callBarrier <- c.numCalls - return []GaugeLabelValues{}, nil - } - - p, err := newGaugeCollectionProcessWithClock( - []string{"example", "count"}, - []Label{{"gauge", "test"}}, - f, - sink, - sink.GaugeInterval, - sink.MaxGaugeCardinality, - log.Default(), - s, - ) - if err != nil { - t.Fatalf("Error creating collection process: %v", err) - } - // Do not run, we'll just going to call an internal function. - p.collectAndFilterGauges() - - if p.currentInterval != 2*p.originalInterval { - t.Errorf("Current interval is %v, should be 2x%v.", - p.currentInterval, - p.originalInterval) - } -} - -func TestGauge_RestartTimer(t *testing.T) { - s := startSimulatedTime() - c := newSimulatedCollector() - sink := BlackholeSink() - sink.GaugeInterval = 2 * time.Hour - - p, err := newGaugeCollectionProcessWithClock( - []string{"example", "count"}, - []Label{{"gauge", "test"}}, - c.EmptyCollectionFunction, - sink, - sink.GaugeInterval, - sink.MaxGaugeCardinality, - log.Default(), - s, - ) - if err != nil { - t.Fatalf("Error creating collection process: %v", err) - } - - p.resetTicker() - t1 := s.waitForTicker(t) - if t1.duration != p.currentInterval { - t.Fatalf("Bad ticker interval, got %v expected %v", - t1.duration, p.currentInterval) - } - - p.currentInterval = 4 * p.originalInterval - p.resetTicker() - t2 := s.waitForTicker(t) - if t2.duration != p.currentInterval { - t.Fatalf("Bad ticker interval, got %v expected %v", - t1.duration, p.currentInterval) - } -} - -func waitForDone(t *testing.T, - tick chan<- time.Time, - done <-chan struct{}, -) int { - t.Helper() - timeout := time.After(500 * time.Millisecond) - - numTicks := 0 - for { - select { - case <-timeout: - t.Fatal("Timeout waiting for metrics to be sent.") - case tick <- time.Now(): - numTicks += 1 - case <-done: - return numTicks - } - } -} - -func makeLabels(numLabels int) []GaugeLabelValues { - values := make([]GaugeLabelValues, numLabels) - for i := range values { - values[i].Labels = []Label{ - {"test", "true"}, - {"which", fmt.Sprintf("%v", i)}, - } - values[i].Value = float32(i + 1) - } - return values -} - -func TestGauge_InterruptedStreaming(t *testing.T) { - s := startSimulatedTime() - // Long bucket time == low chance of crossing interval - inmemSink := metrics.NewInmemSink( - 1000000*time.Hour, - 2000000*time.Hour) - - sink := NewClusterMetricSink("test", inmemSink) - sink.MaxGaugeCardinality = 500 - sink.GaugeInterval = 2 * time.Hour - - p, err := newGaugeCollectionProcessWithClock( - []string{"example", "count"}, - []Label{{"gauge", "test"}}, - nil, // shouldn't be called - sink, - sink.GaugeInterval, - sink.MaxGaugeCardinality, - log.Default(), - s, - ) - if err != nil { - t.Fatalf("Error creating collection process: %v", err) - } - - // We'll queue up at least two batches; only one will be sent - // unless we give a ticker. - values := makeLabels(75) - done := make(chan struct{}) - go func() { - p.streamGaugesToSink(values) - close(done) - }() - - p.Stop() - // a nil channel is never writeable - waitForDone(t, nil, done) - - // If we start close to the end of an interval, metrics will - // be split across two buckets. - intervals := inmemSink.Data() - if len(intervals) > 1 { - t.Skip("Detected interval crossing.") - } - - if len(intervals[0].Gauges) == len(values) { - t.Errorf("Found %v gauges, expected fewer.", - len(intervals[0].Gauges)) - } -} - -// helper function to create a closure that's a GaugeCollector. -func (c *SimulatedCollector) makeFunctionForValues( - values []GaugeLabelValues, - s *SimulatedTime, - advanceTime time.Duration, -) GaugeCollector { - // A function that returns a static list - return func(ctx context.Context) ([]GaugeLabelValues, error) { - atomic.AddUint32(&c.numCalls, 1) - // TODO: this seems like a data race? - s.now = s.now.Add(advanceTime) - c.callBarrier <- c.numCalls - return values, nil - } -} - -func TestGauge_MaximumMeasurements(t *testing.T) { - s := startSimulatedTime() - c := newSimulatedCollector() - - // Long bucket time == low chance of crossing interval - inmemSink := metrics.NewInmemSink( - 1000000*time.Hour, - 2000000*time.Hour) - - sink := NewClusterMetricSink("test", inmemSink) - sink.MaxGaugeCardinality = 100 - sink.GaugeInterval = 2 * time.Hour - - // Create a report larger than the default limit - excessGauges := 20 - values := makeLabels(sink.MaxGaugeCardinality + excessGauges) - rand.Shuffle(len(values), func(i, j int) { - values[i], values[j] = values[j], values[i] - }) - - // Advance time by 0.5% of duration - advance := time.Duration(int(0.005 * float32(sink.GaugeInterval))) - p, err := newGaugeCollectionProcessWithClock( - []string{"example", "count"}, - []Label{{"gauge", "test"}}, - c.makeFunctionForValues(values, s, advance), - sink, - sink.GaugeInterval, - sink.MaxGaugeCardinality, - log.Default(), - s, - ) - if err != nil { - t.Fatalf("Error creating collection process: %v", err) - } - - // This needs a ticker in order to do its thing, - // so run it in the background and we'll send the ticks - // from here. - done := make(chan struct{}, 1) - go func() { - p.collectAndFilterGauges() - close(done) - }() - - sendTicker := s.waitForTicker(t) - numTicksSent := waitForDone(t, sendTicker.sender, done) - - // 100 items, one delay after each batchSize (25), means that - // 3 ticks are consumed, so 3 or 4 must be sent. - expectedTicks := sink.MaxGaugeCardinality/batchSize - 1 - if numTicksSent < expectedTicks || numTicksSent > expectedTicks+1 { - t.Errorf("Number of ticks = %v, expected %v.", numTicksSent, expectedTicks) - } - - // If we start close to the end of an interval, metrics will - // be split across two buckets. - intervals := inmemSink.Data() - if len(intervals) > 1 { - t.Skip("Detected interval crossing.") - } - - if len(intervals[0].Gauges) != sink.MaxGaugeCardinality { - t.Errorf("Found %v gauges, expected %v.", - len(intervals[0].Gauges), - sink.MaxGaugeCardinality) - } - - minVal := float32(excessGauges) - for _, v := range intervals[0].Gauges { - if v.Value < minVal { - t.Errorf("Gauge %v with value %v should not have been included.", v.Labels, v.Value) - break - } - } -} - -func TestGauge_MeasurementError(t *testing.T) { - s := startSimulatedTime() - c := newSimulatedCollector() - inmemSink := metrics.NewInmemSink( - 1000000*time.Hour, - 2000000*time.Hour) - sink := NewClusterMetricSink("test", inmemSink) - sink.MaxGaugeCardinality = 500 - sink.GaugeInterval = 2 * time.Hour - - // Create a small report so we don't have to deal with batching. - numGauges := 10 - values := make([]GaugeLabelValues, numGauges) - for i := range values { - values[i].Labels = []Label{ - {"test", "true"}, - {"which", fmt.Sprintf("%v", i)}, - } - values[i].Value = float32(i + 1) - } - - f := func(ctx context.Context) ([]GaugeLabelValues, error) { - atomic.AddUint32(&c.numCalls, 1) - c.callBarrier <- c.numCalls - return values, errors.New("test error") - } - - p, err := newGaugeCollectionProcessWithClock( - []string{"example", "count"}, - []Label{{"gauge", "test"}}, - f, - sink, - sink.GaugeInterval, - sink.MaxGaugeCardinality, - log.Default(), - s, - ) - if err != nil { - t.Fatalf("Error creating collection process: %v", err) - } - - p.collectAndFilterGauges() - - // We should see no data in the sink - intervals := inmemSink.Data() - if len(intervals) > 1 { - t.Skip("Detected interval crossing.") - } - - if len(intervals[0].Gauges) != 0 { - t.Errorf("Found %v gauges, expected %v.", - len(intervals[0].Gauges), 0) - } -} diff --git a/helper/metricsutil/metricsutil_test.go b/helper/metricsutil/metricsutil_test.go deleted file mode 100644 index ffe77b56c..000000000 --- a/helper/metricsutil/metricsutil_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package metricsutil - -import ( - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestFormatFromRequest(t *testing.T) { - testCases := []struct { - original *logical.Request - expected string - }{ - { - original: &logical.Request{Headers: map[string][]string{ - "Accept": { - "application/vnd.google.protobuf", - "schema=\"prometheus/telemetry\"", - }, - }}, - expected: "prometheus", - }, - { - original: &logical.Request{Headers: map[string][]string{ - "Accept": { - "schema=\"prometheus\"", - }, - }}, - expected: "", - }, - { - original: &logical.Request{Headers: map[string][]string{ - "Accept": { - "application/openmetrics-text", - }, - }}, - expected: "prometheus", - }, - } - - for _, tCase := range testCases { - if metricsType := FormatFromRequest(tCase.original); metricsType != tCase.expected { - t.Fatalf("expected %s but got %s", tCase.expected, metricsType) - } - } -} diff --git a/helper/metricsutil/wrapped_metrics_test.go b/helper/metricsutil/wrapped_metrics_test.go deleted file mode 100644 index 34c5cdda8..000000000 --- a/helper/metricsutil/wrapped_metrics_test.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package metricsutil - -import ( - "testing" - "time" - - "github.com/armon/go-metrics" -) - -func isLabelPresent(toFind Label, ls []Label) bool { - for _, l := range ls { - if l == toFind { - return true - } - } - return false -} - -// We can use a sink directly, or wrap the top-level -// go-metrics implementation for testing purposes. -func defaultMetrics(sink metrics.MetricSink) *metrics.Metrics { - // No service name - config := metrics.DefaultConfig("") - - // No host name - config.EnableHostname = false - m, _ := metrics.New(config, sink) - return m -} - -func TestClusterLabelPresent(t *testing.T) { - testClusterName := "test-cluster" - - // Use a ridiculously long time to minimize the chance - // that we have to deal with more than one interval. - // InMemSink rounds down to an interval boundary rather than - // starting one at the time of initialization. - inmemSink := metrics.NewInmemSink( - 1000000*time.Hour, - 2000000*time.Hour) - clusterSink := NewClusterMetricSink(testClusterName, defaultMetrics(inmemSink)) - - key1 := []string{"aaa", "bbb"} - key2 := []string{"ccc", "ddd"} - key3 := []string{"eee", "fff"} - labels1 := []Label{{"dim1", "val1"}} - labels2 := []Label{{"dim2", "val2"}} - labels3 := []Label{{"dim3", "val3"}} - clusterLabel := Label{"cluster", testClusterName} - expectedKey1 := "aaa.bbb;dim1=val1;cluster=" + testClusterName - expectedKey2 := "ccc.ddd;dim2=val2;cluster=" + testClusterName - expectedKey3 := "eee.fff;dim3=val3;cluster=" + testClusterName - - clusterSink.SetGaugeWithLabels(key1, 1.0, labels1) - clusterSink.IncrCounterWithLabels(key2, 2.0, labels2) - clusterSink.AddSampleWithLabels(key3, 3.0, labels3) - - intervals := inmemSink.Data() - // If we start very close to the end of an interval, then our metrics might be - // split across two different buckets. We won't write the code to try to handle that. - // 100000-hours = at most once every 4167 days - if len(intervals) > 1 { - t.Skip("Detected interval crossing.") - } - - // Check Gauge - g, ok := intervals[0].Gauges[expectedKey1] - if !ok { - t.Fatal("Key", expectedKey1, "not found in map", intervals[0].Gauges) - } - if g.Value != 1.0 { - t.Error("Gauge value", g.Value, "does not match", 1.0) - } - if !isLabelPresent(labels1[0], g.Labels) { - t.Error("Gauge label", g.Labels, "does not include", labels1) - } - if !isLabelPresent(clusterLabel, g.Labels) { - t.Error("Gauge label", g.Labels, "does not include", clusterLabel) - } - - // Check Counter - c, ok := intervals[0].Counters[expectedKey2] - if !ok { - t.Fatal("Key", expectedKey2, "not found in map", intervals[0].Counters) - } - if c.Sum != 2.0 { - t.Error("Counter value", c.Sum, "does not match", 2.0) - } - if !isLabelPresent(labels2[0], c.Labels) { - t.Error("Counter label", c.Labels, "does not include", labels2) - } - if !isLabelPresent(clusterLabel, c.Labels) { - t.Error("Counter label", c.Labels, "does not include", clusterLabel) - } - - // Check Sample - s, ok := intervals[0].Samples[expectedKey3] - if !ok { - t.Fatal("Key", expectedKey3, "not found in map", intervals[0].Samples) - } - if s.Sum != 3.0 { - t.Error("Sample value", s.Sum, "does not match", 3.0) - } - if !isLabelPresent(labels3[0], s.Labels) { - t.Error("Sample label", s.Labels, "does not include", labels3) - } - if !isLabelPresent(clusterLabel, s.Labels) { - t.Error("Sample label", s.Labels, "does not include", clusterLabel) - } -} diff --git a/helper/monitor/monitor_test.go b/helper/monitor/monitor_test.go deleted file mode 100644 index e281952fe..000000000 --- a/helper/monitor/monitor_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package monitor - -import ( - "encoding/json" - "fmt" - "strings" - "testing" - "time" - - log "github.com/hashicorp/go-hclog" - "github.com/stretchr/testify/require" -) - -func TestMonitor_Start(t *testing.T) { - t.Parallel() - - logger := log.NewInterceptLogger(&log.LoggerOptions{ - Level: log.Error, - }) - - m, _ := NewMonitor(512, logger, &log.LoggerOptions{ - Level: log.Debug, - }) - - logCh := m.Start() - defer m.Stop() - - go func() { - logger.Debug("test log") - time.Sleep(10 * time.Millisecond) - }() - - select { - case l := <-logCh: - require.Contains(t, string(l), "[DEBUG] test log") - return - case <-time.After(5 * time.Second): - t.Fatal("Expected to receive from log channel") - } -} - -func TestMonitor_JSONFormat(t *testing.T) { - t.Parallel() - - logger := log.NewInterceptLogger(&log.LoggerOptions{ - Level: log.Error, - }) - - m, _ := NewMonitor(512, logger, &log.LoggerOptions{ - Level: log.Debug, - JSONFormat: true, - }) - - type jsonlog struct { - Level string `json:"@level"` - Message string `json:"@message"` - TimeStamp string `json:"@timestamp"` - } - jsonLog := &jsonlog{} - - logCh := m.Start() - defer m.Stop() - - go func() { - logger.Debug("test json log") - time.Sleep(10 * time.Millisecond) - }() - - select { - case l := <-logCh: - err := json.Unmarshal(l, jsonLog) - if err != nil { - t.Fatal("Expected JSON log from channel") - } - require.Contains(t, jsonLog.Message, "test json log") - return - case <-time.After(5 * time.Second): - t.Fatal("Expected to receive from log channel") - } -} - -func TestMonitor_Start_Unbuffered(t *testing.T) { - t.Parallel() - - logger := log.NewInterceptLogger(&log.LoggerOptions{ - Level: log.Error, - }) - - _, err := NewMonitor(0, logger, &log.LoggerOptions{ - Level: log.Debug, - }) - - if err == nil { - t.Fatal("expected to get an error, but didn't") - } else { - if !strings.Contains(err.Error(), "greater than zero") { - t.Fatal("expected an error about buf being greater than zero") - } - } -} - -// Ensure number of dropped messages are logged -func TestMonitor_DroppedMessages(t *testing.T) { - t.Parallel() - - logger := log.NewInterceptLogger(&log.LoggerOptions{ - Level: log.Warn, - }) - - m, _ := newMonitor(5, logger, &log.LoggerOptions{ - Level: log.Debug, - }) - m.dropCheckInterval = 5 * time.Millisecond - - logCh := m.Start() - defer m.Stop() - - for i := 0; i <= 100; i++ { - logger.Debug(fmt.Sprintf("test message %d", i)) - } - - passed := make(chan struct{}) - go func() { - for recv := range logCh { - if strings.Contains(string(recv), "Monitor dropped") { - close(passed) - return - } - } - }() - - select { - case <-passed: - case <-time.After(2 * time.Second): - require.Fail(t, "expected to see warn dropped messages") - } -} diff --git a/helper/namespace/namespace_test.go b/helper/namespace/namespace_test.go deleted file mode 100644 index 10ee981b9..000000000 --- a/helper/namespace/namespace_test.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package namespace - -import ( - "testing" -) - -func TestSplitIDFromString(t *testing.T) { - tcases := []struct { - input string - id string - prefix string - }{ - { - "foo", - "", - "foo", - }, - { - "foo.id", - "id", - "foo", - }, - { - "foo.foo.id", - "id", - "foo.foo", - }, - { - "foo.foo/foo.id", - "id", - "foo.foo/foo", - }, - { - "foo.foo/.id", - "id", - "foo.foo/", - }, - { - "foo.foo/foo", - "", - "foo.foo/foo", - }, - { - "foo.foo/f", - "", - "foo.foo/f", - }, - { - "foo.foo/", - "", - "foo.foo/", - }, - { - "b.foo", - "", - "b.foo", - }, - { - "s.foo", - "", - "s.foo", - }, - { - "t.foo", - "foo", - "t", - }, - } - - for _, c := range tcases { - pre, id := SplitIDFromString(c.input) - if pre != c.prefix || id != c.id { - t.Fatalf("bad test case: %s != %s, %s != %s", pre, c.prefix, id, c.id) - } - } -} - -func TestHasParent(t *testing.T) { - // Create ns1 - ns1 := &Namespace{ - ID: "id1", - Path: "ns1/", - } - - // Create ns1/ns2 - ns2 := &Namespace{ - ID: "id2", - Path: "ns1/ns2/", - } - - // Create ns1/ns2/ns3 - ns3 := &Namespace{ - ID: "id3", - Path: "ns1/ns2/ns3/", - } - - // Create ns4 - ns4 := &Namespace{ - ID: "id4", - Path: "ns4/", - } - - // Create ns4/ns5 - ns5 := &Namespace{ - ID: "id5", - Path: "ns4/ns5/", - } - - tests := []struct { - name string - parent *Namespace - ns *Namespace - expected bool - }{ - { - "is root an ancestor of ns1", - RootNamespace, - ns1, - true, - }, - { - "is ns1 an ancestor of ns2", - ns1, - ns2, - true, - }, - { - "is ns2 an ancestor of ns3", - ns2, - ns3, - true, - }, - { - "is ns1 an ancestor of ns3", - ns1, - ns3, - true, - }, - { - "is root an ancestor of ns3", - RootNamespace, - ns3, - true, - }, - { - "is ns4 an ancestor of ns3", - ns4, - ns3, - false, - }, - { - "is ns5 an ancestor of ns3", - ns5, - ns3, - false, - }, - { - "is ns1 an ancestor of ns5", - ns1, - ns5, - false, - }, - } - - for _, test := range tests { - actual := test.ns.HasParent(test.parent) - if actual != test.expected { - t.Fatalf("bad ancestor calculation; name: %q, actual: %t, expected: %t", test.name, actual, test.expected) - } - } -} diff --git a/helper/osutil/fileinfo_test.go b/helper/osutil/fileinfo_test.go deleted file mode 100644 index edf7c50c9..000000000 --- a/helper/osutil/fileinfo_test.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package osutil - -import ( - "io/fs" - "os" - "os/user" - "path/filepath" - "runtime" - "strconv" - "testing" -) - -func TestCheckPathInfo(t *testing.T) { - currentUser, err := user.Current() - if err != nil { - t.Errorf("failed to get details of current process owner. The error is: %v", err) - } - uid, err := strconv.ParseInt(currentUser.Uid, 0, 64) - if err != nil { - t.Errorf("failed to convert uid to int64. The error is: %v", err) - } - uid2, err := strconv.ParseInt(currentUser.Uid+"1", 0, 64) - if err != nil { - t.Errorf("failed to convert uid to int64. The error is: %v", err) - } - - testCases := []struct { - uid int - filepermissions fs.FileMode - permissions int - expectError bool - }{ - { - uid: 0, - filepermissions: 0o700, - permissions: 0, - expectError: false, - }, - { - uid: int(uid2), - filepermissions: 0o700, - permissions: 0, - expectError: true, - }, - { - uid: int(uid), - filepermissions: 0o700, - permissions: 0, - expectError: false, - }, - { - uid: 0, - filepermissions: 0o777, - permissions: 744, - expectError: true, - }, - } - - for _, tc := range testCases { - err := os.Mkdir("testFile", tc.filepermissions) - if err != nil { - t.Fatal(err) - } - info, err := os.Stat("testFile") - if err != nil { - t.Errorf("error stating %q: %v", "testFile", err) - } - if tc.uid != 0 && runtime.GOOS == "windows" && tc.expectError == true { - t.Skip("Skipping test in windows environment as no error will be returned in this case") - } - - err = checkPathInfo(info, "testFile", tc.uid, int(tc.permissions)) - if tc.expectError && err == nil { - t.Errorf("invalid result. expected error") - } - if !tc.expectError && err != nil { - t.Errorf(err.Error()) - } - - err = os.RemoveAll("testFile") - if err != nil { - t.Fatal(err) - } - } -} - -// TestOwnerPermissionsMatchFile creates a file and verifies that the current user of the process is the owner of the -// file -func TestOwnerPermissionsMatchFile(t *testing.T) { - currentUser, err := user.Current() - if err != nil { - t.Fatal("failed to get current user", err) - } - uid, err := strconv.ParseInt(currentUser.Uid, 0, 64) - if err != nil { - t.Fatal("failed to convert uid", err) - } - dir := t.TempDir() - path := filepath.Join(dir, "foo") - f, err := os.Create(path) - if err != nil { - t.Fatal("failed to create test file", err) - } - defer f.Close() - - info, err := os.Stat(path) - if err != nil { - t.Fatal("failed to stat test file", err) - } - - if err := OwnerPermissionsMatchFile(f, int(uid), int(info.Mode())); err != nil { - t.Fatalf("expected no error but got %v", err) - } -} - -// TestOwnerPermissionsMatchFile_OtherUser creates a file using the user that started the current process and verifies -// that a different user is not the owner of the file -func TestOwnerPermissionsMatchFile_OtherUser(t *testing.T) { - currentUser, err := user.Current() - if err != nil { - t.Fatal("failed to get current user", err) - } - uid, err := strconv.ParseInt(currentUser.Uid, 0, 64) - if err != nil { - t.Fatal("failed to convert uid", err) - } - dir := t.TempDir() - path := filepath.Join(dir, "foo") - f, err := os.Create(path) - if err != nil { - t.Fatal("failed to create test file", err) - } - defer f.Close() - - info, err := os.Stat(path) - if err != nil { - t.Fatal("failed to stat test file", err) - } - - if err := OwnerPermissionsMatchFile(f, int(uid)+1, int(info.Mode())); err == nil { - t.Fatalf("expected error but none") - } -} - -// TestOwnerPermissionsMatchFile_Symlink creates a file and a symlink to that file. The test verifies that the current -// user of the process is the owner of the file -func TestOwnerPermissionsMatchFile_Symlink(t *testing.T) { - currentUser, err := user.Current() - if err != nil { - t.Fatal("failed to get current user", err) - } - uid, err := strconv.ParseInt(currentUser.Uid, 0, 64) - if err != nil { - t.Fatal("failed to convert uid", err) - } - dir := t.TempDir() - path := filepath.Join(dir, "foo") - f, err := os.Create(path) - if err != nil { - t.Fatal("failed to create test file", err) - } - defer f.Close() - - symlink := filepath.Join(dir, "symlink") - err = os.Symlink(path, symlink) - if err != nil { - t.Fatal("failed to symlink file", err) - } - symlinkedFile, err := os.Open(symlink) - if err != nil { - t.Fatal("failed to open file", err) - } - info, err := os.Stat(symlink) - if err != nil { - t.Fatal("failed to stat test file", err) - } - if err := OwnerPermissionsMatchFile(symlinkedFile, int(uid), int(info.Mode())); err != nil { - t.Fatalf("expected no error but got %v", err) - } -} diff --git a/helper/osutil/fileinfo_unix_test.go b/helper/osutil/fileinfo_unix_test.go deleted file mode 100644 index 65ed863fe..000000000 --- a/helper/osutil/fileinfo_unix_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -//go:build !windows - -package osutil - -import ( - "os" - "os/user" - "strconv" - "testing" -) - -func TestFileUIDEqual(t *testing.T) { - currentUser, err := user.Current() - if err != nil { - t.Errorf("failed to get details of current process owner. The error is: %v", err) - } - uid, err := strconv.Atoi(currentUser.Uid) - if err != nil { - t.Errorf("failed to convert uid to int. The error is: %v", err) - } - - testCases := []struct { - uid int - expected bool - }{ - { - uid: uid, - expected: true, - }, - { - uid: uid + 1, - expected: false, - }, - } - - for _, tc := range testCases { - err := os.Mkdir("testFile", 0o777) - if err != nil { - t.Fatal(err) - } - info, err := os.Stat("testFile") - if err != nil { - t.Errorf("error stating %q: %v", "testFile", err) - } - - result := FileUIDEqual(info, tc.uid) - if result != tc.expected { - t.Errorf("invalid result. expected %t for uid %v", tc.expected, tc.uid) - } - err = os.RemoveAll("testFile") - if err != nil { - t.Fatal(err) - } - } -} - -func TestFileGIDEqual(t *testing.T) { - currentUser, err := user.Current() - if err != nil { - t.Errorf("failed to get details of current process owner. The error is: %v", err) - } - gid, err := strconv.Atoi(currentUser.Gid) - if err != nil { - t.Errorf("failed to convert gid to int. The error is: %v", err) - } - - testCases := []struct { - gid int - expected bool - }{ - { - gid: gid, - expected: true, - }, - { - gid: gid + 1, - expected: false, - }, - } - - for _, tc := range testCases { - err := os.Mkdir("testFile", 0o777) - if err != nil { - t.Fatal(err) - } - info, err := os.Stat("testFile") - if err != nil { - t.Errorf("error stating %q: %v", "testFile", err) - } - - result := FileGIDEqual(info, tc.gid) - if result != tc.expected { - t.Errorf("invalid result. expected %t for gid %v", tc.expected, tc.gid) - } - err = os.RemoveAll("testFile") - if err != nil { - t.Fatal(err) - } - } -} diff --git a/helper/parseip/parseip_test.go b/helper/parseip/parseip_test.go deleted file mode 100644 index fd8169a55..000000000 --- a/helper/parseip/parseip_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package parseip - -import ( - "testing" -) - -func Test_TrimLeadingZeroes(t *testing.T) { - tests := []struct { - in string - want string - }{ - {"127.0.0.1", "127.0.0.1"}, - {"010.010.20.5", "10.10.20.5"}, - {"1.1.1.010", "1.1.1.10"}, - {"64:ff9b::192.00.002.33", "64:ff9b::192.0.2.33"}, - {"2001:db8:122:344:c0:2:2100::", "2001:db8:122:344:c0:2:2100::"}, - {"2001:db8:122:344::192.0.2.033", "2001:db8:122:344::192.0.2.33"}, - } - for _, tt := range tests { - if got := trimLeadingZeroesIP(tt.in); got != tt.want { - t.Errorf("trimLeadingZeroesIP() = %v, want %v", got, tt.want) - } - } - - for _, tt := range tests { - // Non-CIDR addresses are ignored. - if got := TrimLeadingZeroesCIDR(tt.in); got != tt.in { - t.Errorf("TrimLeadingZeroesCIDR() = %v, want %v", got, tt.in) - } - want := tt.want + "/32" - if got := TrimLeadingZeroesCIDR(tt.in + "/32"); got != want { - t.Errorf("TrimLeadingZeroesCIDR() = %v, want %v", got, want) - } - } -} diff --git a/helper/pgpkeys/flag_test.go b/helper/pgpkeys/flag_test.go deleted file mode 100644 index f8447b61c..000000000 --- a/helper/pgpkeys/flag_test.go +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pgpkeys - -import ( - "bytes" - "encoding/base64" - "encoding/hex" - "flag" - "fmt" - "io/ioutil" - "os" - "reflect" - "strings" - "testing" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" -) - -func TestPubKeyFilesFlag_implements(t *testing.T) { - var raw interface{} - raw = new(PubKeyFilesFlag) - if _, ok := raw.(flag.Value); !ok { - t.Fatalf("PubKeysFilesFlag should be a Value") - } -} - -func TestPubKeyFilesFlagSetBinary(t *testing.T) { - tempDir, err := ioutil.TempDir("", "vault-test") - if err != nil { - t.Fatalf("Error creating temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) - - decoder := base64.StdEncoding - pub1Bytes, err := decoder.DecodeString(pubKey1) - if err != nil { - t.Fatalf("Error decoding bytes for public key 1: %s", err) - } - err = ioutil.WriteFile(tempDir+"/pubkey1", pub1Bytes, 0o755) - if err != nil { - t.Fatalf("Error writing pub key 1 to temp file: %s", err) - } - pub2Bytes, err := decoder.DecodeString(pubKey2) - if err != nil { - t.Fatalf("Error decoding bytes for public key 2: %s", err) - } - err = ioutil.WriteFile(tempDir+"/pubkey2", pub2Bytes, 0o755) - if err != nil { - t.Fatalf("Error writing pub key 2 to temp file: %s", err) - } - pub3Bytes, err := decoder.DecodeString(pubKey3) - if err != nil { - t.Fatalf("Error decoding bytes for public key 3: %s", err) - } - err = ioutil.WriteFile(tempDir+"/pubkey3", pub3Bytes, 0o755) - if err != nil { - t.Fatalf("Error writing pub key 3 to temp file: %s", err) - } - - pkf := new(PubKeyFilesFlag) - err = pkf.Set(tempDir + "/pubkey1,@" + tempDir + "/pubkey2") - if err != nil { - t.Fatalf("err: %s", err) - } - - err = pkf.Set(tempDir + "/pubkey3") - if err == nil { - t.Fatalf("err: should not have been able to set a second value") - } - - expected := []string{strings.ReplaceAll(pubKey1, "\n", ""), strings.ReplaceAll(pubKey2, "\n", "")} - if !reflect.DeepEqual(pkf.String(), fmt.Sprint(expected)) { - t.Fatalf("Bad: %#v", pkf) - } -} - -func TestPubKeyFilesFlagSetB64(t *testing.T) { - tempDir, err := ioutil.TempDir("", "vault-test") - if err != nil { - t.Fatalf("Error creating temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) - - err = ioutil.WriteFile(tempDir+"/pubkey1", []byte(pubKey1), 0o755) - if err != nil { - t.Fatalf("Error writing pub key 1 to temp file: %s", err) - } - err = ioutil.WriteFile(tempDir+"/pubkey2", []byte(pubKey2), 0o755) - if err != nil { - t.Fatalf("Error writing pub key 2 to temp file: %s", err) - } - err = ioutil.WriteFile(tempDir+"/pubkey3", []byte(pubKey3), 0o755) - if err != nil { - t.Fatalf("Error writing pub key 3 to temp file: %s", err) - } - - pkf := new(PubKeyFilesFlag) - err = pkf.Set(tempDir + "/pubkey1,@" + tempDir + "/pubkey2") - if err != nil { - t.Fatalf("err: %s", err) - } - - err = pkf.Set(tempDir + "/pubkey3") - if err == nil { - t.Fatalf("err: should not have been able to set a second value") - } - - expected := []string{pubKey1, pubKey2} - if !reflect.DeepEqual(pkf.String(), fmt.Sprint(expected)) { - t.Fatalf("bad: got %s, expected %s", pkf.String(), fmt.Sprint(expected)) - } -} - -func TestPubKeyFilesFlagSetKeybase(t *testing.T) { - tempDir, err := ioutil.TempDir("", "vault-test") - if err != nil { - t.Fatalf("Error creating temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) - - err = ioutil.WriteFile(tempDir+"/pubkey2", []byte(pubKey2), 0o755) - if err != nil { - t.Fatalf("Error writing pub key 2 to temp file: %s", err) - } - - pkf := new(PubKeyFilesFlag) - err = pkf.Set("keybase:jefferai,@" + tempDir + "/pubkey2" + ",keybase:hashicorp") - if err != nil { - t.Fatalf("err: %s", err) - } - fingerprints := []string{} - for _, pubkey := range []string(*pkf) { - keyBytes, err := base64.StdEncoding.DecodeString(pubkey) - if err != nil { - t.Fatalf("bad: %v", err) - } - pubKeyBuf := bytes.NewBuffer(keyBytes) - reader := packet.NewReader(pubKeyBuf) - entity, err := openpgp.ReadEntity(reader) - if err != nil { - t.Fatalf("bad: %v", err) - } - if entity == nil { - t.Fatalf("nil entity encountered") - } - fingerprints = append(fingerprints, hex.EncodeToString(entity.PrimaryKey.Fingerprint[:])) - } - - exp := []string{ - "0f801f518ec853daff611e836528efcac6caa3db", - "cf3d4694c9f57b28cb4092c2eb832c67eb5e8957", - "c874011f0ab405110d02105534365d9472d7468f", - } - - if !reflect.DeepEqual(fingerprints, exp) { - t.Fatalf("bad: got \n%#v\nexpected\n%#v\n", fingerprints, exp) - } -} - -const pubKey1 = `mQENBFXbjPUBCADjNjCUQwfxKL+RR2GA6pv/1K+zJZ8UWIF9S0lk7cVIEfJiprzzwiMwBS5cD0da -rGin1FHvIWOZxujA7oW0O2TUuatqI3aAYDTfRYurh6iKLC+VS+F7H+/mhfFvKmgr0Y5kDCF1j0T/ -063QZ84IRGucR/X43IY7kAtmxGXH0dYOCzOe5UBX1fTn3mXGe2ImCDWBH7gOViynXmb6XNvXkP0f -sF5St9jhO7mbZU9EFkv9O3t3EaURfHopsCVDOlCkFCw5ArY+DUORHRzoMX0PnkyQb5OzibkChzpg -8hQssKeVGpuskTdz5Q7PtdW71jXd4fFVzoNH8fYwRpziD2xNvi6HABEBAAG0EFZhdWx0IFRlc3Qg -S2V5IDGJATgEEwECACIFAlXbjPUCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEOfLr44B -HbeTo+sH/i7bapIgPnZsJ81hmxPj4W12uvunksGJiC7d4hIHsG7kmJRTJfjECi+AuTGeDwBy84TD -cRaOB6e79fj65Fg6HgSahDUtKJbGxj/lWzmaBuTzlN3CEe8cMwIPqPT2kajJVdOyrvkyuFOdPFOE -A7bdCH0MqgIdM2SdF8t40k/ATfuD2K1ZmumJ508I3gF39jgTnPzD4C8quswrMQ3bzfvKC3klXRlB -C0yoArn+0QA3cf2B9T4zJ2qnvgotVbeK/b1OJRNj6Poeo+SsWNc/A5mw7lGScnDgL3yfwCm1gQXa -QKfOt5x+7GqhWDw10q+bJpJlI10FfzAnhMF9etSqSeURBRW5AQ0EVduM9QEIAL53hJ5bZJ7oEDCn -aY+SCzt9QsAfnFTAnZJQrvkvusJzrTQ088eUQmAjvxkfRqnv981fFwGnh2+I1Ktm698UAZS9Jt8y -jak9wWUICKQO5QUt5k8cHwldQXNXVXFa+TpQWQR5yW1a9okjh5o/3d4cBt1yZPUJJyLKY43Wvptb -6EuEsScO2DnRkh5wSMDQ7dTooddJCmaq3LTjOleRFQbu9ij386Do6jzK69mJU56TfdcydkxkWF5N -ZLGnED3lq+hQNbe+8UI5tD2oP/3r5tXKgMy1R/XPvR/zbfwvx4FAKFOP01awLq4P3d/2xOkMu4Lu -9p315E87DOleYwxk+FoTqXEAEQEAAYkCPgQYAQIACQUCVduM9QIbLgEpCRDny6+OAR23k8BdIAQZ -AQIABgUCVduM9QAKCRAID0JGyHtSGmqYB/4m4rJbbWa7dBJ8VqRU7ZKnNRDR9CVhEGipBmpDGRYu -lEimOPzLUX/ZXZmTZzgemeXLBaJJlWnopVUWuAsyjQuZAfdd8nHkGRHG0/DGum0l4sKTta3OPGHN -C1z1dAcQ1RCr9bTD3PxjLBczdGqhzw71trkQRBRdtPiUchltPMIyjUHqVJ0xmg0hPqFic0fICsr0 -YwKoz3h9+QEcZHvsjSZjgydKvfLYcm+4DDMCCqcHuJrbXJKUWmJcXR0y/+HQONGrGJ5xWdO+6eJi -oPn2jVMnXCm4EKc7fcLFrz/LKmJ8seXhxjM3EdFtylBGCrx3xdK0f+JDNQaC/rhUb5V2XuX6VwoH -/AtY+XsKVYRfNIupLOUcf/srsm3IXT4SXWVomOc9hjGQiJ3rraIbADsc+6bCAr4XNZS7moViAAcI -PXFv3m3WfUlnG/om78UjQqyVACRZqqAGmuPq+TSkRUCpt9h+A39LQWkojHqyob3cyLgy6z9Q557O -9uK3lQozbw2gH9zC0RqnePl+rsWIUU/ga16fH6pWc1uJiEBt8UZGypQ/E56/343epmYAe0a87sHx -8iDV+dNtDVKfPRENiLOOc19MmS+phmUyrbHqI91c0pmysYcJZCD3a502X1gpjFbPZcRtiTmGnUKd -OIu60YPNE4+h7u2CfYyFPu3AlUaGNMBlvy6PEpU=` - -const pubKey2 = `mQENBFXbkJEBCADKb1ZvlT14XrJa2rTOe5924LQr2PTZlRv+651TXy33yEhelZ+V4sMrELN8fKEG -Zy1kNixmbq3MCF/671k3LigHA7VrOaH9iiQgr6IIq2MeIkUYKZ27C992vQkYLjbYUG8+zl5h69S4 -0Ixm0yL0M54XOJ0gm+maEK1ZESKTUlDNkIS7l0jLZSYwfUeGXSEt6FWs8OgbyRTaHw4PDHrDEE9e -Q67K6IZ3YMhPOL4fVk4Jwrp5R/RwiklT+lNozWEyFVwPFH4MeQMs9nMbt+fWlTzEA7tI4acI9yDk -Cm1yN2R9rmY0UjODRiJw6z6sLV2T+Pf32n3MNSUOYczOjZa4VBwjABEBAAG0EFZhdWx0IFRlc3Qg -S2V5IDKJATgEEwECACIFAlXbkJECGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEOuDLGfr -XolXqz4H/28IuoRxGKoJ064YHjPkkpoddW6zdzzNfHipZnNfEUiTEls4qF1IB81M2xqfiXIFRIdO -2kaLkRPFhO0hRxbtI6VuZYLgG3QCaXhxW6GyFa5zKABqhb5ojexdnAYRswaHV201ZCclj9rnJN1P -Ag0Rz6MdX/w1euEWktQxWKo42oZKyx8oT9p6lrv5KRmGkdrg8K8ARmRILjmwuBAgJM0eXBZHNGWX -elk4YmOgnAAcZA6ZAo1G+8Pg6pKKP61ETewuCg3/u7N0vDttB+ZXqF88W9jAYlvdgbTtajNF5IDY -DjTzWfeCaIB18F9gOzXq15SwWeDDI+CU9Nmq358IzXlxk4e5AQ0EVduQkQEIAOjZV5tbpfIh5Qef -pIp2dpGMVfpgPj4RNc15CyFnb8y6dhCrdybkY9GveXJe4F3GNYnSfB42cgxrfhizX3LakmZQ/SAg -+YO5KxfCIN7Q9LPNeTgPsZZT6h8lVuXUxOFKXfRaR3/tGF5xE3e5CoZRsHV/c92h3t1LdJNOnC5m -UKIPO4zDxiw/C2T2q3rP1kmIMaOH724kEH5A+xcp1cBHyt0tdHtIWuQv6joTJzujqViRhlCwQYzQ -SKpSBxwhBsorPvyinZI/ZXA4XXZc5RoMqV9rikedrb1rENO8JOuPu6tMS+znFu67skq2gFFZwCQW -IjdHm+2ukE+PE580WAWudyMAEQEAAYkCPgQYAQIACQUCVduQkQIbLgEpCRDrgyxn616JV8BdIAQZ -AQIABgUCVduQkQAKCRArYtevdF38xtzgB/4zVzozBpVOnagRkA7FDsHo36xX60Lik+ew0m28ueDD -hnV3bXQsCvn/6wiCVWqLOTDeYCPlyTTpEMyk8zwdCICW6MgSkVHWcEDOrRqIrqm86rirjTGjJSgQ -e3l4CqJvkn6jybShYoBk1OZZV6vVv9hPTXXv9E6dLKoEW5YZBrrF+VC0w1iOIvaAQ+QXph20eV4K -BIrp/bhG6PdnigKxuBZ79cdqDnXIzT9UiIa6LYpR0rbeg+7BmuZTTPS8t+41hIiKS+UZFdKa67eY -ENtyOmEMWOFCLLRJGxkleukchiMJ70rknloZXsvJIweXBzSZ6m7mJQBgaig/L/dXyjv6+j2pNB4H -/1trYUtJjXQKHmqlgCmpCkHt3g7JoxWvglnDNmE6q3hIWuVIYQpnzZy1g05+X9Egwc1WVpBB02H7 -PkUZTfpaP/L6DLneMmSKPhZE3I+lPIPjwrxqh6xy5uQezcWkJTNKvPWF4FJzrVvx7XTPjfGvOB0U -PEnjvtZTp5yOhTeZK7DgIEtb/Wcrqs+iRArQKboM930ORSZhwvGK3F9V/gMDpIrvge5vDFsTEYQd -w/2epIewH0L/FUb/6jBRcVEpGo9Ayg+Jnhq14GOGcd1y9oMZ48kYVLVBTA9tQ+82WE8Bch7uFPj4 -MFOMVRn1dc3qdXlg3mimA+iK7tABQfG0RJ9YzWs=` - -const pubKey3 = `mQENBFXbkiMBCACiHW4/VI2JkfvSEINddS7vE6wEu5e1leNQDaLUh6PrATQZS2a4Q6kRE6WlJumj -6wCeN753Cm93UGQl2Bi3USIEeArIZnPTcocrckOVXxtoLBNKXgqKvEsDXgfw8A+doSfXoDm/3Js4 -Wy3WsYKNR9LaPuJZHnpjsFAJhvRVyhH4UFD+1RTSSefq1mozPfDdMoZeZNEpfhwt3DuTJs7RqcTH -CgR2CqhEHnOOE5jJUljHKYLCglE2+8dth1bZlQi4xly/VHZzP3Bn7wKeolK/ROP6VZz/e0xq/BKy -resmxvlBWZ1zWwqGIrV9b0uwYvGrh2hOd5C5+5oGaA2MGcjxwaLBABEBAAG0EFZhdWx0IFRlc3Qg -S2V5IDOJATgEEwECACIFAlXbkiMCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPR5S1b8 -LcbdWjEH/2mhqC9a0Vk1IzOgxEoVxYVqVdvaxI0nTZOTfmcFYn4HQlQ+SLEoyNWe5jtkhx4k5uHi -pxwKHzOv02YM14NWC6bvKw2CQETLDPG4Cv8YMUmpho5tnMDdttIzp8HjyJRtHazU1uTes2/yuqh6 -LHCejVJI0uST3RibquwdG3QjPP8Umxu+YC9+FOW2Kit/AQ8JluFDJdq3/wSX8VfYZrGdgmreE7KY -MolhCkzGSPj7oFygw8LqKoJvt9tCuBKhZMBuMv1sB5CoJIWdPoqOZc4U7L1XdqfKvFZR/RhuXgN1 -lkI9MqrnLDpikL3Lk+ctLxWOjUCW8roqKoHZYBF7XPqdAfm5AQ0EVduSIwEIAOPcjd4QgbLlqIk3 -s6BPRRyVzglTgUdf+I0rUDybaDJfJobZd8U6e4hkPvRoQ8tJefnz/qnD/63watAbJYcVTme40I3V -KDOmVGcyaDxiKP1disKqcEJd7XQiI72oAiXmEH0y+5UwnOMks/lwaAGDMGVRjHEXI6fiRPFsfTr8 -7qvMJ3pW1OiOXVSezuBNTlmyJC7srQ1/nwxL337ev6D1zQZd3JuhcxLkHrUELLNwzhvcZ70vg645 -jAmz8EdmvvoqEPPoHqKgP5AeHACOsTm953KHhgx3NYuGPU/RoIvugKt4Iq5nw7TWFTjPHGVF3GTQ -ry5CZ/AzXiL57hVEhDvmuT8AEQEAAYkCPgQYAQIACQUCVduSIwIbLgEpCRD0eUtW/C3G3cBdIAQZ -AQIABgUCVduSIwAKCRAFI/9Nx3K5IPOFCACsZ/Z4s2LcEoA51TW+T5w+YevlIuq+332JtqNIpuGI -WpGxUxyDyPT0YQWr0SObBORYNr7RP8d/I2rbaFKyaDaKvRofYr+TwXy92phBo7pdEUamBpfrm/sr -+2BgAB2x3HWXp+IMdeVVhqQe8t4cnFm3c1fIdxADyiJuV5ge2Ml5gK5yNwqCQPh7U2RqC+lmVlMJ -GvWInIRn2mf6A7phDYNZfOz6dkar4yyh5r9rRgrZw88r/yIlrq/c6KRUIgnPMrFYEauggOceZ827 -+jKkbKWFEuHtiCxW7kRAN25UfnGsPaF+NSGM2q1vCG4HiFydx6lMoXM0Shf8+ZwyrV/5BzAqpWwI -AJ37tEwC58Fboynly6OGOzgPS0xKnzkXMOtquTo0qEH/1bEUsBknn795BmZOTf4oBC5blN6qRv7c -GSP00i+sxql1NhTjJcjJtfOPUzgfW+Af/+HR648z4c7c6MCjDFKnk8ZkoGLRU7ISjenkNFzvu2bj -lxJkil0uJDlLPbbX80ojzV1GS9g+ZxVPR+68N1QLl2FU6zsfg34upmLLHG8VG4vExzgyNkOwfTYv -dgyRNTjnuPue6H12fZZ9uCNeG52v7lR3eoQcCxBOniwgipB8UJ52RWXblwxzCtGtDi/EWB3zLTUn -puKcgucA0LotbihSMxhDylaARfVO1QV6csabM/g=` diff --git a/helper/pgpkeys/keybase_test.go b/helper/pgpkeys/keybase_test.go deleted file mode 100644 index 2c8c229cc..000000000 --- a/helper/pgpkeys/keybase_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pgpkeys - -import ( - "bytes" - "encoding/base64" - "encoding/hex" - "reflect" - "testing" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" -) - -func TestFetchKeybasePubkeys(t *testing.T) { - testset := []string{"keybase:jefferai", "keybase:hashicorp"} - ret, err := FetchKeybasePubkeys(testset) - if err != nil { - t.Fatalf("bad: %v", err) - } - - fingerprints := []string{} - for _, user := range testset { - data, err := base64.StdEncoding.DecodeString(ret[user]) - if err != nil { - t.Fatalf("error decoding key for user %s: %v", user, err) - } - entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(data))) - if err != nil { - t.Fatalf("error parsing key for user %s: %v", user, err) - } - fingerprints = append(fingerprints, hex.EncodeToString(entity.PrimaryKey.Fingerprint[:])) - } - - exp := []string{ - "0f801f518ec853daff611e836528efcac6caa3db", - "c874011f0ab405110d02105534365d9472d7468f", - } - - if !reflect.DeepEqual(fingerprints, exp) { - t.Fatalf("fingerprints do not match; expected \n%#v\ngot\n%#v\n", exp, fingerprints) - } -} diff --git a/helper/pgpkeys/test_keys.go b/helper/pgpkeys/test_keys.go deleted file mode 100644 index cccda6e25..000000000 --- a/helper/pgpkeys/test_keys.go +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package pgpkeys - -const ( - TestPrivKey1 = `lQOYBFXbjPUBCADjNjCUQwfxKL+RR2GA6pv/1K+zJZ8UWIF9S0lk7cVIEfJiprzzwiMwBS5cD0da -rGin1FHvIWOZxujA7oW0O2TUuatqI3aAYDTfRYurh6iKLC+VS+F7H+/mhfFvKmgr0Y5kDCF1j0T/ -063QZ84IRGucR/X43IY7kAtmxGXH0dYOCzOe5UBX1fTn3mXGe2ImCDWBH7gOViynXmb6XNvXkP0f -sF5St9jhO7mbZU9EFkv9O3t3EaURfHopsCVDOlCkFCw5ArY+DUORHRzoMX0PnkyQb5OzibkChzpg -8hQssKeVGpuskTdz5Q7PtdW71jXd4fFVzoNH8fYwRpziD2xNvi6HABEBAAEAB/wL+KX0mdeISEpX -oDgt766Key1Kthe8nbEs5dOXIsP7OR7ZPcnE2hy6gftgVFnBGEZnWVN70vmJd6Z5y9d1mI+GecXj -UL0EpI0EmohyYDJsHUnght/5ecRNFA+VeNmGPYNQGCeHJyZOiFunGGENpHU7BbubAht8delz37Mx -JQgvMyR6AKvg8HKBoQeqV1uMWNJE/vKwV/z1dh1sjK/GFxu05Qaq0GTfAjVLuFOyJTS95yq6gblD -jUdbHLp7tBeqIKo9voWCJF5mGOlq3973vVoWETy9b0YYPCE/M7fXmK9dJITHqkROLMW6TgcFeIw4 -yL5KOBCHk+QGPSvyQN7R7Fd5BADwuT1HZmvg7Y9GjarKXDjxdNemUiHtba2rUzfH6uNmKNQvwQek -nma5palNUJ4/dz1aPB21FUBXJF5yWwXEdApl+lIDU0J5m4UD26rqEVRq9Kx3GsX+yfcwObkrSzW6 -kmnQSB5KI0fIuegMTM+Jxo3pB/mIRwDTMmk+vfzIGyW+7QQA8aFwFLMdKdfLgSGbl5Z6etmOAVQ2 -Oe2ebegU9z/ewi/Rdt2s9yQiAdGVM8+q15Saz8a+kyS/l1CjNPzr3VpYx1OdZ3gb7i2xoy9GdMYR -ZpTq3TuST95kx/9DqA97JrP23G47U0vwF/cg8ixCYF8Fz5dG4DEsxgMwKqhGdW58wMMD/iytkfMk -Vk6Z958Rpy7lhlC6L3zpO38767bSeZ8gRRi/NMFVOSGYepKFarnfxcTiNa+EoSVA6hUo1N64nALE -sJBpyOoTfKIpz7WwTF1+WogkiYrfM6lHon1+3qlziAcRW0IohM3g2C1i3GWdON4Cl8/PDO3R0E52 -N6iG/ctNNeMiPe60EFZhdWx0IFRlc3QgS2V5IDGJATgEEwECACIFAlXbjPUCGy8GCwkIBwMCBhUI -AgkKCwQWAgMBAh4BAheAAAoJEOfLr44BHbeTo+sH/i7bapIgPnZsJ81hmxPj4W12uvunksGJiC7d -4hIHsG7kmJRTJfjECi+AuTGeDwBy84TDcRaOB6e79fj65Fg6HgSahDUtKJbGxj/lWzmaBuTzlN3C -Ee8cMwIPqPT2kajJVdOyrvkyuFOdPFOEA7bdCH0MqgIdM2SdF8t40k/ATfuD2K1ZmumJ508I3gF3 -9jgTnPzD4C8quswrMQ3bzfvKC3klXRlBC0yoArn+0QA3cf2B9T4zJ2qnvgotVbeK/b1OJRNj6Poe -o+SsWNc/A5mw7lGScnDgL3yfwCm1gQXaQKfOt5x+7GqhWDw10q+bJpJlI10FfzAnhMF9etSqSeUR -BRWdA5gEVduM9QEIAL53hJ5bZJ7oEDCnaY+SCzt9QsAfnFTAnZJQrvkvusJzrTQ088eUQmAjvxkf -Rqnv981fFwGnh2+I1Ktm698UAZS9Jt8yjak9wWUICKQO5QUt5k8cHwldQXNXVXFa+TpQWQR5yW1a -9okjh5o/3d4cBt1yZPUJJyLKY43Wvptb6EuEsScO2DnRkh5wSMDQ7dTooddJCmaq3LTjOleRFQbu -9ij386Do6jzK69mJU56TfdcydkxkWF5NZLGnED3lq+hQNbe+8UI5tD2oP/3r5tXKgMy1R/XPvR/z -bfwvx4FAKFOP01awLq4P3d/2xOkMu4Lu9p315E87DOleYwxk+FoTqXEAEQEAAQAH+wVyQXaNwnjQ -xfW+M8SJNo0C7e+0d7HsuBTA/d/eP4bj6+X8RaRFVwiMvSAoxsqBNCLJP00qzzKfRQWJseD1H35z -UjM7rNVUEL2k1yppyp61S0qj0TdhVUfJDYZqRYonVgRMvzfDTB1ryKrefKenQYL/jGd9VYMnKmWZ -6GVk4WWXXx61iOt2HNcmSXKetMM1Mg67woPZkA3fJaXZ+zW0zMu4lTSB7yl3+vLGIFYILkCFnREr -drQ+pmIMwozUAt+pBq8dylnkHh6g/FtRfWmLIMDqM1NlyuHRp3dyLDFdTA93osLG0QJblfX54W34 -byX7a4HASelGi3nPjjOAsTFDkuEEANV2viaWk1CV4ryDrXGmy4Xo32Md+laGPRcVfbJ0mjZjhQsO -gWC1tjMs1qZMPhcrKIBCjjdAcAIrGV9h3CXc0uGuez4XxLO+TPBKaS0B8rKhnKph1YZuf+HrOhzS -astDnOjNIT+qucCL/qSbdYpj9of3yY61S59WphPOBjoVM3BFBADka6ZCk81gx8jA2E1e9UqQDmdM -FZaVA1E7++kqVSFRDJGnq+5GrBTwCJ+sevi+Rvf8Nx4AXvpCdtMBPX9RogsUFcR0pMrKBrgRo/Vg -EpuodY2Ef1VtqXR24OxtRf1UwvHKydIsU05rzMAy5uGgQvTzRTXxZFLGUY31wjWqmo9VPQP+PnwA -K83EV2kk2bsXwZ9MXg05iXqGQYR4bEc/12v04BtaNaDS53hBDO4JIa3Bnz+5oUoYhb8FgezUKA9I -n6RdKTTP1BLAu8titeozpNF07V++dPiSE2wrIVsaNHL1pUwW0ql50titVwe+EglWiCKPtJBcCPUA -3oepSPchiDjPqrNCYIkCPgQYAQIACQUCVduM9QIbLgEpCRDny6+OAR23k8BdIAQZAQIABgUCVduM -9QAKCRAID0JGyHtSGmqYB/4m4rJbbWa7dBJ8VqRU7ZKnNRDR9CVhEGipBmpDGRYulEimOPzLUX/Z -XZmTZzgemeXLBaJJlWnopVUWuAsyjQuZAfdd8nHkGRHG0/DGum0l4sKTta3OPGHNC1z1dAcQ1RCr -9bTD3PxjLBczdGqhzw71trkQRBRdtPiUchltPMIyjUHqVJ0xmg0hPqFic0fICsr0YwKoz3h9+QEc -ZHvsjSZjgydKvfLYcm+4DDMCCqcHuJrbXJKUWmJcXR0y/+HQONGrGJ5xWdO+6eJioPn2jVMnXCm4 -EKc7fcLFrz/LKmJ8seXhxjM3EdFtylBGCrx3xdK0f+JDNQaC/rhUb5V2XuX6VwoH/AtY+XsKVYRf -NIupLOUcf/srsm3IXT4SXWVomOc9hjGQiJ3rraIbADsc+6bCAr4XNZS7moViAAcIPXFv3m3WfUln -G/om78UjQqyVACRZqqAGmuPq+TSkRUCpt9h+A39LQWkojHqyob3cyLgy6z9Q557O9uK3lQozbw2g -H9zC0RqnePl+rsWIUU/ga16fH6pWc1uJiEBt8UZGypQ/E56/343epmYAe0a87sHx8iDV+dNtDVKf -PRENiLOOc19MmS+phmUyrbHqI91c0pmysYcJZCD3a502X1gpjFbPZcRtiTmGnUKdOIu60YPNE4+h -7u2CfYyFPu3AlUaGNMBlvy6PEpU=` - - TestPrivKey2 = `lQOYBFXbkJEBCADKb1ZvlT14XrJa2rTOe5924LQr2PTZlRv+651TXy33yEhelZ+V4sMrELN8fKEG -Zy1kNixmbq3MCF/671k3LigHA7VrOaH9iiQgr6IIq2MeIkUYKZ27C992vQkYLjbYUG8+zl5h69S4 -0Ixm0yL0M54XOJ0gm+maEK1ZESKTUlDNkIS7l0jLZSYwfUeGXSEt6FWs8OgbyRTaHw4PDHrDEE9e -Q67K6IZ3YMhPOL4fVk4Jwrp5R/RwiklT+lNozWEyFVwPFH4MeQMs9nMbt+fWlTzEA7tI4acI9yDk -Cm1yN2R9rmY0UjODRiJw6z6sLV2T+Pf32n3MNSUOYczOjZa4VBwjABEBAAEAB/oCBqTIsxlUgLtz -HRpWW5MJ+93xvmVV0JHhRK/ygKghq+zpC6S+cn7dwrEj1JTPh+17lyemYQK+RMeiBEduoWNKuHUd -WX353w2411rrc/VuGTglzhd8Ir2BdJlPesCzw4JQnrWqcBqN52W+iwhnE7PWVhnvItWnx6APK5Se -q7dzFWy8Z8tNIHm0pBQbeyo6x2rHHSWkr2fs7V02qFQhii1ayFRMcgdOWSNX6CaZJuYhk/DyjApN -9pVhi3P1pNMpFeV0Pt8Gl1f/9o6/HpAYYEt/6vtVRhFUGgtNi95oc0oyzIJxliRvd6+Z236osigQ -QEBwj1ImRK8TKyWPlykiJWc5BADfldgOCA55o3Qz/z/oVE1mm+a3FmPPTQlHBXotNEsrWV2wmJHe -lNQPI6ZwMtLrBSg8PUpG2Rvao6XJ4ZBl/VcDwfcLgCnALPCcL0L0Z3vH3Sc9Ta/bQWJODG7uSaI1 -iVJ7ArKNtVzTqRQWK967mol9CCqh4A0jRrH0aVEFbrqQ/QQA58iEJaFhzFZjufjC9N8Isn3Ky7xu -h+dk001RNCb1GnNZcx4Ld2IB+uXyYjtg7dNaUhGgGuCBo9nax89bMsBzzUukx3SHq1pxopMg6Dm8 -ImBoIAicuQWgEkaP2T0rlwCozUalJZaG1gyrzkPhkeY7CglpJycHLHfY2MIb46c8+58D/iJ83Q5j -Y4x+sqW2QeUYFwqCcOW8Urg64UxEkgXZXiNMwTAJCaxp/Pz7cgeUDwgv+6CXEdnT1910+byzK9ha -V1Q/65+/JYuCeyHxcoAb4Wtpdl7GALGd/1G0UAmq47yrefEr/b00uS35i1qUUhOzo1NmEZch/bvF -kmJ+WtAHunZcOCu0EFZhdWx0IFRlc3QgS2V5IDKJATgEEwECACIFAlXbkJECGy8GCwkIBwMCBhUI -AgkKCwQWAgMBAh4BAheAAAoJEOuDLGfrXolXqz4H/28IuoRxGKoJ064YHjPkkpoddW6zdzzNfHip -ZnNfEUiTEls4qF1IB81M2xqfiXIFRIdO2kaLkRPFhO0hRxbtI6VuZYLgG3QCaXhxW6GyFa5zKABq -hb5ojexdnAYRswaHV201ZCclj9rnJN1PAg0Rz6MdX/w1euEWktQxWKo42oZKyx8oT9p6lrv5KRmG -kdrg8K8ARmRILjmwuBAgJM0eXBZHNGWXelk4YmOgnAAcZA6ZAo1G+8Pg6pKKP61ETewuCg3/u7N0 -vDttB+ZXqF88W9jAYlvdgbTtajNF5IDYDjTzWfeCaIB18F9gOzXq15SwWeDDI+CU9Nmq358IzXlx -k4edA5gEVduQkQEIAOjZV5tbpfIh5QefpIp2dpGMVfpgPj4RNc15CyFnb8y6dhCrdybkY9GveXJe -4F3GNYnSfB42cgxrfhizX3LakmZQ/SAg+YO5KxfCIN7Q9LPNeTgPsZZT6h8lVuXUxOFKXfRaR3/t -GF5xE3e5CoZRsHV/c92h3t1LdJNOnC5mUKIPO4zDxiw/C2T2q3rP1kmIMaOH724kEH5A+xcp1cBH -yt0tdHtIWuQv6joTJzujqViRhlCwQYzQSKpSBxwhBsorPvyinZI/ZXA4XXZc5RoMqV9rikedrb1r -ENO8JOuPu6tMS+znFu67skq2gFFZwCQWIjdHm+2ukE+PE580WAWudyMAEQEAAQAH/i7ndRPI+t0T -AdEu0dTIdyrrg3g7gd471kQtIVZZwTYSy2yhNY/Ciu72s3ab8QNCxY8dNL5bRk8FKjHslAoNSFdO -8iZSLiDgIHOZOcjYe6pqdgQaeTHodm1Otrn2SbB+K/3oX6W/y1xe18aSojGba/nHMj5PeJbIN9Pi -jmh0WMLD/0rmkTTxR7qQ5+kMV4O29xY4qjdYRD5O0adeZX0mNncmlmQ+rX9yxrtSgFROu1jwVtfP -hcNetifTTshJnTwND8hux5ECEadlIVBHypW28Hth9TRBXmddTmv7L7mdtUO6DybgkpWpw4k4LPsk -uZ6aY4wcGRp7EVfWGr9NHbq/n+0EAOlhDXIGdylkQsndjBMyhPsXZa5fFBmOyHjXj733195Jgr1v -ZjaIomrA9cvYrmN75oKrG1jJsMEl6HfC/ZPzEj6E51/p1PRdHP7CdUUA+DG8x4M3jn+e43psVuAR -a1XbN+8/bOa0ubt7ljVPjAEvWRSvU9dRaQz93w3fduAuM07dBAD/ayK3e0d6JMJMrU50lNOXQBgL -rFbg4rWzPO9BJQdhjOhmOZQiUa1Q+EV+s95yIUg1OAfaMP9KRIljr5RCdGNS6WoMNBAQOSrZpelf -jW4NpzphNfWDGVkUoPoskVtJz/nu9d860dGd3Al0kSmtUpMu5QKlo+sSxXUPbWLUn8V9/wP/ScCW -H+0gtL4R7SFazPeTIP+Cu5oR7A/DlFVLJKa3vo+atkhSvwxHGbg04vb/W4mKhGGVtMBtlhRmaWOe -PhUulU5FdaYsdlpN/Yd+hhgU6NHlyImPGVEHWD8c6CG8qoZfpR33j2sqshs4i/MtJZeBvl62vxPn -9bDN7KAjFNll9axAjIkCPgQYAQIACQUCVduQkQIbLgEpCRDrgyxn616JV8BdIAQZAQIABgUCVduQ -kQAKCRArYtevdF38xtzgB/4zVzozBpVOnagRkA7FDsHo36xX60Lik+ew0m28ueDDhnV3bXQsCvn/ -6wiCVWqLOTDeYCPlyTTpEMyk8zwdCICW6MgSkVHWcEDOrRqIrqm86rirjTGjJSgQe3l4CqJvkn6j -ybShYoBk1OZZV6vVv9hPTXXv9E6dLKoEW5YZBrrF+VC0w1iOIvaAQ+QXph20eV4KBIrp/bhG6Pdn -igKxuBZ79cdqDnXIzT9UiIa6LYpR0rbeg+7BmuZTTPS8t+41hIiKS+UZFdKa67eYENtyOmEMWOFC -LLRJGxkleukchiMJ70rknloZXsvJIweXBzSZ6m7mJQBgaig/L/dXyjv6+j2pNB4H/1trYUtJjXQK -HmqlgCmpCkHt3g7JoxWvglnDNmE6q3hIWuVIYQpnzZy1g05+X9Egwc1WVpBB02H7PkUZTfpaP/L6 -DLneMmSKPhZE3I+lPIPjwrxqh6xy5uQezcWkJTNKvPWF4FJzrVvx7XTPjfGvOB0UPEnjvtZTp5yO -hTeZK7DgIEtb/Wcrqs+iRArQKboM930ORSZhwvGK3F9V/gMDpIrvge5vDFsTEYQdw/2epIewH0L/ -FUb/6jBRcVEpGo9Ayg+Jnhq14GOGcd1y9oMZ48kYVLVBTA9tQ+82WE8Bch7uFPj4MFOMVRn1dc3q -dXlg3mimA+iK7tABQfG0RJ9YzWs=` - - TestPrivKey3 = `lQOXBFXbkiMBCACiHW4/VI2JkfvSEINddS7vE6wEu5e1leNQDaLUh6PrATQZS2a4Q6kRE6WlJumj -6wCeN753Cm93UGQl2Bi3USIEeArIZnPTcocrckOVXxtoLBNKXgqKvEsDXgfw8A+doSfXoDm/3Js4 -Wy3WsYKNR9LaPuJZHnpjsFAJhvRVyhH4UFD+1RTSSefq1mozPfDdMoZeZNEpfhwt3DuTJs7RqcTH -CgR2CqhEHnOOE5jJUljHKYLCglE2+8dth1bZlQi4xly/VHZzP3Bn7wKeolK/ROP6VZz/e0xq/BKy -resmxvlBWZ1zWwqGIrV9b0uwYvGrh2hOd5C5+5oGaA2MGcjxwaLBABEBAAEAB/dQbElFIa0VklZa -39ZLhtbBxACSWH3ql3EtRZaB2Mh4zSALbFyJDQfScOy8AZHmv66Ozxit9X9WsYr9OzcHujgl/2da -A3lybF6iLw1YDNaL11G6kuyn5sFP6lYGMRGOIWSik9oSVF6slo8m8ujRLdBsdMXVcElHKzCJiWmt -JZHEnUkl9X96fIPajMBfWjHHwcaeMOc77nvjwqy5wC4EY8TSVYzxeZHL7DADQ0EHBcThlmfizpCq -26LMVb6ju8STH7uDDFyKmhr/hC2vOkt+PKsvBCmW8/ESanO1zKPD9cvSsOWr2rZWNnkDRftqzOU5 -OCrI+3o9E74+toNb07bPntEEAMEStOzSvqZ6NKdh7EZYPA4mkkFC+EiHYIoinP1sd9V8O2Hq+dzx -yFHtWu0LmP6uWXk45vsP9y1UMJcEa33ew5JJa7zgucI772/BNvd/Oys/PqwIAl6uNIY8uYLgmn4L -1IPatp7vDiXzZSivPZd4yN4S4zCypZp9cnpO3qv8q7CtBADW87IA0TabdoxiN+m4XL7sYDRIfglr -MRPAlfrkAUaGDBx/t1xb6IaKk7giFdwHpTI6+g9XNkqKqogMe4Fp+nsd1xtfsNUBn6iKZavm5kXe -Lp9QgE+K6mvIreOTe2PKQqXqgPRG6+SRGatoKeY76fIpd8AxOJyWERxcq2lUHLn45QP/UXDTcYB7 -gzJtZrfpXN0GqQ0lYXMzbQfLnkUsu3mYzArfNy0otzEmKTkwmKclNY1/EJSzSdHfgmeA260a0nLK -64C0wPgSmOqw90qwi5odAYSjSFBapDbyGF86JpHrLxyEEpGoXanRPwWfbiWp19Nwg6nknA87AtaM -3+AHjbWzwCpHL7QQVmF1bHQgVGVzdCBLZXkgM4kBOAQTAQIAIgUCVduSIwIbLwYLCQgHAwIGFQgC -CQoLBBYCAwECHgECF4AACgkQ9HlLVvwtxt1aMQf/aaGoL1rRWTUjM6DEShXFhWpV29rEjSdNk5N+ -ZwVifgdCVD5IsSjI1Z7mO2SHHiTm4eKnHAofM6/TZgzXg1YLpu8rDYJARMsM8bgK/xgxSamGjm2c -wN220jOnwePIlG0drNTW5N6zb/K6qHoscJ6NUkjS5JPdGJuq7B0bdCM8/xSbG75gL34U5bYqK38B -DwmW4UMl2rf/BJfxV9hmsZ2Cat4TspgyiWEKTMZI+PugXKDDwuoqgm+320K4EqFkwG4y/WwHkKgk -hZ0+io5lzhTsvVd2p8q8VlH9GG5eA3WWQj0yqucsOmKQvcuT5y0vFY6NQJbyuioqgdlgEXtc+p0B -+Z0DmARV25IjAQgA49yN3hCBsuWoiTezoE9FHJXOCVOBR1/4jStQPJtoMl8mhtl3xTp7iGQ+9GhD -y0l5+fP+qcP/rfBq0BslhxVOZ7jQjdUoM6ZUZzJoPGIo/V2KwqpwQl3tdCIjvagCJeYQfTL7lTCc -4ySz+XBoAYMwZVGMcRcjp+JE8Wx9Ovzuq8wnelbU6I5dVJ7O4E1OWbIkLuytDX+fDEvfft6/oPXN -Bl3cm6FzEuQetQQss3DOG9xnvS+DrjmMCbPwR2a++ioQ8+geoqA/kB4cAI6xOb3ncoeGDHc1i4Y9 -T9Ggi+6Aq3girmfDtNYVOM8cZUXcZNCvLkJn8DNeIvnuFUSEO+a5PwARAQABAAf/TPd98CmRNdV/ -VUI8aYT9Kkervdi4DVzsfvrHcoFn88PSJrCkVTmI6qw526Kwa6VZD0YMmll7LszLt5nD1lorDrwN -rir3FmMzlVwge20IvXRwX4rkunYxtA2oFvL+LsEEhtXGx0ERbWRDapk+eGxQ15hxIO4Y/Cdg9E+a -CWfQUrTSnC6qMVfVYMGfnM1yNX3OWattEFfmxQas5XqQk/0FgjCZALixdanjN/r1tjp5/2MiSD8N -Wkemzsr6yPicnc3+BOZc5YOOnH8FqBvVHcDlSJI6pCOCEiO3Pq2QEk/1evONulbF116mLnQoGrpp -W77l+5O42VUpZfjROCPd5DYyMQQA492CFXZpDIJ2emB9/nK8X6IzdVRK3oof8btbSNnme5afIzhs -wR1ruX30O7ThfB+5ezpbgK1C988CWkr9SNSTy43omarafGig6/Y1RzdiITILuIGfbChoSpc70jXx -U0nzJ/1i9yZ/vDgP3EC2miRhlDcp5w0Bu0oMBlgG/1uhj0cEAP/+7aFGP0fo2MZPhyl5feHKWj4k -85XoAIpMBnzF6HTGU3ljAE56a+4sVw3bWB755DPhvpZvDkX60I9iIJxio8TK5ITdfjlLhxuskXyt -ycwWI/4J+soeq4meoxK9jxZJuDl/qvoGfyzNg1oy2OBehX8+6erW46kr6Z/MQutS3zJJBACmJHrK -VR40qD7a8KbvfuM3ruwlm5JqT/Ykq1gfKKxHjWDIUIeyBX/axGQvAGNYeuuQCzZ0+QsEWur3C4kN -U+Pb5K1WGyOKkhJzivSI56AG3d8TA/Q0JhqST6maY0fvUoahWSCcpd7MULa3n1zx5Wsvi8mkVtup -Js/IDi/kqneqM0XviQI+BBgBAgAJBQJV25IjAhsuASkJEPR5S1b8LcbdwF0gBBkBAgAGBQJV25Ij -AAoJEAUj/03Hcrkg84UIAKxn9nizYtwSgDnVNb5PnD5h6+Ui6r7ffYm2o0im4YhakbFTHIPI9PRh -BavRI5sE5Fg2vtE/x38jattoUrJoNoq9Gh9iv5PBfL3amEGjul0RRqYGl+ub+yv7YGAAHbHcdZen -4gx15VWGpB7y3hycWbdzV8h3EAPKIm5XmB7YyXmArnI3CoJA+HtTZGoL6WZWUwka9YichGfaZ/oD -umENg1l87Pp2RqvjLKHmv2tGCtnDzyv/IiWur9zopFQiCc8ysVgRq6CA5x5nzbv6MqRspYUS4e2I -LFbuREA3blR+caw9oX41IYzarW8IbgeIXJ3HqUyhczRKF/z5nDKtX/kHMCqlbAgAnfu0TALnwVuj -KeXLo4Y7OA9LTEqfORcw62q5OjSoQf/VsRSwGSefv3kGZk5N/igELluU3qpG/twZI/TSL6zGqXU2 -FOMlyMm1849TOB9b4B//4dHrjzPhztzowKMMUqeTxmSgYtFTshKN6eQ0XO+7ZuOXEmSKXS4kOUs9 -ttfzSiPNXUZL2D5nFU9H7rw3VAuXYVTrOx+Dfi6mYsscbxUbi8THODI2Q7B9Ni92DJE1OOe4+57o -fXZ9ln24I14bna/uVHd6hBwLEE6eLCCKkHxQnnZFZduXDHMK0a0OL8RYHfMtNSem4pyC5wDQui1u -KFIzGEPKVoBF9U7VBXpyxpsz+A==` - - TestPubKey1 = `mQENBFXbjPUBCADjNjCUQwfxKL+RR2GA6pv/1K+zJZ8UWIF9S0lk7cVIEfJiprzzwiMwBS5cD0da -rGin1FHvIWOZxujA7oW0O2TUuatqI3aAYDTfRYurh6iKLC+VS+F7H+/mhfFvKmgr0Y5kDCF1j0T/ -063QZ84IRGucR/X43IY7kAtmxGXH0dYOCzOe5UBX1fTn3mXGe2ImCDWBH7gOViynXmb6XNvXkP0f -sF5St9jhO7mbZU9EFkv9O3t3EaURfHopsCVDOlCkFCw5ArY+DUORHRzoMX0PnkyQb5OzibkChzpg -8hQssKeVGpuskTdz5Q7PtdW71jXd4fFVzoNH8fYwRpziD2xNvi6HABEBAAG0EFZhdWx0IFRlc3Qg -S2V5IDGJATgEEwECACIFAlXbjPUCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEOfLr44B -HbeTo+sH/i7bapIgPnZsJ81hmxPj4W12uvunksGJiC7d4hIHsG7kmJRTJfjECi+AuTGeDwBy84TD -cRaOB6e79fj65Fg6HgSahDUtKJbGxj/lWzmaBuTzlN3CEe8cMwIPqPT2kajJVdOyrvkyuFOdPFOE -A7bdCH0MqgIdM2SdF8t40k/ATfuD2K1ZmumJ508I3gF39jgTnPzD4C8quswrMQ3bzfvKC3klXRlB -C0yoArn+0QA3cf2B9T4zJ2qnvgotVbeK/b1OJRNj6Poeo+SsWNc/A5mw7lGScnDgL3yfwCm1gQXa -QKfOt5x+7GqhWDw10q+bJpJlI10FfzAnhMF9etSqSeURBRW5AQ0EVduM9QEIAL53hJ5bZJ7oEDCn -aY+SCzt9QsAfnFTAnZJQrvkvusJzrTQ088eUQmAjvxkfRqnv981fFwGnh2+I1Ktm698UAZS9Jt8y -jak9wWUICKQO5QUt5k8cHwldQXNXVXFa+TpQWQR5yW1a9okjh5o/3d4cBt1yZPUJJyLKY43Wvptb -6EuEsScO2DnRkh5wSMDQ7dTooddJCmaq3LTjOleRFQbu9ij386Do6jzK69mJU56TfdcydkxkWF5N -ZLGnED3lq+hQNbe+8UI5tD2oP/3r5tXKgMy1R/XPvR/zbfwvx4FAKFOP01awLq4P3d/2xOkMu4Lu -9p315E87DOleYwxk+FoTqXEAEQEAAYkCPgQYAQIACQUCVduM9QIbLgEpCRDny6+OAR23k8BdIAQZ -AQIABgUCVduM9QAKCRAID0JGyHtSGmqYB/4m4rJbbWa7dBJ8VqRU7ZKnNRDR9CVhEGipBmpDGRYu -lEimOPzLUX/ZXZmTZzgemeXLBaJJlWnopVUWuAsyjQuZAfdd8nHkGRHG0/DGum0l4sKTta3OPGHN -C1z1dAcQ1RCr9bTD3PxjLBczdGqhzw71trkQRBRdtPiUchltPMIyjUHqVJ0xmg0hPqFic0fICsr0 -YwKoz3h9+QEcZHvsjSZjgydKvfLYcm+4DDMCCqcHuJrbXJKUWmJcXR0y/+HQONGrGJ5xWdO+6eJi -oPn2jVMnXCm4EKc7fcLFrz/LKmJ8seXhxjM3EdFtylBGCrx3xdK0f+JDNQaC/rhUb5V2XuX6VwoH -/AtY+XsKVYRfNIupLOUcf/srsm3IXT4SXWVomOc9hjGQiJ3rraIbADsc+6bCAr4XNZS7moViAAcI -PXFv3m3WfUlnG/om78UjQqyVACRZqqAGmuPq+TSkRUCpt9h+A39LQWkojHqyob3cyLgy6z9Q557O -9uK3lQozbw2gH9zC0RqnePl+rsWIUU/ga16fH6pWc1uJiEBt8UZGypQ/E56/343epmYAe0a87sHx -8iDV+dNtDVKfPRENiLOOc19MmS+phmUyrbHqI91c0pmysYcJZCD3a502X1gpjFbPZcRtiTmGnUKd -OIu60YPNE4+h7u2CfYyFPu3AlUaGNMBlvy6PEpU=` - - TestPubKey2 = `mQENBFXbkJEBCADKb1ZvlT14XrJa2rTOe5924LQr2PTZlRv+651TXy33yEhelZ+V4sMrELN8fKEG -Zy1kNixmbq3MCF/671k3LigHA7VrOaH9iiQgr6IIq2MeIkUYKZ27C992vQkYLjbYUG8+zl5h69S4 -0Ixm0yL0M54XOJ0gm+maEK1ZESKTUlDNkIS7l0jLZSYwfUeGXSEt6FWs8OgbyRTaHw4PDHrDEE9e -Q67K6IZ3YMhPOL4fVk4Jwrp5R/RwiklT+lNozWEyFVwPFH4MeQMs9nMbt+fWlTzEA7tI4acI9yDk -Cm1yN2R9rmY0UjODRiJw6z6sLV2T+Pf32n3MNSUOYczOjZa4VBwjABEBAAG0EFZhdWx0IFRlc3Qg -S2V5IDKJATgEEwECACIFAlXbkJECGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEOuDLGfr -XolXqz4H/28IuoRxGKoJ064YHjPkkpoddW6zdzzNfHipZnNfEUiTEls4qF1IB81M2xqfiXIFRIdO -2kaLkRPFhO0hRxbtI6VuZYLgG3QCaXhxW6GyFa5zKABqhb5ojexdnAYRswaHV201ZCclj9rnJN1P -Ag0Rz6MdX/w1euEWktQxWKo42oZKyx8oT9p6lrv5KRmGkdrg8K8ARmRILjmwuBAgJM0eXBZHNGWX -elk4YmOgnAAcZA6ZAo1G+8Pg6pKKP61ETewuCg3/u7N0vDttB+ZXqF88W9jAYlvdgbTtajNF5IDY -DjTzWfeCaIB18F9gOzXq15SwWeDDI+CU9Nmq358IzXlxk4e5AQ0EVduQkQEIAOjZV5tbpfIh5Qef -pIp2dpGMVfpgPj4RNc15CyFnb8y6dhCrdybkY9GveXJe4F3GNYnSfB42cgxrfhizX3LakmZQ/SAg -+YO5KxfCIN7Q9LPNeTgPsZZT6h8lVuXUxOFKXfRaR3/tGF5xE3e5CoZRsHV/c92h3t1LdJNOnC5m -UKIPO4zDxiw/C2T2q3rP1kmIMaOH724kEH5A+xcp1cBHyt0tdHtIWuQv6joTJzujqViRhlCwQYzQ -SKpSBxwhBsorPvyinZI/ZXA4XXZc5RoMqV9rikedrb1rENO8JOuPu6tMS+znFu67skq2gFFZwCQW -IjdHm+2ukE+PE580WAWudyMAEQEAAYkCPgQYAQIACQUCVduQkQIbLgEpCRDrgyxn616JV8BdIAQZ -AQIABgUCVduQkQAKCRArYtevdF38xtzgB/4zVzozBpVOnagRkA7FDsHo36xX60Lik+ew0m28ueDD -hnV3bXQsCvn/6wiCVWqLOTDeYCPlyTTpEMyk8zwdCICW6MgSkVHWcEDOrRqIrqm86rirjTGjJSgQ -e3l4CqJvkn6jybShYoBk1OZZV6vVv9hPTXXv9E6dLKoEW5YZBrrF+VC0w1iOIvaAQ+QXph20eV4K -BIrp/bhG6PdnigKxuBZ79cdqDnXIzT9UiIa6LYpR0rbeg+7BmuZTTPS8t+41hIiKS+UZFdKa67eY -ENtyOmEMWOFCLLRJGxkleukchiMJ70rknloZXsvJIweXBzSZ6m7mJQBgaig/L/dXyjv6+j2pNB4H -/1trYUtJjXQKHmqlgCmpCkHt3g7JoxWvglnDNmE6q3hIWuVIYQpnzZy1g05+X9Egwc1WVpBB02H7 -PkUZTfpaP/L6DLneMmSKPhZE3I+lPIPjwrxqh6xy5uQezcWkJTNKvPWF4FJzrVvx7XTPjfGvOB0U -PEnjvtZTp5yOhTeZK7DgIEtb/Wcrqs+iRArQKboM930ORSZhwvGK3F9V/gMDpIrvge5vDFsTEYQd -w/2epIewH0L/FUb/6jBRcVEpGo9Ayg+Jnhq14GOGcd1y9oMZ48kYVLVBTA9tQ+82WE8Bch7uFPj4 -MFOMVRn1dc3qdXlg3mimA+iK7tABQfG0RJ9YzWs=` - - TestPubKey3 = `mQENBFXbkiMBCACiHW4/VI2JkfvSEINddS7vE6wEu5e1leNQDaLUh6PrATQZS2a4Q6kRE6WlJumj -6wCeN753Cm93UGQl2Bi3USIEeArIZnPTcocrckOVXxtoLBNKXgqKvEsDXgfw8A+doSfXoDm/3Js4 -Wy3WsYKNR9LaPuJZHnpjsFAJhvRVyhH4UFD+1RTSSefq1mozPfDdMoZeZNEpfhwt3DuTJs7RqcTH -CgR2CqhEHnOOE5jJUljHKYLCglE2+8dth1bZlQi4xly/VHZzP3Bn7wKeolK/ROP6VZz/e0xq/BKy -resmxvlBWZ1zWwqGIrV9b0uwYvGrh2hOd5C5+5oGaA2MGcjxwaLBABEBAAG0EFZhdWx0IFRlc3Qg -S2V5IDOJATgEEwECACIFAlXbkiMCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPR5S1b8 -LcbdWjEH/2mhqC9a0Vk1IzOgxEoVxYVqVdvaxI0nTZOTfmcFYn4HQlQ+SLEoyNWe5jtkhx4k5uHi -pxwKHzOv02YM14NWC6bvKw2CQETLDPG4Cv8YMUmpho5tnMDdttIzp8HjyJRtHazU1uTes2/yuqh6 -LHCejVJI0uST3RibquwdG3QjPP8Umxu+YC9+FOW2Kit/AQ8JluFDJdq3/wSX8VfYZrGdgmreE7KY -MolhCkzGSPj7oFygw8LqKoJvt9tCuBKhZMBuMv1sB5CoJIWdPoqOZc4U7L1XdqfKvFZR/RhuXgN1 -lkI9MqrnLDpikL3Lk+ctLxWOjUCW8roqKoHZYBF7XPqdAfm5AQ0EVduSIwEIAOPcjd4QgbLlqIk3 -s6BPRRyVzglTgUdf+I0rUDybaDJfJobZd8U6e4hkPvRoQ8tJefnz/qnD/63watAbJYcVTme40I3V -KDOmVGcyaDxiKP1disKqcEJd7XQiI72oAiXmEH0y+5UwnOMks/lwaAGDMGVRjHEXI6fiRPFsfTr8 -7qvMJ3pW1OiOXVSezuBNTlmyJC7srQ1/nwxL337ev6D1zQZd3JuhcxLkHrUELLNwzhvcZ70vg645 -jAmz8EdmvvoqEPPoHqKgP5AeHACOsTm953KHhgx3NYuGPU/RoIvugKt4Iq5nw7TWFTjPHGVF3GTQ -ry5CZ/AzXiL57hVEhDvmuT8AEQEAAYkCPgQYAQIACQUCVduSIwIbLgEpCRD0eUtW/C3G3cBdIAQZ -AQIABgUCVduSIwAKCRAFI/9Nx3K5IPOFCACsZ/Z4s2LcEoA51TW+T5w+YevlIuq+332JtqNIpuGI -WpGxUxyDyPT0YQWr0SObBORYNr7RP8d/I2rbaFKyaDaKvRofYr+TwXy92phBo7pdEUamBpfrm/sr -+2BgAB2x3HWXp+IMdeVVhqQe8t4cnFm3c1fIdxADyiJuV5ge2Ml5gK5yNwqCQPh7U2RqC+lmVlMJ -GvWInIRn2mf6A7phDYNZfOz6dkar4yyh5r9rRgrZw88r/yIlrq/c6KRUIgnPMrFYEauggOceZ827 -+jKkbKWFEuHtiCxW7kRAN25UfnGsPaF+NSGM2q1vCG4HiFydx6lMoXM0Shf8+ZwyrV/5BzAqpWwI -AJ37tEwC58Fboynly6OGOzgPS0xKnzkXMOtquTo0qEH/1bEUsBknn795BmZOTf4oBC5blN6qRv7c -GSP00i+sxql1NhTjJcjJtfOPUzgfW+Af/+HR648z4c7c6MCjDFKnk8ZkoGLRU7ISjenkNFzvu2bj -lxJkil0uJDlLPbbX80ojzV1GS9g+ZxVPR+68N1QLl2FU6zsfg34upmLLHG8VG4vExzgyNkOwfTYv -dgyRNTjnuPue6H12fZZ9uCNeG52v7lR3eoQcCxBOniwgipB8UJ52RWXblwxzCtGtDi/EWB3zLTUn -puKcgucA0LotbihSMxhDylaARfVO1QV6csabM/g=` - - TestAAPubKey1 = `-----BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1 - -mQENBFXbjPUBCADjNjCUQwfxKL+RR2GA6pv/1K+zJZ8UWIF9S0lk7cVIEfJiprzz -wiMwBS5cD0darGin1FHvIWOZxujA7oW0O2TUuatqI3aAYDTfRYurh6iKLC+VS+F7 -H+/mhfFvKmgr0Y5kDCF1j0T/063QZ84IRGucR/X43IY7kAtmxGXH0dYOCzOe5UBX -1fTn3mXGe2ImCDWBH7gOViynXmb6XNvXkP0fsF5St9jhO7mbZU9EFkv9O3t3EaUR -fHopsCVDOlCkFCw5ArY+DUORHRzoMX0PnkyQb5OzibkChzpg8hQssKeVGpuskTdz -5Q7PtdW71jXd4fFVzoNH8fYwRpziD2xNvi6HABEBAAG0EFZhdWx0IFRlc3QgS2V5 -IDGJATgEEwECACIFAlXbjPUCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJ -EOfLr44BHbeTo+sH/i7bapIgPnZsJ81hmxPj4W12uvunksGJiC7d4hIHsG7kmJRT -JfjECi+AuTGeDwBy84TDcRaOB6e79fj65Fg6HgSahDUtKJbGxj/lWzmaBuTzlN3C -Ee8cMwIPqPT2kajJVdOyrvkyuFOdPFOEA7bdCH0MqgIdM2SdF8t40k/ATfuD2K1Z -mumJ508I3gF39jgTnPzD4C8quswrMQ3bzfvKC3klXRlBC0yoArn+0QA3cf2B9T4z -J2qnvgotVbeK/b1OJRNj6Poeo+SsWNc/A5mw7lGScnDgL3yfwCm1gQXaQKfOt5x+ -7GqhWDw10q+bJpJlI10FfzAnhMF9etSqSeURBRW5AQ0EVduM9QEIAL53hJ5bZJ7o -EDCnaY+SCzt9QsAfnFTAnZJQrvkvusJzrTQ088eUQmAjvxkfRqnv981fFwGnh2+I -1Ktm698UAZS9Jt8yjak9wWUICKQO5QUt5k8cHwldQXNXVXFa+TpQWQR5yW1a9okj -h5o/3d4cBt1yZPUJJyLKY43Wvptb6EuEsScO2DnRkh5wSMDQ7dTooddJCmaq3LTj -OleRFQbu9ij386Do6jzK69mJU56TfdcydkxkWF5NZLGnED3lq+hQNbe+8UI5tD2o -P/3r5tXKgMy1R/XPvR/zbfwvx4FAKFOP01awLq4P3d/2xOkMu4Lu9p315E87DOle -Ywxk+FoTqXEAEQEAAYkCPgQYAQIACQUCVduM9QIbLgEpCRDny6+OAR23k8BdIAQZ -AQIABgUCVduM9QAKCRAID0JGyHtSGmqYB/4m4rJbbWa7dBJ8VqRU7ZKnNRDR9CVh -EGipBmpDGRYulEimOPzLUX/ZXZmTZzgemeXLBaJJlWnopVUWuAsyjQuZAfdd8nHk -GRHG0/DGum0l4sKTta3OPGHNC1z1dAcQ1RCr9bTD3PxjLBczdGqhzw71trkQRBRd -tPiUchltPMIyjUHqVJ0xmg0hPqFic0fICsr0YwKoz3h9+QEcZHvsjSZjgydKvfLY -cm+4DDMCCqcHuJrbXJKUWmJcXR0y/+HQONGrGJ5xWdO+6eJioPn2jVMnXCm4EKc7 -fcLFrz/LKmJ8seXhxjM3EdFtylBGCrx3xdK0f+JDNQaC/rhUb5V2XuX6VwoH/AtY -+XsKVYRfNIupLOUcf/srsm3IXT4SXWVomOc9hjGQiJ3rraIbADsc+6bCAr4XNZS7 -moViAAcIPXFv3m3WfUlnG/om78UjQqyVACRZqqAGmuPq+TSkRUCpt9h+A39LQWko -jHqyob3cyLgy6z9Q557O9uK3lQozbw2gH9zC0RqnePl+rsWIUU/ga16fH6pWc1uJ -iEBt8UZGypQ/E56/343epmYAe0a87sHx8iDV+dNtDVKfPRENiLOOc19MmS+phmUy -rbHqI91c0pmysYcJZCD3a502X1gpjFbPZcRtiTmGnUKdOIu60YPNE4+h7u2CfYyF -Pu3AlUaGNMBlvy6PEpU= -=NUTS ------END PGP PUBLIC KEY BLOCK-----` -) diff --git a/helper/policies/policies_test.go b/helper/policies/policies_test.go deleted file mode 100644 index 09c104d21..000000000 --- a/helper/policies/policies_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package policies - -import "testing" - -func TestEquivalentPolicies(t *testing.T) { - a := []string{"foo", "bar"} - var b []string - if EquivalentPolicies(a, b) { - t.Fatal("bad") - } - - b = []string{"foo"} - if EquivalentPolicies(a, b) { - t.Fatal("bad") - } - - b = []string{"bar", "foo"} - if !EquivalentPolicies(a, b) { - t.Fatal("bad") - } - - b = []string{"foo", "default", "bar"} - if !EquivalentPolicies(a, b) { - t.Fatal("bad") - } -} diff --git a/helper/random/parser_test.go b/helper/random/parser_test.go deleted file mode 100644 index 7ca05fd00..000000000 --- a/helper/random/parser_test.go +++ /dev/null @@ -1,590 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package random - -import ( - "encoding/json" - "reflect" - "testing" -) - -func TestParsePolicy(t *testing.T) { - type testCase struct { - rawConfig string - expected StringGenerator - expectErr bool - } - - tests := map[string]testCase{ - "unrecognized rule": { - rawConfig: ` - length = 20 - rule "testrule" { - string = "teststring" - int = 123 - }`, - expected: StringGenerator{}, - expectErr: true, - }, - - "charset restrictions": { - rawConfig: ` - length = 20 - rule "charset" { - charset = "abcde" - min-chars = 2 - }`, - expected: StringGenerator{ - Length: 20, - charset: []rune("abcde"), - Rules: []Rule{ - CharsetRule{ - Charset: []rune("abcde"), - MinChars: 2, - }, - }, - }, - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual, err := ParsePolicy(test.rawConfig) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(actual, test.expected) { - t.Fatalf("Actual: %#v\nExpected:%#v", actual, test.expected) - } - }) - } -} - -func TestParser_ParsePolicy(t *testing.T) { - type testCase struct { - registry map[string]ruleConstructor - - rawConfig string - expected StringGenerator - expectErr bool - } - - tests := map[string]testCase{ - "empty config": { - registry: defaultRuleNameMapping, - rawConfig: "", - expected: StringGenerator{}, - expectErr: true, - }, - "bogus config": { - registry: defaultRuleNameMapping, - rawConfig: "asdf", - expected: StringGenerator{}, - expectErr: true, - }, - "config with only length": { - registry: defaultRuleNameMapping, - rawConfig: ` - length = 20`, - expected: StringGenerator{}, - expectErr: true, - }, - "config with zero length": { - registry: defaultRuleNameMapping, - rawConfig: ` - length = 0 - rule "charset" { - charset = "abcde" - }`, - expected: StringGenerator{}, - expectErr: true, - }, - "config with negative length": { - registry: defaultRuleNameMapping, - rawConfig: ` - length = -2 - rule "charset" { - charset = "abcde" - }`, - expected: StringGenerator{}, - expectErr: true, - }, - "charset restrictions": { - registry: defaultRuleNameMapping, - rawConfig: ` - length = 20 - rule "charset" { - charset = "abcde" - min-chars = 2 - }`, - expected: StringGenerator{ - Length: 20, - charset: []rune("abcde"), - Rules: []Rule{ - CharsetRule{ - Charset: []rune("abcde"), - MinChars: 2, - }, - }, - }, - expectErr: false, - }, - "test rule": { - registry: map[string]ruleConstructor{ - "testrule": newTestRule, - }, - rawConfig: ` - length = 20 - rule "testrule" { - string = "teststring" - int = 123 - }`, - expected: StringGenerator{ - Length: 20, - charset: deduplicateRunes([]rune("teststring")), - Rules: []Rule{ - testCharsetRule{ - String: "teststring", - Integer: 123, - }, - }, - }, - expectErr: false, - }, - "test rule and charset restrictions": { - registry: map[string]ruleConstructor{ - "testrule": newTestRule, - "charset": ParseCharset, - }, - rawConfig: ` - length = 20 - rule "testrule" { - string = "teststring" - int = 123 - } - rule "charset" { - charset = "abcde" - min-chars = 2 - }`, - expected: StringGenerator{ - Length: 20, - charset: deduplicateRunes([]rune("abcdeteststring")), - Rules: []Rule{ - testCharsetRule{ - String: "teststring", - Integer: 123, - }, - CharsetRule{ - Charset: []rune("abcde"), - MinChars: 2, - }, - }, - }, - expectErr: false, - }, - "unrecognized rule": { - registry: defaultRuleNameMapping, - rawConfig: ` - length = 20 - rule "testrule" { - string = "teststring" - int = 123 - }`, - expected: StringGenerator{}, - expectErr: true, - }, - - // ///////////////////////////////////////////////// - // JSON data - "manually JSONified HCL": { - registry: map[string]ruleConstructor{ - "testrule": newTestRule, - "charset": ParseCharset, - }, - rawConfig: ` - { - "charset": "abcde", - "length": 20, - "rule": [ - { - "testrule": [ - { - "string": "teststring", - "int": 123 - } - ] - }, - { - "charset": [ - { - "charset": "abcde", - "min-chars": 2 - } - ] - } - ] - }`, - expected: StringGenerator{ - Length: 20, - charset: deduplicateRunes([]rune("abcdeteststring")), - Rules: []Rule{ - testCharsetRule{ - String: "teststring", - Integer: 123, - }, - CharsetRule{ - Charset: []rune("abcde"), - MinChars: 2, - }, - }, - }, - expectErr: false, - }, - "JSONified HCL": { - registry: map[string]ruleConstructor{ - "testrule": newTestRule, - "charset": ParseCharset, - }, - rawConfig: toJSON(t, StringGenerator{ - Length: 20, - Rules: []Rule{ - testCharsetRule{ - String: "teststring", - Integer: 123, - }, - CharsetRule{ - Charset: []rune("abcde"), - MinChars: 2, - }, - }, - }), - expected: StringGenerator{ - Length: 20, - charset: deduplicateRunes([]rune("abcdeteststring")), - Rules: []Rule{ - testCharsetRule{ - String: "teststring", - Integer: 123, - }, - CharsetRule{ - Charset: []rune("abcde"), - MinChars: 2, - }, - }, - }, - expectErr: false, - }, - "JSON unrecognized rule": { - registry: defaultRuleNameMapping, - rawConfig: ` - { - "charset": "abcde", - "length": 20, - "rule": [ - { - "testrule": [ - { - "string": "teststring", - "int": 123 - } - ], - } - ] - }`, - expected: StringGenerator{}, - expectErr: true, - }, - "config value with empty slice": { - registry: defaultRuleNameMapping, - rawConfig: ` - rule { - n = [] - }`, - expected: StringGenerator{}, - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - parser := PolicyParser{ - RuleRegistry: Registry{ - Rules: test.registry, - }, - } - - actual, err := parser.ParsePolicy(test.rawConfig) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(actual, test.expected) { - t.Fatalf("Actual: %#v\nExpected:%#v", actual, test.expected) - } - }) - } -} - -func TestParseRules(t *testing.T) { - type testCase struct { - registry map[string]ruleConstructor - - rawRules []map[string]interface{} - expectedRules []Rule - expectErr bool - } - - tests := map[string]testCase{ - "nil rule data": { - registry: defaultRuleNameMapping, - rawRules: nil, - expectedRules: nil, - expectErr: false, - }, - "empty rule data": { - registry: defaultRuleNameMapping, - rawRules: []map[string]interface{}{}, - expectedRules: nil, - expectErr: false, - }, - "invalid rule data": { - registry: defaultRuleNameMapping, - rawRules: []map[string]interface{}{ - { - "testrule": map[string]interface{}{ - "string": "teststring", - }, - }, - }, - expectedRules: nil, - expectErr: true, - }, - "unrecognized rule data": { - registry: defaultRuleNameMapping, - rawRules: []map[string]interface{}{ - { - "testrule": []map[string]interface{}{ - { - "string": "teststring", - "int": 123, - }, - }, - }, - }, - expectedRules: nil, - expectErr: true, - }, - "recognized rule": { - registry: map[string]ruleConstructor{ - "testrule": newTestRule, - }, - rawRules: []map[string]interface{}{ - { - "testrule": []map[string]interface{}{ - { - "string": "teststring", - "int": 123, - }, - }, - }, - }, - expectedRules: []Rule{ - testCharsetRule{ - String: "teststring", - Integer: 123, - }, - }, - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - registry := Registry{ - Rules: test.registry, - } - - actualRules, err := parseRules(registry, test.rawRules) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(actualRules, test.expectedRules) { - t.Fatalf("Actual: %#v\nExpected:%#v", actualRules, test.expectedRules) - } - }) - } -} - -func TestGetMapSlice(t *testing.T) { - type testCase struct { - input map[string]interface{} - key string - expectedSlice []map[string]interface{} - expectErr bool - } - - tests := map[string]testCase{ - "nil map": { - input: nil, - key: "testkey", - expectedSlice: nil, - expectErr: false, - }, - "empty map": { - input: map[string]interface{}{}, - key: "testkey", - expectedSlice: nil, - expectErr: false, - }, - "ignored keys": { - input: map[string]interface{}{ - "foo": "bar", - }, - key: "testkey", - expectedSlice: nil, - expectErr: false, - }, - "key has wrong type": { - input: map[string]interface{}{ - "foo": "bar", - }, - key: "foo", - expectedSlice: nil, - expectErr: true, - }, - "good data": { - input: map[string]interface{}{ - "foo": []map[string]interface{}{ - { - "sub-foo": "bar", - }, - }, - }, - key: "foo", - expectedSlice: []map[string]interface{}{ - { - "sub-foo": "bar", - }, - }, - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actualSlice, err := getMapSlice(test.input, test.key) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(actualSlice, test.expectedSlice) { - t.Fatalf("Actual: %#v\nExpected:%#v", actualSlice, test.expectedSlice) - } - }) - } -} - -func TestGetRuleInfo(t *testing.T) { - type testCase struct { - rule map[string]interface{} - expectedInfo ruleInfo - expectErr bool - } - - tests := map[string]testCase{ - "nil rule": { - rule: nil, - expectedInfo: ruleInfo{}, - expectErr: true, - }, - "empty rule": { - rule: map[string]interface{}{}, - expectedInfo: ruleInfo{}, - expectErr: true, - }, - "rule with invalid type": { - rule: map[string]interface{}{ - "TestRuleType": "wrong type", - }, - expectedInfo: ruleInfo{}, - expectErr: true, - }, - "rule with good data": { - rule: map[string]interface{}{ - "TestRuleType": []map[string]interface{}{ - { - "foo": "bar", - }, - }, - }, - expectedInfo: ruleInfo{ - ruleType: "TestRuleType", - data: map[string]interface{}{ - "foo": "bar", - }, - }, - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actualInfo, err := getRuleInfo(test.rule) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(actualInfo, test.expectedInfo) { - t.Fatalf("Actual: %#v\nExpected:%#v", actualInfo, test.expectedInfo) - } - }) - } -} - -func BenchmarkParser_Parse(b *testing.B) { - config := `length = 20 - rule "charset" { - charset = "abcde" - min-chars = 2 - }` - - for i := 0; i < b.N; i++ { - parser := PolicyParser{ - RuleRegistry: Registry{ - Rules: defaultRuleNameMapping, - }, - } - _, err := parser.ParsePolicy(config) - if err != nil { - b.Fatalf("Failed to parse: %s", err) - } - } -} - -func toJSON(t *testing.T, val interface{}) string { - t.Helper() - b, err := json.Marshal(val) - if err != nil { - t.Fatalf("unable to marshal to JSON: %s", err) - } - return string(b) -} diff --git a/helper/random/registry_test.go b/helper/random/registry_test.go deleted file mode 100644 index 10e6af0de..000000000 --- a/helper/random/registry_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package random - -import ( - "fmt" - "reflect" - "testing" - - "github.com/mitchellh/mapstructure" -) - -type testCharsetRule struct { - String string `mapstructure:"string" json:"string"` - Integer int `mapstructure:"int" json:"int"` - - // Default to passing - fail bool -} - -func newTestRule(data map[string]interface{}) (rule Rule, err error) { - tr := &testCharsetRule{} - err = mapstructure.Decode(data, tr) - if err != nil { - return nil, fmt.Errorf("unable to decode test rule") - } - return *tr, nil -} - -func (tr testCharsetRule) Pass([]rune) bool { return !tr.fail } -func (tr testCharsetRule) Type() string { return "testrule" } -func (tr testCharsetRule) Chars() []rune { return []rune(tr.String) } - -func TestParseRule(t *testing.T) { - type testCase struct { - rules map[string]ruleConstructor - - ruleType string - ruleData map[string]interface{} - - expectedRule Rule - expectErr bool - } - - tests := map[string]testCase{ - "missing rule": { - rules: map[string]ruleConstructor{}, - ruleType: "testrule", - ruleData: map[string]interface{}{ - "string": "teststring", - "int": 123, - }, - expectedRule: nil, - expectErr: true, - }, - "nil data": { - rules: map[string]ruleConstructor{ - "testrule": newTestRule, - }, - ruleType: "testrule", - ruleData: nil, - expectedRule: testCharsetRule{}, - expectErr: false, - }, - "good rule": { - rules: map[string]ruleConstructor{ - "testrule": newTestRule, - }, - ruleType: "testrule", - ruleData: map[string]interface{}{ - "string": "teststring", - "int": 123, - }, - expectedRule: testCharsetRule{ - String: "teststring", - Integer: 123, - }, - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - reg := Registry{ - Rules: test.rules, - } - - actualRule, err := reg.parseRule(test.ruleType, test.ruleData) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(actualRule, test.expectedRule) { - t.Fatalf("Actual: %#v\nExpected:%#v", actualRule, test.expectedRule) - } - }) - } -} - -// Ensure the mappings in the defaultRuleNameMapping are consistent between the keys -// in the map and the Type() calls on the Rule values -func TestDefaultRuleNameMapping(t *testing.T) { - for expectedType, constructor := range defaultRuleNameMapping { - // In this case, we don't care about the error since we're checking the types, not the contents - instance, _ := constructor(map[string]interface{}{}) - actualType := instance.Type() - if actualType != expectedType { - t.Fatalf("Default registry mismatched types: Actual: %s Expected: %s", actualType, expectedType) - } - } -} diff --git a/helper/random/rules_test.go b/helper/random/rules_test.go deleted file mode 100644 index 535bac806..000000000 --- a/helper/random/rules_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package random - -import ( - "testing" -) - -func TestCharset(t *testing.T) { - type testCase struct { - charset string - minChars int - input string - expected bool - } - - tests := map[string]testCase{ - "0 minimum, empty input": { - charset: LowercaseCharset, - minChars: 0, - input: "", - expected: true, - }, - "0 minimum, many matching": { - charset: LowercaseCharset, - minChars: 0, - input: LowercaseCharset, - expected: true, - }, - "0 minimum, no matching": { - charset: LowercaseCharset, - minChars: 0, - input: "0123456789", - expected: true, - }, - "1 minimum, empty input": { - charset: LowercaseCharset, - minChars: 1, - input: "", - expected: false, - }, - "1 minimum, no matching": { - charset: LowercaseCharset, - minChars: 1, - input: "0123456789", - expected: false, - }, - "1 minimum, exactly 1 matching": { - charset: LowercaseCharset, - minChars: 1, - input: "a", - expected: true, - }, - "1 minimum, many matching": { - charset: LowercaseCharset, - minChars: 1, - input: "abcdefhaaaa", - expected: true, - }, - "2 minimum, 1 matching": { - charset: LowercaseCharset, - minChars: 2, - input: "f", - expected: false, - }, - "2 minimum, 2 matching": { - charset: LowercaseCharset, - minChars: 2, - input: "fz", - expected: true, - }, - "2 minimum, many matching": { - charset: LowercaseCharset, - minChars: 2, - input: "joixnbonxd", - expected: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - cr := CharsetRule{ - Charset: []rune(test.charset), - MinChars: test.minChars, - } - actual := cr.Pass([]rune(test.input)) - if actual != test.expected { - t.FailNow() - } - }) - } -} diff --git a/helper/random/serializing_test.go b/helper/random/serializing_test.go deleted file mode 100644 index b05afd660..000000000 --- a/helper/random/serializing_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package random - -import ( - "encoding/json" - "reflect" - "testing" -) - -func TestJSONMarshalling(t *testing.T) { - expected := serializableRules{ - CharsetRule{ - Charset: LowercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: UppercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: NumericRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: ShortSymbolRuneset, - MinChars: 1, - }, - } - - marshalled, err := json.Marshal(expected) - if err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - actual := serializableRules{} - err = json.Unmarshal(marshalled, &actual) - if err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Actual: %#v\nExpected: %#v", actual, expected) - } -} - -func TestRunes_UnmarshalJSON(t *testing.T) { - data := []byte(`"noaw8hgfsdjlkfsj3"`) - - expected := runes([]rune("noaw8hgfsdjlkfsj3")) - actual := runes{} - err := (&actual).UnmarshalJSON(data) - if err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Actual: %#v\nExpected: %#v", actual, expected) - } -} diff --git a/helper/random/string_generator_test.go b/helper/random/string_generator_test.go deleted file mode 100644 index c8ab3b6ac..000000000 --- a/helper/random/string_generator_test.go +++ /dev/null @@ -1,832 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package random - -import ( - "context" - "crypto/rand" - "encoding/json" - "fmt" - "io" - "math" - MRAND "math/rand" - "reflect" - "sort" - "testing" - "time" -) - -func TestStringGenerator_Generate_successful(t *testing.T) { - type testCase struct { - timeout time.Duration - generator *StringGenerator - } - - tests := map[string]testCase{ - "common rules": { - timeout: 1 * time.Second, - generator: &StringGenerator{ - Length: 20, - Rules: []Rule{ - CharsetRule{ - Charset: LowercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: UppercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: NumericRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: ShortSymbolRuneset, - MinChars: 1, - }, - }, - charset: AlphaNumericShortSymbolRuneset, - }, - }, - "charset not explicitly specified": { - timeout: 1 * time.Second, - generator: &StringGenerator{ - Length: 20, - Rules: []Rule{ - CharsetRule{ - Charset: LowercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: UppercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: NumericRuneset, - MinChars: 1, - }, - }, - }, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - // One context to rule them all, one context to find them, one context to bring them all and in the darkness bind them. - ctx, cancel := context.WithTimeout(context.Background(), test.timeout) - defer cancel() - - runeset := map[rune]bool{} - runesFound := []rune{} - - for i := 0; i < 100; i++ { - actual, err := test.generator.Generate(ctx, nil) - if err != nil { - t.Fatalf("no error expected, but got: %s", err) - } - for _, r := range actual { - if runeset[r] { - continue - } - runeset[r] = true - runesFound = append(runesFound, r) - } - } - - sort.Sort(runes(runesFound)) - - expectedCharset := getChars(test.generator.Rules) - - if !reflect.DeepEqual(runesFound, expectedCharset) { - t.Fatalf("Didn't find all characters from the charset\nActual : [%s]\nExpected: [%s]", string(runesFound), string(expectedCharset)) - } - }) - } -} - -func TestStringGenerator_Generate_errors(t *testing.T) { - type testCase struct { - timeout time.Duration - generator *StringGenerator - rng io.Reader - } - - tests := map[string]testCase{ - "already timed out": { - timeout: 0, - generator: &StringGenerator{ - Length: 20, - Rules: []Rule{ - testCharsetRule{ - fail: false, - }, - }, - charset: AlphaNumericShortSymbolRuneset, - }, - rng: rand.Reader, - }, - "impossible rules": { - timeout: 10 * time.Millisecond, // Keep this short so the test doesn't take too long - generator: &StringGenerator{ - Length: 20, - Rules: []Rule{ - testCharsetRule{ - fail: true, - }, - }, - charset: AlphaNumericShortSymbolRuneset, - }, - rng: rand.Reader, - }, - "bad RNG reader": { - timeout: 10 * time.Millisecond, // Keep this short so the test doesn't take too long - generator: &StringGenerator{ - Length: 20, - Rules: []Rule{}, - charset: AlphaNumericShortSymbolRuneset, - }, - rng: badReader{}, - }, - "0 length": { - timeout: 10 * time.Millisecond, - generator: &StringGenerator{ - Length: 0, - Rules: []Rule{ - CharsetRule{ - Charset: []rune("abcde"), - MinChars: 0, - }, - }, - charset: []rune("abcde"), - }, - rng: rand.Reader, - }, - "-1 length": { - timeout: 10 * time.Millisecond, - generator: &StringGenerator{ - Length: -1, - Rules: []Rule{ - CharsetRule{ - Charset: []rune("abcde"), - MinChars: 0, - }, - }, - charset: []rune("abcde"), - }, - rng: rand.Reader, - }, - "no charset": { - timeout: 10 * time.Millisecond, - generator: &StringGenerator{ - Length: 20, - Rules: []Rule{}, - }, - rng: rand.Reader, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - // One context to rule them all, one context to find them, one context to bring them all and in the darkness bind them. - ctx, cancel := context.WithTimeout(context.Background(), test.timeout) - defer cancel() - - actual, err := test.generator.Generate(ctx, test.rng) - if err == nil { - t.Fatalf("Expected error but none found") - } - if actual != "" { - t.Fatalf("Random string returned: %s", actual) - } - }) - } -} - -func TestRandomRunes_deterministic(t *testing.T) { - // These tests are to ensure that the charset selection doesn't do anything weird like selecting the same character - // over and over again. The number of test cases here should be kept to a minimum since they are sensitive to changes - type testCase struct { - rngSeed int64 - charset string - length int - expected string - } - - tests := map[string]testCase{ - "small charset": { - rngSeed: 1585593298447807000, - charset: "abcde", - length: 20, - expected: "ddddddcdebbeebdbdbcd", - }, - "common charset": { - rngSeed: 1585593298447807001, - charset: AlphaNumericShortSymbolCharset, - length: 20, - expected: "ON6lVjnBs84zJbUBVEzb", - }, - "max size charset": { - rngSeed: 1585593298447807002, - charset: " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + - "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" + - "ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" + - "šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠", - length: 20, - expected: "tųŎ℄ņ℃Œ.@řHš-ℍ}ħGIJLℏ", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - rng := MRAND.New(MRAND.NewSource(test.rngSeed)) - runes, err := randomRunes(rng, []rune(test.charset), test.length) - if err != nil { - t.Fatalf("Expected no error, but found: %s", err) - } - - str := string(runes) - - if str != test.expected { - t.Fatalf("Actual: %s Expected: %s", str, test.expected) - } - }) - } -} - -func TestRandomRunes_successful(t *testing.T) { - type testCase struct { - charset []rune // Assumes no duplicate runes - length int - } - - tests := map[string]testCase{ - "small charset": { - charset: []rune("abcde"), - length: 20, - }, - "common charset": { - charset: AlphaNumericShortSymbolRuneset, - length: 20, - }, - "max size charset": { - charset: []rune( - " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + - "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" + - "ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" + - "šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠", - ), - length: 20, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - runeset := map[rune]bool{} - runesFound := []rune{} - - for i := 0; i < 10000; i++ { - actual, err := randomRunes(rand.Reader, test.charset, test.length) - if err != nil { - t.Fatalf("no error expected, but got: %s", err) - } - for _, r := range actual { - if runeset[r] { - continue - } - runeset[r] = true - runesFound = append(runesFound, r) - } - } - - sort.Sort(runes(runesFound)) - - // Sort the input too just to ensure that they can be compared - sort.Sort(runes(test.charset)) - - if !reflect.DeepEqual(runesFound, test.charset) { - t.Fatalf("Didn't find all characters from the charset\nActual : [%s]\nExpected: [%s]", string(runesFound), string(test.charset)) - } - }) - } -} - -func TestRandomRunes_errors(t *testing.T) { - type testCase struct { - charset []rune - length int - rng io.Reader - } - - tests := map[string]testCase{ - "nil charset": { - charset: nil, - length: 20, - rng: rand.Reader, - }, - "empty charset": { - charset: []rune{}, - length: 20, - rng: rand.Reader, - }, - "charset is too long": { - charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + - "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" + - "ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" + - "šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠" + - "Σ", - ), - length: 20, - rng: rand.Reader, - }, - "length is zero": { - charset: []rune("abcde"), - length: 0, - rng: rand.Reader, - }, - "length is negative": { - charset: []rune("abcde"), - length: -3, - rng: rand.Reader, - }, - "reader failed": { - charset: []rune("abcde"), - length: 20, - rng: badReader{}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual, err := randomRunes(test.rng, test.charset, test.length) - if err == nil { - t.Fatalf("Expected error but none found") - } - if actual != nil { - t.Fatalf("Expected no value, but found [%s]", string(actual)) - } - }) - } -} - -func BenchmarkStringGenerator_Generate(b *testing.B) { - lengths := []int{ - 8, 12, 16, 20, 24, 28, - } - - type testCase struct { - generator *StringGenerator - } - - benches := map[string]testCase{ - "no restrictions": { - generator: &StringGenerator{ - Rules: []Rule{ - CharsetRule{ - Charset: AlphaNumericFullSymbolRuneset, - }, - }, - }, - }, - "default generator": { - generator: DefaultStringGenerator, - }, - "large symbol set": { - generator: &StringGenerator{ - Rules: []Rule{ - CharsetRule{ - Charset: LowercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: UppercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: NumericRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: FullSymbolRuneset, - MinChars: 1, - }, - }, - }, - }, - "max symbol set": { - generator: &StringGenerator{ - Rules: []Rule{ - CharsetRule{ - Charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + - "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" + - "ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" + - "šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠"), - }, - CharsetRule{ - Charset: LowercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: UppercaseRuneset, - MinChars: 1, - }, - CharsetRule{ - Charset: []rune("ĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒ"), - MinChars: 1, - }, - }, - }, - }, - "restrictive charset rules": { - generator: &StringGenerator{ - Rules: []Rule{ - CharsetRule{ - Charset: AlphaNumericShortSymbolRuneset, - }, - CharsetRule{ - Charset: []rune("A"), - MinChars: 1, - }, - CharsetRule{ - Charset: []rune("1"), - MinChars: 1, - }, - CharsetRule{ - Charset: []rune("a"), - MinChars: 1, - }, - CharsetRule{ - Charset: []rune("-"), - MinChars: 1, - }, - }, - }, - }, - } - - for name, bench := range benches { - b.Run(name, func(b *testing.B) { - for _, length := range lengths { - bench.generator.Length = length - b.Run(fmt.Sprintf("length=%d", length), func(b *testing.B) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - str, err := bench.generator.Generate(ctx, nil) - if err != nil { - b.Fatalf("Failed to generate string: %s", err) - } - if str == "" { - b.Fatalf("Didn't error but didn't generate a string") - } - } - }) - } - }) - } - - // Mimic what the SQLCredentialsProducer is doing - b.Run("SQLCredentialsProducer", func(b *testing.B) { - sg := StringGenerator{ - Length: 16, // 16 because the SQLCredentialsProducer prepends 4 characters to a 20 character password - charset: AlphaNumericRuneset, - Rules: nil, - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - str, err := sg.Generate(ctx, nil) - if err != nil { - b.Fatalf("Failed to generate string: %s", err) - } - if str == "" { - b.Fatalf("Didn't error but didn't generate a string") - } - } - }) -} - -// Ensure the StringGenerator can be properly JSON-ified -func TestStringGenerator_JSON(t *testing.T) { - expected := StringGenerator{ - Length: 20, - charset: deduplicateRunes([]rune("teststring" + ShortSymbolCharset)), - Rules: []Rule{ - testCharsetRule{ - String: "teststring", - Integer: 123, - }, - CharsetRule{ - Charset: ShortSymbolRuneset, - MinChars: 1, - }, - }, - } - - b, err := json.Marshal(expected) - if err != nil { - t.Fatalf("Failed to marshal to JSON: %s", err) - } - - parser := PolicyParser{ - RuleRegistry: Registry{ - Rules: map[string]ruleConstructor{ - "testrule": newTestRule, - "charset": ParseCharset, - }, - }, - } - actual, err := parser.ParsePolicy(string(b)) - if err != nil { - t.Fatalf("Failed to parse JSON: %s", err) - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Actual: %#v\nExpected: %#v", actual, expected) - } -} - -type badReader struct{} - -func (badReader) Read([]byte) (int, error) { - return 0, fmt.Errorf("test error") -} - -func TestValidate(t *testing.T) { - type testCase struct { - generator *StringGenerator - expectErr bool - } - - tests := map[string]testCase{ - "default generator": { - generator: DefaultStringGenerator, - expectErr: false, - }, - "length is 0": { - generator: &StringGenerator{ - Length: 0, - }, - expectErr: true, - }, - "length is negative": { - generator: &StringGenerator{ - Length: -2, - }, - expectErr: true, - }, - "nil charset, no rules": { - generator: &StringGenerator{ - Length: 5, - charset: nil, - }, - expectErr: true, - }, - "zero length charset, no rules": { - generator: &StringGenerator{ - Length: 5, - charset: []rune{}, - }, - expectErr: true, - }, - "rules require password longer than length": { - generator: &StringGenerator{ - Length: 5, - charset: []rune("abcde"), - Rules: []Rule{ - CharsetRule{ - Charset: []rune("abcde"), - MinChars: 6, - }, - }, - }, - expectErr: true, - }, - "charset has non-printable characters": { - generator: &StringGenerator{ - Length: 0, - charset: []rune{ - 'a', - 'b', - 0, // Null character - 'd', - 'e', - }, - }, - expectErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - err := test.generator.validateConfig() - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - }) - } -} - -type testNonCharsetRule struct { - String string `mapstructure:"string" json:"string"` -} - -func (tr testNonCharsetRule) Pass([]rune) bool { return true } -func (tr testNonCharsetRule) Type() string { return "testNonCharsetRule" } - -func TestGetChars(t *testing.T) { - type testCase struct { - rules []Rule - expected []rune - } - - tests := map[string]testCase{ - "nil rules": { - rules: nil, - expected: []rune(nil), - }, - "empty rules": { - rules: []Rule{}, - expected: []rune(nil), - }, - "rule without chars": { - rules: []Rule{ - testNonCharsetRule{ - String: "teststring", - }, - }, - expected: []rune(nil), - }, - "rule with chars": { - rules: []Rule{ - CharsetRule{ - Charset: []rune("abcdefghij"), - MinChars: 1, - }, - }, - expected: []rune("abcdefghij"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual := getChars(test.rules) - if !reflect.DeepEqual(actual, test.expected) { - t.Fatalf("Actual: %v\nExpected: %v", actual, test.expected) - } - }) - } -} - -func TestDeduplicateRunes(t *testing.T) { - type testCase struct { - input []rune - expected []rune - } - - tests := map[string]testCase{ - "empty string": { - input: []rune(""), - expected: []rune(nil), - }, - "no duplicates": { - input: []rune("abcde"), - expected: []rune("abcde"), - }, - "in order duplicates": { - input: []rune("aaaabbbbcccccccddddeeeee"), - expected: []rune("abcde"), - }, - "out of order duplicates": { - input: []rune("abcdeabcdeabcdeabcde"), - expected: []rune("abcde"), - }, - "unicode no duplicates": { - input: []rune("日本語"), - expected: []rune("日本語"), - }, - "unicode in order duplicates": { - input: []rune("日日日日本本本語語語語語"), - expected: []rune("日本語"), - }, - "unicode out of order duplicates": { - input: []rune("日本語日本語日本語日本語"), - expected: []rune("日本語"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual := deduplicateRunes(test.input) - if !reflect.DeepEqual(actual, test.expected) { - t.Fatalf("Actual: %#v\nExpected:%#v", actual, test.expected) - } - }) - } -} - -func TestRandomRunes_Bias(t *testing.T) { - type testCase struct { - charset []rune - maxStdDev float64 - } - - tests := map[string]testCase{ - "small charset": { - charset: []rune("abcde"), - maxStdDev: 2700, - }, - "lowercase characters": { - charset: LowercaseRuneset, - maxStdDev: 1000, - }, - "alphabetical characters": { - charset: AlphabeticRuneset, - maxStdDev: 800, - }, - "alphanumeric": { - charset: AlphaNumericRuneset, - maxStdDev: 800, - }, - "alphanumeric with symbol": { - charset: AlphaNumericShortSymbolRuneset, - maxStdDev: 800, - }, - "charset evenly divisible into 256": { - charset: append(AlphaNumericRuneset, '!', '@'), - maxStdDev: 800, - }, - "large charset": { - charset: FullSymbolRuneset, - maxStdDev: 800, - }, - "just under half size charset": { - charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + - "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğ"), - maxStdDev: 800, - }, - "half size charset": { - charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + - "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ"), - maxStdDev: 800, - }, - } - - for name, test := range tests { - t.Run(fmt.Sprintf("%s (%d chars)", name, len(test.charset)), func(t *testing.T) { - runeCounts := map[rune]int{} - - generations := 50000 - length := 100 - for i := 0; i < generations; i++ { - str, err := randomRunes(nil, test.charset, length) - if err != nil { - t.Fatal(err) - } - for _, r := range str { - runeCounts[r]++ - } - } - - chars := charCounts{} - - var sum float64 - for r, count := range runeCounts { - chars = append(chars, charCount{r, count}) - sum += float64(count) - } - - mean := sum / float64(len(runeCounts)) - var stdDev float64 - for _, count := range runeCounts { - stdDev += math.Pow(float64(count)-mean, 2) - } - - stdDev = math.Sqrt(stdDev / float64(len(runeCounts))) - t.Logf("Mean : %10.4f", mean) - - if stdDev > test.maxStdDev { - t.Fatalf("Standard deviation is too large: %.2f > %.2f", stdDev, test.maxStdDev) - } - }) - } -} - -type charCount struct { - r rune - count int -} - -type charCounts []charCount - -func (s charCounts) Len() int { return len(s) } -func (s charCounts) Less(i, j int) bool { return s[i].r < s[j].r } -func (s charCounts) Swap(i, j int) { s[i], s[j] = s[j], s[i] } diff --git a/helper/storagepacker/storagepacker_test.go b/helper/storagepacker/storagepacker_test.go deleted file mode 100644 index e4799e40e..000000000 --- a/helper/storagepacker/storagepacker_test.go +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package storagepacker - -import ( - "context" - "fmt" - "testing" - - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/ptypes" - log "github.com/hashicorp/go-hclog" - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/helper/identity" - "github.com/hashicorp/vault/sdk/logical" -) - -func BenchmarkStoragePacker(b *testing.B) { - storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "") - if err != nil { - b.Fatal(err) - } - - ctx := context.Background() - - for i := 0; i < b.N; i++ { - itemID, err := uuid.GenerateUUID() - if err != nil { - b.Fatal(err) - } - - item := &Item{ - ID: itemID, - } - - err = storagePacker.PutItem(ctx, item) - if err != nil { - b.Fatal(err) - } - - fetchedItem, err := storagePacker.GetItem(itemID) - if err != nil { - b.Fatal(err) - } - - if fetchedItem == nil { - b.Fatalf("failed to read stored item with ID: %q, iteration: %d", item.ID, i) - } - - if fetchedItem.ID != item.ID { - b.Fatalf("bad: item ID; expected: %q\n actual: %q", item.ID, fetchedItem.ID) - } - - err = storagePacker.DeleteItem(ctx, item.ID) - if err != nil { - b.Fatal(err) - } - - fetchedItem, err = storagePacker.GetItem(item.ID) - if err != nil { - b.Fatal(err) - } - if fetchedItem != nil { - b.Fatalf("failed to delete item") - } - } -} - -func TestStoragePacker(t *testing.T) { - storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "") - if err != nil { - t.Fatal(err) - } - - ctx := context.Background() - - // Persist a storage entry - item1 := &Item{ - ID: "item1", - } - - err = storagePacker.PutItem(ctx, item1) - if err != nil { - t.Fatal(err) - } - - // Verify that it can be read - fetchedItem, err := storagePacker.GetItem(item1.ID) - if err != nil { - t.Fatal(err) - } - if fetchedItem == nil { - t.Fatalf("failed to read the stored item") - } - - if item1.ID != fetchedItem.ID { - t.Fatalf("bad: item ID; expected: %q\n actual: %q\n", item1.ID, fetchedItem.ID) - } - - // Delete item1 - err = storagePacker.DeleteItem(ctx, item1.ID) - if err != nil { - t.Fatal(err) - } - - // Check that the deletion was successful - fetchedItem, err = storagePacker.GetItem(item1.ID) - if err != nil { - t.Fatal(err) - } - - if fetchedItem != nil { - t.Fatalf("failed to delete item") - } -} - -func TestStoragePacker_SerializeDeserializeComplexItem(t *testing.T) { - storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "") - if err != nil { - t.Fatal(err) - } - - ctx := context.Background() - - timeNow := ptypes.TimestampNow() - - alias1 := &identity.Alias{ - ID: "alias_id", - CanonicalID: "canonical_id", - MountType: "mount_type", - MountAccessor: "mount_accessor", - Metadata: map[string]string{ - "aliasmkey": "aliasmvalue", - }, - Name: "alias_name", - CreationTime: timeNow, - LastUpdateTime: timeNow, - MergedFromCanonicalIDs: []string{"merged_from_canonical_id"}, - } - - entity := &identity.Entity{ - Aliases: []*identity.Alias{alias1}, - ID: "entity_id", - Name: "entity_name", - Metadata: map[string]string{ - "testkey1": "testvalue1", - "testkey2": "testvalue2", - }, - CreationTime: timeNow, - LastUpdateTime: timeNow, - BucketKey: "entity_hash", - MergedEntityIDs: []string{"merged_entity_id1", "merged_entity_id2"}, - Policies: []string{"policy1", "policy2"}, - } - - marshaledEntity, err := ptypes.MarshalAny(entity) - if err != nil { - t.Fatal(err) - } - err = storagePacker.PutItem(ctx, &Item{ - ID: entity.ID, - Message: marshaledEntity, - }) - if err != nil { - t.Fatal(err) - } - - itemFetched, err := storagePacker.GetItem(entity.ID) - if err != nil { - t.Fatal(err) - } - - var itemDecoded identity.Entity - err = ptypes.UnmarshalAny(itemFetched.Message, &itemDecoded) - if err != nil { - t.Fatal(err) - } - - if !proto.Equal(&itemDecoded, entity) { - t.Fatalf("bad: expected: %#v\nactual: %#v\n", entity, itemDecoded) - } -} - -func TestStoragePacker_DeleteMultiple(t *testing.T) { - storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "") - if err != nil { - t.Fatal(err) - } - - ctx := context.Background() - - // Persist a storage entry - for i := 0; i < 100; i++ { - item := &Item{ - ID: fmt.Sprintf("item%d", i), - } - - err = storagePacker.PutItem(ctx, item) - if err != nil { - t.Fatal(err) - } - - // Verify that it can be read - fetchedItem, err := storagePacker.GetItem(item.ID) - if err != nil { - t.Fatal(err) - } - if fetchedItem == nil { - t.Fatalf("failed to read the stored item") - } - - if item.ID != fetchedItem.ID { - t.Fatalf("bad: item ID; expected: %q\n actual: %q\n", item.ID, fetchedItem.ID) - } - } - - itemsToDelete := make([]string, 0, 50) - for i := 1; i < 100; i += 2 { - itemsToDelete = append(itemsToDelete, fmt.Sprintf("item%d", i)) - } - - err = storagePacker.DeleteMultipleItems(ctx, nil, itemsToDelete) - if err != nil { - t.Fatal(err) - } - - // Check that the deletion was successful - for i := 0; i < 100; i++ { - fetchedItem, err := storagePacker.GetItem(fmt.Sprintf("item%d", i)) - if err != nil { - t.Fatal(err) - } - - if i%2 == 0 && fetchedItem == nil { - t.Fatal("expected item not found") - } - if i%2 == 1 && fetchedItem != nil { - t.Fatalf("failed to delete item") - } - } -} - -func TestStoragePacker_DeleteMultiple_ALL(t *testing.T) { - storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "") - if err != nil { - t.Fatal(err) - } - - ctx := context.Background() - - // Persist a storage entry - itemsToDelete := make([]string, 0, 10000) - for i := 0; i < 10000; i++ { - item := &Item{ - ID: fmt.Sprintf("item%d", i), - } - - err = storagePacker.PutItem(ctx, item) - if err != nil { - t.Fatal(err) - } - - // Verify that it can be read - fetchedItem, err := storagePacker.GetItem(item.ID) - if err != nil { - t.Fatal(err) - } - if fetchedItem == nil { - t.Fatalf("failed to read the stored item") - } - - if item.ID != fetchedItem.ID { - t.Fatalf("bad: item ID; expected: %q\n actual: %q\n", item.ID, fetchedItem.ID) - } - - itemsToDelete = append(itemsToDelete, fmt.Sprintf("item%d", i)) - } - - err = storagePacker.DeleteMultipleItems(ctx, nil, itemsToDelete) - if err != nil { - t.Fatal(err) - } - - // Check that the deletion was successful - for _, item := range itemsToDelete { - fetchedItem, err := storagePacker.GetItem(item) - if err != nil { - t.Fatal(err) - } - if fetchedItem != nil { - t.Fatal("item not deleted") - } - } -} diff --git a/helper/testhelpers/cassandra/cassandrahelper.go b/helper/testhelpers/cassandra/cassandrahelper.go deleted file mode 100644 index f6ee1f2c2..000000000 --- a/helper/testhelpers/cassandra/cassandrahelper.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cassandra - -import ( - "context" - "fmt" - "net" - "os" - "path/filepath" - "testing" - "time" - - "github.com/gocql/gocql" - "github.com/hashicorp/vault/sdk/helper/docker" -) - -type containerConfig struct { - containerName string - imageName string - version string - copyFromTo map[string]string - env []string - - sslOpts *gocql.SslOptions -} - -type ContainerOpt func(*containerConfig) - -func ContainerName(name string) ContainerOpt { - return func(cfg *containerConfig) { - cfg.containerName = name - } -} - -func Image(imageName string, version string) ContainerOpt { - return func(cfg *containerConfig) { - cfg.imageName = imageName - cfg.version = version - - // Reset the environment because there's a very good chance the default environment doesn't apply to the - // non-default image being used - cfg.env = nil - } -} - -func Version(version string) ContainerOpt { - return func(cfg *containerConfig) { - cfg.version = version - } -} - -func CopyFromTo(copyFromTo map[string]string) ContainerOpt { - return func(cfg *containerConfig) { - cfg.copyFromTo = copyFromTo - } -} - -func Env(keyValue string) ContainerOpt { - return func(cfg *containerConfig) { - cfg.env = append(cfg.env, keyValue) - } -} - -func SslOpts(sslOpts *gocql.SslOptions) ContainerOpt { - return func(cfg *containerConfig) { - cfg.sslOpts = sslOpts - } -} - -type Host struct { - Name string - Port string -} - -func (h Host) ConnectionURL() string { - return net.JoinHostPort(h.Name, h.Port) -} - -func PrepareTestContainer(t *testing.T, opts ...ContainerOpt) (Host, func()) { - t.Helper() - if os.Getenv("CASSANDRA_HOSTS") != "" { - host, port, err := net.SplitHostPort(os.Getenv("CASSANDRA_HOSTS")) - if err != nil { - t.Fatalf("Failed to split host & port from CASSANDRA_HOSTS (%s): %s", os.Getenv("CASSANDRA_HOSTS"), err) - } - h := Host{ - Name: host, - Port: port, - } - return h, func() {} - } - - containerCfg := &containerConfig{ - imageName: "docker.mirror.hashicorp.services/library/cassandra", - containerName: "cassandra", - version: "3.11", - env: []string{"CASSANDRA_BROADCAST_ADDRESS=127.0.0.1"}, - } - - for _, opt := range opts { - opt(containerCfg) - } - - copyFromTo := map[string]string{} - for from, to := range containerCfg.copyFromTo { - absFrom, err := filepath.Abs(from) - if err != nil { - t.Fatalf("Unable to get absolute path for file %s", from) - } - copyFromTo[absFrom] = to - } - - runOpts := docker.RunOptions{ - ContainerName: containerCfg.containerName, - ImageRepo: containerCfg.imageName, - ImageTag: containerCfg.version, - Ports: []string{"9042/tcp"}, - CopyFromTo: copyFromTo, - Env: containerCfg.env, - } - runner, err := docker.NewServiceRunner(runOpts) - if err != nil { - t.Fatalf("Could not start docker cassandra: %s", err) - } - - svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - cfg := docker.NewServiceHostPort(host, port) - clusterConfig := gocql.NewCluster(cfg.Address()) - clusterConfig.Authenticator = gocql.PasswordAuthenticator{ - Username: "cassandra", - Password: "cassandra", - } - clusterConfig.Timeout = 30 * time.Second - clusterConfig.ProtoVersion = 4 - clusterConfig.Port = port - - clusterConfig.SslOpts = containerCfg.sslOpts - - session, err := clusterConfig.CreateSession() - if err != nil { - return nil, fmt.Errorf("error creating session: %s", err) - } - defer session.Close() - - // Create keyspace - query := session.Query(`CREATE KEYSPACE "vault" WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };`) - if err := query.Exec(); err != nil { - t.Fatalf("could not create cassandra keyspace: %v", err) - } - - // Create table - query = session.Query(`CREATE TABLE "vault"."entries" ( - bucket text, - key text, - value blob, - PRIMARY KEY (bucket, key) - ) WITH CLUSTERING ORDER BY (key ASC);`) - if err := query.Exec(); err != nil { - t.Fatalf("could not create cassandra table: %v", err) - } - return cfg, nil - }) - if err != nil { - t.Fatalf("Could not start docker cassandra: %s", err) - } - - host, port, err := net.SplitHostPort(svc.Config.Address()) - if err != nil { - t.Fatalf("Failed to split host & port from address (%s): %s", svc.Config.Address(), err) - } - h := Host{ - Name: host, - Port: port, - } - return h, svc.Cleanup -} diff --git a/helper/testhelpers/certhelpers/cert_helpers.go b/helper/testhelpers/certhelpers/cert_helpers.go deleted file mode 100644 index d9c89735c..000000000 --- a/helper/testhelpers/certhelpers/cert_helpers.go +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package certhelpers - -import ( - "bytes" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/sha1" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" - "net" - "strings" - "testing" - "time" -) - -type CertBuilder struct { - tmpl *x509.Certificate - parentTmpl *x509.Certificate - - selfSign bool - parentKey *rsa.PrivateKey - - isCA bool -} - -type CertOpt func(*CertBuilder) error - -func CommonName(cn string) CertOpt { - return func(builder *CertBuilder) error { - builder.tmpl.Subject.CommonName = cn - return nil - } -} - -func Parent(parent Certificate) CertOpt { - return func(builder *CertBuilder) error { - builder.parentKey = parent.PrivKey.PrivKey - builder.parentTmpl = parent.Template - return nil - } -} - -func IsCA(isCA bool) CertOpt { - return func(builder *CertBuilder) error { - builder.isCA = isCA - return nil - } -} - -func SelfSign() CertOpt { - return func(builder *CertBuilder) error { - builder.selfSign = true - return nil - } -} - -func IP(ip ...string) CertOpt { - return func(builder *CertBuilder) error { - for _, addr := range ip { - if ipAddr := net.ParseIP(addr); ipAddr != nil { - builder.tmpl.IPAddresses = append(builder.tmpl.IPAddresses, ipAddr) - } - } - return nil - } -} - -func DNS(dns ...string) CertOpt { - return func(builder *CertBuilder) error { - builder.tmpl.DNSNames = dns - return nil - } -} - -func NewCert(t *testing.T, opts ...CertOpt) (cert Certificate) { - t.Helper() - - builder := CertBuilder{ - tmpl: &x509.Certificate{ - SerialNumber: makeSerial(t), - Subject: pkix.Name{ - CommonName: makeCommonName(), - }, - NotBefore: time.Now().Add(-1 * time.Hour), - NotAfter: time.Now().Add(1 * time.Hour), - IsCA: false, - KeyUsage: x509.KeyUsageDigitalSignature | - x509.KeyUsageKeyEncipherment | - x509.KeyUsageKeyAgreement, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - }, - } - - for _, opt := range opts { - err := opt(&builder) - if err != nil { - t.Fatalf("Failed to set up certificate builder: %s", err) - } - } - - key := NewPrivateKey(t) - - builder.tmpl.SubjectKeyId = getSubjKeyID(t, key.PrivKey) - - tmpl := builder.tmpl - parent := builder.parentTmpl - publicKey := key.PrivKey.Public() - signingKey := builder.parentKey - - if builder.selfSign { - parent = tmpl - signingKey = key.PrivKey - } - - if builder.isCA { - tmpl.IsCA = true - tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign - tmpl.ExtKeyUsage = nil - } else { - tmpl.KeyUsage = x509.KeyUsageDigitalSignature | - x509.KeyUsageKeyEncipherment | - x509.KeyUsageKeyAgreement - tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} - } - - certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, parent, publicKey, signingKey) - if err != nil { - t.Fatalf("Unable to generate certificate: %s", err) - } - certPem := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: certBytes, - }) - - tlsCert, err := tls.X509KeyPair(certPem, key.Pem) - if err != nil { - t.Fatalf("Unable to parse X509 key pair: %s", err) - } - - return Certificate{ - Template: tmpl, - PrivKey: key, - TLSCert: tlsCert, - RawCert: certBytes, - Pem: certPem, - IsCA: builder.isCA, - } -} - -// //////////////////////////////////////////////////////////////////////////// -// Private Key -// //////////////////////////////////////////////////////////////////////////// -type KeyWrapper struct { - PrivKey *rsa.PrivateKey - Pem []byte -} - -func NewPrivateKey(t *testing.T) (key KeyWrapper) { - t.Helper() - - privKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatalf("Unable to generate key for cert: %s", err) - } - - privKeyPem := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privKey), - }, - ) - - key = KeyWrapper{ - PrivKey: privKey, - Pem: privKeyPem, - } - - return key -} - -// //////////////////////////////////////////////////////////////////////////// -// Certificate -// //////////////////////////////////////////////////////////////////////////// -type Certificate struct { - PrivKey KeyWrapper - Template *x509.Certificate - TLSCert tls.Certificate - RawCert []byte - Pem []byte - IsCA bool -} - -func (cert Certificate) CombinedPEM() []byte { - if cert.IsCA { - return cert.Pem - } - return bytes.Join([][]byte{cert.PrivKey.Pem, cert.Pem}, []byte{'\n'}) -} - -func (cert Certificate) PrivateKeyPEM() []byte { - return cert.PrivKey.Pem -} - -// //////////////////////////////////////////////////////////////////////////// -// Helpers -// //////////////////////////////////////////////////////////////////////////// -func makeSerial(t *testing.T) *big.Int { - t.Helper() - - v := &big.Int{} - serialNumberLimit := v.Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - t.Fatalf("Unable to generate serial number: %s", err) - } - return serialNumber -} - -// Pulled from sdk/helper/certutil & slightly modified for test usage -func getSubjKeyID(t *testing.T, privateKey crypto.Signer) []byte { - t.Helper() - - if privateKey == nil { - t.Fatalf("passed-in private key is nil") - } - - marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public()) - if err != nil { - t.Fatalf("error marshalling public key: %s", err) - } - - subjKeyID := sha1.Sum(marshaledKey) - - return subjKeyID[:] -} - -func makeCommonName() (cn string) { - return strings.ReplaceAll(time.Now().Format("20060102T150405.000"), ".", "") -} diff --git a/helper/testhelpers/consul/cluster_storage.go b/helper/testhelpers/consul/cluster_storage.go deleted file mode 100644 index 9ca1080c6..000000000 --- a/helper/testhelpers/consul/cluster_storage.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - "fmt" - - "github.com/hashicorp/vault/sdk/helper/testcluster" -) - -type ClusterStorage struct { - // Set these after calling `NewConsulClusterStorage` but before `Start` (or - // passing in to NewDockerCluster) to control Consul version specifically in - // your test. Leave empty for latest OSS (defined in consulhelper.go). - ConsulVersion string - ConsulEnterprise bool - - cleanup func() - config *Config -} - -var _ testcluster.ClusterStorage = &ClusterStorage{} - -func NewClusterStorage() *ClusterStorage { - return &ClusterStorage{} -} - -func (s *ClusterStorage) Start(ctx context.Context, opts *testcluster.ClusterOptions) error { - prefix := "" - if opts != nil && opts.ClusterName != "" { - prefix = fmt.Sprintf("%s-", opts.ClusterName) - } - cleanup, config, err := RunContainer(ctx, prefix, s.ConsulVersion, s.ConsulEnterprise, true) - if err != nil { - return err - } - s.cleanup = cleanup - s.config = config - - return nil -} - -func (s *ClusterStorage) Cleanup() error { - if s.cleanup != nil { - s.cleanup() - s.cleanup = nil - } - return nil -} - -func (s *ClusterStorage) Opts() map[string]interface{} { - if s.config == nil { - return nil - } - return map[string]interface{}{ - "address": s.config.ContainerHTTPAddr, - "token": s.config.Token, - "max_parallel": "32", - } -} - -func (s *ClusterStorage) Type() string { - return "consul" -} - -func (s *ClusterStorage) Config() *Config { - return s.config -} diff --git a/helper/testhelpers/consul/consulhelper.go b/helper/testhelpers/consul/consulhelper.go deleted file mode 100644 index d6ab5d72b..000000000 --- a/helper/testhelpers/consul/consulhelper.go +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - "fmt" - "os" - "strings" - "testing" - - consulapi "github.com/hashicorp/consul/api" - goversion "github.com/hashicorp/go-version" - "github.com/hashicorp/vault/sdk/helper/docker" -) - -// LatestConsulVersion is the most recent version of Consul which is used unless -// another version is specified in the test config or environment. This will -// probably go stale as we don't always update it on every release but we rarely -// rely on specific new Consul functionality so that's probably not a problem. -const LatestConsulVersion = "1.15.3" - -type Config struct { - docker.ServiceHostPort - Token string - ContainerHTTPAddr string -} - -func (c *Config) APIConfig() *consulapi.Config { - apiConfig := consulapi.DefaultConfig() - apiConfig.Address = c.Address() - apiConfig.Token = c.Token - return apiConfig -} - -// PrepareTestContainer is a test helper that creates a Consul docker container -// or fails the test if unsuccessful. See RunContainer for more details on the -// configuration. -func PrepareTestContainer(t *testing.T, version string, isEnterprise bool, doBootstrapSetup bool) (func(), *Config) { - t.Helper() - - cleanup, config, err := RunContainer(context.Background(), "", version, isEnterprise, doBootstrapSetup) - if err != nil { - t.Fatalf("failed starting consul: %s", err) - } - return cleanup, config -} - -// RunContainer runs Consul in a Docker container unless CONSUL_HTTP_ADDR is -// already found in the environment. Consul version is determined by the version -// argument. If version is empty string, the CONSUL_DOCKER_VERSION environment -// variable is used and if that is empty too, LatestConsulVersion is used -// (defined above). If namePrefix is provided we assume you have chosen a unique -// enough prefix to avoid collision with other tests that may be running in -// parallel and so _do not_ add an additional unique ID suffix. We will also -// ensure previous instances are deleted and leave the container running for -// debugging. This is useful for using Consul as part of at testcluster (i.e. -// when Vault is in Docker too). If namePrefix is empty then a unique suffix is -// added since many older tests rely on a uniq instance of the container. This -// is used by `PrepareTestContainer` which is used typically in tests that rely -// on Consul but run tested code within the test process. -func RunContainer(ctx context.Context, namePrefix, version string, isEnterprise bool, doBootstrapSetup bool) (func(), *Config, error) { - if retAddress := os.Getenv("CONSUL_HTTP_ADDR"); retAddress != "" { - shp, err := docker.NewServiceHostPortParse(retAddress) - if err != nil { - return nil, nil, err - } - return func() {}, &Config{ServiceHostPort: *shp, Token: os.Getenv("CONSUL_HTTP_TOKEN")}, nil - } - - config := `acl { enabled = true default_policy = "deny" }` - if version == "" { - consulVersion := os.Getenv("CONSUL_DOCKER_VERSION") - if consulVersion != "" { - version = consulVersion - } else { - version = LatestConsulVersion - } - } - if strings.HasPrefix(version, "1.3") { - config = `datacenter = "test" acl_default_policy = "deny" acl_datacenter = "test" acl_master_token = "test"` - } - - name := "consul" - repo := "docker.mirror.hashicorp.services/library/consul" - var envVars []string - // If running the enterprise container, set the appropriate values below. - if isEnterprise { - version += "-ent" - name = "consul-enterprise" - repo = "docker.mirror.hashicorp.services/hashicorp/consul-enterprise" - license, hasLicense := os.LookupEnv("CONSUL_LICENSE") - envVars = append(envVars, "CONSUL_LICENSE="+license) - - if !hasLicense { - return nil, nil, fmt.Errorf("Failed to find enterprise license") - } - } - if namePrefix != "" { - name = namePrefix + name - } - - if dockerRepo, hasEnvRepo := os.LookupEnv("CONSUL_DOCKER_REPO"); hasEnvRepo { - repo = dockerRepo - } - - dockerOpts := docker.RunOptions{ - ContainerName: name, - ImageRepo: repo, - ImageTag: version, - Env: envVars, - Cmd: []string{"agent", "-dev", "-client", "0.0.0.0", "-hcl", config}, - Ports: []string{"8500/tcp"}, - AuthUsername: os.Getenv("CONSUL_DOCKER_USERNAME"), - AuthPassword: os.Getenv("CONSUL_DOCKER_PASSWORD"), - } - - // Add a unique suffix if there is no per-test prefix provided - addSuffix := true - if namePrefix != "" { - // Don't add a suffix if the caller already provided a prefix - addSuffix = false - // Also enable predelete and non-removal to make debugging easier for test - // cases with named containers). - dockerOpts.PreDelete = true - dockerOpts.DoNotAutoRemove = true - } - - runner, err := docker.NewServiceRunner(dockerOpts) - if err != nil { - return nil, nil, fmt.Errorf("Could not start docker Consul: %s", err) - } - - svc, _, err := runner.StartNewService(ctx, addSuffix, false, func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - shp := docker.NewServiceHostPort(host, port) - apiConfig := consulapi.DefaultNonPooledConfig() - apiConfig.Address = shp.Address() - consul, err := consulapi.NewClient(apiConfig) - if err != nil { - return nil, err - } - - // Make sure Consul is up - if _, err = consul.Status().Leader(); err != nil { - return nil, err - } - - // For version of Consul < 1.4 - if strings.HasPrefix(version, "1.3") { - consulToken := "test" - _, err = consul.KV().Put(&consulapi.KVPair{ - Key: "setuptest", - Value: []byte("setuptest"), - }, &consulapi.WriteOptions{ - Token: consulToken, - }) - if err != nil { - return nil, err - } - return &Config{ - ServiceHostPort: *shp, - Token: consulToken, - }, nil - } - - // New default behavior - var consulToken string - if doBootstrapSetup { - aclbootstrap, _, err := consul.ACL().Bootstrap() - if err != nil { - return nil, err - } - consulToken = aclbootstrap.SecretID - policy := &consulapi.ACLPolicy{ - Name: "test", - Description: "test", - Rules: `node_prefix "" { - policy = "write" - } - - service_prefix "" { - policy = "read" - }`, - } - q := &consulapi.WriteOptions{ - Token: consulToken, - } - _, _, err = consul.ACL().PolicyCreate(policy, q) - if err != nil { - return nil, err - } - - // Create a Consul role that contains the test policy, for Consul 1.5 and newer - currVersion, _ := goversion.NewVersion(version) - roleVersion, _ := goversion.NewVersion("1.5") - if currVersion.GreaterThanOrEqual(roleVersion) { - ACLList := []*consulapi.ACLTokenRoleLink{{Name: "test"}} - - role := &consulapi.ACLRole{ - Name: "role-test", - Description: "consul roles test", - Policies: ACLList, - } - - _, _, err = consul.ACL().RoleCreate(role, q) - if err != nil { - return nil, err - } - } - - // Configure a namespace and partition if testing enterprise Consul - if isEnterprise { - // Namespaces require Consul 1.7 or newer - namespaceVersion, _ := goversion.NewVersion("1.7") - if currVersion.GreaterThanOrEqual(namespaceVersion) { - namespace := &consulapi.Namespace{ - Name: "ns1", - Description: "ns1 test", - } - - _, _, err = consul.Namespaces().Create(namespace, q) - if err != nil { - return nil, err - } - - nsPolicy := &consulapi.ACLPolicy{ - Name: "ns-test", - Description: "namespace test", - Namespace: "ns1", - Rules: `service_prefix "" { - policy = "read" - }`, - } - _, _, err = consul.ACL().PolicyCreate(nsPolicy, q) - if err != nil { - return nil, err - } - } - - // Partitions require Consul 1.11 or newer - partitionVersion, _ := goversion.NewVersion("1.11") - if currVersion.GreaterThanOrEqual(partitionVersion) { - partition := &consulapi.Partition{ - Name: "part1", - Description: "part1 test", - } - - _, _, err = consul.Partitions().Create(ctx, partition, q) - if err != nil { - return nil, err - } - - partPolicy := &consulapi.ACLPolicy{ - Name: "part-test", - Description: "partition test", - Partition: "part1", - Rules: `service_prefix "" { - policy = "read" - }`, - } - _, _, err = consul.ACL().PolicyCreate(partPolicy, q) - if err != nil { - return nil, err - } - } - } - } - - return &Config{ - ServiceHostPort: *shp, - Token: consulToken, - }, nil - }) - if err != nil { - return nil, nil, err - } - - // Find the container network info. - if len(svc.Container.NetworkSettings.Networks) < 1 { - svc.Cleanup() - return nil, nil, fmt.Errorf("failed to find any network settings for container") - } - cfg := svc.Config.(*Config) - for _, eps := range svc.Container.NetworkSettings.Networks { - // Just pick the first network, we assume only one for now. - // Pull out the real container IP and set that up - cfg.ContainerHTTPAddr = fmt.Sprintf("http://%s:8500", eps.IPAddress) - break - } - return svc.Cleanup, cfg, nil -} diff --git a/helper/testhelpers/corehelpers/corehelpers.go b/helper/testhelpers/corehelpers/corehelpers.go deleted file mode 100644 index d43ef60ac..000000000 --- a/helper/testhelpers/corehelpers/corehelpers.go +++ /dev/null @@ -1,421 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -// Package corehelpers contains testhelpers that don't depend on package vault, -// and thus can be used within vault (as well as elsewhere.) -package corehelpers - -import ( - "bytes" - "context" - "crypto/sha256" - "io" - "os" - "path/filepath" - "sync" - "time" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/audit" - "github.com/hashicorp/vault/builtin/credential/approle" - "github.com/hashicorp/vault/plugins/database/mysql" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/salt" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/go-testing-interface" -) - -var externalPlugins = []string{"transform", "kmip", "keymgmt"} - -// RetryUntil runs f until it returns a nil result or the timeout is reached. -// If a nil result hasn't been obtained by timeout, calls t.Fatal. -func RetryUntil(t testing.T, timeout time.Duration, f func() error) { - t.Helper() - deadline := time.Now().Add(timeout) - var err error - for time.Now().Before(deadline) { - if err = f(); err == nil { - return - } - time.Sleep(100 * time.Millisecond) - } - t.Fatalf("did not complete before deadline, err: %v", err) -} - -// MakeTestPluginDir creates a temporary directory suitable for holding plugins. -// This helper also resolves symlinks to make tests happy on OS X. -func MakeTestPluginDir(t testing.T) (string, func(t testing.T)) { - if t != nil { - t.Helper() - } - - dir, err := os.MkdirTemp("", "") - if err != nil { - if t == nil { - panic(err) - } - t.Fatal(err) - } - - // OSX tempdir are /var, but actually symlinked to /private/var - dir, err = filepath.EvalSymlinks(dir) - if err != nil { - if t == nil { - panic(err) - } - t.Fatal(err) - } - - return dir, func(t testing.T) { - if err := os.RemoveAll(dir); err != nil { - if t == nil { - panic(err) - } - t.Fatal(err) - } - } -} - -func NewMockBuiltinRegistry() *mockBuiltinRegistry { - return &mockBuiltinRegistry{ - forTesting: map[string]mockBackend{ - "mysql-database-plugin": {PluginType: consts.PluginTypeDatabase}, - "postgresql-database-plugin": {PluginType: consts.PluginTypeDatabase}, - "approle": {PluginType: consts.PluginTypeCredential}, - "pending-removal-test-plugin": { - PluginType: consts.PluginTypeCredential, - DeprecationStatus: consts.PendingRemoval, - }, - "consul": {PluginType: consts.PluginTypeSecrets}, - }, - } -} - -type mockBackend struct { - consts.PluginType - consts.DeprecationStatus -} - -type mockBuiltinRegistry struct { - forTesting map[string]mockBackend -} - -func toFunc(f logical.Factory) func() (interface{}, error) { - return func() (interface{}, error) { - return f, nil - } -} - -func (m *mockBuiltinRegistry) Get(name string, pluginType consts.PluginType) (func() (interface{}, error), bool) { - testBackend, ok := m.forTesting[name] - if !ok { - return nil, false - } - testPluginType := testBackend.PluginType - if pluginType != testPluginType { - return nil, false - } - - switch name { - case "approle", "pending-removal-test-plugin": - return toFunc(approle.Factory), true - case "postgresql-database-plugin": - return toFunc(func(ctx context.Context, config *logical.BackendConfig) (logical.Backend, error) { - b := new(framework.Backend) - b.Setup(ctx, config) - b.BackendType = logical.TypeLogical - return b, nil - }), true - case "mysql-database-plugin": - return mysql.New(mysql.DefaultUserNameTemplate), true - case "consul": - return toFunc(func(ctx context.Context, config *logical.BackendConfig) (logical.Backend, error) { - b := new(framework.Backend) - b.Setup(ctx, config) - b.BackendType = logical.TypeLogical - return b, nil - }), true - default: - return nil, false - } -} - -// Keys only supports getting a realistic list of the keys for database plugins, -// and approle -func (m *mockBuiltinRegistry) Keys(pluginType consts.PluginType) []string { - switch pluginType { - case consts.PluginTypeDatabase: - // This is a hard-coded reproduction of the db plugin keys in - // helper/builtinplugins/registry.go. The registry isn't directly used - // because it causes import cycles. - return []string{ - "mysql-database-plugin", - "mysql-aurora-database-plugin", - "mysql-rds-database-plugin", - "mysql-legacy-database-plugin", - - "cassandra-database-plugin", - "influxdb-database-plugin", - "postgresql-database-plugin", - } - case consts.PluginTypeCredential: - return []string{ - "pending-removal-test-plugin", - "approle", - } - - case consts.PluginTypeSecrets: - return append(externalPlugins, "kv") - } - - return []string{} -} - -func (r *mockBuiltinRegistry) IsBuiltinEntPlugin(name string, pluginType consts.PluginType) bool { - for _, i := range externalPlugins { - if i == name { - return true - } - } - return false -} - -func (m *mockBuiltinRegistry) Contains(name string, pluginType consts.PluginType) bool { - for _, key := range m.Keys(pluginType) { - if key == name { - return true - } - } - return false -} - -func (m *mockBuiltinRegistry) DeprecationStatus(name string, pluginType consts.PluginType) (consts.DeprecationStatus, bool) { - if m.Contains(name, pluginType) { - return m.forTesting[name].DeprecationStatus, true - } - - return consts.Unknown, false -} - -func TestNoopAudit(t testing.T, config map[string]string) *NoopAudit { - n, err := NewNoopAudit(config) - if err != nil { - t.Fatal(err) - } - return n -} - -func NewNoopAudit(config map[string]string) (*NoopAudit, error) { - view := &logical.InmemStorage{} - err := view.Put(context.Background(), &logical.StorageEntry{ - Key: "salt", - Value: []byte("foo"), - }) - if err != nil { - return nil, err - } - - n := &NoopAudit{ - Config: &audit.BackendConfig{ - SaltView: view, - SaltConfig: &salt.Config{ - HMAC: sha256.New, - HMACType: "hmac-sha256", - }, - Config: config, - }, - } - n.formatter.AuditFormatWriter = &audit.JSONFormatWriter{ - SaltFunc: n.Salt, - } - return n, nil -} - -func NoopAuditFactory(records **[][]byte) audit.Factory { - return func(_ context.Context, config *audit.BackendConfig) (audit.Backend, error) { - n, err := NewNoopAudit(config.Config) - if err != nil { - return nil, err - } - if records != nil { - *records = &n.records - } - return n, nil - } -} - -type NoopAudit struct { - Config *audit.BackendConfig - ReqErr error - ReqAuth []*logical.Auth - Req []*logical.Request - ReqHeaders []map[string][]string - ReqNonHMACKeys []string - ReqErrs []error - - RespErr error - RespAuth []*logical.Auth - RespReq []*logical.Request - Resp []*logical.Response - RespNonHMACKeys [][]string - RespReqNonHMACKeys [][]string - RespErrs []error - - formatter audit.AuditFormatter - records [][]byte - l sync.RWMutex - salt *salt.Salt - saltMutex sync.RWMutex -} - -func (n *NoopAudit) LogRequest(ctx context.Context, in *logical.LogInput) error { - n.l.Lock() - defer n.l.Unlock() - if n.formatter.AuditFormatWriter != nil { - var w bytes.Buffer - err := n.formatter.FormatRequest(ctx, &w, audit.FormatterConfig{}, in) - if err != nil { - return err - } - n.records = append(n.records, w.Bytes()) - } - - n.ReqAuth = append(n.ReqAuth, in.Auth) - n.Req = append(n.Req, in.Request) - n.ReqHeaders = append(n.ReqHeaders, in.Request.Headers) - n.ReqNonHMACKeys = in.NonHMACReqDataKeys - n.ReqErrs = append(n.ReqErrs, in.OuterErr) - - return n.ReqErr -} - -func (n *NoopAudit) LogResponse(ctx context.Context, in *logical.LogInput) error { - n.l.Lock() - defer n.l.Unlock() - - if n.formatter.AuditFormatWriter != nil { - var w bytes.Buffer - err := n.formatter.FormatResponse(ctx, &w, audit.FormatterConfig{}, in) - if err != nil { - return err - } - n.records = append(n.records, w.Bytes()) - } - - n.RespAuth = append(n.RespAuth, in.Auth) - n.RespReq = append(n.RespReq, in.Request) - n.Resp = append(n.Resp, in.Response) - n.RespErrs = append(n.RespErrs, in.OuterErr) - - if in.Response != nil { - n.RespNonHMACKeys = append(n.RespNonHMACKeys, in.NonHMACRespDataKeys) - n.RespReqNonHMACKeys = append(n.RespReqNonHMACKeys, in.NonHMACReqDataKeys) - } - - return n.RespErr -} - -func (n *NoopAudit) LogTestMessage(ctx context.Context, in *logical.LogInput, config map[string]string) error { - n.l.Lock() - defer n.l.Unlock() - var w bytes.Buffer - tempFormatter := audit.NewTemporaryFormatter(config["format"], config["prefix"]) - err := tempFormatter.FormatResponse(ctx, &w, audit.FormatterConfig{}, in) - if err != nil { - return err - } - n.records = append(n.records, w.Bytes()) - return nil -} - -func (n *NoopAudit) Salt(ctx context.Context) (*salt.Salt, error) { - n.saltMutex.RLock() - if n.salt != nil { - defer n.saltMutex.RUnlock() - return n.salt, nil - } - n.saltMutex.RUnlock() - n.saltMutex.Lock() - defer n.saltMutex.Unlock() - if n.salt != nil { - return n.salt, nil - } - salt, err := salt.NewSalt(ctx, n.Config.SaltView, n.Config.SaltConfig) - if err != nil { - return nil, err - } - n.salt = salt - return salt, nil -} - -func (n *NoopAudit) GetHash(ctx context.Context, data string) (string, error) { - salt, err := n.Salt(ctx) - if err != nil { - return "", err - } - return salt.GetIdentifiedHMAC(data), nil -} - -func (n *NoopAudit) Reload(ctx context.Context) error { - return nil -} - -func (n *NoopAudit) Invalidate(ctx context.Context) { - n.saltMutex.Lock() - defer n.saltMutex.Unlock() - n.salt = nil -} - -type TestLogger struct { - hclog.InterceptLogger - Path string - File *os.File - sink hclog.SinkAdapter -} - -func NewTestLogger(t testing.T) *TestLogger { - var logFile *os.File - var logPath string - output := os.Stderr - - logDir := os.Getenv("VAULT_TEST_LOG_DIR") - if logDir != "" { - logPath = filepath.Join(logDir, t.Name()+".log") - // t.Name may include slashes. - dir, _ := filepath.Split(logPath) - err := os.MkdirAll(dir, 0o755) - if err != nil { - t.Fatal(err) - } - logFile, err = os.Create(logPath) - if err != nil { - t.Fatal(err) - } - output = logFile - } - - // We send nothing on the regular logger, that way we can later deregister - // the sink to stop logging during cluster cleanup. - logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{ - Output: io.Discard, - IndependentLevels: true, - Name: t.Name(), - }) - sink := hclog.NewSinkAdapter(&hclog.LoggerOptions{ - Output: output, - Level: hclog.Trace, - IndependentLevels: true, - }) - logger.RegisterSink(sink) - return &TestLogger{ - Path: logPath, - File: logFile, - InterceptLogger: logger, - sink: sink, - } -} - -func (tl *TestLogger) StopLogging() { - tl.InterceptLogger.DeregisterSink(tl.sink) -} diff --git a/helper/testhelpers/ldap/ldaphelper.go b/helper/testhelpers/ldap/ldaphelper.go deleted file mode 100644 index 3125516a6..000000000 --- a/helper/testhelpers/ldap/ldaphelper.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package ldap - -import ( - "context" - "fmt" - "testing" - - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/hashicorp/vault/sdk/helper/ldaputil" -) - -func PrepareTestContainer(t *testing.T, version string) (cleanup func(), cfg *ldaputil.ConfigEntry) { - runner, err := docker.NewServiceRunner(docker.RunOptions{ - // Currently set to "michelvocks" until https://github.com/rroemhild/docker-test-openldap/pull/14 - // has been merged. - ImageRepo: "docker.mirror.hashicorp.services/michelvocks/docker-test-openldap", - ImageTag: version, - ContainerName: "ldap", - Ports: []string{"389/tcp"}, - // Env: []string{"LDAP_DEBUG_LEVEL=384"}, - }) - if err != nil { - t.Fatalf("could not start local LDAP docker container: %s", err) - } - - cfg = new(ldaputil.ConfigEntry) - cfg.UserDN = "ou=people,dc=planetexpress,dc=com" - cfg.UserAttr = "cn" - cfg.UserFilter = "({{.UserAttr}}={{.Username}})" - cfg.BindDN = "cn=admin,dc=planetexpress,dc=com" - cfg.BindPassword = "GoodNewsEveryone" - cfg.GroupDN = "ou=people,dc=planetexpress,dc=com" - cfg.GroupAttr = "cn" - cfg.RequestTimeout = 60 - cfg.MaximumPageSize = 1000 - - svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - connURL := fmt.Sprintf("ldap://%s:%d", host, port) - cfg.Url = connURL - logger := hclog.New(nil) - client := ldaputil.Client{ - LDAP: ldaputil.NewLDAP(), - Logger: logger, - } - - conn, err := client.DialLDAP(cfg) - if err != nil { - return nil, err - } - defer conn.Close() - - if _, err := client.GetUserBindDN(cfg, conn, "Philip J. Fry"); err != nil { - return nil, err - } - - return docker.NewServiceURLParse(connURL) - }) - if err != nil { - t.Fatalf("could not start local LDAP docker container: %s", err) - } - - return svc.Cleanup, cfg -} diff --git a/helper/testhelpers/logical/testing.go b/helper/testhelpers/logical/testing.go deleted file mode 100644 index ad8149c1e..000000000 --- a/helper/testhelpers/logical/testing.go +++ /dev/null @@ -1,533 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package testing - -import ( - "context" - "crypto/tls" - "fmt" - "os" - "reflect" - "sort" - "testing" - - "github.com/hashicorp/errwrap" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/physical/inmem" - "github.com/hashicorp/vault/vault" -) - -// TestEnvVar must be set to a non-empty value for acceptance tests to run. -const TestEnvVar = "VAULT_ACC" - -// TestCase is a single set of tests to run for a backend. A TestCase -// should generally map 1:1 to each test method for your acceptance -// tests. -type TestCase struct { - // Precheck, if non-nil, will be called once before the test case - // runs at all. This can be used for some validation prior to the - // test running. - PreCheck func() - - // LogicalBackend is the backend that will be mounted. - LogicalBackend logical.Backend - - // LogicalFactory can be used instead of LogicalBackend if the - // backend requires more construction - LogicalFactory logical.Factory - - // CredentialBackend is the backend that will be mounted. - CredentialBackend logical.Backend - - // CredentialFactory can be used instead of CredentialBackend if the - // backend requires more construction - CredentialFactory logical.Factory - - // Steps are the set of operations that are run for this test case. - Steps []TestStep - - // Teardown will be called before the test case is over regardless - // of if the test succeeded or failed. This should return an error - // in the case that the test can't guarantee all resources were - // properly cleaned up. - Teardown TestTeardownFunc - - // AcceptanceTest, if set, the test case will be run only if - // the environment variable VAULT_ACC is set. If not this test case - // will be run as a unit test. - AcceptanceTest bool -} - -// TestStep is a single step within a TestCase. -type TestStep struct { - // Operation is the operation to execute - Operation logical.Operation - - // Path is the request path. The mount prefix will be automatically added. - Path string - - // Arguments to pass in - Data map[string]interface{} - - // Check is called after this step is executed in order to test that - // the step executed successfully. If this is not set, then the next - // step will be called - Check TestCheckFunc - - // PreFlight is called directly before execution of the request, allowing - // modification of the request parameters (e.g. Path) with dynamic values. - PreFlight PreFlightFunc - - // ErrorOk, if true, will let erroneous responses through to the check - ErrorOk bool - - // Unauthenticated, if true, will make the request unauthenticated. - Unauthenticated bool - - // RemoteAddr, if set, will set the remote addr on the request. - RemoteAddr string - - // ConnState, if set, will set the tls connection state - ConnState *tls.ConnectionState -} - -// TestCheckFunc is the callback used for Check in TestStep. -type TestCheckFunc func(*logical.Response) error - -// PreFlightFunc is used to modify request parameters directly before execution -// in each TestStep. -type PreFlightFunc func(*logical.Request) error - -// TestTeardownFunc is the callback used for Teardown in TestCase. -type TestTeardownFunc func() error - -// Test performs an acceptance test on a backend with the given test case. -// -// Tests are not run unless an environmental variable "VAULT_ACC" is -// set to some non-empty value. This is to avoid test cases surprising -// a user by creating real resources. -// -// Tests will fail unless the verbose flag (`go test -v`, or explicitly -// the "-test.v" flag) is set. Because some acceptance tests take quite -// long, we require the verbose flag so users are able to see progress -// output. -func Test(tt TestT, c TestCase) { - // We only run acceptance tests if an env var is set because they're - // slow and generally require some outside configuration. - if c.AcceptanceTest && os.Getenv(TestEnvVar) == "" { - tt.Skip(fmt.Sprintf( - "Acceptance tests skipped unless env %q set", - TestEnvVar)) - return - } - - // We require verbose mode so that the user knows what is going on. - if c.AcceptanceTest && !testTesting && !testing.Verbose() { - tt.Fatal("Acceptance tests must be run with the -v flag on tests") - return - } - - // Run the PreCheck if we have it - if c.PreCheck != nil { - c.PreCheck() - } - - // Defer on the teardown, regardless of pass/fail at this point - if c.Teardown != nil { - defer c.Teardown() - } - - // Check that something is provided - if c.LogicalBackend == nil && c.LogicalFactory == nil { - if c.CredentialBackend == nil && c.CredentialFactory == nil { - tt.Fatal("Must provide either Backend or Factory") - return - } - } - // We currently only support doing one logical OR one credential test at a time. - if (c.LogicalFactory != nil || c.LogicalBackend != nil) && (c.CredentialFactory != nil || c.CredentialBackend != nil) { - tt.Fatal("Must provide only one backend or factory") - return - } - - // Create an in-memory Vault core - logger := logging.NewVaultLogger(log.Trace) - - phys, err := inmem.NewInmem(nil, logger) - if err != nil { - tt.Fatal(err) - return - } - - config := &vault.CoreConfig{ - Physical: phys, - DisableMlock: true, - BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(), - } - - if c.LogicalBackend != nil || c.LogicalFactory != nil { - config.LogicalBackends = map[string]logical.Factory{ - "test": func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { - if c.LogicalBackend != nil { - return c.LogicalBackend, nil - } - return c.LogicalFactory(ctx, conf) - }, - } - } - if c.CredentialBackend != nil || c.CredentialFactory != nil { - config.CredentialBackends = map[string]logical.Factory{ - "test": func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { - if c.CredentialBackend != nil { - return c.CredentialBackend, nil - } - return c.CredentialFactory(ctx, conf) - }, - } - } - - core, err := vault.NewCore(config) - if err != nil { - tt.Fatal("error initializing core: ", err) - return - } - - // Initialize the core - init, err := core.Initialize(context.Background(), &vault.InitParams{ - BarrierConfig: &vault.SealConfig{ - SecretShares: 1, - SecretThreshold: 1, - }, - RecoveryConfig: nil, - }) - if err != nil { - tt.Fatal("error initializing core: ", err) - return - } - - // Unseal the core - if unsealed, err := core.Unseal(init.SecretShares[0]); err != nil { - tt.Fatal("error unsealing core: ", err) - return - } else if !unsealed { - tt.Fatal("vault shouldn't be sealed") - return - } - - // Create an HTTP API server and client - ln, addr := http.TestServer(nil, core) - defer ln.Close() - clientConfig := api.DefaultConfig() - clientConfig.Address = addr - client, err := api.NewClient(clientConfig) - if err != nil { - tt.Fatal("error initializing HTTP client: ", err) - return - } - - // Set the token so we're authenticated - client.SetToken(init.RootToken) - - prefix := "mnt" - if c.LogicalBackend != nil || c.LogicalFactory != nil { - // Mount the backend - mountInfo := &api.MountInput{ - Type: "test", - Description: "acceptance test", - } - if err := client.Sys().Mount(prefix, mountInfo); err != nil { - tt.Fatal("error mounting backend: ", err) - return - } - } - - isAuthBackend := false - if c.CredentialBackend != nil || c.CredentialFactory != nil { - isAuthBackend = true - - // Enable the test auth method - opts := &api.EnableAuthOptions{ - Type: "test", - } - if err := client.Sys().EnableAuthWithOptions(prefix, opts); err != nil { - tt.Fatal("error enabling backend: ", err) - return - } - } - - tokenInfo, err := client.Auth().Token().LookupSelf() - if err != nil { - tt.Fatal("error looking up token: ", err) - return - } - var tokenPolicies []string - if tokenPoliciesRaw, ok := tokenInfo.Data["policies"]; ok { - if tokenPoliciesSliceRaw, ok := tokenPoliciesRaw.([]interface{}); ok { - for _, p := range tokenPoliciesSliceRaw { - tokenPolicies = append(tokenPolicies, p.(string)) - } - } - } - - // Make requests - var revoke []*logical.Request - for i, s := range c.Steps { - if logger.IsWarn() { - logger.Warn("Executing test step", "step_number", i+1) - } - - // Create the request - req := &logical.Request{ - Operation: s.Operation, - Path: s.Path, - Data: s.Data, - } - if !s.Unauthenticated { - req.ClientToken = client.Token() - req.SetTokenEntry(&logical.TokenEntry{ - ID: req.ClientToken, - NamespaceID: namespace.RootNamespaceID, - Policies: tokenPolicies, - DisplayName: tokenInfo.Data["display_name"].(string), - }) - } - req.Connection = &logical.Connection{RemoteAddr: s.RemoteAddr} - if s.ConnState != nil { - req.Connection.ConnState = s.ConnState - } - - if s.PreFlight != nil { - ct := req.ClientToken - req.ClientToken = "" - if err := s.PreFlight(req); err != nil { - tt.Error(fmt.Sprintf("Failed preflight for step %d: %s", i+1, err)) - break - } - req.ClientToken = ct - } - - // Make sure to prefix the path with where we mounted the thing - req.Path = fmt.Sprintf("%s/%s", prefix, req.Path) - - if isAuthBackend { - // Prepend the path with "auth" - req.Path = "auth/" + req.Path - } - - // Make the request - resp, err := core.HandleRequest(namespace.RootContext(nil), req) - if resp != nil && resp.Secret != nil { - // Revoke this secret later - revoke = append(revoke, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sys/revoke/" + resp.Secret.LeaseID, - }) - } - - // Test step returned an error. - if err != nil { - // But if an error is expected, do not fail the test step, - // regardless of whether the error is a 'logical.ErrorResponse' - // or not. Set the err to nil. If the error is a logical.ErrorResponse, - // it will be handled later. - if s.ErrorOk { - err = nil - } else { - // If the error is not expected, fail right away. - tt.Error(fmt.Sprintf("Failed step %d: %s", i+1, err)) - break - } - } - - // If the error is a 'logical.ErrorResponse' and if error was not expected, - // set the error so that this can be caught below. - if resp.IsError() && !s.ErrorOk { - err = fmt.Errorf("erroneous response:\n\n%#v", resp) - } - - // Either the 'err' was nil or if an error was expected, it was set to nil. - // Call the 'Check' function if there is one. - // - // TODO: This works perfectly for now, but it would be better if 'Check' - // function takes in both the response object and the error, and decide on - // the action on its own. - if err == nil && s.Check != nil { - // Call the test method - err = s.Check(resp) - } - - if err != nil { - tt.Error(fmt.Sprintf("Failed step %d: %s", i+1, err)) - break - } - } - - // Revoke any secrets we might have. - var failedRevokes []*logical.Secret - for _, req := range revoke { - if logger.IsWarn() { - logger.Warn("Revoking secret", "secret", fmt.Sprintf("%#v", req)) - } - req.ClientToken = client.Token() - resp, err := core.HandleRequest(namespace.RootContext(nil), req) - if err == nil && resp.IsError() { - err = fmt.Errorf("erroneous response:\n\n%#v", resp) - } - if err != nil { - failedRevokes = append(failedRevokes, req.Secret) - tt.Error(fmt.Sprintf("Revoke error: %s", err)) - } - } - - // Perform any rollbacks. This should no-op if there aren't any. - // We set the "immediate" flag here that any backend can pick up on - // to do all rollbacks immediately even if the WAL entries are new. - logger.Warn("Requesting RollbackOperation") - rollbackPath := prefix + "/" - if c.CredentialFactory != nil || c.CredentialBackend != nil { - rollbackPath = "auth/" + rollbackPath - } - req := logical.RollbackRequest(rollbackPath) - req.Data["immediate"] = true - req.ClientToken = client.Token() - resp, err := core.HandleRequest(namespace.RootContext(nil), req) - if err == nil && resp.IsError() { - err = fmt.Errorf("erroneous response:\n\n%#v", resp) - } - if err != nil { - if !errwrap.Contains(err, logical.ErrUnsupportedOperation.Error()) { - tt.Error(fmt.Sprintf("[ERR] Rollback error: %s", err)) - } - } - - // If we have any failed revokes, log it. - if len(failedRevokes) > 0 { - for _, s := range failedRevokes { - tt.Error(fmt.Sprintf( - "WARNING: Revoking the following secret failed. It may\n"+ - "still exist. Please verify:\n\n%#v", - s)) - } - } -} - -// TestCheckMulti is a helper to have multiple checks. -func TestCheckMulti(fs ...TestCheckFunc) TestCheckFunc { - return func(resp *logical.Response) error { - for _, f := range fs { - if err := f(resp); err != nil { - return err - } - } - - return nil - } -} - -// TestCheckAuth is a helper to check that a request generated an -// auth token with the proper policies. -func TestCheckAuth(policies []string) TestCheckFunc { - return func(resp *logical.Response) error { - if resp == nil || resp.Auth == nil { - return fmt.Errorf("no auth in response") - } - expected := make([]string, len(policies)) - copy(expected, policies) - sort.Strings(expected) - ret := make([]string, len(resp.Auth.Policies)) - copy(ret, resp.Auth.Policies) - sort.Strings(ret) - if !reflect.DeepEqual(ret, expected) { - return fmt.Errorf("invalid policies: expected %#v, got %#v", expected, ret) - } - - return nil - } -} - -// TestCheckAuthEntityId is a helper to check that a request generated an -// auth token with the expected entity_id. -func TestCheckAuthEntityId(entity_id *string) TestCheckFunc { - return func(resp *logical.Response) error { - if resp == nil || resp.Auth == nil { - return fmt.Errorf("no auth in response") - } - - if *entity_id == "" { - // If we don't know what the entity_id should be, just save it - *entity_id = resp.Auth.EntityID - } else if resp.Auth.EntityID != *entity_id { - return fmt.Errorf("entity_id %s does not match the expected value of %s", resp.Auth.EntityID, *entity_id) - } - - return nil - } -} - -// TestCheckAuthEntityAliasMetadataName is a helper to check that a request generated an -// auth token with the expected alias metadata. -func TestCheckAuthEntityAliasMetadataName(key string, value string) TestCheckFunc { - return func(resp *logical.Response) error { - if resp == nil || resp.Auth == nil { - return fmt.Errorf("no auth in response") - } - - if key == "" || value == "" { - return fmt.Errorf("alias metadata key and value required") - } - - name, ok := resp.Auth.Alias.Metadata[key] - if !ok { - return fmt.Errorf("metadata key %s does not exist, it should", key) - } - - if name != value { - return fmt.Errorf("expected map value %s, got %s", value, name) - } - return nil - } -} - -// TestCheckAuthDisplayName is a helper to check that a request generated a -// valid display name. -func TestCheckAuthDisplayName(n string) TestCheckFunc { - return func(resp *logical.Response) error { - if resp.Auth == nil { - return fmt.Errorf("no auth in response") - } - if n != "" && resp.Auth.DisplayName != "mnt-"+n { - return fmt.Errorf("invalid display name: %#v", resp.Auth.DisplayName) - } - - return nil - } -} - -// TestCheckError is a helper to check that a response is an error. -func TestCheckError() TestCheckFunc { - return func(resp *logical.Response) error { - if !resp.IsError() { - return fmt.Errorf("response should be error") - } - - return nil - } -} - -// TestT is the interface used to handle the test lifecycle of a test. -// -// Users should just use a *testing.T object, which implements this. -type TestT interface { - Error(args ...interface{}) - Fatal(args ...interface{}) - Skip(args ...interface{}) -} - -var testTesting = false diff --git a/helper/testhelpers/logical/testing_test.go b/helper/testhelpers/logical/testing_test.go deleted file mode 100644 index a73b91ecd..000000000 --- a/helper/testhelpers/logical/testing_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package testing - -import ( - "os" - "testing" -) - -func init() { - testTesting = true - - if err := os.Setenv(TestEnvVar, "1"); err != nil { - panic(err) - } -} - -func TestTest_noEnv(t *testing.T) { - // Unset the variable - if err := os.Setenv(TestEnvVar, ""); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Setenv(TestEnvVar, "1") - - mt := new(mockT) - Test(mt, TestCase{ - AcceptanceTest: true, - }) - - if !mt.SkipCalled { - t.Fatal("skip not called") - } -} - -func TestTest_preCheck(t *testing.T) { - called := false - - mt := new(mockT) - Test(mt, TestCase{ - PreCheck: func() { called = true }, - }) - - if !called { - t.Fatal("precheck should be called") - } -} - -// mockT implements TestT for testing -type mockT struct { - ErrorCalled bool - ErrorArgs []interface{} - FatalCalled bool - FatalArgs []interface{} - SkipCalled bool - SkipArgs []interface{} - - f bool -} - -func (t *mockT) Error(args ...interface{}) { - t.ErrorCalled = true - t.ErrorArgs = args - t.f = true -} - -func (t *mockT) Fatal(args ...interface{}) { - t.FatalCalled = true - t.FatalArgs = args - t.f = true -} - -func (t *mockT) Skip(args ...interface{}) { - t.SkipCalled = true - t.SkipArgs = args - t.f = true -} - -func (t *mockT) failed() bool { - return t.f -} - -func (t *mockT) failMessage() string { - if t.FatalCalled { - return t.FatalArgs[0].(string) - } else if t.ErrorCalled { - return t.ErrorArgs[0].(string) - } else if t.SkipCalled { - return t.SkipArgs[0].(string) - } - - return "unknown" -} diff --git a/helper/testhelpers/mysql/mysqlhelper.go b/helper/testhelpers/mysql/mysqlhelper.go deleted file mode 100644 index 9c7d50b22..000000000 --- a/helper/testhelpers/mysql/mysqlhelper.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package mysqlhelper - -import ( - "context" - "database/sql" - "fmt" - "os" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/helper/docker" -) - -type Config struct { - docker.ServiceHostPort - ConnString string -} - -var _ docker.ServiceConfig = &Config{} - -func PrepareTestContainer(t *testing.T, legacy bool, pw string) (func(), string) { - if os.Getenv("MYSQL_URL") != "" { - return func() {}, os.Getenv("MYSQL_URL") - } - - imageVersion := "5.7" - if legacy { - imageVersion = "5.6" - } - - runner, err := docker.NewServiceRunner(docker.RunOptions{ - ContainerName: "mysql", - ImageRepo: "docker.mirror.hashicorp.services/library/mysql", - ImageTag: imageVersion, - Ports: []string{"3306/tcp"}, - Env: []string{"MYSQL_ROOT_PASSWORD=" + pw}, - }) - if err != nil { - t.Fatalf("could not start docker mysql: %s", err) - } - - svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - hostIP := docker.NewServiceHostPort(host, port) - connString := fmt.Sprintf("root:%s@tcp(%s)/mysql?parseTime=true", pw, hostIP.Address()) - db, err := sql.Open("mysql", connString) - if err != nil { - return nil, err - } - defer db.Close() - err = db.Ping() - if err != nil { - return nil, err - } - return &Config{ServiceHostPort: *hostIP, ConnString: connString}, nil - }) - if err != nil { - t.Fatalf("could not start docker mysql: %s", err) - } - return svc.Cleanup, svc.Config.(*Config).ConnString -} - -func TestCredsExist(t testing.TB, connURL, username, password string) error { - // Log in with the new creds - connURL = strings.Replace(connURL, "root:secret", fmt.Sprintf("%s:%s", username, password), 1) - db, err := sql.Open("mysql", connURL) - if err != nil { - return err - } - defer db.Close() - return db.Ping() -} diff --git a/helper/testhelpers/pluginhelpers/pluginhelpers.go b/helper/testhelpers/pluginhelpers/pluginhelpers.go deleted file mode 100644 index 457159f42..000000000 --- a/helper/testhelpers/pluginhelpers/pluginhelpers.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -// Package pluginhelpers contains testhelpers that don't depend on package -// vault, and thus can be used within vault (as well as elsewhere.) -package pluginhelpers - -import ( - "crypto/sha256" - "fmt" - "os" - "os/exec" - "path" - "path/filepath" - "sync" - - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/mitchellh/go-testing-interface" -) - -var ( - testPluginCacheLock sync.Mutex - testPluginCache = map[string][]byte{} -) - -type TestPlugin struct { - Name string - Typ consts.PluginType - Version string - FileName string - Sha256 string -} - -func GetPlugin(t testing.T, typ consts.PluginType) (string, string, string, string) { - t.Helper() - var pluginName string - var pluginType string - var pluginMain string - var pluginVersionLocation string - - switch typ { - case consts.PluginTypeCredential: - pluginType = "approle" - pluginName = "vault-plugin-auth-" + pluginType - pluginMain = filepath.Join("builtin", "credential", pluginType, "cmd", pluginType, "main.go") - pluginVersionLocation = fmt.Sprintf("github.com/hashicorp/vault/builtin/credential/%s.ReportedVersion", pluginType) - case consts.PluginTypeSecrets: - pluginType = "consul" - pluginName = "vault-plugin-secrets-" + pluginType - pluginMain = filepath.Join("builtin", "logical", pluginType, "cmd", pluginType, "main.go") - pluginVersionLocation = fmt.Sprintf("github.com/hashicorp/vault/builtin/logical/%s.ReportedVersion", pluginType) - case consts.PluginTypeDatabase: - pluginType = "postgresql" - pluginName = "vault-plugin-database-" + pluginType - pluginMain = filepath.Join("plugins", "database", pluginType, fmt.Sprintf("%s-database-plugin", pluginType), "main.go") - pluginVersionLocation = fmt.Sprintf("github.com/hashicorp/vault/plugins/database/%s.ReportedVersion", pluginType) - default: - t.Fatal(typ.String()) - } - return pluginName, pluginType, pluginMain, pluginVersionLocation -} - -// to mount a plugin, we need a working binary plugin, so we compile one here. -// pluginVersion is used to override the plugin's self-reported version -func CompilePlugin(t testing.T, typ consts.PluginType, pluginVersion string, pluginDir string) TestPlugin { - t.Helper() - - pluginName, pluginType, pluginMain, pluginVersionLocation := GetPlugin(t, typ) - - testPluginCacheLock.Lock() - defer testPluginCacheLock.Unlock() - - var pluginBytes []byte - - dir := "" - var err error - pluginRootDir := "builtin" - if typ == consts.PluginTypeDatabase { - pluginRootDir = "plugins" - } - for { - dir, err = os.Getwd() - if err != nil { - t.Fatal(err) - } - // detect if we are in a subdirectory or the root directory and compensate - if _, err := os.Stat(pluginRootDir); os.IsNotExist(err) { - err := os.Chdir("..") - if err != nil { - t.Fatal(err) - } - } else { - break - } - } - - pluginPath := path.Join(pluginDir, pluginName) - if pluginVersion != "" { - pluginPath += "-" + pluginVersion - } - - key := fmt.Sprintf("%s %s %s", pluginName, pluginType, pluginVersion) - // cache the compilation to only run once - var ok bool - pluginBytes, ok = testPluginCache[key] - if !ok { - // we need to compile - line := []string{"build"} - if pluginVersion != "" { - line = append(line, "-ldflags", fmt.Sprintf("-X %s=%s", pluginVersionLocation, pluginVersion)) - } - line = append(line, "-o", pluginPath, pluginMain) - cmd := exec.Command("go", line...) - cmd.Dir = dir - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatal(fmt.Errorf("error running go build %v output: %s", err, output)) - } - testPluginCache[key], err = os.ReadFile(pluginPath) - if err != nil { - t.Fatal(err) - } - pluginBytes = testPluginCache[key] - } - - // write the cached plugin if necessary - if _, err := os.Stat(pluginPath); os.IsNotExist(err) { - err = os.WriteFile(pluginPath, pluginBytes, 0o755) - } - if err != nil { - t.Fatal(err) - } - - sha := sha256.New() - _, err = sha.Write(pluginBytes) - if err != nil { - t.Fatal(err) - } - return TestPlugin{ - Name: pluginName, - Typ: typ, - Version: pluginVersion, - FileName: path.Base(pluginPath), - Sha256: fmt.Sprintf("%x", sha.Sum(nil)), - } -} diff --git a/helper/testhelpers/postgresql/postgresqlhelper.go b/helper/testhelpers/postgresql/postgresqlhelper.go deleted file mode 100644 index 7e5f25c62..000000000 --- a/helper/testhelpers/postgresql/postgresqlhelper.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package postgresql - -import ( - "context" - "database/sql" - "fmt" - "net/url" - "os" - "testing" - - "github.com/hashicorp/vault/sdk/helper/docker" -) - -func PrepareTestContainer(t *testing.T, version string) (func(), string) { - env := []string{ - "POSTGRES_PASSWORD=secret", - "POSTGRES_DB=database", - } - - _, cleanup, url, _ := prepareTestContainer(t, "postgres", "docker.mirror.hashicorp.services/postgres", version, "secret", true, false, false, env) - - return cleanup, url -} - -// PrepareTestContainerWithVaultUser will setup a test container with a Vault -// admin user configured so that we can safely call rotate-root without -// rotating the root DB credentials -func PrepareTestContainerWithVaultUser(t *testing.T, ctx context.Context, version string) (func(), string) { - env := []string{ - "POSTGRES_PASSWORD=secret", - "POSTGRES_DB=database", - } - - runner, cleanup, url, id := prepareTestContainer(t, "postgres", "docker.mirror.hashicorp.services/postgres", version, "secret", true, false, false, env) - - cmd := []string{"psql", "-U", "postgres", "-c", "CREATE USER vaultadmin WITH LOGIN PASSWORD 'vaultpass' SUPERUSER"} - _, err := runner.RunCmdInBackground(ctx, id, cmd) - if err != nil { - t.Fatalf("Could not run command (%v) in container: %v", cmd, err) - } - - return cleanup, url -} - -func PrepareTestContainerWithPassword(t *testing.T, version, password string) (func(), string) { - env := []string{ - "POSTGRES_PASSWORD=" + password, - "POSTGRES_DB=database", - } - - _, cleanup, url, _ := prepareTestContainer(t, "postgres", "docker.mirror.hashicorp.services/postgres", version, password, true, false, false, env) - - return cleanup, url -} - -func PrepareTestContainerRepmgr(t *testing.T, name, version string, envVars []string) (*docker.Runner, func(), string, string) { - env := append(envVars, - "REPMGR_PARTNER_NODES=psql-repl-node-0,psql-repl-node-1", - "REPMGR_PRIMARY_HOST=psql-repl-node-0", - "REPMGR_PASSWORD=repmgrpass", - "POSTGRESQL_PASSWORD=secret") - - return prepareTestContainer(t, name, "docker.mirror.hashicorp.services/bitnami/postgresql-repmgr", version, "secret", false, true, true, env) -} - -func prepareTestContainer(t *testing.T, name, repo, version, password string, - addSuffix, forceLocalAddr, doNotAutoRemove bool, envVars []string, -) (*docker.Runner, func(), string, string) { - if os.Getenv("PG_URL") != "" { - return nil, func() {}, "", os.Getenv("PG_URL") - } - - if version == "" { - version = "11" - } - - runOpts := docker.RunOptions{ - ContainerName: name, - ImageRepo: repo, - ImageTag: version, - Env: envVars, - Ports: []string{"5432/tcp"}, - DoNotAutoRemove: doNotAutoRemove, - } - if repo == "bitnami/postgresql-repmgr" { - runOpts.NetworkID = os.Getenv("POSTGRES_MULTIHOST_NET") - } - - runner, err := docker.NewServiceRunner(runOpts) - if err != nil { - t.Fatalf("Could not start docker Postgres: %s", err) - } - - svc, containerID, err := runner.StartNewService(context.Background(), addSuffix, forceLocalAddr, connectPostgres(password, repo)) - if err != nil { - t.Fatalf("Could not start docker Postgres: %s", err) - } - - return runner, svc.Cleanup, svc.Config.URL().String(), containerID -} - -func connectPostgres(password, repo string) docker.ServiceAdapter { - return func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - u := url.URL{ - Scheme: "postgres", - User: url.UserPassword("postgres", password), - Host: fmt.Sprintf("%s:%d", host, port), - Path: "postgres", - RawQuery: "sslmode=disable", - } - - db, err := sql.Open("pgx", u.String()) - if err != nil { - return nil, err - } - defer db.Close() - - if err = db.Ping(); err != nil { - return nil, err - } - return docker.NewServiceURL(u), nil - } -} - -func StopContainer(t *testing.T, ctx context.Context, runner *docker.Runner, containerID string) { - if err := runner.Stop(ctx, containerID); err != nil { - t.Fatalf("Could not stop docker Postgres: %s", err) - } -} - -func RestartContainer(t *testing.T, ctx context.Context, runner *docker.Runner, containerID string) { - if err := runner.Restart(ctx, containerID); err != nil { - t.Fatalf("Could not restart docker Postgres: %s", err) - } -} diff --git a/helper/testhelpers/seal/sealhelper.go b/helper/testhelpers/seal/sealhelper.go deleted file mode 100644 index 93f46315d..000000000 --- a/helper/testhelpers/seal/sealhelper.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package sealhelper - -import ( - "path" - "strconv" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/logical/transit" - "github.com/hashicorp/vault/helper/testhelpers/teststorage" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/internalshared/configutil" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/hashicorp/vault/vault/seal" - "github.com/mitchellh/go-testing-interface" -) - -type TransitSealServer struct { - *vault.TestCluster -} - -func NewTransitSealServer(t testing.T, idx int) *TransitSealServer { - conf := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": transit.Factory, - }, - } - opts := &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: http.Handler, - Logger: logging.NewVaultLogger(hclog.Trace).Named(t.Name()).Named("transit-seal" + strconv.Itoa(idx)), - } - teststorage.InmemBackendSetup(conf, opts) - cluster := vault.NewTestCluster(t, conf, opts) - cluster.Start() - - if err := cluster.Cores[0].Client.Sys().Mount("transit", &api.MountInput{ - Type: "transit", - }); err != nil { - t.Fatal(err) - } - - return &TransitSealServer{cluster} -} - -func (tss *TransitSealServer) MakeKey(t testing.T, key string) { - client := tss.Cores[0].Client - if _, err := client.Logical().Write(path.Join("transit", "keys", key), nil); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write(path.Join("transit", "keys", key, "config"), map[string]interface{}{ - "deletion_allowed": true, - }); err != nil { - t.Fatal(err) - } -} - -func (tss *TransitSealServer) MakeSeal(t testing.T, key string) (vault.Seal, error) { - client := tss.Cores[0].Client - wrapperConfig := map[string]string{ - "address": client.Address(), - "token": client.Token(), - "mount_path": "transit", - "key_name": key, - "tls_ca_cert": tss.CACertPEMFile, - } - transitSeal, _, err := configutil.GetTransitKMSFunc(&configutil.KMS{Config: wrapperConfig}) - if err != nil { - t.Fatalf("error setting wrapper config: %v", err) - } - - return vault.NewAutoSeal(seal.NewAccess(transitSeal)) -} diff --git a/helper/testhelpers/testhelpers.go b/helper/testhelpers/testhelpers.go deleted file mode 100644 index 261e03b6f..000000000 --- a/helper/testhelpers/testhelpers.go +++ /dev/null @@ -1,1055 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package testhelpers - -import ( - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "math/rand" - "net/url" - "os" - "strings" - "sync/atomic" - "time" - - "github.com/armon/go-metrics" - raftlib "github.com/hashicorp/raft" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/metricsutil" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/physical/raft" - "github.com/hashicorp/vault/sdk/helper/xor" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/go-testing-interface" -) - -type GenerateRootKind int - -const ( - GenerateRootRegular GenerateRootKind = iota - GenerateRootDR - GenerateRecovery -) - -// GenerateRoot generates a root token on the target cluster. -func GenerateRoot(t testing.T, cluster *vault.TestCluster, kind GenerateRootKind) string { - t.Helper() - token, err := GenerateRootWithError(t, cluster, kind) - if err != nil { - t.Fatal(err) - } - return token -} - -func GenerateRootWithError(t testing.T, cluster *vault.TestCluster, kind GenerateRootKind) (string, error) { - t.Helper() - // If recovery keys supported, use those to perform root token generation instead - var keys [][]byte - if cluster.Cores[0].SealAccess().RecoveryKeySupported() { - keys = cluster.RecoveryKeys - } else { - keys = cluster.BarrierKeys - } - client := cluster.Cores[0].Client - oldNS := client.Namespace() - defer client.SetNamespace(oldNS) - client.ClearNamespace() - - var err error - var status *api.GenerateRootStatusResponse - switch kind { - case GenerateRootRegular: - status, err = client.Sys().GenerateRootInit("", "") - case GenerateRootDR: - status, err = client.Sys().GenerateDROperationTokenInit("", "") - case GenerateRecovery: - status, err = client.Sys().GenerateRecoveryOperationTokenInit("", "") - } - if err != nil { - return "", err - } - - if status.Required > len(keys) { - return "", fmt.Errorf("need more keys than have, need %d have %d", status.Required, len(keys)) - } - - otp := status.OTP - - for i, key := range keys { - if i >= status.Required { - break - } - - strKey := base64.StdEncoding.EncodeToString(key) - switch kind { - case GenerateRootRegular: - status, err = client.Sys().GenerateRootUpdate(strKey, status.Nonce) - case GenerateRootDR: - status, err = client.Sys().GenerateDROperationTokenUpdate(strKey, status.Nonce) - case GenerateRecovery: - status, err = client.Sys().GenerateRecoveryOperationTokenUpdate(strKey, status.Nonce) - } - if err != nil { - return "", err - } - } - if !status.Complete { - return "", errors.New("generate root operation did not end successfully") - } - - tokenBytes, err := base64.RawStdEncoding.DecodeString(status.EncodedToken) - if err != nil { - return "", err - } - tokenBytes, err = xor.XORBytes(tokenBytes, []byte(otp)) - if err != nil { - return "", err - } - return string(tokenBytes), nil -} - -// RandomWithPrefix is used to generate a unique name with a prefix, for -// randomizing names in acceptance tests -func RandomWithPrefix(name string) string { - return fmt.Sprintf("%s-%d", name, rand.New(rand.NewSource(time.Now().UnixNano())).Int()) -} - -func EnsureCoresSealed(t testing.T, c *vault.TestCluster) { - t.Helper() - for _, core := range c.Cores { - EnsureCoreSealed(t, core) - } -} - -func EnsureCoreSealed(t testing.T, core *vault.TestClusterCore) { - t.Helper() - core.Seal(t) - timeout := time.Now().Add(60 * time.Second) - for { - if time.Now().After(timeout) { - t.Fatal("timeout waiting for core to seal") - } - if core.Core.Sealed() { - break - } - time.Sleep(250 * time.Millisecond) - } -} - -func EnsureCoresUnsealed(t testing.T, c *vault.TestCluster) { - t.Helper() - for i, core := range c.Cores { - err := AttemptUnsealCore(c, core) - if err != nil { - t.Fatalf("failed to unseal core %d: %v", i, err) - } - } -} - -func EnsureCoreUnsealed(t testing.T, c *vault.TestCluster, core *vault.TestClusterCore) { - t.Helper() - err := AttemptUnsealCore(c, core) - if err != nil { - t.Fatalf("failed to unseal core: %v", err) - } -} - -func AttemptUnsealCores(c *vault.TestCluster) error { - for i, core := range c.Cores { - err := AttemptUnsealCore(c, core) - if err != nil { - return fmt.Errorf("failed to unseal core %d: %v", i, err) - } - } - return nil -} - -func AttemptUnsealCore(c *vault.TestCluster, core *vault.TestClusterCore) error { - if !core.Sealed() { - return nil - } - - core.SealAccess().ClearCaches(context.Background()) - if err := core.UnsealWithStoredKeys(context.Background()); err != nil { - return err - } - - client := core.Client - oldNS := client.Namespace() - defer client.SetNamespace(oldNS) - client.ClearNamespace() - - client.Sys().ResetUnsealProcess() - for j := 0; j < len(c.BarrierKeys); j++ { - statusResp, err := client.Sys().Unseal(base64.StdEncoding.EncodeToString(c.BarrierKeys[j])) - if err != nil { - // Sometimes when we get here it's already unsealed on its own - // and then this fails for DR secondaries so check again - if core.Sealed() { - return err - } else { - return nil - } - } - if statusResp == nil { - return fmt.Errorf("nil status response during unseal") - } - if !statusResp.Sealed { - break - } - } - if core.Sealed() { - return fmt.Errorf("core is still sealed") - } - return nil -} - -func EnsureStableActiveNode(t testing.T, cluster *vault.TestCluster) { - t.Helper() - deriveStableActiveCore(t, cluster) -} - -func DeriveStableActiveCore(t testing.T, cluster *vault.TestCluster) *vault.TestClusterCore { - t.Helper() - return deriveStableActiveCore(t, cluster) -} - -func deriveStableActiveCore(t testing.T, cluster *vault.TestCluster) *vault.TestClusterCore { - t.Helper() - activeCore := DeriveActiveCore(t, cluster) - minDuration := time.NewTimer(3 * time.Second) - - for i := 0; i < 60; i++ { - leaderResp, err := activeCore.Client.Sys().Leader() - if err != nil { - t.Fatal(err) - } - if !leaderResp.IsSelf { - minDuration.Reset(3 * time.Second) - } - time.Sleep(200 * time.Millisecond) - } - - select { - case <-minDuration.C: - default: - if stopped := minDuration.Stop(); stopped { - t.Fatal("unstable active node") - } - // Drain the value - <-minDuration.C - } - - return activeCore -} - -func DeriveActiveCore(t testing.T, cluster *vault.TestCluster) *vault.TestClusterCore { - t.Helper() - for i := 0; i < 60; i++ { - for _, core := range cluster.Cores { - oldNS := core.Client.Namespace() - core.Client.ClearNamespace() - leaderResp, err := core.Client.Sys().Leader() - core.Client.SetNamespace(oldNS) - if err != nil { - t.Fatal(err) - } - if leaderResp.IsSelf { - return core - } - } - time.Sleep(1 * time.Second) - } - t.Fatal("could not derive the active core") - return nil -} - -func DeriveStandbyCores(t testing.T, cluster *vault.TestCluster) []*vault.TestClusterCore { - t.Helper() - cores := make([]*vault.TestClusterCore, 0, 2) - for _, core := range cluster.Cores { - oldNS := core.Client.Namespace() - core.Client.ClearNamespace() - leaderResp, err := core.Client.Sys().Leader() - core.Client.SetNamespace(oldNS) - if err != nil { - t.Fatal(err) - } - if !leaderResp.IsSelf { - cores = append(cores, core) - } - } - - return cores -} - -func WaitForNCoresUnsealed(t testing.T, cluster *vault.TestCluster, n int) { - t.Helper() - for i := 0; i < 30; i++ { - unsealed := 0 - for _, core := range cluster.Cores { - if !core.Core.Sealed() { - unsealed++ - } - } - - if unsealed >= n { - return - } - time.Sleep(time.Second) - } - - t.Fatalf("%d cores were not unsealed", n) -} - -func SealCores(t testing.T, cluster *vault.TestCluster) { - t.Helper() - for _, core := range cluster.Cores { - if err := core.Shutdown(); err != nil { - t.Fatal(err) - } - timeout := time.Now().Add(3 * time.Second) - for { - if time.Now().After(timeout) { - t.Fatal("timeout waiting for core to seal") - } - if core.Sealed() { - break - } - time.Sleep(100 * time.Millisecond) - } - } -} - -func WaitForNCoresSealed(t testing.T, cluster *vault.TestCluster, n int) { - t.Helper() - for i := 0; i < 60; i++ { - sealed := 0 - for _, core := range cluster.Cores { - if core.Core.Sealed() { - sealed++ - } - } - - if sealed >= n { - return - } - time.Sleep(time.Second) - } - - t.Fatalf("%d cores were not sealed", n) -} - -func WaitForActiveNode(t testing.T, cluster *vault.TestCluster) *vault.TestClusterCore { - t.Helper() - for i := 0; i < 60; i++ { - for _, core := range cluster.Cores { - if standby, _ := core.Core.Standby(); !standby { - return core - } - } - - time.Sleep(time.Second) - } - - t.Fatalf("node did not become active") - return nil -} - -func WaitForStandbyNode(t testing.T, core *vault.TestClusterCore) { - t.Helper() - for i := 0; i < 30; i++ { - if isLeader, _, clusterAddr, _ := core.Core.Leader(); isLeader != true && clusterAddr != "" { - return - } - if core.Core.ActiveNodeReplicationState() == 0 { - return - } - - time.Sleep(time.Second) - } - - t.Fatalf("node did not become standby") -} - -func RekeyCluster(t testing.T, cluster *vault.TestCluster, recovery bool) [][]byte { - t.Helper() - cluster.Logger.Info("rekeying cluster", "recovery", recovery) - client := cluster.Cores[0].Client - - initFunc := client.Sys().RekeyInit - if recovery { - initFunc = client.Sys().RekeyRecoveryKeyInit - } - init, err := initFunc(&api.RekeyInitRequest{ - SecretShares: 5, - SecretThreshold: 3, - }) - if err != nil { - t.Fatal(err) - } - - var statusResp *api.RekeyUpdateResponse - keys := cluster.BarrierKeys - if cluster.Cores[0].Core.SealAccess().RecoveryKeySupported() { - keys = cluster.RecoveryKeys - } - - updateFunc := client.Sys().RekeyUpdate - if recovery { - updateFunc = client.Sys().RekeyRecoveryKeyUpdate - } - for j := 0; j < len(keys); j++ { - statusResp, err = updateFunc(base64.StdEncoding.EncodeToString(keys[j]), init.Nonce) - if err != nil { - t.Fatal(err) - } - if statusResp == nil { - t.Fatal("nil status response during unseal") - } - if statusResp.Complete { - break - } - } - cluster.Logger.Info("cluster rekeyed", "recovery", recovery) - - if cluster.Cores[0].Core.SealAccess().RecoveryKeySupported() && !recovery { - return nil - } - if len(statusResp.KeysB64) != 5 { - t.Fatal("wrong number of keys") - } - - newKeys := make([][]byte, 5) - for i, key := range statusResp.KeysB64 { - newKeys[i], err = base64.StdEncoding.DecodeString(key) - if err != nil { - t.Fatal(err) - } - } - return newKeys -} - -// TestRaftServerAddressProvider is a ServerAddressProvider that uses the -// ClusterAddr() of each node to provide raft addresses. -// -// Note that TestRaftServerAddressProvider should only be used in cases where -// cores that are part of a raft configuration have already had -// startClusterListener() called (via either unsealing or raft joining). -type TestRaftServerAddressProvider struct { - Cluster *vault.TestCluster -} - -func (p *TestRaftServerAddressProvider) ServerAddr(id raftlib.ServerID) (raftlib.ServerAddress, error) { - for _, core := range p.Cluster.Cores { - if core.NodeID == string(id) { - parsed, err := url.Parse(core.ClusterAddr()) - if err != nil { - return "", err - } - - return raftlib.ServerAddress(parsed.Host), nil - } - } - - return "", errors.New("could not find cluster addr") -} - -func RaftClusterJoinNodes(t testing.T, cluster *vault.TestCluster) { - addressProvider := &TestRaftServerAddressProvider{Cluster: cluster} - - atomic.StoreUint32(&vault.TestingUpdateClusterAddr, 1) - - leader := cluster.Cores[0] - - // Seal the leader so we can install an address provider - { - EnsureCoreSealed(t, leader) - leader.UnderlyingRawStorage.(*raft.RaftBackend).SetServerAddressProvider(addressProvider) - cluster.UnsealCore(t, leader) - vault.TestWaitActive(t, leader.Core) - } - - leaderInfos := []*raft.LeaderJoinInfo{ - { - LeaderAPIAddr: leader.Client.Address(), - TLSConfig: leader.TLSConfig(), - }, - } - - // Join followers - for i := 1; i < len(cluster.Cores); i++ { - core := cluster.Cores[i] - core.UnderlyingRawStorage.(*raft.RaftBackend).SetServerAddressProvider(addressProvider) - _, err := core.JoinRaftCluster(namespace.RootContext(context.Background()), leaderInfos, false) - if err != nil { - t.Fatal(err) - } - - cluster.UnsealCore(t, core) - } - - WaitForNCoresUnsealed(t, cluster, len(cluster.Cores)) -} - -// HardcodedServerAddressProvider is a ServerAddressProvider that uses -// a hardcoded map of raft node addresses. -// -// It is useful in cases where the raft configuration is known ahead of time, -// but some of the cores have not yet had startClusterListener() called (via -// either unsealing or raft joining), and thus do not yet have a ClusterAddr() -// assigned. -type HardcodedServerAddressProvider struct { - Entries map[raftlib.ServerID]raftlib.ServerAddress -} - -func (p *HardcodedServerAddressProvider) ServerAddr(id raftlib.ServerID) (raftlib.ServerAddress, error) { - if addr, ok := p.Entries[id]; ok { - return addr, nil - } - return "", errors.New("could not find cluster addr") -} - -// NewHardcodedServerAddressProvider is a convenience function that makes a -// ServerAddressProvider from a given cluster address base port. -func NewHardcodedServerAddressProvider(numCores, baseClusterPort int) raftlib.ServerAddressProvider { - entries := make(map[raftlib.ServerID]raftlib.ServerAddress) - - for i := 0; i < numCores; i++ { - id := fmt.Sprintf("core-%d", i) - addr := fmt.Sprintf("127.0.0.1:%d", baseClusterPort+i) - entries[raftlib.ServerID(id)] = raftlib.ServerAddress(addr) - } - - return &HardcodedServerAddressProvider{ - entries, - } -} - -// VerifyRaftConfiguration checks that we have a valid raft configuration, i.e. -// the correct number of servers, having the correct NodeIDs, and exactly one -// leader. -func VerifyRaftConfiguration(core *vault.TestClusterCore, numCores int) error { - backend := core.UnderlyingRawStorage.(*raft.RaftBackend) - ctx := namespace.RootContext(context.Background()) - config, err := backend.GetConfiguration(ctx) - if err != nil { - return err - } - - servers := config.Servers - if len(servers) != numCores { - return fmt.Errorf("Found %d servers, not %d", len(servers), numCores) - } - - leaders := 0 - for i, s := range servers { - if s.NodeID != fmt.Sprintf("core-%d", i) { - return fmt.Errorf("Found unexpected node ID %q", s.NodeID) - } - if s.Leader { - leaders++ - } - } - - if leaders != 1 { - return fmt.Errorf("Found %d leaders", leaders) - } - - return nil -} - -func RaftAppliedIndex(core *vault.TestClusterCore) uint64 { - return core.UnderlyingRawStorage.(*raft.RaftBackend).AppliedIndex() -} - -func WaitForRaftApply(t testing.T, core *vault.TestClusterCore, index uint64) { - t.Helper() - - backend := core.UnderlyingRawStorage.(*raft.RaftBackend) - for i := 0; i < 30; i++ { - if backend.AppliedIndex() >= index { - return - } - - time.Sleep(time.Second) - } - - t.Fatalf("node did not apply index") -} - -// AwaitLeader waits for one of the cluster's nodes to become leader. -func AwaitLeader(t testing.T, cluster *vault.TestCluster) (int, error) { - timeout := time.Now().Add(60 * time.Second) - for { - if time.Now().After(timeout) { - break - } - - for i, core := range cluster.Cores { - if core.Core.Sealed() { - continue - } - - isLeader, _, _, _ := core.Leader() - if isLeader { - return i, nil - } - } - - time.Sleep(time.Second) - } - - return 0, fmt.Errorf("timeout waiting leader") -} - -func GenerateDebugLogs(t testing.T, client *api.Client) chan struct{} { - t.Helper() - - stopCh := make(chan struct{}) - - go func() { - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - for { - select { - case <-stopCh: - return - case <-ticker.C: - err := client.Sys().Mount("foo", &api.MountInput{ - Type: "kv", - Options: map[string]string{ - "version": "1", - }, - }) - if err != nil { - t.Fatal(err) - } - - err = client.Sys().Unmount("foo") - if err != nil { - t.Fatal(err) - } - } - } - }() - - return stopCh -} - -// VerifyRaftPeers verifies that the raft configuration contains a given set of peers. -// The `expected` contains a map of expected peers. Existing entries are deleted -// from the map by removing entries whose keys are in the raft configuration. -// Remaining entries result in an error return so that the caller can poll for -// an expected configuration. -func VerifyRaftPeers(t testing.T, client *api.Client, expected map[string]bool) error { - t.Helper() - - resp, err := client.Logical().Read("sys/storage/raft/configuration") - if err != nil { - t.Fatalf("error reading raft config: %v", err) - } - - if resp == nil || resp.Data == nil { - t.Fatal("missing response data") - } - - config, ok := resp.Data["config"].(map[string]interface{}) - if !ok { - t.Fatal("missing config in response data") - } - - servers, ok := config["servers"].([]interface{}) - if !ok { - t.Fatal("missing servers in response data config") - } - - // Iterate through the servers and remove the node found in the response - // from the expected collection - for _, s := range servers { - server := s.(map[string]interface{}) - delete(expected, server["node_id"].(string)) - } - - // If the collection is non-empty, it means that the peer was not found in - // the response. - if len(expected) != 0 { - return fmt.Errorf("failed to read configuration successfully, expected peers not found in configuration list: %v", expected) - } - - return nil -} - -func TestMetricSinkProvider(gaugeInterval time.Duration) func(string) (*metricsutil.ClusterMetricSink, *metricsutil.MetricsHelper) { - return func(clusterName string) (*metricsutil.ClusterMetricSink, *metricsutil.MetricsHelper) { - inm := metrics.NewInmemSink(1000000*time.Hour, 2000000*time.Hour) - clusterSink := metricsutil.NewClusterMetricSink(clusterName, inm) - clusterSink.GaugeInterval = gaugeInterval - return clusterSink, metricsutil.NewMetricsHelper(inm, false) - } -} - -func SysMetricsReq(client *api.Client, cluster *vault.TestCluster, unauth bool) (*SysMetricsJSON, error) { - r := client.NewRequest("GET", "/v1/sys/metrics") - if !unauth { - r.Headers.Set("X-Vault-Token", cluster.RootToken) - } - var data SysMetricsJSON - resp, err := client.RawRequestWithContext(context.Background(), r) - if err != nil { - return nil, err - } - bodyBytes, err := ioutil.ReadAll(resp.Response.Body) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if err := json.Unmarshal(bodyBytes, &data); err != nil { - return nil, errors.New("failed to unmarshal:" + err.Error()) - } - return &data, nil -} - -type SysMetricsJSON struct { - Gauges []gaugeJSON `json:"Gauges"` - Counters []counterJSON `json:"Counters"` - - // note: this is referred to as a "Summary" type in our telemetry docs, but - // the field name in the JSON is "Samples" - Summaries []summaryJSON `json:"Samples"` -} - -type baseInfoJSON struct { - Name string `json:"Name"` - Labels map[string]interface{} `json:"Labels"` -} - -type gaugeJSON struct { - baseInfoJSON - Value int `json:"Value"` -} - -type counterJSON struct { - baseInfoJSON - Count int `json:"Count"` - Rate float64 `json:"Rate"` - Sum int `json:"Sum"` - Min int `json:"Min"` - Max int `json:"Max"` - Mean float64 `json:"Mean"` - Stddev float64 `json:"Stddev"` -} - -type summaryJSON struct { - baseInfoJSON - Count int `json:"Count"` - Rate float64 `json:"Rate"` - Sum float64 `json:"Sum"` - Min float64 `json:"Min"` - Max float64 `json:"Max"` - Mean float64 `json:"Mean"` - Stddev float64 `json:"Stddev"` -} - -// SetNonRootToken sets a token on :client: with a fairly generic policy. -// This is useful if a test needs to examine differing behavior based on if a -// root token is passed with the request. -func SetNonRootToken(client *api.Client) error { - policy := `path "*" { capabilities = ["create", "update", "read"] }` - if err := client.Sys().PutPolicy("policy", policy); err != nil { - return fmt.Errorf("error putting policy: %v", err) - } - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"policy"}, - TTL: "30m", - }) - if err != nil { - return fmt.Errorf("error creating token secret: %v", err) - } - - if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { - return fmt.Errorf("missing token auth data") - } - - client.SetToken(secret.Auth.ClientToken) - return nil -} - -// RetryUntilAtCadence runs f until it returns a nil result or the timeout is reached. -// If a nil result hasn't been obtained by timeout, calls t.Fatal. -func RetryUntilAtCadence(t testing.T, timeout, sleepTime time.Duration, f func() error) { - t.Helper() - deadline := time.Now().Add(timeout) - var err error - for time.Now().Before(deadline) { - if err = f(); err == nil { - return - } - time.Sleep(sleepTime) - } - t.Fatalf("did not complete before deadline, err: %v", err) -} - -// RetryUntil runs f until it returns a nil result or the timeout is reached. -// If a nil result hasn't been obtained by timeout, calls t.Fatal. -func RetryUntil(t testing.T, timeout time.Duration, f func() error) { - t.Helper() - deadline := time.Now().Add(timeout) - var err error - for time.Now().Before(deadline) { - if err = f(); err == nil { - return - } - time.Sleep(100 * time.Millisecond) - } - t.Fatalf("did not complete before deadline, err: %v", err) -} - -// CreateEntityAndAlias clones an existing client and creates an entity/alias. -// It returns the cloned client, entityID, and aliasID. -func CreateEntityAndAlias(t testing.T, client *api.Client, mountAccessor, entityName, aliasName string) (*api.Client, string, string) { - t.Helper() - userClient, err := client.Clone() - if err != nil { - t.Fatalf("failed to clone the client:%v", err) - } - userClient.SetToken(client.Token()) - - resp, err := client.Logical().WriteWithContext(context.Background(), "identity/entity", map[string]interface{}{ - "name": entityName, - }) - if err != nil { - t.Fatalf("failed to create an entity:%v", err) - } - entityID := resp.Data["id"].(string) - - aliasResp, err := client.Logical().WriteWithContext(context.Background(), "identity/entity-alias", map[string]interface{}{ - "name": aliasName, - "canonical_id": entityID, - "mount_accessor": mountAccessor, - }) - if err != nil { - t.Fatalf("failed to create an entity alias:%v", err) - } - aliasID := aliasResp.Data["id"].(string) - if aliasID == "" { - t.Fatal("Alias ID not present in response") - } - _, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("auth/userpass/users/%s", aliasName), map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("failed to configure userpass backend: %v", err) - } - - return userClient, entityID, aliasID -} - -// SetupTOTPMount enables the totp secrets engine by mounting it. This requires -// that the test cluster has a totp backend available. -func SetupTOTPMount(t testing.T, client *api.Client) { - t.Helper() - // Mount the TOTP backend - mountInfo := &api.MountInput{ - Type: "totp", - } - if err := client.Sys().Mount("totp", mountInfo); err != nil { - t.Fatalf("failed to mount totp backend: %v", err) - } -} - -// SetupTOTPMethod configures the TOTP secrets engine with a provided config map. -func SetupTOTPMethod(t testing.T, client *api.Client, config map[string]interface{}) string { - t.Helper() - - resp1, err := client.Logical().Write("identity/mfa/method/totp", config) - - if err != nil || (resp1 == nil) { - t.Fatalf("bad: resp: %#v\n err: %v", resp1, err) - } - - methodID := resp1.Data["method_id"].(string) - if methodID == "" { - t.Fatalf("method ID is empty") - } - - return methodID -} - -// SetupMFALoginEnforcement configures a single enforcement method using the -// provided config map. "name" field is required in the config map. -func SetupMFALoginEnforcement(t testing.T, client *api.Client, config map[string]interface{}) { - t.Helper() - enfName, ok := config["name"] - if !ok { - t.Fatalf("couldn't find name in login-enforcement config") - } - _, err := client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/login-enforcement/%s", enfName), config) - if err != nil { - t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) - } -} - -// SetupUserpassMountAccessor sets up userpass auth and returns its mount -// accessor. This requires that the test cluster has a "userpass" auth method -// available. -func SetupUserpassMountAccessor(t testing.T, client *api.Client) string { - t.Helper() - // Enable Userpass authentication - err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", - }) - if err != nil { - t.Fatalf("failed to enable userpass auth: %v", err) - } - - auths, err := client.Sys().ListAuthWithContext(context.Background()) - if err != nil { - t.Fatalf("failed to list auth methods: %v", err) - } - if auths == nil || auths["userpass/"] == nil { - t.Fatalf("failed to get userpass mount accessor") - } - - return auths["userpass/"].Accessor -} - -// RegisterEntityInTOTPEngine registers an entity with a methodID and returns -// the generated name. -func RegisterEntityInTOTPEngine(t testing.T, client *api.Client, entityID, methodID string) string { - t.Helper() - totpGenName := fmt.Sprintf("%s-%s", entityID, methodID) - secret, err := client.Logical().WriteWithContext(context.Background(), "identity/mfa/method/totp/admin-generate", map[string]interface{}{ - "entity_id": entityID, - "method_id": methodID, - }) - if err != nil { - t.Fatalf("failed to generate a TOTP secret on an entity: %v", err) - } - totpURL := secret.Data["url"].(string) - if totpURL == "" { - t.Fatalf("failed to get TOTP url in secret response: %+v", secret) - } - _, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("totp/keys/%s", totpGenName), map[string]interface{}{ - "url": totpURL, - }) - if err != nil { - t.Fatalf("failed to register a TOTP URL: %v", err) - } - enfPath := fmt.Sprintf("identity/mfa/login-enforcement/%s", methodID[0:4]) - _, err = client.Logical().WriteWithContext(context.Background(), enfPath, map[string]interface{}{ - "name": methodID[0:4], - "identity_entity_ids": []string{entityID}, - "mfa_method_ids": []string{methodID}, - }) - if err != nil { - t.Fatalf("failed to create login enforcement") - } - - return totpGenName -} - -// GetTOTPCodeFromEngine requests a TOTP code from the specified enginePath. -func GetTOTPCodeFromEngine(t testing.T, client *api.Client, enginePath string) string { - t.Helper() - totpPath := fmt.Sprintf("totp/code/%s", enginePath) - secret, err := client.Logical().ReadWithContext(context.Background(), totpPath) - if err != nil { - t.Fatalf("failed to create totp passcode: %v", err) - } - if secret == nil || secret.Data == nil { - t.Fatalf("bad secret returned from %s", totpPath) - } - return secret.Data["code"].(string) -} - -// SetupLoginMFATOTP setups up a TOTP MFA using some basic configuration and -// returns all relevant information to the client. -func SetupLoginMFATOTP(t testing.T, client *api.Client, methodName string, waitPeriod int) (*api.Client, string, string) { - t.Helper() - // Mount the totp secrets engine - SetupTOTPMount(t, client) - - // Create a mount accessor to associate with an entity - mountAccessor := SetupUserpassMountAccessor(t, client) - - // Create a test entity and alias - entityClient, entityID, _ := CreateEntityAndAlias(t, client, mountAccessor, "entity1", "testuser1") - - // Configure a default TOTP method - totpConfig := map[string]interface{}{ - "issuer": "yCorp", - "period": waitPeriod, - "algorithm": "SHA256", - "digits": 6, - "skew": 1, - "key_size": 20, - "qr_size": 200, - "max_validation_attempts": 5, - "method_name": methodName, - } - methodID := SetupTOTPMethod(t, client, totpConfig) - - // Configure a default login enforcement - enforcementConfig := map[string]interface{}{ - "auth_method_types": []string{"userpass"}, - "name": methodID[0:4], - "mfa_method_ids": []string{methodID}, - } - - SetupMFALoginEnforcement(t, client, enforcementConfig) - return entityClient, entityID, methodID -} - -func SkipUnlessEnvVarsSet(t testing.T, envVars []string) { - t.Helper() - - for _, i := range envVars { - if os.Getenv(i) == "" { - t.Skipf("%s must be set for this test to run", strings.Join(envVars, " ")) - } - } -} - -// WaitForNodesExcludingSelectedStandbys is variation on WaitForActiveNodeAndStandbys. -// It waits for the active node before waiting for standby nodes, however -// it will not wait for cores with indexes that match those specified as arguments. -// Whilst you could specify index 0 which is likely to be the leader node, the function -// checks for the leader first regardless of the indexes to skip, so it would be redundant to do so. -// The intention/use case for this function is to allow a cluster to start and become active with one -// or more nodes not joined, so that we can test scenarios where a node joins later. -// e.g. 4 nodes in the cluster, only 3 nodes in cluster 'active', 1 node can be joined later in tests. -func WaitForNodesExcludingSelectedStandbys(t testing.T, cluster *vault.TestCluster, indexesToSkip ...int) { - WaitForActiveNode(t, cluster) - - contains := func(elems []int, e int) bool { - for _, v := range elems { - if v == e { - return true - } - } - - return false - } - for i, core := range cluster.Cores { - if contains(indexesToSkip, i) { - continue - } - - if standby, _ := core.Core.Standby(); standby { - WaitForStandbyNode(t, core) - } - } -} - -// IsLocalOrRegressionTests returns true when the tests are running locally (not in CI), or when -// the regression test env var (VAULT_REGRESSION_TESTS) is provided. -func IsLocalOrRegressionTests() bool { - return os.Getenv("CI") == "" || os.Getenv("VAULT_REGRESSION_TESTS") == "true" -} diff --git a/helper/testhelpers/testhelpers_oss.go b/helper/testhelpers/testhelpers_oss.go deleted file mode 100644 index 8965ed916..000000000 --- a/helper/testhelpers/testhelpers_oss.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -//go:build !enterprise - -package testhelpers - -import ( - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/go-testing-interface" -) - -// WaitForActiveNodeAndStandbys does nothing more than wait for the active node -// on OSS. On enterprise it waits for perf standbys to be healthy too. -func WaitForActiveNodeAndStandbys(t testing.T, cluster *vault.TestCluster) { - WaitForActiveNode(t, cluster) - for _, core := range cluster.Cores { - if standby, _ := core.Core.Standby(); standby { - WaitForStandbyNode(t, core) - } - } -} diff --git a/helper/testhelpers/teststorage/consul/consul.go b/helper/testhelpers/teststorage/consul/consul.go deleted file mode 100644 index 26a2175da..000000000 --- a/helper/testhelpers/teststorage/consul/consul.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "sync" - realtesting "testing" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/helper/testhelpers/consul" - physConsul "github.com/hashicorp/vault/physical/consul" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/go-testing-interface" -) - -func MakeConsulBackend(t testing.T, logger hclog.Logger) *vault.PhysicalBackendBundle { - cleanup, config := consul.PrepareTestContainer(t.(*realtesting.T), "", false, true) - - consulConf := map[string]string{ - "address": config.Address(), - "token": config.Token, - "max_parallel": "32", - } - consulBackend, err := physConsul.NewConsulBackend(consulConf, logger) - if err != nil { - t.Fatal(err) - } - return &vault.PhysicalBackendBundle{ - Backend: consulBackend, - Cleanup: cleanup, - } -} - -func ConsulBackendSetup(conf *vault.CoreConfig, opts *vault.TestClusterOptions) { - m := &consulContainerManager{} - opts.PhysicalFactory = m.Backend -} - -// consulContainerManager exposes Backend which matches the PhysicalFactory func -// type. When called, it will ensure that a separate Consul container is started -// for each distinct vault cluster that calls it and ensures that each Vault -// core gets a separate Consul backend instance since that contains state -// related to lock sessions. The whole test framework doesn't have a concept of -// "cluster names" outside of the prefix attached to the logger and other -// backend factories, mostly via SharedPhysicalFactory currently implicitly rely -// on being called in a sequence of core 0, 1, 2,... on one cluster and then -// core 0, 1, 2... on the next and so on. Refactoring lots of things to make -// first-class cluster identifiers a thing seems like a heavy lift given that we -// already rely on sequence of calls everywhere else anyway so we do the same -// here - each time the Backend method is called with coreIdx == 0 we create a -// whole new Consul and assume subsequent non 0 index cores are in the same -// cluster. -type consulContainerManager struct { - mu sync.Mutex - current *consulContainerBackendFactory -} - -func (m *consulContainerManager) Backend(t testing.T, coreIdx int, - logger hclog.Logger, conf map[string]interface{}, -) *vault.PhysicalBackendBundle { - m.mu.Lock() - if coreIdx == 0 || m.current == nil { - // Create a new consul container factory - m.current = &consulContainerBackendFactory{} - } - f := m.current - m.mu.Unlock() - - return f.Backend(t, coreIdx, logger, conf) -} - -type consulContainerBackendFactory struct { - mu sync.Mutex - refCount int - cleanupFn func() - config map[string]string -} - -func (f *consulContainerBackendFactory) Backend(t testing.T, coreIdx int, - logger hclog.Logger, conf map[string]interface{}, -) *vault.PhysicalBackendBundle { - f.mu.Lock() - defer f.mu.Unlock() - - if f.refCount == 0 { - f.startContainerLocked(t) - logger.Debug("started consul container", "clusterID", conf["cluster_id"], - "address", f.config["address"]) - } - - f.refCount++ - consulBackend, err := physConsul.NewConsulBackend(f.config, logger.Named("consul")) - if err != nil { - t.Fatal(err) - } - return &vault.PhysicalBackendBundle{ - Backend: consulBackend, - Cleanup: f.cleanup, - } -} - -func (f *consulContainerBackendFactory) startContainerLocked(t testing.T) { - cleanup, config := consul.PrepareTestContainer(t.(*realtesting.T), "", false, true) - f.config = map[string]string{ - "address": config.Address(), - "token": config.Token, - "max_parallel": "32", - } - f.cleanupFn = cleanup -} - -func (f *consulContainerBackendFactory) cleanup() { - f.mu.Lock() - defer f.mu.Unlock() - - if f.refCount < 1 || f.cleanupFn == nil { - return - } - f.refCount-- - if f.refCount == 0 { - f.cleanupFn() - f.cleanupFn = nil - } -} diff --git a/helper/testhelpers/teststorage/teststorage.go b/helper/testhelpers/teststorage/teststorage.go deleted file mode 100644 index 9c588078f..000000000 --- a/helper/testhelpers/teststorage/teststorage.go +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package teststorage - -import ( - "fmt" - "io/ioutil" - "math/rand" - "os" - "time" - - "github.com/hashicorp/go-hclog" - logicalKv "github.com/hashicorp/vault-plugin-secrets-kv" - "github.com/hashicorp/vault/audit" - auditFile "github.com/hashicorp/vault/builtin/audit/file" - auditSocket "github.com/hashicorp/vault/builtin/audit/socket" - auditSyslog "github.com/hashicorp/vault/builtin/audit/syslog" - logicalDb "github.com/hashicorp/vault/builtin/logical/database" - "github.com/hashicorp/vault/builtin/plugin" - "github.com/hashicorp/vault/helper/testhelpers" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - vaulthttp "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/physical/raft" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/physical" - physFile "github.com/hashicorp/vault/sdk/physical/file" - "github.com/hashicorp/vault/sdk/physical/inmem" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/go-testing-interface" -) - -func MakeInmemBackend(t testing.T, logger hclog.Logger) *vault.PhysicalBackendBundle { - inm, err := inmem.NewTransactionalInmem(nil, logger) - if err != nil { - t.Fatal(err) - } - inmha, err := inmem.NewInmemHA(nil, logger) - if err != nil { - t.Fatal(err) - } - - return &vault.PhysicalBackendBundle{ - Backend: inm, - HABackend: inmha.(physical.HABackend), - } -} - -func MakeLatentInmemBackend(t testing.T, logger hclog.Logger) *vault.PhysicalBackendBundle { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - jitter := r.Intn(20) - latency := time.Duration(r.Intn(15)) * time.Millisecond - - pbb := MakeInmemBackend(t, logger) - latencyInjector := physical.NewTransactionalLatencyInjector(pbb.Backend, latency, jitter, logger) - pbb.Backend = latencyInjector - return pbb -} - -func MakeInmemNonTransactionalBackend(t testing.T, logger hclog.Logger) *vault.PhysicalBackendBundle { - inm, err := inmem.NewInmem(nil, logger) - if err != nil { - t.Fatal(err) - } - inmha, err := inmem.NewInmemHA(nil, logger) - if err != nil { - t.Fatal(err) - } - - return &vault.PhysicalBackendBundle{ - Backend: inm, - HABackend: inmha.(physical.HABackend), - } -} - -func MakeFileBackend(t testing.T, logger hclog.Logger) *vault.PhysicalBackendBundle { - path, err := ioutil.TempDir("", "vault-integ-file-") - if err != nil { - t.Fatal(err) - } - fileConf := map[string]string{ - "path": path, - } - fileBackend, err := physFile.NewTransactionalFileBackend(fileConf, logger) - if err != nil { - t.Fatal(err) - } - - inmha, err := inmem.NewInmemHA(nil, logger) - if err != nil { - t.Fatal(err) - } - - return &vault.PhysicalBackendBundle{ - Backend: fileBackend, - HABackend: inmha.(physical.HABackend), - Cleanup: func() { - err := os.RemoveAll(path) - if err != nil { - t.Fatal(err) - } - }, - } -} - -func MakeRaftBackend(t testing.T, coreIdx int, logger hclog.Logger, extraConf map[string]interface{}) *vault.PhysicalBackendBundle { - nodeID := fmt.Sprintf("core-%d", coreIdx) - raftDir, err := ioutil.TempDir("", "vault-raft-") - if err != nil { - t.Fatal(err) - } - // t.Logf("raft dir: %s", raftDir) - cleanupFunc := func() { - os.RemoveAll(raftDir) - } - - logger.Info("raft dir", "dir", raftDir) - - conf := map[string]string{ - "path": raftDir, - "node_id": nodeID, - "performance_multiplier": "8", - } - for k, v := range extraConf { - val, ok := v.(string) - if ok { - conf[k] = val - } - } - - backend, err := raft.NewRaftBackend(conf, logger.Named("raft")) - if err != nil { - cleanupFunc() - t.Fatal(err) - } - - return &vault.PhysicalBackendBundle{ - Backend: backend, - Cleanup: cleanupFunc, - } -} - -// RaftHAFactory returns a PhysicalBackendBundle with raft set as the HABackend -// and the physical.Backend provided in PhysicalBackendBundler as the storage -// backend. -func RaftHAFactory(f PhysicalBackendBundler) func(t testing.T, coreIdx int, logger hclog.Logger, conf map[string]interface{}) *vault.PhysicalBackendBundle { - return func(t testing.T, coreIdx int, logger hclog.Logger, conf map[string]interface{}) *vault.PhysicalBackendBundle { - // Call the factory func to create the storage backend - physFactory := SharedPhysicalFactory(f) - bundle := physFactory(t, coreIdx, logger, nil) - - // This can happen if a shared physical backend is called on a non-0th core. - if bundle == nil { - bundle = new(vault.PhysicalBackendBundle) - } - - raftDir := makeRaftDir(t) - cleanupFunc := func() { - os.RemoveAll(raftDir) - } - - nodeID := fmt.Sprintf("core-%d", coreIdx) - backendConf := map[string]string{ - "path": raftDir, - "node_id": nodeID, - "performance_multiplier": "8", - "autopilot_reconcile_interval": "300ms", - "autopilot_update_interval": "100ms", - } - - // Create and set the HA Backend - raftBackend, err := raft.NewRaftBackend(backendConf, logger) - if err != nil { - bundle.Cleanup() - t.Fatal(err) - } - bundle.HABackend = raftBackend.(physical.HABackend) - - // Re-wrap the cleanup func - bundleCleanup := bundle.Cleanup - bundle.Cleanup = func() { - if bundleCleanup != nil { - bundleCleanup() - } - cleanupFunc() - } - - return bundle - } -} - -type PhysicalBackendBundler func(t testing.T, logger hclog.Logger) *vault.PhysicalBackendBundle - -func SharedPhysicalFactory(f PhysicalBackendBundler) func(t testing.T, coreIdx int, logger hclog.Logger, conf map[string]interface{}) *vault.PhysicalBackendBundle { - return func(t testing.T, coreIdx int, logger hclog.Logger, conf map[string]interface{}) *vault.PhysicalBackendBundle { - if coreIdx == 0 { - return f(t, logger) - } - return nil - } -} - -type ClusterSetupMutator func(conf *vault.CoreConfig, opts *vault.TestClusterOptions) - -func InmemBackendSetup(conf *vault.CoreConfig, opts *vault.TestClusterOptions) { - opts.PhysicalFactory = SharedPhysicalFactory(MakeInmemBackend) -} - -func InmemLatentBackendSetup(conf *vault.CoreConfig, opts *vault.TestClusterOptions) { - opts.PhysicalFactory = SharedPhysicalFactory(MakeLatentInmemBackend) -} - -func InmemNonTransactionalBackendSetup(conf *vault.CoreConfig, opts *vault.TestClusterOptions) { - opts.PhysicalFactory = SharedPhysicalFactory(MakeInmemNonTransactionalBackend) -} - -func FileBackendSetup(conf *vault.CoreConfig, opts *vault.TestClusterOptions) { - opts.PhysicalFactory = SharedPhysicalFactory(MakeFileBackend) -} - -func RaftBackendSetup(conf *vault.CoreConfig, opts *vault.TestClusterOptions) { - opts.KeepStandbysSealed = true - opts.PhysicalFactory = MakeRaftBackend - opts.SetupFunc = func(t testing.T, c *vault.TestCluster) { - if opts.NumCores != 1 { - testhelpers.RaftClusterJoinNodes(t, c) - time.Sleep(15 * time.Second) - } - } -} - -func RaftHASetup(conf *vault.CoreConfig, opts *vault.TestClusterOptions, bundler PhysicalBackendBundler) { - opts.KeepStandbysSealed = true - opts.PhysicalFactory = RaftHAFactory(bundler) -} - -func ClusterSetup(conf *vault.CoreConfig, opts *vault.TestClusterOptions, setup ClusterSetupMutator) (*vault.CoreConfig, *vault.TestClusterOptions) { - var localConf vault.CoreConfig - localConf.DisableAutopilot = true - if conf != nil { - localConf = *conf - } - localOpts := vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - } - if opts != nil { - localOpts = *opts - } - if setup == nil { - setup = InmemBackendSetup - } - setup(&localConf, &localOpts) - if localConf.CredentialBackends == nil { - localConf.CredentialBackends = map[string]logical.Factory{ - "plugin": plugin.Factory, - } - } - if localConf.LogicalBackends == nil { - localConf.LogicalBackends = map[string]logical.Factory{ - "plugin": plugin.Factory, - "database": logicalDb.Factory, - // This is also available in the plugin catalog, but is here due to the need to - // automatically mount it. - "kv": logicalKv.Factory, - } - } - if localConf.AuditBackends == nil { - localConf.AuditBackends = map[string]audit.Factory{ - "file": auditFile.Factory, - "socket": auditSocket.Factory, - "syslog": auditSyslog.Factory, - "noop": corehelpers.NoopAuditFactory(nil), - } - } - - return &localConf, &localOpts -} diff --git a/helper/testhelpers/teststorage/teststorage_reusable.go b/helper/testhelpers/teststorage/teststorage_reusable.go deleted file mode 100644 index 1a1ba8b76..000000000 --- a/helper/testhelpers/teststorage/teststorage_reusable.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package teststorage - -import ( - "fmt" - "io/ioutil" - "os" - - hclog "github.com/hashicorp/go-hclog" - raftlib "github.com/hashicorp/raft" - "github.com/hashicorp/vault/physical/raft" - "github.com/hashicorp/vault/sdk/physical" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/go-testing-interface" -) - -// ReusableStorage is a physical backend that can be re-used across -// multiple test clusters in sequence. It is useful for testing things like -// seal migration, wherein a given physical backend must be re-used as several -// test clusters are sequentially created, tested, and discarded. -type ReusableStorage struct { - // IsRaft specifies whether the storage is using a raft backend. - IsRaft bool - - // Setup should be called just before a new TestCluster is created. - Setup ClusterSetupMutator - - // Cleanup should be called after a TestCluster is no longer - // needed -- generally in a defer, just before the call to - // cluster.Cleanup(). - Cleanup func(t testing.T, cluster *vault.TestCluster) -} - -// StorageCleanup is a function that should be called once -- at the very end -// of a given unit test, after each of the sequence of clusters have been -// created, tested, and discarded. -type StorageCleanup func() - -// MakeReusableStorage makes a physical backend that can be re-used across -// multiple test clusters in sequence. -func MakeReusableStorage(t testing.T, logger hclog.Logger, bundle *vault.PhysicalBackendBundle) (ReusableStorage, StorageCleanup) { - storage := ReusableStorage{ - IsRaft: false, - - Setup: func(conf *vault.CoreConfig, opts *vault.TestClusterOptions) { - opts.PhysicalFactory = func(t testing.T, coreIdx int, logger hclog.Logger, conf map[string]interface{}) *vault.PhysicalBackendBundle { - if coreIdx == 0 { - // We intentionally do not clone the backend's Cleanup func, - // because we don't want it to be run until the entire test has - // been completed. - return &vault.PhysicalBackendBundle{ - Backend: bundle.Backend, - HABackend: bundle.HABackend, - } - } - return nil - } - }, - - // No-op - Cleanup: func(t testing.T, cluster *vault.TestCluster) {}, - } - - cleanup := func() { - if bundle.Cleanup != nil { - bundle.Cleanup() - } - } - - return storage, cleanup -} - -// MakeReusableRaftStorage makes a physical raft backend that can be re-used -// across multiple test clusters in sequence. -func MakeReusableRaftStorage(t testing.T, logger hclog.Logger, numCores int, addressProvider raftlib.ServerAddressProvider) (ReusableStorage, StorageCleanup) { - raftDirs := make([]string, numCores) - for i := 0; i < numCores; i++ { - raftDirs[i] = makeRaftDir(t) - } - - storage := ReusableStorage{ - IsRaft: true, - - Setup: func(conf *vault.CoreConfig, opts *vault.TestClusterOptions) { - conf.DisablePerformanceStandby = true - opts.KeepStandbysSealed = true - opts.PhysicalFactory = func(t testing.T, coreIdx int, logger hclog.Logger, conf map[string]interface{}) *vault.PhysicalBackendBundle { - return makeReusableRaftBackend(t, coreIdx, logger, raftDirs[coreIdx], addressProvider, false) - } - }, - - // Close open files being used by raft. - Cleanup: func(t testing.T, cluster *vault.TestCluster) { - for i := 0; i < len(cluster.Cores); i++ { - CloseRaftStorage(t, cluster, i) - } - }, - } - - cleanup := func() { - for _, rd := range raftDirs { - os.RemoveAll(rd) - } - } - - return storage, cleanup -} - -// CloseRaftStorage closes open files being used by raft. -func CloseRaftStorage(t testing.T, cluster *vault.TestCluster, idx int) { - raftStorage := cluster.Cores[idx].UnderlyingRawStorage.(*raft.RaftBackend) - if err := raftStorage.Close(); err != nil { - t.Fatal(err) - } -} - -func MakeReusableRaftHAStorage(t testing.T, logger hclog.Logger, numCores int, bundle *vault.PhysicalBackendBundle) (ReusableStorage, StorageCleanup) { - raftDirs := make([]string, numCores) - for i := 0; i < numCores; i++ { - raftDirs[i] = makeRaftDir(t) - } - - storage := ReusableStorage{ - Setup: func(conf *vault.CoreConfig, opts *vault.TestClusterOptions) { - opts.KeepStandbysSealed = true - opts.PhysicalFactory = func(t testing.T, coreIdx int, logger hclog.Logger, conf map[string]interface{}) *vault.PhysicalBackendBundle { - haBundle := makeReusableRaftBackend(t, coreIdx, logger, raftDirs[coreIdx], nil, true) - - return &vault.PhysicalBackendBundle{ - Backend: bundle.Backend, - HABackend: haBundle.HABackend, - } - } - }, - - // Close open files being used by raft. - Cleanup: func(t testing.T, cluster *vault.TestCluster) { - for _, core := range cluster.Cores { - raftStorage := core.UnderlyingHAStorage.(*raft.RaftBackend) - if err := raftStorage.Close(); err != nil { - t.Fatal(err) - } - } - }, - } - - cleanup := func() { - if bundle.Cleanup != nil { - bundle.Cleanup() - } - - for _, rd := range raftDirs { - os.RemoveAll(rd) - } - } - - return storage, cleanup -} - -func makeRaftDir(t testing.T) string { - raftDir, err := ioutil.TempDir("", "vault-raft-") - if err != nil { - t.Fatal(err) - } - // t.Logf("raft dir: %s", raftDir) - return raftDir -} - -func makeReusableRaftBackend(t testing.T, coreIdx int, logger hclog.Logger, raftDir string, addressProvider raftlib.ServerAddressProvider, ha bool) *vault.PhysicalBackendBundle { - nodeID := fmt.Sprintf("core-%d", coreIdx) - conf := map[string]string{ - "path": raftDir, - "node_id": nodeID, - "performance_multiplier": "8", - "autopilot_reconcile_interval": "300ms", - "autopilot_update_interval": "100ms", - } - - backend, err := raft.NewRaftBackend(conf, logger) - if err != nil { - t.Fatal(err) - } - - if addressProvider != nil { - backend.(*raft.RaftBackend).SetServerAddressProvider(addressProvider) - } - - bundle := new(vault.PhysicalBackendBundle) - - if ha { - bundle.HABackend = backend.(physical.HABackend) - } else { - bundle.Backend = backend - } - return bundle -} diff --git a/helper/timeutil/timeutil.go b/helper/timeutil/timeutil.go index 56a20615a..5663a9d31 100644 --- a/helper/timeutil/timeutil.go +++ b/helper/timeutil/timeutil.go @@ -8,7 +8,6 @@ import ( "fmt" "strconv" "strings" - "testing" "time" ) @@ -146,17 +145,6 @@ func MonthsPreviousTo(months int, now time.Time) time.Time { return firstOfMonth.AddDate(0, -months, 0) } -// Skip this test if too close to the end of a month! -func SkipAtEndOfMonth(t *testing.T) { - t.Helper() - - thisMonth := StartOfMonth(time.Now().UTC()) - endOfMonth := EndOfMonth(thisMonth) - if endOfMonth.Sub(time.Now()) < 10*time.Minute { - t.Skip("too close to end of month") - } -} - // This interface allows unit tests to substitute in a simulated Clock. type Clock interface { Now() time.Time diff --git a/helper/timeutil/timeutil_test.go b/helper/timeutil/timeutil_test.go deleted file mode 100644 index df14a6fd1..000000000 --- a/helper/timeutil/timeutil_test.go +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package timeutil - -import ( - "reflect" - "testing" - "time" -) - -func TestTimeutil_StartOfPreviousMonth(t *testing.T) { - testCases := []struct { - Input time.Time - Expected time.Time - }{ - { - Input: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - Expected: time.Date(2019, 12, 1, 0, 0, 0, 0, time.UTC), - }, - { - Input: time.Date(2020, 1, 15, 0, 0, 0, 0, time.UTC), - Expected: time.Date(2019, 12, 1, 0, 0, 0, 0, time.UTC), - }, - { - Input: time.Date(2020, 3, 31, 23, 59, 59, 999999999, time.UTC), - Expected: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC), - }, - } - - for _, tc := range testCases { - result := StartOfPreviousMonth(tc.Input) - if !result.Equal(tc.Expected) { - t.Errorf("start of month before %v is %v, got %v", tc.Input, tc.Expected, result) - } - } -} - -func TestTimeutil_StartOfMonth(t *testing.T) { - testCases := []struct { - Input time.Time - Expected time.Time - }{ - { - Input: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - }, - { - Input: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - }, - { - Input: time.Date(2020, 1, 1, 0, 0, 0, 1, time.UTC), - Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - }, - { - Input: time.Date(2020, 1, 31, 23, 59, 59, 999999999, time.UTC), - Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - }, - { - Input: time.Date(2020, 2, 28, 1, 2, 3, 4, time.UTC), - Expected: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC), - }, - } - - for _, tc := range testCases { - result := StartOfMonth(tc.Input) - if !result.Equal(tc.Expected) { - t.Errorf("start of %v is %v, expected %v", tc.Input, result, tc.Expected) - } - } -} - -func TestTimeutil_IsMonthStart(t *testing.T) { - testCases := []struct { - input time.Time - expected bool - }{ - { - input: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - expected: true, - }, - { - input: time.Date(2020, 1, 1, 0, 0, 0, 1, time.UTC), - expected: false, - }, - { - input: time.Date(2020, 4, 5, 0, 0, 0, 0, time.UTC), - expected: false, - }, - { - input: time.Date(2020, 1, 31, 23, 59, 59, 999999999, time.UTC), - expected: false, - }, - } - - for _, tc := range testCases { - result := IsMonthStart(tc.input) - if result != tc.expected { - t.Errorf("is %v the start of the month? expected %t, got %t", tc.input, tc.expected, result) - } - } -} - -func TestTimeutil_EndOfMonth(t *testing.T) { - testCases := []struct { - Input time.Time - Expected time.Time - }{ - { - // The current behavior does not use the nanoseconds - // because we didn't want to clutter the result of end-of-month reporting. - Input: time.Date(2020, 1, 31, 23, 59, 59, 0, time.UTC), - Expected: time.Date(2020, 1, 31, 23, 59, 59, 0, time.UTC), - }, - { - Input: time.Date(2020, 1, 31, 23, 59, 59, 999999999, time.UTC), - Expected: time.Date(2020, 1, 31, 23, 59, 59, 0, time.UTC), - }, - { - Input: time.Date(2020, 1, 15, 1, 2, 3, 4, time.UTC), - Expected: time.Date(2020, 1, 31, 23, 59, 59, 0, time.UTC), - }, - { - // Leap year - Input: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC), - Expected: time.Date(2020, 2, 29, 23, 59, 59, 0, time.UTC), - }, - { - // non-leap year - Input: time.Date(2100, 2, 1, 0, 0, 0, 0, time.UTC), - Expected: time.Date(2100, 2, 28, 23, 59, 59, 0, time.UTC), - }, - } - - for _, tc := range testCases { - result := EndOfMonth(tc.Input) - if !result.Equal(tc.Expected) { - t.Errorf("end of %v is %v, expected %v", tc.Input, result, tc.Expected) - } - } -} - -func TestTimeutil_IsPreviousMonth(t *testing.T) { - testCases := []struct { - tInput time.Time - compareInput time.Time - expected bool - }{ - { - tInput: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - compareInput: time.Date(2020, 1, 31, 0, 0, 0, 0, time.UTC), - expected: false, - }, - { - tInput: time.Date(2019, 12, 31, 0, 0, 0, 0, time.UTC), - compareInput: time.Date(2020, 1, 31, 0, 0, 0, 0, time.UTC), - expected: true, - }, - { - // leap year (false) - tInput: time.Date(2019, 12, 29, 10, 10, 10, 0, time.UTC), - compareInput: time.Date(2020, 2, 29, 10, 10, 10, 0, time.UTC), - expected: false, - }, - { - // leap year (true) - tInput: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - compareInput: time.Date(2020, 2, 29, 10, 10, 10, 0, time.UTC), - expected: true, - }, - { - tInput: time.Date(2018, 5, 5, 5, 0, 0, 0, time.UTC), - compareInput: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - expected: false, - }, - { - // test normalization. want to make subtracting 1 month from 3/30/2020 doesn't yield 2/30/2020, normalized - // to 3/1/2020 - tInput: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC), - compareInput: time.Date(2020, 3, 30, 0, 0, 0, 0, time.UTC), - expected: true, - }, - } - - for _, tc := range testCases { - result := IsPreviousMonth(tc.tInput, tc.compareInput) - if result != tc.expected { - t.Errorf("%v in previous month to %v? expected %t, got %t", tc.tInput, tc.compareInput, tc.expected, result) - } - } -} - -func TestTimeutil_IsCurrentMonth(t *testing.T) { - now := time.Now() - testCases := []struct { - input time.Time - expected bool - }{ - { - input: now, - expected: true, - }, - { - input: StartOfMonth(now).AddDate(0, 0, -1), - expected: false, - }, - { - input: EndOfMonth(now).AddDate(0, 0, -1), - expected: true, - }, - { - input: StartOfMonth(now).AddDate(-1, 0, 0), - expected: false, - }, - } - - for _, tc := range testCases { - result := IsCurrentMonth(tc.input, now) - if result != tc.expected { - t.Errorf("invalid result. expected %t for %v", tc.expected, tc.input) - } - } -} - -// TestTimeutil_IsCurrentDay checks if the test times equals the current day or not. -func TestTimeutil_IsCurrentDay(t *testing.T) { - now := time.Now() - testCases := []struct { - input time.Time - expected bool - }{ - { - input: now, - expected: true, - }, - { - input: StartOfDay(now).AddDate(0, 0, -1), - expected: false, - }, - { - input: StartOfDay(now).AddDate(-1, 0, 0), - expected: false, - }, - { - input: StartOfDay(now).Add(1 * time.Second), - expected: true, - }, - { - input: StartOfDay(now).Add(-1 * time.Second), - expected: false, - }, - { - input: StartOfDay(now).Add(86400), // a day is 86400 seconds - expected: true, - }, - } - - for _, tc := range testCases { - result := IsCurrentDay(tc.input, now) - if result != tc.expected { - t.Errorf("invalid result. expected %t for %v", tc.expected, tc.input) - } - } -} - -func TestTimeUtil_ContiguousMonths(t *testing.T) { - testCases := []struct { - input []time.Time - expected []time.Time - }{ - { - input: []time.Time{ - time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 2, 5, 0, 0, 0, 0, time.UTC), - time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - }, - expected: []time.Time{ - time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 2, 5, 0, 0, 0, 0, time.UTC), - }, - }, - { - input: []time.Time{ - time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - }, - expected: []time.Time{ - time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC), - time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - }, - }, - { - input: []time.Time{ - time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC), - }, - expected: []time.Time{ - time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC), - }, - }, - { - input: []time.Time{}, - expected: []time.Time{}, - }, - { - input: nil, - expected: nil, - }, - { - input: []time.Time{ - time.Date(2020, 2, 2, 0, 0, 0, 0, time.UTC), - time.Date(2020, 1, 15, 0, 0, 0, 0, time.UTC), - }, - expected: []time.Time{ - time.Date(2020, 2, 2, 0, 0, 0, 0, time.UTC), - }, - }, - } - - for _, tc := range testCases { - result := GetMostRecentContiguousMonths(tc.input) - - if !reflect.DeepEqual(tc.expected, result) { - t.Errorf("invalid contiguous segment returned. expected %v, got %v", tc.expected, result) - } - } -} - -func TestTimeUtil_ParseTimeFromPath(t *testing.T) { - testCases := []struct { - input string - expectedOut time.Time - expectError bool - }{ - { - input: "719020800/1", - expectedOut: time.Unix(719020800, 0).UTC(), - expectError: false, - }, - { - input: "1601415205/3", - expectedOut: time.Unix(1601415205, 0).UTC(), - expectError: false, - }, - { - input: "baddata/3", - expectedOut: time.Time{}, - expectError: true, - }, - } - - for _, tc := range testCases { - result, err := ParseTimeFromPath(tc.input) - gotError := err != nil - - if result != tc.expectedOut { - t.Errorf("bad timestamp on input %q. expected: %v got: %v", tc.input, tc.expectedOut, result) - } - if gotError != tc.expectError { - t.Errorf("bad error status on input %q. expected error: %t, got error: %t", tc.input, tc.expectError, gotError) - } - } -} diff --git a/helper/useragent/useragent_test.go b/helper/useragent/useragent_test.go deleted file mode 100644 index f58363a8e..000000000 --- a/helper/useragent/useragent_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package useragent - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestUserAgent(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - - act := String() - - exp := "Vault/1.2.3 (+https://vault-test.com; go5.0)" - require.Equal(t, exp, act) -} - -// TestUserAgent_VaultAgent tests the AgentString() function works -// as expected -func TestUserAgent_VaultAgent(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - - act := AgentString() - - exp := "Vault Agent/1.2.3 (+https://vault-test.com; go5.0)" - require.Equal(t, exp, act) -} - -// TestUserAgent_VaultAgentTemplating tests the AgentTemplatingString() function works -// as expected -func TestUserAgent_VaultAgentTemplating(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - - act := AgentTemplatingString() - - exp := "Vault Agent Templating/1.2.3 (+https://vault-test.com; go5.0)" - require.Equal(t, exp, act) -} - -// TestUserAgent_VaultAgentProxy tests the AgentProxyString() function works -// as expected -func TestUserAgent_VaultAgentProxy(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - - act := AgentProxyString() - - exp := "Vault Agent API Proxy/1.2.3 (+https://vault-test.com; go5.0)" - require.Equal(t, exp, act) -} - -// TestUserAgent_VaultAgentProxyWithProxiedUserAgent tests the AgentProxyStringWithProxiedUserAgent() -// function works as expected -func TestUserAgent_VaultAgentProxyWithProxiedUserAgent(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - userAgent := "my-user-agent" - - act := AgentProxyStringWithProxiedUserAgent(userAgent) - - exp := "Vault Agent API Proxy/1.2.3 (+https://vault-test.com; go5.0); my-user-agent" - require.Equal(t, exp, act) -} - -// TestUserAgent_VaultAgentAutoAuth tests the AgentAutoAuthString() function works -// as expected -func TestUserAgent_VaultAgentAutoAuth(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - - act := AgentAutoAuthString() - - exp := "Vault Agent Auto-Auth/1.2.3 (+https://vault-test.com; go5.0)" - require.Equal(t, exp, act) -} - -// TestUserAgent_VaultProxy tests the ProxyString() function works -// as expected -func TestUserAgent_VaultProxy(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - - act := ProxyString() - - exp := "Vault Proxy/1.2.3 (+https://vault-test.com; go5.0)" - require.Equal(t, exp, act) -} - -// TestUserAgent_VaultProxyAPIProxy tests the ProxyAPIProxyString() function works -// as expected -func TestUserAgent_VaultProxyAPIProxy(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - - act := ProxyAPIProxyString() - - exp := "Vault Proxy API Proxy/1.2.3 (+https://vault-test.com; go5.0)" - require.Equal(t, exp, act) -} - -// TestUserAgent_VaultProxyWithProxiedUserAgent tests the ProxyStringWithProxiedUserAgent() -// function works as expected -func TestUserAgent_VaultProxyWithProxiedUserAgent(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - userAgent := "my-user-agent" - - act := ProxyStringWithProxiedUserAgent(userAgent) - - exp := "Vault Proxy API Proxy/1.2.3 (+https://vault-test.com; go5.0); my-user-agent" - require.Equal(t, exp, act) -} - -// TestUserAgent_VaultProxyAutoAuth tests the ProxyAPIProxyString() function works -// as expected -func TestUserAgent_VaultProxyAutoAuth(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - versionFunc = func() string { return "1.2.3" } - - act := ProxyAutoAuthString() - - exp := "Vault Proxy Auto-Auth/1.2.3 (+https://vault-test.com; go5.0)" - require.Equal(t, exp, act) -} diff --git a/helper/versions/version_test.go b/helper/versions/version_test.go deleted file mode 100644 index c6d31f4dc..000000000 --- a/helper/versions/version_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package versions - -import "testing" - -func TestIsBuiltinVersion(t *testing.T) { - for _, tc := range []struct { - version string - builtin bool - }{ - {"v1.0.0+builtin", true}, - {"v2.3.4+builtin.vault", true}, - {"1.0.0+builtin.anythingelse", true}, - {"v1.0.0+other.builtin", true}, - {"v1.0.0+builtinbutnot", false}, - {"v1.0.0", false}, - {"not-a-semver", false}, - } { - builtin := IsBuiltinVersion(tc.version) - if builtin != tc.builtin { - t.Fatalf("%s should give: %v, but got %v", tc.version, tc.builtin, builtin) - } - } -} diff --git a/http/auth_token_test.go b/http/auth_token_test.go deleted file mode 100644 index 37903a418..000000000 --- a/http/auth_token_test.go +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "strings" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/vault" -) - -func TestAuthTokenCreate(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - config := api.DefaultConfig() - config.Address = addr - - client, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - client.SetToken(token) - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", - }) - if err != nil { - t.Fatal(err) - } - if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 1h, got %q", secret.Auth.LeaseDuration) - } - - renewCreateRequest := &api.TokenCreateRequest{ - TTL: "1h", - Renewable: new(bool), - } - - secret, err = client.Auth().Token().Create(renewCreateRequest) - if err != nil { - t.Fatal(err) - } - if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 1h, got %q", secret.Auth.LeaseDuration) - } - if secret.Auth.Renewable { - t.Errorf("expected non-renewable token") - } - - *renewCreateRequest.Renewable = true - secret, err = client.Auth().Token().Create(renewCreateRequest) - if err != nil { - t.Fatal(err) - } - if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 1h, got %q", secret.Auth.LeaseDuration) - } - if !secret.Auth.Renewable { - t.Errorf("expected renewable token") - } - - explicitMaxCreateRequest := &api.TokenCreateRequest{ - TTL: "1h", - ExplicitMaxTTL: "1800s", - } - - secret, err = client.Auth().Token().Create(explicitMaxCreateRequest) - if err != nil { - t.Fatal(err) - } - if secret.Auth.LeaseDuration != 1800 { - t.Errorf("expected 1800 seconds, got %d", secret.Auth.LeaseDuration) - } - - explicitMaxCreateRequest.ExplicitMaxTTL = "2h" - secret, err = client.Auth().Token().Create(explicitMaxCreateRequest) - if err != nil { - t.Fatal(err) - } - if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 3600 seconds, got %q", secret.Auth.LeaseDuration) - } -} - -func TestAuthTokenLookup(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - config := api.DefaultConfig() - config.Address = addr - - client, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - client.SetToken(token) - - // Create a new token ... - secret2, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", - }) - if err != nil { - t.Fatal(err) - } - - // lookup details of this token - secret, err := client.Auth().Token().Lookup(secret2.Auth.ClientToken) - if err != nil { - t.Fatalf("unable to lookup details of token, err = %v", err) - } - - if secret.Data["id"] != secret2.Auth.ClientToken { - t.Errorf("Did not get back details about our provided token, id returned=%s", secret.Data["id"]) - } -} - -func TestAuthTokenLookupSelf(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - config := api.DefaultConfig() - config.Address = addr - - client, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - client.SetToken(token) - - // you should be able to lookup your own token - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatalf("should be allowed to lookup self, err = %v", err) - } - - if secret.Data["id"] != token { - t.Errorf("Did not get back details about our own (self) token, id returned=%s", secret.Data["id"]) - } - if secret.Data["display_name"] != "root" { - t.Errorf("Did not get back details about our own (self) token, display_name returned=%s", secret.Data["display_name"]) - } -} - -func TestAuthTokenRenew(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - config := api.DefaultConfig() - config.Address = addr - - client, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - client.SetToken(token) - - // The default root token is not renewable, so this should not work - _, err = client.Auth().Token().Renew(token, 0) - if err == nil { - t.Fatal("should not be allowed to renew root token") - } - if !strings.Contains(err.Error(), "invalid lease ID") { - t.Fatalf("wrong error; got %v", err) - } - - // Create a new token that should be renewable - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", - }) - if err != nil { - t.Fatal(err) - } - client.SetToken(secret.Auth.ClientToken) - - // Now attempt a renew with the new token - secret, err = client.Auth().Token().Renew(secret.Auth.ClientToken, 3600) - if err != nil { - t.Fatal(err) - } - - if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 1h, got %v", secret.Auth.LeaseDuration) - } - - if secret.Auth.Renewable != true { - t.Error("expected lease to be renewable") - } - - // Do the same thing with the self variant - secret, err = client.Auth().Token().RenewSelf(3600) - if err != nil { - t.Fatal(err) - } - - if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 1h, got %v", secret.Auth.LeaseDuration) - } - - if secret.Auth.Renewable != true { - t.Error("expected lease to be renewable") - } -} diff --git a/http/custom_header_test.go b/http/custom_header_test.go deleted file mode 100644 index 36227cc78..000000000 --- a/http/custom_header_test.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "testing" - - "github.com/hashicorp/vault/vault" -) - -var defaultCustomHeaders = map[string]string{ - "Strict-Transport-Security": "max-age=1; domains", - "Content-Security-Policy": "default-src 'others'", - "X-Custom-Header": "Custom header value default", - "X-Frame-Options": "Deny", - "X-Content-Type-Options": "nosniff", - "Content-Type": "application/json", - "X-XSS-Protection": "1; mode=block", -} - -var customHeader2xx = map[string]string{ - "X-Custom-Header": "Custom header value 2xx", -} - -var customHeader200 = map[string]string{ - "Someheader-200": "200", - "X-Custom-Header": "Custom header value 200", -} - -var customHeader4xx = map[string]string{ - "Someheader-4xx": "4xx", -} - -var customHeader400 = map[string]string{ - "Someheader-400": "400", -} - -var customHeader405 = map[string]string{ - "Someheader-405": "405", -} - -var CustomResponseHeaders = map[string]map[string]string{ - "default": defaultCustomHeaders, - "307": {"X-Custom-Header": "Custom header value 307"}, - "3xx": { - "X-Custom-Header": "Custom header value 3xx", - "X-Vault-Ignored-3xx": "Ignored 3xx", - }, - "200": customHeader200, - "2xx": customHeader2xx, - "400": customHeader400, - "405": customHeader405, - "4xx": customHeader4xx, -} - -func TestCustomResponseHeaders(t *testing.T) { - core, _, token := vault.TestCoreWithCustomResponseHeaderAndUI(t, CustomResponseHeaders, true) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpGet(t, token, addr+"/v1/sys/raw/") - testResponseStatus(t, resp, 404) - testResponseHeader(t, resp, defaultCustomHeaders) - testResponseHeader(t, resp, customHeader4xx) - - resp = testHttpGet(t, token, addr+"/v1/sys/seal") - testResponseStatus(t, resp, 405) - testResponseHeader(t, resp, defaultCustomHeaders) - testResponseHeader(t, resp, customHeader4xx) - testResponseHeader(t, resp, customHeader405) - - resp = testHttpGet(t, token, addr+"/v1/sys/leader") - testResponseStatus(t, resp, 200) - testResponseHeader(t, resp, customHeader200) - - resp = testHttpGet(t, token, addr+"/v1/sys/health") - testResponseStatus(t, resp, 200) - testResponseHeader(t, resp, customHeader200) - - resp = testHttpGet(t, token, addr+"/v1/sys/generate-root/attempt") - testResponseStatus(t, resp, 200) - testResponseHeader(t, resp, customHeader200) - - resp = testHttpGet(t, token, addr+"/v1/sys/generate-root/update") - testResponseStatus(t, resp, 400) - testResponseHeader(t, resp, defaultCustomHeaders) - testResponseHeader(t, resp, customHeader4xx) - testResponseHeader(t, resp, customHeader400) - - resp = testHttpGet(t, token, addr+"/v1/sys/") - testResponseStatus(t, resp, 404) - testResponseHeader(t, resp, defaultCustomHeaders) - testResponseHeader(t, resp, customHeader4xx) - - resp = testHttpGet(t, token, addr+"/v1/sys") - testResponseStatus(t, resp, 404) - testResponseHeader(t, resp, defaultCustomHeaders) - testResponseHeader(t, resp, customHeader4xx) - - resp = testHttpGet(t, token, addr+"/v1/") - testResponseStatus(t, resp, 404) - testResponseHeader(t, resp, defaultCustomHeaders) - testResponseHeader(t, resp, customHeader4xx) - - resp = testHttpGet(t, token, addr+"/v1") - testResponseStatus(t, resp, 404) - testResponseHeader(t, resp, defaultCustomHeaders) - testResponseHeader(t, resp, customHeader4xx) - - resp = testHttpGet(t, token, addr+"/") - testResponseStatus(t, resp, 200) - testResponseHeader(t, resp, customHeader200) - - resp = testHttpGet(t, token, addr+"/ui") - testResponseStatus(t, resp, 200) - testResponseHeader(t, resp, customHeader200) - - resp = testHttpGet(t, token, addr+"/ui/") - testResponseStatus(t, resp, 200) - testResponseHeader(t, resp, customHeader200) - - resp = testHttpPost(t, token, addr+"/v1/sys/auth/foo", map[string]interface{}{ - "type": "noop", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - testResponseHeader(t, resp, customHeader2xx) -} diff --git a/http/events_test.go b/http/events_test.go deleted file mode 100644 index 4d90367e5..000000000 --- a/http/events_test.go +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "nhooyr.io/websocket" -) - -// TestEventsSubscribe tests the websocket endpoint for subscribing to events -// by generating some events. -func TestEventsSubscribe(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - // unseal the core - keys, token := vault.TestCoreInit(t, core) - for _, key := range keys { - _, err := core.Unseal(key) - if err != nil { - t.Fatal(err) - } - } - - stop := atomic.Bool{} - - const eventType = "abc" - - // send some events - go func() { - for !stop.Load() { - id, err := uuid.GenerateUUID() - if err != nil { - core.Logger().Info("Error generating UUID, exiting sender", "error", err) - } - pluginInfo := &logical.EventPluginInfo{ - MountPath: "secret", - } - err = core.Events().SendInternal(namespace.RootContext(context.Background()), namespace.RootNamespace, pluginInfo, logical.EventType(eventType), &logical.EventData{ - Id: id, - Metadata: nil, - EntityIds: nil, - Note: "testing", - }) - if err != nil { - core.Logger().Info("Error sending event, exiting sender", "error", err) - } - time.Sleep(100 * time.Millisecond) - } - }() - - t.Cleanup(func() { - stop.Store(true) - }) - - ctx := context.Background() - wsAddr := strings.Replace(addr, "http", "ws", 1) - - testCases := []struct { - json bool - }{{true}, {false}} - - for _, testCase := range testCases { - url := fmt.Sprintf("%s/v1/sys/events/subscribe/%s?json=%v", wsAddr, eventType, testCase.json) - conn, _, err := websocket.Dial(ctx, url, &websocket.DialOptions{ - HTTPHeader: http.Header{"x-vault-token": []string{token}}, - }) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - conn.Close(websocket.StatusNormalClosure, "") - }) - - _, msg, err := conn.Read(ctx) - if err != nil { - t.Fatal(err) - } - if testCase.json { - event := map[string]interface{}{} - err = json.Unmarshal(msg, &event) - if err != nil { - t.Fatal(err) - } - t.Log(string(msg)) - data := event["data"].(map[string]interface{}) - if actualType := data["event_type"].(string); actualType != eventType { - t.Fatalf("Expeced event type %s, got %s", eventType, actualType) - } - pluginInfo, ok := data["plugin_info"].(map[string]interface{}) - if !ok || pluginInfo == nil { - t.Fatalf("No plugin_info object: %v", data) - } - mountPath, ok := pluginInfo["mount_path"].(string) - if !ok || mountPath != "secret" { - t.Fatalf("Wrong mount_path: %v", data) - } - innerEvent := data["event"].(map[string]interface{}) - if innerEvent["id"].(string) != event["id"].(string) { - t.Fatalf("IDs don't match, expected %s, got %s", innerEvent["id"].(string), event["id"].(string)) - } - if innerEvent["note"].(string) != "testing" { - t.Fatalf("Expected 'testing', got %s", innerEvent["note"].(string)) - } - - checkRequiredCloudEventsFields(t, event) - } - } -} - -func checkRequiredCloudEventsFields(t *testing.T, event map[string]interface{}) { - t.Helper() - for _, attr := range []string{"id", "source", "specversion", "type"} { - if v, ok := event[attr]; !ok { - t.Errorf("Missing attribute %s", attr) - } else if str, ok := v.(string); !ok { - t.Errorf("Expected %s to be string but got %T", attr, v) - } else if str == "" { - t.Errorf("%s was empty string", attr) - } - } -} - -// TestEventsSubscribeAuth tests that unauthenticated and unauthorized subscriptions -// fail correctly. -func TestEventsSubscribeAuth(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - // unseal the core - keys, root := vault.TestCoreInit(t, core) - for _, key := range keys { - _, err := core.Unseal(key) - if err != nil { - t.Fatal(err) - } - } - - var nonPrivilegedToken string - // Fetch a valid non privileged token. - { - config := api.DefaultConfig() - config.Address = addr - - client, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - client.SetToken(root) - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{Policies: []string{"default"}}) - if err != nil { - t.Fatal(err) - } - if secret.Auth.ClientToken == "" { - t.Fatal("Failed to fetch a non privileged token") - } - nonPrivilegedToken = secret.Auth.ClientToken - } - - ctx := context.Background() - wsAddr := strings.Replace(addr, "http", "ws", 1) - - // Get a 403 with no token. - _, resp, err := websocket.Dial(ctx, wsAddr+"/v1/sys/events/subscribe/abc", nil) - if err == nil { - t.Error("Expected websocket error but got none") - } - if resp == nil || resp.StatusCode != http.StatusForbidden { - t.Errorf("Expected 403 but got %+v", resp) - } - - // Get a 403 with a non privileged token. - _, resp, err = websocket.Dial(ctx, wsAddr+"/v1/sys/events/subscribe/abc", &websocket.DialOptions{ - HTTPHeader: http.Header{"x-vault-token": []string{nonPrivilegedToken}}, - }) - if err == nil { - t.Error("Expected websocket error but got none") - } - if resp == nil || resp.StatusCode != http.StatusForbidden { - t.Errorf("Expected 403 but got %+v", resp) - } -} diff --git a/http/forwarded_for_test.go b/http/forwarded_for_test.go deleted file mode 100644 index c0409bab3..000000000 --- a/http/forwarded_for_test.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "bytes" - "net/http" - "strings" - "testing" - - sockaddr "github.com/hashicorp/go-sockaddr" - "github.com/hashicorp/vault/internalshared/configutil" - "github.com/hashicorp/vault/vault" -) - -func getListenerConfigForMarshalerTest(addr sockaddr.IPAddr) *configutil.Listener { - return &configutil.Listener{ - XForwardedForAuthorizedAddrs: []*sockaddr.SockAddrMarshaler{ - { - SockAddr: addr, - }, - }, - } -} - -func TestHandler_XForwardedFor(t *testing.T) { - goodAddr, err := sockaddr.NewIPAddr("127.0.0.1") - if err != nil { - t.Fatal(err) - } - - badAddr, err := sockaddr.NewIPAddr("1.2.3.4") - if err != nil { - t.Fatal(err) - } - - // First: test reject not present - t.Run("reject_not_present", func(t *testing.T) { - t.Parallel() - testHandler := func(props *vault.HandlerProperties) http.Handler { - origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(r.RemoteAddr)) - }) - listenerConfig := getListenerConfigForMarshalerTest(goodAddr) - listenerConfig.XForwardedForRejectNotPresent = true - return WrapForwardedForHandler(origHandler, listenerConfig) - } - - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: HandlerFunc(testHandler), - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - req := client.NewRequest("GET", "/") - _, err := client.RawRequest(req) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "missing x-forwarded-for") { - t.Fatalf("bad error message: %v", err) - } - req = client.NewRequest("GET", "/") - req.Headers = make(http.Header) - req.Headers.Set("x-forwarded-for", "1.2.3.4") - resp, err := client.RawRequest(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - buf := bytes.NewBuffer(nil) - buf.ReadFrom(resp.Body) - if !strings.HasPrefix(buf.String(), "1.2.3.4:") { - t.Fatalf("bad body: %s", buf.String()) - } - }) - - // Next: test allow unauth - t.Run("allow_unauth", func(t *testing.T) { - t.Parallel() - testHandler := func(props *vault.HandlerProperties) http.Handler { - origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(r.RemoteAddr)) - }) - listenerConfig := getListenerConfigForMarshalerTest(badAddr) - listenerConfig.XForwardedForRejectNotPresent = true - return WrapForwardedForHandler(origHandler, listenerConfig) - } - - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: HandlerFunc(testHandler), - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - req := client.NewRequest("GET", "/") - req.Headers = make(http.Header) - req.Headers.Set("x-forwarded-for", "5.6.7.8") - resp, err := client.RawRequest(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - buf := bytes.NewBuffer(nil) - buf.ReadFrom(resp.Body) - if !strings.HasPrefix(buf.String(), "127.0.0.1:") { - t.Fatalf("bad body: %s", buf.String()) - } - }) - - // Next: test fail unauth - t.Run("fail_unauth", func(t *testing.T) { - t.Parallel() - testHandler := func(props *vault.HandlerProperties) http.Handler { - origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(r.RemoteAddr)) - }) - listenerConfig := getListenerConfigForMarshalerTest(badAddr) - listenerConfig.XForwardedForRejectNotPresent = true - listenerConfig.XForwardedForRejectNotAuthorized = true - return WrapForwardedForHandler(origHandler, listenerConfig) - } - - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: HandlerFunc(testHandler), - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - req := client.NewRequest("GET", "/") - req.Headers = make(http.Header) - req.Headers.Set("x-forwarded-for", "5.6.7.8") - _, err := client.RawRequest(req) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "not authorized for x-forwarded-for") { - t.Fatalf("bad error message: %v", err) - } - }) - - // Next: test bad hops (too many) - t.Run("too_many_hops", func(t *testing.T) { - t.Parallel() - testHandler := func(props *vault.HandlerProperties) http.Handler { - origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(r.RemoteAddr)) - }) - listenerConfig := getListenerConfigForMarshalerTest(goodAddr) - listenerConfig.XForwardedForRejectNotPresent = true - listenerConfig.XForwardedForRejectNotAuthorized = true - listenerConfig.XForwardedForHopSkips = 4 - return WrapForwardedForHandler(origHandler, listenerConfig) - } - - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: HandlerFunc(testHandler), - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - req := client.NewRequest("GET", "/") - req.Headers = make(http.Header) - req.Headers.Set("x-forwarded-for", "2.3.4.5,3.4.5.6") - _, err := client.RawRequest(req) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "would skip before earliest") { - t.Fatalf("bad error message: %v", err) - } - }) - - // Next: test picking correct value - t.Run("correct_hop_skipping", func(t *testing.T) { - t.Parallel() - testHandler := func(props *vault.HandlerProperties) http.Handler { - origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(r.RemoteAddr)) - }) - listenerConfig := getListenerConfigForMarshalerTest(goodAddr) - listenerConfig.XForwardedForRejectNotPresent = true - listenerConfig.XForwardedForRejectNotAuthorized = true - listenerConfig.XForwardedForHopSkips = 1 - return WrapForwardedForHandler(origHandler, listenerConfig) - } - - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: HandlerFunc(testHandler), - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - req := client.NewRequest("GET", "/") - req.Headers = make(http.Header) - req.Headers.Set("x-forwarded-for", "2.3.4.5,3.4.5.6,4.5.6.7,5.6.7.8") - resp, err := client.RawRequest(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - buf := bytes.NewBuffer(nil) - buf.ReadFrom(resp.Body) - if !strings.HasPrefix(buf.String(), "4.5.6.7:") { - t.Fatalf("bad body: %s", buf.String()) - } - }) - - // Next: multi-header approach - t.Run("correct_hop_skipping_multi_header", func(t *testing.T) { - t.Parallel() - testHandler := func(props *vault.HandlerProperties) http.Handler { - origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(r.RemoteAddr)) - }) - listenerConfig := getListenerConfigForMarshalerTest(goodAddr) - listenerConfig.XForwardedForRejectNotPresent = true - listenerConfig.XForwardedForRejectNotAuthorized = true - listenerConfig.XForwardedForHopSkips = 1 - return WrapForwardedForHandler(origHandler, listenerConfig) - } - - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: HandlerFunc(testHandler), - }) - cluster.Start() - defer cluster.Cleanup() - client := cluster.Cores[0].Client - - req := client.NewRequest("GET", "/") - req.Headers = make(http.Header) - req.Headers.Add("x-forwarded-for", "2.3.4.5") - req.Headers.Add("x-forwarded-for", "3.4.5.6,4.5.6.7") - req.Headers.Add("x-forwarded-for", "5.6.7.8") - resp, err := client.RawRequest(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - buf := bytes.NewBuffer(nil) - buf.ReadFrom(resp.Body) - if !strings.HasPrefix(buf.String(), "4.5.6.7:") { - t.Fatalf("bad body: %s", buf.String()) - } - }) -} diff --git a/http/forwarding_bench_test.go b/http/forwarding_bench_test.go deleted file mode 100644 index 8d933ac2c..000000000 --- a/http/forwarding_bench_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "net/http" - "strings" - "testing" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/builtin/logical/transit" - "github.com/hashicorp/vault/helper/benchhelpers" - "github.com/hashicorp/vault/helper/forwarding" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "golang.org/x/net/http2" -) - -func BenchmarkHTTP_Forwarding_Stress(b *testing.B) { - testPlaintextB64 := "dGhlIHF1aWNrIGJyb3duIGZveA==" - - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": transit.Factory, - }, - } - - cluster := vault.NewTestCluster(benchhelpers.TBtoT(b), coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - Logger: logging.NewVaultLoggerWithWriter(ioutil.Discard, log.Error), - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - - // make it easy to get access to the active - core := cores[0].Core - vault.TestWaitActive(benchhelpers.TBtoT(b), core) - - handler := cores[0].Handler - host := fmt.Sprintf("https://127.0.0.1:%d/v1/transit/", cores[0].Listeners[0].Address.Port) - - transport := &http.Transport{ - TLSClientConfig: cores[0].TLSConfig(), - } - if err := http2.ConfigureTransport(transport); err != nil { - b.Fatal(err) - } - - client := &http.Client{ - Transport: transport, - } - - req, err := http.NewRequest("POST", fmt.Sprintf("https://127.0.0.1:%d/v1/sys/mounts/transit", cores[0].Listeners[0].Address.Port), - bytes.NewBuffer([]byte("{\"type\": \"transit\"}"))) - if err != nil { - b.Fatal(err) - } - req.Header.Set(consts.AuthHeaderName, cluster.RootToken) - _, err = client.Do(req) - if err != nil { - b.Fatal(err) - } - - var numOps uint32 - - doReq := func(b *testing.B, method, url string, body io.Reader) { - req, err := http.NewRequest(method, url, body) - if err != nil { - b.Fatal(err) - } - req.Header.Set(consts.AuthHeaderName, cluster.RootToken) - w := forwarding.NewRPCResponseWriter() - handler.ServeHTTP(w, req) - switch w.StatusCode() { - case 200: - case 204: - if !strings.Contains(url, "keys") { - b.Fatal("got 204") - } - default: - b.Fatalf("bad status code: %d, resp: %s", w.StatusCode(), w.Body().String()) - } - // b.Log(w.Body().String()) - numOps++ - } - - doReq(b, "POST", host+"keys/test1", bytes.NewBuffer([]byte("{}"))) - keyUrl := host + "encrypt/test1" - reqBuf := []byte(fmt.Sprintf("{\"plaintext\": \"%s\"}", testPlaintextB64)) - - b.Run("doreq", func(b *testing.B) { - for i := 0; i < b.N; i++ { - doReq(b, "POST", keyUrl, bytes.NewReader(reqBuf)) - } - }) - - b.Logf("total ops: %d", numOps) -} diff --git a/http/forwarding_test.go b/http/forwarding_test.go deleted file mode 100644 index f3b81fd83..000000000 --- a/http/forwarding_test.go +++ /dev/null @@ -1,608 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "math/rand" - "net/http" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "golang.org/x/net/http2" - - cleanhttp "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/vault/api" - credCert "github.com/hashicorp/vault/builtin/credential/cert" - "github.com/hashicorp/vault/builtin/logical/transit" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/keysutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestHTTP_Fallback_Bad_Address(t *testing.T) { - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": transit.Factory, - }, - ClusterAddr: "https://127.3.4.1:8382", - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - - // make it easy to get access to the active - core := cores[0].Core - vault.TestWaitActive(t, core) - - addrs := []string{ - fmt.Sprintf("https://127.0.0.1:%d", cores[1].Listeners[0].Address.Port), - fmt.Sprintf("https://127.0.0.1:%d", cores[2].Listeners[0].Address.Port), - } - - for _, addr := range addrs { - config := api.DefaultConfig() - config.Address = addr - config.HttpClient.Transport.(*http.Transport).TLSClientConfig = cores[0].TLSConfig() - - client, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - client.SetToken(cluster.RootToken) - - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - if secret == nil { - t.Fatal("secret is nil") - } - if secret.Data["id"].(string) != cluster.RootToken { - t.Fatal("token mismatch") - } - } -} - -func TestHTTP_Fallback_Disabled(t *testing.T) { - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": transit.Factory, - }, - ClusterAddr: "empty", - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - - // make it easy to get access to the active - core := cores[0].Core - vault.TestWaitActive(t, core) - - addrs := []string{ - fmt.Sprintf("https://127.0.0.1:%d", cores[1].Listeners[0].Address.Port), - fmt.Sprintf("https://127.0.0.1:%d", cores[2].Listeners[0].Address.Port), - } - - for _, addr := range addrs { - config := api.DefaultConfig() - config.Address = addr - config.HttpClient.Transport.(*http.Transport).TLSClientConfig = cores[0].TLSConfig() - - client, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - client.SetToken(cluster.RootToken) - - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - if secret == nil { - t.Fatal("secret is nil") - } - if secret.Data["id"].(string) != cluster.RootToken { - t.Fatal("token mismatch") - } - } -} - -// This function recreates the fuzzy testing from transit to pipe a large -// number of requests from the standbys to the active node. -func TestHTTP_Forwarding_Stress(t *testing.T) { - testHTTP_Forwarding_Stress_Common(t, false, 50) - testHTTP_Forwarding_Stress_Common(t, true, 50) -} - -func testHTTP_Forwarding_Stress_Common(t *testing.T, parallel bool, num uint32) { - testPlaintext := "the quick brown fox" - testPlaintextB64 := "dGhlIHF1aWNrIGJyb3duIGZveA==" - - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": transit.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - - // make it easy to get access to the active - core := cores[0].Core - vault.TestWaitActive(t, core) - - wg := sync.WaitGroup{} - - funcs := []string{"encrypt", "decrypt", "rotate", "change_min_version"} - keys := []string{"test1", "test2", "test3"} - - hosts := []string{ - fmt.Sprintf("https://127.0.0.1:%d/v1/transit/", cores[1].Listeners[0].Address.Port), - fmt.Sprintf("https://127.0.0.1:%d/v1/transit/", cores[2].Listeners[0].Address.Port), - } - - transport := &http.Transport{ - TLSClientConfig: cores[0].TLSConfig(), - } - if err := http2.ConfigureTransport(transport); err != nil { - t.Fatal(err) - } - - client := &http.Client{ - Transport: transport, - CheckRedirect: func(*http.Request, []*http.Request) error { - return fmt.Errorf("redirects not allowed in this test") - }, - } - - // core.Logger().Printf("[TRACE] mounting transit") - req, err := http.NewRequest("POST", fmt.Sprintf("https://127.0.0.1:%d/v1/sys/mounts/transit", cores[0].Listeners[0].Address.Port), - bytes.NewBuffer([]byte("{\"type\": \"transit\"}"))) - if err != nil { - t.Fatal(err) - } - req.Header.Set(consts.AuthHeaderName, cluster.RootToken) - _, err = client.Do(req) - if err != nil { - t.Fatal(err) - } - // core.Logger().Printf("[TRACE] done mounting transit") - - var totalOps *uint32 = new(uint32) - var successfulOps *uint32 = new(uint32) - var key1ver *int32 = new(int32) - *key1ver = 1 - var key2ver *int32 = new(int32) - *key2ver = 1 - var key3ver *int32 = new(int32) - *key3ver = 1 - var numWorkers *uint32 = new(uint32) - *numWorkers = 50 - var numWorkersStarted *uint32 = new(uint32) - var waitLock sync.Mutex - waitCond := sync.NewCond(&waitLock) - - // This is the goroutine loop - doFuzzy := func(id int, parallel bool) { - var myTotalOps uint32 - var mySuccessfulOps uint32 - var keyVer int32 = 1 - // Check for panics, otherwise notify we're done - defer func() { - if err := recover(); err != nil { - core.Logger().Error("got a panic", "error", err) - t.Fail() - } - atomic.AddUint32(totalOps, myTotalOps) - atomic.AddUint32(successfulOps, mySuccessfulOps) - wg.Done() - }() - - // Holds the latest encrypted value for each key - latestEncryptedText := map[string]string{} - - client := &http.Client{ - Transport: transport, - } - - var chosenFunc, chosenKey, chosenHost string - - myRand := rand.New(rand.NewSource(int64(id) * 400)) - - doReq := func(method, url string, body io.Reader) (*http.Response, error) { - req, err := http.NewRequest(method, url, body) - if err != nil { - return nil, err - } - req.Header.Set(consts.AuthHeaderName, cluster.RootToken) - resp, err := client.Do(req) - if err != nil { - return nil, err - } - return resp, nil - } - - doResp := func(resp *http.Response) (*api.Secret, error) { - if resp == nil { - return nil, fmt.Errorf("nil response") - } - defer resp.Body.Close() - - // Make sure we weren't redirected - if resp.StatusCode > 300 && resp.StatusCode < 400 { - return nil, fmt.Errorf("got status code %d, resp was %#v", resp.StatusCode, *resp) - } - - result := &api.Response{Response: resp} - err := result.Error() - if err != nil { - return nil, err - } - - secret, err := api.ParseSecret(result.Body) - if err != nil { - return nil, err - } - - return secret, nil - } - - for _, chosenHost := range hosts { - for _, chosenKey := range keys { - // Try to write the key to make sure it exists - _, err := doReq("POST", chosenHost+"keys/"+fmt.Sprintf("%s-%t", chosenKey, parallel), bytes.NewBuffer([]byte("{}"))) - if err != nil { - panic(err) - } - } - } - - if !parallel { - chosenHost = hosts[id%len(hosts)] - chosenKey = fmt.Sprintf("key-%t-%d", parallel, id) - - _, err := doReq("POST", chosenHost+"keys/"+chosenKey, bytes.NewBuffer([]byte("{}"))) - if err != nil { - panic(err) - } - } - - atomic.AddUint32(numWorkersStarted, 1) - - waitCond.L.Lock() - for atomic.LoadUint32(numWorkersStarted) != atomic.LoadUint32(numWorkers) { - waitCond.Wait() - } - waitCond.L.Unlock() - waitCond.Broadcast() - - core.Logger().Debug("Starting goroutine", "id", id) - - startTime := time.Now() - for { - // Stop after 10 seconds - if time.Now().Sub(startTime) > 10*time.Second { - return - } - - myTotalOps++ - - // Pick a function and a key - chosenFunc = funcs[myRand.Int()%len(funcs)] - if parallel { - chosenKey = fmt.Sprintf("%s-%t", keys[myRand.Int()%len(keys)], parallel) - chosenHost = hosts[myRand.Int()%len(hosts)] - } - - switch chosenFunc { - // Encrypt our plaintext and store the result - case "encrypt": - // core.Logger().Printf("[TRACE] %s, %s, %d", chosenFunc, chosenKey, id) - resp, err := doReq("POST", chosenHost+"encrypt/"+chosenKey, bytes.NewBuffer([]byte(fmt.Sprintf("{\"plaintext\": \"%s\"}", testPlaintextB64)))) - if err != nil { - panic(err) - } - - secret, err := doResp(resp) - if err != nil { - panic(err) - } - - latest := secret.Data["ciphertext"].(string) - if latest == "" { - panic(fmt.Errorf("bad ciphertext")) - } - latestEncryptedText[chosenKey] = secret.Data["ciphertext"].(string) - - mySuccessfulOps++ - - // Decrypt the ciphertext and compare the result - case "decrypt": - ct := latestEncryptedText[chosenKey] - if ct == "" { - mySuccessfulOps++ - continue - } - - // core.Logger().Printf("[TRACE] %s, %s, %d", chosenFunc, chosenKey, id) - resp, err := doReq("POST", chosenHost+"decrypt/"+chosenKey, bytes.NewBuffer([]byte(fmt.Sprintf("{\"ciphertext\": \"%s\"}", ct)))) - if err != nil { - panic(err) - } - - secret, err := doResp(resp) - if err != nil { - // This could well happen since the min version is jumping around - if strings.Contains(err.Error(), keysutil.ErrTooOld) { - mySuccessfulOps++ - continue - } - panic(err) - } - - ptb64 := secret.Data["plaintext"].(string) - pt, err := base64.StdEncoding.DecodeString(ptb64) - if err != nil { - panic(fmt.Errorf("got an error decoding base64 plaintext: %v", err)) - } - if string(pt) != testPlaintext { - panic(fmt.Errorf("got bad plaintext back: %s", pt)) - } - - mySuccessfulOps++ - - // Rotate to a new key version - case "rotate": - // core.Logger().Printf("[TRACE] %s, %s, %d", chosenFunc, chosenKey, id) - _, err := doReq("POST", chosenHost+"keys/"+chosenKey+"/rotate", bytes.NewBuffer([]byte("{}"))) - if err != nil { - panic(err) - } - if parallel { - switch chosenKey { - case "test1": - atomic.AddInt32(key1ver, 1) - case "test2": - atomic.AddInt32(key2ver, 1) - case "test3": - atomic.AddInt32(key3ver, 1) - } - } else { - keyVer++ - } - - mySuccessfulOps++ - - // Change the min version, which also tests the archive functionality - case "change_min_version": - var latestVersion int32 = keyVer - if parallel { - switch chosenKey { - case "test1": - latestVersion = atomic.LoadInt32(key1ver) - case "test2": - latestVersion = atomic.LoadInt32(key2ver) - case "test3": - latestVersion = atomic.LoadInt32(key3ver) - } - } - - setVersion := (myRand.Int31() % latestVersion) + 1 - - // core.Logger().Printf("[TRACE] %s, %s, %d, new min version %d", chosenFunc, chosenKey, id, setVersion) - - _, err := doReq("POST", chosenHost+"keys/"+chosenKey+"/config", bytes.NewBuffer([]byte(fmt.Sprintf("{\"min_decryption_version\": %d}", setVersion)))) - if err != nil { - panic(err) - } - - mySuccessfulOps++ - } - } - } - - atomic.StoreUint32(numWorkers, num) - - // Spawn some of these workers for 10 seconds - for i := 0; i < int(atomic.LoadUint32(numWorkers)); i++ { - wg.Add(1) - // core.Logger().Printf("[TRACE] spawning %d", i) - go doFuzzy(i+1, parallel) - } - - // Wait for them all to finish - wg.Wait() - - if *totalOps == 0 || *totalOps != *successfulOps { - t.Fatalf("total/successful ops zero or mismatch: %d/%d; parallel: %t, num %d", *totalOps, *successfulOps, parallel, num) - } - t.Logf("total operations tried: %d, total successful: %d; parallel: %t, num %d", *totalOps, *successfulOps, parallel, num) -} - -// This tests TLS connection state forwarding by ensuring that we can use a -// client TLS to authenticate against the cert backend -func TestHTTP_Forwarding_ClientTLS(t *testing.T) { - coreConfig := &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "cert": credCert.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - - // make it easy to get access to the active - core := cores[0].Core - vault.TestWaitActive(t, core) - - transport := cleanhttp.DefaultTransport() - transport.TLSClientConfig = cores[0].TLSConfig() - if err := http2.ConfigureTransport(transport); err != nil { - t.Fatal(err) - } - - client := &http.Client{ - Transport: transport, - } - - req, err := http.NewRequest("POST", fmt.Sprintf("https://127.0.0.1:%d/v1/sys/auth/cert", cores[0].Listeners[0].Address.Port), - bytes.NewBuffer([]byte("{\"type\": \"cert\"}"))) - if err != nil { - t.Fatal(err) - } - req.Header.Set(consts.AuthHeaderName, cluster.RootToken) - _, err = client.Do(req) - if err != nil { - t.Fatal(err) - } - - type certConfig struct { - Certificate string `json:"certificate"` - Policies string `json:"policies"` - } - encodedCertConfig, err := json.Marshal(&certConfig{ - Certificate: string(cluster.CACertPEM), - Policies: "default", - }) - if err != nil { - t.Fatal(err) - } - req, err = http.NewRequest("POST", fmt.Sprintf("https://127.0.0.1:%d/v1/auth/cert/certs/test", cores[0].Listeners[0].Address.Port), - bytes.NewBuffer(encodedCertConfig)) - if err != nil { - t.Fatal(err) - } - req.Header.Set(consts.AuthHeaderName, cluster.RootToken) - _, err = client.Do(req) - if err != nil { - t.Fatal(err) - } - - addrs := []string{ - fmt.Sprintf("https://127.0.0.1:%d", cores[1].Listeners[0].Address.Port), - fmt.Sprintf("https://127.0.0.1:%d", cores[2].Listeners[0].Address.Port), - } - - for i, addr := range addrs { - // Ensure we can't possibly use lingering connections even though it should - // be to a different address - transport = cleanhttp.DefaultTransport() - // i starts at zero but cores in addrs start at 1 - transport.TLSClientConfig = cores[i+1].TLSConfig() - if err := http2.ConfigureTransport(transport); err != nil { - t.Fatal(err) - } - httpClient := &http.Client{ - Transport: transport, - CheckRedirect: func(*http.Request, []*http.Request) error { - return fmt.Errorf("redirects not allowed in this test") - }, - } - client, err := api.NewClient(&api.Config{ - Address: addr, - HttpClient: httpClient, - }) - if err != nil { - t.Fatal(err) - } - - secret, err := client.Logical().Write("auth/cert/login", nil) - if err != nil { - t.Fatal(err) - } - if secret == nil { - t.Fatal("secret is nil") - } - if secret.Auth == nil { - t.Fatal("auth is nil") - } - if secret.Auth.Policies == nil || len(secret.Auth.Policies) == 0 || secret.Auth.Policies[0] != "default" { - t.Fatalf("bad policies: %#v", secret.Auth.Policies) - } - if secret.Auth.ClientToken == "" { - t.Fatalf("bad client token: %#v", *secret.Auth) - } - client.SetToken(secret.Auth.ClientToken) - secret, err = client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) - } - if secret == nil { - t.Fatal("secret is nil") - } - if secret.Data == nil || len(secret.Data) == 0 { - t.Fatal("secret data was empty") - } - } -} - -func TestHTTP_Forwarding_HelpOperation(t *testing.T) { - cluster := vault.NewTestCluster(t, &vault.CoreConfig{}, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - testHelp := func(client *api.Client) { - help, err := client.Help("auth/token") - if err != nil { - t.Fatal(err) - } - if help == nil { - t.Fatal("help was nil") - } - } - - testHelp(cores[0].Client) - testHelp(cores[1].Client) -} - -func TestHTTP_Forwarding_LocalOnly(t *testing.T) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - testLocalOnly := func(client *api.Client) { - _, err := client.Logical().Read("sys/config/state/sanitized") - if err == nil { - t.Fatal("expected error") - } - } - - testLocalOnly(cores[1].Client) - testLocalOnly(cores[2].Client) -} diff --git a/http/handler_test.go b/http/handler_test.go deleted file mode 100644 index d4a07b3d4..000000000 --- a/http/handler_test.go +++ /dev/null @@ -1,949 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "bytes" - "context" - "crypto/tls" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/textproto" - "net/url" - "reflect" - "runtime" - "strings" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/helper/versions" - "github.com/hashicorp/vault/internalshared/configutil" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/stretchr/testify/require" -) - -func TestHandler_parseMFAHandler(t *testing.T) { - var err error - var expectedMFACreds logical.MFACreds - req := &logical.Request{ - Headers: make(map[string][]string), - } - - headerName := textproto.CanonicalMIMEHeaderKey(MFAHeaderName) - - // Set TOTP passcode in the MFA header - req.Headers[headerName] = []string{ - "my_totp:123456", - "my_totp:111111", - "my_second_mfa:hi=hello", - "my_third_mfa", - } - err = parseMFAHeader(req) - if err != nil { - t.Fatal(err) - } - - // Verify that it is being parsed properly - expectedMFACreds = logical.MFACreds{ - "my_totp": []string{ - "123456", - "111111", - }, - "my_second_mfa": []string{ - "hi=hello", - }, - "my_third_mfa": []string{}, - } - if !reflect.DeepEqual(expectedMFACreds, req.MFACreds) { - t.Fatalf("bad: parsed MFACreds; expected: %#v\n actual: %#v\n", expectedMFACreds, req.MFACreds) - } - - // Split the creds of a method type in different headers and check if they - // all get merged together - req.Headers[headerName] = []string{ - "my_mfa:passcode=123456", - "my_mfa:month=july", - "my_mfa:day=tuesday", - } - err = parseMFAHeader(req) - if err != nil { - t.Fatal(err) - } - - expectedMFACreds = logical.MFACreds{ - "my_mfa": []string{ - "passcode=123456", - "month=july", - "day=tuesday", - }, - } - if !reflect.DeepEqual(expectedMFACreds, req.MFACreds) { - t.Fatalf("bad: parsed MFACreds; expected: %#v\n actual: %#v\n", expectedMFACreds, req.MFACreds) - } - - // Header without method name should error out - req.Headers[headerName] = []string{ - ":passcode=123456", - } - err = parseMFAHeader(req) - if err == nil { - t.Fatalf("expected an error; actual: %#v\n", req.MFACreds) - } - - // Header without method name and method value should error out - req.Headers[headerName] = []string{ - ":", - } - err = parseMFAHeader(req) - if err == nil { - t.Fatalf("expected an error; actual: %#v\n", req.MFACreds) - } - - // Header without method name and method value should error out - req.Headers[headerName] = []string{ - "my_totp:", - } - err = parseMFAHeader(req) - if err == nil { - t.Fatalf("expected an error; actual: %#v\n", req.MFACreds) - } -} - -func TestHandler_cors(t *testing.T) { - core, _, _ := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - // Enable CORS and allow from any origin for testing. - corsConfig := core.CORSConfig() - err := corsConfig.Enable(context.Background(), []string{addr}, nil) - if err != nil { - t.Fatalf("Error enabling CORS: %s", err) - } - - req, err := http.NewRequest(http.MethodOptions, addr+"/v1/sys/seal-status", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - req.Header.Set("Origin", "BAD ORIGIN") - - // Requests from unacceptable origins will be rejected with a 403. - client := cleanhttp.DefaultClient() - resp, err := client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - if resp.StatusCode != http.StatusForbidden { - t.Fatalf("Bad status:\nexpected: 403 Forbidden\nactual: %s", resp.Status) - } - - // - // Test preflight requests - // - - // Set a valid origin - req.Header.Set("Origin", addr) - - // Server should NOT accept arbitrary methods. - req.Header.Set("Access-Control-Request-Method", "FOO") - - client = cleanhttp.DefaultClient() - resp, err = client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Fail if an arbitrary method is accepted. - if resp.StatusCode != http.StatusMethodNotAllowed { - t.Fatalf("Bad status:\nexpected: 405 Method Not Allowed\nactual: %s", resp.Status) - } - - // Server SHOULD accept acceptable methods. - req.Header.Set("Access-Control-Request-Method", http.MethodPost) - - client = cleanhttp.DefaultClient() - resp, err = client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - // - // Test that the CORS headers are applied correctly. - // - expHeaders := map[string]string{ - "Access-Control-Allow-Origin": addr, - "Access-Control-Allow-Headers": strings.Join(vault.StdAllowedHeaders, ","), - "Access-Control-Max-Age": "300", - "Vary": "Origin", - } - - for expHeader, expected := range expHeaders { - actual := resp.Header.Get(expHeader) - if actual == "" { - t.Fatalf("bad:\nHeader: %#v was not on response.", expHeader) - } - - if actual != expected { - t.Fatalf("bad:\nExpected: %#v\nActual: %#v\n", expected, actual) - } - } -} - -func TestHandler_HostnameHeader(t *testing.T) { - t.Parallel() - testCases := []struct { - description string - config *vault.CoreConfig - headerPresent bool - }{ - { - description: "with no header configured", - config: nil, - headerPresent: false, - }, - { - description: "with header configured", - config: &vault.CoreConfig{ - EnableResponseHeaderHostname: true, - }, - headerPresent: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - var core *vault.Core - - if tc.config == nil { - core, _, _ = vault.TestCoreUnsealed(t) - } else { - core, _, _ = vault.TestCoreUnsealedWithConfig(t, tc.config) - } - - ln, addr := TestServer(t, core) - defer ln.Close() - - req, err := http.NewRequest("GET", addr+"/v1/sys/seal-status", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - client := cleanhttp.DefaultClient() - resp, err := client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - if resp == nil { - t.Fatal("nil response") - } - - hnHeader := resp.Header.Get("X-Vault-Hostname") - if tc.headerPresent && hnHeader == "" { - t.Logf("header configured = %t", core.HostnameHeaderEnabled()) - t.Fatal("missing 'X-Vault-Hostname' header entry in response") - } - if !tc.headerPresent && hnHeader != "" { - t.Fatal("didn't expect 'X-Vault-Hostname' header but it was present anyway") - } - - rniHeader := resp.Header.Get("X-Vault-Raft-Node-ID") - if rniHeader != "" { - t.Fatalf("no raft node ID header was expected, since we're not running a raft cluster. instead, got %s", rniHeader) - } - }) - } -} - -func TestHandler_CacheControlNoStore(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - req, err := http.NewRequest("GET", addr+"/v1/sys/mounts", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - req.Header.Set(consts.AuthHeaderName, token) - req.Header.Set(WrapTTLHeaderName, "60s") - - client := cleanhttp.DefaultClient() - resp, err := client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - if resp == nil { - t.Fatalf("nil response") - } - - actual := resp.Header.Get("Cache-Control") - - if actual == "" { - t.Fatalf("missing 'Cache-Control' header entry in response writer") - } - - if actual != "no-store" { - t.Fatalf("bad: Cache-Control. Expected: 'no-store', Actual: %q", actual) - } -} - -func TestHandler_InFlightRequest(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - req, err := http.NewRequest("GET", addr+"/v1/sys/in-flight-req", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - req.Header.Set(consts.AuthHeaderName, token) - - client := cleanhttp.DefaultClient() - resp, err := client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - if resp == nil { - t.Fatalf("nil response") - } - - var actual map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual == nil || len(actual) == 0 { - t.Fatal("expected to get at least one in-flight request, got nil or zero length map") - } - for _, v := range actual { - reqInfo, ok := v.(map[string]interface{}) - if !ok { - t.Fatal("failed to read in-flight request") - } - if reqInfo["request_path"] != "/v1/sys/in-flight-req" { - t.Fatalf("expected /v1/sys/in-flight-req in-flight request path, got %s", actual["request_path"]) - } - } -} - -// TestHandler_MissingToken tests the response / error code if a request comes -// in with a missing client token. See -// https://github.com/hashicorp/vault/issues/8377 -func TestHandler_MissingToken(t *testing.T) { - // core, _, token := vault.TestCoreUnsealed(t) - core, _, _ := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - req, err := http.NewRequest("GET", addr+"/v1/sys/internal/ui/mounts/cubbyhole", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - req.Header.Set(WrapTTLHeaderName, "60s") - - client := cleanhttp.DefaultClient() - resp, err := client.Do(req) - if err != nil { - t.Fatal(err) - } - if resp.StatusCode != 403 { - t.Fatalf("expected code 403, got: %d", resp.StatusCode) - } -} - -func TestHandler_Accepted(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - req, err := http.NewRequest("POST", addr+"/v1/auth/token/tidy", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - req.Header.Set(consts.AuthHeaderName, token) - - client := cleanhttp.DefaultClient() - resp, err := client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - testResponseStatus(t, resp, 202) -} - -// We use this test to verify header auth -func TestSysMounts_headerAuth(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - req, err := http.NewRequest("GET", addr+"/v1/sys/mounts", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - req.Header.Set(consts.AuthHeaderName, token) - - client := cleanhttp.DefaultClient() - resp, err := client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if diff := deep.Equal(actual, expected); len(diff) > 0 { - t.Fatalf("bad, diff: %#v", diff) - } -} - -// We use this test to verify header auth wrapping -func TestSysMounts_headerAuth_Wrapped(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - req, err := http.NewRequest("GET", addr+"/v1/sys/mounts", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - req.Header.Set(consts.AuthHeaderName, token) - req.Header.Set(WrapTTLHeaderName, "60s") - - client := cleanhttp.DefaultClient() - resp, err := client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - var actual map[string]interface{} - expected := map[string]interface{}{ - "request_id": "", - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "data": nil, - "wrap_info": map[string]interface{}{ - "ttl": json.Number("60"), - }, - "warnings": nil, - "auth": nil, - } - - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - actualToken, ok := actual["wrap_info"].(map[string]interface{})["token"] - if !ok || actualToken == "" { - t.Fatal("token missing in wrap info") - } - expected["wrap_info"].(map[string]interface{})["token"] = actualToken - - actualCreationTime, ok := actual["wrap_info"].(map[string]interface{})["creation_time"] - if !ok || actualCreationTime == "" { - t.Fatal("creation_time missing in wrap info") - } - expected["wrap_info"].(map[string]interface{})["creation_time"] = actualCreationTime - - actualCreationPath, ok := actual["wrap_info"].(map[string]interface{})["creation_path"] - if !ok || actualCreationPath == "" { - t.Fatal("creation_path missing in wrap info") - } - expected["wrap_info"].(map[string]interface{})["creation_path"] = actualCreationPath - - actualAccessor, ok := actual["wrap_info"].(map[string]interface{})["accessor"] - if !ok || actualAccessor == "" { - t.Fatal("accessor missing in wrap info") - } - expected["wrap_info"].(map[string]interface{})["accessor"] = actualAccessor - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual: %#v\n%T %T", expected, actual, actual["warnings"], actual["data"]) - } -} - -func TestHandler_sealed(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - core.Seal(token) - - resp, err := http.Get(addr + "/v1/secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } - testResponseStatus(t, resp, 503) -} - -func TestHandler_ui_default(t *testing.T) { - core := vault.TestCoreUI(t, false) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp, err := http.Get(addr + "/ui/") - if err != nil { - t.Fatalf("err: %s", err) - } - testResponseStatus(t, resp, 404) -} - -func TestHandler_ui_enabled(t *testing.T) { - core := vault.TestCoreUI(t, true) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp, err := http.Get(addr + "/ui/") - if err != nil { - t.Fatalf("err: %s", err) - } - testResponseStatus(t, resp, 200) -} - -func TestHandler_error(t *testing.T) { - w := httptest.NewRecorder() - - respondError(w, 500, errors.New("test Error")) - - if w.Code != 500 { - t.Fatalf("expected 500, got %d", w.Code) - } - - // The code inside of the error should override - // the argument to respondError - w2 := httptest.NewRecorder() - e := logical.CodedError(403, "error text") - - respondError(w2, 500, e) - - if w2.Code != 403 { - t.Fatalf("expected 403, got %d", w2.Code) - } - - // vault.ErrSealed is a special case - w3 := httptest.NewRecorder() - - respondError(w3, 400, consts.ErrSealed) - - if w3.Code != 503 { - t.Fatalf("expected 503, got %d", w3.Code) - } -} - -func TestHandler_requestAuth(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - - rootCtx := namespace.RootContext(nil) - te, err := core.LookupToken(rootCtx, token) - if err != nil { - t.Fatalf("err: %s", err) - } - - rWithAuthorization, err := http.NewRequest("GET", "v1/test/path", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - rWithAuthorization.Header.Set("Authorization", "Bearer "+token) - - rWithVault, err := http.NewRequest("GET", "v1/test/path", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - rWithVault.Header.Set(consts.AuthHeaderName, token) - - for _, r := range []*http.Request{rWithVault, rWithAuthorization} { - req := logical.TestRequest(t, logical.ReadOperation, "test/path") - r = r.WithContext(rootCtx) - requestAuth(r, req) - err = core.PopulateTokenEntry(rootCtx, req) - if err != nil { - t.Fatalf("err: %s", err) - } - - if req.ClientToken != token { - t.Fatalf("client token should be filled with %s, got %s", token, req.ClientToken) - } - if req.TokenEntry() == nil { - t.Fatal("token entry should not be nil") - } - if !reflect.DeepEqual(req.TokenEntry(), te) { - t.Fatalf("token entry should be the same as the core") - } - if req.ClientTokenAccessor == "" { - t.Fatal("token accessor should not be empty") - } - } - - rNothing, err := http.NewRequest("GET", "v1/test/path", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - req := logical.TestRequest(t, logical.ReadOperation, "test/path") - - requestAuth(rNothing, req) - err = core.PopulateTokenEntry(rootCtx, req) - if err != nil { - t.Fatalf("expected no error, got %s", err) - } - if req.ClientToken != "" { - t.Fatalf("client token should not be filled, got %s", req.ClientToken) - } -} - -func TestHandler_getTokenFromReq(t *testing.T) { - r := http.Request{Header: http.Header{}} - - tok, _ := getTokenFromReq(&r) - if tok != "" { - t.Fatalf("expected '' as result, got '%s'", tok) - } - - r.Header.Set("Authorization", "Bearer TOKEN NOT_GOOD_TOKEN") - token, fromHeader := getTokenFromReq(&r) - if !fromHeader { - t.Fatal("expected from header") - } else if token != "TOKEN NOT_GOOD_TOKEN" { - t.Fatal("did not get expected token value") - } else if r.Header.Get("Authorization") == "" { - t.Fatal("expected value to be passed through") - } - - r.Header.Set(consts.AuthHeaderName, "NEWTOKEN") - tok, _ = getTokenFromReq(&r) - if tok == "TOKEN" { - t.Fatalf("%s header should be prioritized", consts.AuthHeaderName) - } else if tok != "NEWTOKEN" { - t.Fatalf("expected 'NEWTOKEN' as result, got '%s'", tok) - } - - r.Header = http.Header{} - r.Header.Set("Authorization", "Basic TOKEN") - tok, fromHeader = getTokenFromReq(&r) - if tok != "" { - t.Fatalf("expected '' as result, got '%s'", tok) - } else if fromHeader { - t.Fatal("expected not from header") - } -} - -func TestHandler_nonPrintableChars(t *testing.T) { - testNonPrintable(t, false) - testNonPrintable(t, true) -} - -func testNonPrintable(t *testing.T, disable bool) { - core, _, token := vault.TestCoreUnsealedWithConfig(t, &vault.CoreConfig{ - DisableKeyEncodingChecks: disable, - }) - ln, addr := TestListener(t) - props := &vault.HandlerProperties{ - Core: core, - DisablePrintableCheck: disable, - } - TestServerWithListenerAndProperties(t, ln, addr, core, props) - defer ln.Close() - - req, err := http.NewRequest("PUT", addr+"/v1/cubbyhole/foo\u2028bar", strings.NewReader(`{"zip": "zap"}`)) - if err != nil { - t.Fatalf("err: %s", err) - } - req.Header.Set(consts.AuthHeaderName, token) - - client := cleanhttp.DefaultClient() - resp, err := client.Do(req) - if err != nil { - t.Fatalf("err: %s", err) - } - - if disable { - testResponseStatus(t, resp, 204) - } else { - testResponseStatus(t, resp, 400) - } -} - -func TestHandler_Parse_Form(t *testing.T) { - cluster := vault.NewTestCluster(t, &vault.CoreConfig{}, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - core := cores[0].Core - vault.TestWaitActive(t, core) - - c := cleanhttp.DefaultClient() - c.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: cluster.RootCAs, - }, - } - - values := url.Values{ - "zip": []string{"zap"}, - "abc": []string{"xyz"}, - "multi": []string{"first", "second"}, - "empty": []string{}, - } - req, err := http.NewRequest("POST", cores[0].Client.Address()+"/v1/secret/foo", nil) - if err != nil { - t.Fatal(err) - } - req.Body = ioutil.NopCloser(strings.NewReader(values.Encode())) - req.Header.Set("x-vault-token", cluster.RootToken) - req.Header.Set("content-type", "application/x-www-form-urlencoded") - resp, err := c.Do(req) - if err != nil { - t.Fatal(err) - } - - if resp.StatusCode != 204 { - t.Fatalf("bad response: %#v\nrequest was: %#v\nurl was: %#v", *resp, *req, req.URL) - } - - client := cores[0].Client - client.SetToken(cluster.RootToken) - - apiResp, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatal(err) - } - if apiResp == nil { - t.Fatal("api resp is nil") - } - expected := map[string]interface{}{ - "zip": "zap", - "abc": "xyz", - "multi": "first,second", - } - if diff := deep.Equal(expected, apiResp.Data); diff != nil { - t.Fatal(diff) - } -} - -// TestHandler_MaxRequestSize verifies that a request larger than the -// MaxRequestSize fails -func TestHandler_MaxRequestSize(t *testing.T) { - t.Parallel() - cluster := vault.NewTestCluster(t, &vault.CoreConfig{}, &vault.TestClusterOptions{ - DefaultHandlerProperties: vault.HandlerProperties{ - ListenerConfig: &configutil.Listener{ - MaxRequestSize: 1024, - }, - }, - HandlerFunc: Handler, - NumCores: 1, - }) - cluster.Start() - defer cluster.Cleanup() - - client := cluster.Cores[0].Client - _, err := client.KVv2("secret").Put(context.Background(), "foo", map[string]interface{}{ - "bar": strings.Repeat("a", 1025), - }) - - require.ErrorContains(t, err, "error parsing JSON") -} - -// TestHandler_MaxRequestSize_Memory sets the max request size to 1024 bytes, -// and creates a 1MB request. The test verifies that less than 1MB of memory is -// allocated when the request is sent. This test shouldn't be run in parallel, -// because it modifies GOMAXPROCS -func TestHandler_MaxRequestSize_Memory(t *testing.T) { - ln, addr := TestListener(t) - core, _, token := vault.TestCoreUnsealed(t) - TestServerWithListenerAndProperties(t, ln, addr, core, &vault.HandlerProperties{ - Core: core, - ListenerConfig: &configutil.Listener{ - Address: addr, - MaxRequestSize: 1024, - }, - }) - defer ln.Close() - - data := bytes.Repeat([]byte{0x1}, 1024*1024) - - req, err := http.NewRequest("POST", addr+"/v1/sys/unseal", bytes.NewReader(data)) - require.NoError(t, err) - req.Header.Set(consts.AuthHeaderName, token) - - client := cleanhttp.DefaultClient() - defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) - var start, end runtime.MemStats - runtime.GC() - runtime.ReadMemStats(&start) - client.Do(req) - runtime.ReadMemStats(&end) - require.Less(t, end.TotalAlloc-start.TotalAlloc, uint64(1024*1024)) -} diff --git a/http/help_test.go b/http/help_test.go deleted file mode 100644 index 5fa96e50d..000000000 --- a/http/help_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "net/http" - "testing" - - "github.com/hashicorp/vault/vault" -) - -func TestHelp(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // request without /v1/ prefix - resp := testHttpGet(t, token, addr+"/?help=1") - testResponseStatus(t, resp, 404) - - resp = testHttpGet(t, "", addr+"/v1/sys/mounts?help=1") - if resp.StatusCode != http.StatusForbidden { - t.Fatal("expected permission denied with no token") - } - - resp = testHttpGet(t, token, addr+"/v1/sys/mounts?help=1") - - var actual map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if _, ok := actual["help"]; !ok { - t.Fatalf("bad: %#v", actual) - } -} diff --git a/http/http_test.go b/http/http_test.go deleted file mode 100644 index f2283eec9..000000000 --- a/http/http_test.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "regexp" - "strings" - "testing" - "time" - - cleanhttp "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/jsonutil" -) - -func testHttpGet(t *testing.T, token string, addr string) *http.Response { - loggedToken := token - if len(token) == 0 { - loggedToken = "" - } - t.Logf("Token is %s", loggedToken) - return testHttpData(t, "GET", token, addr, nil, false, 0) -} - -func testHttpDelete(t *testing.T, token string, addr string) *http.Response { - return testHttpData(t, "DELETE", token, addr, nil, false, 0) -} - -// Go 1.8+ clients redirect automatically which breaks our 307 standby testing -func testHttpDeleteDisableRedirect(t *testing.T, token string, addr string) *http.Response { - return testHttpData(t, "DELETE", token, addr, nil, true, 0) -} - -func testHttpPostWrapped(t *testing.T, token string, addr string, body interface{}, wrapTTL time.Duration) *http.Response { - return testHttpData(t, "POST", token, addr, body, false, wrapTTL) -} - -func testHttpPost(t *testing.T, token string, addr string, body interface{}) *http.Response { - return testHttpData(t, "POST", token, addr, body, false, 0) -} - -func testHttpPut(t *testing.T, token string, addr string, body interface{}) *http.Response { - return testHttpData(t, "PUT", token, addr, body, false, 0) -} - -// Go 1.8+ clients redirect automatically which breaks our 307 standby testing -func testHttpPutDisableRedirect(t *testing.T, token string, addr string, body interface{}) *http.Response { - return testHttpData(t, "PUT", token, addr, body, true, 0) -} - -func testHttpData(t *testing.T, method string, token string, addr string, body interface{}, disableRedirect bool, wrapTTL time.Duration) *http.Response { - bodyReader := new(bytes.Buffer) - if body != nil { - enc := json.NewEncoder(bodyReader) - if err := enc.Encode(body); err != nil { - t.Fatalf("err:%s", err) - } - } - - req, err := http.NewRequest(method, addr, bodyReader) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Get the address of the local listener in order to attach it to an Origin header. - // This will allow for the testing of requests that require CORS, without using a browser. - hostURLRegexp, _ := regexp.Compile("http[s]?://.+:[0-9]+") - req.Header.Set("Origin", hostURLRegexp.FindString(addr)) - - req.Header.Set("Content-Type", "application/json") - - if wrapTTL > 0 { - req.Header.Set("X-Vault-Wrap-TTL", wrapTTL.String()) - } - - if len(token) != 0 { - req.Header.Set(consts.AuthHeaderName, token) - } - - client := cleanhttp.DefaultClient() - client.Timeout = 60 * time.Second - - // From https://github.com/michiwend/gomusicbrainz/pull/4/files - defaultRedirectLimit := 30 - - client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - if disableRedirect { - return fmt.Errorf("checkRedirect disabled for test") - } - if len(via) > defaultRedirectLimit { - return fmt.Errorf("%d consecutive requests(redirects)", len(via)) - } - if len(via) == 0 { - // No redirects - return nil - } - // mutate the subsequent redirect requests with the first Header - if token := via[0].Header.Get(consts.AuthHeaderName); len(token) != 0 { - req.Header.Set(consts.AuthHeaderName, token) - } - return nil - } - - resp, err := client.Do(req) - if err != nil && !strings.Contains(err.Error(), "checkRedirect disabled for test") { - t.Fatalf("err: %s", err) - } - - return resp -} - -func testResponseStatus(t *testing.T, resp *http.Response, code int) { - t.Helper() - if resp.StatusCode != code { - body := new(bytes.Buffer) - io.Copy(body, resp.Body) - resp.Body.Close() - - t.Fatalf( - "Expected status %d, got %d. Body:\n\n%s", - code, resp.StatusCode, body.String()) - } -} - -func testResponseHeader(t *testing.T, resp *http.Response, expectedHeaders map[string]string) { - t.Helper() - for k, v := range expectedHeaders { - hv := resp.Header.Get(k) - if v != hv { - t.Fatalf("expected header value %v=%v, got %v=%v", k, v, k, hv) - } - } -} - -func testResponseBody(t *testing.T, resp *http.Response, out interface{}) { - defer resp.Body.Close() - - if err := jsonutil.DecodeJSONFromReader(resp.Body, out); err != nil { - t.Fatalf("err: %s", err) - } -} diff --git a/http/logical_test.go b/http/logical_test.go deleted file mode 100644 index 83a96c4a3..000000000 --- a/http/logical_test.go +++ /dev/null @@ -1,940 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "bytes" - "context" - "encoding/json" - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "reflect" - "strconv" - "strings" - "testing" - "time" - - kv "github.com/hashicorp/vault-plugin-secrets-kv" - "github.com/hashicorp/vault/api" - auditFile "github.com/hashicorp/vault/builtin/audit/file" - credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/hashicorp/vault/internalshared/configutil" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/physical" - "github.com/hashicorp/vault/sdk/physical/inmem" - - "github.com/go-test/deep" - log "github.com/hashicorp/go-hclog" - - "github.com/hashicorp/vault/audit" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/vault" -) - -func TestLogical(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // WRITE - resp := testHttpPut(t, token, addr+"/v1/secret/foo", map[string]interface{}{ - "data": "bar", - }) - testResponseStatus(t, resp, 204) - - // READ - // Bad token should return a 403 - resp = testHttpGet(t, token+"bad", addr+"/v1/secret/foo") - testResponseStatus(t, resp, 403) - - resp = testHttpGet(t, token, addr+"/v1/secret/foo") - var actual map[string]interface{} - var nilWarnings interface{} - expected := map[string]interface{}{ - "renewable": false, - "lease_duration": json.Number(strconv.Itoa(int((32 * 24 * time.Hour) / time.Second))), - "data": map[string]interface{}{ - "data": "bar", - }, - "auth": nil, - "wrap_info": nil, - "warnings": nilWarnings, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - delete(actual, "lease_id") - expected["request_id"] = actual["request_id"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } - - // DELETE - resp = testHttpDelete(t, token, addr+"/v1/secret/foo") - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/secret/foo") - testResponseStatus(t, resp, 404) -} - -func TestLogical_noExist(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpGet(t, token, addr+"/v1/secret/foo") - testResponseStatus(t, resp, 404) -} - -func TestLogical_StandbyRedirect(t *testing.T) { - ln1, addr1 := TestListener(t) - defer ln1.Close() - ln2, addr2 := TestListener(t) - defer ln2.Close() - - // Create an HA Vault - logger := logging.NewVaultLogger(log.Debug) - - inmha, err := inmem.NewInmemHA(nil, logger) - if err != nil { - t.Fatal(err) - } - conf := &vault.CoreConfig{ - Physical: inmha, - HAPhysical: inmha.(physical.HABackend), - RedirectAddr: addr1, - DisableMlock: true, - } - core1, err := vault.NewCore(conf) - if err != nil { - t.Fatalf("err: %v", err) - } - defer core1.Shutdown() - keys, root := vault.TestCoreInit(t, core1) - for _, key := range keys { - if _, err := core1.Unseal(vault.TestKeyCopy(key)); err != nil { - t.Fatalf("unseal err: %s", err) - } - } - - // Attempt to fix raciness in this test by giving the first core a chance - // to grab the lock - time.Sleep(2 * time.Second) - - // Create a second HA Vault - conf2 := &vault.CoreConfig{ - Physical: inmha, - HAPhysical: inmha.(physical.HABackend), - RedirectAddr: addr2, - DisableMlock: true, - } - core2, err := vault.NewCore(conf2) - if err != nil { - t.Fatalf("err: %v", err) - } - defer core2.Shutdown() - for _, key := range keys { - if _, err := core2.Unseal(vault.TestKeyCopy(key)); err != nil { - t.Fatalf("unseal err: %s", err) - } - } - - TestServerWithListener(t, ln1, addr1, core1) - TestServerWithListener(t, ln2, addr2, core2) - TestServerAuth(t, addr1, root) - - // WRITE to STANDBY - resp := testHttpPutDisableRedirect(t, root, addr2+"/v1/secret/foo", map[string]interface{}{ - "data": "bar", - }) - logger.Debug("307 test one starting") - testResponseStatus(t, resp, 307) - logger.Debug("307 test one stopping") - - //// READ to standby - resp = testHttpGet(t, root, addr2+"/v1/auth/token/lookup-self") - var actual map[string]interface{} - var nilWarnings interface{} - expected := map[string]interface{}{ - "renewable": false, - "lease_duration": json.Number("0"), - "data": map[string]interface{}{ - "meta": nil, - "num_uses": json.Number("0"), - "path": "auth/token/root", - "policies": []interface{}{"root"}, - "display_name": "root", - "orphan": true, - "id": root, - "ttl": json.Number("0"), - "creation_ttl": json.Number("0"), - "explicit_max_ttl": json.Number("0"), - "expire_time": nil, - "entity_id": "", - "type": "service", - }, - "warnings": nilWarnings, - "wrap_info": nil, - "auth": nil, - } - - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - actualDataMap := actual["data"].(map[string]interface{}) - delete(actualDataMap, "creation_time") - delete(actualDataMap, "accessor") - actual["data"] = actualDataMap - expected["request_id"] = actual["request_id"] - delete(actual, "lease_id") - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } - - //// DELETE to standby - resp = testHttpDeleteDisableRedirect(t, root, addr2+"/v1/secret/foo") - logger.Debug("307 test two starting") - testResponseStatus(t, resp, 307) - logger.Debug("307 test two stopping") -} - -func TestLogical_CreateToken(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // WRITE - resp := testHttpPut(t, token, addr+"/v1/auth/token/create", map[string]interface{}{ - "data": "bar", - }) - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "data": nil, - "wrap_info": nil, - "auth": map[string]interface{}{ - "policies": []interface{}{"root"}, - "token_policies": []interface{}{"root"}, - "metadata": nil, - "lease_duration": json.Number("0"), - "renewable": false, - "entity_id": "", - "token_type": "service", - "orphan": false, - "mfa_requirement": nil, - "num_uses": json.Number("0"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - delete(actual["auth"].(map[string]interface{}), "client_token") - delete(actual["auth"].(map[string]interface{}), "accessor") - delete(actual, "warnings") - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nexpected:\n%#v\nactual:\n%#v", expected, actual) - } -} - -func TestLogical_RawHTTP(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/foo", map[string]interface{}{ - "type": "http", - }) - testResponseStatus(t, resp, 204) - - // Get the raw response - resp = testHttpGet(t, token, addr+"/v1/foo/raw") - testResponseStatus(t, resp, 200) - - // Test the headers - if resp.Header.Get("Content-Type") != "plain/text" { - t.Fatalf("Bad: %#v", resp.Header) - } - - // Get the body - body := new(bytes.Buffer) - io.Copy(body, resp.Body) - if string(body.Bytes()) != "hello world" { - t.Fatalf("Bad: %s", body.Bytes()) - } -} - -func TestLogical_RequestSizeLimit(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Write a very large object, should fail. This test works because Go will - // convert the byte slice to base64, which makes it significantly larger - // than the default max request size. - resp := testHttpPut(t, token, addr+"/v1/secret/foo", map[string]interface{}{ - "data": make([]byte, DefaultMaxRequestSize), - }) - testResponseStatus(t, resp, http.StatusRequestEntityTooLarge) -} - -func TestLogical_RequestSizeDisableLimit(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestListener(t) - props := &vault.HandlerProperties{ - Core: core, - ListenerConfig: &configutil.Listener{ - MaxRequestSize: -1, - Address: "127.0.0.1", - TLSDisable: true, - }, - } - TestServerWithListenerAndProperties(t, ln, addr, core, props) - - defer ln.Close() - TestServerAuth(t, addr, token) - - // Write a very large object, should pass as MaxRequestSize set to -1/Negative value - - resp := testHttpPut(t, token, addr+"/v1/secret/foo", map[string]interface{}{ - "data": make([]byte, DefaultMaxRequestSize), - }) - testResponseStatus(t, resp, http.StatusNoContent) -} - -func TestLogical_ListSuffix(t *testing.T) { - core, _, rootToken := vault.TestCoreUnsealed(t) - req, _ := http.NewRequest("GET", "http://127.0.0.1:8200/v1/secret/foo", nil) - req = req.WithContext(namespace.RootContext(nil)) - req.Header.Add(consts.AuthHeaderName, rootToken) - - lreq, _, status, err := buildLogicalRequest(core, nil, req) - if err != nil { - t.Fatal(err) - } - if status != 0 { - t.Fatalf("got status %d", status) - } - if strings.HasSuffix(lreq.Path, "/") { - t.Fatal("trailing slash found on path") - } - - req, _ = http.NewRequest("GET", "http://127.0.0.1:8200/v1/secret/foo?list=true", nil) - req = req.WithContext(namespace.RootContext(nil)) - req.Header.Add(consts.AuthHeaderName, rootToken) - - lreq, _, status, err = buildLogicalRequest(core, nil, req) - if err != nil { - t.Fatal(err) - } - if status != 0 { - t.Fatalf("got status %d", status) - } - if !strings.HasSuffix(lreq.Path, "/") { - t.Fatal("trailing slash not found on path") - } - - req, _ = http.NewRequest("LIST", "http://127.0.0.1:8200/v1/secret/foo", nil) - req = req.WithContext(namespace.RootContext(nil)) - req.Header.Add(consts.AuthHeaderName, rootToken) - - _, _, status, err = buildLogicalRequestNoAuth(core.PerfStandby(), nil, req) - if err != nil || status != 0 { - t.Fatal(err) - } - - lreq, _, status, err = buildLogicalRequest(core, nil, req) - if err != nil { - t.Fatal(err) - } - if status != 0 { - t.Fatalf("got status %d", status) - } - if !strings.HasSuffix(lreq.Path, "/") { - t.Fatal("trailing slash not found on path") - } -} - -func TestLogical_ListWithQueryParameters(t *testing.T) { - core, _, rootToken := vault.TestCoreUnsealed(t) - - tests := []struct { - name string - requestMethod string - url string - expectedData map[string]interface{} - }{ - { - name: "LIST request method parses query parameter", - requestMethod: "LIST", - url: "http://127.0.0.1:8200/v1/secret/foo?key1=value1", - expectedData: map[string]interface{}{ - "key1": "value1", - }, - }, - { - name: "LIST request method parses query multiple parameters", - requestMethod: "LIST", - url: "http://127.0.0.1:8200/v1/secret/foo?key1=value1&key2=value2", - expectedData: map[string]interface{}{ - "key1": "value1", - "key2": "value2", - }, - }, - { - name: "GET request method with list=true parses query parameter", - requestMethod: "GET", - url: "http://127.0.0.1:8200/v1/secret/foo?list=true&key1=value1", - expectedData: map[string]interface{}{ - "key1": "value1", - }, - }, - { - name: "GET request method with list=true parses multiple query parameters", - requestMethod: "GET", - url: "http://127.0.0.1:8200/v1/secret/foo?list=true&key1=value1&key2=value2", - expectedData: map[string]interface{}{ - "key1": "value1", - "key2": "value2", - }, - }, - { - name: "GET request method with alternate order list=true parses multiple query parameters", - requestMethod: "GET", - url: "http://127.0.0.1:8200/v1/secret/foo?key1=value1&list=true&key2=value2", - expectedData: map[string]interface{}{ - "key1": "value1", - "key2": "value2", - }, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - req, _ := http.NewRequest(tc.requestMethod, tc.url, nil) - req = req.WithContext(namespace.RootContext(nil)) - req.Header.Add(consts.AuthHeaderName, rootToken) - - lreq, _, status, err := buildLogicalRequest(core, nil, req) - if err != nil { - t.Fatal(err) - } - if status != 0 { - t.Fatalf("got status %d", status) - } - if !strings.HasSuffix(lreq.Path, "/") { - t.Fatal("trailing slash not found on path") - } - if lreq.Operation != logical.ListOperation { - t.Fatalf("expected logical.ListOperation, got %v", lreq.Operation) - } - if !reflect.DeepEqual(tc.expectedData, lreq.Data) { - t.Fatalf("expected query parameter data %v, got %v", tc.expectedData, lreq.Data) - } - }) - } -} - -func TestLogical_RespondWithStatusCode(t *testing.T) { - resp := &logical.Response{ - Data: map[string]interface{}{ - "test-data": "foo", - }, - } - - resp404, err := logical.RespondWithStatusCode(resp, &logical.Request{ID: "id"}, http.StatusNotFound) - if err != nil { - t.Fatal(err) - } - - w := httptest.NewRecorder() - respondLogical(nil, w, nil, nil, resp404, false) - - if w.Code != 404 { - t.Fatalf("Bad Status code: %d", w.Code) - } - - bodyRaw, err := ioutil.ReadAll(w.Body) - if err != nil { - t.Fatal(err) - } - - expected := `{"request_id":"id","lease_id":"","renewable":false,"lease_duration":0,"data":{"test-data":"foo"},"wrap_info":null,"warnings":null,"auth":null}` - - if string(bodyRaw[:]) != strings.Trim(expected, "\n") { - t.Fatalf("bad response: %s", string(bodyRaw[:])) - } -} - -func TestLogical_Audit_invalidWrappingToken(t *testing.T) { - // Create a noop audit backend - noop := corehelpers.TestNoopAudit(t, nil) - c, _, root := vault.TestCoreUnsealedWithConfig(t, &vault.CoreConfig{ - AuditBackends: map[string]audit.Factory{ - "noop": func(ctx context.Context, config *audit.BackendConfig) (audit.Backend, error) { - return noop, nil - }, - }, - }) - ln, addr := TestServer(t, c) - defer ln.Close() - - // Enable the audit backend - - resp := testHttpPost(t, root, addr+"/v1/sys/audit/noop", map[string]interface{}{ - "type": "noop", - }) - testResponseStatus(t, resp, 204) - - { - // Make a wrapping/unwrap request with an invalid token - resp := testHttpPost(t, root, addr+"/v1/sys/wrapping/unwrap", map[string]interface{}{ - "token": "foo", - }) - testResponseStatus(t, resp, 400) - body := map[string][]string{} - testResponseBody(t, resp, &body) - if body["errors"][0] != "wrapping token is not valid or does not exist" { - t.Fatal(body) - } - - // Check the audit trail on request and response - if len(noop.ReqAuth) != 1 { - t.Fatalf("bad: %#v", noop) - } - auth := noop.ReqAuth[0] - if auth.ClientToken != root { - t.Fatalf("bad client token: %#v", auth) - } - if len(noop.Req) != 1 || noop.Req[0].Path != "sys/wrapping/unwrap" { - t.Fatalf("bad:\ngot:\n%#v", noop.Req[0]) - } - - if len(noop.ReqErrs) != 1 { - t.Fatalf("bad: %#v", noop.RespErrs) - } - if noop.ReqErrs[0] != consts.ErrInvalidWrappingToken { - t.Fatalf("bad: %#v", noop.ReqErrs) - } - } - - { - resp := testHttpPostWrapped(t, root, addr+"/v1/auth/token/create", nil, 10*time.Second) - testResponseStatus(t, resp, 200) - body := map[string]interface{}{} - testResponseBody(t, resp, &body) - - wrapToken := body["wrap_info"].(map[string]interface{})["token"].(string) - - // Make a wrapping/unwrap request with an invalid token - resp = testHttpPost(t, root, addr+"/v1/sys/wrapping/unwrap", map[string]interface{}{ - "token": wrapToken, - }) - testResponseStatus(t, resp, 200) - - // Check the audit trail on request and response - if len(noop.ReqAuth) != 3 { - t.Fatalf("bad: %#v", noop) - } - auth := noop.ReqAuth[2] - if auth.ClientToken != root { - t.Fatalf("bad client token: %#v", auth) - } - if len(noop.Req) != 3 || noop.Req[2].Path != "sys/wrapping/unwrap" { - t.Fatalf("bad:\ngot:\n%#v", noop.Req[2]) - } - - // Make sure there is only one error in the logs - if noop.ReqErrs[1] != nil || noop.ReqErrs[2] != nil { - t.Fatalf("bad: %#v", noop.RespErrs) - } - } -} - -func TestLogical_ShouldParseForm(t *testing.T) { - const formCT = "application/x-www-form-urlencoded" - - tests := map[string]struct { - prefix string - contentType string - isForm bool - }{ - "JSON": {`{"a":42}`, formCT, false}, - "JSON 2": {`[42]`, formCT, false}, - "JSON w/leading space": {" \n\n\r\t [42] ", formCT, false}, - "Form": {"a=42&b=dog", formCT, true}, - "Form w/wrong CT": {"a=42&b=dog", "application/json", false}, - } - - for name, test := range tests { - isForm := isForm([]byte(test.prefix), test.contentType) - - if isForm != test.isForm { - t.Fatalf("%s fail: expected isForm %t, got %t", name, test.isForm, isForm) - } - } -} - -func TestLogical_AuditPort(t *testing.T) { - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "kv": kv.VersionedKVFactory, - }, - AuditBackends: map[string]audit.Factory{ - "file": auditFile.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - core := cores[0].Core - c := cluster.Cores[0].Client - vault.TestWaitActive(t, core) - - if err := c.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - auditLogFile, err := ioutil.TempFile("", "auditport") - if err != nil { - t.Fatal(err) - } - - err = c.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{ - Type: "file", - Options: map[string]string{ - "file_path": auditLogFile.Name(), - }, - }) - if err != nil { - t.Fatalf("failed to enable audit file, err: %#v\n", err) - } - - writeData := map[string]interface{}{ - "data": map[string]interface{}{ - "bar": "a", - }, - } - - // workaround kv-v2 initialization upgrade errors - numFailures := 0 - corehelpers.RetryUntil(t, 10*time.Second, func() error { - resp, err := c.Logical().Write("kv/data/foo", writeData) - if err != nil { - if strings.Contains(err.Error(), "Upgrading from non-versioned to versioned data") { - t.Logf("Retrying fetch KV data due to upgrade error") - time.Sleep(100 * time.Millisecond) - numFailures += 1 - return err - } - - t.Fatalf("write request failed, err: %#v, resp: %#v\n", err, resp) - } - - return nil - }) - - decoder := json.NewDecoder(auditLogFile) - - var auditRecord map[string]interface{} - count := 0 - for decoder.Decode(&auditRecord) == nil { - count += 1 - - // Skip the first line - if count == 1 { - continue - } - - auditRequest := map[string]interface{}{} - - if req, ok := auditRecord["request"]; ok { - auditRequest = req.(map[string]interface{}) - } - - if _, ok := auditRequest["remote_address"].(string); !ok { - t.Fatalf("remote_address should be a string, not %T", auditRequest["remote_address"]) - } - - if _, ok := auditRequest["remote_port"].(float64); !ok { - t.Fatalf("remote_port should be a number, not %T", auditRequest["remote_port"]) - } - } - - // We expect the following items in the audit log: - // audit log header + an entry for updating sys/audit/file - // + request/response per failure (if any) + request/response for creating kv - numExpectedEntries := (numFailures * 2) + 4 - if count != numExpectedEntries { - t.Fatalf("wrong number of audit entries expected: %d got: %d", numExpectedEntries, count) - } -} - -func TestLogical_ErrRelativePath(t *testing.T) { - coreConfig := &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": credUserpass.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - core := cores[0].Core - c := cluster.Cores[0].Client - vault.TestWaitActive(t, core) - - err := c.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", - }) - if err != nil { - t.Fatalf("failed to enable userpass, err: %v", err) - } - - resp, err := c.Logical().Read("auth/userpass/users/user..aaa") - - if err == nil || resp != nil { - t.Fatalf("expected read request to fail, resp: %#v, err: %v", resp, err) - } - - respErr, ok := err.(*api.ResponseError) - - if !ok { - t.Fatalf("unexpected error type, err: %#v", err) - } - - if respErr.StatusCode != 400 { - t.Errorf("expected 400 response for read, actual: %d", respErr.StatusCode) - } - - if !strings.Contains(respErr.Error(), logical.ErrRelativePath.Error()) { - t.Errorf("expected response for read to include %q", logical.ErrRelativePath.Error()) - } - - data := map[string]interface{}{ - "password": "abc123", - } - - resp, err = c.Logical().Write("auth/userpass/users/user..aaa", data) - - if err == nil || resp != nil { - t.Fatalf("expected write request to fail, resp: %#v, err: %v", resp, err) - } - - respErr, ok = err.(*api.ResponseError) - - if !ok { - t.Fatalf("unexpected error type, err: %#v", err) - } - - if respErr.StatusCode != 400 { - t.Errorf("expected 400 response for write, actual: %d", respErr.StatusCode) - } - - if !strings.Contains(respErr.Error(), logical.ErrRelativePath.Error()) { - t.Errorf("expected response for write to include %q", logical.ErrRelativePath.Error()) - } -} - -func testBuiltinPluginMetadataAuditLog(t *testing.T, log map[string]interface{}, expectedMountClass string) { - if mountClass, ok := log["mount_class"].(string); !ok { - t.Fatalf("mount_class should be a string, not %T", log["mount_class"]) - } else if mountClass != expectedMountClass { - t.Fatalf("bad: mount_class should be %s, not %s", expectedMountClass, mountClass) - } - - if _, ok := log["mount_running_version"].(string); !ok { - t.Fatalf("mount_running_version should be a string, not %T", log["mount_running_version"]) - } - - if _, ok := log["mount_running_sha256"].(string); ok { - t.Fatalf("mount_running_sha256 should be nil, not %T", log["mount_running_sha256"]) - } - - if mountIsExternalPlugin, ok := log["mount_is_external_plugin"].(bool); ok && mountIsExternalPlugin { - t.Fatalf("mount_is_external_plugin should be nil or false, not %T", log["mount_is_external_plugin"]) - } -} - -// TestLogical_AuditEnabled_ShouldLogPluginMetadata_Auth tests that we have plugin metadata of a builtin auth plugin -// in audit log when it is enabled -func TestLogical_AuditEnabled_ShouldLogPluginMetadata_Auth(t *testing.T) { - coreConfig := &vault.CoreConfig{ - AuditBackends: map[string]audit.Factory{ - "file": auditFile.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - core := cores[0].Core - c := cluster.Cores[0].Client - vault.TestWaitActive(t, core) - - // Enable the audit backend - tempDir := t.TempDir() - auditLogFile, err := os.CreateTemp(tempDir, "") - if err != nil { - t.Fatal(err) - } - - err = c.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{ - Type: "file", - Options: map[string]string{ - "file_path": auditLogFile.Name(), - }, - }) - if err != nil { - t.Fatal(err) - } - - _, err = c.Logical().Write("auth/token/create", map[string]interface{}{ - "ttl": "10s", - }) - if err != nil { - t.Fatal(err) - } - - // Check the audit trail on request and response - decoder := json.NewDecoder(auditLogFile) - var auditRecord map[string]interface{} - for decoder.Decode(&auditRecord) == nil { - auditRequest := map[string]interface{}{} - if req, ok := auditRecord["request"]; ok { - auditRequest = req.(map[string]interface{}) - if auditRequest["path"] != "auth/token/create" { - continue - } - } - testBuiltinPluginMetadataAuditLog(t, auditRequest, consts.PluginTypeCredential.String()) - - auditResponse := map[string]interface{}{} - if req, ok := auditRecord["response"]; ok { - auditRequest = req.(map[string]interface{}) - if auditResponse["path"] != "auth/token/create" { - continue - } - } - testBuiltinPluginMetadataAuditLog(t, auditResponse, consts.PluginTypeCredential.String()) - } -} - -// TestLogical_AuditEnabled_ShouldLogPluginMetadata_Secret tests that we have plugin metadata of a builtin secret plugin -// in audit log when it is enabled -func TestLogical_AuditEnabled_ShouldLogPluginMetadata_Secret(t *testing.T) { - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "kv": kv.VersionedKVFactory, - }, - AuditBackends: map[string]audit.Factory{ - "file": auditFile.Factory, - }, - } - - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - core := cores[0].Core - c := cluster.Cores[0].Client - vault.TestWaitActive(t, core) - - if err := c.Sys().Mount("kv/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err) - } - - // Enable the audit backend - tempDir := t.TempDir() - auditLogFile, err := os.CreateTemp(tempDir, "") - if err != nil { - t.Fatal(err) - } - - err = c.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{ - Type: "file", - Options: map[string]string{ - "file_path": auditLogFile.Name(), - }, - }) - if err != nil { - t.Fatal(err) - } - - { - writeData := map[string]interface{}{ - "data": map[string]interface{}{ - "bar": "a", - }, - } - corehelpers.RetryUntil(t, 10*time.Second, func() error { - resp, err := c.Logical().Write("kv/data/foo", writeData) - if err != nil { - t.Fatalf("write request failed, err: %#v, resp: %#v\n", err, resp) - } - return nil - }) - } - - // Check the audit trail on request and response - decoder := json.NewDecoder(auditLogFile) - var auditRecord map[string]interface{} - for decoder.Decode(&auditRecord) == nil { - auditRequest := map[string]interface{}{} - if req, ok := auditRecord["request"]; ok { - auditRequest = req.(map[string]interface{}) - if auditRequest["path"] != "kv/data/foo" { - continue - } - } - testBuiltinPluginMetadataAuditLog(t, auditRequest, consts.PluginTypeSecrets.String()) - - auditResponse := map[string]interface{}{} - if req, ok := auditRecord["response"]; ok { - auditRequest = req.(map[string]interface{}) - if auditResponse["path"] != "kv/data/foo" { - continue - } - } - testBuiltinPluginMetadataAuditLog(t, auditResponse, consts.PluginTypeSecrets.String()) - } -} diff --git a/http/plugin_test.go b/http/plugin_test.go deleted file mode 100644 index 5e612e098..000000000 --- a/http/plugin_test.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "io/ioutil" - "os" - "reflect" - "sync" - "testing" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - bplugin "github.com/hashicorp/vault/builtin/plugin" - "github.com/hashicorp/vault/helper/benchhelpers" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/pluginutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/physical" - "github.com/hashicorp/vault/sdk/physical/inmem" - "github.com/hashicorp/vault/sdk/plugin" - "github.com/hashicorp/vault/sdk/plugin/mock" - "github.com/hashicorp/vault/vault" -) - -func getPluginClusterAndCore(t testing.TB, logger log.Logger) (*vault.TestCluster, *vault.TestClusterCore) { - inm, err := inmem.NewTransactionalInmem(nil, logger) - if err != nil { - t.Fatal(err) - } - inmha, err := inmem.NewInmemHA(nil, logger) - if err != nil { - t.Fatal(err) - } - - coreConfig := &vault.CoreConfig{ - Physical: inm, - HAPhysical: inmha.(physical.HABackend), - LogicalBackends: map[string]logical.Factory{ - "plugin": bplugin.Factory, - }, - } - - cluster := vault.NewTestCluster(benchhelpers.TBtoT(t), coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - Logger: logger.Named("testclusteroptions"), - }) - cluster.Start() - - cores := cluster.Cores - core := cores[0] - - os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) - - vault.TestWaitActive(benchhelpers.TBtoT(t), core.Core) - vault.TestAddTestPlugin(benchhelpers.TBtoT(t), core.Core, "mock-plugin", consts.PluginTypeSecrets, "", "TestPlugin_PluginMain", []string{}, "") - - // Mount the mock plugin - err = core.Client.Sys().Mount("mock", &api.MountInput{ - Type: "mock-plugin", - }) - if err != nil { - t.Fatal(err) - } - - return cluster, core -} - -func TestPlugin_PluginMain(t *testing.T) { - if os.Getenv(pluginutil.PluginVaultVersionEnv) == "" { - return - } - - caPEM := os.Getenv(pluginutil.PluginCACertPEMEnv) - if caPEM == "" { - t.Fatal("CA cert not passed in") - } - - args := []string{"--ca-cert=" + caPEM} - - apiClientMeta := &api.PluginAPIClientMeta{} - flags := apiClientMeta.FlagSet() - flags.Parse(args) - - factoryFunc := mock.FactoryType(logical.TypeLogical) - - err := plugin.Serve(&plugin.ServeOpts{ - BackendFactoryFunc: factoryFunc, - }) - if err != nil { - t.Fatal(err) - } - t.Fatal("Why are we here") -} - -func TestPlugin_MockList(t *testing.T) { - logger := log.New(&log.LoggerOptions{ - Mutex: &sync.Mutex{}, - }) - cluster, core := getPluginClusterAndCore(t, logger) - defer cluster.Cleanup() - - _, err := core.Client.Logical().Write("mock/kv/foo", map[string]interface{}{ - "value": "baz", - }) - if err != nil { - t.Fatal(err) - } - - keys, err := core.Client.Logical().List("mock/kv/") - if err != nil { - t.Fatal(err) - } - if keys.Data["keys"].([]interface{})[0].(string) != "foo" { - t.Fatal(keys) - } - - _, err = core.Client.Logical().Write("mock/kv/zoo", map[string]interface{}{ - "value": "baz", - }) - if err != nil { - t.Fatal(err) - } - - keys, err = core.Client.Logical().List("mock/kv/") - if err != nil { - t.Fatal(err) - } - if keys.Data["keys"].([]interface{})[0].(string) != "foo" || keys.Data["keys"].([]interface{})[1].(string) != "zoo" { - t.Fatal(keys) - } -} - -func TestPlugin_MockRawResponse(t *testing.T) { - logger := log.New(&log.LoggerOptions{ - Mutex: &sync.Mutex{}, - }) - cluster, core := getPluginClusterAndCore(t, logger) - defer cluster.Cleanup() - - resp, err := core.Client.RawRequest(core.Client.NewRequest("GET", "/v1/mock/raw")) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - if string(body[:]) != "Response" { - t.Fatal("bad body") - } - - if resp.StatusCode != 200 { - t.Fatal("bad status") - } -} - -func TestPlugin_GetParams(t *testing.T) { - logger := log.New(&log.LoggerOptions{ - Mutex: &sync.Mutex{}, - }) - cluster, core := getPluginClusterAndCore(t, logger) - defer cluster.Cleanup() - - _, err := core.Client.Logical().Write("mock/kv/foo", map[string]interface{}{ - "value": "baz", - }) - if err != nil { - t.Fatal(err) - } - - r := core.Client.NewRequest("GET", "/v1/mock/kv/foo") - r.Params.Add("version", "12") - resp, err := core.Client.RawRequest(r) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - - secret, err := api.ParseSecret(resp.Body) - if err != nil { - t.Fatal(err) - } - - expected := map[string]interface{}{ - "value": "baz", - "version": json.Number("12"), - } - - if !reflect.DeepEqual(secret.Data, expected) { - t.Fatal(secret.Data) - } -} diff --git a/http/sys_audit_test.go b/http/sys_audit_test.go deleted file mode 100644 index d5975909e..000000000 --- a/http/sys_audit_test.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/hashicorp/vault/vault" -) - -func TestSysAudit(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/audit/noop", map[string]interface{}{ - "type": "noop", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/sys/audit") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "noop/": map[string]interface{}{ - "path": "noop/", - "type": "noop", - "description": "", - "options": map[string]interface{}{}, - "local": false, - }, - }, - "noop/": map[string]interface{}{ - "path": "noop/", - "type": "noop", - "description": "", - "options": map[string]interface{}{}, - "local": false, - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["request_id"] = actual["request_id"] - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:\n%#v actual:\n%#v\n", expected, actual) - } -} - -func TestSysDisableAudit(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/audit/foo", map[string]interface{}{ - "type": "noop", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpDelete(t, token, addr+"/v1/sys/audit/foo") - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/sys/audit") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{}, - } - - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["request_id"] = actual["request_id"] - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nactual: %#v\nexpected: %#v\n", actual, expected) - } -} - -func TestSysAuditHash(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/audit/noop", map[string]interface{}{ - "type": "noop", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpPost(t, token, addr+"/v1/sys/audit-hash/noop", map[string]interface{}{ - "input": "bar", - }) - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "hash": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", - }, - "hash": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["request_id"] = actual["request_id"] - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:\n%#v\n, got:\n%#v\n", expected, actual) - } -} diff --git a/http/sys_auth_test.go b/http/sys_auth_test.go deleted file mode 100644 index 03cc8aad2..000000000 --- a/http/sys_auth_test.go +++ /dev/null @@ -1,608 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "fmt" - "reflect" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/hashicorp/vault/helper/versions" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/vault" -) - -func TestSysAuth(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpGet(t, token, addr+"/v1/sys/auth") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "token/": map[string]interface{}{ - "description": "token based credentials", - "type": "token", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "token"), - }, - }, - "token/": map[string]interface{}{ - "description": "token based credentials", - "type": "token", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "token"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) - } -} - -func TestSysEnableAuth(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/auth/foo", map[string]interface{}{ - "type": "approle", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/sys/auth") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "foo/": map[string]interface{}{ - "description": "foo", - "type": "approle", - "external_entropy_access": false, - "deprecation_status": "supported", - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "approle"), - }, - "token/": map[string]interface{}{ - "description": "token based credentials", - "type": "token", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "token_type": "default-service", - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "token"), - }, - }, - "foo/": map[string]interface{}{ - "description": "foo", - "type": "approle", - "external_entropy_access": false, - "deprecation_status": "supported", - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "approle"), - }, - "token/": map[string]interface{}{ - "description": "token based credentials", - "type": "token", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "token"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestSysDisableAuth(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/auth/foo", map[string]interface{}{ - "type": "noop", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpDelete(t, token, addr+"/v1/sys/auth/foo") - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/sys/auth") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "token/": map[string]interface{}{ - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "description": "token based credentials", - "type": "token", - "external_entropy_access": false, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "token"), - }, - }, - "token/": map[string]interface{}{ - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "description": "token based credentials", - "type": "token", - "external_entropy_access": false, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "token"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestSysTuneAuth_nonHMACKeys(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Mount-tune the audit_non_hmac_request_keys - resp := testHttpPost(t, token, addr+"/v1/sys/auth/token/tune", map[string]interface{}{ - "audit_non_hmac_request_keys": "foo", - }) - testResponseStatus(t, resp, 204) - - // Mount-tune the audit_non_hmac_response_keys - resp = testHttpPost(t, token, addr+"/v1/sys/auth/token/tune", map[string]interface{}{ - "audit_non_hmac_response_keys": "bar", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/auth/token/tune") - testResponseStatus(t, resp, 200) - - actual := map[string]interface{}{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "token based credentials", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "audit_non_hmac_request_keys": []interface{}{"foo"}, - "audit_non_hmac_response_keys": []interface{}{"bar"}, - "token_type": "default-service", - }, - "description": "token based credentials", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "audit_non_hmac_request_keys": []interface{}{"foo"}, - "audit_non_hmac_response_keys": []interface{}{"bar"}, - "token_type": "default-service", - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // Unset those mount tune values - resp = testHttpPost(t, token, addr+"/v1/sys/auth/token/tune", map[string]interface{}{ - "audit_non_hmac_request_keys": "", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpPost(t, token, addr+"/v1/sys/auth/token/tune", map[string]interface{}{ - "audit_non_hmac_response_keys": "", - }) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/auth/token/tune") - testResponseStatus(t, resp, 200) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "token based credentials", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "token_type": "default-service", - }, - "description": "token based credentials", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "token_type": "default-service", - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } -} - -func TestSysTuneAuth_showUIMount(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Get original tune values, ensure that listing_visibility is not set - resp := testHttpGet(t, token, addr+"/v1/sys/auth/token/tune") - testResponseStatus(t, resp, 200) - - actual := map[string]interface{}{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "token based credentials", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "token_type": "default-service", - }, - "description": "token based credentials", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "token_type": "default-service", - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // Mount-tune the listing_visibility - resp = testHttpPost(t, token, addr+"/v1/sys/auth/token/tune", map[string]interface{}{ - "listing_visibility": "unauth", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/auth/token/tune") - testResponseStatus(t, resp, 200) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "description": "token based credentials", - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "token based credentials", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "listing_visibility": "unauth", - "token_type": "default-service", - }, - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "listing_visibility": "unauth", - "token_type": "default-service", - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } -} - -func TestSysRemountAuth(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/auth/foo", map[string]interface{}{ - "type": "noop", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpPost(t, token, addr+"/v1/sys/remount", map[string]interface{}{ - "from": "auth/foo", - "to": "auth/bar", - }) - testResponseStatus(t, resp, 200) - - // Poll until the remount succeeds - var remountResp map[string]interface{} - testResponseBody(t, resp, &remountResp) - corehelpers.RetryUntil(t, 5*time.Second, func() error { - resp = testHttpGet(t, token, addr+"/v1/sys/remount/status/"+remountResp["migration_id"].(string)) - testResponseStatus(t, resp, 200) - - var remountStatusResp map[string]interface{} - testResponseBody(t, resp, &remountStatusResp) - - status := remountStatusResp["data"].(map[string]interface{})["migration_info"].(map[string]interface{})["status"] - if status != "success" { - return fmt.Errorf("Expected migration status to be successful, got %q", status) - } - return nil - }) - - resp = testHttpGet(t, token, addr+"/v1/sys/auth") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "bar/": map[string]interface{}{ - "description": "foo", - "type": "noop", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "token/": map[string]interface{}{ - "description": "token based credentials", - "type": "token", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "token_type": "default-service", - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "token"), - }, - }, - "bar/": map[string]interface{}{ - "description": "foo", - "type": "noop", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "token/": map[string]interface{}{ - "description": "token based credentials", - "type": "token", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "token_type": "default-service", - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "token"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } -} diff --git a/http/sys_config_cors_test.go b/http/sys_config_cors_test.go deleted file mode 100644 index 8df6f3194..000000000 --- a/http/sys_config_cors_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "net/http" - "reflect" - "testing" - - "github.com/hashicorp/vault/vault" -) - -func TestSysConfigCors(t *testing.T) { - var resp *http.Response - - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - corsConf := core.CORSConfig() - - // Try to enable CORS without providing a value for allowed_origins - resp = testHttpPut(t, token, addr+"/v1/sys/config/cors", map[string]interface{}{ - "allowed_headers": "X-Custom-Header", - }) - - testResponseStatus(t, resp, 500) - - // Enable CORS, but provide an origin this time. - resp = testHttpPut(t, token, addr+"/v1/sys/config/cors", map[string]interface{}{ - "allowed_origins": addr, - "allowed_headers": "X-Custom-Header", - }) - - testResponseStatus(t, resp, 204) - - // Read the CORS configuration - resp = testHttpGet(t, token, addr+"/v1/sys/config/cors") - testResponseStatus(t, resp, 200) - - var actual map[string]interface{} - var expected map[string]interface{} - - lenStdHeaders := len(corsConf.AllowedHeaders) - - expectedHeaders := make([]interface{}, lenStdHeaders) - - for i := range corsConf.AllowedHeaders { - expectedHeaders[i] = corsConf.AllowedHeaders[i] - } - - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "enabled": true, - "allowed_origins": []interface{}{addr}, - "allowed_headers": expectedHeaders, - }, - "enabled": true, - "allowed_origins": []interface{}{addr}, - "allowed_headers": expectedHeaders, - } - - testResponseStatus(t, resp, 200) - - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual) - } -} diff --git a/http/sys_config_state_test.go b/http/sys_config_state_test.go deleted file mode 100644 index 0d28c9a1c..000000000 --- a/http/sys_config_state_test.go +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "net/http" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/command/server" - "github.com/hashicorp/vault/internalshared/configutil" - "github.com/hashicorp/vault/vault" -) - -func TestSysConfigState_Sanitized(t *testing.T) { - cases := []struct { - name string - storageConfig *server.Storage - haStorageConfig *server.Storage - expectedStorageOutput map[string]interface{} - expectedHAStorageOutput map[string]interface{} - }{ - { - name: "raft storage", - storageConfig: &server.Storage{ - Type: "raft", - RedirectAddr: "http://127.0.0.1:8200", - ClusterAddr: "http://127.0.0.1:8201", - DisableClustering: false, - Config: map[string]string{ - "path": "/storage/path/raft", - "node_id": "raft1", - "max_entry_size": "2097152", - }, - }, - haStorageConfig: nil, - expectedStorageOutput: map[string]interface{}{ - "type": "raft", - "redirect_addr": "http://127.0.0.1:8200", - "cluster_addr": "http://127.0.0.1:8201", - "disable_clustering": false, - "raft": map[string]interface{}{ - "max_entry_size": "2097152", - }, - }, - expectedHAStorageOutput: nil, - }, - { - name: "inmem storage, no HA storage", - storageConfig: &server.Storage{ - Type: "inmem", - RedirectAddr: "http://127.0.0.1:8200", - ClusterAddr: "http://127.0.0.1:8201", - DisableClustering: false, - }, - haStorageConfig: nil, - expectedStorageOutput: map[string]interface{}{ - "type": "inmem", - "redirect_addr": "http://127.0.0.1:8200", - "cluster_addr": "http://127.0.0.1:8201", - "disable_clustering": false, - }, - expectedHAStorageOutput: nil, - }, - { - name: "inmem storage, raft HA storage", - storageConfig: &server.Storage{ - Type: "inmem", - RedirectAddr: "http://127.0.0.1:8200", - ClusterAddr: "http://127.0.0.1:8201", - DisableClustering: false, - }, - haStorageConfig: &server.Storage{ - Type: "raft", - RedirectAddr: "http://127.0.0.1:8200", - ClusterAddr: "http://127.0.0.1:8201", - DisableClustering: false, - Config: map[string]string{ - "path": "/storage/path/raft", - "node_id": "raft1", - "max_entry_size": "2097152", - }, - }, - expectedStorageOutput: map[string]interface{}{ - "type": "inmem", - "redirect_addr": "http://127.0.0.1:8200", - "cluster_addr": "http://127.0.0.1:8201", - "disable_clustering": false, - }, - expectedHAStorageOutput: map[string]interface{}{ - "type": "raft", - "redirect_addr": "http://127.0.0.1:8200", - "cluster_addr": "http://127.0.0.1:8201", - "disable_clustering": false, - "raft": map[string]interface{}{ - "max_entry_size": "2097152", - }, - }, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - var resp *http.Response - confRaw := &server.Config{ - Storage: tc.storageConfig, - HAStorage: tc.haStorageConfig, - SharedConfig: &configutil.SharedConfig{ - Listeners: []*configutil.Listener{ - { - Type: "tcp", - Address: "127.0.0.1", - }, - }, - }, - } - - conf := &vault.CoreConfig{ - RawConfig: confRaw, - } - - core, _, token := vault.TestCoreUnsealedWithConfig(t, conf) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp = testHttpGet(t, token, addr+"/v1/sys/config/state/sanitized") - testResponseStatus(t, resp, 200) - - var actual map[string]interface{} - var expected map[string]interface{} - - configResp := map[string]interface{}{ - "api_addr": "", - "cache_size": json.Number("0"), - "cluster_addr": "", - "cluster_cipher_suites": "", - "cluster_name": "", - "default_lease_ttl": json.Number("0"), - "default_max_request_duration": json.Number("0"), - "disable_cache": false, - "disable_clustering": false, - "disable_indexing": false, - "disable_mlock": false, - "disable_performance_standby": false, - "disable_printable_check": false, - "disable_sealwrap": false, - "experiments": nil, - "raw_storage_endpoint": false, - "detect_deadlocks": "", - "introspection_endpoint": false, - "disable_sentinel_trace": false, - "enable_ui": false, - "log_format": "", - "log_level": "", - "max_lease_ttl": json.Number("0"), - "pid_file": "", - "plugin_directory": "", - "plugin_file_uid": json.Number("0"), - "plugin_file_permissions": json.Number("0"), - "enable_response_header_hostname": false, - "enable_response_header_raft_node_id": false, - "log_requests_level": "", - "listeners": []interface{}{ - map[string]interface{}{ - "config": nil, - "type": "tcp", - }, - }, - "storage": tc.expectedStorageOutput, - "administrative_namespace_path": "", - "imprecise_lease_role_tracking": false, - } - - if tc.expectedHAStorageOutput != nil { - configResp["ha_storage"] = tc.expectedHAStorageOutput - } - - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": configResp, - } - - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - - if diff := deep.Equal(actual, expected); len(diff) > 0 { - t.Fatalf("bad mismatch response body: diff: %v", diff) - } - }) - } -} diff --git a/http/sys_generate_root_test.go b/http/sys_generate_root_test.go deleted file mode 100644 index 8358d18a5..000000000 --- a/http/sys_generate_root_test.go +++ /dev/null @@ -1,477 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "context" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "net" - "net/http" - "reflect" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/audit" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/hashicorp/vault/sdk/helper/xor" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -var tokenLength string = fmt.Sprintf("%d", vault.TokenLength+vault.TokenPrefixLength) - -func TestSysGenerateRootAttempt_Status(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp, err := http.Get(addr + "/v1/sys/generate-root/attempt") - if err != nil { - t.Fatalf("err: %s", err) - } - - var actual map[string]interface{} - expected := map[string]interface{}{ - "started": false, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "nonce": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["otp"] = actual["otp"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestSysGenerateRootAttempt_Setup_OTP(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", nil) - testResponseStatus(t, resp, 200) - - var actual map[string]interface{} - expected := map[string]interface{}{ - "started": true, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - expected["nonce"] = actual["nonce"] - expected["otp"] = actual["otp"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } - - resp = testHttpGet(t, token, addr+"/v1/sys/generate-root/attempt") - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "started": true, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "otp": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - expected["nonce"] = actual["nonce"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } -} - -func TestSysGenerateRootAttempt_Setup_PGP(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", map[string]interface{}{ - "pgp_key": pgpkeys.TestPubKey1, - }) - testResponseStatus(t, resp, 200) - - resp = testHttpGet(t, token, addr+"/v1/sys/generate-root/attempt") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "started": true, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "816938b8a29146fbe245dd29e7cbaf8e011db793", - "otp": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - expected["nonce"] = actual["nonce"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestSysGenerateRootAttempt_Cancel(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", nil) - - var actual map[string]interface{} - expected := map[string]interface{}{ - "started": true, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - expected["nonce"] = actual["nonce"] - expected["otp"] = actual["otp"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } - - resp = testHttpDelete(t, token, addr+"/v1/sys/generate-root/attempt") - testResponseStatus(t, resp, 204) - - resp, err := http.Get(addr + "/v1/sys/generate-root/attempt") - if err != nil { - t.Fatalf("err: %s", err) - } - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "started": false, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "nonce": "", - "otp": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } -} - -func enableNoopAudit(t *testing.T, token string, core *vault.Core) { - t.Helper() - auditReq := &logical.Request{ - Operation: logical.UpdateOperation, - ClientToken: token, - Path: "sys/audit/noop", - Data: map[string]interface{}{ - "type": "noop", - }, - } - resp, err := core.HandleRequest(namespace.RootContext(context.Background()), auditReq) - if err != nil { - t.Fatal(err) - } - - if resp.IsError() { - t.Fatal(err) - } -} - -func testCoreUnsealedWithAudit(t *testing.T, records **[][]byte) (*vault.Core, [][]byte, string) { - conf := &vault.CoreConfig{ - BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(), - AuditBackends: map[string]audit.Factory{ - "noop": corehelpers.NoopAuditFactory(records), - }, - } - core, keys, token := vault.TestCoreUnsealedWithConfig(t, conf) - return core, keys, token -} - -func testServerWithAudit(t *testing.T, records **[][]byte) (net.Listener, string, string, [][]byte) { - core, keys, token := testCoreUnsealedWithAudit(t, records) - ln, addr := TestServer(t, core) - TestServerAuth(t, addr, token) - enableNoopAudit(t, token, core) - return ln, addr, token, keys -} - -func TestSysGenerateRoot_badKey(t *testing.T) { - var records *[][]byte - ln, addr, token, _ := testServerWithAudit(t, &records) - defer ln.Close() - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/update", map[string]interface{}{ - "key": "0123", - }) - testResponseStatus(t, resp, 400) - - if len(*records) < 3 { - // One record for enabling the noop audit device, two for generate root attempt - t.Fatalf("expected at least 3 audit records, got %d", len(*records)) - } - t.Log(string((*records)[2])) -} - -func TestSysGenerateRoot_ReAttemptUpdate(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", nil) - testResponseStatus(t, resp, 200) - - resp = testHttpDelete(t, token, addr+"/v1/sys/generate-root/attempt") - testResponseStatus(t, resp, 204) - - resp = testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", map[string]interface{}{ - "pgp_key": pgpkeys.TestPubKey1, - }) - - testResponseStatus(t, resp, 200) -} - -func TestSysGenerateRoot_Update_OTP(t *testing.T) { - var records *[][]byte - ln, addr, token, keys := testServerWithAudit(t, &records) - defer ln.Close() - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", map[string]interface{}{}) - var rootGenerationStatus map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &rootGenerationStatus) - otp := rootGenerationStatus["otp"].(string) - - var actual map[string]interface{} - var expected map[string]interface{} - for i, key := range keys { - resp = testHttpPut(t, token, addr+"/v1/sys/generate-root/update", map[string]interface{}{ - "nonce": rootGenerationStatus["nonce"].(string), - "key": hex.EncodeToString(key), - }) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "complete": false, - "nonce": rootGenerationStatus["nonce"].(string), - "progress": json.Number(fmt.Sprintf("%d", i+1)), - "required": json.Number(fmt.Sprintf("%d", len(keys))), - "started": true, - "pgp_fingerprint": "", - "otp": "", - "otp_length": json.Number("0"), - } - if i+1 == len(keys) { - expected["complete"] = true - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - } - - if actual["encoded_token"] == nil || actual["encoded_token"] == "" { - t.Fatalf("no encoded token found in response") - } - if actual["encoded_root_token"] == nil || actual["encoded_root-token"] == "" { - t.Fatalf("no encoded root token found in response") - } - expected["encoded_token"] = actual["encoded_token"] - expected["encoded_root_token"] = actual["encoded_root_token"] - expected["encoded_token"] = actual["encoded_token"] - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } - - tokenBytes, err := base64.RawStdEncoding.DecodeString(expected["encoded_token"].(string)) - if err != nil { - t.Fatal(err) - } - - tokenBytes, err = xor.XORBytes(tokenBytes, []byte(otp)) - if err != nil { - t.Fatal(err) - } - newRootToken := string(tokenBytes) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "id": newRootToken, - "display_name": "root", - "meta": interface{}(nil), - "num_uses": json.Number("0"), - "policies": []interface{}{"root"}, - "orphan": true, - "creation_ttl": json.Number("0"), - "ttl": json.Number("0"), - "path": "auth/token/root", - "explicit_max_ttl": json.Number("0"), - "expire_time": nil, - "entity_id": "", - "type": "service", - } - - resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"] - expected["accessor"] = actual["data"].(map[string]interface{})["accessor"] - - if !reflect.DeepEqual(actual["data"], expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"]) - } - - for _, r := range *records { - t.Log(string(r)) - } -} - -func TestSysGenerateRoot_Update_PGP(t *testing.T) { - core, keys, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", map[string]interface{}{ - "pgp_key": pgpkeys.TestPubKey1, - }) - testResponseStatus(t, resp, 200) - - // We need to get the nonce first before we update - resp, err := http.Get(addr + "/v1/sys/generate-root/attempt") - if err != nil { - t.Fatalf("err: %s", err) - } - var rootGenerationStatus map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &rootGenerationStatus) - - var actual map[string]interface{} - var expected map[string]interface{} - for i, key := range keys { - resp = testHttpPut(t, token, addr+"/v1/sys/generate-root/update", map[string]interface{}{ - "nonce": rootGenerationStatus["nonce"].(string), - "key": hex.EncodeToString(key), - }) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "complete": false, - "nonce": rootGenerationStatus["nonce"].(string), - "progress": json.Number(fmt.Sprintf("%d", i+1)), - "required": json.Number(fmt.Sprintf("%d", len(keys))), - "started": true, - "pgp_fingerprint": "816938b8a29146fbe245dd29e7cbaf8e011db793", - "otp": "", - "otp_length": json.Number("0"), - } - if i+1 == len(keys) { - expected["complete"] = true - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - } - - if actual["encoded_token"] == nil || actual["encoded_token"] == "" { - t.Fatalf("no encoded token found in response") - } - if actual["encoded_root_token"] == nil || actual["encoded_root-token"] == "" { - t.Fatalf("no encoded root token found in response") - } - expected["encoded_token"] = actual["encoded_token"] - expected["encoded_root_token"] = actual["encoded_root_token"] - expected["encoded_token"] = actual["encoded_token"] - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } - - decodedTokenBuf, err := pgpkeys.DecryptBytes(actual["encoded_token"].(string), pgpkeys.TestPrivKey1) - if err != nil { - t.Fatal(err) - } - if decodedTokenBuf == nil { - t.Fatal("decoded root token buffer is nil") - } - - newRootToken := decodedTokenBuf.String() - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "id": newRootToken, - "display_name": "root", - "meta": interface{}(nil), - "num_uses": json.Number("0"), - "policies": []interface{}{"root"}, - "orphan": true, - "creation_ttl": json.Number("0"), - "ttl": json.Number("0"), - "path": "auth/token/root", - "explicit_max_ttl": json.Number("0"), - "expire_time": nil, - "entity_id": "", - "type": "service", - } - - resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"] - expected["accessor"] = actual["data"].(map[string]interface{})["accessor"] - - if diff := deep.Equal(actual["data"], expected); diff != nil { - t.Fatal(diff) - } -} diff --git a/http/sys_health_test.go b/http/sys_health_test.go deleted file mode 100644 index 48caa6e1c..000000000 --- a/http/sys_health_test.go +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "io/ioutil" - "net/http" - "net/url" - "reflect" - "testing" - - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/vault" -) - -func TestSysHealth_get(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp, err := http.Get(addr + "/v1/sys/health") - if err != nil { - t.Fatalf("err: %s", err) - } - - var actual map[string]interface{} - expected := map[string]interface{}{ - "replication_performance_mode": consts.ReplicationUnknown.GetPerformanceString(), - "replication_dr_mode": consts.ReplicationUnknown.GetDRString(), - "initialized": false, - "sealed": true, - "standby": true, - "performance_standby": false, - } - testResponseStatus(t, resp, 501) - testResponseBody(t, resp, &actual) - expected["server_time_utc"] = actual["server_time_utc"] - expected["version"] = actual["version"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - delete(actual, "license") - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) - } - - keys, _ := vault.TestCoreInit(t, core) - resp, err = http.Get(addr + "/v1/sys/health") - if err != nil { - t.Fatalf("err: %s", err) - } - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "replication_performance_mode": consts.ReplicationUnknown.GetPerformanceString(), - "replication_dr_mode": consts.ReplicationUnknown.GetDRString(), - "initialized": true, - "sealed": true, - "standby": true, - "performance_standby": false, - } - testResponseStatus(t, resp, 503) - testResponseBody(t, resp, &actual) - expected["server_time_utc"] = actual["server_time_utc"] - expected["version"] = actual["version"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - delete(actual, "license") - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) - } - - for _, key := range keys { - if _, err := vault.TestCoreUnseal(core, vault.TestKeyCopy(key)); err != nil { - t.Fatalf("unseal err: %s", err) - } - } - resp, err = http.Get(addr + "/v1/sys/health") - if err != nil { - t.Fatalf("err: %s", err) - } - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "replication_performance_mode": consts.ReplicationPerformanceDisabled.GetPerformanceString(), - "replication_dr_mode": consts.ReplicationDRDisabled.GetDRString(), - "initialized": true, - "sealed": false, - "standby": false, - "performance_standby": false, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["server_time_utc"] = actual["server_time_utc"] - expected["version"] = actual["version"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - delete(actual, "license") - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) - } -} - -func TestSysHealth_customcodes(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - queryurl, err := url.Parse(addr + "/v1/sys/health?uninitcode=581&sealedcode=523&activecode=202") - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := http.Get(queryurl.String()) - if err != nil { - t.Fatalf("err: %s", err) - } - - var actual map[string]interface{} - expected := map[string]interface{}{ - "replication_performance_mode": consts.ReplicationUnknown.GetPerformanceString(), - "replication_dr_mode": consts.ReplicationUnknown.GetDRString(), - "initialized": false, - "sealed": true, - "standby": true, - "performance_standby": false, - } - testResponseStatus(t, resp, 581) - testResponseBody(t, resp, &actual) - - expected["server_time_utc"] = actual["server_time_utc"] - expected["version"] = actual["version"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - delete(actual, "license") - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) - } - - keys, _ := vault.TestCoreInit(t, core) - resp, err = http.Get(queryurl.String()) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "replication_performance_mode": consts.ReplicationUnknown.GetPerformanceString(), - "replication_dr_mode": consts.ReplicationUnknown.GetDRString(), - "initialized": true, - "sealed": true, - "standby": true, - "performance_standby": false, - } - testResponseStatus(t, resp, 523) - testResponseBody(t, resp, &actual) - - expected["server_time_utc"] = actual["server_time_utc"] - expected["version"] = actual["version"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - delete(actual, "license") - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) - } - - for _, key := range keys { - if _, err := vault.TestCoreUnseal(core, vault.TestKeyCopy(key)); err != nil { - t.Fatalf("unseal err: %s", err) - } - } - resp, err = http.Get(queryurl.String()) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "replication_performance_mode": consts.ReplicationPerformanceDisabled.GetPerformanceString(), - "replication_dr_mode": consts.ReplicationDRDisabled.GetDRString(), - "initialized": true, - "sealed": false, - "standby": false, - "performance_standby": false, - } - testResponseStatus(t, resp, 202) - testResponseBody(t, resp, &actual) - expected["server_time_utc"] = actual["server_time_utc"] - expected["version"] = actual["version"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - delete(actual, "license") - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) - } -} - -func TestSysHealth_head(t *testing.T) { - core, _, _ := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - testData := []struct { - uri string - code int - }{ - {"", 200}, - {"?activecode=503", 503}, - {"?activecode=notacode", 400}, - } - - for _, tt := range testData { - queryurl, err := url.Parse(addr + "/v1/sys/health" + tt.uri) - if err != nil { - t.Fatalf("err on %v: %s", queryurl, err) - } - resp, err := http.Head(queryurl.String()) - if err != nil { - t.Fatalf("err on %v: %s", queryurl, err) - } - - if resp.StatusCode != tt.code { - t.Fatalf("HEAD %v expected code %d, got %d.", queryurl, tt.code, resp.StatusCode) - } - - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf("err on %v: %s", queryurl, err) - } - if len(data) > 0 { - t.Fatalf("HEAD %v expected no body, received \"%v\".", queryurl, data) - } - } -} diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go deleted file mode 100644 index 2df641ea8..000000000 --- a/http/sys_hostinfo_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "testing" - - "github.com/hashicorp/vault/helper/hostutil" - "github.com/hashicorp/vault/vault" -) - -func TestSysHostInfo(t *testing.T) { - cluster := vault.NewTestCluster(t, &vault.CoreConfig{}, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - cores := cluster.Cores - - vault.TestWaitActive(t, cores[0].Core) - - // Query against the active node, should get host information back - secret, err := cores[0].Client.Logical().Read("sys/host-info") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("expected data in the response") - } - - dataBytes, err := json.Marshal(secret.Data) - if err != nil { - t.Fatal(err) - } - - var info hostutil.HostInfo - if err := json.Unmarshal(dataBytes, &info); err != nil { - t.Fatal(err) - } - - if info.Timestamp.IsZero() { - t.Fatal("expected non-zero Timestamp") - } - if info.CPU == nil { - t.Fatal("expected non-nil CPU value") - } - if info.Disk == nil { - t.Fatal("expected disk info") - } - if info.Host == nil { - t.Fatal("expected host info") - } - if info.Memory == nil { - t.Fatal("expected memory info") - } - - // Query against a standby, should error - secret, err = cores[1].Client.Logical().Read("sys/host-info") - if err == nil || secret != nil { - t.Fatalf("expected error on standby node, HostInfo: %v", secret) - } -} diff --git a/http/sys_in_flight_requests_test.go b/http/sys_in_flight_requests_test.go deleted file mode 100644 index 880a9ad61..000000000 --- a/http/sys_in_flight_requests_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "testing" - - "github.com/hashicorp/vault/internalshared/configutil" - "github.com/hashicorp/vault/vault" -) - -func TestInFlightRequestUnauthenticated(t *testing.T) { - conf := &vault.CoreConfig{} - core, _, token := vault.TestCoreUnsealedWithConfig(t, conf) - ln, addr := TestServer(t, core) - TestServerAuth(t, addr, token) - - // Default: Only authenticated access - resp := testHttpGet(t, "", addr+"/v1/sys/in-flight-req") - testResponseStatus(t, resp, 403) - resp = testHttpGet(t, token, addr+"/v1/sys/in-flight-req") - testResponseStatus(t, resp, 200) - - // Close listener - ln.Close() - - // Setup new custom listener with unauthenticated metrics access - ln, addr = TestListener(t) - props := &vault.HandlerProperties{ - Core: core, - ListenerConfig: &configutil.Listener{ - InFlightRequestLogging: configutil.ListenerInFlightRequestLogging{ - UnauthenticatedInFlightAccess: true, - }, - }, - } - TestServerWithListenerAndProperties(t, ln, addr, core, props) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Test without token - resp = testHttpGet(t, "", addr+"/v1/sys/in-flight-req") - testResponseStatus(t, resp, 200) - - // Should also work with token - resp = testHttpGet(t, token, addr+"/v1/sys/in-flight-req") - testResponseStatus(t, resp, 200) -} diff --git a/http/sys_init_test.go b/http/sys_init_test.go deleted file mode 100644 index e6cd8825d..000000000 --- a/http/sys_init_test.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/hex" - "net/http" - "reflect" - "strconv" - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/builtin/logical/transit" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/hashicorp/vault/vault/seal" -) - -func TestSysInit_get(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - { - // Pre-init - resp, err := http.Get(addr + "/v1/sys/init") - if err != nil { - t.Fatalf("err: %s", err) - } - - var actual map[string]interface{} - expected := map[string]interface{}{ - "initialized": false, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } - } - - vault.TestCoreInit(t, core) - - { - // Post-init - resp, err := http.Get(addr + "/v1/sys/init") - if err != nil { - t.Fatalf("err: %s", err) - } - - var actual map[string]interface{} - expected := map[string]interface{}{ - "initialized": true, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } - } -} - -// Test to check if the API errors out when wrong number of PGP keys are -// supplied -func TestSysInit_pgpKeysEntries(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp := testHttpPut(t, "", addr+"/v1/sys/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - "pgp_keys": []string{"pgpkey1"}, - }) - testResponseStatus(t, resp, 400) -} - -// Test to check if the API errors out when wrong number of PGP keys are -// supplied for recovery config -func TestSysInit_pgpKeysEntriesForRecovery(t *testing.T) { - core := vault.TestCoreNewSeal(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp := testHttpPut(t, "", addr+"/v1/sys/init", map[string]interface{}{ - "secret_shares": 1, - "secret_threshold": 1, - "stored_shares": 1, - "recovery_shares": 5, - "recovery_threshold": 3, - "recovery_pgp_keys": []string{"pgpkey1"}, - }) - testResponseStatus(t, resp, 400) -} - -func TestSysInit_put(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp := testHttpPut(t, "", addr+"/v1/sys/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - }) - - var actual map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - keysRaw, ok := actual["keys"] - if !ok { - t.Fatalf("no keys: %#v", actual) - } - - if _, ok := actual["root_token"]; !ok { - t.Fatal("no root token") - } - - for _, key := range keysRaw.([]interface{}) { - keySlice, err := hex.DecodeString(key.(string)) - if err != nil { - t.Fatalf("bad: %s", err) - } - - if _, err := core.Unseal(keySlice); err != nil { - t.Fatalf("bad: %s", err) - } - } - - if core.Sealed() { - t.Fatal("should not be sealed") - } -} - -func TestSysInit_Put_ValidateParams(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp := testHttpPut(t, "", addr+"/v1/sys/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - "recovery_shares": 5, - "recovery_threshold": 3, - }) - testResponseStatus(t, resp, http.StatusBadRequest) - body := map[string][]string{} - testResponseBody(t, resp, &body) - if body["errors"][0] != "parameters recovery_shares,recovery_threshold not applicable to seal type shamir" { - t.Fatal(body) - } -} - -func TestSysInit_Put_ValidateParams_AutoUnseal(t *testing.T) { - testSeal, _ := seal.NewTestSeal(&seal.TestSealOpts{Name: "transit"}) - autoSeal, err := vault.NewAutoSeal(testSeal) - if err != nil { - t.Fatal(err) - } - - // Create the transit server. - conf := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "transit": transit.Factory, - }, - Seal: autoSeal, - } - opts := &vault.TestClusterOptions{ - NumCores: 1, - HandlerFunc: Handler, - Logger: logging.NewVaultLogger(hclog.Trace).Named(t.Name()).Named("transit-seal" + strconv.Itoa(0)), - } - cluster := vault.NewTestCluster(t, conf, opts) - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - core := cores[0].Core - - ln, addr := TestServer(t, core) - defer ln.Close() - - resp := testHttpPut(t, "", addr+"/v1/sys/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - "recovery_shares": 5, - "recovery_threshold": 3, - }) - testResponseStatus(t, resp, http.StatusBadRequest) - body := map[string][]string{} - testResponseBody(t, resp, &body) - if body["errors"][0] != "parameters secret_shares,secret_threshold not applicable to seal type transit" { - t.Fatal(body) - } -} diff --git a/http/sys_internal_test.go b/http/sys_internal_test.go deleted file mode 100644 index 0303a8c7f..000000000 --- a/http/sys_internal_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/hashicorp/vault/vault" -) - -func TestSysInternal_UIMounts(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Get original tune values, ensure that listing_visibility is not set - resp := testHttpGet(t, "", addr+"/v1/sys/internal/ui/mounts") - testResponseStatus(t, resp, 200) - - actual := map[string]interface{}{} - expected := map[string]interface{}{ - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "data": map[string]interface{}{ - "auth": map[string]interface{}{}, - "secret": map[string]interface{}{}, - }, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // Mount-tune the listing_visibility - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "listing_visibility": "unauth", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpPost(t, token, addr+"/v1/sys/auth/token/tune", map[string]interface{}{ - "listing_visibility": "unauth", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, "", addr+"/v1/sys/internal/ui/mounts") - testResponseStatus(t, resp, 200) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "data": map[string]interface{}{ - "secret": map[string]interface{}{ - "secret/": map[string]interface{}{ - "type": "kv", - "description": "key/value secret storage", - "options": map[string]interface{}{"version": "1"}, - }, - }, - "auth": map[string]interface{}{ - "token/": map[string]interface{}{ - "type": "token", - "description": "token based credentials", - "options": interface{}(nil), - }, - }, - }, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } -} diff --git a/http/sys_leader_test.go b/http/sys_leader_test.go deleted file mode 100644 index e495e1187..000000000 --- a/http/sys_leader_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "net/http" - "reflect" - "testing" - "time" - - "github.com/hashicorp/vault/vault" -) - -func TestSysLeader_get(t *testing.T) { - core, _, _ := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp, err := http.Get(addr + "/v1/sys/leader") - if err != nil { - t.Fatalf("err: %s", err) - } - - var actual map[string]interface{} - expected := map[string]interface{}{ - "ha_enabled": false, - "is_self": false, - "leader_address": "", - "leader_cluster_address": "", - "performance_standby": false, - "performance_standby_last_remote_wal": json.Number("0"), - "active_time": time.Time{}.UTC().Format(time.RFC3339), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v \n%#v", actual, expected) - } -} diff --git a/http/sys_lease_test.go b/http/sys_lease_test.go deleted file mode 100644 index 9db9cb0d0..000000000 --- a/http/sys_lease_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "testing" - - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/vault" -) - -func TestSysRenew(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // write secret - resp := testHttpPut(t, token, addr+"/v1/secret/foo", map[string]interface{}{ - "data": "bar", - "lease": "1h", - }) - testResponseStatus(t, resp, 204) - - // read secret - resp = testHttpGet(t, token, addr+"/v1/secret/foo") - var result struct { - LeaseID string `json:"lease_id"` - } - if err := jsonutil.DecodeJSONFromReader(resp.Body, &result); err != nil { - t.Fatalf("bad: %s", err) - } - - var renewResult struct { - LeaseID string `json:"lease_id"` - Data map[string]interface{} `json:"data"` - } - resp = testHttpPut(t, token, addr+"/v1/sys/renew/"+result.LeaseID, nil) - testResponseStatus(t, resp, 200) - if err := jsonutil.DecodeJSONFromReader(resp.Body, &renewResult); err != nil { - t.Fatal(err) - } - if result.LeaseID != renewResult.LeaseID { - t.Fatal("lease id changed in renew request") - } - - resp = testHttpPut(t, token, addr+"/v1/sys/leases/renew/"+result.LeaseID, nil) - testResponseStatus(t, resp, 200) - if err := jsonutil.DecodeJSONFromReader(resp.Body, &renewResult); err != nil { - t.Fatal(err) - } - if result.LeaseID != renewResult.LeaseID { - t.Fatal("lease id changed in renew request") - } -} - -func TestSysRevoke(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/revoke/secret/foo/1234", nil) - testResponseStatus(t, resp, 204) -} - -func TestSysRevokePrefix(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/revoke-prefix/secret/foo/1234", nil) - testResponseStatus(t, resp, 204) -} diff --git a/http/sys_metrics_test.go b/http/sys_metrics_test.go deleted file mode 100644 index 1198815a5..000000000 --- a/http/sys_metrics_test.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "testing" - "time" - - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - - "github.com/armon/go-metrics" - "github.com/hashicorp/vault/helper/metricsutil" - "github.com/hashicorp/vault/internalshared/configutil" - "github.com/hashicorp/vault/vault" -) - -func TestSysMetricsUnauthenticated(t *testing.T) { - inm := metrics.NewInmemSink(10*time.Second, time.Minute) - metrics.DefaultInmemSignal(inm) - conf := &vault.CoreConfig{ - BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(), - MetricsHelper: metricsutil.NewMetricsHelper(inm, true), - } - core, _, token := vault.TestCoreUnsealedWithConfig(t, conf) - ln, addr := TestServer(t, core) - TestServerAuth(t, addr, token) - - // Default: Only authenticated access - resp := testHttpGet(t, "", addr+"/v1/sys/metrics") - testResponseStatus(t, resp, 403) - resp = testHttpGet(t, token, addr+"/v1/sys/metrics") - testResponseStatus(t, resp, 200) - - // Close listener - ln.Close() - - // Setup new custom listener with unauthenticated metrics access - ln, addr = TestListener(t) - props := &vault.HandlerProperties{ - Core: core, - ListenerConfig: &configutil.Listener{ - Telemetry: configutil.ListenerTelemetry{ - UnauthenticatedMetricsAccess: true, - }, - }, - } - TestServerWithListenerAndProperties(t, ln, addr, core, props) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Test without token - resp = testHttpGet(t, "", addr+"/v1/sys/metrics") - testResponseStatus(t, resp, 200) - - // Should also work with token - resp = testHttpGet(t, token, addr+"/v1/sys/metrics") - testResponseStatus(t, resp, 200) - - // Test if prometheus response is correct - resp = testHttpGet(t, "", addr+"/v1/sys/metrics?format=prometheus") - testResponseStatus(t, resp, 200) -} - -func TestSysPProfUnauthenticated(t *testing.T) { - conf := &vault.CoreConfig{} - core, _, token := vault.TestCoreUnsealedWithConfig(t, conf) - ln, addr := TestServer(t, core) - TestServerAuth(t, addr, token) - - // Default: Only authenticated access - resp := testHttpGet(t, "", addr+"/v1/sys/pprof/cmdline") - testResponseStatus(t, resp, 403) - resp = testHttpGet(t, token, addr+"/v1/sys/pprof/cmdline") - testResponseStatus(t, resp, 200) - - // Close listener - ln.Close() - - // Setup new custom listener with unauthenticated metrics access - ln, addr = TestListener(t) - props := &vault.HandlerProperties{ - Core: core, - ListenerConfig: &configutil.Listener{ - Profiling: configutil.ListenerProfiling{ - UnauthenticatedPProfAccess: true, - }, - }, - } - TestServerWithListenerAndProperties(t, ln, addr, core, props) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Test without token - resp = testHttpGet(t, "", addr+"/v1/sys/pprof/cmdline") - testResponseStatus(t, resp, 200) - - // Should also work with token - resp = testHttpGet(t, token, addr+"/v1/sys/pprof/cmdline") - testResponseStatus(t, resp, 200) -} diff --git a/http/sys_monitor_test.go b/http/sys_monitor_test.go deleted file mode 100644 index 5e6b49d0d..000000000 --- a/http/sys_monitor_test.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "context" - "encoding/json" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/helper/testhelpers" - "github.com/hashicorp/vault/vault" -) - -func TestSysMonitorUnknownLogLevel(t *testing.T) { - t.Parallel() - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - NumCores: 1, - }) - defer cluster.Cleanup() - - client := cluster.Cores[0].Client - request := client.NewRequest("GET", "/v1/sys/monitor") - request.Params.Add("log_level", "haha") - _, err := client.RawRequest(request) - - if err == nil { - t.Fatal("expected to get an error, but didn't") - } else { - if !strings.Contains(err.Error(), "Code: 400") { - t.Fatalf("expected to receive a 400 error, but got %s instead", err) - } - - if !strings.Contains(err.Error(), "unknown log level") { - t.Fatalf("expected to receive a message indicating an unknown log level, but got %s instead", err) - } - } -} - -func TestSysMonitorUnknownLogFormat(t *testing.T) { - t.Parallel() - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - NumCores: 1, - }) - defer cluster.Cleanup() - - client := cluster.Cores[0].Client - request := client.NewRequest("GET", "/v1/sys/monitor") - request.Params.Add("log_format", "haha") - _, err := client.RawRequest(request) - - if err == nil { - t.Fatal("expected to get an error, but didn't") - } else { - if !strings.Contains(err.Error(), "Code: 400") { - t.Fatalf("expected to receive a 400 error, but got %s instead", err) - } - - if !strings.Contains(err.Error(), "unknown log format") { - t.Fatalf("expected to receive a message indicating an unknown log format, but got %s instead", err) - } - } -} - -func TestSysMonitorStreamingLogs(t *testing.T) { - t.Parallel() - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - NumCores: 1, - }) - defer cluster.Cleanup() - - client := cluster.Cores[0].Client - stopCh := testhelpers.GenerateDebugLogs(t, client) - defer close(stopCh) - - for _, lf := range []string{"standard", "json"} { - t.Run(lf, func(t *testing.T) { - debugCount := 0 - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - logCh, err := client.Sys().Monitor(ctx, "DEBUG", lf) - if err != nil { - t.Fatal(err) - } - - type jsonlog struct { - Level string `json:"@level"` - Message string `json:"@message"` - TimeStamp string `json:"@timestamp"` - } - jsonLog := &jsonlog{} - - timeCh := time.After(5 * time.Second) - - for { - select { - case log := <-logCh: - if lf == "json" { - err := json.Unmarshal([]byte(log), jsonLog) - if err != nil { - t.Fatal("Expected JSON log from channel") - } - if strings.Contains(jsonLog.Level, "debug") { - debugCount++ - } - } else if strings.Contains(log, "[DEBUG]") { - debugCount++ - } - if debugCount > 3 { - // If we've seen multiple lines that match what we want, - // it's probably safe to assume streaming is working - return - } - case <-timeCh: - t.Fatal("Failed to get a DEBUG message after 5 seconds") - } - } - }) - } -} diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go deleted file mode 100644 index 33da48d80..000000000 --- a/http/sys_mount_test.go +++ /dev/null @@ -1,1891 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "fmt" - "reflect" - "testing" - "time" - - "github.com/fatih/structs" - "github.com/go-test/deep" - "github.com/hashicorp/vault/helper/testhelpers/corehelpers" - "github.com/hashicorp/vault/helper/versions" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/vault" -) - -func TestSysMounts(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpGet(t, token, addr+"/v1/sys/mounts") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if diff := deep.Equal(actual, expected); len(diff) > 0 { - t.Fatalf("bad, diff: %#v", diff) - } -} - -func TestSysMount(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/foo", map[string]interface{}{ - "type": "kv", - "description": "foo", - "options": map[string]string{ - "version": "1", - }, - }) - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/sys/mounts") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "foo/": map[string]interface{}{ - "description": "foo", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - }, - "foo/": map[string]interface{}{ - "description": "foo", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if diff := deep.Equal(actual, expected); len(diff) > 0 { - t.Fatalf("bad, diff: %#v", diff) - } -} - -func TestSysMount_put(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/mounts/foo", map[string]interface{}{ - "type": "kv", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - // The TestSysMount test tests the thing is actually created. See that test - // for more info. -} - -// TestSysRemountSpacesFrom ensure we succeed in a remount where the 'from' mount has spaces in the name -func TestSysRemountSpacesFrom(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/foo%20bar", map[string]interface{}{ - "type": "kv", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpPost(t, token, addr+"/v1/sys/remount", map[string]interface{}{ - "from": "foo bar", - "to": "baz", - }) - testResponseStatus(t, resp, 200) -} - -// TestSysRemountSpacesTo ensure we succeed in a remount where the 'to' mount has spaces in the name -func TestSysRemountSpacesTo(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/foo%20bar", map[string]interface{}{ - "type": "kv", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpPost(t, token, addr+"/v1/sys/remount", map[string]interface{}{ - "from": "foo bar", - "to": "bar baz", - }) - testResponseStatus(t, resp, 200) -} - -// TestSysRemountTrailingSpaces ensures we fail on trailing spaces -func TestSysRemountTrailingSpaces(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/foo%20bar", map[string]interface{}{ - "type": "kv", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpPost(t, token, addr+"/v1/sys/remount", map[string]interface{}{ - "from": "foo bar", - "to": " baz ", - }) - testResponseStatus(t, resp, 400) - - resp = testHttpPost(t, token, addr+"/v1/sys/remount", map[string]interface{}{ - "from": " foo bar ", - "to": "baz", - }) - testResponseStatus(t, resp, 400) -} - -func TestSysRemount(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/foo", map[string]interface{}{ - "type": "kv", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpPost(t, token, addr+"/v1/sys/remount", map[string]interface{}{ - "from": "foo", - "to": "bar", - }) - testResponseStatus(t, resp, 200) - - // Poll until the remount succeeds - var remountResp map[string]interface{} - testResponseBody(t, resp, &remountResp) - corehelpers.RetryUntil(t, 5*time.Second, func() error { - resp = testHttpGet(t, token, addr+"/v1/sys/remount/status/"+remountResp["migration_id"].(string)) - testResponseStatus(t, resp, 200) - - var remountStatusResp map[string]interface{} - testResponseBody(t, resp, &remountStatusResp) - - status := remountStatusResp["data"].(map[string]interface{})["migration_info"].(map[string]interface{})["status"] - if status != "success" { - return fmt.Errorf("Expected migration status to be successful, got %q", status) - } - return nil - }) - resp = testHttpGet(t, token, addr+"/v1/sys/mounts") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "bar/": map[string]interface{}{ - "description": "foo", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - }, - "bar/": map[string]interface{}{ - "description": "foo", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual: %#v\n", expected, actual) - } -} - -func TestSysUnmount(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/foo", map[string]interface{}{ - "type": "kv", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpDelete(t, token, addr+"/v1/sys/mounts/foo") - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/sys/mounts") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if diff := deep.Equal(actual, expected); len(diff) > 0 { - t.Fatalf("bad, diff: %#v", diff) - } -} - -func TestSysTuneMount_Options(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/foo", map[string]interface{}{ - "type": "kv", - "description": "foo", - }) - - testResponseStatus(t, resp, 204) - // Mount-tune the options - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/foo/tune", map[string]interface{}{ - "options": map[string]string{ - "test": "true", - }, - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/foo/tune") - testResponseStatus(t, resp, 200) - - actual := map[string]interface{}{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "foo", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"test": "true"}, - }, - "description": "foo", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"test": "true"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // Check that we're not allowed to unset the options map once that's set - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/foo/tune", map[string]interface{}{ - "options": map[string]string{}, - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/foo/tune") - testResponseStatus(t, resp, 200) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "description": "foo", - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "foo", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"test": "true"}, - }, - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"test": "true"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } -} - -func TestSysTuneMount(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/foo", map[string]interface{}{ - "type": "kv", - "description": "foo", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/sys/mounts") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "foo/": map[string]interface{}{ - "description": "foo", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - }, - "foo/": map[string]interface{}{ - "description": "foo", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if diff := deep.Equal(actual, expected); len(diff) > 0 { - t.Fatalf("bad, diff: %#v", diff) - } - - // Shorter than system default - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/foo/tune", map[string]interface{}{ - "default_lease_ttl": "72h", - }) - testResponseStatus(t, resp, 204) - - // Longer than system max - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/foo/tune", map[string]interface{}{ - "default_lease_ttl": "72000h", - }) - testResponseStatus(t, resp, 204) - - // Longer than system default - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/foo/tune", map[string]interface{}{ - "max_lease_ttl": "72000h", - }) - testResponseStatus(t, resp, 204) - - // Longer than backend max - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/foo/tune", map[string]interface{}{ - "default_lease_ttl": "72001h", - }) - testResponseStatus(t, resp, 400) - - // Shorter than backend default - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/foo/tune", map[string]interface{}{ - "max_lease_ttl": "1h", - }) - testResponseStatus(t, resp, 400) - - // Shorter than backend max, longer than system max - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/foo/tune", map[string]interface{}{ - "default_lease_ttl": "71999h", - }) - testResponseStatus(t, resp, 204) - - // mark as versioned - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/foo/tune", map[string]interface{}{ - "options": map[string]string{ - "version": "1", - }, - }) - testResponseStatus(t, resp, 200) - - resp = testHttpGet(t, token, addr+"/v1/sys/mounts") - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "foo/": map[string]interface{}{ - "description": "foo", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("259196400"), - "max_lease_ttl": json.Number("259200000"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.DefaultBuiltinVersion, - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - }, - "foo/": map[string]interface{}{ - "description": "foo", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("259196400"), - "max_lease_ttl": json.Number("259200000"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "secret/": map[string]interface{}{ - "description": "key/value secret storage", - "type": "kv", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "sys/": map[string]interface{}{ - "description": "system endpoints used for control, policy and debugging", - "type": "system", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Accept"}, - }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), - }, - "cubbyhole/": map[string]interface{}{ - "description": "per-token private secret storage", - "type": "cubbyhole", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), - }, - "identity/": map[string]interface{}{ - "description": "identity store", - "type": "identity", - "external_entropy_access": false, - "config": map[string]interface{}{ - "default_lease_ttl": json.Number("0"), - "max_lease_ttl": json.Number("0"), - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"Authorization"}, - }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), - "plugin_version": "", - "running_sha256": "", - "running_plugin_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), - }, - } - - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - for k, v := range actual["data"].(map[string]interface{}) { - if v.(map[string]interface{})["accessor"] == "" { - t.Fatalf("no accessor from %s", k) - } - if v.(map[string]interface{})["uuid"] == "" { - t.Fatalf("no uuid from %s", k) - } - expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] - expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"] - } - - if diff := deep.Equal(actual, expected); len(diff) > 0 { - t.Fatalf("bad, diff: %#v", diff) - } - - // Check simple configuration endpoint - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/foo/tune") - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "foo", - "default_lease_ttl": json.Number("259196400"), - "max_lease_ttl": json.Number("259200000"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - }, - "description": "foo", - "default_lease_ttl": json.Number("259196400"), - "max_lease_ttl": json.Number("259200000"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - } - - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // Set a low max - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "description": "foobar", - "default_lease_ttl": "40s", - "max_lease_ttl": "80s", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "foobar", - "default_lease_ttl": json.Number("40"), - "max_lease_ttl": json.Number("80"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - }, - "description": "foobar", - "default_lease_ttl": json.Number("40"), - "max_lease_ttl": json.Number("80"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - } - - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // First try with lease above backend max - resp = testHttpPut(t, token, addr+"/v1/secret/foo", map[string]interface{}{ - "data": "bar", - "ttl": "28347h", - }) - testResponseStatus(t, resp, 204) - - // read secret - resp = testHttpGet(t, token, addr+"/v1/secret/foo") - var result struct { - LeaseID string `json:"lease_id" structs:"lease_id"` - LeaseDuration int `json:"lease_duration" structs:"lease_duration"` - } - - testResponseBody(t, resp, &result) - - expected = map[string]interface{}{ - "lease_duration": int(80), - "lease_id": result.LeaseID, - } - - if !reflect.DeepEqual(structs.Map(result), expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, structs.Map(result)) - } - - // Now with lease TTL unspecified - resp = testHttpPut(t, token, addr+"/v1/secret/foo", map[string]interface{}{ - "data": "bar", - }) - testResponseStatus(t, resp, 204) - - // read secret - resp = testHttpGet(t, token, addr+"/v1/secret/foo") - - testResponseBody(t, resp, &result) - - expected = map[string]interface{}{ - "lease_duration": int(40), - "lease_id": result.LeaseID, - } - - if !reflect.DeepEqual(structs.Map(result), expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, structs.Map(result)) - } -} - -func TestSysTuneMount_nonHMACKeys(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Mount-tune the audit_non_hmac_request_keys - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "audit_non_hmac_request_keys": "foo", - }) - testResponseStatus(t, resp, 204) - - // Mount-tune the audit_non_hmac_response_keys - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "audit_non_hmac_response_keys": "bar", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") - testResponseStatus(t, resp, 200) - - actual := map[string]interface{}{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "audit_non_hmac_request_keys": []interface{}{"foo"}, - "audit_non_hmac_response_keys": []interface{}{"bar"}, - "options": map[string]interface{}{"version": "1"}, - }, - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "audit_non_hmac_request_keys": []interface{}{"foo"}, - "audit_non_hmac_response_keys": []interface{}{"bar"}, - "options": map[string]interface{}{"version": "1"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // Unset those mount tune values - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "audit_non_hmac_request_keys": "", - }) - testResponseStatus(t, resp, 204) - - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "audit_non_hmac_response_keys": "", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") - testResponseStatus(t, resp, 200) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - }, - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } -} - -func TestSysTuneMount_listingVisibility(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Get original tune values, ensure that listing_visibility is not set - resp := testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") - testResponseStatus(t, resp, 200) - - actual := map[string]interface{}{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - }, - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // Mount-tune the listing_visibility - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "listing_visibility": "unauth", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") - testResponseStatus(t, resp, 200) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "listing_visibility": "unauth", - "options": map[string]interface{}{"version": "1"}, - }, - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "listing_visibility": "unauth", - "options": map[string]interface{}{"version": "1"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } -} - -func TestSysTuneMount_passthroughRequestHeaders(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Mount-tune the audit_non_hmac_request_keys - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "passthrough_request_headers": "X-Vault-Foo", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") - testResponseStatus(t, resp, 200) - - actual := map[string]interface{}{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "options": map[string]interface{}{"version": "1"}, - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"X-Vault-Foo"}, - }, - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "options": map[string]interface{}{"version": "1"}, - "force_no_cache": false, - "passthrough_request_headers": []interface{}{"X-Vault-Foo"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // Unset the mount tune value - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "passthrough_request_headers": "", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") - testResponseStatus(t, resp, 200) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - }, - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } -} - -func TestSysTuneMount_allowedManagedKeys(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - // Mount-tune the allowed_managed_keys - resp := testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "allowed_managed_keys": "test_key", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") - testResponseStatus(t, resp, 200) - - actual := map[string]interface{}{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "options": map[string]interface{}{"version": "1"}, - "force_no_cache": false, - "allowed_managed_keys": []interface{}{"test_key"}, - }, - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "options": map[string]interface{}{"version": "1"}, - "force_no_cache": false, - "allowed_managed_keys": []interface{}{"test_key"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } - - // Unset the mount tune value - resp = testHttpPost(t, token, addr+"/v1/sys/mounts/secret/tune", map[string]interface{}{ - "allowed_managed_keys": "", - }) - testResponseStatus(t, resp, 204) - - // Check results - resp = testHttpGet(t, token, addr+"/v1/sys/mounts/secret/tune") - testResponseStatus(t, resp, 200) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - }, - "description": "key/value secret storage", - "default_lease_ttl": json.Number("2764800"), - "max_lease_ttl": json.Number("2764800"), - "force_no_cache": false, - "options": map[string]interface{}{"version": "1"}, - } - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual) - } -} diff --git a/http/sys_mounts_test.go b/http/sys_mounts_test.go deleted file mode 100644 index 4f597f474..000000000 --- a/http/sys_mounts_test.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "fmt" - "math/rand" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/vault" -) - -func TestSysMountConfig(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - config := api.DefaultConfig() - config.Address = addr - - client, err := api.NewClient(config) - if err != nil { - t.Fatal(err) - } - client.SetToken(token) - - // Set up a test mount - path, err := testMount(client) - if err != nil { - t.Fatal(err) - } - defer client.Sys().Unmount(path) - - // Get config info for this mount - mountConfig, err := client.Sys().MountConfig(path) - if err != nil { - t.Fatal(err) - } - - expectedDefaultTTL := 2764800 - if mountConfig.DefaultLeaseTTL != expectedDefaultTTL { - t.Fatalf("Expected default lease TTL: %d, got %d", - expectedDefaultTTL, mountConfig.DefaultLeaseTTL) - } - - expectedMaxTTL := 2764800 - if mountConfig.MaxLeaseTTL != expectedMaxTTL { - t.Fatalf("Expected default lease TTL: %d, got %d", - expectedMaxTTL, mountConfig.MaxLeaseTTL) - } - - if mountConfig.ForceNoCache { - t.Fatal("did not expect force cache") - } -} - -// testMount sets up a test mount of a kv backend w/ a random path; caller -// is responsible for unmounting -func testMount(client *api.Client) (string, error) { - rand.Seed(time.Now().UTC().UnixNano()) - randInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() - path := fmt.Sprintf("testmount-%d", randInt) - err := client.Sys().Mount(path, &api.MountInput{Type: "kv"}) - return path, err -} diff --git a/http/sys_policy_test.go b/http/sys_policy_test.go deleted file mode 100644 index 777bd9b91..000000000 --- a/http/sys_policy_test.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/hashicorp/vault/vault" -) - -func TestSysPolicies(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpGet(t, token, addr+"/v1/sys/policy") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "policies": []interface{}{"default", "root"}, - "keys": []interface{}{"default", "root"}, - }, - "policies": []interface{}{"default", "root"}, - "keys": []interface{}{"default", "root"}, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected) - } -} - -func TestSysReadPolicy(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpGet(t, token, addr+"/v1/sys/policy/root") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "name": "root", - "rules": "", - }, - "name": "root", - "rules": "", - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected) - } -} - -func TestSysWritePolicy(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/policy/foo", map[string]interface{}{ - "rules": `path "*" { capabilities = ["read"] }`, - }) - testResponseStatus(t, resp, 200) - - resp = testHttpGet(t, token, addr+"/v1/sys/policy") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "policies": []interface{}{"default", "foo", "root"}, - "keys": []interface{}{"default", "foo", "root"}, - }, - "policies": []interface{}{"default", "foo", "root"}, - "keys": []interface{}{"default", "foo", "root"}, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected) - } - - resp = testHttpPost(t, token, addr+"/v1/sys/policy/response-wrapping", map[string]interface{}{ - "rules": ``, - }) - testResponseStatus(t, resp, 400) -} - -func TestSysDeletePolicy(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/policy/foo", map[string]interface{}{ - "rules": `path "*" { capabilities = ["read"] }`, - }) - testResponseStatus(t, resp, 200) - - resp = testHttpDelete(t, token, addr+"/v1/sys/policy/foo") - testResponseStatus(t, resp, 204) - - // Also attempt to delete these since they should not be allowed (ignore - // responses, if they exist later that's sufficient) - resp = testHttpDelete(t, token, addr+"/v1/sys/policy/default") - resp = testHttpDelete(t, token, addr+"/v1/sys/policy/response-wrapping") - - resp = testHttpGet(t, token, addr+"/v1/sys/policy") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "policies": []interface{}{"default", "root"}, - "keys": []interface{}{"default", "root"}, - }, - "policies": []interface{}{"default", "root"}, - "keys": []interface{}{"default", "root"}, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected) - } -} diff --git a/http/sys_rekey_test.go b/http/sys_rekey_test.go deleted file mode 100644 index e39664447..000000000 --- a/http/sys_rekey_test.go +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "reflect" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" - "github.com/hashicorp/vault/vault" -) - -// Test to check if the API errors out when wrong number of PGP keys are -// supplied for rekey -func TestSysRekey_Init_pgpKeysEntriesForRekey(t *testing.T) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - RequestResponseCallback: schema.ResponseValidatingCallback(t), - }) - cluster.Start() - defer cluster.Cleanup() - cl := cluster.Cores[0].Client - - _, err := cl.Logical().Write("sys/rekey/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - "pgp_keys": []string{"pgpkey1"}, - }) - if err == nil { - t.Fatal("should have failed to write pgp key entry due to mismatched keys", err) - } -} - -func TestSysRekey_Init_Status(t *testing.T) { - t.Run("status-barrier-default", func(t *testing.T) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - RequestResponseCallback: schema.ResponseValidatingCallback(t), - }) - cluster.Start() - defer cluster.Cleanup() - cl := cluster.Cores[0].Client - - resp, err := cl.Logical().Read("sys/rekey/init") - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := resp.Data - expected := map[string]interface{}{ - "started": false, - "t": json.Number("0"), - "n": json.Number("0"), - "progress": json.Number("0"), - "required": json.Number("3"), - "pgp_fingerprints": interface{}(nil), - "backup": false, - "nonce": "", - "verification_required": false, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } - }) -} - -func TestSysRekey_Init_Setup(t *testing.T) { - t.Run("init-barrier-barrier-key", func(t *testing.T) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - RequestResponseCallback: schema.ResponseValidatingCallback(t), - }) - cluster.Start() - defer cluster.Cleanup() - cl := cluster.Cores[0].Client - - // Start rekey - resp, err := cl.Logical().Write("sys/rekey/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := resp.Data - expected := map[string]interface{}{ - "started": true, - "t": json.Number("3"), - "n": json.Number("5"), - "progress": json.Number("0"), - "required": json.Number("3"), - "pgp_fingerprints": interface{}(nil), - "backup": false, - "verification_required": false, - } - - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - expected["nonce"] = actual["nonce"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } - - // Get rekey status - resp, err = cl.Logical().Read("sys/rekey/init") - if err != nil { - t.Fatalf("err: %s", err) - } - - actual = resp.Data - expected = map[string]interface{}{ - "started": true, - "t": json.Number("3"), - "n": json.Number("5"), - "progress": json.Number("0"), - "required": json.Number("3"), - "pgp_fingerprints": interface{}(nil), - "backup": false, - "verification_required": false, - } - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - expected["nonce"] = actual["nonce"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } - }) -} - -func TestSysRekey_Init_Cancel(t *testing.T) { - t.Run("cancel-barrier-barrier-key", func(t *testing.T) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - RequestResponseCallback: schema.ResponseValidatingCallback(t), - }) - cluster.Start() - defer cluster.Cleanup() - cl := cluster.Cores[0].Client - - _, err := cl.Logical().Write("sys/rekey/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - _, err = cl.Logical().Delete("sys/rekey/init") - if err != nil { - t.Fatalf("err: %s", err) - } - - resp, err := cl.Logical().Read("sys/rekey/init") - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := resp.Data - expected := map[string]interface{}{ - "started": false, - "t": json.Number("0"), - "n": json.Number("0"), - "progress": json.Number("0"), - "required": json.Number("3"), - "pgp_fingerprints": interface{}(nil), - "backup": false, - "nonce": "", - "verification_required": false, - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } - }) -} - -func TestSysRekey_badKey(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/rekey/update", map[string]interface{}{ - "key": "0123", - }) - testResponseStatus(t, resp, 400) -} - -func TestSysRekey_Update(t *testing.T) { - t.Run("rekey-barrier-barrier-key", func(t *testing.T) { - core, keys, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/rekey/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - }) - var rekeyStatus map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &rekeyStatus) - - var actual map[string]interface{} - var expected map[string]interface{} - - for i, key := range keys { - resp = testHttpPut(t, token, addr+"/v1/sys/rekey/update", map[string]interface{}{ - "nonce": rekeyStatus["nonce"].(string), - "key": hex.EncodeToString(key), - }) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "started": true, - "nonce": rekeyStatus["nonce"].(string), - "backup": false, - "pgp_fingerprints": interface{}(nil), - "required": json.Number("3"), - "t": json.Number("3"), - "n": json.Number("5"), - "progress": json.Number(fmt.Sprintf("%d", i+1)), - "verification_required": false, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - if i+1 == len(keys) { - delete(expected, "started") - delete(expected, "required") - delete(expected, "t") - delete(expected, "n") - delete(expected, "progress") - expected["complete"] = true - expected["keys"] = actual["keys"] - expected["keys_base64"] = actual["keys_base64"] - } - - if i+1 < len(keys) && (actual["nonce"] == nil || actual["nonce"].(string) == "") { - t.Fatalf("expected a nonce, i is %d, actual is %#v", i, actual) - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: \n%#v\nactual: \n%#v", expected, actual) - } - } - - retKeys := actual["keys"].([]interface{}) - if len(retKeys) != 5 { - t.Fatalf("bad: %#v", retKeys) - } - keysB64 := actual["keys_base64"].([]interface{}) - if len(keysB64) != 5 { - t.Fatalf("bad: %#v", keysB64) - } - }) -} - -func TestSysRekey_ReInitUpdate(t *testing.T) { - core, keys, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/rekey/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - }) - testResponseStatus(t, resp, 200) - - resp = testHttpDelete(t, token, addr+"/v1/sys/rekey/init") - testResponseStatus(t, resp, 204) - - resp = testHttpPut(t, token, addr+"/v1/sys/rekey/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - }) - testResponseStatus(t, resp, 200) - - resp = testHttpPut(t, token, addr+"/v1/sys/rekey/update", map[string]interface{}{ - "key": hex.EncodeToString(keys[0]), - }) - - testResponseStatus(t, resp, 400) -} diff --git a/http/sys_rotate_test.go b/http/sys_rotate_test.go deleted file mode 100644 index 3ceb167c1..000000000 --- a/http/sys_rotate_test.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/vault" -) - -func TestSysRotate(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPost(t, token, addr+"/v1/sys/rotate", map[string]interface{}{}) - testResponseStatus(t, resp, 204) - - resp = testHttpGet(t, token, addr+"/v1/sys/key-status") - - var actual map[string]interface{} - expected := map[string]interface{}{ - "lease_id": "", - "renewable": false, - "lease_duration": json.Number("0"), - "wrap_info": nil, - "warnings": nil, - "auth": nil, - "data": map[string]interface{}{ - "term": json.Number("2"), - }, - "term": json.Number("2"), - } - - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - for _, field := range []string{"install_time", "encryptions"} { - actualVal, ok := actual["data"].(map[string]interface{})[field] - if !ok || actualVal == "" { - t.Fatal(field, " missing in data") - } - expected["data"].(map[string]interface{})[field] = actualVal - expected[field] = actualVal - } - - expected["request_id"] = actual["request_id"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } -} diff --git a/http/sys_seal_test.go b/http/sys_seal_test.go deleted file mode 100644 index f11335bac..000000000 --- a/http/sys_seal_test.go +++ /dev/null @@ -1,558 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "context" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "net/http" - "strconv" - "strings" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" - "github.com/hashicorp/vault/vault/seal" - "github.com/hashicorp/vault/version" -) - -func TestSysSealStatus(t *testing.T) { - core := vault.TestCore(t) - vault.TestCoreInit(t, core) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp, err := http.Get(addr + "/v1/sys/seal-status") - if err != nil { - t.Fatalf("err: %s", err) - } - - var actual map[string]interface{} - expected := map[string]interface{}{ - "sealed": true, - "t": json.Number("3"), - "n": json.Number("3"), - "progress": json.Number("0"), - "nonce": "", - "type": "shamir", - "recovery_seal": false, - "initialized": true, - "migration": false, - "build_date": version.BuildDate, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["version"] == nil { - t.Fatalf("expected version information") - } - expected["version"] = actual["version"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } -} - -func TestSysSealStatus_uninit(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - resp, err := http.Get(addr + "/v1/sys/seal-status") - if err != nil { - t.Fatalf("err: %s", err) - } - testResponseStatus(t, resp, 200) -} - -func TestSysSeal(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/seal", nil) - testResponseStatus(t, resp, 204) - - if !core.Sealed() { - t.Fatal("should be sealed") - } -} - -func TestSysSeal_unsealed(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/seal", nil) - testResponseStatus(t, resp, 204) - - if !core.Sealed() { - t.Fatal("should be sealed") - } -} - -func TestSysUnseal(t *testing.T) { - core := vault.TestCore(t) - keys, _ := vault.TestCoreInit(t, core) - ln, addr := TestServer(t, core) - defer ln.Close() - - for i, key := range keys { - resp := testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ - "key": hex.EncodeToString(key), - }) - - var actual map[string]interface{} - expected := map[string]interface{}{ - "sealed": true, - "t": json.Number("3"), - "n": json.Number("3"), - "progress": json.Number(fmt.Sprintf("%d", i+1)), - "nonce": "", - "type": "shamir", - "recovery_seal": false, - "initialized": true, - "migration": false, - "build_date": version.BuildDate, - } - if i == len(keys)-1 { - expected["sealed"] = false - expected["progress"] = json.Number("0") - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if i < len(keys)-1 && (actual["nonce"] == nil || actual["nonce"].(string) == "") { - t.Fatalf("got nil nonce, actual is %#v", actual) - } else { - expected["nonce"] = actual["nonce"] - } - if actual["version"] == nil { - t.Fatalf("expected version information") - } - expected["version"] = actual["version"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } - } -} - -func subtestBadSingleKey(t *testing.T, seal vault.Seal) { - core := vault.TestCoreWithSeal(t, seal, false) - _, err := core.Initialize(context.Background(), &vault.InitParams{ - BarrierConfig: &vault.SealConfig{ - SecretShares: 1, - SecretThreshold: 1, - }, - RecoveryConfig: &vault.SealConfig{ - SecretShares: 1, - SecretThreshold: 1, - }, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - ln, addr := TestServer(t, core) - defer ln.Close() - - testCases := []struct { - description string - key string - }{ - // hex key tests - // hexadecimal strings have 2 symbols per byte; size(0xAA) == 1 byte - { - "short hex key", - strings.Repeat("AA", 8), - }, - { - "long hex key", - strings.Repeat("AA", 34), - }, - { - "uneven hex key byte length", - strings.Repeat("AA", 33), - }, - { - "valid hex key but wrong cluster", - "4482691dd3a710723c4f77c4920ee21b96c226bf4829fa6eb8e8262c180ae933", - }, - - // base64 key tests - // base64 strings have min. 1 character per byte; size("m") == 1 byte - { - "short b64 key", - base64.StdEncoding.EncodeToString([]byte(strings.Repeat("m", 8))), - }, - { - "long b64 key", - base64.StdEncoding.EncodeToString([]byte(strings.Repeat("m", 34))), - }, - { - "uneven b64 key byte length", - base64.StdEncoding.EncodeToString([]byte(strings.Repeat("m", 33))), - }, - { - "valid b64 key but wrong cluster", - "RIJpHdOnEHI8T3fEkg7iG5bCJr9IKfpuuOgmLBgK6TM=", - }, - - // other key tests - { - "empty key", - "", - }, - { - "key with bad format", - "ThisKeyIsNeitherB64NorHex", - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - resp := testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ - "key": tc.key, - }) - - testResponseStatus(t, resp, 400) - }) - } -} - -func subtestBadMultiKey(t *testing.T, seal vault.Seal) { - numKeys := 3 - - core := vault.TestCoreWithSeal(t, seal, false) - _, err := core.Initialize(context.Background(), &vault.InitParams{ - BarrierConfig: &vault.SealConfig{ - SecretShares: numKeys, - SecretThreshold: numKeys, - }, - RecoveryConfig: &vault.SealConfig{ - SecretShares: numKeys, - SecretThreshold: numKeys, - }, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - ln, addr := TestServer(t, core) - defer ln.Close() - - testCases := []struct { - description string - keys []string - }{ - { - "all unseal keys from another cluster", - []string{ - "b189d98fdec3a15bed9b1cce5088f82b92896696b788c07bdf03c73da08279a5e8", - "0fa98232f034177d8d9c2824899a2ac1e55dc6799348533e10510b856aef99f61a", - "5344f5caa852f9ba1967d9623ed286a45ea7c4a529522d25f05d29ff44f17930ac", - }, - }, - { - "mixing unseal keys from different cluster, different share config", - []string{ - "b189d98fdec3a15bed9b1cce5088f82b92896696b788c07bdf03c73da08279a5e8", - "0fa98232f034177d8d9c2824899a2ac1e55dc6799348533e10510b856aef99f61a", - "e04ea3020838c2050c4a169d7ba4d30e034eec8e83e8bed9461bf2646ee412c0", - }, - }, - { - "mixing unseal keys from different clusters, similar share config", - []string{ - "b189d98fdec3a15bed9b1cce5088f82b92896696b788c07bdf03c73da08279a5e8", - "0fa98232f034177d8d9c2824899a2ac1e55dc6799348533e10510b856aef99f61a", - "413f80521b393aa6c4e42e9a3a3ab7f00c2002b2c3bf1e273fc6f363f35f2a378b", - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - for i, key := range tc.keys { - resp := testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ - "key": key, - }) - - if i == numKeys-1 { - // last key - testResponseStatus(t, resp, 400) - } else { - // unseal in progress - testResponseStatus(t, resp, 200) - } - - } - }) - } -} - -func TestSysUnseal_BadKeyNewShamir(t *testing.T) { - seal := vault.NewTestSeal(t, - &seal.TestSealOpts{StoredKeys: seal.StoredKeysSupportedShamirRoot}) - - subtestBadSingleKey(t, seal) - subtestBadMultiKey(t, seal) -} - -func TestSysUnseal_BadKeyAutoUnseal(t *testing.T) { - seal := vault.NewTestSeal(t, - &seal.TestSealOpts{StoredKeys: seal.StoredKeysSupportedGeneric}) - - subtestBadSingleKey(t, seal) - subtestBadMultiKey(t, seal) -} - -func TestSysUnseal_Reset(t *testing.T) { - core := vault.TestCore(t) - ln, addr := TestServer(t, core) - defer ln.Close() - - thresh := 3 - resp := testHttpPut(t, "", addr+"/v1/sys/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": thresh, - }) - - var actual map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - keysRaw, ok := actual["keys"] - if !ok { - t.Fatalf("no keys: %#v", actual) - } - for i, key := range keysRaw.([]interface{}) { - if i > thresh-2 { - break - } - - resp := testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ - "key": key.(string), - }) - - var actual map[string]interface{} - expected := map[string]interface{}{ - "sealed": true, - "t": json.Number("3"), - "n": json.Number("5"), - "progress": json.Number(strconv.Itoa(i + 1)), - "type": "shamir", - "recovery_seal": false, - "initialized": true, - "migration": false, - "build_date": version.BuildDate, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["version"] == nil { - t.Fatalf("expected version information") - } - expected["version"] = actual["version"] - if actual["nonce"] == "" && expected["sealed"].(bool) { - t.Fatalf("expected a nonce") - } - expected["nonce"] = actual["nonce"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } - } - - resp = testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ - "reset": true, - }) - - actual = map[string]interface{}{} - expected := map[string]interface{}{ - "sealed": true, - "t": json.Number("3"), - "n": json.Number("5"), - "progress": json.Number("0"), - "type": "shamir", - "recovery_seal": false, - "initialized": true, - "build_date": version.BuildDate, - "migration": false, - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["version"] == nil { - t.Fatalf("expected version information") - } - expected["version"] = actual["version"] - expected["nonce"] = actual["nonce"] - if actual["cluster_name"] == nil { - delete(expected, "cluster_name") - } else { - expected["cluster_name"] = actual["cluster_name"] - } - if actual["cluster_id"] == nil { - delete(expected, "cluster_id") - } else { - expected["cluster_id"] = actual["cluster_id"] - } - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } -} - -// Test Seal's permissions logic, which is slightly different than normal code -// paths in that it queries the ACL rather than having checkToken do it. This -// is because it was abusing RootPaths in logical_system, but that caused some -// haywire with code paths that expected there to be an actual corresponding -// logical.Path for it. This way is less hacky, but this test ensures that we -// have not opened up a permissions hole. -func TestSysSeal_Permissions(t *testing.T) { - core, _, root := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, root) - - // Set the 'test' policy object to permit write access to sys/seal - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sys/policy/test", - Data: map[string]interface{}{ - "rules": `path "sys/seal" { capabilities = ["read"] }`, - }, - ClientToken: root, - } - resp, err := core.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp == nil || resp.IsError() { - t.Fatalf("bad: %#v", resp) - } - - // Create a non-root token with access to that policy - req.Path = "auth/token/create" - req.Data = map[string]interface{}{ - "id": "child", - "policies": []string{"test"}, - } - - resp, err = core.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatalf("err: %v %v", err, resp) - } - if resp.Auth.ClientToken != "child" { - t.Fatalf("bad: %#v", resp) - } - - // We must go through the HTTP interface since seal doesn't go through HandleRequest - - // We expect this to fail since it needs update and sudo - httpResp := testHttpPut(t, "child", addr+"/v1/sys/seal", nil) - testResponseStatus(t, httpResp, 403) - - // Now modify to add update capability - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sys/policy/test", - Data: map[string]interface{}{ - "rules": `path "sys/seal" { capabilities = ["update"] }`, - }, - ClientToken: root, - } - resp, err = core.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp == nil || resp.IsError() { - t.Fatalf("bad: %#v", resp) - } - - // We expect this to fail since it needs sudo - httpResp = testHttpPut(t, "child", addr+"/v1/sys/seal", nil) - testResponseStatus(t, httpResp, 403) - - // Now modify to just sudo capability - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sys/policy/test", - Data: map[string]interface{}{ - "rules": `path "sys/seal" { capabilities = ["sudo"] }`, - }, - ClientToken: root, - } - resp, err = core.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp == nil || resp.IsError() { - t.Fatalf("bad: %#v", resp) - } - - // We expect this to fail since it needs update - httpResp = testHttpPut(t, "child", addr+"/v1/sys/seal", nil) - testResponseStatus(t, httpResp, 403) - - // Now modify to add all needed capabilities - req = &logical.Request{ - Operation: logical.UpdateOperation, - Path: "sys/policy/test", - Data: map[string]interface{}{ - "rules": `path "sys/seal" { capabilities = ["update", "sudo"] }`, - }, - ClientToken: root, - } - resp, err = core.HandleRequest(namespace.RootContext(nil), req) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp == nil || resp.IsError() { - t.Fatalf("bad: %#v", resp) - } - - // We expect this to work - httpResp = testHttpPut(t, "child", addr+"/v1/sys/seal", nil) - testResponseStatus(t, httpResp, 204) -} - -func TestSysStepDown(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/step-down", nil) - testResponseStatus(t, resp, 204) -} diff --git a/http/sys_wrapping_test.go b/http/sys_wrapping_test.go deleted file mode 100644 index d059e5830..000000000 --- a/http/sys_wrapping_test.go +++ /dev/null @@ -1,389 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "encoding/json" - "errors" - "reflect" - "testing" - "time" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/vault" -) - -// Test wrapping functionality -func TestHTTP_Wrapping(t *testing.T) { - cluster := vault.NewTestCluster(t, &vault.CoreConfig{}, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - cores := cluster.Cores - - // make it easy to get access to the active - core := cores[0].Core - vault.TestWaitActive(t, core) - - client := cores[0].Client - client.SetToken(cluster.RootToken) - - // Write a value that we will use with wrapping for lookup - _, err := client.Logical().Write("secret/foo", map[string]interface{}{ - "zip": "zap", - }) - if err != nil { - t.Fatal(err) - } - - // Set a wrapping lookup function for reads on that path - client.SetWrappingLookupFunc(func(operation, path string) string { - if operation == "GET" && path == "secret/foo" { - return "5m" - } - - return api.DefaultWrappingLookupFunc(operation, path) - }) - - // First test: basic things that should fail, lookup edition - // Root token isn't a wrapping token - _, err = client.Logical().Write("sys/wrapping/lookup", nil) - if err == nil { - t.Fatal("expected error") - } - // Not supplied - _, err = client.Logical().Write("sys/wrapping/lookup", map[string]interface{}{ - "foo": "bar", - }) - if err == nil { - t.Fatal("expected error") - } - // Nonexistent token isn't a wrapping token - _, err = client.Logical().Write("sys/wrapping/lookup", map[string]interface{}{ - "token": "bar", - }) - if err == nil { - t.Fatal("expected error") - } - - // Second: basic things that should fail, unwrap edition - // Root token isn't a wrapping token - _, err = client.Logical().Unwrap(cluster.RootToken) - if err == nil { - t.Fatal("expected error") - } - // Root token isn't a wrapping token - _, err = client.Logical().Write("sys/wrapping/unwrap", nil) - if err == nil { - t.Fatal("expected error") - } - // Not supplied - _, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ - "foo": "bar", - }) - if err == nil { - t.Fatal("expected error") - } - // Nonexistent token isn't a wrapping token - _, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ - "token": "bar", - }) - if err == nil { - t.Fatal("expected error") - } - - // - // Test lookup - // - - // Create a wrapping token - secret, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.WrapInfo == nil { - t.Fatal("secret or wrap info is nil") - } - wrapInfo := secret.WrapInfo - - // Test this twice to ensure no ill effect to the wrapping token as a result of the lookup - for i := 0; i < 2; i++ { - secret, err = client.Logical().Write("sys/wrapping/lookup", map[string]interface{}{ - "token": wrapInfo.Token, - }) - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.Data == nil { - t.Fatal("secret or secret data is nil") - } - creationTTL, _ := secret.Data["creation_ttl"].(json.Number).Int64() - if int(creationTTL) != wrapInfo.TTL { - t.Fatalf("mismatched ttls: %d vs %d", creationTTL, wrapInfo.TTL) - } - if secret.Data["creation_time"].(string) != wrapInfo.CreationTime.Format(time.RFC3339Nano) { - t.Fatalf("mismatched creation times: %q vs %q", secret.Data["creation_time"].(string), wrapInfo.CreationTime.Format(time.RFC3339Nano)) - } - } - - // - // Test unwrap - // - - // Create a wrapping token - secret, err = client.Logical().Read("secret/foo") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.WrapInfo == nil { - t.Fatal("secret or wrap info is nil") - } - wrapInfo = secret.WrapInfo - - // Test unwrap via the client token - client.SetToken(wrapInfo.Token) - secret, err = client.Logical().Write("sys/wrapping/unwrap", nil) - if err != nil { - t.Fatal(err) - } - if secret.Warnings != nil { - t.Fatalf("Warnings found: %v", secret.Warnings) - } - if secret == nil || secret.Data == nil { - t.Fatal("secret or secret data is nil") - } - ret1 := secret - // Should be expired and fail - _, err = client.Logical().Write("sys/wrapping/unwrap", nil) - if err == nil { - t.Fatal("expected err") - } - - // Create a wrapping token - client.SetToken(cluster.RootToken) - secret, err = client.Logical().Read("secret/foo") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.WrapInfo == nil { - t.Fatal("secret or wrap info is nil") - } - wrapInfo = secret.WrapInfo - - // Test as a separate token - secret, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ - "token": wrapInfo.Token, - }) - if err != nil { - t.Fatal(err) - } - ret2 := secret - // Should be expired and fail - _, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ - "token": wrapInfo.Token, - }) - if err == nil { - t.Fatal("expected err") - } - - // Create a wrapping token - secret, err = client.Logical().Read("secret/foo") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.WrapInfo == nil { - t.Fatal("secret or wrap info is nil") - } - wrapInfo = secret.WrapInfo - - // Read response directly - client.SetToken(wrapInfo.Token) - secret, err = client.Logical().Read("cubbyhole/response") - if err != nil { - t.Fatal(err) - } - ret3 := secret - // Should be expired and fail - _, err = client.Logical().Write("cubbyhole/response", nil) - if err == nil { - t.Fatal("expected err") - } - - // Create a wrapping token - client.SetToken(cluster.RootToken) - secret, err = client.Logical().Read("secret/foo") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.WrapInfo == nil { - t.Fatal("secret or wrap info is nil") - } - wrapInfo = secret.WrapInfo - - // Read via Unwrap method - secret, err = client.Logical().Unwrap(wrapInfo.Token) - if err != nil { - t.Fatal(err) - } - if secret.Warnings != nil { - t.Fatalf("Warnings found: %v", secret.Warnings) - } - ret4 := secret - // Should be expired and fail - _, err = client.Logical().Unwrap(wrapInfo.Token) - if err == nil { - t.Fatal("expected err") - } - - if !reflect.DeepEqual(ret1.Data, map[string]interface{}{ - "zip": "zap", - }) { - t.Fatalf("ret1 data did not match expected: %#v", ret1.Data) - } - if !reflect.DeepEqual(ret2.Data, map[string]interface{}{ - "zip": "zap", - }) { - t.Fatalf("ret2 data did not match expected: %#v", ret2.Data) - } - var ret3Secret api.Secret - err = jsonutil.DecodeJSON([]byte(ret3.Data["response"].(string)), &ret3Secret) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(ret3Secret.Data, map[string]interface{}{ - "zip": "zap", - }) { - t.Fatalf("ret3 data did not match expected: %#v", ret3Secret.Data) - } - if !reflect.DeepEqual(ret4.Data, map[string]interface{}{ - "zip": "zap", - }) { - t.Fatalf("ret4 data did not match expected: %#v", ret4.Data) - } - - // - // Custom wrapping - // - - client.SetToken(cluster.RootToken) - data := map[string]interface{}{ - "zip": "zap", - "three": json.Number("2"), - } - - // Don't set a request TTL on that path, should fail - client.SetWrappingLookupFunc(func(operation, path string) string { - return "" - }) - secret, err = client.Logical().Write("sys/wrapping/wrap", data) - if err == nil { - t.Fatal("expected error") - } - - // Re-set the lookup function - client.SetWrappingLookupFunc(func(operation, path string) string { - if operation == "GET" && path == "secret/foo" { - return "5m" - } - - return api.DefaultWrappingLookupFunc(operation, path) - }) - secret, err = client.Logical().Write("sys/wrapping/wrap", data) - if err != nil { - t.Fatal(err) - } - if secret.Warnings != nil { - t.Fatalf("Warnings found: %v", secret.Warnings) - } - secret, err = client.Logical().Unwrap(secret.WrapInfo.Token) - if err != nil { - t.Fatal(err) - } - if secret.Warnings != nil { - t.Fatalf("Warnings found: %v", secret.Warnings) - } - if !reflect.DeepEqual(data, secret.Data) { - t.Fatalf("custom wrap did not match expected: %#v", secret.Data) - } - - // - // Test rewrap - // - - // Create a wrapping token - secret, err = client.Logical().Read("secret/foo") - if err != nil { - t.Fatal(err) - } - if secret == nil || secret.WrapInfo == nil { - t.Fatal("secret or wrap info is nil") - } - wrapInfo = secret.WrapInfo - - // Check for correct CreationPath before rewrap - if wrapInfo.CreationPath != "secret/foo" { - t.Fatalf("error on wrapInfo.CreationPath: expected: secret/foo, got: %s", wrapInfo.CreationPath) - } - - // Test rewrapping - secret, err = client.Logical().Write("sys/wrapping/rewrap", map[string]interface{}{ - "token": wrapInfo.Token, - }) - if err != nil { - t.Fatal(err) - } - if secret.Warnings != nil { - t.Fatalf("Warnings found: %v", secret.Warnings) - } - - // Check for correct Creation path after rewrap - if wrapInfo.CreationPath != "secret/foo" { - t.Fatalf("error on wrapInfo.CreationPath: expected: secret/foo, got: %s", wrapInfo.CreationPath) - } - - // Should be expired and fail - _, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ - "token": wrapInfo.Token, - }) - if err == nil { - t.Fatal("expected err") - } - - // Attempt unwrapping the rewrapped token - wrapToken := secret.WrapInfo.Token - secret, err = client.Logical().Unwrap(wrapToken) - if err != nil { - t.Fatal(err) - } - // Should be expired and fail - _, err = client.Logical().Unwrap(wrapToken) - if err == nil { - t.Fatal("expected err") - } - - if !reflect.DeepEqual(secret.Data, map[string]interface{}{ - "zip": "zap", - }) { - t.Fatalf("secret data did not match expected: %#v", secret.Data) - } - - // Ensure that wrapping lookup without a client token responds correctly - client.ClearToken() - secret, err = client.Logical().Read("sys/wrapping/lookup") - if secret != nil { - t.Fatalf("expected no response: %#v", secret) - } - - if err == nil { - t.Fatal("expected error") - } - - var respError *api.ResponseError - if errors.As(err, &respError); respError.StatusCode != 403 { - t.Fatalf("expected 403 response, actual: %d", respError.StatusCode) - } -} diff --git a/http/testing.go b/http/testing.go deleted file mode 100644 index 5797e4dc5..000000000 --- a/http/testing.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "fmt" - "net" - "net/http" - "testing" - - "github.com/hashicorp/vault/internalshared/configutil" - "github.com/hashicorp/vault/vault" -) - -func TestListener(tb testing.TB) (net.Listener, string) { - fail := func(format string, args ...interface{}) { - panic(fmt.Sprintf(format, args...)) - } - if tb != nil { - fail = tb.Fatalf - } - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - fail("err: %s", err) - } - addr := "http://" + ln.Addr().String() - return ln, addr -} - -func TestServerWithListenerAndProperties(tb testing.TB, ln net.Listener, addr string, core *vault.Core, props *vault.HandlerProperties) { - // Create a muxer to handle our requests so that we can authenticate - // for tests. - mux := http.NewServeMux() - mux.Handle("/_test/auth", http.HandlerFunc(testHandleAuth)) - mux.Handle("/", Handler.Handler(props)) - - server := &http.Server{ - Addr: ln.Addr().String(), - Handler: mux, - ErrorLog: core.Logger().StandardLogger(nil), - } - go server.Serve(ln) -} - -func TestServerWithListener(tb testing.TB, ln net.Listener, addr string, core *vault.Core) { - ip, _, _ := net.SplitHostPort(ln.Addr().String()) - - // Create a muxer to handle our requests so that we can authenticate - // for tests. - props := &vault.HandlerProperties{ - Core: core, - // This is needed for testing custom response headers - ListenerConfig: &configutil.Listener{ - Address: ip, - }, - } - TestServerWithListenerAndProperties(tb, ln, addr, core, props) -} - -func TestServer(tb testing.TB, core *vault.Core) (net.Listener, string) { - ln, addr := TestListener(tb) - TestServerWithListener(tb, ln, addr, core) - return ln, addr -} - -func TestServerAuth(tb testing.TB, addr string, token string) { - if _, err := http.Get(addr + "/_test/auth?token=" + token); err != nil { - tb.Fatalf("error authenticating: %s", err) - } -} - -func testHandleAuth(w http.ResponseWriter, req *http.Request) { - respondOk(w, nil) -} diff --git a/http/unwrapping_raw_body_test.go b/http/unwrapping_raw_body_test.go deleted file mode 100644 index de145486d..000000000 --- a/http/unwrapping_raw_body_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package http - -import ( - "testing" - - kv "github.com/hashicorp/vault-plugin-secrets-kv" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/vault" -) - -func TestUnwrapping_Raw_Body(t *testing.T) { - coreConfig := &vault.CoreConfig{ - LogicalBackends: map[string]logical.Factory{ - "kv": kv.Factory, - }, - } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ - HandlerFunc: Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - core := cluster.Cores[0].Core - vault.TestWaitActive(t, core) - client := cluster.Cores[0].Client - - // Mount a k/v backend, version 2 - err := client.Sys().Mount("kv", &api.MountInput{ - Type: "kv", - Options: map[string]string{"version": "2"}, - }) - if err != nil { - t.Fatal(err) - } - - client.SetWrappingLookupFunc(func(operation, path string) string { - return "5m" - }) - secret, err := client.Logical().Write("kv/foo/bar", map[string]interface{}{ - "a": "b", - }) - if err != nil { - t.Fatal(err) - } - if secret == nil { - t.Fatal("nil secret") - } - if secret.WrapInfo == nil { - t.Fatal("nil wrap info") - } - wrapToken := secret.WrapInfo.Token - - client.SetWrappingLookupFunc(nil) - secret, err = client.Logical().Unwrap(wrapToken) - if err != nil { - t.Fatal(err) - } - if len(secret.Warnings) != 1 { - t.Fatal("expected 1 warning") - } -} diff --git a/internalshared/configutil/config_test.go b/internalshared/configutil/config_test.go deleted file mode 100644 index 4362f9228..000000000 --- a/internalshared/configutil/config_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package configutil - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -type mapValue[T any] struct { - Value T - IsFound bool -} - -type expectedLogFields struct { - File mapValue[string] - Format mapValue[string] - Level mapValue[string] - RotateBytes mapValue[int] - RotateDuration mapValue[string] - RotateMaxFiles mapValue[int] -} - -// TestSharedConfig_Sanitized_LogFields ensures that 'log related' shared config -// is sanitized as expected. -func TestSharedConfig_Sanitized_LogFields(t *testing.T) { - tests := map[string]struct { - Value *SharedConfig - IsNil bool - Expected expectedLogFields - }{ - "nil": { - Value: nil, - IsNil: true, - }, - "empty": { - Value: &SharedConfig{}, - IsNil: false, - Expected: expectedLogFields{ - Format: mapValue[string]{IsFound: true, Value: ""}, - Level: mapValue[string]{IsFound: true, Value: ""}, - }, - }, - "only-log-level-and-format": { - Value: &SharedConfig{ - LogFormat: "json", - LogLevel: "warn", - }, - IsNil: false, - Expected: expectedLogFields{ - Format: mapValue[string]{IsFound: true, Value: "json"}, - Level: mapValue[string]{IsFound: true, Value: "warn"}, - }, - }, - "valid-log-fields": { - Value: &SharedConfig{ - LogFile: "vault.log", - LogFormat: "json", - LogLevel: "warn", - LogRotateBytes: 1024, - LogRotateDuration: "30m", - LogRotateMaxFiles: -1, - }, - IsNil: false, - Expected: expectedLogFields{ - File: mapValue[string]{IsFound: true, Value: "vault.log"}, - Format: mapValue[string]{IsFound: true, Value: "json"}, - Level: mapValue[string]{IsFound: true, Value: "warn"}, - RotateBytes: mapValue[int]{IsFound: true, Value: 1024}, - RotateDuration: mapValue[string]{IsFound: true, Value: "30m"}, - RotateMaxFiles: mapValue[int]{IsFound: true, Value: -1}, - }, - }, - } - - for name, tc := range tests { - name := name - tc := tc - t.Run(name, func(t *testing.T) { - cfg := tc.Value.Sanitized() - switch { - case tc.IsNil: - require.Nil(t, cfg) - default: - require.NotNil(t, cfg) - - // Log file - val, found := cfg["log_file"] - switch { - case tc.Expected.File.IsFound: - require.True(t, found) - require.NotNil(t, val) - require.Equal(t, tc.Expected.File.Value, val) - default: - require.Nil(t, val) - } - - // Log format - val, found = cfg["log_format"] - switch { - case tc.Expected.Format.IsFound: - require.True(t, found) - require.NotNil(t, val) - require.Equal(t, tc.Expected.Format.Value, val) - default: - require.Nil(t, val) - } - - // Log level - val, found = cfg["log_level"] - switch { - case tc.Expected.Level.IsFound: - require.True(t, found) - require.NotNil(t, val) - require.Equal(t, tc.Expected.Level.Value, val) - default: - require.Nil(t, val) - } - - // Log rotate bytes - val, found = cfg["log_rotate_bytes"] - switch { - case tc.Expected.RotateBytes.IsFound: - require.True(t, found) - require.NotNil(t, val) - require.Equal(t, tc.Expected.RotateBytes.Value, val) - default: - require.Nil(t, val) - } - - // Log rotate duration - val, found = cfg["log_rotate_duration"] - switch { - case tc.Expected.RotateDuration.IsFound: - require.True(t, found) - require.NotNil(t, val) - require.Equal(t, tc.Expected.RotateDuration.Value, val) - default: - require.Nil(t, val) - } - - // Log rotate max files - val, found = cfg["log_rotate_max_files"] - switch { - case tc.Expected.RotateMaxFiles.IsFound: - require.True(t, found) - require.NotNil(t, val) - require.Equal(t, tc.Expected.RotateMaxFiles.Value, val) - default: - require.Nil(t, val) - } - } - }) - } -} diff --git a/internalshared/configutil/encrypt_decrypt_test.go b/internalshared/configutil/encrypt_decrypt_test.go deleted file mode 100644 index 7f2679afd..000000000 --- a/internalshared/configutil/encrypt_decrypt_test.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package configutil - -import ( - "bytes" - "context" - "encoding/base64" - "testing" - - wrapping "github.com/hashicorp/go-kms-wrapping/v2" - "google.golang.org/protobuf/proto" -) - -func getAEADTestKMS(t *testing.T) { -} - -func TestEncryptParams(t *testing.T) { - rawStr := ` -storage "consul" { - api_key = "{{encrypt(foobar)}}" -} - -telemetry { - some_param = "something" - circonus_api_key = "{{encrypt(barfoo)}}" -} -` - - finalStr := ` -storage "consul" { - api_key = "foobar" -} - -telemetry { - some_param = "something" - circonus_api_key = "barfoo" -} -` - - reverser := new(reversingWrapper) - out, err := EncryptDecrypt(rawStr, false, false, reverser) - if err != nil { - t.Fatal(err) - } - - first := true - locs := decryptRegex.FindAllIndex([]byte(out), -1) - for _, match := range locs { - matchBytes := []byte(out)[match[0]:match[1]] - matchBytes = bytes.TrimSuffix(bytes.TrimPrefix(matchBytes, []byte("{{decrypt(")), []byte(")}}")) - inMsg, err := base64.RawURLEncoding.DecodeString(string(matchBytes)) - if err != nil { - t.Fatal(err) - } - inBlob := new(wrapping.BlobInfo) - if err := proto.Unmarshal(inMsg, inBlob); err != nil { - t.Fatal(err) - } - ct := string(inBlob.Ciphertext) - if first { - if ct != "raboof" { - t.Fatal(ct) - } - first = false - } else { - if ct != "oofrab" { - t.Fatal(ct) - } - } - } - - decOut, err := EncryptDecrypt(out, true, false, reverser) - if err != nil { - t.Fatal(err) - } - - if decOut != rawStr { - t.Fatal(decOut) - } - - decOut, err = EncryptDecrypt(out, true, true, reverser) - if err != nil { - t.Fatal(err) - } - - if decOut != finalStr { - t.Fatal(decOut) - } -} - -type reversingWrapper struct{} - -func (r *reversingWrapper) Type(_ context.Context) (wrapping.WrapperType, error) { - return "reverser", nil -} -func (r *reversingWrapper) KeyId(_ context.Context) (string, error) { return "reverser", nil } -func (r *reversingWrapper) HMACKeyID() string { return "" } -func (r *reversingWrapper) Init(_ context.Context) error { return nil } -func (r *reversingWrapper) Finalize(_ context.Context) error { return nil } -func (r *reversingWrapper) SetConfig(_ context.Context, opts ...wrapping.Option) (*wrapping.WrapperConfig, error) { - return &wrapping.WrapperConfig{}, nil -} - -func (r *reversingWrapper) Encrypt(_ context.Context, input []byte, _ ...wrapping.Option) (*wrapping.BlobInfo, error) { - return &wrapping.BlobInfo{ - Ciphertext: r.reverse(input), - }, nil -} - -func (r *reversingWrapper) Decrypt(_ context.Context, input *wrapping.BlobInfo, _ ...wrapping.Option) ([]byte, error) { - return r.reverse(input.Ciphertext), nil -} - -func (r *reversingWrapper) reverse(input []byte) []byte { - output := make([]byte, len(input)) - for i, j := 0, len(input)-1; i < j; i, j = i+1, j-1 { - output[i], output[j] = input[j], input[i] - } - return output -} diff --git a/internalshared/configutil/listener_test.go b/internalshared/configutil/listener_test.go deleted file mode 100644 index 4678ee4df..000000000 --- a/internalshared/configutil/listener_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package configutil - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseSingleIPTemplate(t *testing.T) { - type args struct { - ipTmpl string - } - tests := []struct { - name string - arg string - want string - wantErr assert.ErrorAssertionFunc - }{ - { - name: "test https addr", - arg: "https://vaultproject.io:8200", - want: "https://vaultproject.io:8200", - wantErr: assert.NoError, - }, - { - name: "test invalid template func", - arg: "{{FooBar}}", - want: "", - wantErr: assert.Error, - }, - { - name: "test partial template", - arg: "{{FooBar", - want: "{{FooBar", - wantErr: assert.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ParseSingleIPTemplate(tt.arg) - if !tt.wantErr(t, err, fmt.Sprintf("ParseSingleIPTemplate(%v)", tt.arg)) { - return - } - - assert.Equalf(t, tt.want, got, "ParseSingleIPTemplate(%v)", tt.arg) - }) - } -} diff --git a/internalshared/configutil/telemetry_test.go b/internalshared/configutil/telemetry_test.go deleted file mode 100644 index 285278eea..000000000 --- a/internalshared/configutil/telemetry_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package configutil - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParsePrefixFilters(t *testing.T) { - t.Parallel() - cases := []struct { - inputFilters []string - expectedErrStr string - expectedAllowedPrefixes []string - expectedBlockedPrefixes []string - }{ - { - []string{""}, - "Cannot have empty filter rule in prefix_filter", - []string(nil), - []string(nil), - }, - { - []string{"vault.abc"}, - "Filter rule must begin with either '+' or '-': \"vault.abc\"", - []string(nil), - []string(nil), - }, - { - []string{"+vault.abc", "-vault.bcd"}, - "", - []string{"vault.abc"}, - []string{"vault.bcd"}, - }, - } - t.Run("validate metric filter configs", func(t *testing.T) { - t.Parallel() - - for _, tc := range cases { - - allowedPrefixes, blockedPrefixes, err := parsePrefixFilter(tc.inputFilters) - - if err != nil { - assert.EqualError(t, err, tc.expectedErrStr) - } else { - assert.Equal(t, "", tc.expectedErrStr) - assert.Equal(t, tc.expectedAllowedPrefixes, allowedPrefixes) - - assert.Equal(t, tc.expectedBlockedPrefixes, blockedPrefixes) - } - } - }) -} diff --git a/internalshared/configutil/userlockout_test.go b/internalshared/configutil/userlockout_test.go deleted file mode 100644 index 0bc5f0dce..000000000 --- a/internalshared/configutil/userlockout_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package configutil - -import ( - "reflect" - "testing" - "time" -) - -func TestParseUserLockout(t *testing.T) { - t.Parallel() - t.Run("Missing user lockout block in config file", func(t *testing.T) { - t.Parallel() - inputConfig := make(map[string]*UserLockout) - expectedConfig := make(map[string]*UserLockout) - expectedConfigall := &UserLockout{} - expectedConfigall.Type = "all" - expectedConfigall.LockoutThreshold = UserLockoutThresholdDefault - expectedConfigall.LockoutDuration = UserLockoutDurationDefault - expectedConfigall.LockoutCounterReset = UserLockoutCounterResetDefault - expectedConfigall.DisableLockout = DisableUserLockoutDefault - expectedConfig["all"] = expectedConfigall - - outputConfig := setMissingUserLockoutValuesInMap(inputConfig) - if !reflect.DeepEqual(expectedConfig["all"], outputConfig["all"]) { - t.Errorf("user lockout config: expected %#v\nactual %#v", expectedConfig["all"], outputConfig["all"]) - } - }) - t.Run("setting default lockout counter reset and lockout duration for userpass in config ", func(t *testing.T) { - t.Parallel() - // input user lockout in config file - inputConfig := make(map[string]*UserLockout) - configAll := &UserLockout{} - configAll.Type = "all" - configAll.LockoutCounterReset = 20 * time.Minute - configAll.LockoutCounterResetRaw = "1200000000000" - inputConfig["all"] = configAll - configUserpass := &UserLockout{} - configUserpass.Type = "userpass" - configUserpass.LockoutDuration = 10 * time.Minute - configUserpass.LockoutDurationRaw = "600000000000" - inputConfig["userpass"] = configUserpass - - expectedConfig := make(map[string]*UserLockout) - expectedConfigall := &UserLockout{} - expectedConfigUserpass := &UserLockout{} - // expected default values - expectedConfigall.Type = "all" - expectedConfigall.LockoutThreshold = UserLockoutThresholdDefault - expectedConfigall.LockoutDuration = UserLockoutDurationDefault - expectedConfigall.LockoutCounterReset = 20 * time.Minute - expectedConfigall.DisableLockout = DisableUserLockoutDefault - // expected values for userpass - expectedConfigUserpass.Type = "userpass" - expectedConfigUserpass.LockoutThreshold = UserLockoutThresholdDefault - expectedConfigUserpass.LockoutDuration = 10 * time.Minute - expectedConfigUserpass.LockoutCounterReset = 20 * time.Minute - expectedConfigUserpass.DisableLockout = DisableUserLockoutDefault - expectedConfig["all"] = expectedConfigall - expectedConfig["userpass"] = expectedConfigUserpass - - outputConfig := setMissingUserLockoutValuesInMap(inputConfig) - if !reflect.DeepEqual(expectedConfig["all"], outputConfig["all"]) { - t.Errorf("user lockout config: expected %#v\nactual %#v", expectedConfig["all"], outputConfig["all"]) - } - if !reflect.DeepEqual(expectedConfig["userpass"], outputConfig["userpass"]) { - t.Errorf("user lockout config: expected %#v\nactual %#v", expectedConfig["userpass"], outputConfig["userpass"]) - } - }) -} diff --git a/internalshared/listenerutil/listener_test.go b/internalshared/listenerutil/listener_test.go deleted file mode 100644 index c315fd243..000000000 --- a/internalshared/listenerutil/listener_test.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package listenerutil - -import ( - "io/ioutil" - "os" - osuser "os/user" - "strconv" - "testing" -) - -func TestUnixSocketListener(t *testing.T) { - t.Run("ids", func(t *testing.T) { - socket, err := ioutil.TempFile("", "socket") - if err != nil { - t.Fatal(err) - } - defer os.Remove(socket.Name()) - - uid, gid := os.Getuid(), os.Getgid() - - u, err := osuser.LookupId(strconv.Itoa(uid)) - if err != nil { - t.Fatal(err) - } - user := u.Username - - g, err := osuser.LookupGroupId(strconv.Itoa(gid)) - if err != nil { - t.Fatal(err) - } - group := g.Name - - l, err := UnixSocketListener(socket.Name(), &UnixSocketsConfig{ - User: user, - Group: group, - Mode: "644", - }) - if err != nil { - t.Fatal(err) - } - defer l.Close() - - fi, err := os.Stat(socket.Name()) - if err != nil { - t.Fatal(err) - } - - mode, err := strconv.ParseUint("644", 8, 32) - if err != nil { - t.Fatal(err) - } - if fi.Mode().Perm() != os.FileMode(mode) { - t.Fatalf("failed to set permissions on the socket file") - } - }) - t.Run("names", func(t *testing.T) { - socket, err := ioutil.TempFile("", "socket") - if err != nil { - t.Fatal(err) - } - defer os.Remove(socket.Name()) - - uid, gid := os.Getuid(), os.Getgid() - l, err := UnixSocketListener(socket.Name(), &UnixSocketsConfig{ - User: strconv.Itoa(uid), - Group: strconv.Itoa(gid), - Mode: "644", - }) - if err != nil { - t.Fatal(err) - } - defer l.Close() - - fi, err := os.Stat(socket.Name()) - if err != nil { - t.Fatal(err) - } - - mode, err := strconv.ParseUint("644", 8, 32) - if err != nil { - t.Fatal(err) - } - if fi.Mode().Perm() != os.FileMode(mode) { - t.Fatalf("failed to set permissions on the socket file") - } - }) -} diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 78b0e69a6..000000000 --- a/main_test.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package main // import "github.com/hashicorp/vault" - -// This file is intentionally empty to force early versions of Go -// to test compilation for tests. diff --git a/physical/raft/bolt_32bit_test.go b/physical/raft/bolt_32bit_test.go deleted file mode 100644 index 4e6aaccac..000000000 --- a/physical/raft/bolt_32bit_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -//go:build 386 || arm - -package raft - -import ( - "os" - "strconv" - "testing" -) - -func Test_BoltOptions(t *testing.T) { - t.Parallel() - key := "VAULT_RAFT_INITIAL_MMAP_SIZE" - - testCases := []struct { - name string - env string - expectedSize int - }{ - {"none", "", 0}, - {"5MB", strconv.Itoa(5 * 1024 * 1024), 5 * 1024 * 1024}, - {"negative", "-1", 0}, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - if tc.env != "" { - current := os.Getenv(key) - defer os.Setenv(key, current) - os.Setenv(key, tc.env) - } - - o := boltOptions("") - - if o.InitialMmapSize != tc.expectedSize { - t.Errorf("expected InitialMmapSize to be %d but it was %d", tc.expectedSize, o.InitialMmapSize) - } - }) - } -} diff --git a/physical/raft/bolt_64bit_test.go b/physical/raft/bolt_64bit_test.go deleted file mode 100644 index f6f1bbd2d..000000000 --- a/physical/raft/bolt_64bit_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -//go:build !386 && !arm - -package raft - -import ( - "os" - "strconv" - "testing" -) - -func Test_BoltOptions(t *testing.T) { - t.Parallel() - key := "VAULT_RAFT_INITIAL_MMAP_SIZE" - - testCases := []struct { - name string - env string - expectedSize int - }{ - {"none", "", 100 * 1024 * 1024 * 1024}, - {"5MB", strconv.Itoa(5 * 1024 * 1024), 5 * 1024 * 1024}, - {"negative", "-1", 0}, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - if tc.env != "" { - current := os.Getenv(key) - defer os.Setenv(key, current) - os.Setenv(key, tc.env) - } - - o := boltOptions("") - - if o.InitialMmapSize != tc.expectedSize { - t.Errorf("expected InitialMmapSize to be %d but it was %d", tc.expectedSize, o.InitialMmapSize) - } - }) - } -} diff --git a/physical/raft/chunking_test.go b/physical/raft/chunking_test.go deleted file mode 100644 index 035ccc245..000000000 --- a/physical/raft/chunking_test.go +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package raft - -import ( - "bytes" - "context" - "fmt" - "os" - "testing" - - "github.com/golang/protobuf/proto" - "github.com/hashicorp/go-raftchunking" - raftchunkingtypes "github.com/hashicorp/go-raftchunking/types" - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/raft" - "github.com/hashicorp/raft-boltdb/v2" - "github.com/hashicorp/vault/sdk/physical" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// This chunks encoded data and then performing out-of-order applies of half -// the logs. It then snapshots, restores to a new FSM, and applies the rest. -// The goal is to verify that chunking snapshotting works as expected. -func TestRaft_Chunking_Lifecycle(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - b, dir := GetRaft(t, true, false) - defer os.RemoveAll(dir) - - t.Log("applying configuration") - - b.applyConfigSettings(raft.DefaultConfig()) - - t.Log("chunking") - - buf := []byte("let's see how this goes, shall we?") - logData := &LogData{ - Operations: []*LogOperation{ - { - OpType: putOp, - Key: "foobar", - Value: buf, - }, - }, - } - cmdBytes, err := proto.Marshal(logData) - require.NoError(err) - - var logs []*raft.Log - for i, b := range cmdBytes { - // Stage multiple operations so we can test restoring across multiple opnums - for j := 0; j < 10; j++ { - chunkInfo := &raftchunkingtypes.ChunkInfo{ - OpNum: uint64(32 + j), - SequenceNum: uint32(i), - NumChunks: uint32(len(cmdBytes)), - } - chunkBytes, err := proto.Marshal(chunkInfo) - require.NoError(err) - - logs = append(logs, &raft.Log{ - Data: []byte{b}, - Extensions: chunkBytes, - }) - } - } - - t.Log("applying half of the logs") - - // The reason for the skipping is to test out-of-order applies which are - // theoretically possible. Some of these will actually finish though! - for i := 0; i < len(logs); i += 2 { - resp := b.fsm.chunker.Apply(logs[i]) - if resp != nil { - _, ok := resp.(raftchunking.ChunkingSuccess) - assert.True(ok) - } - } - - t.Log("tearing down cluster") - require.NoError(b.TeardownCluster(nil)) - require.NoError(b.fsm.getDB().Close()) - require.NoError(b.stableStore.(*raftboltdb.BoltStore).Close()) - - t.Log("starting new backend") - backendRaw, err := NewRaftBackend(b.conf, b.logger) - require.NoError(err) - b = backendRaw.(*RaftBackend) - - t.Log("applying rest of the logs") - - // Apply the rest of the logs - var resp interface{} - for i := 1; i < len(logs); i += 2 { - resp = b.fsm.chunker.Apply(logs[i]) - if resp != nil { - _, ok := resp.(raftchunking.ChunkingSuccess) - assert.True(ok) - } - } - - assert.NotNil(resp) - _, ok := resp.(raftchunking.ChunkingSuccess) - assert.True(ok) -} - -func TestFSM_Chunking_TermChange(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - b, dir := GetRaft(t, true, false) - defer os.RemoveAll(dir) - - t.Log("applying configuration") - - b.applyConfigSettings(raft.DefaultConfig()) - - t.Log("chunking") - - buf := []byte("let's see how this goes, shall we?") - logData := &LogData{ - Operations: []*LogOperation{ - { - OpType: putOp, - Key: "foobar", - Value: buf, - }, - }, - } - cmdBytes, err := proto.Marshal(logData) - require.NoError(err) - - // Only need two chunks to test this - chunks := [][]byte{ - cmdBytes[0:2], - cmdBytes[2:], - } - var logs []*raft.Log - for i, b := range chunks { - chunkInfo := &raftchunkingtypes.ChunkInfo{ - OpNum: uint64(32), - SequenceNum: uint32(i), - NumChunks: uint32(len(chunks)), - } - chunkBytes, err := proto.Marshal(chunkInfo) - if err != nil { - t.Fatal(err) - } - logs = append(logs, &raft.Log{ - Term: uint64(i), - Data: b, - Extensions: chunkBytes, - }) - } - - // We should see nil for both - for _, log := range logs { - resp := b.fsm.chunker.Apply(log) - assert.Nil(resp) - } - - // Now verify the other baseline, that when the term doesn't change we see - // non-nil. First make the chunker have a clean state, then set the terms - // to be the same. - b.fsm.chunker.RestoreState(nil) - logs[1].Term = uint64(0) - - // We should see nil only for the first one - for i, log := range logs { - resp := b.fsm.chunker.Apply(log) - if i == 0 { - assert.Nil(resp) - } - if i == 1 { - assert.NotNil(resp) - _, ok := resp.(raftchunking.ChunkingSuccess) - assert.True(ok) - } - } -} - -func TestRaft_Chunking_AppliedIndex(t *testing.T) { - t.Parallel() - - raft, dir := GetRaft(t, true, false) - defer os.RemoveAll(dir) - - // Lower the size for tests - raftchunking.ChunkSize = 1024 - val, err := uuid.GenerateRandomBytes(3 * raftchunking.ChunkSize) - if err != nil { - t.Fatal(err) - } - - // Write a value to fastforward the index - err = raft.Put(context.Background(), &physical.Entry{ - Key: "key", - Value: []byte("test"), - }) - if err != nil { - t.Fatal(err) - } - - currentIndex := raft.AppliedIndex() - // Write some data - for i := 0; i < 10; i++ { - err := raft.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: val, - }) - if err != nil { - t.Fatal(err) - } - } - - newIndex := raft.AppliedIndex() - - // Each put should generate 4 chunks - if newIndex-currentIndex != 10*4 { - t.Fatalf("Did not apply chunks as expected, applied index = %d - %d = %d", newIndex, currentIndex, newIndex-currentIndex) - } - - for i := 0; i < 10; i++ { - entry, err := raft.Get(context.Background(), fmt.Sprintf("key-%d", i)) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(entry.Value, val) { - t.Fatal("value is corrupt") - } - } -} diff --git a/physical/raft/fsm_test.go b/physical/raft/fsm_test.go deleted file mode 100644 index 44557048b..000000000 --- a/physical/raft/fsm_test.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package raft - -import ( - "context" - "fmt" - "io/ioutil" - "math/rand" - "os" - "sort" - "testing" - - "github.com/go-test/deep" - "github.com/golang/protobuf/proto" - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/raft" - "github.com/hashicorp/vault/sdk/physical" -) - -func getFSM(t testing.TB) (*FSM, string) { - raftDir, err := ioutil.TempDir("", "vault-raft-") - if err != nil { - t.Fatal(err) - } - t.Logf("raft dir: %s", raftDir) - - logger := hclog.New(&hclog.LoggerOptions{ - Name: "raft", - Level: hclog.Trace, - }) - - fsm, err := NewFSM(raftDir, "", logger) - if err != nil { - t.Fatal(err) - } - - return fsm, raftDir -} - -func TestFSM_Batching(t *testing.T) { - fsm, dir := getFSM(t) - defer func() { _ = os.RemoveAll(dir) }() - - var index uint64 - var term uint64 = 1 - - getLog := func(i uint64) (int, *raft.Log) { - if rand.Intn(10) >= 8 { - term += 1 - return 0, &raft.Log{ - Index: i, - Term: term, - Type: raft.LogConfiguration, - Data: raft.EncodeConfiguration(raft.Configuration{ - Servers: []raft.Server{ - { - Address: "test", - ID: "test", - }, - }, - }), - } - } - - command := &LogData{ - Operations: make([]*LogOperation, rand.Intn(10)), - } - - for j := range command.Operations { - command.Operations[j] = &LogOperation{ - OpType: putOp, - Key: fmt.Sprintf("key-%d-%d", i, j), - Value: []byte(fmt.Sprintf("value-%d-%d", i, j)), - } - } - commandBytes, err := proto.Marshal(command) - if err != nil { - t.Fatal(err) - } - return len(command.Operations), &raft.Log{ - Index: i, - Term: term, - Type: raft.LogCommand, - Data: commandBytes, - } - } - - totalKeys := 0 - for i := 0; i < 100; i++ { - batchSize := rand.Intn(64) - batch := make([]*raft.Log, batchSize) - for j := 0; j < batchSize; j++ { - var keys int - index++ - keys, batch[j] = getLog(index) - totalKeys += keys - } - - resp := fsm.ApplyBatch(batch) - if len(resp) != batchSize { - t.Fatalf("incorrect response length: got %d expected %d", len(resp), batchSize) - } - - for _, r := range resp { - if _, ok := r.(*FSMApplyResponse); !ok { - t.Fatal("bad response type") - } - } - } - - keys, err := fsm.List(context.Background(), "") - if err != nil { - t.Fatal(err) - } - - if len(keys) != totalKeys { - t.Fatalf("incorrect number of keys: got %d expected %d", len(keys), totalKeys) - } - - latestIndex, latestConfig := fsm.LatestState() - if latestIndex.Index != index { - t.Fatalf("bad latest index: got %d expected %d", latestIndex.Index, index) - } - if latestIndex.Term != term { - t.Fatalf("bad latest term: got %d expected %d", latestIndex.Term, term) - } - - if latestConfig == nil && term > 1 { - t.Fatal("config wasn't updated") - } -} - -func TestFSM_List(t *testing.T) { - fsm, dir := getFSM(t) - defer func() { _ = os.RemoveAll(dir) }() - - ctx := context.Background() - count := 100 - keys := rand.Perm(count) - var sorted []string - for _, k := range keys { - err := fsm.Put(ctx, &physical.Entry{Key: fmt.Sprintf("foo/%d/bar", k)}) - if err != nil { - t.Fatal(err) - } - err = fsm.Put(ctx, &physical.Entry{Key: fmt.Sprintf("foo/%d/baz", k)}) - if err != nil { - t.Fatal(err) - } - sorted = append(sorted, fmt.Sprintf("%d/", k)) - } - sort.Strings(sorted) - - got, err := fsm.List(ctx, "foo/") - if err != nil { - t.Fatal(err) - } - sort.Strings(got) - if diff := deep.Equal(sorted, got); len(diff) > 0 { - t.Fatal(diff) - } -} diff --git a/physical/raft/raft_test.go b/physical/raft/raft_test.go deleted file mode 100644 index e8166ad16..000000000 --- a/physical/raft/raft_test.go +++ /dev/null @@ -1,764 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package raft - -import ( - "bytes" - "context" - "crypto/md5" - "encoding/base64" - "encoding/hex" - "fmt" - "io" - "io/ioutil" - "math/rand" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/golang/protobuf/proto" - bolt "github.com/hashicorp-forge/bbolt" - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-secure-stdlib/base62" - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/raft" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/physical" -) - -func connectPeers(nodes ...*RaftBackend) { - for _, node := range nodes { - for _, peer := range nodes { - if node == peer { - continue - } - - node.raftTransport.(*raft.InmemTransport).Connect(raft.ServerAddress(peer.NodeID()), peer.raftTransport) - peer.raftTransport.(*raft.InmemTransport).Connect(raft.ServerAddress(node.NodeID()), node.raftTransport) - } - } -} - -func stepDownLeader(t *testing.T, node *RaftBackend) { - t.Helper() - - if err := node.raft.LeadershipTransfer().Error(); err != nil { - t.Fatal(err) - } - - timeout := time.Now().Add(time.Second * 10) - for !time.Now().After(timeout) { - if err := node.raft.VerifyLeader().Error(); err != nil { - return - } - time.Sleep(100 * time.Millisecond) - } - - t.Fatal("still leader") -} - -func waitForLeader(t *testing.T, nodes ...*RaftBackend) *RaftBackend { - t.Helper() - timeout := time.Now().Add(time.Second * 10) - for !time.Now().After(timeout) { - for _, node := range nodes { - if node.raft.Leader() == raft.ServerAddress(node.NodeID()) { - return node - } - } - time.Sleep(100 * time.Millisecond) - } - - t.Fatal("no leader") - return nil -} - -func compareFSMs(t *testing.T, fsm1, fsm2 *FSM) { - t.Helper() - if err := compareFSMsWithErr(t, fsm1, fsm2); err != nil { - t.Fatal(err) - } -} - -func compareFSMsWithErr(t *testing.T, fsm1, fsm2 *FSM) error { - t.Helper() - index1, config1 := fsm1.LatestState() - index2, config2 := fsm2.LatestState() - - if !proto.Equal(index1, index2) { - return fmt.Errorf("indexes did not match: %+v != %+v", index1, index2) - } - if !proto.Equal(config1, config2) { - return fmt.Errorf("configs did not match: %+v != %+v", config1, config2) - } - - return compareDBs(t, fsm1.getDB(), fsm2.getDB(), false) -} - -func compareDBs(t *testing.T, boltDB1, boltDB2 *bolt.DB, dataOnly bool) error { - t.Helper() - db1 := make(map[string]string) - db2 := make(map[string]string) - - err := boltDB1.View(func(tx *bolt.Tx) error { - c := tx.Cursor() - for bucketName, _ := c.First(); bucketName != nil; bucketName, _ = c.Next() { - if dataOnly && !bytes.Equal(bucketName, dataBucketName) { - continue - } - - b := tx.Bucket(bucketName) - - cBucket := b.Cursor() - - for k, v := cBucket.First(); k != nil; k, v = cBucket.Next() { - db1[string(k)] = base64.StdEncoding.EncodeToString(v) - } - } - - return nil - }) - if err != nil { - t.Fatal(err) - } - - err = boltDB2.View(func(tx *bolt.Tx) error { - c := tx.Cursor() - for bucketName, _ := c.First(); bucketName != nil; bucketName, _ = c.Next() { - if dataOnly && !bytes.Equal(bucketName, dataBucketName) { - continue - } - b := tx.Bucket(bucketName) - - c := b.Cursor() - - for k, v := c.First(); k != nil; k, v = c.Next() { - db2[string(k)] = base64.StdEncoding.EncodeToString(v) - } - } - - return nil - }) - if err != nil { - t.Fatal(err) - } - - if diff := deep.Equal(db1, db2); diff != nil { - return fmt.Errorf("%+v", diff) - } - - return nil -} - -func TestRaft_Backend(t *testing.T) { - b, dir := GetRaft(t, true, true) - defer os.RemoveAll(dir) - - physical.ExerciseBackend(t, b) -} - -func TestRaft_ParseAutopilotUpgradeVersion(t *testing.T) { - raftDir, err := ioutil.TempDir("", "vault-raft-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(raftDir) - - conf := map[string]string{ - "path": raftDir, - "node_id": "abc123", - "autopilot_upgrade_version": "hahano", - } - - _, err = NewRaftBackend(conf, hclog.NewNullLogger()) - if err == nil { - t.Fatal("expected an error but got none") - } - - if !strings.Contains(err.Error(), "does not parse") { - t.Fatal("expected an error about unparseable versions but got none") - } -} - -func TestRaft_ParseNonVoter(t *testing.T) { - p := func(s string) *string { - return &s - } - - for _, retryJoinConf := range []string{"", "not-empty"} { - t.Run(retryJoinConf, func(t *testing.T) { - for name, tc := range map[string]struct { - envValue *string - configValue *string - expectNonVoter bool - invalidNonVoterValue bool - }{ - "valid false": {nil, p("false"), false, false}, - "valid true": {nil, p("true"), true, false}, - "invalid empty": {nil, p(""), false, true}, - "invalid truthy": {nil, p("no"), false, true}, - "invalid": {nil, p("totallywrong"), false, true}, - "valid env false": {p("false"), nil, true, false}, - "valid env true": {p("true"), nil, true, false}, - "valid env not boolean": {p("anything"), nil, true, false}, - "valid env empty": {p(""), nil, false, false}, - "neither set, default false": {nil, nil, false, false}, - "both set, env preferred": {p("true"), p("false"), true, false}, - } { - t.Run(name, func(t *testing.T) { - if tc.envValue != nil { - t.Setenv(EnvVaultRaftNonVoter, *tc.envValue) - } - raftDir, err := ioutil.TempDir("", "vault-raft-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(raftDir) - - conf := map[string]string{ - "path": raftDir, - "node_id": "abc123", - "retry_join": retryJoinConf, - } - if tc.configValue != nil { - conf[raftNonVoterConfigKey] = *tc.configValue - } - - backend, err := NewRaftBackend(conf, hclog.NewNullLogger()) - switch { - case tc.invalidNonVoterValue || (retryJoinConf == "" && tc.expectNonVoter): - if err == nil { - t.Fatal("expected an error but got none") - } - default: - if err != nil { - t.Fatalf("expected no error but got: %s", err) - } - - raftBackend := backend.(*RaftBackend) - if tc.expectNonVoter != raftBackend.NonVoter() { - t.Fatalf("expected %s %v but got %v", raftNonVoterConfigKey, tc.expectNonVoter, raftBackend.NonVoter()) - } - } - }) - } - }) - } -} - -func TestRaft_Backend_LargeKey(t *testing.T) { - b, dir := GetRaft(t, true, true) - defer os.RemoveAll(dir) - - key, err := base62.Random(bolt.MaxKeySize + 1) - if err != nil { - t.Fatal(err) - } - entry := &physical.Entry{Key: key, Value: []byte(key)} - - err = b.Put(context.Background(), entry) - if err == nil { - t.Fatal("expected error for put entry") - } - - if !strings.Contains(err.Error(), physical.ErrKeyTooLarge) { - t.Fatalf("expected %q, got %v", physical.ErrKeyTooLarge, err) - } - - out, err := b.Get(context.Background(), entry.Key) - if err != nil { - t.Fatalf("unexpected error after failed put: %v", err) - } - if out != nil { - t.Fatal("expected response entry to be nil after a failed put") - } -} - -func TestRaft_Backend_LargeValue(t *testing.T) { - b, dir := GetRaft(t, true, true) - defer os.RemoveAll(dir) - - value := make([]byte, defaultMaxEntrySize+1) - rand.Read(value) - entry := &physical.Entry{Key: "foo", Value: value} - - err := b.Put(context.Background(), entry) - if err == nil { - t.Fatal("expected error for put entry") - } - - if !strings.Contains(err.Error(), physical.ErrValueTooLarge) { - t.Fatalf("expected %q, got %v", physical.ErrValueTooLarge, err) - } - - out, err := b.Get(context.Background(), entry.Key) - if err != nil { - t.Fatalf("unexpected error after failed put: %v", err) - } - if out != nil { - t.Fatal("expected response entry to be nil after a failed put") - } -} - -// TestRaft_TransactionalBackend_GetTransactions tests that passing a slice of transactions to the -// raft backend will populate values for any transactions that are Get operations. -func TestRaft_TransactionalBackend_GetTransactions(t *testing.T) { - b, dir := GetRaft(t, true, true) - defer os.RemoveAll(dir) - - ctx := context.Background() - txns := make([]*physical.TxnEntry, 0) - - // Add some seed values to our FSM, and prepare our slice of transactions at the same time - for i := 0; i < 5; i++ { - key := fmt.Sprintf("foo/%d", i) - err := b.fsm.Put(ctx, &physical.Entry{Key: key, Value: []byte(fmt.Sprintf("value-%d", i))}) - if err != nil { - t.Fatal(err) - } - - txns = append(txns, &physical.TxnEntry{ - Operation: physical.GetOperation, - Entry: &physical.Entry{ - Key: key, - }, - }) - } - - // Add some additional transactions, so we have a mix of operations - for i := 0; i < 10; i++ { - txnEntry := &physical.TxnEntry{ - Entry: &physical.Entry{ - Key: fmt.Sprintf("lol-%d", i), - }, - } - - if i%2 == 0 { - txnEntry.Operation = physical.PutOperation - txnEntry.Entry.Value = []byte("lol") - } else { - txnEntry.Operation = physical.DeleteOperation - } - - txns = append(txns, txnEntry) - } - - err := b.Transaction(ctx, txns) - if err != nil { - t.Fatal(err) - } - - // Check that our Get operations were populated with their values - for i, txn := range txns { - if txn.Operation == physical.GetOperation { - val := []byte(fmt.Sprintf("value-%d", i)) - if !bytes.Equal(val, txn.Entry.Value) { - t.Fatalf("expected %s to equal %s but it didn't", hex.EncodeToString(val), hex.EncodeToString(txn.Entry.Value)) - } - } - } -} - -func TestRaft_TransactionalBackend_LargeKey(t *testing.T) { - b, dir := GetRaft(t, true, true) - defer os.RemoveAll(dir) - - value := make([]byte, defaultMaxEntrySize+1) - rand.Read(value) - - key, err := base62.Random(bolt.MaxKeySize + 1) - if err != nil { - t.Fatal(err) - } - txns := []*physical.TxnEntry{ - { - Operation: physical.PutOperation, - Entry: &physical.Entry{ - Key: key, - Value: []byte(key), - }, - }, - } - - err = b.Transaction(context.Background(), txns) - if err == nil { - t.Fatal("expected error for transactions") - } - - if !strings.Contains(err.Error(), physical.ErrKeyTooLarge) { - t.Fatalf("expected %q, got %v", physical.ErrValueTooLarge, err) - } - - out, err := b.Get(context.Background(), txns[0].Entry.Key) - if err != nil { - t.Fatalf("unexpected error after failed put: %v", err) - } - if out != nil { - t.Fatal("expected response entry to be nil after a failed put") - } -} - -func TestRaft_TransactionalBackend_LargeValue(t *testing.T) { - b, dir := GetRaft(t, true, true) - defer os.RemoveAll(dir) - - value := make([]byte, defaultMaxEntrySize+1) - rand.Read(value) - - txns := []*physical.TxnEntry{ - { - Operation: physical.PutOperation, - Entry: &physical.Entry{ - Key: "foo", - Value: value, - }, - }, - } - - err := b.Transaction(context.Background(), txns) - if err == nil { - t.Fatal("expected error for transactions") - } - - if !strings.Contains(err.Error(), physical.ErrValueTooLarge) { - t.Fatalf("expected %q, got %v", physical.ErrValueTooLarge, err) - } - - out, err := b.Get(context.Background(), txns[0].Entry.Key) - if err != nil { - t.Fatalf("unexpected error after failed put: %v", err) - } - if out != nil { - t.Fatal("expected response entry to be nil after a failed put") - } -} - -func TestRaft_Backend_ListPrefix(t *testing.T) { - b, dir := GetRaft(t, true, true) - defer os.RemoveAll(dir) - - physical.ExerciseBackend_ListPrefix(t, b) -} - -func TestRaft_TransactionalBackend(t *testing.T) { - b, dir := GetRaft(t, true, true) - defer os.RemoveAll(dir) - - physical.ExerciseTransactionalBackend(t, b) -} - -func TestRaft_HABackend(t *testing.T) { - t.Skip() - raft, dir := GetRaft(t, true, true) - defer os.RemoveAll(dir) - raft2, dir2 := GetRaft(t, false, true) - defer os.RemoveAll(dir2) - - // Add raft2 to the cluster - addPeer(t, raft, raft2) - - physical.ExerciseHABackend(t, raft, raft2) -} - -func TestRaft_Backend_ThreeNode(t *testing.T) { - raft1, dir := GetRaft(t, true, true) - raft2, dir2 := GetRaft(t, false, true) - raft3, dir3 := GetRaft(t, false, true) - defer os.RemoveAll(dir) - defer os.RemoveAll(dir2) - defer os.RemoveAll(dir3) - - // Add raft2 to the cluster - addPeer(t, raft1, raft2) - - // Add raft3 to the cluster - addPeer(t, raft1, raft3) - - physical.ExerciseBackend(t, raft1) - - time.Sleep(10 * time.Second) - // Make sure all stores are the same - compareFSMs(t, raft1.fsm, raft2.fsm) - compareFSMs(t, raft1.fsm, raft3.fsm) -} - -func TestRaft_GetOfflineConfig(t *testing.T) { - // Create 3 raft nodes - raft1, dir1 := GetRaft(t, true, true) - raft2, dir2 := GetRaft(t, false, true) - raft3, dir3 := GetRaft(t, false, true) - defer os.RemoveAll(dir1) - defer os.RemoveAll(dir2) - defer os.RemoveAll(dir3) - - // Add them all to the cluster - addPeer(t, raft1, raft2) - addPeer(t, raft1, raft3) - - // Add some data into the FSM - physical.ExerciseBackend(t, raft1) - - time.Sleep(10 * time.Second) - - // Spin down the raft cluster and check that GetConfigurationOffline - // returns 3 voters - raft3.TeardownCluster(nil) - raft2.TeardownCluster(nil) - raft1.TeardownCluster(nil) - - conf, err := raft1.GetConfigurationOffline() - if err != nil { - t.Fatal(err) - } - if len(conf.Servers) != 3 { - t.Fatalf("three raft nodes existed but we only see %d", len(conf.Servers)) - } - for _, s := range conf.Servers { - if s.Voter != true { - t.Fatalf("one of the nodes is not a voter") - } - } -} - -func TestRaft_Recovery(t *testing.T) { - // Create 4 raft nodes - raft1, dir1 := GetRaft(t, true, true) - raft2, dir2 := GetRaft(t, false, true) - raft3, dir3 := GetRaft(t, false, true) - raft4, dir4 := GetRaft(t, false, true) - defer os.RemoveAll(dir1) - defer os.RemoveAll(dir2) - defer os.RemoveAll(dir3) - defer os.RemoveAll(dir4) - - // Add them all to the cluster - addPeer(t, raft1, raft2) - addPeer(t, raft1, raft3) - addPeer(t, raft1, raft4) - - // Add some data into the FSM - physical.ExerciseBackend(t, raft1) - - time.Sleep(10 * time.Second) - - // Bring down all nodes - raft1.TeardownCluster(nil) - raft2.TeardownCluster(nil) - raft3.TeardownCluster(nil) - raft4.TeardownCluster(nil) - - // Prepare peers.json - type RecoveryPeer struct { - ID string `json:"id"` - Address string `json:"address"` - NonVoter bool `json:"non_voter"` - } - - // Leave out node 1 during recovery - peersList := make([]*RecoveryPeer, 0, 3) - peersList = append(peersList, &RecoveryPeer{ - ID: raft1.NodeID(), - Address: raft1.NodeID(), - NonVoter: false, - }) - peersList = append(peersList, &RecoveryPeer{ - ID: raft2.NodeID(), - Address: raft2.NodeID(), - NonVoter: false, - }) - peersList = append(peersList, &RecoveryPeer{ - ID: raft4.NodeID(), - Address: raft4.NodeID(), - NonVoter: false, - }) - - peersJSONBytes, err := jsonutil.EncodeJSON(peersList) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(filepath.Join(dir1, raftState), "peers.json"), peersJSONBytes, 0o644) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(filepath.Join(dir2, raftState), "peers.json"), peersJSONBytes, 0o644) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(filepath.Join(dir4, raftState), "peers.json"), peersJSONBytes, 0o644) - if err != nil { - t.Fatal(err) - } - - // Bring up the nodes again - raft1.SetupCluster(context.Background(), SetupOpts{}) - raft2.SetupCluster(context.Background(), SetupOpts{}) - raft4.SetupCluster(context.Background(), SetupOpts{}) - - peers, err := raft1.Peers(context.Background()) - if err != nil { - t.Fatal(err) - } - if len(peers) != 3 { - t.Fatalf("failed to recover the cluster") - } - - time.Sleep(10 * time.Second) - - compareFSMs(t, raft1.fsm, raft2.fsm) - compareFSMs(t, raft1.fsm, raft4.fsm) -} - -func TestRaft_TransactionalBackend_ThreeNode(t *testing.T) { - raft1, dir := GetRaft(t, true, true) - raft2, dir2 := GetRaft(t, false, true) - raft3, dir3 := GetRaft(t, false, true) - defer os.RemoveAll(dir) - defer os.RemoveAll(dir2) - defer os.RemoveAll(dir3) - - // Add raft2 to the cluster - addPeer(t, raft1, raft2) - - // Add raft3 to the cluster - addPeer(t, raft1, raft3) - - physical.ExerciseTransactionalBackend(t, raft1) - - time.Sleep(10 * time.Second) - // Make sure all stores are the same - compareFSMs(t, raft1.fsm, raft2.fsm) - compareFSMs(t, raft1.fsm, raft3.fsm) -} - -func TestRaft_Backend_Performance(t *testing.T) { - b, dir := GetRaft(t, true, false) - defer os.RemoveAll(dir) - - defaultConfig := raft.DefaultConfig() - - localConfig := raft.DefaultConfig() - b.applyConfigSettings(localConfig) - - if localConfig.ElectionTimeout != defaultConfig.ElectionTimeout*5 { - t.Fatalf("bad config: %v", localConfig) - } - if localConfig.HeartbeatTimeout != defaultConfig.HeartbeatTimeout*5 { - t.Fatalf("bad config: %v", localConfig) - } - if localConfig.LeaderLeaseTimeout != defaultConfig.LeaderLeaseTimeout*5 { - t.Fatalf("bad config: %v", localConfig) - } - - b.conf = map[string]string{ - "path": dir, - "performance_multiplier": "5", - } - - localConfig = raft.DefaultConfig() - b.applyConfigSettings(localConfig) - - if localConfig.ElectionTimeout != defaultConfig.ElectionTimeout*5 { - t.Fatalf("bad config: %v", localConfig) - } - if localConfig.HeartbeatTimeout != defaultConfig.HeartbeatTimeout*5 { - t.Fatalf("bad config: %v", localConfig) - } - if localConfig.LeaderLeaseTimeout != defaultConfig.LeaderLeaseTimeout*5 { - t.Fatalf("bad config: %v", localConfig) - } - - b.conf = map[string]string{ - "path": dir, - "performance_multiplier": "1", - } - - localConfig = raft.DefaultConfig() - b.applyConfigSettings(localConfig) - - if localConfig.ElectionTimeout != defaultConfig.ElectionTimeout { - t.Fatalf("bad config: %v", localConfig) - } - if localConfig.HeartbeatTimeout != defaultConfig.HeartbeatTimeout { - t.Fatalf("bad config: %v", localConfig) - } - if localConfig.LeaderLeaseTimeout != defaultConfig.LeaderLeaseTimeout { - t.Fatalf("bad config: %v", localConfig) - } -} - -func BenchmarkDB_Puts(b *testing.B) { - raft, dir := GetRaft(b, true, false) - defer os.RemoveAll(dir) - raft2, dir2 := GetRaft(b, true, false) - defer os.RemoveAll(dir2) - - bench := func(b *testing.B, s physical.Backend, dataSize int) { - data, err := uuid.GenerateRandomBytes(dataSize) - if err != nil { - b.Fatal(err) - } - - ctx := context.Background() - pe := &physical.Entry{ - Value: data, - } - testName := b.Name() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - pe.Key = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s-%d", testName, i)))) - err := s.Put(ctx, pe) - if err != nil { - b.Fatal(err) - } - } - } - - b.Run("256b", func(b *testing.B) { bench(b, raft, 256) }) - b.Run("256kb", func(b *testing.B) { bench(b, raft2, 256*1024) }) -} - -func BenchmarkDB_Snapshot(b *testing.B) { - raft, dir := GetRaft(b, true, false) - defer os.RemoveAll(dir) - - data, err := uuid.GenerateRandomBytes(256 * 1024) - if err != nil { - b.Fatal(err) - } - - ctx := context.Background() - pe := &physical.Entry{ - Value: data, - } - testName := b.Name() - - for i := 0; i < 100; i++ { - pe.Key = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s-%d", testName, i)))) - err = raft.Put(ctx, pe) - if err != nil { - b.Fatal(err) - } - } - - bench := func(b *testing.B, s *FSM) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - pe.Key = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s-%d", testName, i)))) - s.writeTo(ctx, discardCloser{Writer: ioutil.Discard}, discardCloser{Writer: ioutil.Discard}) - } - } - - b.Run("256kb", func(b *testing.B) { bench(b, raft.fsm) }) -} - -type discardCloser struct { - io.Writer -} - -func (d discardCloser) Close() error { return nil } -func (d discardCloser) CloseWithError(error) error { return nil } diff --git a/physical/raft/snapshot_test.go b/physical/raft/snapshot_test.go deleted file mode 100644 index d85af4da5..000000000 --- a/physical/raft/snapshot_test.go +++ /dev/null @@ -1,960 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package raft - -import ( - "bytes" - "context" - "fmt" - "hash/crc64" - "io" - "io/ioutil" - "net/http/httptest" - "os" - "path/filepath" - "reflect" - "runtime" - "testing" - "time" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/raft" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/physical" - "github.com/hashicorp/vault/sdk/plugin/pb" -) - -type idAddr struct { - id string -} - -func (a *idAddr) Network() string { return "inmem" } -func (a *idAddr) String() string { return a.id } - -func addPeer(t *testing.T, leader, follower *RaftBackend) { - t.Helper() - if err := leader.AddPeer(context.Background(), follower.NodeID(), follower.NodeID()); err != nil { - t.Fatal(err) - } - - peers, err := leader.Peers(context.Background()) - if err != nil { - t.Fatal(err) - } - - err = follower.Bootstrap(peers) - if err != nil { - t.Fatal(err) - } - - err = follower.SetupCluster(context.Background(), SetupOpts{}) - if err != nil { - t.Fatal(err) - } - - leader.raftTransport.(*raft.InmemTransport).Connect(raft.ServerAddress(follower.NodeID()), follower.raftTransport) - follower.raftTransport.(*raft.InmemTransport).Connect(raft.ServerAddress(leader.NodeID()), leader.raftTransport) -} - -func TestRaft_Snapshot_Loading(t *testing.T) { - raft, dir := GetRaft(t, true, false) - defer os.RemoveAll(dir) - - // Write some data - for i := 0; i < 1000; i++ { - err := raft.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - t.Fatal(err) - } - } - - readCloser, writeCloser := io.Pipe() - metaReadCloser, metaWriteCloser := io.Pipe() - - go func() { - raft.fsm.writeTo(context.Background(), metaWriteCloser, writeCloser) - }() - - // Create a CRC64 hash - stateHash := crc64.New(crc64.MakeTable(crc64.ECMA)) - - // Compute the hash - size1, err := io.Copy(stateHash, metaReadCloser) - if err != nil { - t.Fatal(err) - } - - computed1 := stateHash.Sum(nil) - - // Create a CRC64 hash - stateHash = crc64.New(crc64.MakeTable(crc64.ECMA)) - - // Compute the hash - size2, err := io.Copy(stateHash, readCloser) - if err != nil { - t.Fatal(err) - } - - computed2 := stateHash.Sum(nil) - - if size1 != size2 { - t.Fatal("sizes did not match") - } - - if !bytes.Equal(computed1, computed2) { - t.Fatal("hashes did not match") - } - - snapFuture := raft.raft.Snapshot() - if err := snapFuture.Error(); err != nil { - t.Fatal(err) - } - - meta, reader, err := snapFuture.Open() - if err != nil { - t.Fatal(err) - } - if meta.Size != size1 { - t.Fatal("meta size did not match expected") - } - - // Create a CRC64 hash - stateHash = crc64.New(crc64.MakeTable(crc64.ECMA)) - - // Compute the hash - size3, err := io.Copy(stateHash, reader) - if err != nil { - t.Fatal(err) - } - - computed3 := stateHash.Sum(nil) - if size1 != size3 { - t.Fatal("sizes did not match") - } - - if !bytes.Equal(computed1, computed3) { - t.Fatal("hashes did not match") - } -} - -func TestRaft_Snapshot_Index(t *testing.T) { - raft, dir := GetRaft(t, true, false) - defer os.RemoveAll(dir) - - err := raft.Put(context.Background(), &physical.Entry{ - Key: "key", - Value: []byte("value"), - }) - if err != nil { - t.Fatal(err) - } - - // Get index - index, _ := raft.fsm.LatestState() - if index.Term != 2 { - t.Fatalf("unexpected term, got %d expected 2", index.Term) - } - if index.Index != 3 { - t.Fatalf("unexpected index, got %d expected 3", index.Term) - } - - // Write some data - for i := 0; i < 100; i++ { - err := raft.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - t.Fatal(err) - } - } - - // Get index - index, _ = raft.fsm.LatestState() - if index.Term != 2 { - t.Fatalf("unexpected term, got %d expected 2", index.Term) - } - if index.Index != 103 { - t.Fatalf("unexpected index, got %d expected 103", index.Term) - } - - // Take a snapshot - snapFuture := raft.raft.Snapshot() - if err := snapFuture.Error(); err != nil { - t.Fatal(err) - } - - meta, reader, err := snapFuture.Open() - if err != nil { - t.Fatal(err) - } - io.Copy(ioutil.Discard, reader) - - if meta.Index != index.Index { - t.Fatalf("indexes did not match, got %d expected %d", meta.Index, index.Index) - } - if meta.Term != index.Term { - t.Fatalf("term did not match, got %d expected %d", meta.Term, index.Term) - } - - // Write some more data - for i := 0; i < 100; i++ { - err := raft.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - t.Fatal(err) - } - } - - // Open the same snapshot again - meta, reader, err = raft.snapStore.Open(meta.ID) - if err != nil { - t.Fatal(err) - } - io.Copy(ioutil.Discard, reader) - - // Make sure the meta data has updated to the new values - if meta.Index != 203 { - t.Fatalf("unexpected snapshot index %d", meta.Index) - } - if meta.Term != 2 { - t.Fatalf("unexpected snapshot term %d", meta.Term) - } -} - -func TestRaft_Snapshot_Peers(t *testing.T) { - raft1, dir := GetRaft(t, true, false) - raft2, dir2 := GetRaft(t, false, false) - raft3, dir3 := GetRaft(t, false, false) - defer os.RemoveAll(dir) - defer os.RemoveAll(dir2) - defer os.RemoveAll(dir3) - - // Write some data - for i := 0; i < 1000; i++ { - err := raft1.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - t.Fatal(err) - } - } - - // Force a snapshot - snapFuture := raft1.raft.Snapshot() - if err := snapFuture.Error(); err != nil { - t.Fatal(err) - } - - commitIdx := raft1.CommittedIndex() - - // Add raft2 to the cluster - addPeer(t, raft1, raft2) - - ensureCommitApplied(t, commitIdx, raft2) - - // Make sure the snapshot was applied correctly on the follower - if err := compareDBs(t, raft1.fsm.getDB(), raft2.fsm.getDB(), false); err != nil { - t.Fatal(err) - } - - // Write some more data - for i := 1000; i < 2000; i++ { - err := raft1.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - t.Fatal(err) - } - } - - snapFuture = raft1.raft.Snapshot() - if err := snapFuture.Error(); err != nil { - t.Fatal(err) - } - - commitIdx = raft1.CommittedIndex() - - // Add raft3 to the cluster - addPeer(t, raft1, raft3) - - ensureCommitApplied(t, commitIdx, raft2) - ensureCommitApplied(t, commitIdx, raft3) - - // Make sure all stores are the same - compareFSMs(t, raft1.fsm, raft2.fsm) - compareFSMs(t, raft1.fsm, raft3.fsm) -} - -func ensureCommitApplied(t *testing.T, leaderCommitIdx uint64, backend *RaftBackend) { - t.Helper() - - timeout := time.Now().Add(10 * time.Second) - for { - if time.Now().After(timeout) { - t.Fatal("timeout reached while verifying applied index on raft backend") - } - - if backend.AppliedIndex() >= leaderCommitIdx { - break - } - - time.Sleep(1 * time.Second) - } -} - -func TestRaft_Snapshot_Restart(t *testing.T) { - raft1, dir := GetRaft(t, true, false) - defer os.RemoveAll(dir) - raft2, dir2 := GetRaft(t, false, false) - defer os.RemoveAll(dir2) - - // Write some data - for i := 0; i < 100; i++ { - err := raft1.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - t.Fatal(err) - } - } - - // Take a snapshot - snapFuture := raft1.raft.Snapshot() - if err := snapFuture.Error(); err != nil { - t.Fatal(err) - } - // Advance FSM's index past configuration change - raft1.Put(context.Background(), &physical.Entry{ - Key: "key", - Value: []byte("value"), - }) - - // Add raft2 to the cluster - addPeer(t, raft1, raft2) - - time.Sleep(2 * time.Second) - - peers, err := raft2.Peers(context.Background()) - if err != nil { - t.Fatal(err) - } - if len(peers) != 2 { - t.Fatal(peers) - } - - // Finalize raft1 - if err := raft1.TeardownCluster(nil); err != nil { - t.Fatal(err) - } - - // Start Raft - err = raft1.SetupCluster(context.Background(), SetupOpts{}) - if err != nil { - t.Fatal(err) - } - - peers, err = raft1.Peers(context.Background()) - if err != nil { - t.Fatal(err) - } - if len(peers) != 2 { - t.Fatal(peers) - } - - compareFSMs(t, raft1.fsm, raft2.fsm) -} - -/* -func TestRaft_Snapshot_ErrorRecovery(t *testing.T) { - raft1, dir := GetRaft(t, true, false) - raft2, dir2 := GetRaft(t, false, false) - raft3, dir3 := GetRaft(t, false, false) - defer os.RemoveAll(dir) - defer os.RemoveAll(dir2) - defer os.RemoveAll(dir3) - - // Add raft2 to the cluster - addPeer(t, raft1, raft2) - - // Write some data - for i := 0; i < 100; i++ { - err := raft1.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - t.Fatal(err) - } - } - - // Take a snapshot on each node to ensure we no longer have older logs - snapFuture := raft1.raft.Snapshot() - if err := snapFuture.Error(); err != nil { - t.Fatal(err) - } - - stepDownLeader(t, raft1) - leader := waitForLeader(t, raft1, raft2) - - snapFuture = leader.raft.Snapshot() - if err := snapFuture.Error(); err != nil { - t.Fatal(err) - } - - // Advance FSM's index past snapshot index - leader.Put(context.Background(), &physical.Entry{ - Key: "key", - Value: []byte("value"), - }) - - // Error on snapshot restore - raft3.fsm.testSnapshotRestoreError = true - - // Add raft3 to the cluster - addPeer(t, leader, raft3) - - time.Sleep(2 * time.Second) - - // Restart the failing node to make sure fresh state does not have invalid - // values. - if err := raft3.TeardownCluster(nil); err != nil { - t.Fatal(err) - } - - // Ensure the databases are not equal - if err := compareFSMsWithErr(t, leader.fsm, raft3.fsm); err == nil { - t.Fatal("nil error") - } - - // Remove error and make sure we can reconcile state - raft3.fsm.testSnapshotRestoreError = false - - // Step down leader node - stepDownLeader(t, leader) - leader = waitForLeader(t, raft1, raft2) - - // Start Raft3 - if err := raft3.SetupCluster(context.Background(), SetupOpts{}); err != nil { - t.Fatal(err) - } - - connectPeers(raft1, raft2, raft3) - waitForLeader(t, raft1, raft2) - - time.Sleep(5 * time.Second) - - // Make sure state gets re-replicated. - compareFSMs(t, raft1.fsm, raft3.fsm) -}*/ - -func TestRaft_Snapshot_Take_Restore(t *testing.T) { - raft1, dir := GetRaft(t, true, false) - defer os.RemoveAll(dir) - raft2, dir2 := GetRaft(t, false, false) - defer os.RemoveAll(dir2) - - addPeer(t, raft1, raft2) - - // Write some data - for i := 0; i < 100; i++ { - err := raft1.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - t.Fatal(err) - } - } - - recorder := httptest.NewRecorder() - snap := logical.NewHTTPResponseWriter(recorder) - - err := raft1.Snapshot(snap, nil) - if err != nil { - t.Fatal(err) - } - - // Write some more data - for i := 100; i < 200; i++ { - err := raft1.Put(context.Background(), &physical.Entry{ - Key: fmt.Sprintf("key-%d", i), - Value: []byte(fmt.Sprintf("value-%d", i)), - }) - if err != nil { - t.Fatal(err) - } - } - - snapFile, cleanup, metadata, err := raft1.WriteSnapshotToTemp(ioutil.NopCloser(recorder.Body), nil) - if err != nil { - t.Fatal(err) - } - defer cleanup() - - err = raft1.RestoreSnapshot(context.Background(), metadata, snapFile) - if err != nil { - t.Fatal(err) - } - - // make sure we don't have the second batch of writes - for i := 100; i < 200; i++ { - { - value, err := raft1.Get(context.Background(), fmt.Sprintf("key-%d", i)) - if err != nil { - t.Fatal(err) - } - if value != nil { - t.Fatal("didn't remove data") - } - } - { - value, err := raft2.Get(context.Background(), fmt.Sprintf("key-%d", i)) - if err != nil { - t.Fatal(err) - } - if value != nil { - t.Fatal("didn't remove data") - } - } - } - - time.Sleep(10 * time.Second) - compareFSMs(t, raft1.fsm, raft2.fsm) -} - -func TestBoltSnapshotStore_CreateSnapshotMissingParentDir(t *testing.T) { - parent, err := ioutil.TempDir("", "raft") - if err != nil { - t.Fatalf("err: %v ", err) - } - defer os.RemoveAll(parent) - - dir, err := ioutil.TempDir(parent, "raft") - if err != nil { - t.Fatalf("err: %v ", err) - } - - logger := hclog.New(&hclog.LoggerOptions{ - Name: "raft", - Level: hclog.Trace, - }) - - snap, err := NewBoltSnapshotStore(dir, logger, nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - os.RemoveAll(parent) - _, trans := raft.NewInmemTransport(raft.NewInmemAddr()) - sink, err := snap.Create(raft.SnapshotVersionMax, 10, 3, raft.Configuration{}, 0, trans) - if err != nil { - t.Fatal(err) - } - defer sink.Cancel() - - _, err = sink.Write([]byte("test")) - if err != nil { - t.Fatalf("should not fail when using non existing parent: %s", err) - } - - // Ensure the snapshot file exists - _, err = os.Stat(filepath.Join(snap.path, sink.ID()+tmpSuffix, databaseFilename)) - if err != nil { - t.Fatal(err) - } -} - -func TestBoltSnapshotStore_Listing(t *testing.T) { - // Create a test dir - parent, err := ioutil.TempDir("", "raft") - if err != nil { - t.Fatalf("err: %v ", err) - } - defer os.RemoveAll(parent) - - dir, err := ioutil.TempDir(parent, "raft") - if err != nil { - t.Fatalf("err: %v ", err) - } - - logger := hclog.New(&hclog.LoggerOptions{ - Name: "raft", - Level: hclog.Trace, - }) - - fsm, err := NewFSM(parent, "", logger) - if err != nil { - t.Fatal(err) - } - - snap, err := NewBoltSnapshotStore(dir, logger, fsm) - if err != nil { - t.Fatalf("err: %v", err) - } - - // FSM has no data, should have empty snapshot list - snaps, err := snap.List() - if err != nil { - t.Fatalf("err: %v", err) - } - if len(snaps) != 0 { - t.Fatalf("expect 0 snapshots: %v", snaps) - } - - // Move the fsm forward - err = fsm.witnessSnapshot(&raft.SnapshotMeta{ - Index: 100, - Term: 20, - Configuration: raft.Configuration{}, - ConfigurationIndex: 0, - }) - if err != nil { - t.Fatal(err) - } - - snaps, err = snap.List() - if err != nil { - t.Fatal(err) - } - if len(snaps) != 1 { - t.Fatalf("expect 1 snapshots: %v", snaps) - } - - if snaps[0].Index != 100 || snaps[0].Term != 20 { - t.Fatalf("bad snapshot: %+v", snaps[0]) - } - - if snaps[0].ID != boltSnapshotID { - t.Fatalf("bad snapshot: %+v", snaps[0]) - } -} - -func TestBoltSnapshotStore_CreateInstallSnapshot(t *testing.T) { - // Create a test dir - parent, err := ioutil.TempDir("", "raft") - if err != nil { - t.Fatalf("err: %v ", err) - } - defer os.RemoveAll(parent) - - dir, err := ioutil.TempDir(parent, "raft") - if err != nil { - t.Fatalf("err: %v ", err) - } - - logger := hclog.New(&hclog.LoggerOptions{ - Name: "raft", - Level: hclog.Trace, - }) - - fsm, err := NewFSM(parent, "", logger) - if err != nil { - t.Fatal(err) - } - defer fsm.Close() - - snap, err := NewBoltSnapshotStore(dir, logger, fsm) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Check no snapshots - snaps, err := snap.List() - if err != nil { - t.Fatalf("err: %v", err) - } - if len(snaps) != 0 { - t.Fatalf("did not expect any snapshots: %v", snaps) - } - - // Create a new sink - var configuration raft.Configuration - configuration.Servers = append(configuration.Servers, raft.Server{ - Suffrage: raft.Voter, - ID: raft.ServerID("my id"), - Address: raft.ServerAddress("over here"), - }) - _, trans := raft.NewInmemTransport(raft.NewInmemAddr()) - sink, err := snap.Create(raft.SnapshotVersionMax, 10, 3, configuration, 2, trans) - if err != nil { - t.Fatalf("err: %v", err) - } - - protoWriter := NewDelimitedWriter(sink) - - err = fsm.Put(context.Background(), &physical.Entry{ - Key: "test-key", - Value: []byte("test-value"), - }) - if err != nil { - t.Fatal(err) - } - - err = fsm.Put(context.Background(), &physical.Entry{ - Key: "test-key1", - Value: []byte("test-value1"), - }) - if err != nil { - t.Fatal(err) - } - - // Write to the sink - err = protoWriter.WriteMsg(&pb.StorageEntry{ - Key: "test-key", - Value: []byte("test-value"), - }) - if err != nil { - t.Fatalf("err: %v", err) - } - err = protoWriter.WriteMsg(&pb.StorageEntry{ - Key: "test-key1", - Value: []byte("test-value1"), - }) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Done! - err = sink.Close() - if err != nil { - t.Fatalf("err: %v", err) - } - - // Read the snapshot - meta, r, err := snap.Open(sink.ID()) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Check the latest - if meta.Index != 10 { - t.Fatalf("bad snapshot: %+v", meta) - } - if meta.Term != 3 { - t.Fatalf("bad snapshot: %+v", meta) - } - if !reflect.DeepEqual(meta.Configuration, configuration) { - t.Fatalf("bad snapshot: %+v", meta) - } - if meta.ConfigurationIndex != 2 { - t.Fatalf("bad snapshot: %+v", meta) - } - - installer, ok := r.(*boltSnapshotInstaller) - if !ok { - t.Fatal("expected snapshot installer object") - } - - newFSM, err := NewFSM(filepath.Dir(installer.Filename()), "", logger) - if err != nil { - t.Fatal(err) - } - - err = compareDBs(t, fsm.getDB(), newFSM.getDB(), true) - if err != nil { - t.Fatal(err) - } - - // Make sure config data is different - err = compareDBs(t, fsm.getDB(), newFSM.getDB(), false) - if err == nil { - t.Fatal("expected error") - } - - if err := newFSM.Close(); err != nil { - t.Fatal(err) - } - - err = fsm.Restore(installer) - if err != nil { - t.Fatal(err) - } - - for i := 0; i < 2; i++ { - latestIndex, latestConfigRaw := fsm.LatestState() - latestConfigIndex, latestConfig := protoConfigurationToRaftConfiguration(latestConfigRaw) - if latestIndex.Index != 10 { - t.Fatalf("bad install: %+v", latestIndex) - } - if latestIndex.Term != 3 { - t.Fatalf("bad install: %+v", latestIndex) - } - if !reflect.DeepEqual(latestConfig, configuration) { - t.Fatalf("bad install: %+v", latestConfig) - } - if latestConfigIndex != 2 { - t.Fatalf("bad install: %+v", latestConfigIndex) - } - - v, err := fsm.Get(context.Background(), "test-key") - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(v.Value, []byte("test-value")) { - t.Fatalf("bad: %+v", v) - } - - v, err = fsm.Get(context.Background(), "test-key1") - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(v.Value, []byte("test-value1")) { - t.Fatalf("bad: %+v", v) - } - - // Close/Reopen the db and make sure we still match - fsm.Close() - fsm, err = NewFSM(parent, "", logger) - if err != nil { - t.Fatal(err) - } - } -} - -func TestBoltSnapshotStore_CancelSnapshot(t *testing.T) { - // Create a test dir - dir, err := ioutil.TempDir("", "raft") - if err != nil { - t.Fatalf("err: %v ", err) - } - defer os.RemoveAll(dir) - - logger := hclog.New(&hclog.LoggerOptions{ - Name: "raft", - Level: hclog.Trace, - }) - - snap, err := NewBoltSnapshotStore(dir, logger, nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - _, trans := raft.NewInmemTransport(raft.NewInmemAddr()) - sink, err := snap.Create(raft.SnapshotVersionMax, 10, 3, raft.Configuration{}, 0, trans) - if err != nil { - t.Fatal(err) - } - _, err = sink.Write([]byte("test")) - if err != nil { - t.Fatalf("should not fail when using non existing parent: %s", err) - } - - // Ensure the snapshot file exists - _, err = os.Stat(filepath.Join(snap.path, sink.ID()+tmpSuffix, databaseFilename)) - if err != nil { - t.Fatal(err) - } - - // Cancel the snapshot! Should delete - err = sink.Cancel() - if err != nil { - t.Fatalf("err: %v", err) - } - - // Ensure the snapshot file does not exist - _, err = os.Stat(filepath.Join(snap.path, sink.ID()+tmpSuffix, databaseFilename)) - if !os.IsNotExist(err) { - t.Fatal(err) - } - - // Make sure future writes fail - _, err = sink.Write([]byte("test")) - if err == nil { - t.Fatal("expected write to fail") - } -} - -func TestBoltSnapshotStore_BadPerm(t *testing.T) { - var err error - if runtime.GOOS == "windows" { - t.Skip("skipping file permission test on windows") - } - - // Create a temp dir - var dir1 string - dir1, err = ioutil.TempDir("", "raft") - if err != nil { - t.Fatalf("err: %s", err) - } - defer os.RemoveAll(dir1) - - // Create a sub dir and remove all permissions - var dir2 string - dir2, err = ioutil.TempDir(dir1, "badperm") - if err != nil { - t.Fatalf("err: %s", err) - } - if err = os.Chmod(dir2, 0o00); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chmod(dir2, 777) // Set perms back for delete - - logger := hclog.New(&hclog.LoggerOptions{ - Name: "raft", - Level: hclog.Trace, - }) - - _, err = NewBoltSnapshotStore(dir2, logger, nil) - if err == nil { - t.Fatalf("should fail to use dir with bad perms") - } -} - -func TestBoltSnapshotStore_CloseFailure(t *testing.T) { - // Create a test dir - dir, err := ioutil.TempDir("", "raft") - if err != nil { - t.Fatalf("err: %v ", err) - } - defer os.RemoveAll(dir) - - logger := hclog.New(&hclog.LoggerOptions{ - Name: "raft", - Level: hclog.Trace, - }) - - snap, err := NewBoltSnapshotStore(dir, logger, nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - _, trans := raft.NewInmemTransport(raft.NewInmemAddr()) - sink, err := snap.Create(raft.SnapshotVersionMax, 10, 3, raft.Configuration{}, 0, trans) - if err != nil { - t.Fatal(err) - } - - // This should stash an error value - _, err = sink.Write([]byte("test")) - if err != nil { - t.Fatalf("should not fail when using non existing parent: %s", err) - } - - // Cancel the snapshot! Should delete - err = sink.Close() - if err == nil { - t.Fatalf("expected error") - } - - // Ensure the snapshot file does not exist - _, err = os.Stat(filepath.Join(snap.path, sink.ID()+tmpSuffix, databaseFilename)) - if !os.IsNotExist(err) { - t.Fatal(err) - } - - // Make sure future writes fail - _, err = sink.Write([]byte("test")) - if err == nil { - t.Fatal("expected write to fail") - } -} diff --git a/physical/raft/streamlayer_test.go b/physical/raft/streamlayer_test.go deleted file mode 100644 index bc35eb66f..000000000 --- a/physical/raft/streamlayer_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package raft - -import ( - "context" - "crypto/rand" - "crypto/tls" - "net" - "testing" - "time" - - "github.com/hashicorp/vault/vault/cluster" -) - -type mockClusterHook struct { - address net.Addr -} - -func (*mockClusterHook) AddClient(alpn string, client cluster.Client) {} -func (*mockClusterHook) RemoveClient(alpn string) {} -func (*mockClusterHook) AddHandler(alpn string, handler cluster.Handler) {} -func (*mockClusterHook) StopHandler(alpn string) {} -func (*mockClusterHook) TLSConfig(ctx context.Context) (*tls.Config, error) { return nil, nil } -func (m *mockClusterHook) Addr() net.Addr { return m.address } -func (*mockClusterHook) GetDialerFunc(ctx context.Context, alpnProto string) func(string, time.Duration) (net.Conn, error) { - return func(string, time.Duration) (net.Conn, error) { - return nil, nil - } -} - -func TestStreamLayer_UnspecifiedIP(t *testing.T) { - m := &mockClusterHook{ - address: &cluster.NetAddr{ - Host: "0.0.0.0:8200", - }, - } - - raftTLSKey, err := GenerateTLSKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - - raftTLS := &TLSKeyring{ - Keys: []*TLSKey{raftTLSKey}, - ActiveKeyID: raftTLSKey.ID, - } - - layer, err := NewRaftLayer(nil, raftTLS, m) - if err == nil { - t.Fatal("expected error") - } - - if err.Error() != "cannot use unspecified IP with raft storage: 0.0.0.0:8200" { - t.Fatalf("unexpected error: %s", err.Error()) - } - - if layer != nil { - t.Fatal("expected nil layer") - } - - m.address.(*cluster.NetAddr).Host = "10.0.0.1:8200" - - layer, err = NewRaftLayer(nil, raftTLS, m) - if err != nil { - t.Fatal(err) - } - - if layer == nil { - t.Fatal("nil layer") - } -} diff --git a/physical/raft/testing.go b/physical/raft/testing.go deleted file mode 100644 index 30639704b..000000000 --- a/physical/raft/testing.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package raft - -import ( - "context" - "fmt" - "io" - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-uuid" -) - -func GetRaft(t testing.TB, bootstrap bool, noStoreState bool) (*RaftBackend, string) { - return getRaftInternal(t, bootstrap, defaultRaftConfig(t, bootstrap, noStoreState), nil, nil) -} - -func GetRaftWithConfig(t testing.TB, bootstrap bool, noStoreState bool, conf map[string]string) (*RaftBackend, string) { - defaultConf := defaultRaftConfig(t, bootstrap, noStoreState) - conf["path"] = defaultConf["path"] - conf["doNotStoreLatestState"] = defaultConf["doNotStoreLatestState"] - return getRaftInternal(t, bootstrap, conf, nil, nil) -} - -func defaultRaftConfig(t testing.TB, bootstrap bool, noStoreState bool) map[string]string { - raftDir := t.TempDir() - t.Logf("raft dir: %s", raftDir) - - conf := map[string]string{ - "path": raftDir, - "trailing_logs": "100", - } - - if noStoreState { - conf["doNotStoreLatestState"] = "" - } - - return conf -} - -func getRaftInternal(t testing.TB, bootstrap bool, conf map[string]string, logOutput io.Writer, initFn func(b *RaftBackend)) (*RaftBackend, string) { - id, err := uuid.GenerateUUID() - if err != nil { - t.Fatal(err) - } - - logger := hclog.New(&hclog.LoggerOptions{ - Name: fmt.Sprintf("raft-%s", id), - Level: hclog.Trace, - Output: logOutput, - }) - - conf["node_id"] = id - - backendRaw, err := NewRaftBackend(conf, logger) - if err != nil { - t.Fatal(err) - } - backend := backendRaw.(*RaftBackend) - if initFn != nil { - initFn(backend) - } - - if bootstrap { - err = backend.Bootstrap([]Peer{ - { - ID: backend.NodeID(), - Address: backend.NodeID(), - }, - }) - if err != nil { - t.Fatal(err) - } - - err = backend.SetupCluster(context.Background(), SetupOpts{}) - if err != nil { - t.Fatal(err) - } - - for { - if backend.raft.AppliedIndex() >= 2 { - break - } - } - - } - - backend.DisableAutopilot() - return backend, conf["path"] -} diff --git a/plugins/database/cassandra/cassandra_test.go b/plugins/database/cassandra/cassandra_test.go deleted file mode 100644 index 9162c467a..000000000 --- a/plugins/database/cassandra/cassandra_test.go +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cassandra - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/require" - - backoff "github.com/cenkalti/backoff/v3" - "github.com/gocql/gocql" - "github.com/hashicorp/vault/helper/testhelpers/cassandra" - dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing" -) - -func getCassandra(t *testing.T, protocolVersion interface{}) (*Cassandra, func()) { - host, cleanup := cassandra.PrepareTestContainer(t, - cassandra.Version("3.11"), - cassandra.CopyFromTo(insecureFileMounts), - ) - - db := new() - initReq := dbplugin.InitializeRequest{ - Config: map[string]interface{}{ - "hosts": host.ConnectionURL(), - "port": host.Port, - "username": "cassandra", - "password": "cassandra", - "protocol_version": protocolVersion, - "connect_timeout": "20s", - }, - VerifyConnection: true, - } - - expectedConfig := map[string]interface{}{ - "hosts": host.ConnectionURL(), - "port": host.Port, - "username": "cassandra", - "password": "cassandra", - "protocol_version": protocolVersion, - "connect_timeout": "20s", - } - - initResp := dbtesting.AssertInitialize(t, db, initReq) - if !reflect.DeepEqual(initResp.Config, expectedConfig) { - t.Fatalf("Initialize response config actual: %#v\nExpected: %#v", initResp.Config, expectedConfig) - } - - if !db.Initialized { - t.Fatal("Database should be initialized") - } - return db, cleanup -} - -func TestInitialize(t *testing.T) { - t.Run("integer protocol version", func(t *testing.T) { - // getCassandra performs an Initialize call - db, cleanup := getCassandra(t, 4) - t.Cleanup(cleanup) - - err := db.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - }) - - t.Run("string protocol version", func(t *testing.T) { - // getCassandra performs an Initialize call - db, cleanup := getCassandra(t, "4") - t.Cleanup(cleanup) - - err := db.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - }) -} - -func TestCreateUser(t *testing.T) { - type testCase struct { - // Config will have the hosts & port added to it during the test - config map[string]interface{} - newUserReq dbplugin.NewUserRequest - expectErr bool - expectedUsernameRegex string - assertCreds func(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions, timeout time.Duration) - } - - tests := map[string]testCase{ - "default username_template": { - config: map[string]interface{}{ - "username": "cassandra", - "password": "cassandra", - "protocol_version": "4", - "connect_timeout": "20s", - }, - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "token", - RoleName: "mylongrolenamewithmanycharacters", - }, - Statements: dbplugin.Statements{ - Commands: []string{createUserStatements}, - }, - Password: "bfn985wjAHIh6t", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: false, - expectedUsernameRegex: `^v_token_mylongrolenamew_[a-z0-9]{20}_[0-9]{10}$`, - assertCreds: assertCreds, - }, - "custom username_template": { - config: map[string]interface{}{ - "username": "cassandra", - "password": "cassandra", - "protocol_version": "4", - "connect_timeout": "20s", - "username_template": `foo_{{random 20}}_{{.RoleName | replace "e" "3"}}_{{unix_time}}`, - }, - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "token", - RoleName: "mylongrolenamewithmanycharacters", - }, - Statements: dbplugin.Statements{ - Commands: []string{createUserStatements}, - }, - Password: "bfn985wjAHIh6t", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: false, - expectedUsernameRegex: `^foo_[a-zA-Z0-9]{20}_mylongrol3nam3withmanycharact3rs_[0-9]{10}$`, - assertCreds: assertCreds, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - host, cleanup := cassandra.PrepareTestContainer(t, - cassandra.Version("3.11"), - cassandra.CopyFromTo(insecureFileMounts), - ) - defer cleanup() - - db := new() - - config := test.config - config["hosts"] = host.ConnectionURL() - config["port"] = host.Port - - initReq := dbplugin.InitializeRequest{ - Config: config, - VerifyConnection: true, - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - dbtesting.AssertInitialize(t, db, initReq) - - require.True(t, db.Initialized, "Database is not initialized") - - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - newUserResp, err := db.NewUser(ctx, test.newUserReq) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - require.Regexp(t, test.expectedUsernameRegex, newUserResp.Username) - test.assertCreds(t, db.Hosts, db.Port, newUserResp.Username, test.newUserReq.Password, nil, 5*time.Second) - }) - } -} - -func TestUpdateUserPassword(t *testing.T) { - db, cleanup := getCassandra(t, 4) - defer cleanup() - - password := "myreallysecurepassword" - createReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{createUserStatements}, - }, - Password: password, - Expiration: time.Now().Add(1 * time.Minute), - } - - createResp := dbtesting.AssertNewUser(t, db, createReq) - - assertCreds(t, db.Hosts, db.Port, createResp.Username, password, nil, 5*time.Second) - - newPassword := "somenewpassword" - updateReq := dbplugin.UpdateUserRequest{ - Username: createResp.Username, - Password: &dbplugin.ChangePassword{ - NewPassword: newPassword, - Statements: dbplugin.Statements{}, - }, - Expiration: nil, - } - - dbtesting.AssertUpdateUser(t, db, updateReq) - - assertCreds(t, db.Hosts, db.Port, createResp.Username, newPassword, nil, 5*time.Second) -} - -func TestDeleteUser(t *testing.T) { - db, cleanup := getCassandra(t, 4) - defer cleanup() - - password := "myreallysecurepassword" - createReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{createUserStatements}, - }, - Password: password, - Expiration: time.Now().Add(1 * time.Minute), - } - - createResp := dbtesting.AssertNewUser(t, db, createReq) - - assertCreds(t, db.Hosts, db.Port, createResp.Username, password, nil, 5*time.Second) - - deleteReq := dbplugin.DeleteUserRequest{ - Username: createResp.Username, - } - - dbtesting.AssertDeleteUser(t, db, deleteReq) - - assertNoCreds(t, db.Hosts, db.Port, createResp.Username, password, nil, 5*time.Second) -} - -func assertCreds(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions, timeout time.Duration) { - t.Helper() - op := func() error { - return connect(t, address, port, username, password, sslOpts) - } - bo := backoff.NewExponentialBackOff() - bo.MaxElapsedTime = timeout - bo.InitialInterval = 500 * time.Millisecond - bo.MaxInterval = bo.InitialInterval - bo.RandomizationFactor = 0.0 - - err := backoff.Retry(op, bo) - if err != nil { - t.Fatalf("failed to connect after %s: %s", timeout, err) - } -} - -func connect(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions) error { - t.Helper() - clusterConfig := gocql.NewCluster(address) - clusterConfig.Authenticator = gocql.PasswordAuthenticator{ - Username: username, - Password: password, - } - clusterConfig.ProtoVersion = 4 - clusterConfig.Port = port - clusterConfig.SslOpts = sslOpts - - session, err := clusterConfig.CreateSession() - if err != nil { - return err - } - defer session.Close() - return nil -} - -func assertNoCreds(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions, timeout time.Duration) { - t.Helper() - - op := func() error { - // "Invert" the error so the backoff logic sees a failure to connect as a success - err := connect(t, address, port, username, password, sslOpts) - if err != nil { - return nil - } - return nil - } - bo := backoff.NewExponentialBackOff() - bo.MaxElapsedTime = timeout - bo.InitialInterval = 500 * time.Millisecond - bo.MaxInterval = bo.InitialInterval - bo.RandomizationFactor = 0.0 - - err := backoff.Retry(op, bo) - if err != nil { - t.Fatalf("successfully connected after %s when it shouldn't", timeout) - } -} - -const createUserStatements = `CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER; -GRANT ALL PERMISSIONS ON ALL KEYSPACES TO '{{username}}';` diff --git a/plugins/database/cassandra/connection_producer_test.go b/plugins/database/cassandra/connection_producer_test.go deleted file mode 100644 index 306b444f4..000000000 --- a/plugins/database/cassandra/connection_producer_test.go +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package cassandra - -import ( - "context" - "crypto/tls" - "crypto/x509" - "encoding/json" - "io/ioutil" - "testing" - "time" - - "github.com/gocql/gocql" - "github.com/hashicorp/vault/helper/testhelpers/cassandra" - "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing" - "github.com/hashicorp/vault/sdk/helper/certutil" - "github.com/stretchr/testify/require" -) - -var insecureFileMounts = map[string]string{ - "test-fixtures/no_tls/cassandra.yaml": "/etc/cassandra/cassandra.yaml", -} - -func TestSelfSignedCA(t *testing.T) { - copyFromTo := map[string]string{ - "test-fixtures/with_tls/stores": "/bitnami/cassandra/secrets/", - "test-fixtures/with_tls/cqlshrc": "/.cassandra/cqlshrc", - } - - tlsConfig := loadServerCA(t, "test-fixtures/with_tls/ca.pem") - // Note about CI behavior: when running these tests locally, they seem to pass without issue. However, if the - // ServerName is not set, the tests fail within CI. It's not entirely clear to me why they are failing in CI - // however by manually setting the ServerName we can get around the hostname/DNS issue and get them passing. - // Setting the ServerName isn't the ideal solution, but it was the only reliable one I was able to find - tlsConfig.ServerName = "cassandra" - sslOpts := &gocql.SslOptions{ - Config: tlsConfig, - EnableHostVerification: true, - } - - host, cleanup := cassandra.PrepareTestContainer(t, - cassandra.ContainerName("cassandra"), - cassandra.Image("bitnami/cassandra", "3.11.11"), - cassandra.CopyFromTo(copyFromTo), - cassandra.SslOpts(sslOpts), - cassandra.Env("CASSANDRA_KEYSTORE_PASSWORD=cassandra"), - cassandra.Env("CASSANDRA_TRUSTSTORE_PASSWORD=cassandra"), - cassandra.Env("CASSANDRA_INTERNODE_ENCRYPTION=none"), - cassandra.Env("CASSANDRA_CLIENT_ENCRYPTION=true"), - ) - t.Cleanup(cleanup) - - type testCase struct { - config map[string]interface{} - expectErr bool - } - - caPEM := loadFile(t, "test-fixtures/with_tls/ca.pem") - badCAPEM := loadFile(t, "test-fixtures/with_tls/bad_ca.pem") - - tests := map[string]testCase{ - // /////////////////////// - // pem_json tests - "pem_json/ca only": { - config: map[string]interface{}{ - "pem_json": toJSON(t, certutil.CertBundle{ - CAChain: []string{caPEM}, - }), - }, - expectErr: false, - }, - "pem_json/bad ca": { - config: map[string]interface{}{ - "pem_json": toJSON(t, certutil.CertBundle{ - CAChain: []string{badCAPEM}, - }), - }, - expectErr: true, - }, - "pem_json/missing ca": { - config: map[string]interface{}{ - "pem_json": "", - }, - expectErr: true, - }, - - // /////////////////////// - // pem_bundle tests - "pem_bundle/ca only": { - config: map[string]interface{}{ - "pem_bundle": caPEM, - }, - expectErr: false, - }, - "pem_bundle/unrecognized CA": { - config: map[string]interface{}{ - "pem_bundle": badCAPEM, - }, - expectErr: true, - }, - "pem_bundle/missing ca": { - config: map[string]interface{}{ - "pem_bundle": "", - }, - expectErr: true, - }, - - // /////////////////////// - // no cert data provided - "no cert data/tls=true": { - config: map[string]interface{}{ - "tls": "true", - }, - expectErr: true, - }, - "no cert data/tls=false": { - config: map[string]interface{}{ - "tls": "false", - }, - expectErr: true, - }, - "no cert data/insecure_tls": { - config: map[string]interface{}{ - "insecure_tls": "true", - }, - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - // Set values that we don't know until the cassandra container is started - config := map[string]interface{}{ - "hosts": host.Name, - "port": host.Port, - "username": "cassandra", - "password": "cassandra", - "protocol_version": "4", - "connect_timeout": "30s", - "tls": "true", - - // Note about CI behavior: when running these tests locally, they seem to pass without issue. However, if the - // tls_server_name is not set, the tests fail within CI. It's not entirely clear to me why they are failing in CI - // however by manually setting the tls_server_name we can get around the hostname/DNS issue and get them passing. - // Setting the tls_server_name isn't the ideal solution, but it was the only reliable one I was able to find - "tls_server_name": "cassandra", - } - - // Apply the generated & common fields to the config to be sent to the DB - for k, v := range test.config { - config[k] = v - } - - db := new() - initReq := dbplugin.InitializeRequest{ - Config: config, - VerifyConnection: true, - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - _, err := db.Initialize(ctx, initReq) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - // If no error expected, run a NewUser query to make sure the connection - // actually works in case Initialize doesn't catch it - if !test.expectErr { - assertNewUser(t, db, sslOpts) - } - }) - } -} - -func assertNewUser(t *testing.T, db *Cassandra, sslOpts *gocql.SslOptions) { - newUserReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "dispname", - RoleName: "rolename", - }, - Statements: dbplugin.Statements{ - Commands: []string{ - "create user '{{username}}' with password '{{password}}'", - }, - }, - RollbackStatements: dbplugin.Statements{}, - Password: "gh8eruajASDFAsgy89svn", - Expiration: time.Now().Add(5 * time.Second), - } - - newUserResp := dbtesting.AssertNewUser(t, db, newUserReq) - t.Logf("Username: %s", newUserResp.Username) - - assertCreds(t, db.Hosts, db.Port, newUserResp.Username, newUserReq.Password, sslOpts, 5*time.Second) -} - -func loadServerCA(t *testing.T, file string) *tls.Config { - t.Helper() - - pemData, err := ioutil.ReadFile(file) - require.NoError(t, err) - - pool := x509.NewCertPool() - pool.AppendCertsFromPEM(pemData) - - config := &tls.Config{ - RootCAs: pool, - } - return config -} - -func loadFile(t *testing.T, filename string) string { - t.Helper() - - contents, err := ioutil.ReadFile(filename) - require.NoError(t, err) - return string(contents) -} - -func toJSON(t *testing.T, val interface{}) string { - t.Helper() - b, err := json.Marshal(val) - require.NoError(t, err) - return string(b) -} diff --git a/plugins/database/cassandra/test-fixtures/no_tls/cassandra.yaml b/plugins/database/cassandra/test-fixtures/no_tls/cassandra.yaml deleted file mode 100644 index a55afc693..000000000 --- a/plugins/database/cassandra/test-fixtures/no_tls/cassandra.yaml +++ /dev/null @@ -1,1149 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -# Cassandra storage config YAML - -# NOTE: -# See http://wiki.apache.org/cassandra/StorageConfiguration for -# full explanations of configuration directives -# /NOTE - -# The name of the cluster. This is mainly used to prevent machines in -# one logical cluster from joining another. -cluster_name: 'Test Cluster' - -# This defines the number of tokens randomly assigned to this node on the ring -# The more tokens, relative to other nodes, the larger the proportion of data -# that this node will store. You probably want all nodes to have the same number -# of tokens assuming they have equal hardware capability. -# -# If you leave this unspecified, Cassandra will use the default of 1 token for legacy compatibility, -# and will use the initial_token as described below. -# -# Specifying initial_token will override this setting on the node's initial start, -# on subsequent starts, this setting will apply even if initial token is set. -# -# If you already have a cluster with 1 token per node, and wish to migrate to -# multiple tokens per node, see http://wiki.apache.org/cassandra/Operations -num_tokens: 256 - -# Triggers automatic allocation of num_tokens tokens for this node. The allocation -# algorithm attempts to choose tokens in a way that optimizes replicated load over -# the nodes in the datacenter for the replication strategy used by the specified -# keyspace. -# -# The load assigned to each node will be close to proportional to its number of -# vnodes. -# -# Only supported with the Murmur3Partitioner. -# allocate_tokens_for_keyspace: KEYSPACE - -# initial_token allows you to specify tokens manually. While you can use it with -# vnodes (num_tokens > 1, above) -- in which case you should provide a -# comma-separated list -- it's primarily used when adding nodes to legacy clusters -# that do not have vnodes enabled. -# initial_token: - -# See http://wiki.apache.org/cassandra/HintedHandoff -# May either be "true" or "false" to enable globally -hinted_handoff_enabled: true - -# When hinted_handoff_enabled is true, a black list of data centers that will not -# perform hinted handoff -# hinted_handoff_disabled_datacenters: -# - DC1 -# - DC2 - -# this defines the maximum amount of time a dead host will have hints -# generated. After it has been dead this long, new hints for it will not be -# created until it has been seen alive and gone down again. -max_hint_window_in_ms: 10800000 # 3 hours - -# Maximum throttle in KBs per second, per delivery thread. This will be -# reduced proportionally to the number of nodes in the cluster. (If there -# are two nodes in the cluster, each delivery thread will use the maximum -# rate; if there are three, each will throttle to half of the maximum, -# since we expect two nodes to be delivering hints simultaneously.) -hinted_handoff_throttle_in_kb: 1024 - -# Number of threads with which to deliver hints; -# Consider increasing this number when you have multi-dc deployments, since -# cross-dc handoff tends to be slower -max_hints_delivery_threads: 2 - -# Directory where Cassandra should store hints. -# If not set, the default directory is $CASSANDRA_HOME/data/hints. -# hints_directory: /var/lib/cassandra/hints - -# How often hints should be flushed from the internal buffers to disk. -# Will *not* trigger fsync. -hints_flush_period_in_ms: 10000 - -# Maximum size for a single hints file, in megabytes. -max_hints_file_size_in_mb: 128 - -# Compression to apply to the hint files. If omitted, hints files -# will be written uncompressed. LZ4, Snappy, and Deflate compressors -# are supported. -#hints_compression: -# - class_name: LZ4Compressor -# parameters: -# - - -# Maximum throttle in KBs per second, total. This will be -# reduced proportionally to the number of nodes in the cluster. -batchlog_replay_throttle_in_kb: 1024 - -# Authentication backend, implementing IAuthenticator; used to identify users -# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator, -# PasswordAuthenticator}. -# -# - AllowAllAuthenticator performs no checks - set it to disable authentication. -# - PasswordAuthenticator relies on username/password pairs to authenticate -# users. It keeps usernames and hashed passwords in system_auth.credentials table. -# Please increase system_auth keyspace replication factor if you use this authenticator. -# If using PasswordAuthenticator, CassandraRoleManager must also be used (see below) -authenticator: PasswordAuthenticator - -# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions -# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer, -# CassandraAuthorizer}. -# -# - AllowAllAuthorizer allows any action to any user - set it to disable authorization. -# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please -# increase system_auth keyspace replication factor if you use this authorizer. -authorizer: CassandraAuthorizer - -# Part of the Authentication & Authorization backend, implementing IRoleManager; used -# to maintain grants and memberships between roles. -# Out of the box, Cassandra provides org.apache.cassandra.auth.CassandraRoleManager, -# which stores role information in the system_auth keyspace. Most functions of the -# IRoleManager require an authenticated login, so unless the configured IAuthenticator -# actually implements authentication, most of this functionality will be unavailable. -# -# - CassandraRoleManager stores role data in the system_auth keyspace. Please -# increase system_auth keyspace replication factor if you use this role manager. -role_manager: CassandraRoleManager - -# Validity period for roles cache (fetching granted roles can be an expensive -# operation depending on the role manager, CassandraRoleManager is one example) -# Granted roles are cached for authenticated sessions in AuthenticatedUser and -# after the period specified here, become eligible for (async) reload. -# Defaults to 2000, set to 0 to disable caching entirely. -# Will be disabled automatically for AllowAllAuthenticator. -roles_validity_in_ms: 2000 - -# Refresh interval for roles cache (if enabled). -# After this interval, cache entries become eligible for refresh. Upon next -# access, an async reload is scheduled and the old value returned until it -# completes. If roles_validity_in_ms is non-zero, then this must be -# also. -# Defaults to the same value as roles_validity_in_ms. -# roles_update_interval_in_ms: 2000 - -# Validity period for permissions cache (fetching permissions can be an -# expensive operation depending on the authorizer, CassandraAuthorizer is -# one example). Defaults to 2000, set to 0 to disable. -# Will be disabled automatically for AllowAllAuthorizer. -permissions_validity_in_ms: 2000 - -# Refresh interval for permissions cache (if enabled). -# After this interval, cache entries become eligible for refresh. Upon next -# access, an async reload is scheduled and the old value returned until it -# completes. If permissions_validity_in_ms is non-zero, then this must be -# also. -# Defaults to the same value as permissions_validity_in_ms. -# permissions_update_interval_in_ms: 2000 - -# Validity period for credentials cache. This cache is tightly coupled to -# the provided PasswordAuthenticator implementation of IAuthenticator. If -# another IAuthenticator implementation is configured, this cache will not -# be automatically used and so the following settings will have no effect. -# Please note, credentials are cached in their encrypted form, so while -# activating this cache may reduce the number of queries made to the -# underlying table, it may not bring a significant reduction in the -# latency of individual authentication attempts. -# Defaults to 2000, set to 0 to disable credentials caching. -credentials_validity_in_ms: 2000 - -# Refresh interval for credentials cache (if enabled). -# After this interval, cache entries become eligible for refresh. Upon next -# access, an async reload is scheduled and the old value returned until it -# completes. If credentials_validity_in_ms is non-zero, then this must be -# also. -# Defaults to the same value as credentials_validity_in_ms. -# credentials_update_interval_in_ms: 2000 - -# The partitioner is responsible for distributing groups of rows (by -# partition key) across nodes in the cluster. You should leave this -# alone for new clusters. The partitioner can NOT be changed without -# reloading all data, so when upgrading you should set this to the -# same partitioner you were already using. -# -# Besides Murmur3Partitioner, partitioners included for backwards -# compatibility include RandomPartitioner, ByteOrderedPartitioner, and -# OrderPreservingPartitioner. -# -partitioner: org.apache.cassandra.dht.Murmur3Partitioner - -# Directories where Cassandra should store data on disk. Cassandra -# will spread data evenly across them, subject to the granularity of -# the configured compaction strategy. -# If not set, the default directory is $CASSANDRA_HOME/data/data. -data_file_directories: - - /var/lib/cassandra/data - -# commit log. when running on magnetic HDD, this should be a -# separate spindle than the data directories. -# If not set, the default directory is $CASSANDRA_HOME/data/commitlog. -commitlog_directory: /var/lib/cassandra/commitlog - -# Enable / disable CDC functionality on a per-node basis. This modifies the logic used -# for write path allocation rejection (standard: never reject. cdc: reject Mutation -# containing a CDC-enabled table if at space limit in cdc_raw_directory). -cdc_enabled: false - -# CommitLogSegments are moved to this directory on flush if cdc_enabled: true and the -# segment contains mutations for a CDC-enabled table. This should be placed on a -# separate spindle than the data directories. If not set, the default directory is -# $CASSANDRA_HOME/data/cdc_raw. -# cdc_raw_directory: /var/lib/cassandra/cdc_raw - -# Policy for data disk failures: -# -# die -# shut down gossip and client transports and kill the JVM for any fs errors or -# single-sstable errors, so the node can be replaced. -# -# stop_paranoid -# shut down gossip and client transports even for single-sstable errors, -# kill the JVM for errors during startup. -# -# stop -# shut down gossip and client transports, leaving the node effectively dead, but -# can still be inspected via JMX, kill the JVM for errors during startup. -# -# best_effort -# stop using the failed disk and respond to requests based on -# remaining available sstables. This means you WILL see obsolete -# data at CL.ONE! -# -# ignore -# ignore fatal errors and let requests fail, as in pre-1.2 Cassandra -disk_failure_policy: stop - -# Policy for commit disk failures: -# -# die -# shut down gossip and Thrift and kill the JVM, so the node can be replaced. -# -# stop -# shut down gossip and Thrift, leaving the node effectively dead, but -# can still be inspected via JMX. -# -# stop_commit -# shutdown the commit log, letting writes collect but -# continuing to service reads, as in pre-2.0.5 Cassandra -# -# ignore -# ignore fatal errors and let the batches fail -commit_failure_policy: stop - -# Maximum size of the native protocol prepared statement cache -# -# Valid values are either "auto" (omitting the value) or a value greater 0. -# -# Note that specifying a too large value will result in long running GCs and possibly -# out-of-memory errors. Keep the value at a small fraction of the heap. -# -# If you constantly see "prepared statements discarded in the last minute because -# cache limit reached" messages, the first step is to investigate the root cause -# of these messages and check whether prepared statements are used correctly - -# i.e. use bind markers for variable parts. -# -# Do only change the default value, if you really have more prepared statements than -# fit in the cache. In most cases it is not necessary to change this value. -# Constantly re-preparing statements is a performance penalty. -# -# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater -prepared_statements_cache_size_mb: - -# Maximum size of the Thrift prepared statement cache -# -# If you do not use Thrift at all, it is safe to leave this value at "auto". -# -# See description of 'prepared_statements_cache_size_mb' above for more information. -# -# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater -thrift_prepared_statements_cache_size_mb: - -# Maximum size of the key cache in memory. -# -# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the -# minimum, sometimes more. The key cache is fairly tiny for the amount of -# time it saves, so it's worthwhile to use it at large numbers. -# The row cache saves even more time, but must contain the entire row, -# so it is extremely space-intensive. It's best to only use the -# row cache if you have hot rows or static rows. -# -# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. -# -# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache. -key_cache_size_in_mb: - -# Duration in seconds after which Cassandra should -# save the key cache. Caches are saved to saved_caches_directory as -# specified in this configuration file. -# -# Saved caches greatly improve cold-start speeds, and is relatively cheap in -# terms of I/O for the key cache. Row cache saving is much more expensive and -# has limited use. -# -# Default is 14400 or 4 hours. -key_cache_save_period: 14400 - -# Number of keys from the key cache to save -# Disabled by default, meaning all keys are going to be saved -# key_cache_keys_to_save: 100 - -# Row cache implementation class name. Available implementations: -# -# org.apache.cassandra.cache.OHCProvider -# Fully off-heap row cache implementation (default). -# -# org.apache.cassandra.cache.SerializingCacheProvider -# This is the row cache implementation availabile -# in previous releases of Cassandra. -# row_cache_class_name: org.apache.cassandra.cache.OHCProvider - -# Maximum size of the row cache in memory. -# Please note that OHC cache implementation requires some additional off-heap memory to manage -# the map structures and some in-flight memory during operations before/after cache entries can be -# accounted against the cache capacity. This overhead is usually small compared to the whole capacity. -# Do not specify more memory that the system can afford in the worst usual situation and leave some -# headroom for OS block level cache. Do never allow your system to swap. -# -# Default value is 0, to disable row caching. -row_cache_size_in_mb: 0 - -# Duration in seconds after which Cassandra should save the row cache. -# Caches are saved to saved_caches_directory as specified in this configuration file. -# -# Saved caches greatly improve cold-start speeds, and is relatively cheap in -# terms of I/O for the key cache. Row cache saving is much more expensive and -# has limited use. -# -# Default is 0 to disable saving the row cache. -row_cache_save_period: 0 - -# Number of keys from the row cache to save. -# Specify 0 (which is the default), meaning all keys are going to be saved -# row_cache_keys_to_save: 100 - -# Maximum size of the counter cache in memory. -# -# Counter cache helps to reduce counter locks' contention for hot counter cells. -# In case of RF = 1 a counter cache hit will cause Cassandra to skip the read before -# write entirely. With RF > 1 a counter cache hit will still help to reduce the duration -# of the lock hold, helping with hot counter cell updates, but will not allow skipping -# the read entirely. Only the local (clock, count) tuple of a counter cell is kept -# in memory, not the whole counter, so it's relatively cheap. -# -# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. -# -# Default value is empty to make it "auto" (min(2.5% of Heap (in MB), 50MB)). Set to 0 to disable counter cache. -# NOTE: if you perform counter deletes and rely on low gcgs, you should disable the counter cache. -counter_cache_size_in_mb: - -# Duration in seconds after which Cassandra should -# save the counter cache (keys only). Caches are saved to saved_caches_directory as -# specified in this configuration file. -# -# Default is 7200 or 2 hours. -counter_cache_save_period: 7200 - -# Number of keys from the counter cache to save -# Disabled by default, meaning all keys are going to be saved -# counter_cache_keys_to_save: 100 - -# saved caches -# If not set, the default directory is $CASSANDRA_HOME/data/saved_caches. -saved_caches_directory: /var/lib/cassandra/saved_caches - -# commitlog_sync may be either "periodic" or "batch." -# -# When in batch mode, Cassandra won't ack writes until the commit log -# has been fsynced to disk. It will wait -# commitlog_sync_batch_window_in_ms milliseconds between fsyncs. -# This window should be kept short because the writer threads will -# be unable to do extra work while waiting. (You may need to increase -# concurrent_writes for the same reason.) -# -# commitlog_sync: batch -# commitlog_sync_batch_window_in_ms: 2 -# -# the other option is "periodic" where writes may be acked immediately -# and the CommitLog is simply synced every commitlog_sync_period_in_ms -# milliseconds. -commitlog_sync: periodic -commitlog_sync_period_in_ms: 10000 - -# The size of the individual commitlog file segments. A commitlog -# segment may be archived, deleted, or recycled once all the data -# in it (potentially from each columnfamily in the system) has been -# flushed to sstables. -# -# The default size is 32, which is almost always fine, but if you are -# archiving commitlog segments (see commitlog_archiving.properties), -# then you probably want a finer granularity of archiving; 8 or 16 MB -# is reasonable. -# Max mutation size is also configurable via max_mutation_size_in_kb setting in -# cassandra.yaml. The default is half the size commitlog_segment_size_in_mb * 1024. -# -# NOTE: If max_mutation_size_in_kb is set explicitly then commitlog_segment_size_in_mb must -# be set to at least twice the size of max_mutation_size_in_kb / 1024 -# -commitlog_segment_size_in_mb: 32 - -# Compression to apply to the commit log. If omitted, the commit log -# will be written uncompressed. LZ4, Snappy, and Deflate compressors -# are supported. -# commitlog_compression: -# - class_name: LZ4Compressor -# parameters: -# - - -# any class that implements the SeedProvider interface and has a -# constructor that takes a Map of parameters will do. -seed_provider: - # Addresses of hosts that are deemed contact points. - # Cassandra nodes use this list of hosts to find each other and learn - # the topology of the ring. You must change this if you are running - # multiple nodes! - - class_name: org.apache.cassandra.locator.SimpleSeedProvider - parameters: - # seeds is actually a comma-delimited list of addresses. - # Ex: ",," - - seeds: "127.0.0.1" - -# For workloads with more data than can fit in memory, Cassandra's -# bottleneck will be reads that need to fetch data from -# disk. "concurrent_reads" should be set to (16 * number_of_drives) in -# order to allow the operations to enqueue low enough in the stack -# that the OS and drives can reorder them. Same applies to -# "concurrent_counter_writes", since counter writes read the current -# values before incrementing and writing them back. -# -# On the other hand, since writes are almost never IO bound, the ideal -# number of "concurrent_writes" is dependent on the number of cores in -# your system; (8 * number_of_cores) is a good rule of thumb. -concurrent_reads: 32 -concurrent_writes: 32 -concurrent_counter_writes: 32 - -# For materialized view writes, as there is a read involved, so this should -# be limited by the less of concurrent reads or concurrent writes. -concurrent_materialized_view_writes: 32 - -# Maximum memory to use for sstable chunk cache and buffer pooling. -# 32MB of this are reserved for pooling buffers, the rest is used as an -# cache that holds uncompressed sstable chunks. -# Defaults to the smaller of 1/4 of heap or 512MB. This pool is allocated off-heap, -# so is in addition to the memory allocated for heap. The cache also has on-heap -# overhead which is roughly 128 bytes per chunk (i.e. 0.2% of the reserved size -# if the default 64k chunk size is used). -# Memory is only allocated when needed. -# file_cache_size_in_mb: 512 - -# Flag indicating whether to allocate on or off heap when the sstable buffer -# pool is exhausted, that is when it has exceeded the maximum memory -# file_cache_size_in_mb, beyond which it will not cache buffers but allocate on request. - -# buffer_pool_use_heap_if_exhausted: true - -# The strategy for optimizing disk read -# Possible values are: -# ssd (for solid state disks, the default) -# spinning (for spinning disks) -# disk_optimization_strategy: ssd - -# Total permitted memory to use for memtables. Cassandra will stop -# accepting writes when the limit is exceeded until a flush completes, -# and will trigger a flush based on memtable_cleanup_threshold -# If omitted, Cassandra will set both to 1/4 the size of the heap. -# memtable_heap_space_in_mb: 2048 -# memtable_offheap_space_in_mb: 2048 - -# Ratio of occupied non-flushing memtable size to total permitted size -# that will trigger a flush of the largest memtable. Larger mct will -# mean larger flushes and hence less compaction, but also less concurrent -# flush activity which can make it difficult to keep your disks fed -# under heavy write load. -# -# memtable_cleanup_threshold defaults to 1 / (memtable_flush_writers + 1) -# memtable_cleanup_threshold: 0.11 - -# Specify the way Cassandra allocates and manages memtable memory. -# Options are: -# -# heap_buffers -# on heap nio buffers -# -# offheap_buffers -# off heap (direct) nio buffers -# -# offheap_objects -# off heap objects -memtable_allocation_type: heap_buffers - -# Total space to use for commit logs on disk. -# -# If space gets above this value, Cassandra will flush every dirty CF -# in the oldest segment and remove it. So a small total commitlog space -# will tend to cause more flush activity on less-active columnfamilies. -# -# The default value is the smaller of 8192, and 1/4 of the total space -# of the commitlog volume. -# -# commitlog_total_space_in_mb: 8192 - -# This sets the amount of memtable flush writer threads. These will -# be blocked by disk io, and each one will hold a memtable in memory -# while blocked. -# -# memtable_flush_writers defaults to one per data_file_directory. -# -# If your data directories are backed by SSD, you can increase this, but -# avoid having memtable_flush_writers * data_file_directories > number of cores -#memtable_flush_writers: 1 - -# Total space to use for change-data-capture logs on disk. -# -# If space gets above this value, Cassandra will throw WriteTimeoutException -# on Mutations including tables with CDC enabled. A CDCCompactor is responsible -# for parsing the raw CDC logs and deleting them when parsing is completed. -# -# The default value is the min of 4096 mb and 1/8th of the total space -# of the drive where cdc_raw_directory resides. -# cdc_total_space_in_mb: 4096 - -# When we hit our cdc_raw limit and the CDCCompactor is either running behind -# or experiencing backpressure, we check at the following interval to see if any -# new space for cdc-tracked tables has been made available. Default to 250ms -# cdc_free_space_check_interval_ms: 250 - -# A fixed memory pool size in MB for for SSTable index summaries. If left -# empty, this will default to 5% of the heap size. If the memory usage of -# all index summaries exceeds this limit, SSTables with low read rates will -# shrink their index summaries in order to meet this limit. However, this -# is a best-effort process. In extreme conditions Cassandra may need to use -# more than this amount of memory. -index_summary_capacity_in_mb: - -# How frequently index summaries should be resampled. This is done -# periodically to redistribute memory from the fixed-size pool to sstables -# proportional their recent read rates. Setting to -1 will disable this -# process, leaving existing index summaries at their current sampling level. -index_summary_resize_interval_in_minutes: 60 - -# Whether to, when doing sequential writing, fsync() at intervals in -# order to force the operating system to flush the dirty -# buffers. Enable this to avoid sudden dirty buffer flushing from -# impacting read latencies. Almost always a good idea on SSDs; not -# necessarily on platters. -trickle_fsync: false -trickle_fsync_interval_in_kb: 10240 - -# TCP port, for commands and data -# For security reasons, you should not expose this port to the internet. Firewall it if needed. -storage_port: 7000 - -# SSL port, for encrypted communication. Unused unless enabled in -# encryption_options -# For security reasons, you should not expose this port to the internet. Firewall it if needed. -ssl_storage_port: 7001 - -# Address or interface to bind to and tell other Cassandra nodes to connect to. -# You _must_ change this if you want multiple nodes to be able to communicate! -# -# Set listen_address OR listen_interface, not both. -# -# Leaving it blank leaves it up to InetAddress.getLocalHost(). This -# will always do the Right Thing _if_ the node is properly configured -# (hostname, name resolution, etc), and the Right Thing is to use the -# address associated with the hostname (it might not be). -# -# Setting listen_address to 0.0.0.0 is always wrong. -# -listen_address: 172.17.0.3 - -# Set listen_address OR listen_interface, not both. Interfaces must correspond -# to a single address, IP aliasing is not supported. -# listen_interface: eth0 - -# If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address -# you can specify which should be chosen using listen_interface_prefer_ipv6. If false the first ipv4 -# address will be used. If true the first ipv6 address will be used. Defaults to false preferring -# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. -# listen_interface_prefer_ipv6: false - -# Address to broadcast to other Cassandra nodes -# Leaving this blank will set it to the same value as listen_address -broadcast_address: 127.0.0.1 - -# When using multiple physical network interfaces, set this -# to true to listen on broadcast_address in addition to -# the listen_address, allowing nodes to communicate in both -# interfaces. -# Ignore this property if the network configuration automatically -# routes between the public and private networks such as EC2. -# listen_on_broadcast_address: false - -# Internode authentication backend, implementing IInternodeAuthenticator; -# used to allow/disallow connections from peer nodes. -# internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator - -# Whether to start the native transport server. -# Please note that the address on which the native transport is bound is the -# same as the rpc_address. The port however is different and specified below. -start_native_transport: true -# port for the CQL native transport to listen for clients on -# For security reasons, you should not expose this port to the internet. Firewall it if needed. -native_transport_port: 9042 -# Enabling native transport encryption in client_encryption_options allows you to either use -# encryption for the standard port or to use a dedicated, additional port along with the unencrypted -# standard native_transport_port. -# Enabling client encryption and keeping native_transport_port_ssl disabled will use encryption -# for native_transport_port. Setting native_transport_port_ssl to a different value -# from native_transport_port will use encryption for native_transport_port_ssl while -# keeping native_transport_port unencrypted. -# native_transport_port_ssl: 9142 -# The maximum threads for handling requests when the native transport is used. -# This is similar to rpc_max_threads though the default differs slightly (and -# there is no native_transport_min_threads, idle threads will always be stopped -# after 30 seconds). -# native_transport_max_threads: 128 -# -# The maximum size of allowed frame. Frame (requests) larger than this will -# be rejected as invalid. The default is 256MB. If you're changing this parameter, -# you may want to adjust max_value_size_in_mb accordingly. -# native_transport_max_frame_size_in_mb: 256 - -# The maximum number of concurrent client connections. -# The default is -1, which means unlimited. -# native_transport_max_concurrent_connections: -1 - -# The maximum number of concurrent client connections per source ip. -# The default is -1, which means unlimited. -# native_transport_max_concurrent_connections_per_ip: -1 - -# Whether to start the thrift rpc server. -start_rpc: false - -# The address or interface to bind the Thrift RPC service and native transport -# server to. -# -# Set rpc_address OR rpc_interface, not both. -# -# Leaving rpc_address blank has the same effect as on listen_address -# (i.e. it will be based on the configured hostname of the node). -# -# Note that unlike listen_address, you can specify 0.0.0.0, but you must also -# set broadcast_rpc_address to a value other than 0.0.0.0. -# -# For security reasons, you should not expose this port to the internet. Firewall it if needed. -rpc_address: 0.0.0.0 - -# Set rpc_address OR rpc_interface, not both. Interfaces must correspond -# to a single address, IP aliasing is not supported. -# rpc_interface: eth1 - -# If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address -# you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4 -# address will be used. If true the first ipv6 address will be used. Defaults to false preferring -# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. -# rpc_interface_prefer_ipv6: false - -# port for Thrift to listen for clients on -rpc_port: 9160 - -# RPC address to broadcast to drivers and other Cassandra nodes. This cannot -# be set to 0.0.0.0. If left blank, this will be set to the value of -# rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must -# be set. -broadcast_rpc_address: 127.0.0.1 - -# enable or disable keepalive on rpc/native connections -rpc_keepalive: true - -# Cassandra provides two out-of-the-box options for the RPC Server: -# -# sync -# One thread per thrift connection. For a very large number of clients, memory -# will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size -# per thread, and that will correspond to your use of virtual memory (but physical memory -# may be limited depending on use of stack space). -# -# hsha -# Stands for "half synchronous, half asynchronous." All thrift clients are handled -# asynchronously using a small number of threads that does not vary with the amount -# of thrift clients (and thus scales well to many clients). The rpc requests are still -# synchronous (one thread per active request). If hsha is selected then it is essential -# that rpc_max_threads is changed from the default value of unlimited. -# -# The default is sync because on Windows hsha is about 30% slower. On Linux, -# sync/hsha performance is about the same, with hsha of course using less memory. -# -# Alternatively, can provide your own RPC server by providing the fully-qualified class name -# of an o.a.c.t.TServerFactory that can create an instance of it. -rpc_server_type: sync - -# Uncomment rpc_min|max_thread to set request pool size limits. -# -# Regardless of your choice of RPC server (see above), the number of maximum requests in the -# RPC thread pool dictates how many concurrent requests are possible (but if you are using the sync -# RPC server, it also dictates the number of clients that can be connected at all). -# -# The default is unlimited and thus provides no protection against clients overwhelming the server. You are -# encouraged to set a maximum that makes sense for you in production, but do keep in mind that -# rpc_max_threads represents the maximum number of client requests this server may execute concurrently. -# -# rpc_min_threads: 16 -# rpc_max_threads: 2048 - -# uncomment to set socket buffer sizes on rpc connections -# rpc_send_buff_size_in_bytes: -# rpc_recv_buff_size_in_bytes: - -# Uncomment to set socket buffer size for internode communication -# Note that when setting this, the buffer size is limited by net.core.wmem_max -# and when not setting it it is defined by net.ipv4.tcp_wmem -# See also: -# /proc/sys/net/core/wmem_max -# /proc/sys/net/core/rmem_max -# /proc/sys/net/ipv4/tcp_wmem -# /proc/sys/net/ipv4/tcp_wmem -# and 'man tcp' -# internode_send_buff_size_in_bytes: - -# Uncomment to set socket buffer size for internode communication -# Note that when setting this, the buffer size is limited by net.core.wmem_max -# and when not setting it it is defined by net.ipv4.tcp_wmem -# internode_recv_buff_size_in_bytes: - -# Frame size for thrift (maximum message length). -thrift_framed_transport_size_in_mb: 15 - -# Set to true to have Cassandra create a hard link to each sstable -# flushed or streamed locally in a backups/ subdirectory of the -# keyspace data. Removing these links is the operator's -# responsibility. -incremental_backups: false - -# Whether or not to take a snapshot before each compaction. Be -# careful using this option, since Cassandra won't clean up the -# snapshots for you. Mostly useful if you're paranoid when there -# is a data format change. -snapshot_before_compaction: false - -# Whether or not a snapshot is taken of the data before keyspace truncation -# or dropping of column families. The STRONGLY advised default of true -# should be used to provide data safety. If you set this flag to false, you will -# lose data on truncation or drop. -auto_snapshot: true - -# Granularity of the collation index of rows within a partition. -# Increase if your rows are large, or if you have a very large -# number of rows per partition. The competing goals are these: -# -# - a smaller granularity means more index entries are generated -# and looking up rows withing the partition by collation column -# is faster -# - but, Cassandra will keep the collation index in memory for hot -# rows (as part of the key cache), so a larger granularity means -# you can cache more hot rows -column_index_size_in_kb: 64 - -# Per sstable indexed key cache entries (the collation index in memory -# mentioned above) exceeding this size will not be held on heap. -# This means that only partition information is held on heap and the -# index entries are read from disk. -# -# Note that this size refers to the size of the -# serialized index information and not the size of the partition. -column_index_cache_size_in_kb: 2 - -# Number of simultaneous compactions to allow, NOT including -# validation "compactions" for anti-entropy repair. Simultaneous -# compactions can help preserve read performance in a mixed read/write -# workload, by mitigating the tendency of small sstables to accumulate -# during a single long running compactions. The default is usually -# fine and if you experience problems with compaction running too -# slowly or too fast, you should look at -# compaction_throughput_mb_per_sec first. -# -# concurrent_compactors defaults to the smaller of (number of disks, -# number of cores), with a minimum of 2 and a maximum of 8. -# -# If your data directories are backed by SSD, you should increase this -# to the number of cores. -#concurrent_compactors: 1 - -# Throttles compaction to the given total throughput across the entire -# system. The faster you insert data, the faster you need to compact in -# order to keep the sstable count down, but in general, setting this to -# 16 to 32 times the rate you are inserting data is more than sufficient. -# Setting this to 0 disables throttling. Note that this account for all types -# of compaction, including validation compaction. -compaction_throughput_mb_per_sec: 16 - -# When compacting, the replacement sstable(s) can be opened before they -# are completely written, and used in place of the prior sstables for -# any range that has been written. This helps to smoothly transfer reads -# between the sstables, reducing page cache churn and keeping hot rows hot -sstable_preemptive_open_interval_in_mb: 50 - -# Throttles all outbound streaming file transfers on this node to the -# given total throughput in Mbps. This is necessary because Cassandra does -# mostly sequential IO when streaming data during bootstrap or repair, which -# can lead to saturating the network connection and degrading rpc performance. -# When unset, the default is 200 Mbps or 25 MB/s. -# stream_throughput_outbound_megabits_per_sec: 200 - -# Throttles all streaming file transfer between the datacenters, -# this setting allows users to throttle inter dc stream throughput in addition -# to throttling all network stream traffic as configured with -# stream_throughput_outbound_megabits_per_sec -# When unset, the default is 200 Mbps or 25 MB/s -# inter_dc_stream_throughput_outbound_megabits_per_sec: 200 - -# How long the coordinator should wait for read operations to complete -read_request_timeout_in_ms: 5000 -# How long the coordinator should wait for seq or index scans to complete -range_request_timeout_in_ms: 10000 -# How long the coordinator should wait for writes to complete -write_request_timeout_in_ms: 2000 -# How long the coordinator should wait for counter writes to complete -counter_write_request_timeout_in_ms: 5000 -# How long a coordinator should continue to retry a CAS operation -# that contends with other proposals for the same row -cas_contention_timeout_in_ms: 1000 -# How long the coordinator should wait for truncates to complete -# (This can be much longer, because unless auto_snapshot is disabled -# we need to flush first so we can snapshot before removing the data.) -truncate_request_timeout_in_ms: 60000 -# The default timeout for other, miscellaneous operations -request_timeout_in_ms: 10000 - -# Enable operation timeout information exchange between nodes to accurately -# measure request timeouts. If disabled, replicas will assume that requests -# were forwarded to them instantly by the coordinator, which means that -# under overload conditions we will waste that much extra time processing -# already-timed-out requests. -# -# Warning: before enabling this property make sure to ntp is installed -# and the times are synchronized between the nodes. -cross_node_timeout: false - -# Set socket timeout for streaming operation. -# The stream session is failed if no data/ack is received by any of the participants -# within that period, which means this should also be sufficient to stream a large -# sstable or rebuild table indexes. -# Default value is 86400000ms, which means stale streams timeout after 24 hours. -# A value of zero means stream sockets should never time out. -# streaming_socket_timeout_in_ms: 86400000 - -# phi value that must be reached for a host to be marked down. -# most users should never need to adjust this. -# phi_convict_threshold: 8 - -# endpoint_snitch -- Set this to a class that implements -# IEndpointSnitch. The snitch has two functions: -# -# - it teaches Cassandra enough about your network topology to route -# requests efficiently -# - it allows Cassandra to spread replicas around your cluster to avoid -# correlated failures. It does this by grouping machines into -# "datacenters" and "racks." Cassandra will do its best not to have -# more than one replica on the same "rack" (which may not actually -# be a physical location) -# -# CASSANDRA WILL NOT ALLOW YOU TO SWITCH TO AN INCOMPATIBLE SNITCH -# ONCE DATA IS INSERTED INTO THE CLUSTER. This would cause data loss. -# This means that if you start with the default SimpleSnitch, which -# locates every node on "rack1" in "datacenter1", your only options -# if you need to add another datacenter are GossipingPropertyFileSnitch -# (and the older PFS). From there, if you want to migrate to an -# incompatible snitch like Ec2Snitch you can do it by adding new nodes -# under Ec2Snitch (which will locate them in a new "datacenter") and -# decommissioning the old ones. -# -# Out of the box, Cassandra provides: -# -# SimpleSnitch: -# Treats Strategy order as proximity. This can improve cache -# locality when disabling read repair. Only appropriate for -# single-datacenter deployments. -# -# GossipingPropertyFileSnitch -# This should be your go-to snitch for production use. The rack -# and datacenter for the local node are defined in -# cassandra-rackdc.properties and propagated to other nodes via -# gossip. If cassandra-topology.properties exists, it is used as a -# fallback, allowing migration from the PropertyFileSnitch. -# -# PropertyFileSnitch: -# Proximity is determined by rack and data center, which are -# explicitly configured in cassandra-topology.properties. -# -# Ec2Snitch: -# Appropriate for EC2 deployments in a single Region. Loads Region -# and Availability Zone information from the EC2 API. The Region is -# treated as the datacenter, and the Availability Zone as the rack. -# Only private IPs are used, so this will not work across multiple -# Regions. -# -# Ec2MultiRegionSnitch: -# Uses public IPs as broadcast_address to allow cross-region -# connectivity. (Thus, you should set seed addresses to the public -# IP as well.) You will need to open the storage_port or -# ssl_storage_port on the public IP firewall. (For intra-Region -# traffic, Cassandra will switch to the private IP after -# establishing a connection.) -# -# RackInferringSnitch: -# Proximity is determined by rack and data center, which are -# assumed to correspond to the 3rd and 2nd octet of each node's IP -# address, respectively. Unless this happens to match your -# deployment conventions, this is best used as an example of -# writing a custom Snitch class and is provided in that spirit. -# -# You can use a custom Snitch by setting this to the full class name -# of the snitch, which will be assumed to be on your classpath. -endpoint_snitch: SimpleSnitch - -# controls how often to perform the more expensive part of host score -# calculation -dynamic_snitch_update_interval_in_ms: 100 -# controls how often to reset all host scores, allowing a bad host to -# possibly recover -dynamic_snitch_reset_interval_in_ms: 600000 -# if set greater than zero and read_repair_chance is < 1.0, this will allow -# 'pinning' of replicas to hosts in order to increase cache capacity. -# The badness threshold will control how much worse the pinned host has to be -# before the dynamic snitch will prefer other replicas over it. This is -# expressed as a double which represents a percentage. Thus, a value of -# 0.2 means Cassandra would continue to prefer the static snitch values -# until the pinned host was 20% worse than the fastest. -dynamic_snitch_badness_threshold: 0.1 - -# request_scheduler -- Set this to a class that implements -# RequestScheduler, which will schedule incoming client requests -# according to the specific policy. This is useful for multi-tenancy -# with a single Cassandra cluster. -# NOTE: This is specifically for requests from the client and does -# not affect inter node communication. -# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place -# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of -# client requests to a node with a separate queue for each -# request_scheduler_id. The scheduler is further customized by -# request_scheduler_options as described below. -request_scheduler: org.apache.cassandra.scheduler.NoScheduler - -# Scheduler Options vary based on the type of scheduler -# -# NoScheduler -# Has no options -# -# RoundRobin -# throttle_limit -# The throttle_limit is the number of in-flight -# requests per client. Requests beyond -# that limit are queued up until -# running requests can complete. -# The value of 80 here is twice the number of -# concurrent_reads + concurrent_writes. -# default_weight -# default_weight is optional and allows for -# overriding the default which is 1. -# weights -# Weights are optional and will default to 1 or the -# overridden default_weight. The weight translates into how -# many requests are handled during each turn of the -# RoundRobin, based on the scheduler id. -# -# request_scheduler_options: -# throttle_limit: 80 -# default_weight: 5 -# weights: -# Keyspace1: 1 -# Keyspace2: 5 - -# request_scheduler_id -- An identifier based on which to perform -# the request scheduling. Currently the only valid option is keyspace. -# request_scheduler_id: keyspace - -# Enable or disable inter-node encryption -# JVM defaults for supported SSL socket protocols and cipher suites can -# be replaced using custom encryption options. This is not recommended -# unless you have policies in place that dictate certain settings, or -# need to disable vulnerable ciphers or protocols in case the JVM cannot -# be updated. -# FIPS compliant settings can be configured at JVM level and should not -# involve changing encryption settings here: -# https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/FIPS.html -# *NOTE* No custom encryption options are enabled at the moment -# The available internode options are : all, none, dc, rack -# -# If set to dc cassandra will encrypt the traffic between the DCs -# If set to rack cassandra will encrypt the traffic between the racks -# -# The passwords used in these options must match the passwords used when generating -# the keystore and truststore. For instructions on generating these files, see: -# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore -# -server_encryption_options: - internode_encryption: none - keystore: conf/.keystore - keystore_password: cassandra - truststore: conf/.truststore - truststore_password: cassandra - # More advanced defaults below: - # protocol: TLS - # algorithm: SunX509 - # store_type: JKS - # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] - # require_client_auth: false - # require_endpoint_verification: false - -# enable or disable client/server encryption. -client_encryption_options: - enabled: false - # If enabled and optional is set to true encrypted and unencrypted connections are handled. - optional: false - keystore: conf/.keystore - keystore_password: cassandra - # require_client_auth: false - # Set truststore and truststore_password if require_client_auth is true - # truststore: conf/.truststore - # truststore_password: cassandra - # More advanced defaults below: - # protocol: TLS - # algorithm: SunX509 - # store_type: JKS - # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] - -# internode_compression controls whether traffic between nodes is -# compressed. -# Can be: -# -# all -# all traffic is compressed -# -# dc -# traffic between different datacenters is compressed -# -# none -# nothing is compressed. -internode_compression: dc - -# Enable or disable tcp_nodelay for inter-dc communication. -# Disabling it will result in larger (but fewer) network packets being sent, -# reducing overhead from the TCP protocol itself, at the cost of increasing -# latency if you block for cross-datacenter responses. -inter_dc_tcp_nodelay: false - -# TTL for different trace types used during logging of the repair process. -tracetype_query_ttl: 86400 -tracetype_repair_ttl: 604800 - -# By default, Cassandra logs GC Pauses greater than 200 ms at INFO level -# This threshold can be adjusted to minimize logging if necessary -# gc_log_threshold_in_ms: 200 - -# If unset, all GC Pauses greater than gc_log_threshold_in_ms will log at -# INFO level -# UDFs (user defined functions) are disabled by default. -# As of Cassandra 3.0 there is a sandbox in place that should prevent execution of evil code. -enable_user_defined_functions: false - -# Enables scripted UDFs (JavaScript UDFs). -# Java UDFs are always enabled, if enable_user_defined_functions is true. -# Enable this option to be able to use UDFs with "language javascript" or any custom JSR-223 provider. -# This option has no effect, if enable_user_defined_functions is false. -enable_scripted_user_defined_functions: false - -# The default Windows kernel timer and scheduling resolution is 15.6ms for power conservation. -# Lowering this value on Windows can provide much tighter latency and better throughput, however -# some virtualized environments may see a negative performance impact from changing this setting -# below their system default. The sysinternals 'clockres' tool can confirm your system's default -# setting. -windows_timer_interval: 1 - - -# Enables encrypting data at-rest (on disk). Different key providers can be plugged in, but the default reads from -# a JCE-style keystore. A single keystore can hold multiple keys, but the one referenced by -# the "key_alias" is the only key that will be used for encrypt operations; previously used keys -# can still (and should!) be in the keystore and will be used on decrypt operations -# (to handle the case of key rotation). -# -# It is strongly recommended to download and install Java Cryptography Extension (JCE) -# Unlimited Strength Jurisdiction Policy Files for your version of the JDK. -# (current link: http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) -# -# Currently, only the following file types are supported for transparent data encryption, although -# more are coming in future cassandra releases: commitlog, hints -transparent_data_encryption_options: - enabled: false - chunk_length_kb: 64 - cipher: AES/CBC/PKCS5Padding - key_alias: testing:1 - # CBC IV length for AES needs to be 16 bytes (which is also the default size) - # iv_length: 16 - key_provider: - - class_name: org.apache.cassandra.security.JKSKeyProvider - parameters: - - keystore: conf/.keystore - keystore_password: cassandra - store_type: JCEKS - key_password: cassandra - - -##################### -# SAFETY THRESHOLDS # -##################### - -# When executing a scan, within or across a partition, we need to keep the -# tombstones seen in memory so we can return them to the coordinator, which -# will use them to make sure other replicas also know about the deleted rows. -# With workloads that generate a lot of tombstones, this can cause performance -# problems and even exhaust the server heap. -# (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets) -# Adjust the thresholds here if you understand the dangers and want to -# scan more tombstones anyway. These thresholds may also be adjusted at runtime -# using the StorageService mbean. -tombstone_warn_threshold: 1000 -tombstone_failure_threshold: 100000 - -# Log WARN on any batch size exceeding this value. 5kb per batch by default. -# Caution should be taken on increasing the size of this threshold as it can lead to node instability. -batch_size_warn_threshold_in_kb: 5 - -# Fail any batch exceeding this value. 50kb (10x warn threshold) by default. -batch_size_fail_threshold_in_kb: 50 - -# Log WARN on any batches not of type LOGGED than span across more partitions than this limit -unlogged_batch_across_partitions_warn_threshold: 10 - -# Log a warning when compacting partitions larger than this value -compaction_large_partition_warning_threshold_mb: 100 - -# GC Pauses greater than gc_warn_threshold_in_ms will be logged at WARN level -# Adjust the threshold based on your application throughput requirement -# By default, Cassandra logs GC Pauses greater than 200 ms at INFO level -gc_warn_threshold_in_ms: 1000 - -# Maximum size of any value in SSTables. Safety measure to detect SSTable corruption -# early. Any value size larger than this threshold will result into marking an SSTable -# as corrupted. -# max_value_size_in_mb: 256 diff --git a/plugins/database/cassandra/test-fixtures/with_tls/bad_ca.pem b/plugins/database/cassandra/test-fixtures/with_tls/bad_ca.pem deleted file mode 100644 index 6674fa7fa..000000000 --- a/plugins/database/cassandra/test-fixtures/with_tls/bad_ca.pem +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEFjCCAv6gAwIBAgIUHNknw0iUWaMC5UCpiribG8DQhZYwDQYJKoZIhvcNAQEL -BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH -Ew1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl -c3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz -dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEwNjE0MjAyNDAwWhcNMjYwNjEz -MjAyNDAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU -BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE -CxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw -ZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBANc0MEZOJ7xm4JrCceerX0kWcdPIczXFIIZTJYdTB7YPHTiL -PFSZ9ugu8W6R7wOMLUazcD7Ugw0hjt+JkiRIY1AOvuZRX7DR3Q0sGy9qFb1y2kOk -lTSAFOV96FxxAg9Fn23mcvjV1TDO1dlxvOuAo0NMjk82TzHk7LVuYOKuJ/Sc9i8a -Ba4vndbiwkSGpytymCu0X4T4ZEARLUZ4feGhr5RbYRehq2Nb8kw/KNLZZyzlzJbr -8OkVizW796bkVJwRfCFubZPl8EvRslxZ2+sMFSozoofoFlB1FsGAvlnEfkxqTJJo -WafmsYnOVnbNfwOogDP0+bp8WAZrAxJqTAWm/LMCAwEAAaNCMEAwDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHyfBUnvAULGlcFSljTI -DegUVLB5MA0GCSqGSIb3DQEBCwUAA4IBAQBOdVqZpMCKq+X2TBi3nJmz6kjePVBh -ocHUG02nRkL533x+PUxRpDG3AMzWF3niPxtMuVIZDfpi27zlm2QCh9b3sQi83w+9 -UX1/j3dUoUyiVi/U0iZeZmuDY3ne59DNFdOgGY9p3FvJ+b9WfPg8+v2w26rGoSMz -21XKNZcRFcjOJ5LJ3i9+liaCkpXLfErA+AtqNeraHOorJ5UO4mA7OlFowV8adOQq -SinFIoXCExBTxqMv0lVzEhGN6Wd261CmKY5e4QLqASCO+s7zwGhHyzwjdA0pCNtI -PmHIk13m0p56G8hpz+M/5hBQFb0MIIR3Je6QVzfRty2ipUO91E9Ydm7C ------END CERTIFICATE----- diff --git a/plugins/database/cassandra/test-fixtures/with_tls/ca.pem b/plugins/database/cassandra/test-fixtures/with_tls/ca.pem deleted file mode 100644 index fdcfb23fe..000000000 --- a/plugins/database/cassandra/test-fixtures/with_tls/ca.pem +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEFjCCAv6gAwIBAgIUWd8FZSev3ygjhWE7O8orqHPQ4IEwDQYJKoZIhvcNAQEL -BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH -Ew1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl -c3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz -dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEwNjEwMjAwNDAwWhcNMjYwNjA5 -MjAwNDAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU -BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE -CxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw -ZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAMXTnIDpOXXiHuKyI9EZxv7qg81DmelOB+iAzhvRsigMSuka -qZH29Aaf4PBvKLlSVN6sVP16cXRvk48qa0C78tP0kTPKWdEyE1xQUZb270SZ6Tm3 -T7sNRTRwWTsgeC1n6SHlBUn3MviQgA1dZM1CbZIXQpBxtuPg+p9eu3YP/CZJFJjT -LYVKT6kRumBQEX/UUesNfUnUpVIOxxOwbVeF6a/wGxeLY6/fOQ+TJhVUjSy/pvaI -6NnycrwD/4ck6gusV5HKakidCID9MwV610Vc7AFi070VGYCjKfiv6EYMMnjycYqi -KHz623Ca4rO4qtWWvT1K/+GkryDKXeI3KHuEsdsCAwEAAaNCMEAwDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIy8cvyabFclVWwcZ4rl -ADoLEdyAMA0GCSqGSIb3DQEBCwUAA4IBAQCzn9QbsOpBuvhhgdH/Jk0q7H0kmpVS -rbLhcQyWv9xiyopYbbUfh0Hud15rnqAkyT9nd2Kvo8T/X9rc1OXa6oDO6aoXjIm1 -aKOFikET8fc/81rT81E7TVPO7TZW5s9Cej30zCOJQWZ+ibHNyequuyihtImNacXF -+1pAAldj/JMu+Ky1YFrs2iccGOpGCGbsWfLQt+wYKwya7dpSz1ceqigKavIJSOMV -CNsyC59UtFbvdk139FyEvCmecsCbWuo0JVg3do5n6upwqrgvLRNP8EHzm17DWu5T -aNtsBbv85uUgMmF7kzxr+t6VdtG9u+q0HCmW1/1VVK3ZsA+UTB7UBddD ------END CERTIFICATE----- diff --git a/plugins/database/cassandra/test-fixtures/with_tls/ca.pem.json b/plugins/database/cassandra/test-fixtures/with_tls/ca.pem.json deleted file mode 100644 index a28e2ed69..000000000 --- a/plugins/database/cassandra/test-fixtures/with_tls/ca.pem.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ca_chain": ["-----BEGIN CERTIFICATE-----\nMIIEFjCCAv6gAwIBAgIUWd8FZSev3ygjhWE7O8orqHPQ4IEwDQYJKoZIhvcNAQEL\nBQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH\nEw1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl\nc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz\ndCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEwNjEwMjAwNDAwWhcNMjYwNjA5\nMjAwNDAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU\nBgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE\nCxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw\nZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAMXTnIDpOXXiHuKyI9EZxv7qg81DmelOB+iAzhvRsigMSuka\nqZH29Aaf4PBvKLlSVN6sVP16cXRvk48qa0C78tP0kTPKWdEyE1xQUZb270SZ6Tm3\nT7sNRTRwWTsgeC1n6SHlBUn3MviQgA1dZM1CbZIXQpBxtuPg+p9eu3YP/CZJFJjT\nLYVKT6kRumBQEX/UUesNfUnUpVIOxxOwbVeF6a/wGxeLY6/fOQ+TJhVUjSy/pvaI\n6NnycrwD/4ck6gusV5HKakidCID9MwV610Vc7AFi070VGYCjKfiv6EYMMnjycYqi\nKHz623Ca4rO4qtWWvT1K/+GkryDKXeI3KHuEsdsCAwEAAaNCMEAwDgYDVR0PAQH/\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIy8cvyabFclVWwcZ4rl\nADoLEdyAMA0GCSqGSIb3DQEBCwUAA4IBAQCzn9QbsOpBuvhhgdH/Jk0q7H0kmpVS\nrbLhcQyWv9xiyopYbbUfh0Hud15rnqAkyT9nd2Kvo8T/X9rc1OXa6oDO6aoXjIm1\naKOFikET8fc/81rT81E7TVPO7TZW5s9Cej30zCOJQWZ+ibHNyequuyihtImNacXF\n+1pAAldj/JMu+Ky1YFrs2iccGOpGCGbsWfLQt+wYKwya7dpSz1ceqigKavIJSOMV\nCNsyC59UtFbvdk139FyEvCmecsCbWuo0JVg3do5n6upwqrgvLRNP8EHzm17DWu5T\naNtsBbv85uUgMmF7kzxr+t6VdtG9u+q0HCmW1/1VVK3ZsA+UTB7UBddD\n-----END CERTIFICATE-----\n"] -} diff --git a/plugins/database/cassandra/test-fixtures/with_tls/cqlshrc b/plugins/database/cassandra/test-fixtures/with_tls/cqlshrc deleted file mode 100644 index 6a226e4b6..000000000 --- a/plugins/database/cassandra/test-fixtures/with_tls/cqlshrc +++ /dev/null @@ -1,3 +0,0 @@ -[ssl] -validate = false -version = SSLv23 diff --git a/plugins/database/cassandra/test-fixtures/with_tls/stores/keystore b/plugins/database/cassandra/test-fixtures/with_tls/stores/keystore deleted file mode 100644 index fce8a7771..000000000 Binary files a/plugins/database/cassandra/test-fixtures/with_tls/stores/keystore and /dev/null differ diff --git a/plugins/database/cassandra/test-fixtures/with_tls/stores/server.p12 b/plugins/database/cassandra/test-fixtures/with_tls/stores/server.p12 deleted file mode 100644 index c775b5432..000000000 Binary files a/plugins/database/cassandra/test-fixtures/with_tls/stores/server.p12 and /dev/null differ diff --git a/plugins/database/cassandra/test-fixtures/with_tls/stores/truststore b/plugins/database/cassandra/test-fixtures/with_tls/stores/truststore deleted file mode 100644 index e29c3fe1d..000000000 Binary files a/plugins/database/cassandra/test-fixtures/with_tls/stores/truststore and /dev/null differ diff --git a/plugins/database/influxdb/influxdb_test.go b/plugins/database/influxdb/influxdb_test.go deleted file mode 100644 index c7584960f..000000000 --- a/plugins/database/influxdb/influxdb_test.go +++ /dev/null @@ -1,484 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package influxdb - -import ( - "context" - "fmt" - "net/url" - "os" - "reflect" - "strconv" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing" - "github.com/hashicorp/vault/sdk/helper/docker" - influx "github.com/influxdata/influxdb1-client/v2" - "github.com/stretchr/testify/require" -) - -const createUserStatements = `CREATE USER "{{username}}" WITH PASSWORD '{{password}}';GRANT ALL ON "vault" TO "{{username}}";` - -type Config struct { - docker.ServiceURL - Username string - Password string -} - -var _ docker.ServiceConfig = &Config{} - -func (c *Config) apiConfig() influx.HTTPConfig { - return influx.HTTPConfig{ - Addr: c.URL().String(), - Username: c.Username, - Password: c.Password, - } -} - -func (c *Config) connectionParams() map[string]interface{} { - pieces := strings.Split(c.Address(), ":") - port, _ := strconv.Atoi(pieces[1]) - return map[string]interface{}{ - "host": pieces[0], - "port": port, - "username": c.Username, - "password": c.Password, - } -} - -func prepareInfluxdbTestContainer(t *testing.T) (func(), *Config) { - c := &Config{ - Username: "influx-root", - Password: "influx-root", - } - if host := os.Getenv("INFLUXDB_HOST"); host != "" { - c.ServiceURL = *docker.NewServiceURL(url.URL{Scheme: "http", Host: host}) - return func() {}, c - } - - runner, err := docker.NewServiceRunner(docker.RunOptions{ - ImageRepo: "docker.mirror.hashicorp.services/influxdb", - ContainerName: "influxdb", - ImageTag: "1.8-alpine", - Env: []string{ - "INFLUXDB_DB=vault", - "INFLUXDB_ADMIN_USER=" + c.Username, - "INFLUXDB_ADMIN_PASSWORD=" + c.Password, - "INFLUXDB_HTTP_AUTH_ENABLED=true", - }, - Ports: []string{"8086/tcp"}, - }) - if err != nil { - t.Fatalf("Could not start docker InfluxDB: %s", err) - } - svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - c.ServiceURL = *docker.NewServiceURL(url.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s:%d", host, port), - }) - cli, err := influx.NewHTTPClient(c.apiConfig()) - if err != nil { - return nil, fmt.Errorf("error creating InfluxDB client: %w", err) - } - defer cli.Close() - _, _, err = cli.Ping(1) - if err != nil { - return nil, fmt.Errorf("error checking cluster status: %w", err) - } - - return c, nil - }) - if err != nil { - t.Fatalf("Could not start docker InfluxDB: %s", err) - } - - return svc.Cleanup, svc.Config.(*Config) -} - -func TestInfluxdb_Initialize(t *testing.T) { - cleanup, config := prepareInfluxdbTestContainer(t) - defer cleanup() - - type testCase struct { - req dbplugin.InitializeRequest - expectedResponse dbplugin.InitializeResponse - expectErr bool - expectInitialized bool - } - - tests := map[string]testCase{ - "port is an int": { - req: dbplugin.InitializeRequest{ - Config: makeConfig(config.connectionParams()), - VerifyConnection: true, - }, - expectedResponse: dbplugin.InitializeResponse{ - Config: config.connectionParams(), - }, - expectErr: false, - expectInitialized: true, - }, - "port is a string": { - req: dbplugin.InitializeRequest{ - Config: makeConfig(config.connectionParams(), "port", strconv.Itoa(config.connectionParams()["port"].(int))), - VerifyConnection: true, - }, - expectedResponse: dbplugin.InitializeResponse{ - Config: makeConfig(config.connectionParams(), "port", strconv.Itoa(config.connectionParams()["port"].(int))), - }, - expectErr: false, - expectInitialized: true, - }, - "missing config": { - req: dbplugin.InitializeRequest{ - Config: nil, - VerifyConnection: true, - }, - expectedResponse: dbplugin.InitializeResponse{}, - expectErr: true, - expectInitialized: false, - }, - "missing host": { - req: dbplugin.InitializeRequest{ - Config: makeConfig(config.connectionParams(), "host", ""), - VerifyConnection: true, - }, - expectedResponse: dbplugin.InitializeResponse{}, - expectErr: true, - expectInitialized: false, - }, - "missing username": { - req: dbplugin.InitializeRequest{ - Config: makeConfig(config.connectionParams(), "username", ""), - VerifyConnection: true, - }, - expectedResponse: dbplugin.InitializeResponse{}, - expectErr: true, - expectInitialized: false, - }, - "missing password": { - req: dbplugin.InitializeRequest{ - Config: makeConfig(config.connectionParams(), "password", ""), - VerifyConnection: true, - }, - expectedResponse: dbplugin.InitializeResponse{}, - expectErr: true, - expectInitialized: false, - }, - "failed to validate connection": { - req: dbplugin.InitializeRequest{ - // Host exists, but isn't a running instance - Config: makeConfig(config.connectionParams(), "host", "foobar://bad_connection"), - VerifyConnection: true, - }, - expectedResponse: dbplugin.InitializeResponse{}, - expectErr: true, - expectInitialized: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - db := new() - defer dbtesting.AssertClose(t, db) - - resp, err := db.Initialize(context.Background(), test.req) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(resp, test.expectedResponse) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResponse) - } - - if test.expectInitialized && !db.Initialized { - t.Fatalf("Database should be initialized but wasn't") - } else if !test.expectInitialized && db.Initialized { - t.Fatalf("Database was initiailized when it shouldn't") - } - }) - } -} - -func makeConfig(rootConfig map[string]interface{}, keyValues ...interface{}) map[string]interface{} { - if len(keyValues)%2 != 0 { - panic("makeConfig must be provided with key and value pairs") - } - - // Make a copy of the map so there isn't a chance of test bleedover between maps - config := make(map[string]interface{}, len(rootConfig)+(len(keyValues)/2)) - for k, v := range rootConfig { - config[k] = v - } - for i := 0; i < len(keyValues); i += 2 { - k := keyValues[i].(string) // Will panic if the key field isn't a string and that's fine in a test - v := keyValues[i+1] - config[k] = v - } - return config -} - -func TestInfluxdb_CreateUser_DefaultUsernameTemplate(t *testing.T) { - cleanup, config := prepareInfluxdbTestContainer(t) - defer cleanup() - - db := new() - req := dbplugin.InitializeRequest{ - Config: config.connectionParams(), - VerifyConnection: true, - } - dbtesting.AssertInitialize(t, db, req) - - password := "nuozxby98523u89bdfnkjl" - newUserReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "token", - RoleName: "mylongrolenamewithmanycharacters", - }, - Statements: dbplugin.Statements{ - Commands: []string{createUserStatements}, - }, - Password: password, - Expiration: time.Now().Add(1 * time.Minute), - } - resp := dbtesting.AssertNewUser(t, db, newUserReq) - - if resp.Username == "" { - t.Fatalf("Missing username") - } - - assertCredsExist(t, config.URL().String(), resp.Username, password) - - require.Regexp(t, `^v_token_mylongrolenamew_[a-z0-9]{20}_[0-9]{10}$`, resp.Username) -} - -func TestInfluxdb_CreateUser_CustomUsernameTemplate(t *testing.T) { - cleanup, config := prepareInfluxdbTestContainer(t) - defer cleanup() - - db := new() - - conf := config.connectionParams() - conf["username_template"] = "{{.DisplayName}}_{{random 10}}" - - req := dbplugin.InitializeRequest{ - Config: conf, - VerifyConnection: true, - } - dbtesting.AssertInitialize(t, db, req) - - password := "nuozxby98523u89bdfnkjl" - newUserReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "token", - RoleName: "mylongrolenamewithmanycharacters", - }, - Statements: dbplugin.Statements{ - Commands: []string{createUserStatements}, - }, - Password: password, - Expiration: time.Now().Add(1 * time.Minute), - } - resp := dbtesting.AssertNewUser(t, db, newUserReq) - - if resp.Username == "" { - t.Fatalf("Missing username") - } - - assertCredsExist(t, config.URL().String(), resp.Username, password) - - require.Regexp(t, `^token_[a-zA-Z0-9]{10}$`, resp.Username) -} - -func TestUpdateUser_expiration(t *testing.T) { - // This test should end up with a no-op since the expiration doesn't do anything in Influx - - cleanup, config := prepareInfluxdbTestContainer(t) - defer cleanup() - - db := new() - req := dbplugin.InitializeRequest{ - Config: config.connectionParams(), - VerifyConnection: true, - } - dbtesting.AssertInitialize(t, db, req) - - password := "nuozxby98523u89bdfnkjl" - newUserReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{createUserStatements}, - }, - Password: password, - Expiration: time.Now().Add(1 * time.Minute), - } - newUserResp := dbtesting.AssertNewUser(t, db, newUserReq) - - assertCredsExist(t, config.URL().String(), newUserResp.Username, password) - - renewReq := dbplugin.UpdateUserRequest{ - Username: newUserResp.Username, - Expiration: &dbplugin.ChangeExpiration{ - NewExpiration: time.Now().Add(5 * time.Minute), - }, - } - dbtesting.AssertUpdateUser(t, db, renewReq) - - // Make sure the user hasn't changed - assertCredsExist(t, config.URL().String(), newUserResp.Username, password) -} - -func TestUpdateUser_password(t *testing.T) { - cleanup, config := prepareInfluxdbTestContainer(t) - defer cleanup() - - db := new() - req := dbplugin.InitializeRequest{ - Config: config.connectionParams(), - VerifyConnection: true, - } - dbtesting.AssertInitialize(t, db, req) - - initialPassword := "nuozxby98523u89bdfnkjl" - newUserReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{createUserStatements}, - }, - Password: initialPassword, - Expiration: time.Now().Add(1 * time.Minute), - } - newUserResp := dbtesting.AssertNewUser(t, db, newUserReq) - - assertCredsExist(t, config.URL().String(), newUserResp.Username, initialPassword) - - newPassword := "y89qgmbzadiygry8uazodijnb" - newPasswordReq := dbplugin.UpdateUserRequest{ - Username: newUserResp.Username, - Password: &dbplugin.ChangePassword{ - NewPassword: newPassword, - }, - } - dbtesting.AssertUpdateUser(t, db, newPasswordReq) - - assertCredsDoNotExist(t, config.URL().String(), newUserResp.Username, initialPassword) - assertCredsExist(t, config.URL().String(), newUserResp.Username, newPassword) -} - -// TestInfluxdb_RevokeDeletedUser tests attempting to revoke a user that was -// deleted externally. Guards against a panic, see -// https://github.com/hashicorp/vault/issues/6734 -// Updated to attempt to delete a user that never existed to replicate a similar scenario since -// the cleanup function from `prepareInfluxdbTestContainer` does not do anything if using an -// external InfluxDB instance rather than spinning one up for the test. -func TestInfluxdb_RevokeDeletedUser(t *testing.T) { - cleanup, config := prepareInfluxdbTestContainer(t) - defer cleanup() - - db := new() - req := dbplugin.InitializeRequest{ - Config: config.connectionParams(), - VerifyConnection: true, - } - dbtesting.AssertInitialize(t, db, req) - - // attempt to revoke a user that does not exist - delReq := dbplugin.DeleteUserRequest{ - Username: "someuser", - } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _, err := db.DeleteUser(ctx, delReq) - if err == nil { - t.Fatalf("Expected err, got nil") - } -} - -func TestInfluxdb_RevokeUser(t *testing.T) { - cleanup, config := prepareInfluxdbTestContainer(t) - defer cleanup() - - db := new() - req := dbplugin.InitializeRequest{ - Config: config.connectionParams(), - VerifyConnection: true, - } - dbtesting.AssertInitialize(t, db, req) - - initialPassword := "nuozxby98523u89bdfnkjl" - newUserReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{createUserStatements}, - }, - Password: initialPassword, - Expiration: time.Now().Add(1 * time.Minute), - } - newUserResp := dbtesting.AssertNewUser(t, db, newUserReq) - - assertCredsExist(t, config.URL().String(), newUserResp.Username, initialPassword) - - delReq := dbplugin.DeleteUserRequest{ - Username: newUserResp.Username, - } - dbtesting.AssertDeleteUser(t, db, delReq) - assertCredsDoNotExist(t, config.URL().String(), newUserResp.Username, initialPassword) -} - -func assertCredsExist(t testing.TB, address, username, password string) { - t.Helper() - err := testCredsExist(address, username, password) - if err != nil { - t.Fatalf("Could not log in as %q", username) - } -} - -func assertCredsDoNotExist(t testing.TB, address, username, password string) { - t.Helper() - err := testCredsExist(address, username, password) - if err == nil { - t.Fatalf("Able to log in as %q when it shouldn't", username) - } -} - -func testCredsExist(address, username, password string) error { - conf := influx.HTTPConfig{ - Addr: address, - Username: username, - Password: password, - } - cli, err := influx.NewHTTPClient(conf) - if err != nil { - return fmt.Errorf("Error creating InfluxDB Client: %w", err) - } - defer cli.Close() - _, _, err = cli.Ping(1) - if err != nil { - return fmt.Errorf("error checking server ping: %w", err) - } - q := influx.NewQuery("SHOW SERIES ON vault", "", "") - response, err := cli.Query(q) - if err != nil { - return fmt.Errorf("error querying influxdb server: %w", err) - } - if response != nil && response.Error() != nil { - return fmt.Errorf("error using the correct influx database: %w", response.Error()) - } - return nil -} diff --git a/plugins/database/mysql/connection_producer_test.go b/plugins/database/mysql/connection_producer_test.go deleted file mode 100644 index 566dab8c3..000000000 --- a/plugins/database/mysql/connection_producer_test.go +++ /dev/null @@ -1,319 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package mysql - -import ( - "context" - "database/sql" - "fmt" - "io/ioutil" - "os" - paths "path" - "path/filepath" - "reflect" - "testing" - "time" - - "github.com/hashicorp/vault/helper/testhelpers/certhelpers" - "github.com/hashicorp/vault/sdk/database/helper/dbutil" - "github.com/ory/dockertest" -) - -func Test_addTLStoDSN(t *testing.T) { - type testCase struct { - rootUrl string - tlsConfigName string - expectedResult string - } - - tests := map[string]testCase{ - "no tls, no query string": { - rootUrl: "user:password@tcp(localhost:3306)/test", - tlsConfigName: "", - expectedResult: "user:password@tcp(localhost:3306)/test", - }, - "tls, no query string": { - rootUrl: "user:password@tcp(localhost:3306)/test", - tlsConfigName: "tlsTest101", - expectedResult: "user:password@tcp(localhost:3306)/test?tls=tlsTest101", - }, - "tls, query string": { - rootUrl: "user:password@tcp(localhost:3306)/test?foo=bar", - tlsConfigName: "tlsTest101", - expectedResult: "user:password@tcp(localhost:3306)/test?tls=tlsTest101&foo=bar", - }, - "tls, query string, ? in password": { - rootUrl: "user:pa?ssword?@tcp(localhost:3306)/test?foo=bar", - tlsConfigName: "tlsTest101", - expectedResult: "user:pa?ssword?@tcp(localhost:3306)/test?tls=tlsTest101&foo=bar", - }, - "tls, valid tls parameter in query string": { - rootUrl: "user:password@tcp(localhost:3306)/test?tls=true", - tlsConfigName: "", - expectedResult: "user:password@tcp(localhost:3306)/test?tls=true", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - tCase := mySQLConnectionProducer{ - ConnectionURL: test.rootUrl, - tlsConfigName: test.tlsConfigName, - } - - actual, err := tCase.addTLStoDSN() - if err != nil { - t.Fatalf("error occurred in test: %s", err) - } - if actual != test.expectedResult { - t.Fatalf("generated: %s, expected: %s", actual, test.expectedResult) - } - }) - } -} - -func TestInit_clientTLS(t *testing.T) { - t.Skip("Skipping this test because CircleCI can't mount the files we need without further investigation: " + - "https://support.circleci.com/hc/en-us/articles/360007324514-How-can-I-mount-volumes-to-docker-containers-") - - // Set up temp directory so we can mount it to the docker container - confDir := makeTempDir(t) - defer os.RemoveAll(confDir) - - // Create certificates for MySQL authentication - caCert := certhelpers.NewCert(t, - certhelpers.CommonName("test certificate authority"), - certhelpers.IsCA(true), - certhelpers.SelfSign(), - ) - serverCert := certhelpers.NewCert(t, - certhelpers.CommonName("server"), - certhelpers.DNS("localhost"), - certhelpers.Parent(caCert), - ) - clientCert := certhelpers.NewCert(t, - certhelpers.CommonName("client"), - certhelpers.DNS("client"), - certhelpers.Parent(caCert), - ) - - writeFile(t, paths.Join(confDir, "ca.pem"), caCert.CombinedPEM(), 0o644) - writeFile(t, paths.Join(confDir, "server-cert.pem"), serverCert.Pem, 0o644) - writeFile(t, paths.Join(confDir, "server-key.pem"), serverCert.PrivateKeyPEM(), 0o644) - writeFile(t, paths.Join(confDir, "client.pem"), clientCert.CombinedPEM(), 0o644) - - // ////////////////////////////////////////////////////// - // Set up MySQL config file - rawConf := ` -[mysqld] -ssl -ssl-ca=/etc/mysql/ca.pem -ssl-cert=/etc/mysql/server-cert.pem -ssl-key=/etc/mysql/server-key.pem` - - writeFile(t, paths.Join(confDir, "my.cnf"), []byte(rawConf), 0o644) - - // ////////////////////////////////////////////////////// - // Start MySQL container - retURL, cleanup := startMySQLWithTLS(t, "5.7", confDir) - defer cleanup() - - // ////////////////////////////////////////////////////// - // Set up x509 user - mClient := connect(t, retURL) - - username := setUpX509User(t, mClient, clientCert) - - // ////////////////////////////////////////////////////// - // Test - mysql := newMySQL(DefaultUserNameTemplate) - - conf := map[string]interface{}{ - "connection_url": retURL, - "username": username, - "tls_certificate_key": clientCert.CombinedPEM(), - "tls_ca": caCert.Pem, - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - _, err := mysql.Init(ctx, conf, true) - if err != nil { - t.Fatalf("Unable to initialize mysql engine: %s", err) - } - - // Initialization complete. The connection was established, but we need to ensure - // that we're connected as the right user - whoamiCmd := "SELECT CURRENT_USER()" - - client, err := mysql.getConnection(ctx) - if err != nil { - t.Fatalf("Unable to make connection to MySQL: %s", err) - } - stmt, err := client.Prepare(whoamiCmd) - if err != nil { - t.Fatalf("Unable to prepare MySQL statementL %s", err) - } - - results := stmt.QueryRow() - - expected := fmt.Sprintf("%s@%%", username) - - var result string - if err := results.Scan(&result); err != nil { - t.Fatalf("result could not be scanned from result set: %s", err) - } - - if !reflect.DeepEqual(result, expected) { - t.Fatalf("Actual:%#v\nExpected:\n%#v", result, expected) - } -} - -func makeTempDir(t *testing.T) (confDir string) { - confDir, err := ioutil.TempDir(".", "mysql-test-data") - if err != nil { - t.Fatalf("Unable to make temp directory: %s", err) - } - // Convert the directory to an absolute path because docker needs it when mounting - confDir, err = filepath.Abs(filepath.Clean(confDir)) - if err != nil { - t.Fatalf("Unable to determine where temp directory is on absolute path: %s", err) - } - return confDir -} - -func startMySQLWithTLS(t *testing.T, version string, confDir string) (retURL string, cleanup func()) { - if os.Getenv("MYSQL_URL") != "" { - return os.Getenv("MYSQL_URL"), func() {} - } - - pool, err := dockertest.NewPool("") - if err != nil { - t.Fatalf("Failed to connect to docker: %s", err) - } - pool.MaxWait = 30 * time.Second - - containerName := "mysql-unit-test" - - // Remove previously running container if it is still running because cleanup failed - err = pool.RemoveContainerByName(containerName) - if err != nil { - t.Fatalf("Unable to remove old running containers: %s", err) - } - - username := "root" - password := "x509test" - - runOpts := &dockertest.RunOptions{ - Name: containerName, - Repository: "mysql", - Tag: version, - Cmd: []string{"--defaults-extra-file=/etc/mysql/my.cnf", "--auto-generate-certs=OFF"}, - Env: []string{fmt.Sprintf("MYSQL_ROOT_PASSWORD=%s", password)}, - // Mount the directory from local filesystem into the container - Mounts: []string{ - fmt.Sprintf("%s:/etc/mysql", confDir), - }, - } - - resource, err := pool.RunWithOptions(runOpts) - if err != nil { - t.Fatalf("Could not start local mysql docker container: %s", err) - } - resource.Expire(30) - - cleanup = func() { - err := pool.Purge(resource) - if err != nil { - t.Fatalf("Failed to cleanup local container: %s", err) - } - } - - dsn := fmt.Sprintf("{{username}}:{{password}}@tcp(localhost:%s)/mysql", resource.GetPort("3306/tcp")) - - url := dbutil.QueryHelper(dsn, map[string]string{ - "username": username, - "password": password, - }) - // exponential backoff-retry - err = pool.Retry(func() error { - var err error - - db, err := sql.Open("mysql", url) - if err != nil { - t.Logf("err: %s", err) - return err - } - defer db.Close() - return db.Ping() - }) - if err != nil { - cleanup() - t.Fatalf("Could not connect to mysql docker container: %s", err) - } - - return dsn, cleanup -} - -func connect(t *testing.T, dsn string) (db *sql.DB) { - url := dbutil.QueryHelper(dsn, map[string]string{ - "username": "root", - "password": "x509test", - }) - - db, err := sql.Open("mysql", url) - if err != nil { - t.Fatalf("Unable to make connection to MySQL: %s", err) - } - - err = db.Ping() - if err != nil { - t.Fatalf("Failed to ping MySQL server: %s", err) - } - - return db -} - -func setUpX509User(t *testing.T, db *sql.DB, cert certhelpers.Certificate) (username string) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - username = cert.Template.Subject.CommonName - - cmds := []string{ - fmt.Sprintf("CREATE USER %s IDENTIFIED BY '' REQUIRE X509", username), - fmt.Sprintf("GRANT ALL ON mysql.* TO '%s'@'%s' REQUIRE X509", username, "%"), - } - - for _, cmd := range cmds { - stmt, err := db.PrepareContext(ctx, cmd) - if err != nil { - t.Fatalf("Failed to prepare query: %s", err) - } - - _, err = stmt.ExecContext(ctx) - if err != nil { - t.Fatalf("Failed to create x509 user in database: %s", err) - } - err = stmt.Close() - if err != nil { - t.Fatalf("Failed to close prepared statement: %s", err) - } - } - - return username -} - -// //////////////////////////////////////////////////////////////////////////// -// Writing to file -// //////////////////////////////////////////////////////////////////////////// -func writeFile(t *testing.T, filename string, data []byte, perms os.FileMode) { - t.Helper() - - err := ioutil.WriteFile(filename, data, perms) - if err != nil { - t.Fatalf("Unable to write to file [%s]: %s", filename, err) - } -} diff --git a/plugins/database/mysql/mysql_test.go b/plugins/database/mysql/mysql_test.go deleted file mode 100644 index 1a3c5b1d1..000000000 --- a/plugins/database/mysql/mysql_test.go +++ /dev/null @@ -1,796 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package mysql - -import ( - "context" - "database/sql" - "fmt" - "strings" - "testing" - "time" - - stdmysql "github.com/go-sql-driver/mysql" - "github.com/hashicorp/go-secure-stdlib/strutil" - mysqlhelper "github.com/hashicorp/vault/helper/testhelpers/mysql" - dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing" - "github.com/hashicorp/vault/sdk/database/helper/credsutil" - "github.com/hashicorp/vault/sdk/database/helper/dbutil" - "github.com/stretchr/testify/require" -) - -var _ dbplugin.Database = (*MySQL)(nil) - -func TestMySQL_Initialize(t *testing.T) { - type testCase struct { - rootPassword string - } - - tests := map[string]testCase{ - "non-special characters in root password": { - rootPassword: "B44a30c4C04D0aAaE140", - }, - "special characters in root password": { - rootPassword: "#secret!%25#{@}", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - testInitialize(t, test.rootPassword) - }) - } -} - -func testInitialize(t *testing.T, rootPassword string) { - cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, rootPassword) - defer cleanup() - - mySQLConfig, err := stdmysql.ParseDSN(connURL) - if err != nil { - panic(fmt.Sprintf("Test failure: connection URL is invalid: %s", err)) - } - rootUser := mySQLConfig.User - mySQLConfig.User = "{{username}}" - mySQLConfig.Passwd = "{{password}}" - tmplConnURL := mySQLConfig.FormatDSN() - - type testCase struct { - initRequest dbplugin.InitializeRequest - expectedResp dbplugin.InitializeResponse - - expectErr bool - expectInitialized bool - } - - tests := map[string]testCase{ - "missing connection_url": { - initRequest: dbplugin.InitializeRequest{ - Config: map[string]interface{}{}, - VerifyConnection: true, - }, - expectedResp: dbplugin.InitializeResponse{}, - expectErr: true, - expectInitialized: false, - }, - "basic config": { - initRequest: dbplugin.InitializeRequest{ - Config: map[string]interface{}{ - "connection_url": connURL, - }, - VerifyConnection: true, - }, - expectedResp: dbplugin.InitializeResponse{ - Config: map[string]interface{}{ - "connection_url": connURL, - }, - }, - expectErr: false, - expectInitialized: true, - }, - "username and password replacement in connection_url": { - initRequest: dbplugin.InitializeRequest{ - Config: map[string]interface{}{ - "connection_url": tmplConnURL, - "username": rootUser, - "password": rootPassword, - }, - VerifyConnection: true, - }, - expectedResp: dbplugin.InitializeResponse{ - Config: map[string]interface{}{ - "connection_url": tmplConnURL, - "username": rootUser, - "password": rootPassword, - }, - }, - expectErr: false, - expectInitialized: true, - }, - "invalid username template": { - initRequest: dbplugin.InitializeRequest{ - Config: map[string]interface{}{ - "connection_url": connURL, - "username_template": "{{.FieldThatDoesNotExist}}", - }, - VerifyConnection: true, - }, - expectedResp: dbplugin.InitializeResponse{}, - expectErr: true, - expectInitialized: false, - }, - "bad username template": { - initRequest: dbplugin.InitializeRequest{ - Config: map[string]interface{}{ - "connection_url": connURL, - "username_template": "{{ .DisplayName", // Explicitly bad template - }, - VerifyConnection: true, - }, - expectedResp: dbplugin.InitializeResponse{}, - expectErr: true, - expectInitialized: false, - }, - "custom username template": { - initRequest: dbplugin.InitializeRequest{ - Config: map[string]interface{}{ - "connection_url": connURL, - "username_template": "foo-{{random 10}}-{{.DisplayName}}", - }, - VerifyConnection: true, - }, - expectedResp: dbplugin.InitializeResponse{ - Config: map[string]interface{}{ - "connection_url": connURL, - "username_template": "foo-{{random 10}}-{{.DisplayName}}", - }, - }, - expectErr: false, - expectInitialized: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - db := newMySQL(DefaultUserNameTemplate) - defer dbtesting.AssertClose(t, db) - initResp, err := db.Initialize(context.Background(), test.initRequest) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - require.Equal(t, test.expectedResp, initResp) - require.Equal(t, test.expectInitialized, db.Initialized, "Initialized variable not set correctly") - }) - } -} - -func TestMySQL_NewUser_nonLegacy(t *testing.T) { - displayName := "token" - roleName := "testrole" - - type testCase struct { - usernameTemplate string - - newUserReq dbplugin.NewUserRequest - - expectedUsernameRegex string - expectErr bool - } - - tests := map[string]testCase{ - "name statements": { - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; - GRANT SELECT ON *.* TO '{{name}}'@'%';`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^v-token-testrole-[a-zA-Z0-9]{15}$`, - expectErr: false, - }, - "username statements": { - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}'; - GRANT SELECT ON *.* TO '{{username}}'@'%';`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^v-token-testrole-[a-zA-Z0-9]{15}$`, - expectErr: false, - }, - "prepared name statements": { - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; - set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{name}}'@'%'"); - PREPARE grantStmt from @grants; - EXECUTE grantStmt; - DEALLOCATE PREPARE grantStmt;`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^v-token-testrole-[a-zA-Z0-9]{15}$`, - expectErr: false, - }, - "prepared username statements": { - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}'; - set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{username}}'@'%'"); - PREPARE grantStmt from @grants; - EXECUTE grantStmt; - DEALLOCATE PREPARE grantStmt;`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^v-token-testrole-[a-zA-Z0-9]{15}$`, - expectErr: false, - }, - "custom username template": { - usernameTemplate: "foo-{{random 10}}-{{.RoleName | uppercase}}", - - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}'; - GRANT SELECT ON *.* TO '{{username}}'@'%';`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^foo-[a-zA-Z0-9]{10}-TESTROLE$`, - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret") - defer cleanup() - - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - "username_template": test.usernameTemplate, - } - - initReq := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - db := newMySQL(DefaultUserNameTemplate) - defer db.Close() - _, err := db.Initialize(context.Background(), initReq) - require.NoError(t, err) - - userResp, err := db.NewUser(context.Background(), test.newUserReq) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - require.Regexp(t, test.expectedUsernameRegex, userResp.Username) - - err = mysqlhelper.TestCredsExist(t, connURL, userResp.Username, test.newUserReq.Password) - require.NoError(t, err, "Failed to connect with credentials") - }) - } -} - -func TestMySQL_NewUser_legacy(t *testing.T) { - displayName := "token" - roleName := "testrole" - - type testCase struct { - usernameTemplate string - - newUserReq dbplugin.NewUserRequest - - expectedUsernameRegex string - expectErr bool - } - - tests := map[string]testCase{ - "name statements": { - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; - GRANT SELECT ON *.* TO '{{name}}'@'%';`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^v-test-[a-zA-Z0-9]{9}$`, - expectErr: false, - }, - "username statements": { - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}'; - GRANT SELECT ON *.* TO '{{username}}'@'%';`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^v-test-[a-zA-Z0-9]{9}$`, - expectErr: false, - }, - "prepared name statements": { - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; - set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{name}}'@'%'"); - PREPARE grantStmt from @grants; - EXECUTE grantStmt; - DEALLOCATE PREPARE grantStmt;`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^v-test-[a-zA-Z0-9]{9}$`, - expectErr: false, - }, - "prepared username statements": { - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}'; - set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{username}}'@'%'"); - PREPARE grantStmt from @grants; - EXECUTE grantStmt; - DEALLOCATE PREPARE grantStmt;`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^v-test-[a-zA-Z0-9]{9}$`, - expectErr: false, - }, - "custom username template": { - usernameTemplate: `{{printf "foo-%s-%s" (random 5) (.RoleName | uppercase) | truncate 16}}`, - - newUserReq: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: displayName, - RoleName: roleName, - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}'; - GRANT SELECT ON *.* TO '{{username}}'@'%';`, - }, - }, - Password: "09g8hanbdfkVSM", - Expiration: time.Now().Add(time.Minute), - }, - - expectedUsernameRegex: `^foo-[a-zA-Z0-9]{5}-TESTRO$`, - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret") - defer cleanup() - - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - "username_template": test.usernameTemplate, - } - - initReq := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - db := newMySQL(DefaultLegacyUserNameTemplate) - defer db.Close() - _, err := db.Initialize(context.Background(), initReq) - require.NoError(t, err) - - userResp, err := db.NewUser(context.Background(), test.newUserReq) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - require.Regexp(t, test.expectedUsernameRegex, userResp.Username) - - err = mysqlhelper.TestCredsExist(t, connURL, userResp.Username, test.newUserReq.Password) - require.NoError(t, err, "Failed to connect with credentials") - }) - } -} - -func TestMySQL_RotateRootCredentials(t *testing.T) { - type testCase struct { - statements []string - } - - tests := map[string]testCase{ - "empty statements": { - statements: nil, - }, - "default username": { - statements: []string{defaultMySQLRotateCredentialsSQL}, - }, - "default name": { - statements: []string{ - ` - ALTER USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';`, - }, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret") - defer cleanup() - - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - "username": "root", - "password": "secret", - } - - // Give a timeout just in case the test decides to be problematic - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - initReq := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - db := newMySQL(DefaultUserNameTemplate) - defer db.Close() - _, err := db.Initialize(context.Background(), initReq) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !db.Initialized { - t.Fatal("Database should be initialized") - } - - updateReq := dbplugin.UpdateUserRequest{ - Username: "root", - Password: &dbplugin.ChangePassword{ - NewPassword: "different_sercret", - Statements: dbplugin.Statements{ - Commands: test.statements, - }, - }, - } - - _, err = db.UpdateUser(ctx, updateReq) - if err != nil { - t.Fatalf("err: %v", err) - } - err = mysqlhelper.TestCredsExist(t, connURL, updateReq.Username, updateReq.Password.NewPassword) - if err != nil { - t.Fatalf("Could not connect with new credentials: %s", err) - } - - // verify old password doesn't work - if err := mysqlhelper.TestCredsExist(t, connURL, updateReq.Username, "secret"); err == nil { - t.Fatalf("Should not be able to connect with initial credentials") - } - - err = db.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - }) - } -} - -func TestMySQL_DeleteUser(t *testing.T) { - type testCase struct { - revokeStmts []string - } - - tests := map[string]testCase{ - "empty statements": { - revokeStmts: nil, - }, - "default name": { - revokeStmts: []string{defaultMysqlRevocationStmts}, - }, - "default username": { - revokeStmts: []string{ - ` - REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{username}}'@'%'; - DROP USER '{{username}}'@'%'`, - }, - }, - } - - // Shared test container for speed - there should not be any overlap between the tests - cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret") - defer cleanup() - - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - } - - initReq := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - db := newMySQL(DefaultUserNameTemplate) - defer db.Close() - _, err := db.Initialize(context.Background(), initReq) - if err != nil { - t.Fatalf("err: %s", err) - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - password, err := credsutil.RandomAlphaNumeric(32, false) - if err != nil { - t.Fatalf("unable to generate password: %s", err) - } - - createReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{ - ` - CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; - GRANT SELECT ON *.* TO '{{name}}'@'%';`, - }, - }, - Password: password, - Expiration: time.Now().Add(time.Minute), - } - - // Give a timeout just in case the test decides to be problematic - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - userResp, err := db.NewUser(ctx, createReq) - if err != nil { - t.Fatalf("err: %s", err) - } - - if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err != nil { - t.Fatalf("Could not connect with new credentials: %s", err) - } - - deleteReq := dbplugin.DeleteUserRequest{ - Username: userResp.Username, - Statements: dbplugin.Statements{ - Commands: test.revokeStmts, - }, - } - _, err = db.DeleteUser(context.Background(), deleteReq) - if err != nil { - t.Fatalf("err: %s", err) - } - - if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err == nil { - t.Fatalf("Credentials were not revoked!") - } - }) - } -} - -func TestMySQL_UpdateUser(t *testing.T) { - type testCase struct { - rotateStmts []string - } - - tests := map[string]testCase{ - "empty statements": { - rotateStmts: nil, - }, - "custom statement name": { - rotateStmts: []string{` - ALTER USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';`}, - }, - "custom statement username": { - rotateStmts: []string{` - ALTER USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';`}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret") - defer cleanup() - - // create the database user and verify we can access - dbUser := "vaultstatictest" - initPassword := "password" - - createStatements := ` - CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; - GRANT SELECT ON *.* TO '{{name}}'@'%';` - - createTestMySQLUser(t, connURL, dbUser, initPassword, createStatements) - if err := mysqlhelper.TestCredsExist(t, connURL, dbUser, initPassword); err != nil { - t.Fatalf("Could not connect with credentials: %s", err) - } - - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - } - - initReq := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - // Give a timeout just in case the test decides to be problematic - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - db := newMySQL(DefaultUserNameTemplate) - defer db.Close() - _, err := db.Initialize(context.Background(), initReq) - if err != nil { - t.Fatalf("err: %s", err) - } - - newPassword, err := credsutil.RandomAlphaNumeric(32, false) - if err != nil { - t.Fatalf("unable to generate password: %s", err) - } - - updateReq := dbplugin.UpdateUserRequest{ - Username: dbUser, - Password: &dbplugin.ChangePassword{ - NewPassword: newPassword, - Statements: dbplugin.Statements{ - Commands: test.rotateStmts, - }, - }, - } - - _, err = db.UpdateUser(ctx, updateReq) - if err != nil { - t.Fatalf("err: %s", err) - } - - // verify new password works - if err := mysqlhelper.TestCredsExist(t, connURL, dbUser, newPassword); err != nil { - t.Fatalf("Could not connect with new credentials: %s", err) - } - - // verify old password doesn't work - if err := mysqlhelper.TestCredsExist(t, connURL, dbUser, initPassword); err == nil { - t.Fatalf("Should not be able to connect with initial credentials") - } - }) - } -} - -func createTestMySQLUser(t *testing.T, connURL, username, password, query string) { - t.Helper() - db, err := sql.Open("mysql", connURL) - defer db.Close() - if err != nil { - t.Fatal(err) - } - - // Start a transaction - ctx := context.Background() - tx, err := db.BeginTx(ctx, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = tx.Rollback() - }() - - // copied from mysql.go - for _, query := range strutil.ParseArbitraryStringSlice(query, ";") { - query = strings.TrimSpace(query) - if len(query) == 0 { - continue - } - query = dbutil.QueryHelper(query, map[string]string{ - "name": username, - "password": password, - }) - - stmt, err := tx.PrepareContext(ctx, query) - if err != nil { - if e, ok := err.(*stdmysql.MySQLError); ok && e.Number == 1295 { - _, err = tx.ExecContext(ctx, query) - if err != nil { - t.Fatal(err) - } - stmt.Close() - continue - } - - t.Fatal(err) - } - if _, err := stmt.ExecContext(ctx); err != nil { - stmt.Close() - t.Fatal(err) - } - stmt.Close() - } -} diff --git a/plugins/database/postgresql/postgresql_test.go b/plugins/database/postgresql/postgresql_test.go deleted file mode 100644 index 06f1e61b4..000000000 --- a/plugins/database/postgresql/postgresql_test.go +++ /dev/null @@ -1,1240 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package postgresql - -import ( - "context" - "database/sql" - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/hashicorp/vault/helper/testhelpers/postgresql" - "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing" - "github.com/hashicorp/vault/sdk/database/helper/dbutil" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/hashicorp/vault/sdk/helper/template" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func getPostgreSQL(t *testing.T, options map[string]interface{}) (*PostgreSQL, func()) { - cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster") - - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - } - for k, v := range options { - connectionDetails[k] = v - } - - req := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - db := new() - dbtesting.AssertInitialize(t, db, req) - - if !db.Initialized { - t.Fatal("Database should be initialized") - } - return db, cleanup -} - -func TestPostgreSQL_Initialize(t *testing.T) { - db, cleanup := getPostgreSQL(t, map[string]interface{}{ - "max_open_connections": 5, - }) - defer cleanup() - - if err := db.Close(); err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestPostgreSQL_InitializeWithStringVals(t *testing.T) { - db, cleanup := getPostgreSQL(t, map[string]interface{}{ - "max_open_connections": "5", - }) - defer cleanup() - - if err := db.Close(); err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestPostgreSQL_Initialize_ConnURLWithDSNFormat(t *testing.T) { - cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - dsnConnURL, err := dbutil.ParseURL(connURL) - if err != nil { - t.Fatal(err) - } - - connectionDetails := map[string]interface{}{ - "connection_url": dsnConnURL, - } - - req := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - db := new() - dbtesting.AssertInitialize(t, db, req) - - if !db.Initialized { - t.Fatal("Database should be initialized") - } -} - -// TestPostgreSQL_PasswordAuthentication tests that the default "password_authentication" is "none", and that -// an error is returned if an invalid "password_authentication" is provided. -func TestPostgreSQL_PasswordAuthentication(t *testing.T) { - cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - dsnConnURL, err := dbutil.ParseURL(connURL) - assert.NoError(t, err) - db := new() - - ctx := context.Background() - - t.Run("invalid-password-authentication", func(t *testing.T) { - connectionDetails := map[string]interface{}{ - "connection_url": dsnConnURL, - "password_authentication": "invalid-password-authentication", - } - - req := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - _, err := db.Initialize(ctx, req) - assert.EqualError(t, err, "'invalid-password-authentication' is not a valid password authentication type") - }) - - t.Run("default-is-none", func(t *testing.T) { - connectionDetails := map[string]interface{}{ - "connection_url": dsnConnURL, - } - - req := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - _ = dbtesting.AssertInitialize(t, db, req) - assert.Equal(t, passwordAuthenticationPassword, db.passwordAuthentication) - }) -} - -// TestPostgreSQL_PasswordAuthentication_SCRAMSHA256 tests that password_authentication works when set to scram-sha-256. -// When sending an encrypted password, the raw password should still successfully authenticate the user. -func TestPostgreSQL_PasswordAuthentication_SCRAMSHA256(t *testing.T) { - cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - dsnConnURL, err := dbutil.ParseURL(connURL) - if err != nil { - t.Fatal(err) - } - - connectionDetails := map[string]interface{}{ - "connection_url": dsnConnURL, - "password_authentication": string(passwordAuthenticationSCRAMSHA256), - } - - req := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - db := new() - resp := dbtesting.AssertInitialize(t, db, req) - assert.Equal(t, string(passwordAuthenticationSCRAMSHA256), resp.Config["password_authentication"]) - - if !db.Initialized { - t.Fatal("Database should be initialized") - } - - ctx := context.Background() - newUserRequest := dbplugin.NewUserRequest{ - Statements: dbplugin.Statements{ - Commands: []string{ - ` - CREATE ROLE "{{name}}" WITH - LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; - GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";`, - }, - }, - Password: "somesecurepassword", - Expiration: time.Now().Add(1 * time.Minute), - } - newUserResponse, err := db.NewUser(ctx, newUserRequest) - - assertCredsExist(t, db.ConnectionURL, newUserResponse.Username, newUserRequest.Password) -} - -func TestPostgreSQL_NewUser(t *testing.T) { - type testCase struct { - req dbplugin.NewUserRequest - expectErr bool - credsAssertion credsAssertion - } - - tests := map[string]testCase{ - "no creation statements": { - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - // No statements - Password: "somesecurepassword", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: true, - credsAssertion: assertCreds( - assertUsernameRegex("^$"), - assertCredsDoNotExist, - ), - }, - "admin name": { - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{ - ` - CREATE ROLE "{{name}}" WITH - LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; - GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";`, - }, - }, - Password: "somesecurepassword", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: false, - credsAssertion: assertCreds( - assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"), - assertCredsExist, - ), - }, - "admin username": { - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{ - ` - CREATE ROLE "{{username}}" WITH - LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; - GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{username}}";`, - }, - }, - Password: "somesecurepassword", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: false, - credsAssertion: assertCreds( - assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"), - assertCredsExist, - ), - }, - "read only name": { - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{ - ` - CREATE ROLE "{{name}}" WITH - LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; - GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}"; - GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO "{{name}}";`, - }, - }, - Password: "somesecurepassword", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: false, - credsAssertion: assertCreds( - assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"), - assertCredsExist, - ), - }, - "read only username": { - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{ - ` - CREATE ROLE "{{username}}" WITH - LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; - GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{username}}"; - GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO "{{username}}";`, - }, - }, - Password: "somesecurepassword", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: false, - credsAssertion: assertCreds( - assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"), - assertCredsExist, - ), - }, - // https://github.com/hashicorp/vault/issues/6098 - "reproduce GH-6098": { - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{ - // NOTE: "rolname" in the following line is not a typo. - "DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname='my_role') THEN CREATE ROLE my_role; END IF; END $$", - }, - }, - Password: "somesecurepassword", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: false, - credsAssertion: assertCreds( - assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"), - assertCredsDoNotExist, - ), - }, - "reproduce issue with template": { - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{ - `DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname='my_role') THEN CREATE ROLE "{{username}}"; END IF; END $$`, - }, - }, - Password: "somesecurepassword", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: false, - credsAssertion: assertCreds( - assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"), - assertCredsDoNotExist, - ), - }, - "large block statements": { - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: newUserLargeBlockStatements, - }, - Password: "somesecurepassword", - Expiration: time.Now().Add(1 * time.Minute), - }, - expectErr: false, - credsAssertion: assertCreds( - assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"), - assertCredsExist, - ), - }, - } - - // Shared test container for speed - there should not be any overlap between the tests - db, cleanup := getPostgreSQL(t, nil) - defer cleanup() - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - // Give a timeout just in case the test decides to be problematic - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - resp, err := db.NewUser(ctx, test.req) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - test.credsAssertion(t, db.ConnectionURL, resp.Username, test.req.Password) - - // Ensure that the role doesn't expire immediately - time.Sleep(2 * time.Second) - - test.credsAssertion(t, db.ConnectionURL, resp.Username, test.req.Password) - }) - } -} - -func TestUpdateUser_Password(t *testing.T) { - type testCase struct { - statements []string - expectErr bool - credsAssertion credsAssertion - } - - tests := map[string]testCase{ - "default statements": { - statements: nil, - expectErr: false, - credsAssertion: assertCredsExist, - }, - "explicit default statements": { - statements: []string{defaultChangePasswordStatement}, - expectErr: false, - credsAssertion: assertCredsExist, - }, - "name instead of username": { - statements: []string{`ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}';`}, - expectErr: false, - credsAssertion: assertCredsExist, - }, - "bad statements": { - statements: []string{`asdofyas8uf77asoiajv`}, - expectErr: true, - credsAssertion: assertCredsDoNotExist, - }, - } - - // Shared test container for speed - there should not be any overlap between the tests - db, cleanup := getPostgreSQL(t, nil) - defer cleanup() - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - initialPass := "myreallysecurepassword" - createReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{createAdminUser}, - }, - Password: initialPass, - Expiration: time.Now().Add(2 * time.Second), - } - createResp := dbtesting.AssertNewUser(t, db, createReq) - - assertCredsExist(t, db.ConnectionURL, createResp.Username, initialPass) - - newPass := "somenewpassword" - updateReq := dbplugin.UpdateUserRequest{ - Username: createResp.Username, - Password: &dbplugin.ChangePassword{ - NewPassword: newPass, - Statements: dbplugin.Statements{ - Commands: test.statements, - }, - }, - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _, err := db.UpdateUser(ctx, updateReq) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - test.credsAssertion(t, db.ConnectionURL, createResp.Username, newPass) - }) - } - - t.Run("user does not exist", func(t *testing.T) { - newPass := "somenewpassword" - updateReq := dbplugin.UpdateUserRequest{ - Username: "missing-user", - Password: &dbplugin.ChangePassword{ - NewPassword: newPass, - Statements: dbplugin.Statements{}, - }, - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _, err := db.UpdateUser(ctx, updateReq) - if err == nil { - t.Fatalf("err expected, got nil") - } - - assertCredsDoNotExist(t, db.ConnectionURL, updateReq.Username, newPass) - }) -} - -func TestUpdateUser_Expiration(t *testing.T) { - type testCase struct { - initialExpiration time.Time - newExpiration time.Time - expectedExpiration time.Time - statements []string - expectErr bool - } - - now := time.Now() - tests := map[string]testCase{ - "no statements": { - initialExpiration: now.Add(1 * time.Minute), - newExpiration: now.Add(5 * time.Minute), - expectedExpiration: now.Add(5 * time.Minute), - statements: nil, - expectErr: false, - }, - "default statements with name": { - initialExpiration: now.Add(1 * time.Minute), - newExpiration: now.Add(5 * time.Minute), - expectedExpiration: now.Add(5 * time.Minute), - statements: []string{defaultExpirationStatement}, - expectErr: false, - }, - "default statements with username": { - initialExpiration: now.Add(1 * time.Minute), - newExpiration: now.Add(5 * time.Minute), - expectedExpiration: now.Add(5 * time.Minute), - statements: []string{`ALTER ROLE "{{username}}" VALID UNTIL '{{expiration}}';`}, - expectErr: false, - }, - "bad statements": { - initialExpiration: now.Add(1 * time.Minute), - newExpiration: now.Add(5 * time.Minute), - expectedExpiration: now.Add(1 * time.Minute), - statements: []string{"ladshfouay09sgj"}, - expectErr: true, - }, - } - - // Shared test container for speed - there should not be any overlap between the tests - db, cleanup := getPostgreSQL(t, nil) - defer cleanup() - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - password := "myreallysecurepassword" - initialExpiration := test.initialExpiration.Truncate(time.Second) - createReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{createAdminUser}, - }, - Password: password, - Expiration: initialExpiration, - } - createResp := dbtesting.AssertNewUser(t, db, createReq) - - assertCredsExist(t, db.ConnectionURL, createResp.Username, password) - - actualExpiration := getExpiration(t, db, createResp.Username) - if actualExpiration.IsZero() { - t.Fatalf("Initial expiration is zero but should be set") - } - if !actualExpiration.Equal(initialExpiration) { - t.Fatalf("Actual expiration: %s Expected expiration: %s", actualExpiration, initialExpiration) - } - - newExpiration := test.newExpiration.Truncate(time.Second) - updateReq := dbplugin.UpdateUserRequest{ - Username: createResp.Username, - Expiration: &dbplugin.ChangeExpiration{ - NewExpiration: newExpiration, - Statements: dbplugin.Statements{ - Commands: test.statements, - }, - }, - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _, err := db.UpdateUser(ctx, updateReq) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - expectedExpiration := test.expectedExpiration.Truncate(time.Second) - actualExpiration = getExpiration(t, db, createResp.Username) - if !actualExpiration.Equal(expectedExpiration) { - t.Fatalf("Actual expiration: %s Expected expiration: %s", actualExpiration, expectedExpiration) - } - }) - } -} - -func getExpiration(t testing.TB, db *PostgreSQL, username string) time.Time { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - query := fmt.Sprintf("select valuntil from pg_catalog.pg_user where usename = '%s'", username) - conn, err := db.getConnection(ctx) - if err != nil { - t.Fatalf("Failed to get connection to database: %s", err) - } - - stmt, err := conn.PrepareContext(ctx, query) - if err != nil { - t.Fatalf("Failed to prepare statement: %s", err) - } - defer stmt.Close() - - rows, err := stmt.QueryContext(ctx) - if err != nil { - t.Fatalf("Failed to execute query to get expiration: %s", err) - } - - if !rows.Next() { - return time.Time{} // No expiration - } - rawExp := "" - err = rows.Scan(&rawExp) - if err != nil { - t.Fatalf("Unable to get raw expiration: %s", err) - } - if rawExp == "" { - return time.Time{} // No expiration - } - exp, err := time.Parse(time.RFC3339, rawExp) - if err != nil { - t.Fatalf("Failed to parse expiration %q: %s", rawExp, err) - } - return exp -} - -func TestDeleteUser(t *testing.T) { - type testCase struct { - revokeStmts []string - expectErr bool - credsAssertion credsAssertion - } - - tests := map[string]testCase{ - "no statements": { - revokeStmts: nil, - expectErr: false, - // Wait for a short time before failing because postgres takes a moment to finish deleting the user - credsAssertion: waitUntilCredsDoNotExist(2 * time.Second), - }, - "statements with name": { - revokeStmts: []string{` - REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{name}}"; - REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM "{{name}}"; - REVOKE USAGE ON SCHEMA public FROM "{{name}}"; - - DROP ROLE IF EXISTS "{{name}}";`}, - expectErr: false, - // Wait for a short time before failing because postgres takes a moment to finish deleting the user - credsAssertion: waitUntilCredsDoNotExist(2 * time.Second), - }, - "statements with username": { - revokeStmts: []string{` - REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}"; - REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM "{{username}}"; - REVOKE USAGE ON SCHEMA public FROM "{{username}}"; - - DROP ROLE IF EXISTS "{{username}}";`}, - expectErr: false, - // Wait for a short time before failing because postgres takes a moment to finish deleting the user - credsAssertion: waitUntilCredsDoNotExist(2 * time.Second), - }, - "bad statements": { - revokeStmts: []string{`8a9yhfoiasjff`}, - expectErr: true, - // Wait for a short time before checking because postgres takes a moment to finish deleting the user - credsAssertion: assertCredsExistAfter(100 * time.Millisecond), - }, - "multiline": { - revokeStmts: []string{` - DO $$ BEGIN - REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}"; - REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM "{{username}}"; - REVOKE USAGE ON SCHEMA public FROM "{{username}}"; - DROP ROLE IF EXISTS "{{username}}"; - END $$; - `}, - expectErr: false, - // Wait for a short time before checking because postgres takes a moment to finish deleting the user - credsAssertion: waitUntilCredsDoNotExist(2 * time.Second), - }, - } - - // Shared test container for speed - there should not be any overlap between the tests - db, cleanup := getPostgreSQL(t, nil) - defer cleanup() - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - password := "myreallysecurepassword" - createReq := dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "test", - RoleName: "test", - }, - Statements: dbplugin.Statements{ - Commands: []string{createAdminUser}, - }, - Password: password, - Expiration: time.Now().Add(2 * time.Second), - } - createResp := dbtesting.AssertNewUser(t, db, createReq) - - assertCredsExist(t, db.ConnectionURL, createResp.Username, password) - - deleteReq := dbplugin.DeleteUserRequest{ - Username: createResp.Username, - Statements: dbplugin.Statements{ - Commands: test.revokeStmts, - }, - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - _, err := db.DeleteUser(ctx, deleteReq) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - test.credsAssertion(t, db.ConnectionURL, createResp.Username, password) - }) - } -} - -type credsAssertion func(t testing.TB, connURL, username, password string) - -func assertCreds(assertions ...credsAssertion) credsAssertion { - return func(t testing.TB, connURL, username, password string) { - t.Helper() - for _, assertion := range assertions { - assertion(t, connURL, username, password) - } - } -} - -func assertUsernameRegex(rawRegex string) credsAssertion { - return func(t testing.TB, _, username, _ string) { - t.Helper() - require.Regexp(t, rawRegex, username) - } -} - -func assertCredsExist(t testing.TB, connURL, username, password string) { - t.Helper() - err := testCredsExist(t, connURL, username, password) - if err != nil { - t.Fatalf("user does not exist: %s", err) - } -} - -func assertCredsDoNotExist(t testing.TB, connURL, username, password string) { - t.Helper() - err := testCredsExist(t, connURL, username, password) - if err == nil { - t.Fatalf("user should not exist but does") - } -} - -func waitUntilCredsDoNotExist(timeout time.Duration) credsAssertion { - return func(t testing.TB, connURL, username, password string) { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - ticker := time.NewTicker(10 * time.Millisecond) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - t.Fatalf("Timed out waiting for user %s to be deleted", username) - case <-ticker.C: - err := testCredsExist(t, connURL, username, password) - if err != nil { - // Happy path - return - } - } - } - } -} - -func assertCredsExistAfter(timeout time.Duration) credsAssertion { - return func(t testing.TB, connURL, username, password string) { - t.Helper() - time.Sleep(timeout) - assertCredsExist(t, connURL, username, password) - } -} - -func testCredsExist(t testing.TB, connURL, username, password string) error { - t.Helper() - // Log in with the new creds - connURL = strings.Replace(connURL, "postgres:secret", fmt.Sprintf("%s:%s", username, password), 1) - db, err := sql.Open("pgx", connURL) - if err != nil { - return err - } - defer db.Close() - return db.Ping() -} - -const createAdminUser = ` -CREATE ROLE "{{name}}" WITH - LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; -` - -var newUserLargeBlockStatements = []string{ - ` -DO $$ -BEGIN - IF NOT EXISTS (SELECT * FROM pg_catalog.pg_roles WHERE rolname='foo-role') THEN - CREATE ROLE "foo-role"; - CREATE SCHEMA IF NOT EXISTS foo AUTHORIZATION "foo-role"; - ALTER ROLE "foo-role" SET search_path = foo; - GRANT TEMPORARY ON DATABASE "postgres" TO "foo-role"; - GRANT ALL PRIVILEGES ON SCHEMA foo TO "foo-role"; - GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA foo TO "foo-role"; - GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA foo TO "foo-role"; - GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA foo TO "foo-role"; - END IF; -END -$$ -`, - `CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';`, - `GRANT "foo-role" TO "{{name}}";`, - `ALTER ROLE "{{name}}" SET search_path = foo;`, - `GRANT CONNECT ON DATABASE "postgres" TO "{{name}}";`, -} - -func TestContainsMultilineStatement(t *testing.T) { - type testCase struct { - Input string - Expected bool - } - - testCases := map[string]*testCase{ - "issue 6098 repro": { - Input: `DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname='my_role') THEN CREATE ROLE my_role; END IF; END $$`, - Expected: true, - }, - "multiline with template fields": { - Input: `DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname="{{name}}") THEN CREATE ROLE {{name}}; END IF; END $$`, - Expected: true, - }, - "docs example": { - Input: `CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \ - GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";`, - Expected: false, - }, - } - - for tName, tCase := range testCases { - t.Run(tName, func(t *testing.T) { - if containsMultilineStatement(tCase.Input) != tCase.Expected { - t.Fatalf("%q should be %t for multiline input", tCase.Input, tCase.Expected) - } - }) - } -} - -func TestExtractQuotedStrings(t *testing.T) { - type testCase struct { - Input string - Expected []string - } - - testCases := map[string]*testCase{ - "no quotes": { - Input: `Five little monkeys jumping on the bed`, - Expected: []string{}, - }, - "two of both quote types": { - Input: `"Five" little 'monkeys' "jumping on" the' 'bed`, - Expected: []string{`"Five"`, `"jumping on"`, `'monkeys'`, `' '`}, - }, - "one single quote": { - Input: `Five little monkeys 'jumping on the bed`, - Expected: []string{}, - }, - "empty string": { - Input: ``, - Expected: []string{}, - }, - "templated field": { - Input: `DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname="{{name}}") THEN CREATE ROLE {{name}}; END IF; END $$`, - Expected: []string{`"{{name}}"`}, - }, - } - - for tName, tCase := range testCases { - t.Run(tName, func(t *testing.T) { - results, err := extractQuotedStrings(tCase.Input) - if err != nil { - t.Fatal(err) - } - if len(results) != len(tCase.Expected) { - t.Fatalf("%s isn't equal to %s", results, tCase.Expected) - } - for i := range results { - if results[i] != tCase.Expected[i] { - t.Fatalf(`expected %q but received %q`, tCase.Expected, results[i]) - } - } - }) - } -} - -func TestUsernameGeneration(t *testing.T) { - type testCase struct { - data dbplugin.UsernameMetadata - expectedRegex string - } - - tests := map[string]testCase{ - "simple display and role names": { - data: dbplugin.UsernameMetadata{ - DisplayName: "token", - RoleName: "myrole", - }, - expectedRegex: `v-token-myrole-[a-zA-Z0-9]{20}-[0-9]{10}`, - }, - "display name has dash": { - data: dbplugin.UsernameMetadata{ - DisplayName: "token-foo", - RoleName: "myrole", - }, - expectedRegex: `v-token-fo-myrole-[a-zA-Z0-9]{20}-[0-9]{10}`, - }, - "display name has underscore": { - data: dbplugin.UsernameMetadata{ - DisplayName: "token_foo", - RoleName: "myrole", - }, - expectedRegex: `v-token_fo-myrole-[a-zA-Z0-9]{20}-[0-9]{10}`, - }, - "display name has period": { - data: dbplugin.UsernameMetadata{ - DisplayName: "token.foo", - RoleName: "myrole", - }, - expectedRegex: `v-token.fo-myrole-[a-zA-Z0-9]{20}-[0-9]{10}`, - }, - "role name has dash": { - data: dbplugin.UsernameMetadata{ - DisplayName: "token", - RoleName: "myrole-foo", - }, - expectedRegex: `v-token-myrole-f-[a-zA-Z0-9]{20}-[0-9]{10}`, - }, - "role name has underscore": { - data: dbplugin.UsernameMetadata{ - DisplayName: "token", - RoleName: "myrole_foo", - }, - expectedRegex: `v-token-myrole_f-[a-zA-Z0-9]{20}-[0-9]{10}`, - }, - "role name has period": { - data: dbplugin.UsernameMetadata{ - DisplayName: "token", - RoleName: "myrole.foo", - }, - expectedRegex: `v-token-myrole.f-[a-zA-Z0-9]{20}-[0-9]{10}`, - }, - } - - for name, test := range tests { - t.Run(fmt.Sprintf("new-%s", name), func(t *testing.T) { - up, err := template.NewTemplate( - template.Template(defaultUserNameTemplate), - ) - require.NoError(t, err) - - for i := 0; i < 1000; i++ { - username, err := up.Generate(test.data) - require.NoError(t, err) - require.Regexp(t, test.expectedRegex, username) - } - }) - } -} - -func TestNewUser_CustomUsername(t *testing.T) { - cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster") - defer cleanup() - - type testCase struct { - usernameTemplate string - newUserData dbplugin.UsernameMetadata - expectedRegex string - } - - tests := map[string]testCase{ - "default template": { - usernameTemplate: "", - newUserData: dbplugin.UsernameMetadata{ - DisplayName: "displayname", - RoleName: "longrolename", - }, - expectedRegex: "^v-displayn-longrole-[a-zA-Z0-9]{20}-[0-9]{10}$", - }, - "explicit default template": { - usernameTemplate: defaultUserNameTemplate, - newUserData: dbplugin.UsernameMetadata{ - DisplayName: "displayname", - RoleName: "longrolename", - }, - expectedRegex: "^v-displayn-longrole-[a-zA-Z0-9]{20}-[0-9]{10}$", - }, - "unique template": { - usernameTemplate: "foo-bar", - newUserData: dbplugin.UsernameMetadata{ - DisplayName: "displayname", - RoleName: "longrolename", - }, - expectedRegex: "^foo-bar$", - }, - "custom prefix": { - usernameTemplate: "foobar-{{.DisplayName | truncate 8}}-{{.RoleName | truncate 8}}-{{random 20}}-{{unix_time}}", - newUserData: dbplugin.UsernameMetadata{ - DisplayName: "displayname", - RoleName: "longrolename", - }, - expectedRegex: "^foobar-displayn-longrole-[a-zA-Z0-9]{20}-[0-9]{10}$", - }, - "totally custom template": { - usernameTemplate: "foobar_{{random 10}}-{{.RoleName | uppercase}}.{{unix_time}}x{{.DisplayName | truncate 5}}", - newUserData: dbplugin.UsernameMetadata{ - DisplayName: "displayname", - RoleName: "longrolename", - }, - expectedRegex: `^foobar_[a-zA-Z0-9]{10}-LONGROLENAME\.[0-9]{10}xdispl$`, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - initReq := dbplugin.InitializeRequest{ - Config: map[string]interface{}{ - "connection_url": connURL, - "username_template": test.usernameTemplate, - }, - VerifyConnection: true, - } - - db := new() - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - _, err := db.Initialize(ctx, initReq) - require.NoError(t, err) - - newUserReq := dbplugin.NewUserRequest{ - UsernameConfig: test.newUserData, - Statements: dbplugin.Statements{ - Commands: []string{ - ` - CREATE ROLE "{{name}}" WITH - LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; - GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";`, - }, - }, - Password: "myReally-S3curePassword", - Expiration: time.Now().Add(1 * time.Hour), - } - ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - newUserResp, err := db.NewUser(ctx, newUserReq) - require.NoError(t, err) - - require.Regexp(t, test.expectedRegex, newUserResp.Username) - }) - } -} - -// This is a long-running integration test which tests the functionality of Postgres's multi-host -// connection strings. It uses two Postgres containers preconfigured with Replication Manager -// provided by Bitnami. This test currently does not run in CI and must be run manually. This is -// due to the test length, as it requires multiple sleep calls to ensure cluster setup and -// primary node failover occurs before the test steps continue. -// -// To run the test, set the environment variable POSTGRES_MULTIHOST_NET to the value of -// a docker network you've preconfigured, e.g. -// 'docker network create -d bridge postgres-repmgr' -// 'export POSTGRES_MULTIHOST_NET=postgres-repmgr' -func TestPostgreSQL_Repmgr(t *testing.T) { - _, exists := os.LookupEnv("POSTGRES_MULTIHOST_NET") - if !exists { - t.Skipf("POSTGRES_MULTIHOST_NET not set, skipping test") - } - - // Run two postgres-repmgr containers in a replication cluster - db0, runner0, url0, container0 := testPostgreSQL_Repmgr_Container(t, "psql-repl-node-0") - _, _, url1, _ := testPostgreSQL_Repmgr_Container(t, "psql-repl-node-1") - - ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second) - defer cancel() - - time.Sleep(10 * time.Second) - - // Write a read role to the cluster - _, err := db0.NewUser(ctx, dbplugin.NewUserRequest{ - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE ROLE "ro" NOINHERIT; - GRANT SELECT ON ALL TABLES IN SCHEMA public TO "ro";`, - }, - }, - }) - if err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - // Open a connection to both databases using the multihost connection string - connectionDetails := map[string]interface{}{ - "connection_url": fmt.Sprintf("postgresql://{{username}}:{{password}}@%s,%s/postgres?target_session_attrs=read-write", getHost(url0), getHost(url1)), - "username": "postgres", - "password": "secret", - } - req := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - - db := new() - dbtesting.AssertInitialize(t, db, req) - if !db.Initialized { - t.Fatal("Database should be initialized") - } - defer db.Close() - - // Add a user to the cluster, then stop the primary container - if err = testPostgreSQL_Repmgr_AddUser(ctx, db); err != nil { - t.Fatalf("no error expected, got: %s", err) - } - postgresql.StopContainer(t, ctx, runner0, container0) - - // Try adding a new user immediately - expect failure as the database - // cluster is still switching primaries - err = testPostgreSQL_Repmgr_AddUser(ctx, db) - if !strings.HasSuffix(err.Error(), "ValidateConnect failed (read only connection)") { - t.Fatalf("expected error was not received, got: %s", err) - } - - time.Sleep(20 * time.Second) - - // Try adding a new user again which should succeed after the sleep - // as the primary failover should have finished. Then, restart - // the first container which should become a secondary DB. - if err = testPostgreSQL_Repmgr_AddUser(ctx, db); err != nil { - t.Fatalf("no error expected, got: %s", err) - } - postgresql.RestartContainer(t, ctx, runner0, container0) - - time.Sleep(10 * time.Second) - - // A final new user to add, which should succeed after the secondary joins. - if err = testPostgreSQL_Repmgr_AddUser(ctx, db); err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if err := db.Close(); err != nil { - t.Fatalf("err: %s", err) - } -} - -func testPostgreSQL_Repmgr_Container(t *testing.T, name string) (*PostgreSQL, *docker.Runner, string, string) { - envVars := []string{ - "REPMGR_NODE_NAME=" + name, - "REPMGR_NODE_NETWORK_NAME=" + name, - } - - runner, cleanup, connURL, containerID := postgresql.PrepareTestContainerRepmgr(t, name, "13.4.0", envVars) - t.Cleanup(cleanup) - - connectionDetails := map[string]interface{}{ - "connection_url": connURL, - } - req := dbplugin.InitializeRequest{ - Config: connectionDetails, - VerifyConnection: true, - } - db := new() - dbtesting.AssertInitialize(t, db, req) - if !db.Initialized { - t.Fatal("Database should be initialized") - } - - if err := db.Close(); err != nil { - t.Fatalf("err: %s", err) - } - - return db, runner, connURL, containerID -} - -func testPostgreSQL_Repmgr_AddUser(ctx context.Context, db *PostgreSQL) error { - _, err := db.NewUser(ctx, dbplugin.NewUserRequest{ - Statements: dbplugin.Statements{ - Commands: []string{ - `CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' INHERIT; - GRANT ro TO "{{name}}";`, - }, - }, - }) - - return err -} - -func getHost(url string) string { - splitCreds := strings.Split(url, "@")[1] - - return strings.Split(splitCreds, "/")[0] -} diff --git a/plugins/database/postgresql/scram/scram_test.go b/plugins/database/postgresql/scram/scram_test.go deleted file mode 100644 index d2933ebbc..000000000 --- a/plugins/database/postgresql/scram/scram_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package scram - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -// TestScram tests the Hash method. The hashed password string should have a SCRAM-SHA-256 prefix. -func TestScram(t *testing.T) { - tcs := map[string]struct { - Password string - }{ - "empty-password": {Password: ""}, - "simple-password": {Password: "password"}, - } - - for name, tc := range tcs { - t.Run(name, func(t *testing.T) { - got, err := Hash(tc.Password) - assert.NoError(t, err) - assert.True(t, strings.HasPrefix(got, "SCRAM-SHA-256$4096:")) - assert.Len(t, got, 133) - }) - } -} diff --git a/scripts/testciphers.sh b/scripts/testciphers.sh deleted file mode 100755 index 89c1e9304..000000000 --- a/scripts/testciphers.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - - -# Adapted from https://superuser.com/a/224263 - -# OpenSSL requires the port number. -SERVER=$1 -ciphers=$(openssl ciphers 'ALL:eNULL' | sed -e 's/:/ /g') - -echo Obtaining cipher list from $(openssl version). - -for cipher in ${ciphers[@]} -do -echo -n Testing $cipher... -result=$(echo -n | openssl s_client -cipher "$cipher" -alpn req_fw_sb-act_v1 -connect $SERVER 2>&1) -if [[ "$result" =~ ":error:" ]] ; then - error=$(echo -n $result | cut -d':' -f6) - echo NO \($error\) -else - if [[ "$result" =~ "Cipher is ${cipher}" || "$result" =~ "Cipher :" ]] ; then - echo YES - else - echo UNKNOWN RESPONSE - echo $result - fi -fi -done diff --git a/sdk/database/dbplugin/v5/conversions_test.go b/sdk/database/dbplugin/v5/conversions_test.go deleted file mode 100644 index 5e65c3467..000000000 --- a/sdk/database/dbplugin/v5/conversions_test.go +++ /dev/null @@ -1,530 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package dbplugin - -import ( - "fmt" - "reflect" - "strings" - "testing" - "time" - "unicode" - - "github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -func TestConversionsHaveAllFields(t *testing.T) { - t.Run("initReqToProto", func(t *testing.T) { - req := InitializeRequest{ - Config: map[string]interface{}{ - "foo": map[string]interface{}{ - "bar": "baz", - }, - }, - VerifyConnection: true, - } - - protoReq, err := initReqToProto(req) - if err != nil { - t.Fatalf("Failed to convert request to proto request: %s", err) - } - - values := getAllGetterValues(protoReq) - if len(values) == 0 { - // Probably a test failure - the protos used in these tests should have Get functions on them - t.Fatalf("No values found from Get functions!") - } - - for _, gtr := range values { - err := assertAllFieldsSet(fmt.Sprintf("InitializeRequest.%s", gtr.name), gtr.value) - if err != nil { - t.Fatalf("%s", err) - } - } - }) - - t.Run("newUserReqToProto", func(t *testing.T) { - req := NewUserRequest{ - UsernameConfig: UsernameMetadata{ - DisplayName: "dispName", - RoleName: "roleName", - }, - Statements: Statements{ - Commands: []string{ - "statement", - }, - }, - RollbackStatements: Statements{ - Commands: []string{ - "rollback_statement", - }, - }, - CredentialType: CredentialTypeRSAPrivateKey, - PublicKey: []byte("-----BEGIN PUBLIC KEY-----"), - Password: "password", - Subject: "subject", - Expiration: time.Now(), - } - - protoReq, err := newUserReqToProto(req) - if err != nil { - t.Fatalf("Failed to convert request to proto request: %s", err) - } - - values := getAllGetterValues(protoReq) - if len(values) == 0 { - // Probably a test failure - the protos used in these tests should have Get functions on them - t.Fatalf("No values found from Get functions!") - } - - for _, gtr := range values { - err := assertAllFieldsSet(fmt.Sprintf("NewUserRequest.%s", gtr.name), gtr.value) - if err != nil { - t.Fatalf("%s", err) - } - } - }) - - t.Run("updateUserReqToProto", func(t *testing.T) { - req := UpdateUserRequest{ - Username: "username", - CredentialType: CredentialTypeRSAPrivateKey, - Password: &ChangePassword{ - NewPassword: "newpassword", - Statements: Statements{ - Commands: []string{ - "statement", - }, - }, - }, - PublicKey: &ChangePublicKey{ - NewPublicKey: []byte("-----BEGIN PUBLIC KEY-----"), - Statements: Statements{ - Commands: []string{ - "statement", - }, - }, - }, - Expiration: &ChangeExpiration{ - NewExpiration: time.Now(), - Statements: Statements{ - Commands: []string{ - "statement", - }, - }, - }, - } - - protoReq, err := updateUserReqToProto(req) - if err != nil { - t.Fatalf("Failed to convert request to proto request: %s", err) - } - - values := getAllGetterValues(protoReq) - if len(values) == 0 { - // Probably a test failure - the protos used in these tests should have Get functions on them - t.Fatalf("No values found from Get functions!") - } - - for _, gtr := range values { - err := assertAllFieldsSet(fmt.Sprintf("UpdateUserRequest.%s", gtr.name), gtr.value) - if err != nil { - t.Fatalf("%s", err) - } - } - }) - - t.Run("deleteUserReqToProto", func(t *testing.T) { - req := DeleteUserRequest{ - Username: "username", - Statements: Statements{ - Commands: []string{ - "statement", - }, - }, - } - - protoReq, err := deleteUserReqToProto(req) - if err != nil { - t.Fatalf("Failed to convert request to proto request: %s", err) - } - - values := getAllGetterValues(protoReq) - if len(values) == 0 { - // Probably a test failure - the protos used in these tests should have Get functions on them - t.Fatalf("No values found from Get functions!") - } - - for _, gtr := range values { - err := assertAllFieldsSet(fmt.Sprintf("DeleteUserRequest.%s", gtr.name), gtr.value) - if err != nil { - t.Fatalf("%s", err) - } - } - }) - - t.Run("getUpdateUserRequest", func(t *testing.T) { - req := &proto.UpdateUserRequest{ - Username: "username", - CredentialType: int32(CredentialTypeRSAPrivateKey), - Password: &proto.ChangePassword{ - NewPassword: "newpass", - Statements: &proto.Statements{ - Commands: []string{ - "statement", - }, - }, - }, - PublicKey: &proto.ChangePublicKey{ - NewPublicKey: []byte("-----BEGIN PUBLIC KEY-----"), - Statements: &proto.Statements{ - Commands: []string{ - "statement", - }, - }, - }, - Expiration: &proto.ChangeExpiration{ - NewExpiration: timestamppb.Now(), - Statements: &proto.Statements{ - Commands: []string{ - "statement", - }, - }, - }, - } - - protoReq, err := getUpdateUserRequest(req) - if err != nil { - t.Fatalf("Failed to convert request to proto request: %s", err) - } - - err = assertAllFieldsSet("proto.UpdateUserRequest", protoReq) - if err != nil { - t.Fatalf("%s", err) - } - }) -} - -type getter struct { - name string - value interface{} -} - -func getAllGetterValues(value interface{}) (values []getter) { - typ := reflect.TypeOf(value) - val := reflect.ValueOf(value) - for i := 0; i < typ.NumMethod(); i++ { - method := typ.Method(i) - if !strings.HasPrefix(method.Name, "Get") { - continue - } - valMethod := val.Method(i) - resp := valMethod.Call(nil) - getVal := resp[0].Interface() - gtr := getter{ - name: strings.TrimPrefix(method.Name, "Get"), - value: getVal, - } - values = append(values, gtr) - } - return values -} - -// Ensures the assertion works properly -func TestAssertAllFieldsSet(t *testing.T) { - type testCase struct { - value interface{} - expectErr bool - } - - tests := map[string]testCase{ - "zero int": { - value: 0, - expectErr: true, - }, - "non-zero int": { - value: 1, - expectErr: false, - }, - "zero float64": { - value: 0.0, - expectErr: true, - }, - "non-zero float64": { - value: 1.0, - expectErr: false, - }, - "empty string": { - value: "", - expectErr: true, - }, - "true boolean": { - value: true, - expectErr: false, - }, - "false boolean": { // False is an exception to the "is zero" rule - value: false, - expectErr: false, - }, - "blank struct": { - value: struct{}{}, - expectErr: true, - }, - "non-blank but empty struct": { - value: struct { - str string - }{ - str: "", - }, - expectErr: true, - }, - "non-empty string": { - value: "foo", - expectErr: false, - }, - "non-empty struct": { - value: struct { - str string - }{ - str: "foo", - }, - expectErr: false, - }, - "empty nested struct": { - value: struct { - Str string - Substruct struct { - Substr string - } - }{ - Str: "foo", - Substruct: struct { - Substr string - }{}, // Empty sub-field - }, - expectErr: true, - }, - "filled nested struct": { - value: struct { - str string - substruct struct { - substr string - } - }{ - str: "foo", - substruct: struct { - substr string - }{ - substr: "sub-foo", - }, - }, - expectErr: false, - }, - "nil map": { - value: map[string]string(nil), - expectErr: true, - }, - "empty map": { - value: map[string]string{}, - expectErr: true, - }, - "filled map": { - value: map[string]string{ - "foo": "bar", - "int": "42", - }, - expectErr: false, - }, - "map with empty string value": { - value: map[string]string{ - "foo": "", - }, - expectErr: true, - }, - "nested map with empty string value": { - value: map[string]interface{}{ - "bar": "baz", - "foo": map[string]interface{}{ - "subfoo": "", - }, - }, - expectErr: true, - }, - "nil slice": { - value: []string(nil), - expectErr: true, - }, - "empty slice": { - value: []string{}, - expectErr: true, - }, - "filled slice": { - value: []string{ - "foo", - }, - expectErr: false, - }, - "slice with empty string value": { - value: []string{ - "", - }, - expectErr: true, - }, - "empty structpb": { - value: newStructPb(t, map[string]interface{}{}), - expectErr: true, - }, - "filled structpb": { - value: newStructPb(t, map[string]interface{}{ - "foo": "bar", - "int": 42, - }), - expectErr: false, - }, - - "pointer to zero int": { - value: intPtr(0), - expectErr: true, - }, - "pointer to non-zero int": { - value: intPtr(1), - expectErr: false, - }, - "pointer to zero float64": { - value: float64Ptr(0.0), - expectErr: true, - }, - "pointer to non-zero float64": { - value: float64Ptr(1.0), - expectErr: false, - }, - "pointer to nil string": { - value: new(string), - expectErr: true, - }, - "pointer to non-nil string": { - value: strPtr("foo"), - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - err := assertAllFieldsSet("", test.value) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - }) - } -} - -func assertAllFieldsSet(name string, val interface{}) error { - if val == nil { - return fmt.Errorf("value is nil") - } - - rVal := reflect.ValueOf(val) - return assertAllFieldsSetValue(name, rVal) -} - -func assertAllFieldsSetValue(name string, rVal reflect.Value) error { - // All booleans are allowed - we don't have a way of differentiating between - // and intentional false and a missing false - if rVal.Kind() == reflect.Bool { - return nil - } - - // Primitives fall through here - if rVal.IsZero() { - return fmt.Errorf("%s is zero", name) - } - - switch rVal.Kind() { - case reflect.Ptr, reflect.Interface: - return assertAllFieldsSetValue(name, rVal.Elem()) - case reflect.Struct: - return assertAllFieldsSetStruct(name, rVal) - case reflect.Map: - if rVal.Len() == 0 { - return fmt.Errorf("%s (map type) is empty", name) - } - - iter := rVal.MapRange() - for iter.Next() { - k := iter.Key() - v := iter.Value() - - err := assertAllFieldsSetValue(fmt.Sprintf("%s[%s]", name, k), v) - if err != nil { - return err - } - } - case reflect.Slice: - if rVal.Len() == 0 { - return fmt.Errorf("%s (slice type) is empty", name) - } - for i := 0; i < rVal.Len(); i++ { - sliceVal := rVal.Index(i) - err := assertAllFieldsSetValue(fmt.Sprintf("%s[%d]", name, i), sliceVal) - if err != nil { - return err - } - } - } - return nil -} - -func assertAllFieldsSetStruct(name string, rVal reflect.Value) error { - switch rVal.Type() { - case reflect.TypeOf(timestamppb.Timestamp{}): - ts := rVal.Interface().(timestamppb.Timestamp) - if ts.AsTime().IsZero() { - return fmt.Errorf("%s is zero", name) - } - return nil - default: - for i := 0; i < rVal.NumField(); i++ { - field := rVal.Field(i) - fieldName := rVal.Type().Field(i) - - // Skip fields that aren't exported - if unicode.IsLower([]rune(fieldName.Name)[0]) { - continue - } - - err := assertAllFieldsSetValue(fmt.Sprintf("%s.%s", name, fieldName.Name), field) - if err != nil { - return err - } - } - return nil - } -} - -func intPtr(i int) *int { - return &i -} - -func float64Ptr(f float64) *float64 { - return &f -} - -func strPtr(str string) *string { - return &str -} - -func newStructPb(t *testing.T, m map[string]interface{}) *structpb.Struct { - t.Helper() - - s, err := structpb.NewStruct(m) - if err != nil { - t.Fatalf("Failed to convert map to struct: %s", err) - } - return s -} diff --git a/sdk/database/dbplugin/v5/grpc_client_test.go b/sdk/database/dbplugin/v5/grpc_client_test.go deleted file mode 100644 index 05ecb960e..000000000 --- a/sdk/database/dbplugin/v5/grpc_client_test.go +++ /dev/null @@ -1,564 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package dbplugin - -import ( - "context" - "encoding/json" - "errors" - "reflect" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto" - "google.golang.org/grpc" -) - -func TestGRPCClient_Initialize(t *testing.T) { - type testCase struct { - client proto.DatabaseClient - req InitializeRequest - expectedResp InitializeResponse - assertErr errorAssertion - } - - tests := map[string]testCase{ - "bad config": { - client: fakeClient{}, - req: InitializeRequest{ - Config: map[string]interface{}{ - "foo": badJSONValue{}, - }, - }, - assertErr: assertErrNotNil, - }, - "database error": { - client: fakeClient{ - initErr: errors.New("initialize error"), - }, - req: InitializeRequest{ - Config: map[string]interface{}{ - "foo": "bar", - }, - }, - assertErr: assertErrNotNil, - }, - "happy path": { - client: fakeClient{ - initResp: &proto.InitializeResponse{ - ConfigData: marshal(t, map[string]interface{}{ - "foo": "bar", - "baz": "biz", - }), - }, - }, - req: InitializeRequest{ - Config: map[string]interface{}{ - "foo": "bar", - }, - }, - expectedResp: InitializeResponse{ - Config: map[string]interface{}{ - "foo": "bar", - "baz": "biz", - }, - }, - assertErr: assertErrNil, - }, - "JSON number type in initialize request": { - client: fakeClient{ - initResp: &proto.InitializeResponse{ - ConfigData: marshal(t, map[string]interface{}{ - "foo": "bar", - "max": "10", - }), - }, - }, - req: InitializeRequest{ - Config: map[string]interface{}{ - "foo": "bar", - "max": json.Number("10"), - }, - }, - expectedResp: InitializeResponse{ - Config: map[string]interface{}{ - "foo": "bar", - "max": "10", - }, - }, - assertErr: assertErrNil, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - c := gRPCClient{ - client: test.client, - doneCtx: nil, - } - - // Context doesn't need to timeout since this is just passed through - ctx := context.Background() - - resp, err := c.Initialize(ctx, test.req) - test.assertErr(t, err) - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -func TestGRPCClient_NewUser(t *testing.T) { - runningCtx := context.Background() - cancelledCtx, cancel := context.WithCancel(context.Background()) - cancel() - - type testCase struct { - client proto.DatabaseClient - req NewUserRequest - doneCtx context.Context - expectedResp NewUserResponse - assertErr errorAssertion - } - - tests := map[string]testCase{ - "missing password": { - client: fakeClient{}, - req: NewUserRequest{ - Password: "", - Expiration: time.Now(), - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "bad expiration": { - client: fakeClient{}, - req: NewUserRequest{ - Password: "njkvcb8y934u90grsnkjl", - Expiration: invalidExpiration, - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "database error": { - client: fakeClient{ - newUserErr: errors.New("new user error"), - }, - req: NewUserRequest{ - Password: "njkvcb8y934u90grsnkjl", - Expiration: time.Now(), - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "plugin shut down": { - client: fakeClient{ - newUserErr: errors.New("new user error"), - }, - req: NewUserRequest{ - Password: "njkvcb8y934u90grsnkjl", - Expiration: time.Now(), - }, - doneCtx: cancelledCtx, - assertErr: assertErrEquals(ErrPluginShutdown), - }, - "happy path": { - client: fakeClient{ - newUserResp: &proto.NewUserResponse{ - Username: "new_user", - }, - }, - req: NewUserRequest{ - Password: "njkvcb8y934u90grsnkjl", - Expiration: time.Now(), - }, - doneCtx: runningCtx, - expectedResp: NewUserResponse{ - Username: "new_user", - }, - assertErr: assertErrNil, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - c := gRPCClient{ - client: test.client, - doneCtx: test.doneCtx, - } - - ctx := context.Background() - - resp, err := c.NewUser(ctx, test.req) - test.assertErr(t, err) - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -func TestGRPCClient_UpdateUser(t *testing.T) { - runningCtx := context.Background() - cancelledCtx, cancel := context.WithCancel(context.Background()) - cancel() - - type testCase struct { - client proto.DatabaseClient - req UpdateUserRequest - doneCtx context.Context - assertErr errorAssertion - } - - tests := map[string]testCase{ - "missing username": { - client: fakeClient{}, - req: UpdateUserRequest{}, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "missing changes": { - client: fakeClient{}, - req: UpdateUserRequest{ - Username: "user", - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "empty password": { - client: fakeClient{}, - req: UpdateUserRequest{ - Username: "user", - Password: &ChangePassword{ - NewPassword: "", - }, - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "zero expiration": { - client: fakeClient{}, - req: UpdateUserRequest{ - Username: "user", - Expiration: &ChangeExpiration{ - NewExpiration: time.Time{}, - }, - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "bad expiration": { - client: fakeClient{}, - req: UpdateUserRequest{ - Username: "user", - Expiration: &ChangeExpiration{ - NewExpiration: invalidExpiration, - }, - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "database error": { - client: fakeClient{ - updateUserErr: errors.New("update user error"), - }, - req: UpdateUserRequest{ - Username: "user", - Password: &ChangePassword{ - NewPassword: "asdf", - }, - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "plugin shut down": { - client: fakeClient{ - updateUserErr: errors.New("update user error"), - }, - req: UpdateUserRequest{ - Username: "user", - Password: &ChangePassword{ - NewPassword: "asdf", - }, - }, - doneCtx: cancelledCtx, - assertErr: assertErrEquals(ErrPluginShutdown), - }, - "happy path - change password": { - client: fakeClient{}, - req: UpdateUserRequest{ - Username: "user", - Password: &ChangePassword{ - NewPassword: "asdf", - }, - }, - doneCtx: runningCtx, - assertErr: assertErrNil, - }, - "happy path - change expiration": { - client: fakeClient{}, - req: UpdateUserRequest{ - Username: "user", - Expiration: &ChangeExpiration{ - NewExpiration: time.Now(), - }, - }, - doneCtx: runningCtx, - assertErr: assertErrNil, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - c := gRPCClient{ - client: test.client, - doneCtx: test.doneCtx, - } - - ctx := context.Background() - - _, err := c.UpdateUser(ctx, test.req) - test.assertErr(t, err) - }) - } -} - -func TestGRPCClient_DeleteUser(t *testing.T) { - runningCtx := context.Background() - cancelledCtx, cancel := context.WithCancel(context.Background()) - cancel() - - type testCase struct { - client proto.DatabaseClient - req DeleteUserRequest - doneCtx context.Context - assertErr errorAssertion - } - - tests := map[string]testCase{ - "missing username": { - client: fakeClient{}, - req: DeleteUserRequest{}, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "database error": { - client: fakeClient{ - deleteUserErr: errors.New("delete user error'"), - }, - req: DeleteUserRequest{ - Username: "user", - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "plugin shut down": { - client: fakeClient{ - deleteUserErr: errors.New("delete user error'"), - }, - req: DeleteUserRequest{ - Username: "user", - }, - doneCtx: cancelledCtx, - assertErr: assertErrEquals(ErrPluginShutdown), - }, - "happy path": { - client: fakeClient{}, - req: DeleteUserRequest{ - Username: "user", - }, - doneCtx: runningCtx, - assertErr: assertErrNil, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - c := gRPCClient{ - client: test.client, - doneCtx: test.doneCtx, - } - - ctx := context.Background() - - _, err := c.DeleteUser(ctx, test.req) - test.assertErr(t, err) - }) - } -} - -func TestGRPCClient_Type(t *testing.T) { - runningCtx := context.Background() - cancelledCtx, cancel := context.WithCancel(context.Background()) - cancel() - - type testCase struct { - client proto.DatabaseClient - doneCtx context.Context - expectedType string - assertErr errorAssertion - } - - tests := map[string]testCase{ - "database error": { - client: fakeClient{ - typeErr: errors.New("type error"), - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "plugin shut down": { - client: fakeClient{ - typeErr: errors.New("type error"), - }, - doneCtx: cancelledCtx, - assertErr: assertErrEquals(ErrPluginShutdown), - }, - "happy path": { - client: fakeClient{ - typeResp: &proto.TypeResponse{ - Type: "test type", - }, - }, - doneCtx: runningCtx, - expectedType: "test type", - assertErr: assertErrNil, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - c := gRPCClient{ - client: test.client, - doneCtx: test.doneCtx, - } - - dbType, err := c.Type() - test.assertErr(t, err) - - if dbType != test.expectedType { - t.Fatalf("Actual type: %s Expected type: %s", dbType, test.expectedType) - } - }) - } -} - -func TestGRPCClient_Close(t *testing.T) { - runningCtx := context.Background() - cancelledCtx, cancel := context.WithCancel(context.Background()) - cancel() - - type testCase struct { - client proto.DatabaseClient - doneCtx context.Context - assertErr errorAssertion - } - - tests := map[string]testCase{ - "database error": { - client: fakeClient{ - typeErr: errors.New("type error"), - }, - doneCtx: runningCtx, - assertErr: assertErrNotNil, - }, - "plugin shut down": { - client: fakeClient{ - typeErr: errors.New("type error"), - }, - doneCtx: cancelledCtx, - assertErr: assertErrEquals(ErrPluginShutdown), - }, - "happy path": { - client: fakeClient{}, - doneCtx: runningCtx, - assertErr: assertErrNil, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - c := gRPCClient{ - client: test.client, - doneCtx: test.doneCtx, - } - - err := c.Close() - test.assertErr(t, err) - }) - } -} - -type errorAssertion func(*testing.T, error) - -func assertErrNotNil(t *testing.T, err error) { - t.Helper() - if err == nil { - t.Fatalf("err expected, got nil") - } -} - -func assertErrNil(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Fatalf("no error expected, got: %s", err) - } -} - -func assertErrEquals(expectedErr error) errorAssertion { - return func(t *testing.T, err error) { - t.Helper() - if err != expectedErr { - t.Fatalf("Actual err: %#v Expected err: %#v", err, expectedErr) - } - } -} - -var _ proto.DatabaseClient = fakeClient{} - -type fakeClient struct { - initResp *proto.InitializeResponse - initErr error - - newUserResp *proto.NewUserResponse - newUserErr error - - updateUserResp *proto.UpdateUserResponse - updateUserErr error - - deleteUserResp *proto.DeleteUserResponse - deleteUserErr error - - typeResp *proto.TypeResponse - typeErr error - - closeErr error -} - -func (f fakeClient) Initialize(context.Context, *proto.InitializeRequest, ...grpc.CallOption) (*proto.InitializeResponse, error) { - return f.initResp, f.initErr -} - -func (f fakeClient) NewUser(context.Context, *proto.NewUserRequest, ...grpc.CallOption) (*proto.NewUserResponse, error) { - return f.newUserResp, f.newUserErr -} - -func (f fakeClient) UpdateUser(context.Context, *proto.UpdateUserRequest, ...grpc.CallOption) (*proto.UpdateUserResponse, error) { - return f.updateUserResp, f.updateUserErr -} - -func (f fakeClient) DeleteUser(context.Context, *proto.DeleteUserRequest, ...grpc.CallOption) (*proto.DeleteUserResponse, error) { - return f.deleteUserResp, f.deleteUserErr -} - -func (f fakeClient) Type(context.Context, *proto.Empty, ...grpc.CallOption) (*proto.TypeResponse, error) { - return f.typeResp, f.typeErr -} - -func (f fakeClient) Close(context.Context, *proto.Empty, ...grpc.CallOption) (*proto.Empty, error) { - return &proto.Empty{}, f.typeErr -} diff --git a/sdk/database/dbplugin/v5/grpc_server_test.go b/sdk/database/dbplugin/v5/grpc_server_test.go deleted file mode 100644 index 53d44c7c2..000000000 --- a/sdk/database/dbplugin/v5/grpc_server_test.go +++ /dev/null @@ -1,839 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package dbplugin - -import ( - "context" - "errors" - "fmt" - "reflect" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/logical" - "google.golang.org/protobuf/types/known/structpb" - - "github.com/golang/protobuf/ptypes" - "github.com/golang/protobuf/ptypes/timestamp" - "github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto" - "github.com/hashicorp/vault/sdk/helper/pluginutil" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -// Before minValidSeconds in ptypes package -var invalidExpiration = time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC) - -func TestGRPCServer_Initialize(t *testing.T) { - type testCase struct { - db Database - req *proto.InitializeRequest - expectedResp *proto.InitializeResponse - expectErr bool - expectCode codes.Code - grpcSetupFunc func(*testing.T, Database) (context.Context, gRPCServer) - } - - tests := map[string]testCase{ - "database errored": { - db: fakeDatabase{ - initErr: errors.New("initialization error"), - }, - req: &proto.InitializeRequest{}, - expectedResp: &proto.InitializeResponse{}, - expectErr: true, - expectCode: codes.Internal, - grpcSetupFunc: testGrpcServer, - }, - "newConfig can't marshal to JSON": { - db: fakeDatabase{ - initResp: InitializeResponse{ - Config: map[string]interface{}{ - "bad-data": badJSONValue{}, - }, - }, - }, - req: &proto.InitializeRequest{}, - expectedResp: &proto.InitializeResponse{}, - expectErr: true, - expectCode: codes.Internal, - grpcSetupFunc: testGrpcServer, - }, - "happy path with config data for multiplexed plugin": { - db: fakeDatabase{ - initResp: InitializeResponse{ - Config: map[string]interface{}{ - "foo": "bar", - }, - }, - }, - req: &proto.InitializeRequest{ - ConfigData: marshal(t, map[string]interface{}{ - "foo": "bar", - }), - }, - expectedResp: &proto.InitializeResponse{ - ConfigData: marshal(t, map[string]interface{}{ - "foo": "bar", - }), - }, - expectErr: false, - expectCode: codes.OK, - grpcSetupFunc: testGrpcServer, - }, - "happy path with config data for non-multiplexed plugin": { - db: fakeDatabase{ - initResp: InitializeResponse{ - Config: map[string]interface{}{ - "foo": "bar", - }, - }, - }, - req: &proto.InitializeRequest{ - ConfigData: marshal(t, map[string]interface{}{ - "foo": "bar", - }), - }, - expectedResp: &proto.InitializeResponse{ - ConfigData: marshal(t, map[string]interface{}{ - "foo": "bar", - }), - }, - expectErr: false, - expectCode: codes.OK, - grpcSetupFunc: testGrpcServerSingleImpl, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - idCtx, g := test.grpcSetupFunc(t, test.db) - resp, err := g.Initialize(idCtx, test.req) - - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - actualCode := status.Code(err) - if actualCode != test.expectCode { - t.Fatalf("Actual code: %s Expected code: %s", actualCode, test.expectCode) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -func TestCoerceFloatsToInt(t *testing.T) { - type testCase struct { - input map[string]interface{} - expected map[string]interface{} - } - - tests := map[string]testCase{ - "no numbers": { - input: map[string]interface{}{ - "foo": "bar", - }, - expected: map[string]interface{}{ - "foo": "bar", - }, - }, - "raw integers": { - input: map[string]interface{}{ - "foo": 42, - }, - expected: map[string]interface{}{ - "foo": 42, - }, - }, - "floats ": { - input: map[string]interface{}{ - "foo": 42.2, - }, - expected: map[string]interface{}{ - "foo": 42.2, - }, - }, - "floats coerced to ints": { - input: map[string]interface{}{ - "foo": float64(42), - }, - expected: map[string]interface{}{ - "foo": int64(42), - }, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual := copyMap(test.input) - coerceFloatsToInt(actual) - if !reflect.DeepEqual(actual, test.expected) { - t.Fatalf("Actual: %#v\nExpected: %#v", actual, test.expected) - } - }) - } -} - -func copyMap(m map[string]interface{}) map[string]interface{} { - newMap := map[string]interface{}{} - for k, v := range m { - newMap[k] = v - } - return newMap -} - -func TestGRPCServer_NewUser(t *testing.T) { - type testCase struct { - db Database - req *proto.NewUserRequest - expectedResp *proto.NewUserResponse - expectErr bool - expectCode codes.Code - } - - tests := map[string]testCase{ - "missing username config": { - db: fakeDatabase{}, - req: &proto.NewUserRequest{}, - expectedResp: &proto.NewUserResponse{}, - expectErr: true, - expectCode: codes.InvalidArgument, - }, - "bad expiration": { - db: fakeDatabase{}, - req: &proto.NewUserRequest{ - UsernameConfig: &proto.UsernameConfig{ - DisplayName: "dispname", - RoleName: "rolename", - }, - Expiration: ×tamp.Timestamp{ - Seconds: invalidExpiration.Unix(), - }, - }, - expectedResp: &proto.NewUserResponse{}, - expectErr: true, - expectCode: codes.InvalidArgument, - }, - "database error": { - db: fakeDatabase{ - newUserErr: errors.New("new user error"), - }, - req: &proto.NewUserRequest{ - UsernameConfig: &proto.UsernameConfig{ - DisplayName: "dispname", - RoleName: "rolename", - }, - Expiration: ptypes.TimestampNow(), - }, - expectedResp: &proto.NewUserResponse{}, - expectErr: true, - expectCode: codes.Internal, - }, - "happy path with expiration": { - db: fakeDatabase{ - newUserResp: NewUserResponse{ - Username: "someuser_foo", - }, - }, - req: &proto.NewUserRequest{ - UsernameConfig: &proto.UsernameConfig{ - DisplayName: "dispname", - RoleName: "rolename", - }, - Expiration: ptypes.TimestampNow(), - }, - expectedResp: &proto.NewUserResponse{ - Username: "someuser_foo", - }, - expectErr: false, - expectCode: codes.OK, - }, - "happy path without expiration": { - db: fakeDatabase{ - newUserResp: NewUserResponse{ - Username: "someuser_foo", - }, - }, - req: &proto.NewUserRequest{ - UsernameConfig: &proto.UsernameConfig{ - DisplayName: "dispname", - RoleName: "rolename", - }, - }, - expectedResp: &proto.NewUserResponse{ - Username: "someuser_foo", - }, - expectErr: false, - expectCode: codes.OK, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - idCtx, g := testGrpcServer(t, test.db) - resp, err := g.NewUser(idCtx, test.req) - - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - actualCode := status.Code(err) - if actualCode != test.expectCode { - t.Fatalf("Actual code: %s Expected code: %s", actualCode, test.expectCode) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -func TestGRPCServer_UpdateUser(t *testing.T) { - type testCase struct { - db Database - req *proto.UpdateUserRequest - expectedResp *proto.UpdateUserResponse - expectErr bool - expectCode codes.Code - } - - tests := map[string]testCase{ - "missing username": { - db: fakeDatabase{}, - req: &proto.UpdateUserRequest{}, - expectedResp: &proto.UpdateUserResponse{}, - expectErr: true, - expectCode: codes.InvalidArgument, - }, - "missing changes": { - db: fakeDatabase{}, - req: &proto.UpdateUserRequest{ - Username: "someuser", - }, - expectedResp: &proto.UpdateUserResponse{}, - expectErr: true, - expectCode: codes.InvalidArgument, - }, - "database error": { - db: fakeDatabase{ - updateUserErr: errors.New("update user error"), - }, - req: &proto.UpdateUserRequest{ - Username: "someuser", - Password: &proto.ChangePassword{ - NewPassword: "90ughaino", - }, - }, - expectedResp: &proto.UpdateUserResponse{}, - expectErr: true, - expectCode: codes.Internal, - }, - "bad expiration date": { - db: fakeDatabase{}, - req: &proto.UpdateUserRequest{ - Username: "someuser", - Expiration: &proto.ChangeExpiration{ - NewExpiration: ×tamp.Timestamp{ - // Before minValidSeconds in ptypes package - Seconds: invalidExpiration.Unix(), - }, - }, - }, - expectedResp: &proto.UpdateUserResponse{}, - expectErr: true, - expectCode: codes.InvalidArgument, - }, - "change password happy path": { - db: fakeDatabase{}, - req: &proto.UpdateUserRequest{ - Username: "someuser", - Password: &proto.ChangePassword{ - NewPassword: "90ughaino", - }, - }, - expectedResp: &proto.UpdateUserResponse{}, - expectErr: false, - expectCode: codes.OK, - }, - "change expiration happy path": { - db: fakeDatabase{}, - req: &proto.UpdateUserRequest{ - Username: "someuser", - Expiration: &proto.ChangeExpiration{ - NewExpiration: ptypes.TimestampNow(), - }, - }, - expectedResp: &proto.UpdateUserResponse{}, - expectErr: false, - expectCode: codes.OK, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - idCtx, g := testGrpcServer(t, test.db) - resp, err := g.UpdateUser(idCtx, test.req) - - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - actualCode := status.Code(err) - if actualCode != test.expectCode { - t.Fatalf("Actual code: %s Expected code: %s", actualCode, test.expectCode) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -func TestGRPCServer_DeleteUser(t *testing.T) { - type testCase struct { - db Database - req *proto.DeleteUserRequest - expectedResp *proto.DeleteUserResponse - expectErr bool - expectCode codes.Code - } - - tests := map[string]testCase{ - "missing username": { - db: fakeDatabase{}, - req: &proto.DeleteUserRequest{}, - expectedResp: &proto.DeleteUserResponse{}, - expectErr: true, - expectCode: codes.InvalidArgument, - }, - "database error": { - db: fakeDatabase{ - deleteUserErr: errors.New("delete user error"), - }, - req: &proto.DeleteUserRequest{ - Username: "someuser", - }, - expectedResp: &proto.DeleteUserResponse{}, - expectErr: true, - expectCode: codes.Internal, - }, - "happy path": { - db: fakeDatabase{}, - req: &proto.DeleteUserRequest{ - Username: "someuser", - }, - expectedResp: &proto.DeleteUserResponse{}, - expectErr: false, - expectCode: codes.OK, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - idCtx, g := testGrpcServer(t, test.db) - resp, err := g.DeleteUser(idCtx, test.req) - - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - actualCode := status.Code(err) - if actualCode != test.expectCode { - t.Fatalf("Actual code: %s Expected code: %s", actualCode, test.expectCode) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -func TestGRPCServer_Type(t *testing.T) { - type testCase struct { - db Database - expectedResp *proto.TypeResponse - expectErr bool - expectCode codes.Code - } - - tests := map[string]testCase{ - "database error": { - db: fakeDatabase{ - typeErr: errors.New("type error"), - }, - expectedResp: &proto.TypeResponse{}, - expectErr: true, - expectCode: codes.Internal, - }, - "happy path": { - db: fakeDatabase{ - typeResp: "fake database", - }, - expectedResp: &proto.TypeResponse{ - Type: "fake database", - }, - expectErr: false, - expectCode: codes.OK, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - idCtx, g := testGrpcServer(t, test.db) - resp, err := g.Type(idCtx, &proto.Empty{}) - - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - actualCode := status.Code(err) - if actualCode != test.expectCode { - t.Fatalf("Actual code: %s Expected code: %s", actualCode, test.expectCode) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -func TestGRPCServer_Close(t *testing.T) { - type testCase struct { - db Database - expectErr bool - expectCode codes.Code - grpcSetupFunc func(*testing.T, Database) (context.Context, gRPCServer) - assertFunc func(t *testing.T, g gRPCServer) - } - - tests := map[string]testCase{ - "database error": { - db: fakeDatabase{ - closeErr: errors.New("close error"), - }, - expectErr: true, - expectCode: codes.Internal, - grpcSetupFunc: testGrpcServer, - assertFunc: nil, - }, - "happy path for multiplexed plugin": { - db: fakeDatabase{}, - expectErr: false, - expectCode: codes.OK, - grpcSetupFunc: testGrpcServer, - assertFunc: func(t *testing.T, g gRPCServer) { - if len(g.instances) != 0 { - t.Fatalf("err expected instances map to be empty") - } - }, - }, - "happy path for non-multiplexed plugin": { - db: fakeDatabase{}, - expectErr: false, - expectCode: codes.OK, - grpcSetupFunc: testGrpcServerSingleImpl, - assertFunc: nil, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - idCtx, g := test.grpcSetupFunc(t, test.db) - _, err := g.Close(idCtx, &proto.Empty{}) - - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - actualCode := status.Code(err) - if actualCode != test.expectCode { - t.Fatalf("Actual code: %s Expected code: %s", actualCode, test.expectCode) - } - - if test.assertFunc != nil { - test.assertFunc(t, g) - } - }) - } -} - -func TestGRPCServer_Version(t *testing.T) { - type testCase struct { - db Database - expectedResp string - expectErr bool - expectCode codes.Code - } - - tests := map[string]testCase{ - "backend that does not implement version": { - db: fakeDatabase{}, - expectedResp: "", - expectErr: false, - expectCode: codes.OK, - }, - "backend with version": { - db: fakeDatabaseWithVersion{ - version: "v123", - }, - expectedResp: "v123", - expectErr: false, - expectCode: codes.OK, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - idCtx, g := testGrpcServer(t, test.db) - resp, err := g.Version(idCtx, &logical.Empty{}) - - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - actualCode := status.Code(err) - if actualCode != test.expectCode { - t.Fatalf("Actual code: %s Expected code: %s", actualCode, test.expectCode) - } - - if !reflect.DeepEqual(resp.PluginVersion, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -// testGrpcServer is a test helper that returns a context with an ID set in its -// metadata and a gRPCServer instance for a multiplexed plugin -func testGrpcServer(t *testing.T, db Database) (context.Context, gRPCServer) { - t.Helper() - g := gRPCServer{ - factoryFunc: func() (interface{}, error) { - return db, nil - }, - instances: make(map[string]Database), - } - - id := "12345" - idCtx := idCtx(t, id) - g.instances[id] = db - - return idCtx, g -} - -// testGrpcServerSingleImpl is a test helper that returns a context and a -// gRPCServer instance for a non-multiplexed plugin -func testGrpcServerSingleImpl(t *testing.T, db Database) (context.Context, gRPCServer) { - t.Helper() - return context.Background(), gRPCServer{ - singleImpl: db, - } -} - -// idCtx is a test helper that will return a context with the IDs set in its -// metadata -func idCtx(t *testing.T, ids ...string) context.Context { - t.Helper() - // Context doesn't need to timeout since this is just passed through - ctx := context.Background() - md := metadata.MD{} - for _, id := range ids { - md.Append(pluginutil.MultiplexingCtxKey, id) - } - return metadata.NewIncomingContext(ctx, md) -} - -func marshal(t *testing.T, m map[string]interface{}) *structpb.Struct { - t.Helper() - - strct, err := mapToStruct(m) - if err != nil { - t.Fatalf("unable to marshal to protobuf: %s", err) - } - return strct -} - -type badJSONValue struct{} - -func (badJSONValue) MarshalJSON() ([]byte, error) { - return nil, fmt.Errorf("this cannot be marshalled to JSON") -} - -func (badJSONValue) UnmarshalJSON([]byte) error { - return fmt.Errorf("this cannot be unmarshalled from JSON") -} - -var _ Database = fakeDatabase{} - -type fakeDatabase struct { - initResp InitializeResponse - initErr error - - newUserResp NewUserResponse - newUserErr error - - updateUserResp UpdateUserResponse - updateUserErr error - - deleteUserResp DeleteUserResponse - deleteUserErr error - - typeResp string - typeErr error - - closeErr error -} - -func (e fakeDatabase) Initialize(ctx context.Context, req InitializeRequest) (InitializeResponse, error) { - return e.initResp, e.initErr -} - -func (e fakeDatabase) NewUser(ctx context.Context, req NewUserRequest) (NewUserResponse, error) { - return e.newUserResp, e.newUserErr -} - -func (e fakeDatabase) UpdateUser(ctx context.Context, req UpdateUserRequest) (UpdateUserResponse, error) { - return e.updateUserResp, e.updateUserErr -} - -func (e fakeDatabase) DeleteUser(ctx context.Context, req DeleteUserRequest) (DeleteUserResponse, error) { - return e.deleteUserResp, e.deleteUserErr -} - -func (e fakeDatabase) Type() (string, error) { - return e.typeResp, e.typeErr -} - -func (e fakeDatabase) Close() error { - return e.closeErr -} - -var _ Database = &recordingDatabase{} - -type recordingDatabase struct { - initializeCalls int - newUserCalls int - updateUserCalls int - deleteUserCalls int - typeCalls int - closeCalls int - - // recordingDatabase can act as middleware so we can record the calls to other test Database implementations - next Database -} - -func (f *recordingDatabase) Initialize(ctx context.Context, req InitializeRequest) (InitializeResponse, error) { - f.initializeCalls++ - if f.next == nil { - return InitializeResponse{}, nil - } - return f.next.Initialize(ctx, req) -} - -func (f *recordingDatabase) NewUser(ctx context.Context, req NewUserRequest) (NewUserResponse, error) { - f.newUserCalls++ - if f.next == nil { - return NewUserResponse{}, nil - } - return f.next.NewUser(ctx, req) -} - -func (f *recordingDatabase) UpdateUser(ctx context.Context, req UpdateUserRequest) (UpdateUserResponse, error) { - f.updateUserCalls++ - if f.next == nil { - return UpdateUserResponse{}, nil - } - return f.next.UpdateUser(ctx, req) -} - -func (f *recordingDatabase) DeleteUser(ctx context.Context, req DeleteUserRequest) (DeleteUserResponse, error) { - f.deleteUserCalls++ - if f.next == nil { - return DeleteUserResponse{}, nil - } - return f.next.DeleteUser(ctx, req) -} - -func (f *recordingDatabase) Type() (string, error) { - f.typeCalls++ - if f.next == nil { - return "recordingDatabase", nil - } - return f.next.Type() -} - -func (f *recordingDatabase) Close() error { - f.closeCalls++ - if f.next == nil { - return nil - } - return f.next.Close() -} - -type fakeDatabaseWithVersion struct { - version string -} - -func (e fakeDatabaseWithVersion) PluginVersion() logical.PluginVersion { - return logical.PluginVersion{Version: e.version} -} - -func (e fakeDatabaseWithVersion) Initialize(_ context.Context, _ InitializeRequest) (InitializeResponse, error) { - return InitializeResponse{}, nil -} - -func (e fakeDatabaseWithVersion) NewUser(_ context.Context, _ NewUserRequest) (NewUserResponse, error) { - return NewUserResponse{}, nil -} - -func (e fakeDatabaseWithVersion) UpdateUser(_ context.Context, _ UpdateUserRequest) (UpdateUserResponse, error) { - return UpdateUserResponse{}, nil -} - -func (e fakeDatabaseWithVersion) DeleteUser(_ context.Context, _ DeleteUserRequest) (DeleteUserResponse, error) { - return DeleteUserResponse{}, nil -} - -func (e fakeDatabaseWithVersion) Type() (string, error) { - return "", nil -} - -func (e fakeDatabaseWithVersion) Close() error { - return nil -} - -var ( - _ Database = (*fakeDatabaseWithVersion)(nil) - _ logical.PluginVersioner = (*fakeDatabaseWithVersion)(nil) -) diff --git a/sdk/database/dbplugin/v5/middleware_test.go b/sdk/database/dbplugin/v5/middleware_test.go deleted file mode 100644 index a2a76336f..000000000 --- a/sdk/database/dbplugin/v5/middleware_test.go +++ /dev/null @@ -1,487 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package dbplugin - -import ( - "context" - "errors" - "net/url" - "reflect" - "testing" - - "github.com/hashicorp/go-hclog" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -func TestDatabaseErrorSanitizerMiddleware(t *testing.T) { - type testCase struct { - inputErr error - secretsFunc func() map[string]string - - expectedError error - } - - tests := map[string]testCase{ - "nil error": { - inputErr: nil, - expectedError: nil, - }, - "url error": { - inputErr: new(url.Error), - expectedError: errors.New("unable to parse connection url"), - }, - "nil secrets func": { - inputErr: errors.New("here is my password: iofsd9473tg"), - expectedError: errors.New("here is my password: iofsd9473tg"), - }, - "secrets with empty string": { - inputErr: errors.New("here is my password: iofsd9473tg"), - secretsFunc: secretFunc(t, "", ""), - expectedError: errors.New("here is my password: iofsd9473tg"), - }, - "secrets that do not match": { - inputErr: errors.New("here is my password: iofsd9473tg"), - secretsFunc: secretFunc(t, "asdf", ""), - expectedError: errors.New("here is my password: iofsd9473tg"), - }, - "secrets that do match": { - inputErr: errors.New("here is my password: iofsd9473tg"), - secretsFunc: secretFunc(t, "iofsd9473tg", ""), - expectedError: errors.New("here is my password: "), - }, - "multiple secrets": { - inputErr: errors.New("here is my password: iofsd9473tg"), - secretsFunc: secretFunc(t, - "iofsd9473tg", "", - "password", "", - ), - expectedError: errors.New("here is my : "), - }, - "gRPC status error": { - inputErr: status.Error(codes.InvalidArgument, "an error with a password iofsd9473tg"), - secretsFunc: secretFunc(t, "iofsd9473tg", ""), - expectedError: status.Errorf(codes.InvalidArgument, "an error with a password "), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - db := fakeDatabase{} - mw := NewDatabaseErrorSanitizerMiddleware(db, test.secretsFunc) - - actualErr := mw.sanitize(test.inputErr) - if !reflect.DeepEqual(actualErr, test.expectedError) { - t.Fatalf("Actual error: %s\nExpected error: %s", actualErr, test.expectedError) - } - }) - } - - t.Run("Initialize", func(t *testing.T) { - db := &recordingDatabase{ - next: fakeDatabase{ - initErr: errors.New("password: iofsd9473tg with some stuff after it"), - }, - } - mw := DatabaseErrorSanitizerMiddleware{ - next: db, - secretsFn: secretFunc(t, "iofsd9473tg", ""), - } - - expectedErr := errors.New("password: with some stuff after it") - - _, err := mw.Initialize(context.Background(), InitializeRequest{}) - if !reflect.DeepEqual(err, expectedErr) { - t.Fatalf("Actual err: %s\n Expected err: %s", err, expectedErr) - } - - assertEquals(t, db.initializeCalls, 1) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("NewUser", func(t *testing.T) { - db := &recordingDatabase{ - next: fakeDatabase{ - newUserErr: errors.New("password: iofsd9473tg with some stuff after it"), - }, - } - mw := DatabaseErrorSanitizerMiddleware{ - next: db, - secretsFn: secretFunc(t, "iofsd9473tg", ""), - } - - expectedErr := errors.New("password: with some stuff after it") - - _, err := mw.NewUser(context.Background(), NewUserRequest{}) - if !reflect.DeepEqual(err, expectedErr) { - t.Fatalf("Actual err: %s\n Expected err: %s", err, expectedErr) - } - - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 1) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("UpdateUser", func(t *testing.T) { - db := &recordingDatabase{ - next: fakeDatabase{ - updateUserErr: errors.New("password: iofsd9473tg with some stuff after it"), - }, - } - mw := DatabaseErrorSanitizerMiddleware{ - next: db, - secretsFn: secretFunc(t, "iofsd9473tg", ""), - } - - expectedErr := errors.New("password: with some stuff after it") - - _, err := mw.UpdateUser(context.Background(), UpdateUserRequest{}) - if !reflect.DeepEqual(err, expectedErr) { - t.Fatalf("Actual err: %s\n Expected err: %s", err, expectedErr) - } - - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 1) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("DeleteUser", func(t *testing.T) { - db := &recordingDatabase{ - next: fakeDatabase{ - deleteUserErr: errors.New("password: iofsd9473tg with some stuff after it"), - }, - } - mw := DatabaseErrorSanitizerMiddleware{ - next: db, - secretsFn: secretFunc(t, "iofsd9473tg", ""), - } - - expectedErr := errors.New("password: with some stuff after it") - - _, err := mw.DeleteUser(context.Background(), DeleteUserRequest{}) - if !reflect.DeepEqual(err, expectedErr) { - t.Fatalf("Actual err: %s\n Expected err: %s", err, expectedErr) - } - - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 1) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("Type", func(t *testing.T) { - db := &recordingDatabase{ - next: fakeDatabase{ - typeErr: errors.New("password: iofsd9473tg with some stuff after it"), - }, - } - mw := DatabaseErrorSanitizerMiddleware{ - next: db, - secretsFn: secretFunc(t, "iofsd9473tg", ""), - } - - expectedErr := errors.New("password: with some stuff after it") - - _, err := mw.Type() - if !reflect.DeepEqual(err, expectedErr) { - t.Fatalf("Actual err: %s\n Expected err: %s", err, expectedErr) - } - - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 1) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("Close", func(t *testing.T) { - db := &recordingDatabase{ - next: fakeDatabase{ - closeErr: errors.New("password: iofsd9473tg with some stuff after it"), - }, - } - mw := DatabaseErrorSanitizerMiddleware{ - next: db, - secretsFn: secretFunc(t, "iofsd9473tg", ""), - } - - expectedErr := errors.New("password: with some stuff after it") - - err := mw.Close() - if !reflect.DeepEqual(err, expectedErr) { - t.Fatalf("Actual err: %s\n Expected err: %s", err, expectedErr) - } - - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 1) - }) -} - -func secretFunc(t *testing.T, vals ...string) func() map[string]string { - t.Helper() - if len(vals)%2 != 0 { - t.Fatalf("Test configuration error: secretFunc must be called with an even number of values") - } - - m := map[string]string{} - - for i := 0; i < len(vals); i += 2 { - key := vals[i] - m[key] = vals[i+1] - } - - return func() map[string]string { - return m - } -} - -func TestTracingMiddleware(t *testing.T) { - t.Run("Initialize", func(t *testing.T) { - db := &recordingDatabase{} - logger := hclog.NewNullLogger() - mw := databaseTracingMiddleware{ - next: db, - logger: logger, - } - _, err := mw.Initialize(context.Background(), InitializeRequest{}) - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 1) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("NewUser", func(t *testing.T) { - db := &recordingDatabase{} - logger := hclog.NewNullLogger() - mw := databaseTracingMiddleware{ - next: db, - logger: logger, - } - _, err := mw.NewUser(context.Background(), NewUserRequest{}) - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 1) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("UpdateUser", func(t *testing.T) { - db := &recordingDatabase{} - logger := hclog.NewNullLogger() - mw := databaseTracingMiddleware{ - next: db, - logger: logger, - } - _, err := mw.UpdateUser(context.Background(), UpdateUserRequest{}) - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 1) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("DeleteUser", func(t *testing.T) { - db := &recordingDatabase{} - logger := hclog.NewNullLogger() - mw := databaseTracingMiddleware{ - next: db, - logger: logger, - } - _, err := mw.DeleteUser(context.Background(), DeleteUserRequest{}) - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 1) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("Type", func(t *testing.T) { - db := &recordingDatabase{} - logger := hclog.NewNullLogger() - mw := databaseTracingMiddleware{ - next: db, - logger: logger, - } - _, err := mw.Type() - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 1) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("Close", func(t *testing.T) { - db := &recordingDatabase{} - logger := hclog.NewNullLogger() - mw := databaseTracingMiddleware{ - next: db, - logger: logger, - } - err := mw.Close() - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 1) - }) -} - -func TestMetricsMiddleware(t *testing.T) { - t.Run("Initialize", func(t *testing.T) { - db := &recordingDatabase{} - mw := databaseMetricsMiddleware{ - next: db, - typeStr: "metrics", - } - _, err := mw.Initialize(context.Background(), InitializeRequest{}) - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 1) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("NewUser", func(t *testing.T) { - db := &recordingDatabase{} - mw := databaseMetricsMiddleware{ - next: db, - typeStr: "metrics", - } - _, err := mw.NewUser(context.Background(), NewUserRequest{}) - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 1) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("UpdateUser", func(t *testing.T) { - db := &recordingDatabase{} - mw := databaseMetricsMiddleware{ - next: db, - typeStr: "metrics", - } - _, err := mw.UpdateUser(context.Background(), UpdateUserRequest{}) - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 1) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("DeleteUser", func(t *testing.T) { - db := &recordingDatabase{} - mw := databaseMetricsMiddleware{ - next: db, - typeStr: "metrics", - } - _, err := mw.DeleteUser(context.Background(), DeleteUserRequest{}) - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 1) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("Type", func(t *testing.T) { - db := &recordingDatabase{} - mw := databaseMetricsMiddleware{ - next: db, - typeStr: "metrics", - } - _, err := mw.Type() - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 1) - assertEquals(t, db.closeCalls, 0) - }) - - t.Run("Close", func(t *testing.T) { - db := &recordingDatabase{} - mw := databaseMetricsMiddleware{ - next: db, - typeStr: "metrics", - } - err := mw.Close() - if err != nil { - t.Fatalf("Expected no error, but got: %s", err) - } - assertEquals(t, db.initializeCalls, 0) - assertEquals(t, db.newUserCalls, 0) - assertEquals(t, db.updateUserCalls, 0) - assertEquals(t, db.deleteUserCalls, 0) - assertEquals(t, db.typeCalls, 0) - assertEquals(t, db.closeCalls, 1) - }) -} - -func assertEquals(t *testing.T, actual, expected int) { - t.Helper() - if actual != expected { - t.Fatalf("Actual: %d Expected: %d", actual, expected) - } -} diff --git a/sdk/database/dbplugin/v5/plugin_client_test.go b/sdk/database/dbplugin/v5/plugin_client_test.go deleted file mode 100644 index 10f02b7be..000000000 --- a/sdk/database/dbplugin/v5/plugin_client_test.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package dbplugin - -import ( - "context" - "errors" - "reflect" - "testing" - "time" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/helper/pluginutil" - "github.com/hashicorp/vault/sdk/helper/wrapping" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" -) - -func TestNewPluginClient(t *testing.T) { - type testCase struct { - config pluginutil.PluginClientConfig - pluginClient pluginutil.PluginClient - expectedResp *DatabasePluginClient - expectedErr error - } - - tests := map[string]testCase{ - "happy path": { - config: testPluginClientConfig(), - pluginClient: &fakePluginClient{ - connResp: nil, - dispenseResp: gRPCClient{client: fakeClient{}}, - dispenseErr: nil, - }, - expectedResp: &DatabasePluginClient{ - client: &fakePluginClient{ - connResp: nil, - dispenseResp: gRPCClient{client: fakeClient{}}, - dispenseErr: nil, - }, - Database: gRPCClient{client: proto.NewDatabaseClient(nil), versionClient: logical.NewPluginVersionClient(nil), doneCtx: context.Context(nil)}, - }, - expectedErr: nil, - }, - "dispense error": { - config: testPluginClientConfig(), - pluginClient: &fakePluginClient{ - connResp: nil, - dispenseResp: gRPCClient{}, - dispenseErr: errors.New("dispense error"), - }, - expectedResp: nil, - expectedErr: errors.New("dispense error"), - }, - "error unsupported client type": { - config: testPluginClientConfig(), - pluginClient: &fakePluginClient{ - connResp: nil, - dispenseResp: nil, - dispenseErr: nil, - }, - expectedResp: nil, - expectedErr: errors.New("unsupported client type"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - ctx := context.Background() - - mockWrapper := new(mockRunnerUtil) - mockWrapper.On("NewPluginClient", ctx, mock.Anything). - Return(test.pluginClient, nil) - defer mockWrapper.AssertNumberOfCalls(t, "NewPluginClient", 1) - - resp, err := NewPluginClient(ctx, mockWrapper, test.config) - if test.expectedErr != nil && err == nil { - t.Fatalf("err expected, got nil") - } - if test.expectedErr == nil && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - if test.expectedErr == nil && !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -func testPluginClientConfig() pluginutil.PluginClientConfig { - return pluginutil.PluginClientConfig{ - Name: "test-plugin", - PluginSets: PluginSets, - PluginType: consts.PluginTypeDatabase, - HandshakeConfig: HandshakeConfig, - Logger: log.NewNullLogger(), - IsMetadataMode: true, - AutoMTLS: true, - } -} - -var _ pluginutil.PluginClient = &fakePluginClient{} - -type fakePluginClient struct { - connResp grpc.ClientConnInterface - - dispenseResp interface{} - dispenseErr error -} - -func (f *fakePluginClient) Conn() grpc.ClientConnInterface { - return nil -} - -func (f *fakePluginClient) Reload() error { - return nil -} - -func (f *fakePluginClient) Dispense(name string) (interface{}, error) { - return f.dispenseResp, f.dispenseErr -} - -func (f *fakePluginClient) Ping() error { - return nil -} - -func (f *fakePluginClient) Close() error { - return nil -} - -var _ pluginutil.RunnerUtil = &mockRunnerUtil{} - -type mockRunnerUtil struct { - mock.Mock -} - -func (m *mockRunnerUtil) VaultVersion(ctx context.Context) (string, error) { - return "dummyversion", nil -} - -func (m *mockRunnerUtil) NewPluginClient(ctx context.Context, config pluginutil.PluginClientConfig) (pluginutil.PluginClient, error) { - args := m.Called(ctx, config) - return args.Get(0).(pluginutil.PluginClient), args.Error(1) -} - -func (m *mockRunnerUtil) ResponseWrapData(ctx context.Context, data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error) { - args := m.Called(ctx, data, ttl, jwt) - return args.Get(0).(*wrapping.ResponseWrapInfo), args.Error(1) -} - -func (m *mockRunnerUtil) MlockEnabled() bool { - args := m.Called() - return args.Bool(0) -} diff --git a/sdk/database/dbplugin/v5/testing/test_helpers.go b/sdk/database/dbplugin/v5/testing/test_helpers.go deleted file mode 100644 index 9be65c6e6..000000000 --- a/sdk/database/dbplugin/v5/testing/test_helpers.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package dbtesting - -import ( - "context" - "os" - "testing" - "time" - - "github.com/hashicorp/go-secure-stdlib/parseutil" - "github.com/hashicorp/vault/sdk/database/dbplugin/v5" -) - -func getRequestTimeout(t *testing.T) time.Duration { - rawDur := os.Getenv("VAULT_TEST_DATABASE_REQUEST_TIMEOUT") - if rawDur == "" { - // Note: we incremented the default timeout from 5 to 10 seconds in a bid - // to fix sporadic failures of mssql_test.go tests TestInitialize() and - // TestUpdateUser_password(). - - return 10 * time.Second - } - - dur, err := parseutil.ParseDurationSecond(rawDur) - if err != nil { - t.Fatalf("Failed to parse custom request timeout %q: %s", rawDur, err) - } - return dur -} - -// AssertInitializeCircleCiTest help to diagnose CircleCI failures within AssertInitialize for mssql tests failing -// with "Failed to initialize: error verifying connection ...". This will now mark a test as failed instead of being fatal -func AssertInitializeCircleCiTest(t *testing.T, db dbplugin.Database, req dbplugin.InitializeRequest) dbplugin.InitializeResponse { - t.Helper() - maxAttempts := 5 - var resp dbplugin.InitializeResponse - var err error - - for i := 1; i <= maxAttempts; i++ { - resp, err = verifyInitialize(t, db, req) - if err != nil { - t.Errorf("Failed AssertInitialize attempt: %d with error:\n%+v\n", i, err) - time.Sleep(1 * time.Second) - continue - } - - if i > 1 { - t.Logf("AssertInitialize worked the %d time around with a 1 second sleep", i) - } - break - } - - return resp -} - -func AssertInitialize(t *testing.T, db dbplugin.Database, req dbplugin.InitializeRequest) dbplugin.InitializeResponse { - t.Helper() - resp, err := verifyInitialize(t, db, req) - if err != nil { - t.Fatalf("Failed to initialize: %s", err) - } - return resp -} - -func verifyInitialize(t *testing.T, db dbplugin.Database, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) { - ctx, cancel := context.WithTimeout(context.Background(), getRequestTimeout(t)) - defer cancel() - - return db.Initialize(ctx, req) -} - -func AssertNewUser(t *testing.T, db dbplugin.Database, req dbplugin.NewUserRequest) dbplugin.NewUserResponse { - t.Helper() - - ctx, cancel := context.WithTimeout(context.Background(), getRequestTimeout(t)) - defer cancel() - - resp, err := db.NewUser(ctx, req) - if err != nil { - t.Fatalf("Failed to create new user: %s", err) - } - - if resp.Username == "" { - t.Fatalf("Missing username from NewUser response") - } - return resp -} - -func AssertUpdateUser(t *testing.T, db dbplugin.Database, req dbplugin.UpdateUserRequest) { - t.Helper() - - ctx, cancel := context.WithTimeout(context.Background(), getRequestTimeout(t)) - defer cancel() - - _, err := db.UpdateUser(ctx, req) - if err != nil { - t.Fatalf("Failed to update user: %s", err) - } -} - -func AssertDeleteUser(t *testing.T, db dbplugin.Database, req dbplugin.DeleteUserRequest) { - t.Helper() - - ctx, cancel := context.WithTimeout(context.Background(), getRequestTimeout(t)) - defer cancel() - - _, err := db.DeleteUser(ctx, req) - if err != nil { - t.Fatalf("Failed to delete user %q: %s", req.Username, err) - } -} - -func AssertClose(t *testing.T, db dbplugin.Database) { - t.Helper() - err := db.Close() - if err != nil { - t.Fatalf("Failed to close database: %s", err) - } -} diff --git a/sdk/database/helper/connutil/sql_test.go b/sdk/database/helper/connutil/sql_test.go deleted file mode 100644 index 9f29d4ae2..000000000 --- a/sdk/database/helper/connutil/sql_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package connutil - -import ( - "context" - "net/url" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSQLPasswordChars(t *testing.T) { - testCases := []struct { - Username string - Password string - }{ - {"postgres", "password{0}"}, - {"postgres", "pass:word"}, - {"postgres", "pass/word"}, - {"postgres", "p@ssword"}, - {"postgres", "pass\"word\""}, - } - for _, tc := range testCases { - t.Logf("username %q password %q", tc.Username, tc.Password) - - sql := &SQLConnectionProducer{} - ctx := context.Background() - conf := map[string]interface{}{ - "connection_url": "postgres://{{username}}:{{password}}@localhost:5432/mydb", - "username": tc.Username, - "password": tc.Password, - "disable_escaping": false, - } - _, err := sql.Init(ctx, conf, false) - if err != nil { - t.Errorf("Init error on %q %q: %+v", tc.Username, tc.Password, err) - } else { - // This jumps down a few layers... - // Connection() uses sql.Open uses lib/pq uses net/url.Parse - u, err := url.Parse(sql.ConnectionURL) - if err != nil { - t.Errorf("URL parse error on %q %q: %+v", tc.Username, tc.Password, err) - } else { - username := u.User.Username() - password, pPresent := u.User.Password() - if username != tc.Username { - t.Errorf("Parsed username %q != original username %q", username, tc.Username) - } - if !pPresent { - t.Errorf("Password %q not present", tc.Password) - } else if password != tc.Password { - t.Errorf("Parsed password %q != original password %q", password, tc.Password) - } - } - } - } -} - -func TestSQLDisableEscaping(t *testing.T) { - testCases := []struct { - Username string - Password string - DisableEscaping bool - }{ - {"mssql{0}", "password{0}", true}, - {"mssql{0}", "password{0}", false}, - {"ms\"sql\"", "pass\"word\"", true}, - {"ms\"sql\"", "pass\"word\"", false}, - {"ms'sq;l", "pass'wor;d", true}, - {"ms'sq;l", "pass'wor;d", false}, - } - for _, tc := range testCases { - t.Logf("username %q password %q disable_escaling %t", tc.Username, tc.Password, tc.DisableEscaping) - - sql := &SQLConnectionProducer{} - ctx := context.Background() - conf := map[string]interface{}{ - "connection_url": "server=localhost;port=1433;user id={{username}};password={{password}};database=mydb;", - "username": tc.Username, - "password": tc.Password, - "disable_escaping": tc.DisableEscaping, - } - _, err := sql.Init(ctx, conf, false) - if err != nil { - t.Errorf("Init error on %q %q: %+v", tc.Username, tc.Password, err) - } else { - if tc.DisableEscaping { - if !strings.Contains(sql.ConnectionURL, tc.Username) || !strings.Contains(sql.ConnectionURL, tc.Password) { - t.Errorf("Raw username and/or password missing from ConnectionURL") - } - } else { - if strings.Contains(sql.ConnectionURL, tc.Username) || strings.Contains(sql.ConnectionURL, tc.Password) { - t.Errorf("Raw username and/or password was present in ConnectionURL") - } - } - } - } -} - -func TestSQLDisallowTemplates(t *testing.T) { - testCases := []struct { - Username string - Password string - }{ - {"{{username}}", "pass"}, - {"{{password}}", "pass"}, - {"user", "{{username}}"}, - {"user", "{{password}}"}, - {"{{username}}", "{{password}}"}, - {"abc{username}xyz", "123{password}789"}, - {"abc{{username}}xyz", "123{{password}}789"}, - {"abc{{{username}}}xyz", "123{{{password}}}789"}, - } - for _, disableEscaping := range []bool{true, false} { - for _, tc := range testCases { - t.Logf("username %q password %q disable_escaping %t", tc.Username, tc.Password, disableEscaping) - - sql := &SQLConnectionProducer{} - ctx := context.Background() - conf := map[string]interface{}{ - "connection_url": "server=localhost;port=1433;user id={{username}};password={{password}};database=mydb;", - "username": tc.Username, - "password": tc.Password, - "disable_escaping": disableEscaping, - } - _, err := sql.Init(ctx, conf, false) - if disableEscaping { - if err != nil { - if !assert.EqualError(t, err, "username and/or password cannot contain the template variables") { - t.Errorf("Init error on %q %q: %+v", tc.Username, tc.Password, err) - } - } else { - assert.Equal(t, sql.ConnectionURL, "server=localhost;port=1433;user id=abc{username}xyz;password=123{password}789;database=mydb;") - } - } - } - } -} diff --git a/sdk/database/helper/credsutil/credsutil_test.go b/sdk/database/helper/credsutil/credsutil_test.go deleted file mode 100644 index 77e1a2862..000000000 --- a/sdk/database/helper/credsutil/credsutil_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package credsutil - -import ( - "strings" - "testing" -) - -func TestRandomAlphaNumeric(t *testing.T) { - s, err := RandomAlphaNumeric(10, true) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - if len(s) != 10 { - t.Fatalf("Unexpected length of string, expected 10, got string: %s", s) - } - - s, err = RandomAlphaNumeric(20, true) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - if len(s) != 20 { - t.Fatalf("Unexpected length of string, expected 20, got string: %s", s) - } - - if !strings.Contains(s, reqStr) { - t.Fatalf("Expected %s to contain %s", s, reqStr) - } - - s, err = RandomAlphaNumeric(20, false) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - if len(s) != 20 { - t.Fatalf("Unexpected length of string, expected 20, got string: %s", s) - } - - if strings.Contains(s, reqStr) { - t.Fatalf("Expected %s not to contain %s", s, reqStr) - } -} diff --git a/sdk/database/helper/credsutil/usernames_test.go b/sdk/database/helper/credsutil/usernames_test.go deleted file mode 100644 index a3e883491..000000000 --- a/sdk/database/helper/credsutil/usernames_test.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package credsutil - -import ( - "regexp" - "testing" -) - -func TestGenerateUsername(t *testing.T) { - type testCase struct { - displayName string - displayNameLen int - - roleName string - roleNameLen int - - usernameLen int - separator string - caseOp CaseOp - - regex string - } - tests := map[string]testCase{ - "all opts": { - displayName: "abcdefghijklmonpqrstuvwxyz", - displayNameLen: 10, - roleName: "zyxwvutsrqpnomlkjihgfedcba", - roleNameLen: 10, - usernameLen: 45, - separator: ".", - caseOp: KeepCase, - - regex: "^v.abcdefghij.zyxwvutsrq.[a-zA-Z0-9]{20}.$", - }, - "no separator": { - displayName: "abcdefghijklmonpqrstuvwxyz", - displayNameLen: 10, - roleName: "zyxwvutsrqpnomlkjihgfedcba", - roleNameLen: 10, - usernameLen: 45, - separator: "", - caseOp: KeepCase, - - regex: "^vabcdefghijzyxwvutsrq[a-zA-Z0-9]{20}[0-9]{4}$", - }, - "lowercase": { - displayName: "abcdefghijklmonpqrstuvwxyz", - displayNameLen: 10, - roleName: "zyxwvutsrqpnomlkjihgfedcba", - roleNameLen: 10, - usernameLen: 45, - separator: "_", - caseOp: Lowercase, - - regex: "^v_abcdefghij_zyxwvutsrq_[a-z0-9]{20}_$", - }, - "uppercase": { - displayName: "abcdefghijklmonpqrstuvwxyz", - displayNameLen: 10, - roleName: "zyxwvutsrqpnomlkjihgfedcba", - roleNameLen: 10, - usernameLen: 45, - separator: "_", - caseOp: Uppercase, - - regex: "^V_ABCDEFGHIJ_ZYXWVUTSRQ_[A-Z0-9]{20}_$", - }, - "short username": { - displayName: "abcdefghijklmonpqrstuvwxyz", - displayNameLen: 5, - roleName: "zyxwvutsrqpnomlkjihgfedcba", - roleNameLen: 5, - usernameLen: 15, - separator: "_", - caseOp: KeepCase, - - regex: "^v_abcde_zyxwv_[a-zA-Z0-9]{1}$", - }, - "long username": { - displayName: "abcdefghijklmonpqrstuvwxyz", - displayNameLen: 0, - roleName: "zyxwvutsrqpnomlkjihgfedcba", - roleNameLen: 0, - usernameLen: 100, - separator: "_", - caseOp: KeepCase, - - regex: "^v_abcdefghijklmonpqrstuvwxyz_zyxwvutsrqpnomlkjihgfedcba_[a-zA-Z0-9]{20}_[0-9]{1,23}$", - }, - "zero max length": { - displayName: "abcdefghijklmonpqrstuvwxyz", - displayNameLen: 0, - roleName: "zyxwvutsrqpnomlkjihgfedcba", - roleNameLen: 0, - usernameLen: 0, - separator: "_", - caseOp: KeepCase, - - regex: "^v_abcdefghijklmonpqrstuvwxyz_zyxwvutsrqpnomlkjihgfedcba_[a-zA-Z0-9]{20}_[0-9]+$", - }, - "no display name": { - displayName: "abcdefghijklmonpqrstuvwxyz", - displayNameLen: NoneLength, - roleName: "zyxwvutsrqpnomlkjihgfedcba", - roleNameLen: 15, - usernameLen: 100, - separator: "_", - caseOp: KeepCase, - - regex: "^v_zyxwvutsrqpnoml_[a-zA-Z0-9]{20}_[0-9]+$", - }, - "no role name": { - displayName: "abcdefghijklmonpqrstuvwxyz", - displayNameLen: 15, - roleName: "zyxwvutsrqpnomlkjihgfedcba", - roleNameLen: NoneLength, - usernameLen: 100, - separator: "_", - caseOp: KeepCase, - - regex: "^v_abcdefghijklmon_[a-zA-Z0-9]{20}_[0-9]+$", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - re := regexp.MustCompile(test.regex) - - username, err := GenerateUsername( - DisplayName(test.displayName, test.displayNameLen), - RoleName(test.roleName, test.roleNameLen), - Separator(test.separator), - MaxLength(test.usernameLen), - Case(test.caseOp), - ) - if err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !re.MatchString(username) { - t.Fatalf("username %q does not match regex %q", username, test.regex) - } - }) - } -} diff --git a/sdk/database/helper/dbutil/dbutil_test.go b/sdk/database/helper/dbutil/dbutil_test.go deleted file mode 100644 index 797712b4d..000000000 --- a/sdk/database/helper/dbutil/dbutil_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package dbutil - -import ( - "reflect" - "testing" - - "github.com/hashicorp/vault/sdk/database/dbplugin" -) - -func TestStatementCompatibilityHelper(t *testing.T) { - const ( - creationStatement = "creation" - renewStatement = "renew" - revokeStatement = "revoke" - rollbackStatement = "rollback" - ) - - expectedStatements := dbplugin.Statements{ - Creation: []string{creationStatement}, - Rollback: []string{rollbackStatement}, - Revocation: []string{revokeStatement}, - Renewal: []string{renewStatement}, - CreationStatements: creationStatement, - RenewStatements: renewStatement, - RollbackStatements: rollbackStatement, - RevocationStatements: revokeStatement, - } - - statements1 := dbplugin.Statements{ - CreationStatements: creationStatement, - RenewStatements: renewStatement, - RollbackStatements: rollbackStatement, - RevocationStatements: revokeStatement, - } - - if !reflect.DeepEqual(expectedStatements, StatementCompatibilityHelper(statements1)) { - t.Fatalf("mismatch: %#v, %#v", expectedStatements, statements1) - } - - statements2 := dbplugin.Statements{ - Creation: []string{creationStatement}, - Rollback: []string{rollbackStatement}, - Revocation: []string{revokeStatement}, - Renewal: []string{renewStatement}, - } - - if !reflect.DeepEqual(expectedStatements, StatementCompatibilityHelper(statements2)) { - t.Fatalf("mismatch: %#v, %#v", expectedStatements, statements2) - } - - statements3 := dbplugin.Statements{ - CreationStatements: creationStatement, - } - expectedStatements3 := dbplugin.Statements{ - Creation: []string{creationStatement}, - CreationStatements: creationStatement, - } - if !reflect.DeepEqual(expectedStatements3, StatementCompatibilityHelper(statements3)) { - t.Fatalf("mismatch: %#v, %#v", expectedStatements3, statements3) - } -} diff --git a/sdk/framework/backend_test.go b/sdk/framework/backend_test.go deleted file mode 100644 index 0b7a20543..000000000 --- a/sdk/framework/backend_test.go +++ /dev/null @@ -1,837 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "context" - "fmt" - "net/http" - "reflect" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/hashicorp/go-secure-stdlib/strutil" - - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/logical" - "github.com/stretchr/testify/require" -) - -func BenchmarkBackendRoute(b *testing.B) { - patterns := []string{ - "foo", - "bar/(?P.+?)", - "baz/(?Pwhat)", - `aws/policy/(?P\w)`, - `aws/(?P\w)`, - } - - backend := &Backend{Paths: make([]*Path, 0, len(patterns))} - for _, p := range patterns { - backend.Paths = append(backend.Paths, &Path{Pattern: p}) - } - - // Warm any caches - backend.Route("aws/policy/foo") - - // Reset the timer since we did a lot above - b.ResetTimer() - - // Run through and route. We do a sanity check of the return value - for i := 0; i < b.N; i++ { - if p := backend.Route("aws/policy/foo"); p == nil { - b.Fatal("p should not be nil") - } - } -} - -func TestBackend_impl(t *testing.T) { - var _ logical.Backend = new(Backend) -} - -func TestBackendHandleRequestFieldWarnings(t *testing.T) { - handler := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { - return &logical.Response{ - Data: map[string]interface{}{ - "an_int": data.Get("an_int"), - "a_string": data.Get("a_string"), - "name": data.Get("name"), - }, - }, nil - } - - backend := &Backend{ - Paths: []*Path{ - { - Pattern: "foo/bar/(?P.+)", - Fields: map[string]*FieldSchema{ - "an_int": {Type: TypeInt}, - "a_string": {Type: TypeString}, - "name": {Type: TypeString}, - }, - Operations: map[logical.Operation]OperationHandler{ - logical.UpdateOperation: &PathOperation{Callback: handler}, - }, - }, - }, - } - ctx := context.Background() - resp, err := backend.HandleRequest(ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "foo/bar/baz", - Data: map[string]interface{}{ - "an_int": 10, - "a_string": "accepted", - "unrecognized1": "unrecognized", - "unrecognized2": 20.2, - "name": "noop", - }, - }) - require.NoError(t, err) - require.NotNil(t, resp) - t.Log(resp.Warnings) - require.Len(t, resp.Warnings, 2) - require.True(t, strutil.StrListContains(resp.Warnings, "Endpoint ignored these unrecognized parameters: [unrecognized1 unrecognized2]")) - require.True(t, strutil.StrListContains(resp.Warnings, "Endpoint replaced the value of these parameters with the values captured from the endpoint's path: [name]")) -} - -func TestBackendHandleRequest(t *testing.T) { - callback := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { - return &logical.Response{ - Data: map[string]interface{}{ - "value": data.Get("value"), - }, - }, nil - } - handler := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { - return &logical.Response{ - Data: map[string]interface{}{ - "amount": data.Get("amount"), - }, - }, nil - } - - b := &Backend{ - Paths: []*Path{ - { - Pattern: "foo/bar", - Fields: map[string]*FieldSchema{ - "value": {Type: TypeInt}, - }, - Callbacks: map[logical.Operation]OperationFunc{ - logical.ReadOperation: callback, - }, - }, - { - Pattern: "foo/baz/handler", - Fields: map[string]*FieldSchema{ - "amount": {Type: TypeInt}, - }, - Operations: map[logical.Operation]OperationHandler{ - logical.ReadOperation: &PathOperation{Callback: handler}, - }, - }, - { - Pattern: "foo/both/handler", - Fields: map[string]*FieldSchema{ - "amount": {Type: TypeInt}, - }, - Callbacks: map[logical.Operation]OperationFunc{ - logical.ReadOperation: callback, - }, - Operations: map[logical.Operation]OperationHandler{ - logical.ReadOperation: &PathOperation{Callback: handler}, - }, - }, - }, - system: &logical.StaticSystemView{}, - } - - for _, path := range []string{"foo/bar", "foo/baz/handler", "foo/both/handler"} { - key := "value" - if strings.Contains(path, "handler") { - key = "amount" - } - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: path, - Data: map[string]interface{}{key: "42"}, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - if resp.Data[key] != 42 { - t.Fatalf("bad: %#v", resp) - } - } -} - -func TestBackendHandleRequest_Forwarding(t *testing.T) { - tests := map[string]struct { - fwdStandby bool - fwdSecondary bool - isLocal bool - isStandby bool - isSecondary bool - expectFwd bool - nilSysView bool - }{ - "no forward": { - expectFwd: false, - }, - "no forward, local restricted": { - isSecondary: true, - fwdSecondary: true, - isLocal: true, - expectFwd: false, - }, - "no forward, forwarding not requested": { - isSecondary: true, - isStandby: true, - expectFwd: false, - }, - "forward, secondary": { - fwdSecondary: true, - isSecondary: true, - expectFwd: true, - }, - "forward, standby": { - fwdStandby: true, - isStandby: true, - expectFwd: true, - }, - "no forward, only secondary": { - fwdSecondary: true, - isStandby: true, - expectFwd: false, - }, - "no forward, only standby": { - fwdStandby: true, - isSecondary: true, - expectFwd: false, - }, - "nil system view": { - nilSysView: true, - expectFwd: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - var replState consts.ReplicationState - if test.isStandby { - replState.AddState(consts.ReplicationPerformanceStandby) - } - if test.isSecondary { - replState.AddState(consts.ReplicationPerformanceSecondary) - } - - b := &Backend{ - Paths: []*Path{ - { - Pattern: "foo", - Operations: map[logical.Operation]OperationHandler{ - logical.ReadOperation: &PathOperation{ - Callback: func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { - return nil, nil - }, - ForwardPerformanceSecondary: test.fwdSecondary, - ForwardPerformanceStandby: test.fwdStandby, - }, - }, - }, - }, - - system: &logical.StaticSystemView{ - LocalMountVal: test.isLocal, - ReplicationStateVal: replState, - }, - } - - if test.nilSysView { - b.system = nil - } - - _, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "foo", - }) - - if !test.expectFwd && err != nil { - t.Fatalf("unexpected err: %v", err) - } - if test.expectFwd && err != logical.ErrReadOnly { - t.Fatalf("expected ErrReadOnly, got: %v", err) - } - }) - } -} - -func TestBackendHandleRequest_badwrite(t *testing.T) { - callback := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { - return &logical.Response{ - Data: map[string]interface{}{ - "value": data.Get("value").(bool), - }, - }, nil - } - - b := &Backend{ - Paths: []*Path{ - { - Pattern: "foo/bar", - Fields: map[string]*FieldSchema{ - "value": {Type: TypeBool}, - }, - Callbacks: map[logical.Operation]OperationFunc{ - logical.UpdateOperation: callback, - }, - }, - }, - } - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "foo/bar", - Data: map[string]interface{}{"value": "3false3"}, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !strings.Contains(resp.Data["error"].(string), "Field validation failed") { - t.Fatalf("bad: %#v", resp) - } -} - -func TestBackendHandleRequest_404(t *testing.T) { - callback := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { - return &logical.Response{ - Data: map[string]interface{}{ - "value": data.Get("value"), - }, - }, nil - } - - b := &Backend{ - Paths: []*Path{ - { - Pattern: `foo/bar`, - Fields: map[string]*FieldSchema{ - "value": {Type: TypeInt}, - }, - Callbacks: map[logical.Operation]OperationFunc{ - logical.ReadOperation: callback, - }, - }, - }, - } - - _, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "foo/baz", - Data: map[string]interface{}{"value": "84"}, - }) - if err != logical.ErrUnsupportedPath { - t.Fatalf("err: %s", err) - } -} - -func TestBackendHandleRequest_help(t *testing.T) { - b := &Backend{ - Paths: []*Path{ - { - Pattern: "foo/bar", - Fields: map[string]*FieldSchema{ - "value": {Type: TypeInt}, - }, - HelpSynopsis: "foo", - HelpDescription: "bar", - }, - }, - } - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.HelpOperation, - Path: "foo/bar", - Data: map[string]interface{}{"value": "42"}, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - if resp.Data["help"] == nil { - t.Fatalf("bad: %#v", resp) - } -} - -func TestBackendHandleRequest_helpRoot(t *testing.T) { - b := &Backend{ - Help: "42", - } - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.HelpOperation, - Path: "", - }) - if err != nil { - t.Fatalf("err: %s", err) - } - if resp.Data["help"] == nil { - t.Fatalf("bad: %#v", resp) - } -} - -func TestBackendHandleRequest_renewAuth(t *testing.T) { - b := &Backend{} - - resp, err := b.HandleRequest(context.Background(), logical.RenewAuthRequest("/foo", &logical.Auth{}, nil)) - if err != nil { - t.Fatalf("err: %s", err) - } - if !resp.IsError() { - t.Fatalf("bad: %#v", resp) - } -} - -func TestBackendHandleRequest_renewAuthCallback(t *testing.T) { - called := new(uint32) - callback := func(context.Context, *logical.Request, *FieldData) (*logical.Response, error) { - atomic.AddUint32(called, 1) - return nil, nil - } - - b := &Backend{ - AuthRenew: callback, - } - - _, err := b.HandleRequest(context.Background(), logical.RenewAuthRequest("/foo", &logical.Auth{}, nil)) - if err != nil { - t.Fatalf("err: %s", err) - } - if v := atomic.LoadUint32(called); v != 1 { - t.Fatalf("bad: %#v", v) - } -} - -func TestBackendHandleRequest_renew(t *testing.T) { - called := new(uint32) - callback := func(context.Context, *logical.Request, *FieldData) (*logical.Response, error) { - atomic.AddUint32(called, 1) - return nil, nil - } - - secret := &Secret{ - Type: "foo", - Renew: callback, - } - b := &Backend{ - Secrets: []*Secret{secret}, - } - - _, err := b.HandleRequest(context.Background(), logical.RenewRequest("/foo", secret.Response(nil, nil).Secret, nil)) - if err != nil { - t.Fatalf("err: %s", err) - } - if v := atomic.LoadUint32(called); v != 1 { - t.Fatalf("bad: %#v", v) - } -} - -func TestBackendHandleRequest_revoke(t *testing.T) { - called := new(uint32) - callback := func(context.Context, *logical.Request, *FieldData) (*logical.Response, error) { - atomic.AddUint32(called, 1) - return nil, nil - } - - secret := &Secret{ - Type: "foo", - Revoke: callback, - } - b := &Backend{ - Secrets: []*Secret{secret}, - } - - _, err := b.HandleRequest(context.Background(), logical.RevokeRequest("/foo", secret.Response(nil, nil).Secret, nil)) - if err != nil { - t.Fatalf("err: %s", err) - } - if v := atomic.LoadUint32(called); v != 1 { - t.Fatalf("bad: %#v", v) - } -} - -func TestBackendHandleRequest_rollback(t *testing.T) { - called := new(uint32) - callback := func(_ context.Context, req *logical.Request, kind string, data interface{}) error { - if data == "foo" { - atomic.AddUint32(called, 1) - } - return nil - } - - b := &Backend{ - WALRollback: callback, - WALRollbackMinAge: 1 * time.Millisecond, - } - - storage := new(logical.InmemStorage) - if _, err := PutWAL(context.Background(), storage, "kind", "foo"); err != nil { - t.Fatalf("err: %s", err) - } - - time.Sleep(10 * time.Millisecond) - - _, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.RollbackOperation, - Path: "", - Storage: storage, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - if v := atomic.LoadUint32(called); v != 1 { - t.Fatalf("bad: %#v", v) - } -} - -func TestBackendHandleRequest_rollbackMinAge(t *testing.T) { - called := new(uint32) - callback := func(_ context.Context, req *logical.Request, kind string, data interface{}) error { - if data == "foo" { - atomic.AddUint32(called, 1) - } - return nil - } - - b := &Backend{ - WALRollback: callback, - WALRollbackMinAge: 5 * time.Second, - } - - storage := new(logical.InmemStorage) - if _, err := PutWAL(context.Background(), storage, "kind", "foo"); err != nil { - t.Fatalf("err: %s", err) - } - - _, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.RollbackOperation, - Path: "", - Storage: storage, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - if v := atomic.LoadUint32(called); v != 0 { - t.Fatalf("bad: %#v", v) - } -} - -func TestBackendHandleRequest_unsupportedOperation(t *testing.T) { - callback := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { - return &logical.Response{ - Data: map[string]interface{}{ - "value": data.Get("value"), - }, - }, nil - } - - b := &Backend{ - Paths: []*Path{ - { - Pattern: `foo/bar`, - Fields: map[string]*FieldSchema{ - "value": {Type: TypeInt}, - }, - Callbacks: map[logical.Operation]OperationFunc{ - logical.ReadOperation: callback, - }, - }, - }, - } - - _, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.UpdateOperation, - Path: "foo/bar", - Data: map[string]interface{}{"value": "84"}, - }) - if err != logical.ErrUnsupportedOperation { - t.Fatalf("err: %s", err) - } -} - -func TestBackendHandleRequest_urlPriority(t *testing.T) { - callback := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { - return &logical.Response{ - Data: map[string]interface{}{ - "value": data.Get("value"), - }, - }, nil - } - - b := &Backend{ - Paths: []*Path{ - { - Pattern: `foo/(?P\d+)`, - Fields: map[string]*FieldSchema{ - "value": {Type: TypeInt}, - }, - Callbacks: map[logical.Operation]OperationFunc{ - logical.ReadOperation: callback, - }, - }, - }, - } - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.ReadOperation, - Path: "foo/42", - Data: map[string]interface{}{"value": "84"}, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - if resp.Data["value"] != 42 { - t.Fatalf("bad: %#v", resp) - } -} - -func TestBackendRoute(t *testing.T) { - cases := map[string]struct { - Patterns []string - Path string - Match string - }{ - "no match": { - []string{"foo"}, - "bar", - "", - }, - - "exact": { - []string{"foo"}, - "foo", - "^foo$", - }, - - "regexp": { - []string{"fo+"}, - "foo", - "^fo+$", - }, - - "anchor-start": { - []string{"bar"}, - "foobar", - "", - }, - - "anchor-end": { - []string{"bar"}, - "barfoo", - "", - }, - - "anchor-ambiguous": { - []string{"mounts", "sys/mounts"}, - "sys/mounts", - "^sys/mounts$", - }, - } - - for n, tc := range cases { - paths := make([]*Path, len(tc.Patterns)) - for i, pattern := range tc.Patterns { - paths[i] = &Path{Pattern: pattern} - } - - b := &Backend{Paths: paths} - result := b.Route(tc.Path) - match := "" - if result != nil { - match = result.Pattern - } - - if match != tc.Match { - t.Fatalf("bad: %s\n\nExpected: %s\nGot: %s", - n, tc.Match, match) - } - } -} - -func TestBackendSecret(t *testing.T) { - cases := map[string]struct { - Secrets []*Secret - Search string - Match bool - }{ - "no match": { - []*Secret{{Type: "foo"}}, - "bar", - false, - }, - - "match": { - []*Secret{{Type: "foo"}}, - "foo", - true, - }, - } - - for n, tc := range cases { - b := &Backend{Secrets: tc.Secrets} - result := b.Secret(tc.Search) - if tc.Match != (result != nil) { - t.Fatalf("bad: %s\n\nExpected match: %v", n, tc.Match) - } - if result != nil && result.Type != tc.Search { - t.Fatalf("bad: %s\n\nExpected matching type: %#v", n, result) - } - } -} - -func TestFieldSchemaDefaultOrZero(t *testing.T) { - cases := map[string]struct { - Schema *FieldSchema - Value interface{} - }{ - "default set": { - &FieldSchema{Type: TypeString, Default: "foo"}, - "foo", - }, - - "default not set": { - &FieldSchema{Type: TypeString}, - "", - }, - - "default duration set": { - &FieldSchema{Type: TypeDurationSecond, Default: 60}, - 60, - }, - - "default duration int64": { - &FieldSchema{Type: TypeDurationSecond, Default: int64(60)}, - 60, - }, - - "default duration string": { - &FieldSchema{Type: TypeDurationSecond, Default: "60s"}, - 60, - }, - - "illegal default duration string": { - &FieldSchema{Type: TypeDurationSecond, Default: "h1"}, - 0, - }, - - "default duration time.Duration": { - &FieldSchema{Type: TypeDurationSecond, Default: 60 * time.Second}, - 60, - }, - - "default duration not set": { - &FieldSchema{Type: TypeDurationSecond}, - 0, - }, - - "default signed positive duration set": { - &FieldSchema{Type: TypeSignedDurationSecond, Default: 60}, - 60, - }, - - "default signed positive duration int64": { - &FieldSchema{Type: TypeSignedDurationSecond, Default: int64(60)}, - 60, - }, - - "default signed positive duration string": { - &FieldSchema{Type: TypeSignedDurationSecond, Default: "60s"}, - 60, - }, - - "illegal default signed duration string": { - &FieldSchema{Type: TypeDurationSecond, Default: "-h1"}, - 0, - }, - - "default signed positive duration time.Duration": { - &FieldSchema{Type: TypeSignedDurationSecond, Default: 60 * time.Second}, - 60, - }, - - "default signed negative duration set": { - &FieldSchema{Type: TypeSignedDurationSecond, Default: -60}, - -60, - }, - - "default signed negative duration int64": { - &FieldSchema{Type: TypeSignedDurationSecond, Default: int64(-60)}, - -60, - }, - - "default signed negative duration string": { - &FieldSchema{Type: TypeSignedDurationSecond, Default: "-60s"}, - -60, - }, - - "default signed negative duration time.Duration": { - &FieldSchema{Type: TypeSignedDurationSecond, Default: -60 * time.Second}, - -60, - }, - - "default signed negative duration not set": { - &FieldSchema{Type: TypeSignedDurationSecond}, - 0, - }, - "default header not set": { - &FieldSchema{Type: TypeHeader}, - http.Header{}, - }, - } - - for name, tc := range cases { - actual := tc.Schema.DefaultOrZero() - if !reflect.DeepEqual(actual, tc.Value) { - t.Errorf("bad: %s\n\nExpected: %#v\nGot: %#v", - name, tc.Value, actual) - } - } -} - -func TestInitializeBackend(t *testing.T) { - var inited bool - backend := &Backend{InitializeFunc: func(context.Context, *logical.InitializationRequest) error { - inited = true - return nil - }} - - backend.Initialize(nil, &logical.InitializationRequest{Storage: nil}) - - if !inited { - t.Fatal("backend should be open") - } -} - -// TestFieldTypeMethods tries to ensure our switch-case statements for the -// FieldType "enum" are complete. -func TestFieldTypeMethods(t *testing.T) { - unknownFormat := convertType(TypeInvalid).format - - for i := TypeInvalid + 1; i < typeInvalidMax; i++ { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - if i.String() == TypeInvalid.String() { - t.Errorf("unknown type string for %d", i) - } - - if convertType(i).format == unknownFormat { - t.Errorf("unknown schema for %d", i) - } - - _ = i.Zero() - }) - } -} diff --git a/sdk/framework/field_data_test.go b/sdk/framework/field_data_test.go deleted file mode 100644 index 078c6fbcd..000000000 --- a/sdk/framework/field_data_test.go +++ /dev/null @@ -1,1277 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "encoding/json" - "net/http" - "reflect" - "testing" - "time" -) - -func TestFieldDataGet(t *testing.T) { - cases := map[string]struct { - Schema map[string]*FieldSchema - Raw map[string]interface{} - Key string - Value interface{} - ExpectError bool - }{ - "string type, string value": { - map[string]*FieldSchema{ - "foo": {Type: TypeString}, - }, - map[string]interface{}{ - "foo": "bar", - }, - "foo", - "bar", - false, - }, - - "string type, int value": { - map[string]*FieldSchema{ - "foo": {Type: TypeString}, - }, - map[string]interface{}{ - "foo": 42, - }, - "foo", - "42", - false, - }, - - "string type, unset value": { - map[string]*FieldSchema{ - "foo": {Type: TypeString}, - }, - map[string]interface{}{}, - "foo", - "", - false, - }, - - "string type, unset value with default": { - map[string]*FieldSchema{ - "foo": { - Type: TypeString, - Default: "bar", - }, - }, - map[string]interface{}{}, - "foo", - "bar", - false, - }, - - "lowercase string type, lowercase string value": { - map[string]*FieldSchema{ - "foo": {Type: TypeLowerCaseString}, - }, - map[string]interface{}{ - "foo": "bar", - }, - "foo", - "bar", - false, - }, - - "lowercase string type, mixed-case string value": { - map[string]*FieldSchema{ - "foo": {Type: TypeLowerCaseString}, - }, - map[string]interface{}{ - "foo": "BaR", - }, - "foo", - "bar", - false, - }, - - "lowercase string type, int value": { - map[string]*FieldSchema{ - "foo": {Type: TypeLowerCaseString}, - }, - map[string]interface{}{ - "foo": 42, - }, - "foo", - "42", - false, - }, - - "lowercase string type, unset value": { - map[string]*FieldSchema{ - "foo": {Type: TypeLowerCaseString}, - }, - map[string]interface{}{}, - "foo", - "", - false, - }, - - "lowercase string type, unset value with lowercase default": { - map[string]*FieldSchema{ - "foo": { - Type: TypeLowerCaseString, - Default: "bar", - }, - }, - map[string]interface{}{}, - "foo", - "bar", - false, - }, - - "int type, int value": { - map[string]*FieldSchema{ - "foo": {Type: TypeInt}, - }, - map[string]interface{}{ - "foo": 42, - }, - "foo", - 42, - false, - }, - - "bool type, bool value": { - map[string]*FieldSchema{ - "foo": {Type: TypeBool}, - }, - map[string]interface{}{ - "foo": false, - }, - "foo", - false, - false, - }, - - "map type, map value": { - map[string]*FieldSchema{ - "foo": {Type: TypeMap}, - }, - map[string]interface{}{ - "foo": map[string]interface{}{ - "child": true, - }, - }, - "foo", - map[string]interface{}{ - "child": true, - }, - false, - }, - - "duration type, string value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": "42", - }, - "foo", - 42, - false, - }, - - "duration type, string duration value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": "42m", - }, - "foo", - 2520, - false, - }, - - "duration type, int value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": 42, - }, - "foo", - 42, - false, - }, - - "duration type, float value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": 42.0, - }, - "foo", - 42, - false, - }, - - "duration type, nil value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": nil, - }, - "foo", - 0, - false, - }, - - "duration type, 0 value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": 0, - }, - "foo", - 0, - false, - }, - - "signed duration type, positive string value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": "42", - }, - "foo", - 42, - false, - }, - - "signed duration type, positive string duration value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": "42m", - }, - "foo", - 2520, - false, - }, - - "signed duration type, positive int value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": 42, - }, - "foo", - 42, - false, - }, - - "signed duration type, positive float value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": 42.0, - }, - "foo", - 42, - false, - }, - - "signed duration type, negative string value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": "-42", - }, - "foo", - -42, - false, - }, - - "signed duration type, negative string duration value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": "-42m", - }, - "foo", - -2520, - false, - }, - - "signed duration type, negative int value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": -42, - }, - "foo", - -42, - false, - }, - - "signed duration type, negative float value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": -42.0, - }, - "foo", - -42, - false, - }, - - "signed duration type, nil value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": nil, - }, - "foo", - 0, - false, - }, - - "signed duration type, 0 value": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{ - "foo": 0, - }, - "foo", - 0, - false, - }, - - "slice type, empty slice": { - map[string]*FieldSchema{ - "foo": {Type: TypeSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{}, - }, - "foo", - []interface{}{}, - false, - }, - - "slice type, filled, mixed slice": { - map[string]*FieldSchema{ - "foo": {Type: TypeSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{123, "abc"}, - }, - "foo", - []interface{}{123, "abc"}, - false, - }, - - "string slice type, filled slice": { - map[string]*FieldSchema{ - "foo": {Type: TypeStringSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{123, "abc"}, - }, - "foo", - []string{"123", "abc"}, - false, - }, - - "string slice type, single value": { - map[string]*FieldSchema{ - "foo": {Type: TypeStringSlice}, - }, - map[string]interface{}{ - "foo": "abc", - }, - "foo", - []string{"abc"}, - false, - }, - - "string slice type, empty string": { - map[string]*FieldSchema{ - "foo": {Type: TypeStringSlice}, - }, - map[string]interface{}{ - "foo": "", - }, - "foo", - []string{}, - false, - }, - - "comma string slice type, empty string": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaStringSlice}, - }, - map[string]interface{}{ - "foo": "", - }, - "foo", - []string{}, - false, - }, - - "comma string slice type, comma string with one value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaStringSlice}, - }, - map[string]interface{}{ - "foo": "value1", - }, - "foo", - []string{"value1"}, - false, - }, - - "comma string slice type, comma string with multi value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaStringSlice}, - }, - map[string]interface{}{ - "foo": "value1,value2,value3", - }, - "foo", - []string{"value1", "value2", "value3"}, - false, - }, - - "comma string slice type, nil string slice value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaStringSlice}, - }, - map[string]interface{}{ - "foo": "", - }, - "foo", - []string{}, - false, - }, - - "comma string slice type, string slice with one value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaStringSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{"value1"}, - }, - "foo", - []string{"value1"}, - false, - }, - - "comma string slice type, string slice with multi value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaStringSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{"value1", "value2", "value3"}, - }, - "foo", - []string{"value1", "value2", "value3"}, - false, - }, - - "comma string slice type, empty string slice value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaStringSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{}, - }, - "foo", - []string{}, - false, - }, - - "comma int slice type, comma int with one value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaIntSlice}, - }, - map[string]interface{}{ - "foo": 1, - }, - "foo", - []int{1}, - false, - }, - - "comma int slice type, comma int with multi value slice": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaIntSlice}, - }, - map[string]interface{}{ - "foo": []int{1, 2, 3}, - }, - "foo", - []int{1, 2, 3}, - false, - }, - - "comma int slice type, comma int with multi value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaIntSlice}, - }, - map[string]interface{}{ - "foo": "1,2,3", - }, - "foo", - []int{1, 2, 3}, - false, - }, - - "comma int slice type, nil int slice value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaIntSlice}, - }, - map[string]interface{}{ - "foo": "", - }, - "foo", - []int{}, - false, - }, - - "comma int slice type, int slice with one value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaIntSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{"1"}, - }, - "foo", - []int{1}, - false, - }, - - "comma int slice type, int slice with multi value strings": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaIntSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{"1", "2", "3"}, - }, - "foo", - []int{1, 2, 3}, - false, - }, - - "comma int slice type, int slice with multi value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaIntSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{1, 2, 3}, - }, - "foo", - []int{1, 2, 3}, - false, - }, - - "comma int slice type, empty int slice value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaIntSlice}, - }, - map[string]interface{}{ - "foo": []interface{}{}, - }, - "foo", - []int{}, - false, - }, - - "comma int slice type, json number": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaIntSlice}, - }, - map[string]interface{}{ - "foo": json.Number("1"), - }, - "foo", - []int{1}, - false, - }, - - "name string type, valid string": { - map[string]*FieldSchema{ - "foo": {Type: TypeNameString}, - }, - map[string]interface{}{ - "foo": "bar", - }, - "foo", - "bar", - false, - }, - - "name string type, valid value with special characters": { - map[string]*FieldSchema{ - "foo": {Type: TypeNameString}, - }, - map[string]interface{}{ - "foo": "bar.baz-bay123", - }, - "foo", - "bar.baz-bay123", - false, - }, - - "keypair type, valid value map type": { - map[string]*FieldSchema{ - "foo": {Type: TypeKVPairs}, - }, - map[string]interface{}{ - "foo": map[string]interface{}{ - "key1": "value1", - "key2": "value2", - "key3": 1, - }, - }, - "foo", - map[string]string{ - "key1": "value1", - "key2": "value2", - "key3": "1", - }, - false, - }, - - "keypair type, list of equal sign delim key pairs type": { - map[string]*FieldSchema{ - "foo": {Type: TypeKVPairs}, - }, - map[string]interface{}{ - "foo": []interface{}{"key1=value1", "key2=value2", "key3=1"}, - }, - "foo", - map[string]string{ - "key1": "value1", - "key2": "value2", - "key3": "1", - }, - false, - }, - - "keypair type, single equal sign delim value": { - map[string]*FieldSchema{ - "foo": {Type: TypeKVPairs}, - }, - map[string]interface{}{ - "foo": "key1=value1", - }, - "foo", - map[string]string{ - "key1": "value1", - }, - false, - }, - - "type header, keypair string array": { - map[string]*FieldSchema{ - "foo": {Type: TypeHeader}, - }, - map[string]interface{}{ - "foo": []interface{}{"key1:value1", "key2:value2", "key3:1"}, - }, - "foo", - http.Header{ - "Key1": []string{"value1"}, - "Key2": []string{"value2"}, - "Key3": []string{"1"}, - }, - false, - }, - - "type header, b64 string": { - map[string]*FieldSchema{ - "foo": {Type: TypeHeader}, - }, - map[string]interface{}{ - "foo": "eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdLCAiRm9vIjogNDJ9", - }, - "foo", - http.Header{ - "Content-Length": []string{"43"}, - "User-Agent": []string{"aws-sdk-go/1.4.12 (go1.7.1; linux; amd64)"}, - "X-Vault-Awsiam-Server-Id": []string{"vault.example.com"}, - "X-Amz-Date": []string{"20160930T043121Z"}, - "Content-Type": []string{"application/x-www-form-urlencoded; charset=utf-8"}, - "Authorization": []string{"AWS4-HMAC-SHA256 Credential=foo/20160930/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-server, Signature=a69fd750a3445c4e553e1b3e79d3da90eef54047f1eb4efe8ffbc9c428c2655b"}, - "Foo": []string{"42"}, - }, - false, - }, - - "type header, json string": { - map[string]*FieldSchema{ - "foo": {Type: TypeHeader}, - }, - map[string]interface{}{ - "foo": `{"hello":"world","bonjour":["monde","dieu"], "Guten Tag": 42, "你好": ["10", 20, 3.14]}`, - }, - "foo", - http.Header{ - "Hello": []string{"world"}, - "Bonjour": []string{"monde", "dieu"}, - "Guten Tag": []string{"42"}, - "你好": []string{"10", "20", "3.14"}, - }, - false, - }, - - "type header, keypair string array with dupe key": { - map[string]*FieldSchema{ - "foo": {Type: TypeHeader}, - }, - map[string]interface{}{ - "foo": []interface{}{"key1:value1", "key2:value2", "key3:1", "key3:true"}, - }, - "foo", - http.Header{ - "Key1": []string{"value1"}, - "Key2": []string{"value2"}, - "Key3": []string{"1", "true"}, - }, - false, - }, - - "type header, map string slice": { - map[string]*FieldSchema{ - "foo": {Type: TypeHeader}, - }, - map[string]interface{}{ - "foo": map[string][]string{ - "key1": {"value1"}, - "key2": {"value2"}, - "key3": {"1"}, - }, - }, - "foo", - http.Header{ - "Key1": []string{"value1"}, - "Key2": []string{"value2"}, - "Key3": []string{"1"}, - }, - false, - }, - - "name string type, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeNameString}, - }, - map[string]interface{}{}, - "foo", - "", - false, - }, - - "string type, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeString}, - }, - map[string]interface{}{}, - "foo", - "", - false, - }, - - "type int, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeInt}, - }, - map[string]interface{}{}, - "foo", - 0, - false, - }, - - "type bool, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeBool}, - }, - map[string]interface{}{}, - "foo", - false, - false, - }, - - "type map, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeMap}, - }, - map[string]interface{}{}, - "foo", - map[string]interface{}{}, - false, - }, - - "type duration second, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{}, - "foo", - 0, - false, - }, - - "type signed duration second, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeSignedDurationSecond}, - }, - map[string]interface{}{}, - "foo", - 0, - false, - }, - - "type slice, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeSlice}, - }, - map[string]interface{}{}, - "foo", - []interface{}{}, - false, - }, - - "type string slice, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeStringSlice}, - }, - map[string]interface{}{}, - "foo", - []string{}, - false, - }, - - "type comma string slice, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaStringSlice}, - }, - map[string]interface{}{}, - "foo", - []string{}, - false, - }, - - "comma string slice type, single JSON number value": { - map[string]*FieldSchema{ - "foo": {Type: TypeCommaStringSlice}, - }, - map[string]interface{}{ - "foo": json.Number("123"), - }, - "foo", - []string{"123"}, - false, - }, - - "type kv pair, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeKVPairs}, - }, - map[string]interface{}{}, - "foo", - map[string]string{}, - false, - }, - - "type header, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeHeader}, - }, - map[string]interface{}{}, - "foo", - http.Header{}, - false, - }, - - "float type, positive with decimals, as string": { - map[string]*FieldSchema{ - "foo": {Type: TypeFloat}, - }, - map[string]interface{}{ - "foo": "1234567.891234567", - }, - "foo", - 1234567.891234567, - false, - }, - - "float type, negative with decimals, as string": { - map[string]*FieldSchema{ - "foo": {Type: TypeFloat}, - }, - map[string]interface{}{ - "foo": "-1234567.891234567", - }, - "foo", - -1234567.891234567, - false, - }, - - "float type, positive without decimals": { - map[string]*FieldSchema{ - "foo": {Type: TypeFloat}, - }, - map[string]interface{}{ - "foo": 1234567, - }, - "foo", - 1234567.0, - false, - }, - - "type float, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeFloat}, - }, - map[string]interface{}{}, - "foo", - 0.0, - false, - }, - - "type float, invalid value": { - map[string]*FieldSchema{ - "foo": {Type: TypeFloat}, - }, - map[string]interface{}{ - "foo": "invalid0.0", - }, - "foo", - 0.0, - true, - }, - - "type time, not supplied": { - map[string]*FieldSchema{ - "foo": {Type: TypeTime}, - }, - map[string]interface{}{}, - "foo", - time.Time{}, - false, - }, - "type time, string value": { - map[string]*FieldSchema{ - "foo": {Type: TypeTime}, - }, - map[string]interface{}{ - "foo": "2021-12-11T09:08:07Z", - }, - "foo", - // Comparison uses DeepEqual() so better match exactly, - // can't have a different location. - time.Date(2021, 12, 11, 9, 8, 7, 0, time.UTC), - false, - }, - "type time, invalid value": { - map[string]*FieldSchema{ - "foo": {Type: TypeTime}, - }, - map[string]interface{}{ - "foo": "2021-13-11T09:08:07+02:00", - }, - "foo", - time.Time{}, - true, - }, - } - - for name, tc := range cases { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - data := &FieldData{ - Raw: tc.Raw, - Schema: tc.Schema, - } - - err := data.Validate() - switch { - case tc.ExpectError && err == nil: - t.Fatalf("expected error") - case tc.ExpectError && err != nil: - return - case !tc.ExpectError && err != nil: - t.Fatal(err) - default: - // Continue if !tc.ExpectError && err == nil - } - - actual := data.Get(tc.Key) - if !reflect.DeepEqual(actual, tc.Value) { - t.Fatalf("Expected: %#v\nGot: %#v", tc.Value, actual) - } - }) - } -} - -func TestFieldDataGet_Error(t *testing.T) { - cases := map[string]struct { - Schema map[string]*FieldSchema - Raw map[string]interface{} - Key string - }{ - "name string type, invalid value with invalid characters": { - map[string]*FieldSchema{ - "foo": {Type: TypeNameString}, - }, - map[string]interface{}{ - "foo": "bar baz", - }, - "foo", - }, - "name string type, invalid value with special characters at beginning": { - map[string]*FieldSchema{ - "foo": {Type: TypeNameString}, - }, - map[string]interface{}{ - "foo": ".barbaz", - }, - "foo", - }, - "name string type, invalid value with special characters at end": { - map[string]*FieldSchema{ - "foo": {Type: TypeNameString}, - }, - map[string]interface{}{ - "foo": "barbaz-", - }, - "foo", - }, - "name string type, empty string": { - map[string]*FieldSchema{ - "foo": {Type: TypeNameString}, - }, - map[string]interface{}{ - "foo": "", - }, - "foo", - }, - "keypair type, csv version empty key name": { - map[string]*FieldSchema{ - "foo": {Type: TypeKVPairs}, - }, - map[string]interface{}{ - "foo": []interface{}{"=value1", "key2=value2", "key3=1"}, - }, - "foo", - }, - "duration type, negative string value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": "-42", - }, - "foo", - }, - "duration type, negative string duration value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": "-42m", - }, - "foo", - }, - "duration type, negative int value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": -42, - }, - "foo", - }, - "duration type, negative float value": { - map[string]*FieldSchema{ - "foo": {Type: TypeDurationSecond}, - }, - map[string]interface{}{ - "foo": -42.0, - }, - "foo", - }, - } - - for name, tc := range cases { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - data := &FieldData{ - Raw: tc.Raw, - Schema: tc.Schema, - } - - got, _, err := data.GetOkErr(tc.Key) - if err == nil { - t.Fatalf("error expected, none received, got result: %#v", got) - } - }) - } -} - -func TestFieldDataGetFirst(t *testing.T) { - data := &FieldData{ - Raw: map[string]interface{}{ - "foo": "bar", - "fizz": "buzz", - }, - Schema: map[string]*FieldSchema{ - "foo": {Type: TypeNameString}, - "fizz": {Type: TypeNameString}, - }, - } - - result, ok := data.GetFirst("foo", "fizz") - if !ok { - t.Fatal("should have found value for foo") - } - if result.(string) != "bar" { - t.Fatal("should have gotten bar for foo") - } - - result, ok = data.GetFirst("fizz", "foo") - if !ok { - t.Fatal("should have found value for fizz") - } - if result.(string) != "buzz" { - t.Fatal("should have gotten buzz for fizz") - } - - _, ok = data.GetFirst("cats") - if ok { - t.Fatal("shouldn't have gotten anything for cats") - } -} - -func TestValidateStrict(t *testing.T) { - cases := map[string]struct { - Schema map[string]*FieldSchema - Raw map[string]interface{} - ExpectError bool - }{ - "string type, string value": { - map[string]*FieldSchema{ - "foo": {Type: TypeString}, - }, - map[string]interface{}{ - "foo": "bar", - }, - false, - }, - - "string type, int value": { - map[string]*FieldSchema{ - "foo": {Type: TypeString}, - }, - map[string]interface{}{ - "foo": 42, - }, - false, - }, - - "string type, unset value": { - map[string]*FieldSchema{ - "foo": {Type: TypeString}, - }, - map[string]interface{}{}, - false, - }, - - "string type, unset required value": { - map[string]*FieldSchema{ - "foo": { - Type: TypeString, - Required: true, - }, - }, - map[string]interface{}{}, - true, - }, - - "value not in schema": { - map[string]*FieldSchema{ - "foo": { - Type: TypeString, - Required: true, - }, - }, - map[string]interface{}{ - "foo": 42, - "bar": 43, - }, - true, - }, - - "value not in schema, empty schema": { - map[string]*FieldSchema{}, - map[string]interface{}{ - "foo": 42, - "bar": 43, - }, - true, - }, - - "value not in schema, nil schema": { - nil, - map[string]interface{}{ - "foo": 42, - "bar": 43, - }, - false, - }, - - "type time, invalid value": { - map[string]*FieldSchema{ - "foo": {Type: TypeTime}, - }, - map[string]interface{}{ - "foo": "2021-13-11T09:08:07+02:00", - }, - true, - }, - } - - for name, tc := range cases { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - data := &FieldData{ - Raw: tc.Raw, - Schema: tc.Schema, - } - - err := data.ValidateStrict() - - if err == nil && tc.ExpectError == true { - t.Fatalf("expected an error, got nil") - } - if err != nil && tc.ExpectError == false { - t.Fatalf("unexpected error: %v", err) - } - }) - } -} diff --git a/sdk/framework/identity_test.go b/sdk/framework/identity_test.go deleted file mode 100644 index 1667fb960..000000000 --- a/sdk/framework/identity_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestIdentityTemplating(t *testing.T) { - sysView := &logical.StaticSystemView{ - EntityVal: &logical.Entity{ - ID: "test-id", - Name: "test", - Aliases: []*logical.Alias{ - { - ID: "alias-id", - Name: "test alias", - MountAccessor: "test_mount", - MountType: "secret", - Metadata: map[string]string{ - "alias-metadata": "alias-metadata-value", - }, - }, - }, - Metadata: map[string]string{ - "entity-metadata": "entity-metadata-value", - }, - }, - GroupsVal: []*logical.Group{ - { - ID: "group1-id", - Name: "group1", - Metadata: map[string]string{ - "group-metadata": "group-metadata-value", - }, - }, - }, - } - - tCases := []struct { - tpl string - expected string - }{ - { - tpl: "{{identity.entity.id}}", - expected: "test-id", - }, - { - tpl: "{{identity.entity.name}}", - expected: "test", - }, - { - tpl: "{{identity.entity.metadata.entity-metadata}}", - expected: "entity-metadata-value", - }, - { - tpl: "{{identity.entity.aliases.test_mount.id}}", - expected: "alias-id", - }, - { - tpl: "{{identity.entity.aliases.test_mount.id}}", - expected: "alias-id", - }, - { - tpl: "{{identity.entity.aliases.test_mount.name}}", - expected: "test alias", - }, - { - tpl: "{{identity.entity.aliases.test_mount.metadata.alias-metadata}}", - expected: "alias-metadata-value", - }, - { - tpl: "{{identity.groups.ids.group1-id.name}}", - expected: "group1", - }, - { - tpl: "{{identity.groups.names.group1.id}}", - expected: "group1-id", - }, - { - tpl: "{{identity.groups.names.group1.metadata.group-metadata}}", - expected: "group-metadata-value", - }, - { - tpl: "{{identity.groups.ids.group1-id.metadata.group-metadata}}", - expected: "group-metadata-value", - }, - } - - for _, tCase := range tCases { - out, err := PopulateIdentityTemplate(tCase.tpl, "test", sysView) - if err != nil { - t.Fatal(err) - } - - if out != tCase.expected { - t.Fatalf("got %q, expected %q", out, tCase.expected) - } - } -} diff --git a/sdk/framework/lease_test.go b/sdk/framework/lease_test.go deleted file mode 100644 index 5d1f9f091..000000000 --- a/sdk/framework/lease_test.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "testing" - "time" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestCalculateTTL(t *testing.T) { - testSysView := logical.StaticSystemView{ - DefaultLeaseTTLVal: 5 * time.Hour, - MaxLeaseTTLVal: 30 * time.Hour, - } - - cases := map[string]struct { - Increment time.Duration - BackendDefault time.Duration - BackendMax time.Duration - Period time.Duration - ExplicitMaxTTL time.Duration - Result time.Duration - Warnings int - Error bool - }{ - "valid request, good bounds, increment is preferred": { - BackendDefault: 30 * time.Hour, - Increment: 1 * time.Hour, - Result: 1 * time.Hour, - }, - - "valid request, zero backend default, uses increment": { - BackendDefault: 0, - Increment: 1 * time.Hour, - Result: 1 * time.Hour, - }, - - "lease increment is zero, uses backend default": { - BackendDefault: 30 * time.Hour, - Increment: 0, - Result: 30 * time.Hour, - }, - - "lease increment and default are zero, uses systemview": { - BackendDefault: 0, - Increment: 0, - Result: 5 * time.Hour, - }, - - "backend max and associated request are too long": { - BackendDefault: 40 * time.Hour, - BackendMax: 45 * time.Hour, - Result: 30 * time.Hour, - Warnings: 1, - }, - - "all request values are larger than the system view, so the system view limits": { - BackendDefault: 40 * time.Hour, - BackendMax: 50 * time.Hour, - Increment: 40 * time.Hour, - Result: 30 * time.Hour, - Warnings: 1, - }, - - "request within backend max": { - BackendDefault: 9 * time.Hour, - BackendMax: 5 * time.Hour, - Increment: 4 * time.Hour, - Result: 4 * time.Hour, - }, - - "request outside backend max": { - BackendDefault: 9 * time.Hour, - BackendMax: 4 * time.Hour, - Increment: 5 * time.Hour, - Result: 4 * time.Hour, - Warnings: 1, - }, - - "request is negative, no backend default, use sysview": { - Increment: -7 * time.Hour, - Result: 5 * time.Hour, - }, - - "lease increment too large": { - Increment: 40 * time.Hour, - Result: 30 * time.Hour, - Warnings: 1, - }, - - "periodic, good request, period is preferred": { - Increment: 3 * time.Hour, - BackendDefault: 4 * time.Hour, - BackendMax: 2 * time.Hour, - Period: 1 * time.Hour, - Result: 1 * time.Hour, - }, - - "period too large, explicit max ttl is preferred": { - Period: 2 * time.Hour, - ExplicitMaxTTL: 1 * time.Hour, - Result: 1 * time.Hour, - Warnings: 1, - }, - - "period too large, capped by backend max": { - Period: 2 * time.Hour, - BackendMax: 1 * time.Hour, - Result: 1 * time.Hour, - Warnings: 1, - }, - } - - for name, tc := range cases { - ttl, warnings, err := CalculateTTL(testSysView, tc.Increment, tc.BackendDefault, tc.Period, tc.BackendMax, tc.ExplicitMaxTTL, time.Time{}) - if (err != nil) != tc.Error { - t.Fatalf("bad: %s\nerr: %s", name, err) - } - if tc.Error { - continue - } - - // Round it to the nearest hour - now := time.Now().Round(time.Hour) - lease := now.Add(ttl).Round(time.Hour).Sub(now) - if lease != tc.Result { - t.Fatalf("bad: %s\nlease: %s", name, lease) - } - - if tc.Warnings != len(warnings) { - t.Fatalf("bad: %s\nwarning count mismatch, expect %d, got %d: %#v", name, tc.Warnings, len(warnings), warnings) - } - } -} diff --git a/sdk/framework/openapi_test.go b/sdk/framework/openapi_test.go deleted file mode 100644 index 9e2763f12..000000000 --- a/sdk/framework/openapi_test.go +++ /dev/null @@ -1,925 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "path/filepath" - "reflect" - "regexp" - "sort" - "strings" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/helper/wrapping" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestOpenAPI_Regex(t *testing.T) { - t.Run("Path fields", func(t *testing.T) { - input := `/foo/bar/{inner}/baz/{outer}` - - matches := pathFieldsRe.FindAllStringSubmatch(input, -1) - - exp1 := "inner" - exp2 := "outer" - if matches[0][1] != exp1 || matches[1][1] != exp2 { - t.Fatalf("Capture error. Expected %s and %s, got %v", exp1, exp2, matches) - } - - input = `/foo/bar/inner/baz/outer` - matches = pathFieldsRe.FindAllStringSubmatch(input, -1) - - if matches != nil { - t.Fatalf("Expected nil match (%s), got %+v", input, matches) - } - }) - t.Run("Filtering", func(t *testing.T) { - tests := []struct { - input string - regex *regexp.Regexp - output string - }{ - { - input: `abcde`, - regex: wsRe, - output: "abcde", - }, - { - input: ` a b cd e `, - regex: wsRe, - output: "abcde", - }, - } - - for _, test := range tests { - result := test.regex.ReplaceAllString(test.input, "") - if result != test.output { - t.Fatalf("Clean Regex error (%s). Expected %s, got %s", test.input, test.output, result) - } - } - }) -} - -func TestOpenAPI_ExpandPattern(t *testing.T) { - tests := []struct { - inPattern string - outPathlets []string - }{ - // A simple string without regexp metacharacters passes through as is - {"rekey/backup", []string{"rekey/backup"}}, - // A trailing regexp anchor metacharacter is removed - {"rekey/backup$", []string{"rekey/backup"}}, - // As is a leading one - {"^rekey/backup", []string{"rekey/backup"}}, - // Named capture groups become OpenAPI parameters - {"auth/(?P.+?)/tune$", []string{"auth/{path}/tune"}}, - {"auth/(?P.+?)/tune/(?P.*?)$", []string{"auth/{path}/tune/{more}"}}, - // Even if the capture group contains very complex regexp structure inside it - {"something/(?P(a|b(c|d))|e+|f{1,3}[ghi-k]?.*)", []string{"something/{something}"}}, - // A question-mark results in a result without and with the optional path part - {"tools/hash(/(?P.+))?", []string{ - "tools/hash", - "tools/hash/{urlalgorithm}", - }}, - // Multiple question-marks evaluate each possible combination - {"(leases/)?renew(/(?P.+))?", []string{ - "leases/renew", - "leases/renew/{url_lease_id}", - "renew", - "renew/{url_lease_id}", - }}, - // GenericNameRegex is one particular way of writing a named capture group, so behaves the same - {`config/ui/headers/` + GenericNameRegex("header"), []string{"config/ui/headers/{header}"}}, - // The question-mark behaviour is still works when the question-mark is directly applied to a named capture group - {`leases/lookup/(?P.+?)?`, []string{ - "leases/lookup/", - "leases/lookup/{prefix}", - }}, - // Optional trailing slashes at the end of the path get stripped - even if appearing deep inside an alternation - {`(raw/?$|raw/(?P.+))`, []string{ - "raw", - "raw/{path}", - }}, - // OptionalParamRegex is also another way of writing a named capture group, that is optional - {"lookup" + OptionalParamRegex("urltoken"), []string{ - "lookup", - "lookup/{urltoken}", - }}, - // Optional trailign slashes at the end of the path get stripped in simpler cases too - {"roles/?$", []string{ - "roles", - }}, - {"roles/?", []string{ - "roles", - }}, - // Non-optional trailing slashes remain... although don't do this, it breaks HelpOperation! - // (Existing real examples of this pattern being fixed via https://github.com/hashicorp/vault/pull/18571) - {"accessors/$", []string{ - "accessors/", - }}, - // GenericNameRegex and OptionalParamRegex still work when concatenated - {"verify/" + GenericNameRegex("name") + OptionalParamRegex("urlalgorithm"), []string{ - "verify/{name}", - "verify/{name}/{urlalgorithm}", - }}, - // Named capture groups that specify enum-like parameters work as expected - {"^plugins/catalog/(?Pauth|database|secret)/(?P.+)$", []string{ - "plugins/catalog/{type}/{name}", - }}, - {"^plugins/catalog/(?Pauth|database|secret)/?$", []string{ - "plugins/catalog/{type}", - }}, - // Alternations between various literal path segments work - {"(pathOne|pathTwo)/", []string{"pathOne/", "pathTwo/"}}, - {"(pathOne|pathTwo)/" + GenericNameRegex("name"), []string{"pathOne/{name}", "pathTwo/{name}"}}, - { - "(pathOne|path-2|Path_3)/" + GenericNameRegex("name"), - []string{"Path_3/{name}", "path-2/{name}", "pathOne/{name}"}, - }, - // They still work when combined with GenericNameWithAtRegex - {"(creds|sts)/" + GenericNameWithAtRegex("name"), []string{ - "creds/{name}", - "sts/{name}", - }}, - // And when they're somewhere other than the start of the pattern - {"keys/generate/(internal|exported|kms)", []string{ - "keys/generate/exported", - "keys/generate/internal", - "keys/generate/kms", - }}, - // If a plugin author makes their list operation support both singular and plural forms, the OpenAPI notices - {"rolesets?/?", []string{"roleset", "rolesets"}}, - // Complex nested alternation and question-marks are correctly interpreted - {"crl(/pem|/delta(/pem)?)?", []string{"crl", "crl/delta", "crl/delta/pem", "crl/pem"}}, - } - - for i, test := range tests { - out, err := expandPattern(test.inPattern) - if err != nil { - t.Fatal(err) - } - sort.Strings(out) - if !reflect.DeepEqual(out, test.outPathlets) { - t.Fatalf("Test %d: Expected %v got %v", i, test.outPathlets, out) - } - } -} - -func TestOpenAPI_ExpandPattern_ReturnsError(t *testing.T) { - tests := []struct { - inPattern string - outError error - }{ - // None of these regexp constructs are allowed outside of named capture groups - {"[a-z]", errUnsupportableRegexpOperationForOpenAPI}, - {".", errUnsupportableRegexpOperationForOpenAPI}, - {"a+", errUnsupportableRegexpOperationForOpenAPI}, - {"a*", errUnsupportableRegexpOperationForOpenAPI}, - // So this pattern, which is a combination of two of the above isn't either - this pattern occurs in the KV - // secrets engine for its catch-all error handler, which provides a helpful hint to people treating a KV v2 as - // a KV v1. - {".*", errUnsupportableRegexpOperationForOpenAPI}, - } - - for i, test := range tests { - _, err := expandPattern(test.inPattern) - if err != test.outError { - t.Fatalf("Test %d: Expected %q got %q", i, test.outError, err) - } - } -} - -func TestOpenAPI_SplitFields(t *testing.T) { - fields := map[string]*FieldSchema{ - "a": {Description: "path"}, - "b": {Description: "body"}, - "c": {Description: "body"}, - "d": {Description: "body"}, - "e": {Description: "path"}, - } - - pathFields, bodyFields := splitFields(fields, "some/{a}/path/{e}") - - lp := len(pathFields) - lb := len(bodyFields) - l := len(fields) - if lp+lb != l { - t.Fatalf("split length error: %d + %d != %d", lp, lb, l) - } - - for name, field := range pathFields { - if field.Description != "path" { - t.Fatalf("expected field %s to be in 'path', found in %s", name, field.Description) - } - } - for name, field := range bodyFields { - if field.Description != "body" { - t.Fatalf("expected field %s to be in 'body', found in %s", name, field.Description) - } - } -} - -func TestOpenAPI_SpecialPaths(t *testing.T) { - tests := map[string]struct { - pattern string - rootPaths []string - rootExpected bool - unauthenticatedPaths []string - unauthenticatedExpected bool - }{ - "empty": { - pattern: "foo", - rootPaths: []string{}, - rootExpected: false, - unauthenticatedPaths: []string{}, - unauthenticatedExpected: false, - }, - "exact-match-unauthenticated": { - pattern: "foo", - rootPaths: []string{}, - rootExpected: false, - unauthenticatedPaths: []string{"foo"}, - unauthenticatedExpected: true, - }, - "exact-match-root": { - pattern: "foo", - rootPaths: []string{"foo"}, - rootExpected: true, - unauthenticatedPaths: []string{"bar"}, - unauthenticatedExpected: false, - }, - "asterisk-match-unauthenticated": { - pattern: "foo/bar", - rootPaths: []string{"foo"}, - rootExpected: false, - unauthenticatedPaths: []string{"foo/*"}, - unauthenticatedExpected: true, - }, - "asterisk-match-root": { - pattern: "foo/bar", - rootPaths: []string{"foo/*"}, - rootExpected: true, - unauthenticatedPaths: []string{"foo"}, - unauthenticatedExpected: false, - }, - "path-ends-with-slash": { - pattern: "foo/", - rootPaths: []string{"foo/*"}, - rootExpected: true, - unauthenticatedPaths: []string{"a", "b", "foo*"}, - unauthenticatedExpected: true, - }, - "asterisk-match-no-slash": { - pattern: "foo", - rootPaths: []string{"foo*"}, - rootExpected: true, - unauthenticatedPaths: []string{"a", "fo*"}, - unauthenticatedExpected: true, - }, - "multiple-root-paths": { - pattern: "foo/bar", - rootPaths: []string{"a", "b", "foo/*"}, - rootExpected: true, - unauthenticatedPaths: []string{"foo/baz/*"}, - unauthenticatedExpected: false, - }, - "plus-match-unauthenticated": { - pattern: "foo/bar/baz", - rootPaths: []string{"foo/bar"}, - rootExpected: false, - unauthenticatedPaths: []string{"foo/+/baz"}, - unauthenticatedExpected: true, - }, - "plus-match-root": { - pattern: "foo/bar/baz", - rootPaths: []string{"foo/+/baz"}, - rootExpected: true, - unauthenticatedPaths: []string{"foo/bar"}, - unauthenticatedExpected: false, - }, - "plus-and-asterisk": { - pattern: "foo/bar/baz/something", - rootPaths: []string{"foo/+/baz/*"}, - rootExpected: true, - unauthenticatedPaths: []string{"foo/+/baz*"}, - unauthenticatedExpected: true, - }, - "double-plus-good": { - pattern: "foo/bar/baz", - rootPaths: []string{"foo/+/+"}, - rootExpected: true, - unauthenticatedPaths: []string{"foo/bar"}, - unauthenticatedExpected: false, - }, - } - for name, test := range tests { - t.Run(name, func(t *testing.T) { - doc := NewOASDocument("version") - path := Path{ - Pattern: test.pattern, - } - specialPaths := &logical.Paths{ - Root: test.rootPaths, - Unauthenticated: test.unauthenticatedPaths, - } - - if err := documentPath(&path, specialPaths, "kv", logical.TypeLogical, doc); err != nil { - t.Fatal(err) - } - - actual := doc.Paths["/"+test.pattern].Sudo - if actual != test.rootExpected { - t.Fatalf("Test (root): expected: %v; got: %v", test.rootExpected, actual) - } - - actual = doc.Paths["/"+test.pattern].Unauthenticated - if actual != test.unauthenticatedExpected { - t.Fatalf("Test (unauth): expected: %v; got: %v", test.unauthenticatedExpected, actual) - } - }) - } -} - -func TestOpenAPI_Paths(t *testing.T) { - origDepth := deep.MaxDepth - defer func() { deep.MaxDepth = origDepth }() - deep.MaxDepth = 20 - - t.Run("Legacy callbacks", func(t *testing.T) { - p := &Path{ - Pattern: "lookup/" + GenericNameRegex("id"), - - Fields: map[string]*FieldSchema{ - "id": { - Type: TypeString, - Description: "My id parameter", - }, - "token": { - Type: TypeString, - Description: "My token", - }, - }, - - Callbacks: map[logical.Operation]OperationFunc{ - logical.ReadOperation: nil, - logical.UpdateOperation: nil, - }, - - HelpSynopsis: "Synopsis", - HelpDescription: "Description", - } - - sp := &logical.Paths{ - Root: []string{}, - Unauthenticated: []string{}, - } - testPath(t, p, sp, expected("legacy")) - }) - - t.Run("Operations - All Operations", func(t *testing.T) { - p := &Path{ - Pattern: "foo/" + GenericNameRegex("id"), - Fields: map[string]*FieldSchema{ - "id": { - Type: TypeString, - Description: "id path parameter", - }, - "flavors": { - Type: TypeCommaStringSlice, - Description: "the flavors", - }, - "name": { - Type: TypeNameString, - Default: "Larry", - Description: "the name", - }, - "age": { - Type: TypeInt, - Description: "the age", - AllowedValues: []interface{}{1, 2, 3}, - Required: true, - DisplayAttrs: &DisplayAttributes{ - Name: "Age", - Sensitive: true, - Group: "Some Group", - Value: 7, - }, - }, - "x-abc-token": { - Type: TypeHeader, - Description: "a header value", - AllowedValues: []interface{}{"a", "b", "c"}, - }, - "maximum": { - Type: TypeInt64, - Description: "a maximum value", - }, - "format": { - Type: TypeString, - Description: "a query param", - Query: true, - }, - }, - HelpSynopsis: "Synopsis", - HelpDescription: "Description", - Operations: map[logical.Operation]OperationHandler{ - logical.ReadOperation: &PathOperation{ - Summary: "My Summary", - Description: "My Description", - }, - logical.UpdateOperation: &PathOperation{ - Summary: "Update Summary", - Description: "Update Description", - }, - logical.CreateOperation: &PathOperation{ - Summary: "Create Summary", - Description: "Create Description", - }, - logical.ListOperation: &PathOperation{ - Summary: "List Summary", - Description: "List Description", - }, - logical.DeleteOperation: &PathOperation{ - Summary: "This shouldn't show up", - Unpublished: true, - }, - }, - DisplayAttrs: &DisplayAttributes{ - Navigation: true, - }, - } - - sp := &logical.Paths{ - Root: []string{"foo*"}, - } - testPath(t, p, sp, expected("operations")) - }) - - t.Run("Operations - List Only", func(t *testing.T) { - p := &Path{ - Pattern: "foo/" + GenericNameRegex("id"), - Fields: map[string]*FieldSchema{ - "id": { - Type: TypeString, - Description: "id path parameter", - }, - "flavors": { - Type: TypeCommaStringSlice, - Description: "the flavors", - }, - "name": { - Type: TypeNameString, - Default: "Larry", - Description: "the name", - }, - "age": { - Type: TypeInt, - Description: "the age", - AllowedValues: []interface{}{1, 2, 3}, - Required: true, - DisplayAttrs: &DisplayAttributes{ - Name: "Age", - Sensitive: true, - Group: "Some Group", - Value: 7, - }, - }, - "x-abc-token": { - Type: TypeHeader, - Description: "a header value", - AllowedValues: []interface{}{"a", "b", "c"}, - }, - "format": { - Type: TypeString, - Description: "a query param", - Query: true, - }, - }, - HelpSynopsis: "Synopsis", - HelpDescription: "Description", - Operations: map[logical.Operation]OperationHandler{ - logical.ListOperation: &PathOperation{ - Summary: "List Summary", - Description: "List Description", - }, - }, - DisplayAttrs: &DisplayAttributes{ - Navigation: true, - }, - } - - sp := &logical.Paths{ - Root: []string{"foo*"}, - } - testPath(t, p, sp, expected("operations_list")) - }) - - t.Run("Responses", func(t *testing.T) { - p := &Path{ - Pattern: "foo", - HelpSynopsis: "Synopsis", - HelpDescription: "Description", - Operations: map[logical.Operation]OperationHandler{ - logical.ReadOperation: &PathOperation{ - Summary: "My Summary", - Description: "My Description", - Responses: map[int][]Response{ - 202: {{ - Description: "Amazing", - Example: &logical.Response{ - Data: map[string]interface{}{ - "amount": 42, - }, - }, - Fields: map[string]*FieldSchema{ - "field_a": { - Type: TypeString, - Description: "field_a description", - }, - "field_b": { - Type: TypeBool, - Description: "field_b description", - }, - }, - }}, - }, - }, - logical.DeleteOperation: &PathOperation{ - Summary: "Delete stuff", - }, - }, - } - - sp := &logical.Paths{ - Unauthenticated: []string{"x", "y", "foo"}, - } - - testPath(t, p, sp, expected("responses")) - }) -} - -func TestOpenAPI_CustomDecoder(t *testing.T) { - p := &Path{ - Pattern: "foo", - HelpSynopsis: "Synopsis", - Operations: map[logical.Operation]OperationHandler{ - logical.ReadOperation: &PathOperation{ - Summary: "My Summary", - Responses: map[int][]Response{ - 100: {{ - Description: "OK", - Example: &logical.Response{ - Data: map[string]interface{}{ - "foo": 42, - }, - }, - }}, - 200: {{ - Description: "Good", - Example: (*logical.Response)(nil), - }}, - 599: {{ - Description: "Bad", - }}, - }, - }, - }, - } - - docOrig := NewOASDocument("version") - err := documentPath(p, nil, "kv", logical.TypeLogical, docOrig) - if err != nil { - t.Fatal(err) - } - - docJSON := mustJSONMarshal(t, docOrig) - - var intermediate map[string]interface{} - if err := jsonutil.DecodeJSON(docJSON, &intermediate); err != nil { - t.Fatal(err) - } - - docNew, err := NewOASDocumentFromMap(intermediate) - if err != nil { - t.Fatal(err) - } - - docNewJSON := mustJSONMarshal(t, docNew) - - if diff := deep.Equal(docJSON, docNewJSON); diff != nil { - t.Fatal(diff) - } -} - -func TestOpenAPI_CleanResponse(t *testing.T) { - // Verify that an all-null input results in empty JSON - orig := &logical.Response{} - - cr := cleanResponse(orig) - - newJSON := mustJSONMarshal(t, cr) - - if !bytes.Equal(newJSON, []byte("{}")) { - t.Fatalf("expected {}, got: %q", newJSON) - } - - // Verify that all non-null inputs results in JSON that matches the marshalling of - // logical.Response. This will fail if logical.Response changes without a corresponding - // change to cleanResponse() - orig = &logical.Response{ - Secret: new(logical.Secret), - Auth: new(logical.Auth), - Data: map[string]interface{}{"foo": 42}, - Redirect: "foo", - Warnings: []string{"foo"}, - WrapInfo: &wrapping.ResponseWrapInfo{Token: "foo"}, - Headers: map[string][]string{"foo": {"bar"}}, - } - origJSON := mustJSONMarshal(t, orig) - - cr = cleanResponse(orig) - - cleanJSON := mustJSONMarshal(t, cr) - - if diff := deep.Equal(origJSON, cleanJSON); diff != nil { - t.Fatal(diff) - } -} - -func TestOpenAPI_constructOperationID(t *testing.T) { - tests := map[string]struct { - path string - pathIndex int - pathAttributes *DisplayAttributes - operation logical.Operation - operationAttributes *DisplayAttributes - defaultPrefix string - expected string - }{ - "empty": { - path: "", - pathIndex: 0, - pathAttributes: nil, - operation: logical.Operation(""), - operationAttributes: nil, - defaultPrefix: "", - expected: "", - }, - "simple-read": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: nil, - operation: logical.ReadOperation, - operationAttributes: nil, - defaultPrefix: "test", - expected: "test-read-path-to-thing", - }, - "simple-write": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: nil, - operation: logical.UpdateOperation, - operationAttributes: nil, - defaultPrefix: "test", - expected: "test-write-path-to-thing", - }, - "operation-verb": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: &DisplayAttributes{OperationVerb: "do-something"}, - operation: logical.UpdateOperation, - operationAttributes: nil, - defaultPrefix: "test", - expected: "do-something", - }, - "operation-verb-override": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: &DisplayAttributes{OperationVerb: "do-something"}, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationVerb: "do-something-else"}, - defaultPrefix: "test", - expected: "do-something-else", - }, - "operation-prefix": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix"}, - operation: logical.UpdateOperation, - operationAttributes: nil, - defaultPrefix: "test", - expected: "my-prefix-write-path-to-thing", - }, - "operation-prefix-override": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix"}, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix"}, - defaultPrefix: "test", - expected: "better-prefix-write-path-to-thing", - }, - "operation-prefix-and-suffix": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix"}, - operation: logical.UpdateOperation, - operationAttributes: nil, - defaultPrefix: "test", - expected: "my-prefix-write-my-suffix", - }, - "operation-prefix-and-suffix-override": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix"}, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix"}, - defaultPrefix: "test", - expected: "better-prefix-write-better-suffix", - }, - "operation-prefix-verb-suffix": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix", OperationVerb: "Create"}, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix"}, - defaultPrefix: "test", - expected: "better-prefix-create-better-suffix", - }, - "operation-prefix-verb-suffix-override": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix", OperationVerb: "Create"}, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix", OperationVerb: "Login"}, - defaultPrefix: "test", - expected: "better-prefix-login-better-suffix", - }, - "operation-prefix-verb": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: nil, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationVerb: "Login"}, - defaultPrefix: "test", - expected: "better-prefix-login", - }, - "operation-verb-suffix": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: nil, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationVerb: "Login", OperationSuffix: "better-suffix"}, - defaultPrefix: "test", - expected: "login-better-suffix", - }, - "pipe-delimited-suffix-0": { - path: "path/to/thing", - pathIndex: 0, - pathAttributes: nil, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"}, - defaultPrefix: "test", - expected: "better-prefix-write-suffix0", - }, - "pipe-delimited-suffix-1": { - path: "path/to/thing", - pathIndex: 1, - pathAttributes: nil, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"}, - defaultPrefix: "test", - expected: "better-prefix-write-suffix1", - }, - "pipe-delimited-suffix-2-fallback": { - path: "path/to/thing", - pathIndex: 2, - pathAttributes: nil, - operation: logical.UpdateOperation, - operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"}, - defaultPrefix: "test", - expected: "better-prefix-write-path-to-thing", - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - t.Parallel() - actual := constructOperationID( - test.path, - test.pathIndex, - test.pathAttributes, - test.operation, - test.operationAttributes, - test.defaultPrefix, - ) - if actual != test.expected { - t.Fatalf("expected: %s; got: %s", test.expected, actual) - } - }) - } -} - -func TestOpenAPI_hyphenatedToTitleCase(t *testing.T) { - tests := map[string]struct { - in string - expected string - }{ - "simple": { - in: "test", - expected: "Test", - }, - "two-words": { - in: "two-words", - expected: "TwoWords", - }, - "three-words": { - in: "one-two-three", - expected: "OneTwoThree", - }, - "not-hyphenated": { - in: "something_like_this", - expected: "Something_like_this", - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - t.Parallel() - actual := hyphenatedToTitleCase(test.in) - if actual != test.expected { - t.Fatalf("expected: %s; got: %s", test.expected, actual) - } - }) - } -} - -func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string) { - t.Helper() - - doc := NewOASDocument("dummyversion") - if err := documentPath(path, sp, "kv", logical.TypeLogical, doc); err != nil { - t.Fatal(err) - } - doc.CreateOperationIDs("") - - docJSON, err := json.MarshalIndent(doc, "", " ") - if err != nil { - t.Fatal(err) - } - - // Compare json by first decoding, then comparing with a deep equality check. - var expected, actual interface{} - if err := jsonutil.DecodeJSON(docJSON, &actual); err != nil { - t.Fatal(err) - } - - if err := jsonutil.DecodeJSON([]byte(expectedJSON), &expected); err != nil { - t.Fatal(err) - } - - if diff := deep.Equal(actual, expected); diff != nil { - // fmt.Println(string(docJSON)) // uncomment to debug generated JSON (very helpful when fixing tests) - t.Fatal(diff) - } -} - -func getPathOp(pi *OASPathItem, op string) *OASOperation { - switch op { - case "get": - return pi.Get - case "post": - return pi.Post - case "delete": - return pi.Delete - default: - panic("unexpected operation: " + op) - } -} - -func expected(name string) string { - data, err := ioutil.ReadFile(filepath.Join("testdata", name+".json")) - if err != nil { - panic(err) - } - - content := strings.Replace(string(data), "", "dummyversion", 1) - - return content -} - -func mustJSONMarshal(t *testing.T, data interface{}) []byte { - j, err := json.MarshalIndent(data, "", " ") - if err != nil { - t.Fatal(err) - } - return j -} diff --git a/sdk/framework/path_map_test.go b/sdk/framework/path_map_test.go deleted file mode 100644 index 3fe6308cb..000000000 --- a/sdk/framework/path_map_test.go +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "context" - "testing" - - saltpkg "github.com/hashicorp/vault/sdk/helper/salt" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestPathMap(t *testing.T) { - p := &PathMap{Name: "foo"} - storage := new(logical.InmemStorage) - var b logical.Backend = &Backend{Paths: p.Paths()} - - ctx := context.Background() - - // Write via HTTP - _, err := b.HandleRequest(ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "map/foo/a", - Data: map[string]interface{}{ - "value": "bar", - }, - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - - // Read via HTTP - resp, err := b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "map/foo/a", - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if resp.Data["value"] != "bar" { - t.Fatalf("bad: %#v", resp) - } - - // Read via API - v, err := p.Get(ctx, storage, "a") - if err != nil { - t.Fatalf("bad: %#v", err) - } - if v["value"] != "bar" { - t.Fatalf("bad: %#v", v) - } - - // Read via API with other casing - v, err = p.Get(ctx, storage, "A") - if err != nil { - t.Fatalf("bad: %#v", err) - } - if v["value"] != "bar" { - t.Fatalf("bad: %#v", v) - } - - // Verify List - keys, err := p.List(ctx, storage, "") - if err != nil { - t.Fatalf("bad: %#v", err) - } - if len(keys) != 1 || keys[0] != "a" { - t.Fatalf("bad: %#v", keys) - } - - // LIST via HTTP - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ListOperation, - Path: "map/foo/", - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if len(resp.Data) != 1 || len(resp.Data["keys"].([]string)) != 1 || - resp.Data["keys"].([]string)[0] != "a" { - t.Fatalf("bad: %#v", resp) - } - - // Delete via HTTP - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.DeleteOperation, - Path: "map/foo/a", - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if resp != nil { - t.Fatalf("bad: %#v", resp) - } - - // Re-read via HTTP - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "map/foo/a", - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if _, ok := resp.Data["value"]; ok { - t.Fatalf("bad: %#v", resp) - } - - // Re-read via API - v, err = p.Get(ctx, storage, "a") - if err != nil { - t.Fatalf("bad: %#v", err) - } - if v != nil { - t.Fatalf("bad: %#v", v) - } -} - -func TestPathMap_getInvalid(t *testing.T) { - p := &PathMap{Name: "foo"} - storage := new(logical.InmemStorage) - - v, err := p.Get(context.Background(), storage, "nope") - if err != nil { - t.Fatalf("bad: %#v", err) - } - if v != nil { - t.Fatalf("bad: %#v", v) - } -} - -func TestPathMap_routes(t *testing.T) { - p := &PathMap{Name: "foo"} - TestBackendRoutes(t, &Backend{Paths: p.Paths()}, []string{ - "map/foo", // Normal - "map/foo/bar", // Normal - "map/foo/bar-baz", // Hyphen key - }) -} - -func TestPathMap_Salted(t *testing.T) { - storage := new(logical.InmemStorage) - - salt, err := saltpkg.NewSalt(context.Background(), storage, &saltpkg.Config{ - HashFunc: saltpkg.SHA1Hash, - }) - if err != nil { - t.Fatalf("err: %v", err) - } - - testSalting(t, context.Background(), storage, salt, &PathMap{Name: "foo", Salt: salt}) -} - -func testSalting(t *testing.T, ctx context.Context, storage logical.Storage, salt *saltpkg.Salt, p *PathMap) { - var b logical.Backend = &Backend{Paths: p.Paths()} - var err error - - // Write via HTTP - _, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "map/foo/a", - Data: map[string]interface{}{ - "value": "bar", - }, - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - - // Non-salted version should not be there - out, err := storage.Get(ctx, "struct/map/foo/a") - if err != nil { - t.Fatalf("err: %v", err) - } - if out != nil { - t.Fatalf("non-salted key found") - } - - // Ensure the path is salted - expect := "s" + salt.SaltIDHashFunc("a", saltpkg.SHA256Hash) - out, err = storage.Get(ctx, "struct/map/foo/"+expect) - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("missing salted key") - } - - // Read via HTTP - resp, err := b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "map/foo/a", - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if resp.Data["value"] != "bar" { - t.Fatalf("bad: %#v", resp) - } - - // Read via API - v, err := p.Get(ctx, storage, "a") - if err != nil { - t.Fatalf("bad: %#v", err) - } - if v["value"] != "bar" { - t.Fatalf("bad: %#v", v) - } - - // Read via API with other casing - v, err = p.Get(ctx, storage, "A") - if err != nil { - t.Fatalf("bad: %#v", err) - } - if v["value"] != "bar" { - t.Fatalf("bad: %#v", v) - } - - // Verify List - keys, err := p.List(ctx, storage, "") - if err != nil { - t.Fatalf("bad: %#v", err) - } - if len(keys) != 1 || keys[0] != expect { - t.Fatalf("bad: %#v", keys) - } - - // Delete via HTTP - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.DeleteOperation, - Path: "map/foo/a", - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if resp != nil { - t.Fatalf("bad: %#v", resp) - } - - // Re-read via HTTP - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "map/foo/a", - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if _, ok := resp.Data["value"]; ok { - t.Fatalf("bad: %#v", resp) - } - - // Re-read via API - v, err = p.Get(ctx, storage, "a") - if err != nil { - t.Fatalf("bad: %#v", err) - } - if v != nil { - t.Fatalf("bad: %#v", v) - } - - // Put in a non-salted version and make sure that after reading it's been - // upgraded - err = storage.Put(ctx, &logical.StorageEntry{ - Key: "struct/map/foo/b", - Value: []byte(`{"foo": "bar"}`), - }) - if err != nil { - t.Fatalf("err: %v", err) - } - // A read should transparently upgrade - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "map/foo/b", - Storage: storage, - }) - if err != nil { - t.Fatal(err) - } - list, _ := storage.List(ctx, "struct/map/foo/") - if len(list) != 1 { - t.Fatalf("unexpected number of entries left after upgrade; expected 1, got %d", len(list)) - } - found := false - for _, v := range list { - if v == "s"+salt.SaltIDHashFunc("b", saltpkg.SHA256Hash) { - found = true - break - } - } - if !found { - t.Fatal("did not find upgraded value") - } - - // Put in a SHA1 salted version and make sure that after reading its been - // upgraded - err = storage.Put(ctx, &logical.StorageEntry{ - Key: "struct/map/foo/" + salt.SaltID("b"), - Value: []byte(`{"foo": "bar"}`), - }) - if err != nil { - t.Fatal(err) - } - - // A read should transparently upgrade - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "map/foo/b", - Storage: storage, - }) - if err != nil { - t.Fatal(err) - } - list, _ = storage.List(ctx, "struct/map/foo/") - if len(list) != 1 { - t.Fatalf("unexpected number of entries left after upgrade; expected 1, got %d", len(list)) - } - found = false - for _, v := range list { - if v == "s"+salt.SaltIDHashFunc("b", saltpkg.SHA256Hash) { - found = true - break - } - } - if !found { - t.Fatal("did not find upgraded value") - } -} - -func TestPathMap_SaltFunc(t *testing.T) { - storage := new(logical.InmemStorage) - - salt, err := saltpkg.NewSalt(context.Background(), storage, &saltpkg.Config{ - HashFunc: saltpkg.SHA1Hash, - }) - if err != nil { - t.Fatalf("err: %v", err) - } - - saltFunc := func(context.Context) (*saltpkg.Salt, error) { - return salt, nil - } - - testSalting(t, context.Background(), storage, salt, &PathMap{Name: "foo", SaltFunc: saltFunc}) -} diff --git a/sdk/framework/path_struct_test.go b/sdk/framework/path_struct_test.go deleted file mode 100644 index 88662af53..000000000 --- a/sdk/framework/path_struct_test.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "context" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestPathStruct(t *testing.T) { - p := &PathStruct{ - Name: "foo", - Path: "bar", - Schema: map[string]*FieldSchema{ - "value": {Type: TypeString}, - }, - Read: true, - } - - storage := new(logical.InmemStorage) - var b logical.Backend = &Backend{Paths: p.Paths()} - - ctx := context.Background() - - // Write via HTTP - _, err := b.HandleRequest(ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "bar", - Data: map[string]interface{}{ - "value": "baz", - }, - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - - // Read via HTTP - resp, err := b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "bar", - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if resp.Data["value"] != "baz" { - t.Fatalf("bad: %#v", resp) - } - - // Read via API - v, err := p.Get(ctx, storage) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if v["value"] != "baz" { - t.Fatalf("bad: %#v", v) - } - - // Delete via HTTP - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.DeleteOperation, - Path: "bar", - Data: nil, - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if resp != nil { - t.Fatalf("bad: %#v", resp) - } - - // Re-read via HTTP - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "bar", - Storage: storage, - }) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if _, ok := resp.Data["value"]; ok { - t.Fatalf("bad: %#v", resp) - } - - // Re-read via API - v, err = p.Get(ctx, storage) - if err != nil { - t.Fatalf("bad: %#v", err) - } - if v != nil { - t.Fatalf("bad: %#v", v) - } -} diff --git a/sdk/framework/path_test.go b/sdk/framework/path_test.go deleted file mode 100644 index 4541930ed..000000000 --- a/sdk/framework/path_test.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "testing" - - "github.com/go-test/deep" -) - -func TestPath_Regex(t *testing.T) { - tests := []struct { - pattern string - input string - pathMatch bool - captures map[string]string - }{ - { - pattern: "a/b/" + GenericNameRegex("val"), - input: "a/b/foo", - pathMatch: true, - captures: map[string]string{"val": "foo"}, - }, - { - pattern: "a/b/" + GenericNameRegex("val"), - input: "a/b/foo/more", - pathMatch: false, - captures: nil, - }, - { - pattern: "a/b/" + GenericNameRegex("val"), - input: "a/b/abc-.123", - pathMatch: true, - captures: map[string]string{"val": "abc-.123"}, - }, - { - pattern: "a/b/" + GenericNameRegex("val") + "/c/d", - input: "a/b/foo/c/d", - pathMatch: true, - captures: map[string]string{"val": "foo"}, - }, - { - pattern: "a/b/" + GenericNameRegex("val") + "/c/d", - input: "a/b/foo/c/d/e", - pathMatch: false, - captures: nil, - }, - { - pattern: "a/b" + OptionalParamRegex("val"), - input: "a/b", - pathMatch: true, - captures: map[string]string{"val": ""}, - }, - { - pattern: "a/b" + OptionalParamRegex("val"), - input: "a/b/foo", - pathMatch: true, - captures: map[string]string{"val": "foo"}, - }, - { - pattern: "foo/" + MatchAllRegex("val"), - input: "foos/ball", - pathMatch: false, - captures: nil, - }, - { - pattern: "foos/" + MatchAllRegex("val"), - input: "foos/ball", - pathMatch: true, - captures: map[string]string{"val": "ball"}, - }, - { - pattern: "foos/ball/" + MatchAllRegex("val"), - input: "foos/ball/with/more/stuff/at_the/end", - pathMatch: true, - captures: map[string]string{"val": "with/more/stuff/at_the/end"}, - }, - { - pattern: MatchAllRegex("val"), - input: "foos/ball/with/more/stuff/at_the/end", - pathMatch: true, - captures: map[string]string{"val": "foos/ball/with/more/stuff/at_the/end"}, - }, - } - - for i, test := range tests { - b := Backend{ - Paths: []*Path{{Pattern: test.pattern}}, - } - path, captures := b.route(test.input) - pathMatch := path != nil - if pathMatch != test.pathMatch { - t.Fatalf("[%d] unexpected path match result (%s): expected %t, actual %t", i, test.pattern, test.pathMatch, pathMatch) - } - if diff := deep.Equal(captures, test.captures); diff != nil { - t.Fatal(diff) - } - } -} diff --git a/sdk/framework/policy_map_test.go b/sdk/framework/policy_map_test.go deleted file mode 100644 index b785fddc7..000000000 --- a/sdk/framework/policy_map_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "context" - "reflect" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestPolicyMap(t *testing.T) { - p := &PolicyMap{} - p.PathMap.Name = "foo" - s := new(logical.InmemStorage) - - ctx := context.Background() - - p.Put(ctx, s, "foo", map[string]interface{}{"value": "bar"}) - p.Put(ctx, s, "bar", map[string]interface{}{"value": "foo,baz "}) - - // Read via API - actual, err := p.Policies(ctx, s, "foo", "bar") - if err != nil { - t.Fatalf("bad: %#v", err) - } - - expected := []string{"bar", "baz", "foo"} - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} diff --git a/sdk/framework/secret_test.go b/sdk/framework/secret_test.go deleted file mode 100644 index 29058dc84..000000000 --- a/sdk/framework/secret_test.go +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework diff --git a/sdk/framework/testdata/legacy.json b/sdk/framework/testdata/legacy.json deleted file mode 100644 index 548151c6f..000000000 --- a/sdk/framework/testdata/legacy.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "openapi": "3.0.2", - "info": { - "title": "HashiCorp Vault API", - "description": "HTTP API that gives you full access to Vault. All API routes are prefixed with `/v1/`.", - "version": "", - "license": { - "name": "Mozilla Public License 2.0", - "url": "https://www.mozilla.org/en-US/MPL/2.0" - } - }, - "paths": { - "/lookup/{id}": { - "description": "Synopsis", - "parameters": [ - { - "name": "id", - "description": "My id parameter", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "get": { - "operationId": "kv-read-lookup-id", - "summary": "Synopsis", - "tags": [ - "secrets" - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "operationId": "kv-write-lookup-id", - "summary": "Synopsis", - "tags": [ - "secrets" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KvWriteLookupIdRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK" - } - } - } - } - }, - "components": { - "schemas": { - "KvWriteLookupIdRequest": { - "type": "object", - "properties": { - "token": { - "type": "string", - "description": "My token" - } - } - } - } - } -} diff --git a/sdk/framework/testdata/operations.json b/sdk/framework/testdata/operations.json deleted file mode 100644 index 421fd0c39..000000000 --- a/sdk/framework/testdata/operations.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "openapi": "3.0.2", - "info": { - "title": "HashiCorp Vault API", - "description": "HTTP API that gives you full access to Vault. All API routes are prefixed with `/v1/`.", - "version": "", - "license": { - "name": "Mozilla Public License 2.0", - "url": "https://www.mozilla.org/en-US/MPL/2.0" - } - }, - "paths": { - "/foo/{id}": { - "description": "Synopsis", - "parameters": [ - { - "name": "format", - "description": "a query param", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "id", - "description": "id path parameter", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "x-vault-sudo": true, - "x-vault-createSupported": true, - "x-vault-displayAttrs": { - "navigation": true - }, - "get": { - "summary": "My Summary", - "description": "My Description", - "operationId": "kv-read-foo-id", - "tags": [ - "secrets" - ], - "parameters": [ - { - "name": "list", - "description": "Return a list if `true`", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "summary": "Update Summary", - "description": "Update Description", - "operationId": "kv-write-foo-id", - "tags": [ - "secrets" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KvWriteFooIdRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK" - } - } - } - } - }, - "components": { - "schemas": { - "KvWriteFooIdRequest": { - "type": "object", - "properties": { - "age": { - "type": "integer", - "description": "the age", - "enum": [ - 1, - 2, - 3 - ], - "x-vault-displayAttrs": { - "name": "Age", - "value": 7, - "sensitive": true, - "group": "Some Group" - } - }, - "flavors": { - "type": "array", - "description": "the flavors", - "items": { - "type": "string" - } - }, - "maximum": { - "type": "integer", - "description": "a maximum value", - "format": "int64" - }, - "name": { - "type": "string", - "description": "the name", - "pattern": "\\w([\\w-.]*\\w)?", - "default": "Larry" - }, - "x-abc-token": { - "type": "string", - "description": "a header value", - "enum": [ - "a", - "b", - "c" - ] - } - }, - "required": [ - "age" - ] - } - } - } -} diff --git a/sdk/framework/testdata/operations_list.json b/sdk/framework/testdata/operations_list.json deleted file mode 100644 index bb16061e0..000000000 --- a/sdk/framework/testdata/operations_list.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "openapi": "3.0.2", - "info": { - "title": "HashiCorp Vault API", - "description": "HTTP API that gives you full access to Vault. All API routes are prefixed with `/v1/`.", - "version": "", - "license": { - "name": "Mozilla Public License 2.0", - "url": "https://www.mozilla.org/en-US/MPL/2.0" - } - }, - "paths": { - "/foo/{id}": { - "description": "Synopsis", - "parameters": [ - { - "name": "format", - "description": "a query param", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "id", - "description": "id path parameter", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "x-vault-sudo": true, - "x-vault-displayAttrs": { - "navigation": true - }, - "get": { - "summary": "List Summary", - "description": "List Description", - "operationId": "kv-list-foo-id", - "tags": [ - "secrets" - ], - "parameters": [ - { - "name": "list", - "description": "Must be set to `true`", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "true" - ] - }, - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - } - }, - "components": { - "schemas": {} - } -} diff --git a/sdk/framework/testdata/responses.json b/sdk/framework/testdata/responses.json deleted file mode 100644 index 98d501ec5..000000000 --- a/sdk/framework/testdata/responses.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "openapi": "3.0.2", - "info": { - "title": "HashiCorp Vault API", - "description": "HTTP API that gives you full access to Vault. All API routes are prefixed with `/v1/`.", - "version": "", - "license": { - "name": "Mozilla Public License 2.0", - "url": "https://www.mozilla.org/en-US/MPL/2.0" - } - }, - "paths": { - "/foo": { - "description": "Synopsis", - "x-vault-unauthenticated": true, - "delete": { - "operationId": "kv-delete-foo", - "tags": [ - "secrets" - ], - "summary": "Delete stuff", - "responses": { - "204": { - "description": "empty body" - } - } - }, - "get": { - "operationId": "kv-read-foo", - "tags": [ - "secrets" - ], - "summary": "My Summary", - "description": "My Description", - "responses": { - "202": { - "description": "Amazing", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KvReadFooResponse" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "KvReadFooResponse": { - "type": "object", - "properties": { - "field_a": { - "type": "string", - "description": "field_a description" - }, - "field_b": { - "type": "boolean", - "description": "field_b description" - } - } - } - } - } -} diff --git a/sdk/framework/testing.go b/sdk/framework/testing.go deleted file mode 100644 index d2035d676..000000000 --- a/sdk/framework/testing.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "testing" -) - -// TestBackendRoutes is a helper to test that all the given routes will -// route properly in the backend. -func TestBackendRoutes(t *testing.T, b *Backend, rs []string) { - for _, r := range rs { - if b.Route(r) == nil { - t.Fatalf("bad route: %s", r) - } - } -} diff --git a/sdk/framework/wal_test.go b/sdk/framework/wal_test.go deleted file mode 100644 index 040749239..000000000 --- a/sdk/framework/wal_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "context" - "reflect" - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestWAL(t *testing.T) { - s := new(logical.InmemStorage) - - ctx := context.Background() - - // WAL should be empty to start - keys, err := ListWAL(ctx, s) - if err != nil { - t.Fatalf("err: %s", err) - } - if len(keys) > 0 { - t.Fatalf("bad: %#v", keys) - } - - // Write an entry to the WAL - id, err := PutWAL(ctx, s, "foo", "bar") - if err != nil { - t.Fatalf("err: %s", err) - } - - // The key should be in the WAL - keys, err = ListWAL(ctx, s) - if err != nil { - t.Fatalf("err: %s", err) - } - if !reflect.DeepEqual(keys, []string{id}) { - t.Fatalf("bad: %#v", keys) - } - - // Should be able to get the value - entry, err := GetWAL(ctx, s, id) - if err != nil { - t.Fatalf("err: %s", err) - } - if entry.Kind != "foo" { - t.Fatalf("bad: %#v", entry) - } - if entry.Data != "bar" { - t.Fatalf("bad: %#v", entry) - } - - // Should be able to delete the value - if err := DeleteWAL(ctx, s, id); err != nil { - t.Fatalf("err: %s", err) - } - entry, err = GetWAL(ctx, s, id) - if err != nil { - t.Fatalf("err: %s", err) - } - if entry != nil { - t.Fatalf("bad: %#v", entry) - } -} diff --git a/sdk/helper/authmetadata/auth_metadata_acc_test.go b/sdk/helper/authmetadata/auth_metadata_acc_test.go deleted file mode 100644 index 189c96009..000000000 --- a/sdk/helper/authmetadata/auth_metadata_acc_test.go +++ /dev/null @@ -1,480 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package authmetadata - -import ( - "context" - "fmt" - "reflect" - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -type environment struct { - ctx context.Context - storage logical.Storage - backend logical.Backend -} - -func TestAcceptance(t *testing.T) { - ctx := context.Background() - storage := &logical.InmemStorage{} - b, err := backend(ctx, storage) - if err != nil { - t.Fatal(err) - } - env := &environment{ - ctx: ctx, - storage: storage, - backend: b, - } - t.Run("test initial fields are default", env.TestInitialFieldsAreDefault) - t.Run("test fields can be unset", env.TestAuthMetadataCanBeUnset) - t.Run("test defaults can be restored", env.TestDefaultCanBeReused) - t.Run("test default plus more cannot be selected", env.TestDefaultPlusMoreCannotBeSelected) - t.Run("test only non-defaults can be selected", env.TestOnlyNonDefaultsCanBeSelected) - t.Run("test bad field results in useful error", env.TestAddingBadField) -} - -func (e *environment) TestInitialFieldsAreDefault(t *testing.T) { - // On the first read of auth_metadata, when nothing has been touched, - // we should receive the default field(s) if a read is performed. - resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "config", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Data == nil { - t.Fatal("expected non-nil response") - } - if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{"role_name"}) { - t.Fatal("expected default field of role_name to be returned") - } - - // The auth should only have the default metadata. - resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - Data: map[string]interface{}{ - "role_name": "something", - }, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil { - t.Fatalf("expected alias metadata") - } - if len(resp.Auth.Alias.Metadata) != 1 { - t.Fatal("expected only 1 field") - } - if resp.Auth.Alias.Metadata["role_name"] != "something" { - t.Fatal("expected role_name to be something") - } -} - -func (e *environment) TestAuthMetadataCanBeUnset(t *testing.T) { - // We should be able to set the auth_metadata to empty by sending an - // explicitly empty array. - resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - Data: map[string]interface{}{ - authMetadataFields.FieldName: []string{}, - }, - }) - if err != nil { - t.Fatal(err) - } - if resp != nil { - t.Fatal("expected nil response") - } - - // Now we should receive no fields for auth_metadata. - resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "config", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Data == nil { - t.Fatal("expected non-nil response") - } - if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{}) { - t.Fatal("expected no fields to be returned") - } - - // The auth should have no metadata. - resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - Data: map[string]interface{}{ - "role_name": "something", - }, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil { - t.Fatal("expected alias metadata") - } - if len(resp.Auth.Alias.Metadata) != 0 { - t.Fatal("expected 0 fields") - } -} - -func (e *environment) TestDefaultCanBeReused(t *testing.T) { - // Now if we set it to "default", the default fields should - // be restored. - resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - Data: map[string]interface{}{ - authMetadataFields.FieldName: []string{"default"}, - }, - }) - if err != nil { - t.Fatal(err) - } - if resp != nil { - t.Fatal("expected nil response") - } - - // Let's make sure we've returned to the default fields. - resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "config", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Data == nil { - t.Fatal("expected non-nil response") - } - if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{"role_name"}) { - t.Fatal("expected default field of role_name to be returned") - } - - // We should again only receive the default field on the login. - resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - Data: map[string]interface{}{ - "role_name": "something", - }, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil { - t.Fatal("expected alias metadata") - } - if len(resp.Auth.Alias.Metadata) != 1 { - t.Fatal("expected only 1 field") - } - if resp.Auth.Alias.Metadata["role_name"] != "something" { - t.Fatal("expected role_name to be something") - } -} - -func (e *environment) TestDefaultPlusMoreCannotBeSelected(t *testing.T) { - // We should not be able to set it to "default" plus 1 optional field. - _, err := e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - Data: map[string]interface{}{ - authMetadataFields.FieldName: []string{"default", "remote_addr"}, - }, - }) - if err == nil { - t.Fatal("expected err") - } -} - -func (e *environment) TestOnlyNonDefaultsCanBeSelected(t *testing.T) { - // Omit all default fields and just select one. - resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - Data: map[string]interface{}{ - authMetadataFields.FieldName: []string{"remote_addr"}, - }, - }) - if err != nil { - t.Fatal(err) - } - if resp != nil { - t.Fatal("expected nil response") - } - - // Make sure that worked. - resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "config", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Data == nil { - t.Fatal("expected non-nil response") - } - if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{"remote_addr"}) { - t.Fatal("expected remote_addr to be returned") - } - - // Ensure only the selected one is on logins. - // They both should now appear on the login. - resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - Data: map[string]interface{}{ - "role_name": "something", - }, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil { - t.Fatal("expected alias metadata") - } - if len(resp.Auth.Alias.Metadata) != 1 { - t.Fatal("expected only 1 field") - } - if resp.Auth.Alias.Metadata["remote_addr"] != "http://foo.com" { - t.Fatal("expected remote_addr to be http://foo.com") - } -} - -func (e *environment) TestAddingBadField(t *testing.T) { - // Try adding an unsupported field. - resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config", - Storage: e.storage, - Connection: &logical.Connection{ - RemoteAddr: "http://foo.com", - }, - Data: map[string]interface{}{ - authMetadataFields.FieldName: []string{"asl;dfkj"}, - }, - }) - if err == nil { - t.Fatal("expected err") - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if !resp.IsError() { - t.Fatal("expected error response") - } -} - -// We expect people to embed the Handler on their -// config so it automatically makes its helper methods -// available and easy to find wherever the config is -// needed. Explicitly naming it in json avoids it -// automatically being named "Handler" by Go's JSON -// marshalling library. -type fakeConfig struct { - *Handler `json:"auth_metadata_handler"` -} - -type fakeBackend struct { - *framework.Backend -} - -// We expect each back-end to explicitly define the fields that -// will be included by default, and optionally available. -var authMetadataFields = &Fields{ - FieldName: "some_field_name", - Default: []string{ - "role_name", // This would likely never change because the alias is the role name. - }, - AvailableToAdd: []string{ - "remote_addr", // This would likely change with every new caller. - }, -} - -func configPath() *framework.Path { - return &framework.Path{ - Pattern: "config", - Fields: map[string]*framework.FieldSchema{ - authMetadataFields.FieldName: FieldSchema(authMetadataFields), - }, - Operations: map[logical.Operation]framework.OperationHandler{ - logical.ReadOperation: &framework.PathOperation{ - Callback: func(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) { - entryRaw, err := req.Storage.Get(ctx, "config") - if err != nil { - return nil, err - } - conf := &fakeConfig{ - Handler: NewHandler(authMetadataFields), - } - if entryRaw != nil { - if err := entryRaw.DecodeJSON(conf); err != nil { - return nil, err - } - } - // Note that even if the config entry was nil, we return - // a populated response to give info on what the default - // auth metadata is when unconfigured. - return &logical.Response{ - Data: map[string]interface{}{ - authMetadataFields.FieldName: conf.AuthMetadata(), - }, - }, nil - }, - }, - logical.UpdateOperation: &framework.PathOperation{ - Callback: func(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) { - entryRaw, err := req.Storage.Get(ctx, "config") - if err != nil { - return nil, err - } - conf := &fakeConfig{ - Handler: NewHandler(authMetadataFields), - } - if entryRaw != nil { - if err := entryRaw.DecodeJSON(conf); err != nil { - return nil, err - } - } - // This is where we read in the user's given auth metadata. - if err := conf.ParseAuthMetadata(fd); err != nil { - // Since this will only error on bad input, it's best to give - // a 400 response with the explicit problem included. - return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest - } - entry, err := logical.StorageEntryJSON("config", conf) - if err != nil { - return nil, err - } - if err = req.Storage.Put(ctx, entry); err != nil { - return nil, err - } - return nil, nil - }, - }, - }, - } -} - -func loginPath() *framework.Path { - return &framework.Path{ - Pattern: "login", - Fields: map[string]*framework.FieldSchema{ - "role_name": { - Type: framework.TypeString, - Required: true, - }, - }, - Operations: map[logical.Operation]framework.OperationHandler{ - logical.UpdateOperation: &framework.PathOperation{ - Callback: func(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) { - entryRaw, err := req.Storage.Get(ctx, "config") - if err != nil { - return nil, err - } - conf := &fakeConfig{ - Handler: NewHandler(authMetadataFields), - } - if entryRaw != nil { - if err := entryRaw.DecodeJSON(conf); err != nil { - return nil, err - } - } - auth := &logical.Auth{ - Alias: &logical.Alias{ - Name: fd.Get("role_name").(string), - }, - } - // Here we provide everything and let the method strip out - // the undesired stuff. - if err := conf.PopulateDesiredMetadata(auth, map[string]string{ - "role_name": fd.Get("role_name").(string), - "remote_addr": req.Connection.RemoteAddr, - }); err != nil { - fmt.Println("unable to populate due to " + err.Error()) - } - return &logical.Response{ - Auth: auth, - }, nil - }, - }, - }, - } -} - -func backend(ctx context.Context, storage logical.Storage) (logical.Backend, error) { - b := &fakeBackend{ - Backend: &framework.Backend{ - Paths: []*framework.Path{ - configPath(), - loginPath(), - }, - }, - } - if err := b.Setup(ctx, &logical.BackendConfig{ - StorageView: storage, - Logger: hclog.Default(), - }); err != nil { - return nil, err - } - return b, nil -} diff --git a/sdk/helper/authmetadata/auth_metadata_test.go b/sdk/helper/authmetadata/auth_metadata_test.go deleted file mode 100644 index a82044f9b..000000000 --- a/sdk/helper/authmetadata/auth_metadata_test.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package authmetadata - -import ( - "fmt" - "reflect" - "sort" - "testing" - - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -var testFields = &Fields{ - FieldName: "some-field-name", - Default: []string{"fizz", "buzz"}, - AvailableToAdd: []string{"foo", "bar"}, -} - -func TestFieldSchema(t *testing.T) { - schema := FieldSchema(testFields) - if schema.Type != framework.TypeCommaStringSlice { - t.Fatal("expected TypeCommaStringSlice") - } - if schema.Description != `The metadata to include on the aliases and audit logs generated by this plugin. When set to 'default', includes: fizz, buzz. These fields are available to add: foo, bar. Not editing this field means the 'default' fields are included. Explicitly setting this field to empty overrides the 'default' and means no metadata will be included. If not using 'default', explicit fields must be sent like: 'field1,field2'.` { - t.Fatal("received unexpected description: " + schema.Description) - } - if schema.DisplayAttrs == nil { - t.Fatal("expected display attributes") - } - if schema.DisplayAttrs.Name != testFields.FieldName { - t.Fatalf("expected name of %s", testFields.FieldName) - } - if schema.DisplayAttrs.Value != "field1,field2" { - t.Fatal("expected field1,field2") - } - if !reflect.DeepEqual(schema.Default, []string{"default"}) { - t.Fatal("expected default") - } -} - -func TestGetAuthMetadata(t *testing.T) { - h := NewHandler(testFields) - expected := []string{"fizz", "buzz"} - sort.Strings(expected) - actual := h.AuthMetadata() - sort.Strings(actual) - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("expected %s but received %s", expected, actual) - } -} - -func TestParseAuthMetadata(t *testing.T) { - h := NewHandler(testFields) - data := &framework.FieldData{ - Raw: map[string]interface{}{ - testFields.FieldName: []string{"default"}, - }, - Schema: map[string]*framework.FieldSchema{ - testFields.FieldName: FieldSchema(testFields), - }, - } - if err := h.ParseAuthMetadata(data); err != nil { - t.Fatal(err) - } - expected := []string{"fizz", "buzz"} - sort.Strings(expected) - actual := h.AuthMetadata() - sort.Strings(actual) - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("expected %s but received %s", expected, actual) - } -} - -func TestPopulateDesiredAuthMetadata(t *testing.T) { - h := NewHandler(testFields) - data := &framework.FieldData{ - Raw: map[string]interface{}{ - testFields.FieldName: []string{"foo"}, - }, - Schema: map[string]*framework.FieldSchema{ - testFields.FieldName: FieldSchema(testFields), - }, - } - if err := h.ParseAuthMetadata(data); err != nil { - t.Fatal(err) - } - auth := &logical.Auth{ - Alias: &logical.Alias{ - Name: "foo", - }, - } - if err := h.PopulateDesiredMetadata(auth, map[string]string{ - "fizz": "fizzval", - "buzz": "buzzval", - "foo": "fooval", - }); err != nil { - t.Fatal(err) - } - if len(auth.Alias.Metadata) != 1 { - t.Fatal("expected only 1 configured field to be populated") - } - if auth.Alias.Metadata["foo"] != "fooval" { - t.Fatal("expected foova;") - } -} - -func TestMarshalJSON(t *testing.T) { - h := NewHandler(&Fields{}) - h.authMetadata = []string{"fizz", "buzz"} - b, err := h.MarshalJSON() - if err != nil { - t.Fatal(err) - } - if string(b) != `{"auth_metadata":["fizz","buzz"]}` { - t.Fatal(`expected {"auth_metadata":["fizz","buzz"]}`) - } -} - -func TestUnmarshalJSON(t *testing.T) { - h := NewHandler(&Fields{}) - if err := h.UnmarshalJSON([]byte(`{"auth_metadata":["fizz","buzz"]}`)); err != nil { - t.Fatal(err) - } - if fmt.Sprintf("%s", h.authMetadata) != `[fizz buzz]` { - t.Fatal(`expected [fizz buzz]`) - } -} diff --git a/sdk/helper/certutil/certutil_test.go b/sdk/helper/certutil/certutil_test.go deleted file mode 100644 index 8b5509461..000000000 --- a/sdk/helper/certutil/certutil_test.go +++ /dev/null @@ -1,1047 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package certutil - -import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/json" - "encoding/pem" - "fmt" - "math/big" - mathrand "math/rand" - "reflect" - "strings" - "sync" - "testing" - "time" - - "github.com/fatih/structs" -) - -// Tests converting back and forth between a CertBundle and a ParsedCertBundle. -// -// Also tests the GetSubjKeyID, GetHexFormatted, ParseHexFormatted and -// ParsedCertBundle.getSigner functions. -func TestCertBundleConversion(t *testing.T) { - cbuts := []*CertBundle{ - refreshRSACertBundle(), - refreshRSACertBundleWithChain(), - refreshRSA8CertBundle(), - refreshRSA8CertBundleWithChain(), - refreshECCertBundle(), - refreshECCertBundleWithChain(), - refreshEC8CertBundle(), - refreshEC8CertBundleWithChain(), - refreshEd255198CertBundle(), - refreshEd255198CertBundleWithChain(), - } - - for i, cbut := range cbuts { - pcbut, err := cbut.ToParsedCertBundle() - if err != nil { - t.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - t.Errorf("Error converting to parsed cert bundle: %s", err) - continue - } - - err = compareCertBundleToParsedCertBundle(cbut, pcbut) - if err != nil { - t.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - t.Errorf(err.Error()) - } - - cbut, err := pcbut.ToCertBundle() - if err != nil { - t.Fatalf("Error converting to cert bundle: %s", err) - } - - err = compareCertBundleToParsedCertBundle(cbut, pcbut) - if err != nil { - t.Fatalf(err.Error()) - } - } -} - -func BenchmarkCertBundleParsing(b *testing.B) { - for i := 0; i < b.N; i++ { - cbuts := []*CertBundle{ - refreshRSACertBundle(), - refreshRSACertBundleWithChain(), - refreshRSA8CertBundle(), - refreshRSA8CertBundleWithChain(), - refreshECCertBundle(), - refreshECCertBundleWithChain(), - refreshEC8CertBundle(), - refreshEC8CertBundleWithChain(), - refreshEd255198CertBundle(), - refreshEd255198CertBundleWithChain(), - } - - for i, cbut := range cbuts { - pcbut, err := cbut.ToParsedCertBundle() - if err != nil { - b.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - b.Errorf("Error converting to parsed cert bundle: %s", err) - continue - } - - cbut, err = pcbut.ToCertBundle() - if err != nil { - b.Fatalf("Error converting to cert bundle: %s", err) - } - } - } -} - -func TestCertBundleParsing(t *testing.T) { - cbuts := []*CertBundle{ - refreshRSACertBundle(), - refreshRSACertBundleWithChain(), - refreshRSA8CertBundle(), - refreshRSA8CertBundleWithChain(), - refreshECCertBundle(), - refreshECCertBundleWithChain(), - refreshEC8CertBundle(), - refreshEC8CertBundleWithChain(), - refreshEd255198CertBundle(), - refreshEd255198CertBundleWithChain(), - } - - for i, cbut := range cbuts { - jsonString, err := json.Marshal(cbut) - if err != nil { - t.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - t.Fatalf("Error marshaling testing certbundle to JSON: %s", err) - } - pcbut, err := ParsePKIJSON(jsonString) - if err != nil { - t.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - t.Fatalf("Error during JSON bundle handling: %s", err) - } - err = compareCertBundleToParsedCertBundle(cbut, pcbut) - if err != nil { - t.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - t.Fatalf(err.Error()) - } - - dataMap := structs.New(cbut).Map() - pcbut, err = ParsePKIMap(dataMap) - if err != nil { - t.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - t.Fatalf("Error during JSON bundle handling: %s", err) - } - err = compareCertBundleToParsedCertBundle(cbut, pcbut) - if err != nil { - t.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - t.Fatalf(err.Error()) - } - - pcbut, err = ParsePEMBundle(cbut.ToPEMBundle()) - if err != nil { - t.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - t.Fatalf("Error during JSON bundle handling: %s", err) - } - err = compareCertBundleToParsedCertBundle(cbut, pcbut) - if err != nil { - t.Logf("Error occurred with bundle %d in test array (index %d).\n", i+1, i) - t.Fatalf(err.Error()) - } - } -} - -func compareCertBundleToParsedCertBundle(cbut *CertBundle, pcbut *ParsedCertBundle) error { - if cbut == nil { - return fmt.Errorf("got nil bundle") - } - if pcbut == nil { - return fmt.Errorf("got nil parsed bundle") - } - - switch { - case pcbut.Certificate == nil: - return fmt.Errorf("parsed bundle has nil certificate") - case pcbut.PrivateKey == nil: - return fmt.Errorf("parsed bundle has nil private key") - } - - switch cbut.PrivateKey { - case privRSAKeyPem: - if pcbut.PrivateKeyType != RSAPrivateKey { - return fmt.Errorf("parsed bundle has wrong private key type: %v, should be 'rsa' (%v)", pcbut.PrivateKeyType, RSAPrivateKey) - } - case privRSA8KeyPem: - if pcbut.PrivateKeyType != RSAPrivateKey { - return fmt.Errorf("parsed bundle has wrong pkcs8 private key type: %v, should be 'rsa' (%v)", pcbut.PrivateKeyType, RSAPrivateKey) - } - case privECKeyPem: - if pcbut.PrivateKeyType != ECPrivateKey { - return fmt.Errorf("parsed bundle has wrong private key type: %v, should be 'ec' (%v)", pcbut.PrivateKeyType, ECPrivateKey) - } - case privEC8KeyPem: - if pcbut.PrivateKeyType != ECPrivateKey { - return fmt.Errorf("parsed bundle has wrong pkcs8 private key type: %v, should be 'ec' (%v)", pcbut.PrivateKeyType, ECPrivateKey) - } - case privEd255198KeyPem: - if pcbut.PrivateKeyType != Ed25519PrivateKey { - return fmt.Errorf("parsed bundle has wrong pkcs8 private key type: %v, should be 'ed25519' (%v)", pcbut.PrivateKeyType, ECPrivateKey) - } - default: - return fmt.Errorf("parsed bundle has unknown private key type") - } - - subjKeyID, err := GetSubjKeyID(pcbut.PrivateKey) - if err != nil { - return fmt.Errorf("error when getting subject key id: %s", err) - } - if bytes.Compare(subjKeyID, pcbut.Certificate.SubjectKeyId) != 0 { - return fmt.Errorf("parsed bundle private key does not match subject key id\nGot\n%#v\nExpected\n%#v\nCert\n%#v", subjKeyID, pcbut.Certificate.SubjectKeyId, *pcbut.Certificate) - } - - switch { - case len(pcbut.CAChain) > 0 && len(cbut.CAChain) == 0: - return fmt.Errorf("parsed bundle ca chain has certs when cert bundle does not") - case len(pcbut.CAChain) == 0 && len(cbut.CAChain) > 0: - return fmt.Errorf("cert bundle ca chain has certs when parsed cert bundle does not") - } - - cb, err := pcbut.ToCertBundle() - if err != nil { - return fmt.Errorf("thrown error during parsed bundle conversion: %s\n\nInput was: %#v", err, *pcbut) - } - - switch { - case len(cb.Certificate) == 0: - return fmt.Errorf("bundle has nil certificate") - case len(cb.PrivateKey) == 0: - return fmt.Errorf("bundle has nil private key") - case len(cb.CAChain[0]) == 0: - return fmt.Errorf("bundle has nil issuing CA") - } - - switch pcbut.PrivateKeyType { - case RSAPrivateKey: - if cb.PrivateKey != privRSAKeyPem && cb.PrivateKey != privRSA8KeyPem { - return fmt.Errorf("bundle private key does not match") - } - case ECPrivateKey: - if cb.PrivateKey != privECKeyPem && cb.PrivateKey != privEC8KeyPem { - return fmt.Errorf("bundle private key does not match") - } - case Ed25519PrivateKey: - if cb.PrivateKey != privEd255198KeyPem { - return fmt.Errorf("bundle private key does not match") - } - default: - return fmt.Errorf("certBundle has unknown private key type") - } - - if cb.SerialNumber != GetHexFormatted(pcbut.Certificate.SerialNumber.Bytes(), ":") { - return fmt.Errorf("bundle serial number does not match") - } - - if !bytes.Equal(pcbut.Certificate.SerialNumber.Bytes(), ParseHexFormatted(cb.SerialNumber, ":")) { - return fmt.Errorf("failed re-parsing hex formatted number %s", cb.SerialNumber) - } - - switch { - case len(pcbut.CAChain) > 0 && len(cb.CAChain) == 0: - return fmt.Errorf("parsed bundle ca chain has certs when cert bundle does not") - case len(pcbut.CAChain) == 0 && len(cb.CAChain) > 0: - return fmt.Errorf("cert bundle ca chain has certs when parsed cert bundle does not") - case !reflect.DeepEqual(cbut.CAChain, cb.CAChain): - return fmt.Errorf("cert bundle ca chain does not match: %#v\n\n%#v", cbut.CAChain, cb.CAChain) - } - - return nil -} - -func TestCSRBundleConversion(t *testing.T) { - csrbuts := []*CSRBundle{ - refreshRSACSRBundle(), - refreshECCSRBundle(), - refreshEd25519CSRBundle(), - } - - for _, csrbut := range csrbuts { - pcsrbut, err := csrbut.ToParsedCSRBundle() - if err != nil { - t.Fatalf("Error converting to parsed CSR bundle: %v", err) - } - - err = compareCSRBundleToParsedCSRBundle(csrbut, pcsrbut) - if err != nil { - t.Fatalf(err.Error()) - } - - csrbut, err = pcsrbut.ToCSRBundle() - if err != nil { - t.Fatalf("Error converting to CSR bundle: %v", err) - } - - err = compareCSRBundleToParsedCSRBundle(csrbut, pcsrbut) - if err != nil { - t.Fatalf(err.Error()) - } - } -} - -func compareCSRBundleToParsedCSRBundle(csrbut *CSRBundle, pcsrbut *ParsedCSRBundle) error { - if csrbut == nil { - return fmt.Errorf("got nil bundle") - } - if pcsrbut == nil { - return fmt.Errorf("got nil parsed bundle") - } - - switch { - case pcsrbut.CSR == nil: - return fmt.Errorf("parsed bundle has nil csr") - case pcsrbut.PrivateKey == nil: - return fmt.Errorf("parsed bundle has nil private key") - } - - switch csrbut.PrivateKey { - case privRSAKeyPem: - if pcsrbut.PrivateKeyType != RSAPrivateKey { - return fmt.Errorf("parsed bundle has wrong private key type") - } - case privECKeyPem: - if pcsrbut.PrivateKeyType != ECPrivateKey { - return fmt.Errorf("parsed bundle has wrong private key type") - } - case privEd255198KeyPem: - if pcsrbut.PrivateKeyType != Ed25519PrivateKey { - return fmt.Errorf("parsed bundle has wrong private key type") - } - default: - return fmt.Errorf("parsed bundle has unknown private key type") - } - - csrb, err := pcsrbut.ToCSRBundle() - if err != nil { - return fmt.Errorf("Thrown error during parsed bundle conversion: %s\n\nInput was: %#v", err, *pcsrbut) - } - - switch { - case len(csrb.CSR) == 0: - return fmt.Errorf("bundle has nil certificate") - case len(csrb.PrivateKey) == 0: - return fmt.Errorf("bundle has nil private key") - } - - switch csrb.PrivateKeyType { - case "rsa": - if pcsrbut.PrivateKeyType != RSAPrivateKey { - return fmt.Errorf("bundle has wrong private key type") - } - if csrb.PrivateKey != privRSAKeyPem { - return fmt.Errorf("bundle rsa private key does not match\nGot\n%#v\nExpected\n%#v", csrb.PrivateKey, privRSAKeyPem) - } - case "ec": - if pcsrbut.PrivateKeyType != ECPrivateKey { - return fmt.Errorf("bundle has wrong private key type") - } - if csrb.PrivateKey != privECKeyPem { - return fmt.Errorf("bundle ec private key does not match") - } - case "ed25519": - if pcsrbut.PrivateKeyType != Ed25519PrivateKey { - return fmt.Errorf("bundle has wrong private key type") - } - if csrb.PrivateKey != privEd255198KeyPem { - return fmt.Errorf("bundle ed25519 private key does not match") - } - default: - return fmt.Errorf("bundle has unknown private key type") - } - - return nil -} - -func TestTLSConfig(t *testing.T) { - cbut := refreshRSACertBundle() - - pcbut, err := cbut.ToParsedCertBundle() - if err != nil { - t.Fatalf("Error getting parsed cert bundle: %s", err) - } - - usages := []TLSUsage{ - TLSUnknown, - TLSClient, - TLSServer, - TLSClient | TLSServer, - } - - for _, usage := range usages { - tlsConfig, err := pcbut.GetTLSConfig(usage) - if err != nil { - t.Fatalf("Error getting tls config: %s", err) - } - if tlsConfig == nil { - t.Fatalf("Got nil tls.Config") - } - - if len(tlsConfig.Certificates) != 1 { - t.Fatalf("Unexpected length in config.Certificates") - } - - // Length should be 2, since we passed in a CA - if len(tlsConfig.Certificates[0].Certificate) != 2 { - t.Fatalf("Did not find both certificates in config.Certificates.Certificate") - } - - if tlsConfig.Certificates[0].Leaf != pcbut.Certificate { - t.Fatalf("Leaf certificate does not match parsed bundle's certificate") - } - - if tlsConfig.Certificates[0].PrivateKey != pcbut.PrivateKey { - t.Fatalf("Config's private key does not match parsed bundle's private key") - } - - switch usage { - case TLSServer | TLSClient: - if len(tlsConfig.ClientCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.ClientCAs.Subjects()[0], pcbut.CAChain[0].Certificate.RawSubject) != 0 { - t.Fatalf("CA certificate not in client cert pool as expected") - } - if len(tlsConfig.RootCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.RootCAs.Subjects()[0], pcbut.CAChain[0].Certificate.RawSubject) != 0 { - t.Fatalf("CA certificate not in root cert pool as expected") - } - case TLSServer: - if len(tlsConfig.ClientCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.ClientCAs.Subjects()[0], pcbut.CAChain[0].Certificate.RawSubject) != 0 { - t.Fatalf("CA certificate not in client cert pool as expected") - } - if tlsConfig.RootCAs != nil { - t.Fatalf("Found root pools in config object when not expected") - } - case TLSClient: - if len(tlsConfig.RootCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.RootCAs.Subjects()[0], pcbut.CAChain[0].Certificate.RawSubject) != 0 { - t.Fatalf("CA certificate not in root cert pool as expected") - } - if tlsConfig.ClientCAs != nil { - t.Fatalf("Found root pools in config object when not expected") - } - default: - if tlsConfig.RootCAs != nil || tlsConfig.ClientCAs != nil { - t.Fatalf("Found root pools in config object when not expected") - } - } - } -} - -func TestNewCertPool(t *testing.T) { - caExample := `-----BEGIN CERTIFICATE----- -MIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p -a3ViZUNBMB4XDTE5MTIxMDIzMDUxOVoXDTI5MTIwODIzMDUxOVowFTETMBEGA1UE -AxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANFi -/RIdMHd865X6JygTb9riX01DA3QnR+RoXDXNnj8D3LziLG2n8ItXMJvWbU3sxxyy -nX9HxJ0SIeexj1cYzdQBtJDjO1/PeuKc4CZ7zCukCAtHz8mC7BDPOU7F7pggpcQ0 -/t/pa2m22hmCu8aDF9WlUYHtJpYATnI/A5vz/VFLR9daxmkl59Qo3oHITj7vAzSx -/75r9cibpQyJ+FhiHOZHQWYY2JYw2g4v5hm5hg5SFM9yFcZ75ISI9ebyFFIl9iBY -zAk9jqv1mXvLr0Q39AVwMTamvGuap1oocjM9NIhQvaFL/DNqF1ouDQjCf5u2imLc -TraO1/2KO8fqwOZCOrMCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW -MBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 -DQEBCwUAA4IBAQBtVZCwCPqUUUpIClAlE9nc2fo2bTs9gsjXRmqdQ5oaSomSLE93 -aJWYFuAhxPXtlApbLYZfW2m1sM3mTVQN60y0uE4e1jdSN1ErYQ9slJdYDAMaEmOh -iSexj+Nd1scUiMHV9lf3ps5J8sYeCpwZX3sPmw7lqZojTS12pANBDcigsaj5RRyN -9GyP3WkSQUsTpWlDb9Fd+KNdkCVw7nClIpBPA2KW4BQKw/rNSvOFD61mbzc89lo0 -Q9IFGQFFF8jO18lbyWqnRBGXcS4/G7jQ3S7C121d14YLUeAYOM7pJykI1g4CLx9y -vitin0L6nprauWkKO38XgM4T75qKZpqtiOcT ------END CERTIFICATE----- -` - if _, err := NewCertPool(bytes.NewReader([]byte(caExample))); err != nil { - t.Fatal(err) - } -} - -func TestGetPublicKeySize(t *testing.T) { - rsa, err := rsa.GenerateKey(rand.Reader, 3072) - if err != nil { - t.Fatal(err) - } - if GetPublicKeySize(&rsa.PublicKey) != 3072 { - t.Fatal("unexpected rsa key size") - } - ecdsa, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - t.Fatal(err) - } - if GetPublicKeySize(&ecdsa.PublicKey) != 384 { - t.Fatal("unexpected ecdsa key size") - } - ed25519, _, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - if GetPublicKeySize(ed25519) != 256 { - t.Fatal("unexpected ed25519 key size") - } - // Skipping DSA as too slow -} - -func refreshRSA8CertBundle() *CertBundle { - initTest.Do(setCerts) - return &CertBundle{ - Certificate: certRSAPem, - PrivateKey: privRSA8KeyPem, - CAChain: []string{issuingCaChainPem[0]}, - } -} - -func refreshRSA8CertBundleWithChain() *CertBundle { - initTest.Do(setCerts) - ret := refreshRSA8CertBundle() - ret.CAChain = issuingCaChainPem - return ret -} - -func refreshRSACertBundle() *CertBundle { - initTest.Do(setCerts) - return &CertBundle{ - Certificate: certRSAPem, - CAChain: []string{issuingCaChainPem[0]}, - PrivateKey: privRSAKeyPem, - } -} - -func refreshRSACertBundleWithChain() *CertBundle { - initTest.Do(setCerts) - ret := refreshRSACertBundle() - ret.CAChain = issuingCaChainPem - return ret -} - -func refreshECCertBundle() *CertBundle { - initTest.Do(setCerts) - return &CertBundle{ - Certificate: certECPem, - CAChain: []string{issuingCaChainPem[0]}, - PrivateKey: privECKeyPem, - } -} - -func refreshECCertBundleWithChain() *CertBundle { - initTest.Do(setCerts) - ret := refreshECCertBundle() - ret.CAChain = issuingCaChainPem - return ret -} - -func refreshEd255198CertBundle() *CertBundle { - initTest.Do(setCerts) - return &CertBundle{ - Certificate: certEd25519Pem, - PrivateKey: privEd255198KeyPem, - CAChain: []string{issuingCaChainPem[0]}, - } -} - -func refreshEd255198CertBundleWithChain() *CertBundle { - initTest.Do(setCerts) - ret := refreshEd255198CertBundle() - ret.CAChain = issuingCaChainPem - return ret -} - -func refreshEd25519CSRBundle() *CSRBundle { - initTest.Do(setCerts) - return &CSRBundle{ - CSR: csrEd25519Pem, - PrivateKey: privEd255198KeyPem, - } -} - -func refreshRSACSRBundle() *CSRBundle { - initTest.Do(setCerts) - return &CSRBundle{ - CSR: csrRSAPem, - PrivateKey: privRSAKeyPem, - } -} - -func refreshECCSRBundle() *CSRBundle { - initTest.Do(setCerts) - return &CSRBundle{ - CSR: csrECPem, - PrivateKey: privECKeyPem, - } -} - -func refreshEC8CertBundle() *CertBundle { - initTest.Do(setCerts) - return &CertBundle{ - Certificate: certECPem, - PrivateKey: privEC8KeyPem, - CAChain: []string{issuingCaChainPem[0]}, - } -} - -func refreshEC8CertBundleWithChain() *CertBundle { - initTest.Do(setCerts) - ret := refreshEC8CertBundle() - ret.CAChain = issuingCaChainPem - return ret -} - -func setCerts() { - caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(err) - } - subjKeyID, err := GetSubjKeyID(caKey) - if err != nil { - panic(err) - } - caCertTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "root.localhost", - }, - SubjectKeyId: subjKeyID, - DNSNames: []string{"root.localhost"}, - KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - BasicConstraintsValid: true, - IsCA: true, - } - caBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, caKey.Public(), caKey) - if err != nil { - panic(err) - } - caCert, err := x509.ParseCertificate(caBytes) - if err != nil { - panic(err) - } - caCertPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - } - caCertPEM := strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) - - intKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(err) - } - subjKeyID, err = GetSubjKeyID(intKey) - if err != nil { - panic(err) - } - intCertTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "int.localhost", - }, - SubjectKeyId: subjKeyID, - DNSNames: []string{"int.localhost"}, - KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - BasicConstraintsValid: true, - IsCA: true, - } - intBytes, err := x509.CreateCertificate(rand.Reader, intCertTemplate, caCert, intKey.Public(), caKey) - if err != nil { - panic(err) - } - intCert, err := x509.ParseCertificate(intBytes) - if err != nil { - panic(err) - } - intCertPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: intBytes, - } - intCertPEM := strings.TrimSpace(string(pem.EncodeToMemory(intCertPEMBlock))) - - // EC generation - { - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(err) - } - subjKeyID, err := GetSubjKeyID(key) - if err != nil { - panic(err) - } - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - SubjectKeyId: subjKeyID, - DNSNames: []string{"localhost"}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - csrTemplate := &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - DNSNames: []string{"localhost"}, - } - csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, key) - if err != nil { - panic(err) - } - csrPEMBlock := &pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csrBytes, - } - csrECPem = strings.TrimSpace(string(pem.EncodeToMemory(csrPEMBlock))) - certBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, intCert, key.Public(), intKey) - if err != nil { - panic(err) - } - certPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: certBytes, - } - certECPem = strings.TrimSpace(string(pem.EncodeToMemory(certPEMBlock))) - marshaledKey, err := x509.MarshalECPrivateKey(key) - if err != nil { - panic(err) - } - keyPEMBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: marshaledKey, - } - privECKeyPem = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - marshaledKey, err = x509.MarshalPKCS8PrivateKey(key) - if err != nil { - panic(err) - } - keyPEMBlock = &pem.Block{ - Type: "PRIVATE KEY", - Bytes: marshaledKey, - } - privEC8KeyPem = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - } - - // RSA generation - { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - panic(err) - } - subjKeyID, err := GetSubjKeyID(key) - if err != nil { - panic(err) - } - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - SubjectKeyId: subjKeyID, - DNSNames: []string{"localhost"}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - csrTemplate := &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - DNSNames: []string{"localhost"}, - } - csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, key) - if err != nil { - panic(err) - } - csrPEMBlock := &pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csrBytes, - } - csrRSAPem = strings.TrimSpace(string(pem.EncodeToMemory(csrPEMBlock))) - certBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, intCert, key.Public(), intKey) - if err != nil { - panic(err) - } - certPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: certBytes, - } - certRSAPem = strings.TrimSpace(string(pem.EncodeToMemory(certPEMBlock))) - marshaledKey := x509.MarshalPKCS1PrivateKey(key) - keyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: marshaledKey, - } - privRSAKeyPem = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - marshaledKey, err = x509.MarshalPKCS8PrivateKey(key) - if err != nil { - panic(err) - } - keyPEMBlock = &pem.Block{ - Type: "PRIVATE KEY", - Bytes: marshaledKey, - } - privRSA8KeyPem = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - } - - // Ed25519 generation - { - pubkey, privkey, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - panic(err) - } - subjKeyID, err := GetSubjKeyID(privkey) - if err != nil { - panic(err) - } - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - SubjectKeyId: subjKeyID, - DNSNames: []string{"localhost"}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(mathrand.Int63()), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - csrTemplate := &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - DNSNames: []string{"localhost"}, - } - csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, privkey) - if err != nil { - panic(err) - } - csrPEMBlock := &pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csrBytes, - } - csrEd25519Pem = strings.TrimSpace(string(pem.EncodeToMemory(csrPEMBlock))) - certBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, intCert, pubkey, intKey) - if err != nil { - panic(err) - } - certPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: certBytes, - } - certEd25519Pem = strings.TrimSpace(string(pem.EncodeToMemory(certPEMBlock))) - marshaledKey, err := x509.MarshalPKCS8PrivateKey(privkey) - if err != nil { - panic(err) - } - keyPEMBlock := &pem.Block{ - Type: "PRIVATE KEY", - Bytes: marshaledKey, - } - privEd255198KeyPem = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) - } - - issuingCaChainPem = []string{intCertPEM, caCertPEM} -} - -func TestComparePublicKeysAndType(t *testing.T) { - rsa1 := genRsaKey(t).Public() - rsa2 := genRsaKey(t).Public() - eddsa1 := genEdDSA(t).Public() - eddsa2 := genEdDSA(t).Public() - ed25519_1, _ := genEd25519Key(t) - ed25519_2, _ := genEd25519Key(t) - - type args struct { - key1Iface crypto.PublicKey - key2Iface crypto.PublicKey - } - tests := []struct { - name string - args args - want bool - wantErr bool - }{ - {name: "RSA_Equal", args: args{key1Iface: rsa1, key2Iface: rsa1}, want: true, wantErr: false}, - {name: "RSA_NotEqual", args: args{key1Iface: rsa1, key2Iface: rsa2}, want: false, wantErr: false}, - {name: "EDDSA_Equal", args: args{key1Iface: eddsa1, key2Iface: eddsa1}, want: true, wantErr: false}, - {name: "EDDSA_NotEqual", args: args{key1Iface: eddsa1, key2Iface: eddsa2}, want: false, wantErr: false}, - {name: "ED25519_Equal", args: args{key1Iface: ed25519_1, key2Iface: ed25519_1}, want: true, wantErr: false}, - {name: "ED25519_NotEqual", args: args{key1Iface: ed25519_1, key2Iface: ed25519_2}, want: false, wantErr: false}, - {name: "Mismatched_RSA", args: args{key1Iface: rsa1, key2Iface: ed25519_2}, want: false, wantErr: false}, - {name: "Mismatched_EDDSA", args: args{key1Iface: ed25519_1, key2Iface: rsa1}, want: false, wantErr: false}, - {name: "Mismatched_ED25519", args: args{key1Iface: ed25519_1, key2Iface: rsa1}, want: false, wantErr: false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ComparePublicKeysAndType(tt.args.key1Iface, tt.args.key2Iface) - if (err != nil) != tt.wantErr { - t.Errorf("ComparePublicKeysAndType() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("ComparePublicKeysAndType() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestNotAfterValues(t *testing.T) { - if ErrNotAfterBehavior != 0 { - t.Fatalf("Expected ErrNotAfterBehavior=%v to have value 0", ErrNotAfterBehavior) - } - - if TruncateNotAfterBehavior != 1 { - t.Fatalf("Expected TruncateNotAfterBehavior=%v to have value 1", TruncateNotAfterBehavior) - } - - if PermitNotAfterBehavior != 2 { - t.Fatalf("Expected PermitNotAfterBehavior=%v to have value 2", PermitNotAfterBehavior) - } -} - -func TestSignatureAlgorithmRoundTripping(t *testing.T) { - for leftName, value := range SignatureAlgorithmNames { - if leftName == "pureed25519" && value == x509.PureEd25519 { - continue - } - - rightName, present := InvSignatureAlgorithmNames[value] - if !present { - t.Fatalf("%v=%v is present in SignatureAlgorithmNames but not in InvSignatureAlgorithmNames", leftName, value) - } - - if strings.ToLower(rightName) != leftName { - t.Fatalf("%v=%v is present in SignatureAlgorithmNames but inverse for %v has different name: %v", leftName, value, value, rightName) - } - } - - for leftValue, name := range InvSignatureAlgorithmNames { - rightValue, present := SignatureAlgorithmNames[strings.ToLower(name)] - if !present { - t.Fatalf("%v=%v is present in InvSignatureAlgorithmNames but not in SignatureAlgorithmNames", leftValue, name) - } - - if rightValue != leftValue { - t.Fatalf("%v=%v is present in InvSignatureAlgorithmNames but forwards for %v has different value: %v", leftValue, name, name, rightValue) - } - } -} - -// TestParseBasicConstraintExtension Verify extension generation and parsing of x509 basic constraint extensions -// works as expected. -func TestBasicConstraintExtension(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - isCA bool - maxPathLen int - }{ - {"empty-seq", false, -1}, - {"just-ca-true", true, -1}, - {"just-ca-with-maxpathlen", true, 2}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ext, err := CreateBasicConstraintExtension(tt.isCA, tt.maxPathLen) - if err != nil { - t.Fatalf("failed generating basic extension: %v", err) - } - - gotIsCa, gotMaxPathLen, err := ParseBasicConstraintExtension(ext) - if err != nil { - t.Fatalf("failed parsing basic extension: %v", err) - } - - if tt.isCA != gotIsCa { - t.Fatalf("expected isCa (%v) got isCa (%v)", tt.isCA, gotIsCa) - } - - if tt.maxPathLen != gotMaxPathLen { - t.Fatalf("expected maxPathLen (%v) got maxPathLen (%v)", tt.maxPathLen, gotMaxPathLen) - } - }) - } - - t.Run("bad-extension-oid", func(t *testing.T) { - // Test invalid type errors out - _, _, err := ParseBasicConstraintExtension(pkix.Extension{}) - if err == nil { - t.Fatalf("should have failed parsing non-basic constraint extension") - } - }) - - t.Run("garbage-value", func(t *testing.T) { - extraBytes, err := asn1.Marshal("a string") - if err != nil { - t.Fatalf("failed encoding the struct: %v", err) - } - ext := pkix.Extension{ - Id: ExtensionBasicConstraintsOID, - Value: extraBytes, - } - _, _, err = ParseBasicConstraintExtension(ext) - if err == nil { - t.Fatalf("should have failed parsing basic constraint with extra information") - } - }) -} - -func genRsaKey(t *testing.T) *rsa.PrivateKey { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatal(err) - } - return key -} - -func genEdDSA(t *testing.T) *ecdsa.PrivateKey { - key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - t.Fatal(err) - } - return key -} - -func genEd25519Key(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) { - key, priv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - return key, priv -} - -var ( - initTest sync.Once - privRSA8KeyPem string - privRSAKeyPem string - csrRSAPem string - certRSAPem string - privEd255198KeyPem string - csrEd25519Pem string - certEd25519Pem string - privECKeyPem string - csrECPem string - privEC8KeyPem string - certECPem string - issuingCaChainPem []string -) diff --git a/sdk/helper/cidrutil/cidr_test.go b/sdk/helper/cidrutil/cidr_test.go deleted file mode 100644 index e6fc57644..000000000 --- a/sdk/helper/cidrutil/cidr_test.go +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package cidrutil - -import ( - "testing" - - sockaddr "github.com/hashicorp/go-sockaddr" -) - -func TestCIDRUtil_IPBelongsToCIDR(t *testing.T) { - ip := "192.168.25.30" - cidr := "192.168.26.30/16" - - belongs, err := IPBelongsToCIDR(ip, cidr) - if err != nil { - t.Fatal(err) - } - if !belongs { - t.Fatalf("expected IP %q to belong to CIDR %q", ip, cidr) - } - - ip = "10.197.192.6" - cidr = "10.197.192.0/18" - belongs, err = IPBelongsToCIDR(ip, cidr) - if err != nil { - t.Fatal(err) - } - if !belongs { - t.Fatalf("expected IP %q to belong to CIDR %q", ip, cidr) - } - - ip = "192.168.25.30" - cidr = "192.168.26.30/24" - belongs, err = IPBelongsToCIDR(ip, cidr) - if err != nil { - t.Fatal(err) - } - if belongs { - t.Fatalf("expected IP %q to not belong to CIDR %q", ip, cidr) - } - - ip = "192.168.25.30.100" - cidr = "192.168.26.30/24" - belongs, err = IPBelongsToCIDR(ip, cidr) - if err == nil { - t.Fatalf("expected an error") - } -} - -func TestCIDRUtil_IPBelongsToCIDRBlocksSlice(t *testing.T) { - ip := "192.168.27.29" - cidrList := []string{"172.169.100.200/18", "192.168.0.0/16", "10.10.20.20/24"} - - belongs, err := IPBelongsToCIDRBlocksSlice(ip, cidrList) - if err != nil { - t.Fatal(err) - } - if !belongs { - t.Fatalf("expected IP %q to belong to one of the CIDRs in %q", ip, cidrList) - } - - ip = "192.168.27.29" - cidrList = []string{"172.169.100.200/18", "192.168.0.0.0/16", "10.10.20.20/24"} - - belongs, err = IPBelongsToCIDRBlocksSlice(ip, cidrList) - if err == nil { - t.Fatalf("expected an error") - } - - ip = "30.40.50.60" - cidrList = []string{"172.169.100.200/18", "192.168.0.0/16", "10.10.20.20/24"} - - belongs, err = IPBelongsToCIDRBlocksSlice(ip, cidrList) - if err != nil { - t.Fatal(err) - } - if belongs { - t.Fatalf("expected IP %q to not belong to one of the CIDRs in %q", ip, cidrList) - } -} - -func TestCIDRUtil_ValidateCIDRListString(t *testing.T) { - cidrList := "172.169.100.200/18,192.168.0.0/16,10.10.20.20/24" - - valid, err := ValidateCIDRListString(cidrList, ",") - if err != nil { - t.Fatal(err) - } - if !valid { - t.Fatalf("expected CIDR list %q to be valid", cidrList) - } - - cidrList = "172.169.100.200,192.168.0.0/16,10.10.20.20/24" - valid, err = ValidateCIDRListString(cidrList, ",") - if err == nil { - t.Fatal("expected an error") - } - - cidrList = "172.169.100.200/18,192.168.0.0.0/16,10.10.20.20/24" - valid, err = ValidateCIDRListString(cidrList, ",") - if err == nil { - t.Fatal("expected an error") - } -} - -func TestCIDRUtil_ValidateCIDRListSlice(t *testing.T) { - cidrList := []string{"172.169.100.200/18", "192.168.0.0/16", "10.10.20.20/24"} - - valid, err := ValidateCIDRListSlice(cidrList) - if err != nil { - t.Fatal(err) - } - if !valid { - t.Fatalf("expected CIDR list %q to be valid", cidrList) - } - - cidrList = []string{"172.169.100.200", "192.168.0.0/16", "10.10.20.20/24"} - valid, err = ValidateCIDRListSlice(cidrList) - if err == nil { - t.Fatal("expected an error") - } - - cidrList = []string{"172.169.100.200/18", "192.168.0.0.0/16", "10.10.20.20/24"} - valid, err = ValidateCIDRListSlice(cidrList) - if err == nil { - t.Fatal("expected an error") - } -} - -func TestCIDRUtil_Subset(t *testing.T) { - cidr1 := "192.168.27.29/24" - cidr2 := "192.168.27.29/24" - subset, err := Subset(cidr1, cidr2) - if err != nil { - t.Fatal(err) - } - if !subset { - t.Fatalf("expected CIDR %q to be a subset of CIDR %q", cidr2, cidr1) - } - - cidr1 = "192.168.27.29/16" - cidr2 = "192.168.27.29/24" - subset, err = Subset(cidr1, cidr2) - if err != nil { - t.Fatal(err) - } - if !subset { - t.Fatalf("expected CIDR %q to be a subset of CIDR %q", cidr2, cidr1) - } - - cidr1 = "192.168.27.29/24" - cidr2 = "192.168.27.29/16" - subset, err = Subset(cidr1, cidr2) - if err != nil { - t.Fatal(err) - } - if subset { - t.Fatalf("expected CIDR %q to not be a subset of CIDR %q", cidr2, cidr1) - } - - cidr1 = "192.168.0.128/25" - cidr2 = "192.168.0.0/24" - subset, err = Subset(cidr1, cidr2) - if err != nil { - t.Fatal(err) - } - if subset { - t.Fatalf("expected CIDR %q to not be a subset of CIDR %q", cidr2, cidr1) - } - subset, err = Subset(cidr2, cidr1) - if err != nil { - t.Fatal(err) - } - if !subset { - t.Fatalf("expected CIDR %q to be a subset of CIDR %q", cidr1, cidr2) - } -} - -func TestCIDRUtil_SubsetBlocks(t *testing.T) { - cidrBlocks1 := []string{"192.168.27.29/16", "172.245.30.40/24", "10.20.30.40/30"} - cidrBlocks2 := []string{"192.168.27.29/20", "172.245.30.40/25", "10.20.30.40/32"} - - subset, err := SubsetBlocks(cidrBlocks1, cidrBlocks2) - if err != nil { - t.Fatal(err) - } - if !subset { - t.Fatalf("expected CIDR blocks %q to be a subset of CIDR blocks %q", cidrBlocks2, cidrBlocks1) - } - - cidrBlocks1 = []string{"192.168.27.29/16", "172.245.30.40/25", "10.20.30.40/30"} - cidrBlocks2 = []string{"192.168.27.29/20", "172.245.30.40/24", "10.20.30.40/32"} - - subset, err = SubsetBlocks(cidrBlocks1, cidrBlocks2) - if err != nil { - t.Fatal(err) - } - if subset { - t.Fatalf("expected CIDR blocks %q to not be a subset of CIDR blocks %q", cidrBlocks2, cidrBlocks1) - } -} - -func TestCIDRUtil_RemoteAddrIsOk_NegativeTest(t *testing.T) { - addr, err := sockaddr.NewSockAddr("127.0.0.1/8") - if err != nil { - t.Fatal(err) - } - boundCIDRs := []*sockaddr.SockAddrMarshaler{ - {addr}, - } - if RemoteAddrIsOk("123.0.0.1", boundCIDRs) { - t.Fatal("remote address of 123.0.0.1/2 should not be allowed for 127.0.0.1/8") - } -} - -func TestCIDRUtil_RemoteAddrIsOk_PositiveTest(t *testing.T) { - addr, err := sockaddr.NewSockAddr("127.0.0.1/8") - if err != nil { - t.Fatal(err) - } - boundCIDRs := []*sockaddr.SockAddrMarshaler{ - {addr}, - } - if !RemoteAddrIsOk("127.0.0.1", boundCIDRs) { - t.Fatal("remote address of 127.0.0.1 should be allowed for 127.0.0.1/8") - } -} diff --git a/sdk/helper/compressutil/compress_test.go b/sdk/helper/compressutil/compress_test.go deleted file mode 100644 index 28117d8c2..000000000 --- a/sdk/helper/compressutil/compress_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package compressutil - -import ( - "bytes" - "compress/gzip" - "testing" -) - -func TestCompressUtil_CompressDecompress(t *testing.T) { - t.Parallel() - - tests := []struct { - compressionType string - compressionConfig CompressionConfig - canary byte - }{ - { - "GZIP default implicit", - CompressionConfig{Type: CompressionTypeGzip}, - CompressionCanaryGzip, - }, - { - "GZIP default explicit", - CompressionConfig{Type: CompressionTypeGzip, GzipCompressionLevel: gzip.DefaultCompression}, - CompressionCanaryGzip, - }, - { - "GZIP best speed", - CompressionConfig{Type: CompressionTypeGzip, GzipCompressionLevel: gzip.BestSpeed}, - CompressionCanaryGzip, - }, - { - "GZIP best compression", - CompressionConfig{Type: CompressionTypeGzip, GzipCompressionLevel: gzip.BestCompression}, - CompressionCanaryGzip, - }, - { - "Snappy", - CompressionConfig{Type: CompressionTypeSnappy}, - CompressionCanarySnappy, - }, - { - "LZ4", - CompressionConfig{Type: CompressionTypeLZ4}, - CompressionCanaryLZ4, - }, - { - "LZW", - CompressionConfig{Type: CompressionTypeLZW}, - CompressionCanaryLZW, - }, - } - - inputJSONBytes := []byte(`{"sample":"data","verification":"process"}`) - - for _, test := range tests { - // Compress the input - compressedJSONBytes, err := Compress(inputJSONBytes, &test.compressionConfig) - if err != nil { - t.Fatalf("compress error (%s): %s", test.compressionType, err) - } - if len(compressedJSONBytes) == 0 { - t.Fatalf("failed to compress data in %s format", test.compressionType) - } - - // Check the presence of the canary - if compressedJSONBytes[0] != test.canary { - t.Fatalf("bad (%s): compression canary: expected: %d actual: %d", test.compressionType, test.canary, compressedJSONBytes[0]) - } - - decompressedJSONBytes, wasNotCompressed, err := Decompress(compressedJSONBytes) - if err != nil { - t.Fatalf("decompress error (%s): %s", test.compressionType, err) - } - - // Check if the input for decompress was not compressed in the first place - if wasNotCompressed { - t.Fatalf("bad (%s): expected compressed bytes", test.compressionType) - } - - if len(decompressedJSONBytes) == 0 { - t.Fatalf("bad (%s): expected decompressed bytes", test.compressionType) - } - - // Compare the value after decompression - if !bytes.Equal(inputJSONBytes, decompressedJSONBytes) { - t.Fatalf("bad (%s): decompressed value;\nexpected: %q\nactual: %q", test.compressionType, string(inputJSONBytes), string(decompressedJSONBytes)) - } - - decompressedJSONBytes, compressionType, wasNotCompressed, err := DecompressWithCanary(compressedJSONBytes) - if err != nil { - t.Fatalf("decompress error (%s): %s", test.compressionType, err) - } - - if compressionType != test.compressionConfig.Type { - t.Fatalf("bad compressionType value;\nexpected: %q\naction: %q", test.compressionConfig.Type, compressionType) - } - } -} - -func TestCompressUtil_InvalidConfigurations(t *testing.T) { - t.Parallel() - - inputJSONBytes := []byte(`{"sample":"data","verification":"process"}`) - - // Test nil configuration - if _, err := Compress(inputJSONBytes, nil); err == nil { - t.Fatal("expected an error") - } - - // Test invalid configuration - if _, err := Compress(inputJSONBytes, &CompressionConfig{}); err == nil { - t.Fatal("expected an error") - } -} - -// TestDecompressWithCanaryLargeInput tests that DecompressWithCanary works -// as expected even with large values. -func TestDecompressWithCanaryLargeInput(t *testing.T) { - t.Parallel() - - inputJSON := `{"sample":"data` - for i := 0; i < 100000; i++ { - inputJSON += " and data" - } - inputJSON += `"}` - inputJSONBytes := []byte(inputJSON) - - compressedJSONBytes, err := Compress(inputJSONBytes, &CompressionConfig{Type: CompressionTypeGzip, GzipCompressionLevel: gzip.BestCompression}) - if err != nil { - t.Fatal(err) - } - - decompressedJSONBytes, wasNotCompressed, err := Decompress(compressedJSONBytes) - if err != nil { - t.Fatal(err) - } - - // Check if the input for decompress was not compressed in the first place - if wasNotCompressed { - t.Fatalf("bytes were not compressed as expected") - } - - if len(decompressedJSONBytes) == 0 { - t.Fatalf("bytes were not compressed as expected") - } - - // Compare the value after decompression - if !bytes.Equal(inputJSONBytes, decompressedJSONBytes) { - t.Fatalf("decompressed value differs: decompressed value;\nexpected: %q\nactual: %q", string(inputJSONBytes), string(decompressedJSONBytes)) - } -} diff --git a/sdk/helper/cryptoutil/cryptoutil_test.go b/sdk/helper/cryptoutil/cryptoutil_test.go deleted file mode 100644 index 35799e42a..000000000 --- a/sdk/helper/cryptoutil/cryptoutil_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package cryptoutil - -import "testing" - -func TestBlake2b256Hash(t *testing.T) { - hashVal := Blake2b256Hash("sampletext") - - if string(hashVal) == "" || string(hashVal) == "sampletext" { - t.Fatalf("failed to hash the text") - } -} diff --git a/sdk/helper/custommetadata/custom_metadata_test.go b/sdk/helper/custommetadata/custom_metadata_test.go deleted file mode 100644 index 2b25d9912..000000000 --- a/sdk/helper/custommetadata/custom_metadata_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package custommetadata - -import ( - "strconv" - "strings" - "testing" -) - -func TestValidate(t *testing.T) { - cases := []struct { - name string - input map[string]string - shouldPass bool - }{ - { - "valid", - map[string]string{ - "foo": "abc", - "bar": "def", - "baz": "ghi", - }, - true, - }, - { - "too_many_keys", - func() map[string]string { - cm := make(map[string]string) - - for i := 0; i < maxKeyLength+1; i++ { - s := strconv.Itoa(i) - cm[s] = s - } - - return cm - }(), - false, - }, - { - "key_too_long", - map[string]string{ - strings.Repeat("a", maxKeyLength+1): "abc", - }, - false, - }, - { - "value_too_long", - map[string]string{ - "foo": strings.Repeat("a", maxValueLength+1), - }, - false, - }, - { - "unprintable_key", - map[string]string{ - "unprint\u200bable": "abc", - }, - false, - }, - { - "unprintable_value", - map[string]string{ - "foo": "unprint\u200bable", - }, - false, - }, - } - - for _, tc := range cases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - err := Validate(tc.input) - - if tc.shouldPass && err != nil { - t.Fatalf("expected validation to pass, input: %#v, err: %v", tc.input, err) - } - - if !tc.shouldPass && err == nil { - t.Fatalf("expected validation to fail, input: %#v, err: %v", tc.input, err) - } - }) - } -} diff --git a/sdk/helper/docker/testhelpers.go b/sdk/helper/docker/testhelpers.go deleted file mode 100644 index e28990a0f..000000000 --- a/sdk/helper/docker/testhelpers.go +++ /dev/null @@ -1,950 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package docker - -import ( - "archive/tar" - "bufio" - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/url" - "os" - "strconv" - "strings" - "sync" - "time" - - "github.com/cenkalti/backoff/v3" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/strslice" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/stdcopy" - "github.com/docker/go-connections/nat" - "github.com/hashicorp/go-uuid" -) - -const DockerAPIVersion = "1.40" - -type Runner struct { - DockerAPI *client.Client - RunOptions RunOptions -} - -type RunOptions struct { - ImageRepo string - ImageTag string - ContainerName string - Cmd []string - Entrypoint []string - Env []string - NetworkName string - NetworkID string - CopyFromTo map[string]string - Ports []string - DoNotAutoRemove bool - AuthUsername string - AuthPassword string - OmitLogTimestamps bool - LogConsumer func(string) - Capabilities []string - PreDelete bool - PostStart func(string, string) error - LogStderr io.Writer - LogStdout io.Writer - VolumeNameToMountPoint map[string]string -} - -func NewDockerAPI() (*client.Client, error) { - return client.NewClientWithOpts(client.FromEnv, client.WithVersion(DockerAPIVersion)) -} - -func NewServiceRunner(opts RunOptions) (*Runner, error) { - dapi, err := NewDockerAPI() - if err != nil { - return nil, err - } - - if opts.NetworkName == "" { - opts.NetworkName = os.Getenv("TEST_DOCKER_NETWORK_NAME") - } - if opts.NetworkName != "" { - nets, err := dapi.NetworkList(context.TODO(), types.NetworkListOptions{ - Filters: filters.NewArgs(filters.Arg("name", opts.NetworkName)), - }) - if err != nil { - return nil, err - } - if len(nets) != 1 { - return nil, fmt.Errorf("expected exactly one docker network named %q, got %d", opts.NetworkName, len(nets)) - } - opts.NetworkID = nets[0].ID - } - if opts.NetworkID == "" { - opts.NetworkID = os.Getenv("TEST_DOCKER_NETWORK_ID") - } - if opts.ContainerName == "" { - if strings.Contains(opts.ImageRepo, "/") { - return nil, fmt.Errorf("ContainerName is required for non-library images") - } - // If there's no slash in the repo it's almost certainly going to be - // a good container name. - opts.ContainerName = opts.ImageRepo - } - return &Runner{ - DockerAPI: dapi, - RunOptions: opts, - }, nil -} - -type ServiceConfig interface { - Address() string - URL() *url.URL -} - -func NewServiceHostPort(host string, port int) *ServiceHostPort { - return &ServiceHostPort{address: fmt.Sprintf("%s:%d", host, port)} -} - -func NewServiceHostPortParse(s string) (*ServiceHostPort, error) { - pieces := strings.Split(s, ":") - if len(pieces) != 2 { - return nil, fmt.Errorf("address must be of the form host:port, got: %v", s) - } - - port, err := strconv.Atoi(pieces[1]) - if err != nil || port < 1 { - return nil, fmt.Errorf("address must be of the form host:port, got: %v", s) - } - - return &ServiceHostPort{s}, nil -} - -type ServiceHostPort struct { - address string -} - -func (s ServiceHostPort) Address() string { - return s.address -} - -func (s ServiceHostPort) URL() *url.URL { - return &url.URL{Host: s.address} -} - -func NewServiceURLParse(s string) (*ServiceURL, error) { - u, err := url.Parse(s) - if err != nil { - return nil, err - } - return &ServiceURL{u: *u}, nil -} - -func NewServiceURL(u url.URL) *ServiceURL { - return &ServiceURL{u: u} -} - -type ServiceURL struct { - u url.URL -} - -func (s ServiceURL) Address() string { - return s.u.Host -} - -func (s ServiceURL) URL() *url.URL { - return &s.u -} - -// ServiceAdapter verifies connectivity to the service, then returns either the -// connection string (typically a URL) and nil, or empty string and an error. -type ServiceAdapter func(ctx context.Context, host string, port int) (ServiceConfig, error) - -// StartService will start the runner's configured docker container with a -// random UUID suffix appended to the name to make it unique and will return -// either a hostname or local address depending on if a Docker network was given. -// -// Most tests can default to using this. -func (d *Runner) StartService(ctx context.Context, connect ServiceAdapter) (*Service, error) { - serv, _, err := d.StartNewService(ctx, true, false, connect) - - return serv, err -} - -type LogConsumerWriter struct { - consumer func(string) -} - -func (l LogConsumerWriter) Write(p []byte) (n int, err error) { - // TODO this assumes that we're never passed partial log lines, which - // seems a safe assumption for now based on how docker looks to implement - // logging, but might change in the future. - scanner := bufio.NewScanner(bytes.NewReader(p)) - scanner.Buffer(make([]byte, 64*1024), bufio.MaxScanTokenSize) - for scanner.Scan() { - l.consumer(scanner.Text()) - } - return len(p), nil -} - -var _ io.Writer = &LogConsumerWriter{} - -// StartNewService will start the runner's configured docker container but with the -// ability to control adding a name suffix or forcing a local address to be returned. -// 'addSuffix' will add a random UUID to the end of the container name. -// 'forceLocalAddr' will force the container address returned to be in the -// form of '127.0.0.1:1234' where 1234 is the mapped container port. -func (d *Runner) StartNewService(ctx context.Context, addSuffix, forceLocalAddr bool, connect ServiceAdapter) (*Service, string, error) { - if d.RunOptions.PreDelete { - name := d.RunOptions.ContainerName - matches, err := d.DockerAPI.ContainerList(ctx, container.ListOptions{ - All: true, - // TODO use labels to ensure we don't delete anything we shouldn't - Filters: filters.NewArgs( - filters.Arg("name", name), - ), - }) - if err != nil { - return nil, "", fmt.Errorf("failed to list containers named %q", name) - } - for _, cont := range matches { - err = d.DockerAPI.ContainerRemove(ctx, cont.ID, container.RemoveOptions{Force: true}) - if err != nil { - return nil, "", fmt.Errorf("failed to pre-delete container named %q", name) - } - } - } - result, err := d.Start(context.Background(), addSuffix, forceLocalAddr) - if err != nil { - return nil, "", err - } - - var wg sync.WaitGroup - consumeLogs := false - var logStdout, logStderr io.Writer - if d.RunOptions.LogStdout != nil && d.RunOptions.LogStderr != nil { - consumeLogs = true - logStdout = d.RunOptions.LogStdout - logStderr = d.RunOptions.LogStderr - } else if d.RunOptions.LogConsumer != nil { - consumeLogs = true - logStdout = &LogConsumerWriter{d.RunOptions.LogConsumer} - logStderr = &LogConsumerWriter{d.RunOptions.LogConsumer} - } - - // The waitgroup wg is used here to support some stuff in NewDockerCluster. - // We can't generate the PKI cert for the https listener until we know the - // container's address, meaning we must first start the container, then - // generate the cert, then copy it into the container, then signal Vault - // to reload its config/certs. However, if we SIGHUP Vault before Vault - // has installed its signal handler, that will kill Vault, since the default - // behaviour for HUP is termination. So the PostStart that NewDockerCluster - // passes in (which does all that PKI cert stuff) waits to see output from - // Vault on stdout/stderr before it sends the signal, and we don't want to - // run the PostStart until we've hooked into the docker logs. - if consumeLogs { - wg.Add(1) - go func() { - // We must run inside a goroutine because we're using Follow:true, - // and StdCopy will block until the log stream is closed. - stream, err := d.DockerAPI.ContainerLogs(context.Background(), result.Container.ID, types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Timestamps: !d.RunOptions.OmitLogTimestamps, - Details: true, - Follow: true, - }) - wg.Done() - if err != nil { - d.RunOptions.LogConsumer(fmt.Sprintf("error reading container logs: %v", err)) - } else { - _, err := stdcopy.StdCopy(logStdout, logStderr, stream) - if err != nil { - d.RunOptions.LogConsumer(fmt.Sprintf("error demultiplexing docker logs: %v", err)) - } - } - }() - } - wg.Wait() - - if d.RunOptions.PostStart != nil { - if err := d.RunOptions.PostStart(result.Container.ID, result.RealIP); err != nil { - return nil, "", fmt.Errorf("poststart failed: %w", err) - } - } - - cleanup := func() { - for i := 0; i < 10; i++ { - err := d.DockerAPI.ContainerRemove(ctx, result.Container.ID, container.RemoveOptions{Force: true}) - if err == nil || client.IsErrNotFound(err) { - return - } - time.Sleep(1 * time.Second) - } - } - - bo := backoff.NewExponentialBackOff() - bo.MaxInterval = time.Second * 5 - bo.MaxElapsedTime = 2 * time.Minute - - pieces := strings.Split(result.Addrs[0], ":") - portInt, err := strconv.Atoi(pieces[1]) - if err != nil { - return nil, "", err - } - - var config ServiceConfig - err = backoff.Retry(func() error { - container, err := d.DockerAPI.ContainerInspect(ctx, result.Container.ID) - if err != nil || !container.State.Running { - return backoff.Permanent(fmt.Errorf("failed inspect or container %q not running: %w", result.Container.ID, err)) - } - - c, err := connect(ctx, pieces[0], portInt) - if err != nil { - return err - } - if c == nil { - return fmt.Errorf("service adapter returned nil error and config") - } - config = c - return nil - }, bo) - if err != nil { - if !d.RunOptions.DoNotAutoRemove { - cleanup() - } - return nil, "", err - } - - return &Service{ - Config: config, - Cleanup: cleanup, - Container: result.Container, - StartResult: result, - }, result.Container.ID, nil -} - -// createLogConsumer returns a function to consume the logs of the container with the given ID. -// If a wait group is given, `WaitGroup.Done()` will be called as soon as the call to the -// ContainerLogs Docker API call is done. -// The returned function will block, so it should be run on a goroutine. -func (d *Runner) createLogConsumer(containerId string, wg *sync.WaitGroup) func() { - if d.RunOptions.LogStdout != nil && d.RunOptions.LogStderr != nil { - return func() { - d.consumeLogs(containerId, wg, d.RunOptions.LogStdout, d.RunOptions.LogStderr) - } - } - if d.RunOptions.LogConsumer != nil { - return func() { - d.consumeLogs(containerId, wg, &LogConsumerWriter{d.RunOptions.LogConsumer}, &LogConsumerWriter{d.RunOptions.LogConsumer}) - } - } - return nil -} - -// consumeLogs is the function called by the function returned by createLogConsumer. -func (d *Runner) consumeLogs(containerId string, wg *sync.WaitGroup, logStdout, logStderr io.Writer) { - // We must run inside a goroutine because we're using Follow:true, - // and StdCopy will block until the log stream is closed. - stream, err := d.DockerAPI.ContainerLogs(context.Background(), containerId, container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - Timestamps: !d.RunOptions.OmitLogTimestamps, - Details: true, - Follow: true, - }) - wg.Done() - if err != nil { - d.RunOptions.LogConsumer(fmt.Sprintf("error reading container logs: %v", err)) - } else { - _, err := stdcopy.StdCopy(logStdout, logStderr, stream) - if err != nil { - d.RunOptions.LogConsumer(fmt.Sprintf("error demultiplexing docker logs: %v", err)) - } - } -} - -type Service struct { - Config ServiceConfig - Cleanup func() - Container *types.ContainerJSON - StartResult *StartResult -} - -type StartResult struct { - Container *types.ContainerJSON - Addrs []string - RealIP string -} - -func (d *Runner) Start(ctx context.Context, addSuffix, forceLocalAddr bool) (*StartResult, error) { - name := d.RunOptions.ContainerName - if addSuffix { - suffix, err := uuid.GenerateUUID() - if err != nil { - return nil, err - } - name += "-" + suffix - } - - cfg := &container.Config{ - Hostname: name, - Image: fmt.Sprintf("%s:%s", d.RunOptions.ImageRepo, d.RunOptions.ImageTag), - Env: d.RunOptions.Env, - Cmd: d.RunOptions.Cmd, - } - if len(d.RunOptions.Ports) > 0 { - cfg.ExposedPorts = make(map[nat.Port]struct{}) - for _, p := range d.RunOptions.Ports { - cfg.ExposedPorts[nat.Port(p)] = struct{}{} - } - } - if len(d.RunOptions.Entrypoint) > 0 { - cfg.Entrypoint = strslice.StrSlice(d.RunOptions.Entrypoint) - } - - hostConfig := &container.HostConfig{ - AutoRemove: !d.RunOptions.DoNotAutoRemove, - PublishAllPorts: true, - } - if len(d.RunOptions.Capabilities) > 0 { - hostConfig.CapAdd = d.RunOptions.Capabilities - } - - netConfig := &network.NetworkingConfig{} - if d.RunOptions.NetworkID != "" { - netConfig.EndpointsConfig = map[string]*network.EndpointSettings{ - d.RunOptions.NetworkID: {}, - } - } - - // best-effort pull - var opts types.ImageCreateOptions - if d.RunOptions.AuthUsername != "" && d.RunOptions.AuthPassword != "" { - var buf bytes.Buffer - auth := map[string]string{ - "username": d.RunOptions.AuthUsername, - "password": d.RunOptions.AuthPassword, - } - if err := json.NewEncoder(&buf).Encode(auth); err != nil { - return nil, err - } - opts.RegistryAuth = base64.URLEncoding.EncodeToString(buf.Bytes()) - } - resp, _ := d.DockerAPI.ImageCreate(ctx, cfg.Image, opts) - if resp != nil { - _, _ = ioutil.ReadAll(resp) - } - - for vol, mtpt := range d.RunOptions.VolumeNameToMountPoint { - hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ - Type: "volume", - Source: vol, - Target: mtpt, - ReadOnly: false, - }) - } - - c, err := d.DockerAPI.ContainerCreate(ctx, cfg, hostConfig, netConfig, nil, cfg.Hostname) - if err != nil { - return nil, fmt.Errorf("container create failed: %v", err) - } - - for from, to := range d.RunOptions.CopyFromTo { - if err := copyToContainer(ctx, d.DockerAPI, c.ID, from, to); err != nil { - _ = d.DockerAPI.ContainerRemove(ctx, c.ID, container.RemoveOptions{}) - return nil, err - } - } - - err = d.DockerAPI.ContainerStart(ctx, c.ID, container.StartOptions{}) - if err != nil { - _ = d.DockerAPI.ContainerRemove(ctx, c.ID, container.RemoveOptions{}) - return nil, fmt.Errorf("container start failed: %v", err) - } - - inspect, err := d.DockerAPI.ContainerInspect(ctx, c.ID) - if err != nil { - _ = d.DockerAPI.ContainerRemove(ctx, c.ID, container.RemoveOptions{}) - return nil, err - } - - var addrs []string - for _, port := range d.RunOptions.Ports { - pieces := strings.Split(port, "/") - if len(pieces) < 2 { - return nil, fmt.Errorf("expected port of the form 1234/tcp, got: %s", port) - } - if d.RunOptions.NetworkID != "" && !forceLocalAddr { - addrs = append(addrs, fmt.Sprintf("%s:%s", cfg.Hostname, pieces[0])) - } else { - mapped, ok := inspect.NetworkSettings.Ports[nat.Port(port)] - if !ok || len(mapped) == 0 { - return nil, fmt.Errorf("no port mapping found for %s", port) - } - addrs = append(addrs, fmt.Sprintf("127.0.0.1:%s", mapped[0].HostPort)) - } - } - - var realIP string - if d.RunOptions.NetworkID == "" { - if len(inspect.NetworkSettings.Networks) > 1 { - return nil, fmt.Errorf("Set d.RunOptions.NetworkName instead for container with multiple networks: %v", inspect.NetworkSettings.Networks) - } - for _, network := range inspect.NetworkSettings.Networks { - realIP = network.IPAddress - break - } - } else { - realIP = inspect.NetworkSettings.Networks[d.RunOptions.NetworkName].IPAddress - } - - return &StartResult{ - Container: &inspect, - Addrs: addrs, - RealIP: realIP, - }, nil -} - -func (d *Runner) RefreshFiles(ctx context.Context, containerID string) error { - for from, to := range d.RunOptions.CopyFromTo { - if err := copyToContainer(ctx, d.DockerAPI, containerID, from, to); err != nil { - // TODO too drastic? - _ = d.DockerAPI.ContainerRemove(ctx, containerID, container.RemoveOptions{}) - return err - } - } - return d.DockerAPI.ContainerKill(ctx, containerID, "SIGHUP") -} - -func (d *Runner) Stop(ctx context.Context, containerID string) error { - if d.RunOptions.NetworkID != "" { - if err := d.DockerAPI.NetworkDisconnect(ctx, d.RunOptions.NetworkID, containerID, true); err != nil { - return fmt.Errorf("error disconnecting network (%v): %v", d.RunOptions.NetworkID, err) - } - } - - // timeout in seconds - timeout := 5 - options := container.StopOptions{ - Timeout: &timeout, - } - if err := d.DockerAPI.ContainerStop(ctx, containerID, options); err != nil { - return fmt.Errorf("error stopping container: %v", err) - } - - return nil -} - -func (d *Runner) Restart(ctx context.Context, containerID string) error { - if err := d.DockerAPI.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { - return err - } - - ends := &network.EndpointSettings{ - NetworkID: d.RunOptions.NetworkID, - } - - return d.DockerAPI.NetworkConnect(ctx, d.RunOptions.NetworkID, containerID, ends) -} - -func copyToContainer(ctx context.Context, dapi *client.Client, containerID, from, to string) error { - srcInfo, err := archive.CopyInfoSourcePath(from, false) - if err != nil { - return fmt.Errorf("error copying from source %q: %v", from, err) - } - - srcArchive, err := archive.TarResource(srcInfo) - if err != nil { - return fmt.Errorf("error creating tar from source %q: %v", from, err) - } - defer srcArchive.Close() - - dstInfo := archive.CopyInfo{Path: to} - - dstDir, content, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) - if err != nil { - return fmt.Errorf("error preparing copy from %q -> %q: %v", from, to, err) - } - defer content.Close() - err = dapi.CopyToContainer(ctx, containerID, dstDir, content, types.CopyToContainerOptions{}) - if err != nil { - return fmt.Errorf("error copying from %q -> %q: %v", from, to, err) - } - - return nil -} - -type RunCmdOpt interface { - Apply(cfg *types.ExecConfig) error -} - -type RunCmdUser string - -var _ RunCmdOpt = (*RunCmdUser)(nil) - -func (u RunCmdUser) Apply(cfg *types.ExecConfig) error { - cfg.User = string(u) - return nil -} - -func (d *Runner) RunCmdWithOutput(ctx context.Context, container string, cmd []string, opts ...RunCmdOpt) ([]byte, []byte, int, error) { - return RunCmdWithOutput(d.DockerAPI, ctx, container, cmd, opts...) -} - -func RunCmdWithOutput(api *client.Client, ctx context.Context, container string, cmd []string, opts ...RunCmdOpt) ([]byte, []byte, int, error) { - runCfg := types.ExecConfig{ - AttachStdout: true, - AttachStderr: true, - Cmd: cmd, - } - - for index, opt := range opts { - if err := opt.Apply(&runCfg); err != nil { - return nil, nil, -1, fmt.Errorf("error applying option (%d / %v): %w", index, opt, err) - } - } - - ret, err := api.ContainerExecCreate(ctx, container, runCfg) - if err != nil { - return nil, nil, -1, fmt.Errorf("error creating execution environment: %v\ncfg: %v\n", err, runCfg) - } - - resp, err := api.ContainerExecAttach(ctx, ret.ID, types.ExecStartCheck{}) - if err != nil { - return nil, nil, -1, fmt.Errorf("error attaching to command execution: %v\ncfg: %v\nret: %v\n", err, runCfg, ret) - } - defer resp.Close() - - var stdoutB bytes.Buffer - var stderrB bytes.Buffer - if _, err := stdcopy.StdCopy(&stdoutB, &stderrB, resp.Reader); err != nil { - return nil, nil, -1, fmt.Errorf("error reading command output: %v", err) - } - - stdout := stdoutB.Bytes() - stderr := stderrB.Bytes() - - // Fetch return code. - info, err := api.ContainerExecInspect(ctx, ret.ID) - if err != nil { - return stdout, stderr, -1, fmt.Errorf("error reading command exit code: %v", err) - } - - return stdout, stderr, info.ExitCode, nil -} - -func (d *Runner) RunCmdInBackground(ctx context.Context, container string, cmd []string, opts ...RunCmdOpt) (string, error) { - return RunCmdInBackground(d.DockerAPI, ctx, container, cmd, opts...) -} - -func RunCmdInBackground(api *client.Client, ctx context.Context, container string, cmd []string, opts ...RunCmdOpt) (string, error) { - runCfg := types.ExecConfig{ - AttachStdout: true, - AttachStderr: true, - Cmd: cmd, - } - - for index, opt := range opts { - if err := opt.Apply(&runCfg); err != nil { - return "", fmt.Errorf("error applying option (%d / %v): %w", index, opt, err) - } - } - - ret, err := api.ContainerExecCreate(ctx, container, runCfg) - if err != nil { - return "", fmt.Errorf("error creating execution environment: %w\ncfg: %v\n", err, runCfg) - } - - err = api.ContainerExecStart(ctx, ret.ID, types.ExecStartCheck{}) - if err != nil { - return "", fmt.Errorf("error starting command execution: %w\ncfg: %v\nret: %v\n", err, runCfg, ret) - } - - return ret.ID, nil -} - -// Mapping of path->contents -type PathContents interface { - UpdateHeader(header *tar.Header) error - Get() ([]byte, error) - SetMode(mode int64) - SetOwners(uid int, gid int) -} - -type FileContents struct { - Data []byte - Mode int64 - UID int - GID int -} - -func (b FileContents) UpdateHeader(header *tar.Header) error { - header.Mode = b.Mode - header.Uid = b.UID - header.Gid = b.GID - return nil -} - -func (b FileContents) Get() ([]byte, error) { - return b.Data, nil -} - -func (b *FileContents) SetMode(mode int64) { - b.Mode = mode -} - -func (b *FileContents) SetOwners(uid int, gid int) { - b.UID = uid - b.GID = gid -} - -func PathContentsFromBytes(data []byte) PathContents { - return &FileContents{ - Data: data, - Mode: 0o644, - } -} - -func PathContentsFromString(data string) PathContents { - return PathContentsFromBytes([]byte(data)) -} - -type BuildContext map[string]PathContents - -func NewBuildContext() BuildContext { - return BuildContext{} -} - -func BuildContextFromTarball(reader io.Reader) (BuildContext, error) { - archive := tar.NewReader(reader) - bCtx := NewBuildContext() - - for true { - header, err := archive.Next() - if err != nil { - if err == io.EOF { - break - } - - return nil, fmt.Errorf("failed to parse provided tarball: %v", err) - } - - data := make([]byte, int(header.Size)) - read, err := archive.Read(data) - if err != nil { - return nil, fmt.Errorf("failed to parse read from provided tarball: %v", err) - } - - if read != int(header.Size) { - return nil, fmt.Errorf("unexpectedly short read on tarball: %v of %v", read, header.Size) - } - - bCtx[header.Name] = &FileContents{ - Data: data, - Mode: header.Mode, - UID: header.Uid, - GID: header.Gid, - } - } - - return bCtx, nil -} - -func (bCtx *BuildContext) ToTarball() (io.Reader, error) { - var err error - buffer := new(bytes.Buffer) - tarBuilder := tar.NewWriter(buffer) - defer tarBuilder.Close() - - now := time.Now() - for filepath, contents := range *bCtx { - fileHeader := &tar.Header{ - Name: filepath, - ModTime: now, - AccessTime: now, - ChangeTime: now, - } - if contents == nil && !strings.HasSuffix(filepath, "/") { - return nil, fmt.Errorf("expected file path (%v) to have trailing / due to nil contents, indicating directory", filepath) - } - - if err := contents.UpdateHeader(fileHeader); err != nil { - return nil, fmt.Errorf("failed to update tar header entry for %v: %w", filepath, err) - } - - var rawContents []byte - if contents != nil { - rawContents, err = contents.Get() - if err != nil { - return nil, fmt.Errorf("failed to get file contents for %v: %w", filepath, err) - } - - fileHeader.Size = int64(len(rawContents)) - } - - if err := tarBuilder.WriteHeader(fileHeader); err != nil { - return nil, fmt.Errorf("failed to write tar header entry for %v: %w", filepath, err) - } - - if contents != nil { - if _, err := tarBuilder.Write(rawContents); err != nil { - return nil, fmt.Errorf("failed to write tar file entry for %v: %w", filepath, err) - } - } - } - - return bytes.NewReader(buffer.Bytes()), nil -} - -type BuildOpt interface { - Apply(cfg *types.ImageBuildOptions) error -} - -type BuildRemove bool - -var _ BuildOpt = (*BuildRemove)(nil) - -func (u BuildRemove) Apply(cfg *types.ImageBuildOptions) error { - cfg.Remove = bool(u) - return nil -} - -type BuildForceRemove bool - -var _ BuildOpt = (*BuildForceRemove)(nil) - -func (u BuildForceRemove) Apply(cfg *types.ImageBuildOptions) error { - cfg.ForceRemove = bool(u) - return nil -} - -type BuildPullParent bool - -var _ BuildOpt = (*BuildPullParent)(nil) - -func (u BuildPullParent) Apply(cfg *types.ImageBuildOptions) error { - cfg.PullParent = bool(u) - return nil -} - -type BuildArgs map[string]*string - -var _ BuildOpt = (*BuildArgs)(nil) - -func (u BuildArgs) Apply(cfg *types.ImageBuildOptions) error { - cfg.BuildArgs = u - return nil -} - -type BuildTags []string - -var _ BuildOpt = (*BuildTags)(nil) - -func (u BuildTags) Apply(cfg *types.ImageBuildOptions) error { - cfg.Tags = u - return nil -} - -const containerfilePath = "_containerfile" - -func (d *Runner) BuildImage(ctx context.Context, containerfile string, containerContext BuildContext, opts ...BuildOpt) ([]byte, error) { - return BuildImage(ctx, d.DockerAPI, containerfile, containerContext, opts...) -} - -func BuildImage(ctx context.Context, api *client.Client, containerfile string, containerContext BuildContext, opts ...BuildOpt) ([]byte, error) { - var cfg types.ImageBuildOptions - - // Build container context tarball, provisioning containerfile in. - containerContext[containerfilePath] = PathContentsFromBytes([]byte(containerfile)) - tar, err := containerContext.ToTarball() - if err != nil { - return nil, fmt.Errorf("failed to create build image context tarball: %w", err) - } - cfg.Dockerfile = "/" + containerfilePath - - // Apply all given options - for index, opt := range opts { - if err := opt.Apply(&cfg); err != nil { - return nil, fmt.Errorf("failed to apply option (%d / %v): %w", index, opt, err) - } - } - - resp, err := api.ImageBuild(ctx, tar, cfg) - if err != nil { - return nil, fmt.Errorf("failed to build image: %v", err) - } - - output, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read image build output: %w", err) - } - - return output, nil -} - -func (d *Runner) CopyTo(container string, destination string, contents BuildContext) error { - // XXX: currently we use the default options but we might want to allow - // modifying cfg.CopyUIDGID in the future. - var cfg types.CopyToContainerOptions - - // Convert our provided contents to a tarball to ship up. - tar, err := contents.ToTarball() - if err != nil { - return fmt.Errorf("failed to build contents into tarball: %v", err) - } - - return d.DockerAPI.CopyToContainer(context.Background(), container, destination, tar, cfg) -} - -func (d *Runner) CopyFrom(container string, source string) (BuildContext, *types.ContainerPathStat, error) { - reader, stat, err := d.DockerAPI.CopyFromContainer(context.Background(), container, source) - if err != nil { - return nil, nil, fmt.Errorf("failed to read %v from container: %v", source, err) - } - - result, err := BuildContextFromTarball(reader) - if err != nil { - return nil, nil, fmt.Errorf("failed to build archive from result: %v", err) - } - - return result, &stat, nil -} - -func (d *Runner) GetNetworkAndAddresses(container string) (map[string]string, error) { - response, err := d.DockerAPI.ContainerInspect(context.Background(), container) - if err != nil { - return nil, fmt.Errorf("failed to fetch container inspection data: %v", err) - } - - if response.NetworkSettings == nil || len(response.NetworkSettings.Networks) == 0 { - return nil, fmt.Errorf("container (%v) had no associated network settings: %v", container, response) - } - - ret := make(map[string]string) - ns := response.NetworkSettings.Networks - for network, data := range ns { - if data == nil { - continue - } - - ret[network] = data.IPAddress - } - - if len(ret) == 0 { - return nil, fmt.Errorf("no valid network data for container (%v): %v", container, response) - } - - return ret, nil -} diff --git a/sdk/helper/identitytpl/templating_test.go b/sdk/helper/identitytpl/templating_test.go deleted file mode 100644 index d17409e78..000000000 --- a/sdk/helper/identitytpl/templating_test.go +++ /dev/null @@ -1,558 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package identitytpl - -import ( - "errors" - "fmt" - "math" - "strconv" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/logical" -) - -// intentionally != time.Now() to catch latent used of time.Now instead of -// passed in values -var testNow = time.Now().Add(100 * time.Hour) - -func TestPopulate_Basic(t *testing.T) { - tests := []struct { - mode int - name string - input string - output string - err error - entityName string - metadata map[string]string - aliasAccessor string - aliasID string - aliasName string - nilEntity bool - validityCheckOnly bool - aliasMetadata map[string]string - aliasCustomMetadata map[string]string - groupName string - groupMetadata map[string]string - groupMemberships []string - now time.Time - }{ - // time.* tests. Keep tests with time.Now() at the front to avoid false - // positives due to the second changing during the test - { - name: "time now", - input: "{{time.now}}", - output: strconv.Itoa(int(testNow.Unix())), - now: testNow, - }, - { - name: "time plus", - input: "{{time.now.plus.1h}}", - output: strconv.Itoa(int(testNow.Unix() + (60 * 60))), - now: testNow, - }, - { - name: "time plus", - input: "{{time.now.minus.5m}}", - output: strconv.Itoa(int(testNow.Unix() - (5 * 60))), - now: testNow, - }, - { - name: "invalid operator", - input: "{{time.now.divide.5m}}", - err: errors.New("invalid time operator \"divide\""), - }, - { - name: "time missing operand", - input: "{{time.now.plus}}", - err: errors.New("missing time operand"), - }, - - { - name: "no_templating", - input: "path foobar {", - output: "path foobar {", - }, - { - name: "only_closing", - input: "path foobar}} {", - err: ErrUnbalancedTemplatingCharacter, - }, - { - name: "closing_in_front", - input: "path }} {{foobar}} {", - err: ErrUnbalancedTemplatingCharacter, - }, - { - name: "closing_in_back", - input: "path {{foobar}} }}", - err: ErrUnbalancedTemplatingCharacter, - }, - { - name: "basic", - input: "path /{{identity.entity.id}}/ {", - output: "path /entityID/ {", - }, - { - name: "multiple", - input: "path {{identity.entity.name}} {\n\tval = {{identity.entity.metadata.foo}}\n}", - entityName: "entityName", - metadata: map[string]string{"foo": "bar"}, - output: "path entityName {\n\tval = bar\n}", - }, - { - name: "multiple_bad_name", - input: "path {{identity.entity.name}} {\n\tval = {{identity.entity.metadata.foo}}\n}", - metadata: map[string]string{"foo": "bar"}, - err: ErrTemplateValueNotFound, - }, - { - name: "unbalanced_close", - input: "path {{identity.entity.id}} {\n\tval = {{ent}}ity.metadata.foo}}\n}", - err: ErrUnbalancedTemplatingCharacter, - }, - { - name: "unbalanced_open", - input: "path {{identity.entity.id}} {\n\tval = {{ent{{ity.metadata.foo}}\n}", - err: ErrUnbalancedTemplatingCharacter, - }, - { - name: "no_entity_no_directives", - input: "path {{identity.entity.id}} {\n\tval = {{ent{{ity.metadata.foo}}\n}", - err: ErrNoEntityAttachedToToken, - nilEntity: true, - }, - { - name: "no_entity_no_diretives", - input: "path name {\n\tval = foo\n}", - output: "path name {\n\tval = foo\n}", - nilEntity: true, - }, - { - name: "alias_id_name", - input: "path {{ identity.entity.name}} {\n\tval = {{identity.entity.aliases.foomount.id}} nval = {{identity.entity.aliases.foomount.name}}\n}", - entityName: "entityName", - aliasAccessor: "foomount", - aliasID: "aliasID", - aliasName: "aliasName", - metadata: map[string]string{"foo": "bar"}, - output: "path entityName {\n\tval = aliasID nval = aliasName\n}", - }, - { - name: "alias_id_name_bad_selector", - input: "path foobar {\n\tval = {{identity.entity.aliases.foomount}}\n}", - aliasAccessor: "foomount", - err: errors.New("invalid alias selector"), - }, - { - name: "alias_id_name_bad_accessor", - input: "path \"foobar\" {\n\tval = {{identity.entity.aliases.barmount.id}}\n}", - aliasAccessor: "foomount", - err: errors.New("alias not found"), - }, - { - name: "alias_id_name", - input: "path \"{{identity.entity.name}}\" {\n\tval = {{identity.entity.aliases.foomount.metadata.zip}}\n}", - entityName: "entityName", - aliasAccessor: "foomount", - aliasID: "aliasID", - metadata: map[string]string{"foo": "bar"}, - aliasMetadata: map[string]string{"zip": "zap"}, - output: "path \"entityName\" {\n\tval = zap\n}", - }, - { - name: "group_name", - input: "path \"{{identity.groups.ids.groupID.name}}\" {\n\tval = {{identity.entity.name}}\n}", - entityName: "entityName", - groupName: "groupName", - output: "path \"groupName\" {\n\tval = entityName\n}", - }, - { - name: "group_bad_id", - input: "path \"{{identity.groups.ids.hroupID.name}}\" {\n\tval = {{identity.entity.name}}\n}", - entityName: "entityName", - groupName: "groupName", - err: errors.New("entity is not a member of group \"hroupID\""), - }, - { - name: "group_id", - input: "path \"{{identity.groups.names.groupName.id}}\" {\n\tval = {{identity.entity.name}}\n}", - entityName: "entityName", - groupName: "groupName", - output: "path \"groupID\" {\n\tval = entityName\n}", - }, - { - name: "group_bad_name", - input: "path \"{{identity.groups.names.hroupName.id}}\" {\n\tval = {{identity.entity.name}}\n}", - entityName: "entityName", - groupName: "groupName", - err: errors.New("entity is not a member of group \"hroupName\""), - }, - { - name: "metadata_object_disallowed", - input: "{{identity.entity.metadata}}", - metadata: map[string]string{"foo": "bar"}, - err: ErrTemplateValueNotFound, - }, - { - name: "alias_metadata_object_disallowed", - input: "{{identity.entity.aliases.foomount.metadata}}", - aliasAccessor: "foomount", - aliasMetadata: map[string]string{"foo": "bar"}, - err: ErrTemplateValueNotFound, - }, - { - name: "groups.names_disallowed", - input: "{{identity.entity.groups.names}}", - groupMemberships: []string{"foo", "bar"}, - err: ErrTemplateValueNotFound, - }, - { - name: "groups.ids_disallowed", - input: "{{identity.entity.groups.ids}}", - groupMemberships: []string{"foo", "bar"}, - err: ErrTemplateValueNotFound, - }, - - // missing selector cases - { - mode: JSONTemplating, - name: "entity id", - input: "{{identity.entity.id}}", - output: `"entityID"`, - }, - { - mode: JSONTemplating, - name: "entity name", - input: "{{identity.entity.name}}", - entityName: "entityName", - output: `"entityName"`, - }, - { - mode: JSONTemplating, - name: "entity name missing", - input: "{{identity.entity.name}}", - output: `""`, - }, - { - mode: JSONTemplating, - name: "alias name/id", - input: "{{identity.entity.aliases.foomount.id}} {{identity.entity.aliases.foomount.name}}", - aliasAccessor: "foomount", - aliasID: "aliasID", - aliasName: "aliasName", - output: `"aliasID" "aliasName"`, - }, - { - mode: JSONTemplating, - name: "one metadata key", - input: "{{identity.entity.metadata.color}}", - metadata: map[string]string{"foo": "bar", "color": "green"}, - output: `"green"`, - }, - { - mode: JSONTemplating, - name: "one metadata key not found", - input: "{{identity.entity.metadata.size}}", - metadata: map[string]string{"foo": "bar", "color": "green"}, - output: `""`, - }, - { - mode: JSONTemplating, - name: "all entity metadata", - input: "{{identity.entity.metadata}}", - metadata: map[string]string{"foo": "bar", "color": "green"}, - output: `{"color":"green","foo":"bar"}`, - }, - { - mode: JSONTemplating, - name: "null entity metadata", - input: "{{identity.entity.metadata}}", - output: `{}`, - }, - { - mode: JSONTemplating, - name: "groups.names", - input: "{{identity.entity.groups.names}}", - groupMemberships: []string{"foo", "bar"}, - output: `["foo","bar"]`, - }, - { - mode: JSONTemplating, - name: "groups.ids", - input: "{{identity.entity.groups.ids}}", - groupMemberships: []string{"foo", "bar"}, - output: `["foo_0","bar_1"]`, - }, - { - mode: JSONTemplating, - name: "one alias metadata key", - input: "{{identity.entity.aliases.aws_123.metadata.color}}", - aliasAccessor: "aws_123", - aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, - output: `"green"`, - }, - { - mode: JSONTemplating, - name: "one alias metadata key not found", - input: "{{identity.entity.aliases.aws_123.metadata.size}}", - aliasAccessor: "aws_123", - aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, - output: `""`, - }, - { - mode: JSONTemplating, - name: "one alias metadata, accessor not found", - input: "{{identity.entity.aliases.aws_123.metadata.size}}", - aliasAccessor: "not_gonna_match", - aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, - output: `""`, - }, - { - mode: JSONTemplating, - name: "all alias metadata", - input: "{{identity.entity.aliases.aws_123.metadata}}", - aliasAccessor: "aws_123", - aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, - output: `{"color":"green","foo":"bar"}`, - }, - { - mode: JSONTemplating, - name: "null alias metadata", - input: "{{identity.entity.aliases.aws_123.metadata}}", - aliasAccessor: "aws_123", - output: `{}`, - }, - { - mode: JSONTemplating, - name: "all alias metadata, accessor not found", - input: "{{identity.entity.aliases.aws_123.metadata}}", - aliasAccessor: "not_gonna_match", - aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, - output: `{}`, - }, - { - mode: JSONTemplating, - name: "one alias custom metadata key", - input: "{{identity.entity.aliases.aws_123.custom_metadata.foo}}", - aliasAccessor: "aws_123", - aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, - output: `"abc"`, - }, - { - mode: JSONTemplating, - name: "one alias custom metadata key not found", - input: "{{identity.entity.aliases.aws_123.custom_metadata.size}}", - aliasAccessor: "aws_123", - aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, - output: `""`, - }, - { - mode: JSONTemplating, - name: "one alias custom metadata, accessor not found", - input: "{{identity.entity.aliases.aws_123.custom_metadata.size}}", - aliasAccessor: "not_gonna_match", - aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, - output: `""`, - }, - { - mode: JSONTemplating, - name: "all alias custom metadata", - input: "{{identity.entity.aliases.aws_123.custom_metadata}}", - aliasAccessor: "aws_123", - aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, - output: `{"bar":"123","foo":"abc"}`, - }, - { - mode: JSONTemplating, - name: "null alias custom metadata", - input: "{{identity.entity.aliases.aws_123.custom_metadata}}", - aliasAccessor: "aws_123", - output: `{}`, - }, - { - mode: JSONTemplating, - name: "all alias custom metadata, accessor not found", - input: "{{identity.entity.aliases.aws_123.custom_metadata}}", - aliasAccessor: "not_gonna_match", - aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, - output: `{}`, - }, - } - - for _, test := range tests { - var entity *logical.Entity - if !test.nilEntity { - entity = &logical.Entity{ - ID: "entityID", - Name: test.entityName, - Metadata: test.metadata, - } - } - if test.aliasAccessor != "" { - entity.Aliases = []*logical.Alias{ - { - MountAccessor: test.aliasAccessor, - ID: test.aliasID, - Name: test.aliasName, - Metadata: test.aliasMetadata, - CustomMetadata: test.aliasCustomMetadata, - }, - } - } - var groups []*logical.Group - if test.groupName != "" { - groups = append(groups, &logical.Group{ - ID: "groupID", - Name: test.groupName, - Metadata: test.groupMetadata, - NamespaceID: "root", - }) - } - - if test.groupMemberships != nil { - for i, groupName := range test.groupMemberships { - groups = append(groups, &logical.Group{ - ID: fmt.Sprintf("%s_%d", groupName, i), - Name: groupName, - }) - } - } - - subst, out, err := PopulateString(PopulateStringInput{ - Mode: test.mode, - ValidityCheckOnly: test.validityCheckOnly, - String: test.input, - Entity: entity, - Groups: groups, - NamespaceID: "root", - Now: test.now, - }) - if err != nil { - if test.err == nil { - t.Fatalf("%s: expected success, got error: %v", test.name, err) - } - if err.Error() != test.err.Error() { - t.Fatalf("%s: got error: %v", test.name, err) - } - } - if out != test.output { - t.Fatalf("%s: bad output: %s, expected: %s", test.name, out, test.output) - } - if err == nil && !subst && out != test.input { - t.Fatalf("%s: bad subst flag", test.name) - } - } -} - -func TestPopulate_CurrentTime(t *testing.T) { - now := time.Now() - - // Test that an unset Now parameter results in current time - input := PopulateStringInput{ - Mode: JSONTemplating, - String: `{{time.now}}`, - } - - _, out, err := PopulateString(input) - if err != nil { - t.Fatal(err) - } - - nowPopulated, err := strconv.Atoi(out) - if err != nil { - t.Fatal(err) - } - - diff := math.Abs(float64(int64(nowPopulated) - now.Unix())) - if diff > 1 { - t.Fatalf("expected time within 1 second. Got diff of: %f", diff) - } -} - -func TestPopulate_FullObject(t *testing.T) { - testEntity := &logical.Entity{ - ID: "abc-123", - Name: "Entity Name", - Metadata: map[string]string{ - "color": "green", - "size": "small", - "non-printable": "\"\n\t", - }, - Aliases: []*logical.Alias{ - { - MountAccessor: "aws_123", - Metadata: map[string]string{ - "service": "ec2", - "region": "west", - }, - CustomMetadata: map[string]string{ - "foo": "abc", - "bar": "123", - }, - }, - }, - } - - testGroups := []*logical.Group{ - {ID: "a08b0c02", Name: "g1"}, - {ID: "239bef91", Name: "g2"}, - } - - template := ` - { - "id": {{identity.entity.id}}, - "name": {{identity.entity.name}}, - "all metadata": {{identity.entity.metadata}}, - "one metadata key": {{identity.entity.metadata.color}}, - "one metadata key not found": {{identity.entity.metadata.asldfk}}, - "alias metadata": {{identity.entity.aliases.aws_123.metadata}}, - "alias not found metadata": {{identity.entity.aliases.blahblah.metadata}}, - "one alias metadata key": {{identity.entity.aliases.aws_123.metadata.service}}, - "one not found alias metadata key": {{identity.entity.aliases.blahblah.metadata.service}}, - "group names": {{identity.entity.groups.names}}, - "group ids": {{identity.entity.groups.ids}}, - "repeated and": {"nested element": {{identity.entity.name}}}, - "alias custom metadata": {{identity.entity.aliases.aws_123.custom_metadata}}, - "alias not found custom metadata": {{identity.entity.aliases.blahblah.custom_metadata}}, - "one alias custom metadata key": {{identity.entity.aliases.aws_123.custom_metadata.foo}}, - "one not found alias custom metadata key": {{identity.entity.aliases.blahblah.custom_metadata.foo}}, - }` - - expected := ` - { - "id": "abc-123", - "name": "Entity Name", - "all metadata": {"color":"green","non-printable":"\"\n\t","size":"small"}, - "one metadata key": "green", - "one metadata key not found": "", - "alias metadata": {"region":"west","service":"ec2"}, - "alias not found metadata": {}, - "one alias metadata key": "ec2", - "one not found alias metadata key": "", - "group names": ["g1","g2"], - "group ids": ["a08b0c02","239bef91"], - "repeated and": {"nested element": "Entity Name"}, - "alias custom metadata": {"bar":"123","foo":"abc"}, - "alias not found custom metadata": {}, - "one alias custom metadata key": "abc", - "one not found alias custom metadata key": "", - }` - - input := PopulateStringInput{ - Mode: JSONTemplating, - String: template, - Entity: testEntity, - Groups: testGroups, - } - _, out, err := PopulateString(input) - if err != nil { - t.Fatal(err) - } - - if out != expected { - t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, out) - } -} diff --git a/sdk/helper/jsonutil/json_test.go b/sdk/helper/jsonutil/json_test.go deleted file mode 100644 index 10aabf1b9..000000000 --- a/sdk/helper/jsonutil/json_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package jsonutil - -import ( - "bytes" - "compress/gzip" - "fmt" - "reflect" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/helper/compressutil" -) - -func TestJSONUtil_CompressDecompressJSON(t *testing.T) { - expected := map[string]interface{}{ - "test": "data", - "validation": "process", - } - - // Compress an object - compressedBytes, err := EncodeJSONAndCompress(expected, nil) - if err != nil { - t.Fatal(err) - } - if len(compressedBytes) == 0 { - t.Fatal("expected compressed data") - } - - // Check if canary is present in the compressed data - if compressedBytes[0] != compressutil.CompressionCanaryGzip { - t.Fatalf("canary missing in compressed data") - } - - // Decompress and decode the compressed information and verify the functional - // behavior - var actual map[string]interface{} - if err = DecodeJSON(compressedBytes, &actual); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual) - } - for key := range actual { - delete(actual, key) - } - - // Test invalid data - if err = DecodeJSON([]byte{}, &actual); err == nil { - t.Fatalf("expected a failure") - } - - // Test invalid data after the canary byte - var buf bytes.Buffer - buf.Write([]byte{compressutil.CompressionCanaryGzip}) - if err = DecodeJSON(buf.Bytes(), &actual); err == nil { - t.Fatalf("expected a failure") - } - - // Compress an object - compressedBytes, err = EncodeJSONAndCompress(expected, &compressutil.CompressionConfig{ - Type: compressutil.CompressionTypeGzip, - GzipCompressionLevel: gzip.BestSpeed, - }) - if err != nil { - t.Fatal(err) - } - if len(compressedBytes) == 0 { - t.Fatal("expected compressed data") - } - - // Check if canary is present in the compressed data - if compressedBytes[0] != compressutil.CompressionCanaryGzip { - t.Fatalf("canary missing in compressed data") - } - - // Decompress and decode the compressed information and verify the functional - // behavior - if err = DecodeJSON(compressedBytes, &actual); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual) - } -} - -func TestJSONUtil_EncodeJSON(t *testing.T) { - input := map[string]interface{}{ - "test": "data", - "validation": "process", - } - - actualBytes, err := EncodeJSON(input) - if err != nil { - t.Fatalf("failed to encode JSON: %v", err) - } - - actual := strings.TrimSpace(string(actualBytes)) - expected := `{"test":"data","validation":"process"}` - - if actual != expected { - t.Fatalf("bad: encoded JSON: expected:%s\nactual:%s\n", expected, string(actualBytes)) - } -} - -func TestJSONUtil_DecodeJSON(t *testing.T) { - input := `{"test":"data","validation":"process"}` - - var actual map[string]interface{} - - err := DecodeJSON([]byte(input), &actual) - if err != nil { - fmt.Printf("decoding err: %v\n", err) - } - - expected := map[string]interface{}{ - "test": "data", - "validation": "process", - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) - } -} - -func TestJSONUtil_DecodeJSONFromReader(t *testing.T) { - input := `{"test":"data","validation":"process"}` - - var actual map[string]interface{} - - err := DecodeJSONFromReader(bytes.NewReader([]byte(input)), &actual) - if err != nil { - fmt.Printf("decoding err: %v\n", err) - } - - expected := map[string]interface{}{ - "test": "data", - "validation": "process", - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) - } -} diff --git a/sdk/helper/kdf/kdf_test.go b/sdk/helper/kdf/kdf_test.go deleted file mode 100644 index ed5c0a13d..000000000 --- a/sdk/helper/kdf/kdf_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package kdf - -import ( - "bytes" - "testing" -) - -func TestCounterMode(t *testing.T) { - key := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} - context := []byte("the quick brown fox") - prf := HMACSHA256PRF - prfLen := HMACSHA256PRFLen - - // Expect256 was generated in python with - // import hashlib, hmac - // hash = hashlib.sha256 - // context = "the quick brown fox" - // key = "".join([chr(x) for x in range(1, 17)]) - // inp = "\x00\x00\x00\x00"+context+"\x00\x00\x01\x00" - // digest = hmac.HMAC(key, inp, hash).digest() - // print [ord(x) for x in digest] - expect256 := []byte{ - 219, 25, 238, 6, 185, 236, 180, 64, 248, 152, 251, - 153, 79, 5, 141, 222, 66, 200, 66, 143, 40, 3, 101, 221, 206, 163, 102, - 80, 88, 234, 87, 157, - } - - for _, l := range []uint32{128, 256, 384, 1024} { - out, err := CounterMode(prf, prfLen, key, context, l) - if err != nil { - t.Fatalf("err: %v", err) - } - - if uint32(len(out)*8) != l { - t.Fatalf("bad length: %#v", out) - } - - if bytes.Contains(out, key) { - t.Fatalf("output contains key") - } - - if l == 256 && !bytes.Equal(out, expect256) { - t.Fatalf("mis-match") - } - } -} - -func TestHMACSHA256PRF(t *testing.T) { - key := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} - data := []byte("foobarbaz") - out, err := HMACSHA256PRF(key, data) - if err != nil { - t.Fatalf("err: %v", err) - } - - if uint32(len(out)*8) != HMACSHA256PRFLen { - t.Fatalf("Bad len") - } - - // Expect was generated in python with: - // import hashlib, hmac - // hash = hashlib.sha256 - // msg = "foobarbaz" - // key = "".join([chr(x) for x in range(1, 17)]) - // hm = hmac.HMAC(key, msg, hash) - // print [ord(x) for x in hm.digest()] - expect := []byte{ - 9, 50, 146, 8, 188, 130, 150, 107, 205, 147, 82, 170, - 253, 183, 26, 38, 167, 194, 220, 111, 56, 118, 219, 209, 31, 52, 137, - 90, 246, 133, 191, 124, - } - if !bytes.Equal(expect, out) { - t.Fatalf("mis-matched output") - } -} diff --git a/sdk/helper/keysutil/encrypted_key_storage_test.go b/sdk/helper/keysutil/encrypted_key_storage_test.go deleted file mode 100644 index 5147027fc..000000000 --- a/sdk/helper/keysutil/encrypted_key_storage_test.go +++ /dev/null @@ -1,325 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package keysutil - -import ( - "context" - "crypto/rand" - "fmt" - "reflect" - "testing" - - "github.com/hashicorp/go-secure-stdlib/strutil" - "github.com/hashicorp/vault/sdk/logical" -) - -var compilerOpt []string - -func TestEncrytedKeysStorage_BadPolicy(t *testing.T) { - policy := NewPolicy(PolicyConfig{ - Name: "metadata", - Type: KeyType_AES256_GCM96, - Derived: false, - KDF: Kdf_hkdf_sha256, - ConvergentEncryption: true, - VersionTemplate: EncryptedKeyPolicyVersionTpl, - }) - - _, err := NewEncryptedKeyStorageWrapper(EncryptedKeyStorageConfig{ - Policy: policy, - Prefix: "prefix", - }) - if err != ErrPolicyDerivedKeys { - t.Fatalf("Unexpected Error: %s", err) - } - - policy = NewPolicy(PolicyConfig{ - Name: "metadata", - Type: KeyType_AES256_GCM96, - Derived: true, - KDF: Kdf_hkdf_sha256, - ConvergentEncryption: false, - VersionTemplate: EncryptedKeyPolicyVersionTpl, - }) - - _, err = NewEncryptedKeyStorageWrapper(EncryptedKeyStorageConfig{ - Policy: policy, - Prefix: "prefix", - }) - if err != ErrPolicyConvergentEncryption { - t.Fatalf("Unexpected Error: %s", err) - } - - policy = NewPolicy(PolicyConfig{ - Name: "metadata", - Type: KeyType_AES256_GCM96, - Derived: true, - KDF: Kdf_hkdf_sha256, - ConvergentEncryption: true, - VersionTemplate: EncryptedKeyPolicyVersionTpl, - }) - _, err = NewEncryptedKeyStorageWrapper(EncryptedKeyStorageConfig{ - Policy: policy, - Prefix: "prefix", - }) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } -} - -func TestEncryptedKeysStorage_List(t *testing.T) { - s := &logical.InmemStorage{} - policy := NewPolicy(PolicyConfig{ - Name: "metadata", - Type: KeyType_AES256_GCM96, - Derived: true, - KDF: Kdf_hkdf_sha256, - ConvergentEncryption: true, - VersionTemplate: EncryptedKeyPolicyVersionTpl, - }) - - ctx := context.Background() - - err := policy.Rotate(ctx, s, rand.Reader) - if err != nil { - t.Fatal(err) - } - - es, err := NewEncryptedKeyStorageWrapper(EncryptedKeyStorageConfig{ - Policy: policy, - Prefix: "prefix", - }) - if err != nil { - t.Fatal(err) - } - - err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ - Key: "test", - Value: []byte("test"), - }) - if err != nil { - t.Fatal(err) - } - - err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ - Key: "test/foo", - Value: []byte("test"), - }) - if err != nil { - t.Fatal(err) - } - - err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ - Key: "test/foo1/test", - Value: []byte("test"), - }) - if err != nil { - t.Fatal(err) - } - - keys, err := es.Wrap(s).List(ctx, "test/") - if err != nil { - t.Fatal(err) - } - - // Test prefixed with "/" - keys, err = es.Wrap(s).List(ctx, "/test/") - if err != nil { - t.Fatal(err) - } - - if len(keys) != 2 || keys[1] != "foo1/" || keys[0] != "foo" { - t.Fatalf("bad keys: %#v", keys) - } - - keys, err = es.Wrap(s).List(ctx, "/") - if err != nil { - t.Fatal(err) - } - if len(keys) != 2 || keys[0] != "test" || keys[1] != "test/" { - t.Fatalf("bad keys: %#v", keys) - } - - keys, err = es.Wrap(s).List(ctx, "") - if err != nil { - t.Fatal(err) - } - if len(keys) != 2 || keys[0] != "test" || keys[1] != "test/" { - t.Fatalf("bad keys: %#v", keys) - } -} - -func TestEncryptedKeysStorage_CRUD(t *testing.T) { - s := &logical.InmemStorage{} - policy := NewPolicy(PolicyConfig{ - Name: "metadata", - Type: KeyType_AES256_GCM96, - Derived: true, - KDF: Kdf_hkdf_sha256, - ConvergentEncryption: true, - VersionTemplate: EncryptedKeyPolicyVersionTpl, - }) - - ctx := context.Background() - - err := policy.Rotate(ctx, s, rand.Reader) - if err != nil { - t.Fatal(err) - } - - es, err := NewEncryptedKeyStorageWrapper(EncryptedKeyStorageConfig{ - Policy: policy, - Prefix: "prefix", - }) - if err != nil { - t.Fatal(err) - } - - err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ - Key: "test/foo", - Value: []byte("test"), - }) - if err != nil { - t.Fatal(err) - } - - err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ - Key: "test/foo1/test", - Value: []byte("test"), - }) - if err != nil { - t.Fatal(err) - } - - keys, err := es.Wrap(s).List(ctx, "test/") - if err != nil { - t.Fatal(err) - } - - // Test prefixed with "/" - keys, err = es.Wrap(s).List(ctx, "/test/") - if err != nil { - t.Fatal(err) - } - - if len(keys) != 2 || !strutil.StrListContains(keys, "foo1/") || !strutil.StrListContains(keys, "foo") { - t.Fatalf("bad keys: %#v", keys) - } - - // Test the cached value is correct - keys, err = es.Wrap(s).List(ctx, "test/") - if err != nil { - t.Fatal(err) - } - - if len(keys) != 2 || !strutil.StrListContains(keys, "foo1/") || !strutil.StrListContains(keys, "foo") { - t.Fatalf("bad keys: %#v", keys) - } - - data, err := es.Wrap(s).Get(ctx, "test/foo") - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(data.Value, []byte("test")) { - t.Fatalf("bad data: %#v", data) - } - - err = es.Wrap(s).Delete(ctx, "test/foo") - if err != nil { - t.Fatal(err) - } - - data, err = es.Wrap(s).Get(ctx, "test/foo") - if err != nil { - t.Fatal(err) - } - if data != nil { - t.Fatal("data should be nil") - } -} - -func BenchmarkEncrytedKeyStorage_List(b *testing.B) { - s := &logical.InmemStorage{} - policy := NewPolicy(PolicyConfig{ - Name: "metadata", - Type: KeyType_AES256_GCM96, - Derived: true, - KDF: Kdf_hkdf_sha256, - ConvergentEncryption: true, - VersionTemplate: EncryptedKeyPolicyVersionTpl, - }) - - ctx := context.Background() - - err := policy.Rotate(ctx, s, rand.Reader) - if err != nil { - b.Fatal(err) - } - - es, err := NewEncryptedKeyStorageWrapper(EncryptedKeyStorageConfig{ - Policy: policy, - Prefix: "prefix", - }) - if err != nil { - b.Fatal(err) - } - - for i := 0; i < 10000; i++ { - err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ - Key: fmt.Sprintf("test/%d", i), - Value: []byte("test"), - }) - if err != nil { - b.Fatal(err) - } - } - b.ResetTimer() - - for i := 0; i < b.N; i++ { - keys, err := es.Wrap(s).List(ctx, "test/") - if err != nil { - b.Fatal(err) - } - compilerOpt = keys - } -} - -func BenchmarkEncrytedKeyStorage_Put(b *testing.B) { - s := &logical.InmemStorage{} - policy := NewPolicy(PolicyConfig{ - Name: "metadata", - Type: KeyType_AES256_GCM96, - Derived: true, - KDF: Kdf_hkdf_sha256, - ConvergentEncryption: true, - VersionTemplate: EncryptedKeyPolicyVersionTpl, - }) - - ctx := context.Background() - - err := policy.Rotate(ctx, s, rand.Reader) - if err != nil { - b.Fatal(err) - } - - es, err := NewEncryptedKeyStorageWrapper(EncryptedKeyStorageConfig{ - Policy: policy, - Prefix: "prefix", - }) - if err != nil { - b.Fatal(err) - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ - Key: fmt.Sprintf("test/%d", i), - Value: []byte("test"), - }) - if err != nil { - b.Fatal(err) - } - } -} diff --git a/sdk/helper/keysutil/policy_test.go b/sdk/helper/keysutil/policy_test.go deleted file mode 100644 index f5e4d35eb..000000000 --- a/sdk/helper/keysutil/policy_test.go +++ /dev/null @@ -1,1069 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package keysutil - -import ( - "bytes" - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "errors" - "fmt" - mathrand "math/rand" - "reflect" - "strconv" - "strings" - "sync" - "testing" - "time" - - "golang.org/x/crypto/ed25519" - - "github.com/hashicorp/vault/sdk/helper/errutil" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/copystructure" -) - -func TestPolicy_KeyEntryMapUpgrade(t *testing.T) { - now := time.Now() - old := map[int]KeyEntry{ - 1: { - Key: []byte("samplekey"), - HMACKey: []byte("samplehmackey"), - CreationTime: now, - FormattedPublicKey: "sampleformattedpublickey", - }, - 2: { - Key: []byte("samplekey2"), - HMACKey: []byte("samplehmackey2"), - CreationTime: now.Add(10 * time.Second), - FormattedPublicKey: "sampleformattedpublickey2", - }, - } - - oldEncoded, err := jsonutil.EncodeJSON(old) - if err != nil { - t.Fatal(err) - } - - var new keyEntryMap - err = jsonutil.DecodeJSON(oldEncoded, &new) - if err != nil { - t.Fatal(err) - } - - newEncoded, err := jsonutil.EncodeJSON(&new) - if err != nil { - t.Fatal(err) - } - - if string(oldEncoded) != string(newEncoded) { - t.Fatalf("failed to upgrade key entry map;\nold: %q\nnew: %q", string(oldEncoded), string(newEncoded)) - } -} - -func Test_KeyUpgrade(t *testing.T) { - lockManagerWithCache, _ := NewLockManager(true, 0) - lockManagerWithoutCache, _ := NewLockManager(false, 0) - testKeyUpgradeCommon(t, lockManagerWithCache) - testKeyUpgradeCommon(t, lockManagerWithoutCache) -} - -func testKeyUpgradeCommon(t *testing.T, lm *LockManager) { - ctx := context.Background() - - storage := &logical.InmemStorage{} - p, upserted, err := lm.GetPolicy(ctx, PolicyRequest{ - Upsert: true, - Storage: storage, - KeyType: KeyType_AES256_GCM96, - Name: "test", - }, rand.Reader) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("nil policy") - } - if !upserted { - t.Fatal("expected an upsert") - } - if !lm.useCache { - p.Unlock() - } - - testBytes := make([]byte, len(p.Keys["1"].Key)) - copy(testBytes, p.Keys["1"].Key) - - p.Key = p.Keys["1"].Key - p.Keys = nil - p.MigrateKeyToKeysMap() - if p.Key != nil { - t.Fatal("policy.Key is not nil") - } - if len(p.Keys) != 1 { - t.Fatal("policy.Keys is the wrong size") - } - if !reflect.DeepEqual(testBytes, p.Keys["1"].Key) { - t.Fatal("key mismatch") - } -} - -func Test_ArchivingUpgrade(t *testing.T) { - lockManagerWithCache, _ := NewLockManager(true, 0) - lockManagerWithoutCache, _ := NewLockManager(false, 0) - testArchivingUpgradeCommon(t, lockManagerWithCache) - testArchivingUpgradeCommon(t, lockManagerWithoutCache) -} - -func testArchivingUpgradeCommon(t *testing.T, lm *LockManager) { - ctx := context.Background() - - // First, we generate a policy and rotate it a number of times. Each time - // we'll ensure that we have the expected number of keys in the archive and - // the main keys object, which without changing the min version should be - // zero and latest, respectively - - storage := &logical.InmemStorage{} - p, _, err := lm.GetPolicy(ctx, PolicyRequest{ - Upsert: true, - Storage: storage, - KeyType: KeyType_AES256_GCM96, - Name: "test", - }, rand.Reader) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("nil policy") - } - if !lm.useCache { - p.Unlock() - } - - // Store the initial key in the archive - keysArchive := []KeyEntry{{}, p.Keys["1"]} - checkKeys(t, ctx, p, storage, keysArchive, "initial", 1, 1, 1) - - for i := 2; i <= 10; i++ { - err = p.Rotate(ctx, storage, rand.Reader) - if err != nil { - t.Fatal(err) - } - keysArchive = append(keysArchive, p.Keys[strconv.Itoa(i)]) - checkKeys(t, ctx, p, storage, keysArchive, "rotate", i, i, i) - } - - // Now, wipe the archive and set the archive version to zero - err = storage.Delete(ctx, "archive/test") - if err != nil { - t.Fatal(err) - } - p.ArchiveVersion = 0 - - // Store it, but without calling persist, so we don't trigger - // handleArchiving() - buf, err := p.Serialize() - if err != nil { - t.Fatal(err) - } - - // Write the policy into storage - err = storage.Put(ctx, &logical.StorageEntry{ - Key: "policy/" + p.Name, - Value: buf, - }) - if err != nil { - t.Fatal(err) - } - - // If we're caching, expire from the cache since we modified it - // under-the-hood - if lm.useCache { - lm.cache.Delete("test") - } - - // Now get the policy again; the upgrade should happen automatically - p, _, err = lm.GetPolicy(ctx, PolicyRequest{ - Storage: storage, - Name: "test", - }, rand.Reader) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("nil policy") - } - if !lm.useCache { - p.Unlock() - } - - checkKeys(t, ctx, p, storage, keysArchive, "upgrade", 10, 10, 10) - - // Let's check some deletion logic while we're at it - - // The policy should be in there - if lm.useCache { - _, ok := lm.cache.Load("test") - if !ok { - t.Fatal("nil policy in cache") - } - } - - // First we'll do this wrong, by not setting the deletion flag - err = lm.DeletePolicy(ctx, storage, "test") - if err == nil { - t.Fatal("got nil error, but should not have been able to delete since we didn't set the deletion flag on the policy") - } - - // The policy should still be in there - if lm.useCache { - _, ok := lm.cache.Load("test") - if !ok { - t.Fatal("nil policy in cache") - } - } - - p, _, err = lm.GetPolicy(ctx, PolicyRequest{ - Storage: storage, - Name: "test", - }, rand.Reader) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("policy nil after bad delete") - } - if !lm.useCache { - p.Unlock() - } - - // Now do it properly - p.DeletionAllowed = true - err = p.Persist(ctx, storage) - if err != nil { - t.Fatal(err) - } - err = lm.DeletePolicy(ctx, storage, "test") - if err != nil { - t.Fatal(err) - } - - // The policy should *not* be in there - if lm.useCache { - _, ok := lm.cache.Load("test") - if ok { - t.Fatal("non-nil policy in cache") - } - } - - p, _, err = lm.GetPolicy(ctx, PolicyRequest{ - Storage: storage, - Name: "test", - }, rand.Reader) - if err != nil { - t.Fatal(err) - } - if p != nil { - t.Fatal("policy not nil after delete") - } -} - -func Test_Archiving(t *testing.T) { - lockManagerWithCache, _ := NewLockManager(true, 0) - lockManagerWithoutCache, _ := NewLockManager(false, 0) - testArchivingUpgradeCommon(t, lockManagerWithCache) - testArchivingUpgradeCommon(t, lockManagerWithoutCache) -} - -func testArchivingCommon(t *testing.T, lm *LockManager) { - ctx := context.Background() - - // First, we generate a policy and rotate it a number of times. Each time - // we'll ensure that we have the expected number of keys in the archive and - // the main keys object, which without changing the min version should be - // zero and latest, respectively - - storage := &logical.InmemStorage{} - p, _, err := lm.GetPolicy(ctx, PolicyRequest{ - Upsert: true, - Storage: storage, - KeyType: KeyType_AES256_GCM96, - Name: "test", - }, rand.Reader) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("nil policy") - } - if !lm.useCache { - p.Unlock() - } - - // Store the initial key in the archive - keysArchive := []KeyEntry{{}, p.Keys["1"]} - checkKeys(t, ctx, p, storage, keysArchive, "initial", 1, 1, 1) - - for i := 2; i <= 10; i++ { - err = p.Rotate(ctx, storage, rand.Reader) - if err != nil { - t.Fatal(err) - } - keysArchive = append(keysArchive, p.Keys[strconv.Itoa(i)]) - checkKeys(t, ctx, p, storage, keysArchive, "rotate", i, i, i) - } - - // Move the min decryption version up - for i := 1; i <= 10; i++ { - p.MinDecryptionVersion = i - - err = p.Persist(ctx, storage) - if err != nil { - t.Fatal(err) - } - // We expect to find: - // * The keys in archive are the same as the latest version - // * The latest version is constant - // * The number of keys in the policy itself is from the min - // decryption version up to the latest version, so for e.g. 7 and - // 10, you'd need 7, 8, 9, and 10 -- IOW, latest version - min - // decryption version plus 1 (the min decryption version key - // itself) - checkKeys(t, ctx, p, storage, keysArchive, "minadd", 10, 10, p.LatestVersion-p.MinDecryptionVersion+1) - } - - // Move the min decryption version down - for i := 10; i >= 1; i-- { - p.MinDecryptionVersion = i - - err = p.Persist(ctx, storage) - if err != nil { - t.Fatal(err) - } - // We expect to find: - // * The keys in archive are never removed so same as the latest version - // * The latest version is constant - // * The number of keys in the policy itself is from the min - // decryption version up to the latest version, so for e.g. 7 and - // 10, you'd need 7, 8, 9, and 10 -- IOW, latest version - min - // decryption version plus 1 (the min decryption version key - // itself) - checkKeys(t, ctx, p, storage, keysArchive, "minsub", 10, 10, p.LatestVersion-p.MinDecryptionVersion+1) - } -} - -func checkKeys(t *testing.T, - ctx context.Context, - p *Policy, - storage logical.Storage, - keysArchive []KeyEntry, - action string, - archiveVer, latestVer, keysSize int, -) { - // Sanity check - if len(keysArchive) != latestVer+1 { - t.Fatalf("latest expected key version is %d, expected test keys archive size is %d, "+ - "but keys archive is of size %d", latestVer, latestVer+1, len(keysArchive)) - } - - archive, err := p.LoadArchive(ctx, storage) - if err != nil { - t.Fatal(err) - } - - badArchiveVer := false - if archiveVer == 0 { - if len(archive.Keys) != 0 || p.ArchiveVersion != 0 { - badArchiveVer = true - } - } else { - // We need to subtract one because we have the indexes match key - // versions, which start at 1. So for an archive version of 1, we - // actually have two entries -- a blank 0 entry, and the key at spot 1 - if archiveVer != len(archive.Keys)-1 || archiveVer != p.ArchiveVersion { - badArchiveVer = true - } - } - if badArchiveVer { - t.Fatalf( - "expected archive version %d, found length of archive keys %d and policy archive version %d", - archiveVer, len(archive.Keys), p.ArchiveVersion, - ) - } - - if latestVer != p.LatestVersion { - t.Fatalf( - "expected latest version %d, found %d", - latestVer, p.LatestVersion, - ) - } - - if keysSize != len(p.Keys) { - t.Fatalf( - "expected keys size %d, found %d, action is %s, policy is \n%#v\n", - keysSize, len(p.Keys), action, p, - ) - } - - for i := p.MinDecryptionVersion; i <= p.LatestVersion; i++ { - if _, ok := p.Keys[strconv.Itoa(i)]; !ok { - t.Fatalf( - "expected key %d, did not find it in policy keys", i, - ) - } - } - - for i := p.MinDecryptionVersion; i <= p.LatestVersion; i++ { - ver := strconv.Itoa(i) - if !p.Keys[ver].CreationTime.Equal(keysArchive[i].CreationTime) { - t.Fatalf("key %d not equivalent between policy keys and test keys archive; policy keys:\n%#v\ntest keys archive:\n%#v\n", i, p.Keys[ver], keysArchive[i]) - } - polKey := p.Keys[ver] - polKey.CreationTime = keysArchive[i].CreationTime - p.Keys[ver] = polKey - if !reflect.DeepEqual(p.Keys[ver], keysArchive[i]) { - t.Fatalf("key %d not equivalent between policy keys and test keys archive; policy keys:\n%#v\ntest keys archive:\n%#v\n", i, p.Keys[ver], keysArchive[i]) - } - } - - for i := 1; i < len(archive.Keys); i++ { - if !reflect.DeepEqual(archive.Keys[i].Key, keysArchive[i].Key) { - t.Fatalf("key %d not equivalent between policy archive and test keys archive; policy archive:\n%#v\ntest keys archive:\n%#v\n", i, archive.Keys[i].Key, keysArchive[i].Key) - } - } -} - -func Test_StorageErrorSafety(t *testing.T) { - ctx := context.Background() - lm, _ := NewLockManager(true, 0) - - storage := &logical.InmemStorage{} - p, _, err := lm.GetPolicy(ctx, PolicyRequest{ - Upsert: true, - Storage: storage, - KeyType: KeyType_AES256_GCM96, - Name: "test", - }, rand.Reader) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("nil policy") - } - - // Store the initial key in the archive - keysArchive := []KeyEntry{{}, p.Keys["1"]} - checkKeys(t, ctx, p, storage, keysArchive, "initial", 1, 1, 1) - - // We use checkKeys here just for sanity; it doesn't really handle cases of - // errors below so we do more targeted testing later - for i := 2; i <= 5; i++ { - err = p.Rotate(ctx, storage, rand.Reader) - if err != nil { - t.Fatal(err) - } - keysArchive = append(keysArchive, p.Keys[strconv.Itoa(i)]) - checkKeys(t, ctx, p, storage, keysArchive, "rotate", i, i, i) - } - - underlying := storage.Underlying() - underlying.FailPut(true) - - priorLen := len(p.Keys) - - err = p.Rotate(ctx, storage, rand.Reader) - if err == nil { - t.Fatal("expected error") - } - - if len(p.Keys) != priorLen { - t.Fatal("length of keys should not have changed") - } -} - -func Test_BadUpgrade(t *testing.T) { - ctx := context.Background() - lm, _ := NewLockManager(true, 0) - storage := &logical.InmemStorage{} - p, _, err := lm.GetPolicy(ctx, PolicyRequest{ - Upsert: true, - Storage: storage, - KeyType: KeyType_AES256_GCM96, - Name: "test", - }, rand.Reader) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("nil policy") - } - - orig, err := copystructure.Copy(p) - if err != nil { - t.Fatal(err) - } - orig.(*Policy).l = p.l - - p.Key = p.Keys["1"].Key - p.Keys = nil - p.MinDecryptionVersion = 0 - - if err := p.Upgrade(ctx, storage, rand.Reader); err != nil { - t.Fatal(err) - } - - k := p.Keys["1"] - o := orig.(*Policy).Keys["1"] - k.CreationTime = o.CreationTime - k.HMACKey = o.HMACKey - p.Keys["1"] = k - p.versionPrefixCache = sync.Map{} - - if !reflect.DeepEqual(orig, p) { - t.Fatalf("not equal:\n%#v\n%#v", orig, p) - } - - // Do it again with a failing storage call - underlying := storage.Underlying() - underlying.FailPut(true) - - p.Key = p.Keys["1"].Key - p.Keys = nil - p.MinDecryptionVersion = 0 - - if err := p.Upgrade(ctx, storage, rand.Reader); err == nil { - t.Fatal("expected error") - } - - if p.MinDecryptionVersion == 1 { - t.Fatal("min decryption version was changed") - } - if p.Keys != nil { - t.Fatal("found upgraded keys") - } - if p.Key == nil { - t.Fatal("non-upgraded key not found") - } -} - -func Test_BadArchive(t *testing.T) { - ctx := context.Background() - lm, _ := NewLockManager(true, 0) - storage := &logical.InmemStorage{} - p, _, err := lm.GetPolicy(ctx, PolicyRequest{ - Upsert: true, - Storage: storage, - KeyType: KeyType_AES256_GCM96, - Name: "test", - }, rand.Reader) - if err != nil { - t.Fatal(err) - } - if p == nil { - t.Fatal("nil policy") - } - - for i := 2; i <= 10; i++ { - err = p.Rotate(ctx, storage, rand.Reader) - if err != nil { - t.Fatal(err) - } - } - - p.MinDecryptionVersion = 5 - if err := p.Persist(ctx, storage); err != nil { - t.Fatal(err) - } - if p.ArchiveVersion != 10 { - t.Fatalf("unexpected archive version %d", p.ArchiveVersion) - } - if len(p.Keys) != 6 { - t.Fatalf("unexpected key length %d", len(p.Keys)) - } - - // Set back - p.MinDecryptionVersion = 1 - if err := p.Persist(ctx, storage); err != nil { - t.Fatal(err) - } - if p.ArchiveVersion != 10 { - t.Fatalf("unexpected archive version %d", p.ArchiveVersion) - } - if len(p.Keys) != 10 { - t.Fatalf("unexpected key length %d", len(p.Keys)) - } - - // Run it again but we'll turn off storage along the way - p.MinDecryptionVersion = 5 - if err := p.Persist(ctx, storage); err != nil { - t.Fatal(err) - } - if p.ArchiveVersion != 10 { - t.Fatalf("unexpected archive version %d", p.ArchiveVersion) - } - if len(p.Keys) != 6 { - t.Fatalf("unexpected key length %d", len(p.Keys)) - } - - underlying := storage.Underlying() - underlying.FailPut(true) - - // Set back, which should cause p.Keys to be changed if the persist works, - // but it doesn't - p.MinDecryptionVersion = 1 - if err := p.Persist(ctx, storage); err == nil { - t.Fatal("expected error during put") - } - if p.ArchiveVersion != 10 { - t.Fatalf("unexpected archive version %d", p.ArchiveVersion) - } - // Here's the expected change - if len(p.Keys) != 6 { - t.Fatalf("unexpected key length %d", len(p.Keys)) - } -} - -func Test_Import(t *testing.T) { - ctx := context.Background() - storage := &logical.InmemStorage{} - testKeys, err := generateTestKeys() - if err != nil { - t.Fatalf("error generating test keys: %s", err) - } - - tests := map[string]struct { - policy Policy - key []byte - shouldError bool - }{ - "import AES key": { - policy: Policy{ - Name: "test-aes-key", - Type: KeyType_AES256_GCM96, - }, - key: testKeys[KeyType_AES256_GCM96], - shouldError: false, - }, - "import RSA key": { - policy: Policy{ - Name: "test-rsa-key", - Type: KeyType_RSA2048, - }, - key: testKeys[KeyType_RSA2048], - shouldError: false, - }, - "import ECDSA key": { - policy: Policy{ - Name: "test-ecdsa-key", - Type: KeyType_ECDSA_P256, - }, - key: testKeys[KeyType_ECDSA_P256], - shouldError: false, - }, - "import ED25519 key": { - policy: Policy{ - Name: "test-ed25519-key", - Type: KeyType_ED25519, - }, - key: testKeys[KeyType_ED25519], - shouldError: false, - }, - "import incorrect key type": { - policy: Policy{ - Name: "test-ed25519-key", - Type: KeyType_ED25519, - }, - key: testKeys[KeyType_AES256_GCM96], - shouldError: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - if err := test.policy.Import(ctx, storage, test.key, rand.Reader); (err != nil) != test.shouldError { - t.Fatalf("error importing key: %s", err) - } - }) - } -} - -func generateTestKeys() (map[KeyType][]byte, error) { - keyMap := make(map[KeyType][]byte) - - rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, err - } - rsaKeyBytes, err := x509.MarshalPKCS8PrivateKey(rsaKey) - if err != nil { - return nil, err - } - keyMap[KeyType_RSA2048] = rsaKeyBytes - - rsaKey, err = rsa.GenerateKey(rand.Reader, 3072) - if err != nil { - return nil, err - } - rsaKeyBytes, err = x509.MarshalPKCS8PrivateKey(rsaKey) - if err != nil { - return nil, err - } - keyMap[KeyType_RSA3072] = rsaKeyBytes - - rsaKey, err = rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - return nil, err - } - rsaKeyBytes, err = x509.MarshalPKCS8PrivateKey(rsaKey) - if err != nil { - return nil, err - } - keyMap[KeyType_RSA4096] = rsaKeyBytes - - ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, err - } - ecdsaKeyBytes, err := x509.MarshalPKCS8PrivateKey(ecdsaKey) - if err != nil { - return nil, err - } - keyMap[KeyType_ECDSA_P256] = ecdsaKeyBytes - - _, ed25519Key, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return nil, err - } - ed25519KeyBytes, err := x509.MarshalPKCS8PrivateKey(ed25519Key) - if err != nil { - return nil, err - } - keyMap[KeyType_ED25519] = ed25519KeyBytes - - aesKey := make([]byte, 32) - _, err = rand.Read(aesKey) - if err != nil { - return nil, err - } - keyMap[KeyType_AES256_GCM96] = aesKey - - return keyMap, nil -} - -func BenchmarkSymmetric(b *testing.B) { - ctx := context.Background() - lm, _ := NewLockManager(true, 0) - storage := &logical.InmemStorage{} - p, _, _ := lm.GetPolicy(ctx, PolicyRequest{ - Upsert: true, - Storage: storage, - KeyType: KeyType_AES256_GCM96, - Name: "test", - }, rand.Reader) - key, _ := p.GetKey(nil, 1, 32) - pt := make([]byte, 10) - ad := make([]byte, 10) - for i := 0; i < b.N; i++ { - ct, _ := p.SymmetricEncryptRaw(1, key, pt, - SymmetricOpts{ - AdditionalData: ad, - }) - pt2, _ := p.SymmetricDecryptRaw(key, ct, SymmetricOpts{ - AdditionalData: ad, - }) - if !bytes.Equal(pt, pt2) { - b.Fail() - } - } -} - -func saltOptions(options SigningOptions, saltLength int) SigningOptions { - return SigningOptions{ - HashAlgorithm: options.HashAlgorithm, - Marshaling: options.Marshaling, - SaltLength: saltLength, - SigAlgorithm: options.SigAlgorithm, - } -} - -func manualVerify(depth int, t *testing.T, p *Policy, input []byte, sig *SigningResult, options SigningOptions) { - tabs := strings.Repeat("\t", depth) - t.Log(tabs, "Manually verifying signature with options:", options) - - tabs = strings.Repeat("\t", depth+1) - verified, err := p.VerifySignatureWithOptions(nil, input, sig.Signature, &options) - if err != nil { - t.Fatal(tabs, "❌ Failed to manually verify signature:", err) - } - if !verified { - t.Fatal(tabs, "❌ Failed to manually verify signature") - } -} - -func autoVerify(depth int, t *testing.T, p *Policy, input []byte, sig *SigningResult, options SigningOptions) { - tabs := strings.Repeat("\t", depth) - t.Log(tabs, "Automatically verifying signature with options:", options) - - tabs = strings.Repeat("\t", depth+1) - verified, err := p.VerifySignature(nil, input, options.HashAlgorithm, options.SigAlgorithm, options.Marshaling, sig.Signature) - if err != nil { - t.Fatal(tabs, "❌ Failed to automatically verify signature:", err) - } - if !verified { - t.Fatal(tabs, "❌ Failed to automatically verify signature") - } -} - -func Test_RSA_PSS(t *testing.T) { - t.Log("Testing RSA PSS") - mathrand.Seed(time.Now().UnixNano()) - - var userError errutil.UserError - ctx := context.Background() - storage := &logical.InmemStorage{} - // https://crypto.stackexchange.com/a/1222 - input := []byte("the ancients say the longer the salt, the more provable the security") - sigAlgorithm := "pss" - - tabs := make(map[int]string) - for i := 1; i <= 6; i++ { - tabs[i] = strings.Repeat("\t", i) - } - - test_RSA_PSS := func(t *testing.T, p *Policy, rsaKey *rsa.PrivateKey, hashType HashType, - marshalingType MarshalingType, - ) { - unsaltedOptions := SigningOptions{ - HashAlgorithm: hashType, - Marshaling: marshalingType, - SigAlgorithm: sigAlgorithm, - } - cryptoHash := CryptoHashMap[hashType] - minSaltLength := p.minRSAPSSSaltLength() - maxSaltLength := p.maxRSAPSSSaltLength(rsaKey.N.BitLen(), cryptoHash) - hash := cryptoHash.New() - hash.Write(input) - input = hash.Sum(nil) - - // 1. Make an "automatic" signature with the given key size and hash algorithm, - // but an automatically chosen salt length. - t.Log(tabs[3], "Make an automatic signature") - sig, err := p.Sign(0, nil, input, hashType, sigAlgorithm, marshalingType) - if err != nil { - // A bit of a hack but FIPS go does not support some hash types - if isUnsupportedGoHashType(hashType, err) { - t.Skip(tabs[4], "skipping test as FIPS Go does not support hash type") - return - } - t.Fatal(tabs[4], "❌ Failed to automatically sign:", err) - } - - // 1.1 Verify this automatic signature using the *inferred* salt length. - autoVerify(4, t, p, input, sig, unsaltedOptions) - - // 1.2. Verify this automatic signature using the *correct, given* salt length. - manualVerify(4, t, p, input, sig, saltOptions(unsaltedOptions, maxSaltLength)) - - // 1.3. Try to verify this automatic signature using *incorrect, given* salt lengths. - t.Log(tabs[4], "Test incorrect salt lengths") - incorrectSaltLengths := []int{minSaltLength, maxSaltLength - 1} - for _, saltLength := range incorrectSaltLengths { - t.Log(tabs[5], "Salt length:", saltLength) - saltedOptions := saltOptions(unsaltedOptions, saltLength) - - verified, _ := p.VerifySignatureWithOptions(nil, input, sig.Signature, &saltedOptions) - if verified { - t.Fatal(tabs[6], "❌ Failed to invalidate", verified, "signature using incorrect salt length:", err) - } - } - - // 2. Rule out boundary, invalid salt lengths. - t.Log(tabs[3], "Test invalid salt lengths") - invalidSaltLengths := []int{minSaltLength - 1, maxSaltLength + 1} - for _, saltLength := range invalidSaltLengths { - t.Log(tabs[4], "Salt length:", saltLength) - saltedOptions := saltOptions(unsaltedOptions, saltLength) - - // 2.1. Fail to sign. - t.Log(tabs[5], "Try to make a manual signature") - _, err := p.SignWithOptions(0, nil, input, &saltedOptions) - if !errors.As(err, &userError) { - t.Fatal(tabs[6], "❌ Failed to reject invalid salt length:", err) - } - - // 2.2. Fail to verify. - t.Log(tabs[5], "Try to verify an automatic signature using an invalid salt length") - _, err = p.VerifySignatureWithOptions(nil, input, sig.Signature, &saltedOptions) - if !errors.As(err, &userError) { - t.Fatal(tabs[6], "❌ Failed to reject invalid salt length:", err) - } - } - - // 3. For three possible valid salt lengths... - t.Log(tabs[3], "Test three possible valid salt lengths") - midSaltLength := mathrand.Intn(maxSaltLength-1) + 1 // [1, maxSaltLength) - validSaltLengths := []int{minSaltLength, midSaltLength, maxSaltLength} - for _, saltLength := range validSaltLengths { - t.Log(tabs[4], "Salt length:", saltLength) - saltedOptions := saltOptions(unsaltedOptions, saltLength) - - // 3.1. Make a "manual" signature with the given key size, hash algorithm, and salt length. - t.Log(tabs[5], "Make a manual signature") - sig, err := p.SignWithOptions(0, nil, input, &saltedOptions) - if err != nil { - t.Fatal(tabs[6], "❌ Failed to manually sign:", err) - } - - // 3.2. Verify this manual signature using the *correct, given* salt length. - manualVerify(6, t, p, input, sig, saltedOptions) - - // 3.3. Verify this manual signature using the *inferred* salt length. - autoVerify(6, t, p, input, sig, unsaltedOptions) - } - } - - rsaKeyTypes := []KeyType{KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096} - testKeys, err := generateTestKeys() - if err != nil { - t.Fatalf("error generating test keys: %s", err) - } - - // 1. For each standard RSA key size 2048, 3072, and 4096... - for _, rsaKeyType := range rsaKeyTypes { - t.Log("Key size: ", rsaKeyType) - p := &Policy{ - Name: fmt.Sprint(rsaKeyType), // NOTE: crucial to create a new key per key size - Type: rsaKeyType, - } - - rsaKeyBytes := testKeys[rsaKeyType] - err := p.Import(ctx, storage, rsaKeyBytes, rand.Reader) - if err != nil { - t.Fatal(tabs[1], "❌ Failed to import key:", err) - } - rsaKeyAny, err := x509.ParsePKCS8PrivateKey(rsaKeyBytes) - if err != nil { - t.Fatalf("error parsing test keys: %s", err) - } - rsaKey := rsaKeyAny.(*rsa.PrivateKey) - - // 2. For each hash algorithm... - for hashAlgorithm, hashType := range HashTypeMap { - t.Log(tabs[1], "Hash algorithm:", hashAlgorithm) - if hashAlgorithm == "none" { - continue - } - - // 3. For each marshaling type... - for marshalingName, marshalingType := range MarshalingTypeMap { - t.Log(tabs[2], "Marshaling type:", marshalingName) - testName := fmt.Sprintf("%s-%s-%s", rsaKeyType, hashAlgorithm, marshalingName) - t.Run(testName, func(t *testing.T) { test_RSA_PSS(t, p, rsaKey, hashType, marshalingType) }) - } - } - } -} - -func Test_RSA_PKCS1(t *testing.T) { - t.Log("Testing RSA PKCS#1v1.5") - - ctx := context.Background() - storage := &logical.InmemStorage{} - // https://crypto.stackexchange.com/a/1222 - input := []byte("Sphinx of black quartz, judge my vow") - sigAlgorithm := "pkcs1v15" - - tabs := make(map[int]string) - for i := 1; i <= 6; i++ { - tabs[i] = strings.Repeat("\t", i) - } - - test_RSA_PKCS1 := func(t *testing.T, p *Policy, rsaKey *rsa.PrivateKey, hashType HashType, - marshalingType MarshalingType, - ) { - unsaltedOptions := SigningOptions{ - HashAlgorithm: hashType, - Marshaling: marshalingType, - SigAlgorithm: sigAlgorithm, - } - cryptoHash := CryptoHashMap[hashType] - - // PKCS#1v1.5 NoOID uses a direct input and assumes it is pre-hashed. - if hashType != 0 { - hash := cryptoHash.New() - hash.Write(input) - input = hash.Sum(nil) - } - - // 1. Make a signature with the given key size and hash algorithm. - t.Log(tabs[3], "Make an automatic signature") - sig, err := p.Sign(0, nil, input, hashType, sigAlgorithm, marshalingType) - if err != nil { - // A bit of a hack but FIPS go does not support some hash types - if isUnsupportedGoHashType(hashType, err) { - t.Skip(tabs[4], "skipping test as FIPS Go does not support hash type") - return - } - t.Fatal(tabs[4], "❌ Failed to automatically sign:", err) - } - - // 1.1 Verify this signature using the *inferred* salt length. - autoVerify(4, t, p, input, sig, unsaltedOptions) - } - - rsaKeyTypes := []KeyType{KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096} - testKeys, err := generateTestKeys() - if err != nil { - t.Fatalf("error generating test keys: %s", err) - } - - // 1. For each standard RSA key size 2048, 3072, and 4096... - for _, rsaKeyType := range rsaKeyTypes { - t.Log("Key size: ", rsaKeyType) - p := &Policy{ - Name: fmt.Sprint(rsaKeyType), // NOTE: crucial to create a new key per key size - Type: rsaKeyType, - } - - rsaKeyBytes := testKeys[rsaKeyType] - err := p.Import(ctx, storage, rsaKeyBytes, rand.Reader) - if err != nil { - t.Fatal(tabs[1], "❌ Failed to import key:", err) - } - rsaKeyAny, err := x509.ParsePKCS8PrivateKey(rsaKeyBytes) - if err != nil { - t.Fatalf("error parsing test keys: %s", err) - } - rsaKey := rsaKeyAny.(*rsa.PrivateKey) - - // 2. For each hash algorithm... - for hashAlgorithm, hashType := range HashTypeMap { - t.Log(tabs[1], "Hash algorithm:", hashAlgorithm) - - // 3. For each marshaling type... - for marshalingName, marshalingType := range MarshalingTypeMap { - t.Log(tabs[2], "Marshaling type:", marshalingName) - testName := fmt.Sprintf("%s-%s-%s", rsaKeyType, hashAlgorithm, marshalingName) - t.Run(testName, func(t *testing.T) { test_RSA_PKCS1(t, p, rsaKey, hashType, marshalingType) }) - } - } - } -} - -// Normal Go builds support all the hash functions for RSA_PSS signatures but the -// FIPS Go build does not support at this time the SHA3 hashes as FIPS 140_2 does -// not accept them. -func isUnsupportedGoHashType(hashType HashType, err error) bool { - switch hashType { - case HashTypeSHA3224, HashTypeSHA3256, HashTypeSHA3384, HashTypeSHA3512: - return strings.Contains(err.Error(), "unsupported hash function") - } - - return false -} diff --git a/sdk/helper/ldaputil/client_test.go b/sdk/helper/ldaputil/client_test.go deleted file mode 100644 index dcce9c6e0..000000000 --- a/sdk/helper/ldaputil/client_test.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package ldaputil - -import ( - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestDialLDAP duplicates a potential panic that was -// present in the previous version of TestDialLDAP, -// then confirms its fix by passing. -func TestDialLDAP(t *testing.T) { - ldapClient := Client{ - Logger: hclog.NewNullLogger(), - LDAP: NewLDAP(), - } - - ce := &ConfigEntry{ - Url: "ldap://localhost:384654786", - RequestTimeout: 3, - } - if _, err := ldapClient.DialLDAP(ce); err == nil { - t.Fatal("expected error") - } -} - -func TestLDAPEscape(t *testing.T) { - testcases := map[string]string{ - "#test": "\\#test", - "test,hello": "test\\,hello", - "test,hel+lo": "test\\,hel\\+lo", - "test\\hello": "test\\\\hello", - " test ": "\\ test \\ ", - "": "", - `\`: `\\`, - "trailing\000": `trailing\00`, - "mid\000dle": `mid\00dle`, - "\000": `\00`, - "multiple\000\000": `multiple\00\00`, - "backlash-before-null\\\000": `backlash-before-null\\\00`, - "trailing\\": `trailing\\`, - "double-escaping\\>": `double-escaping\\\>`, - } - - for test, answer := range testcases { - res := EscapeLDAPValue(test) - if res != answer { - t.Errorf("Failed to escape %s: %s != %s\n", test, res, answer) - } - } -} - -func TestGetTLSConfigs(t *testing.T) { - config := testConfig(t) - if err := config.Validate(); err != nil { - t.Fatal(err) - } - tlsConfig, err := getTLSConfig(config, "138.91.247.105") - if err != nil { - t.Fatal(err) - } - if tlsConfig == nil { - t.Fatal("expected 1 TLS config because there's 1 url") - } - if tlsConfig.InsecureSkipVerify { - t.Fatal("InsecureSkipVerify should be false because we should default to the most secure connection") - } - if tlsConfig.ServerName != "138.91.247.105" { - t.Fatalf("expected ServerName of \"138.91.247.105\" but received %q", tlsConfig.ServerName) - } - expected := uint16(771) - if tlsConfig.MinVersion != expected || tlsConfig.MaxVersion != expected { - t.Fatal("expected TLS min and max version of 771 which corresponds with TLS 1.2 since TLS 1.1 and 1.0 have known vulnerabilities") - } -} - -func TestSIDBytesToString(t *testing.T) { - testcases := map[string][]byte{ - "S-1-5-21-2127521184-1604012920-1887927527-72713": {0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x15, 0x00, 0x00, 0x00, 0xA0, 0x65, 0xCF, 0x7E, 0x78, 0x4B, 0x9B, 0x5F, 0xE7, 0x7C, 0x87, 0x70, 0x09, 0x1C, 0x01, 0x00}, - "S-1-1-0": {0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}, - "S-1-5": {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05}, - } - - for answer, test := range testcases { - res, err := sidBytesToString(test) - if err != nil { - t.Errorf("Failed to conver %#v: %s", test, err) - } else if answer != res { - t.Errorf("Failed to convert %#v: %s != %s", test, res, answer) - } - } -} - -func TestClient_renderUserSearchFilter(t *testing.T) { - t.Parallel() - tests := []struct { - name string - conf *ConfigEntry - username string - want string - errContains string - }{ - { - name: "valid-default", - username: "alice", - conf: &ConfigEntry{ - UserAttr: "cn", - }, - want: "(cn=alice)", - }, - { - name: "escaped-malicious-filter", - username: "foo@example.com)((((((((((((((((((((((((((((((((((((((userPrincipalName=foo", - conf: &ConfigEntry{ - UPNDomain: "example.com", - UserFilter: "(&({{.UserAttr}}={{.Username}})({{.UserAttr}}=admin@example.com))", - }, - want: "(&(userPrincipalName=foo@example.com\\29\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28\\28userPrincipalName=foo@example.com)(userPrincipalName=admin@example.com))", - }, - { - name: "bad-filter-unclosed-action", - username: "alice", - conf: &ConfigEntry{ - UserFilter: "hello{{range", - }, - errContains: "search failed due to template compilation error", - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - c := Client{ - Logger: hclog.NewNullLogger(), - LDAP: NewLDAP(), - } - - f, err := c.RenderUserSearchFilter(tc.conf, tc.username) - if tc.errContains != "" { - require.Error(t, err) - assert.ErrorContains(t, err, tc.errContains) - return - } - require.NoError(t, err) - assert.NotEmpty(t, f) - assert.Equal(t, tc.want, f) - }) - } -} diff --git a/sdk/helper/ldaputil/config_test.go b/sdk/helper/ldaputil/config_test.go deleted file mode 100644 index b7fd22ccb..000000000 --- a/sdk/helper/ldaputil/config_test.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package ldaputil - -import ( - "encoding/json" - "testing" - - "github.com/go-test/deep" - "github.com/hashicorp/vault/sdk/framework" -) - -func TestCertificateValidation(t *testing.T) { - // certificate should default to "" without error if it doesn't exist - config := testConfig(t) - if err := config.Validate(); err != nil { - t.Fatal(err) - } - if config.Certificate != "" { - t.Fatalf("expected no certificate but received %s", config.Certificate) - } - - // certificate should cause an error if a bad one is provided - config.Certificate = "cats" - if err := config.Validate(); err == nil { - t.Fatal("should err due to bad cert") - } - - // valid certificates should pass inspection - config.Certificate = validCertificate - if err := config.Validate(); err != nil { - t.Fatal(err) - } -} - -func TestNewConfigEntry(t *testing.T) { - s := &framework.FieldData{Schema: ConfigFields()} - config, err := NewConfigEntry(nil, s) - if err != nil { - t.Fatal("error getting default config") - } - configFromJSON := testJSONConfig(t, jsonConfigDefault) - - t.Run("equality_check", func(t *testing.T) { - if diff := deep.Equal(config, configFromJSON); len(diff) > 0 { - t.Fatalf("bad, diff: %#v", diff) - } - }) -} - -func TestConfig(t *testing.T) { - config := testConfig(t) - configFromJSON := testJSONConfig(t, jsonConfig) - - t.Run("equality_check", func(t *testing.T) { - if diff := deep.Equal(config, configFromJSON); len(diff) > 0 { - t.Fatalf("bad, diff: %#v", diff) - } - }) - - t.Run("default_use_token_groups", func(t *testing.T) { - if config.UseTokenGroups { - t.Errorf("expected false UseTokenGroups but got %t", config.UseTokenGroups) - } - - if configFromJSON.UseTokenGroups { - t.Errorf("expected false UseTokenGroups from JSON but got %t", configFromJSON.UseTokenGroups) - } - }) -} - -func testConfig(t *testing.T) *ConfigEntry { - t.Helper() - - return &ConfigEntry{ - Url: "ldap://138.91.247.105", - UserDN: "example,com", - BindDN: "kitty", - BindPassword: "cats", - TLSMaxVersion: "tls12", - TLSMinVersion: "tls12", - RequestTimeout: 30, - ConnectionTimeout: 15, - ClientTLSCert: "", - ClientTLSKey: "", - } -} - -func testJSONConfig(t *testing.T, rawJson []byte) *ConfigEntry { - t.Helper() - - config := new(ConfigEntry) - if err := json.Unmarshal(rawJson, config); err != nil { - t.Fatal(err) - } - return config -} - -const validCertificate = ` ------BEGIN CERTIFICATE----- -MIIF7zCCA9egAwIBAgIJAOY2qjn64Qq5MA0GCSqGSIb3DQEBCwUAMIGNMQswCQYD -VQQGEwJVUzEQMA4GA1UECAwHTm93aGVyZTERMA8GA1UEBwwIVGltYnVrdHUxEjAQ -BgNVBAoMCVRlc3QgRmFrZTENMAsGA1UECwwETm9uZTEPMA0GA1UEAwwGTm9ib2R5 -MSUwIwYJKoZIhvcNAQkBFhZkb25vdHRydXN0QG5vd2hlcmUuY29tMB4XDTE4MDQw -MzIwNDQwOFoXDTE5MDQwMzIwNDQwOFowgY0xCzAJBgNVBAYTAlVTMRAwDgYDVQQI -DAdOb3doZXJlMREwDwYDVQQHDAhUaW1idWt0dTESMBAGA1UECgwJVGVzdCBGYWtl -MQ0wCwYDVQQLDAROb25lMQ8wDQYDVQQDDAZOb2JvZHkxJTAjBgkqhkiG9w0BCQEW -FmRvbm90dHJ1c3RAbm93aGVyZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQDzQPGErqjaoFcuUV6QFpSMU6w8wO8F0othik+rrlKERmrGonUGsoum -WqRe6L4ZnxBvCKB6EWjvf894TXOF2cpUnjDAyBePISyPkRBEJS6VS2SEC4AJzmVu -a+P+fZr4Hf7/bEcUr7Ax37yGVZ5i5ByNHgZkBlPxKiGWSmAqIDRZLp9gbu2EkG9q -NOjNLPU+QI2ov6U/laGS1vbE2LahTYeT5yscu9LpllxzFv4lM1f4wYEaM3HuOxzT -l86cGmEr9Q2N4PZ2T0O/s6D4but7c6Bz2XPXy9nWb5bqu0n5bJEpbRFrkryW1ozh -L9uVVz4dyW10pFBJtE42bqA4PRCDQsUof7UfsQF11D1ThrDfKsQa8PxrYdGUHUG9 -GFF1MdTTwaoT90RI582p+6XYV+LNlXcdfyNZO9bMThu9fnCvT7Ey0TKU4MfPrlfT -aIhZmyaHt6mL5p881UPDIvy7paTLgL+C1orLjZAiT//c4Zn+0qG0//Cirxr020UF -3YiEFk2H0bBVwOHoOGw4w5HrvLdyy0ZLDSPQbzkSZ0RusHb5TjiyhtTk/h9vvJv7 -u1fKJub4MzgrBRi16ejFdiWoVuMXRC6fu/ERy3+9DH6LURerbPrdroYypUmTe9N6 -XPeaF1Tc+WO7O/yW96mV7X/D211qjkOtwboZC5kjogVbaZgGzjHCVwIDAQABo1Aw -TjAdBgNVHQ4EFgQU2zWT3HeiMBzusz7AggVqVEL5g0UwHwYDVR0jBBgwFoAU2zWT -3HeiMBzusz7AggVqVEL5g0UwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC -AgEAwTGcppY86mNRE43uOimeApTfqHJv+lGDTjEoJCZZmzmtxFe6O9+Vk4bH/8/i -gVQvqzBpaWXRt9OhqlFMK7OkX4ZvqXmnShmxib1dz1XxGhbwSec9ca8bill59Jqa -bIOq2SXVMcFD0GwFxfJRBVzHHuB6AwV9B2QN61zeB1oxNGJrUOo80jVkB7+MWMyD -bQqiFCHWGMa6BG4N91KGOTveZCGdBvvVw5j6lt731KjbvL2hB1UHioucOweKLfa4 -QWDImTEjgV68699wKERNL0DCpeD7PcP/L3SY2RJzdyC1CSR7O8yU4lQK7uZGusgB -Mgup+yUaSjxasIqYMebNDDocr5kdwG0+2r2gQdRwc5zLX6YDBn6NLSWjRnY04ZuK -P1cF68rWteWpzJu8bmkJ5r2cqskqrnVK+zz8xMQyEaj548Bnt51ARLHOftR9jkSU -NJWh7zOLZ1r2UUKdDlrMoh3GQO3rvnCJJ16NBM1dB7TUyhMhtF6UOE62BSKdHtQn -d6TqelcRw9WnDsb9IPxRwaXhvGljnYVAgXXlJEI/6nxj2T4wdmL1LWAr6C7DuWGz -8qIvxc4oAau4DsZs2+BwolCFtYc98OjWGcBStBfZz/YYXM+2hKjbONKFxWdEPxGR -Beq3QOqp2+dga36IzQybzPQ8QtotrpSJ3q82zztEvyWiJ7E= ------END CERTIFICATE----- -` - -var jsonConfig = []byte(`{ - "url": "ldap://138.91.247.105", - "userdn": "example,com", - "binddn": "kitty", - "bindpass": "cats", - "tls_max_version": "tls12", - "tls_min_version": "tls12", - "request_timeout": 30, - "connection_timeout": 15, - "ClientTLSCert": "", - "ClientTLSKey": "" -}`) - -var jsonConfigDefault = []byte(` -{ - "url": "ldap://127.0.0.1", - "userdn": "", - "anonymous_group_search": false, - "groupdn": "", - "groupfilter": "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))", - "groupattr": "cn", - "upndomain": "", - "userattr": "cn", - "userfilter": "({{.UserAttr}}={{.Username}})", - "certificate": "", - "client_tls_cert": "", - "client_tsl_key": "", - "insecure_tls": false, - "starttls": false, - "binddn": "", - "bindpass": "", - "deny_null_bind": true, - "discoverdn": false, - "tls_min_version": "tls12", - "tls_max_version": "tls12", - "use_token_groups": false, - "use_pre111_group_cn_behavior": null, - "username_as_alias": false, - "request_timeout": 90, - "connection_timeout": 30, - "dereference_aliases": "never", - "max_page_size": 0, - "CaseSensitiveNames": false, - "ClientTLSCert": "", - "ClientTLSKey": "" -} -`) diff --git a/sdk/helper/locksutil/locks_test.go b/sdk/helper/locksutil/locks_test.go deleted file mode 100644 index 954a46349..000000000 --- a/sdk/helper/locksutil/locks_test.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package locksutil - -import "testing" - -func Test_CreateLocks(t *testing.T) { - locks := CreateLocks() - if len(locks) != 256 { - t.Fatalf("bad: len(locks): expected:256 actual:%d", len(locks)) - } -} diff --git a/sdk/helper/logging/logging_test.go b/sdk/helper/logging/logging_test.go deleted file mode 100644 index 16075524b..000000000 --- a/sdk/helper/logging/logging_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logging - -import ( - "errors" - "os" - "reflect" - "testing" -) - -func Test_ParseLogFormat(t *testing.T) { - type testData struct { - format string - expected LogFormat - expectedErr error - } - - tests := []testData{ - {format: "", expected: UnspecifiedFormat, expectedErr: nil}, - {format: " ", expected: UnspecifiedFormat, expectedErr: nil}, - {format: "standard", expected: StandardFormat, expectedErr: nil}, - {format: "STANDARD", expected: StandardFormat, expectedErr: nil}, - {format: "json", expected: JSONFormat, expectedErr: nil}, - {format: " json ", expected: JSONFormat, expectedErr: nil}, - {format: "bogus", expected: UnspecifiedFormat, expectedErr: errors.New("unknown log format: bogus")}, - } - - for _, test := range tests { - result, err := ParseLogFormat(test.format) - if test.expected != result { - t.Errorf("expected %s, got %s", test.expected, result) - } - if !reflect.DeepEqual(test.expectedErr, err) { - t.Errorf("expected error %v, got %v", test.expectedErr, err) - } - } -} - -func Test_ParseEnv_VAULT_LOG_FORMAT(t *testing.T) { - oldVLF := os.Getenv("VAULT_LOG_FORMAT") - defer os.Setenv("VAULT_LOG_FORMAT", oldVLF) - - testParseEnvLogFormat(t, "VAULT_LOG_FORMAT") -} - -func testParseEnvLogFormat(t *testing.T, name string) { - env := []string{ - "json", "vauLT_Json", "VAULT-JSON", "vaulTJSon", - "standard", "STANDARD", - "bogus", - } - - formats := []LogFormat{ - JSONFormat, JSONFormat, JSONFormat, JSONFormat, - StandardFormat, StandardFormat, - UnspecifiedFormat, - } - - for i, e := range env { - os.Setenv(name, e) - if lf := ParseEnvLogFormat(); formats[i] != lf { - t.Errorf("expected %s, got %s", formats[i], lf) - } - } -} diff --git a/sdk/helper/ocsp/ocsp_test.go b/sdk/helper/ocsp/ocsp_test.go deleted file mode 100644 index 326a5b233..000000000 --- a/sdk/helper/ocsp/ocsp_test.go +++ /dev/null @@ -1,909 +0,0 @@ -// Copyright (c) 2017-2022 Snowflake Computing Inc. All rights reserved. - -package ocsp - -import ( - "bytes" - "context" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "errors" - "fmt" - "io" - "io/ioutil" - "math/big" - "net" - "net/http" - "net/http/httptest" - "net/url" - "sync/atomic" - "testing" - "time" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-retryablehttp" - lru "github.com/hashicorp/golang-lru" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/ocsp" -) - -func TestOCSP(t *testing.T) { - targetURL := []string{ - "https://sfcdev1.blob.core.windows.net/", - "https://sfctest0.snowflakecomputing.com/", - "https://s3-us-west-2.amazonaws.com/sfc-snowsql-updates/?prefix=1.1/windows_x86_64", - } - - conf := VerifyConfig{ - OcspFailureMode: FailOpenFalse, - } - c := New(testLogFactory, 10) - transports := []*http.Transport{ - newInsecureOcspTransport(nil), - c.NewTransport(&conf), - } - - for _, tgt := range targetURL { - c.ocspResponseCache, _ = lru.New2Q(10) - for _, tr := range transports { - c := &http.Client{ - Transport: tr, - Timeout: 30 * time.Second, - } - req, err := http.NewRequest("GET", tgt, bytes.NewReader(nil)) - if err != nil { - t.Fatalf("fail to create a request. err: %v", err) - } - res, err := c.Do(req) - if err != nil { - t.Fatalf("failed to GET contents. err: %v", err) - } - defer res.Body.Close() - _, err = ioutil.ReadAll(res.Body) - if err != nil { - t.Fatalf("failed to read content body for %v", tgt) - } - - } - } -} - -/** -// Used for development, requires an active Vault with PKI setup -func TestMultiOCSP(t *testing.T) { - - targetURL := []string{ - "https://localhost:8200/v1/pki/ocsp", - "https://localhost:8200/v1/pki/ocsp", - "https://localhost:8200/v1/pki/ocsp", - } - - b, _ := pem.Decode([]byte(vaultCert)) - caCert, _ := x509.ParseCertificate(b.Bytes) - conf := VerifyConfig{ - OcspFailureMode: FailOpenFalse, - QueryAllServers: true, - OcspServersOverride: targetURL, - ExtraCas: []*x509.Certificate{caCert}, - } - c := New(testLogFactory, 10) - transports := []*http.Transport{ - newInsecureOcspTransport(conf.ExtraCas), - c.NewTransport(&conf), - } - - tgt := "https://localhost:8200/v1/pki/ca/pem" - c.ocspResponseCache, _ = lru.New2Q(10) - for _, tr := range transports { - c := &http.Client{ - Transport: tr, - Timeout: 30 * time.Second, - } - req, err := http.NewRequest("GET", tgt, bytes.NewReader(nil)) - if err != nil { - t.Fatalf("fail to create a request. err: %v", err) - } - res, err := c.Do(req) - if err != nil { - t.Fatalf("failed to GET contents. err: %v", err) - } - defer res.Body.Close() - _, err = ioutil.ReadAll(res.Body) - if err != nil { - t.Fatalf("failed to read content body for %v", tgt) - } - } -} -*/ - -func TestUnitEncodeCertIDGood(t *testing.T) { - targetURLs := []string{ - "faketestaccount.snowflakecomputing.com:443", - "s3-us-west-2.amazonaws.com:443", - "sfcdev1.blob.core.windows.net:443", - } - for _, tt := range targetURLs { - chainedCerts := getCert(tt) - for i := 0; i < len(chainedCerts)-1; i++ { - subject := chainedCerts[i] - issuer := chainedCerts[i+1] - ocspServers := subject.OCSPServer - if len(ocspServers) == 0 { - t.Fatalf("no OCSP server is found. cert: %v", subject.Subject) - } - ocspReq, err := ocsp.CreateRequest(subject, issuer, &ocsp.RequestOptions{}) - if err != nil { - t.Fatalf("failed to create OCSP request. err: %v", err) - } - var ost *ocspStatus - _, ost = extractCertIDKeyFromRequest(ocspReq) - if ost.err != nil { - t.Fatalf("failed to extract cert ID from the OCSP request. err: %v", ost.err) - } - // better hash. Not sure if the actual OCSP server accepts this, though. - ocspReq, err = ocsp.CreateRequest(subject, issuer, &ocsp.RequestOptions{Hash: crypto.SHA512}) - if err != nil { - t.Fatalf("failed to create OCSP request. err: %v", err) - } - _, ost = extractCertIDKeyFromRequest(ocspReq) - if ost.err != nil { - t.Fatalf("failed to extract cert ID from the OCSP request. err: %v", ost.err) - } - // tweaked request binary - ocspReq, err = ocsp.CreateRequest(subject, issuer, &ocsp.RequestOptions{Hash: crypto.SHA512}) - if err != nil { - t.Fatalf("failed to create OCSP request. err: %v", err) - } - ocspReq[10] = 0 // random change - _, ost = extractCertIDKeyFromRequest(ocspReq) - if ost.err == nil { - t.Fatal("should have failed") - } - } - } -} - -func TestUnitCheckOCSPResponseCache(t *testing.T) { - conf := &VerifyConfig{OcspEnabled: true} - c := New(testLogFactory, 10) - dummyKey0 := certIDKey{ - NameHash: "dummy0", - IssuerKeyHash: "dummy0", - SerialNumber: "dummy0", - } - dummyKey := certIDKey{ - NameHash: "dummy1", - IssuerKeyHash: "dummy1", - SerialNumber: "dummy1", - } - currentTime := float64(time.Now().UTC().Unix()) - c.ocspResponseCache.Add(dummyKey0, &ocspCachedResponse{time: currentTime}) - subject := &x509.Certificate{} - issuer := &x509.Certificate{} - ost, err := c.checkOCSPResponseCache(&dummyKey, subject, issuer, conf) - if err != nil { - t.Fatal(err) - } - if ost.code != ocspMissedCache { - t.Fatalf("should have failed. expected: %v, got: %v", ocspMissedCache, ost.code) - } - // old timestamp - c.ocspResponseCache.Add(dummyKey, &ocspCachedResponse{time: float64(1395054952)}) - ost, err = c.checkOCSPResponseCache(&dummyKey, subject, issuer, conf) - if err != nil { - t.Fatal(err) - } - if ost.code != ocspCacheExpired { - t.Fatalf("should have failed. expected: %v, got: %v", ocspCacheExpired, ost.code) - } - - // invalid validity - c.ocspResponseCache.Add(dummyKey, &ocspCachedResponse{time: float64(currentTime - 1000)}) - ost, err = c.checkOCSPResponseCache(&dummyKey, subject, nil, conf) - if err == nil && isValidOCSPStatus(ost.code) { - t.Fatalf("should have failed.") - } -} - -// TestUnitValidOCSPResponse validates various combinations of acceptable OCSP responses -func TestUnitValidOCSPResponse(t *testing.T) { - rootCaKey, rootCa, leafCert := createCaLeafCerts(t) - - type tests struct { - name string - ocspRes ocsp.Response - expectedStatus ocspStatusCode - } - - now := time.Now() - ctx := context.Background() - - tt := []tests{ - { - name: "normal", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: ocsp.Good, - }, - expectedStatus: ocspStatusGood, - }, - { - name: "no-next-update", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - Status: ocsp.Good, - }, - expectedStatus: ocspStatusGood, - }, - { - name: "revoked-update", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - Status: ocsp.Revoked, - }, - expectedStatus: ocspStatusRevoked, - }, - { - name: "revoked-update-with-next-update", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(1 * time.Hour), - Status: ocsp.Revoked, - }, - expectedStatus: ocspStatusRevoked, - }, - } - for _, tc := range tt { - for _, maxAge := range []time.Duration{time.Duration(0), time.Duration(2 * time.Hour)} { - t.Run(tc.name+"-max-age-"+maxAge.String(), func(t *testing.T) { - ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - response := buildOcspResponse(t, rootCa, rootCaKey, tc.ocspRes) - _, _ = w.Write(response) - }) - ts := httptest.NewServer(ocspHandler) - defer ts.Close() - - logFactory := func() hclog.Logger { - return hclog.NewNullLogger() - } - client := New(logFactory, 100) - config := &VerifyConfig{ - OcspEnabled: true, - OcspServersOverride: []string{ts.URL}, - OcspFailureMode: FailOpenFalse, - QueryAllServers: false, - OcspThisUpdateMaxAge: maxAge, - } - - status, err := client.GetRevocationStatus(ctx, leafCert, rootCa, config) - require.NoError(t, err, "ocsp response should have been considered valid") - require.NoError(t, status.err, "ocsp status should not contain an error") - require.Equal(t, &ocspStatus{code: tc.expectedStatus}, status) - }) - } - } -} - -// TestUnitBadOCSPResponses verifies that we fail properly on a bunch of different -// OCSP response conditions -func TestUnitBadOCSPResponses(t *testing.T) { - rootCaKey, rootCa, leafCert := createCaLeafCerts(t) - rootCaKey2, rootCa2, _ := createCaLeafCerts(t) - - type tests struct { - name string - ocspRes ocsp.Response - maxAge time.Duration - ca *x509.Certificate - caKey *ecdsa.PrivateKey - errContains string - } - - now := time.Now() - ctx := context.Background() - - tt := []tests{ - { - name: "bad-signing-issuer", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: ocsp.Good, - }, - ca: rootCa2, - caKey: rootCaKey2, - errContains: "error directly verifying signature", - }, - { - name: "incorrect-serial-number", - ocspRes: ocsp.Response{ - SerialNumber: big.NewInt(1000), - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: ocsp.Good, - }, - ca: rootCa, - caKey: rootCaKey, - errContains: "did not match the leaf certificate serial number", - }, - { - name: "expired-next-update", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(-30 * time.Minute), - Status: ocsp.Good, - }, - errContains: "invalid validity", - }, - { - name: "this-update-in-future", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(1 * time.Hour), - NextUpdate: now.Add(2 * time.Hour), - Status: ocsp.Good, - }, - errContains: "invalid validity", - }, - { - name: "next-update-before-this-update", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(-2 * time.Hour), - Status: ocsp.Good, - }, - errContains: "invalid validity", - }, - { - name: "missing-this-update", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - NextUpdate: now.Add(2 * time.Hour), - Status: ocsp.Good, - }, - errContains: "invalid validity", - }, - { - name: "unknown-status", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: ocsp.Unknown, - }, - errContains: "OCSP status unknown", - }, - { - name: "over-max-age", - ocspRes: ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(30 * time.Minute), - Status: ocsp.Good, - }, - maxAge: 10 * time.Minute, - errContains: "is greater than max age", - }, - } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - useCa := rootCa - useCaKey := rootCaKey - if tc.ca != nil { - useCa = tc.ca - } - if tc.caKey != nil { - useCaKey = tc.caKey - } - response := buildOcspResponse(t, useCa, useCaKey, tc.ocspRes) - _, _ = w.Write(response) - }) - ts := httptest.NewServer(ocspHandler) - defer ts.Close() - - logFactory := func() hclog.Logger { - return hclog.NewNullLogger() - } - client := New(logFactory, 100) - - config := &VerifyConfig{ - OcspEnabled: true, - OcspServersOverride: []string{ts.URL}, - OcspFailureMode: FailOpenFalse, - QueryAllServers: false, - OcspThisUpdateMaxAge: tc.maxAge, - } - - status, err := client.GetRevocationStatus(ctx, leafCert, rootCa, config) - if err == nil && status == nil || (status != nil && status.err == nil) { - t.Fatalf("expected an error got none") - } - if err != nil { - require.ErrorContains(t, err, tc.errContains, - "Expected error got response: %v, %v", status, err) - } - if status != nil && status.err != nil { - require.ErrorContains(t, status.err, tc.errContains, - "Expected error got response: %v, %v", status, err) - } - }) - } -} - -// TestUnitZeroNextUpdateAreNotCached verifies that we are not caching the responses -// with no NextUpdate field set as according to RFC6960 4.2.2.1 -// "If nextUpdate is not set, the responder is indicating that newer -// revocation information is available all the time." -func TestUnitZeroNextUpdateAreNotCached(t *testing.T) { - rootCaKey, rootCa, leafCert := createCaLeafCerts(t) - numQueries := &atomic.Uint32{} - ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - numQueries.Add(1) - now := time.Now() - ocspRes := ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - Status: ocsp.Good, - } - response := buildOcspResponse(t, rootCa, rootCaKey, ocspRes) - _, _ = w.Write(response) - }) - ts := httptest.NewServer(ocspHandler) - defer ts.Close() - - logFactory := func() hclog.Logger { - return hclog.NewNullLogger() - } - client := New(logFactory, 100) - - config := &VerifyConfig{ - OcspEnabled: true, - OcspServersOverride: []string{ts.URL}, - } - - _, err := client.GetRevocationStatus(context.Background(), leafCert, rootCa, config) - require.NoError(t, err, "Failed fetching revocation status") - - _, err = client.GetRevocationStatus(context.Background(), leafCert, rootCa, config) - require.NoError(t, err, "Failed fetching revocation status second time") - - require.Equal(t, uint32(2), numQueries.Load()) -} - -// TestUnitResponsesAreCached verify that the OCSP responses are properly cached when -// querying for the same leaf certificates -func TestUnitResponsesAreCached(t *testing.T) { - rootCaKey, rootCa, leafCert := createCaLeafCerts(t) - numQueries := &atomic.Uint32{} - ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - numQueries.Add(1) - now := time.Now() - ocspRes := ocsp.Response{ - SerialNumber: leafCert.SerialNumber, - ThisUpdate: now.Add(-1 * time.Hour), - NextUpdate: now.Add(1 * time.Hour), - Status: ocsp.Good, - } - response := buildOcspResponse(t, rootCa, rootCaKey, ocspRes) - _, _ = w.Write(response) - }) - ts1 := httptest.NewServer(ocspHandler) - ts2 := httptest.NewServer(ocspHandler) - defer ts1.Close() - defer ts2.Close() - - logFactory := func() hclog.Logger { - return hclog.NewNullLogger() - } - client := New(logFactory, 100) - - config := &VerifyConfig{ - OcspEnabled: true, - OcspServersOverride: []string{ts1.URL, ts2.URL}, - QueryAllServers: true, - } - - _, err := client.GetRevocationStatus(context.Background(), leafCert, rootCa, config) - require.NoError(t, err, "Failed fetching revocation status") - // Make sure that we queried both servers and not the cache - require.Equal(t, uint32(2), numQueries.Load()) - - // These query should be cached and not influence our counter - _, err = client.GetRevocationStatus(context.Background(), leafCert, rootCa, config) - require.NoError(t, err, "Failed fetching revocation status second time") - - require.Equal(t, uint32(2), numQueries.Load()) -} - -func buildOcspResponse(t *testing.T, ca *x509.Certificate, caKey *ecdsa.PrivateKey, ocspRes ocsp.Response) []byte { - response, err := ocsp.CreateResponse(ca, ca, ocspRes, caKey) - if err != nil { - t.Fatalf("failed generating OCSP response: %v", err) - } - return response -} - -func createCaLeafCerts(t *testing.T) (*ecdsa.PrivateKey, *x509.Certificate, *x509.Certificate) { - rootCaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated root key for CA") - - // Validate we reject CSRs that contain CN that aren't in the original order - cr := &x509.Certificate{ - Subject: pkix.Name{CommonName: "Root Cert"}, - SerialNumber: big.NewInt(1), - IsCA: true, - BasicConstraintsValid: true, - SignatureAlgorithm: x509.ECDSAWithSHA256, - NotBefore: time.Now().Add(-1 * time.Second), - NotAfter: time.Now().AddDate(1, 0, 0), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning}, - } - rootCaBytes, err := x509.CreateCertificate(rand.Reader, cr, cr, &rootCaKey.PublicKey, rootCaKey) - require.NoError(t, err, "failed generating root ca") - - rootCa, err := x509.ParseCertificate(rootCaBytes) - require.NoError(t, err, "failed parsing root ca") - - leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "failed generated leaf key") - - cr = &x509.Certificate{ - Subject: pkix.Name{CommonName: "Leaf Cert"}, - SerialNumber: big.NewInt(2), - SignatureAlgorithm: x509.ECDSAWithSHA256, - NotBefore: time.Now().Add(-1 * time.Second), - NotAfter: time.Now().AddDate(1, 0, 0), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - } - leafCertBytes, err := x509.CreateCertificate(rand.Reader, cr, rootCa, &leafKey.PublicKey, rootCaKey) - require.NoError(t, err, "failed generating root ca") - - leafCert, err := x509.ParseCertificate(leafCertBytes) - require.NoError(t, err, "failed parsing root ca") - return rootCaKey, rootCa, leafCert -} - -func TestUnitValidateOCSP(t *testing.T) { - conf := &VerifyConfig{OcspEnabled: true} - ocspRes := &ocsp.Response{} - ost, err := validateOCSP(conf, ocspRes) - if err == nil && isValidOCSPStatus(ost.code) { - t.Fatalf("should have failed.") - } - - currentTime := time.Now() - ocspRes.ThisUpdate = currentTime.Add(-2 * time.Hour) - ocspRes.NextUpdate = currentTime.Add(2 * time.Hour) - ocspRes.Status = ocsp.Revoked - ost, err = validateOCSP(conf, ocspRes) - if err != nil { - t.Fatal(err) - } - - if ost.code != ocspStatusRevoked { - t.Fatalf("should have failed. expected: %v, got: %v", ocspStatusRevoked, ost.code) - } - ocspRes.Status = ocsp.Good - ost, err = validateOCSP(conf, ocspRes) - if err != nil { - t.Fatal(err) - } - - if ost.code != ocspStatusGood { - t.Fatalf("should have success. expected: %v, got: %v", ocspStatusGood, ost.code) - } - ocspRes.Status = ocsp.Unknown - ost, err = validateOCSP(conf, ocspRes) - if err != nil { - t.Fatal(err) - } - if ost.code != ocspStatusUnknown { - t.Fatalf("should have failed. expected: %v, got: %v", ocspStatusUnknown, ost.code) - } - ocspRes.Status = ocsp.ServerFailed - ost, err = validateOCSP(conf, ocspRes) - if err != nil { - t.Fatal(err) - } - if ost.code != ocspStatusOthers { - t.Fatalf("should have failed. expected: %v, got: %v", ocspStatusOthers, ost.code) - } -} - -func TestUnitEncodeCertID(t *testing.T) { - var st *ocspStatus - _, st = extractCertIDKeyFromRequest([]byte{0x1, 0x2}) - if st.code != ocspFailedDecomposeRequest { - t.Fatalf("failed to get OCSP status. expected: %v, got: %v", ocspFailedDecomposeRequest, st.code) - } -} - -func getCert(addr string) []*x509.Certificate { - tcpConn, err := net.DialTimeout("tcp", addr, 40*time.Second) - if err != nil { - panic(err) - } - defer tcpConn.Close() - - err = tcpConn.SetDeadline(time.Now().Add(10 * time.Second)) - if err != nil { - panic(err) - } - config := tls.Config{InsecureSkipVerify: true, ServerName: addr} - - conn := tls.Client(tcpConn, &config) - defer conn.Close() - - err = conn.Handshake() - if err != nil { - panic(err) - } - - state := conn.ConnectionState() - - return state.PeerCertificates -} - -func TestOCSPRetry(t *testing.T) { - c := New(testLogFactory, 10) - certs := getCert("s3-us-west-2.amazonaws.com:443") - dummyOCSPHost := &url.URL{ - Scheme: "https", - Host: "dummyOCSPHost", - } - client := &fakeHTTPClient{ - cnt: 3, - success: true, - body: []byte{1, 2, 3}, - logger: hclog.New(hclog.DefaultOptions), - t: t, - } - res, b, st, err := c.retryOCSP( - context.TODO(), - client, fakeRequestFunc, - dummyOCSPHost, - make(map[string]string), []byte{0}, certs[0], certs[len(certs)-1]) - if err == nil { - fmt.Printf("should fail: %v, %v, %v\n", res, b, st) - } - client = &fakeHTTPClient{ - cnt: 30, - success: true, - body: []byte{1, 2, 3}, - logger: hclog.New(hclog.DefaultOptions), - t: t, - } - res, b, st, err = c.retryOCSP( - context.TODO(), - client, fakeRequestFunc, - dummyOCSPHost, - make(map[string]string), []byte{0}, certs[0], certs[len(certs)-1]) - if err == nil { - fmt.Printf("should fail: %v, %v, %v\n", res, b, st) - } -} - -type tcCanEarlyExit struct { - results []*ocspStatus - resultLen int - retFailOpen *ocspStatus - retFailClosed *ocspStatus -} - -func TestCanEarlyExitForOCSP(t *testing.T) { - testcases := []tcCanEarlyExit{ - { // 0 - results: []*ocspStatus{ - { - code: ocspStatusGood, - }, - { - code: ocspStatusGood, - }, - { - code: ocspStatusGood, - }, - }, - retFailOpen: nil, - retFailClosed: nil, - }, - { // 1 - results: []*ocspStatus{ - { - code: ocspStatusRevoked, - err: errors.New("revoked"), - }, - { - code: ocspStatusGood, - }, - { - code: ocspStatusGood, - }, - }, - retFailOpen: &ocspStatus{ocspStatusRevoked, errors.New("revoked")}, - retFailClosed: &ocspStatus{ocspStatusRevoked, errors.New("revoked")}, - }, - { // 2 - results: []*ocspStatus{ - { - code: ocspStatusUnknown, - err: errors.New("unknown"), - }, - { - code: ocspStatusGood, - }, - { - code: ocspStatusGood, - }, - }, - retFailOpen: nil, - retFailClosed: &ocspStatus{ocspStatusUnknown, errors.New("unknown")}, - }, - { // 3: not taken as revoked if any invalid OCSP response (ocspInvalidValidity) is included. - results: []*ocspStatus{ - { - code: ocspStatusRevoked, - err: errors.New("revoked"), - }, - { - code: ocspInvalidValidity, - }, - { - code: ocspStatusGood, - }, - }, - retFailOpen: nil, - retFailClosed: &ocspStatus{ocspStatusRevoked, errors.New("revoked")}, - }, - { // 4: not taken as revoked if the number of results don't match the expected results. - results: []*ocspStatus{ - { - code: ocspStatusRevoked, - err: errors.New("revoked"), - }, - { - code: ocspStatusGood, - }, - }, - resultLen: 3, - retFailOpen: nil, - retFailClosed: &ocspStatus{ocspStatusRevoked, errors.New("revoked")}, - }, - } - c := New(testLogFactory, 10) - for idx, tt := range testcases { - expectedLen := len(tt.results) - if tt.resultLen > 0 { - expectedLen = tt.resultLen - } - r := c.canEarlyExitForOCSP(tt.results, expectedLen, &VerifyConfig{OcspFailureMode: FailOpenTrue}) - if !(tt.retFailOpen == nil && r == nil) && !(tt.retFailOpen != nil && r != nil && tt.retFailOpen.code == r.code) { - t.Fatalf("%d: failed to match return. expected: %v, got: %v", idx, tt.retFailOpen, r) - } - r = c.canEarlyExitForOCSP(tt.results, expectedLen, &VerifyConfig{OcspFailureMode: FailOpenFalse}) - if !(tt.retFailClosed == nil && r == nil) && !(tt.retFailClosed != nil && r != nil && tt.retFailClosed.code == r.code) { - t.Fatalf("%d: failed to match return. expected: %v, got: %v", idx, tt.retFailClosed, r) - } - } -} - -var testLogger = hclog.New(hclog.DefaultOptions) - -func testLogFactory() hclog.Logger { - return testLogger -} - -type fakeHTTPClient struct { - cnt int // number of retry - success bool // return success after retry in cnt times - timeout bool // timeout - body []byte // return body - t *testing.T - logger hclog.Logger - redirected bool -} - -func (c *fakeHTTPClient) Do(_ *retryablehttp.Request) (*http.Response, error) { - c.cnt-- - if c.cnt < 0 { - c.cnt = 0 - } - c.t.Log("fakeHTTPClient.cnt", c.cnt) - - var retcode int - if !c.redirected { - c.redirected = true - c.cnt++ - retcode = 405 - } else if c.success && c.cnt == 1 { - retcode = 200 - } else { - if c.timeout { - // simulate timeout - time.Sleep(time.Second * 1) - return nil, &fakeHTTPError{ - err: "Whatever reason (Client.Timeout exceeded while awaiting headers)", - timeout: true, - } - } - retcode = 0 - } - - ret := &http.Response{ - StatusCode: retcode, - Body: &fakeResponseBody{body: c.body}, - } - return ret, nil -} - -type fakeHTTPError struct { - err string - timeout bool -} - -func (e *fakeHTTPError) Error() string { return e.err } -func (e *fakeHTTPError) Timeout() bool { return e.timeout } -func (e *fakeHTTPError) Temporary() bool { return true } - -type fakeResponseBody struct { - body []byte - cnt int -} - -func (b *fakeResponseBody) Read(p []byte) (n int, err error) { - if b.cnt == 0 { - copy(p, b.body) - b.cnt = 1 - return len(b.body), nil - } - b.cnt = 0 - return 0, io.EOF -} - -func (b *fakeResponseBody) Close() error { - return nil -} - -func fakeRequestFunc(_, _ string, _ interface{}) (*retryablehttp.Request, error) { - return nil, nil -} - -const vaultCert = `-----BEGIN CERTIFICATE----- -MIIDuTCCAqGgAwIBAgIUA6VeVD1IB5rXcCZRAqPO4zr/GAMwDQYJKoZIhvcNAQEL -BQAwcjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMREwDwYDVQQHDAhTb21lQ2l0 -eTESMBAGA1UECgwJTXlDb21wYW55MRMwEQYDVQQLDApNeURpdmlzaW9uMRowGAYD -VQQDDBF3d3cuY29uaHVnZWNvLmNvbTAeFw0yMjA5MDcxOTA1MzdaFw0yNDA5MDYx -OTA1MzdaMHIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJWQTERMA8GA1UEBwwIU29t -ZUNpdHkxEjAQBgNVBAoMCU15Q29tcGFueTETMBEGA1UECwwKTXlEaXZpc2lvbjEa -MBgGA1UEAwwRd3d3LmNvbmh1Z2Vjby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDL9qzEXi4PIafSAqfcwcmjujFvbG1QZbI8swxnD+w8i4ufAQU5 -LDmvMrGo3ZbhJ0mCihYmFxpjhRdP2raJQ9TysHlPXHtDRpr9ckWTKBz2oIfqVtJ2 -qzteQkWCkDAO7kPqzgCFsMeoMZeONRkeGib0lEzQAbW/Rqnphg8zVVkyQ71DZ7Pc -d5WkC2E28kKcSramhWfVFpxG3hSIrLOX2esEXteLRzKxFPf+gi413JZFKYIWrebP -u5t0++MLNpuX322geoki4BWMjQsd47XILmxZ4aj33ScZvdrZESCnwP76hKIxg9mO -lMxrqSWKVV5jHZrElSEj9LYJgDO1Y6eItn7hAgMBAAGjRzBFMAsGA1UdDwQEAwIE -MDATBgNVHSUEDDAKBggrBgEFBQcDATAhBgNVHREEGjAYggtleGFtcGxlLmNvbYIJ -bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQA5dPdf5SdtMwe2uSspO/EuWqbM -497vMQBW1Ey8KRKasJjhvOVYMbe7De5YsnW4bn8u5pl0zQGF4hEtpmifAtVvziH/ -K+ritQj9VVNbLLCbFcg+b0kfjt4yrDZ64vWvIeCgPjG1Kme8gdUUWgu9dOud5gdx -qg/tIFv4TRS/eIIymMlfd9owOD3Ig6S5fy4NaAJFAwXf8+3Rzuc+e7JSAPgAufjh -tOTWinxvoiOLuYwo9CyGgq4qKBFsrY0aE0gdA7oTQkpbEbo2EbqiWUl/PTCl1Y4Z -nSZ0n+4q9QC9RLrWwYTwh838d5RVLUst2mBKSA+vn7YkqmBJbdBC6nkd7n7H ------END CERTIFICATE----- -` diff --git a/sdk/helper/pathmanager/pathmanager_test.go b/sdk/helper/pathmanager/pathmanager_test.go deleted file mode 100644 index 515d83032..000000000 --- a/sdk/helper/pathmanager/pathmanager_test.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package pathmanager - -import ( - "reflect" - "testing" -) - -func TestPathManager(t *testing.T) { - m := New() - - if m.Len() != 0 { - t.Fatalf("bad: path length expect 0, got %d", len(m.Paths())) - } - - paths := []string{ - "path1/", - "path2/", - "path3/", - } - - for _, path := range paths { - if m.HasPath(path) { - t.Fatalf("path should not exist in filtered paths %q", path) - } - } - - // add paths - m.AddPaths(paths) - if m.Len() != 3 { - t.Fatalf("bad: path length expect 3, got %d", len(m.Paths())) - } - if !reflect.DeepEqual(paths, m.Paths()) { - t.Fatalf("mismatch in paths") - } - for _, path := range paths { - if !m.HasPath(path) { - t.Fatalf("path should exist in filtered paths %q", path) - } - } - - // remove the paths - m.RemovePaths(paths) - - for _, path := range paths { - if m.HasPath(path) { - t.Fatalf("path should not exist in filtered paths %q", path) - } - } -} - -func TestPathManager_RemovePrefix(t *testing.T) { - m := New() - - if m.Len() != 0 { - t.Fatalf("bad: path length expect 0, got %d", len(m.Paths())) - } - - paths := []string{ - "path1/", - "path2/", - "path3/", - } - - for _, path := range paths { - if m.HasPath(path) { - t.Fatalf("path should not exist in filtered paths %q", path) - } - } - - // add paths - m.AddPaths(paths) - if m.Len() != 3 { - t.Fatalf("bad: path length expect 3, got %d", len(m.Paths())) - } - if !reflect.DeepEqual(paths, m.Paths()) { - t.Fatalf("mismatch in paths") - } - for _, path := range paths { - if !m.HasPath(path) { - t.Fatalf("path should exist in filtered paths %q", path) - } - } - - // remove the paths - m.RemovePathPrefix("path") - - if m.Len() != 0 { - t.Fatalf("bad: path length expect 0, got %d", len(m.Paths())) - } - - for _, path := range paths { - if m.HasPath(path) { - t.Fatalf("path should not exist in filtered paths %q", path) - } - } -} - -func TestPathManager_HasExactPath(t *testing.T) { - m := New() - paths := []string{ - "path1/key1", - "path1/key1/subkey1", - "path1/key1/subkey2", - "path1/key1/subkey3", - "path2/*", - "path3/", - "!path4/key1", - "!path5/*", - } - m.AddPaths(paths) - if m.Len() != len(paths) { - t.Fatalf("path count does not match: expected %d, got %d", len(paths), m.Len()) - } - - type tCase struct { - key string - expect bool - } - - tcases := []tCase{ - {"path1/key1", true}, - {"path2/key1", true}, - {"path3/key1", true}, - {"path1/key1/subkey1", true}, - {"path1/key1/subkey99", false}, - {"path2/key1/subkey1", true}, - {"path1/key1/subkey1/subkey1", false}, - {"nonexistentpath/key1", false}, - {"path4/key1", false}, - {"path5/key1/subkey1", false}, - } - - for _, tc := range tcases { - if match := m.HasExactPath(tc.key); match != tc.expect { - t.Fatalf("incorrect match: key %q", tc.key) - } - } - - m.RemovePaths(paths) - if len(m.Paths()) != 0 { - t.Fatalf("removing all paths did not clear manager: paths %v", m.Paths()) - } -} - -func TestPathManager_HasPath(t *testing.T) { - m := New() - - m.AddPaths([]string{"a/b/c/"}) - if m.HasPath("a/") { - t.Fatal("should not have path 'a/'") - } - if m.HasPath("a/b/") { - t.Fatal("should not have path 'a/b/'") - } - if !m.HasPath("a/b/c/") { - t.Fatal("should have path 'a/b/c'") - } - - m.AddPaths([]string{"a/"}) - if !m.HasPath("a/") { - t.Fatal("should have path 'a/'") - } - if !m.HasPath("a/b/") { - t.Fatal("should have path 'a/b/'") - } - if !m.HasPath("a/b/c/") { - t.Fatal("should have path 'a/b/c'") - } - - m.RemovePaths([]string{"a/"}) - if m.HasPath("a/") { - t.Fatal("should not have path 'a/'") - } - if m.HasPath("a/b/") { - t.Fatal("should not have path 'a/b/'") - } - if !m.HasPath("a/b/c/") { - t.Fatal("should have path 'a/b/c'") - } -} diff --git a/sdk/helper/pluginutil/env_test.go b/sdk/helper/pluginutil/env_test.go deleted file mode 100644 index 21f77faba..000000000 --- a/sdk/helper/pluginutil/env_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package pluginutil - -import ( - "os" - "testing" -) - -func TestGRPCSupport(t *testing.T) { - cases := []struct { - envVersion string - expected bool - }{ - { - "0.8.3", - false, - }, - { - "0.9.2", - false, - }, - { - "0.9.3", - false, - }, - { - "0.9.4+ent", - true, - }, - { - "0.9.4-beta", - false, - }, - { - "0.9.4", - true, - }, - { - "unknown", - true, - }, - { - "", - false, - }, - } - - for _, tc := range cases { - t.Run(tc.envVersion, func(t *testing.T) { - err := os.Setenv(PluginVaultVersionEnv, tc.envVersion) - if err != nil { - t.Fatal(err) - } - - result := GRPCSupport() - - if result != tc.expected { - t.Fatalf("got: %t, expected: %t", result, tc.expected) - } - }) - } -} diff --git a/sdk/helper/pluginutil/multiplexing_test.go b/sdk/helper/pluginutil/multiplexing_test.go deleted file mode 100644 index 3f589ffa7..000000000 --- a/sdk/helper/pluginutil/multiplexing_test.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package pluginutil - -import ( - "context" - "fmt" - "reflect" - "testing" - - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" -) - -func TestMultiplexingSupported(t *testing.T) { - type args struct { - ctx context.Context - cc grpc.ClientConnInterface - name string - } - - type testCase struct { - name string - args args - env string - want bool - wantErr bool - } - - tests := []testCase{ - { - name: "multiplexing is supported if plugin is not opted out", - args: args{ - ctx: context.Background(), - cc: &MockClientConnInterfaceNoop{}, - name: "plugin", - }, - env: "", - want: true, - }, - { - name: "multiplexing is not supported if plugin is opted out", - args: args{ - ctx: context.Background(), - cc: &MockClientConnInterfaceNoop{}, - name: "optedOutPlugin", - }, - env: "optedOutPlugin", - want: false, - }, - { - name: "multiplexing is not supported if plugin among one of the opted out", - args: args{ - ctx: context.Background(), - cc: &MockClientConnInterfaceNoop{}, - name: "optedOutPlugin", - }, - env: "firstPlugin,optedOutPlugin,otherPlugin", - want: false, - }, - { - name: "multiplexing is supported if different plugin is opted out", - args: args{ - ctx: context.Background(), - cc: &MockClientConnInterfaceNoop{}, - name: "plugin", - }, - env: "optedOutPlugin", - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Setenv(PluginMultiplexingOptOut, tt.env) - got, err := MultiplexingSupported(tt.args.ctx, tt.args.cc, tt.args.name) - if (err != nil) != tt.wantErr { - t.Errorf("MultiplexingSupported() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("MultiplexingSupported() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestGetMultiplexIDFromContext(t *testing.T) { - type testCase struct { - ctx context.Context - expectedResp string - expectedErr error - } - - tests := map[string]testCase{ - "missing plugin multiplexing metadata": { - ctx: context.Background(), - expectedResp: "", - expectedErr: fmt.Errorf("missing plugin multiplexing metadata"), - }, - "unexpected number of IDs in metadata": { - ctx: idCtx(t, "12345", "67891"), - expectedResp: "", - expectedErr: fmt.Errorf("unexpected number of IDs in metadata: (2)"), - }, - "empty multiplex ID in metadata": { - ctx: idCtx(t, ""), - expectedResp: "", - expectedErr: fmt.Errorf("empty multiplex ID in metadata"), - }, - "happy path, id is returned from metadata": { - ctx: idCtx(t, "12345"), - expectedResp: "12345", - expectedErr: nil, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - resp, err := GetMultiplexIDFromContext(test.ctx) - - if test.expectedErr != nil && test.expectedErr.Error() != "" && err == nil { - t.Fatalf("err expected, got nil") - } else if !reflect.DeepEqual(err, test.expectedErr) { - t.Fatalf("Actual error: %#v\nExpected error: %#v", err, test.expectedErr) - } - - if test.expectedErr != nil && test.expectedErr.Error() == "" && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if !reflect.DeepEqual(resp, test.expectedResp) { - t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) - } - }) - } -} - -// idCtx is a test helper that will return a context with the IDs set in its -// metadata -func idCtx(t *testing.T, ids ...string) context.Context { - // Context doesn't need to timeout since this is just passed through - ctx := context.Background() - md := metadata.MD{} - for _, id := range ids { - md.Append(MultiplexingCtxKey, id) - } - return metadata.NewIncomingContext(ctx, md) -} - -type MockClientConnInterfaceNoop struct{} - -func (m *MockClientConnInterfaceNoop) Invoke(_ context.Context, _ string, _ interface{}, reply interface{}, _ ...grpc.CallOption) error { - reply.(*MultiplexingSupportResponse).Supported = true - return nil -} - -func (m *MockClientConnInterfaceNoop) NewStream(_ context.Context, _ *grpc.StreamDesc, _ string, _ ...grpc.CallOption) (grpc.ClientStream, error) { - return nil, nil -} diff --git a/sdk/helper/pluginutil/run_config_test.go b/sdk/helper/pluginutil/run_config_test.go deleted file mode 100644 index e64057783..000000000 --- a/sdk/helper/pluginutil/run_config_test.go +++ /dev/null @@ -1,360 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package pluginutil - -import ( - "context" - "fmt" - "os/exec" - "testing" - "time" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-plugin" - "github.com/hashicorp/vault/sdk/helper/wrapping" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestMakeConfig(t *testing.T) { - type testCase struct { - rc runConfig - - responseWrapInfo *wrapping.ResponseWrapInfo - responseWrapInfoErr error - responseWrapInfoTimes int - - mlockEnabled bool - mlockEnabledTimes int - - expectedConfig *plugin.ClientConfig - expectTLSConfig bool - } - - tests := map[string]testCase{ - "metadata mode, not-AutoMTLS": { - rc: runConfig{ - command: "echo", - args: []string{"foo", "bar"}, - sha256: []byte("some_sha256"), - env: []string{"initial=true"}, - PluginClientConfig: PluginClientConfig{ - PluginSets: map[int]plugin.PluginSet{ - 1: { - "bogus": nil, - }, - }, - HandshakeConfig: plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "magic_cookie_key", - MagicCookieValue: "magic_cookie_value", - }, - Logger: hclog.NewNullLogger(), - IsMetadataMode: true, - AutoMTLS: false, - }, - }, - - responseWrapInfoTimes: 0, - - mlockEnabled: false, - mlockEnabledTimes: 1, - - expectedConfig: &plugin.ClientConfig{ - HandshakeConfig: plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "magic_cookie_key", - MagicCookieValue: "magic_cookie_value", - }, - VersionedPlugins: map[int]plugin.PluginSet{ - 1: { - "bogus": nil, - }, - }, - Cmd: commandWithEnv( - "echo", - []string{"foo", "bar"}, - []string{ - "initial=true", - fmt.Sprintf("%s=%s", PluginVaultVersionEnv, "dummyversion"), - fmt.Sprintf("%s=%t", PluginMetadataModeEnv, true), - fmt.Sprintf("%s=%t", PluginAutoMTLSEnv, false), - }, - ), - SecureConfig: &plugin.SecureConfig{ - Checksum: []byte("some_sha256"), - // Hash is generated - }, - AllowedProtocols: []plugin.Protocol{ - plugin.ProtocolNetRPC, - plugin.ProtocolGRPC, - }, - Logger: hclog.NewNullLogger(), - AutoMTLS: false, - }, - expectTLSConfig: false, - }, - "non-metadata mode, not-AutoMTLS": { - rc: runConfig{ - command: "echo", - args: []string{"foo", "bar"}, - sha256: []byte("some_sha256"), - env: []string{"initial=true"}, - PluginClientConfig: PluginClientConfig{ - PluginSets: map[int]plugin.PluginSet{ - 1: { - "bogus": nil, - }, - }, - HandshakeConfig: plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "magic_cookie_key", - MagicCookieValue: "magic_cookie_value", - }, - Logger: hclog.NewNullLogger(), - IsMetadataMode: false, - AutoMTLS: false, - }, - }, - - responseWrapInfo: &wrapping.ResponseWrapInfo{ - Token: "testtoken", - }, - responseWrapInfoTimes: 1, - - mlockEnabled: true, - mlockEnabledTimes: 1, - - expectedConfig: &plugin.ClientConfig{ - HandshakeConfig: plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "magic_cookie_key", - MagicCookieValue: "magic_cookie_value", - }, - VersionedPlugins: map[int]plugin.PluginSet{ - 1: { - "bogus": nil, - }, - }, - Cmd: commandWithEnv( - "echo", - []string{"foo", "bar"}, - []string{ - "initial=true", - fmt.Sprintf("%s=%t", PluginMlockEnabled, true), - fmt.Sprintf("%s=%s", PluginVaultVersionEnv, "dummyversion"), - fmt.Sprintf("%s=%t", PluginMetadataModeEnv, false), - fmt.Sprintf("%s=%t", PluginAutoMTLSEnv, false), - fmt.Sprintf("%s=%s", PluginUnwrapTokenEnv, "testtoken"), - }, - ), - SecureConfig: &plugin.SecureConfig{ - Checksum: []byte("some_sha256"), - // Hash is generated - }, - AllowedProtocols: []plugin.Protocol{ - plugin.ProtocolNetRPC, - plugin.ProtocolGRPC, - }, - Logger: hclog.NewNullLogger(), - AutoMTLS: false, - }, - expectTLSConfig: true, - }, - "metadata mode, AutoMTLS": { - rc: runConfig{ - command: "echo", - args: []string{"foo", "bar"}, - sha256: []byte("some_sha256"), - env: []string{"initial=true"}, - PluginClientConfig: PluginClientConfig{ - PluginSets: map[int]plugin.PluginSet{ - 1: { - "bogus": nil, - }, - }, - HandshakeConfig: plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "magic_cookie_key", - MagicCookieValue: "magic_cookie_value", - }, - Logger: hclog.NewNullLogger(), - IsMetadataMode: true, - AutoMTLS: true, - }, - }, - - responseWrapInfoTimes: 0, - - mlockEnabled: false, - mlockEnabledTimes: 1, - - expectedConfig: &plugin.ClientConfig{ - HandshakeConfig: plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "magic_cookie_key", - MagicCookieValue: "magic_cookie_value", - }, - VersionedPlugins: map[int]plugin.PluginSet{ - 1: { - "bogus": nil, - }, - }, - Cmd: commandWithEnv( - "echo", - []string{"foo", "bar"}, - []string{ - "initial=true", - fmt.Sprintf("%s=%s", PluginVaultVersionEnv, "dummyversion"), - fmt.Sprintf("%s=%t", PluginMetadataModeEnv, true), - fmt.Sprintf("%s=%t", PluginAutoMTLSEnv, true), - }, - ), - SecureConfig: &plugin.SecureConfig{ - Checksum: []byte("some_sha256"), - // Hash is generated - }, - AllowedProtocols: []plugin.Protocol{ - plugin.ProtocolNetRPC, - plugin.ProtocolGRPC, - }, - Logger: hclog.NewNullLogger(), - AutoMTLS: true, - }, - expectTLSConfig: false, - }, - "not-metadata mode, AutoMTLS": { - rc: runConfig{ - command: "echo", - args: []string{"foo", "bar"}, - sha256: []byte("some_sha256"), - env: []string{"initial=true"}, - PluginClientConfig: PluginClientConfig{ - PluginSets: map[int]plugin.PluginSet{ - 1: { - "bogus": nil, - }, - }, - HandshakeConfig: plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "magic_cookie_key", - MagicCookieValue: "magic_cookie_value", - }, - Logger: hclog.NewNullLogger(), - IsMetadataMode: false, - AutoMTLS: true, - }, - }, - - responseWrapInfoTimes: 0, - - mlockEnabled: false, - mlockEnabledTimes: 1, - - expectedConfig: &plugin.ClientConfig{ - HandshakeConfig: plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "magic_cookie_key", - MagicCookieValue: "magic_cookie_value", - }, - VersionedPlugins: map[int]plugin.PluginSet{ - 1: { - "bogus": nil, - }, - }, - Cmd: commandWithEnv( - "echo", - []string{"foo", "bar"}, - []string{ - "initial=true", - fmt.Sprintf("%s=%s", PluginVaultVersionEnv, "dummyversion"), - fmt.Sprintf("%s=%t", PluginMetadataModeEnv, false), - fmt.Sprintf("%s=%t", PluginAutoMTLSEnv, true), - }, - ), - SecureConfig: &plugin.SecureConfig{ - Checksum: []byte("some_sha256"), - // Hash is generated - }, - AllowedProtocols: []plugin.Protocol{ - plugin.ProtocolNetRPC, - plugin.ProtocolGRPC, - }, - Logger: hclog.NewNullLogger(), - AutoMTLS: true, - }, - expectTLSConfig: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - mockWrapper := new(mockRunnerUtil) - mockWrapper.On("ResponseWrapData", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(test.responseWrapInfo, test.responseWrapInfoErr) - mockWrapper.On("MlockEnabled"). - Return(test.mlockEnabled) - test.rc.Wrapper = mockWrapper - defer mockWrapper.AssertNumberOfCalls(t, "ResponseWrapData", test.responseWrapInfoTimes) - defer mockWrapper.AssertNumberOfCalls(t, "MlockEnabled", test.mlockEnabledTimes) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - config, err := test.rc.makeConfig(ctx) - if err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - // The following fields are generated, so we just need to check for existence, not specific value - // The value must be nilled out before performing a DeepEqual check - hsh := config.SecureConfig.Hash - if hsh == nil { - t.Fatalf("Missing SecureConfig.Hash") - } - config.SecureConfig.Hash = nil - - if test.expectTLSConfig && config.TLSConfig == nil { - t.Fatalf("TLS config expected, got nil") - } - if !test.expectTLSConfig && config.TLSConfig != nil { - t.Fatalf("no TLS config expected, got: %#v", config.TLSConfig) - } - config.TLSConfig = nil - - require.Equal(t, test.expectedConfig, config) - }) - } -} - -func commandWithEnv(cmd string, args []string, env []string) *exec.Cmd { - c := exec.Command(cmd, args...) - c.Env = env - return c -} - -var _ RunnerUtil = &mockRunnerUtil{} - -type mockRunnerUtil struct { - mock.Mock -} - -func (m *mockRunnerUtil) VaultVersion(ctx context.Context) (string, error) { - return "dummyversion", nil -} - -func (m *mockRunnerUtil) NewPluginClient(ctx context.Context, config PluginClientConfig) (PluginClient, error) { - args := m.Called(ctx, config) - return args.Get(0).(PluginClient), args.Error(1) -} - -func (m *mockRunnerUtil) ResponseWrapData(ctx context.Context, data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error) { - args := m.Called(ctx, data, ttl, jwt) - return args.Get(0).(*wrapping.ResponseWrapInfo), args.Error(1) -} - -func (m *mockRunnerUtil) MlockEnabled() bool { - args := m.Called() - return args.Bool(0) -} diff --git a/sdk/helper/policyutil/policyutil_test.go b/sdk/helper/policyutil/policyutil_test.go deleted file mode 100644 index 2280ba93e..000000000 --- a/sdk/helper/policyutil/policyutil_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package policyutil - -import "testing" - -func TestSanitizePolicies(t *testing.T) { - expected := []string{"foo", "bar"} - actual := SanitizePolicies([]string{"foo", "bar"}, false) - if !EquivalentPolicies(expected, actual) { - t.Fatalf("bad: expected:%s\ngot:%s\n", expected, actual) - } - - // If 'default' is already added, do not remove it. - expected = []string{"foo", "bar", "default"} - actual = SanitizePolicies([]string{"foo", "bar", "default"}, false) - if !EquivalentPolicies(expected, actual) { - t.Fatalf("bad: expected:%s\ngot:%s\n", expected, actual) - } -} - -func TestParsePolicies(t *testing.T) { - expected := []string{"foo", "bar", "default"} - actual := ParsePolicies("foo,bar") - // add default if not present. - if !EquivalentPolicies(expected, actual) { - t.Fatalf("bad: expected:%s\ngot:%s\n", expected, actual) - } - - // do not add default more than once. - actual = ParsePolicies("foo,bar,default") - if !EquivalentPolicies(expected, actual) { - t.Fatalf("bad: expected:%s\ngot:%s\n", expected, actual) - } - - // handle spaces and tabs. - actual = ParsePolicies(" foo , bar , default") - if !EquivalentPolicies(expected, actual) { - t.Fatalf("bad: expected:%s\ngot:%s\n", expected, actual) - } - - // ignore all others if root is present. - expected = []string{"root"} - actual = ParsePolicies("foo,bar,root") - if !EquivalentPolicies(expected, actual) { - t.Fatalf("bad: expected:%s\ngot:%s\n", expected, actual) - } - - // with spaces and tabs. - expected = []string{"root"} - actual = ParsePolicies("foo ,bar, root ") - if !EquivalentPolicies(expected, actual) { - t.Fatalf("bad: expected:%s\ngot:%s\n", expected, actual) - } -} - -func TestEquivalentPolicies(t *testing.T) { - a := []string{"foo", "bar"} - var b []string - if EquivalentPolicies(a, b) { - t.Fatal("bad") - } - - b = []string{"foo"} - if EquivalentPolicies(a, b) { - t.Fatal("bad") - } - - b = []string{"bar", "foo"} - if !EquivalentPolicies(a, b) { - t.Fatal("bad") - } - - b = []string{"foo", "default", "bar"} - if !EquivalentPolicies(a, b) { - t.Fatal("bad") - } -} diff --git a/sdk/helper/roottoken/encode_test.go b/sdk/helper/roottoken/encode_test.go deleted file mode 100644 index 269bf65b0..000000000 --- a/sdk/helper/roottoken/encode_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package roottoken - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTokenEncodingDecodingWithOTP(t *testing.T) { - otpTestCases := []struct { - token string - name string - otpLength int - expectedEncodingErr string - expectedDecodingErr string - }{ - { - token: "someToken", - name: "test token encoding with base64", - otpLength: 0, - expectedEncodingErr: "xor of root token failed: length of byte slices is not equivalent: 24 != 9", - expectedDecodingErr: "", - }, - { - token: "someToken", - name: "test token encoding with base62", - otpLength: len("someToken"), - expectedEncodingErr: "", - expectedDecodingErr: "", - }, - { - token: "someToken", - name: "test token encoding with base62 - wrong otp length", - otpLength: len("someToken") + 1, - expectedEncodingErr: "xor of root token failed: length of byte slices is not equivalent: 10 != 9", - expectedDecodingErr: "", - }, - { - token: "", - name: "test no token to encode", - otpLength: 0, - expectedEncodingErr: "no token provided", - expectedDecodingErr: "", - }, - } - for _, otpTestCase := range otpTestCases { - t.Run(otpTestCase.name, func(t *testing.T) { - otp, err := GenerateOTP(otpTestCase.otpLength) - if err != nil { - t.Fatal(err.Error()) - } - encodedToken, err := EncodeToken(otpTestCase.token, otp) - if err != nil || otpTestCase.expectedDecodingErr != "" { - assert.EqualError(t, err, otpTestCase.expectedEncodingErr) - return - } - assert.NotEqual(t, otp, encodedToken) - assert.NotEqual(t, encodedToken, otpTestCase.token) - decodedToken, err := DecodeToken(encodedToken, otp, len(otp)) - if err != nil || otpTestCase.expectedDecodingErr != "" { - assert.EqualError(t, err, otpTestCase.expectedDecodingErr) - return - } - assert.Equal(t, otpTestCase.token, decodedToken) - }) - } -} - -func TestTokenEncodingDecodingWithNoOTPorPGPKey(t *testing.T) { - _, err := EncodeToken("", "") - assert.EqualError(t, err, "no token provided") -} diff --git a/sdk/helper/roottoken/otp_test.go b/sdk/helper/roottoken/otp_test.go deleted file mode 100644 index 53776ec21..000000000 --- a/sdk/helper/roottoken/otp_test.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package roottoken - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBase64OTPGeneration(t *testing.T) { - token, err := GenerateOTP(0) - assert.Len(t, token, 24) - assert.Nil(t, err) -} - -func TestBase62OTPGeneration(t *testing.T) { - token, err := GenerateOTP(20) - assert.Len(t, token, 20) - assert.Nil(t, err) -} diff --git a/sdk/helper/salt/salt_test.go b/sdk/helper/salt/salt_test.go deleted file mode 100644 index 3aec9a27b..000000000 --- a/sdk/helper/salt/salt_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package salt - -import ( - "context" - "crypto/sha1" - "crypto/sha256" - "testing" - - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestSalt(t *testing.T) { - inm := &logical.InmemStorage{} - conf := &Config{} - - salt, err := NewSalt(context.Background(), inm, conf) - if err != nil { - t.Fatalf("err: %v", err) - } - - if !salt.DidGenerate() { - t.Fatalf("expected generation") - } - - // Verify the salt exists - out, err := inm.Get(context.Background(), DefaultLocation) - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("missing salt") - } - - // Create a new salt, should restore - salt2, err := NewSalt(context.Background(), inm, conf) - if err != nil { - t.Fatalf("err: %v", err) - } - - if salt2.DidGenerate() { - t.Fatalf("unexpected generation") - } - - // Check for a match - if salt.salt != salt2.salt { - t.Fatalf("salt mismatch: %s %s", salt.salt, salt2.salt) - } - - // Verify a match - id := "foobarbaz" - sid1 := salt.SaltID(id) - sid2 := salt2.SaltID(id) - - if sid1 != sid2 { - t.Fatalf("mismatch") - } -} - -func TestSaltID(t *testing.T) { - salt, err := uuid.GenerateUUID() - if err != nil { - t.Fatal(err) - } - id := "foobarbaz" - - sid1 := SaltID(salt, id, SHA1Hash) - sid2 := SaltID(salt, id, SHA1Hash) - - if len(sid1) != sha1.Size*2 { - t.Fatalf("Bad len: %d %s", len(sid1), sid1) - } - - if sid1 != sid2 { - t.Fatalf("mismatch") - } - - sid1 = SaltID(salt, id, SHA256Hash) - sid2 = SaltID(salt, id, SHA256Hash) - - if len(sid1) != sha256.Size*2 { - t.Fatalf("Bad len: %d", len(sid1)) - } - - if sid1 != sid2 { - t.Fatalf("mismatch") - } -} diff --git a/sdk/helper/template/funcs_test.go b/sdk/helper/template/funcs_test.go deleted file mode 100644 index 496511596..000000000 --- a/sdk/helper/template/funcs_test.go +++ /dev/null @@ -1,359 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package template - -import ( - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestUnixTimestamp(t *testing.T) { - now := time.Now().Unix() - for i := 0; i < 100; i++ { - str := unixTime() - actual, err := strconv.Atoi(str) - require.NoError(t, err) - // Make sure the value generated is from now (or later if the clock ticked over) - require.GreaterOrEqual(t, int64(actual), now) - } -} - -func TestNowNano(t *testing.T) { - now := time.Now().UnixNano() / int64(time.Millisecond) - for i := 0; i < 100; i++ { - str := unixTimeMillis() - actual, err := strconv.ParseUint(str, 10, 64) - require.NoError(t, err) - // Make sure the value generated is from now (or later if the clock ticked over) - require.GreaterOrEqual(t, int64(actual), now) - } -} - -func TestTruncate(t *testing.T) { - type testCase struct { - maxLen int - input string - expected string - expectErr bool - } - - tests := map[string]testCase{ - "negative max length": { - maxLen: -1, - input: "foobarbaz", - expected: "", - expectErr: true, - }, - "zero max length": { - maxLen: 0, - input: "foobarbaz", - expected: "", - expectErr: true, - }, - "one max length": { - maxLen: 1, - input: "foobarbaz", - expected: "f", - expectErr: false, - }, - "half max length": { - maxLen: 5, - input: "foobarbaz", - expected: "fooba", - expectErr: false, - }, - "max length one less than length": { - maxLen: 8, - input: "foobarbaz", - expected: "foobarba", - expectErr: false, - }, - "max length equals string length": { - maxLen: 9, - input: "foobarbaz", - expected: "foobarbaz", - expectErr: false, - }, - "max length greater than string length": { - maxLen: 10, - input: "foobarbaz", - expected: "foobarbaz", - expectErr: false, - }, - "max length significantly greater than string length": { - maxLen: 100, - input: "foobarbaz", - expected: "foobarbaz", - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual, err := truncate(test.maxLen, test.input) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - require.Equal(t, test.expected, actual) - }) - } -} - -func TestTruncateSHA256(t *testing.T) { - type testCase struct { - maxLen int - input string - expected string - expectErr bool - } - - tests := map[string]testCase{ - "negative max length": { - maxLen: -1, - input: "thisisareallylongstring", - expected: "", - expectErr: true, - }, - "zero max length": { - maxLen: 0, - input: "thisisareallylongstring", - expected: "", - expectErr: true, - }, - "8 max length": { - maxLen: 8, - input: "thisisareallylongstring", - expected: "", - expectErr: true, - }, - "nine max length": { - maxLen: 9, - input: "thisisareallylongstring", - expected: "t4bb25641", - expectErr: false, - }, - "half max length": { - maxLen: 12, - input: "thisisareallylongstring", - expected: "this704cd12b", - expectErr: false, - }, - "max length one less than length": { - maxLen: 22, - input: "thisisareallylongstring", - expected: "thisisareallyl7f978be6", - expectErr: false, - }, - "max length equals string length": { - maxLen: 23, - input: "thisisareallylongstring", - expected: "thisisareallylongstring", - expectErr: false, - }, - "max length greater than string length": { - maxLen: 24, - input: "thisisareallylongstring", - expected: "thisisareallylongstring", - expectErr: false, - }, - "max length significantly greater than string length": { - maxLen: 100, - input: "thisisareallylongstring", - expected: "thisisareallylongstring", - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual, err := truncateSHA256(test.maxLen, test.input) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - require.Equal(t, test.expected, actual) - }) - } -} - -func TestSHA256(t *testing.T) { - type testCase struct { - input string - expected string - } - - tests := map[string]testCase{ - "empty string": { - input: "", - expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - }, - "foobar": { - input: "foobar", - expected: "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", - }, - "mystring": { - input: "mystring", - expected: "bd3ff47540b31e62d4ca6b07794e5a886b0f655fc322730f26ecd65cc7dd5c90", - }, - "very long string": { - input: "Nullam pharetra mattis laoreet. Mauris feugiat, tortor in malesuada convallis, " + - "eros nunc dapibus erat, eget malesuada purus leo id lorem. Morbi pharetra, libero at malesuada bibendum, " + - "dui quam tristique libero, bibendum cursus diam quam at sem. Vivamus vestibulum orci vel odio posuere, " + - "quis tincidunt ipsum lacinia. Donec elementum a orci quis lobortis. Etiam bibendum ullamcorper varius. " + - "Mauris tempor eros est, at porta erat rutrum ac. Aliquam erat volutpat. Sed sagittis leo non bibendum " + - "lacinia. Praesent id justo iaculis, mattis libero vel, feugiat dui. Morbi id diam non magna imperdiet " + - "imperdiet. Ut tortor arcu, mollis ac maximus ac, sagittis commodo augue. Ut semper, diam pulvinar porta " + - "dignissim, massa ex condimentum enim, sed euismod urna quam vitae ex. Sed id neque vitae magna sagittis " + - "pretium. Suspendisse potenti.", - expected: "3e2a996c20b7a02378204f0843507d335e1ba203df2c4ded8d839d44af24482f", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual := hashSHA256(test.input) - require.Equal(t, test.expected, actual) - }) - } -} - -func TestUppercase(t *testing.T) { - type testCase struct { - input string - expected string - } - - tests := map[string]testCase{ - "empty string": { - input: "", - expected: "", - }, - "lowercase": { - input: "foobar", - expected: "FOOBAR", - }, - "uppercase": { - input: "FOOBAR", - expected: "FOOBAR", - }, - "mixed case": { - input: "fOoBaR", - expected: "FOOBAR", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual := uppercase(test.input) - require.Equal(t, test.expected, actual) - }) - } -} - -func TestLowercase(t *testing.T) { - type testCase struct { - input string - expected string - } - - tests := map[string]testCase{ - "empty string": { - input: "", - expected: "", - }, - "lowercase": { - input: "foobar", - expected: "foobar", - }, - "uppercase": { - input: "FOOBAR", - expected: "foobar", - }, - "mixed case": { - input: "fOoBaR", - expected: "foobar", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual := lowercase(test.input) - require.Equal(t, test.expected, actual) - }) - } -} - -func TestReplace(t *testing.T) { - type testCase struct { - input string - find string - replace string - expected string - } - - tests := map[string]testCase{ - "empty string": { - input: "", - find: "", - replace: "", - expected: "", - }, - "search not found": { - input: "foobar", - find: ".", - replace: "_", - expected: "foobar", - }, - "single character found": { - input: "foo.bar", - find: ".", - replace: "_", - expected: "foo_bar", - }, - "multiple characters found": { - input: "foo.bar.baz", - find: ".", - replace: "_", - expected: "foo_bar_baz", - }, - "find and remove": { - input: "foo.bar", - find: ".", - replace: "", - expected: "foobar", - }, - "find full string": { - input: "foobarbaz", - find: "bar", - replace: "_", - expected: "foo_baz", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - actual := replace(test.find, test.replace, test.input) - require.Equal(t, test.expected, actual) - }) - } -} - -func TestUUID(t *testing.T) { - re := "^[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$" - for i := 0; i < 100; i++ { - id, err := uuid() - require.NoError(t, err) - require.Regexp(t, re, id) - } -} diff --git a/sdk/helper/template/template_test.go b/sdk/helper/template/template_test.go deleted file mode 100644 index 2f66bf36f..000000000 --- a/sdk/helper/template/template_test.go +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package template - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGenerate(t *testing.T) { - type testCase struct { - template string - additionalOpts []Opt - data interface{} - - expected string - expectErr bool - } - - tests := map[string]testCase{ - "template without arguments": { - template: "this is a template", - data: nil, - expected: "this is a template", - expectErr: false, - }, - "template with arguments but no data": { - template: "this is a {{.String}}", - data: nil, - expected: "this is a ", - expectErr: false, - }, - "template with arguments": { - template: "this is a {{.String}}", - data: struct { - String string - }{ - String: "foobar", - }, - expected: "this is a foobar", - expectErr: false, - }, - "template with builtin functions": { - template: `{{.String | truncate 10}} -{{.String | uppercase}} -{{.String | lowercase}} -{{.String | replace " " "."}} -{{.String | sha256}} -{{.String | base64}} -{{.String | truncate_sha256 20}}`, - data: struct { - String string - }{ - String: "Some string with Multiple Capitals LETTERS", - }, - expected: `Some strin -SOME STRING WITH MULTIPLE CAPITALS LETTERS -some string with multiple capitals letters -Some.string.with.Multiple.Capitals.LETTERS -da9872dd96609c72897defa11fe81017a62c3f44339d9d3b43fe37540ede3601 -U29tZSBzdHJpbmcgd2l0aCBNdWx0aXBsZSBDYXBpdGFscyBMRVRURVJT -Some string 6841cf80`, - expectErr: false, - }, - "custom function": { - template: "{{foo}}", - additionalOpts: []Opt{ - Function("foo", func() string { - return "custom-foo" - }), - }, - expected: "custom-foo", - expectErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - opts := append(test.additionalOpts, Template(test.template)) - st, err := NewTemplate(opts...) - require.NoError(t, err) - - actual, err := st.Generate(test.data) - if test.expectErr && err == nil { - t.Fatalf("err expected, got nil") - } - if !test.expectErr && err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - require.Equal(t, test.expected, actual) - }) - } - - t.Run("random", func(t *testing.T) { - for i := 1; i < 100; i++ { - st, err := NewTemplate( - Template(fmt.Sprintf("{{random %d}}", i)), - ) - require.NoError(t, err) - - actual, err := st.Generate(nil) - require.NoError(t, err) - - require.Regexp(t, fmt.Sprintf("^[a-zA-Z0-9]{%d}$", i), actual) - } - }) - - t.Run("unix_time", func(t *testing.T) { - for i := 0; i < 100; i++ { - st, err := NewTemplate( - Template("{{unix_time}}"), - ) - require.NoError(t, err) - - actual, err := st.Generate(nil) - require.NoError(t, err) - - require.Regexp(t, "^[0-9]+$", actual) - } - }) - - t.Run("unix_time_millis", func(t *testing.T) { - for i := 0; i < 100; i++ { - st, err := NewTemplate( - Template("{{unix_time_millis}}"), - ) - require.NoError(t, err) - - actual, err := st.Generate(nil) - require.NoError(t, err) - - require.Regexp(t, "^[0-9]+$", actual) - } - }) - - t.Run("timestamp", func(t *testing.T) { - for i := 0; i < 100; i++ { - st, err := NewTemplate( - Template(`{{timestamp "2006-01-02T15:04:05.000Z"}}`), - ) - require.NoError(t, err) - - actual, err := st.Generate(nil) - require.NoError(t, err) - - require.Regexp(t, `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$`, actual) - } - }) -} - -func TestBadConstructorArguments(t *testing.T) { - type testCase struct { - opts []Opt - } - - tests := map[string]testCase{ - "missing template": { - opts: nil, - }, - "missing custom function name": { - opts: []Opt{ - Template("foo bar"), - Function("", func() string { - return "foo" - }), - }, - }, - "missing custom function": { - opts: []Opt{ - Template("foo bar"), - Function("foo", nil), - }, - }, - "bad template": { - opts: []Opt{ - Template("{{.String"), - }, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - st, err := NewTemplate(test.opts...) - require.Error(t, err) - - str, err := st.Generate(nil) - require.Error(t, err) - require.Equal(t, "", str) - }) - } - - t.Run("erroring custom function", func(t *testing.T) { - st, err := NewTemplate( - Template("{{foo}}"), - Function("foo", func() (string, error) { - return "", fmt.Errorf("an error!") - }), - ) - require.NoError(t, err) - - str, err := st.Generate(nil) - require.Error(t, err) - require.Equal(t, "", str) - }) -} diff --git a/sdk/helper/testcluster/consts.go b/sdk/helper/testcluster/consts.go deleted file mode 100644 index b736b5f88..000000000 --- a/sdk/helper/testcluster/consts.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package testcluster - -const ( - // EnvVaultLicenseCI is the name of an environment variable that contains - // a signed license string used for Vault Enterprise binary-based tests. - // The binary will be run with the env var VAULT_LICENSE set to this value. - EnvVaultLicenseCI = "VAULT_LICENSE_CI" - - // DefaultCAFile is the path to the CA file. This is a docker-specific - // constant. TODO: needs to be moved to a more relevant place - DefaultCAFile = "/vault/config/ca.pem" -) diff --git a/sdk/helper/testcluster/docker/cert.go b/sdk/helper/testcluster/docker/cert.go deleted file mode 100644 index 4704030cb..000000000 --- a/sdk/helper/testcluster/docker/cert.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package docker - -import ( - "crypto/tls" - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - "io/ioutil" - "sync" - - "github.com/hashicorp/errwrap" -) - -// ReloadFunc are functions that are called when a reload is requested -type ReloadFunc func() error - -// CertificateGetter satisfies ReloadFunc and its GetCertificate method -// satisfies the tls.GetCertificate function signature. Currently it does not -// allow changing paths after the fact. -type CertificateGetter struct { - sync.RWMutex - - cert *tls.Certificate - - certFile string - keyFile string - passphrase string -} - -func NewCertificateGetter(certFile, keyFile, passphrase string) *CertificateGetter { - return &CertificateGetter{ - certFile: certFile, - keyFile: keyFile, - passphrase: passphrase, - } -} - -func (cg *CertificateGetter) Reload() error { - certPEMBlock, err := ioutil.ReadFile(cg.certFile) - if err != nil { - return err - } - keyPEMBlock, err := ioutil.ReadFile(cg.keyFile) - if err != nil { - return err - } - - // Check for encrypted pem block - keyBlock, _ := pem.Decode(keyPEMBlock) - if keyBlock == nil { - return errors.New("decoded PEM is blank") - } - - if x509.IsEncryptedPEMBlock(keyBlock) { - keyBlock.Bytes, err = x509.DecryptPEMBlock(keyBlock, []byte(cg.passphrase)) - if err != nil { - return errwrap.Wrapf("Decrypting PEM block failed {{err}}", err) - } - keyPEMBlock = pem.EncodeToMemory(keyBlock) - } - - cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) - if err != nil { - return err - } - - cg.Lock() - defer cg.Unlock() - - cg.cert = &cert - - return nil -} - -func (cg *CertificateGetter) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - cg.RLock() - defer cg.RUnlock() - - if cg.cert == nil { - return nil, fmt.Errorf("nil certificate") - } - - return cg.cert, nil -} diff --git a/sdk/helper/testcluster/docker/environment.go b/sdk/helper/testcluster/docker/environment.go deleted file mode 100644 index b0d72a00d..000000000 --- a/sdk/helper/testcluster/docker/environment.go +++ /dev/null @@ -1,1153 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package docker - -import ( - "bufio" - "bytes" - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/hex" - "encoding/json" - "encoding/pem" - "fmt" - "io" - "io/ioutil" - "math/big" - mathrand "math/rand" - "net" - "net/http" - "os" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/volume" - docker "github.com/docker/docker/client" - "github.com/hashicorp/go-cleanhttp" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/vault/api" - dockhelper "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/helper/testcluster" - uberAtomic "go.uber.org/atomic" - "golang.org/x/net/http2" -) - -var ( - _ testcluster.VaultCluster = &DockerCluster{} - _ testcluster.VaultClusterNode = &DockerClusterNode{} -) - -const MaxClusterNameLength = 52 - -// DockerCluster is used to managing the lifecycle of the test Vault cluster -type DockerCluster struct { - ClusterName string - - ClusterNodes []*DockerClusterNode - - // Certificate fields - *testcluster.CA - RootCAs *x509.CertPool - - barrierKeys [][]byte - recoveryKeys [][]byte - tmpDir string - - // rootToken is the initial root token created when the Vault cluster is - // created. - rootToken string - DockerAPI *docker.Client - ID string - Logger log.Logger - builtTags map[string]struct{} - - storage testcluster.ClusterStorage -} - -func (dc *DockerCluster) NamedLogger(s string) log.Logger { - return dc.Logger.Named(s) -} - -func (dc *DockerCluster) ClusterID() string { - return dc.ID -} - -func (dc *DockerCluster) Nodes() []testcluster.VaultClusterNode { - ret := make([]testcluster.VaultClusterNode, len(dc.ClusterNodes)) - for i := range dc.ClusterNodes { - ret[i] = dc.ClusterNodes[i] - } - return ret -} - -func (dc *DockerCluster) GetBarrierKeys() [][]byte { - return dc.barrierKeys -} - -func testKeyCopy(key []byte) []byte { - result := make([]byte, len(key)) - copy(result, key) - return result -} - -func (dc *DockerCluster) GetRecoveryKeys() [][]byte { - ret := make([][]byte, len(dc.recoveryKeys)) - for i, k := range dc.recoveryKeys { - ret[i] = testKeyCopy(k) - } - return ret -} - -func (dc *DockerCluster) GetBarrierOrRecoveryKeys() [][]byte { - return dc.GetBarrierKeys() -} - -func (dc *DockerCluster) SetBarrierKeys(keys [][]byte) { - dc.barrierKeys = make([][]byte, len(keys)) - for i, k := range keys { - dc.barrierKeys[i] = testKeyCopy(k) - } -} - -func (dc *DockerCluster) SetRecoveryKeys(keys [][]byte) { - dc.recoveryKeys = make([][]byte, len(keys)) - for i, k := range keys { - dc.recoveryKeys[i] = testKeyCopy(k) - } -} - -func (dc *DockerCluster) GetCACertPEMFile() string { - return dc.CACertPEMFile -} - -func (dc *DockerCluster) Cleanup() { - dc.cleanup() -} - -func (dc *DockerCluster) cleanup() error { - var result *multierror.Error - for _, node := range dc.ClusterNodes { - if err := node.cleanup(); err != nil { - result = multierror.Append(result, err) - } - } - - return result.ErrorOrNil() -} - -// GetRootToken returns the root token of the cluster, if set -func (dc *DockerCluster) GetRootToken() string { - return dc.rootToken -} - -func (dc *DockerCluster) SetRootToken(s string) { - dc.Logger.Trace("cluster root token changed", "helpful_env", fmt.Sprintf("VAULT_TOKEN=%s VAULT_CACERT=/vault/config/ca.pem", s)) - dc.rootToken = s -} - -func (n *DockerClusterNode) Name() string { - return n.Cluster.ClusterName + "-" + n.NodeID -} - -func (dc *DockerCluster) setupNode0(ctx context.Context) error { - client := dc.ClusterNodes[0].client - - var resp *api.InitResponse - var err error - for ctx.Err() == nil { - resp, err = client.Sys().Init(&api.InitRequest{ - SecretShares: 3, - SecretThreshold: 3, - }) - if err == nil && resp != nil { - break - } - time.Sleep(500 * time.Millisecond) - } - if err != nil { - return err - } - if resp == nil { - return fmt.Errorf("nil response to init request") - } - - for _, k := range resp.Keys { - raw, err := hex.DecodeString(k) - if err != nil { - return err - } - dc.barrierKeys = append(dc.barrierKeys, raw) - } - - for _, k := range resp.RecoveryKeys { - raw, err := hex.DecodeString(k) - if err != nil { - return err - } - dc.recoveryKeys = append(dc.recoveryKeys, raw) - } - - dc.rootToken = resp.RootToken - client.SetToken(dc.rootToken) - dc.ClusterNodes[0].client = client - - err = testcluster.UnsealNode(ctx, dc, 0) - if err != nil { - return err - } - - err = ensureLeaderMatches(ctx, client, func(leader *api.LeaderResponse) error { - if !leader.IsSelf { - return fmt.Errorf("node %d leader=%v, expected=%v", 0, leader.IsSelf, true) - } - - return nil - }) - - status, err := client.Sys().SealStatusWithContext(ctx) - if err != nil { - return err - } - dc.ID = status.ClusterID - return err -} - -func (dc *DockerCluster) clusterReady(ctx context.Context) error { - for i, node := range dc.ClusterNodes { - expectLeader := i == 0 - err := ensureLeaderMatches(ctx, node.client, func(leader *api.LeaderResponse) error { - if expectLeader != leader.IsSelf { - return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader) - } - - return nil - }) - if err != nil { - return err - } - } - - return nil -} - -func (dc *DockerCluster) setupCA(opts *DockerClusterOptions) error { - var err error - var ca testcluster.CA - - if opts != nil && opts.CAKey != nil { - ca.CAKey = opts.CAKey - } else { - ca.CAKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return err - } - } - - var caBytes []byte - if opts != nil && len(opts.CACert) > 0 { - caBytes = opts.CACert - } else { - serialNumber := mathrand.New(mathrand.NewSource(time.Now().UnixNano())).Int63() - CACertTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "localhost", - }, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - SerialNumber: big.NewInt(serialNumber), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - BasicConstraintsValid: true, - IsCA: true, - } - caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, ca.CAKey.Public(), ca.CAKey) - if err != nil { - return err - } - } - CACert, err := x509.ParseCertificate(caBytes) - if err != nil { - return err - } - ca.CACert = CACert - ca.CACertBytes = caBytes - - CACertPEMBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - } - ca.CACertPEM = pem.EncodeToMemory(CACertPEMBlock) - - ca.CACertPEMFile = filepath.Join(dc.tmpDir, "ca", "ca.pem") - err = os.WriteFile(ca.CACertPEMFile, ca.CACertPEM, 0o755) - if err != nil { - return err - } - - marshaledCAKey, err := x509.MarshalECPrivateKey(ca.CAKey) - if err != nil { - return err - } - CAKeyPEMBlock := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: marshaledCAKey, - } - ca.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock) - - dc.CA = &ca - - return nil -} - -func (n *DockerClusterNode) setupCert(ip string) error { - var err error - - n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return err - } - - serialNumber := mathrand.New(mathrand.NewSource(time.Now().UnixNano())).Int63() - certTemplate := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: n.Name(), - }, - DNSNames: []string{"localhost", n.Name()}, - IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1"), net.ParseIP(ip)}, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, - SerialNumber: big.NewInt(serialNumber), - NotBefore: time.Now().Add(-30 * time.Second), - NotAfter: time.Now().Add(262980 * time.Hour), - } - n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey) - if err != nil { - return err - } - n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes) - if err != nil { - return err - } - n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: n.ServerCertBytes, - }) - - marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey) - if err != nil { - return err - } - n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: marshaledKey, - }) - - n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem") - err = os.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0o755) - if err != nil { - return err - } - - n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem") - err = os.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0o755) - if err != nil { - return err - } - - tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM) - if err != nil { - return err - } - - certGetter := NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "") - if err := certGetter.Reload(); err != nil { - return err - } - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{tlsCert}, - RootCAs: n.Cluster.RootCAs, - ClientCAs: n.Cluster.RootCAs, - ClientAuth: tls.RequestClientCert, - NextProtos: []string{"h2", "http/1.1"}, - GetCertificate: certGetter.GetCertificate, - } - - n.tlsConfig = tlsConfig - - err = os.WriteFile(filepath.Join(n.WorkDir, "ca.pem"), n.Cluster.CACertPEM, 0o755) - if err != nil { - return err - } - return nil -} - -func NewTestDockerCluster(t *testing.T, opts *DockerClusterOptions) *DockerCluster { - if opts == nil { - opts = &DockerClusterOptions{} - } - if opts.ClusterName == "" { - opts.ClusterName = strings.ReplaceAll(t.Name(), "/", "-") - } - if opts.Logger == nil { - opts.Logger = logging.NewVaultLogger(log.Trace).Named(t.Name()) - } - if opts.NetworkName == "" { - opts.NetworkName = os.Getenv("TEST_DOCKER_NETWORK_NAME") - } - - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) - t.Cleanup(cancel) - - dc, err := NewDockerCluster(ctx, opts) - if err != nil { - t.Fatal(err) - } - dc.Logger.Trace("cluster started", "helpful_env", fmt.Sprintf("VAULT_TOKEN=%s VAULT_CACERT=/vault/config/ca.pem", dc.GetRootToken())) - return dc -} - -func NewDockerCluster(ctx context.Context, opts *DockerClusterOptions) (*DockerCluster, error) { - api, err := dockhelper.NewDockerAPI() - if err != nil { - return nil, err - } - - if opts == nil { - opts = &DockerClusterOptions{} - } - if opts.Logger == nil { - opts.Logger = log.NewNullLogger() - } - if opts.VaultLicense == "" { - opts.VaultLicense = os.Getenv(testcluster.EnvVaultLicenseCI) - } - - dc := &DockerCluster{ - DockerAPI: api, - ClusterName: opts.ClusterName, - Logger: opts.Logger, - builtTags: map[string]struct{}{}, - CA: opts.CA, - storage: opts.Storage, - } - - if err := dc.setupDockerCluster(ctx, opts); err != nil { - dc.Cleanup() - return nil, err - } - - return dc, nil -} - -// DockerClusterNode represents a single instance of Vault in a cluster -type DockerClusterNode struct { - NodeID string - HostPort string - client *api.Client - ServerCert *x509.Certificate - ServerCertBytes []byte - ServerCertPEM []byte - ServerCertPEMFile string - ServerKey *ecdsa.PrivateKey - ServerKeyPEM []byte - ServerKeyPEMFile string - tlsConfig *tls.Config - WorkDir string - Cluster *DockerCluster - Container *types.ContainerJSON - DockerAPI *docker.Client - runner *dockhelper.Runner - Logger log.Logger - cleanupContainer func() - RealAPIAddr string - ContainerNetworkName string - ContainerIPAddress string - ImageRepo string - ImageTag string - DataVolumeName string - cleanupVolume func() -} - -func (n *DockerClusterNode) TLSConfig() *tls.Config { - return n.tlsConfig.Clone() -} - -func (n *DockerClusterNode) APIClient() *api.Client { - // We clone to ensure that whenever this method is called, the caller gets - // back a pristine client, without e.g. any namespace or token changes that - // might pollute a shared client. We clone the config instead of the - // client because (1) Client.clone propagates the replicationStateStore and - // the httpClient pointers, (2) it doesn't copy the tlsConfig at all, and - // (3) if clone returns an error, it doesn't feel as appropriate to panic - // below. Who knows why clone might return an error? - cfg := n.client.CloneConfig() - client, err := api.NewClient(cfg) - if err != nil { - // It seems fine to panic here, since this should be the same input - // we provided to NewClient when we were setup, and we didn't panic then. - // Better not to completely ignore the error though, suppose there's a - // bug in CloneConfig? - panic(fmt.Sprintf("NewClient error on cloned config: %v", err)) - } - client.SetToken(n.Cluster.rootToken) - return client -} - -// NewAPIClient creates and configures a Vault API client to communicate with -// the running Vault Cluster for this DockerClusterNode -func (n *DockerClusterNode) apiConfig() (*api.Config, error) { - transport := cleanhttp.DefaultPooledTransport() - transport.TLSClientConfig = n.TLSConfig() - if err := http2.ConfigureTransport(transport); err != nil { - return nil, err - } - client := &http.Client{ - Transport: transport, - CheckRedirect: func(*http.Request, []*http.Request) error { - // This can of course be overridden per-test by using its own client - return fmt.Errorf("redirects not allowed in these tests") - }, - } - config := api.DefaultConfig() - if config.Error != nil { - return nil, config.Error - } - config.Address = fmt.Sprintf("https://%s", n.HostPort) - config.HttpClient = client - config.MaxRetries = 0 - return config, nil -} - -func (n *DockerClusterNode) newAPIClient() (*api.Client, error) { - config, err := n.apiConfig() - if err != nil { - return nil, err - } - client, err := api.NewClient(config) - if err != nil { - return nil, err - } - client.SetToken(n.Cluster.GetRootToken()) - return client, nil -} - -// Cleanup kills the container of the node and deletes its data volume -func (n *DockerClusterNode) Cleanup() { - n.cleanup() -} - -// Stop kills the container of the node -func (n *DockerClusterNode) Stop() { - n.cleanupContainer() -} - -func (n *DockerClusterNode) cleanup() error { - if n.Container == nil || n.Container.ID == "" { - return nil - } - n.cleanupContainer() - n.cleanupVolume() - return nil -} - -func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOptions) error { - if n.DataVolumeName == "" { - vol, err := n.DockerAPI.VolumeCreate(ctx, volume.CreateOptions{}) - if err != nil { - return err - } - n.DataVolumeName = vol.Name - n.cleanupVolume = func() { - _ = n.DockerAPI.VolumeRemove(ctx, vol.Name, false) - } - } - vaultCfg := map[string]interface{}{} - vaultCfg["listener"] = map[string]interface{}{ - "tcp": map[string]interface{}{ - "address": fmt.Sprintf("%s:%d", "0.0.0.0", 8200), - "tls_cert_file": "/vault/config/cert.pem", - "tls_key_file": "/vault/config/key.pem", - "telemetry": map[string]interface{}{ - "unauthenticated_metrics_access": true, - }, - }, - } - vaultCfg["telemetry"] = map[string]interface{}{ - "disable_hostname": true, - } - - // Setup storage. Default is raft. - storageType := "raft" - storageOpts := map[string]interface{}{ - // TODO add options from vnc - "path": "/vault/file", - "node_id": n.NodeID, - } - - if opts.Storage != nil { - storageType = opts.Storage.Type() - storageOpts = opts.Storage.Opts() - } - - if opts != nil && opts.VaultNodeConfig != nil { - for k, v := range opts.VaultNodeConfig.StorageOptions { - if _, ok := storageOpts[k].(string); !ok { - storageOpts[k] = v - } - } - } - vaultCfg["storage"] = map[string]interface{}{ - storageType: storageOpts, - } - - //// disable_mlock is required for working in the Docker environment with - //// custom plugins - vaultCfg["disable_mlock"] = true - vaultCfg["api_addr"] = `https://{{- GetAllInterfaces | exclude "flags" "loopback" | attr "address" -}}:8200` - vaultCfg["cluster_addr"] = `https://{{- GetAllInterfaces | exclude "flags" "loopback" | attr "address" -}}:8201` - - vaultCfg["administrative_namespace_path"] = opts.AdministrativeNamespacePath - - systemJSON, err := json.Marshal(vaultCfg) - if err != nil { - return err - } - err = os.WriteFile(filepath.Join(n.WorkDir, "system.json"), systemJSON, 0o644) - if err != nil { - return err - } - - if opts.VaultNodeConfig != nil { - localCfg := *opts.VaultNodeConfig - if opts.VaultNodeConfig.LicensePath != "" { - b, err := os.ReadFile(opts.VaultNodeConfig.LicensePath) - if err != nil || len(b) == 0 { - return fmt.Errorf("unable to read LicensePath at %q: %w", opts.VaultNodeConfig.LicensePath, err) - } - localCfg.LicensePath = "/vault/config/license" - dest := filepath.Join(n.WorkDir, "license") - err = os.WriteFile(dest, b, 0o644) - if err != nil { - return fmt.Errorf("error writing license to %q: %w", dest, err) - } - - } - userJSON, err := json.Marshal(localCfg) - if err != nil { - return err - } - err = os.WriteFile(filepath.Join(n.WorkDir, "user.json"), userJSON, 0o644) - if err != nil { - return err - } - } - - // Create a temporary cert so vault will start up - err = n.setupCert("127.0.0.1") - if err != nil { - return err - } - - caDir := filepath.Join(n.Cluster.tmpDir, "ca") - - // setup plugin bin copy if needed - copyFromTo := map[string]string{ - n.WorkDir: "/vault/config", - caDir: "/usr/local/share/ca-certificates/", - } - - var wg sync.WaitGroup - wg.Add(1) - var seenLogs uberAtomic.Bool - logConsumer := func(s string) { - if seenLogs.CAS(false, true) { - wg.Done() - } - n.Logger.Trace(s) - } - logStdout := &LogConsumerWriter{logConsumer} - logStderr := &LogConsumerWriter{func(s string) { - if seenLogs.CAS(false, true) { - wg.Done() - } - testcluster.JSONLogNoTimestamp(n.Logger, s) - }} - r, err := dockhelper.NewServiceRunner(dockhelper.RunOptions{ - ImageRepo: n.ImageRepo, - ImageTag: n.ImageTag, - // We don't need to run update-ca-certificates in the container, because - // we're providing the CA in the raft join call, and otherwise Vault - // servers don't talk to one another on the API port. - Cmd: append([]string{"server"}, opts.Args...), - Env: []string{ - // For now we're using disable_mlock, because this is for testing - // anyway, and because it prevents us using external plugins. - "SKIP_SETCAP=true", - "VAULT_LOG_FORMAT=json", - "VAULT_LICENSE=" + opts.VaultLicense, - }, - Ports: []string{"8200/tcp", "8201/tcp"}, - ContainerName: n.Name(), - NetworkName: opts.NetworkName, - CopyFromTo: copyFromTo, - LogConsumer: logConsumer, - LogStdout: logStdout, - LogStderr: logStderr, - PreDelete: true, - DoNotAutoRemove: true, - PostStart: func(containerID string, realIP string) error { - err := n.setupCert(realIP) - if err != nil { - return err - } - - // If we signal Vault before it installs its sighup handler, it'll die. - wg.Wait() - n.Logger.Trace("running poststart", "containerID", containerID, "IP", realIP) - return n.runner.RefreshFiles(ctx, containerID) - }, - Capabilities: []string{"NET_ADMIN"}, - OmitLogTimestamps: true, - VolumeNameToMountPoint: map[string]string{ - n.DataVolumeName: "/vault/file", - }, - }) - if err != nil { - return err - } - n.runner = r - - probe := opts.StartProbe - if probe == nil { - probe = func(c *api.Client) error { - _, err = c.Sys().SealStatus() - return err - } - } - svc, _, err := r.StartNewService(ctx, false, false, func(ctx context.Context, host string, port int) (dockhelper.ServiceConfig, error) { - config, err := n.apiConfig() - if err != nil { - return nil, err - } - config.Address = fmt.Sprintf("https://%s:%d", host, port) - client, err := api.NewClient(config) - if err != nil { - return nil, err - } - err = probe(client) - if err != nil { - return nil, err - } - - return dockhelper.NewServiceHostPort(host, port), nil - }) - if err != nil { - return err - } - - n.HostPort = svc.Config.Address() - n.Container = svc.Container - netName := opts.NetworkName - if netName == "" { - if len(svc.Container.NetworkSettings.Networks) > 1 { - return fmt.Errorf("Set d.RunOptions.NetworkName instead for container with multiple networks: %v", svc.Container.NetworkSettings.Networks) - } - for netName = range svc.Container.NetworkSettings.Networks { - // Networks above is a map; we just need to find the first and - // only key of this map (network name). The range handles this - // for us, but we need a loop construction in order to use range. - } - } - n.ContainerNetworkName = netName - n.ContainerIPAddress = svc.Container.NetworkSettings.Networks[netName].IPAddress - n.RealAPIAddr = "https://" + n.ContainerIPAddress + ":8200" - n.cleanupContainer = svc.Cleanup - - client, err := n.newAPIClient() - if err != nil { - return err - } - client.SetToken(n.Cluster.rootToken) - n.client = client - return nil -} - -func (n *DockerClusterNode) Pause(ctx context.Context) error { - return n.DockerAPI.ContainerPause(ctx, n.Container.ID) -} - -func (n *DockerClusterNode) AddNetworkDelay(ctx context.Context, delay time.Duration, targetIP string) error { - ip := net.ParseIP(targetIP) - if ip == nil { - return fmt.Errorf("targetIP %q is not an IP address", targetIP) - } - // Let's attempt to get a unique handle for the filter rule; we'll assume that - // every targetIP has a unique last octet, which is true currently for how - // we're doing docker networking. - lastOctet := ip.To4()[3] - - stdout, stderr, exitCode, err := n.runner.RunCmdWithOutput(ctx, n.Container.ID, []string{ - "/bin/sh", - "-xec", strings.Join([]string{ - fmt.Sprintf("echo isolating node %s", targetIP), - "apk add iproute2", - // If we're running this script a second time on the same node, - // the add dev will fail; since we only want to run the netem - // command once, we'll do so in the case where the add dev doesn't fail. - "tc qdisc add dev eth0 root handle 1: prio && " + - fmt.Sprintf("tc qdisc add dev eth0 parent 1:1 handle 2: netem delay %dms", delay/time.Millisecond), - // Here we create a u32 filter as per https://man7.org/linux/man-pages/man8/tc-u32.8.html - // Its parent is 1:0 (which I guess is the root?) - // Its handle must be unique, so we base it on targetIP - fmt.Sprintf("tc filter add dev eth0 parent 1:0 protocol ip pref 55 handle ::%x u32 match ip dst %s flowid 2:1", lastOctet, targetIP), - }, "; "), - }) - if err != nil { - return err - } - - n.Logger.Trace(string(stdout)) - n.Logger.Trace(string(stderr)) - if exitCode != 0 { - return fmt.Errorf("got nonzero exit code from iptables: %d", exitCode) - } - return nil -} - -// PartitionFromCluster will cause the node to be disconnected at the network -// level from the rest of the docker cluster. It does so in a way that the node -// will not see TCP RSTs and all packets it sends will be "black holed". It -// attempts to keep packets to and from the host intact which allows docker -// daemon to continue streaming logs and any test code to continue making -// requests from the host to the partitioned node. -func (n *DockerClusterNode) PartitionFromCluster(ctx context.Context) error { - stdout, stderr, exitCode, err := n.runner.RunCmdWithOutput(ctx, n.Container.ID, []string{ - "/bin/sh", - "-xec", strings.Join([]string{ - fmt.Sprintf("echo partitioning container from network"), - "apk add iproute2", - // Get the gateway address for the bridge so we can allow host to - // container traffic still. - "GW=$(ip r | grep default | grep eth0 | cut -f 3 -d' ')", - // First delete the rules in case this is called twice otherwise we'll add - // multiple copies and only remove one in Unpartition (yay iptables). - // Ignore the error if it didn't exist. - "iptables -D INPUT -i eth0 ! -s \"$GW\" -j DROP | true", - "iptables -D OUTPUT -o eth0 ! -d \"$GW\" -j DROP | true", - // Add rules to drop all packets in and out of the docker network - // connection. - "iptables -I INPUT -i eth0 ! -s \"$GW\" -j DROP", - "iptables -I OUTPUT -o eth0 ! -d \"$GW\" -j DROP", - }, "; "), - }) - if err != nil { - return err - } - - n.Logger.Trace(string(stdout)) - n.Logger.Trace(string(stderr)) - if exitCode != 0 { - return fmt.Errorf("got nonzero exit code from iptables: %d", exitCode) - } - return nil -} - -// UnpartitionFromCluster reverses a previous call to PartitionFromCluster and -// restores full connectivity. Currently assumes the default "bridge" network. -func (n *DockerClusterNode) UnpartitionFromCluster(ctx context.Context) error { - stdout, stderr, exitCode, err := n.runner.RunCmdWithOutput(ctx, n.Container.ID, []string{ - "/bin/sh", - "-xec", strings.Join([]string{ - fmt.Sprintf("echo un-partitioning container from network"), - // Get the gateway address for the bridge so we can allow host to - // container traffic still. - "GW=$(ip r | grep default | grep eth0 | cut -f 3 -d' ')", - // Remove the rules, ignore if they are not present or iptables wasn't - // installed yet (i.e. no-one called PartitionFromCluster yet). - "iptables -D INPUT -i eth0 ! -s \"$GW\" -j DROP | true", - "iptables -D OUTPUT -o eth0 ! -d \"$GW\" -j DROP | true", - }, "; "), - }) - if err != nil { - return err - } - - n.Logger.Trace(string(stdout)) - n.Logger.Trace(string(stderr)) - if exitCode != 0 { - return fmt.Errorf("got nonzero exit code from iptables: %d", exitCode) - } - return nil -} - -type LogConsumerWriter struct { - consumer func(string) -} - -func (l LogConsumerWriter) Write(p []byte) (n int, err error) { - // TODO this assumes that we're never passed partial log lines, which - // seems a safe assumption for now based on how docker looks to implement - // logging, but might change in the future. - scanner := bufio.NewScanner(bytes.NewReader(p)) - scanner.Buffer(make([]byte, 64*1024), bufio.MaxScanTokenSize) - for scanner.Scan() { - l.consumer(scanner.Text()) - } - return len(p), nil -} - -// DockerClusterOptions has options for setting up the docker cluster -type DockerClusterOptions struct { - testcluster.ClusterOptions - CAKey *ecdsa.PrivateKey - NetworkName string - ImageRepo string - ImageTag string - CA *testcluster.CA - VaultBinary string - Args []string - StartProbe func(*api.Client) error - Storage testcluster.ClusterStorage -} - -func ensureLeaderMatches(ctx context.Context, client *api.Client, ready func(response *api.LeaderResponse) error) error { - var leader *api.LeaderResponse - var err error - for ctx.Err() == nil { - leader, err = client.Sys().Leader() - switch { - case err != nil: - case leader == nil: - err = fmt.Errorf("nil response to leader check") - default: - err = ready(leader) - if err == nil { - return nil - } - } - time.Sleep(500 * time.Millisecond) - } - return fmt.Errorf("error checking leader: %v", err) -} - -const DefaultNumCores = 3 - -// creates a managed docker container running Vault -func (dc *DockerCluster) setupDockerCluster(ctx context.Context, opts *DockerClusterOptions) error { - if opts.TmpDir != "" { - if _, err := os.Stat(opts.TmpDir); os.IsNotExist(err) { - if err := os.MkdirAll(opts.TmpDir, 0o700); err != nil { - return err - } - } - dc.tmpDir = opts.TmpDir - } else { - tempDir, err := ioutil.TempDir("", "vault-test-cluster-") - if err != nil { - return err - } - dc.tmpDir = tempDir - } - caDir := filepath.Join(dc.tmpDir, "ca") - if err := os.MkdirAll(caDir, 0o755); err != nil { - return err - } - - var numCores int - if opts.NumCores == 0 { - numCores = DefaultNumCores - } else { - numCores = opts.NumCores - } - - if dc.CA == nil { - if err := dc.setupCA(opts); err != nil { - return err - } - } - dc.RootCAs = x509.NewCertPool() - dc.RootCAs.AddCert(dc.CA.CACert) - - if dc.storage != nil { - if err := dc.storage.Start(ctx, &opts.ClusterOptions); err != nil { - return err - } - } - - for i := 0; i < numCores; i++ { - if err := dc.addNode(ctx, opts); err != nil { - return err - } - if opts.SkipInit { - continue - } - if i == 0 { - if err := dc.setupNode0(ctx); err != nil { - return nil - } - } else { - if err := dc.joinNode(ctx, i, 0); err != nil { - return err - } - } - } - - return nil -} - -func (dc *DockerCluster) AddNode(ctx context.Context, opts *DockerClusterOptions) error { - leaderIdx, err := testcluster.LeaderNode(ctx, dc) - if err != nil { - return err - } - if err := dc.addNode(ctx, opts); err != nil { - return err - } - - return dc.joinNode(ctx, len(dc.ClusterNodes)-1, leaderIdx) -} - -func (dc *DockerCluster) addNode(ctx context.Context, opts *DockerClusterOptions) error { - tag, err := dc.setupImage(ctx, opts) - if err != nil { - return err - } - i := len(dc.ClusterNodes) - nodeID := fmt.Sprintf("core-%d", i) - node := &DockerClusterNode{ - DockerAPI: dc.DockerAPI, - NodeID: nodeID, - Cluster: dc, - WorkDir: filepath.Join(dc.tmpDir, nodeID), - Logger: dc.Logger.Named(nodeID), - ImageRepo: opts.ImageRepo, - ImageTag: tag, - } - dc.ClusterNodes = append(dc.ClusterNodes, node) - if err := os.MkdirAll(node.WorkDir, 0o755); err != nil { - return err - } - if err := node.Start(ctx, opts); err != nil { - return err - } - return nil -} - -func (dc *DockerCluster) joinNode(ctx context.Context, nodeIdx int, leaderIdx int) error { - if dc.storage != nil && dc.storage.Type() != "raft" { - // Storage is not raft so nothing to do but unseal. - return testcluster.UnsealNode(ctx, dc, nodeIdx) - } - - leader := dc.ClusterNodes[leaderIdx] - - if nodeIdx >= len(dc.ClusterNodes) { - return fmt.Errorf("invalid node %d", nodeIdx) - } - node := dc.ClusterNodes[nodeIdx] - client := node.APIClient() - - var resp *api.RaftJoinResponse - resp, err := client.Sys().RaftJoinWithContext(ctx, &api.RaftJoinRequest{ - // When running locally on a bridge network, the containers must use their - // actual (private) IP to talk to one another. Our code must instead use - // the portmapped address since we're not on their network in that case. - LeaderAPIAddr: leader.RealAPIAddr, - LeaderCACert: string(dc.CACertPEM), - LeaderClientCert: string(node.ServerCertPEM), - LeaderClientKey: string(node.ServerKeyPEM), - }) - if resp == nil || !resp.Joined { - return fmt.Errorf("nil or negative response from raft join request: %v", resp) - } - if err != nil { - return fmt.Errorf("failed to join cluster: %w", err) - } - - return testcluster.UnsealNode(ctx, dc, nodeIdx) -} - -func (dc *DockerCluster) setupImage(ctx context.Context, opts *DockerClusterOptions) (string, error) { - if opts == nil { - opts = &DockerClusterOptions{} - } - sourceTag := opts.ImageTag - if sourceTag == "" { - sourceTag = "latest" - } - - if opts.VaultBinary == "" { - return sourceTag, nil - } - - suffix := "testing" - if sha := os.Getenv("COMMIT_SHA"); sha != "" { - suffix = sha - } - tag := sourceTag + "-" + suffix - if _, ok := dc.builtTags[tag]; ok { - return tag, nil - } - - f, err := os.Open(opts.VaultBinary) - if err != nil { - return "", err - } - data, err := io.ReadAll(f) - if err != nil { - return "", err - } - bCtx := dockhelper.NewBuildContext() - bCtx["vault"] = &dockhelper.FileContents{ - Data: data, - Mode: 0o755, - } - - containerFile := fmt.Sprintf(` -FROM %s:%s -COPY vault /bin/vault -`, opts.ImageRepo, sourceTag) - - _, err = dockhelper.BuildImage(ctx, dc.DockerAPI, containerFile, bCtx, - dockhelper.BuildRemove(true), dockhelper.BuildForceRemove(true), - dockhelper.BuildPullParent(true), - dockhelper.BuildTags([]string{opts.ImageRepo + ":" + tag})) - if err != nil { - return "", err - } - dc.builtTags[tag] = struct{}{} - return tag, nil -} - -/* Notes on testing the non-bridge network case: -- you need the test itself to be running in a container so that it can use - the network; create the network using - docker network create testvault -- this means that you need to mount the docker socket in that test container, - but on macos there's stuff that prevents that from working; to hack that, - on the host run - sudo ln -s "$HOME/Library/Containers/com.docker.docker/Data/docker.raw.sock" /var/run/docker.sock.raw -- run the test container like - docker run --rm -it --network testvault \ - -v /var/run/docker.sock.raw:/var/run/docker.sock \ - -v $(pwd):/home/circleci/go/src/github.com/hashicorp/vault/ \ - -w /home/circleci/go/src/github.com/hashicorp/vault/ \ - "docker.mirror.hashicorp.services/cimg/go:1.19.2" /bin/bash -- in the container you may need to chown/chmod /var/run/docker.sock; use `docker ps` - to test if it's working - -*/ diff --git a/sdk/helper/testcluster/docker/replication.go b/sdk/helper/testcluster/docker/replication.go deleted file mode 100644 index 0bd8feea2..000000000 --- a/sdk/helper/testcluster/docker/replication.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package docker - -import ( - "context" - "fmt" - "os" - "strings" - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/helper/testcluster" -) - -func DefaultOptions(t *testing.T) *DockerClusterOptions { - return &DockerClusterOptions{ - ImageRepo: "hashicorp/vault", - ImageTag: "latest", - VaultBinary: os.Getenv("VAULT_BINARY"), - ClusterOptions: testcluster.ClusterOptions{ - NumCores: 3, - ClusterName: strings.ReplaceAll(t.Name(), "/", "-"), - VaultNodeConfig: &testcluster.VaultNodeConfig{ - LogLevel: "TRACE", - }, - }, - } -} - -func NewReplicationSetDocker(t *testing.T, opts *DockerClusterOptions) (*testcluster.ReplicationSet, error) { - binary := os.Getenv("VAULT_BINARY") - if binary == "" { - t.Skip("only running docker test when $VAULT_BINARY present") - } - - r := &testcluster.ReplicationSet{ - Clusters: map[string]testcluster.VaultCluster{}, - Logger: logging.NewVaultLogger(hclog.Trace).Named(t.Name()), - } - - // clusterName is used for container name as well. - // A container name should not exceed 64 chars. - // There are additional chars that are added to the name as well - // like "-A-core0". So, setting a max limit for a cluster name. - if len(opts.ClusterName) > MaxClusterNameLength { - return nil, fmt.Errorf("cluster name length exceeded the maximum allowed length of %v", MaxClusterNameLength) - } - - r.Builder = func(ctx context.Context, name string, baseLogger hclog.Logger) (testcluster.VaultCluster, error) { - myOpts := *opts - myOpts.Logger = baseLogger.Named(name) - myOpts.ClusterName += "-" + strings.ReplaceAll(name, "/", "-") - myOpts.CA = r.CA - return NewTestDockerCluster(t, &myOpts), nil - } - - a, err := r.Builder(context.TODO(), "A", r.Logger) - if err != nil { - return nil, err - } - r.Clusters["A"] = a - r.CA = a.(*DockerCluster).CA - - return r, err -} diff --git a/sdk/helper/testcluster/exec.go b/sdk/helper/testcluster/exec.go deleted file mode 100644 index d91a3de03..000000000 --- a/sdk/helper/testcluster/exec.go +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package testcluster - -import ( - "bufio" - "context" - "crypto/tls" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/helper/logging" -) - -type ExecDevCluster struct { - ID string - ClusterName string - ClusterNodes []*execDevClusterNode - CACertPEMFile string - barrierKeys [][]byte - recoveryKeys [][]byte - tmpDir string - clientAuthRequired bool - rootToken string - stop func() - stopCh chan struct{} - Logger log.Logger -} - -func (dc *ExecDevCluster) SetRootToken(token string) { - dc.rootToken = token -} - -func (dc *ExecDevCluster) NamedLogger(s string) log.Logger { - return dc.Logger.Named(s) -} - -var _ VaultCluster = &ExecDevCluster{} - -type ExecDevClusterOptions struct { - ClusterOptions - BinaryPath string - // this is -dev-listen-address, defaults to "127.0.0.1:8200" - BaseListenAddress string -} - -func NewTestExecDevCluster(t *testing.T, opts *ExecDevClusterOptions) *ExecDevCluster { - if opts == nil { - opts = &ExecDevClusterOptions{} - } - if opts.ClusterName == "" { - opts.ClusterName = strings.ReplaceAll(t.Name(), "/", "-") - } - if opts.Logger == nil { - opts.Logger = logging.NewVaultLogger(log.Trace).Named(t.Name()) // .Named("container") - } - if opts.VaultLicense == "" { - opts.VaultLicense = os.Getenv(EnvVaultLicenseCI) - } - - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - t.Cleanup(cancel) - - dc, err := NewExecDevCluster(ctx, opts) - if err != nil { - t.Fatal(err) - } - return dc -} - -func NewExecDevCluster(ctx context.Context, opts *ExecDevClusterOptions) (*ExecDevCluster, error) { - dc := &ExecDevCluster{ - ClusterName: opts.ClusterName, - stopCh: make(chan struct{}), - } - - if opts == nil { - opts = &ExecDevClusterOptions{} - } - if opts.NumCores == 0 { - opts.NumCores = 3 - } - if err := dc.setupExecDevCluster(ctx, opts); err != nil { - dc.Cleanup() - return nil, err - } - - return dc, nil -} - -func (dc *ExecDevCluster) setupExecDevCluster(ctx context.Context, opts *ExecDevClusterOptions) (retErr error) { - if opts == nil { - opts = &ExecDevClusterOptions{} - } - if opts.Logger == nil { - opts.Logger = log.NewNullLogger() - } - dc.Logger = opts.Logger - - if opts.TmpDir != "" { - if _, err := os.Stat(opts.TmpDir); os.IsNotExist(err) { - if err := os.MkdirAll(opts.TmpDir, 0o700); err != nil { - return err - } - } - dc.tmpDir = opts.TmpDir - } else { - tempDir, err := os.MkdirTemp("", "vault-test-cluster-") - if err != nil { - return err - } - dc.tmpDir = tempDir - } - - // This context is used to stop the subprocess - execCtx, cancel := context.WithCancel(context.Background()) - dc.stop = func() { - cancel() - close(dc.stopCh) - } - defer func() { - if retErr != nil { - cancel() - } - }() - - bin := opts.BinaryPath - if bin == "" { - bin = "vault" - } - - clusterJsonPath := filepath.Join(dc.tmpDir, "cluster.json") - args := []string{"server", "-dev", "-dev-cluster-json", clusterJsonPath} - switch { - case opts.NumCores == 3: - args = append(args, "-dev-three-node") - case opts.NumCores == 1: - args = append(args, "-dev-tls") - default: - return fmt.Errorf("NumCores=1 and NumCores=3 are the only supported options right now") - } - if opts.BaseListenAddress != "" { - args = append(args, "-dev-listen-address", opts.BaseListenAddress) - } - cmd := exec.CommandContext(execCtx, bin, args...) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "VAULT_LICENSE="+opts.VaultLicense) - cmd.Env = append(cmd.Env, "VAULT_LOG_FORMAT=json") - cmd.Env = append(cmd.Env, "VAULT_DEV_TEMP_DIR="+dc.tmpDir) - if opts.Logger != nil { - stdout, err := cmd.StdoutPipe() - if err != nil { - return err - } - go func() { - outlog := opts.Logger.Named("stdout") - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - outlog.Trace(scanner.Text()) - } - }() - stderr, err := cmd.StderrPipe() - if err != nil { - return err - } - go func() { - errlog := opts.Logger.Named("stderr") - scanner := bufio.NewScanner(stderr) - // The default buffer is 4k, and Vault can emit bigger log lines - scanner.Buffer(make([]byte, 64*1024), bufio.MaxScanTokenSize) - for scanner.Scan() { - JSONLogNoTimestamp(errlog, scanner.Text()) - } - }() - } - - if err := cmd.Start(); err != nil { - return err - } - - for ctx.Err() == nil { - if b, err := os.ReadFile(clusterJsonPath); err == nil && len(b) > 0 { - var clusterJson ClusterJson - if err := jsonutil.DecodeJSON(b, &clusterJson); err != nil { - continue - } - dc.CACertPEMFile = clusterJson.CACertPath - dc.rootToken = clusterJson.RootToken - for i, node := range clusterJson.Nodes { - config := api.DefaultConfig() - config.Address = node.APIAddress - err := config.ConfigureTLS(&api.TLSConfig{ - CACert: clusterJson.CACertPath, - }) - if err != nil { - return err - } - client, err := api.NewClient(config) - if err != nil { - return err - } - client.SetToken(dc.rootToken) - _, err = client.Sys().ListMounts() - if err != nil { - return err - } - - dc.ClusterNodes = append(dc.ClusterNodes, &execDevClusterNode{ - name: fmt.Sprintf("core-%d", i), - client: client, - }) - } - return nil - } - time.Sleep(500 * time.Millisecond) - } - return ctx.Err() -} - -type execDevClusterNode struct { - name string - client *api.Client -} - -var _ VaultClusterNode = &execDevClusterNode{} - -func (e *execDevClusterNode) Name() string { - return e.name -} - -func (e *execDevClusterNode) APIClient() *api.Client { - // We clone to ensure that whenever this method is called, the caller gets - // back a pristine client, without e.g. any namespace or token changes that - // might pollute a shared client. We clone the config instead of the - // client because (1) Client.clone propagates the replicationStateStore and - // the httpClient pointers, (2) it doesn't copy the tlsConfig at all, and - // (3) if clone returns an error, it doesn't feel as appropriate to panic - // below. Who knows why clone might return an error? - cfg := e.client.CloneConfig() - client, err := api.NewClient(cfg) - if err != nil { - // It seems fine to panic here, since this should be the same input - // we provided to NewClient when we were setup, and we didn't panic then. - // Better not to completely ignore the error though, suppose there's a - // bug in CloneConfig? - panic(fmt.Sprintf("NewClient error on cloned config: %v", err)) - } - client.SetToken(e.client.Token()) - return client -} - -func (e *execDevClusterNode) TLSConfig() *tls.Config { - return e.client.CloneConfig().TLSConfig() -} - -func (dc *ExecDevCluster) ClusterID() string { - return dc.ID -} - -func (dc *ExecDevCluster) Nodes() []VaultClusterNode { - ret := make([]VaultClusterNode, len(dc.ClusterNodes)) - for i := range dc.ClusterNodes { - ret[i] = dc.ClusterNodes[i] - } - return ret -} - -func (dc *ExecDevCluster) GetBarrierKeys() [][]byte { - return dc.barrierKeys -} - -func copyKey(key []byte) []byte { - result := make([]byte, len(key)) - copy(result, key) - return result -} - -func (dc *ExecDevCluster) GetRecoveryKeys() [][]byte { - ret := make([][]byte, len(dc.recoveryKeys)) - for i, k := range dc.recoveryKeys { - ret[i] = copyKey(k) - } - return ret -} - -func (dc *ExecDevCluster) GetBarrierOrRecoveryKeys() [][]byte { - return dc.GetBarrierKeys() -} - -func (dc *ExecDevCluster) SetBarrierKeys(keys [][]byte) { - dc.barrierKeys = make([][]byte, len(keys)) - for i, k := range keys { - dc.barrierKeys[i] = copyKey(k) - } -} - -func (dc *ExecDevCluster) SetRecoveryKeys(keys [][]byte) { - dc.recoveryKeys = make([][]byte, len(keys)) - for i, k := range keys { - dc.recoveryKeys[i] = copyKey(k) - } -} - -func (dc *ExecDevCluster) GetCACertPEMFile() string { - return dc.CACertPEMFile -} - -func (dc *ExecDevCluster) Cleanup() { - dc.stop() -} - -// GetRootToken returns the root token of the cluster, if set -func (dc *ExecDevCluster) GetRootToken() string { - return dc.rootToken -} diff --git a/sdk/helper/testcluster/logging.go b/sdk/helper/testcluster/logging.go deleted file mode 100644 index dda759c7f..000000000 --- a/sdk/helper/testcluster/logging.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package testcluster - -import ( - "encoding/json" - "strings" - - "github.com/hashicorp/go-hclog" -) - -func JSONLogNoTimestamp(outlog hclog.Logger, text string) { - d := json.NewDecoder(strings.NewReader(text)) - m := map[string]interface{}{} - if err := d.Decode(&m); err != nil { - outlog.Error("failed to decode json output from dev vault", "error", err, "input", text) - return - } - - delete(m, "@timestamp") - message := m["@message"].(string) - delete(m, "@message") - level := m["@level"].(string) - delete(m, "@level") - if module, ok := m["@module"]; ok { - delete(m, "@module") - outlog = outlog.Named(module.(string)) - } - - var pairs []interface{} - for k, v := range m { - pairs = append(pairs, k, v) - } - - outlog.Log(hclog.LevelFromString(level), message, pairs...) -} diff --git a/sdk/helper/testcluster/replication.go b/sdk/helper/testcluster/replication.go deleted file mode 100644 index 3c696dfb8..000000000 --- a/sdk/helper/testcluster/replication.go +++ /dev/null @@ -1,899 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package testcluster - -import ( - "context" - "encoding/json" - "fmt" - "reflect" - "strings" - "time" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-secure-stdlib/strutil" - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/mitchellh/mapstructure" -) - -func GetPerformanceToken(pri VaultCluster, id, secondaryPublicKey string) (string, error) { - client := pri.Nodes()[0].APIClient() - req := map[string]interface{}{ - "id": id, - } - if secondaryPublicKey != "" { - req["secondary_public_key"] = secondaryPublicKey - } - secret, err := client.Logical().Write("sys/replication/performance/primary/secondary-token", req) - if err != nil { - return "", err - } - - if secondaryPublicKey != "" { - return secret.Data["token"].(string), nil - } - return secret.WrapInfo.Token, nil -} - -func EnablePerfPrimary(ctx context.Context, pri VaultCluster) error { - client := pri.Nodes()[0].APIClient() - _, err := client.Logical().WriteWithContext(ctx, "sys/replication/performance/primary/enable", nil) - if err != nil { - return err - } - - err = WaitForPerfReplicationState(ctx, pri, consts.ReplicationPerformancePrimary) - if err != nil { - return err - } - return WaitForActiveNodeAndPerfStandbys(ctx, pri) -} - -func WaitForPerfReplicationState(ctx context.Context, cluster VaultCluster, state consts.ReplicationState) error { - client := cluster.Nodes()[0].APIClient() - var health *api.HealthResponse - var err error - for ctx.Err() == nil { - health, err = client.Sys().HealthWithContext(ctx) - if err == nil && health.ReplicationPerformanceMode == state.GetPerformanceString() { - return nil - } - time.Sleep(500 * time.Millisecond) - } - if err == nil { - err = ctx.Err() - } - return err -} - -func EnablePerformanceSecondaryNoWait(ctx context.Context, perfToken string, pri, sec VaultCluster, updatePrimary bool) error { - postData := map[string]interface{}{ - "token": perfToken, - "ca_file": DefaultCAFile, - } - path := "sys/replication/performance/secondary/enable" - if updatePrimary { - path = "sys/replication/performance/secondary/update-primary" - } - err := WaitForActiveNodeAndPerfStandbys(ctx, sec) - if err != nil { - return err - } - _, err = sec.Nodes()[0].APIClient().Logical().Write(path, postData) - if err != nil { - return err - } - - return WaitForPerfReplicationState(ctx, sec, consts.ReplicationPerformanceSecondary) -} - -func EnablePerformanceSecondary(ctx context.Context, perfToken string, pri, sec VaultCluster, updatePrimary, skipPoisonPill bool) (string, error) { - if err := EnablePerformanceSecondaryNoWait(ctx, perfToken, pri, sec, updatePrimary); err != nil { - return "", err - } - if err := WaitForMatchingMerkleRoots(ctx, "sys/replication/performance/", pri, sec); err != nil { - return "", err - } - root, err := WaitForPerformanceSecondary(ctx, pri, sec, skipPoisonPill) - if err != nil { - return "", err - } - if err := WaitForPerfReplicationWorking(ctx, pri, sec); err != nil { - return "", err - } - return root, nil -} - -func WaitForMatchingMerkleRoots(ctx context.Context, endpoint string, pri, sec VaultCluster) error { - getRoot := func(mode string, cli *api.Client) (string, error) { - status, err := cli.Logical().Read(endpoint + "status") - if err != nil { - return "", err - } - if status == nil || status.Data == nil || status.Data["mode"] == nil { - return "", fmt.Errorf("got nil secret or data") - } - if status.Data["mode"].(string) != mode { - return "", fmt.Errorf("expected mode=%s, got %s", mode, status.Data["mode"].(string)) - } - return status.Data["merkle_root"].(string), nil - } - - secClient := sec.Nodes()[0].APIClient() - priClient := pri.Nodes()[0].APIClient() - for i := 0; i < 30; i++ { - secRoot, err := getRoot("secondary", secClient) - if err != nil { - return err - } - priRoot, err := getRoot("primary", priClient) - if err != nil { - return err - } - - if reflect.DeepEqual(priRoot, secRoot) { - return nil - } - time.Sleep(time.Second) - } - - return fmt.Errorf("roots did not become equal") -} - -func WaitForPerformanceWAL(ctx context.Context, pri, sec VaultCluster) error { - endpoint := "sys/replication/performance/" - if err := WaitForMatchingMerkleRoots(ctx, endpoint, pri, sec); err != nil { - return nil - } - getWAL := func(mode, walKey string, cli *api.Client) (int64, error) { - status, err := cli.Logical().Read(endpoint + "status") - if err != nil { - return 0, err - } - if status == nil || status.Data == nil || status.Data["mode"] == nil { - return 0, fmt.Errorf("got nil secret or data") - } - if status.Data["mode"].(string) != mode { - return 0, fmt.Errorf("expected mode=%s, got %s", mode, status.Data["mode"].(string)) - } - return status.Data[walKey].(json.Number).Int64() - } - - secClient := sec.Nodes()[0].APIClient() - priClient := pri.Nodes()[0].APIClient() - for ctx.Err() == nil { - secLastRemoteWAL, err := getWAL("secondary", "last_remote_wal", secClient) - if err != nil { - return err - } - priLastPerfWAL, err := getWAL("primary", "last_performance_wal", priClient) - if err != nil { - return err - } - - if secLastRemoteWAL >= priLastPerfWAL { - return nil - } - time.Sleep(time.Second) - } - - return fmt.Errorf("performance WALs on the secondary did not catch up with the primary, context err: %w", ctx.Err()) -} - -func WaitForPerformanceSecondary(ctx context.Context, pri, sec VaultCluster, skipPoisonPill bool) (string, error) { - if len(pri.GetRecoveryKeys()) > 0 { - sec.SetBarrierKeys(pri.GetRecoveryKeys()) - sec.SetRecoveryKeys(pri.GetRecoveryKeys()) - } else { - sec.SetBarrierKeys(pri.GetBarrierKeys()) - sec.SetRecoveryKeys(pri.GetBarrierKeys()) - } - - if len(sec.Nodes()) > 1 { - if skipPoisonPill { - // As part of prepareSecondary on the active node the keyring is - // deleted from storage. Its absence can cause standbys to seal - // themselves. But it's not reliable, so we'll seal them - // ourselves to force the issue. - for i := range sec.Nodes()[1:] { - if err := SealNode(ctx, sec, i+1); err != nil { - return "", err - } - } - } else { - // We want to make sure we unseal all the nodes so we first need to wait - // until two of the nodes seal due to the poison pill being written - if err := WaitForNCoresSealed(ctx, sec, len(sec.Nodes())-1); err != nil { - return "", err - } - } - } - if _, err := WaitForActiveNode(ctx, sec); err != nil { - return "", err - } - if err := UnsealAllNodes(ctx, sec); err != nil { - return "", err - } - - perfSecondaryRootToken, err := GenerateRoot(sec, GenerateRootRegular) - if err != nil { - return "", err - } - sec.SetRootToken(perfSecondaryRootToken) - if err := WaitForActiveNodeAndPerfStandbys(ctx, sec); err != nil { - return "", err - } - - return perfSecondaryRootToken, nil -} - -func WaitForPerfReplicationWorking(ctx context.Context, pri, sec VaultCluster) error { - priActiveIdx, err := WaitForActiveNode(ctx, pri) - if err != nil { - return err - } - secActiveIdx, err := WaitForActiveNode(ctx, sec) - if err != nil { - return err - } - - priClient, secClient := pri.Nodes()[priActiveIdx].APIClient(), sec.Nodes()[secActiveIdx].APIClient() - mountPoint, err := uuid.GenerateUUID() - if err != nil { - return err - } - err = priClient.Sys().Mount(mountPoint, &api.MountInput{ - Type: "kv", - Local: false, - }) - if err != nil { - return fmt.Errorf("unable to mount KV engine on primary") - } - - path := mountPoint + "/foo" - _, err = priClient.Logical().Write(path, map[string]interface{}{ - "bar": 1, - }) - if err != nil { - return fmt.Errorf("unable to write KV on primary, path=%s", path) - } - - for ctx.Err() == nil { - var secret *api.Secret - secret, err = secClient.Logical().Read(path) - if err == nil && secret != nil { - err = priClient.Sys().Unmount(mountPoint) - if err != nil { - return fmt.Errorf("unable to unmount KV engine on primary") - } - return nil - } - time.Sleep(100 * time.Millisecond) - } - if err == nil { - err = ctx.Err() - } - return fmt.Errorf("unable to read replicated KV on secondary, path=%s, err=%v", path, err) -} - -func SetupTwoClusterPerfReplication(ctx context.Context, pri, sec VaultCluster) error { - if err := EnablePerfPrimary(ctx, pri); err != nil { - return err - } - perfToken, err := GetPerformanceToken(pri, sec.ClusterID(), "") - if err != nil { - return err - } - - _, err = EnablePerformanceSecondary(ctx, perfToken, pri, sec, false, false) - return err -} - -// PassiveWaitForActiveNodeAndPerfStandbys should be used instead of -// WaitForActiveNodeAndPerfStandbys when you don't want to do any writes -// as a side-effect. This returns perfStandby nodes in the cluster and -// an error. -func PassiveWaitForActiveNodeAndPerfStandbys(ctx context.Context, pri VaultCluster) (VaultClusterNode, []VaultClusterNode, error) { - leaderNode, standbys, err := GetActiveAndStandbys(ctx, pri) - if err != nil { - return nil, nil, fmt.Errorf("failed to derive standby nodes, %w", err) - } - - for i, node := range standbys { - client := node.APIClient() - // Make sure we get perf standby nodes - if err = EnsureCoreIsPerfStandby(ctx, client); err != nil { - return nil, nil, fmt.Errorf("standby node %d is not a perfStandby, %w", i, err) - } - } - - return leaderNode, standbys, nil -} - -func GetActiveAndStandbys(ctx context.Context, cluster VaultCluster) (VaultClusterNode, []VaultClusterNode, error) { - var leaderIndex int - var err error - if leaderIndex, err = WaitForActiveNode(ctx, cluster); err != nil { - return nil, nil, err - } - - var leaderNode VaultClusterNode - var nodes []VaultClusterNode - for i, node := range cluster.Nodes() { - if i == leaderIndex { - leaderNode = node - continue - } - nodes = append(nodes, node) - } - - return leaderNode, nodes, nil -} - -func EnsureCoreIsPerfStandby(ctx context.Context, client *api.Client) error { - var err error - var health *api.HealthResponse - for ctx.Err() == nil { - health, err = client.Sys().HealthWithContext(ctx) - if err == nil && health.PerformanceStandby { - return nil - } - time.Sleep(time.Millisecond * 500) - } - if err == nil { - err = ctx.Err() - } - return err -} - -func WaitForDRReplicationState(ctx context.Context, cluster VaultCluster, state consts.ReplicationState) error { - client := cluster.Nodes()[0].APIClient() - var health *api.HealthResponse - var err error - for ctx.Err() == nil { - health, err = client.Sys().HealthWithContext(ctx) - if err == nil && health.ReplicationDRMode == state.GetDRString() { - return nil - } - time.Sleep(500 * time.Millisecond) - } - if err == nil { - err = ctx.Err() - } - return err -} - -func EnableDrPrimary(ctx context.Context, pri VaultCluster) error { - client := pri.Nodes()[0].APIClient() - _, err := client.Logical().Write("sys/replication/dr/primary/enable", nil) - if err != nil { - return err - } - - err = WaitForDRReplicationState(ctx, pri, consts.ReplicationDRPrimary) - if err != nil { - return err - } - return WaitForActiveNodeAndPerfStandbys(ctx, pri) -} - -func GenerateDRActivationToken(pri VaultCluster, id, secondaryPublicKey string) (string, error) { - client := pri.Nodes()[0].APIClient() - req := map[string]interface{}{ - "id": id, - } - if secondaryPublicKey != "" { - req["secondary_public_key"] = secondaryPublicKey - } - secret, err := client.Logical().Write("sys/replication/dr/primary/secondary-token", req) - if err != nil { - return "", err - } - - if secondaryPublicKey != "" { - return secret.Data["token"].(string), nil - } - return secret.WrapInfo.Token, nil -} - -func WaitForDRSecondary(ctx context.Context, pri, sec VaultCluster, skipPoisonPill bool) error { - if len(pri.GetRecoveryKeys()) > 0 { - sec.SetBarrierKeys(pri.GetRecoveryKeys()) - sec.SetRecoveryKeys(pri.GetRecoveryKeys()) - } else { - sec.SetBarrierKeys(pri.GetBarrierKeys()) - sec.SetRecoveryKeys(pri.GetBarrierKeys()) - } - - if len(sec.Nodes()) > 1 { - if skipPoisonPill { - // As part of prepareSecondary on the active node the keyring is - // deleted from storage. Its absence can cause standbys to seal - // themselves. But it's not reliable, so we'll seal them - // ourselves to force the issue. - for i := range sec.Nodes()[1:] { - if err := SealNode(ctx, sec, i+1); err != nil { - return err - } - } - } else { - // We want to make sure we unseal all the nodes so we first need to wait - // until two of the nodes seal due to the poison pill being written - if err := WaitForNCoresSealed(ctx, sec, len(sec.Nodes())-1); err != nil { - return err - } - } - } - if _, err := WaitForActiveNode(ctx, sec); err != nil { - return err - } - - // unseal nodes - for i := range sec.Nodes() { - if err := UnsealNode(ctx, sec, i); err != nil { - // Sometimes when we get here it's already unsealed on its own - // and then this fails for DR secondaries so check again - // The error is "path disabled in replication DR secondary mode". - if healthErr := NodeHealthy(ctx, sec, i); healthErr != nil { - // return the original error - return err - } - } - } - - sec.SetRootToken(pri.GetRootToken()) - - if _, err := WaitForActiveNode(ctx, sec); err != nil { - return err - } - - return nil -} - -func EnableDRSecondaryNoWait(ctx context.Context, sec VaultCluster, drToken string) error { - postData := map[string]interface{}{ - "token": drToken, - "ca_file": DefaultCAFile, - } - - _, err := sec.Nodes()[0].APIClient().Logical().Write("sys/replication/dr/secondary/enable", postData) - if err != nil { - return err - } - - return WaitForDRReplicationState(ctx, sec, consts.ReplicationDRSecondary) -} - -func WaitForReplicationStatus(ctx context.Context, client *api.Client, dr bool, accept func(map[string]interface{}) bool) error { - url := "sys/replication/performance/status" - if dr { - url = "sys/replication/dr/status" - } - - var err error - var secret *api.Secret - for ctx.Err() == nil { - secret, err = client.Logical().Read(url) - if err == nil && secret != nil && secret.Data != nil { - if accept(secret.Data) { - return nil - } - } - time.Sleep(500 * time.Millisecond) - } - if err == nil { - err = ctx.Err() - } - - return fmt.Errorf("unable to get acceptable replication status: error=%v secret=%#v", err, secret) -} - -func WaitForDRReplicationWorking(ctx context.Context, pri, sec VaultCluster) error { - priClient := pri.Nodes()[0].APIClient() - secClient := sec.Nodes()[0].APIClient() - - // Make sure we've entered stream-wals mode - err := WaitForReplicationStatus(ctx, secClient, true, func(secret map[string]interface{}) bool { - return secret["state"] == string("stream-wals") - }) - if err != nil { - return err - } - - // Now write some data and make sure that we see last_remote_wal nonzero, i.e. - // at least one WAL has been streamed. - secret, err := priClient.Auth().Token().Create(&api.TokenCreateRequest{}) - if err != nil { - return err - } - - // Revoke the token since some tests won't be happy to see it. - err = priClient.Auth().Token().RevokeTree(secret.Auth.ClientToken) - if err != nil { - return err - } - - err = WaitForReplicationStatus(ctx, secClient, true, func(secret map[string]interface{}) bool { - if secret["state"] != string("stream-wals") { - return false - } - if secret["last_remote_wal"] != nil { - lastRemoteWal, _ := secret["last_remote_wal"].(json.Number).Int64() - return lastRemoteWal > 0 - } - - return false - }) - if err != nil { - return err - } - return nil -} - -func EnableDrSecondary(ctx context.Context, pri, sec VaultCluster, drToken string) error { - err := EnableDRSecondaryNoWait(ctx, sec, drToken) - if err != nil { - return err - } - - if err = WaitForMatchingMerkleRoots(ctx, "sys/replication/dr/", pri, sec); err != nil { - return err - } - - err = WaitForDRSecondary(ctx, pri, sec, false) - if err != nil { - return err - } - - if err = WaitForDRReplicationWorking(ctx, pri, sec); err != nil { - return err - } - return nil -} - -func SetupTwoClusterDRReplication(ctx context.Context, pri, sec VaultCluster) error { - if err := EnableDrPrimary(ctx, pri); err != nil { - return err - } - - drToken, err := GenerateDRActivationToken(pri, sec.ClusterID(), "") - if err != nil { - return err - } - err = EnableDrSecondary(ctx, pri, sec, drToken) - if err != nil { - return err - } - return nil -} - -func DemoteDRPrimary(client *api.Client) error { - _, err := client.Logical().Write("sys/replication/dr/primary/demote", map[string]interface{}{}) - return err -} - -func createBatchToken(client *api.Client, path string) (string, error) { - // TODO: should these be more random in case more than one batch token needs to be created? - suffix := strings.Replace(path, "/", "", -1) - policyName := "path-batch-policy-" + suffix - roleName := "path-batch-role-" + suffix - - rules := fmt.Sprintf(`path "%s" { capabilities = [ "read", "update" ] }`, path) - - // create policy - _, err := client.Logical().Write("sys/policy/"+policyName, map[string]interface{}{ - "policy": rules, - }) - if err != nil { - return "", err - } - - // create a role - _, err = client.Logical().Write("auth/token/roles/"+roleName, map[string]interface{}{ - "allowed_policies": policyName, - "orphan": true, - "renewable": false, - "token_type": "batch", - }) - if err != nil { - return "", err - } - - // create batch token - secret, err := client.Logical().Write("auth/token/create/"+roleName, nil) - if err != nil { - return "", err - } - - return secret.Auth.ClientToken, nil -} - -// PromoteDRSecondaryWithBatchToken creates a batch token for DR promotion -// before promotion, it demotes the primary cluster. The primary cluster needs -// to be functional for the generation of the batch token -func PromoteDRSecondaryWithBatchToken(ctx context.Context, pri, sec VaultCluster) error { - client := pri.Nodes()[0].APIClient() - drToken, err := createBatchToken(client, "sys/replication/dr/secondary/promote") - if err != nil { - return err - } - - err = DemoteDRPrimary(client) - if err != nil { - return err - } - - return promoteDRSecondaryInternal(ctx, sec, drToken) -} - -// PromoteDRSecondary generates a DR operation token on the secondary using -// unseal/recovery keys. Therefore, the primary cluster could potentially -// be out of service. -func PromoteDRSecondary(ctx context.Context, sec VaultCluster) error { - // generate DR operation token to do update primary on vC to point to - // the new perfSec primary vD - drToken, err := GenerateRoot(sec, GenerateRootDR) - if err != nil { - return err - } - return promoteDRSecondaryInternal(ctx, sec, drToken) -} - -func promoteDRSecondaryInternal(ctx context.Context, sec VaultCluster, drToken string) error { - secClient := sec.Nodes()[0].APIClient() - - // Allow retries of 503s, e.g.: replication is still catching up, - // try again later or provide the "force" argument - oldMaxRetries := secClient.MaxRetries() - secClient.SetMaxRetries(10) - defer secClient.SetMaxRetries(oldMaxRetries) - resp, err := secClient.Logical().Write("sys/replication/dr/secondary/promote", map[string]interface{}{ - "dr_operation_token": drToken, - }) - if err != nil { - return err - } - if resp == nil { - return fmt.Errorf("nil status response during DR promotion") - } - - if _, err := WaitForActiveNode(ctx, sec); err != nil { - return err - } - - return WaitForDRReplicationState(ctx, sec, consts.ReplicationDRPrimary) -} - -func checkClusterAddr(ctx context.Context, pri, sec VaultCluster) error { - priClient := pri.Nodes()[0].APIClient() - priLeader, err := priClient.Sys().LeaderWithContext(ctx) - if err != nil { - return err - } - secClient := sec.Nodes()[0].APIClient() - endpoint := "sys/replication/dr/" - status, err := secClient.Logical().Read(endpoint + "status") - if err != nil { - return err - } - if status == nil || status.Data == nil { - return fmt.Errorf("got nil secret or data") - } - - var priAddrs []string - err = mapstructure.Decode(status.Data["known_primary_cluster_addrs"], &priAddrs) - if err != nil { - return err - } - if !strutil.StrListContains(priAddrs, priLeader.LeaderClusterAddress) { - return fmt.Errorf("failed to fine the expected primary cluster address %v in known_primary_cluster_addrs", priLeader.LeaderClusterAddress) - } - - return nil -} - -func UpdatePrimary(ctx context.Context, pri, sec VaultCluster) error { - // generate DR operation token to do update primary on vC to point to - // the new perfSec primary vD - rootToken, err := GenerateRoot(sec, GenerateRootDR) - if err != nil { - return err - } - - // secondary activation token - drToken, err := GenerateDRActivationToken(pri, sec.ClusterID(), "") - if err != nil { - return err - } - - // update-primary on vC (new perfSec Dr secondary) to point to - // the new perfSec Dr primary - secClient := sec.Nodes()[0].APIClient() - resp, err := secClient.Logical().Write("sys/replication/dr/secondary/update-primary", map[string]interface{}{ - "dr_operation_token": rootToken, - "token": drToken, - "ca_file": DefaultCAFile, - }) - if err != nil { - return err - } - if resp == nil { - return fmt.Errorf("nil status response during update primary") - } - - if _, err = WaitForActiveNode(ctx, sec); err != nil { - return err - } - - if err = WaitForDRReplicationState(ctx, sec, consts.ReplicationDRSecondary); err != nil { - return err - } - - if err = checkClusterAddr(ctx, pri, sec); err != nil { - return err - } - - return nil -} - -func SetupFourClusterReplication(ctx context.Context, pri, sec, pridr, secdr VaultCluster) error { - err := SetupTwoClusterPerfReplication(ctx, pri, sec) - if err != nil { - return err - } - err = SetupTwoClusterDRReplication(ctx, pri, pridr) - if err != nil { - return err - } - err = SetupTwoClusterDRReplication(ctx, sec, secdr) - if err != nil { - return err - } - return nil -} - -type ReplicationSet struct { - // By convention, we recommend the following naming scheme for - // clusters in this map: - // A: perf primary - // B: primary's DR - // C: first perf secondary of A - // D: C's DR - // E: second perf secondary of A - // F: E's DR - // ... etc. - // - // We use generic names rather than role-specific names because - // that's less confusing when promotions take place that result in role - // changes. In other words, if D gets promoted to replace C as a perf - // secondary, and C gets demoted and updated to become D's DR secondary, - // they should maintain their initial names of D and C throughout. - Clusters map[string]VaultCluster - Builder ClusterBuilder - Logger hclog.Logger - CA *CA -} - -type ClusterBuilder func(ctx context.Context, name string, logger hclog.Logger) (VaultCluster, error) - -func NewReplicationSet(b ClusterBuilder) (*ReplicationSet, error) { - return &ReplicationSet{ - Clusters: map[string]VaultCluster{}, - Builder: b, - Logger: hclog.NewNullLogger(), - }, nil -} - -func (r *ReplicationSet) StandardPerfReplication(ctx context.Context) error { - for _, name := range []string{"A", "C"} { - if _, ok := r.Clusters[name]; !ok { - cluster, err := r.Builder(ctx, name, r.Logger) - if err != nil { - return err - } - r.Clusters[name] = cluster - } - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - err := SetupTwoClusterPerfReplication(ctx, r.Clusters["A"], r.Clusters["C"]) - if err != nil { - return err - } - - return nil -} - -func (r *ReplicationSet) StandardDRReplication(ctx context.Context) error { - for _, name := range []string{"A", "B"} { - if _, ok := r.Clusters[name]; !ok { - cluster, err := r.Builder(ctx, name, r.Logger) - if err != nil { - return err - } - r.Clusters[name] = cluster - } - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - err := SetupTwoClusterDRReplication(ctx, r.Clusters["A"], r.Clusters["B"]) - if err != nil { - return err - } - - return nil -} - -func (r *ReplicationSet) GetFourReplicationCluster(ctx context.Context) error { - for _, name := range []string{"A", "B", "C", "D"} { - if _, ok := r.Clusters[name]; !ok { - cluster, err := r.Builder(ctx, name, r.Logger) - if err != nil { - return err - } - r.Clusters[name] = cluster - } - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - err := SetupFourClusterReplication(ctx, r.Clusters["A"], r.Clusters["C"], r.Clusters["B"], r.Clusters["D"]) - if err != nil { - return err - } - return nil -} - -func (r *ReplicationSet) Cleanup() { - for _, cluster := range r.Clusters { - cluster.Cleanup() - } -} - -func WaitForPerfReplicationConnectionStatus(ctx context.Context, client *api.Client) error { - type Primary struct { - APIAddress string `mapstructure:"api_address"` - ConnectionStatus string `mapstructure:"connection_status"` - ClusterAddress string `mapstructure:"cluster_address"` - LastHeartbeat string `mapstructure:"last_heartbeat"` - } - type Status struct { - Primaries []Primary `mapstructure:"primaries"` - } - return WaitForPerfReplicationStatus(ctx, client, func(m map[string]interface{}) error { - var status Status - err := mapstructure.Decode(m, &status) - if err != nil { - return err - } - if len(status.Primaries) == 0 { - return fmt.Errorf("primaries is zero") - } - for _, v := range status.Primaries { - if v.ConnectionStatus == "connected" { - return nil - } - } - return fmt.Errorf("no primaries connected") - }) -} - -func WaitForPerfReplicationStatus(ctx context.Context, client *api.Client, accept func(map[string]interface{}) error) error { - var err error - var secret *api.Secret - for ctx.Err() == nil { - secret, err = client.Logical().Read("sys/replication/performance/status") - if err == nil && secret != nil && secret.Data != nil { - if err = accept(secret.Data); err == nil { - return nil - } - } - time.Sleep(500 * time.Millisecond) - } - return fmt.Errorf("unable to get acceptable replication status within allotted time: error=%v secret=%#v", err, secret) -} diff --git a/sdk/helper/testcluster/types.go b/sdk/helper/testcluster/types.go deleted file mode 100644 index 989908fb1..000000000 --- a/sdk/helper/testcluster/types.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package testcluster - -import ( - "context" - "crypto/ecdsa" - "crypto/tls" - "crypto/x509" - "time" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" -) - -type VaultClusterNode interface { - APIClient() *api.Client - TLSConfig() *tls.Config -} - -type VaultCluster interface { - Nodes() []VaultClusterNode - GetBarrierKeys() [][]byte - GetRecoveryKeys() [][]byte - GetBarrierOrRecoveryKeys() [][]byte - SetBarrierKeys([][]byte) - SetRecoveryKeys([][]byte) - GetCACertPEMFile() string - Cleanup() - ClusterID() string - NamedLogger(string) hclog.Logger - SetRootToken(token string) - GetRootToken() string -} - -type VaultNodeConfig struct { - // Not configurable because cluster creator wants to control these: - // PluginDirectory string `hcl:"plugin_directory"` - // APIAddr string `hcl:"api_addr"` - // ClusterAddr string `hcl:"cluster_addr"` - // Storage *Storage `hcl:"-"` - // HAStorage *Storage `hcl:"-"` - // DisableMlock bool `hcl:"disable_mlock"` - // ClusterName string `hcl:"cluster_name"` - - // Not configurable yet: - // Listeners []*Listener `hcl:"-"` - // Seals []*KMS `hcl:"-"` - // Entropy *Entropy `hcl:"-"` - // Telemetry *Telemetry `hcl:"telemetry"` - // HCPLinkConf *HCPLinkConfig `hcl:"cloud"` - // PidFile string `hcl:"pid_file"` - // ServiceRegistrationType string - // ServiceRegistrationOptions map[string]string - - StorageOptions map[string]string - - DefaultMaxRequestDuration time.Duration `json:"default_max_request_duration"` - LogFormat string `json:"log_format"` - LogLevel string `json:"log_level"` - CacheSize int `json:"cache_size"` - DisableCache bool `json:"disable_cache"` - DisablePrintableCheck bool `json:"disable_printable_check"` - EnableUI bool `json:"ui"` - MaxLeaseTTL time.Duration `json:"max_lease_ttl"` - DefaultLeaseTTL time.Duration `json:"default_lease_ttl"` - ClusterCipherSuites string `json:"cluster_cipher_suites"` - PluginFileUid int `json:"plugin_file_uid"` - PluginFilePermissions int `json:"plugin_file_permissions"` - EnableRawEndpoint bool `json:"raw_storage_endpoint"` - DisableClustering bool `json:"disable_clustering"` - DisablePerformanceStandby bool `json:"disable_performance_standby"` - DisableSealWrap bool `json:"disable_sealwrap"` - DisableIndexing bool `json:"disable_indexing"` - DisableSentinelTrace bool `json:"disable_sentinel"` - EnableResponseHeaderHostname bool `json:"enable_response_header_hostname"` - LogRequestsLevel string `json:"log_requests_level"` - EnableResponseHeaderRaftNodeID bool `json:"enable_response_header_raft_node_id"` - LicensePath string `json:"license_path"` -} - -type ClusterNode struct { - APIAddress string `json:"api_address"` -} - -type ClusterJson struct { - Nodes []ClusterNode `json:"nodes"` - CACertPath string `json:"ca_cert_path"` - RootToken string `json:"root_token"` -} - -type ClusterOptions struct { - ClusterName string - KeepStandbysSealed bool - SkipInit bool - CACert []byte - NumCores int - TmpDir string - Logger hclog.Logger - VaultNodeConfig *VaultNodeConfig - VaultLicense string - AdministrativeNamespacePath string -} - -type CA struct { - CACert *x509.Certificate - CACertBytes []byte - CACertPEM []byte - CACertPEMFile string - CAKey *ecdsa.PrivateKey - CAKeyPEM []byte -} - -type ClusterStorage interface { - Start(context.Context, *ClusterOptions) error - Cleanup() error - Opts() map[string]interface{} - Type() string -} diff --git a/sdk/helper/testcluster/util.go b/sdk/helper/testcluster/util.go deleted file mode 100644 index 883cd6992..000000000 --- a/sdk/helper/testcluster/util.go +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package testcluster - -import ( - "context" - "encoding/base64" - "encoding/hex" - "fmt" - "sync/atomic" - "time" - - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/helper/xor" -) - -// Note that OSS standbys will not accept seal requests. And ent perf standbys -// may fail it as well if they haven't yet been able to get "elected" as perf standbys. -func SealNode(ctx context.Context, cluster VaultCluster, nodeIdx int) error { - if nodeIdx >= len(cluster.Nodes()) { - return fmt.Errorf("invalid nodeIdx %d for cluster", nodeIdx) - } - node := cluster.Nodes()[nodeIdx] - client := node.APIClient() - - err := client.Sys().SealWithContext(ctx) - if err != nil { - return err - } - - return NodeSealed(ctx, cluster, nodeIdx) -} - -func SealAllNodes(ctx context.Context, cluster VaultCluster) error { - for i := range cluster.Nodes() { - if err := SealNode(ctx, cluster, i); err != nil { - return err - } - } - return nil -} - -func UnsealNode(ctx context.Context, cluster VaultCluster, nodeIdx int) error { - if nodeIdx >= len(cluster.Nodes()) { - return fmt.Errorf("invalid nodeIdx %d for cluster", nodeIdx) - } - node := cluster.Nodes()[nodeIdx] - client := node.APIClient() - - for _, key := range cluster.GetBarrierOrRecoveryKeys() { - _, err := client.Sys().UnsealWithContext(ctx, hex.EncodeToString(key)) - if err != nil { - return err - } - } - - return NodeHealthy(ctx, cluster, nodeIdx) -} - -func UnsealAllNodes(ctx context.Context, cluster VaultCluster) error { - for i := range cluster.Nodes() { - if err := UnsealNode(ctx, cluster, i); err != nil { - return err - } - } - return nil -} - -func NodeSealed(ctx context.Context, cluster VaultCluster, nodeIdx int) error { - if nodeIdx >= len(cluster.Nodes()) { - return fmt.Errorf("invalid nodeIdx %d for cluster", nodeIdx) - } - node := cluster.Nodes()[nodeIdx] - client := node.APIClient() - - var health *api.HealthResponse - var err error - for ctx.Err() == nil { - health, err = client.Sys().HealthWithContext(ctx) - switch { - case err != nil: - case !health.Sealed: - err = fmt.Errorf("unsealed: %#v", health) - default: - return nil - } - time.Sleep(500 * time.Millisecond) - } - return fmt.Errorf("node %d is not sealed: %v", nodeIdx, err) -} - -func WaitForNCoresSealed(ctx context.Context, cluster VaultCluster, n int) error { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - errs := make(chan error) - for i := range cluster.Nodes() { - go func(i int) { - var err error - for ctx.Err() == nil { - err = NodeSealed(ctx, cluster, i) - if err == nil { - errs <- nil - return - } - time.Sleep(100 * time.Millisecond) - } - if err == nil { - err = ctx.Err() - } - errs <- err - }(i) - } - - var merr *multierror.Error - var sealed int - for range cluster.Nodes() { - err := <-errs - if err != nil { - merr = multierror.Append(merr, err) - } else { - sealed++ - if sealed == n { - return nil - } - } - } - - return fmt.Errorf("%d cores were not sealed, errs: %v", n, merr.ErrorOrNil()) -} - -func NodeHealthy(ctx context.Context, cluster VaultCluster, nodeIdx int) error { - if nodeIdx >= len(cluster.Nodes()) { - return fmt.Errorf("invalid nodeIdx %d for cluster", nodeIdx) - } - node := cluster.Nodes()[nodeIdx] - client := node.APIClient() - - var health *api.HealthResponse - var err error - for ctx.Err() == nil { - health, err = client.Sys().HealthWithContext(ctx) - switch { - case err != nil: - case health == nil: - err = fmt.Errorf("nil response to health check") - case health.Sealed: - err = fmt.Errorf("sealed: %#v", health) - default: - return nil - } - time.Sleep(500 * time.Millisecond) - } - return fmt.Errorf("node %d is unhealthy: %v", nodeIdx, err) -} - -func LeaderNode(ctx context.Context, cluster VaultCluster) (int, error) { - // Be robust to multiple nodes thinking they are active. This is possible in - // certain network partition situations where the old leader has not - // discovered it's lost leadership yet. In tests this is only likely to come - // up when we are specifically provoking it, but it's possible it could happen - // at any point if leadership flaps of connectivity suffers transient errors - // etc. so be robust against it. The best solution would be to have some sort - // of epoch like the raft term that is guaranteed to be monotonically - // increasing through elections, however we don't have that abstraction for - // all HABackends in general. The best we have is the ActiveTime. In a - // distributed systems text book this would be bad to rely on due to clock - // sync issues etc. but for our tests it's likely fine because even if we are - // running separate Vault containers, they are all using the same hardware - // clock in the system. - leaderActiveTimes := make(map[int]time.Time) - for i, node := range cluster.Nodes() { - client := node.APIClient() - ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) - resp, err := client.Sys().LeaderWithContext(ctx) - cancel() - if err != nil || resp == nil || !resp.IsSelf { - continue - } - leaderActiveTimes[i] = resp.ActiveTime - } - if len(leaderActiveTimes) == 0 { - return -1, fmt.Errorf("no leader found") - } - // At least one node thinks it is active. If multiple, pick the one with the - // most recent ActiveTime. Note if there is only one then this just returns - // it. - var newestLeaderIdx int - var newestActiveTime time.Time - for i, at := range leaderActiveTimes { - if at.After(newestActiveTime) { - newestActiveTime = at - newestLeaderIdx = i - } - } - return newestLeaderIdx, nil -} - -func WaitForActiveNode(ctx context.Context, cluster VaultCluster) (int, error) { - for ctx.Err() == nil { - if idx, _ := LeaderNode(ctx, cluster); idx != -1 { - return idx, nil - } - time.Sleep(500 * time.Millisecond) - } - return -1, ctx.Err() -} - -func WaitForActiveNodeAndPerfStandbys(ctx context.Context, cluster VaultCluster) error { - logger := cluster.NamedLogger("WaitForActiveNodeAndPerfStandbys") - // This WaitForActiveNode was added because after a Raft cluster is sealed - // and then unsealed, when it comes up it may have a different leader than - // Core0, making this helper fail. - // A sleep before calling WaitForActiveNodeAndPerfStandbys seems to sort - // things out, but so apparently does this. We should be able to eliminate - // this call to WaitForActiveNode by reworking the logic in this method. - leaderIdx, err := WaitForActiveNode(ctx, cluster) - if err != nil { - return err - } - - if len(cluster.Nodes()) == 1 { - return nil - } - - expectedStandbys := len(cluster.Nodes()) - 1 - - mountPoint, err := uuid.GenerateUUID() - if err != nil { - return err - } - leaderClient := cluster.Nodes()[leaderIdx].APIClient() - - for ctx.Err() == nil { - err = leaderClient.Sys().MountWithContext(ctx, mountPoint, &api.MountInput{ - Type: "kv", - Local: true, - }) - if err == nil { - break - } - time.Sleep(1 * time.Second) - } - if err != nil { - return fmt.Errorf("unable to mount KV engine: %v", err) - } - path := mountPoint + "/waitforactivenodeandperfstandbys" - var standbys, actives int64 - errchan := make(chan error, len(cluster.Nodes())) - for i := range cluster.Nodes() { - go func(coreNo int) { - node := cluster.Nodes()[coreNo] - client := node.APIClient() - val := 1 - var err error - defer func() { - errchan <- err - }() - - var lastWAL uint64 - for ctx.Err() == nil { - _, err = leaderClient.Logical().WriteWithContext(ctx, path, map[string]interface{}{ - "bar": val, - }) - val++ - time.Sleep(250 * time.Millisecond) - if err != nil { - continue - } - var leader *api.LeaderResponse - leader, err = client.Sys().LeaderWithContext(ctx) - if err != nil { - logger.Trace("waiting for core", "core", coreNo, "err", err) - continue - } - switch { - case leader.IsSelf: - logger.Trace("waiting for core", "core", coreNo, "isLeader", true) - atomic.AddInt64(&actives, 1) - return - case leader.PerfStandby && leader.PerfStandbyLastRemoteWAL > 0: - switch { - case lastWAL == 0: - lastWAL = leader.PerfStandbyLastRemoteWAL - logger.Trace("waiting for core", "core", coreNo, "lastRemoteWAL", leader.PerfStandbyLastRemoteWAL, "lastWAL", lastWAL) - case lastWAL < leader.PerfStandbyLastRemoteWAL: - logger.Trace("waiting for core", "core", coreNo, "lastRemoteWAL", leader.PerfStandbyLastRemoteWAL, "lastWAL", lastWAL) - atomic.AddInt64(&standbys, 1) - return - } - default: - logger.Trace("waiting for core", "core", coreNo, - "ha_enabled", leader.HAEnabled, - "is_self", leader.IsSelf, - "perf_standby", leader.PerfStandby, - "perf_standby_remote_wal", leader.PerfStandbyLastRemoteWAL) - } - } - }(i) - } - - errs := make([]error, 0, len(cluster.Nodes())) - for range cluster.Nodes() { - errs = append(errs, <-errchan) - } - if actives != 1 || int(standbys) != expectedStandbys { - return fmt.Errorf("expected 1 active core and %d standbys, got %d active and %d standbys, errs: %v", - expectedStandbys, actives, standbys, errs) - } - - for ctx.Err() == nil { - err = leaderClient.Sys().UnmountWithContext(ctx, mountPoint) - if err == nil { - break - } - time.Sleep(time.Second) - } - if err != nil { - return fmt.Errorf("unable to unmount KV engine on primary") - } - return nil -} - -type GenerateRootKind int - -const ( - GenerateRootRegular GenerateRootKind = iota - GenerateRootDR - GenerateRecovery -) - -func GenerateRoot(cluster VaultCluster, kind GenerateRootKind) (string, error) { - // If recovery keys supported, use those to perform root token generation instead - keys := cluster.GetBarrierOrRecoveryKeys() - - client := cluster.Nodes()[0].APIClient() - - var err error - var status *api.GenerateRootStatusResponse - switch kind { - case GenerateRootRegular: - status, err = client.Sys().GenerateRootInit("", "") - case GenerateRootDR: - status, err = client.Sys().GenerateDROperationTokenInit("", "") - case GenerateRecovery: - status, err = client.Sys().GenerateRecoveryOperationTokenInit("", "") - } - if err != nil { - return "", err - } - - if status.Required > len(keys) { - return "", fmt.Errorf("need more keys than have, need %d have %d", status.Required, len(keys)) - } - - otp := status.OTP - - for i, key := range keys { - if i >= status.Required { - break - } - - strKey := base64.StdEncoding.EncodeToString(key) - switch kind { - case GenerateRootRegular: - status, err = client.Sys().GenerateRootUpdate(strKey, status.Nonce) - case GenerateRootDR: - status, err = client.Sys().GenerateDROperationTokenUpdate(strKey, status.Nonce) - case GenerateRecovery: - status, err = client.Sys().GenerateRecoveryOperationTokenUpdate(strKey, status.Nonce) - } - if err != nil { - return "", err - } - } - if !status.Complete { - return "", fmt.Errorf("generate root operation did not end successfully") - } - - tokenBytes, err := base64.RawStdEncoding.DecodeString(status.EncodedToken) - if err != nil { - return "", err - } - tokenBytes, err = xor.XORBytes(tokenBytes, []byte(otp)) - if err != nil { - return "", err - } - return string(tokenBytes), nil -} diff --git a/sdk/helper/testhelpers/output.go b/sdk/helper/testhelpers/output.go deleted file mode 100644 index c18b1bb6d..000000000 --- a/sdk/helper/testhelpers/output.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package testhelpers - -import ( - "crypto/sha256" - "fmt" - "reflect" - - "github.com/mitchellh/go-testing-interface" - "github.com/mitchellh/mapstructure" -) - -// ToMap renders an input value of any type as a map. This is intended for -// logging human-readable data dumps in test logs, so it uses the `json` -// tags on struct fields: this makes it easy to exclude `"-"` values that -// are typically not interesting, respect omitempty, etc. -// -// We also replace any []byte fields with a hash of their value. -// This is usually sufficient for test log purposes, and is a lot more readable -// than a big array of individual byte values like Go would normally stringify a -// byte slice. -func ToMap(in any) (map[string]any, error) { - temp := make(map[string]any) - cfg := &mapstructure.DecoderConfig{ - TagName: "json", - IgnoreUntaggedFields: true, - Result: &temp, - } - md, err := mapstructure.NewDecoder(cfg) - if err != nil { - return nil, err - } - err = md.Decode(in) - if err != nil { - return nil, err - } - - // mapstructure doesn't call the DecodeHook for each field when doing - // struct->map conversions, but it does for map->map, so call it a second - // time to convert each []byte field. - out := make(map[string]any) - md2, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - Result: &out, - DecodeHook: func(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) { - if from.Kind() != reflect.Slice || from.Elem().Kind() != reflect.Uint8 { - return data, nil - } - b := data.([]byte) - return fmt.Sprintf("%x", sha256.Sum256(b)), nil - }, - }) - if err != nil { - return nil, err - } - err = md2.Decode(temp) - if err != nil { - return nil, err - } - - return out, nil -} - -// ToString renders its input using ToMap, and returns a string containing the -// result or an error if that fails. -func ToString(in any) string { - m, err := ToMap(in) - if err != nil { - return err.Error() - } - return fmt.Sprintf("%v", m) -} - -// StringOrDie renders its input using ToMap, and returns a string containing the -// result. If rendering yields an error, calls t.Fatal. -func StringOrDie(t testing.T, in any) string { - t.Helper() - m, err := ToMap(in) - if err != nil { - t.Fatal(err) - } - return fmt.Sprintf("%v", m) -} diff --git a/sdk/helper/testhelpers/output_test.go b/sdk/helper/testhelpers/output_test.go deleted file mode 100644 index ada51a1fe..000000000 --- a/sdk/helper/testhelpers/output_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package testhelpers - -import ( - "fmt" - "reflect" - "testing" -) - -func TestToMap(t *testing.T) { - type s struct { - A string `json:"a"` - B []byte `json:"b"` - C map[string]string `json:"c"` - D string `json:"-"` - } - type args struct { - in s - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "basic", - args: args{s{A: "a", B: []byte("bytes"), C: map[string]string{"k": "v"}, D: "d"}}, - want: "map[a:a b:277089d91c0bdf4f2e6862ba7e4a07605119431f5d13f726dd352b06f1b206a9 c:map[k:v]]", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m, err := ToMap(&tt.args.in) - if (err != nil) != tt.wantErr { - t.Errorf("ToMap() error = %v, wantErr %v", err, tt.wantErr) - return - } - got := fmt.Sprintf("%s", m) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ToMap() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/sdk/helper/testhelpers/schema/response_validation.go b/sdk/helper/testhelpers/schema/response_validation.go deleted file mode 100644 index 430d1754a..000000000 --- a/sdk/helper/testhelpers/schema/response_validation.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package schema - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - "testing" - - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -// ValidateResponse is a test helper that validates whether the given response -// object conforms to the response schema (schema.Fields). It cycles through -// the data map and validates conversions in the schema. In "strict" mode, this -// function will also ensure that the data map has all schema-required fields -// and does not have any fields outside of the schema. -func ValidateResponse(t *testing.T, schema *framework.Response, response *logical.Response, strict bool) { - t.Helper() - - if response != nil { - ValidateResponseData(t, schema, response.Data, strict) - } else { - ValidateResponseData(t, schema, nil, strict) - } -} - -// ValidateResponseData is a test helper that validates whether the given -// response data map conforms to the response schema (schema.Fields). It cycles -// through the data map and validates conversions in the schema. In "strict" -// mode, this function will also ensure that the data map has all schema's -// requred fields and does not have any fields outside of the schema. -func ValidateResponseData(t *testing.T, schema *framework.Response, data map[string]interface{}, strict bool) { - t.Helper() - - if err := validateResponseDataImpl( - schema, - data, - strict, - ); err != nil { - t.Fatalf("validation error: %v; response data: %#v", err, data) - } -} - -// validateResponseDataImpl is extracted so that it can be tested -func validateResponseDataImpl(schema *framework.Response, data map[string]interface{}, strict bool) error { - // nothing to validate - if schema == nil { - return nil - } - - // Certain responses may come through with non-2xx status codes. While - // these are not always errors (e.g. 3xx redirection codes), we don't - // consider them for the purposes of schema validation - if status, exists := data[logical.HTTPStatusCode]; exists { - s, ok := status.(int) - if ok && (s < 200 || s > 299) { - return nil - } - } - - // Marshal the data to JSON and back to convert the map's values into - // JSON strings expected by Validate() and ValidateStrict(). This is - // not efficient and is done for testing purposes only. - jsonBytes, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("failed to convert input to json: %w", err) - } - - var dataWithStringValues map[string]interface{} - if err := json.Unmarshal( - jsonBytes, - &dataWithStringValues, - ); err != nil { - return fmt.Errorf("failed to unmashal data: %w", err) - } - - // these are special fields that will not show up in the final response and - // should be ignored - for _, field := range []string{ - logical.HTTPContentType, - logical.HTTPRawBody, - logical.HTTPStatusCode, - logical.HTTPRawBodyAlreadyJSONDecoded, - logical.HTTPCacheControlHeader, - logical.HTTPPragmaHeader, - logical.HTTPWWWAuthenticateHeader, - } { - delete(dataWithStringValues, field) - - if _, ok := schema.Fields[field]; ok { - return fmt.Errorf("encountered a reserved field in response schema: %s", field) - } - } - - // Validate - fd := framework.FieldData{ - Raw: dataWithStringValues, - Schema: schema.Fields, - } - - if strict { - return fd.ValidateStrict() - } - - return fd.Validate() -} - -// FindResponseSchema is a test helper to extract response schema from the -// given framework path / operation. -func FindResponseSchema(t *testing.T, paths []*framework.Path, pathIdx int, operation logical.Operation) *framework.Response { - t.Helper() - - if pathIdx >= len(paths) { - t.Fatalf("path index %d is out of range", pathIdx) - } - - schemaPath := paths[pathIdx] - - return GetResponseSchema(t, schemaPath, operation) -} - -func GetResponseSchema(t *testing.T, path *framework.Path, operation logical.Operation) *framework.Response { - t.Helper() - - schemaOperation, ok := path.Operations[operation] - if !ok { - t.Fatalf( - "could not find response schema: %s: %q operation does not exist", - path.Pattern, - operation, - ) - } - - var schemaResponses []framework.Response - - for _, status := range []int{ - http.StatusOK, // 200 - http.StatusAccepted, // 202 - http.StatusNoContent, // 204 - } { - schemaResponses, ok = schemaOperation.Properties().Responses[status] - if ok { - break - } - } - - if len(schemaResponses) == 0 { - t.Fatalf( - "could not find response schema: %s: %q operation: no responses found", - path.Pattern, - operation, - ) - } - - return &schemaResponses[0] -} - -// ResponseValidatingCallback can be used in setting up a [vault.TestCluster] -// that validates every response against the openapi specifications. -// -// [vault.TestCluster]: https://pkg.go.dev/github.com/hashicorp/vault/vault#TestCluster -func ResponseValidatingCallback(t *testing.T) func(logical.Backend, *logical.Request, *logical.Response) { - type PathRouter interface { - Route(string) *framework.Path - } - - return func(b logical.Backend, req *logical.Request, resp *logical.Response) { - t.Helper() - - if b == nil { - t.Fatalf("non-nil backend required") - } - - backend, ok := b.(PathRouter) - if !ok { - t.Fatalf("could not cast %T to have `Route(string) *framework.Path`", b) - } - - // The full request path includes the backend but when passing to the - // backend, we have to trim the mount point: - // `sys/mounts/secret` -> `mounts/secret` - // `auth/token/create` -> `create` - requestPath := strings.TrimPrefix(req.Path, req.MountPoint) - - route := backend.Route(requestPath) - if route == nil { - t.Fatalf("backend %T could not find a route for %s", b, req.Path) - } - - ValidateResponse( - t, - GetResponseSchema(t, route, req.Operation), - resp, - true, - ) - } -} diff --git a/sdk/helper/testhelpers/schema/response_validation_test.go b/sdk/helper/testhelpers/schema/response_validation_test.go deleted file mode 100644 index 4f4aa8b1c..000000000 --- a/sdk/helper/testhelpers/schema/response_validation_test.go +++ /dev/null @@ -1,359 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package schema - -import ( - "testing" - "time" - - "github.com/hashicorp/vault/sdk/framework" -) - -func TestValidateResponse(t *testing.T) { - cases := map[string]struct { - schema *framework.Response - response map[string]interface{} - strict bool - errorExpected bool - }{ - "nil schema, nil response, strict": { - schema: nil, - response: nil, - strict: true, - errorExpected: false, - }, - - "nil schema, nil response, not strict": { - schema: nil, - response: nil, - strict: false, - errorExpected: false, - }, - - "nil schema, good response, strict": { - schema: nil, - response: map[string]interface{}{ - "foo": "bar", - }, - strict: true, - errorExpected: false, - }, - - "nil schema, good response, not strict": { - schema: nil, - response: map[string]interface{}{ - "foo": "bar", - }, - strict: true, - errorExpected: false, - }, - - "nil schema fields, good response, strict": { - schema: &framework.Response{}, - response: map[string]interface{}{ - "foo": "bar", - }, - strict: true, - errorExpected: false, - }, - - "nil schema fields, good response, not strict": { - schema: &framework.Response{}, - response: map[string]interface{}{ - "foo": "bar", - }, - strict: true, - errorExpected: false, - }, - - "string schema field, string response, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "foo": { - Type: framework.TypeString, - }, - }, - }, - response: map[string]interface{}{ - "foo": "bar", - }, - strict: true, - errorExpected: false, - }, - - "string schema field, string response, not strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "foo": { - Type: framework.TypeString, - }, - }, - }, - response: map[string]interface{}{ - "foo": "bar", - }, - strict: false, - errorExpected: false, - }, - - "string schema not required field, empty response, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "foo": { - Type: framework.TypeString, - Required: false, - }, - }, - }, - response: map[string]interface{}{}, - strict: true, - errorExpected: false, - }, - - "string schema required field, empty response, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "foo": { - Type: framework.TypeString, - Required: true, - }, - }, - }, - response: map[string]interface{}{}, - strict: true, - errorExpected: true, - }, - - "string schema required field, empty response, not strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "foo": { - Type: framework.TypeString, - Required: true, - }, - }, - }, - response: map[string]interface{}{}, - strict: false, - errorExpected: false, - }, - - "string schema required field, nil response, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "foo": { - Type: framework.TypeString, - Required: true, - }, - }, - }, - response: nil, - strict: true, - errorExpected: true, - }, - - "string schema required field, nil response, not strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "foo": { - Type: framework.TypeString, - Required: true, - }, - }, - }, - response: nil, - strict: false, - errorExpected: false, - }, - - "empty schema, string response, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{}, - }, - response: map[string]interface{}{ - "foo": "bar", - }, - strict: true, - errorExpected: true, - }, - - "empty schema, string response, not strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{}, - }, - response: map[string]interface{}{ - "foo": "bar", - }, - strict: false, - errorExpected: false, - }, - - "time schema, string response, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "time": { - Type: framework.TypeTime, - Required: true, - }, - }, - }, - response: map[string]interface{}{ - "time": "2024-12-11T09:08:07Z", - }, - strict: true, - errorExpected: false, - }, - - "time schema, string response, not strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "time": { - Type: framework.TypeTime, - Required: true, - }, - }, - }, - response: map[string]interface{}{ - "time": "2024-12-11T09:08:07Z", - }, - strict: false, - errorExpected: false, - }, - - "time schema, time response, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "time": { - Type: framework.TypeTime, - Required: true, - }, - }, - }, - response: map[string]interface{}{ - "time": time.Date(2024, 12, 11, 9, 8, 7, 0, time.UTC), - }, - strict: true, - errorExpected: false, - }, - - "time schema, time response, not strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "time": { - Type: framework.TypeTime, - Required: true, - }, - }, - }, - response: map[string]interface{}{ - "time": time.Date(2024, 12, 11, 9, 8, 7, 0, time.UTC), - }, - strict: false, - errorExpected: false, - }, - - "empty schema, response has http_raw_body, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{}, - }, - response: map[string]interface{}{ - "http_raw_body": "foo", - }, - strict: true, - errorExpected: false, - }, - - "empty schema, response has http_raw_body, not strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{}, - }, - response: map[string]interface{}{ - "http_raw_body": "foo", - }, - strict: false, - errorExpected: false, - }, - - "string schema field, response has non-200 http_status_code, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "foo": { - Type: framework.TypeString, - }, - }, - }, - response: map[string]interface{}{ - "http_status_code": 304, - }, - strict: true, - errorExpected: false, - }, - - "string schema field, response has non-200 http_status_code, not strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "foo": { - Type: framework.TypeString, - }, - }, - }, - response: map[string]interface{}{ - "http_status_code": 304, - }, - strict: false, - errorExpected: false, - }, - - "schema has http_raw_body, strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "http_raw_body": { - Type: framework.TypeString, - Required: false, - }, - }, - }, - response: map[string]interface{}{ - "http_raw_body": "foo", - }, - strict: true, - errorExpected: true, - }, - - "schema has http_raw_body, not strict": { - schema: &framework.Response{ - Fields: map[string]*framework.FieldSchema{ - "http_raw_body": { - Type: framework.TypeString, - Required: false, - }, - }, - }, - response: map[string]interface{}{ - "http_raw_body": "foo", - }, - strict: false, - errorExpected: true, - }, - } - - for name, tc := range cases { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - err := validateResponseDataImpl( - tc.schema, - tc.response, - tc.strict, - ) - if err == nil && tc.errorExpected == true { - t.Fatalf("expected an error, got nil") - } - if err != nil && tc.errorExpected == false { - t.Fatalf("unexpected error: %v", err) - } - }) - } -} diff --git a/sdk/helper/useragent/useragent_test.go b/sdk/helper/useragent/useragent_test.go deleted file mode 100644 index 4677bb62f..000000000 --- a/sdk/helper/useragent/useragent_test.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package useragent - -import ( - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestUserAgent(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - - type args struct { - comments []string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "User agent", - args: args{}, - want: "Vault (+https://vault-test.com; go5.0)", - }, - { - name: "User agent with additional comment", - args: args{ - comments: []string{"pid-abcdefg"}, - }, - want: "Vault (+https://vault-test.com; go5.0; pid-abcdefg)", - }, - { - name: "User agent with additional comments", - args: args{ - comments: []string{"pid-abcdefg", "cloud-provider"}, - }, - want: "Vault (+https://vault-test.com; go5.0; pid-abcdefg; cloud-provider)", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := String(tt.args.comments...); got != tt.want { - t.Errorf("String() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestUserAgentPlugin(t *testing.T) { - projectURL = "https://vault-test.com" - rt = "go5.0" - - type args struct { - pluginName string - pluginEnv *logical.PluginEnvironment - comments []string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "Plugin user agent with nil plugin env", - args: args{ - pluginEnv: nil, - }, - want: "", - }, - { - name: "Plugin user agent without plugin name", - args: args{ - pluginEnv: &logical.PluginEnvironment{ - VaultVersion: "1.2.3", - }, - }, - want: "Vault/1.2.3 (+https://vault-test.com; go5.0)", - }, - { - name: "Plugin user agent without plugin name", - args: args{ - pluginEnv: &logical.PluginEnvironment{ - VaultVersion: "1.2.3", - }, - }, - want: "Vault/1.2.3 (+https://vault-test.com; go5.0)", - }, - { - name: "Plugin user agent with plugin name", - args: args{ - pluginName: "azure-auth", - pluginEnv: &logical.PluginEnvironment{ - VaultVersion: "1.2.3", - }, - }, - want: "Vault/1.2.3 (+https://vault-test.com; azure-auth; go5.0)", - }, - { - name: "Plugin user agent with plugin name and additional comment", - args: args{ - pluginName: "azure-auth", - pluginEnv: &logical.PluginEnvironment{ - VaultVersion: "1.2.3", - }, - comments: []string{"pid-abcdefg"}, - }, - want: "Vault/1.2.3 (+https://vault-test.com; azure-auth; go5.0; pid-abcdefg)", - }, - { - name: "Plugin user agent with plugin name and additional comments", - args: args{ - pluginName: "azure-auth", - pluginEnv: &logical.PluginEnvironment{ - VaultVersion: "1.2.3", - }, - comments: []string{"pid-abcdefg", "cloud-provider"}, - }, - want: "Vault/1.2.3 (+https://vault-test.com; azure-auth; go5.0; pid-abcdefg; cloud-provider)", - }, - { - name: "Plugin user agent with no plugin name and additional comments", - args: args{ - pluginEnv: &logical.PluginEnvironment{ - VaultVersion: "1.2.3", - }, - comments: []string{"pid-abcdefg", "cloud-provider"}, - }, - want: "Vault/1.2.3 (+https://vault-test.com; go5.0; pid-abcdefg; cloud-provider)", - }, - { - name: "Plugin user agent with version prerelease", - args: args{ - pluginName: "azure-auth", - pluginEnv: &logical.PluginEnvironment{ - VaultVersion: "1.2.3", - VaultVersionPrerelease: "dev", - }, - comments: []string{"pid-abcdefg", "cloud-provider"}, - }, - want: "Vault/1.2.3-dev (+https://vault-test.com; azure-auth; go5.0; pid-abcdefg; cloud-provider)", - }, - { - name: "Plugin user agent with version metadata", - args: args{ - pluginName: "azure-auth", - pluginEnv: &logical.PluginEnvironment{ - VaultVersion: "1.2.3", - VaultVersionMetadata: "ent", - }, - comments: []string{"pid-abcdefg", "cloud-provider"}, - }, - want: "Vault/1.2.3+ent (+https://vault-test.com; azure-auth; go5.0; pid-abcdefg; cloud-provider)", - }, - { - name: "Plugin user agent with version prerelease and metadata", - args: args{ - pluginName: "azure-auth", - pluginEnv: &logical.PluginEnvironment{ - VaultVersion: "1.2.3", - VaultVersionPrerelease: "dev", - VaultVersionMetadata: "ent", - }, - comments: []string{"pid-abcdefg", "cloud-provider"}, - }, - want: "Vault/1.2.3-dev+ent (+https://vault-test.com; azure-auth; go5.0; pid-abcdefg; cloud-provider)", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := PluginString(tt.args.pluginEnv, tt.args.pluginName, tt.args.comments...); got != tt.want { - t.Errorf("PluginString() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/sdk/helper/xor/xor_test.go b/sdk/helper/xor/xor_test.go deleted file mode 100644 index 143345d9a..000000000 --- a/sdk/helper/xor/xor_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package xor - -import ( - "encoding/base64" - "testing" -) - -const ( - tokenB64 = "ZGE0N2JiODkzYjhkMDYxYw==" - xorB64 = "iGiQYG9L0nIp+jRL5+Zk2w==" - expectedB64 = "7AmkVw0p6ksamAwv19BVuA==" -) - -func TestBase64XOR(t *testing.T) { - ret, err := XORBase64(tokenB64, xorB64) - if err != nil { - t.Fatal(err) - } - if res := base64.StdEncoding.EncodeToString(ret); res != expectedB64 { - t.Fatalf("bad: %s", res) - } -} diff --git a/sdk/logical/lease_test.go b/sdk/logical/lease_test.go deleted file mode 100644 index aee2bbdbc..000000000 --- a/sdk/logical/lease_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logical - -import ( - "testing" - "time" -) - -func TestLeaseOptionsLeaseTotal(t *testing.T) { - var l LeaseOptions - l.TTL = 1 * time.Hour - - actual := l.LeaseTotal() - expected := l.TTL - if actual != expected { - t.Fatalf("bad: %s", actual) - } -} - -func TestLeaseOptionsLeaseTotal_grace(t *testing.T) { - var l LeaseOptions - l.TTL = 1 * time.Hour - - actual := l.LeaseTotal() - if actual != l.TTL { - t.Fatalf("bad: %s", actual) - } -} - -func TestLeaseOptionsLeaseTotal_negLease(t *testing.T) { - var l LeaseOptions - l.TTL = -1 * 1 * time.Hour - - actual := l.LeaseTotal() - expected := time.Duration(0) - if actual != expected { - t.Fatalf("bad: %s", actual) - } -} - -func TestLeaseOptionsExpirationTime(t *testing.T) { - var l LeaseOptions - l.TTL = 1 * time.Hour - - limit := time.Now().Add(time.Hour) - exp := l.ExpirationTime() - if exp.Before(limit) { - t.Fatalf("bad: %s", exp) - } -} - -func TestLeaseOptionsExpirationTime_noLease(t *testing.T) { - var l LeaseOptions - if !l.ExpirationTime().IsZero() { - t.Fatal("should be zero") - } -} diff --git a/sdk/logical/response_util_test.go b/sdk/logical/response_util_test.go deleted file mode 100644 index eafaa2fc7..000000000 --- a/sdk/logical/response_util_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logical - -import ( - "errors" - "strings" - "testing" -) - -func TestResponseUtil_RespondErrorCommon_basic(t *testing.T) { - testCases := []struct { - title string - req *Request - resp *Response - respErr error - expectedStatus int - expectedErr error - }{ - { - title: "Throttled, no error", - respErr: ErrUpstreamRateLimited, - resp: &Response{}, - expectedStatus: 502, - }, - { - title: "Throttled, with error", - respErr: ErrUpstreamRateLimited, - resp: &Response{ - Data: map[string]interface{}{ - "error": "rate limited", - }, - }, - expectedStatus: 502, - }, - { - title: "Read not found", - req: &Request{ - Operation: ReadOperation, - }, - respErr: nil, - expectedStatus: 404, - }, - { - title: "Header not found", - req: &Request{ - Operation: HeaderOperation, - }, - respErr: nil, - expectedStatus: 404, - }, - { - title: "List with response and no keys", - req: &Request{ - Operation: ListOperation, - }, - resp: &Response{}, - respErr: nil, - expectedStatus: 404, - }, - { - title: "List with response and keys", - req: &Request{ - Operation: ListOperation, - }, - resp: &Response{ - Data: map[string]interface{}{ - "keys": []string{"some", "things", "here"}, - }, - }, - respErr: nil, - expectedStatus: 0, - }, - { - title: "Invalid Credentials error ", - respErr: ErrInvalidCredentials, - resp: &Response{ - Data: map[string]interface{}{ - "error": "error due to wrong credentials", - }, - }, - expectedErr: errors.New("error due to wrong credentials"), - expectedStatus: 400, - }, - } - - for _, tc := range testCases { - t.Run(tc.title, func(t *testing.T) { - var status int - var err, respErr error - if tc.respErr != nil { - respErr = tc.respErr - } - status, err = RespondErrorCommon(tc.req, tc.resp, respErr) - if status != tc.expectedStatus { - t.Fatalf("Expected (%d) status code, got (%d)", tc.expectedStatus, status) - } - if tc.expectedErr != nil { - if !strings.Contains(tc.expectedErr.Error(), err.Error()) { - t.Fatalf("Expected error to contain:\n%s\n\ngot:\n%s\n", tc.expectedErr, err) - } - } - }) - } -} diff --git a/sdk/logical/storage_inmem_test.go b/sdk/logical/storage_inmem_test.go deleted file mode 100644 index 2ed776b20..000000000 --- a/sdk/logical/storage_inmem_test.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logical - -import ( - "testing" -) - -func TestInmemStorage(t *testing.T) { - TestStorage(t, new(InmemStorage)) -} diff --git a/sdk/logical/storage_test.go b/sdk/logical/storage_test.go deleted file mode 100644 index 1d6014dd9..000000000 --- a/sdk/logical/storage_test.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logical - -import ( - "context" - "testing" - - "github.com/go-test/deep" -) - -var keyList = []string{ - "a", - "b", - "d", - "foo", - "foo42", - "foo/a/b/c", - "c/d/e/f/g", -} - -func TestScanView(t *testing.T) { - s := prepKeyStorage(t) - - keys := make([]string, 0) - err := ScanView(context.Background(), s, func(path string) { - keys = append(keys, path) - }) - if err != nil { - t.Fatal(err) - } - - if diff := deep.Equal(keys, keyList); diff != nil { - t.Fatal(diff) - } -} - -func TestScanView_CancelContext(t *testing.T) { - s := prepKeyStorage(t) - - ctx, cancelCtx := context.WithCancel(context.Background()) - var i int - err := ScanView(ctx, s, func(path string) { - cancelCtx() - i++ - }) - - if err == nil { - t.Error("Want context cancel err, got none") - } - if i != 1 { - t.Errorf("Want i==1, got %d", i) - } -} - -func TestCollectKeys(t *testing.T) { - s := prepKeyStorage(t) - - keys, err := CollectKeys(context.Background(), s) - if err != nil { - t.Fatal(err) - } - - if diff := deep.Equal(keys, keyList); diff != nil { - t.Fatal(diff) - } -} - -func TestCollectKeysPrefix(t *testing.T) { - s := prepKeyStorage(t) - - keys, err := CollectKeysWithPrefix(context.Background(), s, "foo") - if err != nil { - t.Fatal(err) - } - - exp := []string{ - "foo", - "foo42", - "foo/a/b/c", - } - - if diff := deep.Equal(keys, exp); diff != nil { - t.Fatal(diff) - } -} - -func prepKeyStorage(t *testing.T) Storage { - t.Helper() - s := &InmemStorage{} - - for _, key := range keyList { - if err := s.Put(context.Background(), &StorageEntry{ - Key: key, - Value: nil, - SealWrap: false, - }); err != nil { - t.Fatal(err) - } - } - - return s -} diff --git a/sdk/logical/testing.go b/sdk/logical/testing.go deleted file mode 100644 index a173c7c5f..000000000 --- a/sdk/logical/testing.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logical - -import ( - "context" - "reflect" - "time" - - testing "github.com/mitchellh/go-testing-interface" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/logging" -) - -// TestRequest is a helper to create a purely in-memory Request struct. -func TestRequest(t testing.T, op Operation, path string) *Request { - return &Request{ - Operation: op, - Path: path, - Data: make(map[string]interface{}), - Storage: new(InmemStorage), - Connection: &Connection{}, - } -} - -// TestStorage is a helper that can be used from unit tests to verify -// the behavior of a Storage impl. -func TestStorage(t testing.T, s Storage) { - keys, err := s.List(context.Background(), "") - if err != nil { - t.Fatalf("list error: %s", err) - } - if len(keys) > 0 { - t.Fatalf("should have no keys to start: %#v", keys) - } - - entry := &StorageEntry{Key: "foo", Value: []byte("bar")} - if err := s.Put(context.Background(), entry); err != nil { - t.Fatalf("put error: %s", err) - } - - actual, err := s.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("get error: %s", err) - } - if !reflect.DeepEqual(actual, entry) { - t.Fatalf("wrong value. Expected: %#v\nGot: %#v", entry, actual) - } - - keys, err = s.List(context.Background(), "") - if err != nil { - t.Fatalf("list error: %s", err) - } - if !reflect.DeepEqual(keys, []string{"foo"}) { - t.Fatalf("bad keys: %#v", keys) - } - - if err := s.Delete(context.Background(), "foo"); err != nil { - t.Fatalf("put error: %s", err) - } - - keys, err = s.List(context.Background(), "") - if err != nil { - t.Fatalf("list error: %s", err) - } - if len(keys) > 0 { - t.Fatalf("should have no keys to start: %#v", keys) - } -} - -func TestSystemView() *StaticSystemView { - defaultLeaseTTLVal := time.Hour * 24 - maxLeaseTTLVal := time.Hour * 24 * 2 - return &StaticSystemView{ - DefaultLeaseTTLVal: defaultLeaseTTLVal, - MaxLeaseTTLVal: maxLeaseTTLVal, - VersionString: "testVersionString", - } -} - -func TestBackendConfig() *BackendConfig { - bc := &BackendConfig{ - Logger: logging.NewVaultLogger(log.Trace), - System: TestSystemView(), - Config: make(map[string]string), - } - - return bc -} diff --git a/sdk/logical/token_test.go b/sdk/logical/token_test.go deleted file mode 100644 index 641d688b9..000000000 --- a/sdk/logical/token_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logical - -import ( - "crypto/sha256" - "encoding/base64" - "encoding/json" - "testing" -) - -func TestJSONSerialization(t *testing.T) { - tt := TokenTypeDefaultBatch - s, err := json.Marshal(tt) - if err != nil { - t.Fatal(err) - } - - var utt TokenType - err = json.Unmarshal(s, &utt) - if err != nil { - t.Fatal(err) - } - - if tt != utt { - t.Fatalf("expected %v, got %v", tt, utt) - } - - utt = TokenTypeDefault - err = json.Unmarshal([]byte(`"default-batch"`), &utt) - if err != nil { - t.Fatal(err) - } - if tt != utt { - t.Fatalf("expected %v, got %v", tt, utt) - } - - // Test on an empty value, which should unmarshal into TokenTypeDefault - tt = TokenTypeDefault - err = json.Unmarshal([]byte(`""`), &utt) - if err != nil { - t.Fatal(err) - } - if tt != utt { - t.Fatalf("expected %v, got %v", tt, utt) - } -} - -// TestCreateClientID verifies that CreateClientID uses the entity ID for a token -// entry if one exists, and creates an appropriate client ID otherwise. -func TestCreateClientID(t *testing.T) { - entry := TokenEntry{NamespaceID: "namespaceFoo", Policies: []string{"bar", "baz", "foo", "banana"}} - id, isTWE := entry.CreateClientID() - if !isTWE { - t.Fatalf("TWE token should return true value in isTWE bool") - } - expectedIDPlaintext := "banana" + string(SortedPoliciesTWEDelimiter) + "bar" + - string(SortedPoliciesTWEDelimiter) + "baz" + - string(SortedPoliciesTWEDelimiter) + "foo" + string(ClientIDTWEDelimiter) + "namespaceFoo" - - hashed := sha256.Sum256([]byte(expectedIDPlaintext)) - expectedID := base64.StdEncoding.EncodeToString(hashed[:]) - if expectedID != id { - t.Fatalf("wrong ID: expected %s, found %s", expectedID, id) - } - // Test with entityID - entry = TokenEntry{EntityID: "entityFoo", NamespaceID: "namespaceFoo", Policies: []string{"bar", "baz", "foo", "banana"}} - id, isTWE = entry.CreateClientID() - if isTWE { - t.Fatalf("token with entity should return false value in isTWE bool") - } - if id != "entityFoo" { - t.Fatalf("client ID should be entity ID") - } - - // Test without namespace - entry = TokenEntry{Policies: []string{"bar", "baz", "foo", "banana"}} - id, isTWE = entry.CreateClientID() - if !isTWE { - t.Fatalf("TWE token should return true value in isTWE bool") - } - expectedIDPlaintext = "banana" + string(SortedPoliciesTWEDelimiter) + "bar" + - string(SortedPoliciesTWEDelimiter) + "baz" + - string(SortedPoliciesTWEDelimiter) + "foo" + string(ClientIDTWEDelimiter) - - hashed = sha256.Sum256([]byte(expectedIDPlaintext)) - expectedID = base64.StdEncoding.EncodeToString(hashed[:]) - if expectedID != id { - t.Fatalf("wrong ID: expected %s, found %s", expectedID, id) - } - - // Test without policies - entry = TokenEntry{NamespaceID: "namespaceFoo"} - id, isTWE = entry.CreateClientID() - if !isTWE { - t.Fatalf("TWE token should return true value in isTWE bool") - } - expectedIDPlaintext = "namespaceFoo" - - hashed = sha256.Sum256([]byte(expectedIDPlaintext)) - expectedID = base64.StdEncoding.EncodeToString(hashed[:]) - if expectedID != id { - t.Fatalf("wrong ID: expected %s, found %s", expectedID, id) - } -} diff --git a/sdk/physical/file/file_test.go b/sdk/physical/file/file_test.go deleted file mode 100644 index 7fc6398c8..000000000 --- a/sdk/physical/file/file_test.go +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package file - -import ( - "context" - "encoding/json" - "io/ioutil" - "os" - "path/filepath" - "reflect" - "testing" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/physical" -) - -func TestFileBackend_Base64URLEncoding(t *testing.T) { - backendPath, err := ioutil.TempDir("", "vault") - if err != nil { - t.Fatalf("err: %s", err) - } - defer os.RemoveAll(backendPath) - - logger := logging.NewVaultLogger(log.Debug) - - b, err := NewFileBackend(map[string]string{ - "path": backendPath, - }, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - - // List the entries. Length should be zero. - keys, err := b.List(context.Background(), "") - if err != nil { - t.Fatalf("err: %v", err) - } - if len(keys) != 0 { - t.Fatalf("bad: len(keys): expected: 0, actual: %d", len(keys)) - } - - // Create a storage entry without base64 encoding the file name - rawFullPath := filepath.Join(backendPath, "_foo") - e := &physical.Entry{Key: "foo", Value: []byte("test")} - f, err := os.OpenFile( - rawFullPath, - os.O_CREATE|os.O_TRUNC|os.O_WRONLY, - 0o600) - if err != nil { - t.Fatal(err) - } - json.NewEncoder(f).Encode(e) - f.Close() - - // Get should work - out, err := b.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if !reflect.DeepEqual(out, e) { - t.Fatalf("bad: %v expected: %v", out, e) - } - - // List the entries. There should be one entry. - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("err: %v", err) - } - if len(keys) != 1 { - t.Fatalf("bad: len(keys): expected: 1, actual: %d", len(keys)) - } - - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("err: %v", err) - } - - // List the entries again. There should still be one entry. - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("err: %v", err) - } - if len(keys) != 1 { - t.Fatalf("bad: len(keys): expected: 1, actual: %d", len(keys)) - } - - // Get should work - out, err = b.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if !reflect.DeepEqual(out, e) { - t.Fatalf("bad: %v expected: %v", out, e) - } - - err = b.Delete(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - - out, err = b.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out != nil { - t.Fatalf("bad: entry: expected: nil, actual: %#v", e) - } - - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("err: %v", err) - } - if len(keys) != 0 { - t.Fatalf("bad: len(keys): expected: 0, actual: %d", len(keys)) - } - - f, err = os.OpenFile( - rawFullPath, - os.O_CREATE|os.O_TRUNC|os.O_WRONLY, - 0o600) - if err != nil { - t.Fatal(err) - } - json.NewEncoder(f).Encode(e) - f.Close() - - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("err: %v", err) - } - if len(keys) != 1 { - t.Fatalf("bad: len(keys): expected: 1, actual: %d", len(keys)) - } -} - -func TestFileBackend_ValidatePath(t *testing.T) { - dir, err := ioutil.TempDir("", "vault") - if err != nil { - t.Fatalf("err: %s", err) - } - defer os.RemoveAll(dir) - - logger := logging.NewVaultLogger(log.Debug) - - b, err := NewFileBackend(map[string]string{ - "path": dir, - }, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - - if err := b.Delete(context.Background(), "foo/bar/../zip"); err == nil { - t.Fatal("expected error") - } - if err := b.Delete(context.Background(), "foo/bar/zip"); err != nil { - t.Fatal("did not expect error") - } -} - -func TestFileBackend(t *testing.T) { - dir, err := ioutil.TempDir("", "vault") - if err != nil { - t.Fatalf("err: %s", err) - } - defer os.RemoveAll(dir) - - logger := logging.NewVaultLogger(log.Debug) - - b, err := NewFileBackend(map[string]string{ - "path": dir, - }, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - - physical.ExerciseBackend(t, b) - - // Underscores should not trip things up; ref GH-3476 - e := &physical.Entry{Key: "_zip", Value: []byte("foobar")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("err: %v", err) - } - e = &physical.Entry{Key: "_zip/_zap", Value: []byte("boofar")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("err: %v", err) - } - e, err = b.Get(context.Background(), "_zip/_zap") - if err != nil { - t.Fatalf("err: %v", err) - } - if e == nil { - t.Fatal("got nil entry") - } - vals, err := b.List(context.Background(), "") - if err != nil { - t.Fatal(err) - } - if len(vals) != 2 || vals[0] == vals[1] { - t.Fatalf("bad: %v", vals) - } - for _, val := range vals { - if val != "_zip/" && val != "_zip" { - t.Fatalf("bad val: %v", val) - } - } - vals, err = b.List(context.Background(), "_zip/") - if err != nil { - t.Fatal(err) - } - if len(vals) != 1 || vals[0] != "_zap" { - t.Fatalf("bad: %v", vals) - } - err = b.Delete(context.Background(), "_zip/_zap") - if err != nil { - t.Fatal(err) - } - vals, err = b.List(context.Background(), "") - if err != nil { - t.Fatal(err) - } - if len(vals) != 1 || vals[0] != "_zip" { - t.Fatalf("bad: %v", vals) - } - err = b.Delete(context.Background(), "_zip") - if err != nil { - t.Fatal(err) - } - vals, err = b.List(context.Background(), "") - if err != nil { - t.Fatal(err) - } - if len(vals) != 0 { - t.Fatalf("bad: %v", vals) - } - - physical.ExerciseBackend_ListPrefix(t, b) -} - -func TestFileBackendCreateTempKey(t *testing.T) { - dir := t.TempDir() - - logger := logging.NewVaultLogger(log.Debug) - - b, err := NewFileBackend(map[string]string{ - "path": dir, - }, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - temp := &physical.Entry{Key: "example.temp", Value: []byte("tempfoo")} - err = b.Put(context.Background(), temp) - if err != nil { - t.Fatalf("err: %v", err) - } - - nonTemp := &physical.Entry{Key: "example", Value: []byte("foobar")} - err = b.Put(context.Background(), nonTemp) - if err != nil { - t.Fatalf("err: %v", err) - } - - vals, err := b.List(context.Background(), "") - if err != nil { - t.Fatal(err) - } - if len(vals) != 2 || vals[0] == vals[1] { - t.Fatalf("bad: %v", vals) - } - for _, val := range vals { - if val != "example.temp" && val != "example" { - t.Fatalf("bad val: %v", val) - } - } - out, err := b.Get(context.Background(), "example") - if err != nil { - t.Fatalf("err: %v", err) - } - if !reflect.DeepEqual(out, nonTemp) { - t.Fatalf("bad: %v expected: %v", out, nonTemp) - } - out, err = b.Get(context.Background(), "example.temp") - if err != nil { - t.Fatalf("err: %v", err) - } - if !reflect.DeepEqual(out, temp) { - t.Fatalf("bad: %v expected: %v", out, temp) - } -} diff --git a/sdk/physical/inmem/cache_test.go b/sdk/physical/inmem/cache_test.go deleted file mode 100644 index 3014fc176..000000000 --- a/sdk/physical/inmem/cache_test.go +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package inmem - -import ( - "context" - "testing" - - "github.com/armon/go-metrics" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/physical" -) - -func TestCache(t *testing.T) { - logger := logging.NewVaultLogger(log.Debug) - - inm, err := NewInmem(nil, logger) - if err != nil { - t.Fatal(err) - } - - cache := physical.NewCache(inm, 0, logger, &metrics.BlackholeSink{}) - cache.SetEnabled(true) - physical.ExerciseBackend(t, cache) - physical.ExerciseBackend_ListPrefix(t, cache) -} - -func TestCache_Purge(t *testing.T) { - logger := logging.NewVaultLogger(log.Debug) - - inm, err := NewInmem(nil, logger) - if err != nil { - t.Fatal(err) - } - cache := physical.NewCache(inm, 0, logger, &metrics.BlackholeSink{}) - cache.SetEnabled(true) - - ent := &physical.Entry{ - Key: "foo", - Value: []byte("bar"), - } - err = cache.Put(context.Background(), ent) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Delete from under - inm.Delete(context.Background(), "foo") - if err != nil { - t.Fatal(err) - } - - // Read should work - out, err := cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - - // Clear the cache - cache.Purge(context.Background()) - - // Read should fail - out, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out != nil { - t.Fatalf("should not have key") - } -} - -func TestCache_Disable(t *testing.T) { - logger := logging.NewVaultLogger(log.Debug) - - inm, err := NewInmem(nil, logger) - if err != nil { - t.Fatal(err) - } - cache := physical.NewCache(inm, 0, logger, &metrics.BlackholeSink{}) - - disabledTests := func() { - ent := &physical.Entry{ - Key: "foo", - Value: []byte("bar"), - } - err = inm.Put(context.Background(), ent) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Read should work - out, err := cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - - err = inm.Delete(context.Background(), ent.Key) - if err != nil { - t.Fatal(err) - } - - // Should not work - out, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out != nil { - t.Fatalf("should not have key") - } - - // Put through the cache and try again - err = cache.Put(context.Background(), ent) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Read should work in both - out, err = inm.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - out, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - - err = inm.Delete(context.Background(), ent.Key) - if err != nil { - t.Fatal(err) - } - - // Should not work - out, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out != nil { - t.Fatalf("should not have key") - } - } - - enabledTests := func() { - ent := &physical.Entry{ - Key: "foo", - Value: []byte("bar"), - } - err = inm.Put(context.Background(), ent) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Read should work - out, err := cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - - err = inm.Delete(context.Background(), ent.Key) - if err != nil { - t.Fatal(err) - } - - // Should work - out, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - - // Put through the cache and try again - err = cache.Put(context.Background(), ent) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Read should work for both - out, err = inm.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - out, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - - err = inm.Delete(context.Background(), ent.Key) - if err != nil { - t.Fatal(err) - } - - // Should work - out, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - - // Put through the cache - err = cache.Put(context.Background(), ent) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Read should work for both - out, err = inm.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - out, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("should have key") - } - - // Delete via cache - err = cache.Delete(context.Background(), ent.Key) - if err != nil { - t.Fatal(err) - } - - // Read should not work for either - out, err = inm.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out != nil { - t.Fatalf("should not have key") - } - out, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if out != nil { - t.Fatalf("should not have key") - } - } - - disabledTests() - cache.SetEnabled(true) - enabledTests() - cache.SetEnabled(false) - disabledTests() -} - -func TestCache_Refresh(t *testing.T) { - logger := logging.NewVaultLogger(log.Debug) - - inm, err := NewInmem(nil, logger) - if err != nil { - t.Fatal(err) - } - cache := physical.NewCache(inm, 0, logger, &metrics.BlackholeSink{}) - cache.SetEnabled(true) - - ent := &physical.Entry{ - Key: "foo", - Value: []byte("bar"), - } - err = cache.Put(context.Background(), ent) - if err != nil { - t.Fatalf("err: %v", err) - } - - ent2 := &physical.Entry{ - Key: "foo", - Value: []byte("baz"), - } - // Update below cache - err = inm.Put(context.Background(), ent2) - if err != nil { - t.Fatalf("err: %v", err) - } - - r, err := cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - - if string(r.Value) != "bar" { - t.Fatalf("expected value bar, got %s", string(r.Value)) - } - - // Refresh the cache - r, err = cache.Get(physical.CacheRefreshContext(context.Background(), true), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - - if string(r.Value) != "baz" { - t.Fatalf("expected value baz, got %s", string(r.Value)) - } - - // Make sure new value is in cache - r, err = cache.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if string(r.Value) != "baz" { - t.Fatalf("expected value baz, got %s", string(r.Value)) - } -} diff --git a/sdk/physical/inmem/inmem_ha_test.go b/sdk/physical/inmem/inmem_ha_test.go deleted file mode 100644 index bb427a385..000000000 --- a/sdk/physical/inmem/inmem_ha_test.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package inmem - -import ( - "testing" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/physical" -) - -func TestInmemHA(t *testing.T) { - logger := logging.NewVaultLogger(log.Debug) - - inm, err := NewInmemHA(nil, logger) - if err != nil { - t.Fatal(err) - } - - // Use the same inmem backend to acquire the same set of locks - physical.ExerciseHABackend(t, inm.(physical.HABackend), inm.(physical.HABackend)) -} diff --git a/sdk/physical/inmem/inmem_test.go b/sdk/physical/inmem/inmem_test.go deleted file mode 100644 index 56c029a43..000000000 --- a/sdk/physical/inmem/inmem_test.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package inmem - -import ( - "testing" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/physical" -) - -func TestInmem(t *testing.T) { - logger := logging.NewVaultLogger(log.Debug) - - inm, err := NewInmem(nil, logger) - if err != nil { - t.Fatal(err) - } - physical.ExerciseBackend(t, inm) - physical.ExerciseBackend_ListPrefix(t, inm) -} diff --git a/sdk/physical/inmem/physical_view_test.go b/sdk/physical/inmem/physical_view_test.go deleted file mode 100644 index 24b47d7ae..000000000 --- a/sdk/physical/inmem/physical_view_test.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package inmem - -import ( - "context" - "testing" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/physical" -) - -func TestPhysicalView_impl(t *testing.T) { - var _ physical.Backend = new(physical.View) -} - -func newInmemTestBackend() (physical.Backend, error) { - logger := logging.NewVaultLogger(log.Debug) - return NewInmem(nil, logger) -} - -func TestPhysicalView_BadKeysKeys(t *testing.T) { - backend, err := newInmemTestBackend() - if err != nil { - t.Fatal(err) - } - view := physical.NewView(backend, "foo/") - - _, err = view.List(context.Background(), "../") - if err == nil { - t.Fatalf("expected error") - } - - _, err = view.Get(context.Background(), "../") - if err == nil { - t.Fatalf("expected error") - } - - err = view.Delete(context.Background(), "../foo") - if err == nil { - t.Fatalf("expected error") - } - - le := &physical.Entry{ - Key: "../foo", - Value: []byte("test"), - } - err = view.Put(context.Background(), le) - if err == nil { - t.Fatalf("expected error") - } -} - -func TestPhysicalView(t *testing.T) { - backend, err := newInmemTestBackend() - if err != nil { - t.Fatal(err) - } - - view := physical.NewView(backend, "foo/") - - // Write a key outside of foo/ - entry := &physical.Entry{Key: "test", Value: []byte("test")} - if err := backend.Put(context.Background(), entry); err != nil { - t.Fatalf("bad: %v", err) - } - - // List should have no visibility - keys, err := view.List(context.Background(), "") - if err != nil { - t.Fatalf("err: %v", err) - } - if len(keys) != 0 { - t.Fatalf("bad: %v", err) - } - - // Get should have no visibility - out, err := view.Get(context.Background(), "test") - if err != nil { - t.Fatalf("err: %v", err) - } - if out != nil { - t.Fatalf("bad: %v", out) - } - - // Try to put the same entry via the view - if err := view.Put(context.Background(), entry); err != nil { - t.Fatalf("err: %v", err) - } - - // Check it is nested - entry, err = backend.Get(context.Background(), "foo/test") - if err != nil { - t.Fatalf("err: %v", err) - } - if entry == nil { - t.Fatalf("missing nested foo/test") - } - - // Delete nested - if err := view.Delete(context.Background(), "test"); err != nil { - t.Fatalf("err: %v", err) - } - - // Check the nested key - entry, err = backend.Get(context.Background(), "foo/test") - if err != nil { - t.Fatalf("err: %v", err) - } - if entry != nil { - t.Fatalf("nested foo/test should be gone") - } - - // Check the non-nested key - entry, err = backend.Get(context.Background(), "test") - if err != nil { - t.Fatalf("err: %v", err) - } - if entry == nil { - t.Fatalf("root test missing") - } -} diff --git a/sdk/physical/inmem/transactions_test.go b/sdk/physical/inmem/transactions_test.go deleted file mode 100644 index 71a4829f9..000000000 --- a/sdk/physical/inmem/transactions_test.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package inmem - -import ( - "context" - "fmt" - "reflect" - "sort" - "testing" - - radix "github.com/armon/go-radix" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/physical" -) - -type faultyPseudo struct { - underlying InmemBackend - faultyPaths map[string]struct{} -} - -func (f *faultyPseudo) Get(ctx context.Context, key string) (*physical.Entry, error) { - return f.underlying.Get(context.Background(), key) -} - -func (f *faultyPseudo) Put(ctx context.Context, entry *physical.Entry) error { - return f.underlying.Put(context.Background(), entry) -} - -func (f *faultyPseudo) Delete(ctx context.Context, key string) error { - return f.underlying.Delete(context.Background(), key) -} - -func (f *faultyPseudo) GetInternal(ctx context.Context, key string) (*physical.Entry, error) { - if _, ok := f.faultyPaths[key]; ok { - return nil, fmt.Errorf("fault") - } - return f.underlying.GetInternal(context.Background(), key) -} - -func (f *faultyPseudo) PutInternal(ctx context.Context, entry *physical.Entry) error { - if _, ok := f.faultyPaths[entry.Key]; ok { - return fmt.Errorf("fault") - } - return f.underlying.PutInternal(context.Background(), entry) -} - -func (f *faultyPseudo) DeleteInternal(ctx context.Context, key string) error { - if _, ok := f.faultyPaths[key]; ok { - return fmt.Errorf("fault") - } - return f.underlying.DeleteInternal(context.Background(), key) -} - -func (f *faultyPseudo) List(ctx context.Context, prefix string) ([]string, error) { - return f.underlying.List(context.Background(), prefix) -} - -func (f *faultyPseudo) Transaction(ctx context.Context, txns []*physical.TxnEntry) error { - f.underlying.permitPool.Acquire() - defer f.underlying.permitPool.Release() - - f.underlying.Lock() - defer f.underlying.Unlock() - - return physical.GenericTransactionHandler(ctx, f, txns) -} - -func newFaultyPseudo(logger log.Logger, faultyPaths []string) *faultyPseudo { - out := &faultyPseudo{ - underlying: InmemBackend{ - root: radix.New(), - permitPool: physical.NewPermitPool(1), - logger: logger.Named("storage.inmembackend"), - failGet: new(uint32), - failPut: new(uint32), - failDelete: new(uint32), - failList: new(uint32), - }, - faultyPaths: make(map[string]struct{}, len(faultyPaths)), - } - for _, v := range faultyPaths { - out.faultyPaths[v] = struct{}{} - } - return out -} - -func TestPseudo_Basic(t *testing.T) { - logger := logging.NewVaultLogger(log.Debug) - p := newFaultyPseudo(logger, nil) - physical.ExerciseBackend(t, p) - physical.ExerciseBackend_ListPrefix(t, p) -} - -func TestPseudo_SuccessfulTransaction(t *testing.T) { - logger := logging.NewVaultLogger(log.Debug) - p := newFaultyPseudo(logger, nil) - - physical.ExerciseTransactionalBackend(t, p) -} - -func TestPseudo_FailedTransaction(t *testing.T) { - logger := logging.NewVaultLogger(log.Debug) - p := newFaultyPseudo(logger, []string{"zip"}) - - txns := physical.SetupTestingTransactions(t, p) - if err := p.Transaction(context.Background(), txns); err == nil { - t.Fatal("expected error during transaction") - } - - keys, err := p.List(context.Background(), "") - if err != nil { - t.Fatal(err) - } - - expected := []string{"foo", "zip", "deleteme", "deleteme2"} - - sort.Strings(keys) - sort.Strings(expected) - if !reflect.DeepEqual(keys, expected) { - t.Fatalf("mismatch: expected\n%#v\ngot\n%#v\n", expected, keys) - } - - entry, err := p.Get(context.Background(), "foo") - if err != nil { - t.Fatal(err) - } - if entry == nil { - t.Fatal("got nil entry") - } - if entry.Value == nil { - t.Fatal("got nil value") - } - if string(entry.Value) != "bar" { - t.Fatal("values did not rollback correctly") - } - - entry, err = p.Get(context.Background(), "zip") - if err != nil { - t.Fatal(err) - } - if entry == nil { - t.Fatal("got nil entry") - } - if entry.Value == nil { - t.Fatal("got nil value") - } - if string(entry.Value) != "zap" { - t.Fatal("values did not rollback correctly") - } -} diff --git a/sdk/physical/testing.go b/sdk/physical/testing.go deleted file mode 100644 index b80f3697e..000000000 --- a/sdk/physical/testing.go +++ /dev/null @@ -1,520 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package physical - -import ( - "context" - "reflect" - "sort" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func ExerciseBackend(t testing.TB, b Backend) { - t.Helper() - - // Should be empty - keys, err := b.List(context.Background(), "") - if err != nil { - t.Fatalf("initial list failed: %v", err) - } - if len(keys) != 0 { - t.Errorf("initial not empty: %v", keys) - } - - // Delete should work if it does not exist - err = b.Delete(context.Background(), "foo") - if err != nil { - t.Fatalf("idempotent delete: %v", err) - } - - // Get should not fail, but be nil - out, err := b.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("initial get failed: %v", err) - } - if out != nil { - t.Errorf("initial get was not nil: %v", out) - } - - // Make an entry - e := &Entry{Key: "foo", Value: []byte("test")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("put failed: %v", err) - } - - // Get should work - out, err = b.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("get failed: %v", err) - } - if !reflect.DeepEqual(out, e) { - t.Errorf("bad: %v expected: %v", out, e) - } - - // List should not be empty - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("list failed: %v", err) - } - if len(keys) != 1 || keys[0] != "foo" { - t.Errorf("keys[0] did not equal foo: %v", keys) - } - - // Delete should work - err = b.Delete(context.Background(), "foo") - if err != nil { - t.Fatalf("delete: %v", err) - } - - // Should be empty - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("list after delete: %v", err) - } - if len(keys) != 0 { - t.Errorf("list after delete not empty: %v", keys) - } - - // Get should fail - out, err = b.Get(context.Background(), "foo") - if err != nil { - t.Fatalf("get after delete: %v", err) - } - if out != nil { - t.Errorf("get after delete not nil: %v", out) - } - - // Multiple Puts should work; GH-189 - e = &Entry{Key: "foo", Value: []byte("test")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("multi put 1 failed: %v", err) - } - e = &Entry{Key: "foo", Value: []byte("test")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("multi put 2 failed: %v", err) - } - - // Make a nested entry - e = &Entry{Key: "foo/bar", Value: []byte("baz")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("nested put failed: %v", err) - } - - // Get should work - out, err = b.Get(context.Background(), "foo/bar") - if err != nil { - t.Fatalf("get failed: %v", err) - } - if !reflect.DeepEqual(out, e) { - t.Errorf("bad: %v expected: %v", out, e) - } - - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("list multi failed: %v", err) - } - sort.Strings(keys) - if len(keys) != 2 || keys[0] != "foo" || keys[1] != "foo/" { - t.Errorf("expected 2 keys [foo, foo/]: %v", keys) - } - - // Delete with children should work - err = b.Delete(context.Background(), "foo") - if err != nil { - t.Fatalf("delete after multi: %v", err) - } - - // Get should return the child - out, err = b.Get(context.Background(), "foo/bar") - if err != nil { - t.Fatalf("get after multi delete: %v", err) - } - if out == nil { - t.Errorf("get after multi delete not nil: %v", out) - } - - // Removal of nested secret should not leave artifacts - e = &Entry{Key: "foo/nested1/nested2/nested3", Value: []byte("baz")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("deep nest: %v", err) - } - - err = b.Delete(context.Background(), "foo/nested1/nested2/nested3") - if err != nil { - t.Fatalf("failed to remove deep nest: %v", err) - } - - keys, err = b.List(context.Background(), "foo/") - if err != nil { - t.Fatalf("err: %v", err) - } - if len(keys) != 1 || keys[0] != "bar" { - t.Errorf("should be exactly 1 key == bar: %v", keys) - } - - // Make a second nested entry to test prefix removal - e = &Entry{Key: "foo/zip", Value: []byte("zap")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("failed to create second nested: %v", err) - } - - // Delete should not remove the prefix - err = b.Delete(context.Background(), "foo/bar") - if err != nil { - t.Fatalf("failed to delete nested prefix: %v", err) - } - - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("list nested prefix: %v", err) - } - if len(keys) != 1 || keys[0] != "foo/" { - t.Errorf("should be exactly 1 key == foo/: %v", keys) - } - - // Delete should remove the prefix - err = b.Delete(context.Background(), "foo/zip") - if err != nil { - t.Fatalf("failed to delete second prefix: %v", err) - } - - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("listing after second delete failed: %v", err) - } - if len(keys) != 0 { - t.Errorf("should be empty at end: %v", keys) - } - - // When the root path is empty, adding and removing deep nested values should not break listing - e = &Entry{Key: "foo/nested1/nested2/value1", Value: []byte("baz")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("deep nest: %v", err) - } - - e = &Entry{Key: "foo/nested1/nested2/value2", Value: []byte("baz")} - err = b.Put(context.Background(), e) - if err != nil { - t.Fatalf("deep nest: %v", err) - } - - err = b.Delete(context.Background(), "foo/nested1/nested2/value2") - if err != nil { - t.Fatalf("failed to remove deep nest: %v", err) - } - - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("listing of root failed after deletion: %v", err) - } - if len(keys) == 0 { - t.Errorf("root is returning empty after deleting a single nested value, expected nested1/: %v", keys) - keys, err = b.List(context.Background(), "foo/nested1") - if err != nil { - t.Fatalf("listing of expected nested path 'foo/nested1' failed: %v", err) - } - // prove that the root should not be empty and that foo/nested1 exists - if len(keys) != 0 { - t.Logf(" keys can still be listed from nested1/ so it's not empty, expected nested2/: %v", keys) - } - } - - // cleanup left over listing bug test value - err = b.Delete(context.Background(), "foo/nested1/nested2/value1") - if err != nil { - t.Fatalf("failed to remove deep nest: %v", err) - } - - keys, err = b.List(context.Background(), "") - if err != nil { - t.Fatalf("listing of root failed after delete of deep nest: %v", err) - } - if len(keys) != 0 { - t.Errorf("should be empty at end: %v", keys) - } -} - -func ExerciseBackend_ListPrefix(t testing.TB, b Backend) { - t.Helper() - - e1 := &Entry{Key: "foo", Value: []byte("test")} - e2 := &Entry{Key: "foo/bar", Value: []byte("test")} - e3 := &Entry{Key: "foo/bar/baz", Value: []byte("test")} - - defer func() { - b.Delete(context.Background(), "foo") - b.Delete(context.Background(), "foo/bar") - b.Delete(context.Background(), "foo/bar/baz") - }() - - err := b.Put(context.Background(), e1) - if err != nil { - t.Fatalf("failed to put entry 1: %v", err) - } - err = b.Put(context.Background(), e2) - if err != nil { - t.Fatalf("failed to put entry 2: %v", err) - } - err = b.Put(context.Background(), e3) - if err != nil { - t.Fatalf("failed to put entry 3: %v", err) - } - - // Scan the root - keys, err := b.List(context.Background(), "") - if err != nil { - t.Fatalf("list root: %v", err) - } - sort.Strings(keys) - if len(keys) != 2 || keys[0] != "foo" || keys[1] != "foo/" { - t.Errorf("root expected [foo foo/]: %v", keys) - } - - // Scan foo/ - keys, err = b.List(context.Background(), "foo/") - if err != nil { - t.Fatalf("list level 1: %v", err) - } - sort.Strings(keys) - if len(keys) != 2 || keys[0] != "bar" || keys[1] != "bar/" { - t.Errorf("level 1 expected [bar bar/]: %v", keys) - } - - // Scan foo/bar/ - keys, err = b.List(context.Background(), "foo/bar/") - if err != nil { - t.Fatalf("list level 2: %v", err) - } - sort.Strings(keys) - if len(keys) != 1 || keys[0] != "baz" { - t.Errorf("level 1 expected [baz]: %v", keys) - } -} - -func ExerciseHABackend(t testing.TB, b HABackend, b2 HABackend) { - t.Helper() - - // Get the lock - lock, err := b.LockWith("foo", "bar") - if err != nil { - t.Fatalf("initial lock: %v", err) - } - - // Attempt to lock - leaderCh, err := lock.Lock(nil) - if err != nil { - t.Fatalf("lock attempt 1: %v", err) - } - if leaderCh == nil { - t.Fatalf("missing leaderCh") - } - - // Check the value - held, val, err := lock.Value() - if err != nil { - t.Fatalf("err: %v", err) - } - if !held { - t.Errorf("should be held") - } - if val != "bar" { - t.Errorf("expected value bar: %v", err) - } - - // Check if it's fencing that we can register the lock - if fba, ok := b.(FencingHABackend); ok { - require.NoError(t, fba.RegisterActiveNodeLock(lock)) - } - - // Second acquisition should fail - lock2, err := b2.LockWith("foo", "baz") - if err != nil { - t.Fatalf("lock 2: %v", err) - } - - // Checking the lock from b2 should discover that the lock is held since held - // implies only that there is _some_ leader not that b2 is leader (this was - // not clear before so we make it explicit with this assertion). - held2, val2, err := lock2.Value() - require.NoError(t, err) - require.Equal(t, "bar", val2) - require.True(t, held2) - - // Cancel attempt in 50 msec - stopCh := make(chan struct{}) - time.AfterFunc(50*time.Millisecond, func() { - close(stopCh) - }) - - // Attempt to lock - leaderCh2, err := lock2.Lock(stopCh) - if err != nil { - t.Fatalf("stop lock 2: %v", err) - } - if leaderCh2 != nil { - t.Errorf("should not have gotten leaderCh: %v", leaderCh2) - } - - // Release the first lock - lock.Unlock() - - // Attempt to lock should work - leaderCh2, err = lock2.Lock(nil) - if err != nil { - t.Fatalf("lock 2 lock: %v", err) - } - if leaderCh2 == nil { - t.Errorf("should get leaderCh") - } - - // Check if it's fencing that we can register the lock - if fba2, ok := b2.(FencingHABackend); ok { - require.NoError(t, fba2.RegisterActiveNodeLock(lock)) - } - - // Check the value - held, val, err = lock2.Value() - if err != nil { - t.Fatalf("value: %v", err) - } - if !held { - t.Errorf("should still be held") - } - if val != "baz" { - t.Errorf("expected: baz, got: %v", val) - } - - // Cleanup - lock2.Unlock() -} - -func ExerciseTransactionalBackend(t testing.TB, b Backend) { - t.Helper() - tb, ok := b.(Transactional) - if !ok { - t.Fatal("Not a transactional backend") - } - - txns := SetupTestingTransactions(t, b) - - if err := tb.Transaction(context.Background(), txns); err != nil { - t.Fatal(err) - } - - keys, err := b.List(context.Background(), "") - if err != nil { - t.Fatal(err) - } - - expected := []string{"foo", "zip"} - - sort.Strings(keys) - sort.Strings(expected) - if !reflect.DeepEqual(keys, expected) { - t.Fatalf("mismatch: expected\n%#v\ngot\n%#v\n", expected, keys) - } - - entry, err := b.Get(context.Background(), "foo") - if err != nil { - t.Fatal(err) - } - if entry == nil { - t.Fatal("got nil entry") - } - if entry.Value == nil { - t.Fatal("got nil value") - } - if string(entry.Value) != "bar3" { - t.Fatal("updates did not apply correctly") - } - - entry, err = b.Get(context.Background(), "zip") - if err != nil { - t.Fatal(err) - } - if entry == nil { - t.Fatal("got nil entry") - } - if entry.Value == nil { - t.Fatal("got nil value") - } - if string(entry.Value) != "zap3" { - t.Fatal("updates did not apply correctly") - } -} - -func SetupTestingTransactions(t testing.TB, b Backend) []*TxnEntry { - t.Helper() - // Add a few keys so that we test rollback with deletion - if err := b.Put(context.Background(), &Entry{ - Key: "foo", - Value: []byte("bar"), - }); err != nil { - t.Fatal(err) - } - if err := b.Put(context.Background(), &Entry{ - Key: "zip", - Value: []byte("zap"), - }); err != nil { - t.Fatal(err) - } - if err := b.Put(context.Background(), &Entry{ - Key: "deleteme", - }); err != nil { - t.Fatal(err) - } - if err := b.Put(context.Background(), &Entry{ - Key: "deleteme2", - }); err != nil { - t.Fatal(err) - } - - txns := []*TxnEntry{ - { - Operation: PutOperation, - Entry: &Entry{ - Key: "foo", - Value: []byte("bar2"), - }, - }, - { - Operation: DeleteOperation, - Entry: &Entry{ - Key: "deleteme", - }, - }, - { - Operation: PutOperation, - Entry: &Entry{ - Key: "foo", - Value: []byte("bar3"), - }, - }, - { - Operation: DeleteOperation, - Entry: &Entry{ - Key: "deleteme2", - }, - }, - { - Operation: PutOperation, - Entry: &Entry{ - Key: "zip", - Value: []byte("zap3"), - }, - }, - } - - return txns -} diff --git a/sdk/plugin/grpc_backend_test.go b/sdk/plugin/grpc_backend_test.go deleted file mode 100644 index 01a6ea609..000000000 --- a/sdk/plugin/grpc_backend_test.go +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package plugin - -import ( - "context" - "os" - "testing" - "time" - - log "github.com/hashicorp/go-hclog" - gplugin "github.com/hashicorp/go-plugin" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/plugin/mock" -) - -func TestGRPCBackendPlugin_impl(t *testing.T) { - var _ gplugin.Plugin = new(GRPCBackendPlugin) - var _ logical.Backend = new(backendGRPCPluginClient) -} - -func TestGRPCBackendPlugin_HandleRequest(t *testing.T) { - b, cleanup := testGRPCBackend(t) - defer cleanup() - - resp, err := b.HandleRequest(context.Background(), &logical.Request{ - Operation: logical.CreateOperation, - Path: "kv/foo", - Data: map[string]interface{}{ - "value": "bar", - }, - }) - if err != nil { - t.Fatal(err) - } - if resp.Data["value"] != "bar" { - t.Fatalf("bad: %#v", resp) - } -} - -func TestGRPCBackendPlugin_SpecialPaths(t *testing.T) { - b, cleanup := testGRPCBackend(t) - defer cleanup() - - paths := b.SpecialPaths() - if paths == nil { - t.Fatal("SpecialPaths() returned nil") - } -} - -func TestGRPCBackendPlugin_System(t *testing.T) { - b, cleanup := testGRPCBackend(t) - defer cleanup() - - sys := b.System() - if sys == nil { - t.Fatal("System() returned nil") - } - - actual := sys.DefaultLeaseTTL() - expected := 300 * time.Second - - if actual != expected { - t.Fatalf("bad: %v, expected %v", actual, expected) - } -} - -func TestGRPCBackendPlugin_Logger(t *testing.T) { - b, cleanup := testGRPCBackend(t) - defer cleanup() - - logger := b.Logger() - if logger == nil { - t.Fatal("Logger() returned nil") - } -} - -func TestGRPCBackendPlugin_HandleExistenceCheck(t *testing.T) { - b, cleanup := testGRPCBackend(t) - defer cleanup() - - checkFound, exists, err := b.HandleExistenceCheck(context.Background(), &logical.Request{ - Operation: logical.CreateOperation, - Path: "kv/foo", - Data: map[string]interface{}{"value": "bar"}, - }) - if err != nil { - t.Fatal(err) - } - if !checkFound { - t.Fatal("existence check not found for path 'kv/foo") - } - if exists { - t.Fatal("existence check should have returned 'false' for 'kv/foo'") - } -} - -func TestGRPCBackendPlugin_Cleanup(t *testing.T) { - b, cleanup := testGRPCBackend(t) - defer cleanup() - - b.Cleanup(context.Background()) -} - -func TestGRPCBackendPlugin_InvalidateKey(t *testing.T) { - b, cleanup := testGRPCBackend(t) - defer cleanup() - - ctx := context.Background() - - resp, err := b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "internal", - }) - if err != nil { - t.Fatal(err) - } - if resp.Data["value"] == "" { - t.Fatalf("bad: %#v, expected non-empty value", resp) - } - - b.InvalidateKey(ctx, "internal") - - resp, err = b.HandleRequest(ctx, &logical.Request{ - Operation: logical.ReadOperation, - Path: "internal", - }) - if err != nil { - t.Fatal(err) - } - if resp.Data["value"] != "" { - t.Fatalf("bad: expected empty response data, got %#v", resp) - } -} - -func TestGRPCBackendPlugin_Setup(t *testing.T) { - _, cleanup := testGRPCBackend(t) - defer cleanup() -} - -func TestGRPCBackendPlugin_Initialize(t *testing.T) { - b, cleanup := testGRPCBackend(t) - defer cleanup() - - err := b.Initialize(context.Background(), &logical.InitializationRequest{}) - if err != nil { - t.Fatal(err) - } -} - -func TestGRPCBackendPlugin_Version(t *testing.T) { - b, cleanup := testGRPCBackend(t) - defer cleanup() - - versioner, ok := b.(logical.PluginVersioner) - if !ok { - t.Fatalf("Expected %T to implement logical.PluginVersioner interface", b) - } - - version := versioner.PluginVersion().Version - if version != "v0.0.0+mock" { - t.Fatalf("Got version %s, expected 'mock'", version) - } -} - -func testGRPCBackend(t *testing.T) (logical.Backend, func()) { - // Create a mock provider - pluginMap := map[string]gplugin.Plugin{ - "backend": &GRPCBackendPlugin{ - Factory: mock.Factory, - Logger: log.New(&log.LoggerOptions{ - Level: log.Debug, - Output: os.Stderr, - JSONFormat: true, - }), - }, - } - client, _ := gplugin.TestPluginGRPCConn(t, pluginMap) - cleanup := func() { - client.Close() - } - - // Request the backend - raw, err := client.Dispense(BackendPluginName) - if err != nil { - t.Fatal(err) - } - b := raw.(logical.Backend) - - err = b.Setup(context.Background(), &logical.BackendConfig{ - Logger: logging.NewVaultLogger(log.Debug), - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: 300 * time.Second, - MaxLeaseTTLVal: 1800 * time.Second, - }, - StorageView: &logical.InmemStorage{}, - }) - if err != nil { - t.Fatal(err) - } - - return b, cleanup -} diff --git a/sdk/plugin/grpc_system_test.go b/sdk/plugin/grpc_system_test.go deleted file mode 100644 index 19a5ecbaa..000000000 --- a/sdk/plugin/grpc_system_test.go +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package plugin - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/hashicorp/go-plugin" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/plugin/pb" - "google.golang.org/grpc" - "google.golang.org/protobuf/proto" -) - -func TestSystem_GRPC_ReturnsErrIfSystemViewNil(t *testing.T) { - _, err := new(gRPCSystemViewServer).ReplicationState(context.Background(), nil) - if err == nil { - t.Error("Expected error when using server with no impl") - } -} - -func TestSystem_GRPC_GRPC_impl(t *testing.T) { - var _ logical.SystemView = new(gRPCSystemViewClient) -} - -func TestSystem_GRPC_defaultLeaseTTL(t *testing.T) { - sys := logical.TestSystemView() - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - testSystemView := newGRPCSystemView(client) - - expected := sys.DefaultLeaseTTL() - actual := testSystemView.DefaultLeaseTTL() - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("expected: %v, got: %v", expected, actual) - } -} - -func TestSystem_GRPC_maxLeaseTTL(t *testing.T) { - sys := logical.TestSystemView() - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - testSystemView := newGRPCSystemView(client) - - expected := sys.MaxLeaseTTL() - actual := testSystemView.MaxLeaseTTL() - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("expected: %v, got: %v", expected, actual) - } -} - -func TestSystem_GRPC_tainted(t *testing.T) { - sys := logical.TestSystemView() - sys.TaintedVal = true - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - testSystemView := newGRPCSystemView(client) - - expected := sys.Tainted() - actual := testSystemView.Tainted() - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("expected: %v, got: %v", expected, actual) - } -} - -func TestSystem_GRPC_cachingDisabled(t *testing.T) { - sys := logical.TestSystemView() - sys.CachingDisabledVal = true - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - testSystemView := newGRPCSystemView(client) - - expected := sys.CachingDisabled() - actual := testSystemView.CachingDisabled() - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("expected: %v, got: %v", expected, actual) - } -} - -func TestSystem_GRPC_replicationState(t *testing.T) { - sys := logical.TestSystemView() - sys.ReplicationStateVal = consts.ReplicationPerformancePrimary - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - testSystemView := newGRPCSystemView(client) - - expected := sys.ReplicationState() - actual := testSystemView.ReplicationState() - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("expected: %v, got: %v", expected, actual) - } -} - -func TestSystem_GRPC_lookupPlugin(t *testing.T) { - sys := logical.TestSystemView() - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - - testSystemView := newGRPCSystemView(client) - - if _, err := testSystemView.LookupPlugin(context.Background(), "foo", consts.PluginTypeDatabase); err == nil { - t.Fatal("LookPlugin(): expected error on due to unsupported call from plugin") - } -} - -func TestSystem_GRPC_mlockEnabled(t *testing.T) { - sys := logical.TestSystemView() - sys.EnableMlock = true - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - - testSystemView := newGRPCSystemView(client) - - expected := sys.MlockEnabled() - actual := testSystemView.MlockEnabled() - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("expected: %v, got: %v", expected, actual) - } -} - -func TestSystem_GRPC_entityInfo(t *testing.T) { - sys := logical.TestSystemView() - sys.EntityVal = &logical.Entity{ - ID: "id", - Name: "name", - Metadata: map[string]string{ - "foo": "bar", - }, - Aliases: []*logical.Alias{ - { - MountType: "logical", - MountAccessor: "accessor", - Name: "name", - Metadata: map[string]string{ - "zip": "zap", - }, - }, - }, - Disabled: true, - } - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - testSystemView := newGRPCSystemView(client) - - actual, err := testSystemView.EntityInfo("") - if err != nil { - t.Fatal(err) - } - if !proto.Equal(sys.EntityVal, actual) { - t.Fatalf("expected: %v, got: %v", sys.EntityVal, actual) - } -} - -func TestSystem_GRPC_groupsForEntity(t *testing.T) { - sys := logical.TestSystemView() - sys.GroupsVal = []*logical.Group{ - { - ID: "group1-id", - Name: "group1", - Metadata: map[string]string{ - "group-metadata": "metadata-value", - }, - }, - } - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - testSystemView := newGRPCSystemView(client) - - actual, err := testSystemView.GroupsForEntity("") - if err != nil { - t.Fatal(err) - } - if !proto.Equal(sys.GroupsVal[0], actual[0]) { - t.Fatalf("expected: %v, got: %v", sys.GroupsVal, actual) - } -} - -func TestSystem_GRPC_pluginEnv(t *testing.T) { - sys := logical.TestSystemView() - sys.PluginEnvironment = &logical.PluginEnvironment{ - VaultVersion: "0.10.42", - VaultVersionPrerelease: "dev", - VaultVersionMetadata: "ent", - } - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer client.Close() - - testSystemView := newGRPCSystemView(client) - - expected, err := sys.PluginEnv(context.Background()) - if err != nil { - t.Fatal(err) - } - - actual, err := testSystemView.PluginEnv(context.Background()) - if err != nil { - t.Fatal(err) - } - - if !proto.Equal(expected, actual) { - t.Fatalf("expected: %v, got: %v", expected, actual) - } -} - -func TestSystem_GRPC_GeneratePasswordFromPolicy(t *testing.T) { - policyName := "testpolicy" - expectedPassword := "87354qtnjgrehiogd9u0t43" - passGen := func() (password string, err error) { - return expectedPassword, nil - } - sys := &logical.StaticSystemView{ - PasswordPolicies: map[string]logical.PasswordGenerator{ - policyName: passGen, - }, - } - - client, server := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterSystemViewServer(s, &gRPCSystemViewServer{ - impl: sys, - }) - }) - defer server.Stop() - defer client.Close() - - testSystemView := newGRPCSystemView(client) - - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() - - password, err := testSystemView.GeneratePasswordFromPolicy(ctx, policyName) - if err != nil { - t.Fatalf("no error expected, got: %s", err) - } - - if password != expectedPassword { - t.Fatalf("Actual password: %s\nExpected password: %s", password, expectedPassword) - } -} diff --git a/sdk/plugin/logger_test.go b/sdk/plugin/logger_test.go deleted file mode 100644 index c47a70b1c..000000000 --- a/sdk/plugin/logger_test.go +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package plugin - -import ( - "bufio" - "bytes" - "io/ioutil" - "net/rpc" - "strings" - "testing" - - hclog "github.com/hashicorp/go-hclog" - plugin "github.com/hashicorp/go-plugin" - "github.com/hashicorp/vault/sdk/helper/logging" -) - -func TestLogger_levels(t *testing.T) { - client, server := plugin.TestRPCConn(t) - defer client.Close() - - var buf bytes.Buffer - writer := bufio.NewWriter(&buf) - - l := logging.NewVaultLoggerWithWriter(writer, hclog.Trace) - - server.RegisterName("Plugin", &LoggerServer{ - logger: l, - }) - - expected := "foobar" - testLogger := &deprecatedLoggerClient{client: client} - - // Test trace - testLogger.Trace(expected) - if err := writer.Flush(); err != nil { - t.Fatal(err) - } - result := buf.String() - buf.Reset() - if !strings.Contains(result, expected) { - t.Fatalf("expected log to contain %s, got %s", expected, result) - } - - // Test debug - testLogger.Debug(expected) - if err := writer.Flush(); err != nil { - t.Fatal(err) - } - result = buf.String() - buf.Reset() - if !strings.Contains(result, expected) { - t.Fatalf("expected log to contain %s, got %s", expected, result) - } - - // Test debug - testLogger.Info(expected) - if err := writer.Flush(); err != nil { - t.Fatal(err) - } - result = buf.String() - buf.Reset() - if !strings.Contains(result, expected) { - t.Fatalf("expected log to contain %s, got %s", expected, result) - } - - // Test warn - testLogger.Warn(expected) - if err := writer.Flush(); err != nil { - t.Fatal(err) - } - result = buf.String() - buf.Reset() - if !strings.Contains(result, expected) { - t.Fatalf("expected log to contain %s, got %s", expected, result) - } - - // Test error - testLogger.Error(expected) - if err := writer.Flush(); err != nil { - t.Fatal(err) - } - result = buf.String() - buf.Reset() - if !strings.Contains(result, expected) { - t.Fatalf("expected log to contain %s, got %s", expected, result) - } - - // Test fatal - testLogger.Fatal(expected) - if err := writer.Flush(); err != nil { - t.Fatal(err) - } - result = buf.String() - buf.Reset() - if result != "" { - t.Fatalf("expected log Fatal() to be no-op, got %s", result) - } -} - -func TestLogger_isLevels(t *testing.T) { - client, server := plugin.TestRPCConn(t) - defer client.Close() - - l := logging.NewVaultLoggerWithWriter(ioutil.Discard, hclog.Trace) - - server.RegisterName("Plugin", &LoggerServer{ - logger: l, - }) - - testLogger := &deprecatedLoggerClient{client: client} - - if !testLogger.IsDebug() || !testLogger.IsInfo() || !testLogger.IsTrace() || !testLogger.IsWarn() { - t.Fatal("expected logger to return true for all logger level checks") - } -} - -func TestLogger_log(t *testing.T) { - client, server := plugin.TestRPCConn(t) - defer client.Close() - - var buf bytes.Buffer - writer := bufio.NewWriter(&buf) - - l := logging.NewVaultLoggerWithWriter(writer, hclog.Trace) - - server.RegisterName("Plugin", &LoggerServer{ - logger: l, - }) - - expected := "foobar" - testLogger := &deprecatedLoggerClient{client: client} - - // Test trace 6 = logxi.LevelInfo - testLogger.Log(6, expected, nil) - if err := writer.Flush(); err != nil { - t.Fatal(err) - } - result := buf.String() - if !strings.Contains(result, expected) { - t.Fatalf("expected log to contain %s, got %s", expected, result) - } -} - -func TestLogger_setLevel(t *testing.T) { - client, server := plugin.TestRPCConn(t) - defer client.Close() - - l := hclog.New(&hclog.LoggerOptions{Output: ioutil.Discard}) - - server.RegisterName("Plugin", &LoggerServer{ - logger: l, - }) - - testLogger := &deprecatedLoggerClient{client: client} - testLogger.SetLevel(4) // 4 == logxi.LevelWarn - - if !testLogger.IsWarn() { - t.Fatal("expected logger to support warn level") - } -} - -type deprecatedLoggerClient struct { - client *rpc.Client -} - -func (l *deprecatedLoggerClient) Trace(msg string, args ...interface{}) { - cArgs := &LoggerArgs{ - Msg: msg, - Args: args, - } - l.client.Call("Plugin.Trace", cArgs, &struct{}{}) -} - -func (l *deprecatedLoggerClient) Debug(msg string, args ...interface{}) { - cArgs := &LoggerArgs{ - Msg: msg, - Args: args, - } - l.client.Call("Plugin.Debug", cArgs, &struct{}{}) -} - -func (l *deprecatedLoggerClient) Info(msg string, args ...interface{}) { - cArgs := &LoggerArgs{ - Msg: msg, - Args: args, - } - l.client.Call("Plugin.Info", cArgs, &struct{}{}) -} - -func (l *deprecatedLoggerClient) Warn(msg string, args ...interface{}) error { - var reply LoggerReply - cArgs := &LoggerArgs{ - Msg: msg, - Args: args, - } - err := l.client.Call("Plugin.Warn", cArgs, &reply) - if err != nil { - return err - } - if reply.Error != nil { - return reply.Error - } - - return nil -} - -func (l *deprecatedLoggerClient) Error(msg string, args ...interface{}) error { - var reply LoggerReply - cArgs := &LoggerArgs{ - Msg: msg, - Args: args, - } - err := l.client.Call("Plugin.Error", cArgs, &reply) - if err != nil { - return err - } - if reply.Error != nil { - return reply.Error - } - - return nil -} - -func (l *deprecatedLoggerClient) Fatal(msg string, args ...interface{}) { - // NOOP since it's not actually used within vault -} - -func (l *deprecatedLoggerClient) Log(level int, msg string, args []interface{}) { - cArgs := &LoggerArgs{ - Level: level, - Msg: msg, - Args: args, - } - l.client.Call("Plugin.Log", cArgs, &struct{}{}) -} - -func (l *deprecatedLoggerClient) SetLevel(level int) { - l.client.Call("Plugin.SetLevel", level, &struct{}{}) -} - -func (l *deprecatedLoggerClient) IsTrace() bool { - var reply LoggerReply - l.client.Call("Plugin.IsTrace", new(interface{}), &reply) - return reply.IsTrue -} - -func (l *deprecatedLoggerClient) IsDebug() bool { - var reply LoggerReply - l.client.Call("Plugin.IsDebug", new(interface{}), &reply) - return reply.IsTrue -} - -func (l *deprecatedLoggerClient) IsInfo() bool { - var reply LoggerReply - l.client.Call("Plugin.IsInfo", new(interface{}), &reply) - return reply.IsTrue -} - -func (l *deprecatedLoggerClient) IsWarn() bool { - var reply LoggerReply - l.client.Call("Plugin.IsWarn", new(interface{}), &reply) - return reply.IsTrue -} diff --git a/sdk/plugin/mock/backend_test.go b/sdk/plugin/mock/backend_test.go deleted file mode 100644 index 640eec116..000000000 --- a/sdk/plugin/mock/backend_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package mock - -import ( - "testing" - - "github.com/hashicorp/vault/sdk/logical" -) - -func TestBackend_impl(t *testing.T) { - var _ logical.Backend = new(backend) -} diff --git a/sdk/plugin/pb/translation_test.go b/sdk/plugin/pb/translation_test.go deleted file mode 100644 index 30979257a..000000000 --- a/sdk/plugin/pb/translation_test.go +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package pb - -import ( - "crypto/tls" - "crypto/x509" - "encoding/pem" - "errors" - "reflect" - "testing" - "time" - - "github.com/hashicorp/vault/sdk/helper/errutil" - "github.com/hashicorp/vault/sdk/helper/wrapping" - "github.com/hashicorp/vault/sdk/logical" -) - -func TestTranslation_Errors(t *testing.T) { - errs := []error{ - nil, - errors.New("test"), - errutil.UserError{Err: "test"}, - errutil.InternalError{Err: "test"}, - logical.CodedError(403, "test"), - &logical.StatusBadRequest{Err: "test"}, - logical.ErrUnsupportedOperation, - logical.ErrUnsupportedPath, - logical.ErrInvalidRequest, - logical.ErrPermissionDenied, - logical.ErrMultiAuthzPending, - } - - for _, err := range errs { - pe := ErrToProtoErr(err) - e := ProtoErrToErr(pe) - - if !reflect.DeepEqual(e, err) { - t.Fatalf("Errs did not match: %#v, %#v", e, err) - } - } -} - -func TestTranslation_StorageEntry(t *testing.T) { - tCases := []*logical.StorageEntry{ - nil, - {Key: "key", Value: []byte("value")}, - {Key: "key1", Value: []byte("value1"), SealWrap: true}, - {Key: "key1", SealWrap: true}, - } - - for _, c := range tCases { - p := LogicalStorageEntryToProtoStorageEntry(c) - e := ProtoStorageEntryToLogicalStorageEntry(p) - - if !reflect.DeepEqual(c, e) { - t.Fatalf("Entries did not match: %#v, %#v", e, c) - } - } -} - -func TestTranslation_Request(t *testing.T) { - certs, err := peerCertificates() - if err != nil { - t.Logf("No test certificates were generated: %v", err) - } - - tCases := []*logical.Request{ - nil, - { - ID: "ID", - ReplicationCluster: "RID", - Operation: logical.CreateOperation, - Path: "test/foo", - ClientToken: "token", - ClientTokenAccessor: "accessor", - DisplayName: "display", - MountPoint: "test", - MountType: "secret", - MountAccessor: "test-231234", - ClientTokenRemainingUses: 1, - EntityID: "tester", - PolicyOverride: true, - Unauthenticated: true, - Connection: &logical.Connection{ - RemoteAddr: "localhost", - ConnState: &tls.ConnectionState{ - Version: tls.VersionTLS12, - HandshakeComplete: true, - PeerCertificates: certs, - }, - }, - }, - { - ID: "ID", - ReplicationCluster: "RID", - Operation: logical.CreateOperation, - Path: "test/foo", - Data: map[string]interface{}{ - "string": "string", - "bool": true, - "array": []interface{}{"1", "2"}, - "map": map[string]interface{}{ - "key": "value", - }, - }, - Secret: &logical.Secret{ - LeaseOptions: logical.LeaseOptions{ - TTL: time.Second, - MaxTTL: time.Second, - Renewable: true, - Increment: time.Second, - IssueTime: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), - }, - InternalData: map[string]interface{}{ - "role": "test", - }, - LeaseID: "LeaseID", - }, - Auth: &logical.Auth{ - LeaseOptions: logical.LeaseOptions{ - TTL: time.Second, - MaxTTL: time.Second, - Renewable: true, - Increment: time.Second, - IssueTime: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), - }, - InternalData: map[string]interface{}{ - "role": "test", - }, - DisplayName: "test", - Policies: []string{"test", "Test"}, - Metadata: map[string]string{ - "test": "test", - }, - ClientToken: "token", - Accessor: "accessor", - Period: 5 * time.Second, - NumUses: 1, - EntityID: "id", - Alias: &logical.Alias{ - MountType: "type", - MountAccessor: "accessor", - Name: "name", - }, - GroupAliases: []*logical.Alias{ - { - MountType: "type", - MountAccessor: "accessor", - Name: "name", - }, - }, - }, - Headers: map[string][]string{ - "X-Vault-Test": {"test"}, - }, - ClientToken: "token", - ClientTokenAccessor: "accessor", - DisplayName: "display", - MountPoint: "test", - MountType: "secret", - MountAccessor: "test-231234", - WrapInfo: &logical.RequestWrapInfo{ - TTL: time.Second, - Format: "token", - SealWrap: true, - }, - ClientTokenRemainingUses: 1, - EntityID: "tester", - PolicyOverride: true, - Unauthenticated: true, - }, - } - - for _, c := range tCases { - p, err := LogicalRequestToProtoRequest(c) - if err != nil { - t.Fatal(err) - } - r, err := ProtoRequestToLogicalRequest(p) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(c, r) { - t.Fatalf("Requests did not match: \n%#v, \n%#v", c, r) - } - } -} - -func TestTranslation_Response(t *testing.T) { - tCases := []*logical.Response{ - nil, - { - Data: map[string]interface{}{ - "data": "blah", - }, - Warnings: []string{"warning"}, - }, - { - Data: map[string]interface{}{ - "string": "string", - "bool": true, - "array": []interface{}{"1", "2"}, - "map": map[string]interface{}{ - "key": "value", - }, - }, - Secret: &logical.Secret{ - LeaseOptions: logical.LeaseOptions{ - TTL: time.Second, - MaxTTL: time.Second, - Renewable: true, - Increment: time.Second, - IssueTime: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), - }, - InternalData: map[string]interface{}{ - "role": "test", - }, - LeaseID: "LeaseID", - }, - Auth: &logical.Auth{ - LeaseOptions: logical.LeaseOptions{ - TTL: time.Second, - MaxTTL: time.Second, - Renewable: true, - Increment: time.Second, - IssueTime: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), - }, - InternalData: map[string]interface{}{ - "role": "test", - }, - DisplayName: "test", - Policies: []string{"test", "Test"}, - Metadata: map[string]string{ - "test": "test", - }, - ClientToken: "token", - Accessor: "accessor", - Period: 5 * time.Second, - NumUses: 1, - EntityID: "id", - Alias: &logical.Alias{ - MountType: "type", - MountAccessor: "accessor", - Name: "name", - }, - GroupAliases: []*logical.Alias{ - { - MountType: "type", - MountAccessor: "accessor", - Name: "name", - }, - }, - }, - WrapInfo: &wrapping.ResponseWrapInfo{ - TTL: time.Second, - Token: "token", - Accessor: "accessor", - CreationTime: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), - WrappedAccessor: "wrapped-accessor", - WrappedEntityID: "id", - Format: "token", - CreationPath: "test/foo", - SealWrap: true, - }, - }, - } - - for _, c := range tCases { - p, err := LogicalResponseToProtoResponse(c) - if err != nil { - t.Fatal(err) - } - r, err := ProtoResponseToLogicalResponse(p) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(c, r) { - t.Fatalf("Requests did not match: \n%#v, \n%#v", c, r) - } - } -} - -// This is the contents of $GOROOT/src/crypto/tls/testdata/example-cert.pem -// If it's good enough for testing the crypto/tls package it's good enough -// for Vault. -const exampleCert = ` ------BEGIN CERTIFICATE----- -MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw -DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow -EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d -7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B -5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr -BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1 -NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l -Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc -6MF9+Yw1Yy0t ------END CERTIFICATE-----` - -func peerCertificates() ([]*x509.Certificate, error) { - blk, _ := pem.Decode([]byte(exampleCert)) - if blk == nil { - return nil, errors.New("cannot decode example certificate") - } - - cert, err := x509.ParseCertificate(blk.Bytes) - if err != nil { - return nil, err - } - - return []*x509.Certificate{cert}, nil -} diff --git a/sdk/plugin/storage_test.go b/sdk/plugin/storage_test.go deleted file mode 100644 index 61a5deec6..000000000 --- a/sdk/plugin/storage_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package plugin - -import ( - "context" - "testing" - - "google.golang.org/grpc" - - "github.com/hashicorp/go-plugin" - "github.com/hashicorp/vault/sdk/logical" - "github.com/hashicorp/vault/sdk/plugin/pb" -) - -func TestStorage_GRPC_ReturnsErrIfStorageNil(t *testing.T) { - _, err := new(GRPCStorageServer).Get(context.Background(), nil) - if err == nil { - t.Error("Expected error when using server with no impl") - } -} - -func TestStorage_impl(t *testing.T) { - var _ logical.Storage = new(GRPCStorageClient) -} - -func TestStorage_GRPC(t *testing.T) { - storage := &logical.InmemStorage{} - client, _ := plugin.TestGRPCConn(t, func(s *grpc.Server) { - pb.RegisterStorageServer(s, &GRPCStorageServer{ - impl: storage, - }) - }) - defer client.Close() - - testStorage := &GRPCStorageClient{client: pb.NewStorageClient(client)} - - logical.TestStorage(t, testStorage) -} diff --git a/sdk/queue/priority_queue_test.go b/sdk/queue/priority_queue_test.go deleted file mode 100644 index 108a26cc0..000000000 --- a/sdk/queue/priority_queue_test.go +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package queue - -import ( - "container/heap" - "fmt" - "testing" - "time" -) - -// Ensure we satisfy the heap.Interface -var _ heap.Interface = &queue{} - -// some tests rely on the ordering of items from this method -func testCases() (tc []*Item) { - // create a slice of items with priority / times offest by these seconds - for i, m := range []time.Duration{ - 5, - 183600, // 51 hours - 15, // 15 seconds - 45, // 45 seconds - 900, // 15 minutes - 300, // 5 minutes - 7200, // 2 hours - 183600, // 51 hours - 7201, // 2 hours, 1 second - 115200, // 32 hours - 1209600, // 2 weeks - } { - n := time.Now() - ft := n.Add(time.Second * m) - tc = append(tc, &Item{ - Key: fmt.Sprintf("item-%d", i), - Value: 1, - Priority: ft.Unix(), - }) - } - return -} - -func TestPriorityQueue_New(t *testing.T) { - pq := New() - - if len(pq.data) != len(pq.dataMap) || len(pq.data) != 0 { - t.Fatalf("error in queue/map size, expected data and map to be initialized, got (%d) and (%d)", len(pq.data), len(pq.dataMap)) - } - - if pq.Len() != 0 { - t.Fatalf("expected new queue to have zero size, got (%d)", pq.Len()) - } -} - -func TestPriorityQueue_Push(t *testing.T) { - pq := New() - - // don't allow nil pushing - if err := pq.Push(nil); err == nil { - t.Fatal("Expected error on pushing nil") - } - - tc := testCases() - tcl := len(tc) - for _, i := range tc { - if err := pq.Push(i); err != nil { - t.Fatal(err) - } - } - - if pq.Len() != tcl { - t.Fatalf("error adding items, expected (%d) items, got (%d)", tcl, pq.Len()) - } - - testValidateInternalData(t, pq, len(tc), false) - - item, err := pq.Pop() - if err != nil { - t.Fatalf("error popping item: %s", err) - } - if tc[0].Priority != item.Priority { - t.Fatalf("expected tc[0] and popped item to match, got (%q) and (%q)", tc[0], item.Priority) - } - if tc[0].Key != item.Key { - t.Fatalf("expected tc[0] and popped item to match, got (%q) and (%q)", tc[0], item.Priority) - } - - testValidateInternalData(t, pq, len(tc)-1, false) - - // push item with no key - dErr := pq.Push(tc[1]) - if dErr != ErrDuplicateItem { - t.Fatal(err) - } - // push item with no key - tc[2].Key = "" - kErr := pq.Push(tc[2]) - if kErr != nil && kErr.Error() != "error adding item: Item Key is required" { - t.Fatal(kErr) - } - - testValidateInternalData(t, pq, len(tc)-1, true) - - // check nil,nil error for not found - i, err := pq.PopByKey("empty") - if err != nil && i != nil { - t.Fatalf("expected nil error for PopByKey of non-existing key, got: %s", err) - } -} - -func TestPriorityQueue_Pop(t *testing.T) { - pq := New() - - tc := testCases() - for _, i := range tc { - if err := pq.Push(i); err != nil { - t.Fatal(err) - } - } - - topItem, err := pq.Pop() - if err != nil { - t.Fatalf("error calling pop: %s", err) - } - if tc[0].Priority != topItem.Priority { - t.Fatalf("expected tc[0] and popped item to match, got (%q) and (%q)", tc[0], topItem.Priority) - } - if tc[0].Key != topItem.Key { - t.Fatalf("expected tc[0] and popped item to match, got (%q) and (%q)", tc[0], topItem.Priority) - } - - var items []*Item - items = append(items, topItem) - // pop the remaining items, compare size of input and output - i, _ := pq.Pop() - for ; i != nil; i, _ = pq.Pop() { - items = append(items, i) - } - if len(items) != len(tc) { - t.Fatalf("expected popped item count to match test cases, got (%d)", len(items)) - } -} - -func TestPriorityQueue_PopByKey(t *testing.T) { - pq := New() - - tc := testCases() - for _, i := range tc { - if err := pq.Push(i); err != nil { - t.Fatal(err) - } - } - - // grab the top priority item, to capture it's value for checking later - item, _ := pq.Pop() - oldPriority := item.Priority - oldKey := item.Key - - // push the item back on, so it gets removed with PopByKey and we verify - // the top item has changed later - err := pq.Push(item) - if err != nil { - t.Fatalf("error re-pushing top item: %s", err) - } - - popKeys := []int{2, 4, 7, 1, 0} - for _, i := range popKeys { - item, err := pq.PopByKey(fmt.Sprintf("item-%d", i)) - if err != nil { - t.Fatalf("failed to pop item-%d, \n\terr: %s\n\titem: %#v", i, err, item) - } - } - - testValidateInternalData(t, pq, len(tc)-len(popKeys), false) - - // grab the top priority item again, to compare with the top item priority - // from above - item, _ = pq.Pop() - newPriority := item.Priority - newKey := item.Key - - if oldPriority == newPriority || oldKey == newKey { - t.Fatalf("expected old/new key and priority to differ, got (%s/%s) and (%d/%d)", oldKey, newKey, oldPriority, newPriority) - } - - testValidateInternalData(t, pq, len(tc)-len(popKeys)-1, true) -} - -// testValidateInternalData checks the internal data structure of the PriorityQueue -// and verifies that items are in-sync. Use drain only at the end of a test, -// because it will mutate the input queue -func testValidateInternalData(t *testing.T, pq *PriorityQueue, expectedSize int, drain bool) { - actualSize := pq.Len() - if actualSize != expectedSize { - t.Fatalf("expected new queue size to be (%d), got (%d)", expectedSize, actualSize) - } - - if len(pq.data) != len(pq.dataMap) || len(pq.data) != expectedSize { - t.Fatalf("error in queue/map size, expected data and map to be (%d), got (%d) and (%d)", expectedSize, len(pq.data), len(pq.dataMap)) - } - - if drain && pq.Len() > 0 { - // pop all the items, verify lengths - i, _ := pq.Pop() - for ; i != nil; i, _ = pq.Pop() { - expectedSize-- - if len(pq.data) != len(pq.dataMap) || len(pq.data) != expectedSize { - t.Fatalf("error in queue/map size, expected data and map to be (%d), got (%d) and (%d)", expectedSize, len(pq.data), len(pq.dataMap)) - } - } - } -} diff --git a/serviceregistration/consul/consul_service_registration_test.go b/serviceregistration/consul/consul_service_registration_test.go deleted file mode 100644 index 4457cb45d..000000000 --- a/serviceregistration/consul/consul_service_registration_test.go +++ /dev/null @@ -1,565 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "os" - "reflect" - "sync" - "testing" - "time" - - "github.com/go-test/deep" - "github.com/hashicorp/consul/api" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/helper/testhelpers/consul" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/physical" - "github.com/hashicorp/vault/sdk/physical/inmem" - sr "github.com/hashicorp/vault/serviceregistration" - "github.com/hashicorp/vault/vault" -) - -type consulConf map[string]string - -func testConsulServiceRegistration(t *testing.T) *serviceRegistration { - return testConsulServiceRegistrationConfig(t, &consulConf{}) -} - -func testConsulServiceRegistrationConfig(t *testing.T, conf *consulConf) *serviceRegistration { - logger := logging.NewVaultLogger(log.Debug) - - shutdownCh := make(chan struct{}) - defer func() { - close(shutdownCh) - }() - be, err := NewServiceRegistration(*conf, logger, sr.State{}) - if err != nil { - t.Fatalf("Expected Consul to initialize: %v", err) - } - if err := be.Run(shutdownCh, &sync.WaitGroup{}, ""); err != nil { - t.Fatal(err) - } - - c, ok := be.(*serviceRegistration) - if !ok { - t.Fatalf("Expected serviceRegistration") - } - - return c -} - -// TestConsul_ServiceRegistration tests whether consul ServiceRegistration works -func TestConsul_ServiceRegistration(t *testing.T) { - // Prepare a docker-based consul instance - cleanup, config := consul.PrepareTestContainer(t, "", false, true) - defer cleanup() - - // Create a consul client - client, err := api.NewClient(config.APIConfig()) - if err != nil { - t.Fatal(err) - } - - // waitForServices waits for the services in the Consul catalog to - // reach an expected value, returning the delta if that doesn't happen in time. - waitForServices := func(t *testing.T, expected map[string][]string) map[string][]string { - t.Helper() - // Wait for up to 10 seconds - var services map[string][]string - var err error - for i := 0; i < 10; i++ { - services, _, err = client.Catalog().Services(nil) - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(services, expected); diff == nil { - return services - } - time.Sleep(time.Second) - } - t.Fatalf("Catalog Services never reached: got: %v, expected state: %v", services, expected) - return nil - } - - shutdownCh := make(chan struct{}) - defer func() { - close(shutdownCh) - }() - const redirectAddr = "http://127.0.0.1:8200" - - // Create a ServiceRegistration that points to our consul instance - logger := logging.NewVaultLogger(log.Trace) - sd, err := NewServiceRegistration(map[string]string{ - "address": config.Address(), - "token": config.Token, - }, logger, sr.State{}) - if err != nil { - t.Fatal(err) - } - if err := sd.Run(shutdownCh, &sync.WaitGroup{}, redirectAddr); err != nil { - t.Fatal(err) - } - - // Create the core - inm, err := inmem.NewInmemHA(nil, logger) - if err != nil { - t.Fatal(err) - } - inmha, err := inmem.NewInmemHA(nil, logger) - if err != nil { - t.Fatal(err) - } - core, err := vault.NewCore(&vault.CoreConfig{ - ServiceRegistration: sd, - Physical: inm, - HAPhysical: inmha.(physical.HABackend), - RedirectAddr: redirectAddr, - DisableMlock: true, - }) - if err != nil { - t.Fatal(err) - } - defer core.Shutdown() - - waitForServices(t, map[string][]string{ - "consul": {}, - "vault": {"standby"}, - }) - - // Initialize and unseal the core - keys, _ := vault.TestCoreInit(t, core) - for _, key := range keys { - if _, err := vault.TestCoreUnseal(core, vault.TestKeyCopy(key)); err != nil { - t.Fatalf("unseal err: %s", err) - } - } - if core.Sealed() { - t.Fatal("should not be sealed") - } - - // Wait for the core to become active - vault.TestWaitActive(t, core) - - waitForServices(t, map[string][]string{ - "consul": {}, - "vault": {"active", "initialized"}, - }) -} - -func TestConsul_ServiceAddress(t *testing.T) { - tests := []struct { - consulConfig map[string]string - serviceAddrNil bool - }{ - { - consulConfig: map[string]string{ - "service_address": "", - }, - }, - { - consulConfig: map[string]string{ - "service_address": "vault.example.com", - }, - }, - { - serviceAddrNil: true, - }, - } - - for _, test := range tests { - shutdownCh := make(chan struct{}) - logger := logging.NewVaultLogger(log.Debug) - - be, err := NewServiceRegistration(test.consulConfig, logger, sr.State{}) - if err != nil { - t.Fatalf("expected Consul to initialize: %v", err) - } - if err := be.Run(shutdownCh, &sync.WaitGroup{}, ""); err != nil { - t.Fatal(err) - } - - c, ok := be.(*serviceRegistration) - if !ok { - t.Fatalf("Expected ConsulServiceRegistration") - } - - if test.serviceAddrNil { - if c.serviceAddress != nil { - t.Fatalf("expected service address to be nil") - } - } else { - if c.serviceAddress == nil { - t.Fatalf("did not expect service address to be nil") - } - } - close(shutdownCh) - } -} - -func TestConsul_newConsulServiceRegistration(t *testing.T) { - tests := []struct { - name string - consulConfig map[string]string - fail bool - redirectAddr string - checkTimeout time.Duration - path string - service string - address string - scheme string - token string - max_parallel int - disableReg bool - consistencyMode string - }{ - { - name: "Valid default config", - consulConfig: map[string]string{}, - checkTimeout: 5 * time.Second, - redirectAddr: "http://127.0.0.1:8200", - path: "vault/", - service: "vault", - address: "127.0.0.1:8500", - scheme: "http", - token: "", - max_parallel: 4, - disableReg: false, - consistencyMode: "default", - }, - { - name: "Valid modified config", - consulConfig: map[string]string{ - "path": "seaTech/", - "service": "astronomy", - "redirect_addr": "http://127.0.0.2:8200", - "check_timeout": "6s", - "address": "127.0.0.2", - "scheme": "https", - "token": "deadbeef-cafeefac-deadc0de-feedface", - "max_parallel": "4", - "disable_registration": "false", - "consistency_mode": "strong", - }, - checkTimeout: 6 * time.Second, - path: "seaTech/", - service: "astronomy", - redirectAddr: "http://127.0.0.2:8200", - address: "127.0.0.2", - scheme: "https", - token: "deadbeef-cafeefac-deadc0de-feedface", - max_parallel: 4, - consistencyMode: "strong", - }, - { - name: "Unix socket", - consulConfig: map[string]string{ - "address": "unix:///tmp/.consul.http.sock", - }, - address: "/tmp/.consul.http.sock", - scheme: "http", // Default, not overridden? - - // Defaults - checkTimeout: 5 * time.Second, - redirectAddr: "http://127.0.0.1:8200", - path: "vault/", - service: "vault", - token: "", - max_parallel: 4, - disableReg: false, - consistencyMode: "default", - }, - { - name: "Scheme in address", - consulConfig: map[string]string{ - "address": "https://127.0.0.2:5000", - }, - address: "127.0.0.2:5000", - scheme: "https", - - // Defaults - checkTimeout: 5 * time.Second, - redirectAddr: "http://127.0.0.1:8200", - path: "vault/", - service: "vault", - token: "", - max_parallel: 4, - disableReg: false, - consistencyMode: "default", - }, - { - name: "check timeout too short", - fail: true, - consulConfig: map[string]string{ - "check_timeout": "99ms", - }, - }, - } - - for _, test := range tests { - shutdownCh := make(chan struct{}) - logger := logging.NewVaultLogger(log.Debug) - - be, err := NewServiceRegistration(test.consulConfig, logger, sr.State{}) - if test.fail { - if err == nil { - t.Fatalf(`Expected config "%s" to fail`, test.name) - } else { - continue - } - } else if !test.fail && err != nil { - t.Fatalf("Expected config %s to not fail: %v", test.name, err) - } - if err := be.Run(shutdownCh, &sync.WaitGroup{}, ""); err != nil { - t.Fatal(err) - } - - c, ok := be.(*serviceRegistration) - if !ok { - t.Fatalf("Expected ConsulServiceRegistration: %s", test.name) - } - c.disableRegistration = true - - if !c.disableRegistration { - addr := os.Getenv("CONSUL_HTTP_ADDR") - if addr == "" { - continue - } - } - - if test.checkTimeout != c.checkTimeout { - t.Errorf("bad: %v != %v", test.checkTimeout, c.checkTimeout) - } - - if test.service != c.serviceName { - t.Errorf("bad: %v != %v", test.service, c.serviceName) - } - - // The configuration stored in the Consul "client" object is not exported, so - // we either have to skip validating it, or add a method to export it, or use reflection. - consulConfig := reflect.Indirect(reflect.ValueOf(c.Client)).FieldByName("config") - consulConfigScheme := consulConfig.FieldByName("Scheme").String() - consulConfigAddress := consulConfig.FieldByName("Address").String() - - if test.scheme != consulConfigScheme { - t.Errorf("bad scheme value: %v != %v", test.scheme, consulConfigScheme) - } - - if test.address != consulConfigAddress { - t.Errorf("bad address value: %v != %v", test.address, consulConfigAddress) - } - - // FIXME(sean@): Unable to test max_parallel - // if test.max_parallel != cap(c.permitPool) { - // t.Errorf("bad: %v != %v", test.max_parallel, cap(c.permitPool)) - // } - close(shutdownCh) - } -} - -func TestConsul_serviceTags(t *testing.T) { - tests := []struct { - active bool - perfStandby bool - initialized bool - tags []string - }{ - { - active: true, - perfStandby: false, - initialized: false, - tags: []string{"active"}, - }, - { - active: false, - perfStandby: false, - initialized: false, - tags: []string{"standby"}, - }, - { - active: false, - perfStandby: true, - initialized: false, - tags: []string{"performance-standby"}, - }, - { - active: true, - perfStandby: true, - initialized: false, - tags: []string{"performance-standby"}, - }, - { - active: true, - perfStandby: false, - initialized: true, - tags: []string{"active", "initialized"}, - }, - { - active: false, - perfStandby: false, - initialized: true, - tags: []string{"standby", "initialized"}, - }, - { - active: false, - perfStandby: true, - initialized: true, - tags: []string{"performance-standby", "initialized"}, - }, - { - active: true, - perfStandby: true, - initialized: true, - tags: []string{"performance-standby", "initialized"}, - }, - } - - c := testConsulServiceRegistration(t) - - for _, test := range tests { - tags := c.fetchServiceTags(test.active, test.perfStandby, test.initialized) - if !reflect.DeepEqual(tags[:], test.tags[:]) { - t.Errorf("Bad %v: %v %v", test.active, tags, test.tags) - } - } -} - -func TestConsul_setRedirectAddr(t *testing.T) { - tests := []struct { - addr string - host string - port int64 - pass bool - }{ - { - addr: "http://127.0.0.1:8200/", - host: "127.0.0.1", - port: 8200, - pass: true, - }, - { - addr: "http://127.0.0.1:8200", - host: "127.0.0.1", - port: 8200, - pass: true, - }, - { - addr: "https://127.0.0.1:8200", - host: "127.0.0.1", - port: 8200, - pass: true, - }, - { - addr: "unix:///tmp/.vault.addr.sock", - host: "/tmp/.vault.addr.sock", - port: -1, - pass: true, - }, - { - addr: "127.0.0.1:8200", - pass: false, - }, - { - addr: "127.0.0.1", - pass: false, - }, - } - for _, test := range tests { - c := testConsulServiceRegistration(t) - err := c.setRedirectAddr(test.addr) - if test.pass { - if err != nil { - t.Fatalf("bad: %v", err) - } - } else { - if err == nil { - t.Fatalf("bad, expected fail") - } else { - continue - } - } - - if c.redirectHost != test.host { - t.Fatalf("bad: %v != %v", c.redirectHost, test.host) - } - - if c.redirectPort != test.port { - t.Fatalf("bad: %v != %v", c.redirectPort, test.port) - } - } -} - -func TestConsul_serviceID(t *testing.T) { - tests := []struct { - name string - redirectAddr string - serviceName string - expected string - valid bool - }{ - { - name: "valid host w/o slash", - redirectAddr: "http://127.0.0.1:8200", - serviceName: "sea-tech-astronomy", - expected: "sea-tech-astronomy:127.0.0.1:8200", - valid: true, - }, - { - name: "valid host w/ slash", - redirectAddr: "http://127.0.0.1:8200/", - serviceName: "sea-tech-astronomy", - expected: "sea-tech-astronomy:127.0.0.1:8200", - valid: true, - }, - { - name: "valid https host w/ slash", - redirectAddr: "https://127.0.0.1:8200/", - serviceName: "sea-tech-astronomy", - expected: "sea-tech-astronomy:127.0.0.1:8200", - valid: true, - }, - { - name: "invalid host name", - redirectAddr: "https://127.0.0.1:8200/", - serviceName: "sea_tech_astronomy", - expected: "", - valid: false, - }, - } - - logger := logging.NewVaultLogger(log.Debug) - - for _, test := range tests { - shutdownCh := make(chan struct{}) - be, err := NewServiceRegistration(consulConf{ - "service": test.serviceName, - }, logger, sr.State{}) - if !test.valid { - if err == nil { - t.Fatalf("expected an error initializing for name %q", test.serviceName) - } - continue - } - if test.valid && err != nil { - t.Fatalf("expected Consul to initialize: %v", err) - } - if err := be.Run(shutdownCh, &sync.WaitGroup{}, ""); err != nil { - t.Fatal(err) - } - - c, ok := be.(*serviceRegistration) - if !ok { - t.Fatalf("Expected serviceRegistration") - } - - if err := c.setRedirectAddr(test.redirectAddr); err != nil { - t.Fatalf("bad: %s %v", test.name, err) - } - - serviceID := c.serviceID() - if serviceID != test.expected { - t.Fatalf("bad: %v != %v", serviceID, test.expected) - } - } -} diff --git a/serviceregistration/kubernetes/client/client_test.go b/serviceregistration/kubernetes/client/client_test.go deleted file mode 100644 index a02749e39..000000000 --- a/serviceregistration/kubernetes/client/client_test.go +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package client - -import ( - "errors" - "os" - "testing" - - "github.com/hashicorp/go-hclog" - kubetest "github.com/hashicorp/vault/serviceregistration/kubernetes/testing" -) - -func TestClient(t *testing.T) { - testState, testConf, closeFunc := kubetest.Server(t) - defer closeFunc() - - Scheme = testConf.ClientScheme - TokenFile = testConf.PathToTokenFile - RootCAFile = testConf.PathToRootCAFile - if err := os.Setenv(EnvVarKubernetesServiceHost, testConf.ServiceHost); err != nil { - t.Fatal(err) - } - if err := os.Setenv(EnvVarKubernetesServicePort, testConf.ServicePort); err != nil { - t.Fatal(err) - } - - client, err := New(hclog.Default()) - if err != nil { - t.Fatal(err) - } - e := &env{ - client: client, - testState: testState, - } - e.TestGetPod(t) - e.TestGetPodNotFound(t) - e.TestUpdatePodTags(t) - e.TestUpdatePodTagsNotFound(t) -} - -type env struct { - client *Client - testState *kubetest.State -} - -func (e *env) TestGetPod(t *testing.T) { - pod, err := e.client.GetPod(kubetest.ExpectedNamespace, kubetest.ExpectedPodName) - if err != nil { - t.Fatal(err) - } - if pod.Metadata.Name != "shell-demo" { - t.Fatalf("expected %q but received %q", "shell-demo", pod.Metadata.Name) - } -} - -func (e *env) TestGetPodNotFound(t *testing.T) { - _, err := e.client.GetPod(kubetest.ExpectedNamespace, "no-exist") - if err == nil { - t.Fatal("expected error because pod is unfound") - } - if wrapped := errors.Unwrap(err); wrapped != nil { - err = wrapped - } - if _, ok := err.(*ErrNotFound); !ok { - t.Fatalf("expected *ErrNotFound but received %T (%s)", err, err) - } -} - -func (e *env) TestUpdatePodTags(t *testing.T) { - if err := e.client.PatchPod(kubetest.ExpectedNamespace, kubetest.ExpectedPodName, &Patch{ - Operation: Add, - Path: "/metadata/labels/fizz", - Value: "buzz", - }); err != nil { - t.Fatal(err) - } - if e.testState.NumPatches() != 1 { - t.Fatalf("expected 1 label but received %+v", e.testState) - } - if e.testState.Get("/metadata/labels/fizz")["value"] != "buzz" { - t.Fatalf("expected buzz but received %q", e.testState.Get("fizz")["value"]) - } - if e.testState.Get("/metadata/labels/fizz")["op"] != "add" { - t.Fatalf("expected add but received %q", e.testState.Get("fizz")["op"]) - } -} - -func (e *env) TestUpdatePodTagsNotFound(t *testing.T) { - err := e.client.PatchPod(kubetest.ExpectedNamespace, "no-exist", &Patch{ - Operation: Add, - Path: "/metadata/labels/fizz", - Value: "buzz", - }) - if err == nil { - t.Fatal("expected error because pod is unfound") - } - if wrapped := errors.Unwrap(err); wrapped != nil { - err = wrapped - } - if _, ok := err.(*ErrNotFound); !ok { - t.Fatalf("expected *ErrNotFound but received %T", err) - } -} - -func TestSanitize(t *testing.T) { - expected := "fizz-buzz" - result := Sanitize("fizz+buzz") - if result != expected { - t.Fatalf("expected %q but received %q", expected, result) - } - - expected = "fizz_buzz" - result = Sanitize("fizz_buzz") - if result != expected { - t.Fatalf("expected %q but received %q", expected, result) - } - - expected = "fizz.buzz" - result = Sanitize("fizz.buzz") - if result != expected { - t.Fatalf("expected %q but received %q", expected, result) - } - - expected = "fizz-buzz" - result = Sanitize("fizz-buzz") - if result != expected { - t.Fatalf("expected %q but received %q", expected, result) - } - - expected = "123--fhd" - result = Sanitize("123-*fhd") - if result != expected { - t.Fatalf("expected %q but received %q", expected, result) - } - - expected = "1.4.0-beta1-ent" - result = Sanitize("1.4.0-beta1+ent") - if result != expected { - t.Fatalf("expected %q but received %q", expected, result) - } -} diff --git a/serviceregistration/kubernetes/retry_handler_test.go b/serviceregistration/kubernetes/retry_handler_test.go deleted file mode 100644 index 19f1c5b31..000000000 --- a/serviceregistration/kubernetes/retry_handler_test.go +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package kubernetes - -import ( - "os" - "sync" - "testing" - "time" - - "github.com/hashicorp/go-hclog" - sr "github.com/hashicorp/vault/serviceregistration" - "github.com/hashicorp/vault/serviceregistration/kubernetes/client" - kubetest "github.com/hashicorp/vault/serviceregistration/kubernetes/testing" -) - -func TestRetryHandlerSimple(t *testing.T) { - if testing.Short() { - t.Skip("skipping because this test takes 10-15 seconds") - } - - testState, testConf, closeFunc := kubetest.Server(t) - defer closeFunc() - - client.Scheme = testConf.ClientScheme - client.TokenFile = testConf.PathToTokenFile - client.RootCAFile = testConf.PathToRootCAFile - if err := os.Setenv(client.EnvVarKubernetesServiceHost, testConf.ServiceHost); err != nil { - t.Fatal(err) - } - if err := os.Setenv(client.EnvVarKubernetesServicePort, testConf.ServicePort); err != nil { - t.Fatal(err) - } - - logger := hclog.NewNullLogger() - shutdownCh := make(chan struct{}) - wait := &sync.WaitGroup{} - - c, err := client.New(logger) - if err != nil { - t.Fatal(err) - } - - r := &retryHandler{ - logger: logger, - namespace: kubetest.ExpectedNamespace, - podName: kubetest.ExpectedPodName, - patchesToRetry: make(map[string]*client.Patch), - client: c, - initialState: sr.State{}, - } - r.Run(shutdownCh, wait) - - // Initial number of patches upon Run from setting the initial state - initStatePatches := testState.NumPatches() - if initStatePatches == 0 { - t.Fatalf("expected number of states patches after initial patches to be non-zero") - } - - // Send a new patch - testPatch := &client.Patch{ - Operation: client.Add, - Path: "patch-path", - Value: "true", - } - r.Notify(testPatch) - - // Wait ample until the next try should have occurred. - <-time.NewTimer(retryFreq * 2).C - - if testState.NumPatches() != initStatePatches+1 { - t.Fatalf("expected 1 patch, got: %d", testState.NumPatches()) - } -} - -func TestRetryHandlerAdd(t *testing.T) { - _, testConf, closeFunc := kubetest.Server(t) - defer closeFunc() - - client.Scheme = testConf.ClientScheme - client.TokenFile = testConf.PathToTokenFile - client.RootCAFile = testConf.PathToRootCAFile - if err := os.Setenv(client.EnvVarKubernetesServiceHost, testConf.ServiceHost); err != nil { - t.Fatal(err) - } - if err := os.Setenv(client.EnvVarKubernetesServicePort, testConf.ServicePort); err != nil { - t.Fatal(err) - } - - logger := hclog.NewNullLogger() - c, err := client.New(logger) - if err != nil { - t.Fatal(err) - } - - r := &retryHandler{ - logger: hclog.NewNullLogger(), - namespace: "some-namespace", - podName: "some-pod-name", - patchesToRetry: make(map[string]*client.Patch), - client: c, - } - - testPatch1 := &client.Patch{ - Operation: client.Add, - Path: "one", - Value: "true", - } - testPatch2 := &client.Patch{ - Operation: client.Add, - Path: "two", - Value: "true", - } - testPatch3 := &client.Patch{ - Operation: client.Add, - Path: "three", - Value: "true", - } - testPatch4 := &client.Patch{ - Operation: client.Add, - Path: "four", - Value: "true", - } - - // Should be able to add all 4 patches. - r.Notify(testPatch1) - if len(r.patchesToRetry) != 1 { - t.Fatal("expected 1 patch") - } - - r.Notify(testPatch2) - if len(r.patchesToRetry) != 2 { - t.Fatal("expected 2 patches") - } - - r.Notify(testPatch3) - if len(r.patchesToRetry) != 3 { - t.Fatal("expected 3 patches") - } - - r.Notify(testPatch4) - if len(r.patchesToRetry) != 4 { - t.Fatal("expected 4 patches") - } - - // Adding a dupe should result in no change. - r.Notify(testPatch4) - if len(r.patchesToRetry) != 4 { - t.Fatal("expected 4 patches") - } - - // Adding a reversion should result in its twin being subtracted. - r.Notify(&client.Patch{ - Operation: client.Add, - Path: "four", - Value: "false", - }) - if len(r.patchesToRetry) != 4 { - t.Fatal("expected 4 patches") - } - - r.Notify(&client.Patch{ - Operation: client.Add, - Path: "three", - Value: "false", - }) - if len(r.patchesToRetry) != 4 { - t.Fatal("expected 4 patches") - } - - r.Notify(&client.Patch{ - Operation: client.Add, - Path: "two", - Value: "false", - }) - if len(r.patchesToRetry) != 4 { - t.Fatal("expected 4 patches") - } - - r.Notify(&client.Patch{ - Operation: client.Add, - Path: "one", - Value: "false", - }) - if len(r.patchesToRetry) != 4 { - t.Fatal("expected 4 patches") - } -} - -// This is meant to be run with the -race flag on. -func TestRetryHandlerRacesAndDeadlocks(t *testing.T) { - _, testConf, closeFunc := kubetest.Server(t) - defer closeFunc() - - client.Scheme = testConf.ClientScheme - client.TokenFile = testConf.PathToTokenFile - client.RootCAFile = testConf.PathToRootCAFile - if err := os.Setenv(client.EnvVarKubernetesServiceHost, testConf.ServiceHost); err != nil { - t.Fatal(err) - } - if err := os.Setenv(client.EnvVarKubernetesServicePort, testConf.ServicePort); err != nil { - t.Fatal(err) - } - - logger := hclog.NewNullLogger() - shutdownCh := make(chan struct{}) - wait := &sync.WaitGroup{} - testPatch := &client.Patch{ - Operation: client.Add, - Path: "patch-path", - Value: "true", - } - - c, err := client.New(logger) - if err != nil { - t.Fatal(err) - } - - r := &retryHandler{ - logger: logger, - namespace: kubetest.ExpectedNamespace, - podName: kubetest.ExpectedPodName, - patchesToRetry: make(map[string]*client.Patch), - initialState: sr.State{}, - client: c, - } - - // Now hit it as quickly as possible to see if we can produce - // races or deadlocks. - start := make(chan struct{}) - done := make(chan bool) - numRoutines := 100 - for i := 0; i < numRoutines; i++ { - go func() { - <-start - r.Notify(testPatch) - done <- true - }() - go func() { - <-start - r.Run(shutdownCh, wait) - done <- true - }() - } - close(start) - - // Allow up to 5 seconds for everything to finish. - timer := time.NewTimer(5 * time.Second) - for i := 0; i < numRoutines*2; i++ { - select { - case <-timer.C: - t.Fatal("test took too long to complete, check for deadlock") - case <-done: - } - } -} - -// In this test, the API server sends bad responses for 5 seconds, -// then sends good responses, and we make sure we get the expected behavior. -func TestRetryHandlerAPIConnectivityProblemsInitialState(t *testing.T) { - if testing.Short() { - t.Skip() - } - - testState, testConf, closeFunc := kubetest.Server(t) - defer closeFunc() - kubetest.ReturnGatewayTimeouts.Store(true) - - client.Scheme = testConf.ClientScheme - client.TokenFile = testConf.PathToTokenFile - client.RootCAFile = testConf.PathToRootCAFile - client.RetryMax = 0 - if err := os.Setenv(client.EnvVarKubernetesServiceHost, testConf.ServiceHost); err != nil { - t.Fatal(err) - } - if err := os.Setenv(client.EnvVarKubernetesServicePort, testConf.ServicePort); err != nil { - t.Fatal(err) - } - - shutdownCh := make(chan struct{}) - wait := &sync.WaitGroup{} - reg, err := NewServiceRegistration(map[string]string{ - "namespace": kubetest.ExpectedNamespace, - "pod_name": kubetest.ExpectedPodName, - }, hclog.NewNullLogger(), sr.State{ - VaultVersion: "vault-version", - IsInitialized: true, - IsSealed: true, - IsActive: true, - IsPerformanceStandby: true, - }) - if err != nil { - t.Fatal(err) - } - if err := reg.Run(shutdownCh, wait, ""); err != nil { - t.Fatal(err) - } - - // At this point, since the initial state can't be set, - // remotely we should have false for all these labels. - patch := testState.Get(pathToLabels + labelVaultVersion) - if patch != nil { - t.Fatal("expected no value") - } - patch = testState.Get(pathToLabels + labelActive) - if patch != nil { - t.Fatal("expected no value") - } - patch = testState.Get(pathToLabels + labelSealed) - if patch != nil { - t.Fatal("expected no value") - } - patch = testState.Get(pathToLabels + labelPerfStandby) - if patch != nil { - t.Fatal("expected no value") - } - patch = testState.Get(pathToLabels + labelInitialized) - if patch != nil { - t.Fatal("expected no value") - } - - kubetest.ReturnGatewayTimeouts.Store(false) - - // Now we need to wait to give the retry handler - // a chance to update these values. - time.Sleep(retryFreq + time.Second) - val := testState.Get(pathToLabels + labelVaultVersion)["value"] - if val != "vault-version" { - t.Fatal("expected vault-version") - } - val = testState.Get(pathToLabels + labelActive)["value"] - if val != "true" { - t.Fatal("expected true") - } - val = testState.Get(pathToLabels + labelSealed)["value"] - if val != "true" { - t.Fatal("expected true") - } - val = testState.Get(pathToLabels + labelPerfStandby)["value"] - if val != "true" { - t.Fatal("expected true") - } - val = testState.Get(pathToLabels + labelInitialized)["value"] - if val != "true" { - t.Fatal("expected true") - } -} - -// In this test, the API server sends bad responses for 5 seconds, -// then sends good responses, and we make sure we get the expected behavior. -func TestRetryHandlerAPIConnectivityProblemsNotifications(t *testing.T) { - if testing.Short() { - t.Skip() - } - - testState, testConf, closeFunc := kubetest.Server(t) - defer closeFunc() - kubetest.ReturnGatewayTimeouts.Store(true) - - client.Scheme = testConf.ClientScheme - client.TokenFile = testConf.PathToTokenFile - client.RootCAFile = testConf.PathToRootCAFile - client.RetryMax = 0 - if err := os.Setenv(client.EnvVarKubernetesServiceHost, testConf.ServiceHost); err != nil { - t.Fatal(err) - } - if err := os.Setenv(client.EnvVarKubernetesServicePort, testConf.ServicePort); err != nil { - t.Fatal(err) - } - - shutdownCh := make(chan struct{}) - wait := &sync.WaitGroup{} - reg, err := NewServiceRegistration(map[string]string{ - "namespace": kubetest.ExpectedNamespace, - "pod_name": kubetest.ExpectedPodName, - }, hclog.NewNullLogger(), sr.State{ - VaultVersion: "vault-version", - IsInitialized: false, - IsSealed: false, - IsActive: false, - IsPerformanceStandby: false, - }) - if err != nil { - t.Fatal(err) - } - - if err := reg.NotifyActiveStateChange(true); err != nil { - t.Fatal(err) - } - if err := reg.NotifyInitializedStateChange(true); err != nil { - t.Fatal(err) - } - if err := reg.NotifyPerformanceStandbyStateChange(true); err != nil { - t.Fatal(err) - } - if err := reg.NotifySealedStateChange(true); err != nil { - t.Fatal(err) - } - - if err := reg.Run(shutdownCh, wait, ""); err != nil { - t.Fatal(err) - } - - // At this point, since the initial state can't be set, - // remotely we should have false for all these labels. - patch := testState.Get(pathToLabels + labelVaultVersion) - if patch != nil { - t.Fatal("expected no value") - } - patch = testState.Get(pathToLabels + labelActive) - if patch != nil { - t.Fatal("expected no value") - } - patch = testState.Get(pathToLabels + labelSealed) - if patch != nil { - t.Fatal("expected no value") - } - patch = testState.Get(pathToLabels + labelPerfStandby) - if patch != nil { - t.Fatal("expected no value") - } - patch = testState.Get(pathToLabels + labelInitialized) - if patch != nil { - t.Fatal("expected no value") - } - - kubetest.ReturnGatewayTimeouts.Store(false) - - // Now we need to wait to give the retry handler - // a chance to update these values. - time.Sleep(retryFreq + time.Second) - - // They should be "true" if the Notifications were set after the - // initial state. - val := testState.Get(pathToLabels + labelVaultVersion)["value"] - if val != "vault-version" { - t.Fatal("expected vault-version") - } - val = testState.Get(pathToLabels + labelActive)["value"] - if val != "true" { - t.Fatal("expected true") - } - val = testState.Get(pathToLabels + labelSealed)["value"] - if val != "true" { - t.Fatal("expected true") - } - val = testState.Get(pathToLabels + labelPerfStandby)["value"] - if val != "true" { - t.Fatal("expected true") - } - val = testState.Get(pathToLabels + labelInitialized)["value"] - if val != "true" { - t.Fatal("expected true") - } -} diff --git a/serviceregistration/kubernetes/service_registration_test.go b/serviceregistration/kubernetes/service_registration_test.go deleted file mode 100644 index 808807d1a..000000000 --- a/serviceregistration/kubernetes/service_registration_test.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package kubernetes - -import ( - "os" - "strconv" - "sync" - "testing" - - "github.com/hashicorp/go-hclog" - sr "github.com/hashicorp/vault/serviceregistration" - "github.com/hashicorp/vault/serviceregistration/kubernetes/client" - kubetest "github.com/hashicorp/vault/serviceregistration/kubernetes/testing" -) - -var testVersion = "version1" - -func TestServiceRegistration(t *testing.T) { - testState, testConf, closeFunc := kubetest.Server(t) - defer closeFunc() - - client.Scheme = testConf.ClientScheme - client.TokenFile = testConf.PathToTokenFile - client.RootCAFile = testConf.PathToRootCAFile - if err := os.Setenv(client.EnvVarKubernetesServiceHost, testConf.ServiceHost); err != nil { - t.Fatal(err) - } - if err := os.Setenv(client.EnvVarKubernetesServicePort, testConf.ServicePort); err != nil { - t.Fatal(err) - } - - if testState.NumPatches() != 0 { - t.Fatalf("expected 0 patches but have %d: %+v", testState.NumPatches(), testState) - } - shutdownCh := make(chan struct{}) - config := map[string]string{ - "namespace": kubetest.ExpectedNamespace, - "pod_name": kubetest.ExpectedPodName, - } - logger := hclog.NewNullLogger() - state := sr.State{ - VaultVersion: testVersion, - IsInitialized: true, - IsSealed: true, - IsActive: true, - IsPerformanceStandby: true, - } - reg, err := NewServiceRegistration(config, logger, state) - if err != nil { - t.Fatal(err) - } - if err := reg.Run(shutdownCh, &sync.WaitGroup{}, ""); err != nil { - t.Fatal(err) - } - - // Test initial state. - if testState.NumPatches() != 5 { - t.Fatalf("expected 5 current labels but have %d: %+v", testState.NumPatches(), testState) - } - if testState.Get(pathToLabels + labelVaultVersion)["value"] != testVersion { - t.Fatalf("expected %q but received %q", testVersion, testState.Get(pathToLabels + labelVaultVersion)["value"]) - } - if testState.Get(pathToLabels + labelActive)["value"] != strconv.FormatBool(true) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(true), testState.Get(pathToLabels + labelActive)["value"]) - } - if testState.Get(pathToLabels + labelSealed)["value"] != strconv.FormatBool(true) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(true), testState.Get(pathToLabels + labelSealed)["value"]) - } - if testState.Get(pathToLabels + labelPerfStandby)["value"] != strconv.FormatBool(true) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(true), testState.Get(pathToLabels + labelPerfStandby)["value"]) - } - if testState.Get(pathToLabels + labelInitialized)["value"] != strconv.FormatBool(true) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(true), testState.Get(pathToLabels + labelInitialized)["value"]) - } - - // Test NotifyActiveStateChange. - if err := reg.NotifyActiveStateChange(false); err != nil { - t.Fatal(err) - } - if testState.Get(pathToLabels + labelActive)["value"] != strconv.FormatBool(false) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(false), testState.Get(pathToLabels + labelActive)["value"]) - } - if err := reg.NotifyActiveStateChange(true); err != nil { - t.Fatal(err) - } - if testState.Get(pathToLabels + labelActive)["value"] != strconv.FormatBool(true) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(true), testState.Get(pathToLabels + labelActive)["value"]) - } - - // Test NotifySealedStateChange. - if err := reg.NotifySealedStateChange(false); err != nil { - t.Fatal(err) - } - if testState.Get(pathToLabels + labelSealed)["value"] != strconv.FormatBool(false) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(false), testState.Get(pathToLabels + labelSealed)["value"]) - } - if err := reg.NotifySealedStateChange(true); err != nil { - t.Fatal(err) - } - if testState.Get(pathToLabels + labelSealed)["value"] != strconv.FormatBool(true) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(true), testState.Get(pathToLabels + labelSealed)["value"]) - } - - // Test NotifyPerformanceStandbyStateChange. - if err := reg.NotifyPerformanceStandbyStateChange(false); err != nil { - t.Fatal(err) - } - if testState.Get(pathToLabels + labelPerfStandby)["value"] != strconv.FormatBool(false) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(false), testState.Get(pathToLabels + labelPerfStandby)["value"]) - } - if err := reg.NotifyPerformanceStandbyStateChange(true); err != nil { - t.Fatal(err) - } - if testState.Get(pathToLabels + labelPerfStandby)["value"] != strconv.FormatBool(true) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(true), testState.Get(pathToLabels + labelPerfStandby)["value"]) - } - - // Test NotifyInitializedStateChange. - if err := reg.NotifyInitializedStateChange(false); err != nil { - t.Fatal(err) - } - if testState.Get(pathToLabels + labelInitialized)["value"] != strconv.FormatBool(false) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(false), testState.Get(pathToLabels + labelInitialized)["value"]) - } - if err := reg.NotifyInitializedStateChange(true); err != nil { - t.Fatal(err) - } - if testState.Get(pathToLabels + labelInitialized)["value"] != strconv.FormatBool(true) { - t.Fatalf("expected %q but received %q", strconv.FormatBool(true), testState.Get(pathToLabels + labelInitialized)["value"]) - } -} diff --git a/serviceregistration/kubernetes/testing/README.md b/serviceregistration/kubernetes/testing/README.md deleted file mode 100644 index 940415b6b..000000000 --- a/serviceregistration/kubernetes/testing/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# How to Test Manually - -- `$ minikube start` -- In the Vault folder, `$ make dev XC_ARCH=amd64 XC_OS=linux XC_OSARCH=linux/amd64` -- Create a file called `vault-test.yaml` with the following contents: - -``` -apiVersion: v1 -kind: Pod -metadata: - name: vault -spec: - containers: - - name: nginx - image: nginx - command: [ "sh", "-c"] - args: - - while true; do - echo -en '\n'; - printenv VAULT_K8S_POD_NAME VAULT_K8S_NAMESPACE; - sleep 10; - done; - env: - - name: VAULT_K8S_POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: VAULT_K8S_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - restartPolicy: Never -``` - -- Create the pod: `$ kubectl apply -f vault-test.yaml` -- View the full initial state of the pod: `$ kubectl get pod vault -o=yaml > initialstate.txt` -- Drop the Vault binary into the pod: `$ kubectl cp bin/vault /vault:/` -- Drop to the shell within the pod: `$ kubectl exec -it vault -- /bin/bash` -- Install a text editor: `$ apt-get update`, `$ apt-get install nano` -- Write a test Vault config to `vault.config` like: - -``` -storage "inmem" {} -service_registration "kubernetes" {} -disable_mlock = true -ui = true -api_addr = "http://127.0.0.1:8200" -log_level = "debug" -``` - -- Run Vault: `$ ./vault server -config=vault.config -dev -dev-root-token-id=root` -- If 403's are received, you may need to grant RBAC, example here: https://github.com/fabric8io/fabric8/issues/6840#issuecomment-307560275 -- In a separate window outside the pod, view the resulting state of the pod: `$ kubectl get pod vault -o=yaml > currentstate.txt` -- View the differences: `$ diff initialstate.txt currentstate.txt` diff --git a/serviceregistration/kubernetes/testing/ca.crt b/serviceregistration/kubernetes/testing/ca.crt deleted file mode 100644 index 077f48b86..000000000 --- a/serviceregistration/kubernetes/testing/ca.crt +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p -a3ViZUNBMB4XDTE5MTIxMDIzMDUxOVoXDTI5MTIwODIzMDUxOVowFTETMBEGA1UE -AxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANFi -/RIdMHd865X6JygTb9riX01DA3QnR+RoXDXNnj8D3LziLG2n8ItXMJvWbU3sxxyy -nX9HxJ0SIeexj1cYzdQBtJDjO1/PeuKc4CZ7zCukCAtHz8mC7BDPOU7F7pggpcQ0 -/t/pa2m22hmCu8aDF9WlUYHtJpYATnI/A5vz/VFLR9daxmkl59Qo3oHITj7vAzSx -/75r9cibpQyJ+FhiHOZHQWYY2JYw2g4v5hm5hg5SFM9yFcZ75ISI9ebyFFIl9iBY -zAk9jqv1mXvLr0Q39AVwMTamvGuap1oocjM9NIhQvaFL/DNqF1ouDQjCf5u2imLc -TraO1/2KO8fqwOZCOrMCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW -MBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 -DQEBCwUAA4IBAQBtVZCwCPqUUUpIClAlE9nc2fo2bTs9gsjXRmqdQ5oaSomSLE93 -aJWYFuAhxPXtlApbLYZfW2m1sM3mTVQN60y0uE4e1jdSN1ErYQ9slJdYDAMaEmOh -iSexj+Nd1scUiMHV9lf3ps5J8sYeCpwZX3sPmw7lqZojTS12pANBDcigsaj5RRyN -9GyP3WkSQUsTpWlDb9Fd+KNdkCVw7nClIpBPA2KW4BQKw/rNSvOFD61mbzc89lo0 -Q9IFGQFFF8jO18lbyWqnRBGXcS4/G7jQ3S7C121d14YLUeAYOM7pJykI1g4CLx9y -vitin0L6nprauWkKO38XgM4T75qKZpqtiOcT ------END CERTIFICATE----- diff --git a/serviceregistration/kubernetes/testing/resp-get-pod.json b/serviceregistration/kubernetes/testing/resp-get-pod.json deleted file mode 100644 index 229eb1fbd..000000000 --- a/serviceregistration/kubernetes/testing/resp-get-pod.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "kind": "Pod", - "apiVersion": "v1", - "metadata": { - "name": "shell-demo", - "labels": {"fizz": "buzz"}, - "namespace": "default", - "selfLink": "/api/v1/namespaces/default/pods/shell-demo", - "uid": "7ecb93ff-aa64-426d-b330-2c0b2c0957a2", - "resourceVersion": "87798", - "creationTimestamp": "2020-01-10T19:22:40Z", - "annotations": { - "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"shell-demo\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx\",\"name\":\"nginx\",\"volumeMounts\":[{\"mountPath\":\"/usr/share/nginx/html\",\"name\":\"shared-data\"}]}],\"dnsPolicy\":\"Default\",\"hostNetwork\":true,\"volumes\":[{\"emptyDir\":{},\"name\":\"shared-data\"}]}}\n" - } - }, - "spec": { - "volumes": [{ - "name": "shared-data", - "emptyDir": {} - }, { - "name": "default-token-5fjt9", - "secret": { - "secretName": "default-token-5fjt9", - "defaultMode": 420 - } - }], - "containers": [{ - "name": "nginx", - "image": "nginx", - "resources": {}, - "volumeMounts": [{ - "name": "shared-data", - "mountPath": "/usr/share/nginx/html" - }, { - "name": "default-token-5fjt9", - "readOnly": true, - "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" - }], - "terminationMessagePath": "/dev/termination-log", - "terminationMessagePolicy": "File", - "imagePullPolicy": "Always" - }], - "restartPolicy": "Always", - "terminationGracePeriodSeconds": 30, - "dnsPolicy": "Default", - "serviceAccountName": "default", - "serviceAccount": "default", - "nodeName": "minikube", - "hostNetwork": true, - "securityContext": {}, - "schedulerName": "default-scheduler", - "tolerations": [{ - "key": "node.kubernetes.io/not-ready", - "operator": "Exists", - "effect": "NoExecute", - "tolerationSeconds": 300 - }, { - "key": "node.kubernetes.io/unreachable", - "operator": "Exists", - "effect": "NoExecute", - "tolerationSeconds": 300 - }], - "priority": 0, - "enableServiceLinks": true - }, - "status": { - "phase": "Running", - "conditions": [{ - "type": "Initialized", - "status": "True", - "lastProbeTime": null, - "lastTransitionTime": "2020-01-10T19:22:40Z" - }, { - "type": "Ready", - "status": "True", - "lastProbeTime": null, - "lastTransitionTime": "2020-01-10T20:20:55Z" - }, { - "type": "ContainersReady", - "status": "True", - "lastProbeTime": null, - "lastTransitionTime": "2020-01-10T20:20:55Z" - }, { - "type": "PodScheduled", - "status": "True", - "lastProbeTime": null, - "lastTransitionTime": "2020-01-10T19:22:40Z" - }], - "hostIP": "192.168.99.100", - "podIP": "192.168.99.100", - "podIPs": [{ - "ip": "192.168.99.100" - }], - "startTime": "2020-01-10T19:22:40Z", - "containerStatuses": [{ - "name": "nginx", - "state": { - "running": { - "startedAt": "2020-01-10T20:20:55Z" - } - }, - "lastState": { - "terminated": { - "exitCode": 0, - "reason": "Completed", - "startedAt": "2020-01-10T19:22:53Z", - "finishedAt": "2020-01-10T20:12:03Z", - "containerID": "docker://ed8bc068cd313ea5adb72780e8015ab09ecb61ea077e39304b4a3fe581f471c4" - } - }, - "ready": true, - "restartCount": 1, - "image": "nginx:latest", - "imageID": "docker-pullable://nginx@sha256:8aa7f6a9585d908a63e5e418dc5d14ae7467d2e36e1ab4f0d8f9d059a3d071ce", - "containerID": "docker://a8ee34466791bc6f082f271f40cdfc43625cea81831b1029b1e90b4f6949f6df", - "started": true - }], - "qosClass": "BestEffort" - } -} diff --git a/serviceregistration/kubernetes/testing/resp-not-found.json b/serviceregistration/kubernetes/testing/resp-not-found.json deleted file mode 100644 index 800a9624f..000000000 --- a/serviceregistration/kubernetes/testing/resp-not-found.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "kind": "Status", - "apiVersion": "v1", - "metadata": {}, - "status": "Failure", - "message": "pods \"shell-dem\" not found", - "reason": "NotFound", - "details": { - "name": "shell-dem", - "kind": "pods" - }, - "code": 404 -} diff --git a/serviceregistration/kubernetes/testing/resp-update-pod.json b/serviceregistration/kubernetes/testing/resp-update-pod.json deleted file mode 100644 index e808691f9..000000000 --- a/serviceregistration/kubernetes/testing/resp-update-pod.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "kind": "Pod", - "apiVersion": "v1", - "metadata": { - "name": "shell-demo", - "namespace": "default", - "selfLink": "/api/v1/namespaces/default/pods/shell-demo", - "uid": "7ecb93ff-aa64-426d-b330-2c0b2c0957a2", - "resourceVersion": "96433", - "creationTimestamp": "2020-01-10T19:22:40Z", - "labels": { - "fizz": "buzz", - "foo": "bar" - }, - "annotations": { - "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"shell-demo\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx\",\"name\":\"nginx\",\"volumeMounts\":[{\"mountPath\":\"/usr/share/nginx/html\",\"name\":\"shared-data\"}]}],\"dnsPolicy\":\"Default\",\"hostNetwork\":true,\"volumes\":[{\"emptyDir\":{},\"name\":\"shared-data\"}]}}\n" - } - }, - "spec": { - "volumes": [{ - "name": "shared-data", - "emptyDir": {} - }, { - "name": "default-token-5fjt9", - "secret": { - "secretName": "default-token-5fjt9", - "defaultMode": 420 - } - }], - "containers": [{ - "name": "nginx", - "image": "nginx", - "resources": {}, - "volumeMounts": [{ - "name": "shared-data", - "mountPath": "/usr/share/nginx/html" - }, { - "name": "default-token-5fjt9", - "readOnly": true, - "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" - }], - "terminationMessagePath": "/dev/termination-log", - "terminationMessagePolicy": "File", - "imagePullPolicy": "Always" - }], - "restartPolicy": "Always", - "terminationGracePeriodSeconds": 30, - "dnsPolicy": "Default", - "serviceAccountName": "default", - "serviceAccount": "default", - "nodeName": "minikube", - "hostNetwork": true, - "securityContext": {}, - "schedulerName": "default-scheduler", - "tolerations": [{ - "key": "node.kubernetes.io/not-ready", - "operator": "Exists", - "effect": "NoExecute", - "tolerationSeconds": 300 - }, { - "key": "node.kubernetes.io/unreachable", - "operator": "Exists", - "effect": "NoExecute", - "tolerationSeconds": 300 - }], - "priority": 0, - "enableServiceLinks": true - }, - "status": { - "phase": "Running", - "conditions": [{ - "type": "Initialized", - "status": "True", - "lastProbeTime": null, - "lastTransitionTime": "2020-01-10T19:22:40Z" - }, { - "type": "Ready", - "status": "True", - "lastProbeTime": null, - "lastTransitionTime": "2020-01-10T20:20:55Z" - }, { - "type": "ContainersReady", - "status": "True", - "lastProbeTime": null, - "lastTransitionTime": "2020-01-10T20:20:55Z" - }, { - "type": "PodScheduled", - "status": "True", - "lastProbeTime": null, - "lastTransitionTime": "2020-01-10T19:22:40Z" - }], - "hostIP": "192.168.99.100", - "podIP": "192.168.99.100", - "podIPs": [{ - "ip": "192.168.99.100" - }], - "startTime": "2020-01-10T19:22:40Z", - "containerStatuses": [{ - "name": "nginx", - "state": { - "running": { - "startedAt": "2020-01-10T20:20:55Z" - } - }, - "lastState": { - "terminated": { - "exitCode": 0, - "reason": "Completed", - "startedAt": "2020-01-10T19:22:53Z", - "finishedAt": "2020-01-10T20:12:03Z", - "containerID": "docker://ed8bc068cd313ea5adb72780e8015ab09ecb61ea077e39304b4a3fe581f471c4" - } - }, - "ready": true, - "restartCount": 1, - "image": "nginx:latest", - "imageID": "docker-pullable://nginx@sha256:8aa7f6a9585d908a63e5e418dc5d14ae7467d2e36e1ab4f0d8f9d059a3d071ce", - "containerID": "docker://a8ee34466791bc6f082f271f40cdfc43625cea81831b1029b1e90b4f6949f6df", - "started": true - }], - "qosClass": "BestEffort" - } -} diff --git a/serviceregistration/kubernetes/testing/testserver.go b/serviceregistration/kubernetes/testing/testserver.go deleted file mode 100644 index 225431fc1..000000000 --- a/serviceregistration/kubernetes/testing/testserver.go +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package testing - -import ( - _ "embed" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path" - "strings" - "sync" - "testing" - - "go.uber.org/atomic" -) - -const ( - ExpectedNamespace = "default" - ExpectedPodName = "shell-demo" -) - -// Pull real-life-based testing data in from files at compile time. -// We decided to embed them in the test binary because of past issues -// with reading files that we encountered on CI workers. - -//go:embed ca.crt -var caCrt string - -//go:embed resp-get-pod.json -var getPodResponse string - -//go:embed resp-not-found.json -var notFoundResponse string - -//go:embed resp-update-pod.json -var updatePodTagsResponse string - -//go:embed token -var token string - -var ( - // ReturnGatewayTimeouts toggles whether the test server should return, - // well, gateway timeouts... - ReturnGatewayTimeouts = atomic.NewBool(false) - - pathToFiles = func() string { - wd, _ := os.Getwd() - repoName := "vault-enterprise" - if !strings.Contains(wd, repoName) { - repoName = "vault" - } - pathParts := strings.Split(wd, repoName) - return pathParts[0] + "vault/serviceregistration/kubernetes/testing/" - }() -) - -// Conf returns the info needed to configure the client to point at -// the test server. This must be done by the caller to avoid an import -// cycle between the client and the testserver. Example usage: -// -// client.Scheme = testConf.ClientScheme -// client.TokenFile = testConf.PathToTokenFile -// client.RootCAFile = testConf.PathToRootCAFile -// if err := os.Setenv(client.EnvVarKubernetesServiceHost, testConf.ServiceHost); err != nil { -// t.Fatal(err) -// } -// if err := os.Setenv(client.EnvVarKubernetesServicePort, testConf.ServicePort); err != nil { -// t.Fatal(err) -// } -type Conf struct { - ClientScheme, PathToTokenFile, PathToRootCAFile, ServiceHost, ServicePort string -} - -// Server returns an http test server that can be used to test -// Kubernetes client code. It also retains the current state, -// and a func to close the server and to clean up any temporary -// files. -func Server(t *testing.T) (testState *State, testConf *Conf, closeFunc func()) { - testState = &State{m: &sync.Map{}} - testConf = &Conf{ - ClientScheme: "http://", - } - - // We're going to have multiple close funcs to call. - var closers []func() - closeFunc = func() { - for _, closer := range closers { - closer() - } - } - - // Plant our token in a place where it can be read for the config. - tmpToken, err := ioutil.TempFile("", "token") - if err != nil { - t.Fatal(err) - } - closers = append(closers, func() { - os.Remove(tmpToken.Name()) - }) - if _, err = tmpToken.WriteString(token); err != nil { - closeFunc() - t.Fatal(err) - } - if err := tmpToken.Close(); err != nil { - closeFunc() - t.Fatal(err) - } - testConf.PathToTokenFile = tmpToken.Name() - - tmpCACrt, err := ioutil.TempFile("", "ca.crt") - if err != nil { - closeFunc() - t.Fatal(err) - } - closers = append(closers, func() { - os.Remove(tmpCACrt.Name()) - }) - if _, err = tmpCACrt.WriteString(caCrt); err != nil { - closeFunc() - t.Fatal(err) - } - if err := tmpCACrt.Close(); err != nil { - closeFunc() - t.Fatal(err) - } - testConf.PathToRootCAFile = tmpCACrt.Name() - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if ReturnGatewayTimeouts.Load() { - w.WriteHeader(504) - return - } - namespace, podName, err := parsePath(r.URL.Path) - if err != nil { - w.WriteHeader(400) - w.Write([]byte(fmt.Sprintf("unable to parse %s: %s", r.URL.Path, err.Error()))) - return - } - - switch { - case namespace != ExpectedNamespace, podName != ExpectedPodName: - w.WriteHeader(404) - w.Write([]byte(notFoundResponse)) - return - case r.Method == http.MethodGet: - w.WriteHeader(200) - w.Write([]byte(getPodResponse)) - return - case r.Method == http.MethodPatch: - var patches []interface{} - if err := json.NewDecoder(r.Body).Decode(&patches); err != nil { - w.WriteHeader(400) - w.Write([]byte(fmt.Sprintf("unable to decode patches %s: %s", r.URL.Path, err.Error()))) - return - } - for _, patch := range patches { - patchMap := patch.(map[string]interface{}) - p := patchMap["path"].(string) - testState.store(p, patchMap) - } - w.WriteHeader(200) - w.Write([]byte(updatePodTagsResponse)) - return - default: - w.WriteHeader(400) - w.Write([]byte(fmt.Sprintf("unexpected request method: %s", r.Method))) - } - })) - closers = append(closers, ts.Close) - - // ts.URL example: http://127.0.0.1:35681 - urlFields := strings.Split(ts.URL, "://") - if len(urlFields) != 2 { - closeFunc() - t.Fatal("received unexpected test url: " + ts.URL) - } - urlFields = strings.Split(urlFields[1], ":") - if len(urlFields) != 2 { - closeFunc() - t.Fatal("received unexpected test url: " + ts.URL) - } - testConf.ServiceHost = urlFields[0] - testConf.ServicePort = urlFields[1] - return testState, testConf, closeFunc -} - -type State struct { - m *sync.Map -} - -func (s *State) NumPatches() int { - l := 0 - f := func(key, value interface{}) bool { - l++ - return true - } - s.m.Range(f) - return l -} - -func (s *State) Get(key string) map[string]interface{} { - v, ok := s.m.Load(key) - if !ok { - return nil - } - patch, ok := v.(map[string]interface{}) - if !ok { - return nil - } - return patch -} - -func (s *State) store(k string, p map[string]interface{}) { - s.m.Store(k, p) -} - -// The path should be formatted like this: -// fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, podName) -func parsePath(urlPath string) (namespace, podName string, err error) { - original := urlPath - podName = path.Base(urlPath) - urlPath = strings.TrimSuffix(urlPath, "/pods/"+podName) - namespace = path.Base(urlPath) - if original != fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, podName) { - return "", "", fmt.Errorf("received unexpected path: %s", original) - } - return namespace, podName, nil -} - -func readFile(fileName string) (string, error) { - b, err := ioutil.ReadFile(pathToFiles + fileName) - if err != nil { - return "", err - } - return string(b), nil -} diff --git a/serviceregistration/kubernetes/testing/token b/serviceregistration/kubernetes/testing/token deleted file mode 100644 index 42d4949f1..000000000 --- a/serviceregistration/kubernetes/testing/token +++ /dev/null @@ -1 +0,0 @@ -eyJhbGciOiJSUzI1NiIsImtpZCI6IjZVQU91ckJYcTZKRHQtWHpaOExib2EyUlFZQWZObms2d25mY3ZtVm1NNUUifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tNWZqdDkiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImY0NGUyMDIxLTU2YWItNDEzNC1hMjMxLTBlMDJmNjhmNzJhNiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.hgMbuT0hlxG04fDvI_Iyxtbwc8M-i3q3K7CqIGC_jYSjVlyezHN_0BeIB3rE0_M2xvbIs6chsWFZVsK_8Pj6ho7VT0x5PWy5n6KsqTBz8LPpjWpsaxpYQos0RzgA3KLnuzZE8Cl-v-PwWQK57jgbS4AdlXujQXdtLXJNwNAKI0pvCASA6UXP55_X845EsJkyT1J-bURSS3Le3g9A4pDoQ_MUv7hqa-p7yQEtFfYCkq1KKrUJZMRjmS4qda1rg-Em-dw9RFvQtPodRYF0DKT7A7qgmLUfIkuky3NnsQtvaUo8ZVtUiwIEfRdqw1oQIY4CSYz-wUl2xZa7n2QQBROE7w \ No newline at end of file diff --git a/shamir/shamir_test.go b/shamir/shamir_test.go deleted file mode 100644 index 940a34ecf..000000000 --- a/shamir/shamir_test.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package shamir - -import ( - "bytes" - "testing" -) - -func TestSplit_invalid(t *testing.T) { - secret := []byte("test") - - if _, err := Split(secret, 0, 0); err == nil { - t.Fatalf("expect error") - } - - if _, err := Split(secret, 2, 3); err == nil { - t.Fatalf("expect error") - } - - if _, err := Split(secret, 1000, 3); err == nil { - t.Fatalf("expect error") - } - - if _, err := Split(secret, 10, 1); err == nil { - t.Fatalf("expect error") - } - - if _, err := Split(nil, 3, 2); err == nil { - t.Fatalf("expect error") - } -} - -func TestSplit(t *testing.T) { - secret := []byte("test") - - out, err := Split(secret, 5, 3) - if err != nil { - t.Fatalf("err: %v", err) - } - - if len(out) != 5 { - t.Fatalf("bad: %v", out) - } - - for _, share := range out { - if len(share) != len(secret)+1 { - t.Fatalf("bad: %v", out) - } - } -} - -func TestCombine_invalid(t *testing.T) { - // Not enough parts - if _, err := Combine(nil); err == nil { - t.Fatalf("should err") - } - - // Mis-match in length - parts := [][]byte{ - []byte("foo"), - []byte("ba"), - } - if _, err := Combine(parts); err == nil { - t.Fatalf("should err") - } - - // Too short - parts = [][]byte{ - []byte("f"), - []byte("b"), - } - if _, err := Combine(parts); err == nil { - t.Fatalf("should err") - } - - parts = [][]byte{ - []byte("foo"), - []byte("foo"), - } - if _, err := Combine(parts); err == nil { - t.Fatalf("should err") - } -} - -func TestCombine(t *testing.T) { - secret := []byte("test") - - out, err := Split(secret, 5, 3) - if err != nil { - t.Fatalf("err: %v", err) - } - - // There is 5*4*3 possible choices, - // we will just brute force try them all - for i := 0; i < 5; i++ { - for j := 0; j < 5; j++ { - if j == i { - continue - } - for k := 0; k < 5; k++ { - if k == i || k == j { - continue - } - parts := [][]byte{out[i], out[j], out[k]} - recomb, err := Combine(parts) - if err != nil { - t.Fatalf("err: %v", err) - } - - if !bytes.Equal(recomb, secret) { - t.Errorf("parts: (i:%d, j:%d, k:%d) %v", i, j, k, parts) - t.Fatalf("bad: %v %v", recomb, secret) - } - } - } - } -} - -func TestField_Add(t *testing.T) { - if out := add(16, 16); out != 0 { - t.Fatalf("Bad: %v 16", out) - } - - if out := add(3, 4); out != 7 { - t.Fatalf("Bad: %v 7", out) - } -} - -func TestField_Mult(t *testing.T) { - if out := mult(3, 7); out != 9 { - t.Fatalf("Bad: %v 9", out) - } - - if out := mult(3, 0); out != 0 { - t.Fatalf("Bad: %v 0", out) - } - - if out := mult(0, 3); out != 0 { - t.Fatalf("Bad: %v 0", out) - } -} - -func TestField_Divide(t *testing.T) { - if out := div(0, 7); out != 0 { - t.Fatalf("Bad: %v 0", out) - } - - if out := div(3, 3); out != 1 { - t.Fatalf("Bad: %v 1", out) - } - - if out := div(6, 3); out != 2 { - t.Fatalf("Bad: %v 2", out) - } -} - -func TestPolynomial_Random(t *testing.T) { - p, err := makePolynomial(42, 2) - if err != nil { - t.Fatalf("err: %v", err) - } - - if p.coefficients[0] != 42 { - t.Fatalf("bad: %v", p.coefficients) - } -} - -func TestPolynomial_Eval(t *testing.T) { - p, err := makePolynomial(42, 1) - if err != nil { - t.Fatalf("err: %v", err) - } - - if out := p.evaluate(0); out != 42 { - t.Fatalf("bad: %v", out) - } - - out := p.evaluate(1) - exp := add(42, mult(1, p.coefficients[1])) - if out != exp { - t.Fatalf("bad: %v %v %v", out, exp, p.coefficients) - } -} - -func TestInterpolate_Rand(t *testing.T) { - for i := 0; i < 256; i++ { - p, err := makePolynomial(uint8(i), 2) - if err != nil { - t.Fatalf("err: %v", err) - } - - x_vals := []uint8{1, 2, 3} - y_vals := []uint8{p.evaluate(1), p.evaluate(2), p.evaluate(3)} - out := interpolatePolynomial(x_vals, y_vals, 0) - if out != uint8(i) { - t.Fatalf("Bad: %v %d", out, i) - } - } -} diff --git a/tools/codechecker/pkg/godoctests/analyzer_test.go b/tools/codechecker/pkg/godoctests/analyzer_test.go deleted file mode 100644 index 65bf6af1d..000000000 --- a/tools/codechecker/pkg/godoctests/analyzer_test.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package godoctests - -import ( - "os" - "path/filepath" - "testing" - - "golang.org/x/tools/go/analysis/analysistest" -) - -// TestAnalyzer runs the analyzer on the test functions in testdata/funcs.go. The report from the analyzer is compared against -// the comments in funcs.go beginning with "want." If there is no comment beginning with "want", then the analyzer is expected -// not to report anything. -func TestAnalyzer(t *testing.T) { - f, err := os.Getwd() - if err != nil { - t.Fatal("failed to get working directory", err) - } - analysistest.Run(t, filepath.Join(f, "testdata"), Analyzer, ".") -} diff --git a/tools/codechecker/pkg/godoctests/testdata/funcs.go b/tools/codechecker/pkg/godoctests/testdata/funcs.go deleted file mode 100644 index ddaf56bfd..000000000 --- a/tools/codechecker/pkg/godoctests/testdata/funcs.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package testdata - -import "testing" - -// Test_GoDocOK is a test that has a go doc -func Test_GoDocOK(t *testing.T) {} - -func Test_NoGoDocFails(t *testing.T) {} // want "Test Test_NoGoDocFails is missing a go doc" - -// This test does not have a go doc beginning with the function name -func Test_BadGoDocFails(t *testing.T) {} // want "Test Test_BadGoDocFails must have a go doc beginning with the function name" - -func test_TestHelperNoGoDocOK(t *testing.T) {} - -func Test_DifferentSignatureNoGoDocOK() {} - -func Test_DifferentSignature2NoGoDocOK(t *testing.T, a int) {} diff --git a/tools/codechecker/pkg/gonilnilfunctions/analyzer_test.go b/tools/codechecker/pkg/gonilnilfunctions/analyzer_test.go deleted file mode 100644 index 4cfac4af4..000000000 --- a/tools/codechecker/pkg/gonilnilfunctions/analyzer_test.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package gonilnilfunctions - -import ( - "os" - "path/filepath" - "testing" - - "golang.org/x/tools/go/analysis/analysistest" -) - -// TestAnalyzer runs the analyzer on the test functions in testdata/funcs.go. The report from the analyzer is compared against -// the comments in funcs.go beginning with "want." If there is no comment beginning with "want", then the analyzer is expected -// not to report anything. -func TestAnalyzer(t *testing.T) { - f, err := os.Getwd() - if err != nil { - t.Fatal("failed to get working directory", err) - } - analysistest.Run(t, filepath.Join(f, "testdata"), Analyzer, ".") -} diff --git a/tools/codechecker/pkg/gonilnilfunctions/testdata/funcs.go b/tools/codechecker/pkg/gonilnilfunctions/testdata/funcs.go deleted file mode 100644 index 73f3ee9f5..000000000 --- a/tools/codechecker/pkg/gonilnilfunctions/testdata/funcs.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package testdata - -func ReturnReturnOkay() (any, error) { - var i interface{} - return i, nil -} - -func OneGoodOneBad() (any, error) { // want "Function OneGoodOneBad can return an error, and has a statement that returns only nils" - var i interface{} - if true { - return i, nil - } - return nil, nil -} - -func OneBadOneGood() (any, error) { // want "Function OneBadOneGood can return an error, and has a statement that returns only nils" - var i interface{} - if true { - return nil, nil - } - return i, nil -} - -func EmptyFunc() {} - -func TwoNilNils() (any, error) { // want "Function TwoNilNils can return an error, and has a statement that returns only nils" - if true { - return nil, nil - } - return nil, nil -} - -// ThreeResults should not fail, as while it returns nil, nil, nil, it has three results, not two. -func ThreeResults() (any, any, error) { - return nil, nil, nil -} - -func TwoArgsNoError() (any, any) { - return nil, nil -} - -func NestedReturn() (any, error) { // want "Function NestedReturn can return an error, and has a statement that returns only nils" - { - { - { - return nil, nil - } - } - } -} - -func NestedForReturn() (any, error) { // want "Function NestedForReturn can return an error, and has a statement that returns only nils" - for { - for i := 0; i < 100; i++ { - { - return nil, nil - } - } - } -} - -func AnyErrorNilNil() (any, error) { // want "Function AnyErrorNilNil can return an error, and has a statement that returns only nils" - return nil, nil -} - -// Skipped should be skipped because of the following line: -// ignore-nil-nil-function-check -func Skipped() (any, error) { - return nil, nil -} diff --git a/ui/mirage/config.js b/ui/mirage/config.js deleted file mode 100644 index 022fe3d49..000000000 --- a/ui/mirage/config.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ENV from 'vault/config/environment'; -import handlers from './handlers'; - -// remember to export handler name from mirage/handlers/index.js file - -export default function () { - this.namespace = 'v1'; - - // start ember in development running mirage -> yarn start:mirage handlerName - // if handler is not provided, general config will be used - // this is useful for feature development when a specific and limited config is required - const { handler } = ENV['ember-cli-mirage']; - const handlerName = handler in handlers ? handler : 'base'; - handlers[handlerName](this); - this.logging = false; // disables passthrough logging which spams the console - console.log(`⚙ Using ${handlerName} Mirage request handlers ⚙`); // eslint-disable-line - // passthrough all unhandled requests - this.passthrough(); -} diff --git a/ui/mirage/factories/configuration.js b/ui/mirage/factories/configuration.js deleted file mode 100644 index 4082ef63d..000000000 --- a/ui/mirage/factories/configuration.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory, trait } from 'ember-cli-mirage'; - -export default Factory.extend({ - auth: null, - data: null, // populated via traits - lease_duration: 0, - lease_id: '', - renewable: true, - request_id: '22068a49-a504-41ad-b5b0-1eac71659190', - warnings: null, - wrap_info: null, - - // add servers to test raft storage configuration - withRaft: trait({ - afterCreate(config, server) { - if (!config.data) { - config.data = { - config: { - index: 0, - servers: server.serializerOrRegistry.serialize(server.createList('server', 2)), - }, - }; - } - }, - }), -}); diff --git a/ui/mirage/factories/database-connection.js b/ui/mirage/factories/database-connection.js deleted file mode 100644 index 2f6e8db22..000000000 --- a/ui/mirage/factories/database-connection.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory } from 'ember-cli-mirage'; - -// For the purposes of testing, we only use a subset of fields relevant to mysql -export default Factory.extend({ - backend: 'database', - name: 'connection', - plugin_name: 'mysql-database-plugin', - verify_connection: true, - connection_url: '{{username}}:{{password}}@tcp(127.0.0.1:33060)/', - username: 'admin', - max_open_connections: 4, - max_idle_connections: 0, - max_connection_lifetime: '0s', - allowed_roles: () => [], - root_rotation_statements: () => [ - 'SELECT user from mysql.user', - "GRANT ALL PRIVILEGES ON *.* to 'sudo'@'%'", - ], -}); diff --git a/ui/mirage/factories/feature.js b/ui/mirage/factories/feature.js deleted file mode 100644 index c1e9ee9dc..000000000 --- a/ui/mirage/factories/feature.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory } from 'ember-cli-mirage'; - -export default Factory.extend({ - feature_flags() { - return []; // VAULT_CLOUD_ADMIN_NAMESPACE - }, -}); diff --git a/ui/mirage/factories/kubernetes-config.js b/ui/mirage/factories/kubernetes-config.js deleted file mode 100644 index 7b8eb3539..000000000 --- a/ui/mirage/factories/kubernetes-config.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory } from 'ember-cli-mirage'; - -export default Factory.extend({ - kubernetes_host: 'https://192.168.99.100:8443', - kubernetes_ca_cert: - '-----BEGIN CERTIFICATE-----\nMIIDNTCCAh2gApGgAwIBAgIULNEk+01LpkDeJujfsAgIULNEkAgIULNEckApGgAwIBAg+01LpkDeJuj\n-----END CERTIFICATE-----', - disable_local_ca_jwt: true, - - // property used only for record lookup and filtered from response payload - path: null, -}); diff --git a/ui/mirage/factories/kubernetes-role.js b/ui/mirage/factories/kubernetes-role.js deleted file mode 100644 index 028eb9624..000000000 --- a/ui/mirage/factories/kubernetes-role.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory, trait } from 'ember-cli-mirage'; - -const generated_role_rules = `rules: -- apiGroups: [""] - resources: ["secrets", "services"] - verbs: ["get", "watch", "list", "create", "delete", "deletecollection", "patch", "update"] -`; -const name_template = '{{.FieldName | lowercase}}'; -const extra_annotations = { foo: 'bar', baz: 'qux' }; -const extra_labels = { foobar: 'baz', barbaz: 'foo' }; - -export default Factory.extend({ - name: (i) => `role-${i}`, - allowed_kubernetes_namespaces: '*', - allowed_kubernetes_namespace_selector: '', - token_max_ttl: 86400, - token_default_ttl: 600, - service_account_name: 'default', - kubernetes_role_name: '', - kubernetes_role_type: 'Role', - generated_role_rules: '', - name_template: '', - extra_annotations: null, - extra_labels: null, - - afterCreate(record) { - // only one of these three props can be defined - if (record.generated_role_rules) { - record.service_account_name = null; - record.kubernetes_role_name = null; - } else if (record.kubernetes_role_name) { - record.service_account_name = null; - record.generated_role_rules = null; - } else if (record.service_account_name) { - record.generated_role_rules = null; - record.kubernetes_role_name = null; - } - }, - withRoleName: trait({ - service_account_name: null, - generated_role_rules: null, - kubernetes_role_name: 'vault-k8s-secrets-role', - extra_annotations, - name_template, - }), - withRoleRules: trait({ - service_account_name: null, - kubernetes_role_name: null, - generated_role_rules, - extra_annotations, - extra_labels, - name_template, - }), -}); diff --git a/ui/mirage/factories/mfa-login-enforcement.js b/ui/mirage/factories/mfa-login-enforcement.js deleted file mode 100644 index bfc2b5bfb..000000000 --- a/ui/mirage/factories/mfa-login-enforcement.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory } from 'ember-cli-mirage'; - -export default Factory.extend({ - auth_method_accessors: null, - auth_method_types: null, - identity_entity_ids: null, - identity_group_ids: null, - mfa_method_ids: null, - name: null, - namespace_id: 'root', - - afterCreate(record, server) { - // initialize arrays and stub some data if not provided - if (!record.name) { - // use random string for generated name - record.update('name', (Math.random() + 1).toString(36).substring(2)); - } - if (!record.mfa_method_ids) { - // aggregate all existing methods and choose a random one - const methods = ['Totp', 'Duo', 'Okta', 'Pingid'].reduce((methods, type) => { - const records = server.schema.db[`mfa${type}Methods`].where({}); - if (records.length) { - methods.push(...records); - } - return methods; - }, []); - // if no methods were found create one since it is a required for login enforcements - if (!methods.length) { - methods.push(server.create('mfa-totp-method')); - } - const method = methods.length ? methods[Math.floor(Math.random() * methods.length)] : null; - record.update('mfa_method_ids', method ? [method.id] : []); - } - const targets = { - auth_method_accessors: ['auth_userpass_bb95c2b1'], - auth_method_types: ['userpass'], - identity_group_ids: ['34db6b52-591e-bc22-8af0-4add5e167326'], - identity_entity_ids: ['f831667b-7392-7a1c-c0fc-33d48cb1c57d'], - }; - for (const key in targets) { - if (!record.key) { - record.update(key, targets[key]); - } - } - }, -}); diff --git a/ui/mirage/factories/mfa-method.js b/ui/mirage/factories/mfa-method.js deleted file mode 100644 index 67e3e9030..000000000 --- a/ui/mirage/factories/mfa-method.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory } from 'ember-cli-mirage'; - -export default Factory.extend({ - type: 'okta', - uses_passcode: false, - - afterCreate(mfaMethod) { - if (mfaMethod.type === 'totp') { - mfaMethod.uses_passcode = true; - } - }, -}); diff --git a/ui/mirage/factories/mfa-totp-method.js b/ui/mirage/factories/mfa-totp-method.js deleted file mode 100644 index b4ac63b97..000000000 --- a/ui/mirage/factories/mfa-totp-method.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory } from 'ember-cli-mirage'; - -export default Factory.extend({ - algorithm: 'SHA1', - digits: 6, - issuer: 'Vault', - key_size: 20, - max_validation_attempts: 5, - name: '', // returned but cannot be set at this time - namespace_id: 'root', - period: 30, - qr_size: 200, - skew: 1, - type: 'totp', - - afterCreate(record) { - if (record.name) { - console.warn('Endpoint ignored these unrecognized parameters: [name]'); // eslint-disable-line - record.name = ''; - } - }, -}); diff --git a/ui/mirage/factories/open-api-explorer.js b/ui/mirage/factories/open-api-explorer.js deleted file mode 100644 index 6d18fd8ec..000000000 --- a/ui/mirage/factories/open-api-explorer.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory } from 'ember-cli-mirage'; -/* eslint-disable ember/avoid-leaking-state-in-ember-objects */ -export default Factory.extend({ - openapi: '3.0.2', - info: { - title: 'HashiCorp Vault API', - description: 'HTTP API that gives you full access to Vault. All API routes are prefixed with `/v1/`.', - version: '1.0.0', - license: { - name: 'Mozilla Public License 2.0', - url: 'https://www.mozilla.org/en-US/MPL/2.0', - }, - }, - paths: { - '/auth/token/create': { - description: 'The token create path is used to create new tokens.', - post: { - summary: 'The token create path is used to create new tokens.', - tags: ['auth'], - responses: { - 200: { - description: 'OK', - }, - }, - }, - }, - '/secret/data/{path}': { - description: 'Location of a secret.', - post: { - summary: 'Location of a secret.', - tags: ['secret'], - responses: { - 200: { - description: 'OK', - }, - }, - }, - }, - }, -}); diff --git a/ui/mirage/factories/secret-engine.js b/ui/mirage/factories/secret-engine.js deleted file mode 100644 index 7fc316781..000000000 --- a/ui/mirage/factories/secret-engine.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory } from 'ember-cli-mirage'; - -export default Factory.extend({ - path: 'foo/', - description: 'secret-engine generated by mirage', - local: true, - sealWrap: true, - // set in afterCreate - accessor: 'type_7f52940', - type: 'kv', - options: null, - - afterCreate(secretEngine) { - if (!secretEngine.options && ['generic', 'kv'].includes(secretEngine.type)) { - secretEngine.options = { - version: '2', - }; - } - }, -}); diff --git a/ui/mirage/factories/server.js b/ui/mirage/factories/server.js deleted file mode 100644 index dfdbd9507..000000000 --- a/ui/mirage/factories/server.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Factory } from 'ember-cli-mirage'; - -export default Factory.extend({ - address: '127.0.0.1', - node_id: (i) => `raft_node_${i}`, - protocol_version: '3', - voter: true, - leader: true, -}); diff --git a/ui/mirage/handlers/base.js b/ui/mirage/handlers/base.js deleted file mode 100644 index 2693f4a75..000000000 --- a/ui/mirage/handlers/base.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -// base handlers used in mirage config when a specific handler is not specified -const EXPIRY_DATE = '2021-05-12T23:20:50.52Z'; - -export default function (server) { - server.get('/sys/internal/ui/feature-flags', (db) => { - const featuresResponse = db.features.first(); - return { - data: { - feature_flags: featuresResponse ? featuresResponse.feature_flags : null, - }, - }; - }); - - server.get('/sys/health', function () { - return { - initialized: true, - sealed: false, - standby: false, - license: { - expiry: '2021-05-12T23:20:50.52Z', - state: 'stored', - }, - performance_standby: false, - replication_performance_mode: 'disabled', - replication_dr_mode: 'disabled', - server_time_utc: 1622562585, - version: '1.9.0+ent', - cluster_name: 'vault-cluster-e779cd7c', - cluster_id: '5f20f5ab-acea-0481-787e-71ec2ff5a60b', - last_wal: 121, - }; - }); - - server.get('/sys/license/status', function () { - return { - data: { - autoloading_used: false, - persisted_autoload: { - expiration_time: EXPIRY_DATE, - features: ['DR Replication', 'Namespaces', 'Lease Count Quotas', 'Automated Snapshots'], - license_id: '0eca7ef8-ebc0-f875-315e-3cc94a7870cf', - performance_standby_count: 0, - start_time: '2020-04-28T00:00:00Z', - }, - autoloaded: { - expiration_time: EXPIRY_DATE, - features: ['DR Replication', 'Namespaces', 'Lease Count Quotas', 'Automated Snapshots'], - license_id: '0eca7ef8-ebc0-f875-315e-3cc94a7870cf', - performance_standby_count: 0, - start_time: '2020-04-28T00:00:00Z', - }, - }, - }; - }); - - server.get('sys/namespaces', function () { - return { - data: { - keys: [ - 'ns1/', - 'ns2/', - 'ns3/', - 'ns4/', - 'ns5/', - 'ns6/', - 'ns7/', - 'ns8/', - 'ns9/', - 'ns10/', - 'ns11/', - 'ns12/', - 'ns13/', - 'ns14/', - 'ns15/', - 'ns16/', - 'ns17/', - 'ns18/', - ], - }, - }; - }); -} diff --git a/ui/mirage/handlers/clients.js b/ui/mirage/handlers/clients.js deleted file mode 100644 index 475756111..000000000 --- a/ui/mirage/handlers/clients.js +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { - isBefore, - startOfMonth, - endOfMonth, - addMonths, - subMonths, - differenceInCalendarMonths, - fromUnixTime, - isAfter, - formatRFC3339, -} from 'date-fns'; -import { parseAPITimestamp } from 'core/utils/date-formatters'; - -// Matches mocked date in client-dashboard-test file -const CURRENT_DATE = new Date('2023-01-13T14:15:00'); -const COUNTS_START = subMonths(CURRENT_DATE, 12); // pretend vault user started cluster 6 months ago -// for testing, we're in the middle of a license/billing period -const LICENSE_START = startOfMonth(subMonths(CURRENT_DATE, 6)); -// upgrade happened 1 month after license start -const UPGRADE_DATE = addMonths(LICENSE_START, 1); - -function getSum(array, key) { - return array.reduce((sum, { counts }) => sum + counts[key], 0); -} - -function getTotalCounts(array) { - return { - distinct_entities: getSum(array, 'entity_clients'), - entity_clients: getSum(array, 'entity_clients'), - non_entity_tokens: getSum(array, 'non_entity_clients'), - non_entity_clients: getSum(array, 'non_entity_clients'), - clients: getSum(array, 'clients'), - }; -} - -function randomBetween(min, max) { - return Math.floor(Math.random() * (max - min + 1) + min); -} - -function arrayOfCounts(max, arrayLength) { - var result = []; - var sum = 0; - for (var i = 0; i < arrayLength - 1; i++) { - result[i] = randomBetween(1, max - (arrayLength - i - 1) - sum); - sum += result[i]; - } - result[arrayLength - 1] = max - sum; - return result.sort((a, b) => b - a); -} - -function generateNamespaceBlock(idx = 0, isLowerCounts = false, ns) { - const min = isLowerCounts ? 10 : 50; - const max = isLowerCounts ? 100 : 5000; - const nsBlock = { - namespace_id: ns?.namespace_id || (idx === 0 ? 'root' : Math.random().toString(36).slice(2, 7) + idx), - namespace_path: ns?.namespace_path || (idx === 0 ? '' : `ns/${idx}`), - counts: {}, - }; - const mounts = []; - Array.from(Array(10)).forEach((mount, index) => { - const mountClients = randomBetween(min, max); - const [nonEntity, entity] = arrayOfCounts(mountClients, 2); - mounts.push({ - mount_path: `auth/authid${index}`, - counts: { - clients: mountClients, - entity_clients: entity, - non_entity_clients: nonEntity, - distinct_entities: entity, - non_entity_tokens: nonEntity, - }, - }); - }); - mounts.sort((a, b) => b.counts.clients - a.counts.clients); - nsBlock.mounts = mounts; - nsBlock.counts = getTotalCounts(mounts); - return nsBlock; -} - -function generateMonths(startDate, endDate, namespaces) { - const startDateObject = startOfMonth(parseAPITimestamp(startDate)); - const endDateObject = startOfMonth(parseAPITimestamp(endDate)); - const numberOfMonths = differenceInCalendarMonths(endDateObject, startDateObject) + 1; - const months = []; - if (isBefore(startDateObject, UPGRADE_DATE) && isBefore(endDateObject, UPGRADE_DATE)) { - // months block is empty if dates do not span an upgrade - return []; - } - for (let i = 0; i < numberOfMonths; i++) { - const month = addMonths(startDateObject, i); - const hasNoData = isBefore(month, UPGRADE_DATE); - if (hasNoData) { - months.push({ - timestamp: formatRFC3339(month), - counts: null, - namespaces: null, - new_clients: null, - }); - continue; - } - - const monthNs = namespaces.map((ns, idx) => generateNamespaceBlock(idx, true, ns)); - const newClients = namespaces.map((ns, idx) => generateNamespaceBlock(idx, true, ns)); - months.push({ - timestamp: formatRFC3339(month), - counts: getTotalCounts(monthNs), - namespaces: monthNs.sort((a, b) => b.counts.clients - a.counts.clients), - new_clients: { - counts: getTotalCounts(newClients), - namespaces: newClients.sort((a, b) => b.counts.clients - a.counts.clients), - }, - }); - } - return months; -} - -function generateActivityResponse(namespaces, startDate, endDate) { - return { - start_time: isAfter(new Date(startDate), COUNTS_START) ? startDate : formatRFC3339(COUNTS_START), - end_time: endDate, - by_namespace: namespaces.sort((a, b) => b.counts.clients - a.counts.clients), - months: generateMonths(startDate, endDate, namespaces), - total: getTotalCounts(namespaces), - }; -} - -export default function (server) { - server.get('sys/license/status', function () { - return { - request_id: 'my-license-request-id', - data: { - autoloaded: { - license_id: 'my-license-id', - start_time: formatRFC3339(LICENSE_START), - expiration_time: formatRFC3339(endOfMonth(addMonths(CURRENT_DATE, 6))), - }, - }, - }; - }); - - server.get('sys/internal/counters/config', function () { - return { - request_id: 'some-config-id', - data: { - default_report_months: 12, - enabled: 'default-enable', - queries_available: true, - retention_months: 24, - }, - }; - }); - - server.get('/sys/internal/counters/activity', (schema, req) => { - let { start_time, end_time } = req.queryParams; - // backend returns a timestamp if given unix time, so first convert to timestamp string here - if (!start_time.includes('T')) start_time = fromUnixTime(start_time).toISOString(); - if (!end_time.includes('T')) end_time = fromUnixTime(end_time).toISOString(); - const namespaces = Array.from(Array(12)).map((v, idx) => generateNamespaceBlock(idx)); - return { - request_id: 'some-activity-id', - lease_id: '', - renewable: false, - lease_duration: 0, - data: generateActivityResponse(namespaces, start_time, end_time), - wrap_info: null, - warnings: null, - auth: null, - }; - }); -} diff --git a/ui/mirage/handlers/database.js b/ui/mirage/handlers/database.js deleted file mode 100644 index e4c22aac5..000000000 --- a/ui/mirage/handlers/database.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Response } from 'miragejs'; - -export default function (server) { - const getRecord = (schema, req, dbKey) => { - const { backend, name } = req.params; - const record = schema.db[dbKey].findBy({ name, backend }); - if (record) { - delete record.backend; - delete record.id; - } - return record ? { data: record } : new Response(404, {}, { errors: [] }); - }; - const createOrUpdateRecord = (schema, req, key) => { - const { backend, name } = req.params; - const payload = JSON.parse(req.requestBody); - const record = schema[key].findOrCreateBy({ name, backend }); - record.update(payload); - return new Response(204); - }; - const deleteRecord = (schema, req, dbKey) => { - const { name } = req.params; - const record = schema.db[dbKey].findBy({ name }); - if (record) { - schema.db[dbKey].remove(record.id); - } - return new Response(204); - }; - - // Connection mgmt - server.get('/:backend/config/:name', (schema, req) => { - return getRecord(schema, req, 'database/connections'); - }); - server.get('/:backend/config', (schema) => { - const keys = schema.db['databaseConnections'].map((record) => record.name); - if (!keys.length) { - return new Response(404, {}, { errors: [] }); - } - return { - data: { - keys, - }, - }; - }); - server.post('/:backend/config/:name', (schema, req) => { - const { name } = req.params; - const { username } = JSON.parse(req.requestBody); - if (name === 'bad-connection') { - return new Response( - 500, - {}, - { - errors: [ - `error creating database object: error verifying - ping: Error 1045 (28000): Access denied for user '${username}'@'192.168.65.1' (using password: YES)`, - ], - } - ); - } - return createOrUpdateRecord(schema, req, 'database/connections'); - }); - server.delete('/:backend/config/:name', (schema, req) => { - return deleteRecord(schema, req, 'database-connection'); - }); - // Rotate root - server.post('/:backend/rotate-root/:name', (schema, req) => { - const { name } = req.params; - if (name === 'fail-rotate') { - return new Response( - 500, - {}, - { - errors: [ - "1 error occurred:\n\t* failed to update user: failed to change password: Error 1045 (28000): Access denied for user 'admin'@'%' (using password: YES)\n\n", - ], - } - ); - } - return new Response(204); - }); - - // Generate credentials - server.get('/:backend/creds/:role', (schema, req) => { - const { role } = req.params; - if (role === 'static-role') { - // static creds - return { - request_id: 'static-1234', - lease_id: '', - renewable: false, - lease_duration: 0, - data: { - last_vault_rotation: '2024-01-18T10:45:47.227193-06:00', - password: 'generated-password', - rotation_period: 86400, - ttl: 3600, - username: 'static-username', - }, - wrap_info: null, - warnings: null, - auth: null, - mount_type: 'database', - }; - } - // dynamic creds - return { - request_id: 'dynamic-1234', - lease_id: `database/creds/${role}/abcd`, - renewable: true, - lease_duration: 3600, - data: { - password: 'generated-password', - username: 'generated-username', - }, - wrap_info: null, - warnings: null, - auth: null, - mount_type: 'database', - }; - }); -} diff --git a/ui/mirage/handlers/hcp-link.js b/ui/mirage/handlers/hcp-link.js deleted file mode 100644 index c4e695036..000000000 --- a/ui/mirage/handlers/hcp-link.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import modifyPassthroughResponse from '../helpers/modify-passthrough-response'; - -export const statuses = [ - 'connected', - 'disconnected since 2022-09-21T11:25:02.196835-07:00; error: unable to establish a connection with HCP', - 'connecting since 2022-09-21T11:25:02.196835-07:00; error: unable to establish a connection with HCP', - 'connecting since 2022-09-21T11:25:02.196835-07:00; error: principal does not have the permission to register as a provider', - 'connecting since 2022-09-21T11:25:02.196835-07:00; error: could not obtain a token with the supplied credentials', -]; -let index = null; - -export default function (server) { - server.get('sys/seal-status', (schema, req) => { - // return next status from statuses array - if (index === null || index === statuses.length - 1) { - index = 0; - } else { - index++; - } - return modifyPassthroughResponse(req, { hcp_link_status: statuses[index] }); - }); - // enterprise only feature initially - server.get('sys/health', (schema, req) => modifyPassthroughResponse(req, { version: '1.12.0-dev1+ent' })); -} diff --git a/ui/mirage/handlers/index.js b/ui/mirage/handlers/index.js deleted file mode 100644 index f612fa3f0..000000000 --- a/ui/mirage/handlers/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -// add all handlers here -// individual lookup done in mirage config -import base from './base'; -import clients from './clients'; -import database from './database'; -import kms from './kms'; -import mfaConfig from './mfa-config'; -import mfaLogin from './mfa-login'; -import oidcConfig from './oidc-config'; -import hcpLink from './hcp-link'; -import kubernetes from './kubernetes'; - -export { base, clients, database, kms, mfaConfig, mfaLogin, oidcConfig, hcpLink, kubernetes }; diff --git a/ui/mirage/handlers/kms.js b/ui/mirage/handlers/kms.js deleted file mode 100644 index 36fd25080..000000000 --- a/ui/mirage/handlers/kms.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export default function (server) { - server.get('keymgmt/key?list=true', function () { - return { - data: { - keys: ['example-1', 'example-2', 'example-3'], - }, - }; - }); - - server.get('keymgmt/key/:name', function (_, request) { - const name = request.params.name; - return { - data: { - name, - deletion_allowed: false, - keys: { - 1: { - creation_time: '2020-11-02T15:54:58.768473-08:00', - public_key: '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----', - }, - 2: { - creation_time: '2020-11-04T16:58:47.591718-08:00', - public_key: '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----', - }, - }, - latest_version: 2, - min_enabled_version: 1, - type: 'rsa-2048', - }, - }; - }); - - server.get('keymgmt/key/:name/kms', function () { - return { - data: { - keys: ['example-kms'], - }, - }; - }); - - server.post('keymgmt/key/:name', function () { - return {}; - }); - - server.put('keymgmt/key/:name', function () { - return {}; - }); - - server.get('/keymgmt/kms/:provider/key', () => { - const keys = []; - let i = 1; - while (i <= 75) { - keys.push(`testkey-${i}`); - i++; - } - return { data: { keys } }; - }); -} diff --git a/ui/mirage/handlers/kubernetes.js b/ui/mirage/handlers/kubernetes.js deleted file mode 100644 index 2f4e8b266..000000000 --- a/ui/mirage/handlers/kubernetes.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Response } from 'miragejs'; - -export default function (server) { - const getRecord = (schema, req, dbKey) => { - const { path, name } = req.params; - const findBy = dbKey === 'kubernetesConfigs' ? { path } : { name }; - const record = schema.db[dbKey].findBy(findBy); - if (record) { - delete record.path; - delete record.id; - } - return record ? { data: record } : new Response(404, {}, { errors: [] }); - }; - const createRecord = (req, key) => { - const data = JSON.parse(req.requestBody); - if (key === 'kubernetes-config') { - data.path = req.params.path; - } - server.create(key, data); - return new Response(204); - }; - const deleteRecord = (schema, req, dbKey) => { - const { name } = req.params; - const record = schema.db[dbKey].findBy({ name }); - if (record) { - schema.db[dbKey].remove(record.id); - } - return new Response(204); - }; - - server.get('/:path/config', (schema, req) => { - return getRecord(schema, req, 'kubernetesConfigs'); - }); - server.post('/:path/config', (schema, req) => { - return createRecord(req, 'kubernetes-config'); - }); - server.delete('/:path/config', (schema, req) => { - return deleteRecord(schema, req, 'kubernetesConfigs'); - }); - // endpoint for checking for environment variables necessary for inferred config - server.get('/:path/check', () => { - const response = {}; - const status = Math.random() > 0.5 ? 204 : 404; - if (status === 404) { - response.errors = [ - 'Missing environment variables: KUBERNETES_SERVICE_HOST, KUBERNETES_SERVICE_PORT_HTTPS', - ]; - } - return new Response(status, response); - }); - server.get('/:path/roles', (schema) => { - return { - data: { - keys: schema.db.kubernetesRoles.where({}).mapBy('name'), - }, - }; - }); - server.get('/:path/roles/:name', (schema, req) => { - return getRecord(schema, req, 'kubernetesRoles'); - }); - server.post('/:path/roles/:name', (schema, req) => { - return createRecord(req, 'kubernetes-role'); - }); - server.delete('/:path/roles/:name', (schema, req) => { - return deleteRecord(schema, req, 'kubernetesRoles'); - }); - server.post('/:path/creds/:role', (schema, req) => { - const { role } = req.params; - const record = schema.db.kubernetesRoles.findBy({ name: role }); - const data = JSON.parse(req.requestBody); - let errors; - if (!record) { - errors = [`role '${role}' does not exist`]; - } else if (!data.kubernetes_namespace) { - errors = ["'kubernetes_namespace' is required"]; - } - // creds cannot be fetched after creation so we don't need to store them - return errors - ? new Response(400, {}, { errors }) - : { - request_id: '58fefc6c-5195-c17a-94f2-8f889f3df57c', - lease_id: 'kubernetes/creds/default-role/aWczfcfJ7NKUdiirJrPXIs38', - renewable: false, - lease_duration: 3600, - data: { - service_account_name: 'default', - service_account_namespace: 'default', - service_account_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6Imlr', - }, - }; - }); - - server.get('/sys/internal/ui/mounts/kubernetes', () => ({ - data: { - accessor: 'kubernetes_9f846a87', - path: 'kubernetes/', - type: 'kubernetes', - }, - })); -} diff --git a/ui/mirage/handlers/mfa-config.js b/ui/mirage/handlers/mfa-config.js deleted file mode 100644 index f16caa5ec..000000000 --- a/ui/mirage/handlers/mfa-config.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Response } from 'miragejs'; - -export default function (server) { - const methods = ['totp', 'duo', 'okta', 'pingid']; - const required = { - totp: ['issuer'], - duo: ['secret_key', 'integration_key', 'api_hostname'], - okta: ['org_name', 'api_token'], - pingid: ['settings_file_base64'], - }; - - const validate = (type, data, cb) => { - if (!methods.includes(type)) { - return new Response(400, {}, { errors: [`Method ${type} not found`] }); - } - if (data) { - const missing = required[type].reduce((params, key) => { - if (!data[key]) { - params.push(key); - } - return params; - }, []); - if (missing.length) { - return new Response(400, {}, { errors: [`Missing required parameters: [${missing.join(', ')}]`] }); - } - } - return cb(); - }; - - const dbKeyFromType = (type) => `mfa${type.charAt(0).toUpperCase()}${type.slice(1)}Methods`; - - const generateListResponse = (schema, isMethod) => { - let records = []; - if (isMethod) { - methods.forEach((method) => { - records.addObjects(schema.db[dbKeyFromType(method)].where({})); - }); - } else { - records = schema.db.mfaLoginEnforcements.where({}); - } - // seed the db with a few records if none exist - if (!records.length) { - if (isMethod) { - methods.forEach((type) => { - records.push(server.create(`mfa-${type}-method`)); - }); - } else { - records = server.createList('mfa-login-enforcement', 4).toArray(); - } - } - const dataKey = isMethod ? 'id' : 'name'; - const data = records.reduce( - (resp, record) => { - resp.key_info[record[dataKey]] = record; - resp.keys.push(record[dataKey]); - return resp; - }, - { - key_info: {}, - keys: [], - } - ); - return { data }; - }; - - // list methods - server.get('/identity/mfa/method/', (schema) => { - return generateListResponse(schema, true); - }); - // fetch method by id - server.get('/identity/mfa/method/:id', (schema, { params: { id } }) => { - let record; - for (const method of methods) { - record = schema.db[dbKeyFromType(method)].find(id); - if (record) { - break; - } - } - // inconvenient when testing edit route to return a 404 on refresh since mirage memory is cleared - // flip this variable to test 404 state if needed - const shouldError = false; - // create a new record so data is always returned - if (!record && !shouldError) { - return { data: server.create('mfa-totp-method') }; - } - return !record ? new Response(404, {}, { errors: [] }) : { data: record }; - }); - // create method - server.post('/identity/mfa/method/:type', (schema, { params: { type }, requestBody }) => { - const data = JSON.parse(requestBody); - return validate(type, data, () => { - const record = server.create(`mfa-${type}-method`, data); - return { data: { method_id: record.id } }; - }); - }); - // update method - server.post('/identity/mfa/method/:type/:id', (schema, { params: { type, id }, requestBody }) => { - const data = JSON.parse(requestBody); - return validate(type, data, () => { - schema.db[dbKeyFromType(type)].update(id, data); - return {}; - }); - }); - // delete method - server.delete('/identity/mfa/method/:type/:id', (schema, { params: { type, id } }) => { - return validate(type, null, () => { - schema.db[dbKeyFromType(type)].remove(id); - return {}; - }); - }); - // list enforcements - server.get('/identity/mfa/login-enforcement', (schema) => { - return generateListResponse(schema); - }); - // fetch enforcement by name - server.get('/identity/mfa/login-enforcement/:name', (schema, { params: { name } }) => { - const record = schema.db.mfaLoginEnforcements.findBy({ name }); - // inconvenient when testing edit route to return a 404 on refresh since mirage memory is cleared - // flip this variable to test 404 state if needed - const shouldError = false; - // create a new record so data is always returned - if (!record && !shouldError) { - return { data: server.create('mfa-login-enforcement', { name }) }; - } - return !record ? new Response(404, {}, { errors: [] }) : { data: record }; - }); - // create/update enforcement - server.post('/identity/mfa/login-enforcement/:name', (schema, { params: { name }, requestBody }) => { - const data = JSON.parse(requestBody); - // at least one method id is required - if (!data.mfa_method_ids?.length) { - return new Response(400, {}, { errors: ['missing method ids'] }); - } - // at least one of the following targets is required - const required = [ - 'auth_method_accessors', - 'auth_method_types', - 'identity_group_ids', - 'identity_entity_ids', - ]; - let hasRequired = false; - for (const key of required) { - if (data[key]?.length) { - hasRequired = true; - break; - } - } - if (!hasRequired) { - return new Response( - 400, - {}, - { - errors: [ - 'One of auth_method_accessors, auth_method_types, identity_group_ids, identity_entity_ids must be specified', - ], - } - ); - } - if (schema.db.mfaLoginEnforcements.findBy({ name })) { - schema.db.mfaLoginEnforcements.update({ name }, data); - } else { - schema.db.mfaLoginEnforcements.insert(data); - } - return { ...data, id: data.name }; - }); - // delete enforcement - server.delete('/identity/mfa/login-enforcement/:name', (schema, { params: { name } }) => { - schema.db.mfaLoginEnforcements.remove({ name }); - return {}; - }); - // endpoints for target selection - server.get('/identity/group/id', () => ({ - data: { - key_info: { '34db6b52-591e-bc22-8af0-4add5e167326': { name: 'test-group' } }, - keys: ['34db6b52-591e-bc22-8af0-4add5e167326'], - }, - })); - server.get('/identity/group/id/:id', () => ({ - data: { - id: '34db6b52-591e-bc22-8af0-4add5e167326', - name: 'test-group', - }, - })); - server.get('/identity/entity/id', () => ({ - data: { - key_info: { 'f831667b-7392-7a1c-c0fc-33d48cb1c57d': { name: 'test-entity' } }, - keys: ['f831667b-7392-7a1c-c0fc-33d48cb1c57d'], - }, - })); - server.get('/identity/entity/id/:id', () => ({ - data: { - id: 'f831667b-7392-7a1c-c0fc-33d48cb1c57d', - name: 'test-entity', - }, - })); - server.get('/sys/auth', () => ({ - data: { - 'userpass/': { accessor: 'auth_userpass_bb95c2b1', type: 'userpass' }, - }, - })); -} diff --git a/ui/mirage/handlers/mfa-login.js b/ui/mirage/handlers/mfa-login.js deleted file mode 100644 index 9ae39f6f8..000000000 --- a/ui/mirage/handlers/mfa-login.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Response } from 'miragejs'; -import Ember from 'ember'; -import fetch from 'fetch'; - -// initial auth response cache -- lookup by mfa_request_id key -const authResponses = {}; -// mfa requirement cache -- lookup by mfa_request_id key -const mfaRequirement = {}; - -// may be imported in tests when the validation request needs to be intercepted to make assertions prior to returning a response -// in that case it may be helpful to still use this validation logic to ensure to payload is as expected -export const validationHandler = (schema, req) => { - try { - const { mfa_request_id, mfa_payload } = JSON.parse(req.requestBody); - const mfaRequest = mfaRequirement[mfa_request_id]; - - if (!mfaRequest) { - return new Response(404, {}, { errors: ['MFA Request ID not found'] }); - } - // validate request body - for (const constraintId in mfa_payload) { - // ensure ids were passed in map - const method = mfaRequest.methods.find(({ id }) => id === constraintId); - if (!method) { - return new Response(400, {}, { errors: [`Invalid MFA constraint id ${constraintId} passed in map`] }); - } - // test non-totp validation by rejecting all pingid requests - if (method.type === 'pingid') { - return new Response(403, {}, { errors: ['PingId MFA validation failed'] }); - } - // validate totp passcode - const passcode = mfa_payload[constraintId][0]; - if (method.uses_passcode) { - const expectedPasscode = method.type === 'duo' ? 'passcode=test' : 'test'; - if (passcode !== expectedPasscode) { - const error = - { - used: 'code already used; new code is available in 30 seconds', - limit: - 'maximum TOTP validation attempts 4 exceeded the allowed attempts 3. Please try again in 15 seconds', - }[passcode] || 'failed to validate'; - console.log(error); // eslint-disable-line - return new Response(403, {}, { errors: [error] }); - } - } else if (passcode) { - // for okta and duo, reject if a passcode was provided - return new Response(400, {}, { errors: ['Passcode should only be provided for TOTP MFA type'] }); - } - } - return authResponses[mfa_request_id]; - } catch (error) { - console.log(error); // eslint-disable-line - return new Response(500, {}, { errors: ['Mirage Handler Error: /sys/mfa/validate'] }); - } -}; - -export default function (server) { - // generate different constraint scenarios and return mfa_requirement object - const generateMfaRequirement = (req, res) => { - const { user } = req.params; - // uses_passcode automatically set to true in factory for totp type - const m = (type, uses_passcode = false) => server.create('mfa-method', { type, uses_passcode }); - let mfa_constraints = {}; - let methods = []; // flat array of methods for easy lookup during validation - - function generator() { - const methods = []; - const constraintObj = [...arguments].reduce((obj, methodArray, index) => { - obj[`test_${index}`] = { any: methodArray }; - methods.push(...methodArray); - return obj; - }, {}); - return [constraintObj, methods]; - } - - if (user === 'mfa-a') { - [mfa_constraints, methods] = generator([m('totp')]); // 1 constraint 1 passcode - } else if (user === 'mfa-b') { - [mfa_constraints, methods] = generator([m('okta')]); // 1 constraint 1 non-passcode - } else if (user === 'mfa-c') { - [mfa_constraints, methods] = generator([m('totp'), m('duo', true)]); // 1 constraint 2 passcodes - } else if (user === 'mfa-d') { - [mfa_constraints, methods] = generator([m('okta'), m('duo')]); // 1 constraint 2 non-passcode - } else if (user === 'mfa-e') { - [mfa_constraints, methods] = generator([m('okta'), m('totp')]); // 1 constraint 1 passcode 1 non-passcode - } else if (user === 'mfa-f') { - [mfa_constraints, methods] = generator([m('totp')], [m('duo', true)]); // 2 constraints 1 passcode for each - } else if (user === 'mfa-g') { - [mfa_constraints, methods] = generator([m('okta')], [m('duo')]); // 2 constraints 1 non-passcode for each - } else if (user === 'mfa-h') { - [mfa_constraints, methods] = generator([m('totp')], [m('okta')]); // 2 constraints 1 passcode 1 non-passcode - } else if (user === 'mfa-i') { - [mfa_constraints, methods] = generator([m('okta'), m('totp')], [m('totp')]); // 2 constraints 1 passcode/1 non-passcode 1 non-passcode - } else if (user === 'mfa-j') { - [mfa_constraints, methods] = generator([m('pingid')]); // use to test push failures - } else if (user === 'mfa-k') { - [mfa_constraints, methods] = generator([m('duo', true)]); // test duo passcode and prepending passcode= to user input - } - const mfa_request_id = crypto.randomUUID(); - const mfa_requirement = { - mfa_request_id, - mfa_constraints, - }; - // cache mfa requests to test different validation scenarios - mfaRequirement[mfa_request_id] = { methods }; - // cache auth response to be returned later by sys/mfa/validate - authResponses[mfa_request_id] = { ...res }; - return mfa_requirement; - }; - // passthrough original request, cache response and return mfa stub - const passthroughLogin = async (schema, req) => { - // test totp not configured scenario - if (req.params.user === 'totp-na') { - return new Response(400, {}, { errors: ['TOTP mfa required but not configured'] }); - } - const mock = req.params.user ? req.params.user.includes('mfa') : null; - // bypass mfa for users that do not match type - if (!mock) { - req.passthrough(); - } else if (Ember.testing) { - // use root token in test environment - const res = await fetch('/v1/auth/token/lookup-self', { headers: { 'X-Vault-Token': 'root' } }); - if (res.status < 300) { - const json = res.json(); - if (Ember.testing) { - json.auth = { - ...json.data, - policies: [], - metadata: { username: 'foobar' }, - }; - json.data = null; - } - return { auth: { mfa_requirement: generateMfaRequirement(req, json) } }; - } - return new Response(500, {}, { errors: ['Mirage error fetching root token in testing'] }); - } else { - const xhr = req.passthrough(); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4 && xhr.status < 300) { - // XMLHttpRequest response prop only has a getter -- redefine as writable and set value - Object.defineProperty(xhr, 'response', { - writable: true, - value: JSON.stringify({ - auth: { mfa_requirement: generateMfaRequirement(req, JSON.parse(xhr.responseText)) }, - }), - }); - } - }; - } - }; - server.post('/auth/:method/login/:user', passthroughLogin); - - server.post('/sys/mfa/validate', validationHandler); -} diff --git a/ui/mirage/handlers/oidc-config.js b/ui/mirage/handlers/oidc-config.js deleted file mode 100644 index 542141b0f..000000000 --- a/ui/mirage/handlers/oidc-config.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export default function (server) { - // ENTITY SEARCH SELECT - server.get('/identity/entity/id', () => ({ - data: { - key_info: { '1234-12345': { name: 'test-entity' } }, - keys: ['1234-12345'], - }, - })); - - // GROUP SEARCH SELECT - server.get('/identity/group/id', () => ({ - data: { - key_info: { 'abcdef-123': { name: 'test-group' } }, - keys: ['abcdef-123'], - }, - })); -} diff --git a/ui/mirage/helpers/modify-passthrough-response.js b/ui/mirage/helpers/modify-passthrough-response.js deleted file mode 100644 index c86a23eb0..000000000 --- a/ui/mirage/helpers/modify-passthrough-response.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -// passthrough request and modify response from server -// pass object as second arg of properties in response to override -export default function (req, props = {}) { - return new Promise((resolve) => { - const xhr = req.passthrough(); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status < 300) { - // XMLHttpRequest response prop only has a getter -- redefine as writable and set value - Object.defineProperty(xhr, 'response', { - writable: true, - value: JSON.stringify({ - ...JSON.parse(xhr.responseText), - ...props, - }), - }); - } - resolve(); - } - }; - }); -} diff --git a/ui/mirage/identity-managers/application.js b/ui/mirage/identity-managers/application.js deleted file mode 100644 index ad0350939..000000000 --- a/ui/mirage/identity-managers/application.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import IdentityManager from 'vault/utils/identity-manager'; -// to more closely match the Vault backend this will return UUIDs as identifiers for records in mirage -export default IdentityManager; diff --git a/ui/mirage/models/feature.js b/ui/mirage/models/feature.js deleted file mode 100644 index 77cae4e5e..000000000 --- a/ui/mirage/models/feature.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Model } from 'ember-cli-mirage'; - -export default Model.extend({ - feature_flags: null, -}); diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js deleted file mode 100644 index 72963c52f..000000000 --- a/ui/mirage/scenarios/default.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ENV from 'vault/config/environment'; -const { handler } = ENV['ember-cli-mirage']; -import kubernetesScenario from './kubernetes'; - -export default function (server) { - server.create('clients/config'); - server.create('feature', { feature_flags: ['SOME_FLAG', 'VAULT_CLOUD_ADMIN_NAMESPACE'] }); - - if (handler === 'kubernetes') { - kubernetesScenario(server); - } -} diff --git a/ui/mirage/scenarios/kubernetes.js b/ui/mirage/scenarios/kubernetes.js deleted file mode 100644 index 6fa194715..000000000 --- a/ui/mirage/scenarios/kubernetes.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export default function (server, shouldConfigureRoles = true) { - server.create('kubernetes-config', { path: 'kubernetes' }); - if (shouldConfigureRoles) { - server.create('kubernetes-role'); - server.create('kubernetes-role', 'withRoleName'); - server.create('kubernetes-role', 'withRoleRules'); - } -} diff --git a/ui/mirage/serializers/application.js b/ui/mirage/serializers/application.js deleted file mode 100644 index a7dc21ae9..000000000 --- a/ui/mirage/serializers/application.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { JSONAPISerializer } from 'ember-cli-mirage'; - -export default JSONAPISerializer.extend({ - typeKeyForModel(model) { - return model.modelName; - }, -}); diff --git a/ui/scripts/enos-test-ember.js b/ui/scripts/enos-test-ember.js deleted file mode 100755 index 6ba89336b..000000000 --- a/ui/scripts/enos-test-ember.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -/* eslint-env node */ -/* eslint-disable no-console */ - -const testHelper = require('./test-helper'); - -(async function () { - try { - let unsealKeys = process.env.VAULT_UNSEAL_KEYS; - if (!unsealKeys) { - console.error( - 'Cannot run ember tests without unseal keys, please make sure to export the keys, in an env ' + - 'var named: VAULT_UNSEAL_KEYS' - ); - process.exit(1); - } else { - unsealKeys = JSON.parse(unsealKeys); - } - - const rootToken = process.env.VAULT_TOKEN; - if (!rootToken) { - console.error( - 'Cannot run ember tests without root token, please make sure to export the root token, in an env ' + - 'var named: VAULT_TOKEN' - ); - process.exit(1); - } - - testHelper.writeKeysFile(unsealKeys, rootToken); - } catch (error) { - console.log(error); - process.exit(1); - } - - const vaultAddr = process.env.VAULT_ADDR; - if (!vaultAddr) { - console.error( - 'Cannot run ember tests without the Vault Address, please make sure to export the vault address, in an env ' + - 'var named: VAULT_ADDR' - ); - process.exit(1); - } - - console.log('VAULT_ADDR=' + vaultAddr); - - try { - const testArgs = ['test', '-c', 'testem.enos.js']; - - if (process.env.TEST_FILTER && process.env.TEST_FILTER.length > 0) { - testArgs.push('-f=' + process.env.TEST_FILTER); - } - - await testHelper.run('ember', [...testArgs, ...process.argv.slice(2)], false); - } catch (error) { - console.log(error); - process.exit(1); - } finally { - process.exit(0); - } -})(); diff --git a/ui/scripts/start-vault.js b/ui/scripts/start-vault.js deleted file mode 100755 index 9a001007e..000000000 --- a/ui/scripts/start-vault.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -/* eslint-env node */ -/* eslint-disable no-console */ -/* eslint-disable no-process-exit */ -/* eslint-disable node/no-extraneous-require */ - -var readline = require('readline'); -const testHelper = require('./test-helper'); - -var output = ''; -var unseal, root, written, initError; - -async function processLines(input, eachLine = () => {}) { - const rl = readline.createInterface({ - input, - terminal: true, - }); - for await (const line of rl) { - eachLine(line); - } -} - -(async function () { - try { - const vault = testHelper.run( - 'vault', - [ - 'server', - '-dev', - '-dev-ha', - '-dev-transactional', - '-dev-root-token-id=root', - '-dev-listen-address=127.0.0.1:9200', - ], - false - ); - processLines(vault.stdout, function (line) { - if (written) { - output = null; - return; - } - output = output + line; - var unsealMatch = output.match(/Unseal Key: (.+)$/m); - if (unsealMatch && !unseal) { - unseal = [unsealMatch[1]]; - } - var rootMatch = output.match(/Root Token: (.+)$/m); - if (rootMatch && !root) { - root = rootMatch[1]; - } - var errorMatch = output.match(/Error initializing core: (.*)$/m); - if (errorMatch) { - initError = errorMatch[1]; - } - if (root && unseal && !written) { - testHelper.writeKeysFile(unseal, root); - written = true; - console.log('VAULT SERVER READY'); - } else if (initError) { - console.log('VAULT SERVER START FAILED'); - console.log( - 'If this is happening, run `export VAULT_LICENSE_PATH=/Users/username/license.hclic` to your valid local vault license filepath, or use OSS Vault' - ); - process.exit(1); - } - }); - try { - await testHelper.run('ember', ['test', ...process.argv.slice(2)]); - } catch (error) { - console.log(error); - process.exit(1); - } finally { - process.exit(0); - } - } catch (error) { - console.log(error); - process.exit(1); - } -})(); diff --git a/ui/scripts/test-helper.js b/ui/scripts/test-helper.js deleted file mode 100644 index 1af4a2493..000000000 --- a/ui/scripts/test-helper.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -/* eslint-env node */ -/* eslint-disable no-console */ - -const fs = require('fs'); -const path = require('path'); -const chalk = require('chalk'); -const execa = require('execa'); - -/** - * Writes a vault keys file that can be imported in other scripts, that includes the unseal keys and the root token. - * @param unsealKeys an array of unseal keys, must contain at least one key - * @param rootToken the root token - * @param filePath optional file path, if not provided the default path of /tests/helpers/vault-keys.js - * will be used. - */ -function writeKeysFile(unsealKeys, rootToken, filePath) { - if (filePath === undefined) { - filePath = path.join(process.cwd(), 'tests/helpers/vault-keys.js'); - } - const keys = {}; - keys.unsealKeys = unsealKeys; - keys.rootToken = rootToken; - - fs.writeFile(filePath, `export default ${JSON.stringify(keys, null, 2)}`, (err) => { - if (err) throw err; - }); -} - -/** - * Runs the provided command and pipes the processes stdout and stderr to the terminal. Upon completion with - * success or error the child process will be cleaned up. - * @param command some command to run - * @param args some arguments for the command to run - * @param shareStd if true the sub process created by the command will share the stdout and stderr of the parent - * process - * @returns {*} The child_process for the executed command which is also a Promise. - */ -function run(command, args = [], shareStd = true) { - console.log(chalk.dim('$ ' + command + ' ' + args.join(' '))); - // cleanup means that execa will handle stopping the subprocess - // inherit all of the stdin/out/err so that testem still works as if you were running it directly - if (shareStd) { - return execa(command, args, { cleanup: true, stdin: 'inherit', stdout: 'inherit', stderr: 'inherit' }); - } - const p = execa(command, args, { cleanup: true }); - p.stdout.pipe(process.stdout); - p.stderr.pipe(process.stderr); - return p; -} - -module.exports = { - writeKeysFile: writeKeysFile, - run: run, -}; diff --git a/ui/testem.enos.js b/ui/testem.enos.js deleted file mode 100644 index 0c8578045..000000000 --- a/ui/testem.enos.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -/* eslint-env node */ - -'use strict'; - -const vault_addr = process.env.VAULT_ADDR; -console.log('VAULT_ADDR=' + vault_addr); // eslint-disable-line - -module.exports = { - test_page: 'tests/index.html?hidepassed', - tap_quiet_logs: true, - tap_failed_tests_only: true, - disable_watching: true, - launch_in_ci: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - // --no-sandbox is needed when running Chrome inside a container - process.env.CI ? '--no-sandbox' : null, - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - ].filter(Boolean), - }, - }, - proxies: { - '/v1': { - target: vault_addr, - }, - }, -}; - -if (process.env.CI) { - module.exports.reporter = 'xunit'; - module.exports.report_file = 'test-results/qunit/results.xml'; - module.exports.xunit_intermediate_output = true; -} diff --git a/ui/testem.js b/ui/testem.js deleted file mode 100644 index bd5b8787c..000000000 --- a/ui/testem.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -'use strict'; - -module.exports = { - test_page: 'tests/index.html?hidepassed', - tap_quiet_logs: true, - tap_failed_tests_only: true, - disable_watching: true, - launch_in_ci: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - // --no-sandbox is needed when running Chrome inside a container - process.env.CI ? '--no-sandbox' : null, - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - ].filter(Boolean), - }, - }, - proxies: { - '/v1': { - target: 'http://localhost:9200', - }, - }, -}; - -if (process.env.CI) { - module.exports.reporter = 'xunit'; - module.exports.report_file = 'test-results/qunit/results.xml'; - module.exports.xunit_intermediate_output = true; -} diff --git a/ui/tests/.eslintrc.js b/ui/tests/.eslintrc.js deleted file mode 100644 index 3eae41024..000000000 --- a/ui/tests/.eslintrc.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -/* eslint-disable no-undef */ -module.exports = { - env: { - embertest: true, - }, - globals: { - server: true, - $: true, - authLogout: false, - authLogin: false, - pollCluster: false, - mountSupportedSecretBackend: false, - }, -}; diff --git a/ui/tests/acceptance/access/identity/_shared-alias-tests.js b/ui/tests/acceptance/access/identity/_shared-alias-tests.js deleted file mode 100644 index 555118792..000000000 --- a/ui/tests/acceptance/access/identity/_shared-alias-tests.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName, settled } from '@ember/test-helpers'; -import page from 'vault/tests/pages/access/identity/aliases/add'; -import aliasIndexPage from 'vault/tests/pages/access/identity/aliases/index'; -import aliasShowPage from 'vault/tests/pages/access/identity/aliases/show'; -import createItemPage from 'vault/tests/pages/access/identity/create'; -import showItemPage from 'vault/tests/pages/access/identity/show'; - -export const testAliasCRUD = async function (name, itemType, assert) { - if (itemType === 'groups') { - await createItemPage.createItem(itemType, 'external'); - await settled(); - } else { - await createItemPage.createItem(itemType); - await settled(); - } - let idRow = showItemPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; - const itemID = idRow.rowValue; - await page.visit({ item_type: itemType, id: itemID }); - await settled(); - await page.editForm.name(name).submit(); - await settled(); - assert.ok( - aliasShowPage.flashMessage.latestMessage.startsWith('Successfully saved'), - `${itemType}: shows a flash message` - ); - - idRow = aliasShowPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; - const aliasID = idRow.rowValue; - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.aliases.show', - 'navigates to the correct route' - ); - assert.ok(aliasShowPage.nameContains(name), `${itemType}: renders the name on the show page`); - - await aliasIndexPage.visit({ item_type: itemType }); - await settled(); - assert.strictEqual( - aliasIndexPage.items.filterBy('name', name).length, - 1, - `${itemType}: lists the entity in the entity list` - ); - - const item = aliasIndexPage.items.filterBy('name', name)[0]; - await item.menu(); - await settled(); - await aliasIndexPage.delete(); - await settled(); - await aliasIndexPage.confirmDelete(); - await settled(); - assert.ok( - aliasIndexPage.flashMessage.latestMessage.startsWith('Successfully deleted'), - `${itemType}: shows flash message` - ); - - assert.strictEqual( - aliasIndexPage.items.filterBy('id', aliasID).length, - 0, - `${itemType}: the row is deleted` - ); -}; - -export const testAliasDeleteFromForm = async function (name, itemType, assert) { - if (itemType === 'groups') { - await createItemPage.createItem(itemType, 'external'); - await settled(); - } else { - await createItemPage.createItem(itemType); - await settled(); - } - let idRow = showItemPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; - const itemID = idRow.rowValue; - await page.visit({ item_type: itemType, id: itemID }); - await settled(); - await page.editForm.name(name).submit(); - await settled(); - idRow = aliasShowPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; - const aliasID = idRow.rowValue; - await aliasShowPage.edit(); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.aliases.edit', - `${itemType}: navigates to edit on create` - ); - await page.editForm.delete(); - await page.editForm.waitForConfirm(); - await page.editForm.confirmDelete(); - await settled(); - assert.ok( - aliasIndexPage.flashMessage.latestMessage.startsWith('Successfully deleted'), - `${itemType}: shows flash message` - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.aliases.index', - `${itemType}: navigates to list page on delete` - ); - assert.strictEqual( - aliasIndexPage.items.filterBy('id', aliasID).length, - 0, - `${itemType}: the row does not show in the list` - ); -}; diff --git a/ui/tests/acceptance/access/identity/_shared-tests.js b/ui/tests/acceptance/access/identity/_shared-tests.js deleted file mode 100644 index 69de4b281..000000000 --- a/ui/tests/acceptance/access/identity/_shared-tests.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { settled, currentRouteName, click, waitUntil, find } from '@ember/test-helpers'; -import { selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers'; -import page from 'vault/tests/pages/access/identity/create'; -import showPage from 'vault/tests/pages/access/identity/show'; -import indexPage from 'vault/tests/pages/access/identity/index'; - -export const testCRUD = async (name, itemType, assert) => { - await page.visit({ item_type: itemType }); - await settled(); - await page.editForm.name(name).submit(); - await settled(); - assert.ok( - showPage.flashMessage.latestMessage.startsWith('Successfully saved'), - `${itemType}: shows a flash message` - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.show', - `${itemType}: navigates to show on create` - ); - assert.ok(showPage.nameContains(name), `${itemType}: renders the name on the show page`); - - await indexPage.visit({ item_type: itemType }); - await settled(); - assert.strictEqual( - indexPage.items.filterBy('name', name).length, - 1, - `${itemType}: lists the entity in the entity list` - ); - await indexPage.items.filterBy('name', name)[0].menu(); - await waitUntil(() => find('[data-test-item-delete]')); - await indexPage.delete(); - await settled(); - await indexPage.confirmDelete(); - await settled(); - assert.ok( - indexPage.flashMessage.latestMessage.startsWith('Successfully deleted'), - `${itemType}: shows flash message` - ); - assert.strictEqual(indexPage.items.filterBy('name', name).length, 0, `${itemType}: the row is deleted`); -}; - -export const testDeleteFromForm = async (name, itemType, assert) => { - await page.visit({ item_type: itemType }); - await settled(); - await page.editForm.name(name); - await page.editForm.metadataKey('hello'); - await page.editForm.metadataValue('goodbye'); - await clickTrigger('#policies'); - // first option should be "default" - await selectChoose('#policies', '.ember-power-select-option', 0); - await page.editForm.submit(); - await click('[data-test-tab-subnav="policies"]'); - assert.dom('.list-item-row').exists({ count: 1 }, 'One item is under policies'); - await click('[data-test-tab-subnav="metadata"]'); - assert.dom('.info-table-row').hasText('hello goodbye', 'Metadata shows on tab'); - await showPage.edit(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.edit', - `${itemType}: navigates to edit on create` - ); - await settled(); - await page.editForm.delete(); - await settled(); - await page.editForm.confirmDelete(); - await settled(); - assert.ok( - indexPage.flashMessage.latestMessage.startsWith('Successfully deleted'), - `${itemType}: shows flash message` - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.index', - `${itemType}: navigates to list page on delete` - ); - assert.strictEqual( - indexPage.items.filterBy('name', name).length, - 0, - `${itemType}: the row does not show in the list` - ); -}; diff --git a/ui/tests/acceptance/access/identity/entities/aliases/create-test.js b/ui/tests/acceptance/access/identity/entities/aliases/create-test.js deleted file mode 100644 index 99b29fa7c..000000000 --- a/ui/tests/acceptance/access/identity/entities/aliases/create-test.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, skip, test } from 'qunit'; -import { settled } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import { testAliasCRUD, testAliasDeleteFromForm } from '../../_shared-alias-tests'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | /access/identity/entities/aliases/add', function (hooks) { - // TODO come back and figure out why this is failing. Seems to be a race condition - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - await authPage.login(); - return; - }); - - skip('it allows create, list, delete of an entity alias', async function (assert) { - assert.expect(6); - const name = `alias-${Date.now()}`; - await testAliasCRUD(name, 'entities', assert); - await settled(); - }); - - test('it allows delete from the edit form', async function (assert) { - assert.expect(4); - const name = `alias-${Date.now()}`; - await testAliasDeleteFromForm(name, 'entities', assert); - await settled(); - }); -}); diff --git a/ui/tests/acceptance/access/identity/entities/create-test.js b/ui/tests/acceptance/access/identity/entities/create-test.js deleted file mode 100644 index 6d7b44580..000000000 --- a/ui/tests/acceptance/access/identity/entities/create-test.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import page from 'vault/tests/pages/access/identity/create'; -import { testCRUD, testDeleteFromForm } from '../_shared-tests'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | /access/identity/entities/create', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it visits the correct page', async function (assert) { - await page.visit({ item_type: 'entities' }); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.create', - 'navigates to the correct route' - ); - }); - - test('it allows create, list, delete of an entity', async function (assert) { - assert.expect(6); - const name = `entity-${Date.now()}`; - await testCRUD(name, 'entities', assert); - }); - - test('it can be deleted from the edit form', async function (assert) { - assert.expect(6); - const name = `entity-${Date.now()}`; - await testDeleteFromForm(name, 'entities', assert); - }); -}); diff --git a/ui/tests/acceptance/access/identity/entities/index-test.js b/ui/tests/acceptance/access/identity/entities/index-test.js deleted file mode 100644 index fa9bcc5de..000000000 --- a/ui/tests/acceptance/access/identity/entities/index-test.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import page from 'vault/tests/pages/access/identity/index'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | /access/identity/entities', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it renders the entities page', async function (assert) { - await page.visit({ item_type: 'entities' }); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.index', - 'navigates to the correct route' - ); - }); - - test('it renders the groups page', async function (assert) { - await page.visit({ item_type: 'groups' }); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.index', - 'navigates to the correct route' - ); - }); -}); diff --git a/ui/tests/acceptance/access/identity/groups/aliases/create-test.js b/ui/tests/acceptance/access/identity/groups/aliases/create-test.js deleted file mode 100644 index c04a6d175..000000000 --- a/ui/tests/acceptance/access/identity/groups/aliases/create-test.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, skip, test } from 'qunit'; -import { settled } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import { testAliasCRUD, testAliasDeleteFromForm } from '../../_shared-alias-tests'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | /access/identity/groups/aliases/add', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - await authPage.login(); - return; - }); - - skip('it allows create, list, delete of an entity alias', async function (assert) { - // TODO figure out what is wrong with this test - assert.expect(6); - const name = `alias-${Date.now()}`; - await testAliasCRUD(name, 'groups', assert); - await settled(); - }); - - test('it allows delete from the edit form', async function (assert) { - // TODO figure out what is wrong with this test - assert.expect(4); - const name = `alias-${Date.now()}`; - await testAliasDeleteFromForm(name, 'groups', assert); - await settled(); - }); -}); diff --git a/ui/tests/acceptance/access/identity/groups/create-test.js b/ui/tests/acceptance/access/identity/groups/create-test.js deleted file mode 100644 index 41f9c2f4c..000000000 --- a/ui/tests/acceptance/access/identity/groups/create-test.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import page from 'vault/tests/pages/access/identity/create'; -import { testCRUD, testDeleteFromForm } from '../_shared-tests'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | /access/identity/groups/create', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it visits the correct page', async function (assert) { - await page.visit({ item_type: 'groups' }); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.identity.create', - 'navigates to the correct route' - ); - }); - - test('it allows create, list, delete of an group', async function (assert) { - assert.expect(6); - const name = `group-${Date.now()}`; - await testCRUD(name, 'groups', assert); - }); - - test('it can be deleted from the group edit form', async function (assert) { - assert.expect(6); - const name = `group-${Date.now()}`; - await testDeleteFromForm(name, 'groups', assert); - }); -}); diff --git a/ui/tests/acceptance/access/methods-test.js b/ui/tests/acceptance/access/methods-test.js deleted file mode 100644 index 62b4d74c9..000000000 --- a/ui/tests/acceptance/access/methods-test.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName } from '@ember/test-helpers'; -import { clickTrigger } from 'ember-power-select/test-support/helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; -import page from 'vault/tests/pages/access/methods'; -import authEnable from 'vault/tests/pages/settings/auth/enable'; -import authPage from 'vault/tests/pages/auth'; -import ss from 'vault/tests/pages/components/search-select'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -import { v4 as uuidv4 } from 'uuid'; - -const consoleComponent = create(consoleClass); -const searchSelect = create(ss); - -module('Acceptance | auth-methods list view', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('it navigates to auth method', async function (assert) { - await page.visit(); - assert.strictEqual(currentRouteName(), 'vault.cluster.access.methods', 'navigates to the correct route'); - assert.ok(page.methodsLink.isActive, 'the first link is active'); - assert.strictEqual(page.methodsLink.text, 'Authentication methods'); - }); - - test('it filters by name and auth type', async function (assert) { - assert.expect(4); - const authPath1 = `userpass-1-${this.uid}`; - const authPath2 = `userpass-2-${this.uid}`; - const type = 'userpass'; - await authEnable.visit(); - await authEnable.enable(type, authPath1); - await authEnable.visit(); - await authEnable.enable(type, authPath2); - await page.visit(); - // filter by auth type - - await clickTrigger('#filter-by-auth-type'); - await searchSelect.options.objectAt(0).click(); - - const rows = document.querySelectorAll('[data-test-auth-backend-link]'); - const rowsUserpass = Array.from(rows).filter((row) => row.innerText.includes('userpass')); - - assert.strictEqual(rows.length, rowsUserpass.length, 'all rows returned are userpass'); - - // filter by name - await clickTrigger('#filter-by-auth-name'); - const firstItemToSelect = searchSelect.options.objectAt(0).text; - await searchSelect.options.objectAt(0).click(); - const singleRow = document.querySelectorAll('[data-test-auth-backend-link]'); - - assert.strictEqual(singleRow.length, 1, 'returns only one row'); - assert.dom(singleRow[0]).includesText(firstItemToSelect, 'shows the filtered by auth name'); - // clear filter by engine name - await searchSelect.deleteButtons.objectAt(1).click(); - const rowsAgain = document.querySelectorAll('[data-test-auth-backend-link]'); - assert.ok(rowsAgain.length > 1, 'filter has been removed'); - - // cleanup - await consoleComponent.runCommands([`delete sys/auth/${authPath1}`]); - await consoleComponent.runCommands([`delete sys/auth/${authPath2}`]); - }); -}); diff --git a/ui/tests/acceptance/access/namespaces/index-test.js b/ui/tests/acceptance/access/namespaces/index-test.js deleted file mode 100644 index 252d6b54a..000000000 --- a/ui/tests/acceptance/access/namespaces/index-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import page from 'vault/tests/pages/access/namespaces/index'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; - -module('Acceptance | Enterprise | /access/namespaces', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - test('it navigates to namespaces page', async function (assert) { - assert.expect(1); - await page.visit(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.namespaces.index', - 'navigates to the correct route' - ); - }); - - test('it should render correct number of namespaces', async function (assert) { - assert.expect(3); - await page.visit(); - const store = this.owner.lookup('service:store'); - // Default page size is 15 - assert.strictEqual(store.peekAll('namespace').length, 15, 'Store has 15 namespaces records'); - assert.dom('.list-item-row').exists({ count: 15 }); - assert.dom('[data-test-list-view-pagination]').exists(); - }); -}); diff --git a/ui/tests/acceptance/auth-list-test.js b/ui/tests/acceptance/auth-list-test.js deleted file mode 100644 index 316bba061..000000000 --- a/ui/tests/acceptance/auth-list-test.js +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -/* eslint qunit/no-conditional-assertions: "warn" */ -import { - click, - fillIn, - settled, - visit, - triggerKeyEvent, - find, - waitUntil, - currentURL, -} from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/auth/enable'; -import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends'; -import { supportedManagedAuthBackends } from 'vault/helpers/supported-managed-auth-backends'; -import { create } from 'ember-cli-page-object'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const consoleComponent = create(consoleClass); - -module('Acceptance | auth backend list', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - test('userpass secret backend', async function (assert) { - let n = Math.random(); - const path1 = `userpass-${++n}`; - const path2 = `userpass-${++n}`; - const user1 = 'user1'; - const user2 = 'user2'; - - // enable the first userpass method with one username - await enablePage.enable('userpass', path1); - await settled(); - await click('[data-test-save-config="true"]'); - - await visit(`/vault/access/${path1}/item/user/create`); - await waitUntil(() => find('[data-test-input="username"]') && find('[data-test-textarea]')); - await fillIn('[data-test-input="username"]', user1); - await triggerKeyEvent('[data-test-input="username"]', 'keyup', 65); - await fillIn('[data-test-textarea]', user1); - await triggerKeyEvent('[data-test-textarea]', 'keyup', 65); - await click('[data-test-save-config="true"]'); - - // enable the first userpass method with one username - await visit(`/vault/settings/auth/enable`); - - await click('[data-test-mount-type="userpass"]'); - - await click('[data-test-mount-next]'); - - await fillIn('[data-test-input="path"]', path2); - - await click('[data-test-mount-submit="true"]'); - - await click('[data-test-save-config="true"]'); - - await click(`[data-test-auth-backend-link="${path2}"]`); - - await click('[data-test-entity-create-link="user"]'); - - await fillIn('[data-test-input="username"]', user2); - await triggerKeyEvent('[data-test-input="username"]', 'keyup', 65); - await fillIn('[data-test-textarea]', user2); - await triggerKeyEvent('[data-test-textarea]', 'keyup', 65); - - await click('[data-test-save-config="true"]'); - - //confirming that the user was created. There was a bug where the apiPath was not being updated when toggling between auth routes - assert - .dom('[data-test-list-item-content]') - .hasText(user2, 'user just created shows in current auth list'); - - //confirm that the auth method 1 shows the user1. There was a bug where it was not updated the list when toggling between auth routes - await visit(`/vault/access/${path1}/item/user`); - - assert - .dom('[data-test-list-item-content]') - .hasText(user1, 'first user created shows in current auth list'); - }); - - test('auth methods are linkable and link to correct view', async function (assert) { - assert.expect(16); - const uid = uuidv4(); - await visit('/vault/access'); - - const supportManaged = supportedManagedAuthBackends(); - const backends = supportedAuthBackends(); - for (const backend of backends) { - const { type } = backend; - const path = `auth-list-${type}-${uid}`; - if (type !== 'token') { - await enablePage.enable(type, path); - } - await settled(); - await visit('/vault/access'); - - // all auth methods should be linkable - await click(`[data-test-auth-backend-link="${type === 'token' ? type : path}"]`); - if (!supportManaged.includes(type)) { - assert.dom('[data-test-auth-section-tab]').exists({ count: 1 }); - assert - .dom('[data-test-auth-section-tab]') - .hasText('Configuration', `only shows configuration tab for ${type} auth method`); - assert.dom('[data-test-doc-link] .doc-link').exists(`includes doc link for ${type} auth method`); - } else { - let expectedTabs = 2; - if (type == 'ldap' || type === 'okta') { - expectedTabs = 3; - } - assert - .dom('[data-test-auth-section-tab]') - .exists({ count: expectedTabs }, `has management tabs for ${type} auth method`); - // cleanup method - await consoleComponent.runCommands(`delete sys/auth/${path}`); - } - } - }); - - test('enterprise: token config within namespace', async function (assert) { - const ns = 'ns-wxyz'; - await consoleComponent.runCommands(`write sys/namespaces/${ns} -f`); - await authPage.loginNs(ns); - // go directly to token configure route - await visit('/vault/settings/auth/configure/token/options'); - await fillIn('[data-test-input="description"]', 'My custom description'); - await click('[data-test-save-config="true"]'); - assert.strictEqual(currentURL(), '/vault/access', 'successfully saves and navigates away'); - await click('[data-test-auth-backend-link="token"]'); - assert - .dom('[data-test-row-value="Description"]') - .hasText('My custom description', 'description was saved'); - await consoleComponent.runCommands(`delete sys/namespaces/${ns}`); - }); -}); diff --git a/ui/tests/acceptance/auth-test.js b/ui/tests/acceptance/auth-test.js deleted file mode 100644 index fe6bb226d..000000000 --- a/ui/tests/acceptance/auth-test.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -/* eslint qunit/no-conditional-assertions: "warn" */ -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import sinon from 'sinon'; -import { click, currentURL, visit, waitUntil, find } from '@ember/test-helpers'; -import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends'; -import authForm from '../pages/components/auth-form'; -import jwtForm from '../pages/components/auth-jwt'; -import { create } from 'ember-cli-page-object'; -import apiStub from 'vault/tests/helpers/noop-all-api-requests'; -import logout from 'vault/tests/pages/logout'; - -const component = create(authForm); -const jwtComponent = create(jwtForm); - -module('Acceptance | auth', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.clock = sinon.useFakeTimers({ - now: Date.now(), - shouldAdvanceTime: true, - }); - this.server = apiStub({ usePassthrough: true }); - return logout.visit(); - }); - - hooks.afterEach(function () { - this.clock.restore(); - this.server.shutdown(); - return logout.visit(); - }); - - test('auth query params', async function (assert) { - const backends = supportedAuthBackends(); - assert.expect(backends.length + 1); - await visit('/vault/auth'); - assert.strictEqual(currentURL(), '/vault/auth?with=token'); - for (const backend of backends.reverse()) { - await component.selectMethod(backend.type); - assert.strictEqual( - currentURL(), - `/vault/auth?with=${backend.type}`, - `has the correct URL for ${backend.type}` - ); - } - }); - - test('it clears token when changing selected auth method', async function (assert) { - await visit('/vault/auth'); - assert.strictEqual(currentURL(), '/vault/auth?with=token'); - await component.token('token').selectMethod('github'); - await component.selectMethod('token'); - assert.strictEqual(component.tokenValue, '', 'it clears the token value when toggling methods'); - }); - - test('it sends the right attributes when authenticating', async function (assert) { - assert.expect(8); - const backends = supportedAuthBackends(); - await visit('/vault/auth'); - for (const backend of backends.reverse()) { - await component.selectMethod(backend.type); - if (backend.type === 'github') { - await component.token('token'); - } - if (backend.type === 'jwt' || backend.type === 'oidc') { - await jwtComponent.role('test'); - } - await component.login(); - const lastRequest = this.server.passthroughRequests[this.server.passthroughRequests.length - 1]; - let body = JSON.parse(lastRequest.requestBody); - // Note: x-vault-token used to be lowercase prior to upgrade - if (backend.type === 'token') { - assert.ok( - Object.keys(lastRequest.requestHeaders).includes('X-Vault-Token'), - 'token uses vault token header' - ); - } else if (backend.type === 'github') { - assert.ok(Object.keys(body).includes('token'), 'GitHub includes token'); - } else if (backend.type === 'jwt' || backend.type === 'oidc') { - const authReq = this.server.passthroughRequests[this.server.passthroughRequests.length - 2]; - body = JSON.parse(authReq.requestBody); - assert.ok(Object.keys(body).includes('role'), `${backend.type} includes role`); - } else { - assert.ok(Object.keys(body).includes('password'), `${backend.type} includes password`); - } - } - }); - - test('it shows the push notification warning after submit', async function (assert) { - assert.expect(1); - - this.server.get('/v1/auth/token/lookup-self', async () => { - assert.ok( - await waitUntil(() => find('[data-test-auth-message="push"]')), - 'shows push notification message' - ); - return [204, { 'Content-Type': 'application/json' }, JSON.stringify({})]; - }); - - await visit('/vault/auth'); - await component.selectMethod('token'); - await click('[data-test-auth-submit]'); - }); -}); diff --git a/ui/tests/acceptance/aws-test.js b/ui/tests/acceptance/aws-test.js deleted file mode 100644 index 1982d2e10..000000000 --- a/ui/tests/acceptance/aws-test.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, fillIn, findAll, currentURL, find, settled, waitUntil } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; - -module('Acceptance | aws secret backend', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - const POLICY = { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Action: 'iam:*', - Resource: '*', - }, - ], - }; - test('aws backend', async function (assert) { - assert.expect(12); - const path = `aws-${this.uid}`; - const roleName = 'awsrole'; - - await enablePage.enable('aws', path); - await settled(); - await click('[data-test-configuration-tab]'); - - await click('[data-test-secret-backend-configure]'); - - assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${path}`); - assert.ok(findAll('[data-test-aws-root-creds-form]').length, 'renders the empty root creds form'); - assert.ok(findAll('[data-test-aws-link="root-creds"]').length, 'renders the root creds link'); - assert.ok(findAll('[data-test-aws-link="leases"]').length, 'renders the leases config link'); - - await fillIn('[data-test-aws-input="accessKey"]', 'foo'); - await fillIn('[data-test-aws-input="secretKey"]', 'bar'); - - await click('[data-test-aws-input="root-save"]'); - - assert.ok( - find('[data-test-flash-message]').textContent.trim(), - `The backend configuration saved successfully!` - ); - - await click('[data-test-aws-link="leases"]'); - - await click('[data-test-aws-input="lease-save"]'); - - assert.ok( - find('[data-test-flash-message]').textContent.trim(), - `The backend configuration saved successfully!` - ); - - await click('[data-test-backend-view-link]'); - - assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`, `navigates to the roles list`); - - await click('[data-test-secret-create]'); - - assert.ok( - find('[data-test-secret-header]').textContent.includes('AWS Role'), - `aws: renders the create page` - ); - - await fillIn('[data-test-input="name"]', roleName); - - findAll('.CodeMirror')[0].CodeMirror.setValue(JSON.stringify(POLICY)); - - // save the role - await click('[data-test-role-aws-create]'); - await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/show/${roleName}`, - `$aws: navigates to the show page on creation` - ); - - await click('[data-test-secret-root-link]'); - - assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`); - assert.ok(findAll(`[data-test-secret-link="${roleName}"]`).length, `aws: role shows in the list`); - - //and delete - await click(`[data-test-secret-link="${roleName}"] [data-test-popup-menu-trigger]`); - await waitUntil(() => find(`[data-test-aws-role-delete="${roleName}"]`)); // flaky without - await click(`[data-test-aws-role-delete="${roleName}"]`); - await click(`[data-test-confirm-button]`); - assert.dom(`[data-test-secret-link="${roleName}"]`).doesNotExist(`aws: role is no longer in the list`); - }); -}); diff --git a/ui/tests/acceptance/client-dashboard-test.js b/ui/tests/acceptance/client-dashboard-test.js deleted file mode 100644 index f50b00be8..000000000 --- a/ui/tests/acceptance/client-dashboard-test.js +++ /dev/null @@ -1,393 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { visit, currentURL, click, findAll, find, settled } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import authPage from 'vault/tests/pages/auth'; -import { addMonths, formatRFC3339, startOfMonth, subMonths } from 'date-fns'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import ENV from 'vault/config/environment'; -import { SELECTORS, overrideResponse } from '../helpers/clients'; -import { create } from 'ember-cli-page-object'; -import ss from 'vault/tests/pages/components/search-select'; -import { clickTrigger } from 'ember-power-select/test-support/helpers'; -import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters'; -import { formatNumber } from 'core/helpers/format-number'; -import timestamp from 'core/utils/timestamp'; - -const searchSelect = create(ss); - -const STATIC_NOW = new Date('2023-01-13T14:15:00'); -// for testing, we're in the middle of a license/billing period -const LICENSE_START = startOfMonth(subMonths(STATIC_NOW, 6)); // 2022-07-01 -// upgrade happened 1 month after license start -const UPGRADE_DATE = addMonths(LICENSE_START, 1); // 2022-08-01 - -module('Acceptance | client counts dashboard tab', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - sinon.stub(timestamp, 'now').callsFake(() => STATIC_NOW); - ENV['ember-cli-mirage'].handler = 'clients'; - }); - - hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - }); - - hooks.after(function () { - timestamp.now.restore(); - ENV['ember-cli-mirage'].handler = null; - }); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('shows warning when config off, no data', async function (assert) { - assert.expect(4); - this.server.get('sys/internal/counters/activity', () => overrideResponse(204)); - this.server.get('sys/internal/counters/config', () => { - return { - request_id: 'some-config-id', - data: { - default_report_months: 12, - enabled: 'default-disable', - queries_available: false, - retention_months: 24, - }, - }; - }); - await visit('/vault/clients/dashboard'); - assert.strictEqual(currentURL(), '/vault/clients/dashboard'); - assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active'); - assert.dom(SELECTORS.emptyStateTitle).hasText('Data tracking is disabled'); - assert.dom(SELECTORS.filterBar).doesNotExist('Filter bar is hidden when no data available'); - }); - - test('shows empty state when config enabled and no data', async function (assert) { - assert.expect(4); - this.server.get('sys/internal/counters/activity', () => overrideResponse(204)); - this.server.get('sys/internal/counters/config', () => { - return { - request_id: 'some-config-id', - data: { - default_report_months: 12, - enabled: 'default-enable', - retention_months: 24, - }, - }; - }); - await visit('/vault/clients/dashboard'); - assert.strictEqual(currentURL(), '/vault/clients/dashboard'); - assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active'); - assert.dom(SELECTORS.emptyStateTitle).hasTextContaining('No data received'); - assert.dom(SELECTORS.filterBar).doesNotExist('Does not show filter bar'); - }); - - test('visiting dashboard tab config on and data with mounts', async function (assert) { - assert.expect(8); - await visit('/vault/clients/dashboard'); - assert.strictEqual(currentURL(), '/vault/clients/dashboard'); - assert - .dom(SELECTORS.dateDisplay) - .hasText('July 2022', 'billing start month is correctly parsed from license'); - assert - .dom(SELECTORS.rangeDropdown) - .hasText(`Jul 2022 - Jan 2023`, 'Date range shows dates correctly parsed activity response'); - assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area'); - assert.dom(SELECTORS.monthlyUsageBlock).exists('Shows monthly usage block'); - assert - .dom(SELECTORS.runningTotalMonthlyCharts) - .exists('Shows running totals with monthly breakdown charts'); - assert - .dom(find('[data-test-line-chart="x-axis-labels"] g.tick text')) - .hasText(`7/22`, 'x-axis labels start with billing start date'); - assert.strictEqual( - findAll('[data-test-line-chart="plot-point"]').length, - 6, - `line chart plots 6 points to match query` - ); - }); - - test('updates correctly when querying date ranges', async function (assert) { - assert.expect(27); - await visit('/vault/clients/dashboard'); - assert.strictEqual(currentURL(), '/vault/clients/dashboard'); - // query for single, historical month with no new counts - await click(SELECTORS.rangeDropdown); - await click('[data-test-show-calendar]'); - if (parseInt(find('[data-test-display-year]').innerText) > LICENSE_START.getFullYear()) { - await click('[data-test-previous-year]'); - } - await click(find(`[data-test-calendar-month=${ARRAY_OF_MONTHS[LICENSE_START.getMonth()]}]`)); - assert.dom('[data-test-usage-stats]').exists('total usage stats show'); - assert - .dom(SELECTORS.runningTotalMonthStats) - .doesNotExist('running total single month stat boxes do not show'); - assert - .dom(SELECTORS.runningTotalMonthlyCharts) - .doesNotExist('running total month over month charts do not show'); - assert.dom(SELECTORS.monthlyUsageBlock).doesNotExist('does not show monthly usage block'); - assert.dom(SELECTORS.attributionBlock).exists('attribution area shows'); - assert - .dom('[data-test-chart-container="new-clients"] [data-test-component="empty-state"]') - .exists('new client attribution has empty state'); - assert - .dom('[data-test-empty-state-subtext]') - .hasText('There are no new clients for this namespace during this time period. '); - assert.dom('[data-test-chart-container="total-clients"]').exists('total client attribution chart shows'); - - // reset to billing period - await click(SELECTORS.rangeDropdown); - await click('[data-test-current-billing-period]'); - - // change billing start to month/year of first upgrade - await click('[data-test-start-date-editor] button'); - await click(SELECTORS.monthDropdown); - await click(`[data-test-dropdown-month="${ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()]}"]`); - await click(SELECTORS.yearDropdown); - await click(`[data-test-dropdown-year="${UPGRADE_DATE.getFullYear()}"]`); - await click('[data-test-date-dropdown-submit]'); - assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area'); - assert.dom(SELECTORS.monthlyUsageBlock).exists('Shows monthly usage block'); - assert - .dom(SELECTORS.runningTotalMonthlyCharts) - .exists('Shows running totals with monthly breakdown charts'); - assert - .dom(find('[data-test-line-chart="x-axis-labels"] g.tick text')) - .hasText(`8/22`, 'x-axis labels start with updated billing start month'); - assert.strictEqual( - findAll('[data-test-line-chart="plot-point"]').length, - 6, - `line chart plots 6 points to match query` - ); - - // query three months ago (Oct 2022) - await click(SELECTORS.rangeDropdown); - await click('[data-test-show-calendar]'); - await click('[data-test-previous-year]'); - await click(find(`[data-test-calendar-month="October"]`)); - - assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area'); - assert.dom(SELECTORS.monthlyUsageBlock).exists('Shows monthly usage block'); - assert - .dom(SELECTORS.runningTotalMonthlyCharts) - .exists('Shows running totals with monthly breakdown charts'); - assert.strictEqual( - findAll('[data-test-line-chart="plot-point"]').length, - 3, - `line chart plots 3 points to match query` - ); - const xAxisLabels = findAll('[data-test-line-chart="x-axis-labels"] g.tick text'); - assert - .dom(xAxisLabels[xAxisLabels.length - 1]) - .hasText(`10/22`, 'x-axis labels end with queried end month'); - - // query for single, historical month (upgrade month) - await click(SELECTORS.rangeDropdown); - await click('[data-test-show-calendar]'); - assert.dom('[data-test-display-year]').hasText('2022'); - await click(find(`[data-test-calendar-month="August"]`)); - - assert.dom(SELECTORS.runningTotalMonthStats).exists('running total single month stat boxes show'); - assert - .dom(SELECTORS.runningTotalMonthlyCharts) - .doesNotExist('running total month over month charts do not show'); - assert.dom(SELECTORS.monthlyUsageBlock).doesNotExist('Does not show monthly usage block'); - assert.dom(SELECTORS.attributionBlock).exists('attribution area shows'); - assert.dom('[data-test-chart-container="new-clients"]').exists('new client attribution chart shows'); - assert.dom('[data-test-chart-container="total-clients"]').exists('total client attribution chart shows'); - - // reset to billing period - await click(SELECTORS.rangeDropdown); - await click('[data-test-current-billing-period]'); - // query month older than count start date - await click('[data-test-start-date-editor] button'); - await click(SELECTORS.monthDropdown); - await click(`[data-test-dropdown-month="${ARRAY_OF_MONTHS[LICENSE_START.getMonth()]}"]`); - await click(SELECTORS.yearDropdown); - await click(`[data-test-dropdown-year="${LICENSE_START.getFullYear() - 3}"]`); - await click('[data-test-date-dropdown-submit]'); - assert - .dom('[data-test-alert-banner="alert"]') - .hasTextContaining( - `We only have data from January 2022`, - 'warning banner displays that date queried was prior to count start date' - ); - }); - - test('dashboard filters correctly with full data', async function (assert) { - assert.expect(21); - await visit('/vault/clients/dashboard'); - assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct'); - assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active'); - assert - .dom(SELECTORS.runningTotalMonthlyCharts) - .exists('Shows running totals with monthly breakdown charts'); - assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area'); - assert.dom(SELECTORS.monthlyUsageBlock).exists('Shows monthly usage block'); - const response = await this.store.peekRecord('clients/activity', 'some-activity-id'); - - // FILTER BY NAMESPACE - await clickTrigger(); - await searchSelect.options.objectAt(0).click(); - await settled(); - const topNamespace = response.byNamespace[0]; - const topMount = topNamespace.mounts[0]; - assert.ok(true, 'Filter by first namespace'); - assert.strictEqual( - find(SELECTORS.selectedNs).innerText.toLowerCase(), - topNamespace.label, - 'selects top namespace' - ); - assert.dom('[data-test-top-attribution]').includesText('Top auth method'); - assert - .dom('[data-test-running-total-entity] p') - .includesText(`${formatNumber([topNamespace.entity_clients])}`, 'total entity clients is accurate'); - assert - .dom('[data-test-running-total-nonentity] p') - .includesText( - `${formatNumber([topNamespace.non_entity_clients])}`, - 'total non-entity clients is accurate' - ); - assert - .dom('[data-test-attribution-clients] p') - .includesText(`${formatNumber([topMount.clients])}`, 'top attribution clients accurate'); - - // FILTER BY AUTH METHOD - await clickTrigger(); - await searchSelect.options.objectAt(0).click(); - await settled(); - assert.ok(true, 'Filter by first auth method'); - assert.strictEqual( - find(SELECTORS.selectedAuthMount).innerText.toLowerCase(), - topMount.label, - 'selects top mount' - ); - assert - .dom('[data-test-running-total-entity] p') - .includesText(`${formatNumber([topMount.entity_clients])}`, 'total entity clients is accurate'); - assert - .dom('[data-test-running-total-nonentity] p') - .includesText(`${formatNumber([topMount.non_entity_clients])}`, 'total non-entity clients is accurate'); - assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution block'); - - await click('#namespace-search-select [data-test-selected-list-button="delete"]'); - assert.ok(true, 'Remove namespace filter without first removing auth method filter'); - assert.dom('[data-test-top-attribution]').includesText('Top namespace'); - assert - .dom('[data-test-running-total-entity]') - .hasTextContaining( - `${formatNumber([response.total.entity_clients])}`, - 'total entity clients is back to unfiltered value' - ); - assert - .dom('[data-test-running-total-nonentity]') - .hasTextContaining( - `${formatNumber([formatNumber([response.total.non_entity_clients])])}`, - 'total non-entity clients is back to unfiltered value' - ); - assert - .dom('[data-test-attribution-clients]') - .hasTextContaining( - `${formatNumber([topNamespace.clients])}`, - 'top attribution clients back to unfiltered value' - ); - }); - - test('shows warning if upgrade happened within license period', async function (assert) { - assert.expect(3); - this.server.get('sys/version-history', function () { - return { - data: { - keys: ['1.9.0', '1.9.1', '1.9.2', '1.10.1'], - key_info: { - '1.9.0': { - previous_version: null, - timestamp_installed: formatRFC3339(subMonths(UPGRADE_DATE, 4)), - }, - '1.9.1': { - previous_version: '1.9.0', - timestamp_installed: formatRFC3339(subMonths(UPGRADE_DATE, 3)), - }, - '1.9.2': { - previous_version: '1.9.1', - timestamp_installed: formatRFC3339(subMonths(UPGRADE_DATE, 2)), - }, - '1.10.1': { - previous_version: '1.9.2', - timestamp_installed: formatRFC3339(UPGRADE_DATE), - }, - }, - }, - }; - }); - await visit('/vault/clients/dashboard'); - assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct'); - assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active'); - assert - .dom('[data-test-alert-banner="alert"]') - .hasTextContaining( - `Warning Vault was upgraded to 1.10.1 on Aug 1, 2022. We added monthly breakdowns and mount level attribution starting in 1.10, so keep that in mind when looking at the data. Learn more here.` - ); - }); - - test('Shows empty if license start date is current month', async function (assert) { - // TODO cmb update to reflect new behavior - const licenseStart = STATIC_NOW; - const licenseEnd = addMonths(licenseStart, 12); - this.server.get('sys/license/status', function () { - return { - request_id: 'my-license-request-id', - data: { - autoloaded: { - license_id: 'my-license-id', - start_time: formatRFC3339(licenseStart), - expiration_time: formatRFC3339(licenseEnd), - }, - }, - }; - }); - await visit('/vault/clients/dashboard'); - assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct'); - assert.dom(SELECTORS.emptyStateTitle).doesNotExist('No data for this billing period'); - }); - - test('shows correct interface if no permissions on license', async function (assert) { - this.server.get('/sys/license/status', () => overrideResponse(403)); - await visit('/vault/clients/dashboard'); - assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct'); - assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active'); - // Message changes depending on ent or OSS - assert.dom(SELECTORS.emptyStateTitle).exists('Empty state exists'); - assert.dom(SELECTORS.monthDropdown).exists('Dropdown exists to select month'); - assert.dom(SELECTORS.yearDropdown).exists('Dropdown exists to select year'); - }); - - test('shows error template if permissions denied querying activity response with no data', async function (assert) { - this.server.get('sys/license/status', () => overrideResponse(403)); - this.server.get('sys/version-history', () => overrideResponse(403)); - this.server.get('sys/internal/counters/config', () => overrideResponse(403)); - this.server.get('sys/internal/counters/activity', () => overrideResponse(403)); - - await visit('/vault/clients/dashboard'); - assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct'); - assert - .dom(SELECTORS.emptyStateTitle) - .includesText('start date found', 'Empty state shows no billing start date'); - await click(SELECTORS.monthDropdown); - await click(this.element.querySelector('[data-test-month-list] button:not([disabled])')); - await click(SELECTORS.yearDropdown); - await click(this.element.querySelector('[data-test-year-list] button:not([disabled])')); - await click(SELECTORS.dateDropdownSubmit); - assert - .dom(SELECTORS.emptyStateTitle) - .hasText('You are not authorized', 'Empty state displays not authorized message'); - }); -}); diff --git a/ui/tests/acceptance/cluster-test.js b/ui/tests/acceptance/cluster-test.js deleted file mode 100644 index ddfc3b0af..000000000 --- a/ui/tests/acceptance/cluster-test.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { create } from 'ember-cli-page-object'; -import { settled, click, visit } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/auth/enable'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const consoleComponent = create(consoleClass); - -const tokenWithPolicy = async function (name, policy) { - await consoleComponent.runCommands([ - `write sys/policies/acl/${name} policy=${btoa(policy)}`, - `write -field=client_token auth/token/create policies=${name}`, - ]); - - return consoleComponent.lastLogOutput; -}; - -module('Acceptance | cluster', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - await logout.visit(); - return authPage.login(); - }); - - test('hides nav item if user does not have permission', async function (assert) { - const deny_policies_policy = ` - path "sys/policies/*" { - capabilities = ["deny"] - }, - `; - - const userToken = await tokenWithPolicy('hide-policies-nav', deny_policies_policy); - await logout.visit(); - await authPage.login(userToken); - await visit('/vault/access'); - - assert.dom('[data-test-sidebar-nav-link="Policies"]').doesNotExist(); - await logout.visit(); - }); - - test('it hides mfa setup if user has not entityId (ex: is a root user)', async function (assert) { - const user = 'end-user'; - const password = 'mypassword'; - const path = `cluster-userpass-${uuidv4()}`; - - await enablePage.enable('userpass', path); - await consoleComponent.runCommands([`write auth/${path}/users/end-user password="${password}"`]); - - await logout.visit(); - await settled(); - await authPage.loginUsername(user, password, path); - await click('[data-test-user-menu-trigger]'); - assert.dom('[data-test-user-menu-item="mfa"]').exists(); - await logout.visit(); - - await authPage.login('root'); - await settled(); - await click('[data-test-user-menu-trigger]'); - assert.dom('[data-test-user-menu-item="mfa"]').doesNotExist(); - }); - - test('enterprise nav item links to first route that user has access to', async function (assert) { - const read_rgp_policy = ` - path "sys/policies/rgp" { - capabilities = ["read"] - }, - `; - - const userToken = await tokenWithPolicy('show-policies-nav', read_rgp_policy); - await logout.visit(); - await authPage.login(userToken); - await visit('/vault/access'); - - assert.dom('[data-test-sidebar-nav-link="Policies"]').hasAttribute('href', '/ui/vault/policies/rgp'); - await logout.visit(); - }); -}); diff --git a/ui/tests/acceptance/console-test.js b/ui/tests/acceptance/console-test.js deleted file mode 100644 index 409a70a49..000000000 --- a/ui/tests/acceptance/console-test.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { settled, waitUntil, click } from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import { setupApplicationTest } from 'ember-qunit'; -import enginesPage from 'vault/tests/pages/secrets/backends'; -import authPage from 'vault/tests/pages/auth'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const consoleComponent = create(consoleClass); - -module('Acceptance | console', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test("refresh reloads the current route's data", async function (assert) { - await enginesPage.visit(); - await settled(); - const numEngines = enginesPage.rows.length; - await consoleComponent.toggle(); - await settled(); - for (const num of [1, 2, 3]) { - const inputString = `write sys/mounts/console-route-${num} type=kv`; - await consoleComponent.runCommands(inputString); - await settled(); - } - await consoleComponent.runCommands('refresh'); - await settled(); - assert.strictEqual(enginesPage.rows.length, numEngines + 3, 'new engines were added to the page'); - // Clean up - for (const num of [1, 2, 3]) { - const inputString = `delete sys/mounts/console-route-${num}`; - await consoleComponent.runCommands(inputString); - await settled(); - } - await consoleComponent.runCommands('refresh'); - await settled(); - assert.strictEqual(enginesPage.rows.length, numEngines, 'engines were removed from the page'); - }); - - test('fullscreen command expands the cli panel', async function (assert) { - await consoleComponent.toggle(); - await settled(); - await consoleComponent.runCommands('fullscreen'); - await settled(); - const consoleEle = document.querySelector('[data-test-component="console/ui-panel"]'); - // wait for the CSS transition to finish - await waitUntil(() => consoleEle.offsetHeight === window.innerHeight); - assert.strictEqual( - consoleEle.offsetHeight, - window.innerHeight, - 'fullscreen is the same height as the window' - ); - }); - - test('array output is correctly formatted', async function (assert) { - await consoleComponent.toggle(); - await settled(); - await consoleComponent.runCommands('read -field=policies /auth/token/lookup-self'); - await settled(); - const consoleOut = document.querySelector('.console-ui-output>pre'); - // wait for the CSS transition to finish - await waitUntil(() => consoleOut.innerText); - assert.notOk(consoleOut.innerText.includes('function(){')); - assert.strictEqual(consoleOut.innerText, '["root"]'); - }); - - test('number output is correctly formatted', async function (assert) { - await consoleComponent.toggle(); - await settled(); - await consoleComponent.runCommands('read -field=creation_time /auth/token/lookup-self'); - await settled(); - const consoleOut = document.querySelector('.console-ui-output>pre'); - // wait for the CSS transition to finish - await waitUntil(() => consoleOut.innerText); - assert.strictEqual(consoleOut.innerText.match(/^\d+$/).length, 1); - }); - - test('boolean output is correctly formatted', async function (assert) { - await consoleComponent.toggle(); - await settled(); - await consoleComponent.runCommands('read -field=orphan /auth/token/lookup-self'); - await settled(); - const consoleOut = document.querySelector('.console-ui-output>pre'); - // have to wrap in a later so that we can wait for the CSS transition to finish - await waitUntil(() => consoleOut.innerText); - assert.strictEqual(consoleOut.innerText.match(/^(true|false)$/g).length, 1); - }); - - test('it should open and close console panel', async function (assert) { - await click('[data-test-console-toggle]'); - assert.dom('[data-test-console-panel]').hasClass('panel-open', 'Sidebar button opens console panel'); - await click('[data-test-console-toggle]'); - assert - .dom('[data-test-console-panel]') - .doesNotHaveClass('panel-open', 'Sidebar button closes console panel'); - await click('[data-test-console-toggle]'); - await click('[data-test-console-panel-close]'); - assert - .dom('[data-test-console-panel]') - .doesNotHaveClass('panel-open', 'Console panel close button closes console panel'); - }); -}); diff --git a/ui/tests/acceptance/enterprise-control-groups-test.js b/ui/tests/acceptance/enterprise-control-groups-test.js deleted file mode 100644 index 233e93227..000000000 --- a/ui/tests/acceptance/enterprise-control-groups-test.js +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { settled, currentURL, currentRouteName, visit, waitUntil } from '@ember/test-helpers'; -import { module, test, skip } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; - -import { storageKey } from 'vault/services/control-group'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import authForm from 'vault/tests/pages/components/auth-form'; -import controlGroup from 'vault/tests/pages/components/control-group'; -import controlGroupSuccess from 'vault/tests/pages/components/control-group-success'; -import authPage from 'vault/tests/pages/auth'; -import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret'; -import listPage from 'vault/tests/pages/secrets/backend/list'; - -const consoleComponent = create(consoleClass); -const authFormComponent = create(authForm); -const controlGroupComponent = create(controlGroup); -const controlGroupSuccessComponent = create(controlGroupSuccess); - -module('Acceptance | Enterprise | control groups', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - const POLICY = ` - path "kv/foo" { - capabilities = ["create", "read", "update", "delete", "list"] - control_group = { - max_ttl = "24h" - factor "ops_manager" { - identity { - group_names = ["managers"] - approvals = 1 - } - } - } - } - - path "kv-v2-mount/data/foo" { - capabilities = ["create", "read", "update", "list"] - control_group = { - max_ttl = "24h" - factor "ops_manager" { - identity { - group_names = ["managers"] - approvals = 1 - } - } - } - } - - path "kv-v2-mount/*" { - capabilities = ["list"] - } - `; - - const AUTHORIZER_POLICY = ` - path "sys/control-group/authorize" { - capabilities = ["update"] - } - - path "sys/control-group/request" { - capabilities = ["update"] - } - `; - - const ADMIN_USER = 'authorizer'; - const ADMIN_PASSWORD = 'test'; - const setupControlGroup = async (context) => { - await visit('/vault/secrets'); - await consoleComponent.toggle(); - await settled(); - await consoleComponent.runCommands([ - //enable kv-v1 mount and write a secret - 'write sys/mounts/kv type=kv', - 'write kv/foo bar=baz', - - //enable userpass, create user and associated entity - 'write sys/auth/userpass type=userpass', - `write auth/userpass/users/${ADMIN_USER} password=${ADMIN_PASSWORD} policies=default`, - `write identity/entity name=${ADMIN_USER} policies=test`, - // write policies for control group + authorization - `write sys/policies/acl/kv-control-group policy=${btoa(POLICY)}`, - `write sys/policies/acl/authorizer policy=${btoa(AUTHORIZER_POLICY)}`, - // read out mount to get the accessor - 'read -field=accessor sys/internal/ui/mounts/auth/userpass', - ]); - await settled(); - const userpassAccessor = consoleComponent.lastTextOutput; - - await consoleComponent.runCommands([ - // lookup entity id for our authorizer - `write -field=id identity/lookup/entity name=${ADMIN_USER}`, - ]); - await settled(); - const authorizerEntityId = consoleComponent.lastTextOutput; - await consoleComponent.runCommands([ - // create alias for authorizor and add them to the managers group - `write identity/alias mount_accessor=${userpassAccessor} entity_id=${authorizerEntityId} name=${ADMIN_USER}`, - `write identity/group name=managers member_entity_ids=${authorizerEntityId} policies=authorizer`, - // create a token to request access to kv/foo - 'write -field=client_token auth/token/create policies=kv-control-group', - ]); - await settled(); - context.userToken = consoleComponent.lastLogOutput; - - await authPage.login(context.userToken); - await settled(); - return this; - }; - - const writeSecret = async function (backend, path, key, val) { - await listPage.visitRoot({ backend }); - await listPage.create(); - await editPage.createSecret(path, key, val); - }; - - test('for v2 secrets it redirects you if you try to navigate to a Control Group restricted path', async function (assert) { - await consoleComponent.runCommands([ - 'write sys/mounts/kv-v2-mount type=kv-v2', - 'delete kv-v2-mount/metadata/foo', - ]); - await writeSecret('kv-v2-mount', 'foo', 'bar', 'baz'); - await settled(); - await setupControlGroup(this); - await settled(); - await visit('/vault/secrets/kv-v2-mount/show/foo'); - - assert.ok( - await waitUntil(() => currentRouteName() === 'vault.cluster.access.control-group-accessor'), - 'redirects to access control group route' - ); - // without waiting for a settled state before test teardown there was an occasional async request leak causing failures - // the queryRecord method in the capabilities adapter was seemingly resolving after the store was destroyed - // "Error: Async Request leaks detected. Add a breakpoint here and set store.generateStackTracesForTrackedRequests = true; to inspect traces for leak origins" - // this should allow the pending request to resolve before tear down - await settled(); - }); - - const workflow = async (assert, context, shouldStoreToken) => { - const url = '/vault/secrets/kv/show/foo'; - await setupControlGroup(context); - await settled(); - // as the requestor, go to the URL that's blocked by the control group - // and store the values - await visit(url); - - const accessor = controlGroupComponent.accessor; - const controlGroupToken = controlGroupComponent.token; - await authPage.logout(); - await settled(); - // log in as the admin, navigate to the accessor page, - // and authorize the control group request - await visit('/vault/auth?with=userpass'); - - await authFormComponent.username(ADMIN_USER); - await settled(); - await authFormComponent.password(ADMIN_PASSWORD); - await settled(); - await authFormComponent.login(); - await settled(); - await visit(`/vault/access/control-groups/${accessor}`); - - // putting here to help with flaky test - assert.dom('[data-test-authorize-button]').exists(); - await controlGroupComponent.authorize(); - await settled(); - assert.strictEqual(controlGroupComponent.bannerPrefix, 'Thanks!', 'text display changes'); - await settled(); - await authPage.logout(); - await settled(); - await authPage.login(context.userToken); - await settled(); - if (shouldStoreToken) { - localStorage.setItem( - storageKey(accessor, 'kv/foo'), - JSON.stringify({ - accessor, - token: controlGroupToken, - creation_path: 'kv/foo', - uiParams: { - url, - }, - }) - ); - await visit(`/vault/access/control-groups/${accessor}`); - - assert.ok(controlGroupSuccessComponent.showsNavigateMessage, 'shows user the navigate message'); - await controlGroupSuccessComponent.navigate(); - await settled(); - assert.strictEqual(currentURL(), url, 'successfully loads the target url'); - } else { - await visit(`/vault/access/control-groups/${accessor}`); - - await controlGroupSuccessComponent.token(controlGroupToken); - await settled(); - await controlGroupSuccessComponent.unwrap(); - await settled(); - assert.ok(controlGroupSuccessComponent.showsJsonViewer, 'shows the json viewer'); - } - }; - - skip('it allows the full flow to work without a saved token', async function (assert) { - await workflow(assert, this); - await settled(); - }); - - skip('it allows the full flow to work with a saved token', async function (assert) { - await workflow(assert, this, true); - await settled(); - }); - - test('it displays the warning in the console when making a request to a Control Group path', async function (assert) { - await setupControlGroup(this); - await settled(); - await consoleComponent.toggle(); - await settled(); - await consoleComponent.runCommands('read kv/foo'); - await settled(); - const output = consoleComponent.lastLogOutput; - assert.ok(output.includes('A Control Group was encountered at kv/foo')); - assert.ok(output.includes('The Control Group Token is')); - assert.ok(output.includes('The Accessor is')); - assert.ok(output.includes('Visit /ui/vault/access/control-groups/')); - }); -}); diff --git a/ui/tests/acceptance/enterprise-kmip-test.js b/ui/tests/acceptance/enterprise-kmip-test.js deleted file mode 100644 index 161e4a0d1..000000000 --- a/ui/tests/acceptance/enterprise-kmip-test.js +++ /dev/null @@ -1,309 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentURL, currentRouteName, settled, fillIn, waitUntil, find } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; - -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import authPage from 'vault/tests/pages/auth'; -import scopesPage from 'vault/tests/pages/secrets/backend/kmip/scopes'; -import rolesPage from 'vault/tests/pages/secrets/backend/kmip/roles'; -import credentialsPage from 'vault/tests/pages/secrets/backend/kmip/credentials'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; - -const uiConsole = create(consoleClass); - -// port has a lower limit of 1024 -const getRandomPort = () => Math.floor(Math.random() * 5000 + 1024); - -const mount = async (shouldConfig = true) => { - const now = Date.now(); - const path = `kmip-${now}`; - const addr = `127.0.0.1:${getRandomPort()}`; // use random port - await settled(); - const commands = shouldConfig - ? [`write sys/mounts/${path} type=kmip`, `write ${path}/config listen_addrs=${addr}`] - : [`write sys/mounts/${path} type=kmip`]; - await uiConsole.runCommands(commands); - await settled(); - const res = uiConsole.lastLogOutput; - if (res.includes('Error')) { - throw new Error(`Error mounting secrets engine: ${res}`); - } - return path; -}; - -const createScope = async () => { - const path = await mount(); - await settled(); - const scope = `scope-${Date.now()}`; - await settled(); - await uiConsole.runCommands([`write ${path}/scope/${scope} -force`]); - await settled(); - const res = uiConsole.lastLogOutput; - if (res.includes('Error')) { - throw new Error(`Error creating scope: ${res}`); - } - return { path, scope }; -}; - -const createRole = async () => { - const { path, scope } = await createScope(); - await settled(); - const role = `role-${Date.now()}`; - await uiConsole.runCommands([`write ${path}/scope/${scope}/role/${role} operation_all=true`]); - await settled(); - const res = uiConsole.lastLogOutput; - if (res.includes('Error')) { - throw new Error(`Error creating role: ${res}`); - } - return { path, scope, role }; -}; - -const generateCreds = async () => { - const { path, scope, role } = await createRole(); - await settled(); - await uiConsole.runCommands([ - `write ${path}/scope/${scope}/role/${role}/credential/generate format=pem -field=serial_number`, - ]); - const serial = uiConsole.lastLogOutput; - if (serial.includes('Error')) { - throw new Error(`Credential generation failed with error: ${serial}`); - } - return { path, scope, role, serial }; -}; -module('Acceptance | Enterprise | KMIP secrets', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - await authPage.login(); - return; - }); - - test('it enables KMIP secrets engine', async function (assert) { - const path = `kmip-${Date.now()}`; - await mountSecrets.enable('kmip', path); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes`, - 'mounts and redirects to the kmip scopes page' - ); - assert.ok(scopesPage.isEmpty, 'renders empty state'); - }); - - test('it can configure a KMIP secrets engine', async function (assert) { - const path = await mount(false); - await scopesPage.visit({ backend: path }); - await settled(); - await scopesPage.configurationLink(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/configuration`, - 'configuration navigates to the config page' - ); - assert.ok(scopesPage.isEmpty, 'config page renders empty state'); - - await scopesPage.configureLink(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/configure`, - 'configuration navigates to the configure page' - ); - const addr = `127.0.0.1:${getRandomPort()}`; - await fillIn('[data-test-string-list-input="0"]', addr); - - await scopesPage.submit(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/configuration`, - 'redirects to configuration page after saving config' - ); - assert.notOk(scopesPage.isEmpty, 'configuration page no longer renders empty state'); - }); - - test('it can revoke from the credentials show page', async function (assert) { - const { path, scope, role, serial } = await generateCreds(); - await settled(); - await credentialsPage.visitDetail({ backend: path, scope, role, serial }); - await settled(); - await waitUntil(() => find('[data-test-confirm-action-trigger]')); - assert.dom('[data-test-confirm-action-trigger]').exists('delete button exists'); - await credentialsPage.delete().confirmDelete(); - await settled(); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes/${scope}/roles/${role}/credentials`, - 'redirects to the credentials list' - ); - assert.ok(credentialsPage.isEmpty, 'renders an empty credentials page'); - }); - - test('it can create a scope', async function (assert) { - const path = await mount(this); - await scopesPage.visit({ backend: path }); - await settled(); - await scopesPage.createLink(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes/create`, - 'navigates to the kmip scope create page' - ); - - // create scope - await scopesPage.scopeName('foo'); - await settled(); - await scopesPage.submit(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes`, - 'navigates to the kmip scopes page after create' - ); - assert.strictEqual(scopesPage.listItemLinks.length, 1, 'renders a single scope'); - }); - - test('it can delete a scope from the list', async function (assert) { - const { path } = await createScope(); - await scopesPage.visit({ backend: path }); - await settled(); - // delete the scope - await scopesPage.listItemLinks.objectAt(0).menuToggle(); - await settled(); - await scopesPage.delete(); - await settled(); - await scopesPage.confirmDelete(); - await settled(); - assert.strictEqual(scopesPage.listItemLinks.length, 0, 'no scopes'); - assert.ok(scopesPage.isEmpty, 'renders the empty state'); - }); - - test('it can create a role', async function (assert) { - // moving create scope here to help with flaky test - const path = await mount(); - await settled(); - const scope = `scope-for-can-create-role`; - await settled(); - await uiConsole.runCommands([`write ${path}/scope/${scope} -force`]); - await settled(); - const res = uiConsole.lastLogOutput; - if (res.includes('Error')) { - throw new Error(`Error creating scope: ${res}`); - } - const role = `role-new-role`; - await rolesPage.visit({ backend: path, scope }); - await settled(); - assert.ok(rolesPage.isEmpty, 'renders the empty role page'); - await rolesPage.create(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes/${scope}/roles/create`, - 'links to the role create form' - ); - - await rolesPage.roleName(role); - await settled(); - await rolesPage.submit(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes/${scope}/roles`, - 'redirects to roles list' - ); - - assert.strictEqual(rolesPage.listItemLinks.length, 1, 'renders a single role'); - }); - - test('it can delete a role from the list', async function (assert) { - const { path, scope } = await createRole(); - await rolesPage.visit({ backend: path, scope }); - await settled(); - // delete the role - await rolesPage.listItemLinks.objectAt(0).menuToggle(); - await settled(); - await rolesPage.delete(); - await settled(); - await rolesPage.confirmDelete(); - await settled(); - assert.strictEqual(rolesPage.listItemLinks.length, 0, 'renders no roles'); - assert.ok(rolesPage.isEmpty, 'renders empty'); - }); - - test('it can delete a role from the detail page', async function (assert) { - const { path, scope, role } = await createRole(); - await settled(); - await rolesPage.visitDetail({ backend: path, scope, role }); - await settled(); - await waitUntil(() => find('[data-test-kmip-link-edit-role]')); - await rolesPage.detailEditLink(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes/${scope}/roles/${role}/edit`, - 'navigates to role edit' - ); - await rolesPage.cancelLink(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes/${scope}/roles/${role}`, - 'cancel navigates to role show' - ); - await rolesPage.delete().confirmDelete(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes/${scope}/roles`, - 'redirects to the roles list' - ); - assert.ok(rolesPage.isEmpty, 'renders an empty roles page'); - }); - - test('it can create a credential', async function (assert) { - // TODO come back and figure out why issue here with test - const { path, scope, role } = await createRole(); - await credentialsPage.visit({ backend: path, scope, role }); - await settled(); - assert.ok(credentialsPage.isEmpty, 'renders empty creds page'); - await credentialsPage.generateCredentialsLink(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${path}/kmip/scopes/${scope}/roles/${role}/credentials/generate`, - 'navigates to generate credentials' - ); - await credentialsPage.submit(); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.kmip.credentials.show', - 'generate redirects to the show page' - ); - await credentialsPage.backToRoleLink(); - await settled(); - assert.strictEqual(credentialsPage.listItemLinks.length, 1, 'renders a single credential'); - }); - - test('it can revoke a credential from the list', async function (assert) { - const { path, scope, role } = await generateCreds(); - await credentialsPage.visit({ backend: path, scope, role }); - // revoke the credentials - await settled(); - await credentialsPage.listItemLinks.objectAt(0).menuToggle(); - await settled(); - await credentialsPage.delete().confirmDelete(); - await settled(); - assert.strictEqual(credentialsPage.listItemLinks.length, 0, 'renders no credentials'); - assert.ok(credentialsPage.isEmpty, 'renders empty'); - }); -}); diff --git a/ui/tests/acceptance/enterprise-kmse-test.js b/ui/tests/acceptance/enterprise-kmse-test.js deleted file mode 100644 index d7834ab59..000000000 --- a/ui/tests/acceptance/enterprise-kmse-test.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, fillIn } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import { setupMirage } from 'ember-cli-mirage/test-support'; - -module('Acceptance | Enterprise | keymgmt', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - await logout.visit(); - return authPage.login(); - }); - - test('it should add new key and distribute to provider', async function (assert) { - const path = `keymgmt-${Date.now()}`; - this.server.post(`/${path}/key/test-key`, () => ({})); - this.server.put(`/${path}/kms/test-keyvault/key/test-key`, () => ({})); - - await mountSecrets.enable('keymgmt', path); - await click('[data-test-secret-create]'); - await fillIn('[data-test-input="provider"]', 'azurekeyvault'); - await fillIn('[data-test-input="name"]', 'test-keyvault'); - await fillIn('[data-test-input="keyCollection"]', 'test-keycollection'); - await fillIn('[data-test-input="credentials.client_id"]', '123'); - await fillIn('[data-test-input="credentials.client_secret"]', '456'); - await fillIn('[data-test-input="credentials.tenant_id"]', '789'); - await click('[data-test-kms-provider-submit]'); - await click('[data-test-distribute-key]'); - await click('[data-test-component="search-select"] .ember-basic-dropdown-trigger'); - await fillIn('.ember-power-select-search-input', 'test-key'); - await click('.ember-power-select-option'); - await fillIn('[data-test-keymgmt-dist-keytype]', 'rsa-2048'); - await click('[data-test-operation="encrypt"]'); - await fillIn('[data-test-protection="hsm"]', 'hsm'); - - this.server.get(`/${path}/kms/test-keyvault/key`, () => ({ data: { keys: ['test-key'] } })); - await click('[data-test-secret-save]'); - await click('[data-test-kms-provider-tab="keys"] a'); - assert.dom('[data-test-secret-link="test-key"]').exists('Key is listed under keys tab of provider'); - }); -}); diff --git a/ui/tests/acceptance/enterprise-license-banner-test.js b/ui/tests/acceptance/enterprise-license-banner-test.js deleted file mode 100644 index 2dfcd8996..000000000 --- a/ui/tests/acceptance/enterprise-license-banner-test.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { visit } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import Pretender from 'pretender'; -import formatRFC3339 from 'date-fns/formatRFC3339'; -import { addDays, subDays } from 'date-fns'; -import timestamp from 'core/utils/timestamp'; - -const generateHealthResponse = (now, state) => { - let expiry; - switch (state) { - case 'expired': - expiry = subDays(now, 2); - break; - case 'expiring': - expiry = addDays(now, 10); - break; - default: - expiry = addDays(now, 33); - break; - } - return { - initialized: true, - sealed: false, - standby: false, - license: { - expiry_time: formatRFC3339(expiry), - state: 'stored', - }, - performance_standby: false, - replication_performance_mode: 'disabled', - replication_dr_mode: 'disabled', - server_time_utc: 1622562585, - version: '1.9.0+ent', - cluster_name: 'vault-cluster-e779cd7c', - cluster_id: '5f20f5ab-acea-0481-787e-71ec2ff5a60b', - last_wal: 121, - }; -}; - -module('Acceptance | Enterprise | License banner warnings', function (hooks) { - setupApplicationTest(hooks); - - hooks.before(function () { - sinon.stub(timestamp, 'now').callsFake(() => new Date('2018-04-03T14:15:30')); - }); - hooks.beforeEach(function () { - this.now = timestamp.now(); - }); - hooks.afterEach(function () { - this.server.shutdown(); - }); - hooks.after(function () { - timestamp.now.restore(); - }); - - test('it shows no license banner if license expires in > 30 days', async function (assert) { - const healthResp = generateHealthResponse(this.now); - this.server = new Pretender(function () { - this.get('/v1/sys/health', (response) => { - return [response, { 'Content-Type': 'application/json' }, JSON.stringify(healthResp)]; - }); - this.get('/v1/sys/internal/ui/feature-flags', this.passthrough); - this.get('/v1/sys/internal/ui/mounts', this.passthrough); - this.get('/v1/sys/seal-status', this.passthrough); - this.get('/v1/sys/license/features', this.passthrough); - }); - await visit('/vault/auth'); - assert.dom('[data-test-license-banner]').doesNotExist('license banner does not show'); - this.server.shutdown(); - }); - test('it shows license banner warning if license expires within 30 days', async function (assert) { - const healthResp = generateHealthResponse(this.now, 'expiring'); - this.server = new Pretender(function () { - this.get('/v1/sys/health', (response) => { - return [response, { 'Content-Type': 'application/json' }, JSON.stringify(healthResp)]; - }); - this.get('/v1/sys/internal/ui/feature-flags', this.passthrough); - this.get('/v1/sys/internal/ui/mounts', this.passthrough); - this.get('/v1/sys/seal-status', this.passthrough); - this.get('/v1/sys/license/features', this.passthrough); - }); - await visit('/vault/auth'); - assert.dom('[data-test-license-banner-warning]').exists('license warning shows'); - this.server.shutdown(); - }); - - test('it shows license banner alert if license has already expired', async function (assert) { - const healthResp = generateHealthResponse(this.now, 'expired'); - this.server = new Pretender(function () { - this.get('/v1/sys/health', (response) => { - return [response, { 'Content-Type': 'application/json' }, JSON.stringify(healthResp)]; - }); - this.get('/v1/sys/internal/ui/feature-flags', this.passthrough); - this.get('/v1/sys/internal/ui/mounts', this.passthrough); - this.get('/v1/sys/seal-status', this.passthrough); - this.get('/v1/sys/license/features', this.passthrough); - }); - await visit('/vault/auth'); - assert.dom('[data-test-license-banner-expired]').exists('expired license message shows'); - this.server.shutdown(); - }); -}); diff --git a/ui/tests/acceptance/enterprise-namespaces-test.js b/ui/tests/acceptance/enterprise-namespaces-test.js deleted file mode 100644 index eb37dda50..000000000 --- a/ui/tests/acceptance/enterprise-namespaces-test.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, settled, visit, fillIn, currentURL, waitFor } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; - -const shell = create(consoleClass); - -const createNS = async (name) => shell.runCommands(`write sys/namespaces/${name} -force`); - -module('Acceptance | Enterprise | namespaces', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it clears namespaces when you log out', async function (assert) { - const ns = 'foo'; - await createNS(ns); - await shell.runCommands(`write -field=client_token auth/token/create policies=default`); - const token = shell.lastLogOutput; - await logout.visit(); - await authPage.login(token); - await click('[data-test-namespace-toggle]'); - assert.dom('[data-test-current-namespace]').hasText('root', 'root renders as current namespace'); - assert.dom('[data-test-namespace-link]').doesNotExist('Additional namespace have been cleared'); - await logout.visit(); - }); - - test('it shows nested namespaces if you log in with a namspace starting with a /', async function (assert) { - assert.expect(5); - - await click('[data-test-namespace-toggle]'); - - const nses = ['beep', 'boop', 'bop']; - for (const [i, ns] of nses.entries()) { - await createNS(ns); - await settled(); - // the namespace path will include all of the namespaces up to this point - const targetNamespace = nses.slice(0, i + 1).join('/'); - const url = `/vault/secrets?namespace=${targetNamespace}`; - // this is usually triggered when creating a ns in the form -- trigger a reload of the namespaces manually - await click('[data-test-refresh-namespaces]'); - await waitFor(`[data-test-namespace-link="${targetNamespace}"]`); - // check that the single namespace "beep" or "boop" not "beep/boop" shows in the toggle display - assert - .dom(`[data-test-namespace-link="${targetNamespace}"]`) - .hasText(ns, `shows the namespace ${ns} in the toggle component`); - // because quint does not like page reloads, visiting url directing instead of clicking on namespace in toggle - await visit(url); - } - - await logout.visit(); - await settled(); - await authPage.visit({ namespace: '/beep/boop' }); - await settled(); - await authPage.tokenInput('root').submit(); - await settled(); - await click('[data-test-namespace-toggle]'); - await waitFor('[data-test-current-namespace]'); - assert.dom('[data-test-current-namespace]').hasText('/beep/boop/', 'current namespace begins with a /'); - assert - .dom('[data-test-namespace-link="beep/boop/bop"]') - .exists('renders the link to the nested namespace'); - }); - - test('it shows the regular namespace toolbar when not managed', async function (assert) { - // This test is the opposite of the test in managed-namespace-test - await logout.visit(); - assert.strictEqual(currentURL(), '/vault/auth?with=token', 'Does not redirect'); - assert.dom('[data-test-namespace-toolbar]').exists('Normal namespace toolbar exists'); - assert - .dom('[data-test-managed-namespace-toolbar]') - .doesNotExist('Managed namespace toolbar does not exist'); - assert.dom('input#namespace').hasAttribute('placeholder', '/ (Root)'); - await fillIn('input#namespace', '/foo'); - const encodedNamespace = encodeURIComponent('/foo'); - assert.strictEqual( - currentURL(), - `/vault/auth?namespace=${encodedNamespace}&with=token`, - 'Does not prepend root to namespace' - ); - }); -}); diff --git a/ui/tests/acceptance/enterprise-oidc-namespace-test.js b/ui/tests/acceptance/enterprise-oidc-namespace-test.js deleted file mode 100644 index b7b1c2d8e..000000000 --- a/ui/tests/acceptance/enterprise-oidc-namespace-test.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { visit, currentURL } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import parseURL from 'core/utils/parse-url'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import authPage from 'vault/tests/pages/auth'; - -const shell = create(consoleClass); - -const createNS = async (name) => { - await shell.runCommands(`write sys/namespaces/${name} -force`); -}; -const SELECTORS = { - authTab: (path) => `[data-test-auth-method="${path}"] a`, -}; - -module('Acceptance | Enterprise | oidc auth namespace test', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - this.namespace = 'test-ns'; - this.rootOidc = 'root-oidc'; - this.nsOidc = 'ns-oidc'; - - this.server.post(`/auth/:path/config`, () => {}); - - this.enableOidc = (path, role = '') => { - return shell.runCommands([ - `write sys/auth/${path} type=oidc`, - `write auth/${path}/config default_role="${role}" oidc_discovery_url="https://example.com"`, - // show method as tab - `write sys/auth/${path}/tune listing_visibility="unauth"`, - ]); - }; - - this.disableOidc = (path) => shell.runCommands([`delete /sys/auth/${path}`]); - }); - - test('oidc: request is made to auth_url when a namespace is inputted', async function (assert) { - assert.expect(5); - - this.server.post(`/auth/${this.rootOidc}/oidc/auth_url`, (schema, req) => { - const { redirect_uri } = JSON.parse(req.requestBody); - const { pathname, search } = parseURL(redirect_uri); - assert.strictEqual( - pathname + search, - `/ui/vault/auth/${this.rootOidc}/oidc/callback`, - 'request made to auth_url when the login page is visited' - ); - }); - this.server.post(`/auth/${this.nsOidc}/oidc/auth_url`, (schema, req) => { - const { redirect_uri } = JSON.parse(req.requestBody); - const { pathname, search } = parseURL(redirect_uri); - assert.strictEqual( - pathname + search, - `/ui/vault/auth/${this.nsOidc}/oidc/callback?namespace=${this.namespace}`, - 'request made to correct auth_url when namespace is filled in' - ); - }); - - await authPage.login(); - // enable oidc in root namespace, without default role - await this.enableOidc(this.rootOidc); - // create child namespace to enable oidc - await createNS(this.namespace); - // enable oidc in child namespace with default role - await authPage.loginNs(this.namespace); - await this.enableOidc(this.nsOidc, `${this.nsOidc}-role`); - await authPage.logout(); - - await visit('/vault/auth'); - assert.dom(SELECTORS.authTab(this.rootOidc)).exists('renders oidc method tab for root'); - await authPage.namespaceInput(this.namespace); - assert.strictEqual( - currentURL(), - `/vault/auth?namespace=${this.namespace}&with=${this.nsOidc}%2F`, - 'url updates with namespace value' - ); - assert.dom(SELECTORS.authTab(this.nsOidc)).exists('renders oidc method tab for child namespace'); - - // disable methods to cleanup test state for re-running - await authPage.login(); - await this.disableOidc(this.rootOidc); - await this.disableOidc(this.nsOidc); - await shell.runCommands([`delete /sys/auth/${this.namespace}`]); - await authPage.logout(); - }); -}); diff --git a/ui/tests/acceptance/enterprise-replication-test.js b/ui/tests/acceptance/enterprise-replication-test.js deleted file mode 100644 index 3115290ca..000000000 --- a/ui/tests/acceptance/enterprise-replication-test.js +++ /dev/null @@ -1,387 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { clickTrigger } from 'ember-power-select/test-support/helpers'; -import { click, fillIn, findAll, currentURL, find, visit, settled, waitUntil } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import authPage from 'vault/tests/pages/auth'; -import { pollCluster } from 'vault/tests/helpers/poll-cluster'; -import { create } from 'ember-cli-page-object'; -import flashMessage from 'vault/tests/pages/components/flash-message'; -import ss from 'vault/tests/pages/components/search-select'; - -const searchSelect = create(ss); -const flash = create(flashMessage); - -const disableReplication = async (type, assert) => { - // disable performance replication - await visit(`/vault/replication/${type}`); - - if (findAll('[data-test-replication-link="manage"]').length) { - await click('[data-test-replication-link="manage"]'); - - await click('[data-test-disable-replication] button'); - - const typeDisplay = type === 'dr' ? 'Disaster Recovery' : 'Performance'; - await fillIn('[data-test-confirmation-modal-input="Disable Replication?"]', typeDisplay); - await click('[data-test-confirm-button]'); - await settled(); // eslint-disable-line - - if (assert) { - // bypassing for now -- remove if tests pass reliably - // assert.strictEqual( - // flash.latestMessage, - // 'This cluster is having replication disabled. Vault will be unavailable for a brief period and will resume service shortly.', - // 'renders info flash when disabled' - // ); - assert.ok( - await waitUntil(() => currentURL() === '/vault/replication'), - 'redirects to the replication page' - ); - } - await settled(); - } -}; - -module('Acceptance | Enterprise | replication', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - await authPage.login(); - await settled(); - await disableReplication('dr'); - await settled(); - await disableReplication('performance'); - await settled(); - }); - - hooks.afterEach(async function () { - await disableReplication('dr'); - await settled(); - await disableReplication('performance'); - await settled(); - }); - - test('replication', async function (assert) { - assert.expect(17); - const secondaryName = 'firstSecondary'; - const mode = 'deny'; - - // confirm unable to visit dr secondary details page when both replications are disabled - await visit('/vault/replication-dr-promote/details'); - - assert.dom('[data-test-component="empty-state"]').exists(); - assert - .dom('[data-test-empty-state-title]') - .includesText('Disaster Recovery secondary not set up', 'shows the correct title of the empty state'); - - assert - .dom('[data-test-empty-state-message]') - .hasText( - 'This cluster has not been enabled as a Disaster Recovery Secondary. You can do so by enabling replication and adding a secondary from the Disaster Recovery Primary.', - 'renders default message specific to when no replication is enabled' - ); - - await visit('/vault/replication'); - - assert.strictEqual(currentURL(), '/vault/replication'); - - // enable perf replication - await click('[data-test-replication-type-select="performance"]'); - - await fillIn('[data-test-replication-cluster-mode-select]', 'primary'); - - await click('[data-test-replication-enable]'); - - await pollCluster(this.owner); - - // confirm that the details dashboard shows - assert.ok(await waitUntil(() => find('[data-test-replication-dashboard]')), 'details dashboard is shown'); - - // add a secondary with a mount filter config - await click('[data-test-replication-link="secondaries"]'); - - await click('[data-test-secondary-add]'); - - await fillIn('[data-test-replication-secondary-id]', secondaryName); - - await click('#deny'); - await clickTrigger(); - const mountPath = searchSelect.options.objectAt(0).text; - await searchSelect.options.objectAt(0).click(); - await click('[data-test-secondary-add]'); - - await pollCluster(this.owner); - // click into the added secondary's mount filter config - await click('[data-test-replication-link="secondaries"]'); - - await click('[data-test-popup-menu-trigger]'); - - await click('[data-test-replication-path-filter-link]'); - - assert.strictEqual( - currentURL(), - `/vault/replication/performance/secondaries/config/show/${secondaryName}` - ); - assert.dom('[data-test-mount-config-mode]').includesText(mode, 'show page renders the correct mode'); - assert - .dom('[data-test-mount-config-paths]') - .includesText(mountPath, 'show page renders the correct mount path'); - - // delete config by choosing "no filter" in the edit screen - await click('[data-test-replication-link="edit-mount-config"]'); - - await click('#no-filtering'); - - await click('[data-test-config-save]'); - await settled(); // eslint-disable-line - - assert.strictEqual( - flash.latestMessage, - `The performance mount filter config for the secondary ${secondaryName} was successfully deleted.`, - 'renders success flash upon deletion' - ); - assert.strictEqual( - currentURL(), - `/vault/replication/performance/secondaries`, - 'redirects to the secondaries page' - ); - // nav back to details page and confirm secondary is in the known secondaries table - await click('[data-test-replication-link="details"]'); - - assert - .dom(`[data-test-secondaries=row-for-${secondaryName}]`) - .exists('shows a table row the recently added secondary'); - - // nav to DR - await visit('/vault/replication/dr'); - - await fillIn('[data-test-replication-cluster-mode-select]', 'secondary'); - assert - .dom('[data-test-replication-enable]') - .isDisabled('dr secondary enable is disabled when other replication modes are on'); - - // disable performance replication - await disableReplication('performance', assert); - await settled(); - await pollCluster(this.owner); - - // enable dr replication - await visit('vault/replication/dr'); - - await fillIn('[data-test-replication-cluster-mode-select]', 'primary'); - await click('button[type="submit"]'); - - await pollCluster(this.owner); - await waitUntil(() => find('[data-test-empty-state-title]')); - // empty state inside of know secondaries table - assert - .dom('[data-test-empty-state-title]') - .includesText( - 'No known dr secondary clusters associated with this cluster', - 'shows the correct title of the empty state' - ); - - assert.ok( - find('[data-test-replication-title]').textContent.includes('Disaster Recovery'), - 'it displays the replication type correctly' - ); - assert.ok( - find('[data-test-replication-mode-display]').textContent.includes('primary'), - 'it displays the cluster mode correctly' - ); - - // add dr secondary - await click('[data-test-replication-link="secondaries"]'); - - await click('[data-test-secondary-add]'); - - await fillIn('[data-test-replication-secondary-id]', secondaryName); - - await click('[data-test-secondary-add]'); - - await pollCluster(this.owner); - await click('[data-test-replication-link="secondaries"]'); - - assert - .dom('[data-test-secondary-name]') - .includesText(secondaryName, 'it displays the secondary in the list of known secondaries'); - }); - - test('disabling dr primary when perf replication is enabled', async function (assert) { - await visit('vault/replication/performance'); - - // enable perf replication - await fillIn('[data-test-replication-cluster-mode-select]', 'primary'); - await click('[data-test-replication-enable]'); - - await pollCluster(this.owner); - - // enable dr replication - await visit('/vault/replication/dr'); - - await fillIn('[data-test-replication-cluster-mode-select]', 'primary'); - - await click('[data-test-replication-enable]'); - - await pollCluster(this.owner); - await visit('/vault/replication/dr/manage'); - - await click('[data-test-demote-replication] [data-test-replication-action-trigger]'); - - assert.ok(findAll('[data-test-demote-warning]').length, 'displays the demotion warning'); - }); - - test('navigating to dr secondary details page when dr secondary is not enabled', async function (assert) { - // enable dr replication - - await visit('/vault/replication/dr'); - - await fillIn('[data-test-replication-cluster-mode-select]', 'primary'); - await click('[data-test-replication-enable]'); - await settled(); // eslint-disable-line - await pollCluster(this.owner); - await visit('/vault/replication-dr-promote/details'); - - assert.dom('[data-test-component="empty-state"]').exists(); - assert - .dom('[data-test-empty-state-message]') - .hasText( - 'This Disaster Recovery secondary has not been enabled. You can do so from the Disaster Recovery Primary.', - 'renders message when replication is enabled' - ); - }); - - test('add secondary and navigate through token generation modal', async function (assert) { - const secondaryNameFirst = 'firstSecondary'; - const secondaryNameSecond = 'secondSecondary'; - await visit('/vault/replication'); - - // enable perf replication - await click('[data-test-replication-type-select="performance"]'); - - await fillIn('[data-test-replication-cluster-mode-select]', 'primary'); - await click('[data-test-replication-enable]'); - - await pollCluster(this.owner); - await settled(); - - // add a secondary with default TTL - await click('[data-test-replication-link="secondaries"]'); - - await click('[data-test-secondary-add]'); - - await fillIn('[data-test-replication-secondary-id]', secondaryNameFirst); - await click('[data-test-secondary-add]'); - - await pollCluster(this.owner); - await settled(); - const modalDefaultTtl = document.querySelector('[data-test-row-value="TTL"]').innerText; - // checks on secondary token modal - assert.dom('#modal-wormhole').exists(); - assert.strictEqual(modalDefaultTtl, '1800s', 'shows the correct TTL of 1800s'); - // click off the modal to make sure you don't just have to click on the copy-close button to copy the token - await click('[data-test-modal-background="Copy your token"]'); - - // add another secondary not using the default ttl - await click('[data-test-secondary-add]'); - - await fillIn('[data-test-replication-secondary-id]', secondaryNameSecond); - await click('[data-test-toggle-input]'); - - await fillIn('[data-test-ttl-value]', 3); - await click('[data-test-secondary-add]'); - - await pollCluster(this.owner); - await settled(); - const modalTtl = document.querySelector('[data-test-row-value="TTL"]').innerText; - assert.strictEqual(modalTtl, '180s', 'shows the correct TTL of 180s'); - await click('[data-test-modal-background="Copy your token"]'); - - // confirm you were redirected to the secondaries page - assert.strictEqual( - currentURL(), - `/vault/replication/performance/secondaries`, - 'redirects to the secondaries page' - ); - assert - .dom('[data-test-secondary-name]') - .includesText(secondaryNameFirst, 'it displays the secondary in the list of secondaries'); - }); - - test('render performance and dr primary and navigate to details page', async function (assert) { - // enable perf primary replication - await visit('/vault/replication'); - await click('[data-test-replication-type-select="performance"]'); - - await fillIn('[data-test-replication-cluster-mode-select]', 'primary'); - await click('[data-test-replication-enable]'); - - await pollCluster(this.owner); - await settled(); - - await visit('/vault/replication'); - - assert - .dom(`[data-test-replication-summary-card]`) - .doesNotExist(`does not render replication summary card when both modes are not enabled as primary`); - - // enable DR primary replication - await click('[data-test-replication-promote-secondary]'); - await click('[data-test-replication-enable]'); - - await pollCluster(this.owner); - await settled(); - - // navigate using breadcrumbs back to replication.index - await click('[data-test-replication-breadcrumb]'); - - assert - .dom('[data-test-replication-summary-card]') - .exists({ count: 2 }, 'renders two replication-summary-card components'); - - // navigate to details page using the "Details" link - await click('[data-test-manage-link="Disaster Recovery"]'); - - assert - .dom('[data-test-selectable-card-container="primary"]') - .exists('shows the correct card on the details dashboard'); - assert.strictEqual(currentURL(), '/vault/replication/dr'); - }); - - test('render performance secondary and navigate to the details page', async function (assert) { - // enable perf replication - await visit('/vault/replication'); - - await click('[data-test-replication-type-select="performance"]'); - - await fillIn('[data-test-replication-cluster-mode-select]', 'primary'); - await click('[data-test-replication-enable]'); - - await pollCluster(this.owner); - await settled(); - - // demote perf primary to a secondary - await click('[data-test-replication-link="manage"]'); - - // open demote modal - await click('[data-test-demote-replication] [data-test-replication-action-trigger]'); - - // enter confirmation text - await fillIn('[data-test-confirmation-modal-input="Demote to secondary?"]', 'Performance'); - // Click confirm button - await click('[data-test-confirm-button="Demote to secondary?"]'); - - await click('[data-test-replication-link="details"]'); - - assert.dom('[data-test-replication-dashboard]').exists(); - assert.dom('[data-test-selectable-card-container="secondary"]').exists(); - assert.ok( - find('[data-test-replication-mode-display]').textContent.includes('secondary'), - 'it displays the cluster mode correctly' - ); - }); -}); diff --git a/ui/tests/acceptance/enterprise-replication-unsupported-test.js b/ui/tests/acceptance/enterprise-replication-unsupported-test.js deleted file mode 100644 index cfcb2a73c..000000000 --- a/ui/tests/acceptance/enterprise-replication-unsupported-test.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import authPage from 'vault/tests/pages/auth'; -import { visit } from '@ember/test-helpers'; - -module('Acceptance | Enterprise | replication unsupported', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - this.server.get('/sys/replication/status', function () { - return { - data: { - mode: 'unsupported', - }, - }; - }); - return authPage.login(); - }); - - test('replication page when unsupported', async function (assert) { - await visit('/vault/replication'); - assert - .dom('[data-test-replication-title]') - .hasText('Replication unsupported', 'it shows the unsupported view'); - }); -}); diff --git a/ui/tests/acceptance/enterprise-sidebar-nav-test.js b/ui/tests/acceptance/enterprise-sidebar-nav-test.js deleted file mode 100644 index 58e3db9cd..000000000 --- a/ui/tests/acceptance/enterprise-sidebar-nav-test.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, currentURL, fillIn } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import authPage from 'vault/tests/pages/auth'; - -const link = (label) => `[data-test-sidebar-nav-link="${label}"]`; -const panel = (label) => `[data-test-sidebar-nav-panel="${label}"]`; - -module('Acceptance | Enterprise | sidebar navigation', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - // common links are tested in the sidebar-nav test and will not be covered here - test('it should render enterprise only navigation links', async function (assert) { - assert.dom(panel('Cluster')).exists('Cluster nav panel renders'); - - await click(link('Replication')); - assert.strictEqual(currentURL(), '/vault/replication', 'Replication route renders'); - await click('[data-test-replication-enable]'); - - await click(link('Performance')); - assert.strictEqual( - currentURL(), - '/vault/replication/performance', - 'Replication performance route renders' - ); - - await click(link('Disaster Recovery')); - assert.strictEqual(currentURL(), '/vault/replication/dr', 'Replication dr route renders'); - // disable replication now that we have checked the links - await click('[data-test-replication-link="manage"]'); - await click('[data-test-replication-action-trigger]'); - await fillIn('[data-test-confirmation-modal-input="Disable Replication?"]', 'Disaster Recovery'); - await click('[data-test-confirm-button="Disable Replication?"]'); - - await click(link('Client count')); - assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'Client counts route renders'); - - await click(link('License')); - assert.strictEqual(currentURL(), '/vault/license', 'License route renders'); - - await click(link('Access')); - await click(link('Control Groups')); - assert.strictEqual(currentURL(), '/vault/access/control-groups', 'Control groups route renders'); - - await click(link('Namespaces')); - assert.strictEqual(currentURL(), '/vault/access/namespaces?page=1', 'Replication route renders'); - - await click(link('Back to main navigation')); - await click(link('Policies')); - await click(link('Role-Governing Policies')); - assert.strictEqual(currentURL(), '/vault/policies/rgp', 'Role-Governing Policies route renders'); - - await click(link('Endpoint Governing Policies')); - assert.strictEqual(currentURL(), '/vault/policies/egp', 'Endpoint Governing Policies route renders'); - }); -}); diff --git a/ui/tests/acceptance/enterprise-transform-test.js b/ui/tests/acceptance/enterprise-transform-test.js deleted file mode 100644 index 32ac442a7..000000000 --- a/ui/tests/acceptance/enterprise-transform-test.js +++ /dev/null @@ -1,300 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { currentURL, click, settled } from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import { typeInSearch, selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers'; - -import authPage from 'vault/tests/pages/auth'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import transformationsPage from 'vault/tests/pages/secrets/backend/transform/transformations'; -import rolesPage from 'vault/tests/pages/secrets/backend/transform/roles'; -import templatesPage from 'vault/tests/pages/secrets/backend/transform/templates'; -import alphabetsPage from 'vault/tests/pages/secrets/backend/transform/alphabets'; -import searchSelect from 'vault/tests/pages/components/search-select'; - -const searchSelectComponent = create(searchSelect); - -const mount = async () => { - const path = `transform-${Date.now()}`; - await mountSecrets.enable('transform', path); - await settled(); - return path; -}; - -const newTransformation = async (backend, name, submit = false) => { - const transformationName = name || 'foo'; - await transformationsPage.visitCreate({ backend }); - await settled(); - await transformationsPage.name(transformationName); - await settled(); - await clickTrigger('#template'); - await selectChoose('#template', '.ember-power-select-option', 0); - await settled(); - // Don't automatically choose role because we might be testing that - if (submit) { - await transformationsPage.submit(); - await settled(); - } - return transformationName; -}; - -const newRole = async (backend, name) => { - const roleName = name || 'bar'; - await rolesPage.visitCreate({ backend }); - await settled(); - await rolesPage.name(roleName); - await settled(); - await clickTrigger('#transformations'); - await settled(); - await selectChoose('#transformations', '.ember-power-select-option', 0); - await settled(); - await rolesPage.submit(); - await settled(); - return roleName; -}; - -module('Acceptance | Enterprise | Transform secrets', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it enables Transform secrets engine and shows tabs', async function (assert) { - const backend = `transform-${Date.now()}`; - await mountSecrets.enable('transform', backend); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/list`, - 'mounts and redirects to the transformations list page' - ); - assert.ok(transformationsPage.isEmpty, 'renders empty state'); - assert - .dom('.active[data-test-secret-list-tab="Transformations"]') - .exists('Has Transformations tab which is active'); - assert.dom('[data-test-secret-list-tab="Roles"]').exists('Has Roles tab'); - assert.dom('[data-test-secret-list-tab="Templates"]').exists('Has Templates tab'); - assert.dom('[data-test-secret-list-tab="Alphabets"]').exists('Has Alphabets tab'); - }); - - test('it can create a transformation and add itself to the role attached', async function (assert) { - const backend = await mount(); - const transformationName = 'foo'; - const roleName = 'foo-role'; - await settled(); - await transformationsPage.createLink({ backend }); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/create`, - 'redirects to create transformation page' - ); - await transformationsPage.name(transformationName); - await settled(); - assert.dom('[data-test-input="type"]').hasValue('fpe', 'Has type FPE by default'); - assert.dom('[data-test-input="tweak_source"]').exists('Shows tweak source when FPE'); - await transformationsPage.type('masking'); - await settled(); - assert - .dom('[data-test-input="masking_character"]') - .exists('Shows masking character input when changed to masking type'); - assert.dom('[data-test-input="tweak_source"]').doesNotExist('Does not show tweak source when masking'); - await clickTrigger('#template'); - await settled(); - assert.strictEqual(searchSelectComponent.options.length, 2, 'list shows two builtin options by default'); - await selectChoose('#template', '.ember-power-select-option', 0); - await settled(); - - await clickTrigger('#allowed_roles'); - await settled(); - await typeInSearch(roleName); - await settled(); - await selectChoose('#allowed_roles', '.ember-power-select-option', 0); - await settled(); - await transformationsPage.submit(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/show/${transformationName}`, - 'redirects to show transformation page after submit' - ); - await click(`[data-test-secret-breadcrumb="${backend}"]`); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/list`, - 'Links back to list view from breadcrumb' - ); - }); - - test('it can create a role and add itself to the transformation attached', async function (assert) { - const roleName = 'my-role'; - const backend = await mount(); - // create transformation without role - await newTransformation(backend, 'a-transformation', true); - await click(`[data-test-secret-breadcrumb="${backend}"]`); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/list`, - 'Links back to list view from breadcrumb' - ); - await click('[data-test-secret-list-tab="Roles"]'); - assert.strictEqual(currentURL(), `/vault/secrets/${backend}/list?tab=role`, 'links to role list page'); - // create role with transformation attached - await rolesPage.createLink(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/create?itemType=role`, - 'redirects to create role page' - ); - await rolesPage.name(roleName); - await clickTrigger('#transformations'); - assert.strictEqual(searchSelectComponent.options.length, 1, 'lists the transformation'); - await selectChoose('#transformations', '.ember-power-select-option', 0); - await rolesPage.submit(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/show/role/${roleName}`, - 'redirects to show role page after submit' - ); - await click(`[data-test-secret-breadcrumb="${backend}"]`); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/list?tab=role`, - 'Links back to role list view from breadcrumb' - ); - }); - - test('it adds a role to a transformation when added to a role', async function (assert) { - const roleName = 'role-test'; - const backend = await mount(); - const transformation = await newTransformation(backend, 'b-transformation', true); - await newRole(backend, roleName); - await transformationsPage.visitShow({ backend, id: transformation }); - await settled(); - assert.dom('[data-test-row-value="Allowed roles"]').hasText(roleName); - }); - - test('it shows a message if an update fails after save', async function (assert) { - const roleName = 'role-remove'; - const backend = await mount(); - // Create transformation - const transformation = await newTransformation(backend, 'c-transformation', true); - // create role - await newRole(backend, roleName); - await settled(); - await transformationsPage.visitShow({ backend, id: transformation }); - assert.dom('[data-test-row-value="Allowed roles"]').hasText(roleName); - // Edit transformation - await click('[data-test-edit-link]'); - assert.dom('.modal.is-active').exists('Confirmation modal appears'); - await rolesPage.modalConfirm(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/edit/${transformation}`, - 'Correctly links to edit page for secret' - ); - // remove role - await settled(); - await click('#allowed_roles [data-test-selected-list-button="delete"]'); - - await transformationsPage.save(); - await settled(); - assert.dom('.flash-message.is-info').exists('Shows info message since role could not be updated'); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/show/${transformation}`, - 'Correctly links to show page for secret' - ); - assert - .dom('[data-test-row-value="Allowed roles"]') - .doesNotExist('Allowed roles are no longer on the transformation'); - }); - - test('it allows creation and edit of a template', async function (assert) { - const templateName = 'my-template'; - const backend = await mount(); - await click('[data-test-secret-list-tab="Templates"]'); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/list?tab=template`, - 'links to template list page' - ); - await settled(); - await templatesPage.createLink(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/create?itemType=template`, - 'redirects to create template page' - ); - await templatesPage.name(templateName); - await templatesPage.pattern(`(\\d{4})`); - await clickTrigger('#alphabet'); - await settled(); - assert.ok(searchSelectComponent.options.length > 0, 'lists built-in alphabets'); - await selectChoose('#alphabet', '.ember-power-select-option', 0); - assert.dom('#alphabet .ember-power-select-trigger').doesNotExist('Alphabet input no longer searchable'); - await templatesPage.submit(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/show/template/${templateName}`, - 'redirects to show template page after submit' - ); - await templatesPage.editLink(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/edit/template/${templateName}`, - 'Links to template edit page' - ); - await settled(); - assert.dom('[data-test-input="name"]').hasAttribute('readonly'); - }); - - test('it allows creation and edit of an alphabet', async function (assert) { - const alphabetName = 'vowels-only'; - const backend = await mount(); - await click('[data-test-secret-list-tab="Alphabets"]'); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/list?tab=alphabet`, - 'links to alphabet list page' - ); - await alphabetsPage.createLink(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/create?itemType=alphabet`, - 'redirects to create alphabet page' - ); - await alphabetsPage.name(alphabetName); - await alphabetsPage.alphabet('aeiou'); - await alphabetsPage.submit(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/show/alphabet/${alphabetName}`, - 'redirects to show alphabet page after submit' - ); - assert.dom('[data-test-row-value="Name"]').hasText(alphabetName); - assert.dom('[data-test-row-value="Alphabet"]').hasText('aeiou'); - await alphabetsPage.editLink(); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/edit/alphabet/${alphabetName}`, - 'Links to alphabet edit page' - ); - assert.dom('[data-test-input="name"]').hasAttribute('readonly'); - }); -}); diff --git a/ui/tests/acceptance/init-test.js b/ui/tests/acceptance/init-test.js deleted file mode 100644 index 9619e7865..000000000 --- a/ui/tests/acceptance/init-test.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; - -import initPage from 'vault/tests/pages/init'; -import Pretender from 'pretender'; - -const HEALTH_RESPONSE = { - initialized: false, - sealed: true, - standby: true, - performance_standby: false, - replication_performance_mode: 'unknown', - replication_dr_mode: 'unknown', - server_time_utc: 1538066726, - version: '1.13.0-dev1', -}; - -const CLOUD_SEAL_RESPONSE = { - keys: [], - keys_base64: [], - recovery_keys: [ - '1659986a8d56b998b175b6e259998f3c064c061d256c2a331681b8d122fedf0db4', - '4d34c58f56e4f077e3b74f9e8db2850fc251ac3f16e952441301eedc462addeb84', - '3b3cbdf4b2f5ac1e809ff1bb72fd9778e460856561728a871a9370345bd52e97f4', - 'aa99b46e2ed5d837ee9824b7894b24987be2f32c81ab9ff5ce9e07d2012eaf4158', - 'c2bf6d71d8db8ae09b26177ed393ecb274740fe9ab51884eaa00ac113a74c08ba7', - ], - recovery_keys_base64: [ - 'FlmYao1WuZixdbbiWZmPPAZMBh0lbCozFoG40SL+3w20', - 'TTTFj1bk8Hfjt0+ejbKFD8JRrD8W6VJEEwHu3EYq3euE', - 'Ozy99LL1rB6An/G7cv2XeORghWVhcoqHGpNwNFvVLpf0', - 'qpm0bi7V2DfumCS3iUskmHvi8yyBq5/1zp4H0gEur0FY', - 'wr9tcdjbiuCbJhd+05PssnR0D+mrUYhOqgCsETp0wIun', - ], - root_token: '48dF3Drr1jl4ayM0jcHrN4NC', -}; -const SEAL_RESPONSE = { - keys: [ - '1659986a8d56b998b175b6e259998f3c064c061d256c2a331681b8d122fedf0db4', - '4d34c58f56e4f077e3b74f9e8db2850fc251ac3f16e952441301eedc462addeb84', - '3b3cbdf4b2f5ac1e809ff1bb72fd9778e460856561728a871a9370345bd52e97f4', - ], - keys_base64: [ - 'FlmYao1WuZixdbbiWZmPPAZMBh0lbCozFoG40SL+3w20', - 'TTTFj1bk8Hfjt0+ejbKFD8JRrD8W6VJEEwHu3EYq3euE', - 'Ozy99LL1rB6An/G7cv2XeORghWVhcoqHGpNwNFvVLpf0', - ], - root_token: '48dF3Drr1jl4ayM0jcHrN4NC', -}; - -const CLOUD_SEAL_STATUS_RESPONSE = { - type: 'awskms', - sealed: true, - initialized: false, -}; -const SEAL_STATUS_RESPONSE = { - type: 'shamir', - sealed: true, - initialized: false, -}; - -const assertRequest = (req, assert, isCloud) => { - const json = JSON.parse(req.requestBody); - for (const key of ['recovery_shares', 'recovery_threshold']) { - assert[isCloud ? 'ok' : 'notOk']( - json[key], - `requestBody ${isCloud ? 'includes' : 'does not include'} cloud seal specific attribute: ${key}` - ); - } - for (const key of ['secret_shares', 'secret_threshold']) { - assert[isCloud ? 'notOk' : 'ok']( - json[key], - `requestBody ${isCloud ? 'does not include' : 'includes'} shamir specific attribute: ${key}` - ); - } -}; - -module('Acceptance | init', function (hooks) { - setupApplicationTest(hooks); - - const setInitResponse = (server, resp) => { - server.put('/v1/sys/init', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify(resp)]; - }); - }; - const setStatusResponse = (server, resp) => { - server.get('/v1/sys/seal-status', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify(resp)]; - }); - }; - hooks.beforeEach(function () { - this.server = new Pretender(); - this.server.get('/v1/sys/health', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify(HEALTH_RESPONSE)]; - }); - this.server.get('/v1/sys/internal/ui/feature-flags', this.server.passthrough); - }); - - hooks.afterEach(function () { - this.server.shutdown(); - }); - - test('cloud seal init', async function (assert) { - assert.expect(6); - - setInitResponse(this.server, CLOUD_SEAL_RESPONSE); - setStatusResponse(this.server, CLOUD_SEAL_STATUS_RESPONSE); - - await initPage.init(5, 3); - - assert.strictEqual( - initPage.keys.length, - CLOUD_SEAL_RESPONSE.recovery_keys.length, - 'shows all of the recovery keys' - ); - assert.strictEqual(initPage.buttonText, 'Continue to Authenticate', 'links to authenticate'); - assertRequest(this.server.handledRequests.findBy('url', '/v1/sys/init'), assert, true); - }); - - test('shamir seal init', async function (assert) { - assert.expect(6); - - setInitResponse(this.server, SEAL_RESPONSE); - setStatusResponse(this.server, SEAL_STATUS_RESPONSE); - - await initPage.init(3, 2); - - assert.strictEqual(initPage.keys.length, SEAL_RESPONSE.keys.length, 'shows all of the recovery keys'); - assert.strictEqual(initPage.buttonText, 'Continue to Unseal', 'links to unseal'); - assertRequest(this.server.handledRequests.findBy('url', '/v1/sys/init'), assert, false); - }); -}); diff --git a/ui/tests/acceptance/jwt-auth-method-test.js b/ui/tests/acceptance/jwt-auth-method-test.js deleted file mode 100644 index 805b0a347..000000000 --- a/ui/tests/acceptance/jwt-auth-method-test.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, visit, fillIn } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import sinon from 'sinon'; -import { Response } from 'miragejs'; -import { ERROR_JWT_LOGIN } from 'vault/components/auth-jwt'; - -module('Acceptance | jwt auth method', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - localStorage.clear(); // ensure that a token isn't stored otherwise visit('/vault/auth') will redirect to secrets - this.stub = sinon.stub(); - this.server.post( - '/auth/:path/oidc/auth_url', - () => - new Response( - 400, - { 'Content-Type': 'application/json' }, - JSON.stringify({ errors: [ERROR_JWT_LOGIN] }) - ) - ); - this.server.get('/auth/foo/oidc/callback', () => ({ - auth: { client_token: 'root' }, - })); - }); - - test('it works correctly with default name and no role', async function (assert) { - assert.expect(6); - this.server.post('/auth/jwt/login', (schema, req) => { - const { jwt, role } = JSON.parse(req.requestBody); - assert.ok(true, 'request made to auth/jwt/login after submit'); - assert.strictEqual(jwt, 'my-test-jwt-token', 'JWT token is sent in body'); - assert.strictEqual(role, undefined, 'role is not sent in body when not filled in'); - req.passthrough(); - }); - await visit('/vault/auth'); - await fillIn('[data-test-select="auth-method"]', 'jwt'); - assert.dom('[data-test-role]').exists({ count: 1 }, 'Role input exists'); - assert.dom('[data-test-jwt]').exists({ count: 1 }, 'JWT input exists'); - await fillIn('[data-test-jwt]', 'my-test-jwt-token'); - await click('[data-test-auth-submit]'); - assert.dom('[data-test-error]').exists('Failed login'); - }); - - test('it works correctly with default name and a role', async function (assert) { - assert.expect(7); - this.server.post('/auth/jwt/login', (schema, req) => { - const { jwt, role } = JSON.parse(req.requestBody); - assert.ok(true, 'request made to auth/jwt/login after login'); - assert.strictEqual(jwt, 'my-test-jwt-token', 'JWT token is sent in body'); - assert.strictEqual(role, 'some-role', 'role is sent in the body when filled in'); - req.passthrough(); - }); - await visit('/vault/auth'); - await fillIn('[data-test-select="auth-method"]', 'jwt'); - assert.dom('[data-test-role]').exists({ count: 1 }, 'Role input exists'); - assert.dom('[data-test-jwt]').exists({ count: 1 }, 'JWT input exists'); - await fillIn('[data-test-role]', 'some-role'); - await fillIn('[data-test-jwt]', 'my-test-jwt-token'); - assert.dom('[data-test-jwt]').exists({ count: 1 }, 'JWT input exists'); - await click('[data-test-auth-submit]'); - assert.dom('[data-test-error]').exists('Failed login'); - }); - - test('it works correctly with custom endpoint and a role', async function (assert) { - assert.expect(6); - this.server.get('/sys/internal/ui/mounts', () => ({ - data: { - auth: { - 'test-jwt/': { description: '', options: {}, type: 'jwt' }, - }, - }, - })); - this.server.post('/auth/test-jwt/login', (schema, req) => { - const { jwt, role } = JSON.parse(req.requestBody); - assert.ok(true, 'request made to auth/custom-jwt-login after login'); - assert.strictEqual(jwt, 'my-test-jwt-token', 'JWT token is sent in body'); - assert.strictEqual(role, 'some-role', 'role is sent in body when filled in'); - req.passthrough(); - }); - await visit('/vault/auth'); - await click('[data-test-auth-method-link="jwt"]'); - assert.dom('[data-test-role]').exists({ count: 1 }, 'Role input exists'); - assert.dom('[data-test-jwt]').exists({ count: 1 }, 'JWT input exists'); - await fillIn('[data-test-role]', 'some-role'); - await fillIn('[data-test-jwt]', 'my-test-jwt-token'); - await click('[data-test-auth-submit]'); - assert.dom('[data-test-error]').exists('Failed login'); - }); -}); diff --git a/ui/tests/acceptance/leases-test.js b/ui/tests/acceptance/leases-test.js deleted file mode 100644 index 611f6d827..000000000 --- a/ui/tests/acceptance/leases-test.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, currentRouteName, visit } from '@ember/test-helpers'; -// TESTS HERE ARE SKIPPED -// running vault with -dev-leased-kv flag lets you run some of these tests -// but generating leases programmatically is currently difficult -// -// TODO revisit this when it's easier to create leases - -import { module, skip } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; -import secretList from 'vault/tests/pages/secrets/backend/list'; -import secretEdit from 'vault/tests/pages/secrets/backend/kv/edit-secret'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; - -module('Acceptance | leases', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - await authPage.login(); - this.enginePath = `kv-for-lease-${uuidv4()}`; - // need a version 1 mount for leased secrets here - return mountSecrets.visit().path(this.enginePath).type('kv').version(1).submit(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - const createSecret = async (context, isRenewable) => { - context.name = `secret-${uuidv4()}`; - await secretList.visitRoot({ backend: context.enginePath }); - await secretList.create(); - if (isRenewable) { - await secretEdit.createSecret(context.name, 'ttl', '30h'); - } else { - await secretEdit.createSecret(context.name, 'foo', 'bar'); - } - }; - - const navToDetail = async (context) => { - await visit('/vault/access/leases/'); - await click(`[data-test-lease-link="${context.enginePath}/"]`); - await click(`[data-test-lease-link="${context.enginePath}/${context.name}/"]`); - await click(`[data-test-lease-link]:eq(0)`); - }; - - skip('it renders the show page', function (assert) { - createSecret(this); - navToDetail(this); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.leases.show', - 'a lease for the secret is in the list' - ); - assert - .dom('[data-test-lease-renew-picker]') - .doesNotExist('non-renewable lease does not render a renew picker'); - }); - - // skip for now until we find an easy way to generate a renewable lease - skip('it renders the show page with a picker', function (assert) { - createSecret(this, true); - navToDetail(this); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.leases.show', - 'a lease for the secret is in the list' - ); - assert - .dom('[data-test-lease-renew-picker]') - .exists({ count: 1 }, 'renewable lease renders a renew picker'); - }); - - skip('it removes leases upon revocation', async function (assert) { - createSecret(this); - navToDetail(this); - await click('[data-test-lease-revoke] button'); - await click('[data-test-confirm-button]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.leases.list-root', - 'it navigates back to the leases root on revocation' - ); - await click(`[data-test-lease-link="${this.enginePath}/"]`); - await click('[data-test-lease-link="data/"]'); - assert - .dom(`[data-test-lease-link="${this.enginePath}/data/${this.name}/"]`) - .doesNotExist('link to the lease was removed with revocation'); - }); - - skip('it removes branches when a prefix is revoked', async function (assert) { - createSecret(this); - await visit(`/vault/access/leases/list/${this.enginePath}`); - await click('[data-test-lease-revoke-prefix] button'); - await click('[data-test-confirm-button]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.leases.list-root', - 'it navigates back to the leases root on revocation' - ); - assert - .dom(`[data-test-lease-link="${this.enginePath}/"]`) - .doesNotExist('link to the prefix was removed with revocation'); - }); - - skip('lease not found', async function (assert) { - await visit('/vault/access/leases/show/not-found'); - assert - .dom('[data-test-lease-error]') - .hasText('not-found is not a valid lease ID', 'it shows an error when the lease is not found'); - }); -}); diff --git a/ui/tests/acceptance/managed-namespace-test.js b/ui/tests/acceptance/managed-namespace-test.js deleted file mode 100644 index 92a363402..000000000 --- a/ui/tests/acceptance/managed-namespace-test.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { currentURL, visit, fillIn } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import Pretender from 'pretender'; -import logout from 'vault/tests/pages/logout'; -import { getManagedNamespace } from 'vault/routes/vault/cluster'; - -const FEATURE_FLAGS_RESPONSE = { - feature_flags: ['VAULT_CLOUD_ADMIN_NAMESPACE'], -}; - -module('Acceptance | Enterprise | Managed namespace root', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - /** - * Since the features are fetched on the application load, - * we have to populate them on the beforeEach hook because - * the fetch won't trigger again within the tests - */ - this.server = new Pretender(function () { - this.get('/v1/sys/internal/ui/feature-flags', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify(FEATURE_FLAGS_RESPONSE)]; - }); - this.get('/v1/sys/health', this.passthrough); - this.get('/v1/sys/seal-status', this.passthrough); - this.get('/v1/sys/license/features', this.passthrough); - this.get('/v1/sys/internal/ui/mounts', this.passthrough); - }); - }); - - hooks.afterEach(function () { - this.server.shutdown(); - }); - - test('it shows the managed namespace toolbar when feature flag exists', async function (assert) { - await logout.visit(); - await visit('/vault/auth'); - assert.ok(currentURL().startsWith('/vault/auth'), 'Redirected to auth'); - assert.ok(currentURL().includes('?namespace=admin'), 'with base namespace'); - assert.dom('[data-test-namespace-toolbar]').doesNotExist('Normal namespace toolbar does not exist'); - assert.dom('[data-test-managed-namespace-toolbar]').exists('Managed namespace toolbar exists'); - assert.dom('[data-test-managed-namespace-root]').hasText('/admin', 'Shows /admin namespace prefix'); - assert.dom('input#namespace').hasAttribute('placeholder', '/ (Default)'); - await fillIn('input#namespace', '/foo'); - const encodedNamespace = encodeURIComponent('admin/foo'); - assert.strictEqual( - currentURL(), - `/vault/auth?namespace=${encodedNamespace}&with=token`, - 'Correctly prepends root to namespace' - ); - }); - - test('getManagedNamespace helper works as expected', function (assert) { - let managedNs = getManagedNamespace(null, 'admin'); - assert.strictEqual(managedNs, 'admin', 'returns root ns when no namespace present'); - managedNs = getManagedNamespace('admin/', 'admin'); - assert.strictEqual(managedNs, 'admin', 'returns root ns when matches passed ns'); - managedNs = getManagedNamespace('adminfoo/', 'admin'); - assert.strictEqual( - managedNs, - 'admin/adminfoo/', - 'appends passed namespace to root even if it matches without slashes' - ); - managedNs = getManagedNamespace('admin/foo/', 'admin'); - assert.strictEqual(managedNs, 'admin/foo/', 'returns passed namespace if it starts with root and /'); - }); - - test('it redirects to root prefixed ns when non-root passed', async function (assert) { - await logout.visit(); - await visit('/vault/auth?namespace=admindev'); - assert.ok(currentURL().startsWith('/vault/auth'), 'Redirected to auth'); - assert.ok( - currentURL().includes(`?namespace=${encodeURIComponent('admin/admindev')}`), - 'with appended namespace' - ); - - assert.dom('[data-test-managed-namespace-root]').hasText('/admin', 'Shows /admin namespace prefix'); - assert.dom('input#namespace').hasValue('/admindev', 'Input has /dev value'); - }); -}); diff --git a/ui/tests/acceptance/mfa-login-enforcement-test.js b/ui/tests/acceptance/mfa-login-enforcement-test.js deleted file mode 100644 index 8383c3c1f..000000000 --- a/ui/tests/acceptance/mfa-login-enforcement-test.js +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, currentRouteName, fillIn, visit } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import ENV from 'vault/config/environment'; - -module('Acceptance | mfa-login-enforcement', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'mfaConfig'; - }); - hooks.beforeEach(function () { - return authPage.login(); - }); - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - test('it should create login enforcement', async function (assert) { - await visit('/ui/vault/access'); - await click('[data-test-sidebar-nav-link="Multi-factor authentication"]'); - await click('[data-test-tab="enforcements"]'); - await click('[data-test-enforcement-create]'); - - assert.dom('[data-test-mleh-title]').hasText('New enforcement', 'Title renders'); - await click('[data-test-mlef-save]'); - assert - .dom('[data-test-inline-error-message]') - .exists({ count: 3 }, 'Validation error messages are displayed'); - - await click('[data-test-mlef-cancel]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.enforcements.index', - 'Cancel transitions to enforcements list' - ); - await click('[data-test-enforcement-create]'); - await click('.breadcrumb a'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.enforcements.index', - 'Breadcrumb transitions to enforcements list' - ); - await click('[data-test-enforcement-create]'); - - await fillIn('[data-test-mlef-input="name"]', 'foo'); - await click('[data-test-component="search-select"] .ember-basic-dropdown-trigger'); - await click('.ember-power-select-option'); - await fillIn('[data-test-mount-accessor-select]', 'auth_userpass_bb95c2b1'); - await click('[data-test-mlef-add-target]'); - await click('[data-test-mlef-save]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.enforcements.enforcement.index', - 'Route transitions to enforcement on save success' - ); - }); - - test('it should list login enforcements', async function (assert) { - await visit('/vault/access/mfa/enforcements'); - assert.dom('[data-test-tab="enforcements"]').hasClass('active', 'Enforcements tab is active'); - assert.dom('.toolbar-link').exists({ count: 1 }, 'Correct number of toolbar links render'); - assert - .dom('[data-test-enforcement-create]') - .includesText('New enforcement', 'New enforcement link renders'); - - await click('[data-test-enforcement-create]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.enforcements.create', - 'New enforcement link transitions to create route' - ); - await click('[data-test-mlef-cancel]'); - - const enforcements = this.server.db.mfaLoginEnforcements.where({}); - const item = enforcements[0]; - assert.dom('[data-test-list-item]').exists({ count: enforcements.length }, 'Enforcements list renders'); - assert - .dom(`[data-test-list-item="${item.name}"] svg`) - .hasClass('flight-icon-lock', 'Lock icon renders for list item'); - assert.dom(`[data-test-list-item-title="${item.name}"]`).hasText(item.name, 'Enforcement name renders'); - - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-list-item-link="details"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.enforcements.enforcement.index', - 'Details more menu action transitions to enforcement route' - ); - await click('.breadcrumb a'); - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-list-item-link="edit"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.enforcements.enforcement.edit', - 'Edit more menu action transitions to enforcement edit route' - ); - }); - - test('it should display login enforcement', async function (assert) { - await visit('/vault/access/mfa/enforcements'); - const enforcement = this.server.db.mfaLoginEnforcements.where({})[0]; - await click(`[data-test-list-item="${enforcement.name}"]`); - // heading - assert.dom('h1').includesText(enforcement.name, 'Name renders in title'); - assert.dom('h1 svg').hasClass('flight-icon-lock', 'Lock icon renders in title'); - assert.dom('[data-test-tab="targets"]').hasClass('active', 'Targets tab is active by default'); - assert.dom('[data-test-target]').exists({ count: 4 }, 'Targets render in list'); - // targets tab - const targets = { - accessor: ['userpass/', 'auth_userpass_bb95c2b1', '/ui/vault/access/userpass'], - method: ['userpass', 'All userpass mounts (1)'], - entity: [ - 'test-entity', - 'f831667b-7392-7a1c-c0fc-33d48cb1c57d', - '/ui/vault/access/identity/entities/f831667b-7392-7a1c-c0fc-33d48cb1c57d/details', - ], - group: [ - 'test-group', - '34db6b52-591e-bc22-8af0-4add5e167326', - '/ui/vault/access/identity/groups/34db6b52-591e-bc22-8af0-4add5e167326/details', - ], - }; - for (const key in targets) { - const t = targets[key]; - const selector = `[data-test-target="${t[0]}"]`; - assert.dom(selector).includesText(`${t[0]} ${t[1]}`, `Target text renders for ${key} type`); - if (key !== 'method') { - await click(`${selector} [data-test-popup-menu-trigger]`); - assert - .dom(`[data-test-target-link="${t[0]}"]`) - .hasAttribute('href', t[2], `Details link renders for ${key} type`); - } else { - assert.dom(`${selector} [data-test-popup-menu-trigger]`).doesNotExist('Method type has no link'); - } - } - // methods tab - await click('[data-test-tab="methods"]'); - assert.dom('[data-test-tab="methods"]').hasClass('active', 'Methods tab is active'); - const method = this.owner.lookup('service:store').peekRecord('mfa-method', enforcement.mfa_method_ids[0]); - assert - .dom(`[data-test-mfa-method-list-item="${method.id}"]`) - .includesText( - `${method.name} ${method.id} Namespace: ${method.namespace_id}`, - 'Method list item renders' - ); - await click('[data-test-popup-menu-trigger]'); - assert - .dom(`[data-test-mfa-method-menu-link="details"]`) - .hasAttribute('href', `/ui/vault/access/mfa/methods/${method.id}`, `Details link renders for method`); - assert - .dom(`[data-test-mfa-method-menu-link="edit"]`) - .hasAttribute('href', `/ui/vault/access/mfa/methods/${method.id}/edit`, `Edit link renders for method`); - // toolbar - assert - .dom('[data-test-enforcement-edit]') - .hasAttribute( - 'href', - `/ui/vault/access/mfa/enforcements/${enforcement.name}/edit`, - 'Toolbar edit action has link to edit route' - ); - await click('[data-test-enforcement-delete]'); - assert.dom('[data-test-confirm-button]').isDisabled('Delete button disabled with no confirmation'); - await fillIn('[data-test-confirmation-modal-input]', enforcement.name); - await click('[data-test-confirm-button]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.enforcements.index', - 'Route transitions to enforcements list on delete success' - ); - }); - - test('it should edit login enforcement', async function (assert) { - await visit('/vault/access/mfa/enforcements'); - const enforcement = this.server.db.mfaLoginEnforcements.where({})[0]; - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-list-item-link="edit"]'); - - assert.dom('h1').hasText('Update enforcement', 'Title renders'); - assert.dom('[data-test-mlef-input="name"]').hasValue(enforcement.name, 'Name input is populated'); - assert.dom('[data-test-mlef-input="name"]').isDisabled('Name is disabled and cannot be changed'); - - const method = this.owner.lookup('service:store').peekRecord('mfa-method', enforcement.mfa_method_ids[0]); - assert - .dom('[data-test-selected-option]') - .includesText(`${method.name} ${method.id}`, 'Selected mfa method renders'); - assert - .dom('[data-test-mlef-target="Authentication mount"]') - .includesText('Authentication mount auth_userpass_bb95c2b1', 'Accessor target populates'); - assert - .dom('[data-test-mlef-target="Authentication method"]') - .includesText('Authentication method userpass', 'Method target populates'); - assert - .dom('[data-test-mlef-target="Group"]') - .includesText('Group test-group 34db6b52-591e-bc22-8af0-4add5e167326', 'Group target populates'); - assert - .dom('[data-test-mlef-target="Entity"]') - .includesText('Entity test-entity f831667b-7392-7a1c-c0fc-33d48cb1c57d', 'Entity target populates'); - - await click('[data-test-mlef-remove-target="Entity"]'); - await click('[data-test-mlef-remove-target="Group"]'); - await click('[data-test-mlef-remove-target="Authentication method"]'); - await click('[data-test-mlef-save]'); - - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.enforcements.enforcement.index', - 'Route transitions to enforcement on save success' - ); - assert.dom('[data-test-target]').exists({ count: 1 }, 'Targets were successfully removed on save'); - }); -}); diff --git a/ui/tests/acceptance/mfa-login-test.js b/ui/tests/acceptance/mfa-login-test.js deleted file mode 100644 index 60e69d82f..000000000 --- a/ui/tests/acceptance/mfa-login-test.js +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, currentRouteName, fillIn, visit, waitUntil, find } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import ENV from 'vault/config/environment'; -import { validationHandler } from '../../mirage/handlers/mfa-login'; - -module('Acceptance | mfa-login', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'mfaLogin'; - }); - hooks.beforeEach(function () { - this.select = async (select = 0, option = 1) => { - const selector = `[data-test-mfa-select="${select}"]`; - const value = this.element.querySelector(`${selector} option:nth-child(${option + 1})`).value; - await fillIn(`${selector} select`, value); - }; - }); - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - const login = async (user) => { - await visit('/vault/auth'); - await fillIn('[data-test-select="auth-method"]', 'userpass'); - await fillIn('[data-test-username]', user); - await fillIn('[data-test-password]', 'test'); - await click('[data-test-auth-submit]'); - }; - const didLogin = (assert) => { - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backends', 'Route transitions after login'); - }; - const validate = async (multi) => { - await fillIn('[data-test-mfa-passcode="0"]', 'test'); - if (multi) { - await fillIn('[data-test-mfa-passcode="1"]', 'test'); - } - await click('[data-test-mfa-validate]'); - }; - - test('it should handle single mfa constraint with passcode method', async function (assert) { - assert.expect(4); - await login('mfa-a'); - assert - .dom('[data-test-mfa-description]') - .includesText( - 'Enter your authentication code to log in.', - 'Mfa form displays with correct description' - ); - assert.dom('[data-test-mfa-select]').doesNotExist('Select is hidden for single method'); - assert.dom('[data-test-mfa-passcode]').exists({ count: 1 }, 'Single passcode input renders'); - await validate(); - didLogin(assert); - }); - - test('it should handle single mfa constraint with push method', async function (assert) { - assert.expect(6); - - server.post('/sys/mfa/validate', async (schema, req) => { - await waitUntil(() => find('[data-test-mfa-description]')); - assert - .dom('[data-test-mfa-description]') - .hasText( - 'Multi-factor authentication is enabled for your account.', - 'Mfa form displays with correct description' - ); - assert.dom('[data-test-mfa-label]').hasText('Okta push notification', 'Correct method renders'); - assert - .dom('[data-test-mfa-push-instruction]') - .hasText('Check device for push notification', 'Push notification instruction renders'); - assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled while validating'); - assert - .dom('[data-test-mfa-validate]') - .hasClass('is-loading', 'Loading class applied to button while validating'); - return validationHandler(schema, req); - }); - - await login('mfa-b'); - didLogin(assert); - }); - - test('it should handle single mfa constraint with 2 passcode methods', async function (assert) { - assert.expect(4); - await login('mfa-c'); - assert - .dom('[data-test-mfa-description]') - .includesText('Select the MFA method you wish to use.', 'Mfa form displays with correct description'); - assert - .dom('[data-test-mfa-select]') - .exists({ count: 1 }, 'Select renders for single constraint with multiple methods'); - assert.dom('[data-test-mfa-passcode]').doesNotExist('Passcode input hidden until selection is made'); - await this.select(); - await validate(); - didLogin(assert); - }); - - test('it should handle single mfa constraint with 2 push methods', async function (assert) { - assert.expect(1); - await login('mfa-d'); - await this.select(); - await click('[data-test-mfa-validate]'); - didLogin(assert); - }); - - test('it should handle single mfa constraint with 1 passcode and 1 push method', async function (assert) { - assert.expect(3); - await login('mfa-e'); - await this.select(0, 2); - assert.dom('[data-test-mfa-passcode]').exists('Passcode input renders'); - await this.select(); - assert.dom('[data-test-mfa-passcode]').doesNotExist('Passcode input is hidden for push method'); - await click('[data-test-mfa-validate]'); - didLogin(assert); - }); - - test('it should handle multiple mfa constraints with 1 passcode method each', async function (assert) { - assert.expect(3); - await login('mfa-f'); - assert - .dom('[data-test-mfa-description]') - .includesText( - 'Two methods are required for successful authentication.', - 'Mfa form displays with correct description' - ); - assert.dom('[data-test-mfa-select]').doesNotExist('Selects do not render for single methods'); - await validate(true); - didLogin(assert); - }); - - test('it should handle multi mfa constraint with 1 push method each', async function (assert) { - assert.expect(1); - await login('mfa-g'); - didLogin(assert); - }); - - test('it should handle multiple mfa constraints with 1 passcode and 1 push method', async function (assert) { - assert.expect(4); - await login('mfa-h'); - assert - .dom('[data-test-mfa-description]') - .includesText( - 'Two methods are required for successful authentication.', - 'Mfa form displays with correct description' - ); - assert.dom('[data-test-mfa-select]').doesNotExist('Select is hidden for single method'); - assert.dom('[data-test-mfa-passcode]').exists({ count: 1 }, 'Passcode input renders'); - await validate(); - didLogin(assert); - }); - - test('it should handle multiple mfa constraints with multiple mixed methods', async function (assert) { - assert.expect(2); - await login('mfa-i'); - assert - .dom('[data-test-mfa-description]') - .includesText( - 'Two methods are required for successful authentication.', - 'Mfa form displays with correct description' - ); - await this.select(); - await fillIn('[data-test-mfa-passcode="1"]', 'test'); - await click('[data-test-mfa-validate]'); - didLogin(assert); - }); - - test('it should render unauthorized message for push failure', async function (assert) { - await login('mfa-j'); - assert.dom('[data-test-auth-form]').doesNotExist('Auth form hidden when mfa fails'); - assert.dom('[data-test-empty-state-title]').hasText('Unauthorized', 'Error title renders'); - assert - .dom('[data-test-empty-state-subText]') - .hasText('PingId MFA validation failed', 'Error message from server renders'); - assert - .dom('[data-test-empty-state-message]') - .hasText( - 'Multi-factor authentication is required, but failed. Go back and try again, or contact your administrator.', - 'Error description renders' - ); - await click('[data-test-mfa-error] button'); - assert.dom('[data-test-auth-form]').exists('Auth form renders after mfa error dismissal'); - }); -}); diff --git a/ui/tests/acceptance/mfa-method-test.js b/ui/tests/acceptance/mfa-method-test.js deleted file mode 100644 index 90ff5b8e3..000000000 --- a/ui/tests/acceptance/mfa-method-test.js +++ /dev/null @@ -1,309 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, currentRouteName, currentURL, fillIn, visit } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import ENV from 'vault/config/environment'; -import { Response } from 'miragejs'; -import { underscore } from '@ember/string'; - -module('Acceptance | mfa-method', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'mfaConfig'; - }); - hooks.beforeEach(async function () { - this.store = this.owner.lookup('service:store'); - this.getMethods = () => - ['Totp', 'Duo', 'Okta', 'Pingid'].reduce((methods, type) => { - methods.addObjects(this.server.db[`mfa${type}Methods`].where({})); - return methods; - }, []); - await logout.visit(); - return authPage.login(); - }); - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - test('it should display landing page when no methods exist', async function (assert) { - this.server.get('/identity/mfa/method/', () => new Response(404, {}, { errors: [] })); - await visit('/vault/access/mfa/methods'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.index', - 'Route redirects to mfa index when no methods exist' - ); - await click('[data-test-mfa-configure]'); - assert.strictEqual(currentRouteName(), 'vault.cluster.access.mfa.methods.create'); - }); - - test('it should list methods', async function (assert) { - await visit('/vault/access/mfa'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.methods.index', - 'Parent route redirects to methods when some exist' - ); - assert.dom('[data-test-tab="methods"]').hasClass('active', 'Methods tab is active'); - assert.dom('.toolbar-link').exists({ count: 1 }, 'Correct number of toolbar links render'); - assert.dom('[data-test-mfa-method-create]').includesText('New MFA method', 'New mfa link renders'); - - await click('[data-test-mfa-method-create]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.methods.create', - 'New method link transitions to create route' - ); - await click('.breadcrumb a'); - - const methods = this.getMethods(); - const model = this.store.peekRecord('mfa-method', methods[0].id); - assert.dom('[data-test-mfa-method-list-item]').exists({ count: methods.length }, 'Methods list renders'); - assert.dom(`[data-test-mfa-method-list-icon="${model.type}"]`).exists('Icon renders for list item'); - assert - .dom(`[data-test-mfa-method-list-item="${model.id}"]`) - .includesText( - `${model.name} ${model.id} Namespace: ${model.namespace_id}`, - 'Copy renders for list item' - ); - - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-mfa-method-menu-link="details"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.methods.method.index', - 'Details more menu action transitions to method route' - ); - await click('.breadcrumb a'); - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-mfa-method-menu-link="edit"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.methods.method.edit', - 'Edit more menu action transitions to method edit route' - ); - }); - - test('it should display method details', async function (assert) { - // ensure methods are tied to an enforcement - this.server.get('/identity/mfa/login-enforcement', () => { - const record = this.server.create('mfa-login-enforcement', { - mfa_method_ids: this.getMethods().mapBy('id'), - }); - return { - data: { - key_info: { [record.name]: record }, - keys: [record.name], - }, - }; - }); - await visit('/vault/access/mfa/methods'); - await click('[data-test-mfa-method-list-item]'); - assert.dom('[data-test-tab="config"]').hasClass('active', 'Configuration tab is active by default'); - assert - .dom('[data-test-confirm-action-trigger]') - .isDisabled('Delete toolbar action disabled when method is attached to an enforcement'); - - const fields = [ - ['Issuer', 'Period', 'Key size', 'QR size', 'Algorithm', 'Digits', 'Skew', 'Max validation attempts'], - ['Duo API hostname', 'Passcode reminder'], - ['Organization name', 'Base URL'], - ['Use signature', 'Idp url', 'Admin url', 'Authenticator url', 'Org alias'], - ]; - for (const [index, labels] of fields.entries()) { - if (index) { - await click(`[data-test-mfa-method-list-item]:nth-of-type(${index + 2})`); - } - const url = currentURL(); - const id = url.slice(url.lastIndexOf('/') + 1); - const model = this.store.peekRecord('mfa-method', id); - - labels.forEach((label) => { - assert.dom(`[data-test-row-label="${label}"]`).hasText(label, `${label} field label renders`); - const key = - { - 'Duo API hostname': 'api_hostname', - 'Passcode reminder': 'use_passcode', - 'Organization name': 'org_name', - }[label] || underscore(label); - const value = typeof model[key] === 'boolean' ? (model[key] ? 'Yes' : 'No') : model[key].toString(); - assert.dom(`[data-test-value-div="${label}"]`).hasText(value, `${label} value renders`); - }); - await click('.breadcrumb a'); - } - - await click('[data-test-mfa-method-list-item]'); - await click('[data-test-mfa-method-edit]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.methods.method.edit', - 'Toolbar action transitions to edit route' - ); - }); - - test('it should delete method that is not associated with any login enforcements', async function (assert) { - this.server.get('/identity/mfa/login-enforcement', () => new Response(404, {}, { errors: [] })); - - await visit('/vault/access/mfa/methods'); - const methodCount = this.element.querySelectorAll('[data-test-mfa-method-list-item]').length; - await click('[data-test-mfa-method-list-item]'); - await click('[data-test-confirm-action-trigger]'); - await click('[data-test-confirm-button]'); - assert.dom('[data-test-mfa-method-list-item]').exists({ count: methodCount - 1 }, 'Method was deleted'); - }); - - test('it should create methods', async function (assert) { - assert.expect(12); - - await visit('/vault/access/mfa/methods'); - const methodCount = this.element.querySelectorAll('[data-test-mfa-method-list-item]').length; - - const methods = [ - { type: 'totp', required: ['issuer'] }, - { type: 'duo', required: ['secret_key', 'integration_key', 'api_hostname'] }, - { type: 'okta', required: ['org_name', 'api_token'] }, - { type: 'pingid', required: ['settings_file_base64'] }, - ]; - for (const [index, method] of methods.entries()) { - const { type, required } = method; - await click('[data-test-mfa-method-create]'); - await click(`[data-test-radio-card="${method.type}"]`); - await click('[data-test-mfa-create-next]'); - await click('[data-test-mleh-radio="skip"]'); - await click('[data-test-mfa-create-save]'); - assert - .dom('[data-test-inline-error-message]') - .exists({ count: required.length }, `Required field validations display for ${type}`); - - for (const [i, field] of required.entries()) { - let inputType = 'input'; - // this is less than ideal but updating the test selectors in masked-input break a bunch of tests - // add value to the masked input text area data-test attributes for selection - if (['secret_key', 'integration_key'].includes(field)) { - inputType = 'textarea'; - const textareas = this.element.querySelectorAll('[data-test-textarea]'); - textareas[i].setAttribute('data-test-textarea', field); - } - await fillIn(`[data-test-${inputType}="${field}"]`, 'foo'); - } - await click('[data-test-mfa-create-save]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.methods.method.index', - `${type} method is displayed on save` - ); - await click('.breadcrumb a'); - assert - .dom('[data-test-mfa-method-list-item]') - .exists({ count: methodCount + index + 1 }, `List updates with new ${type} method`); - } - }); - - test('it should create method with new enforcement', async function (assert) { - await visit('/vault/access/mfa/methods/create'); - await click('[data-test-radio-card="totp"]'); - await click('[data-test-mfa-create-next]'); - await fillIn('[data-test-input="issuer"]', 'foo'); - await fillIn('[data-test-mlef-input="name"]', 'bar'); - await fillIn('[data-test-mount-accessor-select]', 'auth_userpass_bb95c2b1'); - await click('[data-test-mlef-add-target]'); - await click('[data-test-mfa-create-save]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.methods.method.index', - 'Route transitions to method on save' - ); - await click('[data-test-tab="enforcements"]'); - assert.dom('[data-test-list-item]').hasText('bar', 'Enforcement is listed in method view'); - await click('[data-test-sidebar-nav-link="Multi-factor authentication"]'); - await click('[data-test-tab="enforcements"]'); - assert.dom('[data-test-list-item="bar"]').hasText('bar', 'Enforcement is listed in enforcements view'); - await click('[data-test-list-item="bar"]'); - await click('[data-test-tab="methods"]'); - assert - .dom('[data-test-mfa-method-list-item]') - .includesText('TOTP', 'TOTP method is listed in enforcement view'); - }); - - test('it should create method and add it to existing enforcement', async function (assert) { - await visit('/vault/access/mfa/methods/create'); - await click('[data-test-radio-card="totp"]'); - await click('[data-test-mfa-create-next]'); - await fillIn('[data-test-input="issuer"]', 'foo'); - await click('[data-test-mleh-radio="existing"]'); - await click('[data-test-component="search-select"] .ember-basic-dropdown-trigger'); - const enforcement = this.element.querySelector('.ember-power-select-option'); - const name = enforcement.children[0].textContent.trim(); - await click(enforcement); - await click('[data-test-mfa-create-save]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.methods.method.index', - 'Route transitions to method on save' - ); - await click('[data-test-tab="enforcements"]'); - assert.dom('[data-test-list-item]').hasText(name, 'Enforcement is listed in method view'); - }); - - test('it should edit methods', async function (assert) { - await visit('/vault/access/mfa/methods'); - const id = this.element.querySelector('[data-test-mfa-method-list-item] .tag').textContent.trim(); - const model = this.store.peekRecord('mfa-method', id); - await click('[data-test-mfa-method-list-item] .ember-basic-dropdown-trigger'); - await click('[data-test-mfa-method-menu-link="edit"]'); - - const keys = ['issuer', 'period', 'key_size', 'qr_size', 'algorithm', 'digits', 'skew']; - keys.forEach((key) => { - if (key === 'period') { - assert - .dom('[data-test-ttl-value="Period"]') - .hasValue(model.period.toString(), 'Period form field is populated with model value'); - assert.dom('[data-test-select="ttl-unit"]').hasValue('s', 'Correct time unit is shown for period'); - } else if (key === 'algorithm' || key === 'digits' || key === 'skew') { - const radioElem = this.element.querySelector(`input[name=${key}]:checked`); - assert - .dom(radioElem) - .hasValue(model[key].toString(), `${key} form field is populated with model value`); - } else { - assert - .dom(`[data-test-input="${key}"]`) - .hasValue(model[key].toString(), `${key} form field is populated with model value`); - } - }); - - await fillIn('[data-test-input="issuer"]', 'foo'); - const SHA1radioBtn = this.element.querySelectorAll('input[name=algorithm]')[0]; - await click(SHA1radioBtn); - await fillIn('[data-test-input="max_validation_attempts"]', 10); - await click('[data-test-mfa-method-save]'); - await fillIn('[data-test-confirmation-modal-input]', model.type); - await click('[data-test-confirm-button]'); - - assert.dom('[data-test-row-value="Issuer"]').hasText('foo', 'Issuer field is updated'); - assert.dom('[data-test-row-value="Algorithm"]').hasText('SHA1', 'Algorithm field is updated'); - assert - .dom('[data-test-row-value="Max validation attempts"]') - .hasText('10', 'Max validation attempts field is updated'); - }); - - test('it should navigate to enforcements create route from method enforcement tab', async function (assert) { - await visit('/vault/access/mfa/methods'); - await click('[data-test-mfa-method-list-item]'); - await click('[data-test-tab="enforcements"]'); - await click('[data-test-enforcement-create]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.mfa.enforcements.create', - 'Navigates to enforcements create route from toolbar action' - ); - }); -}); diff --git a/ui/tests/acceptance/mfa-setup-test.js b/ui/tests/acceptance/mfa-setup-test.js deleted file mode 100644 index abfcf7e80..000000000 --- a/ui/tests/acceptance/mfa-setup-test.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { create } from 'ember-cli-page-object'; -import { v4 as uuidv4 } from 'uuid'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, fillIn } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import enablePage from 'vault/tests/pages/settings/auth/enable'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import { setupMirage } from 'ember-cli-mirage/test-support'; - -const consoleComponent = create(consoleClass); -const USER = 'end-user'; -const PASSWORD = 'mypassword'; -const POLICY_NAME = 'identity_policy'; - -const writePolicy = async function (path) { - await enablePage.enable('userpass', path); - const identityPolicy = `path "identity/*" { - capabilities = ["create", "read", "update", "delete", "list"] - }`; - await consoleComponent.runCommands([ - `write sys/policies/acl/${POLICY_NAME} policy=${btoa(identityPolicy)}`, - ]); -}; - -const writeUserWithPolicy = async function (path) { - await consoleComponent.runCommands([ - `write auth/${path}/users/${USER} password=${PASSWORD} policies=${POLICY_NAME}`, - ]); -}; - -const setupUser = async function (path) { - await writePolicy(path); - await writeUserWithPolicy(path); - await click('[data-test-save-config="true"]'); -}; - -module('Acceptance | mfa-setup', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - const path = `userpass-${uuidv4()}`; - await authPage.login(); - await setupUser(path); - await authPage.logout(); - await authPage.loginUsername(USER, PASSWORD, path); - await click('[data-test-user-menu-trigger]'); - await click('[data-test-user-menu-item="mfa"]'); - }); - - test('it should login through MFA and post to generate and be able to restart the setup', async function (assert) { - assert.expect(5); - // the network requests required in this test - this.server.post('/identity/mfa/method/totp/generate', (scheme, req) => { - const json = JSON.parse(req.requestBody); - assert.strictEqual(json.method_id, '123', 'sends the UUID value'); - return { - data: { - barcode: - 'iVBORw0KGgoAAAANSUhEUgAAAMgAAADIEAAAAADYoy0BAAAGbUlEQVR4nOydUW7kOAxEk0Xuf+VZzABepAXTrKLUmMrivY8AacuSkgJFSyTdX79+fUAQ//ztCcArX79/fH7Oblat6+q/a7+2c++r5qX+fdU4av/rvF1+34+FhIEgYSBIGF/ff3F9gtuuW4u7Nbi6v1v7q36nT5i7PumpPywkDAQJA0HC+Lr7sFoj3ef0bj/h+gR13PX3ype4+4tufHWe1XgfWEgeCBIGgoRx60OmVGute/aj+oaq/a5vWXHnswMWEgaChIEgYRz1IRfdc7e65rrP/9Var/oKN47yjmgrFhIGgoSBIGHc+pDdOMGpOMPanxprX8+qVF/S+aBqXh3O/wMLCQNBwkCQMF58yDSf6KJbqzsf4LZf5z3tz92nqD5l8v/EQsJAkDAQJIw/PuRdGfDdc37X/sI9g+rG7/7eqS/r5qOAhYSBIGEgSBgv9SFufUN3xqSeHU2f36fxCjVuosbaT9ajYCFhIEgYCBLG7VnW9Axomv+krunV9RX3jErFzQ+bjIuFhIEgYSBIGLc1htMzp1P14ru+rPM9Fe5+5FRM/ft4WEgYCBIGgoTxGFPv6j2qWr21v2lsXPUF0zOrFfUMa/ouFsWnYiFhIEgYCBLG47tOurVvWoe+G5verT85lUOgnpk5NZZYSBgIEgaChHGbl+XGvt19htrvivu8r67t3bynOb/rvJRxsZAwECQMBAnj8/v6peY5vTtWvZsn5tYAuvld7j7M8ZFYSBgIEgaChPF5t85On9/d+MDKbr7V6TqXaTxE3UexD/kBIEgYCBLG4/eHTHNV1Rxg9Qyp69dl+nepuctVewUsJAwECQNBwrDelzWNHVf3d3T1J7vz7eY1zSFW+78DCwkDQcJAkDBuz7LKxhv5RnecWnvds7fqczWvTL3ezfPucywkDAQJA0HCePwOKrcOY7pPmPY/9R0V3b5nWje/tnsCCwkDQcJAkDCsfch/N23uR9wYtHt9WtNYXVfnTV7W/xAECQNBwrh95+LK9Pl8ty59N/9q6juq+3f3Icr8sJAwECQMBAnjzz7EfV6uUJ/Tp7m40/lM4xZdf9P6lWoc9iGBIEgYCBLGY43htP5cbbfinn3t5mPtnoW581H6x0LCQJAwECSMx+9Td3Nhq+vqPketU3Hn456Vdfd1uGde5GUFgyBhIEgYo3e/T2sCq89P1berqL6rus+NozjXsZAwECQMBAnjaDzkYpqf5D7nn5rXev1d8ZBuHPYhgSBIGAgSxuP3GLpxiAr1jGnXl53ygV1/p+I2d/dhIWEgSBgIEoZVY9hdP/UeqmkeleoDu3FdX+S2fzrLw0LCQJAwECSMl+8PmT7vV9fVM6Fu7b9wY+In6jUUqvmr8Rv2IcEgSBgIEsZtjeE0HrAba76o9gvdeN3v1TjT3GF13ur97EMCQZAwECQM6zuoLqb1H11/p3OG3x3DP1VPz1lWMAgSBoKE8fju92m9xLQW8XTdd9W+OkOb3t+1q+Z7BxYSBoKEgSBhPH6PYYXqY9TP5cmavmVa57KON53npF4GCwkDQcJAkDBu4yG7NYHTnN1p/7ux8Kr/U/si5+wPCwkDQcJAkDBu60Mupmv4tHbP7d/NAeg4ldOr+tA7sJAwECQMBAlDqlOf7ktOxaJP1ZOvTPOvOtz/w3ewkDAQJAwECeM2L2t65uSeTbk1f7txmq7fUz6waq+AhYSBIGEgSBiP7+29cPOZujXVrSdR1/jd3GC3JrJrp47//X4sJAwECQNBwnh818lFVz++tpvGMab7BXWNrzhVT7Ib4//AQvJAkDAQJIyXeMiKG0fY9R3T+EOFmydVtTs1XyVOg4WEgSBhIEgYL+/LUtmt7ZvGD9x3iXRnSt381Hm583nqBwsJA0HCQJAwXupDXLozLrcuvXvur67v1pOovk3dj6hnbnfzxELCQJAwECSM2+8x7HDX8Op+t76iQvUd1Ty6+9zc32ku8QcWkgeChIEgYUgx9YvdM69qHDeu0s2z6mfqM9R8rt0zPfYhgSBIGAgShpTb+y52fYQbl1nbuXlm1efqPkXxaVhIGAgSBoKEcdSHnM7Vre5zx1Ovu59PaxLJy/pBIEgYCBLG47vfVdw68hV3LXfnNZ2Put/YnQ9nWcEgSBgIEsbjOxdVujOad7wT5MR907OobvxpbsIHFpIHgoSBIGHcfn8I/D2wkDD+DQAA//8FNJPArbdKOwAAAABJRU5ErkJggg==', - url: 'otpauth://totp/Vault:26606dbe-d8ea-82ca-41b0-1250a4484079?algorithm=SHA1&digits=6&issuer=Vault&period=30&secret=FID3WRPRRADQDN3CGPVVOLKCXTZZPSML', - lease_duration: 0, - }, - }; - }); - this.server.post('/identity/mfa/method/totp/admin-destroy', (scheme, req) => { - const json = JSON.parse(req.requestBody); - assert.strictEqual(json.method_id, '123', 'sends the UUID value'); - // returns nothing - return {}; - }); - await fillIn('[data-test-input="uuid"]', 123); - await click('[data-test-verify]'); - assert.dom('[data-test-qrcode]').exists('the qrCode is shown.'); - assert.dom('[data-test-mfa-enabled-warning]').doesNotExist('warning does not show.'); - await click('[data-test-restart]'); - assert.dom('[data-test-step-one]').exists('back to step one.'); - }); - - test('it should show a warning if you enter in the same UUID without restarting the setup', async function (assert) { - assert.expect(2); - // the network requests required in this test - this.server.post('/identity/mfa/method/totp/generate', () => { - return { - data: null, - warnings: ['Entity already has a secret for MFA method “”'], - }; - }); - - await fillIn('[data-test-input="uuid"]', 123); - await click('[data-test-verify]'); - assert.dom('[data-test-qrcode]').doesNotExist('the qrCode is not shown.'); - assert.dom('[data-test-mfa-enabled-warning]').exists('the mfa-enabled warning shows.'); - }); -}); diff --git a/ui/tests/acceptance/not-found-test.js b/ui/tests/acceptance/not-found-test.js deleted file mode 100644 index 3e805d4ff..000000000 --- a/ui/tests/acceptance/not-found-test.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { visit } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import Ember from 'ember'; - -let adapterException; - -module('Acceptance | not-found', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - adapterException = Ember.Test.adapter.exception; - Ember.Test.adapter.exception = () => {}; - return authPage.login(); - }); - - hooks.afterEach(function () { - Ember.Test.adapter.exception = adapterException; - return logout.visit(); - }); - - test('top-level not-found', async function (assert) { - await visit('/404'); - assert - .dom('[data-test-error-description]') - .hasText( - 'Sorry, we were unable to find any content at that URL. Double check it or go back home.', - 'renders cluster error template' - ); - }); - - test('vault route not-found', async function (assert) { - await visit('/vault/404'); - assert.dom('[data-test-not-found]').exists('renders the not found component'); - }); - - test('cluster route not-found', async function (assert) { - await visit('/vault/secrets/secret/404/show'); - assert.dom('[data-test-not-found]').exists('renders the not found component'); - }); - - test('secret not-found', async function (assert) { - await visit('/vault/secrets/secret/show/404'); - assert.dom('[data-test-secret-not-found]').exists('renders the message about the secret not being found'); - }); -}); diff --git a/ui/tests/acceptance/oidc-auth-method-test.js b/ui/tests/acceptance/oidc-auth-method-test.js deleted file mode 100644 index 4dfa29612..000000000 --- a/ui/tests/acceptance/oidc-auth-method-test.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, fillIn, find, waitUntil } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { fakeWindow, buildMessage } from '../helpers/oidc-window-stub'; -import sinon from 'sinon'; -import { later, _cancelTimers as cancelTimers } from '@ember/runloop'; - -module('Acceptance | oidc auth method', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.openStub = sinon.stub(window, 'open').callsFake(() => fakeWindow.create()); - // OIDC test fails when using fake timestamps, we use the real timestamp.now here - this.server.post('/auth/oidc/oidc/auth_url', () => ({ - data: { auth_url: 'http://example.com' }, - })); - this.server.get('/auth/foo/oidc/callback', () => ({ - auth: { client_token: 'root' }, - })); - // ensure clean state - localStorage.removeItem('selectedAuth'); - }); - - hooks.afterEach(function () { - this.openStub.restore(); - }); - - test('it should login with oidc when selected from auth methods dropdown', async function (assert) { - assert.expect(1); - - this.server.get('/auth/token/lookup-self', (schema, req) => { - assert.ok(true, 'request made to auth/token/lookup-self after oidc callback'); - req.passthrough(); - }); - authPage.logout(); - // select from dropdown or click auth path tab - await waitUntil(() => find('[data-test-select="auth-method"]')); - await fillIn('[data-test-select="auth-method"]', 'oidc'); - later(() => { - window.postMessage(buildMessage().data, window.origin); - cancelTimers(); - }, 50); - await click('[data-test-auth-submit]'); - }); - - test('it should login with oidc from listed auth mount tab', async function (assert) { - assert.expect(3); - - this.server.get('/sys/internal/ui/mounts', () => ({ - data: { - auth: { - 'test-path/': { description: '', options: {}, type: 'oidc' }, - }, - }, - })); - // this request is fired twice -- total assertion count should be 3 rather than 2 - // JLR TODO - auth-jwt: verify whether additional request is necessary, especially when glimmerizing component - // look into whether didReceiveAttrs is necessary to trigger this request - this.server.post('/auth/test-path/oidc/auth_url', () => { - assert.ok(true, 'auth_url request made to correct non-standard mount path'); - return { data: { auth_url: 'http://example.com' } }; - }); - // there was a bug that would result in the /auth/:path/login endpoint hit with an empty payload rather than lookup-self - // ensure that the correct endpoint is hit after the oidc callback - this.server.get('/auth/token/lookup-self', (schema, req) => { - assert.ok(true, 'request made to auth/token/lookup-self after oidc callback'); - req.passthrough(); - }); - - authPage.logout(); - // select from dropdown or click auth path tab - await waitUntil(() => find('[data-test-auth-method-link="oidc"]')); - await click('[data-test-auth-method-link="oidc"]'); - later(() => { - window.postMessage(buildMessage().data, window.origin); - cancelTimers(); - }, 50); - await click('[data-test-auth-submit]'); - }); - - // coverage for bug where token was selected as auth method for oidc and jwt - test('it should populate oidc auth method on logout', async function (assert) { - authPage.logout(); - // select from dropdown or click auth path tab - await waitUntil(() => find('[data-test-select="auth-method"]')); - await fillIn('[data-test-select="auth-method"]', 'oidc'); - later(() => { - window.postMessage(buildMessage().data, window.origin); - cancelTimers(); - }, 50); - await click('[data-test-auth-submit]'); - await waitUntil(() => find('[data-test-user-menu-trigger]')); - await click('[data-test-user-menu-trigger]'); - await click('#logout'); - assert - .dom('[data-test-select="auth-method"]') - .hasValue('oidc', 'Previous auth method selected on logout'); - }); -}); diff --git a/ui/tests/acceptance/oidc-config/clients-assignments-test.js b/ui/tests/acceptance/oidc-config/clients-assignments-test.js deleted file mode 100644 index 3b670c5f0..000000000 --- a/ui/tests/acceptance/oidc-config/clients-assignments-test.js +++ /dev/null @@ -1,360 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { visit, currentURL, click, fillIn, findAll, currentRouteName } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import ENV from 'vault/config/environment'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import { create } from 'ember-cli-page-object'; -import { clickTrigger } from 'ember-power-select/test-support/helpers'; -import ss from 'vault/tests/pages/components/search-select'; -import fm from 'vault/tests/pages/components/flash-message'; -import { - OIDC_BASE_URL, // -> '/vault/access/oidc' - SELECTORS, - clearRecord, - overrideCapabilities, - overrideMirageResponse, - ASSIGNMENT_LIST_RESPONSE, - ASSIGNMENT_DATA_RESPONSE, -} from 'vault/tests/helpers/oidc-config'; -const searchSelect = create(ss); -const flashMessage = create(fm); - -module('Acceptance | oidc-config clients and assignments', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'oidcConfig'; - }); - - hooks.beforeEach(async function () { - this.store = await this.owner.lookup('service:store'); - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - test('it renders only allow_all when no assignments are configured', async function (assert) { - assert.expect(3); - - //* clear out test state - await clearRecord(this.store, 'oidc/assignment', 'test-assignment'); - - await visit(OIDC_BASE_URL + '/assignments'); - assert.strictEqual(currentURL(), '/vault/access/oidc/assignments'); - assert.dom('[data-test-tab="assignments"]').hasClass('active', 'assignments tab is active'); - assert - .dom('[data-test-oidc-assignment-linked-block="allow_all"]') - .hasClass('is-disabled', 'renders default allow all assignment and is disabled.'); - }); - - test('it renders empty state when no clients are configured', async function (assert) { - assert.expect(5); - this.server.get('/identity/oidc/client', () => overrideMirageResponse(404)); - - await visit(OIDC_BASE_URL); - assert.strictEqual(currentURL(), '/vault/access/oidc'); - assert.dom('h1.title.is-3').hasText('OIDC Provider'); - assert.dom(SELECTORS.oidcHeader).hasText( - `Configure Vault to act as an OIDC identity provider, and offer Vault’s various authentication - methods and source of identity to any client applications. Learn more Create your first app`, - 'renders call to action header when no clients are configured' - ); - assert.dom('[data-test-oidc-landing]').exists('landing page renders when no clients are configured'); - assert - .dom(SELECTORS.oidcLandingImg) - .hasAttribute('src', '/ui/images/oidc-landing.png', 'image renders image when no clients configured'); - }); - - test('it creates an assignment inline, creates a client, updates client to limit access, deletes client', async function (assert) { - assert.expect(22); - - //* clear out test state - await clearRecord(this.store, 'oidc/client', 'test-app'); - await clearRecord(this.store, 'oidc/client', 'my-webapp'); // created by oidc-provider-test - await clearRecord(this.store, 'oidc/assignment', 'assignment-inline'); - - // create a client with allow all access - await visit(OIDC_BASE_URL + '/clients/create'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.clients.create', - 'navigates to create form' - ); - await fillIn('[data-test-input="name"]', 'test-app'); - await click('[data-test-toggle-group="More options"]'); - // toggle ttls to false, testing it sets correct default duration - await click('[data-test-input="idTokenTtl"]'); - await click('[data-test-input="accessTokenTtl"]'); - await click(SELECTORS.clientSaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully created the application test-app.', - 'renders success flash upon client creation' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.clients.client.details', - 'navigates to client details view after save' - ); - // assert default values in details view are correct - assert.dom('[data-test-value-div="Assignment"]').hasText('allow_all', 'client allows all assignments'); - assert.dom('[data-test-value-div="Type"]').hasText('confidential', 'type defaults to confidential'); - assert - .dom('[data-test-value-div="Key"] a') - .hasText('default', 'client uses default key and renders a link'); - assert - .dom('[data-test-value-div="Client ID"] [data-test-copy-button]') - .exists('client ID exists and has copy button'); - assert - .dom('[data-test-value-div="Client Secret"] [data-test-copy-button]') - .exists('client secret exists and has copy button'); - assert - .dom('[data-test-value-div="ID Token TTL"]') - .hasText('1 day', 'ID token ttl toggled off sets default of 24h'); - assert - .dom('[data-test-value-div="Access Token TTL"]') - .hasText('1 day', 'access token ttl toggled off sets default of 24h'); - - // edit client - await click(SELECTORS.clientDetailsTab); - await click(SELECTORS.clientEditButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.clients.client.edit', - 'navigates to edit page from details' - ); - await fillIn('[data-test-input="redirectUris"] [data-test-string-list-input="0"]', 'some-url.com'); - - // limit access & create new assignment inline - await click('[data-test-oidc-radio="limited"]'); - await clickTrigger(); - await fillIn('.ember-power-select-search input', 'assignment-inline'); - await searchSelect.options.objectAt(0).click(); - await click('[data-test-search-select="entities"] .ember-basic-dropdown-trigger'); - await searchSelect.options.objectAt(0).click(); - await click('[data-test-search-select="groups"] .ember-basic-dropdown-trigger'); - await searchSelect.options.objectAt(0).click(); - await click(SELECTORS.assignmentSaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully created the assignment assignment-inline.', - 'renders success flash upon assignment creating' - ); - await click(SELECTORS.clientSaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully updated the application test-app.', - 'renders success flash upon client updating' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.clients.client.details', - 'navigates back to details on update' - ); - assert.dom('[data-test-value-div="Redirect URI"]').hasText('some-url.com', 'shows updated attribute'); - assert - .dom('[data-test-value-div="Assignment"]') - .hasText('assignment-inline', 'updated to limited assignment'); - - // edit back to allow_all - await click(SELECTORS.clientEditButton); - assert.dom(SELECTORS.clientSaveButton).hasText('Update', 'form button renders correct text'); - await click('[data-test-oidc-radio="allow-all"]'); - await click(SELECTORS.clientSaveButton); - assert - .dom('[data-test-value-div="Assignment"]') - .hasText('allow_all', 'client updated to allow all assignments'); - - // create another client - await visit(OIDC_BASE_URL + '/clients/create'); - await fillIn('[data-test-input="name"]', 'app-to-delete'); - await click(SELECTORS.clientSaveButton); - - // immediately delete client, test transition - await click(SELECTORS.clientDeleteButton); - await click(SELECTORS.confirmActionButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Application deleted successfully', - 'renders success flash upon deleting client' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.clients.index', - 'navigates back to list view after delete' - ); - // delete last client - await click('[data-test-oidc-client-linked-block]'); - assert.strictEqual(currentRouteName(), 'vault.cluster.access.oidc.clients.client.details'); - await click(SELECTORS.clientDeleteButton); - await click(SELECTORS.confirmActionButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.index', - 'redirects to call to action if only existing client is deleted' - ); - //* clean up test state - await clearRecord(this.store, 'oidc/assignment', 'assignment-inline'); - }); - - test('it creates, updates, and deletes an assignment', async function (assert) { - assert.expect(14); - await visit(OIDC_BASE_URL + '/assignments'); - - //* ensure clean test state - await clearRecord(this.store, 'oidc/assignment', 'test-assignment'); - - // create a new assignment - await click(SELECTORS.assignmentCreateButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.assignments.create', - 'navigates to create form' - ); - assert.dom('[data-test-oidc-assignment-title]').hasText('Create assignment', 'Form title renders'); - await fillIn('[data-test-input="name"]', 'test-assignment'); - await click('[data-test-component="search-select"]#entities .ember-basic-dropdown-trigger'); - await click('.ember-power-select-option'); - await click(SELECTORS.assignmentSaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully created the assignment test-assignment.', - 'renders success flash upon creating the assignment' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.assignments.assignment.details', - 'navigates to the assignments detail view after save' - ); - - // assert default values in assignment details view are correct - assert.dom('[data-test-value-div="Name"]').hasText('test-assignment'); - assert.dom('[data-test-value-div="Entities"]').hasText('test-entity', 'shows the entity name.'); - - // edit assignment - await click(SELECTORS.assignmentEditButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.assignments.assignment.edit', - 'navigates to the assignment edit page from details' - ); - assert.dom('[data-test-oidc-assignment-title]').hasText('Edit assignment', 'Form title renders'); - await click('[data-test-component="search-select"]#groups .ember-basic-dropdown-trigger'); - await click('.ember-power-select-option'); - assert.dom('[data-test-oidc-assignment-save]').hasText('Update'); - await click(SELECTORS.assignmentSaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully updated the assignment test-assignment.', - 'renders success flash upon updating the assignment' - ); - - assert.dom('[data-test-value-div="Entities"]').hasText('test-entity', 'it still shows the entity name.'); - assert.dom('[data-test-value-div="Groups"]').hasText('test-group', 'shows updated group name id.'); - - // delete the assignment - await click(SELECTORS.assignmentDeleteButton); - await click(SELECTORS.confirmActionButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Assignment deleted successfully', - 'renders success flash upon deleting assignment' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.assignments.index', - 'navigates back to assignment list view after delete' - ); - }); - - test('it navigates to and from an assignment from the list view', async function (assert) { - assert.expect(6); - this.server.get('/identity/oidc/assignment', () => - overrideMirageResponse(null, ASSIGNMENT_LIST_RESPONSE) - ); - this.server.get('/identity/oidc/assignment/test-assignment', () => - overrideMirageResponse(null, ASSIGNMENT_DATA_RESPONSE) - ); - await visit(OIDC_BASE_URL + '/assignments'); - assert - .dom('[data-test-oidc-assignment-linked-block="test-assignment"]') - .exists('displays linked block for test-assignment'); - - await click(SELECTORS.assignmentCreateButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.assignments.create', - 'assignments index toolbar navigates to create form' - ); - await click(SELECTORS.assignmentCancelButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.assignments.index', - 'create form navigates back to assignment index on cancel' - ); - - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-oidc-assignment-menu-link="edit"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.assignments.assignment.edit', - 'linked block popup menu navigates to edit' - ); - await click(SELECTORS.assignmentCancelButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.assignments.assignment.details', - 'edit form navigates back to assignment details on cancel' - ); - // navigate to details from index page - await visit('/vault/access/oidc/assignments'); - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-oidc-assignment-menu-link="details"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.assignments.assignment.details', - 'popup menu navigates to assignment details' - ); - }); - - test('it hides assignment delete and edit when no permission', async function (assert) { - assert.expect(5); - this.server.get('/identity/oidc/assignment', () => - overrideMirageResponse(null, ASSIGNMENT_LIST_RESPONSE) - ); - this.server.get('/identity/oidc/assignment/test-assignment', () => - overrideMirageResponse(null, ASSIGNMENT_DATA_RESPONSE) - ); - this.server.post('/sys/capabilities-self', () => - overrideCapabilities(OIDC_BASE_URL + '/assignment/test-assignment', ['read']) - ); - - await visit(OIDC_BASE_URL + '/assignments'); - await click('[data-test-oidc-assignment-linked-block="test-assignment"]'); - assert - .dom('[data-test-oidc-assignment-title]') - .hasText('test-assignment', 'renders assignment name as title'); - assert.dom(SELECTORS.assignmentDetailsTab).hasClass('active', 'details tab is active'); - assert.dom(SELECTORS.assignmentDeleteButton).doesNotExist('delete option is hidden'); - assert.dom(SELECTORS.assignmentEditButton).doesNotExist('edit button is hidden'); - assert.strictEqual( - findAll('[data-test-component="info-table-row"]').length, - 3, - 'renders all assignment info rows' - ); - }); -}); diff --git a/ui/tests/acceptance/oidc-config/clients-keys-test.js b/ui/tests/acceptance/oidc-config/clients-keys-test.js deleted file mode 100644 index 0905e64f2..000000000 --- a/ui/tests/acceptance/oidc-config/clients-keys-test.js +++ /dev/null @@ -1,318 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { visit, click, fillIn, findAll, currentRouteName } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import ENV from 'vault/config/environment'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import { create } from 'ember-cli-page-object'; -import { clickTrigger, selectChoose } from 'ember-power-select/test-support/helpers'; -import ss from 'vault/tests/pages/components/search-select'; -import fm from 'vault/tests/pages/components/flash-message'; -import { - OIDC_BASE_URL, // -> '/vault/access/oidc' - SELECTORS, - clearRecord, - overrideCapabilities, - overrideMirageResponse, - CLIENT_LIST_RESPONSE, - CLIENT_DATA_RESPONSE, -} from 'vault/tests/helpers/oidc-config'; -const searchSelect = create(ss); -const flashMessage = create(fm); - -// in congruency with backend verbiage 'applications' are referred to as 'clients' -// throughout the codebase and the term 'applications' only appears in the UI - -module('Acceptance | oidc-config clients and keys', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'oidcConfig'; - }); - - hooks.beforeEach(async function () { - this.store = await this.owner.lookup('service:store'); - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - test('it creates a key, signs a client and edits key access to only that client', async function (assert) { - assert.expect(21); - - //* start with clean test state - await clearRecord(this.store, 'oidc/client', 'client-with-test-key'); - await clearRecord(this.store, 'oidc/client', 'client-with-default-key'); - await clearRecord(this.store, 'oidc/key', 'test-key'); - - // create client with default key - await visit(OIDC_BASE_URL + '/clients/create'); - await fillIn('[data-test-input="name"]', 'client-with-default-key'); - await click(SELECTORS.clientSaveButton); - - // check reroutes from oidc index to clients index when client exists - await visit(OIDC_BASE_URL); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.clients.index', - 'redirects to clients index route when clients exist' - ); - assert.dom('[data-test-tab="clients"]').hasClass('active', 'clients tab is active'); - assert - .dom('[data-test-oidc-client-linked-block]') - .hasTextContaining('client-with-default-key', 'displays linked block for client'); - - // navigate to keys - await click('[data-test-tab="keys"]'); - assert.dom('[data-test-tab="keys"]').hasClass('active', 'keys tab is active'); - assert.strictEqual(currentRouteName(), 'vault.cluster.access.oidc.keys.index'); - assert - .dom('[data-test-oidc-key-linked-block="default"]') - .hasText('default', 'index page lists default key'); - - // navigate to default key details from pop-up menu - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-oidc-key-menu-link="details"]'); - assert.dom(SELECTORS.keyDeleteButton).isDisabled('delete button is disabled for default key'); - await click(SELECTORS.keyEditButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.keys.key.edit', - 'navigates to edit from key details' - ); - await click(SELECTORS.keyCancelButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.keys.key.details', - 'key edit form navigates back to details on cancel' - ); - await click(SELECTORS.keyClientsTab); - assert - .dom('[data-test-oidc-client-linked-block="client-with-default-key"]') - .exists('lists correct app with default'); - - // create a new key - await click('[data-test-breadcrumb-link="oidc-keys"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.keys.index', - 'keys breadcrumb navigates back to list view' - ); - await click('[data-test-oidc-key-create]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.keys.create', - 'navigates to key create form' - ); - await fillIn('[data-test-input="name"]', 'test-key'); - await click(SELECTORS.keySaveButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.keys.key.details', - 'navigates to key details after save' - ); - - // create client with test-key - await visit(OIDC_BASE_URL + '/clients'); - await click('[data-test-oidc-client-create]'); - await fillIn('[data-test-input="name"]', 'client-with-test-key'); - await click('[data-test-toggle-group="More options"]'); - await click('[data-test-component="search-select"] [data-test-icon="trash"]'); - await clickTrigger('#key'); - await selectChoose('[data-test-component="search-select"]#key', 'test-key'); - await click(SELECTORS.clientSaveButton); - - // edit key and limit applications - await visit(OIDC_BASE_URL + '/keys'); - await click('[data-test-oidc-key-linked-block="test-key"] [data-test-popup-menu-trigger]'); - await click('[data-test-oidc-key-menu-link="edit"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.keys.key.edit', - 'key linked block popup menu navigates to edit' - ); - await click('[data-test-oidc-radio="limited"]'); - await clickTrigger(); - assert.strictEqual(searchSelect.options.length, 1, 'dropdown has only application that uses this key'); - assert - .dom('.ember-power-select-option') - .hasTextContaining('client-with-test-key', 'dropdown renders correct application'); - await searchSelect.options.objectAt(0).click(); - await click(SELECTORS.keySaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully updated the key test-key.', - 'renders success flash upon key updating' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.keys.key.details', - 'navigates back to details on update' - ); - await click(SELECTORS.keyClientsTab); - assert - .dom('[data-test-oidc-client-linked-block="client-with-test-key"]') - .exists('lists client-with-test-key'); - assert.strictEqual(findAll('[data-test-oidc-client-linked-block]').length, 1, 'it lists only one client'); - - // edit back to allow all - await click(SELECTORS.keyDetailsTab); - await click(SELECTORS.keyEditButton); - await click('[data-test-oidc-radio="allow-all"]'); - await click(SELECTORS.keySaveButton); - await click(SELECTORS.keyClientsTab); - assert.notEqual( - findAll('[data-test-oidc-client-linked-block]').length, - 1, - 'more than one client appears in key applications tab' - ); - - //* clean up test state - await clearRecord(this.store, 'oidc/client', 'client-with-test-key'); - await clearRecord(this.store, 'oidc/client', 'client-with-default-key'); - await clearRecord(this.store, 'oidc/key', 'test-key'); - }); - - test('it creates, rotates and deletes a key', async function (assert) { - assert.expect(10); - // mock client list so OIDC url does not redirect to landing page - this.server.get('/identity/oidc/client', () => overrideMirageResponse(null, CLIENT_LIST_RESPONSE)); - this.server.post('/identity/oidc/key/test-key/rotate', (schema, req) => { - const json = JSON.parse(req.requestBody); - assert.strictEqual(json.verification_ttl, 86400, 'request made with correct args to accurate endpoint'); - }); - - //* clear out test state - await clearRecord(this.store, 'oidc/key', 'test-key'); - - // create a new key - await visit(OIDC_BASE_URL + '/keys/create'); - await fillIn('[data-test-input="name"]', 'test-key'); - // toggle ttls to false, testing it sets correct default duration - await click('[data-test-input="rotationPeriod"]'); - await click('[data-test-input="verificationTtl"]'); - assert - .dom('[data-test-oidc-radio="limited"] input') - .isDisabled('limiting access radio button is disabled on create'); - assert - .dom('[data-test-oidc-radio="limited"]') - .hasClass('is-disabled', 'limited radio button label has disabled class'); - await click(SELECTORS.keySaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully created the key test-key.', - 'renders success flash upon key creation' - ); - - // assert default values in details view are correct - assert.dom('[data-test-value-div="Algorithm"]').hasText('RS256', 'defaults to RS526 algorithm'); - assert - .dom('[data-test-value-div="Rotation period"]') - .hasText('1 day', 'when toggled off rotation period defaults to 1 day'); - assert - .dom('[data-test-value-div="Verification TTL"]') - .hasText('1 day', 'when toggled off verification ttl defaults to 1 day'); - - // rotate key - await click(SELECTORS.keyDetailsTab); - await click(SELECTORS.keyRotateButton); - await click(SELECTORS.confirmActionButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Success: test-key connection was rotated.', - 'renders success flash upon key rotation' - ); - // delete - await click(SELECTORS.keyDeleteButton); - await click(SELECTORS.confirmActionButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Key deleted successfully', - 'success flash message renders after deleting key' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.keys.index', - 'navigates back to list view after delete' - ); - }); - - test('it renders client details and providers', async function (assert) { - assert.expect(8); - this.server.get('/identity/oidc/client', () => overrideMirageResponse(null, CLIENT_LIST_RESPONSE)); - this.server.get('/identity/oidc/client/test-app', () => - overrideMirageResponse(null, CLIENT_DATA_RESPONSE) - ); - await visit(OIDC_BASE_URL); - await click('[data-test-oidc-client-linked-block]'); - assert.dom('[data-test-oidc-client-header]').hasText('test-app', 'renders application name as title'); - assert.dom(SELECTORS.clientDetailsTab).hasClass('active', 'details tab is active'); - assert.dom(SELECTORS.clientDeleteButton).exists('toolbar renders delete option'); - assert.dom(SELECTORS.clientEditButton).exists('toolbar renders edit button'); - assert.strictEqual(findAll('[data-test-component="info-table-row"]').length, 9, 'renders all info rows'); - - await click(SELECTORS.clientProvidersTab); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.clients.client.providers', - 'navigates to client providers route' - ); - assert.dom(SELECTORS.clientProvidersTab).hasClass('active', 'providers tab is active'); - assert.dom('[data-test-oidc-provider-linked-block="default"]').exists('lists default provider'); - }); - - test('it hides delete and edit client when no permission', async function (assert) { - assert.expect(5); - this.server.get('/identity/oidc/client', () => overrideMirageResponse(null, CLIENT_LIST_RESPONSE)); - this.server.get('/identity/oidc/client/test-app', () => - overrideMirageResponse(null, CLIENT_DATA_RESPONSE) - ); - this.server.post('/sys/capabilities-self', () => - overrideCapabilities(OIDC_BASE_URL + '/client/test-app', ['read']) - ); - - await visit(OIDC_BASE_URL); - await click('[data-test-oidc-client-linked-block]'); - assert.dom('[data-test-oidc-client-header]').hasText('test-app', 'renders application name as title'); - assert.dom(SELECTORS.clientDetailsTab).hasClass('active', 'details tab is active'); - assert.dom(SELECTORS.clientDeleteButton).doesNotExist('delete option is hidden'); - assert.dom(SELECTORS.clientEditButton).doesNotExist('edit button is hidden'); - assert.strictEqual(findAll('[data-test-component="info-table-row"]').length, 9, 'renders all info rows'); - }); - - test('it hides delete and edit key when no permission', async function (assert) { - assert.expect(4); - this.server.get('/identity/oidc/keys', () => overrideMirageResponse(null, { keys: ['test-key'] })); - this.server.get('/identity/oidc/key/test-key', () => - overrideMirageResponse(null, { - algorithm: 'RS256', - allowed_client_ids: ['*'], - rotation_period: 86400, - verification_ttl: 86400, - }) - ); - this.server.post('/sys/capabilities-self', () => - overrideCapabilities(OIDC_BASE_URL + '/key/test-key', ['read']) - ); - - await visit(OIDC_BASE_URL + '/keys'); - await click('[data-test-oidc-key-linked-block]'); - assert.dom(SELECTORS.keyDetailsTab).hasClass('active', 'details tab is active'); - assert.dom(SELECTORS.keyDeleteButton).doesNotExist('delete option is hidden'); - assert.dom(SELECTORS.keyEditButton).doesNotExist('edit button is hidden'); - assert.strictEqual(findAll('[data-test-component="info-table-row"]').length, 4, 'renders all info rows'); - }); -}); diff --git a/ui/tests/acceptance/oidc-config/providers-scopes-test.js b/ui/tests/acceptance/oidc-config/providers-scopes-test.js deleted file mode 100644 index bd00c237c..000000000 --- a/ui/tests/acceptance/oidc-config/providers-scopes-test.js +++ /dev/null @@ -1,410 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { visit, currentURL, click, fillIn, findAll, currentRouteName } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import ENV from 'vault/config/environment'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import { create } from 'ember-cli-page-object'; -import { clickTrigger, selectChoose } from 'ember-power-select/test-support/helpers'; -import ss from 'vault/tests/pages/components/search-select'; -import fm from 'vault/tests/pages/components/flash-message'; -import { - OIDC_BASE_URL, - SELECTORS, - CLIENT_LIST_RESPONSE, - SCOPE_LIST_RESPONSE, - SCOPE_DATA_RESPONSE, - PROVIDER_LIST_RESPONSE, - PROVIDER_DATA_RESPONSE, - clearRecord, - overrideCapabilities, - overrideMirageResponse, -} from 'vault/tests/helpers/oidc-config'; -const searchSelect = create(ss); -const flashMessage = create(fm); - -// OIDC_BASE_URL = '/vault/access/oidc' - -module('Acceptance | oidc-config providers and scopes', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'oidcConfig'; - }); - - hooks.beforeEach(async function () { - this.store = await this.owner.lookup('service:store'); - // mock client list so OIDC BASE URL does not redirect to landing call-to-action image - this.server.get('/identity/oidc/client', () => overrideMirageResponse(null, CLIENT_LIST_RESPONSE)); - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - // LIST SCOPES EMPTY - test('it navigates to scopes list view and renders empty state when no scopes are configured', async function (assert) { - assert.expect(4); - this.server.get('/identity/oidc/scope', () => overrideMirageResponse(404)); - await visit(OIDC_BASE_URL); - await click('[data-test-tab="scopes"]'); - assert.strictEqual(currentURL(), '/vault/access/oidc/scopes'); - assert.dom('[data-test-tab="scopes"]').hasClass('active', 'scopes tab is active'); - assert - .dom(SELECTORS.scopeEmptyState) - .hasText( - `No scopes yet Use scope to define identity information about the authenticated user. Learn more. Create scope`, - 'renders empty state no scopes are configured' - ); - assert - .dom(SELECTORS.scopeCreateButtonEmptyState) - .hasAttribute('href', '/ui/vault/access/oidc/scopes/create', 'empty state renders create scope link'); - }); - - // LIST SCOPE EXIST - test('it renders scope list when scopes exist', async function (assert) { - assert.expect(11); - this.server.get('/identity/oidc/scope', () => overrideMirageResponse(null, SCOPE_LIST_RESPONSE)); - this.server.get('/identity/oidc/scope/test-scope', () => - overrideMirageResponse(null, SCOPE_DATA_RESPONSE) - ); - await visit(OIDC_BASE_URL + '/scopes'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.index', - 'redirects to scopes index route when scopes exist' - ); - assert - .dom('[data-test-oidc-scope-linked-block="test-scope"]') - .exists('displays linked block for test scope'); - - // navigates to/from create, edit, detail views from list view - await click(SELECTORS.scopeCreateButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.create', - 'scope index toolbar navigates to create form' - ); - await click(SELECTORS.scopeCancelButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.index', - 'create form navigates back to index on cancel' - ); - - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-oidc-scope-menu-link="edit"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.scope.edit', - 'linked block popup menu navigates to edit' - ); - await click(SELECTORS.scopeCancelButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.scope.details', - 'scope edit form navigates back to details on cancel' - ); - - // navigate to details from index page - await click('[data-test-breadcrumb-link="oidc-scopes"]'); - await click('[data-test-popup-menu-trigger]'); - await click('[data-test-oidc-scope-menu-link="details"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.scope.details', - 'popup menu navigates to details' - ); - // check that details tab has all the information - assert.dom(SELECTORS.scopeDetailsTab).hasClass('active', 'details tab is active'); - assert.dom(SELECTORS.scopeDeleteButton).exists('toolbar renders delete option'); - assert.dom(SELECTORS.scopeEditButton).exists('toolbar renders edit button'); - assert.strictEqual(findAll('[data-test-component="info-table-row"]').length, 2, 'renders all info rows'); - }); - - // ERROR DELETING SCOPE - test('it throws error when trying to delete when scope is currently being associated with any provider', async function (assert) { - assert.expect(3); - this.server.get('/identity/oidc/scope', () => overrideMirageResponse(null, SCOPE_LIST_RESPONSE)); - this.server.get('/identity/oidc/scope/test-scope', () => - overrideMirageResponse(null, SCOPE_DATA_RESPONSE) - ); - this.server.get('/identity/oidc/provider', () => overrideMirageResponse(null, PROVIDER_LIST_RESPONSE)); - this.server.get('/identity/oidc/provider/test-provider', () => { - overrideMirageResponse(null, PROVIDER_DATA_RESPONSE); - }); - // throw error when trying to delete test-scope since it is associated to test-provider - this.server.delete( - '/identity/oidc/scope/test-scope', - () => ({ - errors: [ - 'unable to delete scope "test-scope" because it is currently referenced by these providers: test-provider', - ], - }), - 400 - ); - await visit(OIDC_BASE_URL + '/scopes'); - await click('[data-test-oidc-scope-linked-block="test-scope"]'); - assert.dom('[data-test-oidc-scope-header]').hasText('test-scope', 'renders scope name'); - assert.dom(SELECTORS.scopeDetailsTab).hasClass('active', 'details tab is active'); - - // try to delete scope - await click(SELECTORS.scopeDeleteButton); - await click(SELECTORS.confirmActionButton); - assert.strictEqual( - flashMessage.latestMessage, - 'unable to delete scope "test-scope" because it is currently referenced by these providers: test-provider', - 'renders error flash upon scope deletion' - ); - }); - - // CRUD SCOPE + CRUD PROVIDER - test('it creates a scope, and creates a provider with that scope', async function (assert) { - assert.expect(28); - - //* clear out test state - await clearRecord(this.store, 'oidc/scope', 'test-scope'); - await clearRecord(this.store, 'oidc/provider', 'test-provider'); - - // create a new scope - await visit(OIDC_BASE_URL + '/scopes/create'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.create', - 'navigates to create form' - ); - await fillIn('[data-test-input="name"]', 'test-scope'); - await fillIn('[data-test-input="description"]', 'this is a test'); - await fillIn('[data-test-component="code-mirror-modifier"] textarea', SCOPE_DATA_RESPONSE.template); - await click(SELECTORS.scopeSaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully created the scope test-scope.', - 'renders success flash upon scope creation' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.scope.details', - 'navigates to scope detail view after save' - ); - assert.dom(SELECTORS.scopeDetailsTab).hasClass('active', 'scope details tab is active'); - assert.dom('[data-test-value-div="Name"]').hasText('test-scope', 'has correct created name'); - assert - .dom('[data-test-value-div="Description"]') - .hasText('this is a test', 'has correct created description'); - - // edit scope - await click(SELECTORS.scopeEditButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.scope.edit', - 'navigates to edit page from details' - ); - await fillIn('[data-test-input="description"]', 'this is an edit test'); - await click(SELECTORS.scopeSaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully updated the scope test-scope.', - 'renders success flash upon scope updating' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.scope.details', - 'navigates back to scope details on update' - ); - assert - .dom('[data-test-value-div="Description"]') - .hasText('this is an edit test', 'has correct edited description'); - - // create a provider using test-scope - await click('[data-test-breadcrumb-link="oidc-scopes"]'); - await click('[data-test-tab="providers"]'); - assert.dom('[data-test-tab="providers"]').hasClass('active', 'providers tab is active'); - await click('[data-test-oidc-provider-create]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.providers.create', - 'navigates to provider create form' - ); - await fillIn('[data-test-input="name"]', 'test-provider'); - await clickTrigger('#scopesSupported'); - await selectChoose('#scopesSupported', 'test-scope'); - await click(SELECTORS.providerSaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully created the OIDC provider test-provider.', - 'renders success flash upon provider creation' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.providers.provider.details', - 'navigates to provider detail view after save' - ); - - // assert default values in details view are correct - assert.dom('[data-test-value-div="Issuer URL"]').hasTextContaining('http://', 'issuer includes scheme'); - assert - .dom('[data-test-value-div="Issuer URL"]') - .hasTextContaining('identity/oidc/provider/test', 'issuer path populates correctly'); - assert - .dom('[data-test-value-div="Scopes"] a') - .hasAttribute('href', '/ui/vault/access/oidc/scopes/test-scope/details', 'lists scopes as links'); - - // check provider's application list view - await click(SELECTORS.providerClientsTab); - assert.strictEqual( - findAll('[data-test-oidc-client-linked-block]').length, - 2, - 'all applications appear in provider applications tab' - ); - - // edit and limit applications - await click(SELECTORS.providerDetailsTab); - await click(SELECTORS.providerEditButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.providers.provider.edit', - 'navigates to provider edit page from details' - ); - await click('[data-test-oidc-radio="limited"]'); - await click('[data-test-component="search-select"]#allowedClientIds .ember-basic-dropdown-trigger'); - await fillIn('.ember-power-select-search input', 'test-app'); - await searchSelect.options.objectAt(0).click(); - await click(SELECTORS.providerSaveButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Successfully updated the OIDC provider test-provider.', - 'renders success flash upon provider updating' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.providers.provider.details', - 'navigates back to provider details after updating' - ); - const providerModel = this.store.peekRecord('oidc/provider', 'test-provider'); - assert.propEqual( - providerModel.allowedClientIds, - ['whaT7KB0C3iBH1l3rXhd5HPf0n6vXU0s'], - 'provider saves client_id (not id or name) in allowed_client_ids param' - ); - await click(SELECTORS.providerClientsTab); - assert - .dom('[data-test-oidc-client-linked-block]') - .hasTextContaining('test-app', 'list of applications is just test-app'); - - // edit back to allow all - await click(SELECTORS.providerDetailsTab); - await click(SELECTORS.providerEditButton); - await click('[data-test-oidc-radio="allow-all"]'); - await click(SELECTORS.providerSaveButton); - await click(SELECTORS.providerClientsTab); - assert.strictEqual( - findAll('[data-test-oidc-client-linked-block]').length, - 2, - 'all applications appear in provider applications tab' - ); - - // delete - await click(SELECTORS.providerDetailsTab); - await click(SELECTORS.providerDeleteButton); - await click(SELECTORS.confirmActionButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Provider deleted successfully', - 'success flash message renders after deleting provider' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.providers.index', - 'navigates back to list view after delete' - ); - - // delete scope - await visit(OIDC_BASE_URL + '/scopes/test-scope/details'); - await click(SELECTORS.scopeDeleteButton); - await click(SELECTORS.confirmActionButton); - assert.strictEqual( - flashMessage.latestMessage, - 'Scope deleted successfully', - 'renders success flash upon deleting scope' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.scopes.index', - 'navigates back to list view after delete' - ); - }); - - // LIST PROVIDERS - test('it lists default provider and navigates to details', async function (assert) { - assert.expect(7); - await visit(OIDC_BASE_URL); - await click('[data-test-tab="providers"]'); - assert.dom('[data-test-tab="providers"]').hasClass('active', 'providers tab is active'); - assert.strictEqual(currentURL(), '/vault/access/oidc/providers'); - assert - .dom('[data-test-oidc-provider-linked-block="default"]') - .exists('index page lists default provider'); - await click('[data-test-popup-menu-trigger]'); - - await click('[data-test-oidc-provider-menu-link="edit"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.providers.provider.edit', - 'provider linked block popup menu navigates to edit' - ); - await click(SELECTORS.providerCancelButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.providers.provider.details', - 'provider edit form navigates back to details on cancel' - ); - - // navigate to details from index page - await click('[data-test-breadcrumb-link="oidc-providers"]'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.oidc.providers.index', - 'providers breadcrumb navigates back to list view' - ); - await click('[data-test-oidc-provider-linked-block="default"] [data-test-popup-menu-trigger]'); - await click('[data-test-oidc-provider-menu-link="details"]'); - assert.dom(SELECTORS.providerDeleteButton).isDisabled('delete button is disabled for default provider'); - }); - - // PROVIDER DELETE + EDIT PERMISSIONS - test('it hides delete and edit for a provider when no permission', async function (assert) { - assert.expect(3); - this.server.get('/identity/oidc/providers', () => - overrideMirageResponse(null, { providers: ['test-provider'] }) - ); - this.server.get('/identity/oidc/provider/test-provider', () => - overrideMirageResponse(null, { - allowed_client_ids: ['*'], - issuer: 'http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider', - scopes_supported: ['test-scope'], - }) - ); - this.server.post('/sys/capabilities-self', () => - overrideCapabilities(OIDC_BASE_URL + '/provider/test-provider', ['read']) - ); - - await visit(OIDC_BASE_URL + '/providers'); - await click('[data-test-oidc-provider-linked-block]'); - assert.dom(SELECTORS.providerDetailsTab).hasClass('active', 'details tab is active'); - assert.dom(SELECTORS.providerDeleteButton).doesNotExist('delete option is hidden'); - assert.dom(SELECTORS.providerEditButton).doesNotExist('edit button is hidden'); - }); -}); diff --git a/ui/tests/acceptance/oidc-provider-test.js b/ui/tests/acceptance/oidc-provider-test.js deleted file mode 100644 index 724282bab..000000000 --- a/ui/tests/acceptance/oidc-provider-test.js +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { create } from 'ember-cli-page-object'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import authForm from 'vault/tests/pages/components/auth-form'; -import enablePage from 'vault/tests/pages/settings/auth/enable'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import { visit, settled, currentURL } from '@ember/test-helpers'; -import { clearRecord } from 'vault/tests/helpers/oidc-config'; -const consoleComponent = create(consoleClass); -const authFormComponent = create(authForm); - -const OIDC_USER = 'end-user'; -const USER_PASSWORD = 'mypassword'; -const OIDC_POLICY = `path "identity/oidc/provider/+/userinfo" { - capabilities = ["read", "update"] -}`; -const USER_TOKEN_TEMPLATE = `{ - "username": {{identity.entity.aliases.$USERPASS_ACCESSOR.name}}, - "contact": { - "email": {{identity.entity.metadata.email}}, - "phone_number": {{identity.entity.metadata.phone_number}} - } -}`; -const GROUP_TOKEN_TEMPLATE = `{ - "groups": {{identity.entity.groups.names}} -}`; -const oidcEntity = async function (name, policy) { - await consoleComponent.runCommands([ - `write sys/policies/acl/${name} policy=${window.btoa(policy)}`, - `write identity/entity name="${OIDC_USER}" policies="${name}" metadata="email=vault@hashicorp.com" metadata="phone_number=123-456-7890"`, - `read -field=id identity/entity/name/${OIDC_USER}`, - ]); - return consoleComponent.lastLogOutput; -}; - -const oidcGroup = async function (entityId) { - await consoleComponent.runCommands([ - `write identity/group name="engineering" member_entity_ids="${entityId}"`, - `read -field=id identity/group/name/engineering`, - ]); - return consoleComponent.lastLogOutput; -}; - -const authAccessor = async function (path = 'userpass') { - await enablePage.enable('userpass', path); - await consoleComponent.runCommands([ - `write auth/${path}/users/end-user password="${USER_PASSWORD}"`, - `read -field=accessor sys/internal/ui/mounts/auth/${path}`, - ]); - return consoleComponent.lastLogOutput; -}; - -const entityAlias = async function (entityId, accessor, groupId) { - const userTokenTemplate = btoa(USER_TOKEN_TEMPLATE); - const groupTokenTemplate = btoa(GROUP_TOKEN_TEMPLATE); - - await consoleComponent.runCommands([ - `write identity/entity-alias name="end-user" canonical_id="${entityId}" mount_accessor="${accessor}"`, - `write identity/oidc/key/sigkey allowed_client_ids="*"`, - `write identity/oidc/assignment/my-assignment group_ids="${groupId}" entity_ids="${entityId}"`, - `write identity/oidc/scope/user description="scope for user metadata" template="${userTokenTemplate}"`, - `write identity/oidc/scope/groups description="scope for groups" template="${groupTokenTemplate}"`, - ]); - return consoleComponent.lastLogOutput.includes('Success'); -}; -const setupWebapp = async function (redirect) { - const webappName = 'my-webapp'; - await consoleComponent.runCommands([ - `write identity/oidc/client/${webappName} redirect_uris="${redirect}" assignments="my-assignment" key="sigkey" id_token_ttl="30m" access_token_ttl="1h"`, - `read -field=client_id identity/oidc/client/${webappName}`, - ]); - const output = consoleComponent.lastLogOutput; - if (output.includes('error occurred')) { - throw new Error(`OIDC setup failed: ${output}`); - } - return output; -}; -const setupProvider = async function (clientId) { - const providerName = `my-provider`; - await consoleComponent.runCommands( - `write identity/oidc/provider/${providerName} allowed_client_ids="${clientId}" scopes="user,groups"` - ); - return providerName; -}; - -const getAuthzUrl = (providerName, redirect, clientId, params) => { - const queryParams = { - client_id: clientId, - nonce: 'abc123', - redirect_uri: encodeURIComponent(redirect), - response_type: 'code', - scope: 'openid', - state: 'foobar', - ...params, - }; - const queryString = Object.keys(queryParams).reduce((prev, key, idx) => { - if (idx === 0) { - return `${prev}${key}=${queryParams[key]}`; - } - return `${prev}&${key}=${queryParams[key]}`; - }, '?'); - return `/vault/identity/oidc/provider/${providerName}/authorize${queryString}`; -}; - -const setupOidc = async function (uid) { - const callback = 'http://127.0.0.1:8251/callback'; - const entityId = await oidcEntity('oidc', OIDC_POLICY); - const groupId = await oidcGroup(entityId); - const authMethodPath = `oidc-userpass-${uid}`; - const accessor = await authAccessor(authMethodPath); - await entityAlias(entityId, accessor, groupId); - const clientId = await setupWebapp(callback); - const providerName = await setupProvider(clientId); - return { - providerName, - callback, - clientId, - authMethodPath, - }; -}; - -module('Acceptance | oidc provider', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.uid = uuidv4(); - this.store = await this.owner.lookup('service:store'); - await logout.visit(); - return authPage.login(); - }); - - test('OIDC Provider logs in and redirects correctly', async function (assert) { - const { providerName, callback, clientId, authMethodPath } = await setupOidc(this.uid); - - await logout.visit(); - await settled(); - const url = getAuthzUrl(providerName, callback, clientId); - await visit(url); - - assert.ok(currentURL().startsWith('/vault/auth'), 'redirects to auth when no token'); - assert.ok( - currentURL().includes(`redirect_to=${encodeURIComponent(url)}`), - 'encodes url for the query param' - ); - assert.dom('[data-test-auth-logo]').exists('Vault logo exists on auth page'); - assert - .dom('[data-test-auth-helptext]') - .hasText( - 'Once you log in, you will be redirected back to your application. If you require login credentials, contact your administrator.', - 'Has updated text for client authorization flow' - ); - await authFormComponent.selectMethod(authMethodPath); - await authFormComponent.username(OIDC_USER); - await authFormComponent.password(USER_PASSWORD); - await authFormComponent.login(); - await settled(); - assert.strictEqual(currentURL(), url, 'URL is as expected after login'); - assert - .dom('[data-test-oidc-redirect]') - .hasTextContaining(`click here to go back to app`, 'Shows link back to app'); - const link = document.querySelector('[data-test-oidc-redirect]').getAttribute('href'); - assert.ok(link.includes('/callback?code='), 'Redirects to correct url'); - - //* clean up test state - await clearRecord(this.store, 'oidc/client', 'my-webapp'); - await clearRecord(this.store, 'oidc/provider', 'my-provider'); - }); - - test('OIDC Provider redirects to auth if current token and prompt = login', async function (assert) { - const { providerName, callback, clientId, authMethodPath } = await setupOidc(this.uid); - await settled(); - const url = getAuthzUrl(providerName, callback, clientId, { prompt: 'login' }); - await visit(url); - - assert.ok(currentURL().startsWith('/vault/auth'), 'redirects to auth when no token'); - assert.notOk( - currentURL().includes('prompt=login'), - 'Url params no longer include prompt=login after redirect' - ); - await authFormComponent.selectMethod(authMethodPath); - await authFormComponent.username(OIDC_USER); - await authFormComponent.password(USER_PASSWORD); - await authFormComponent.login(); - await settled(); - assert - .dom('[data-test-oidc-redirect]') - .hasTextContaining(`click here to go back to app`, 'Shows link back to app'); - const link = document.querySelector('[data-test-oidc-redirect]').getAttribute('href'); - assert.ok(link.includes('/callback?code='), 'Redirects to correct url'); - - //* clean up test state - await clearRecord(this.store, 'oidc/client', 'my-webapp'); - await clearRecord(this.store, 'oidc/provider', 'my-provider'); - }); - - test('OIDC Provider shows consent form when prompt = consent', async function (assert) { - const { providerName, callback, clientId, authMethodPath } = await setupOidc(this.uid); - const url = getAuthzUrl(providerName, callback, clientId, { prompt: 'consent' }); - await logout.visit(); - await authFormComponent.selectMethod(authMethodPath); - await authFormComponent.username(OIDC_USER); - await authFormComponent.password(USER_PASSWORD); - await authFormComponent.login(); - await visit(url); - - assert.notOk( - currentURL().startsWith('/vault/auth'), - 'Does not redirect to auth because user is already logged in' - ); - assert.dom('[data-test-consent-form]').exists('Consent form exists'); - - //* clean up test state - await clearRecord(this.store, 'oidc/client', 'my-webapp'); - await clearRecord(this.store, 'oidc/provider', 'my-provider'); - }); -}); diff --git a/ui/tests/acceptance/pki/pki-action-forms-test.js b/ui/tests/acceptance/pki/pki-action-forms-test.js deleted file mode 100644 index e51379ca2..000000000 --- a/ui/tests/acceptance/pki/pki-action-forms-test.js +++ /dev/null @@ -1,298 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, currentURL, fillIn, typeIn, visit } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands'; -import { SELECTORS as S } from 'vault/tests/helpers/pki/workflow'; -import { issuerPemBundle } from 'vault/tests/helpers/pki/values'; - -module('Acceptance | pki action forms test', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - await authPage.login(); - // Setup PKI engine - const mountPath = `pki-workflow-${uuidv4()}`; - await enablePage.enable('pki', mountPath); - this.mountPath = mountPath; - await logout.visit(); - }); - - hooks.afterEach(async function () { - await logout.visit(); - await authPage.login(); - // Cleanup engine - await runCommands([`delete sys/mounts/${this.mountPath}`]); - await logout.visit(); - }); - - module('import', function (hooks) { - setupMirage(hooks); - - hooks.beforeEach(function () { - this.pemBundle = issuerPemBundle; - }); - - test('happy path', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - await click(S.emptyStateLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/create`); - assert.dom(S.configuration.title).hasText('Configure PKI'); - assert.dom(S.configuration.emptyState).exists({ count: 1 }, 'Shows empty state by default'); - await click(S.configuration.optionByKey('import')); - assert.dom(S.configuration.emptyState).doesNotExist(); - // Submit before filling out form shows an error - await click('[data-test-pki-import-pem-bundle]'); - assert.dom('[data-test-alert-banner="alert"]').hasText('Error please upload your PEM bundle'); - // Fill in form data - await click('[data-test-text-toggle]'); - await fillIn('[data-test-text-file-textarea]', this.pemBundle); - await click('[data-test-pki-import-pem-bundle]'); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/configuration/create`, - 'stays on page on success' - ); - assert.dom(S.configuration.title).hasText('View imported items'); - assert.dom(S.configuration.importForm).doesNotExist('import form is hidden after save'); - assert.dom(S.configuration.importMapping).exists('import mapping is shown after save'); - await click('[data-test-done]'); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/overview`, - 'redirects to overview when done' - ); - }); - test('with many imports', async function (assert) { - this.server.post(`${this.mountPath}/config/ca`, () => { - return { - request_id: 'some-config-id', - data: { - imported_issuers: ['my-imported-issuer', 'imported2'], - imported_keys: ['my-imported-key', 'imported3'], - mapping: { - 'my-imported-issuer': 'my-imported-key', - imported2: '', - }, - }, - }; - }); - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration/create`); - await click(S.configuration.optionByKey('import')); - assert.dom(S.configuration.importForm).exists('import form is shown save'); - await click('[data-test-text-toggle]'); - await fillIn('[data-test-text-file-textarea]', this.pemBundle); - await click('[data-test-pki-import-pem-bundle]'); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/configuration/create`, - 'stays on page on success' - ); - assert.dom(S.configuration.title).hasText('View imported items'); - assert.dom(S.configuration.importForm).doesNotExist('import form is hidden after save'); - assert.dom(S.configuration.importMapping).exists('import mapping is shown after save'); - assert.dom(S.configuration.importedIssuer).hasText('my-imported-issuer', 'Issuer value is displayed'); - assert.dom(S.configuration.importedKey).hasText('my-imported-key', 'Key value is displayed'); - await click('[data-test-done]'); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/overview`, - 'redirects to overview when done' - ); - }); - test('shows imported items when keys is empty', async function (assert) { - this.server.post(`${this.mountPath}/config/ca`, () => { - return { - request_id: 'some-config-id', - data: { - imported_issuers: ['my-imported-issuer', 'my-imported-issuer2'], - imported_keys: null, - mapping: { - 'my-imported-issuer': '', - 'my-imported-issuer2': '', - }, - }, - }; - }); - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration/create`); - await click(S.configuration.optionByKey('import')); - assert.dom(S.configuration.importForm).exists('import form is shown save'); - await click('[data-test-text-toggle]'); - await fillIn('[data-test-text-file-textarea]', this.pemBundle); - await click('[data-test-pki-import-pem-bundle]'); - - assert.dom(S.configuration.importForm).doesNotExist('import form is hidden after save'); - assert.dom(S.configuration.importMapping).exists('import mapping is shown after save'); - assert.dom(S.configuration.importedIssuer).hasText('my-imported-issuer', 'Issuer value is displayed'); - assert.dom(S.configuration.importedKey).hasText('None', 'Shows placeholder value for key'); - }); - test('shows None for imported items if nothing new imported', async function (assert) { - this.server.post(`${this.mountPath}/config/ca`, () => { - return { - request_id: 'some-config-id', - data: { - imported_issuers: null, - imported_keys: null, - mapping: {}, - }, - }; - }); - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration/create`); - await click(S.configuration.optionByKey('import')); - assert.dom(S.configuration.importForm).exists('import form is shown save'); - await click('[data-test-text-toggle]'); - await fillIn('[data-test-text-file-textarea]', this.pemBundle); - await click('[data-test-pki-import-pem-bundle]'); - - assert.dom(S.configuration.importForm).doesNotExist('import form is hidden after save'); - assert.dom(S.configuration.importMapping).exists('import mapping is shown after save'); - assert.dom(S.configuration.importedIssuer).hasText('None', 'Shows placeholder value for issuer'); - assert.dom(S.configuration.importedKey).hasText('None', 'Shows placeholder value for key'); - }); - }); - - module('generate root', function () { - test('happy path', async function (assert) { - const commonName = 'my-common-name'; - const issuerName = 'my-first-issuer'; - const keyName = 'my-first-key'; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - await click(S.emptyStateLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/create`); - assert.dom(S.configuration.title).hasText('Configure PKI'); - assert.dom(S.configuration.emptyState).exists({ count: 1 }, 'Shows empty state by default'); - await click(S.configuration.optionByKey('generate-root')); - assert.dom(S.configuration.emptyState).doesNotExist(); - // The URLs section is populated based on params returned from OpenAPI. This test will break when - // the backend adds fields. We should update the count accordingly. - assert.dom(S.configuration.urlField).exists({ count: 4 }); - // Fill in form - await fillIn(S.configuration.typeField, 'internal'); - await typeIn(S.configuration.inputByName('commonName'), commonName); - await typeIn(S.configuration.inputByName('issuerName'), issuerName); - await click(S.configuration.keyParamsGroupToggle); - await typeIn(S.configuration.inputByName('keyName'), keyName); - await click(S.configuration.generateRootSave); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/configuration/create`, - 'stays on page on success' - ); - assert.dom(S.configuration.title).hasText('View root certificate'); - assert.dom('[data-test-alert-banner="alert"]').doesNotExist('no private key warning'); - assert.dom(S.configuration.title).hasText('View root certificate', 'Updates title on page'); - assert.dom(S.configuration.saved.certificate).hasClass('allow-copy', 'copyable certificate is masked'); - assert.dom(S.configuration.saved.issuerName).hasText(issuerName); - assert.dom(S.configuration.saved.issuerLink).exists('Issuer link exists'); - assert.dom(S.configuration.saved.keyLink).exists('Key link exists'); - assert.dom(S.configuration.saved.keyName).hasText(keyName); - assert.dom('[data-test-done]').exists('Done button exists'); - // Check that linked issuer has correct common name - await click(S.configuration.saved.issuerLink); - assert.dom(S.issuerDetails.valueByName('Common name')).hasText(commonName); - }); - test('type=exported', async function (assert) { - const commonName = 'my-exported-name'; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration/create`); - await click(S.configuration.optionByKey('generate-root')); - // Fill in form - await fillIn(S.configuration.typeField, 'exported'); - await typeIn(S.configuration.inputByName('commonName'), commonName); - await click(S.configuration.generateRootSave); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/configuration/create`, - 'stays on page on success' - ); - assert.dom(S.configuration.title).hasText('View root certificate'); - assert - .dom('[data-test-alert-banner="alert"]') - .hasText('Next steps The private_key is only available once. Make sure you copy and save it now.'); - assert.dom(S.configuration.title).hasText('View root certificate', 'Updates title on page'); - assert - .dom(S.configuration.saved.certificate) - .hasClass('allow-copy', 'copyable masked certificate exists'); - assert - .dom(S.configuration.saved.issuerName) - .doesNotExist('Issuer name not shown because it was not named'); - assert.dom(S.configuration.saved.issuerLink).exists('Issuer link exists'); - assert.dom(S.configuration.saved.keyLink).exists('Key link exists'); - assert - .dom(S.configuration.saved.privateKey) - .hasClass('allow-copy', 'copyable masked private key exists'); - assert.dom(S.configuration.saved.keyName).doesNotExist('Key name not shown because it was not named'); - assert.dom('[data-test-done]').exists('Done button exists'); - // Check that linked issuer has correct common name - await click(S.configuration.saved.issuerLink); - assert.dom(S.issuerDetails.valueByName('Common name')).hasText(commonName); - }); - }); - - module('generate CSR', function () { - test('happy path', async function (assert) { - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(S.emptyStateLink); - assert.dom(S.configuration.title).hasText('Configure PKI'); - await click(S.configuration.optionByKey('generate-csr')); - await fillIn(S.configuration.typeField, 'internal'); - await fillIn(S.configuration.inputByName('commonName'), 'my-common-name'); - await click('[data-test-save]'); - assert.dom(S.configuration.title).hasText('View generated CSR'); - await assert.dom(S.configuration.csrDetails).exists('renders CSR details after save'); - await click('[data-test-done]'); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/overview`, - 'Transitions to overview after viewing csr details' - ); - }); - test('type = exported', async function (assert) { - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(S.emptyStateLink); - await click(S.configuration.optionByKey('generate-csr')); - await fillIn(S.configuration.typeField, 'exported'); - await fillIn(S.configuration.inputByName('commonName'), 'my-common-name'); - await click('[data-test-save]'); - await assert.dom(S.configuration.csrDetails).exists('renders CSR details after save'); - assert.dom(S.configuration.title).hasText('View generated CSR'); - assert - .dom('[data-test-alert-banner="alert"]') - .hasText( - 'Next steps Copy the CSR below for a parent issuer to sign and then import the signed certificate back into this mount. The private_key is only available once. Make sure you copy and save it now.' - ); - assert - .dom(S.configuration.saved.privateKey) - .hasClass('allow-copy', 'copyable masked private key exists'); - await click('[data-test-done]'); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/overview`, - 'Transitions to overview after viewing csr details' - ); - }); - }); -}); diff --git a/ui/tests/acceptance/pki/pki-configuration-test.js b/ui/tests/acceptance/pki/pki-configuration-test.js deleted file mode 100644 index dd77773f1..000000000 --- a/ui/tests/acceptance/pki/pki-configuration-test.js +++ /dev/null @@ -1,244 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { click, currentURL, fillIn, visit, isSettled, waitUntil, find } from '@ember/test-helpers'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands'; -import { SELECTORS } from 'vault/tests/helpers/pki/workflow'; -import { issuerPemBundle } from 'vault/tests/helpers/pki/values'; - -module('Acceptance | pki configuration test', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.pemBundle = issuerPemBundle; - await authPage.login(); - // Setup PKI engine - const mountPath = `pki-workflow-${uuidv4()}`; - await enablePage.enable('pki', mountPath); - this.mountPath = mountPath; - await logout.visit(); - }); - - hooks.afterEach(async function () { - await logout.visit(); - await authPage.login(); - // Cleanup engine - await runCommands([`delete sys/mounts/${this.mountPath}`]); - await logout.visit(); - }); - - module('delete all issuers modal and empty states', function (hooks) { - setupMirage(hooks); - - test('it shows the delete all issuers modal', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration`); - await click(SELECTORS.configuration.configureButton); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/create`); - await isSettled(); - await click(SELECTORS.configuration.generateRootOption); - await fillIn(SELECTORS.configuration.typeField, 'exported'); - await fillIn(SELECTORS.configuration.generateRootCommonNameField, 'issuer-common-0'); - await fillIn(SELECTORS.configuration.generateRootIssuerNameField, 'issuer-0'); - await click(SELECTORS.configuration.generateRootSave); - await click(SELECTORS.configuration.doneButton); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - await isSettled(); - await click(SELECTORS.configTab); - await isSettled(); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`); - await click(SELECTORS.configuration.issuerLink); - await isSettled(); - assert.dom(SELECTORS.configuration.deleteAllIssuerModal).exists(); - await fillIn(SELECTORS.configuration.deleteAllIssuerInput, 'delete-all'); - await click(SELECTORS.configuration.deleteAllIssuerButton); - assert.dom(SELECTORS.configuration.deleteAllIssuerModal).doesNotExist(); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`); - }); - - test('it shows the correct empty state message if certificates exists after delete all issuers', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration`); - await click(SELECTORS.configuration.configureButton); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/configuration/create`, - 'goes to pki configure page' - ); - await click(SELECTORS.configuration.generateRootOption); - await fillIn(SELECTORS.configuration.typeField, 'exported'); - await fillIn(SELECTORS.configuration.generateRootCommonNameField, 'issuer-common-0'); - await fillIn(SELECTORS.configuration.generateRootIssuerNameField, 'issuer-0'); - await click(SELECTORS.configuration.generateRootSave); - await click(SELECTORS.configuration.doneButton); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/overview`, - 'goes to overview page' - ); - await click(SELECTORS.configTab); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/configuration`, - 'goes to configuration page' - ); - await click(SELECTORS.configuration.issuerLink); - assert.dom(SELECTORS.configuration.deleteAllIssuerModal).exists(); - await fillIn(SELECTORS.configuration.deleteAllIssuerInput, 'delete-all'); - await click(SELECTORS.configuration.deleteAllIssuerButton); - await isSettled(); - assert - .dom(SELECTORS.configuration.deleteAllIssuerModal) - .doesNotExist('delete all issuers modal closes'); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/configuration`, - 'is still on configuration page' - ); - await isSettled(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await waitUntil(() => currentURL() === `/vault/secrets/${this.mountPath}/pki/overview`); - await isSettled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/overview`, - 'goes to overview page' - ); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText( - "This PKI mount hasn't yet been configured with a certificate issuer. There are existing certificates. Use the CLI to perform any operations with them until an issuer is configured." - ); - - await visit(`/vault/secrets/${this.mountPath}/pki/roles`); - await isSettled(); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText("This PKI mount hasn't yet been configured with a certificate issuer."); - - await visit(`/vault/secrets/${this.mountPath}/pki/issuers`); - await isSettled(); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText("This PKI mount hasn't yet been configured with a certificate issuer."); - - await visit(`/vault/secrets/${this.mountPath}/pki/keys`); - await isSettled(); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText("This PKI mount hasn't yet been configured with a certificate issuer."); - - await visit(`/vault/secrets/${this.mountPath}/pki/certificates`); - await isSettled(); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText( - "This PKI mount hasn't yet been configured with a certificate issuer. There are existing certificates. Use the CLI to perform any operations with them until an issuer is configured." - ); - }); - - test('it shows the correct empty state message if roles and certificates exists after delete all issuers', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration`); - await click(SELECTORS.configuration.configureButton); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/create`); - await click(SELECTORS.configuration.generateRootOption); - await fillIn(SELECTORS.configuration.typeField, 'exported'); - await fillIn(SELECTORS.configuration.generateRootCommonNameField, 'issuer-common-0'); - await fillIn(SELECTORS.configuration.generateRootIssuerNameField, 'issuer-0'); - await click(SELECTORS.configuration.generateRootSave); - await click(SELECTORS.configuration.doneButton); - await runCommands([ - `write ${this.mountPath}/roles/some-role \ - issuer_ref="default" \ - allowed_domains="example.com" \ - allow_subdomains=true \ - max_ttl="720h"`, - ]); - await runCommands([`write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test"`]); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.configTab); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`); - await click(SELECTORS.configuration.issuerLink); - assert.dom(SELECTORS.configuration.deleteAllIssuerModal).exists(); - await fillIn(SELECTORS.configuration.deleteAllIssuerInput, 'delete-all'); - await click(SELECTORS.configuration.deleteAllIssuerButton); - await isSettled(); - assert.dom(SELECTORS.configuration.deleteAllIssuerModal).doesNotExist(); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`); - await isSettled(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await waitUntil(() => currentURL() === `/vault/secrets/${this.mountPath}/pki/overview`); - await isSettled(); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText( - "This PKI mount hasn't yet been configured with a certificate issuer. There are existing roles and certificates. Use the CLI to perform any operations with them until an issuer is configured." - ); - - await visit(`/vault/secrets/${this.mountPath}/pki/roles`); - await isSettled(); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText( - "This PKI mount hasn't yet been configured with a certificate issuer. There are existing roles. Use the CLI to perform any operations with them until an issuer is configured." - ); - - await visit(`/vault/secrets/${this.mountPath}/pki/issuers`); - await isSettled(); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText("This PKI mount hasn't yet been configured with a certificate issuer."); - - await visit(`/vault/secrets/${this.mountPath}/pki/keys`); - await isSettled(); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText("This PKI mount hasn't yet been configured with a certificate issuer."); - - await visit(`/vault/secrets/${this.mountPath}/pki/certificates`); - await isSettled(); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText( - "This PKI mount hasn't yet been configured with a certificate issuer. There are existing certificates. Use the CLI to perform any operations with them until an issuer is configured." - ); - }); - - // test coverage for ed25519 certs not displaying because the verify() function errors - test('it generates and displays a root issuer of key type = ed25519', async function (assert) { - assert.expect(4); - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - await click(SELECTORS.generateIssuerDropdown); - await click(SELECTORS.generateIssuerRoot); - await fillIn(SELECTORS.configuration.inputByName('type'), 'internal'); - await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-certificate'); - await click(SELECTORS.configuration.keyParamsGroupToggle); - await fillIn(SELECTORS.configuration.inputByName('keyType'), 'ed25519'); - await click(SELECTORS.configuration.generateRootSave); - - const issuerId = find(SELECTORS.configuration.saved.issuerLink).innerHTML; - await visit(`/vault/secrets/${this.mountPath}/pki/issuers`); - assert.dom(SELECTORS.issuerListItem(issuerId)).exists(); - assert - .dom('[data-test-common-name="0"]') - .hasText('my-certificate', 'parses certificate metadata in the list view'); - await click(SELECTORS.issuerListItem(issuerId)); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/details`); - assert.dom(SELECTORS.configuration.saved.commonName).exists('renders issuer details'); - }); - }); -}); diff --git a/ui/tests/acceptance/pki/pki-cross-sign-test.js b/ui/tests/acceptance/pki/pki-cross-sign-test.js deleted file mode 100644 index c5b8de466..000000000 --- a/ui/tests/acceptance/pki/pki-cross-sign-test.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { visit, click, fillIn, find } from '@ember/test-helpers'; -import { setupApplicationTest } from 'vault/tests/helpers'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands'; -import { SELECTORS } from 'vault/tests/helpers/pki/pki-issuer-cross-sign'; -import { verifyCertificates } from 'vault/utils/parse-pki-cert'; -module('Acceptance | pki/pki cross sign', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - await authPage.login(); - this.parentMountPath = `parent-mount-${uuidv4()}`; - this.oldParentIssuerName = 'old-parent-issuer'; // old parent issuer we're transferring from - this.parentIssuerName = 'new-parent-issuer'; // issuer where cross-signing action will begin - this.intMountPath = `intermediate-mount-${uuidv4()}`; // first input box in cross-signing page - this.intIssuerName = 'my-intermediate-issuer'; // second input box in cross-signing page - this.newlySignedIssuer = 'my-newly-signed-int'; // third input - await enablePage.enable('pki', this.parentMountPath); - await enablePage.enable('pki', this.intMountPath); - - await runCommands([ - `write "${this.parentMountPath}/root/generate/internal" common_name="Long-Lived Root X1" ttl=8960h issuer_name="${this.oldParentIssuerName}"`, - `write "${this.parentMountPath}/root/generate/internal" common_name="Long-Lived Root X2" ttl=8960h issuer_name="${this.parentIssuerName}"`, - `write "${this.parentMountPath}/config/issuers" default="${this.parentIssuerName}"`, - ]); - }); - - hooks.afterEach(async function () { - // Cleanup engine - await runCommands([`delete sys/mounts/${this.intMountPath}`]); - await runCommands([`delete sys/mounts/${this.parentMountPath}`]); - await logout.visit(); - }); - - test('it cross-signs an issuer', async function (assert) { - // configure parent and intermediate mounts to make them cross-signable - await visit(`/vault/secrets/${this.intMountPath}/pki/configuration/create`); - await click(SELECTORS.configure.optionByKey('generate-csr')); - await fillIn(SELECTORS.inputByName('type'), 'internal'); - await fillIn(SELECTORS.inputByName('commonName'), 'Short-Lived Int R1'); - await click('[data-test-save]'); - const csr = find(SELECTORS.copyButton('CSR')).getAttribute('data-clipboard-text'); - await visit(`vault/secrets/${this.parentMountPath}/pki/issuers/${this.oldParentIssuerName}/sign`); - await fillIn(SELECTORS.inputByName('csr'), csr); - await fillIn(SELECTORS.inputByName('format'), 'pem_bundle'); - await click('[data-test-pki-sign-intermediate-save]'); - const pemBundle = find(SELECTORS.copyButton('CA Chain')) - .getAttribute('data-clipboard-text') - .replace(/,/, '\n'); - await visit(`vault/secrets/${this.intMountPath}/pki/configuration/create`); - await click(SELECTORS.configure.optionByKey('import')); - await click('[data-test-text-toggle]'); - await fillIn('[data-test-text-file-textarea]', pemBundle); - await click(SELECTORS.configure.importSubmit); - await visit(`vault/secrets/${this.intMountPath}/pki/issuers`); - await click('[data-test-is-default]'); - // name default issuer of intermediate - const oldIntIssuerId = find(SELECTORS.rowValue('Issuer ID')).innerText; - const oldIntCert = find(SELECTORS.copyButton('Certificate')).getAttribute('data-clipboard-text'); - await click(SELECTORS.details.configure); - await fillIn(SELECTORS.inputByName('issuerName'), this.intIssuerName); - await click('[data-test-save]'); - - // perform cross-sign - await visit(`vault/secrets/${this.parentMountPath}/pki/issuers/${this.parentIssuerName}/cross-sign`); - await fillIn(SELECTORS.objectListInput('intermediateMount'), this.intMountPath); - await fillIn(SELECTORS.objectListInput('intermediateIssuer'), this.intIssuerName); - await fillIn(SELECTORS.objectListInput('newCrossSignedIssuer'), this.newlySignedIssuer); - await click(SELECTORS.submitButton); - assert - .dom(`${SELECTORS.signedIssuerCol('intermediateMount')} a`) - .hasAttribute('href', `/ui/vault/secrets/${this.intMountPath}/pki/overview`); - assert - .dom(`${SELECTORS.signedIssuerCol('intermediateIssuer')} a`) - .hasAttribute('href', `/ui/vault/secrets/${this.intMountPath}/pki/issuers/${oldIntIssuerId}/details`); - - // get certificate data of newly signed issuer - await click(`${SELECTORS.signedIssuerCol('newCrossSignedIssuer')} a`); - const newIntCert = find(SELECTORS.copyButton('Certificate')).getAttribute('data-clipboard-text'); - - // verify cross-sign was accurate by creating a role to issue a leaf certificate - const myRole = 'some-role'; - await runCommands([ - `write ${this.intMountPath}/roles/${myRole} \ - issuer_ref=${this.newlySignedIssuer}\ - allow_any_name=true \ - max_ttl="720h"`, - ]); - await visit(`vault/secrets/${this.intMountPath}/pki/roles/${myRole}/generate`); - await fillIn(SELECTORS.inputByName('commonName'), 'my-leaf'); - await fillIn('[data-test-ttl-value="TTL"]', '3600'); - await click('[data-test-pki-generate-button]'); - const myLeafCert = find(SELECTORS.copyButton('Certificate')).getAttribute('data-clipboard-text'); - - // see comments in utils/parse-pki-cert.js for step-by-step explanation of of verifyCertificates method - assert.true( - await verifyCertificates(oldIntCert, newIntCert, myLeafCert), - 'the leaf certificate validates against both intermediate certificates' - ); - }); -}); diff --git a/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js b/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js deleted file mode 100644 index 488a6fb12..000000000 --- a/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js +++ /dev/null @@ -1,406 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { click, currentURL, fillIn, visit } from '@ember/test-helpers'; -import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands'; -import { SELECTORS } from 'vault/tests/helpers/pki/workflow'; - -/** - * This test module should test that dirty route models are cleaned up when the user leaves the page - */ -module('Acceptance | pki engine route cleanup test', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.store = this.owner.lookup('service:store'); - await authPage.login(); - // Setup PKI engine - const mountPath = `pki-workflow-${uuidv4()}`; - await enablePage.enable('pki', mountPath); - this.mountPath = mountPath; - await logout.visit(); - }); - - hooks.afterEach(async function () { - await logout.visit(); - await authPage.login(); - // Cleanup engine - await runCommands([`delete sys/mounts/${this.mountPath}`]); - await logout.visit(); - }); - - module('configuration', function () { - test('create config', async function (assert) { - let configs, urls, config; - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.emptyStateLink); - configs = this.store.peekAll('pki/action'); - urls = this.store.peekRecord('pki/config/urls', this.mountPath); - config = configs.objectAt(0); - assert.strictEqual(configs.length, 1, 'One config model present'); - assert.false(urls.hasDirtyAttributes, 'URLs is loaded from endpoint'); - assert.true(config.hasDirtyAttributes, 'Config model is dirty'); - - // Cancel button rolls it back - await click(SELECTORS.configuration.cancelButton); - configs = this.store.peekAll('pki/action'); - urls = this.store.peekRecord('pki/config/urls', this.mountPath); - assert.strictEqual(configs.length, 0, 'config model is rolled back on cancel'); - assert.strictEqual(urls.id, this.mountPath, 'Urls still exists on exit'); - - await click(SELECTORS.emptyStateLink); - configs = this.store.peekAll('pki/action'); - urls = this.store.peekRecord('pki/config/urls', this.mountPath); - config = configs.objectAt(0); - assert.strictEqual(configs.length, 1, 'One config model present'); - assert.false(urls.hasDirtyAttributes, 'URLs is loaded from endpoint'); - assert.true(config.hasDirtyAttributes, 'Config model is dirty'); - - // Exit page via link rolls it back - await click(SELECTORS.overviewBreadcrumb); - configs = this.store.peekAll('pki/action'); - urls = this.store.peekRecord('pki/config/urls', this.mountPath); - assert.strictEqual(configs.length, 0, 'config model is rolled back on cancel'); - assert.strictEqual(urls.id, this.mountPath, 'Urls still exists on exit'); - }); - }); - - module('role routes', function (hooks) { - hooks.beforeEach(async function () { - await authPage.login(); - // Configure PKI - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.emptyStateLink); - await click(SELECTORS.configuration.optionByKey('generate-root')); - await fillIn(SELECTORS.configuration.typeField, 'internal'); - await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-root-cert'); - await click(SELECTORS.configuration.generateRootSave); - await logout.visit(); - }); - - test('create role exit via cancel', async function (assert) { - let roles; - await authPage.login(); - // Create PKI - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.rolesTab); - roles = this.store.peekAll('pki/role'); - assert.strictEqual(roles.length, 0, 'No roles exist yet'); - await click(SELECTORS.createRoleLink); - roles = this.store.peekAll('pki/role'); - const role = roles.objectAt(0); - assert.strictEqual(roles.length, 1, 'New role exists'); - assert.true(role.isNew, 'Role is new model'); - await click(SELECTORS.roleForm.roleCancelButton); - roles = this.store.peekAll('pki/role'); - assert.strictEqual(roles.length, 0, 'Role is removed from store'); - }); - test('create role exit via breadcrumb', async function (assert) { - let roles; - await authPage.login(); - // Create PKI - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.rolesTab); - roles = this.store.peekAll('pki/role'); - assert.strictEqual(roles.length, 0, 'No roles exist yet'); - await click(SELECTORS.createRoleLink); - roles = this.store.peekAll('pki/role'); - const role = roles.objectAt(0); - assert.strictEqual(roles.length, 1, 'New role exists'); - assert.true(role.isNew, 'Role is new model'); - await click(SELECTORS.overviewBreadcrumb); - roles = this.store.peekAll('pki/role'); - assert.strictEqual(roles.length, 0, 'Role is removed from store'); - }); - test('edit role', async function (assert) { - let roles, role; - const roleId = 'workflow-edit-role'; - await authPage.login(); - // Create PKI - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.rolesTab); - roles = this.store.peekAll('pki/role'); - assert.strictEqual(roles.length, 0, 'No roles exist yet'); - await click(SELECTORS.createRoleLink); - await fillIn(SELECTORS.roleForm.roleName, roleId); - await click(SELECTORS.roleForm.roleCreateButton); - assert.dom('[data-test-value-div="Role name"]').hasText(roleId, 'Shows correct role after create'); - roles = this.store.peekAll('pki/role'); - role = roles.objectAt(0); - assert.strictEqual(roles.length, 1, 'Role is created'); - assert.false(role.hasDirtyAttributes, 'Role no longer has dirty attributes'); - - // Edit role - await click(SELECTORS.editRoleLink); - await click(SELECTORS.roleForm.issuerRefToggle); - await fillIn(SELECTORS.roleForm.issuerRefSelect, 'foobar'); - role = this.store.peekRecord('pki/role', roleId); - assert.true(role.hasDirtyAttributes, 'Role has dirty attrs'); - // Exit page via cancel button - await click(SELECTORS.roleForm.roleCancelButton); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/${roleId}/details`); - role = this.store.peekRecord('pki/role', roleId); - assert.false(role.hasDirtyAttributes, 'Role dirty attrs have been rolled back'); - - // Edit again - await click(SELECTORS.editRoleLink); - await click(SELECTORS.roleForm.issuerRefToggle); - await fillIn(SELECTORS.roleForm.issuerRefSelect, 'foobar2'); - role = this.store.peekRecord('pki/role', roleId); - assert.true(role.hasDirtyAttributes, 'Role has dirty attrs'); - // Exit page via breadcrumbs - await click(SELECTORS.overviewBreadcrumb); - role = this.store.peekRecord('pki/role', roleId); - assert.false(role.hasDirtyAttributes, 'Role dirty attrs have been rolled back'); - }); - }); - - module('issuer routes', function () { - test('import issuer exit via cancel', async function (assert) { - let issuers; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - issuers = this.store.peekAll('pki/issuer'); - assert.strictEqual(issuers.length, 0, 'No issuer models exist yet'); - await click(SELECTORS.importIssuerLink); - issuers = this.store.peekAll('pki/action'); - assert.strictEqual(issuers.length, 1, 'Action model created'); - const issuer = issuers.objectAt(0); - assert.true(issuer.hasDirtyAttributes, 'Action has dirty attrs'); - assert.true(issuer.isNew, 'Action is new'); - // Exit - await click('[data-test-pki-ca-cert-cancel]'); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`); - issuers = this.store.peekAll('pki/action'); - assert.strictEqual(issuers.length, 0, 'Action is removed from store'); - }); - test('import issuer exit via breadcrumb', async function (assert) { - let issuers; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - issuers = this.store.peekAll('pki/issuer'); - assert.strictEqual(issuers.length, 0, 'No issuers exist yet'); - await click(SELECTORS.importIssuerLink); - issuers = this.store.peekAll('pki/action'); - assert.strictEqual(issuers.length, 1, 'Action model created'); - const issuer = issuers.objectAt(0); - assert.true(issuer.hasDirtyAttributes, 'Action model has dirty attrs'); - assert.true(issuer.isNew, 'Action model is new'); - // Exit - await click(SELECTORS.overviewBreadcrumb); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - issuers = this.store.peekAll('pki/action'); - assert.strictEqual(issuers.length, 0, 'Issuer is removed from store'); - }); - test('generate root exit via cancel', async function (assert) { - let actions; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 0, 'No actions exist yet'); - await click(SELECTORS.generateIssuerDropdown); - await click(SELECTORS.generateIssuerRoot); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 1, 'Action model for generate-root created'); - const action = actions.objectAt(0); - assert.true(action.hasDirtyAttributes, 'Action has dirty attrs'); - assert.true(action.isNew, 'Action is new'); - assert.strictEqual(action.actionType, 'generate-root', 'Action type is correct'); - // Exit - await click(SELECTORS.configuration.generateRootCancel); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 0, 'Action is removed from store'); - }); - test('generate root exit via breadcrumb', async function (assert) { - let actions; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 0, 'No actions exist yet'); - await click(SELECTORS.generateIssuerDropdown); - await click(SELECTORS.generateIssuerRoot); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 1, 'Action model for generate-root created'); - const action = actions.objectAt(0); - assert.true(action.hasDirtyAttributes, 'Action has dirty attrs'); - assert.true(action.isNew, 'Action is new'); - assert.strictEqual(action.actionType, 'generate-root'); - // Exit - await click(SELECTORS.overviewBreadcrumb); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 0, 'Action is removed from store'); - }); - test('generate intermediate csr exit via cancel', async function (assert) { - let actions; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 0, 'No actions exist yet'); - await await click(SELECTORS.generateIssuerDropdown); - await click(SELECTORS.generateIssuerIntermediate); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 1, 'Action model for generate-csr created'); - const action = actions.objectAt(0); - assert.true(action.hasDirtyAttributes, 'Action has dirty attrs'); - assert.true(action.isNew, 'Action is new'); - assert.strictEqual(action.actionType, 'generate-csr'); - // Exit - await click('[data-test-cancel]'); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 0, 'Action is removed from store'); - }); - test('generate intermediate csr exit via breadcrumb', async function (assert) { - let actions; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 0, 'No actions exist yet'); - await click(SELECTORS.generateIssuerDropdown); - await click(SELECTORS.generateIssuerIntermediate); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 1, 'Action model for generate-csr created'); - const action = actions.objectAt(0); - assert.true(action.hasDirtyAttributes, 'Action has dirty attrs'); - assert.true(action.isNew, 'Action is new'); - assert.strictEqual(action.actionType, 'generate-csr'); - // Exit - await click(SELECTORS.overviewBreadcrumb); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - actions = this.store.peekAll('pki/action'); - assert.strictEqual(actions.length, 0, 'Action is removed from store'); - }); - test('edit issuer exit', async function (assert) { - let issuers, issuer; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.emptyStateLink); - await click(SELECTORS.configuration.optionByKey('generate-root')); - await fillIn(SELECTORS.configuration.typeField, 'internal'); - await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-root-cert'); - await click(SELECTORS.configuration.generateRootSave); - // Go to list view so we fetch all the issuers - await visit(`/vault/secrets/${this.mountPath}/pki/issuers`); - - issuers = this.store.peekAll('pki/issuer'); - const issuerId = issuers.objectAt(0).id; - assert.strictEqual(issuers.length, 1, 'Issuer exists on model in list'); - await visit(`/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/details`); - await click(SELECTORS.issuerDetails.configure); - issuer = this.store.peekRecord('pki/issuer', issuerId); - assert.false(issuer.hasDirtyAttributes, 'Model not dirty'); - await fillIn('[data-test-input="issuerName"]', 'foobar'); - assert.true(issuer.hasDirtyAttributes, 'Model is dirty'); - await click(SELECTORS.overviewBreadcrumb); - issuers = this.store.peekAll('pki/issuer'); - assert.strictEqual(issuers.length, 1, 'Issuer exists on model in overview'); - issuer = this.store.peekRecord('pki/issuer', issuerId); - assert.false(issuer.hasDirtyAttributes, 'Dirty attrs were rolled back'); - }); - }); - - module('key routes', function (hooks) { - hooks.beforeEach(async function () { - await authPage.login(); - // Configure PKI -- key creation not allowed unless configured - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.emptyStateLink); - await click(SELECTORS.configuration.optionByKey('generate-root')); - await fillIn(SELECTORS.configuration.typeField, 'internal'); - await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-root-cert'); - await click(SELECTORS.configuration.generateRootSave); - await logout.visit(); - }); - test('create key exit', async function (assert) { - let keys, key; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.keysTab); - keys = this.store.peekAll('pki/key'); - const configKeyId = keys.objectAt(0).id; - assert.strictEqual(keys.length, 1, 'One key exists from config'); - // Create key - await click(SELECTORS.keyPages.generateKey); - keys = this.store.peekAll('pki/key'); - key = keys.objectAt(1); - assert.strictEqual(keys.length, 2, 'New key exists'); - assert.true(key.isNew, 'Role is new model'); - // Exit - await click(SELECTORS.keyForm.keyCancelButton); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'Second key is removed from store'); - assert.strictEqual(keys.objectAt(0).id, configKeyId); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys`, 'url is correct'); - - // Create again - await click(SELECTORS.keyPages.generateKey); - assert.strictEqual(keys.length, 2, 'New key exists'); - keys = this.store.peekAll('pki/key'); - key = keys.objectAt(1); - assert.true(key.isNew, 'Key is new model'); - // Exit - await click(SELECTORS.overviewBreadcrumb); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`, 'url is correct'); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'Key is removed from store'); - }); - test('edit key exit', async function (assert) { - let keys, key; - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.keysTab); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'One key from config exists'); - assert.dom('.list-item-row').exists({ count: 1 }, 'single row for key'); - await click('.list-item-row'); - // Edit - await click(SELECTORS.keyPages.keyEditLink); - await fillIn(SELECTORS.keyForm.keyNameInput, 'foobar'); - keys = this.store.peekAll('pki/key'); - key = keys.objectAt(0); - assert.true(key.hasDirtyAttributes, 'Key model is dirty'); - // Exit - await click(SELECTORS.keyForm.keyCancelButton); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/keys/${key.id}/details`, - 'url is correct' - ); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'Key list has 1'); - assert.false(key.hasDirtyAttributes, 'Key dirty attrs have been rolled back'); - - // Edit again - await click(SELECTORS.keyPages.keyEditLink); - await fillIn(SELECTORS.keyForm.keyNameInput, 'foobar'); - keys = this.store.peekAll('pki/key'); - key = keys.objectAt(0); - assert.true(key.hasDirtyAttributes, 'Key model is dirty'); - - // Exit via breadcrumb - await click(SELECTORS.overviewBreadcrumb); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`, 'url is correct'); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'Key list has 1'); - assert.false(key.hasDirtyAttributes, 'Key dirty attrs have been rolled back'); - }); - }); -}); diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js deleted file mode 100644 index b1a198e83..000000000 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ /dev/null @@ -1,540 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { click, currentURL, fillIn, find, isSettled, visit } from '@ember/test-helpers'; -import { SELECTORS } from 'vault/tests/helpers/pki/workflow'; -import { adminPolicy, readerPolicy, updatePolicy } from 'vault/tests/helpers/policy-generator/pki'; -import { tokenWithPolicy, runCommands, clearRecords } from 'vault/tests/helpers/pki/pki-run-commands'; -import { unsupportedPem } from 'vault/tests/helpers/pki/values'; - -/** - * This test module should test the PKI workflow, including: - * - link between pages and confirm that the url is as expected - * - log in as user with a policy and ensure expected UI elements are shown/hidden - */ -module('Acceptance | pki workflow', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.store = this.owner.lookup('service:store'); - await authPage.login(); - // Setup PKI engine - const mountPath = `pki-workflow-${uuidv4()}`; - await enablePage.enable('pki', mountPath); - this.mountPath = mountPath; - await logout.visit(); - clearRecords(this.store); - }); - - hooks.afterEach(async function () { - await logout.visit(); - await authPage.login(); - // Cleanup engine - await runCommands([`delete sys/mounts/${this.mountPath}`]); - await logout.visit(); - }); - - module('not configured', function (hooks) { - hooks.beforeEach(async function () { - await authPage.login(); - const pki_admin_policy = adminPolicy(this.mountPath, 'roles'); - this.pkiAdminToken = await tokenWithPolicy(`pki-admin-${this.mountPath}`, pki_admin_policy); - await logout.visit(); - clearRecords(this.store); - }); - - test('empty state messages are correct when PKI not configured', async function (assert) { - assert.expect(21); - const assertEmptyState = (assert, resource) => { - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/${resource}`); - assert - .dom(SELECTORS.emptyStateTitle) - .hasText( - 'PKI not configured', - `${resource} index renders correct empty state title when PKI not configured` - ); - assert.dom(SELECTORS.emptyStateLink).hasText('Configure PKI'); - assert - .dom(SELECTORS.emptyStateMessage) - .hasText( - `This PKI mount hasn't yet been configured with a certificate issuer.`, - `${resource} index empty state message correct when PKI not configured` - ); - }; - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - - await click(SELECTORS.rolesTab); - assertEmptyState(assert, 'roles'); - - await click(SELECTORS.issuersTab); - assertEmptyState(assert, 'issuers'); - - await click(SELECTORS.certsTab); - assertEmptyState(assert, 'certificates'); - await click(SELECTORS.keysTab); - assertEmptyState(assert, 'keys'); - await click(SELECTORS.tidyTab); - assertEmptyState(assert, 'tidy'); - }); - }); - - module('roles', function (hooks) { - hooks.beforeEach(async function () { - await authPage.login(); - // Setup role-specific items - await runCommands([ - `write ${this.mountPath}/roles/some-role \ - issuer_ref="default" \ - allowed_domains="example.com" \ - allow_subdomains=true \ - max_ttl="720h"`, - ]); - await runCommands([`write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test"`]); - const pki_admin_policy = adminPolicy(this.mountPath, 'roles'); - const pki_reader_policy = readerPolicy(this.mountPath, 'roles'); - const pki_editor_policy = updatePolicy(this.mountPath, 'roles'); - this.pkiRoleReader = await tokenWithPolicy(`pki-reader-${this.mountPath}`, pki_reader_policy); - this.pkiRoleEditor = await tokenWithPolicy(`pki-editor-${this.mountPath}`, pki_editor_policy); - this.pkiAdminToken = await tokenWithPolicy(`pki-admin-${this.mountPath}`, pki_admin_policy); - await logout.visit(); - clearRecords(this.store); - }); - - test('shows correct items if user has all permissions', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.rolesTab).exists('Roles tab is present'); - await click(SELECTORS.rolesTab); - assert.dom(SELECTORS.createRoleLink).exists({ count: 1 }, 'Create role link is rendered'); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); - assert.dom('.linked-block').exists({ count: 1 }, 'One role is in list'); - await click('.linked-block'); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); - - assert.dom(SELECTORS.generateCertLink).exists('Generate cert link is shown'); - await click(SELECTORS.generateCertLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/generate`); - - // Go back to details and test all the links - await visit(`/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); - assert.dom(SELECTORS.signCertLink).exists('Sign cert link is shown'); - await click(SELECTORS.signCertLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/sign`); - - await visit(`/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); - assert.dom(SELECTORS.editRoleLink).exists('Edit link is shown'); - await click(SELECTORS.editRoleLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/edit`); - - await visit(`/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); - assert.dom(SELECTORS.deleteRoleButton).exists('Delete role button is shown'); - await click(`${SELECTORS.deleteRoleButton} [data-test-confirm-action-trigger]`); - await click(`[data-test-confirm-button]`); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/roles`, - 'redirects to roles list after deletion' - ); - }); - - test('it does not show toolbar items the user does not have permission to see', async function (assert) { - await authPage.login(this.pkiRoleReader); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.rolesTab).exists('Roles tab is present'); - await click(SELECTORS.rolesTab); - assert.dom(SELECTORS.createRoleLink).exists({ count: 1 }, 'Create role link is rendered'); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); - assert.dom('.linked-block').exists({ count: 1 }, 'One role is in list'); - await click('.linked-block'); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); - assert.dom(SELECTORS.deleteRoleButton).doesNotExist('Delete role button is not shown'); - assert.dom(SELECTORS.generateCertLink).doesNotExist('Generate cert link is not shown'); - assert.dom(SELECTORS.signCertLink).doesNotExist('Sign cert link is not shown'); - assert.dom(SELECTORS.editRoleLink).doesNotExist('Edit link is not shown'); - }); - - test('it shows correct toolbar items for the user policy', async function (assert) { - await authPage.login(this.pkiRoleEditor); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.rolesTab).exists('Roles tab is present'); - await click(SELECTORS.rolesTab); - assert.dom(SELECTORS.createRoleLink).exists({ count: 1 }, 'Create role link is rendered'); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); - assert.dom('.linked-block').exists({ count: 1 }, 'One role is in list'); - await click('.linked-block'); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); - assert.dom(SELECTORS.deleteRoleButton).doesNotExist('Delete role button is not shown'); - assert.dom(SELECTORS.generateCertLink).exists('Generate cert link is shown'); - assert.dom(SELECTORS.signCertLink).exists('Sign cert link is shown'); - assert.dom(SELECTORS.editRoleLink).exists('Edit link is shown'); - await click(SELECTORS.editRoleLink); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/roles/some-role/edit`, - 'Links to edit view' - ); - await click(SELECTORS.roleForm.roleCancelButton); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`, - 'Cancel from edit goes to details' - ); - await click(SELECTORS.generateCertLink); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/roles/some-role/generate`, - 'Generate cert button goes to generate page' - ); - await click(SELECTORS.generateCertForm.cancelButton); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`, - 'Cancel from generate goes to details' - ); - }); - - test('create role happy path', async function (assert) { - const roleName = 'another-role'; - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.rolesTab).exists('Roles tab is present'); - await click(SELECTORS.rolesTab); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); - await click(SELECTORS.createRoleLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/create`); - assert.dom(SELECTORS.breadcrumbContainer).exists({ count: 1 }, 'breadcrumbs are rendered'); - assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs'); - assert.dom(SELECTORS.pageTitle).hasText('Create a PKI role'); - - await fillIn(SELECTORS.roleForm.roleName, roleName); - await click(SELECTORS.roleForm.roleCreateButton); - - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/${roleName}/details`); - assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs'); - assert.dom(SELECTORS.pageTitle).hasText(`PKI Role ${roleName}`); - }); - }); - - module('keys', function (hooks) { - hooks.beforeEach(async function () { - await authPage.login(); - // base config pki so empty state doesn't show - await runCommands([`write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test"`]); - const pki_admin_policy = adminPolicy(this.mountPath); - const pki_reader_policy = readerPolicy(this.mountPath, 'keys', true); - const pki_editor_policy = updatePolicy(this.mountPath, 'keys'); - this.pkiKeyReader = await tokenWithPolicy(`pki-reader-${this.mountPath}`, pki_reader_policy); - this.pkiKeyEditor = await tokenWithPolicy(`pki-editor-${this.mountPath}`, pki_editor_policy); - this.pkiAdminToken = await tokenWithPolicy(`pki-admin-${this.mountPath}`, pki_admin_policy); - await logout.visit(); - clearRecords(this.store); - }); - - test('shows correct items if user has all permissions', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.keysTab); - // index page - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys`); - assert - .dom(SELECTORS.keyPages.importKey) - .hasAttribute( - 'href', - `/ui/vault/secrets/${this.mountPath}/pki/keys/import`, - 'import link renders with correct url' - ); - let keyId = find(SELECTORS.keyPages.keyId).innerText; - assert.dom('.linked-block').exists({ count: 1 }, 'One key is in list'); - await click('.linked-block'); - // details page - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`); - assert.dom(SELECTORS.keyPages.downloadButton).doesNotExist('does not download button for private key'); - - // edit page - await click(SELECTORS.keyPages.keyEditLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/edit`); - await click(SELECTORS.keyForm.keyCancelButton); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`, - 'navigates back to details on cancel' - ); - await visit(`/vault/secrets/${this.mountPath}/pki/keys/${keyId}/edit`); - await fillIn(SELECTORS.keyForm.keyNameInput, 'test-key'); - await click(SELECTORS.keyForm.keyCreateButton); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`, - 'navigates to details after save' - ); - assert.dom(SELECTORS.keyPages.keyNameValue).hasText('test-key', 'updates key name'); - - // key generate and delete navigation - await visit(`/vault/secrets/${this.mountPath}/pki/keys`); - await click(SELECTORS.keyPages.generateKey); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/create`); - await fillIn(SELECTORS.keyForm.typeInput, 'exported'); - await fillIn(SELECTORS.keyForm.keyTypeInput, 'rsa'); - await click(SELECTORS.keyForm.keyCreateButton); - keyId = find(SELECTORS.keyPages.keyIdValue).innerText; - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`); - - assert - .dom(SELECTORS.alertBanner) - .hasText( - 'Next steps This private key material will only be available once. Copy or download it now.', - 'renders banner to save private key' - ); - assert.dom(SELECTORS.keyPages.downloadButton).exists('renders download button'); - await click(SELECTORS.keyPages.keyDeleteButton); - await click(SELECTORS.keyPages.confirmDelete); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/keys`, - 'navigates back to key list view on delete' - ); - }); - - test('it hide corrects actions for user with read policy', async function (assert) { - await authPage.login(this.pkiKeyReader); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.keysTab); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys`); - await isSettled(); - assert.dom(SELECTORS.keyPages.importKey).doesNotExist(); - assert.dom(SELECTORS.keyPages.generateKey).doesNotExist(); - assert.dom('.linked-block').exists({ count: 1 }, 'One key is in list'); - const keyId = find(SELECTORS.keyPages.keyId).innerText; - await click(SELECTORS.keyPages.popupMenuTrigger); - assert.dom(SELECTORS.keyPages.popupMenuEdit).hasClass('disabled', 'popup menu edit link is disabled'); - await click(SELECTORS.keyPages.popupMenuDetails); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`); - assert.dom(SELECTORS.keyPages.keyDeleteButton).doesNotExist('Delete key button is not shown'); - assert.dom(SELECTORS.keyPages.keyEditLink).doesNotExist('Edit key button does not render'); - }); - - test('it shows correct toolbar items for the user with update policy', async function (assert) { - await authPage.login(this.pkiKeyEditor); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.keysTab); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys`); - await isSettled(); - assert.dom(SELECTORS.keyPages.importKey).exists('import action exists'); - assert.dom(SELECTORS.keyPages.generateKey).exists('generate action exists'); - assert.dom('.linked-block').exists({ count: 1 }, 'One key is in list'); - const keyId = find(SELECTORS.keyPages.keyId).innerText; - await click(SELECTORS.keyPages.popupMenuTrigger); - assert - .dom(SELECTORS.keyPages.popupMenuEdit) - .doesNotHaveClass('disabled', 'popup menu edit link is not disabled'); - await click('.linked-block'); - assert.dom(SELECTORS.keyPages.keyDeleteButton).doesNotExist('Delete key button is not shown'); - await click(SELECTORS.keyPages.keyEditLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/edit`); - assert.dom(SELECTORS.keyPages.title).hasText('Edit key'); - await click(SELECTORS.keyForm.keyCancelButton); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`); - }); - }); - - module('issuers', function (hooks) { - hooks.beforeEach(async function () { - await authPage.login(); - const pki_admin_policy = adminPolicy(this.mountPath); - this.pkiAdminToken = await tokenWithPolicy(`pki-admin-${this.mountPath}`, pki_admin_policy); - // Configure engine with a default issuer - await runCommands([ - `write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test" name="Hashicorp Test"`, - ]); - await logout.visit(); - clearRecords(this.store); - }); - test('lists the correct issuer metadata info', async function (assert) { - assert.expect(6); - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.issuersTab).exists('Issuers tab is present'); - await click(SELECTORS.issuersTab); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`); - assert.dom('.linked-block').exists({ count: 1 }, 'One issuer is in list'); - assert.dom('[data-test-is-root-tag="0"]').hasText('root'); - assert.dom('[data-test-serial-number="0"]').exists({ count: 1 }, 'displays serial number tag'); - assert.dom('[data-test-common-name="0"]').exists({ count: 1 }, 'displays cert common name tag'); - }); - test('lists the correct issuer metadata info when user has only read permission', async function (assert) { - assert.expect(2); - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - await click(SELECTORS.issuerPopupMenu); - await click(SELECTORS.issuerPopupDetails); - const issuerId = find(SELECTORS.issuerDetails.valueByName('Issuer ID')).innerText; - const pki_issuer_denied_policy = ` - path "${this.mountPath}/*" { - capabilities = ["create", "read", "update", "delete", "list"] - }, - path "${this.mountPath}/issuer/${issuerId}" { - capabilities = ["deny"] - } - `; - this.token = await tokenWithPolicy( - `pki-issuer-denied-policy-${this.mountPath}`, - pki_issuer_denied_policy - ); - await logout.visit(); - await authPage.login(this.token); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - assert.dom('[data-test-serial-number="0"]').exists({ count: 1 }, 'displays serial number tag'); - assert.dom('[data-test-common-name="0"]').doesNotExist('does not display cert common name tag'); - }); - - test('details view renders correct number of info items', async function (assert) { - assert.expect(13); - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.issuersTab).exists('Issuers tab is present'); - await click(SELECTORS.issuersTab); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`); - assert.dom('.linked-block').exists({ count: 1 }, 'One issuer is in list'); - await click('.linked-block'); - assert.ok( - currentURL().match(`/vault/secrets/${this.mountPath}/pki/issuers/.+/details`), - `/vault/secrets/${this.mountPath}/pki/issuers/my-issuer/details` - ); - assert.dom(SELECTORS.issuerDetails.title).hasText('View issuer certificate'); - ['Certificate', 'CA Chain', 'Common name', 'Issuer name', 'Issuer ID', 'Default key ID'].forEach( - (label) => { - assert - .dom(`${SELECTORS.issuerDetails.defaultGroup} ${SELECTORS.issuerDetails.valueByName(label)}`) - .exists({ count: 1 }, `${label} value rendered`); - } - ); - assert - .dom(`${SELECTORS.issuerDetails.urlsGroup} ${SELECTORS.issuerDetails.row}`) - .exists({ count: 3 }, 'Renders 3 info table items under URLs group'); - assert.dom(SELECTORS.issuerDetails.groupTitle).exists({ count: 1 }, 'only 1 group title rendered'); - }); - - test('toolbar links navigate to expected routes', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issuersTab); - await click(SELECTORS.issuerPopupMenu); - await click(SELECTORS.issuerPopupDetails); - - const issuerId = find(SELECTORS.issuerDetails.valueByName('Issuer ID')).innerText; - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/details`, - 'it navigates to details route' - ); - assert - .dom(SELECTORS.issuerDetails.crossSign) - .hasAttribute('href', `/ui/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/cross-sign`); - assert - .dom(SELECTORS.issuerDetails.signIntermediate) - .hasAttribute('href', `/ui/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/sign`); - assert - .dom(SELECTORS.issuerDetails.configure) - .hasAttribute('href', `/ui/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/edit`); - await click(SELECTORS.issuerDetails.rotateRoot); - assert.dom(find(SELECTORS.issuerDetails.rotateModal).parentElement).hasClass('is-active'); - await click(SELECTORS.issuerDetails.rotateModalGenerate); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/rotate-root`, - 'it navigates to root rotate form' - ); - assert - .dom('[data-test-input="commonName"]') - .hasValue('Hashicorp Test', 'form prefilled with parent issuer cn'); - }); - }); - - module('rotate', function (hooks) { - hooks.beforeEach(async function () { - await authPage.login(); - await runCommands([`write ${this.mountPath}/root/generate/internal issuer_name="existing-issuer"`]); - await logout.visit(); - }); - test('it renders a warning banner when parent issuer has unsupported OIDs', async function (assert) { - await authPage.login(); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration/create`); - await click(SELECTORS.configuration.optionByKey('import')); - await click('[data-test-text-toggle]'); - await fillIn('[data-test-text-file-textarea]', unsupportedPem); - await click(SELECTORS.configuration.importSubmit); - const issuerId = find(SELECTORS.configuration.importedIssuer).innerText; - await click(`${SELECTORS.configuration.importedIssuer} a`); - - // navigating directly to route because the rotate button is not visible for non-root issuers - // but we're just testing that route model was parsed and passed as expected - await visit(`/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/rotate-root`); - assert - .dom('[data-test-warning-banner]') - .hasTextContaining( - 'Not all of the certificate values could be parsed and transferred to new root', - 'it renders warning banner' - ); - assert.dom('[data-test-input="commonName"]').hasValue('fancy-cert-unsupported-subj-and-ext-oids'); - await fillIn('[data-test-input="issuerName"]', 'existing-issuer'); - await click('[data-test-pki-rotate-root-save]'); - assert - .dom('[data-test-error-banner]') - .hasText('Error issuer name already in use', 'it renders error banner'); - }); - }); - - module('config', function (hooks) { - hooks.beforeEach(async function () { - await authPage.login(); - await runCommands([`write ${this.mountPath}/root/generate/internal issuer_name="existing-issuer"`]); - const mixed_config_policy = ` - ${adminPolicy(this.mountPath)} - ${readerPolicy(this.mountPath, 'config/cluster')} - `; - this.mixedConfigCapabilities = await tokenWithPolicy( - `pki-reader-${this.mountPath}`, - mixed_config_policy - ); - await logout.visit(); - }); - - test('it updates config when user only has permission to some endpoints', async function (assert) { - await authPage.login(this.mixedConfigCapabilities); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration/edit`); - assert - .dom(`${SELECTORS.configEdit.configEditSection} [data-test-component="empty-state"]`) - .hasText( - `You do not have permission to set this mount's the cluster config Ask your administrator if you think you should have access to: POST /${this.mountPath}/config/cluster` - ); - assert.dom(SELECTORS.configEdit.acmeEditSection).exists(); - assert.dom(SELECTORS.configEdit.urlsEditSection).exists(); - assert.dom(SELECTORS.configEdit.crlEditSection).exists(); - assert.dom(`${SELECTORS.acmeEditSection} [data-test-component="empty-state"]`).doesNotExist(); - assert.dom(`${SELECTORS.urlsEditSection} [data-test-component="empty-state"]`).doesNotExist(); - assert.dom(`${SELECTORS.crlEditSection} [data-test-component="empty-state"]`).doesNotExist(); - await click(SELECTORS.configEdit.crlToggleInput('expiry')); - await click(SELECTORS.configEdit.saveButton); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`); - assert - .dom('[data-test-value-div="CRL building"]') - .hasText('Disabled', 'Successfully saves config with partial permission'); - }); - }); -}); diff --git a/ui/tests/acceptance/pki/pki-overview-test.js b/ui/tests/acceptance/pki/pki-overview-test.js deleted file mode 100644 index bdf8bba14..000000000 --- a/ui/tests/acceptance/pki/pki-overview-test.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { click, currentURL, currentRouteName, visit } from '@ember/test-helpers'; -import { SELECTORS } from 'vault/tests/helpers/pki/overview'; -import { tokenWithPolicy, runCommands, clearRecords } from 'vault/tests/helpers/pki/pki-run-commands'; - -module('Acceptance | pki overview', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.store = this.owner.lookup('service:store'); - await authPage.login(); - // Setup PKI engine - const mountPath = `pki`; - await enablePage.enable('pki', mountPath); - this.mountPath = mountPath; - await runCommands([`write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test"`]); - const pki_admin_policy = ` - path "${this.mountPath}/*" { - capabilities = ["create", "read", "update", "delete", "list"] - }, - `; - const pki_issuers_list_policy = ` - path "${this.mountPath}/issuers" { - capabilities = ["list"] - }, - `; - const pki_roles_list_policy = ` - path "${this.mountPath}/roles" { - capabilities = ["list"] - }, - `; - - this.pkiRolesList = await tokenWithPolicy('pki-roles-list', pki_roles_list_policy); - this.pkiIssuersList = await tokenWithPolicy('pki-issuers-list', pki_issuers_list_policy); - this.pkiAdminToken = await tokenWithPolicy('pki-admin', pki_admin_policy); - await logout.visit(); - clearRecords(this.store); - }); - - hooks.afterEach(async function () { - await logout.visit(); - await authPage.login(); - // Cleanup engine - await runCommands([`delete sys/mounts/${this.mountPath}`]); - await logout.visit(); - }); - - test('navigates to view issuers when link is clicked on issuer card', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.issuersCardTitle).hasText('Issuers'); - assert.dom(SELECTORS.issuersCardOverviewNum).hasText('1'); - await click(SELECTORS.issuersCardLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - }); - - test('navigates to view roles when link is clicked on roles card', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.rolesCardTitle).hasText('Roles'); - assert.dom(SELECTORS.rolesCardOverviewNum).hasText('0'); - await click(SELECTORS.rolesCardLink); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); - await runCommands([ - `write ${this.mountPath}/roles/some-role \ - issuer_ref="default" \ - allowed_domains="example.com" \ - allow_subdomains=true \ - max_ttl="720h"`, - ]); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.rolesCardOverviewNum).hasText('1'); - }); - - test('hides roles card if user does not have permissions', async function (assert) { - await authPage.login(this.pkiIssuersList); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(SELECTORS.rolesCardTitle).doesNotExist('Roles card does not exist'); - assert.dom(SELECTORS.issuersCardTitle).exists('Issuers card exists'); - }); - - test('navigates to generate certificate page for Issue Certificates card', async function (assert) { - await authPage.login(this.pkiAdminToken); - await runCommands([ - `write ${this.mountPath}/roles/some-role \ - issuer_ref="default" \ - allowed_domains="example.com" \ - allow_subdomains=true \ - max_ttl="720h"`, - ]); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.issueCertificatePowerSearch); - await click(SELECTORS.firstPowerSelectOption); - await click(SELECTORS.issueCertificateButton); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.roles.role.generate'); - }); - - test('navigates to certificate details page for View Certificates card', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.viewCertificatePowerSearch); - await click(SELECTORS.firstPowerSelectOption); - await click(SELECTORS.viewCertificateButton); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.pki.certificates.certificate.details' - ); - }); - - test('navigates to issuer details page for View Issuer card', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - await click(SELECTORS.viewIssuerPowerSearch); - await click(SELECTORS.firstPowerSelectOption); - await click(SELECTORS.viewIssuerButton); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.issuers.issuer.details'); - }); -}); diff --git a/ui/tests/acceptance/pki/pki-tidy-test.js b/ui/tests/acceptance/pki/pki-tidy-test.js deleted file mode 100644 index f3d46f9de..000000000 --- a/ui/tests/acceptance/pki/pki-tidy-test.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, currentRouteName, fillIn, visit } from '@ember/test-helpers'; - -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands'; -import { SELECTORS } from 'vault/tests/helpers/pki/page/pki-tidy'; - -module('Acceptance | pki tidy', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - await authPage.login(); - // Setup PKI engine - const mountPath = `pki-workflow-${uuidv4()}`; - await enablePage.enable('pki', mountPath); - this.mountPath = mountPath; - await runCommands([ - `write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test" name="Hashicorp Test"`, - ]); - await logout.visit(); - }); - - hooks.afterEach(async function () { - await logout.visit(); - await authPage.login(); - // Cleanup engine - await runCommands([`delete sys/mounts/${this.mountPath}`]); - await logout.visit(); - }); - - test('it configures a manual tidy operation and shows its details and tidy states', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/tidy`); - await click(SELECTORS.tidyEmptyStateConfigure); - assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists'); - assert.dom(SELECTORS.tidyConfigureModal.tidyModalAutoButton).exists('Configure auto tidy button exists'); - assert - .dom(SELECTORS.tidyConfigureModal.tidyModalManualButton) - .exists('Configure manual tidy button exists'); - await click(SELECTORS.tidyConfigureModal.tidyModalManualButton); - assert.dom(SELECTORS.tidyForm.tidyFormName('manual')).exists('Manual tidy form exists'); - await click(SELECTORS.tidyForm.inputByAttr('tidyCertStore')); - await fillIn(SELECTORS.tidyForm.tidyPauseDuration, '10'); - await click(SELECTORS.tidyForm.tidySave); - await click(SELECTORS.cancelTidyAction); - assert.dom(SELECTORS.cancelTidyModalBackground).exists('Confirm cancel tidy modal exits'); - await click(SELECTORS.tidyConfigureModal.tidyModalCancelButton); - // we can't properly test the background refresh fetching of tidy status in testing - this.server.get(`${this.mountPath}/tidy-status`, () => { - return { - request_id: 'dba2d42d-1a6e-1551-80f8-4ddb364ede4b', - lease_id: '', - renewable: false, - lease_duration: 0, - data: { - acme_account_deleted_count: 0, - acme_account_revoked_count: 0, - acme_account_safety_buffer: 2592000, - acme_orders_deleted_count: 0, - cert_store_deleted_count: 0, - cross_revoked_cert_deleted_count: 0, - current_cert_store_count: null, - current_revoked_cert_count: null, - error: null, - internal_backend_uuid: '964a41f7-a159-53aa-d62e-fc1914e4a7e1', - issuer_safety_buffer: 31536000, - last_auto_tidy_finished: '2023-05-19T10:27:11.721825-07:00', - message: 'Tidying certificate store: checking entry 0 of 1', - missing_issuer_cert_count: 0, - pause_duration: '1m40s', - revocation_queue_deleted_count: 0, - revocation_queue_safety_buffer: 36000, - revoked_cert_deleted_count: 0, - safety_buffer: 2073600, - state: 'Cancelled', - tidy_acme: false, - tidy_cert_store: true, - tidy_cross_cluster_revoked_certs: false, - tidy_expired_issuers: false, - tidy_move_legacy_ca_bundle: false, - tidy_revocation_queue: false, - tidy_revoked_cert_issuer_associations: false, - tidy_revoked_certs: false, - time_finished: '2023-05-19T10:28:51.733092-07:00', - time_started: '2023-05-19T10:27:11.721846-07:00', - total_acme_account_count: 0, - }, - wrap_info: null, - warnings: null, - auth: null, - }; - }); - await visit(`/vault/secrets/${this.mountPath}/pki/configuration`); - await visit(`/vault/secrets/${this.mountPath}/pki/tidy`); - assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy operation cancelled'); - assert - .dom(SELECTORS.hdsAlertDescription) - .hasText( - 'Your tidy operation has been cancelled. If this was a mistake configure and run another tidy operation.' - ); - assert.dom(SELECTORS.alertUpdatedAt).exists(); - }); - - test('it configures an auto tidy operation and shows its details', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/tidy`); - await click(SELECTORS.tidyEmptyStateConfigure); - assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists'); - assert.dom(SELECTORS.tidyConfigureModal.tidyModalAutoButton).exists('Configure auto tidy button exists'); - assert - .dom(SELECTORS.tidyConfigureModal.tidyModalManualButton) - .exists('Configure manual tidy button exists'); - await click(SELECTORS.tidyConfigureModal.tidyModalAutoButton); - assert.dom(SELECTORS.tidyForm.tidyFormName('auto')).exists('Auto tidy form exists'); - await click(SELECTORS.tidyForm.tidyCancel); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.tidy.index'); - await click(SELECTORS.tidyEmptyStateConfigure); - await click(SELECTORS.tidyConfigureModal.tidyModalAutoButton); - assert.dom(SELECTORS.tidyForm.tidyFormName('auto')).exists('Auto tidy form exists'); - await click(SELECTORS.tidyForm.toggleLabel('Automatic tidy disabled')); - assert - .dom(SELECTORS.tidyForm.tidySectionHeader('ACME operations')) - .exists('Auto tidy form enabled shows ACME operations field'); - await click(SELECTORS.tidyForm.inputByAttr('tidyCertStore')); - await click(SELECTORS.tidyForm.tidySave); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.tidy.auto.index'); - await click(SELECTORS.tidyForm.editAutoTidyButton); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.tidy.auto.configure'); - await click(SELECTORS.tidyForm.inputByAttr('tidyRevokedCerts')); - await click(SELECTORS.tidyForm.tidySave); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.tidy.auto.index'); - }); - - test('it opens a tidy modal when the user clicks on the tidy toolbar action', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/tidy`); - await click(SELECTORS.tidyConfigureModal.tidyOptionsModal); - assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists'); - assert.dom(SELECTORS.tidyConfigureModal.tidyModalAutoButton).exists('Configure auto tidy button exists'); - assert - .dom(SELECTORS.tidyConfigureModal.tidyModalManualButton) - .exists('Configure manual tidy button exists'); - await click(SELECTORS.tidyConfigureModal.tidyModalCancelButton); - assert.dom(SELECTORS.tidyEmptyState).exists(); - }); - - test('it should show correct toolbar action depending on whether auto tidy is enabled', async function (assert) { - await authPage.login(this.pkiAdminToken); - await visit(`/vault/secrets/${this.mountPath}/pki/tidy`); - assert - .dom(SELECTORS.tidyConfigureModal.tidyOptionsModal) - .exists('Configure tidy modal options button exists'); - await click(SELECTORS.tidyConfigureModal.tidyOptionsModal); - assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists'); - await click(SELECTORS.tidyConfigureModal.tidyOptionsModal); - await click(SELECTORS.tidyConfigureModal.tidyModalAutoButton); - await click(SELECTORS.tidyForm.toggleLabel('Automatic tidy disabled')); - await click(SELECTORS.tidyForm.inputByAttr('tidyCertStore')); - await click(SELECTORS.tidyForm.inputByAttr('tidyRevokedCerts')); - await click(SELECTORS.tidyForm.tidySave); - await visit(`/vault/secrets/${this.mountPath}/pki/tidy`); - assert - .dom(SELECTORS.manualTidyToolbar) - .exists('Manual tidy toolbar action exists if auto tidy is configured'); - assert - .dom(SELECTORS.autoTidyToolbar) - .exists('Auto tidy toolbar action exists if auto tidy is configured'); - }); -}); diff --git a/ui/tests/acceptance/policies-acl-old-test.js b/ui/tests/acceptance/policies-acl-old-test.js deleted file mode 100644 index 94c82fca9..000000000 --- a/ui/tests/acceptance/policies-acl-old-test.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, fillIn, find, currentURL, waitUntil } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import page from 'vault/tests/pages/policies/index'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | policies (old)', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('policies', async function (assert) { - const policyString = 'path "*" { capabilities = ["update"]}'; - const policyName = `Policy test ${this.uid}`; - const policyLower = policyName.toLowerCase(); - - await page.visit({ type: 'acl' }); - // new policy creation - await click('[data-test-policy-create-link]'); - - await fillIn('[data-test-policy-input="name"]', policyName); - await click('[data-test-policy-save]'); - assert - .dom('[data-test-error]') - .hasText(`Error 'policy' parameter not supplied or empty`, 'renders error message on save'); - find('.CodeMirror').CodeMirror.setValue(policyString); - await click('[data-test-policy-save]'); - - await waitUntil(() => currentURL() === `/vault/policy/acl/${encodeURIComponent(policyLower)}`); - assert.strictEqual( - currentURL(), - `/vault/policy/acl/${encodeURIComponent(policyLower)}`, - 'navigates to policy show on successful save' - ); - assert.dom('[data-test-policy-name]').hasText(policyLower, 'displays the policy name on the show page'); - assert.dom('[data-test-flash-message].is-info').doesNotExist('no flash message is displayed on save'); - await click('[data-test-policy-list-link]'); - await fillIn('[data-test-component="navigate-input"]', policyLower); - - assert - .dom(`[data-test-policy-link="${policyLower}"]`) - .exists({ count: 1 }, 'new policy shown in the list'); - - // policy deletion - await click(`[data-test-policy-link="${policyLower}"]`); - - await click('[data-test-policy-edit-toggle]'); - - await click('[data-test-policy-delete] button'); - - await click('[data-test-confirm-button]'); - await waitUntil(() => currentURL() === `/vault/policies/acl`); - assert.strictEqual( - currentURL(), - `/vault/policies/acl`, - 'navigates to policy list on successful deletion' - ); - await fillIn('[data-test-component="navigate-input"]', policyLower); - assert - .dom(`[data-test-policy-item="${policyLower}"]`) - .doesNotExist('deleted policy is not shown in the list'); - }); - - // https://github.com/hashicorp/vault/issues/4395 - test('it properly fetches policies when the name ends in a ,', async function (assert) { - const policyString = 'path "*" { capabilities = ["update"]}'; - const policyName = `${this.uid}-policy-symbol,.`; - - await page.visit({ type: 'acl' }); - // new policy creation - await click('[data-test-policy-create-link]'); - - await fillIn('[data-test-policy-input="name"]', policyName); - find('.CodeMirror').CodeMirror.setValue(policyString); - await click('[data-test-policy-save]'); - assert.ok( - await waitUntil(() => currentURL() === `/vault/policy/acl/${policyName}`), - 'navigates to policy show on successful save' - ); - assert.dom('[data-test-policy-edit-toggle]').exists({ count: 1 }, 'shows the edit toggle'); - }); -}); diff --git a/ui/tests/acceptance/policies-test.js b/ui/tests/acceptance/policies-test.js deleted file mode 100644 index 8f06cc81d..000000000 --- a/ui/tests/acceptance/policies-test.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentURL, currentRouteName, visit } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | policies', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it redirects to acls on unknown policy type', async function (assert) { - await visit('/vault/policy/foo/default'); - assert.strictEqual(currentRouteName(), 'vault.cluster.policies.index'); - assert.strictEqual(currentURL(), '/vault/policies/acl'); - - await visit('/vault/policy/foo/default/edit'); - assert.strictEqual(currentRouteName(), 'vault.cluster.policies.index'); - assert.strictEqual(currentURL(), '/vault/policies/acl'); - }); - - test('it redirects to acls on index navigation', async function (assert) { - await visit('/vault/policy/acl'); - assert.strictEqual(currentRouteName(), 'vault.cluster.policies.index'); - assert.strictEqual(currentURL(), '/vault/policies/acl'); - }); -}); diff --git a/ui/tests/acceptance/policies/index-test.js b/ui/tests/acceptance/policies/index-test.js deleted file mode 100644 index af8a3f756..000000000 --- a/ui/tests/acceptance/policies/index-test.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentURL, currentRouteName, settled, find, findAll, click } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; - -import page from 'vault/tests/pages/policies/index'; -import authPage from 'vault/tests/pages/auth'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const consoleComponent = create(consoleClass); - -module('Acceptance | policies/acl', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it lists default and root acls', async function (assert) { - await page.visit({ type: 'acl' }); - await settled(); - assert.strictEqual(currentURL(), '/vault/policies/acl'); - assert.ok(page.findPolicyByName('default'), 'default policy shown in the list'); - if (find('nav.pagination')) { - // Root ACL is always last in the list - const paginationLinks = findAll('.pagination-link'); - await click(paginationLinks[paginationLinks.length - 1]); - } - assert.ok(page.findPolicyByName('root'), 'root policy shown in the list'); - }); - - test('it navigates to show when clicking on the link', async function (assert) { - await page.visit({ type: 'acl' }); - await settled(); - await page.findPolicyByName('default').click(); - await settled(); - assert.strictEqual(currentRouteName(), 'vault.cluster.policy.show'); - assert.strictEqual(currentURL(), '/vault/policy/acl/default'); - }); - - test('it allows deletion of policies with dots in names', async function (assert) { - const POLICY = 'path "*" { capabilities = ["list"]}'; - const policyName = 'list.policy'; - await consoleComponent.runCommands([`write sys/policies/acl/${policyName} policy=${btoa(POLICY)}`]); - await settled(); - await page.visit({ type: 'acl' }); - await settled(); - const policy = page.row.filterBy('name', policyName)[0]; - assert.ok(policy, 'policy is shown in the list'); - await policy.menu(); - await settled(); - await page.delete().confirmDelete(); - await settled(); - assert.notOk(page.findPolicyByName(policyName), 'policy is deleted successfully'); - }); -}); diff --git a/ui/tests/acceptance/policy-test.js b/ui/tests/acceptance/policy-test.js deleted file mode 100644 index 7c21b2ca3..000000000 --- a/ui/tests/acceptance/policy-test.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentURL, currentRouteName, visit } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; - -module('Acceptance | policies', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - test('it redirects to acls with unknown policy type', async function (assert) { - await visit('/vault/policies/foo'); - assert.strictEqual(currentRouteName(), 'vault.cluster.policies.index'); - assert.strictEqual(currentURL(), '/vault/policies/acl'); - }); -}); diff --git a/ui/tests/acceptance/policy/edit-test.js b/ui/tests/acceptance/policy/edit-test.js deleted file mode 100644 index 88ae6188e..000000000 --- a/ui/tests/acceptance/policy/edit-test.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentURL } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import page from 'vault/tests/pages/policy/edit'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | policy/acl/:name/edit', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it redirects to list if navigating to root', async function (assert) { - await page.visit({ type: 'acl', name: 'root' }); - assert.strictEqual( - currentURL(), - '/vault/policies/acl', - 'navigation to root show redirects you to policy list' - ); - }); - - test('it does not show delete for default policy', async function (assert) { - await page.visit({ type: 'acl', name: 'default' }); - assert.notOk(page.deleteIsPresent, 'there is no delete button'); - }); - - test('it navigates to show when the toggle is clicked', async function (assert) { - await page.visit({ type: 'acl', name: 'default' }).toggleEdit(); - assert.strictEqual(currentURL(), '/vault/policy/acl/default', 'toggle navigates from edit to show'); - }); -}); diff --git a/ui/tests/acceptance/policy/show-test.js b/ui/tests/acceptance/policy/show-test.js deleted file mode 100644 index 724cb89b4..000000000 --- a/ui/tests/acceptance/policy/show-test.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentURL } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import page from 'vault/tests/pages/policy/show'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | policy/acl/:name', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it redirects to list if navigating to root', async function (assert) { - await page.visit({ type: 'acl', name: 'root' }); - assert.strictEqual( - currentURL(), - '/vault/policies/acl', - 'navigation to root show redirects you to policy list' - ); - }); - - test('it navigates to edit when the toggle is clicked', async function (assert) { - await page.visit({ type: 'acl', name: 'default' }).toggleEdit(); - assert.strictEqual(currentURL(), '/vault/policy/acl/default/edit', 'toggle navigates to edit page'); - }); -}); diff --git a/ui/tests/acceptance/raft-storage-test.js b/ui/tests/acceptance/raft-storage-test.js deleted file mode 100644 index 5bb4a4fea..000000000 --- a/ui/tests/acceptance/raft-storage-test.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { click, visit } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; - -module('Acceptance | raft storage', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - this.config = this.server.create('configuration', 'withRaft'); - this.server.get('/sys/internal/ui/resultant-acl', () => - this.server.create('configuration', { data: { root: true } }) - ); - this.server.get('/sys/license/features', () => ({})); - await authPage.login(); - }); - hooks.afterEach(function () { - return logout.visit(); - }); - - test('it should render correct number of raft peers', async function (assert) { - assert.expect(3); - - let didRemovePeer = false; - this.server.get('/sys/storage/raft/configuration', () => { - if (didRemovePeer) { - this.config.data.config.servers.pop(); - } else { - // consider peer removed by external means (cli) after initial request - didRemovePeer = true; - } - return this.config; - }); - - await visit('/vault/storage/raft'); - assert.dom('[data-raft-row]').exists({ count: 2 }, '2 raft peers render in table'); - // leave route and return to trigger config fetch - await visit('/vault/secrets'); - await visit('/vault/storage/raft'); - const store = this.owner.lookup('service:store'); - assert.strictEqual( - store.peekAll('server').length, - 2, - 'Store contains 2 server records since remove peer was triggered externally' - ); - assert.dom('[data-raft-row]').exists({ count: 1 }, 'Only raft nodes from response are rendered'); - }); - - test('it should remove raft peer', async function (assert) { - assert.expect(3); - - this.server.get('/sys/storage/raft/configuration', () => this.config); - this.server.post('/sys/storage/raft/remove-peer', (schema, req) => { - const body = JSON.parse(req.requestBody); - assert.strictEqual( - body.server_id, - this.config.data.config.servers[1].node_id, - 'Remove peer request made with node id' - ); - return {}; - }); - - await visit('/vault/storage/raft'); - assert.dom('[data-raft-row]').exists({ count: 2 }, '2 raft peers render in table'); - await click('[data-raft-row]:nth-child(2) [data-test-popup-menu-trigger]'); - await click('[data-test-confirm-action-trigger]'); - await click('[data-test-confirm-button]'); - assert.dom('[data-raft-row]').exists({ count: 1 }, 'Raft peer successfully removed'); - }); -}); diff --git a/ui/tests/acceptance/redirect-to-test.js b/ui/tests/acceptance/redirect-to-test.js deleted file mode 100644 index d0ca4a50a..000000000 --- a/ui/tests/acceptance/redirect-to-test.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentURL, visit as _visit, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; -import auth from 'vault/tests/pages/auth'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const visit = async (url) => { - try { - await _visit(url); - } catch (e) { - if (e.message !== 'TransitionAborted') { - throw e; - } - } - - await settled(); -}; - -const consoleComponent = create(consoleClass); - -const wrappedAuth = async () => { - await consoleComponent.runCommands(`write -field=token auth/token/create policies=default -wrap-ttl=5m`); - await settled(); - // because of flaky test, trying to capture the token using a dom selector instead of the page object - const token = document.querySelector('[data-test-component="console/log-text"] pre').textContent; - if (token.includes('Error')) { - throw new Error(`Error mounting secrets engine: ${token}`); - } - return token; -}; - -const setupWrapping = async () => { - await auth.logout(); - await settled(); - await auth.visit(); - await settled(); - await auth.tokenInput('root').submit(); - await settled(); - const wrappedToken = await wrappedAuth(); - return wrappedToken; -}; -module('Acceptance | redirect_to query param functionality', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - // normally we'd use the auth.logout helper to visit the route and reset the app, but in this case that - // also routes us to the auth page, and then all of the transitions from the auth page get redirected back - // to the auth page resulting in no redirect_to query param being set - localStorage.clear(); - }); - test('redirect to a route after authentication', async function (assert) { - const url = '/vault/secrets/secret/create'; - await visit(url); - assert.ok( - currentURL().includes(`redirect_to=${encodeURIComponent(url)}`), - 'encodes url for the query param' - ); - // the login method on this page does another visit call that we don't want here - await auth.tokenInput('root').submit(); - await settled(); - assert.strictEqual(currentURL(), url, 'navigates to the redirect_to url after auth'); - }); - - test('redirect from root does not include redirect_to', async function (assert) { - const url = '/'; - await visit(url); - assert.ok(currentURL().indexOf('redirect_to') < 0, 'there is no redirect_to query param'); - }); - - test('redirect to a route after authentication with a query param', async function (assert) { - const url = '/vault/secrets/secret/create?initialKey=hello'; - await visit(url); - assert.ok( - currentURL().includes(`?redirect_to=${encodeURIComponent(url)}`), - 'encodes url for the query param' - ); - await auth.tokenInput('root').submit(); - await settled(); - assert.strictEqual(currentURL(), url, 'navigates to the redirect_to with the query param after auth'); - }); - - test('redirect to logout with wrapped token authenticates you', async function (assert) { - const wrappedToken = await setupWrapping(); - const url = '/vault/secrets/cubbyhole/create'; - - await auth.logout({ - redirect_to: url, - wrapped_token: wrappedToken, - }); - await settled(); - - assert.strictEqual(currentURL(), url, 'authenticates then navigates to the redirect_to url after auth'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/alicloud/secret-test.js b/ui/tests/acceptance/secrets/backend/alicloud/secret-test.js deleted file mode 100644 index 2cfa1d1fd..000000000 --- a/ui/tests/acceptance/secrets/backend/alicloud/secret-test.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import backendsPage from 'vault/tests/pages/secrets/backends'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | alicloud/enable', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('enable alicloud', async function (assert) { - const enginePath = `alicloud-${this.uid}`; - await mountSecrets.visit(); - await settled(); - await mountSecrets.selectType('alicloud'); - await settled(); - await mountSecrets.next().path(enginePath).submit(); - await settled(); - - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backends', - 'redirects to the backends page' - ); - await settled(); - assert.ok(backendsPage.rows.filterBy('path', `${enginePath}/`)[0], 'shows the alicloud engine'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/cubbyhole/secret-test.js b/ui/tests/acceptance/secrets/backend/cubbyhole/secret-test.js deleted file mode 100644 index de167f002..000000000 --- a/ui/tests/acceptance/secrets/backend/cubbyhole/secret-test.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret'; -import showPage from 'vault/tests/pages/secrets/backend/kv/show'; -import listPage from 'vault/tests/pages/secrets/backend/list'; -import apiStub from 'vault/tests/helpers/noop-all-api-requests'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | secrets/cubbyhole/create', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - this.server = apiStub({ usePassthrough: true }); - return authPage.login(); - }); - - hooks.afterEach(function () { - this.server.shutdown(); - }); - - test('it creates and can view a secret with the cubbyhole backend', async function (assert) { - const kvPath = `cubbyhole-kv-${this.uid}`; - await listPage.visitRoot({ backend: 'cubbyhole' }); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.list-root', - 'navigates to the list page' - ); - - await listPage.create(); - await settled(); - await editPage.createSecret(kvPath, 'foo', 'bar'); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - assert.dom('[data-test-created-time]').hasText('', 'it does not render created time if blank'); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/database/secret-test.js b/ui/tests/acceptance/secrets/backend/database/secret-test.js deleted file mode 100644 index 79ebfc9c8..000000000 --- a/ui/tests/acceptance/secrets/backend/database/secret-test.js +++ /dev/null @@ -1,569 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { currentURL, settled, click, visit, fillIn, typeIn } from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import { selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers'; - -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import connectionPage from 'vault/tests/pages/secrets/backend/database/connection'; -import rolePage from 'vault/tests/pages/secrets/backend/database/role'; -import apiStub from 'vault/tests/helpers/noop-all-api-requests'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import searchSelect from 'vault/tests/pages/components/search-select'; - -const searchSelectComponent = create(searchSelect); - -const consoleComponent = create(consoleClass); - -const MODEL = { - engineType: 'database', - id: 'database-name', -}; - -const mount = async () => { - const path = `database-${Date.now()}`; - await mountSecrets.enable('database', path); - await settled(); - return path; -}; - -const newConnection = async ( - backend, - plugin = 'mongodb-database-plugin', - connectionUrl = `mongodb://127.0.0.1:4321/${name}` -) => { - const name = `connection-${Date.now()}`; - await connectionPage.visitCreate({ backend }); - await connectionPage.dbPlugin(plugin); - await connectionPage.name(name); - await connectionPage.connectionUrl(connectionUrl); - await connectionPage.toggleVerify(); - await connectionPage.save(); - await connectionPage.enable(); - return name; -}; - -const connectionTests = [ - { - name: 'elasticsearch-connection', - plugin: 'elasticsearch-database-plugin', - elasticUser: 'username', - elasticPassword: 'password', - url: 'http://127.0.0.1:9200', - requiredFields: async (assert, name) => { - assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`); - assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`); - assert.dom('[data-test-input="ca_cert"]').exists(`CA certificate field exists for ${name}`); - assert.dom('[data-test-input="ca_path"]').exists(`CA path field exists for ${name}`); - assert.dom('[data-test-input="client_cert"]').exists(`Client certificate field exists for ${name}`); - assert.dom('[data-test-input="client_key"]').exists(`Client key field exists for ${name}`); - assert.dom('[data-test-input="tls_server_name"]').exists(`TLS server name field exists for ${name}`); - assert.dom('[data-test-input="insecure"]').exists(`Insecure checkbox exists for ${name}`); - assert - .dom('[data-test-toggle-input="show-username_template"]') - .exists(`Username template toggle exists for ${name}`); - }, - }, - { - name: 'mongodb-connection', - plugin: 'mongodb-database-plugin', - url: `mongodb://127.0.0.1:4321/test`, - requiredFields: async (assert, name) => { - assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`); - assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`); - assert.dom('[data-test-input="write_concern"]').exists(`Write concern field exists for ${name}`); - assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists'); - assert - .dom('[data-test-input="root_rotation_statements"]') - .exists(`Root rotation statements exists for ${name}`); - }, - }, - { - name: 'mssql-connection', - plugin: 'mssql-database-plugin', - url: `mssql://127.0.0.1:4321/test`, - requiredFields: async (assert, name) => { - assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`); - assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`); - assert - .dom('[data-test-input="max_open_connections"]') - .exists(`Max open connections exists for ${name}`); - assert - .dom('[data-test-input="max_idle_connections"]') - .exists(`Max idle connections exists for ${name}`); - assert - .dom('[data-test-input="max_connection_lifetime"]') - .exists(`Max connection lifetime exists for ${name}`); - assert - .dom('[data-test-input="root_rotation_statements"]') - .exists(`Root rotation statements exists for ${name}`); - }, - }, - { - name: 'mysql-connection', - plugin: 'mysql-database-plugin', - url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`, - requiredFields: async (assert, name) => { - assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`); - assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`); - assert - .dom('[data-test-input="max_open_connections"]') - .exists(`Max open connections exists for ${name}`); - assert - .dom('[data-test-input="max_idle_connections"]') - .exists(`Max idle connections exists for ${name}`); - assert - .dom('[data-test-input="max_connection_lifetime"]') - .exists(`Max connection lifetime exists for ${name}`); - assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists'); - assert - .dom('[data-test-input="root_rotation_statements"]') - .exists(`Root rotation statements exists for ${name}`); - }, - }, - { - name: 'mysql-aurora-connection', - plugin: 'mysql-aurora-database-plugin', - url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`, - requiredFields: async (assert, name) => { - assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`); - assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`); - assert - .dom('[data-test-input="max_open_connections"]') - .exists(`Max open connections exists for ${name}`); - assert - .dom('[data-test-input="max_idle_connections"]') - .exists(`Max idle connections exists for ${name}`); - assert - .dom('[data-test-input="max_connection_lifetime"]') - .exists(`Max connection lifetime exists for ${name}`); - assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists'); - assert - .dom('[data-test-input="root_rotation_statements"]') - .exists(`Root rotation statements exists for ${name}`); - }, - }, - { - name: 'mysql-rds-connection', - plugin: 'mysql-rds-database-plugin', - url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`, - requiredFields: async (assert, name) => { - assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`); - assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`); - assert - .dom('[data-test-input="max_open_connections"]') - .exists(`Max open connections exists for ${name}`); - assert - .dom('[data-test-input="max_idle_connections"]') - .exists(`Max idle connections exists for ${name}`); - assert - .dom('[data-test-input="max_connection_lifetime"]') - .exists(`Max connection lifetime exists for ${name}`); - assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists'); - assert - .dom('[data-test-input="root_rotation_statements"]') - .exists(`Root rotation statements exists for ${name}`); - }, - }, - { - name: 'mysql-legacy-connection', - plugin: 'mysql-legacy-database-plugin', - url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`, - requiredFields: async (assert, name) => { - assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`); - assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`); - assert - .dom('[data-test-input="max_open_connections"]') - .exists(`Max open connections exists for ${name}`); - assert - .dom('[data-test-input="max_idle_connections"]') - .exists(`Max idle connections exists for ${name}`); - assert - .dom('[data-test-input="max_connection_lifetime"]') - .exists(`Max connection lifetime exists for ${name}`); - assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists'); - assert - .dom('[data-test-input="root_rotation_statements"]') - .exists(`Root rotation statements exists for ${name}`); - }, - }, - { - name: 'postgresql-connection', - plugin: 'postgresql-database-plugin', - url: `postgresql://{{username}}:{{password}}@localhost:5432/postgres?sslmode=disable`, - requiredFields: async (assert, name) => { - assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`); - assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`); - assert - .dom('[data-test-input="max_open_connections"]') - .exists(`Max open connections exists for ${name}`); - assert - .dom('[data-test-input="max_idle_connections"]') - .exists(`Max idle connections exists for ${name}`); - assert - .dom('[data-test-input="max_connection_lifetime"]') - .exists(`Max connection lifetime exists for ${name}`); - assert - .dom('[data-test-input="root_rotation_statements"]') - .exists(`Root rotation statements exists for ${name}`); - assert - .dom('[data-test-toggle-input="show-username_template"]') - .exists(`Username template toggle exists for ${name}`); - }, - }, - // keep oracle as last DB because it is skipped in some tests (line 285) the UI doesn't return to empty state after - { - name: 'oracle-connection', - plugin: 'vault-plugin-database-oracle', - url: `{{username}}/{{password}}@localhost:1521/OraDoc.localhost`, - requiredFields: async (assert, name) => { - assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`); - assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`); - assert - .dom('[data-test-input="max_open_connections"]') - .exists(`Max open connections exists for ${name}`); - assert - .dom('[data-test-input="max_idle_connections"]') - .exists(`Max idle connections exists for ${name}`); - assert - .dom('[data-test-input="max_connection_lifetime"]') - .exists(`Max connection lifetime exists for ${name}`); - assert - .dom('[data-test-input="root_rotation_statements"]') - .exists(`Root rotation statements exists for ${name}`); - assert - .dom('[data-test-alert-banner="alert"]') - .hasTextContaining( - `Warning Please ensure that your Oracle plugin has the default name of vault-plugin-database-oracle. Custom naming is not supported in the UI at this time. If the plugin is already named vault-plugin-database-oracle, disregard this warning.`, - 'warning banner displays about connections with SSL.' - ); - }, - }, -]; - -module('Acceptance | secrets/database/*', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.server = apiStub({ usePassthrough: true }); - return authPage.login(); - }); - hooks.afterEach(function () { - this.server.shutdown(); - }); - - test('can enable the database secrets engine', async function (assert) { - const backend = `database-${Date.now()}`; - await mountSecrets.enable('database', backend); - await settled(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/list`, - 'Mounts and redirects to connection list page' - ); - assert.dom('[data-test-component="empty-state"]').exists('Empty state exists'); - assert - .dom('.active[data-test-secret-list-tab="Connections"]') - .exists('Has Connections tab which is active'); - await click('[data-test-tab="overview"]'); - assert.strictEqual(currentURL(), `/vault/secrets/${backend}/overview`, 'Tab links to overview page'); - assert.dom('[data-test-component="empty-state"]').exists('Empty state also exists on overview page'); - assert.dom('[data-test-secret-list-tab="Roles"]').exists('Has Roles tab'); - }); - - test('Connection create and edit form for each plugin', async function (assert) { - assert.expect(161); - const backend = await mount(); - for (const testCase of connectionTests) { - await connectionPage.visitCreate({ backend }); - assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create`, 'Correct creation URL'); - assert - .dom('[data-test-empty-state-title]') - .hasText('No plugin selected', 'No plugin is selected by default and empty state shows'); - await connectionPage.dbPlugin(testCase.plugin); - assert.dom('[data-test-empty-state]').doesNotExist('Empty state goes away after plugin selected'); - await connectionPage.name(testCase.name); - if (testCase.plugin === 'elasticsearch-database-plugin') { - await connectionPage.url(testCase.url); - await connectionPage.username(testCase.elasticUser); - await connectionPage.password(testCase.elasticPassword); - } else { - await connectionPage.connectionUrl(testCase.url); - } - // skip adding oracle db connection since plugin doesn't exist - if (testCase.plugin === 'vault-plugin-database-oracle') { - testCase.requiredFields(assert, testCase.name); - continue; - } - testCase.requiredFields(assert, testCase.name); - await connectionPage.toggleVerify(); - await connectionPage.save(); - await settled(); - assert - .dom('.modal.is-active .title') - .hasText('Rotate your root credentials?', 'Modal appears asking to rotate root credentials'); - await connectionPage.enable(); - assert.ok( - currentURL().startsWith(`/vault/secrets/${backend}/show/${testCase.name}`), - `Saves connection and takes you to show page for ${testCase.name}` - ); - assert - .dom(`[data-test-row-value="Password"]`) - .doesNotExist(`Does not show Password value on show page for ${testCase.name}`); - await connectionPage.edit(); - assert.ok( - currentURL().startsWith(`/vault/secrets/${backend}/edit/${testCase.name}`), - `Edit connection button and takes you to edit page for ${testCase.name}` - ); - assert.dom(`[data-test-input="name"]`).hasAttribute('readonly'); - assert.dom(`[data-test-input="plugin_name"]`).hasAttribute('readonly'); - assert.dom('[data-test-input="password"]').doesNotExist('Password is not displayed on edit form'); - assert.dom('[data-test-toggle-input="show-password"]').exists('Update password toggle exists'); - await connectionPage.toggleVerify(); - await connectionPage.save(); - // click "Add Role" - await connectionPage.addRole(); - await settled(); - assert.strictEqual( - searchSelectComponent.selectedOptions[0].text, - testCase.name, - 'Database connection is pre-selected on the form' - ); - await click('[data-test-secret-breadcrumb]'); - } - }); - - test('Can create and delete a connection', async function (assert) { - const backend = await mount(); - const connectionDetails = { - plugin: 'mongodb-database-plugin', - id: 'horses-db', - fields: [ - { label: 'Connection name', name: 'name', value: 'horses-db' }, - { label: 'Connection URL', name: 'connection_url', value: 'mongodb://127.0.0.1:235/horses' }, - { label: 'Username', name: 'username', value: 'user', hideOnShow: true }, - { label: 'Password', name: 'password', password: 'so-secure', hideOnShow: true }, - { label: 'Write concern', name: 'write_concern' }, - ], - }; - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/list`, - 'Mounts and redirects to connection list page' - ); - await connectionPage.createLink(); - assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create`, 'Create link goes to create page'); - assert - .dom('[data-test-empty-state-title]') - .hasText('No plugin selected', 'No plugin is selected by default and empty state shows'); - await connectionPage.dbPlugin(connectionDetails.plugin); - assert.dom('[data-test-empty-state]').doesNotExist('Empty state goes away after plugin selected'); - connectionDetails.fields.forEach(async ({ name, value }) => { - assert - .dom(`[data-test-input="${name}"]`) - .exists(`Field ${name} exists for ${connectionDetails.plugin}`); - if (value) { - await fillIn(`[data-test-input="${name}"]`, value); - } - }); - // uncheck verify for the save step to work - await connectionPage.toggleVerify(); - await connectionPage.save(); - await settled(); - assert - .dom('.modal.is-active .title') - .hasText('Rotate your root credentials?', 'Modal appears asking to '); - await connectionPage.enable(); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/show/${connectionDetails.id}`, - 'Saves connection and takes you to show page' - ); - connectionDetails.fields.forEach(({ label, name, value, hideOnShow }) => { - if (hideOnShow) { - assert - .dom(`[data-test-row-value="${label}"]`) - .doesNotExist(`Does not show ${name} value on show page for ${connectionDetails.plugin}`); - } else if (!value) { - assert.dom(`[data-test-row-value="${label}"]`).hasText('Default'); - } else { - assert.dom(`[data-test-row-value="${label}"]`).hasText(value); - } - }); - await connectionPage.delete(); - assert - .dom('.modal.is-active .title') - .hasText('Delete connection?', 'Modal appears asking to confirm delete action'); - await fillIn('[data-test-confirmation-modal-input="Delete connection?"]', connectionDetails.id); - await click('[data-test-confirm-button]'); - - assert.strictEqual(currentURL(), `/vault/secrets/${backend}/list`, 'Redirects to connection list page'); - assert - .dom('[data-test-empty-state-title]') - .hasText('No connections in this backend', 'No connections listed because it was deleted'); - }); - - test('buttons show up for managing connection', async function (assert) { - const backend = await mount(); - const connection = await newConnection(backend); - await connectionPage.visitShow({ backend, id: connection }); - assert - .dom('[data-test-database-connection-delete]') - .hasText('Delete connection', 'Delete connection button exists with correct text'); - assert - .dom('[data-test-database-connection-reset]') - .hasText('Reset connection', 'Reset button exists with correct text'); - assert.dom('[data-test-secret-create]').hasText('Add role', 'Add role button exists with correct text'); - assert.dom('[data-test-edit-link]').hasText('Edit configuration', 'Edit button exists with correct text'); - const CONNECTION_VIEW_ONLY = ` - path "${backend}/*" { - capabilities = ["deny"] - } - path "${backend}/config" { - capabilities = ["list"] - } - path "${backend}/config/*" { - capabilities = ["read"] - } - `; - await consoleComponent.runCommands([ - `write sys/mounts/${backend} type=database`, - `write sys/policies/acl/test-policy policy=${btoa(CONNECTION_VIEW_ONLY)}`, - 'write -field=client_token auth/token/create policies=test-policy ttl=1h', - ]); - const token = consoleComponent.lastTextOutput; - await logout.visit(); - await authPage.login(token); - await connectionPage.visitShow({ backend, id: connection }); - assert.strictEqual( - currentURL(), - `/vault/secrets/${backend}/show/${connection}`, - 'Allows reading connection' - ); - assert - .dom('[data-test-database-connection-delete]') - .doesNotExist('Delete button does not show due to permissions'); - assert - .dom('[data-test-database-connection-reset]') - .doesNotExist('Reset button does not show due to permissions'); - assert.dom('[data-test-secret-create]').doesNotExist('Add role button does not show due to permissions'); - assert.dom('[data-test-edit-link]').doesNotExist('Edit button does not show due to permissions'); - await visit(`/vault/secrets/${backend}/overview`); - assert.dom('[data-test-selectable-card="Connections"]').exists('Connections card exists on overview'); - assert - .dom('[data-test-selectable-card="Roles"]') - .doesNotExist('Roles card does not exist on overview w/ policy'); - assert.dom('.title-number').hasText('1', 'Lists the correct number of connections'); - // confirm get credentials card is an option to select. Regression bug. - await typeIn('.ember-text-field', 'blah'); - assert.dom('[data-test-get-credentials]').isEnabled(); - }); - - test('connection_url must be decoded', async function (assert) { - const backend = await mount(); - const connection = await newConnection( - backend, - 'mongodb-database-plugin', - '{{username}}/{{password}}@oracle-xe:1521/XEPDB1' - ); - await connectionPage.visitShow({ backend, id: connection }); - assert - .dom('[data-test-row-value="Connection URL"]') - .hasText('{{username}}/{{password}}@oracle-xe:1521/XEPDB1'); - }); - - test('Role create form', async function (assert) { - const backend = await mount(); - // Connection needed for role fields - await newConnection(backend); - await rolePage.visitCreate({ backend }); - await rolePage.name('bar'); - assert - .dom('[data-test-component="empty-state"]') - .exists({ count: 2 }, 'Two empty states exist before selections made'); - await clickTrigger('#database'); - assert.strictEqual(searchSelectComponent.options.length, 1, 'list shows existing connections so far'); - await selectChoose('#database', '.ember-power-select-option', 0); - assert - .dom('[data-test-component="empty-state"]') - .exists({ count: 2 }, 'Two empty states exist before selections made'); - await rolePage.roleType('static'); - assert.dom('[data-test-component="empty-state"]').doesNotExist('Empty states go away'); - assert.dom('[data-test-input="username"]').exists('Username field appears for static role'); - assert - .dom('[data-test-toggle-input="Rotation period"]') - .exists('Rotation period field appears for static role'); - await rolePage.roleType('dynamic'); - assert - .dom('[data-test-toggle-input="Generated credentials’s Time-to-Live (TTL)"]') - .exists('TTL field exists for dynamic'); - assert - .dom('[data-test-toggle-input="Generated credentials’s maximum Time-to-Live (Max TTL)"]') - .exists('Max TTL field exists for dynamic'); - // Real connection (actual running db) required to save role, so we aren't testing that flow yet - }); - - test('root and limited access', async function (assert) { - this.set('model', MODEL); - const backend = 'database'; - const NO_ROLES_POLICY = ` - path "database/roles/*" { - capabilities = ["delete"] - } - path "database/static-roles/*" { - capabilities = ["delete"] - } - path "database/config/*" { - capabilities = ["list", "create", "read", "update"] - } - path "database/creds/*" { - capabilities = ["list", "create", "read", "update"] - } - `; - await consoleComponent.runCommands([ - `write sys/mounts/${backend} type=database`, - `write sys/policies/acl/test-policy policy=${btoa(NO_ROLES_POLICY)}`, - 'write -field=client_token auth/token/create policies=test-policy ttl=1h', - ]); - const token = consoleComponent.lastTextOutput; - - // test root user flow - await settled(); - - // await click('[data-test-secret-backend-row="database"]'); - // skipping the click because occasionally is shows up on the second page and cannot be found - await visit(`/vault/secrets/database/overview`); - - assert.dom('[data-test-component="empty-state"]').exists('renders empty state'); - assert.dom('[data-test-secret-list-tab="Connections"]').exists('renders connections tab'); - assert.dom('[data-test-secret-list-tab="Roles"]').exists('renders connections tab'); - - await click('[data-test-secret-create="connections"]'); - assert.strictEqual(currentURL(), '/vault/secrets/database/create'); - - // Login with restricted policy - await logout.visit(); - await authPage.login(token); - await settled(); - // skipping the click because occasionally is shows up on the second page and cannot be found - await visit(`/vault/secrets/database/overview`); - assert.dom('[data-test-tab="overview"]').exists('renders overview tab'); - assert.dom('[data-test-secret-list-tab="Connections"]').exists('renders connections tab'); - assert - .dom('[data-test-secret-list-tab="Roles"]') - .doesNotExist(`does not show the roles tab because it does not have permissions`); - assert - .dom('[data-test-selectable-card="Connections"]') - .exists({ count: 1 }, 'renders only the connection card'); - - await click('[data-test-action-text="Configure new"]'); - assert.strictEqual(currentURL(), '/vault/secrets/database/create?itemType=connection'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/database/workflow-test.js b/ui/tests/acceptance/secrets/backend/database/workflow-test.js deleted file mode 100644 index 373267ede..000000000 --- a/ui/tests/acceptance/secrets/backend/database/workflow-test.js +++ /dev/null @@ -1,347 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { v4 as uuidv4 } from 'uuid'; -import { Response } from 'miragejs'; -import { click, currentURL, fillIn, visit } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { create } from 'ember-cli-page-object'; - -import ENV from 'vault/config/environment'; -import { setupApplicationTest } from 'vault/tests/helpers'; -import authPage from 'vault/tests/pages/auth'; -import flashMessage from 'vault/tests/pages/components/flash-message'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const flash = create(flashMessage); -const consoleComponent = create(consoleClass); -function mountEngineCmd(type, customName = '') { - const name = customName || type; - if (type === 'kv-v2') { - return `write sys/mounts/${name} type=kv options=version=2`; - } - return `write sys/mounts/${name} type=${type}`; -} -function deleteEngineCmd(name) { - return `delete sys/mounts/${name}`; -} - -const PAGE = { - // GENERIC - emptyStateTitle: '[data-test-empty-state-title]', - emptyStateAction: '[data-test-secret-create="connections"]', - infoRow: '[data-test-component="info-table-row"]', - infoRowLabel: (label) => `[data-test-row-label="${label}"]`, - infoRowValue: (label) => `[data-test-row-value="${label}"]`, - infoRowValueDiv: (label) => `[data-test-value-div="${label}"]`, - // CONNECTIONS - rotateModal: '[data-test-database-rotate-root-modal] [data-test-modal-title]', - confirmRotate: '[data-test-enable-rotate-connection]', - skipRotate: '[data-test-enable-connection]', - // ROLES - addRole: '[data-test-secret-create]', - roleSettingsSection: '[data-test-role-settings-section]', - statementsSection: '[data-test-statements-section]', - editRole: '[data-test-edit-link]', - generateCredentials: (type = 'dynamic') => `[data-test-database-role-creds="${type}"]`, -}; - -const FORM = { - inputByAttr: (attr) => `[data-test-input="${attr}"]`, - creationStatement: (idx = 0) => - `[data-test-input="creation_statements"] [data-test-string-list-input="${idx}"]`, - saveBtn: '[data-test-secret-save]', -}; - -async function fillOutConnection(name) { - await fillIn(FORM.inputByAttr('name'), name); - await fillIn(FORM.inputByAttr('plugin_name'), 'mysql-database-plugin'); - await fillIn(FORM.inputByAttr('connection_url'), '{{username}}:{{password}}@tcp(127.0.0.1:33060)/'); - await fillIn(FORM.inputByAttr('username'), 'admin'); - await fillIn(FORM.inputByAttr('password'), 'very-secure'); -} - -/** - * This test set is for testing the flow for database secrets engine. - */ -module('Acceptance | database workflow', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'database'; - }); - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - hooks.beforeEach(async function () { - this.backend = `db-workflow-${uuidv4()}`; - this.store = this.owner.lookup('service:store'); - await authPage.login(); - await consoleComponent.runCommands(mountEngineCmd('database', this.backend), false); - }); - - hooks.afterEach(async function () { - await authPage.login(); - return consoleComponent.runCommands(deleteEngineCmd(this.backend)); - }); - - module('connections', function (hooks) { - hooks.beforeEach(function () { - this.expectedRows = [ - { label: 'Database plugin', value: 'mysql-database-plugin' }, - { label: 'Connection name', value: `connect-${this.backend}` }, - { label: 'Use custom password policy', value: 'Default' }, - { label: 'Connection URL', value: '{{username}}:{{password}}@tcp(127.0.0.1:33060)/' }, - { label: 'Max open connections', value: '4' }, - { label: 'Max idle connections', value: '0' }, - { label: 'Max connection lifetime', value: '0s' }, - { label: 'Username template', value: 'Default' }, - { - label: 'Root rotation statements', - value: `Default`, - }, - ]; - }); - test('create with rotate', async function (assert) { - assert.expect(24); - this.server.post('/:backend/rotate-root/:name', () => { - assert.ok(true, 'rotate root called'); - new Response(204); - }); - await visit(`/vault/secrets/${this.backend}/overview`); - assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct'); - await click(PAGE.emptyStateAction); - assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page'); - - // fill in connection details - await fillOutConnection(`connect-${this.backend}`); - await click(FORM.saveBtn); - - assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown'); - await click(PAGE.confirmRotate); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/show/connect-${this.backend}`, - 'Takes you to details page for connection' - ); - assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows'); - this.expectedRows.forEach(({ label, value }) => { - assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`); - assert.dom(PAGE.infoRowValue(label)).hasText(value, `Value for ${label} is correct`); - }); - }); - test('create without rotate', async function (assert) { - assert.expect(23); - this.server.post('/:backend/rotate-root/:name', () => { - assert.notOk(true, 'rotate root called when it should not have been'); - new Response(204); - }); - await visit(`/vault/secrets/${this.backend}/overview`); - assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct'); - await click(PAGE.emptyStateAction); - assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page'); - - // fill in connection details - await fillOutConnection(`connect-${this.backend}`); - await click(FORM.saveBtn); - - assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown'); - await click(PAGE.skipRotate); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/show/connect-${this.backend}`, - 'Takes you to details page for connection' - ); - assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows'); - this.expectedRows.forEach(({ label, value }) => { - assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`); - assert.dom(PAGE.infoRowValue(label)).hasText(value, `Value for ${label} is correct`); - }); - }); - test('create failure', async function (assert) { - assert.expect(25); - this.server.post('/:backend/rotate-root/:name', (schema, req) => { - const okay = req.params.name !== 'bad-connection'; - assert.ok(okay, 'rotate root called but not for bad-connection'); - new Response(204); - }); - await visit(`/vault/secrets/${this.backend}/overview`); - assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct'); - await click(PAGE.emptyStateAction); - assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page'); - - // fill in connection details - await fillOutConnection(`bad-connection`); - await click(FORM.saveBtn); - assert.strictEqual( - flash.latestMessage, - `error creating database object: error verifying - ping: Error 1045 (28000): Access denied for user 'admin'@'192.168.65.1' (using password: YES)`, - 'shows the error message from API' - ); - await fillIn(FORM.inputByAttr('name'), `connect-${this.backend}`); - await click(FORM.saveBtn); - assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown'); - await click(PAGE.confirmRotate); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/show/connect-${this.backend}`, - 'Takes you to details page for connection' - ); - assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows'); - this.expectedRows.forEach(({ label, value }) => { - assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`); - assert.dom(PAGE.infoRowValue(label)).hasText(value, `Value for ${label} is correct`); - }); - }); - - test('create connection with rotate failure', async function (assert) { - await visit(`/vault/secrets/${this.backend}/overview`); - assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct'); - await click(PAGE.emptyStateAction); - assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page'); - - // fill in connection details - await fillOutConnection(`fail-rotate`); - await click(FORM.saveBtn); - assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown'); - await click(PAGE.confirmRotate); - - assert.strictEqual( - flash.latestMessage, - `Error rotating root credentials: 1 error occurred: * failed to update user: failed to change password: Error 1045 (28000): Access denied for user 'admin'@'%' (using password: YES)`, - 'shows the error message from API' - ); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/show/fail-rotate`, - 'Takes you to details page for connection' - ); - }); - }); - module('roles', function (hooks) { - hooks.beforeEach(async function () { - this.connection = `connect-${this.backend}`; - await visit(`/vault/secrets/${this.backend}/create`); - await fillOutConnection(this.connection); - await click(FORM.saveBtn); - await visit(`/vault/secrets/${this.backend}/show/${this.connection}`); - }); - - test('it creates a dynamic role attached to the current connection', async function (assert) { - const roleName = 'dynamic-role'; - await click(PAGE.addRole); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/create?initialKey=${this.connection}&itemType=role`, - 'Takes you to create role page' - ); - - assert - .dom(`${PAGE.roleSettingsSection} ${PAGE.emptyStateTitle}`) - .hasText('No role type selected', 'roles section shows empty state before selecting role type'); - assert - .dom(`${PAGE.statementsSection} ${PAGE.emptyStateTitle}`) - .hasText('No role type selected', 'statements section shows empty state before selecting role type'); - - await fillIn(FORM.inputByAttr('name'), roleName); - assert.dom('[data-test-selected-option]').hasText(this.connection, 'Connection is selected by default'); - - await fillIn(FORM.inputByAttr('type'), 'dynamic'); - assert - .dom(`${PAGE.roleSettingsSection} ${PAGE.emptyStateTitle}`) - .doesNotExist('roles section no longer has empty state'); - assert - .dom(`${PAGE.statementsSection} ${PAGE.emptyStateTitle}`) - .doesNotExist('statements section no longer has empty state'); - - // Fill in multiple creation statements - await fillIn(FORM.creationStatement(), `GRANT SELECT ON *.* TO '{{name}}'@'%'`); - await click(`[data-test-string-list-row="0"] [data-test-string-list-button="add"]`); - await fillIn(FORM.creationStatement(1), `GRANT CREATE ON *.* TO '{{name}}'@'%'`); - await click(FORM.saveBtn); - // DETAILS - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/show/role/${roleName}`, - 'Takes you to details page for role after save' - ); - assert.dom(PAGE.infoRow).exists({ count: 7 }, 'correct number of info rows displayed'); - [ - { label: 'Role name', value: roleName }, - { label: 'Connection name', value: this.connection }, - { label: 'Type of role', value: 'dynamic' }, - { label: 'Generated credentials’s Time-to-Live (TTL)', value: '1 hour' }, - { label: 'Generated credentials’s maximum Time-to-Live (Max TTL)', value: '1 day' }, - { - label: 'Creation statements', - value: `GRANT SELECT ON *.* TO '{{name}}'@'%',GRANT CREATE ON *.* TO '{{name}}'@'%'`, - }, - { label: 'Revocation statements', value: 'Default' }, - ].forEach(({ label, value }) => { - const valueSelector = - label === 'Creation statements' ? PAGE.infoRowValueDiv(label) : PAGE.infoRowValue(label); - assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`); - assert.dom(valueSelector).hasText(value, `Value for ${label} is correct`); - }); - // EDIT - await click(PAGE.editRole); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/edit/role/${roleName}?itemType=role`, - 'Takes you to edit page for role' - ); - // TODO: these should be readonly not disabled - assert.dom(FORM.inputByAttr('name')).isDisabled('Name is read-only'); - assert.dom(FORM.inputByAttr('database')).isDisabled('Database is read-only'); - assert.dom(FORM.inputByAttr('type')).isDisabled('Type is read-only'); - await fillIn('[data-test-ttl-value="Generated credentials’s Time-to-Live (TTL)"]', '2'); - await click(FORM.saveBtn); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/show/role/${roleName}`, - 'Takes you to details page for role after save' - ); - assert - .dom(PAGE.infoRowValue('Generated credentials’s Time-to-Live (TTL)')) - .hasText('2 hours', 'Shows updated TTL'); - - // CREDENTIALS - await click(PAGE.generateCredentials()); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/credentials/${roleName}?roleType=dynamic`, - 'Takes you to credentials page for role' - ); - assert - .dom('[data-test-credentials-warning]') - .exists('shows warning about credentials only being available once'); - assert - .dom(`[data-test-value-div="Username"] [data-test-masked-input]`) - .hasText('***********', 'Username is masked'); - await click(`[data-test-value-div="Username"] [data-test-button="toggle-masked"]`); - assert - .dom(`[data-test-value-div="Username"] [data-test-masked-input]`) - .hasText('generated-username', 'Username is generated'); - - assert - .dom(`[data-test-value-div="Password"] [data-test-masked-input]`) - .hasText('***********', 'Password is masked'); - await click(`[data-test-value-div="Password"] [data-test-button="toggle-masked"]`); - assert - .dom(`[data-test-value-div="Password"] [data-test-masked-input]`) - .hasText('generated-password', 'Password is generated'); - assert.dom(PAGE.infoRowValue('Lease Duration')).hasText('1 hour', 'shows lease duration from response'); - assert - .dom(PAGE.infoRowValue('Lease ID')) - .hasText(`database/creds/${roleName}/abcd`, 'shows lease ID from response'); - }); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/engines-test.js b/ui/tests/acceptance/secrets/backend/engines-test.js deleted file mode 100644 index ed976aad1..000000000 --- a/ui/tests/acceptance/secrets/backend/engines-test.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName, settled } from '@ember/test-helpers'; -import { clickTrigger } from 'ember-power-select/test-support/helpers'; -import { create } from 'ember-cli-page-object'; -import { module, test } from 'qunit'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import backendsPage from 'vault/tests/pages/secrets/backends'; -import authPage from 'vault/tests/pages/auth'; -import ss from 'vault/tests/pages/components/search-select'; - -const consoleComponent = create(consoleClass); -const searchSelect = create(ss); - -module('Acceptance | secret-engine list view', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('it allows you to disable an engine', async function (assert) { - // first mount an engine so we can disable it. - const enginePath = `alicloud-disable-${this.uid}`; - await mountSecrets.enable('alicloud', enginePath); - await settled(); - assert.ok(backendsPage.rows.filterBy('path', `${enginePath}/`)[0], 'shows the mounted engine'); - - await backendsPage.visit(); - await settled(); - const row = backendsPage.rows.filterBy('path', `${enginePath}/`)[0]; - await row.menu(); - await settled(); - await backendsPage.disableButton(); - await settled(); - await backendsPage.confirmDisable(); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backends', - 'redirects to the backends page' - ); - assert.strictEqual( - backendsPage.rows.filterBy('path', `${enginePath}/`).length, - 0, - 'does not show the disabled engine' - ); - }); - - test('it adds disabled css styling to unsupported secret engines', async function (assert) { - assert.expect(2); - // first mount engine that is not supported - const enginePath = `nomad-${this.uid}`; - - await mountSecrets.enable('nomad', enginePath); - await settled(); - await backendsPage.visit(); - await settled(); - - const rows = document.querySelectorAll('[data-test-auth-backend-link]'); - const rowUnsupported = Array.from(rows).filter((row) => row.innerText.includes('nomad')); - const rowSupported = Array.from(rows).filter((row) => row.innerText.includes('cubbyhole')); - assert - .dom(rowUnsupported[0]) - .doesNotHaveClass( - 'linked-block', - `the linked-block class is not added to unsupported engines, which effectively disables it.` - ); - assert.dom(rowSupported[0]).hasClass('linked-block', `linked-block class is added to supported engines.`); - - // cleanup - await consoleComponent.runCommands([`delete sys/mounts/${enginePath}`]); - }); - - test('it filters by name and engine type', async function (assert) { - assert.expect(4); - const enginePath1 = `aws-1-${this.uid}`; - const enginePath2 = `aws-2-${this.uid}`; - - await mountSecrets.enable('aws', enginePath1); - await mountSecrets.enable('aws', enginePath2); - await backendsPage.visit(); - await settled(); - // filter by type - await clickTrigger('#filter-by-engine-type'); - await searchSelect.options.objectAt(0).click(); - - const rows = document.querySelectorAll('[data-test-auth-backend-link]'); - const rowsAws = Array.from(rows).filter((row) => row.innerText.includes('aws')); - - assert.strictEqual(rows.length, rowsAws.length, 'all rows returned are aws'); - // filter by name - await clickTrigger('#filter-by-engine-name'); - const firstItemToSelect = searchSelect.options.objectAt(0).text; - await searchSelect.options.objectAt(0).click(); - const singleRow = document.querySelectorAll('[data-test-auth-backend-link]'); - assert.strictEqual(singleRow.length, 1, 'returns only one row'); - assert.dom(singleRow[0]).includesText(firstItemToSelect, 'shows the filtered by name engine'); - // clear filter by engine name - await searchSelect.deleteButtons.objectAt(1).click(); - const rowsAgain = document.querySelectorAll('[data-test-auth-backend-link]'); - assert.ok(rowsAgain.length > 1, 'filter has been removed'); - - // cleanup - await consoleComponent.runCommands([`delete sys/mounts/${enginePath1}`]); - await consoleComponent.runCommands([`delete sys/mounts/${enginePath2}`]); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/gcpkms/secrets-test.js b/ui/tests/acceptance/secrets/backend/gcpkms/secrets-test.js deleted file mode 100644 index 0b171c3ce..000000000 --- a/ui/tests/acceptance/secrets/backend/gcpkms/secrets-test.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import backendsPage from 'vault/tests/pages/secrets/backends'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | gcpkms/enable', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('enable gcpkms', async function (assert) { - // Error: Cannot call `visit` without having first called `setupApplicationContext`. - const enginePath = `gcpkms-${this.uid}`; - await mountSecrets.visit(); - await settled(); - await mountSecrets.selectType('gcpkms'); - await mountSecrets.next().path(enginePath).submit(); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backends', - 'redirects to the backends page' - ); - assert.ok(backendsPage.rows.filterBy('path', `${enginePath}/`)[0], 'shows the gcpkms engine'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/generic/secret-test.js b/ui/tests/acceptance/secrets/backend/generic/secret-test.js deleted file mode 100644 index 014a0f1c5..000000000 --- a/ui/tests/acceptance/secrets/backend/generic/secret-test.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret'; -import showPage from 'vault/tests/pages/secrets/backend/kv/show'; -import listPage from 'vault/tests/pages/secrets/backend/list'; -import consolePanel from 'vault/tests/pages/components/console/ui-panel'; -import authPage from 'vault/tests/pages/auth'; - -import { create } from 'ember-cli-page-object'; - -import apiStub from 'vault/tests/helpers/noop-all-api-requests'; - -const cli = create(consolePanel); - -module('Acceptance | secrets/generic/create', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - this.server = apiStub({ usePassthrough: true }); - return authPage.login(); - }); - - hooks.afterEach(function () { - this.server.shutdown(); - }); - - test('it creates and can view a secret with the generic backend', async function (assert) { - const path = `generic-${this.uid}`; - const kvPath = `generic-kv-${this.uid}`; - await cli.runCommands([`write sys/mounts/${path} type=generic`, `write ${path}/foo bar=baz`]); - await listPage.visitRoot({ backend: path }); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.list-root', - 'navigates to the list page' - ); - assert.strictEqual(listPage.secrets.length, 1, 'lists one secret in the backend'); - - await listPage.create(); - await editPage.createSecret(kvPath, 'foo', 'bar'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - }); - - test('upgrading generic to version 2 lists all existing secrets, and CRUD continues to work', async function (assert) { - const path = `generic-${this.uid}`; - const kvPath = `generic-kv-${this.uid}`; - await cli.runCommands([ - `write sys/mounts/${path} type=generic`, - `write ${path}/foo bar=baz`, - // upgrade to version 2 generic mount - `write sys/mounts/${path}/tune options=version=2`, - ]); - await listPage.visitRoot({ backend: path }); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.list-root', - 'navigates to the list page' - ); - assert.strictEqual(listPage.secrets.length, 1, 'lists the old secret in the backend'); - - await listPage.create(); - await editPage.createSecret(kvPath, 'foo', 'bar'); - await listPage.visitRoot({ backend: path }); - assert.strictEqual(listPage.secrets.length, 2, 'lists two secrets in the backend'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kubernetes/configuration-test.js b/ui/tests/acceptance/secrets/backend/kubernetes/configuration-test.js deleted file mode 100644 index 6d159fab5..000000000 --- a/ui/tests/acceptance/secrets/backend/kubernetes/configuration-test.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import kubernetesScenario from 'vault/mirage/scenarios/kubernetes'; -import ENV from 'vault/config/environment'; -import authPage from 'vault/tests/pages/auth'; -import { visit, click, currentRouteName } from '@ember/test-helpers'; -import { Response } from 'miragejs'; - -module('Acceptance | kubernetes | configuration', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'kubernetes'; - }); - - hooks.beforeEach(function () { - kubernetesScenario(this.server); - this.visitConfiguration = () => { - return visit('/vault/secrets/kubernetes/kubernetes/configuration'); - }; - this.validateRoute = (assert, route, message) => { - assert.strictEqual(currentRouteName(), `vault.cluster.secrets.backend.kubernetes.${route}`, message); - }; - return authPage.login(); - }); - - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - test('it should transition to configure page on Edit Configuration click from toolbar', async function (assert) { - assert.expect(1); - await this.visitConfiguration(); - await click('[data-test-toolbar-config-action]'); - this.validateRoute(assert, 'configure', 'Transitions to Configure route on click'); - }); - - test('it should transition to the configuration page on Save click in Configure', async function (assert) { - assert.expect(1); - await this.visitConfiguration(); - await click('[data-test-toolbar-config-action]'); - await click('[data-test-config-save]'); - await click('[data-test-config-confirm]'); - this.validateRoute(assert, 'configuration', 'Transitions to Configuration route on click'); - }); - - test('it should transition to the configuration page on Cancel click in Configure', async function (assert) { - assert.expect(1); - await this.visitConfiguration(); - await click('[data-test-toolbar-config-action]'); - await click('[data-test-config-cancel]'); - this.validateRoute(assert, 'configuration', 'Transitions to Configuration route on click'); - }); - - test('it should transition to error route on config fetch error other than 404', async function (assert) { - this.server.get('/kubernetes/config', () => new Response(403)); - await this.visitConfiguration(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.kubernetes.error', - 'Transitions to error route on config fetch error' - ); - }); - - test('it should not transition to error route on config fetch 404', async function (assert) { - this.server.get('/kubernetes/config', () => new Response(404)); - await this.visitConfiguration(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.kubernetes.configuration', - 'Transitions to configuration route on fetch 404' - ); - assert.dom('[data-test-empty-state-title]').hasText('Kubernetes not configured', 'Config cta renders'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kubernetes/credentials-test.js b/ui/tests/acceptance/secrets/backend/kubernetes/credentials-test.js deleted file mode 100644 index 456c171b0..000000000 --- a/ui/tests/acceptance/secrets/backend/kubernetes/credentials-test.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import kubernetesScenario from 'vault/mirage/scenarios/kubernetes'; -import ENV from 'vault/config/environment'; -import authPage from 'vault/tests/pages/auth'; -import { fillIn, visit, click, currentRouteName } from '@ember/test-helpers'; - -module('Acceptance | kubernetes | credentials', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'kubernetes'; - }); - hooks.beforeEach(function () { - kubernetesScenario(this.server); - this.visitRoleCredentials = () => { - return visit('/vault/secrets/kubernetes/kubernetes/roles/role-0/credentials'); - }; - this.validateRoute = (assert, route, message) => { - assert.strictEqual(currentRouteName(), `vault.cluster.secrets.backend.kubernetes.${route}`, message); - }; - return authPage.login(); - }); - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - test('it should have correct breadcrumb links in credentials view', async function (assert) { - assert.expect(3); - await this.visitRoleCredentials(); - await click('[data-test-breadcrumbs] li:nth-child(3) a'); - this.validateRoute(assert, 'roles.role.details', 'Transitions to role details route on breadcrumb click'); - await this.visitRoleCredentials(); - await click('[data-test-breadcrumbs] li:nth-child(2) a'); - this.validateRoute(assert, 'roles.index', 'Transitions to roles route on breadcrumb click'); - await this.visitRoleCredentials(); - await click('[data-test-breadcrumbs] li:nth-child(1) a'); - this.validateRoute(assert, 'overview', 'Transitions to overview route on breadcrumb click'); - }); - - test('it should transition to role details view on Back click', async function (assert) { - assert.expect(1); - await this.visitRoleCredentials(); - await click('[data-test-generate-credentials-back]'); - - await this.validateRoute(assert, 'roles.role.details', 'Transitions to role details on Back click'); - }); - - test('it should transition to role details view on Done click', async function (assert) { - assert.expect(1); - await this.visitRoleCredentials(); - this.server.post('/kubernetes-test/creds/role-0', () => { - assert.ok('POST request made to generate credentials'); - return { - request_id: '58fefc6c-5195-c17a-94f2-8f889f3df57c', - lease_id: 'kubernetes/creds/default-role/aWczfcfJ7NKUdiirJrPXIs38', - renewable: false, - lease_duration: 3600, - data: { - service_account_name: 'default', - service_account_namespace: 'default', - service_account_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6Imlr', - }, - }; - }); - await fillIn('[data-test-kubernetes-namespace]', 'kubernetes-test'); - await click('[data-test-toggle-input]'); - await click('[data-test-toggle-input="Time-to-Live (TTL)"]'); - await fillIn('[data-test-ttl-value="Time-to-Live (TTL)"]', 2); - await click('[data-test-generate-credentials-button]'); - await click('[data-test-generate-credentials-done]'); - - await this.validateRoute(assert, 'roles.role.details', 'Transitions to role details on Done click'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kubernetes/overview-test.js b/ui/tests/acceptance/secrets/backend/kubernetes/overview-test.js deleted file mode 100644 index 2f52636c7..000000000 --- a/ui/tests/acceptance/secrets/backend/kubernetes/overview-test.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import kubernetesScenario from 'vault/mirage/scenarios/kubernetes'; -import ENV from 'vault/config/environment'; -import authPage from 'vault/tests/pages/auth'; -import { visit, click, currentRouteName } from '@ember/test-helpers'; -import { selectChoose } from 'ember-power-select/test-support'; -import { SELECTORS } from 'vault/tests/helpers/kubernetes/overview'; - -module('Acceptance | kubernetes | overview', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'kubernetes'; - }); - hooks.beforeEach(function () { - this.createScenario = (shouldConfigureRoles = true) => - shouldConfigureRoles ? kubernetesScenario(this.server) : kubernetesScenario(this.server, false); - - this.visitOverview = () => { - return visit('/vault/secrets/kubernetes/kubernetes/overview'); - }; - this.validateRoute = (assert, route, message) => { - assert.strictEqual(currentRouteName(), `vault.cluster.secrets.backend.kubernetes.${route}`, message); - }; - return authPage.login(); - }); - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - test('it should transition to configuration page during empty state', async function (assert) { - assert.expect(1); - await this.visitOverview(); - await click('[data-test-component="empty-state"] a'); - this.validateRoute(assert, 'configure', 'Transitions to Configure route on click'); - }); - - test('it should transition to view roles', async function (assert) { - assert.expect(1); - this.createScenario(); - await this.visitOverview(); - await click(SELECTORS.rolesCardLink); - this.validateRoute(assert, 'roles.index', 'Transitions to roles route on View Roles click'); - }); - - test('it should transition to create roles', async function (assert) { - assert.expect(1); - this.createScenario(false); - await this.visitOverview(); - await click(SELECTORS.rolesCardLink); - this.validateRoute(assert, 'roles.create', 'Transitions to roles route on Create Roles click'); - }); - - test('it should transition to generate credentials', async function (assert) { - assert.expect(1); - await this.createScenario(); - await this.visitOverview(); - await selectChoose('.search-select', 'role-0'); - await click('[data-test-generate-credential-button]'); - this.validateRoute(assert, 'roles.role.credentials', 'Transitions to roles route on Generate click'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js b/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js deleted file mode 100644 index 7f9310ba3..000000000 --- a/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import kubernetesScenario from 'vault/mirage/scenarios/kubernetes'; -import ENV from 'vault/config/environment'; -import authPage from 'vault/tests/pages/auth'; -import { fillIn, visit, currentURL, click, currentRouteName } from '@ember/test-helpers'; - -module('Acceptance | kubernetes | roles', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - ENV['ember-cli-mirage'].handler = 'kubernetes'; - }); - hooks.beforeEach(function () { - kubernetesScenario(this.server); - this.visitRoles = () => { - return visit('/vault/secrets/kubernetes/kubernetes/roles'); - }; - this.validateRoute = (assert, route, message) => { - assert.strictEqual(currentRouteName(), `vault.cluster.secrets.backend.kubernetes.${route}`, message); - }; - return authPage.login(); - }); - hooks.after(function () { - ENV['ember-cli-mirage'].handler = null; - }); - - test('it should filter roles', async function (assert) { - await this.visitRoles(); - assert.dom('[data-test-list-item-link]').exists({ count: 3 }, 'Roles list renders'); - await fillIn('[data-test-component="navigate-input"]', '1'); - assert.dom('[data-test-list-item-link]').exists({ count: 1 }, 'Filtered roles list renders'); - assert.ok(currentURL().includes('pageFilter=1'), 'pageFilter query param value is set'); - }); - - test('it should link to role details on list item click', async function (assert) { - assert.expect(1); - await this.visitRoles(); - await click('[data-test-list-item-link]'); - this.validateRoute(assert, 'roles.role.details', 'Transitions to details route on list item click'); - }); - - test('it should have correct breadcrumb links in role details view', async function (assert) { - assert.expect(2); - await this.visitRoles(); - await click('[data-test-list-item-link]'); - await click('[data-test-breadcrumbs] li:nth-child(2) a'); - this.validateRoute(assert, 'roles.index', 'Transitions to roles route on breadcrumb click'); - await click('[data-test-list-item-link]'); - await click('[data-test-breadcrumbs] li:nth-child(1) a'); - this.validateRoute(assert, 'overview', 'Transitions to overview route on breadcrumb click'); - }); - - test('it should have functional list item menu', async function (assert) { - assert.expect(3); - await this.visitRoles(); - for (const action of ['details', 'edit', 'delete']) { - await click('[data-test-list-item-popup] button'); - await click(`[data-test-${action}]`); - if (action === 'delete') { - await click('[data-test-confirm-button]'); - assert.dom('[data-test-list-item-link]').exists({ count: 2 }, 'Deleted role removed from list'); - } else { - this.validateRoute( - assert, - `roles.role.${action}`, - `Transitions to ${action} route on menu action click` - ); - const selector = - action === 'details' ? '[data-test-breadcrumbs] li:nth-child(2) a' : '[data-test-cancel]'; - await click(selector); - } - } - }); - - test('it should create role', async function (assert) { - assert.expect(2); - await this.visitRoles(); - await click('[data-test-toolbar-roles-action]'); - await click('[data-test-radio-card="basic"]'); - await fillIn('[data-test-input="name"]', 'new-test-role'); - await fillIn('[data-test-input="serviceAccountName"]', 'default'); - await fillIn('[data-test-input="allowedKubernetesNamespaces"]', '*'); - await click('[data-test-save]'); - this.validateRoute(assert, 'roles.role.details', 'Transitions to details route on save success'); - await click('[data-test-breadcrumbs] li:nth-child(2) a'); - assert.dom('[data-test-role="new-test-role"]').exists('New role renders in list'); - }); - - test('it should have functional toolbar actions in details view', async function (assert) { - assert.expect(3); - await this.visitRoles(); - await click('[data-test-list-item-link]'); - await click('[data-test-generate-credentials]'); - this.validateRoute(assert, 'roles.role.credentials', 'Transitions to credentials route'); - await click('[data-test-breadcrumbs] li:nth-child(3) a'); - await click('[data-test-edit]'); - this.validateRoute(assert, 'roles.role.edit', 'Transitions to edit route'); - await click('[data-test-cancel]'); - await click('[data-test-list-item-link]'); - await click('[data-test-delete] button'); - await click('[data-test-confirm-button]'); - assert - .dom('[data-test-list-item-link]') - .exists({ count: 2 }, 'Transitions to roles route and deleted role removed from list'); - }); - - test('it should generate credentials for role', async function (assert) { - assert.expect(1); - await this.visitRoles(); - await click('[data-test-list-item-link]'); - await click('[data-test-generate-credentials]'); - await fillIn('[data-test-kubernetes-namespace]', 'test-namespace'); - await click('[data-test-generate-credentials-button]'); - await click('[data-test-generate-credentials-done]'); - this.validateRoute( - assert, - 'roles.role.details', - 'Transitions to details route when done generating credentials' - ); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kv/breadcrumbs-test.js b/ui/tests/acceptance/secrets/backend/kv/breadcrumbs-test.js deleted file mode 100644 index af84b6365..000000000 --- a/ui/tests/acceptance/secrets/backend/kv/breadcrumbs-test.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { create } from 'ember-cli-page-object'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, currentURL, fillIn, find, visit, waitUntil } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const consolePanel = create(consoleClass); - -module('Acceptance | kv | breadcrumbs', function (hooks) { - setupApplicationTest(hooks); - - test('it should route back to parent path from metadata tab', async function (assert) { - await authPage.login(); - await consolePanel.runCommands(['delete sys/mounts/kv', 'write sys/mounts/kv type=kv-v2']); - await visit('/vault/secrets/kv/list'); - await click('[data-test-secret-create]'); - await fillIn('[data-test-secret-path]', 'foo/bar'); - await click('[data-test-secret-save]'); - await waitUntil(() => find('[data-test-secret-metadata-tab]')); - await click('[data-test-secret-metadata-tab]'); - await click('[data-test-secret-breadcrumb="foo"]'); - assert.strictEqual( - currentURL(), - '/vault/secrets/kv/list/foo/', - 'Routes back to list view on breadcrumb click' - ); - await consolePanel.runCommands(['delete sys/mounts/kv']); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kv/diff-test.js b/ui/tests/acceptance/secrets/backend/kv/diff-test.js deleted file mode 100644 index 58cdd487a..000000000 --- a/ui/tests/acceptance/secrets/backend/kv/diff-test.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, settled, fillIn } from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret'; -import listPage from 'vault/tests/pages/secrets/backend/list'; -import apiStub from 'vault/tests/helpers/noop-all-api-requests'; -import authPage from 'vault/tests/pages/auth'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const consoleComponent = create(consoleClass); - -module('Acceptance | kv2 diff view', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.server = apiStub({ usePassthrough: true }); - return authPage.login(); - }); - - hooks.afterEach(function () { - this.server.shutdown(); - }); - - test('it shows correct diff status based on versions', async function (assert) { - const secretPath = `my-secret`; - - await consoleComponent.runCommands([ - `write sys/mounts/secret type=kv options=version=2`, - // delete any kv previously written here so that tests can be re-run - `delete secret/metadata/${secretPath}`, - 'write -field=client_token auth/token/create policies=kv-v2-degrade', - ]); - - await listPage.visitRoot({ backend: 'secret' }); - await settled(); - await listPage.create(); - await settled(); - await editPage.createSecret(secretPath, 'version1', 'hello'); - await settled(); - await click('[data-test-popup-menu-trigger="version"]'); - - assert.dom('[data-test-view-diff]').doesNotExist('does not show diff view with only one version'); - // add another version - await click('[data-test-secret-edit="true"]'); - - const secondKey = document.querySelectorAll('[data-test-secret-key]')[1]; - const secondValue = document.querySelectorAll('.masked-value')[1]; - await fillIn(secondKey, 'version2'); - await fillIn(secondValue, 'world!'); - await click('[data-test-secret-save]'); - - await click('[data-test-popup-menu-trigger="version"]'); - - assert.dom('[data-test-view-diff]').exists('does show diff view with two versions'); - - await click('[data-test-view-diff]'); - - const diffBetweenVersion2and1 = document.querySelector('.jsondiffpatch-added').innerText; - assert.strictEqual(diffBetweenVersion2and1, 'version2"world!"', 'shows the correct added part'); - - await click('[data-test-popup-menu-trigger="right-version"]'); - - await click('[data-test-rightSide-version="2"]'); - - assert.dom('.diff-status').exists('shows States Match'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kv/secret-test.js b/ui/tests/acceptance/secrets/backend/kv/secret-test.js deleted file mode 100644 index 874f6991c..000000000 --- a/ui/tests/acceptance/secrets/backend/kv/secret-test.js +++ /dev/null @@ -1,1084 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { - click, - visit, - settled, - currentURL, - currentRouteName, - fillIn, - triggerKeyEvent, - typeIn, -} from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import { module, skip, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret'; -import showPage from 'vault/tests/pages/secrets/backend/kv/show'; -import listPage from 'vault/tests/pages/secrets/backend/list'; - -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import apiStub from 'vault/tests/helpers/noop-all-api-requests'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; - -const consoleComponent = create(consoleClass); - -const writeSecret = async function (backend, path, key, val) { - await listPage.visitRoot({ backend }); - await listPage.create(); - return editPage.createSecret(path, key, val); -}; - -const deleteEngine = async function (enginePath, assert) { - await logout.visit(); - await authPage.login(); - await consoleComponent.runCommands([`delete sys/mounts/${enginePath}`]); - const response = consoleComponent.lastLogOutput; - assert.strictEqual( - response, - `Success! Data deleted (if it existed) at: sys/mounts/${enginePath}`, - 'Engine successfully deleted' - ); -}; - -const mountEngineGeneratePolicyToken = async (enginePath, secretPath, policy, version = 2) => { - await consoleComponent.runCommands([ - // delete any kv previously written here so that tests can be re-run - `delete ${enginePath}/metadata/${secretPath}`, - // delete any previous mount with same name - `delete sys/mounts/${enginePath}`, - // mount engine and generate policy - `write sys/mounts/${enginePath} type=kv options=version=${version}`, - `write sys/policies/acl/kv-v2-test-policy policy=${btoa(policy)}`, - 'write -field=client_token auth/token/create policies=kv-v2-test-policy', - ]); - return consoleComponent.lastLogOutput; -}; - -module('Acceptance | secrets/secret/create, read, delete', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.uid = uuidv4(); - this.server = apiStub({ usePassthrough: true }); - await authPage.login(); - }); - - hooks.afterEach(async function () { - this.server.shutdown(); - await logout.visit(); - }); - - test('it creates a secret and redirects', async function (assert) { - assert.expect(5); - const secretPath = `kv-path-${this.uid}`; - const path = `kv-engine-${this.uid}`; - await enablePage.enable('kv', path); - await listPage.visitRoot({ backend: path }); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.list-root', - 'navigates to the list page' - ); - await listPage.create(); - await settled(); - await editPage.toggleMetadata(); - await settled(); - assert.ok(editPage.hasMetadataFields, 'shows the metadata form'); - await editPage.createSecret(secretPath, 'foo', 'bar'); - await settled(); - - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - await deleteEngine(path, assert); - }); - - test('it can create a secret when check-and-set is required', async function (assert) { - assert.expect(3); - const enginePath = `kv-secret-${this.uid}`; - const secretPath = 'foo/bar'; - await mountSecrets.visit(); - await mountSecrets.enable('kv', enginePath); - await consoleComponent.runCommands(`write ${enginePath}/config cas_required=true`); - await writeSecret(enginePath, secretPath, 'foo', 'bar'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - await deleteEngine(enginePath, assert); - }); - - test('it can create a secret with a non default max version and add metadata', async function (assert) { - assert.expect(4); - const enginePath = `kv-secret-${this.uid}`; - const secretPath = 'maxVersions'; - const maxVersions = 101; - await mountSecrets.visit(); - await mountSecrets.enable('kv', enginePath); - await settled(); - await editPage.startCreateSecret(); - await editPage.path(secretPath); - await editPage.toggleMetadata(); - await settled(); - await editPage.maxVersion(maxVersions); - await settled(); - await editPage.save(); - await settled(); - await editPage.metadataTab(); - await settled(); - const savedMaxVersions = Number( - document.querySelector('[data-test-value-div="Maximum versions"]').innerText - ); - assert.strictEqual( - maxVersions, - savedMaxVersions, - 'max_version displays the saved number set when creating the secret' - ); - // add metadata - await click('[data-test-add-custom-metadata]'); - await fillIn('[data-test-kv-key]', 'key'); - await fillIn('[data-test-kv-value]', 'value'); - await click('[data-test-save-metadata]'); - const key = document.querySelector('[data-test-row-label="key"]').innerText; - const value = document.querySelector('[data-test-row-value="key"]').innerText; - assert.strictEqual(key, 'key', 'metadata key displays after adding it.'); - assert.strictEqual(value, 'value', 'metadata value displays after adding it.'); - await deleteEngine(enginePath, assert); - }); - - skip('it can handle validation on custom metadata', async function (assert) { - assert.expect(3); - const enginePath = `kv-secret-${this.uid}`; - const secretPath = 'customMetadataValidations'; - - await mountSecrets.visit(); - await mountSecrets.enable('kv', enginePath); - await settled(); - await editPage.startCreateSecret(); - await editPage.path(secretPath); - await editPage.toggleMetadata(); - await settled(); - await typeIn('[data-test-kv-value]', 'invalid\\/'); - assert - .dom('[data-test-inline-error-message]') - .hasText('Custom values cannot contain a backward slash.', 'will not allow backward slash in value.'); - await fillIn('[data-test-kv-value]', ''); // clear previous contents - await typeIn('[data-test-kv-value]', 'removed!'); - assert.dom('[data-test-inline-error-message]').doesNotExist('inline error goes away'); - await click('[data-test-secret-save]'); - assert - .dom('[data-test-error]') - .includesText( - 'custom_metadata validation failed: length of key', - 'shows API error that is not captured by validation' - ); - await deleteEngine(enginePath, assert); - }); - - test('it can mount a KV 2 secret engine with config metadata', async function (assert) { - assert.expect(4); - const enginePath = `kv-secret-${this.uid}`; - const maxVersion = '101'; - await mountSecrets.visit(); - await click('[data-test-mount-type="kv"]'); - - await click('[data-test-mount-next]'); - - await fillIn('[data-test-input="path"]', enginePath); - await fillIn('[data-test-input="maxVersions"]', maxVersion); - await click('[data-test-input="casRequired"]'); - await click('[data-test-toggle-label="Automate secret deletion"]'); - await fillIn('[data-test-select="ttl-unit"]', 's'); - await fillIn('[data-test-ttl-value="Automate secret deletion"]', '1'); - await click('[data-test-mount-submit="true"]'); - - await click('[data-test-configuration-tab]'); - - const cas = document.querySelector('[data-test-value-div="Require Check and Set"]').innerText; - const deleteVersionAfter = document.querySelector( - '[data-test-value-div="Automate secret deletion"]' - ).innerText; - const savedMaxVersion = document.querySelector( - '[data-test-value-div="Maximum number of versions"]' - ).innerText; - - assert.strictEqual( - maxVersion, - savedMaxVersion, - 'displays the max version set when configuring the secret-engine' - ); - assert.strictEqual(cas.trim(), 'Yes', 'displays the cas set when configuring the secret-engine'); - assert.strictEqual( - deleteVersionAfter.trim(), - '1s', - 'displays the delete version after set when configuring the secret-engine' - ); - await deleteEngine(enginePath, assert); - }); - - test('it can create a secret and metadata can be created and edited', async function (assert) { - assert.expect(2); - const enginePath = `kv-secret-${this.uid}`; - const secretPath = 'metadata'; - const maxVersions = 101; - await mountSecrets.visit(); - await mountSecrets.enable('kv', enginePath); - await settled(); - await editPage.startCreateSecret(); - await editPage.path(secretPath); - await editPage.toggleMetadata(); - await settled(); - await fillIn('[data-test-input="maxVersions"]', maxVersions); - - await editPage.save(); - await settled(); - await editPage.metadataTab(); - await settled(); - const savedMaxVersions = Number(document.querySelectorAll('[data-test-value-div]')[0].innerText); - assert.strictEqual( - maxVersions, - savedMaxVersions, - 'max_version displays the saved number set when creating the secret' - ); - await deleteEngine(enginePath, assert); - }); - - test('it shows validation errors', async function (assert) { - assert.expect(5); - const enginePath = `kv-secret-${this.uid}`; - const secretPath = 'not-duplicate'; - await mountSecrets.visit(); - await mountSecrets.enable('kv', enginePath); - await settled(); - await editPage.startCreateSecret(); - await typeIn('[data-test-secret-path="true"]', 'beep'); - assert - .dom('[data-test-inline-error-message]') - .hasText( - 'A secret with this path already exists.', - 'when duplicate path it shows correct error message' - ); - - await editPage.toggleMetadata(); - await settled(); - await typeIn('[data-test-input="maxVersions"]', 'abc'); - assert - .dom('[data-test-input="maxVersions"]') - .hasClass('has-error-border', 'shows border error on input with error'); - assert.dom('[data-test-secret-save]').isNotDisabled('Save button is disabled'); - await fillIn('[data-test-input="maxVersions"]', 20); // fillIn replaces the text, whereas typeIn only adds to it. - await triggerKeyEvent('[data-test-input="maxVersions"]', 'keyup', 65); - await editPage.path(secretPath); - await triggerKeyEvent('[data-test-secret-path="true"]', 'keyup', 65); - await click('[data-test-secret-save]'); - assert.strictEqual( - currentURL(), - `/vault/secrets/${enginePath}/show/${secretPath}`, - 'navigates to show secret' - ); - await deleteEngine(enginePath, assert); - }); - - test('it navigates to version history and to a specific version', async function (assert) { - assert.expect(6); - const enginePath = `kv-secret-${this.uid}`; - const secretPath = `specific-version`; - await mountSecrets.visit(); - await mountSecrets.enable('kv', enginePath); - await settled(); - await listPage.visitRoot({ backend: enginePath }); - await settled(); - await listPage.create(); - await settled(); - await editPage.createSecret(secretPath, 'foo', 'bar'); - await click('[data-test-popup-menu-trigger="version"]'); - - assert.dom('[data-test-created-time]').includesText('Version created ', 'shows version created time'); - - await click('[data-test-version-history]'); - - assert - .dom('[data-test-list-item-content]') - .includesText('Version 1 Current', 'shows version one data on the version history as current'); - assert.dom('[data-test-list-item-content]').exists({ count: 1 }, 'renders a single version'); - - await click('.linked-block'); - await click('button.button.masked-input-toggle'); - assert.dom('[data-test-masked-input]').hasText('bar', 'renders secret on the secret version show page'); - assert.strictEqual( - currentURL(), - `/vault/secrets/${enginePath}/show/${secretPath}?version=1`, - 'redirects to the show page with queryParam version=1' - ); - await deleteEngine(enginePath, assert); - }); - - test('version 1 performs the correct capabilities lookup and does not show metadata tab', async function (assert) { - assert.expect(4); - const enginePath = `kv-secret-${this.uid}`; - const secretPath = 'foo/bar'; - // mount version 1 engine - await mountSecrets.visit(); - await mountSecrets.selectType('kv'); - await mountSecrets.next().path(enginePath).toggleOptions().version(1).submit(); - await listPage.create(); - await editPage.createSecret(secretPath, 'foo', 'bar'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - // check for metadata tab should not exist on KV version 1 - assert.dom('[data-test-secret-metadata-tab]').doesNotExist('does not show metadata tab'); - await deleteEngine(enginePath, assert); - }); - - // https://github.com/hashicorp/vault/issues/5960 - test('version 1: nested paths creation maintains ability to navigate the tree', async function (assert) { - assert.expect(6); - const enginePath = `kv-secret-${this.uid}`; - const secretPath = '1/2/3/4'; - // mount version 1 engine - await mountSecrets.visit(); - await mountSecrets.selectType('kv'); - await mountSecrets.next().path(enginePath).toggleOptions().version(1).submit(); - await listPage.create(); - await editPage.createSecret(secretPath, 'foo', 'bar'); - - // setup an ancestor for when we delete - await listPage.visitRoot({ backend: enginePath }); - await listPage.secrets.filterBy('text', '1/')[0].click(); - await listPage.create(); - await editPage.createSecret('1/2', 'foo', 'bar'); - - // lol we have to do this because ember-cli-page-object doesn't like *'s in visitable - await listPage.visitRoot({ backend: enginePath }); - await listPage.secrets.filterBy('text', '1/')[0].click(); - await listPage.secrets.filterBy('text', '2/')[0].click(); - await listPage.secrets.filterBy('text', '3/')[0].click(); - await listPage.create(); - - await editPage.createSecret(secretPath + 'a', 'foo', 'bar'); - await listPage.visitRoot({ backend: enginePath }); - await listPage.secrets.filterBy('text', '1/')[0].click(); - await listPage.secrets.filterBy('text', '2/')[0].click(); - const secretLink = listPage.secrets.filterBy('text', '3/')[0]; - assert.ok(secretLink, 'link to the 3/ branch displays properly'); - - await listPage.secrets.filterBy('text', '3/')[0].click(); - await listPage.secrets.objectAt(0).menuToggle(); - await settled(); - await listPage.delete(); - await listPage.confirmDelete(); - await settled(); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.list'); - assert.strictEqual(currentURL(), `/vault/secrets/${enginePath}/list/1/2/3/`, 'remains on the page'); - - await listPage.secrets.objectAt(0).menuToggle(); - await listPage.delete(); - await listPage.confirmDelete(); - await settled(); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.list'); - assert.strictEqual( - currentURL(), - `/vault/secrets/${enginePath}/list/1/`, - 'navigates to the ancestor created earlier' - ); - await deleteEngine(enginePath, assert); - }); - - test('first level secrets redirect properly upon deletion', async function (assert) { - assert.expect(2); - const enginePath = `kv-secret-${this.uid}`; - const secretPath = 'test'; - // mount version 1 engine - await mountSecrets.visit(); - await mountSecrets.selectType('kv'); - await mountSecrets.next().path(enginePath).toggleOptions().version(1).submit(); - await listPage.create(); - await editPage.createSecret(secretPath, 'foo', 'bar'); - await showPage.deleteSecretV1(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.list-root', - 'redirected to the list page on delete' - ); - await deleteEngine(enginePath, assert); - }); - - // https://github.com/hashicorp/vault/issues/5994 - test('version 1: key named keys', async function (assert) { - assert.expect(2); - await consoleComponent.runCommands([ - 'vault write sys/mounts/test type=kv', - 'refresh', - 'vault write test/a keys=a keys=b', - ]); - await showPage.visit({ backend: 'test', id: 'a' }); - assert.ok(showPage.editIsPresent, 'renders the page properly'); - await deleteEngine('test', assert); - }); - - test('it redirects to the path ending in / for list pages', async function (assert) { - assert.expect(3); - const secretPath = `foo/bar/kv-list-${this.uid}`; - await consoleComponent.runCommands(['vault write sys/mounts/secret type=kv']); - await listPage.visitRoot({ backend: 'secret' }); - await listPage.create(); - await editPage.createSecret(secretPath, 'foo', 'bar'); - await settled(); - // use visit helper here because ids with / in them get encoded - await visit('/vault/secrets/secret/list/foo/bar'); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.list'); - assert.ok(currentURL().endsWith('/'), 'redirects to the path ending in a slash'); - await deleteEngine('secret', assert); - }); - - test('it can edit via the JSON input', async function (assert) { - assert.expect(4); - const content = JSON.stringify({ foo: 'fa', bar: 'boo' }); - const secretPath = `kv-json-${this.uid}`; - await consoleComponent.runCommands(['vault write sys/mounts/secret type=kv']); - await listPage.visitRoot({ backend: 'secret' }); - await listPage.create(); - await editPage.path(secretPath).toggleJSON(); - const instance = document.querySelector('.CodeMirror').CodeMirror; - instance.setValue(content); - await editPage.save(); - - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - const savedInstance = document.querySelector('.CodeMirror').CodeMirror; - assert.strictEqual( - savedInstance.options.value, - JSON.stringify({ bar: 'boo', foo: 'fa' }, null, 2), - 'saves the content' - ); - await deleteEngine('secret', assert); - }); - - test('paths are properly encoded', async function (assert) { - const backend = `kv-encoding-${this.uid}`; - const paths = [ - '(', - ')', - '"', - //"'", - '!', - '#', - '$', - '&', - '*', - '+', - '@', - '{', - '|', - '}', - '~', - '[', - '\\', - ']', - '^', - '_', - ].map((char) => `${char}some`); - assert.expect(paths.length * 2 + 1); - const secretPath = '2'; - const commands = paths.map((path) => `write '${backend}/${path}/${secretPath}' 3=4`); - await consoleComponent.runCommands([`write sys/mounts/${backend} type=kv`, ...commands]); - for (const path of paths) { - await listPage.visit({ backend, id: path }); - assert.ok(listPage.secrets.filterBy('text', '2')[0], `${path}: secret is displayed properly`); - await listPage.secrets.filterBy('text', '2')[0].click(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - `${path}: show page renders correctly` - ); - } - await deleteEngine(backend, assert); - }); - - test('create secret with space shows version data and shows space warning', async function (assert) { - assert.expect(4); - const enginePath = `kv-engine-${this.uid}`; - const secretPath = 'space space'; - // mount version 2 - await mountSecrets.visit(); - await mountSecrets.selectType('kv'); - await mountSecrets.next().path(enginePath).submit(); - await settled(); - await listPage.create(); - await editPage.createSecretDontSave(secretPath, 'foo', 'bar'); - // to trigger warning need to hit keyup on the secret path - await triggerKeyEvent('[data-test-secret-path="true"]', 'keyup', 65); - - assert.dom('[data-test-whitespace-warning]').exists('renders warning about their being a space'); - await settled(); - await click('[data-test-secret-save]'); - - await click('[data-test-popup-menu-trigger="version"]'); - - await click('[data-test-version-history]'); - - assert.dom('[data-test-list-item-content]').exists('renders the version and not an error state'); - // click on version - await click('[data-test-popup-menu-trigger="true"]'); - await click('[data-test-version]'); - - // perform encode function that should be done by the encodePath - const encodedSecretPath = secretPath.replace(/ /g, '%20'); - assert.strictEqual(currentURL(), `/vault/secrets/${enginePath}/show/${encodedSecretPath}?version=1`); - await deleteEngine(enginePath, assert); - }); - - test('UI handles secret with % in path correctly', async function (assert) { - assert.expect(7); - const enginePath = `kv-engine-${this.uid}`; - const secretPath = 'per%cent/%fu ll'; - const [firstPath, secondPath] = secretPath.split('/'); - const commands = [`write sys/mounts/${enginePath} type=kv`, `write '${enginePath}/${secretPath}' 3=4`]; - await consoleComponent.runCommands(commands); - await listPage.visitRoot({ backend: enginePath }); - assert.dom(`[data-test-secret-link="${firstPath}/"]`).exists('First section item exists'); - await click(`[data-test-secret-link="${firstPath}/"]`); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${enginePath}/list/${encodeURIComponent(firstPath)}/`, - 'First part of path is encoded in URL' - ); - assert.dom(`[data-test-secret-link="${secretPath}"]`).exists('Link to secret exists'); - await click(`[data-test-secret-link="${secretPath}"]`); - assert.strictEqual( - currentURL(), - `/vault/secrets/${enginePath}/show/${encodeURIComponent(firstPath)}/${encodeURIComponent(secondPath)}`, - 'secret path is encoded in URL' - ); - assert.dom('h1').hasText(secretPath, 'Path renders correctly on show page'); - await click(`[data-test-secret-breadcrumb="${firstPath}"]`); - assert.strictEqual( - currentURL(), - `/vault/secrets/${enginePath}/list/${encodeURIComponent(firstPath)}/`, - 'Breadcrumb link encodes correctly' - ); - await deleteEngine(enginePath, assert); - }); - - // the web cli does not handle a quote as part of a path, so we test it here via the UI - test('creating a secret with a single or double quote works properly', async function (assert) { - assert.expect(5); - const backend = `kv-quotes-${this.uid}`; - await consoleComponent.runCommands(`write sys/mounts/${backend} type=kv`); - const paths = ["'some", '"some']; - for (const path of paths) { - await listPage.visitRoot({ backend }); - await listPage.create(); - await editPage.createSecret(`${path}/2`, 'foo', 'bar'); - await listPage.visit({ backend, id: path }); - assert.ok(listPage.secrets.filterBy('text', '2')[0], `${path}: secret is displayed properly`); - await listPage.secrets.filterBy('text', '2')[0].click(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - `${path}: show page renders correctly` - ); - } - await deleteEngine(backend, assert); - }); - - test('filter clears on nav', async function (assert) { - assert.expect(5); - const backend = 'test'; - await consoleComponent.runCommands([ - 'vault write sys/mounts/test type=kv', - 'refresh', - 'vault write test/filter/foo keys=a keys=b', - 'vault write test/filter/foo1 keys=a keys=b', - 'vault write test/filter/foo2 keys=a keys=b', - ]); - await listPage.visit({ backend, id: 'filter' }); - assert.strictEqual(listPage.secrets.length, 3, 'renders three secrets'); - await listPage.filterInput('filter/foo1'); - assert.strictEqual(listPage.secrets.length, 1, 'renders only one secret'); - await listPage.secrets.objectAt(0).click(); - await showPage.breadcrumbs.filterBy('text', 'filter')[0].click(); - assert.strictEqual(listPage.secrets.length, 3, 'renders three secrets'); - assert.strictEqual(listPage.filterInputValue, 'filter/', 'pageFilter has been reset'); - await deleteEngine(backend, assert); - }); - - // All policy tests below this line - test('version 2 with restricted policy still allows creation and does not show metadata tab', async function (assert) { - assert.expect(4); - const enginePath = 'dont-show-metadata-tab'; - const secretPath = 'dont-show-metadata-tab-secret-path'; - const V2_POLICY = ` - path "${enginePath}/metadata/*" { - capabilities = ["list"] - } - path "${enginePath}/data/${secretPath}" { - capabilities = ["create", "read", "update"] - } - `; - const userToken = await mountEngineGeneratePolicyToken(enginePath, secretPath, V2_POLICY); - await logout.visit(); - await authPage.login(userToken); - - await writeSecret(enginePath, secretPath, 'foo', 'bar'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - //check for metadata tab which should not show because you don't have read capabilities - assert.dom('[data-test-secret-metadata-tab]').doesNotExist('does not show metadata tab'); - await deleteEngine(enginePath, assert); - }); - - test('version 2 with no access to data but access to metadata shows metadata tab', async function (assert) { - assert.expect(5); - const enginePath = 'kv-metadata-access-only'; - const secretPath = 'nested/kv-metadata-access-only-secret-name'; - const V2_POLICY = ` - path "${enginePath}/metadata/nested/*" { - capabilities = ["read", "update"] - } - `; - - const userToken = await mountEngineGeneratePolicyToken(enginePath, secretPath, V2_POLICY); - await writeSecret(enginePath, secretPath, 'foo', 'bar'); - await logout.visit(); - await authPage.login(userToken); - await settled(); - await visit(`/vault/secrets/${enginePath}/show/${secretPath}`); - assert.dom('[data-test-empty-state-title]').hasText('You do not have permission to read this secret.'); - assert.dom('[data-test-secret-metadata-tab]').exists('Metadata tab exists'); - await editPage.metadataTab(); - await settled(); - assert.dom('[data-test-empty-state-title]').hasText('No custom metadata'); - assert.dom('[data-test-add-custom-metadata]').exists('it shows link to edit metadata'); - - await deleteEngine(enginePath, assert); - }); - - test('version 2: with metadata no read or list but with delete access and full access to the data endpoint', async function (assert) { - assert.expect(12); - const enginePath = 'no-metadata-read'; - const secretPath = 'no-metadata-read-secret-name'; - const V2_POLICY_NO_LIST = ` - path "${enginePath}/metadata/*" { - capabilities = ["update","delete"] - } - path "${enginePath}/data/*" { - capabilities = ["create", "read", "update", "delete"] - } - `; - const userToken = await mountEngineGeneratePolicyToken(enginePath, secretPath, V2_POLICY_NO_LIST); - await listPage.visitRoot({ backend: enginePath }); - // confirm they see an empty state and not the get-credentials card - assert.dom('[data-test-empty-state-title]').hasText('No secrets in this backend'); - await settled(); - await listPage.create(); - await settled(); - await editPage.createSecretWithMetadata(secretPath, 'secret-key', 'secret-value', 101); - await settled(); - await logout.visit(); - await settled(); - await authPage.login(userToken); - await settled(); - // test if metadata tab there with no read access message and no ability to edit. - await click(`[data-test-auth-backend-link=${enginePath}]`); - assert - .dom('[data-test-get-credentials]') - .exists( - 'They do not have list access so when logged in under the restricted policy they see the get-credentials-card' - ); - - await visit(`/vault/secrets/${enginePath}/show/${secretPath}`); - - assert - .dom('[data-test-value-div="secret-key"]') - .exists('secret view page and info table row with secret-key value'); - - // Create new version - assert.dom('[data-test-secret-edit]').doesNotHaveClass('disabled', 'Create new version is not disabled'); - await click('[data-test-secret-edit]'); - - // create new version should not include version in the URL - assert.strictEqual( - currentURL(), - `/vault/secrets/${enginePath}/edit/${secretPath}`, - 'edit route does not include version query param' - ); - // Update key - await editPage.secretKey('newKey'); - await editPage.secretValue('some-value'); - await editPage.save(); - assert.dom('[data-test-value-div="newKey"]').exists('Info row table exists at newKey'); - - // check metadata tab - await click('[data-test-secret-metadata-tab]'); - - assert - .dom('[data-test-empty-state-message]') - .hasText( - 'In order to edit secret metadata access, the UI requires read permissions; otherwise, data may be deleted. Edits can still be made via the API and CLI.' - ); - // destroy the version - await click('[data-test-secret-tab]'); - - await click('[data-test-delete-open-modal]'); - - assert.dom('.modal.is-active').exists('Modal appears'); - assert.dom('[data-test-delete-modal="destroy-all-versions"]').exists(); // we have a if Ember.testing catch in the delete action because it breaks things in testing - // we can however destroy the versions - await click('#destroy-all-versions'); - - await click('[data-test-modal-delete]'); - - assert.strictEqual(currentURL(), `/vault/secrets/${enginePath}/list`, 'brings you back to the list page'); - await visit(`/vault/secrets/${enginePath}/show/${secretPath}`); - - assert.dom('[data-test-secret-not-found]').exists('secret no longer found'); - await deleteEngine(enginePath, assert); - }); - - // KV delete operations testing - test('version 2 with policy with destroy capabilities shows modal', async function (assert) { - assert.expect(5); - const enginePath = 'kv-v2-destroy-capabilities'; - const secretPath = 'kv-v2-destroy-capabilities-secret-path'; - const V2_POLICY = ` - path "${enginePath}/destroy/*" { - capabilities = ["update"] - } - path "${enginePath}/metadata/*" { - capabilities = ["list", "update", "delete"] - } - path "${enginePath}/data/${secretPath}" { - capabilities = ["create", "read", "update"] - } - `; - const userToken = await mountEngineGeneratePolicyToken(enginePath, secretPath, V2_POLICY); - await logout.visit(); - await authPage.login(userToken); - - await writeSecret(enginePath, secretPath, 'foo', 'bar'); - await click('[data-test-delete-open-modal]'); - - assert.dom('[data-test-delete-modal="destroy-version"]').exists('destroy this version option shows'); - assert.dom('[data-test-delete-modal="destroy-all-versions"]').exists('destroy all versions option shows'); - assert.dom('[data-test-delete-modal="delete-version"]').doesNotExist('delete version does not show'); - - // because destroy requires a page refresh (making the test suite run in a loop) this action is caught in ember testing and does not refresh. - // therefore to show new state change after modal closes we jump to the metadata tab and then back. - await click('#destroy-version'); - await settled(); // eslint-disable-line - await click('[data-test-modal-delete]'); - await settled(); // eslint-disable-line - await click('[data-test-secret-metadata-tab]'); - await settled(); // eslint-disable-line - await click('[data-test-secret-tab]'); - await settled(); // eslint-disable-line - assert - .dom('[data-test-empty-state-title]') - .includesText('Version 1 of this secret has been permanently destroyed'); - await deleteEngine(enginePath, assert); - }); - - test('version 2 with policy with only delete option does not show modal and undelete is an option', async function (assert) { - assert.expect(5); - const enginePath = 'kv-v2-only-delete'; - const secretPath = 'kv-v2-only-delete-secret-path'; - const V2_POLICY = ` - path "${enginePath}/delete/*" { - capabilities = ["update"] - } - path "${enginePath}/undelete/*" { - capabilities = ["update"] - } - path "${enginePath}/metadata/*" { - capabilities = ["list","read","create","update"] - } - path "${enginePath}/data/${secretPath}" { - capabilities = ["create", "read"] - } - `; - const userToken = await mountEngineGeneratePolicyToken(enginePath, secretPath, V2_POLICY); - await logout.visit(); - await authPage.login(userToken); - await writeSecret(enginePath, secretPath, 'foo', 'bar'); - assert.dom('[data-test-delete-open-modal]').doesNotExist('delete version does not show'); - assert.dom('[data-test-secret-v2-delete="true"]').exists('drop down delete shows'); - await showPage.deleteSecretV2(); - // unable to reload page in test scenario so going to list and back to secret to confirm deletion - const url = `/vault/secrets/${enginePath}/list`; - await visit(url); - - await click(`[data-test-secret-link="${secretPath}"]`); - await settled(); // eslint-disable-line - assert.dom('[data-test-component="empty-state"]').exists('secret has been deleted'); - assert.dom('[data-test-secret-undelete]').exists('undelete button shows'); - await deleteEngine(enginePath, assert); - }); - - test('version 2: policy includes "delete" capability for secret path but does not have "update" to /delete endpoint', async function (assert) { - assert.expect(4); - const enginePath = 'kv-v2-soft-delete-only'; - const secretPath = 'kv-v2-delete-capability-not-path'; - const policy = ` - path "${enginePath}/data/${secretPath}" { capabilities = ["create","read","update","delete","list"] } - path "${enginePath}/metadata/*" { capabilities = ["create","update","delete","list","read"] } - path "${enginePath}/undelete/*" { capabilities = ["update"] } - `; - const userToken = await mountEngineGeneratePolicyToken(enginePath, secretPath, policy); - await logout.visit(); - await authPage.login(userToken); - await writeSecret(enginePath, secretPath, 'foo', 'bar'); - // create multiple versions - await click('[data-test-secret-edit]'); - await editPage.editSecret('foo2', 'bar2'); - await click('[data-test-secret-edit]'); - await editPage.editSecret('foo3', 'bar3'); - // delete oldest version - await click('[data-test-popup-menu-trigger="version"]'); - await click('[data-test-version-dropdown-link="1"]'); - await click('[data-test-delete-open-modal]'); - assert - .dom('[data-test-type-select="delete-version"]') - .hasText('Delete latest version', 'modal reads that it will delete latest version'); - await click('input#delete-version'); - await click('[data-test-modal-delete]'); - await visit(`/vault/secrets/${enginePath}/show/${secretPath}?version=3`); - assert - .dom('[data-test-empty-state-title]') - .hasText( - 'Version 3 of this secret has been deleted', - 'empty state renders latest version has been deleted' - ); - await visit(`/vault/secrets/${enginePath}/show/${secretPath}?version=1`); - assert.dom('[data-test-delete-open-modal]').hasText('Delete', 'version 1 has not been deleted'); - await deleteEngine(enginePath, assert); - }); - - test('version 2: policy has "update" to /delete endpoint but not "delete" capability for secret path', async function (assert) { - assert.expect(5); - const enginePath = 'kv-v2-can-delete-version'; - const secretPath = 'kv-v2-delete-path-not-capability'; - const policy = ` - path "${enginePath}/data/${secretPath}" { capabilities = ["create","read","update","list"] } - path "${enginePath}/metadata/*" { capabilities = ["create","update","delete","list","read"] } - path "${enginePath}/undelete/*" { capabilities = ["update"] } - path "${enginePath}/delete/*" { capabilities = ["update"] } - `; - const userToken = await mountEngineGeneratePolicyToken(enginePath, secretPath, policy); - await logout.visit(); - await authPage.login(userToken); - await writeSecret(enginePath, secretPath, 'foo', 'bar'); - // create multiple versions - await click('[data-test-secret-edit]'); - await editPage.editSecret('foo2', 'bar2'); - await click('[data-test-secret-edit]'); - await editPage.editSecret('foo3', 'bar3'); - // delete oldest version - await click('[data-test-popup-menu-trigger="version"]'); - await click('[data-test-version-dropdown-link="1"]'); - await click('[data-test-delete-open-modal]'); - assert - .dom('[data-test-type-select="delete-version"]') - .hasText('Delete this version', 'delete option refers to "this" version'); - assert - .dom('[data-test-delete-modal="delete-version"]') - .hasTextContaining('Version 1', 'modal reads that it will delete version 1'); - await click('input#delete-version'); - await click('[data-test-modal-delete]'); - await visit(`/vault/secrets/${enginePath}/show/${secretPath}?version=3`); - assert.dom('[data-test-delete-open-modal]').hasText('Delete', 'latest version (3) has not been deleted'); - await visit(`/vault/secrets/${enginePath}/show/${secretPath}?version=1`); - assert - .dom('[data-test-empty-state-title]') - .hasText( - 'Version 1 of this secret has been deleted', - 'empty state renders oldest version (1) has been deleted' - ); - await deleteEngine(enginePath, assert); - }); - - test('version 2 with path forward slash will show delete button', async function (assert) { - assert.expect(2); - const enginePath = 'kv-v2-forward-slash'; - const secretPath = 'forward/slash'; - const V2_POLICY = ` - path "${enginePath}/delete/${secretPath}" { - capabilities = ["update"] - } - path "${enginePath}/metadata/*" { - capabilities = ["list","read","create","update"] - } - path "${enginePath}/data/${secretPath}" { - capabilities = ["create", "read"] - } - `; - const userToken = await mountEngineGeneratePolicyToken(enginePath, secretPath, V2_POLICY); - await logout.visit(); - await authPage.login(userToken); - await writeSecret(enginePath, secretPath, 'foo', 'bar'); - assert.dom('[data-test-secret-v2-delete="true"]').exists('drop down delete shows'); - await deleteEngine(enginePath, assert); - }); - - test('version 2 with engine with forward slash will show delete button', async function (assert) { - assert.expect(2); - const enginePath = 'forward/slash'; - const secretPath = 'secret-name'; - const V2_POLICY = ` - path "${enginePath}/delete/${secretPath}" { - capabilities = ["update"] - } - path "${enginePath}/metadata/*" { - capabilities = ["list","read","create","update"] - } - path "${enginePath}/data/*" { - capabilities = ["create", "read"] - } - `; - const userToken = await mountEngineGeneratePolicyToken(enginePath, secretPath, V2_POLICY); - await logout.visit(); - await authPage.login(userToken); - await writeSecret(enginePath, secretPath, 'foo', 'bar'); - assert.dom('[data-test-secret-v2-delete="true"]').exists('drop down delete shows'); - await deleteEngine(enginePath, assert); - }); - - const setupNoRead = async function (backend, canReadMeta = false) { - const V2_WRITE_ONLY_POLICY = ` - path "${backend}/+/+" { - capabilities = ["create", "update", "list"] - } - path "${backend}/+" { - capabilities = ["list"] - } - `; - - const V2_WRITE_WITH_META_READ_POLICY = ` - path "${backend}/+/+" { - capabilities = ["create", "update", "list"] - } - path "${backend}/metadata/+" { - capabilities = ["read"] - } - path "${backend}/+" { - capabilities = ["list"] - } - `; - const V1_WRITE_ONLY_POLICY = ` - path "${backend}/+" { - capabilities = ["create", "update", "list"] - } - `; - - const version = backend === 'kv-v2' ? 2 : 1; - let policy; - if (backend === 'kv-v2' && canReadMeta) { - policy = V2_WRITE_WITH_META_READ_POLICY; - } else if (backend === 'kv-v2') { - policy = V2_WRITE_ONLY_POLICY; - } else if (backend === 'kv-v1') { - policy = V1_WRITE_ONLY_POLICY; - } - - return await mountEngineGeneratePolicyToken(backend, 'nonexistent-secret', policy, version); - }; - test('write without read: version 2', async function (assert) { - assert.expect(5); - const backend = 'kv-v2'; - const userToken = await setupNoRead(backend); - await writeSecret(backend, 'secret', 'foo', 'bar'); - await logout.visit(); - await authPage.login(userToken); - - await showPage.visit({ backend, id: 'secret' }); - assert.ok(showPage.noReadIsPresent, 'shows no read empty state'); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - - await editPage.visitEdit({ backend, id: 'secret' }); - assert.notOk(editPage.hasMetadataFields, 'hides the metadata form'); - - await editPage.editSecret('bar', 'baz'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - await deleteEngine(backend, assert); - }); - - test('write without read: version 2 with metadata read', async function (assert) { - assert.expect(5); - const backend = 'kv-v2'; - const userToken = await setupNoRead(backend, true); - await writeSecret(backend, 'secret', 'foo', 'bar'); - await logout.visit(); - await authPage.login(userToken); - - await showPage.visit({ backend, id: 'secret' }); - assert.ok(showPage.noReadIsPresent, 'shows no read empty state'); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - - await editPage.visitEdit({ backend, id: 'secret' }); - assert - .dom('[data-test-warning-no-read-permissions]') - .exists('shows custom warning instead of default API warning about permissions'); - - await editPage.editSecret('bar', 'baz'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - await deleteEngine(backend, assert); - }); - - test('write without read: version 1', async function (assert) { - assert.expect(4); - const backend = 'kv-v1'; - const userToken = await setupNoRead(backend); - await writeSecret(backend, 'secret', 'foo', 'bar'); - await logout.visit(); - await authPage.login(userToken); - - await showPage.visit({ backend, id: 'secret' }); - assert.ok(showPage.noReadIsPresent, 'shows no read empty state'); - assert.ok(showPage.editIsPresent, 'shows the edit button'); - - await editPage.visitEdit({ backend, id: 'secret' }); - await editPage.editSecret('bar', 'baz'); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - await deleteEngine(backend, assert); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/ssh/role-test.js b/ui/tests/acceptance/secrets/backend/ssh/role-test.js deleted file mode 100644 index fee830da1..000000000 --- a/ui/tests/acceptance/secrets/backend/ssh/role-test.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import editPage from 'vault/tests/pages/secrets/backend/ssh/edit-role'; -import showPage from 'vault/tests/pages/secrets/backend/ssh/show'; -import generatePage from 'vault/tests/pages/secrets/backend/ssh/generate-otp'; -import listPage from 'vault/tests/pages/secrets/backend/list'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | secrets/ssh', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - const mountAndNav = async (uid) => { - const path = `ssh-${uid}`; - await enablePage.enable('ssh', path); - await settled(); - await editPage.visitRoot({ backend: path }); - await settled(); - return path; - }; - - test('it creates a role and redirects', async function (assert) { - assert.expect(5); - const path = await mountAndNav(this.uid); - await editPage.createOTPRole('role'); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - assert.ok(showPage.generateIsPresent, 'shows the generate button'); - - await showPage.visit({ backend: path, id: 'role' }); - await settled(); - await showPage.generate(); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.credentials', - 'navs to the credentials page' - ); - - await listPage.visitRoot({ backend: path }); - await settled(); - assert.strictEqual(listPage.secrets.length, 1, 'shows role in the list'); - const secret = listPage.secrets.objectAt(0); - await secret.menuToggle(); - assert.ok(listPage.menuItems.length > 0, 'shows links in the menu'); - }); - - test('it deletes a role', async function (assert) { - assert.expect(2); - const path = await mountAndNav(this.uid); - await editPage.createOTPRole('role'); - await settled(); - await showPage.visit({ backend: path, id: 'role' }); - await settled(); - await showPage.deleteRole(); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.list-root', - 'redirects to list page' - ); - assert.ok(listPage.backendIsEmpty, 'no roles listed'); - }); - - test('it generates an OTP', async function (assert) { - assert.expect(6); - const path = await mountAndNav(this.uid); - await editPage.createOTPRole('role'); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.show', - 'redirects to the show page' - ); - assert.ok(showPage.generateIsPresent, 'shows the generate button'); - - await showPage.visit({ backend: path, id: 'role' }); - await settled(); - await showPage.generate(); - await settled(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.credentials', - 'navs to the credentials page' - ); - - await generatePage.generateOTP(); - await settled(); - assert.ok(generatePage.warningIsPresent, 'shows warning'); - await generatePage.back(); - await settled(); - assert.ok(generatePage.userIsPresent, 'clears generate, shows user input'); - assert.ok(generatePage.ipIsPresent, 'clears generate, shows ip input'); - }); -}); diff --git a/ui/tests/acceptance/settings-test.js b/ui/tests/acceptance/settings-test.js deleted file mode 100644 index 28eabb7dc..000000000 --- a/ui/tests/acceptance/settings-test.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentURL, find, visit, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import backendListPage from 'vault/tests/pages/secrets/backends'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; - -module('Acceptance | settings', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - test('settings', async function (assert) { - const type = 'consul'; - const path = `settings-path-${this.uid}`; - - // mount unsupported backend - await visit('/vault/settings/mount-secret-backend'); - - assert.strictEqual(currentURL(), '/vault/settings/mount-secret-backend'); - - await mountSecrets.selectType(type); - await mountSecrets - .next() - .path(path) - .toggleOptions() - .enableDefaultTtl() - .defaultTTLUnit('s') - .defaultTTLVal(100) - .submit(); - await settled(); - assert.ok( - find('[data-test-flash-message]').textContent.trim(), - `Successfully mounted '${type}' at '${path}'!` - ); - await settled(); - assert.strictEqual(currentURL(), `/vault/secrets`, 'redirects to secrets page'); - const row = backendListPage.rows.filterBy('path', path + '/')[0]; - await row.menu(); - await backendListPage.configLink(); - assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration`, 'navigates to the config page'); - }); -}); diff --git a/ui/tests/acceptance/settings/auth/configure/index-test.js b/ui/tests/acceptance/settings/auth/configure/index-test.js deleted file mode 100644 index 90b68ee92..000000000 --- a/ui/tests/acceptance/settings/auth/configure/index-test.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentURL, currentRouteName } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import enablePage from 'vault/tests/pages/settings/auth/enable'; -import page from 'vault/tests/pages/settings/auth/configure/index'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | settings/auth/configure', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('it redirects to section options when there are no other sections', async function (assert) { - const path = `approle-config-${this.uid}`; - const type = 'approle'; - await enablePage.enable(type, path); - await page.visit({ path }); - assert.strictEqual(currentRouteName(), 'vault.cluster.settings.auth.configure.section'); - assert.strictEqual( - currentURL(), - `/vault/settings/auth/configure/${path}/options`, - 'loads the options route' - ); - }); - - test('it redirects to the first section', async function (assert) { - const path = `aws-redirect-${this.uid}`; - const type = 'aws'; - await enablePage.enable(type, path); - await page.visit({ path }); - assert.strictEqual(currentRouteName(), 'vault.cluster.settings.auth.configure.section'); - assert.strictEqual( - currentURL(), - `/vault/settings/auth/configure/${path}/client`, - 'loads the first section for the type of auth method' - ); - }); -}); diff --git a/ui/tests/acceptance/settings/auth/configure/section-test.js b/ui/tests/acceptance/settings/auth/configure/section-test.js deleted file mode 100644 index e9c0539c3..000000000 --- a/ui/tests/acceptance/settings/auth/configure/section-test.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; -import { fillIn } from '@ember/test-helpers'; -import { v4 as uuidv4 } from 'uuid'; - -import enablePage from 'vault/tests/pages/settings/auth/enable'; -import page from 'vault/tests/pages/settings/auth/configure/section'; -import indexPage from 'vault/tests/pages/settings/auth/configure/index'; -import apiStub from 'vault/tests/helpers/noop-all-api-requests'; -import consolePanel from 'vault/tests/pages/components/console/ui-panel'; -import authPage from 'vault/tests/pages/auth'; - -const cli = create(consolePanel); - -module('Acceptance | settings/auth/configure/section', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - this.server = apiStub({ usePassthrough: true }); - return authPage.login(); - }); - - hooks.afterEach(function () { - this.server.shutdown(); - }); - - test('it can save options', async function (assert) { - const path = `approle-save-${this.uid}`; - const type = 'approle'; - const section = 'options'; - await enablePage.enable(type, path); - await page.visit({ path, section }); - await page.fillInTextarea('description', 'This is AppRole!'); - assert - .dom('[data-test-input="config.tokenType"]') - .hasValue('default-service', 'as default the token type selected is default-service.'); - await fillIn('[data-test-input="config.tokenType"]', 'batch'); - await page.save(); - assert.strictEqual( - page.flash.latestMessage, - `The configuration was saved successfully.`, - 'success flash shows' - ); - const tuneRequest = this.server.passthroughRequests.filterBy( - 'url', - `/v1/sys/mounts/auth/${path}/tune` - )[0]; - const keys = Object.keys(JSON.parse(tuneRequest.requestBody)); - const token_type = JSON.parse(tuneRequest.requestBody).token_type; - assert.strictEqual(token_type, 'batch', 'passes new token type'); - assert.ok(keys.includes('default_lease_ttl'), 'passes default_lease_ttl on tune'); - assert.ok(keys.includes('max_lease_ttl'), 'passes max_lease_ttl on tune'); - assert.ok(keys.includes('description'), 'passes updated description on tune'); - }); - - for (const type of ['aws', 'azure', 'gcp', 'github', 'kubernetes']) { - test(`it shows tabs for auth method: ${type}`, async function (assert) { - const path = `${type}-showtab-${this.uid}`; - await cli.consoleInput(`write sys/auth/${path} type=${type}`); - await cli.enter(); - await indexPage.visit({ path }); - // aws has 4 tabs, the others will have 'Configuration' and 'Method Options' tabs - const numTabs = type === 'aws' ? 4 : 2; - assert.strictEqual(page.tabs.length, numTabs, 'shows correct number of tabs'); - }); - } -}); diff --git a/ui/tests/acceptance/settings/auth/enable-test.js b/ui/tests/acceptance/settings/auth/enable-test.js deleted file mode 100644 index 45a04123e..000000000 --- a/ui/tests/acceptance/settings/auth/enable-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import page from 'vault/tests/pages/settings/auth/enable'; -import listPage from 'vault/tests/pages/access/methods'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | settings/auth/enable', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('it mounts and redirects', async function (assert) { - // always force the new mount to the top of the list - const path = `aaa-approle-${this.uid}`; - const type = 'approle'; - await page.visit(); - assert.strictEqual(currentRouteName(), 'vault.cluster.settings.auth.enable'); - await page.enable(type, path); - await settled(); - await assert.strictEqual( - page.flash.latestMessage, - `Successfully mounted the ${type} auth method at ${path}.`, - 'success flash shows' - ); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.settings.auth.configure.section', - 'redirects to the auth config page' - ); - - await listPage.visit(); - assert.ok(listPage.findLinkById(path), 'mount is present in the list'); - }); -}); diff --git a/ui/tests/acceptance/settings/configure-secret-backends/configure-ssh-secret-test.js b/ui/tests/acceptance/settings/configure-secret-backends/configure-ssh-secret-test.js deleted file mode 100644 index 80ffda260..000000000 --- a/ui/tests/acceptance/settings/configure-secret-backends/configure-ssh-secret-test.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import { visit } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { create } from 'ember-cli-page-object'; -import fm from 'vault/tests/pages/components/flash-message'; -const flashMessage = create(fm); -const SELECTORS = { - generateSigningKey: '[data-test-ssh-input="generate-signing-key-checkbox"]', - saveConfig: '[data-test-ssh-input="configure-submit"]', - publicKey: '[data-test-ssh-input="public-key"]', -}; -module('Acceptance | settings/configure/secrets/ssh', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('it configures ssh ca', async function (assert) { - const path = `ssh-configure-${this.uid}`; - await enablePage.enable('ssh', path); - await settled(); - visit(`/vault/settings/secrets/configure/${path}`); - await settled(); - assert.dom(SELECTORS.generateSigningKey).isChecked('generate_signing_key defaults to true'); - await click(SELECTORS.generateSigningKey); - await click(SELECTORS.saveConfig); - assert.strictEqual( - flashMessage.latestMessage, - 'missing public_key', - 'renders warning flash message for failed save' - ); - await click(SELECTORS.generateSigningKey); - await click(SELECTORS.saveConfig); - assert.dom(SELECTORS.publicKey).exists('renders public key after saving config'); - }); -}); diff --git a/ui/tests/acceptance/settings/mount-secret-backend-test.js b/ui/tests/acceptance/settings/mount-secret-backend-test.js deleted file mode 100644 index fd08c3720..000000000 --- a/ui/tests/acceptance/settings/mount-secret-backend-test.js +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { currentRouteName, currentURL, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import { create } from 'ember-cli-page-object'; -import page from 'vault/tests/pages/settings/mount-secret-backend'; -import configPage from 'vault/tests/pages/secrets/backend/configuration'; -import authPage from 'vault/tests/pages/auth'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import logout from 'vault/tests/pages/logout'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import { allEngines } from 'vault/helpers/mountable-secret-engines'; - -const consoleComponent = create(consoleClass); - -module('Acceptance | settings/mount-secret-backend', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('it sets the ttl correctly when mounting', async function (assert) { - // always force the new mount to the top of the list - const path = `mount-kv-${this.uid}`; - const defaultTTLHours = 100; - const maxTTLHours = 300; - - await page.visit(); - - assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); - await page.selectType('kv'); - await page - .next() - .path(path) - .toggleOptions() - .enableDefaultTtl() - .defaultTTLUnit('h') - .defaultTTLVal(defaultTTLHours) - .enableMaxTtl() - .maxTTLUnit('h') - .maxTTLVal(maxTTLHours) - .submit(); - await configPage.visit({ backend: path }); - assert.strictEqual(configPage.defaultTTL, `${defaultTTLHours}h`, 'shows the proper TTL'); - assert.strictEqual(configPage.maxTTL, `${maxTTLHours}h`, 'shows the proper max TTL'); - }); - - test('it sets the ttl when enabled then disabled', async function (assert) { - // always force the new mount to the top of the list - const path = `mount-kv-${this.uid}`; - const maxTTLHours = 300; - - await page.visit(); - - assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); - await page.selectType('kv'); - await page - .next() - .path(path) - .toggleOptions() - .enableDefaultTtl() - .enableDefaultTtl() - .enableMaxTtl() - .maxTTLUnit('h') - .maxTTLVal(maxTTLHours) - .submit(); - await configPage.visit({ backend: path }); - assert.strictEqual(configPage.defaultTTL, '0s', 'shows the proper TTL'); - assert.strictEqual(configPage.maxTTL, `${maxTTLHours}h`, 'shows the proper max TTL'); - }); - - test('it sets the max ttl after pki chosen, resets after', async function (assert) { - await page.visit(); - assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); - await page.selectType('pki'); - await page.next(); - assert.dom('[data-test-input="maxLeaseTtl"]').exists(); - assert - .dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-toggle]') - .isChecked('Toggle is checked by default'); - assert.dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-value]').hasValue('3650'); - assert.dom('[data-test-input="maxLeaseTtl"] [data-test-select="ttl-unit"]').hasValue('d'); - - // Go back and choose a different type - await page.back(); - await page.selectType('database'); - await page.next(); - assert.dom('[data-test-input="maxLeaseTtl"]').exists('3650'); - assert - .dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-toggle]') - .isNotChecked('Toggle is unchecked by default'); - await page.enableMaxTtl(); - assert.dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-value]').hasValue(''); - assert.dom('[data-test-input="maxLeaseTtl"] [data-test-select="ttl-unit"]').hasValue('s'); - }); - - test('it throws error if setting duplicate path name', async function (assert) { - const path = `kv-duplicate`; - - await consoleComponent.runCommands([ - // delete any kv-duplicate previously written here so that tests can be re-run - `delete sys/mounts/${path}`, - ]); - - await page.visit(); - - assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); - await page.selectType('kv'); - await page.next().path(path).submit(); - await page.secretList(); - await settled(); - await page.enableEngine(); - await page.selectType('kv'); - await page.next().path(path).submit(); - assert.dom('[data-test-alert-banner="alert"]').containsText(`path is already in use at ${path}`); - assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); - - await page.secretList(); - await settled(); - assert - .dom(`[data-test-auth-backend-link=${path}]`) - .exists({ count: 1 }, 'renders only one instance of the engine'); - }); - - test('version 2 with no update to config endpoint still allows mount of secret engine', async function (assert) { - const enginePath = `kv-noUpdate-${this.uid}`; - const V2_POLICY = ` - path "${enginePath}/*" { - capabilities = ["list","create","read","sudo","delete"] - } - path "sys/mounts/*" - { - capabilities = ["create", "read", "update", "delete", "list", "sudo"] - } - - # List existing secrets engines. - path "sys/mounts" - { - capabilities = ["read"] - } - # Allow page to load after mount - path "sys/internal/ui/mounts/${enginePath}" { - capabilities = ["read"] - } - `; - await consoleComponent.runCommands([ - // delete any previous mount with same name - `delete sys/mounts/${enginePath}`, - `write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`, - 'write -field=client_token auth/token/create policies=kv-v2-degrade', - ]); - await settled(); - const userToken = consoleComponent.lastLogOutput; - await logout.visit(); - await authPage.login(userToken); - // create the engine - await mountSecrets.visit(); - await mountSecrets.selectType('kv'); - await mountSecrets.next().path(enginePath).setMaxVersion(101).submit(); - await settled(); - assert - .dom('[data-test-flash-message]') - .containsText( - `You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.` - ); - assert.strictEqual( - currentURL(), - `/vault/secrets/${enginePath}/list`, - 'After mounting, redirects to secrets list page' - ); - await configPage.visit({ backend: enginePath }); - await settled(); - assert.dom('[data-test-row-value="Maximum number of versions"]').hasText('Not set'); - }); - - test('it should transition to engine route on success if defined in mount config', async function (assert) { - await consoleComponent.runCommands([ - // delete any previous mount with same name - `delete sys/mounts/kubernetes`, - ]); - await mountSecrets.visit(); - await mountSecrets.selectType('kubernetes'); - await mountSecrets.next().path('kubernetes').submit(); - const { engineRoute } = allEngines().findBy('type', 'kubernetes'); - assert.strictEqual( - currentRouteName(), - `vault.cluster.secrets.backend.${engineRoute}`, - 'Transitions to engine route on mount success' - ); - }); -}); diff --git a/ui/tests/acceptance/sidebar-nav-test.js b/ui/tests/acceptance/sidebar-nav-test.js deleted file mode 100644 index 4cf32509d..000000000 --- a/ui/tests/acceptance/sidebar-nav-test.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, currentURL } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import authPage from 'vault/tests/pages/auth'; -import modifyPassthroughResponse from 'vault/mirage/helpers/modify-passthrough-response'; - -const link = (label) => `[data-test-sidebar-nav-link="${label}"]`; -const panel = (label) => `[data-test-sidebar-nav-panel="${label}"]`; - -module('Acceptance | sidebar navigation', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - // set storage_type to raft to test link - this.server.get('/sys/seal-status', (schema, req) => { - return modifyPassthroughResponse(req, { storage_type: 'raft' }); - }); - this.server.get('/sys/storage/raft/configuration', () => this.server.create('configuration', 'withRaft')); - - return authPage.login(); - }); - - test('it should link to correct routes at the cluster level', async function (assert) { - assert.expect(10); - - assert.dom(panel('Cluster')).exists('Cluster nav panel renders'); - - const subNavs = [ - { label: 'Access', route: 'access' }, - { label: 'Policies', route: 'policies/acl' }, - { label: 'Tools', route: 'tools/wrap' }, - ]; - - for (const subNav of subNavs) { - const { label, route } = subNav; - await click(link(label)); - assert.strictEqual(currentURL(), `/vault/${route}`, `${label} route renders`); - assert.dom(panel(label)).exists(`${label} nav panel renders`); - await click(link('Back to main navigation')); - } - - const links = [ - { label: 'Raft Storage', route: '/vault/storage/raft' }, - { label: 'Seal Vault', route: '/vault/settings/seal' }, - { label: 'Secrets engines', route: '/vault/secrets' }, - ]; - - for (const l of links) { - await click(link(l.label)); - assert.strictEqual(currentURL(), l.route, `${l.label} route renders`); - } - }); - - test('it should link to correct routes at the access level', async function (assert) { - assert.expect(7); - - await click(link('Access')); - assert.dom(panel('Access')).exists('Access nav panel renders'); - - const links = [ - { label: 'Multi-factor authentication', route: '/vault/access/mfa' }, - { label: 'OIDC provider', route: '/vault/access/oidc' }, - { label: 'Groups', route: '/vault/access/identity/groups' }, - { label: 'Entities', route: '/vault/access/identity/entities' }, - { label: 'Leases', route: '/vault/access/leases/list' }, - { label: 'Authentication methods', route: '/vault/access' }, - ]; - - for (const l of links) { - await click(link(l.label)); - assert.ok(currentURL().includes(l.route), `${l.label} route renders`); - } - }); - - test('it should link to correct routes at the policies level', async function (assert) { - assert.expect(2); - - await click(link('Policies')); - assert.dom(panel('Policies')).exists('Access nav panel renders'); - - await click(link('ACL Policies')); - assert.strictEqual(currentURL(), '/vault/policies/acl', 'ACL Policies route renders'); - }); - - test('it should link to correct routes at the tools level', async function (assert) { - assert.expect(7); - - await click(link('Tools')); - assert.dom(panel('Tools')).exists('Access nav panel renders'); - - const links = [ - { label: 'Wrap', route: '/vault/tools/wrap' }, - { label: 'Lookup', route: '/vault/tools/lookup' }, - { label: 'Unwrap', route: '/vault/tools/unwrap' }, - { label: 'Rewrap', route: '/vault/tools/rewrap' }, - { label: 'Random', route: '/vault/tools/random' }, - { label: 'Hash', route: '/vault/tools/hash' }, - ]; - - for (const l of links) { - await click(link(l.label)); - assert.strictEqual(currentURL(), l.route, `${l.label} route renders`); - } - }); - - test('it should display access nav when mounting and configuring auth methods', async function (assert) { - await click(link('Access')); - await click('[data-test-auth-enable]'); - assert.dom('[data-test-sidebar-nav-panel="Access"]').exists('Access nav panel renders'); - await click(link('Authentication methods')); - await click('[data-test-auth-backend-link="token"]'); - await click('[data-test-configure-link]'); - assert.dom('[data-test-sidebar-nav-panel="Access"]').exists('Access nav panel renders'); - }); -}); diff --git a/ui/tests/acceptance/ssh-test.js b/ui/tests/acceptance/ssh-test.js deleted file mode 100644 index 8a4be81ff..000000000 --- a/ui/tests/acceptance/ssh-test.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, fillIn, findAll, currentURL, find, settled, waitUntil } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; - -module('Acceptance | ssh secret backend', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - const PUB_KEY = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCn9p5dHNr4aU4R2W7ln+efzO5N2Cdv/SXk6zbCcvhWcblWMjkXf802B0PbKvf6cJIzM/Xalb3qz1cK+UUjCSEAQWefk6YmfzbOikfc5EHaSKUqDdE+HlsGPvl42rjCr28qYfuYh031YfwEQGEAIEypo7OyAj+38NLbHAQxDxuaReee1YCOV5rqWGtEgl2VtP5kG+QEBza4ZfeglS85f/GGTvZC4Jq1GX+wgmFxIPnd6/mUXa4ecoR0QMfOAzzvPm4ajcNCQORfHLQKAcmiBYMiyQJoU+fYpi9CJGT1jWTmR99yBkrSg6yitI2qqXyrpwAbhNGrM0Fw0WpWxh66N9Xp meirish@Macintosh-3.local`; - - const ROLES = [ - { - type: 'ca', - name: 'carole', - async fillInCreate() { - await click('[data-test-input="allowUserCertificates"]'); - }, - async fillInGenerate() { - await fillIn('[data-test-input="publicKey"]', PUB_KEY); - await click('[data-test-toggle-button]'); - - await click('[data-test-toggle-label="TTL"]'); - await fillIn('[data-test-select="ttl-unit"]', 'm'); - - document.querySelector('[data-test-ttl-value="TTL"]').value = 30; - }, - assertBeforeGenerate(assert) { - assert.dom('[data-test-form-field-from-model]').exists('renders the FormFieldFromModel'); - const value = document.querySelector('[data-test-ttl-value="TTL"]').value; - // confirms that the actions are correctly being passed down to the FormFieldFromModel component - assert.strictEqual(value, '30', 'renders action updateTtl'); - }, - assertAfterGenerate(assert, sshPath) { - assert.strictEqual( - currentURL(), - `/vault/secrets/${sshPath}/sign/${this.name}`, - 'ca sign url is correct' - ); - assert.dom('[data-test-row-label="Signed key"]').exists({ count: 1 }, 'renders the signed key'); - assert - .dom('[data-test-row-value="Signed key"]') - .exists({ count: 1 }, "renders the signed key's value"); - assert.dom('[data-test-row-label="Serial number"]').exists({ count: 1 }, 'renders the serial'); - assert.dom('[data-test-row-value="Serial number"]').exists({ count: 1 }, 'renders the serial value'); - }, - }, - { - type: 'otp', - name: 'otprole', - async fillInCreate() { - await fillIn('[data-test-input="defaultUser"]', 'admin'); - await click('[data-test-toggle-group="Options"]'); - await fillIn('[data-test-input="cidrList"]', '1.2.3.4/32'); - }, - async fillInGenerate() { - await fillIn('[data-test-input="username"]', 'admin'); - await fillIn('[data-test-input="ip"]', '1.2.3.4'); - }, - assertAfterGenerate(assert, sshPath) { - assert.strictEqual( - currentURL(), - `/vault/secrets/${sshPath}/credentials/${this.name}`, - 'otp credential url is correct' - ); - assert.dom('[data-test-row-label="Key"]').exists({ count: 1 }, 'renders the key'); - assert.dom('[data-test-masked-input]').exists({ count: 1 }, 'renders mask for key value'); - assert.dom('[data-test-row-label="Port"]').exists({ count: 1 }, 'renders the port'); - assert.dom('[data-test-row-value="Port"]').exists({ count: 1 }, "renders the port's value"); - }, - }, - ]; - test('ssh backend', async function (assert) { - assert.expect(28); - const sshPath = `ssh-${this.uid}`; - - await enablePage.enable('ssh', sshPath); - await settled(); - await click('[data-test-configuration-tab]'); - - await click('[data-test-secret-backend-configure]'); - - assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${sshPath}`); - assert.ok(findAll('[data-test-ssh-configure-form]').length, 'renders the empty configuration form'); - - // default has generate CA checked so we just submit the form - await click('[data-test-ssh-input="configure-submit"]'); - - assert.ok( - await waitUntil(() => findAll('[data-test-ssh-input="public-key"]').length), - 'a public key is fetched' - ); - await click('[data-test-backend-view-link]'); - - assert.strictEqual(currentURL(), `/vault/secrets/${sshPath}/list`, `redirects to ssh index`); - - for (const role of ROLES) { - // create a role - await click('[ data-test-secret-create]'); - - assert.ok( - find('[data-test-secret-header]').textContent.includes('SSH role'), - `${role.type}: renders the create page` - ); - - await fillIn('[data-test-input="name"]', role.name); - await fillIn('[data-test-input="keyType"]', role.type); - await role.fillInCreate(); - await settled(); - - // save the role - await click('[data-test-role-ssh-create]'); - await waitUntil(() => currentURL() === `/vault/secrets/${sshPath}/show/${role.name}`); // flaky without this - assert.strictEqual( - currentURL(), - `/vault/secrets/${sshPath}/show/${role.name}`, - `${role.type}: navigates to the show page on creation` - ); - - // sign a key with this role - await click('[data-test-backend-credentials]'); - - await role.fillInGenerate(); - if (role.type === 'ca') { - await settled(); - role.assertBeforeGenerate(assert); - } - - // generate creds - await click('[data-test-secret-generate]'); - await settled(); // eslint-disable-line - role.assertAfterGenerate(assert, sshPath); - - // click the "Back" button - await click('[data-test-secret-generate-back]'); - - assert.ok( - findAll('[data-test-secret-generate-form]').length, - `${role.type}: back takes you back to the form` - ); - - await click('[data-test-secret-generate-cancel]'); - - assert.strictEqual( - currentURL(), - `/vault/secrets/${sshPath}/list`, - `${role.type}: cancel takes you to ssh index` - ); - assert.ok( - findAll(`[data-test-secret-link="${role.name}"]`).length, - `${role.type}: role shows in the list` - ); - - //and delete - await click(`[data-test-secret-link="${role.name}"] [data-test-popup-menu-trigger]`); - await waitUntil(() => find('[data-test-ssh-role-delete]')); // flaky without - await click(`[data-test-ssh-role-delete]`); - await click(`[data-test-confirm-button]`); - assert - .dom(`[data-test-secret-link="${role.name}"]`) - .doesNotExist(`${role.type}: role is no longer in the list`); - } - }); -}); diff --git a/ui/tests/acceptance/tools-test.js b/ui/tests/acceptance/tools-test.js deleted file mode 100644 index 4d3f2ea27..000000000 --- a/ui/tests/acceptance/tools-test.js +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, fillIn, find, findAll, currentURL, visit, settled, waitUntil } from '@ember/test-helpers'; -import Pretender from 'pretender'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { toolsActions } from 'vault/helpers/tools-actions'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import { capitalize } from '@ember/string'; - -module('Acceptance | tools', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - const DATA_TO_WRAP = JSON.stringify({ tools: 'tests' }); - const TOOLS_ACTIONS = toolsActions(); - - /* - data-test-tools-input="wrapping-token" - data-test-tools-input="rewrapped-token" - data-test-tools="token-lookup-row" - data-test-sidebar-nav-link=supportedAction - */ - - var createTokenStore = () => { - let token; - return { - set(val) { - token = val; - }, - get() { - return token; - }, - }; - }; - test('tools functionality', async function (assert) { - var tokenStore = createTokenStore(); - await visit('/vault/tools'); - - assert.strictEqual(currentURL(), '/vault/tools/wrap', 'forwards to the first action'); - TOOLS_ACTIONS.forEach((action) => { - assert.dom(`[data-test-sidebar-nav-link="${capitalize(action)}"]`).exists(`${action} link renders`); - }); - - const { CodeMirror } = await waitUntil(() => find('.CodeMirror')); - CodeMirror.setValue(DATA_TO_WRAP); - - // wrap - await click('[data-test-tools-submit]'); - const wrappedToken = await waitUntil(() => find('[data-test-tools-input="wrapping-token"]')); - tokenStore.set(wrappedToken.value); - assert - .dom('[data-test-tools-input="wrapping-token"]') - .hasValue(wrappedToken.value, 'has a wrapping token'); - - //lookup - await click('[data-test-sidebar-nav-link="Lookup"]'); - - await fillIn('[data-test-tools-input="wrapping-token"]', tokenStore.get()); - await click('[data-test-tools-submit]'); - await waitUntil(() => findAll('[data-test-tools="token-lookup-row"]').length >= 3); - const rows = findAll('[data-test-tools="token-lookup-row"]'); - assert.dom(rows[0]).hasText(/Creation path/, 'show creation path row'); - assert.dom(rows[1]).hasText(/Creation time/, 'show creation time row'); - assert.dom(rows[2]).hasText(/Creation TTL/, 'show creation ttl row'); - - //rewrap - await click('[data-test-sidebar-nav-link="Rewrap"]'); - - await fillIn('[data-test-tools-input="wrapping-token"]', tokenStore.get()); - await click('[data-test-tools-submit]'); - const rewrappedToken = await waitUntil(() => find('[data-test-tools-input="rewrapped-token"]')); - assert.ok(rewrappedToken.value, 'has a new re-wrapped token'); - assert.notEqual(rewrappedToken.value, tokenStore.get(), 're-wrapped token is not the wrapped token'); - tokenStore.set(rewrappedToken.value); - await settled(); - - //unwrap - await click('[data-test-sidebar-nav-link="Unwrap"]'); - - await fillIn('[data-test-tools-input="wrapping-token"]', tokenStore.get()); - await click('[data-test-tools-submit]'); - assert.deepEqual( - JSON.parse(CodeMirror.getValue()), - JSON.parse(DATA_TO_WRAP), - 'unwrapped data equals input data' - ); - const buttonDetails = await waitUntil(() => find('[data-test-button-details]')); - await click(buttonDetails); - await click('[data-test-button-data]'); - assert.dom('.CodeMirror').exists(); - - //random - await click('[data-test-sidebar-nav-link="Random"]'); - - assert.dom('[data-test-tools-input="bytes"]').hasValue('32', 'defaults to 32 bytes'); - await click('[data-test-tools-submit]'); - const randomBytes = await waitUntil(() => find('[data-test-tools-input="random-bytes"]')); - assert.ok(randomBytes.value, 'shows the returned value of random bytes'); - - //hash - await click('[data-test-sidebar-nav-link="Hash"]'); - - await fillIn('[data-test-tools-input="hash-input"]', 'foo'); - await click('[data-test-transit-b64-toggle="input"]'); - - await click('[data-test-tools-submit]'); - let sumInput = await waitUntil(() => find('[data-test-tools-input="sum"]')); - assert - .dom(sumInput) - .hasValue('LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=', 'hashes the data, encodes input'); - await click('[data-test-tools-back]'); - - await fillIn('[data-test-tools-input="hash-input"]', 'e2RhdGE6ImZvbyJ9'); - - await click('[data-test-tools-submit]'); - sumInput = await waitUntil(() => find('[data-test-tools-input="sum"]')); - assert - .dom(sumInput) - .hasValue('JmSi2Hhbgu2WYOrcOyTqqMdym7KT3sohCwAwaMonVrc=', 'hashes the data, passes b64 input through'); - }); - - const AUTH_RESPONSE = { - request_id: '39802bc4-235c-2f0b-87f3-ccf38503ac3e', - lease_id: '', - renewable: false, - lease_duration: 0, - data: null, - wrap_info: null, - warnings: null, - auth: { - client_token: 'ecfc2758-588e-981d-50f4-a25883bbf03c', - accessor: '6299780b-f2b2-1a3f-7b83-9d3d67629249', - policies: ['root'], - metadata: null, - lease_duration: 0, - renewable: false, - entity_id: '', - }, - }; - - test('ensure unwrap with auth block works properly', async function (assert) { - this.server = new Pretender(function () { - this.post('/v1/sys/wrapping/unwrap', (response) => { - return [response, { 'Content-Type': 'application/json' }, JSON.stringify(AUTH_RESPONSE)]; - }); - }); - await visit('/vault/tools'); - - //unwrap - await click('[data-test-sidebar-nav-link="Unwrap"]'); - - await fillIn('[data-test-tools-input="wrapping-token"]', 'sometoken'); - await click('[data-test-tools-submit]'); - - assert.deepEqual( - JSON.parse(findAll('.CodeMirror')[0].CodeMirror.getValue()), - AUTH_RESPONSE.auth, - 'unwrapped data equals input data' - ); - this.server.shutdown(); - }); -}); diff --git a/ui/tests/acceptance/transit-test.js b/ui/tests/acceptance/transit-test.js deleted file mode 100644 index 2a5462d6b..000000000 --- a/ui/tests/acceptance/transit-test.js +++ /dev/null @@ -1,370 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, fillIn, find, currentURL, settled, visit, waitUntil, findAll } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import { encodeString } from 'vault/utils/b64'; -import authPage from 'vault/tests/pages/auth'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import secretListPage from 'vault/tests/pages/secrets/backend/list'; - -const keyTypes = [ - { - name: (ts) => `aes-${ts}`, - type: 'aes128-gcm96', - exportable: true, - supportsEncryption: true, - }, - { - name: (ts) => `aes-convergent-${ts}`, - type: 'aes128-gcm96', - convergent: true, - supportsEncryption: true, - }, - { - name: (ts) => `aes-${ts}`, - type: 'aes256-gcm96', - exportable: true, - supportsEncryption: true, - }, - { - name: (ts) => `aes-convergent-${ts}`, - type: 'aes256-gcm96', - convergent: true, - supportsEncryption: true, - }, - { - name: (ts) => `chacha-${ts}`, - type: 'chacha20-poly1305', - exportable: true, - supportsEncryption: true, - }, - { - name: (ts) => `chacha-convergent-${ts}`, - type: 'chacha20-poly1305', - convergent: true, - supportsEncryption: true, - autoRotate: true, - }, - { - name: (ts) => `ecdsa-${ts}`, - type: 'ecdsa-p256', - exportable: true, - supportsSigning: true, - }, - { - name: (ts) => `ecdsa-${ts}`, - type: 'ecdsa-p384', - exportable: true, - supportsSigning: true, - }, - { - name: (ts) => `ecdsa-${ts}`, - type: 'ecdsa-p521', - exportable: true, - supportsSigning: true, - }, - { - name: (ts) => `ed25519-${ts}`, - type: 'ed25519', - derived: true, - supportsSigning: true, - }, - { - name: (ts) => `rsa-2048-${ts}`, - type: `rsa-2048`, - supportsSigning: true, - supportsEncryption: true, - }, - { - name: (ts) => `rsa-3072-${ts}`, - type: `rsa-3072`, - supportsSigning: true, - supportsEncryption: true, - }, - { - name: (ts) => `rsa-4096-${ts}`, - type: `rsa-4096`, - supportsSigning: true, - supportsEncryption: true, - autoRotate: true, - }, -]; - -const generateTransitKey = async function (key, now) { - const name = key.name(now); - await click('[data-test-secret-create]'); - - await fillIn('[data-test-transit-key-name]', name); - await fillIn('[data-test-transit-key-type]', key.type); - if (key.exportable) { - await click('[data-test-transit-key-exportable]'); - } - if (key.derived) { - await click('[data-test-transit-key-derived]'); - } - if (key.convergent) { - await click('[data-test-transit-key-convergent-encryption]'); - } - if (key.autoRotate) { - await click('[data-test-toggle-label="Auto-rotation period"]'); - } - await click('[data-test-transit-key-create]'); - await settled(); // eslint-disable-line - // link back to the list - await click('[data-test-secret-root-link]'); - - return name; -}; - -const testConvergentEncryption = async function (assert, keyName) { - const tests = [ - // raw bytes for plaintext and context - { - plaintext: 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', - context: 'nqR8LiVgNh/lwO2rArJJE9F9DMhh0lKo4JX9DAAkCDw=', - encodePlaintext: false, - encodeContext: false, - assertAfterEncrypt: (key) => { - assert.dom('.modal.is-active').exists(`${key}: Modal opens after encrypt`); - assert.ok( - /vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText), - `${key}: ciphertext shows a vault-prefixed ciphertext` - ); - }, - assertBeforeDecrypt: (key) => { - assert.dom('.modal.is-active').doesNotExist(`${key}: Modal not open before decrypt`); - assert - .dom('[data-test-transit-input="context"]') - .hasValue( - 'nqR8LiVgNh/lwO2rArJJE9F9DMhh0lKo4JX9DAAkCDw=', - `${key}: the ui shows the base64-encoded context` - ); - }, - - assertAfterDecrypt: (key) => { - assert.dom('.modal.is-active').exists(`${key}: Modal opens after decrypt`); - assert.strictEqual( - find('[data-test-encrypted-value="plaintext"]').innerText, - 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', - `${key}: the ui shows the base64-encoded plaintext` - ); - }, - }, - // raw bytes for plaintext, string for context - { - plaintext: 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', - context: encodeString('context'), - encodePlaintext: false, - encodeContext: false, - assertAfterEncrypt: (key) => { - assert.dom('.modal.is-active').exists(`${key}: Modal opens after encrypt`); - assert.ok( - /vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText), - `${key}: ciphertext shows a vault-prefixed ciphertext` - ); - }, - assertBeforeDecrypt: (key) => { - assert.dom('.modal.is-active').doesNotExist(`${key}: Modal not open before decrypt`); - assert - .dom('[data-test-transit-input="context"]') - .hasValue(encodeString('context'), `${key}: the ui shows the input context`); - }, - assertAfterDecrypt: (key) => { - assert.dom('.modal.is-active').exists(`${key}: Modal opens after decrypt`); - assert.strictEqual( - find('[data-test-encrypted-value="plaintext"]').innerText, - 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', - `${key}: the ui shows the base64-encoded plaintext` - ); - }, - }, - // base64 input - { - plaintext: encodeString('This is the secret'), - context: encodeString('context'), - encodePlaintext: false, - encodeContext: false, - assertAfterEncrypt: (key) => { - assert.dom('.modal.is-active').exists(`${key}: Modal opens after encrypt`); - assert.ok( - /vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText), - `${key}: ciphertext shows a vault-prefixed ciphertext` - ); - }, - assertBeforeDecrypt: (key) => { - assert.dom('.modal.is-active').doesNotExist(`${key}: Modal not open before decrypt`); - assert - .dom('[data-test-transit-input="context"]') - .hasValue(encodeString('context'), `${key}: the ui shows the input context`); - }, - assertAfterDecrypt: (key) => { - assert.dom('.modal.is-active').exists(`${key}: Modal opens after decrypt`); - assert.strictEqual( - find('[data-test-encrypted-value="plaintext"]').innerText, - encodeString('This is the secret'), - `${key}: the ui decodes plaintext` - ); - }, - }, - - // string input - { - plaintext: 'There are many secrets 🤐', - context: 'secret 2', - encodePlaintext: true, - encodeContext: true, - assertAfterEncrypt: (key) => { - assert.dom('.modal.is-active').exists(`${key}: Modal opens after encrypt`); - assert.ok( - /vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText), - `${key}: ciphertext shows a vault-prefixed ciphertext` - ); - }, - assertBeforeDecrypt: (key) => { - assert.dom('.modal.is-active').doesNotExist(`${key}: Modal not open before decrypt`); - assert - .dom('[data-test-transit-input="context"]') - .hasValue(encodeString('secret 2'), `${key}: the ui shows the encoded context`); - }, - assertAfterDecrypt: (key) => { - assert.dom('.modal.is-active').exists(`${key}: Modal opens after decrypt`); - assert.strictEqual( - find('[data-test-encrypted-value="plaintext"]').innerText, - encodeString('There are many secrets 🤐'), - `${key}: the ui decodes plaintext` - ); - }, - }, - ]; - - for (const testCase of tests) { - await click('[data-test-transit-action-link="encrypt"]'); - - find('#plaintext-control .CodeMirror').CodeMirror.setValue(testCase.plaintext); - await fillIn('[data-test-transit-input="context"]', testCase.context); - - if (!testCase.encodePlaintext) { - // If value is already encoded, check the box - await click('input[data-test-transit-input="encodedBase64"]'); - } - if (testCase.encodeContext) { - await click('[data-test-transit-b64-toggle="context"]'); - } - assert.dom('.modal.is-active').doesNotExist(`${name}: is not open before encrypt`); - await click('[data-test-button-encrypt]'); - - if (testCase.assertAfterEncrypt) { - await settled(); - testCase.assertAfterEncrypt(keyName); - } - // store ciphertext for decryption step - const copiedCiphertext = find('[data-test-encrypted-value="ciphertext"]').innerText; - await click('.modal.is-active [data-test-modal-background]'); - - assert.dom('.modal.is-active').doesNotExist(`${name}: Modal closes after background clicked`); - await click('[data-test-transit-action-link="decrypt"]'); - - if (testCase.assertBeforeDecrypt) { - await settled(); - testCase.assertBeforeDecrypt(keyName); - } - find('#ciphertext-control .CodeMirror').CodeMirror.setValue(copiedCiphertext); - await click('[data-test-button-decrypt]'); - - if (testCase.assertAfterDecrypt) { - await settled(); - testCase.assertAfterDecrypt(keyName); - } - - await click('.modal.is-active [data-test-modal-background]'); - - assert.dom('.modal.is-active').doesNotExist(`${name}: Modal closes after background clicked`); - } -}; -module('Acceptance | transit', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - const uid = uuidv4(); - await authPage.login(); - await settled(); - this.uid = uid; - this.path = `transit-${uid}`; - - await enablePage.enable('transit', `transit-${uid}`); - await settled(); - }); - - test(`transit backend: list menu`, async function (assert) { - await generateTransitKey(keyTypes[0], this.uid); - await secretListPage.secrets.objectAt(0).menuToggle(); - await settled(); - assert.strictEqual(secretListPage.menuItems.length, 2, 'shows 2 items in the menu'); - }); - for (const key of keyTypes) { - test(`transit backend: ${key.type}`, async function (assert) { - assert.expect(key.convergent ? 43 : 7); - const name = await generateTransitKey(key, this.uid); - await visit(`vault/secrets/${this.path}/show/${name}`); - - const expectedRotateValue = key.autoRotate ? '30 days' : 'Key will not be automatically rotated'; - assert - .dom('[data-test-row-value="Auto-rotation period"]') - .hasText(expectedRotateValue, 'Has expected auto rotate value'); - - await click('[data-test-transit-link="versions"]'); - // wait for capabilities - - assert.dom('[data-test-transit-key-version-row]').exists({ count: 1 }, `${name}: only one key version`); - await waitUntil(() => find('[data-test-confirm-action-trigger]')); - await click('[data-test-confirm-action-trigger]'); - - await click('[data-test-confirm-button]'); - // wait for rotate call - await waitUntil(() => findAll('[data-test-transit-key-version-row]').length >= 2); - assert - .dom('[data-test-transit-key-version-row]') - .exists({ count: 2 }, `${name}: two key versions after rotate`); - await click('[data-test-transit-key-actions-link]'); - - assert.ok( - currentURL().startsWith(`/vault/secrets/${this.path}/show/${name}?tab=actions`), - `${name}: navigates to transit actions` - ); - - const keyAction = key.supportsEncryption ? 'encrypt' : 'sign'; - const actionTitle = find(`[data-test-transit-action-title=${keyAction}]`).innerText.toLowerCase(); - - assert.true( - actionTitle.includes(keyAction), - `shows a card with title that links to the ${name} transit action` - ); - - await click(`[data-test-transit-card=${keyAction}]`); - - assert - .dom('[data-test-transit-key-version-select]') - .exists(`${name}: the rotated key allows you to select versions`); - if (key.exportable) { - assert - .dom('[data-test-transit-action-link="export"]') - .exists(`${name}: exportable key has a link to export action`); - } else { - assert - .dom('[data-test-transit-action-link="export"]') - .doesNotExist(`${name}: non-exportable key does not link to export action`); - } - if (key.convergent && key.supportsEncryption) { - await testConvergentEncryption(assert, name); - await settled(); - } - await settled(); - }); - } -}); diff --git a/ui/tests/acceptance/unseal-test.js b/ui/tests/acceptance/unseal-test.js deleted file mode 100644 index 43a1e941b..000000000 --- a/ui/tests/acceptance/unseal-test.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, currentRouteName, currentURL, fillIn, settled, visit } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import VAULT_KEYS from 'vault/tests/helpers/vault-keys'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import { pollCluster } from 'vault/tests/helpers/poll-cluster'; - -const { unsealKeys } = VAULT_KEYS; - -module('Acceptance | unseal', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - test('seal then unseal', async function (assert) { - await visit('/vault/settings/seal'); - - assert.strictEqual(currentURL(), '/vault/settings/seal'); - - // seal - await click('[data-test-seal] button'); - - await click('[data-test-confirm-button]'); - - await pollCluster(this.owner); - await settled(); - assert.strictEqual(currentURL(), '/vault/unseal', 'vault is on the unseal page'); - - // unseal - for (const key of unsealKeys) { - await fillIn('[data-test-shamir-input]', key); - - await click('button[type="submit"]'); - - await pollCluster(this.owner); - await settled(); - } - - assert.dom('[data-test-cluster-status]').doesNotExist('ui does not show sealed warning'); - assert.strictEqual(currentRouteName(), 'vault.cluster.auth', 'vault is ready to authenticate'); - }); -}); diff --git a/ui/tests/acceptance/wrapped-token-test.js b/ui/tests/acceptance/wrapped-token-test.js deleted file mode 100644 index 8242e4a0d..000000000 --- a/ui/tests/acceptance/wrapped-token-test.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { settled, currentURL, visit } from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import auth from 'vault/tests/pages/auth'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const consoleComponent = create(consoleClass); - -const wrappedAuth = async () => { - await consoleComponent.runCommands(`write -field=token auth/token/create policies=default -wrap-ttl=3m`); - await settled(); - return consoleComponent.lastLogOutput; -}; - -const setupWrapping = async () => { - await auth.logout(); - await settled(); - await auth.visit(); - await settled(); - await auth.tokenInput('root').submit(); - await settled(); - const token = await wrappedAuth(); - await auth.logout(); - await settled(); - return token; -}; -module('Acceptance | wrapped_token query param functionality', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - test('it authenticates you if the query param is present', async function (assert) { - const token = await setupWrapping(); - await auth.visit({ wrapped_token: token }); - await settled(); - assert.strictEqual(currentURL(), '/vault/secrets', 'authenticates and redirects to home'); - }); - - test('it authenticates when used with the with=token query param', async function (assert) { - const token = await setupWrapping(); - await auth.visit({ wrapped_token: token, with: 'token' }); - await settled(); - assert.strictEqual(currentURL(), '/vault/secrets', 'authenticates and redirects to home'); - }); - - test('it should authenticate when hitting logout url with wrapped_token when logged out', async function (assert) { - this.server.post('/sys/wrapping/unwrap', () => { - return { auth: { client_token: 'root' } }; - }); - - await visit(`/vault/logout?wrapped_token=1234`); - assert.strictEqual(currentURL(), '/vault/secrets', 'authenticates and redirects to home'); - }); -}); diff --git a/ui/tests/helpers/clients.js b/ui/tests/helpers/clients.js deleted file mode 100644 index 12b564eaa..000000000 --- a/ui/tests/helpers/clients.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Response } from 'miragejs'; - -/** Scenarios - Config off, no data - Config on, no data - Config on, with data - Filtering (data with mounts) - Filtering (data without mounts) - Filtering (data without mounts) - * -- HISTORY ONLY -- - Filtering different date ranges (hist only) - Upgrade warning - No permissions for license - Version - queries available - queries unavailable - License start date this month -*/ -export const SELECTORS = { - dashboardActiveTab: '.active[data-test-dashboard]', - emptyStateTitle: '[data-test-empty-state-title]', - usageStats: '[data-test-usage-stats]', - dateDisplay: '[data-test-date-display]', - attributionBlock: '[data-test-clients-attribution]', - filterBar: '[data-test-clients-filter-bar]', - rangeDropdown: '[data-test-calendar-widget-trigger]', - monthDropdown: '[data-test-popup-menu-trigger="month"]', - yearDropdown: '[data-test-popup-menu-trigger="year"]', - dateDropdownSubmit: '[data-test-date-dropdown-submit]', - runningTotalMonthStats: '[data-test-running-total="single-month-stats"]', - runningTotalMonthlyCharts: '[data-test-running-total="monthly-charts"]', - monthlyUsageBlock: '[data-test-monthly-usage]', - selectedAuthMount: 'div#auth-method-search-select [data-test-selected-option] div', - selectedNs: 'div#namespace-search-select [data-test-selected-option] div', -}; - -export const CHART_ELEMENTS = { - entityClientDataBars: '[data-test-group="entity_clients"]', - nonEntityDataBars: '[data-test-group="non_entity_clients"]', - yLabels: '[data-test-group="y-labels"]', - actionBars: '[data-test-group="action-bars"]', - labelActionBars: '[data-test-group="label-action-bars"]', - totalValues: '[data-test-group="total-values"]', -}; - -export function sendResponse(data, httpStatus = 200) { - if (httpStatus === 403) { - return [ - httpStatus, - { 'Content-Type': 'application/json' }, - JSON.stringify({ errors: ['permission denied'] }), - ]; - } - if (httpStatus === 204) { - // /activity endpoint returns 204 when no data, while - // /activity/monthly returns 200 with zero values on data - return [httpStatus, { 'Content-Type': 'application/json' }]; - } - return [httpStatus, { 'Content-Type': 'application/json' }, JSON.stringify(data)]; -} - -export function overrideResponse(httpStatus, data) { - if (httpStatus === 403) { - return new Response( - 403, - { 'Content-Type': 'application/json' }, - JSON.stringify({ errors: ['permission denied'] }) - ); - } - // /activity endpoint returns 204 when no data, while - // /activity/monthly returns 200 with zero values on data - if (httpStatus === 204) { - return new Response(204, { 'Content-Type': 'application/json' }); - } - return new Response(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); -} diff --git a/ui/tests/helpers/codemirror.js b/ui/tests/helpers/codemirror.js deleted file mode 100644 index c5922ba21..000000000 --- a/ui/tests/helpers/codemirror.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -const invariant = (truthy, error) => { - if (!truthy) throw new Error(error); -}; - -export default function (context, selector) { - const cmService = context.owner.lookup('service:code-mirror'); - - const element = document.querySelector(selector); - invariant(element, `Selector ${selector} matched no elements`); - - const cm = cmService.instanceFor(element.id); - invariant(cm, `No registered CodeMirror instance for ${selector}`); - - return cm; -} diff --git a/ui/tests/helpers/components/sidebar-nav.js b/ui/tests/helpers/components/sidebar-nav.js deleted file mode 100644 index 55830a73b..000000000 --- a/ui/tests/helpers/components/sidebar-nav.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { allFeatures } from 'vault/helpers/all-features'; -import sinon from 'sinon'; - -export const stubFeaturesAndPermissions = (owner, isEnterprise = false, setCluster = false) => { - const permissions = owner.lookup('service:permissions'); - const hasNavPermission = sinon.stub(permissions, 'hasNavPermission'); - hasNavPermission.returns(true); - sinon.stub(permissions, 'navPathParams'); - - const version = owner.lookup('service:version'); - const features = sinon.stub(version, 'features'); - features.value(allFeatures()); - sinon.stub(version, 'isEnterprise').value(isEnterprise); - - const auth = owner.lookup('service:auth'); - sinon.stub(auth, 'authData').value({}); - - if (setCluster) { - owner.lookup('service:currentCluster').setCluster({ - id: 'foo', - anyReplicationEnabled: true, - usingRaft: true, - }); - } - - return { hasNavPermission, features }; -}; diff --git a/ui/tests/helpers/components/ttl-picker.js b/ui/tests/helpers/components/ttl-picker.js deleted file mode 100644 index 539a14df4..000000000 --- a/ui/tests/helpers/components/ttl-picker.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -const selectors = { - ttlFormGroup: '[data-test-ttl-inputs]', - toggle: '[data-test-ttl-toggle]', - toggleByLabel: (label) => `[data-test-ttl-toggle="${label}"]`, - label: '[data-test-ttl-form-label]', - subtext: '[data-test-ttl-form-subtext]', - tooltipTrigger: `[data-test-tooltip-trigger]`, - ttlValue: '[data-test-ttl-value]', - ttlUnit: '[data-test-select="ttl-unit"]', - valueInputByLabel: (label) => `[data-test-ttl-value="${label}"]`, - unitInputByLabel: (label) => `[data-test-ttl-unit="${label}"] [data-test-select="ttl-unit"]`, -}; - -export default selectors; diff --git a/ui/tests/helpers/flash-message.js b/ui/tests/helpers/flash-message.js deleted file mode 100644 index fe35b65a9..000000000 --- a/ui/tests/helpers/flash-message.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import FlashObject from 'ember-cli-flash/flash/object'; - -FlashObject.reopen({ init() {} }); diff --git a/ui/tests/helpers/index.js b/ui/tests/helpers/index.js deleted file mode 100644 index cb8f694d2..000000000 --- a/ui/tests/helpers/index.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { - setupApplicationTest as upstreamSetupApplicationTest, - setupRenderingTest as upstreamSetupRenderingTest, - setupTest as upstreamSetupTest, -} from 'ember-qunit'; - -// This file exists to provide wrappers around ember-qunit's / ember-mocha's -// test setup functions. This way, you can easily extend the setup that is -// needed per test type. - -function setupApplicationTest(hooks, options) { - upstreamSetupApplicationTest(hooks, options); - - // Additional setup for application tests can be done here. - // - // For example, if you need an authenticated session for each - // application test, you could do: - // - // hooks.beforeEach(async function () { - // await authenticateSession(); // ember-simple-auth - // }); - // - // This is also a good place to call test setup functions coming - // from other addons: - // - // setupIntl(hooks); // ember-intl - // setupMirage(hooks); // ember-cli-mirage -} - -function setupRenderingTest(hooks, options) { - upstreamSetupRenderingTest(hooks, options); - - // Additional setup for rendering tests can be done here. -} - -function setupTest(hooks, options) { - upstreamSetupTest(hooks, options); - - // Additional setup for unit tests can be done here. -} - -export { setupApplicationTest, setupRenderingTest, setupTest }; diff --git a/ui/tests/helpers/kubernetes/overview.js b/ui/tests/helpers/kubernetes/overview.js deleted file mode 100644 index 18abb97fa..000000000 --- a/ui/tests/helpers/kubernetes/overview.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - rolesCardTitle: '[data-test-selectable-card="Roles"] .title', - rolesCardSubTitle: '[data-test-selectable-card-container="Roles"] p', - rolesCardLink: '[data-test-selectable-card="Roles"] a', - rolesCardNumRoles: '[data-test-roles-card-overview-num]', - generateCredentialsCardTitle: '[data-test-selectable-card="Generate credentials"] .title', - generateCredentialsCardSubTitle: '[data-test-selectable-card-container="Generate credentials"] p', - generateCredentialsCardButton: '[data-test-generate-credential-button]', - emptyStateTitle: '.empty-state .empty-state-title', - emptyStateMessage: '.empty-state .empty-state-message', - emptyStateActionText: '.empty-state .empty-state-actions', -}; diff --git a/ui/tests/helpers/mirage-to-models.js b/ui/tests/helpers/mirage-to-models.js deleted file mode 100644 index f754fa240..000000000 --- a/ui/tests/helpers/mirage-to-models.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { getContext } from '@ember/test-helpers'; - -export default (data) => { - const context = getContext(); - const store = context.owner.lookup('service:store'); - const modelName = Array.isArray(data) ? data[0].modelName : data.modelName; - const json = context.server.serializerOrRegistry.serialize(data); - store.push(json); - return Array.isArray(data) - ? data.map(({ id }) => store.peekRecord(modelName, id)) - : store.peekRecord(modelName, data.id); -}; diff --git a/ui/tests/helpers/noop-all-api-requests.js b/ui/tests/helpers/noop-all-api-requests.js deleted file mode 100644 index 88e440340..000000000 --- a/ui/tests/helpers/noop-all-api-requests.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Pretender from 'pretender'; -import { noopStub } from './stubs'; - -/** - * DEPRECATED prefer to use `setupMirage` along with stubs in vault/tests/helpers/stubs - */ -export default function (options = { usePassthrough: false }) { - return new Pretender(function () { - let fn = noopStub(); - if (options.usePassthrough) { - fn = this.passthrough; - } - this.post('/v1/**', fn); - this.put('/v1/**', fn); - this.get('/v1/**', fn); - this.delete('/v1/**', fn || noopStub(204)); - }); -} diff --git a/ui/tests/helpers/oidc-config.js b/ui/tests/helpers/oidc-config.js deleted file mode 100644 index f6e3a6ed8..000000000 --- a/ui/tests/helpers/oidc-config.js +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Response } from 'miragejs'; - -export const OIDC_BASE_URL = `/vault/access/oidc`; - -export const SELECTORS = { - oidcHeader: '[data-test-oidc-header]', - oidcClientCreateButton: '[data-test-oidc-configure]', - oidcRouteTabs: '[data-test-oidc-tabs]', - oidcLandingImg: '[data-test-oidc-img]', - confirmActionButton: '[data-test-confirm-button="true"]', - inlineAlert: '[data-test-inline-alert]', - // client route - clientSaveButton: '[data-test-oidc-client-save]', - clientCancelButton: '[data-test-oidc-client-cancel]', - clientDeleteButton: '[data-test-oidc-client-delete] button', - clientEditButton: '[data-test-oidc-client-edit]', - clientDetailsTab: '[data-test-oidc-client-details]', - clientProvidersTab: '[data-test-oidc-client-providers]', - - // assignment route - assignmentSaveButton: '[data-test-oidc-assignment-save]', - assignmentCreateButton: '[data-test-oidc-assignment-create]', - assignmentEditButton: '[data-test-oidc-assignment-edit]', - assignmentDeleteButton: '[data-test-oidc-assignment-delete] button', - assignmentCancelButton: '[data-test-oidc-assignment-cancel]', - assignmentDetailsTab: '[data-test-oidc-assignment-details]', - - // scope routes - scopeSaveButton: '[data-test-oidc-scope-save]', - scopeCancelButton: '[data-test-oidc-scope-cancel]', - scopeDeleteButton: '[data-test-oidc-scope-delete] button', - scopeEditButton: '[data-test-oidc-scope-edit]', - scopeDetailsTab: '[data-test-oidc-scope-details]', - scopeEmptyState: '[data-test-oidc-scope-empty-state]', - scopeCreateButtonEmptyState: '[data-test-oidc-scope-create-empty-state]', - scopeCreateButton: '[data-test-oidc-scope-create]', - - // key route - keySaveButton: '[data-test-oidc-key-save]', - keyCancelButton: '[data-test-oidc-key-cancel]', - keyDeleteButton: '[data-test-oidc-key-delete] button', - keyEditButton: '[data-test-oidc-key-edit]', - keyRotateButton: '[data-test-oidc-key-rotate] button', - keyDetailsTab: '[data-test-oidc-key-details]', - keyClientsTab: '[data-test-oidc-key-clients]', - - // provider route - providerSaveButton: '[data-test-oidc-provider-save]', - providerCancelButton: '[data-test-oidc-provider-cancel]', - providerDeleteButton: '[data-test-oidc-provider-delete] button', - providerEditButton: '[data-test-oidc-provider-edit]', - providerDetailsTab: '[data-test-oidc-provider-details]', - providerClientsTab: '[data-test-oidc-provider-clients]', -}; - -export function overrideMirageResponse(httpStatus, data) { - if (httpStatus === 403) { - return new Response( - 403, - { 'Content-Type': 'application/json' }, - JSON.stringify({ errors: ['permission denied'] }) - ); - } - if (httpStatus === 404) { - return new Response(404, { 'Content-Type': 'application/json' }); - } - if (httpStatus === 200) { - return new Response(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); - } - return { - request_id: crypto.randomUUID(), - lease_id: '', - renewable: false, - lease_duration: 0, - wrap_info: null, - warnings: null, - auth: null, - data: { ...data }, - }; -} - -export function overrideCapabilities(requestPath, capabilitiesArray) { - // sample of capabilitiesArray: ['read', 'update'] - return { - request_id: '40f7e44d-af5c-9b60-bd20-df72eb17e294', - lease_id: '', - renewable: false, - lease_duration: 0, - data: { - capabilities: capabilitiesArray, - [requestPath]: capabilitiesArray, - }, - wrap_info: null, - warnings: null, - auth: null, - }; -} - -export async function clearRecord(store, modelType, id) { - await store - .findRecord(modelType, id) - .then((model) => { - deleteModelRecord(model); - }) - .catch(() => { - // swallow error - }); -} - -const deleteModelRecord = async (model) => { - await model.destroyRecord(); -}; - -// MOCK RESPONSES: - -export const CLIENT_LIST_RESPONSE = { - keys: ['test-app', 'app-1'], - key_info: { - 'test-app': { - assignments: ['allow_all'], - client_id: 'whaT7KB0C3iBH1l3rXhd5HPf0n6vXU0s', - client_secret: 'hvo_secret_nkJSTu2NVYqylXwFbFijsTxJHg4Ic4gqSJw7uOZ4FbSXcObngDkKoVsvyndrf2O8', - client_type: 'confidential', - id_token_ttl: 0, - key: 'default', - redirect_uris: [], - }, - 'app-1': { - assignments: ['allow_all'], - client_id: 'HkmsTA4GG17j0Djy4EUAB2VAyzuLVewg', - client_secret: 'hvo_secret_g3f30MxAJWLXhhrCejbG4zY3O4LEHhEIO24aMy181AYKnfQtWTVV924ZmnlpUFUw', - client_type: 'confidential', - id_token_ttl: 0, - key: 'test-key', - redirect_uris: [], - }, - }, -}; - -export const CLIENT_DATA_RESPONSE = { - access_token_ttl: 0, - assignments: ['allow_all'], - client_id: 'whaT7KB0C3iBH1l3rXhd5HPf0n6vXU0s', - client_secret: 'hvo_secret_nkJSTu2NVYqylXwFbFijsTxJHg4Ic4gqSJw7uOZ4FbSXcObngDkKoVsvyndrf2O8', - client_type: 'confidential', - id_token_ttl: 0, - key: 'default', - redirect_uris: [], -}; - -export const ASSIGNMENT_LIST_RESPONSE = { - keys: ['allow_all', 'test-assignment'], -}; - -export const ASSIGNMENT_DATA_RESPONSE = { - group_ids: ['262ca5b9-7b69-0a84-446a-303dc7d778af'], - entity_ids: ['b6094ac6-baf4-6520-b05a-2bd9f07c66da'], -}; - -export const SCOPE_LIST_RESPONSE = { - keys: ['test-scope'], -}; - -export const SCOPE_DATA_RESPONSE = { - description: 'this is a test', - template: `{ - "groups": {{identity.entity.groups.names}} - }`, -}; - -export const PROVIDER_LIST_RESPONSE = { - keys: ['test-provider'], -}; - -export const PROVIDER_DATA_RESPONSE = { - allowed_client_ids: ['*'], - issuer: '', - scopes_supported: ['test-scope'], -}; diff --git a/ui/tests/helpers/oidc-window-stub.js b/ui/tests/helpers/oidc-window-stub.js deleted file mode 100644 index 8a69fd551..000000000 --- a/ui/tests/helpers/oidc-window-stub.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import EmberObject, { computed } from '@ember/object'; -import Evented from '@ember/object/evented'; - -export const fakeWindow = EmberObject.extend(Evented, { - init() { - this._super(...arguments); - this.on('close', () => { - this.set('closed', true); - }); - }, - screen: computed(function () { - return { - height: 600, - width: 500, - }; - }), - origin: 'https://my-vault.com', - closed: false, - open() {}, - close() {}, -}); - -export const buildMessage = (opts) => ({ - isTrusted: true, - origin: 'https://my-vault.com', - data: { - source: 'oidc-callback', - path: 'foo', - state: 'state', - code: 'code', - }, - ...opts, -}); diff --git a/ui/tests/helpers/pki.js b/ui/tests/helpers/pki.js deleted file mode 100644 index 634b43fec..000000000 --- a/ui/tests/helpers/pki.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - caChain: '[data-test-value-div="CA chain"] [data-test-masked-input]', - certificate: '[data-test-value-div="Certificate"] [data-test-masked-input]', - commonName: '[data-test-row-value="Common name"]', - csr: '[data-test-value-div="CSR"] [data-test-masked-input]', - expiryDate: '[data-test-row-value="Expiration date"]', - issueDate: '[data-test-row-value="Issue date"]', - issuingCa: '[data-test-value-div="Issuing CA"] [data-test-masked-input]', - privateKey: '[data-test-value-div="Private key"] [data-test-masked-input]', - revocationTime: '[data-test-row-value="Revocation time"]', - serialNumber: '[data-test-row-value="Serial number"]', -}; - -export const STANDARD_META = { - total: 2, - currentPage: 1, - pageSize: 100, -}; diff --git a/ui/tests/helpers/pki/overview.js b/ui/tests/helpers/pki/overview.js deleted file mode 100644 index eb4d7a4be..000000000 --- a/ui/tests/helpers/pki/overview.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - issuersCardTitle: '[data-test-selectable-card-container="Issuers"] h3', - issuersCardSubtitle: '[data-test-selectable-card-container="Issuers"] p', - issuersCardLink: '[data-test-selectable-card-container="Issuers"] a', - issuersCardOverviewNum: '[data-test-selectable-card-container="Issuers"] .title-number', - rolesCardTitle: '[data-test-selectable-card-container="Roles"] h3', - rolesCardSubtitle: '[data-test-selectable-card-container="Roles"] p', - rolesCardLink: '[data-test-selectable-card-container="Roles"] a', - rolesCardOverviewNum: '[data-test-selectable-card-container="Roles"] .title-number', - issueCertificate: '[data-test-selectable-card-container="Issue certificate"] h3', - issueCertificateInput: '[data-test-issue-certificate-input]', - issueCertificatePowerSearch: '[data-test-issue-certificate-input] span', - issueCertificateButton: '[data-test-issue-certificate-button]', - viewCertificate: '[data-test-selectable-card-container="View certificate"] h3', - viewCertificateInput: '[data-test-view-certificate-input]', - viewCertificatePowerSearch: '[data-test-view-certificate-input] span', - viewCertificateButton: '[data-test-view-certificate-button]', - viewIssuerInput: '[data-test-issue-issuer-input]', - viewIssuerPowerSearch: '[data-test-issue-issuer-input] span', - viewIssuerButton: '[data-test-view-issuer-button]', - firstPowerSelectOption: '[data-option-index="0"]', -}; diff --git a/ui/tests/helpers/pki/page/pki-configuration-edit.js b/ui/tests/helpers/pki/page/pki-configuration-edit.js deleted file mode 100644 index 0d83efa19..000000000 --- a/ui/tests/helpers/pki/page/pki-configuration-edit.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - errorBanner: '[data-test-error-banner]', - acmeEditSection: '[data-test-acme-edit-section]', - configEditSection: '[data-test-cluster-config-edit-section]', - configInput: (attr) => `[data-test-input="${attr}"]`, - stringListInput: (attr) => `[data-test-input="${attr}"] [data-test-string-list-input="0"]`, - urlsEditSection: '[data-test-urls-edit-section]', - urlFieldInput: (attr) => `[data-test-input="${attr}"] textarea`, - urlFieldLabel: (attr) => `[data-test-input="${attr}"] label`, - crlEditSection: '[data-test-crl-edit-section]', - crlToggleInput: (attr) => `[data-test-input="${attr}"] input`, - crlTtlInput: (attr) => `[data-test-ttl-value="${attr}"]`, - crlFieldLabel: (attr) => `[data-test-input="${attr}"] label`, - saveButton: '[data-test-configuration-edit-save]', - cancelButton: '[data-test-configuration-edit-cancel]', - validationAlert: '[data-test-configuration-edit-validation-alert]', - deleteButton: (attr) => `[data-test-input="${attr}"] [data-test-string-list-button="delete"]`, - groupHeader: (group) => `[data-test-crl-header="${group}"]`, - checkboxInput: (attr) => `[data-test-input="${attr}"]`, -}; diff --git a/ui/tests/helpers/pki/page/pki-keys.js b/ui/tests/helpers/pki/page/pki-keys.js deleted file mode 100644 index 0968a0a66..000000000 --- a/ui/tests/helpers/pki/page/pki-keys.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - // key index - importKey: '[data-test-pki-key-import]', - generateKey: '[data-test-pki-key-generate]', - keyId: '[data-test-key="id"]', - keyName: '[data-test-key="name"]', - popupMenuTrigger: '[data-test-popup-menu-trigger]', - popupMenuDetails: '[data-test-key-menu-link="details"]', - popupMenuEdit: '[data-test-key-menu-link="edit"]', - // key details - title: '[data-test-key-details-title]', - keyIdValue: '[data-test-value-div="Key ID"]', - keyNameValue: '[data-test-value-div="Key name"]', - keyTypeValue: '[data-test-value-div="Key type"]', - keyBitsValue: '[data-test-value-div="Key bits"]', - keyDeleteButton: '[data-test-pki-key-delete] button', - downloadButton: '[data-test-download-button]', - keyEditLink: '[data-test-pki-key-edit]', - confirmDelete: '[data-test-confirm-button]', -}; diff --git a/ui/tests/helpers/pki/page/pki-role-details.js b/ui/tests/helpers/pki/page/pki-role-details.js deleted file mode 100644 index 52b4d0ead..000000000 --- a/ui/tests/helpers/pki/page/pki-role-details.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - issuerLabel: '[data-test-row-label="Issuer"]', - noStoreValue: '[data-test-value-div="Store in storage backend"]', - keyUsageValue: '[data-test-value-div="Key usage"]', - extKeyUsageValue: '[data-test-value-div="Ext key usage"]', - customTtlValue: '[data-test-value-div="Issued certificates expire after"]', -}; diff --git a/ui/tests/helpers/pki/page/pki-tidy-form.js b/ui/tests/helpers/pki/page/pki-tidy-form.js deleted file mode 100644 index e1c4f754c..000000000 --- a/ui/tests/helpers/pki/page/pki-tidy-form.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - tidyFormName: (attr) => `[data-test-tidy-form="${attr}"]`, - inputByAttr: (attr) => `[data-test-input="${attr}"]`, - toggleInput: (attr) => `[data-test-input="${attr}"] input`, - intervalDuration: '[data-test-ttl-value="Automatic tidy enabled"]', - acmeAccountSafetyBuffer: '[data-test-ttl-value="Tidy ACME enabled"]', - toggleLabel: (label) => `[data-test-toggle-label="${label}"]`, - tidySectionHeader: (header) => `[data-test-tidy-header="${header}"]`, - tidySave: '[data-test-pki-tidy-button]', - tidyCancel: '[data-test-pki-tidy-cancel]', - tidyPauseDuration: '[data-test-ttl-value="Pause duration"]', - editAutoTidyButton: '[data-test-pki-edit-tidy-auto-link]', -}; diff --git a/ui/tests/helpers/pki/page/pki-tidy.js b/ui/tests/helpers/pki/page/pki-tidy.js deleted file mode 100644 index 5b0e9f545..000000000 --- a/ui/tests/helpers/pki/page/pki-tidy.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ -import { SELECTORS as TIDY_FORM } from './pki-tidy-form'; - -export const SELECTORS = { - hdsAlertTitle: '[data-test-hds-alert-title]', - hdsAlertDescription: '[data-test-hds-alert-description]', - alertUpdatedAt: '[data-test-hds-alert-updated-at]', - cancelTidyAction: '[data-test-cancel-tidy-action]', - hdsAlertButtonText: '[data-test-cancel-tidy-action] .hds-button__text', - timeStartedRow: '[data-test-value-div="Time started"]', - timeFinishedRow: '[data-test-value-div="Time finished"]', - cancelTidyModalBackground: '[data-test-modal-background="Cancel tidy?"]', - tidyEmptyStateConfigure: '[data-test-tidy-empty-state-configure]', - manualTidyToolbar: '[data-test-pki-manual-tidy-config]', - autoTidyToolbar: '[data-test-pki-auto-tidy-config]', - tidyConfigureModal: { - configureTidyModal: '[data-test-modal-background="Tidy this mount"]', - tidyModalAutoButton: '[data-test-tidy-modal-auto-button]', - tidyModalManualButton: '[data-test-tidy-modal-manual-button]', - tidyModalCancelButton: '[data-test-tidy-modal-cancel-button]', - tidyOptionsModal: '[data-test-pki-tidy-options-modal]', - }, - tidyEmptyState: '[data-test-component="empty-state"]', - tidyForm: { - ...TIDY_FORM, - }, -}; diff --git a/ui/tests/helpers/pki/pki-configure-create.js b/ui/tests/helpers/pki/pki-configure-create.js deleted file mode 100644 index 209b32db4..000000000 --- a/ui/tests/helpers/pki/pki-configure-create.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { SELECTORS as GENERATE_ROOT } from './pki-generate-root'; - -export const SELECTORS = { - // page::pki-configure-create - breadcrumbContainer: '[data-test-breadcrumbs]', - title: '[data-test-pki-engine-page-title]', - option: '[data-test-pki-config-option]', - optionByKey: (key) => `[data-test-pki-config-option="${key}"]`, - cancelButton: '[data-test-pki-config-cancel]', - saveButton: '[data-test-pki-config-save]', - doneButton: '[data-test-done]', - configureButton: '[data-test-configure-pki-button]', - // pki-generate-root - ...GENERATE_ROOT, - generateRootOption: '[data-test-pki-config-option="generate-root"]', - // pki-ca-cert-import - importForm: '[data-test-pki-import-pem-bundle-form]', - importSubmit: '[data-test-pki-import-pem-bundle]', - importSectionLabel: '[data-test-import-section-label]', - importMapping: '[data-test-imported-bundle-mapping]', - importedIssuer: '[data-test-imported-issuer]', - importedKey: '[data-test-imported-key]', - // generate-intermediate - csrDetails: '[data-test-generate-csr-result]', -}; diff --git a/ui/tests/helpers/pki/pki-delete-all-issuers.js b/ui/tests/helpers/pki/pki-delete-all-issuers.js deleted file mode 100644 index 0083e3e56..000000000 --- a/ui/tests/helpers/pki/pki-delete-all-issuers.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - issuerLink: '[data-test-delete-all-issuers-link]', - deleteAllIssuerModal: '[data-test-modal-background="Delete All Issuers?"]', - deleteAllIssuerInput: '[data-test-confirmation-modal-input="Delete All Issuers?"]', - deleteAllIssuerButton: '[data-test-confirm-button="Delete All Issuers?"]', -}; diff --git a/ui/tests/helpers/pki/pki-generate-root.js b/ui/tests/helpers/pki/pki-generate-root.js deleted file mode 100644 index 7f9e197eb..000000000 --- a/ui/tests/helpers/pki/pki-generate-root.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - mainSectionTitle: '[data-test-generate-root-title="Root parameters"]', - urlSectionTitle: '[data-test-generate-root-title="Issuer URLs"]', - keyParamsGroupToggle: '[data-test-toggle-group="Key parameters"]', - sanGroupToggle: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]', - additionalGroupToggle: '[data-test-toggle-group="Additional subject fields"]', - toggleGroupDescription: '[data-test-toggle-group-description]', - formField: '[data-test-field]', - typeField: '[data-test-input="type"]', - inputByName: (name) => `[data-test-input="${name}"]`, - fieldByName: (name) => `[data-test-field="${name}"]`, - generateRootSave: '[data-test-pki-generate-root-save]', - generateRootCancel: '[data-test-pki-generate-root-cancel]', - generateRootCommonNameField: '[data-test-input="commonName"]', - generateRootIssuerNameField: '[data-test-input="issuerName"]', - formInvalidError: '[data-test-pki-generate-root-validation-error]', - urlsSection: '[data-test-urls-section]', - urlField: '[data-test-urls-section] [data-test-input]', - // Shown values after save - saved: { - certificate: '[data-test-value-div="Certificate"] [data-test-masked-input]', - commonName: '[data-test-row-value="Common name"]', - issuerName: '[data-test-row-value="Issuer name"]', - issuerLink: '[data-test-value-div="Issuer ID"] a', - keyName: '[data-test-row-value="Key name"]', - keyLink: '[data-test-value-div="Key ID"] a', - privateKey: '[data-test-value-div="Private key"] [data-test-masked-input]', - serialNumber: '[data-test-row-value="Serial number"]', - }, -}; diff --git a/ui/tests/helpers/pki/pki-issuer-cross-sign.js b/ui/tests/helpers/pki/pki-issuer-cross-sign.js deleted file mode 100644 index 20fc59341..000000000 --- a/ui/tests/helpers/pki/pki-issuer-cross-sign.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { SELECTORS as CONFIGURE } from './pki-configure-create'; -import { SELECTORS as DETAILS } from './pki-issuer-details'; - -export const SELECTORS = { - objectListInput: (key, row = 0) => `[data-test-object-list-input="${key}-${row}"]`, - inputByName: (name) => `[data-test-input="${name}"]`, - addRow: '[data-test-object-list-add-button', - submitButton: '[data-test-cross-sign-submit]', - cancelButton: '[data-test-cross-sign-cancel]', - statusCount: '[data-test-cross-sign-status-count]', - signedIssuerRow: (row = 0) => `[data-test-info-table-row="${row}"]`, - signedIssuerCol: (attr) => `[data-test-info-table-column="${attr}"]`, - // for cross signing acceptance tests - configure: { ...CONFIGURE }, - details: { ...DETAILS }, - rowValue: (attr) => `[data-test-row-value="${attr}"]`, - copyButton: (attr) => `[data-test-value-div="${attr}"] [data-test-copy-button]`, -}; diff --git a/ui/tests/helpers/pki/pki-issuer-details.js b/ui/tests/helpers/pki/pki-issuer-details.js deleted file mode 100644 index 1197a56ac..000000000 --- a/ui/tests/helpers/pki/pki-issuer-details.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - configure: '[data-test-pki-issuer-configure]', - crossSign: '[data-test-pki-issuer-cross-sign]', - defaultGroup: '[data-test-details-group="default"]', - download: '[data-test-issuer-download]', - groupTitle: '[data-test-group-title]', - parsingAlertBanner: '[data-test-parsing-error-alert-banner]', - rotateModal: '[data-test-modal-background="Rotate this root"]', - rotateModalGenerate: '[data-test-root-rotate-step-one]', - rotateRoot: '[data-test-pki-issuer-rotate-root]', - row: '[data-test-component="info-table-row"]', - signIntermediate: '[data-test-pki-issuer-sign-int]', - urlsGroup: '[data-test-details-group="Issuer URLs"]', - valueByName: (name) => `[data-test-value-div="${name}"]`, -}; diff --git a/ui/tests/helpers/pki/pki-key-form.js b/ui/tests/helpers/pki/pki-key-form.js deleted file mode 100644 index cc1e738a2..000000000 --- a/ui/tests/helpers/pki/pki-key-form.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - keyCreateButton: '[data-test-pki-key-save]', - keyCancelButton: '[data-test-pki-key-cancel]', - keyNameInput: '[data-test-input="keyName"]', - typeInput: '[data-test-input="type"]', - keyTypeInput: '[data-test-input="keyType"]', - keyBitsInput: '[data-test-input="keyBits"]', - validationError: '[data-test-pki-key-validation-error]', - fieldErrorByName: (name) => `[data-test-field-validation="${name}"]`, -}; diff --git a/ui/tests/helpers/pki/pki-not-valid-after-form.js b/ui/tests/helpers/pki/pki-not-valid-after-form.js deleted file mode 100644 index 4f7ed34f8..000000000 --- a/ui/tests/helpers/pki/pki-not-valid-after-form.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - radioTtl: '[data-test-radio-button="ttl"]', - radioTtlLabel: '[data-test-radio-label="ttl"]', - radioDate: '[data-test-radio-button="not_after"]', - radioDateLabel: '[data-test-radio-label="specificDate"]', - ttlForm: '[data-test-ttl-inputs]', - ttlTimeInput: '[data-test-ttl-value="TTL"]', - ttlUnitInput: '[data-test-select="ttl-unit"]', - dateInput: '[data-test-input="not_after"]', -}; diff --git a/ui/tests/helpers/pki/pki-role-form.js b/ui/tests/helpers/pki/pki-role-form.js deleted file mode 100644 index 49a9d36b3..000000000 --- a/ui/tests/helpers/pki/pki-role-form.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const PKI_BASE_URL = `/vault/cluster/secrets/backend/pki/roles`; - -export const SELECTORS = { - roleName: '[data-test-input="name"]', - issuerRef: '[data-test-input="issuerRef"]', - issuerRefSelect: '[data-test-select="issuerRef"]', - issuerRefToggle: '[data-test-toggle-label="issuerRef-toggle"]', - customTtl: '[data-test-field="customTtl"]', - backdateValidity: '[data-test-ttl-value="Backdate validity"]', - maxTtl: '[data-test-toggle-label="Max TTL"]', - generateLease: '[data-test-field="generateLease"]', - noStore: '[data-test-field="noStore"]', - addBasicConstraints: '[data-test-input="addBasicConstraints"]', - domainHandling: '[data-test-toggle-group="Domain handling"]', - keyParams: '[data-test-toggle-group="Key parameters"]', - keyType: '[data-test-input="keyType"]', - keyBits: '[data-test-input="keyBits"]', - signatureBits: '[data-test-input="signatureBits"]', - keyUsage: '[data-test-toggle-group="Key usage"]', - extKeyUsageOids: '[data-test-input="extKeyUsageOids"]', - digitalSignature: '[data-test-checkbox="DigitalSignature"]', - keyAgreement: '[data-test-checkbox="KeyAgreement"]', - keyEncipherment: '[data-test-checkbox="KeyEncipherment"]', - any: '[data-test-checkbox="Any"]', - serverAuth: '[data-test-checkbox="ServerAuth"]', - policyIdentifiers: '[data-test-toggle-group="Policy identifiers"]', - san: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]', - additionalSubjectFields: '[data-test-toggle-group="Additional subject fields"]', - roleCreateButton: '[data-test-pki-role-save]', - roleCancelButton: '[data-test-pki-role-cancel]', -}; diff --git a/ui/tests/helpers/pki/pki-role-generate.js b/ui/tests/helpers/pki/pki-role-generate.js deleted file mode 100644 index 26a101763..000000000 --- a/ui/tests/helpers/pki/pki-role-generate.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export const SELECTORS = { - form: '[data-test-pki-generate-cert-form]', - commonNameField: '[data-test-input="commonName"]', - optionsToggle: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]', - generateButton: '[data-test-pki-generate-button]', - cancelButton: '[data-test-pki-generate-cancel]', - downloadButton: '[data-test-pki-cert-download-button]', - revokeButton: '[data-test-pki-cert-revoke-button]', - serialNumber: '[data-test-value-div="Serial number"]', - certificate: '[data-test-value-div="Certificate"]', - inlineAlert: '[data-test-alert]', - commonNameInlineError: '[data-test-field="commonName"] [data-test-inline-alert]', - commonNameErrorBorder: '[data-test-input="commonName"]', -}; diff --git a/ui/tests/helpers/pki/pki-run-commands.js b/ui/tests/helpers/pki/pki-run-commands.js deleted file mode 100644 index 291aab176..000000000 --- a/ui/tests/helpers/pki/pki-run-commands.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import { create } from 'ember-cli-page-object'; - -const consoleComponent = create(consoleClass); - -export const tokenWithPolicy = async function (name, policy) { - await consoleComponent.runCommands([ - `write sys/policies/acl/${name} policy=${btoa(policy)}`, - `write -field=client_token auth/token/create policies=${name}`, - ]); - return consoleComponent.lastLogOutput; -}; - -export const runCommands = async function (commands) { - try { - await consoleComponent.runCommands(commands); - const res = consoleComponent.lastLogOutput; - if (res.includes('Error')) { - throw new Error(res); - } - return res; - } catch (error) { - // eslint-disable-next-line no-console - console.error( - `The following occurred when trying to run the command(s):\n ${commands.join('\n')} \n\n ${ - consoleComponent.lastLogOutput - }` - ); - throw error; - } -}; - -// Clears pki-related data and capabilities so that admin -// capabilities from setup don't rollover -export function clearRecords(store) { - store.unloadAll('pki/action'); - store.unloadAll('pki/issuer'); - store.unloadAll('pki/key'); - store.unloadAll('pki/role'); - store.unloadAll('pki/sign-intermediate'); - store.unloadAll('pki/tidy'); - store.unloadAll('pki/config/urls'); - store.unloadAll('pki/config/crl'); - store.unloadAll('pki/config/cluster'); - store.unloadAll('pki/config/acme'); - store.unloadAll('pki/certificate/generate'); - store.unloadAll('pki/certificate/sign'); - store.unloadAll('capabilities'); -} diff --git a/ui/tests/helpers/pki/values.js b/ui/tests/helpers/pki/values.js deleted file mode 100644 index 39adb9d80..000000000 --- a/ui/tests/helpers/pki/values.js +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -// Expires Jan 10, 2033 -export const rootPem = `-----BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIUTBbQcZijQsmd0rjd6COikPsrGyowDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAxMJdGVzdC1yb290MB4XDTIzMDEyMDE3NTcxMloXDTIzMDIy -MTE3NTc0MlowFDESMBAGA1UEAxMJdGVzdC1yb290MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAlUHfvQLsocXtvwRCpTnXGzwMCD+3KKK7y1+SUCgpAD9Y -RV2xLAbqh0iK3x2WI4+Pek1Ub6dYaWczzBob6wRq9iFB72uLPpbL8yRf+tc1egmP -wwJQS9qidb1hcSi4p6x/JwOpr2v2PDqJPDoHrfaHeJgCuBGS00qUFH7oHQz9Usim -lHjIbVNF3Qa1Hq2bgwkZmRjRn3Bez/xy3YEiQ41GTicUBqY4NAGWuS1LiHyEUW81 -iQ+1iGlbpuAL4H7lpKmrhv1xZXEsF9vNL6H0Y7kjjAImTQnmo+ozcArnKnwh2wmS -f/TrVnN4RRc8dvN8P8nWvVsJYK/D40yc7YMljIESKQIDAQABo4HEMIHBMA4GA1Ud -DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBT6rcf5twb19wLL -JjhPOVywd41d2jAfBgNVHSMEGDAWgBT6rcf5twb19wLLJjhPOVywd41d2jArBggr -BgEFBQcBAQQfMB0wGwYIKwYBBQUHMAKGD2Z1bmZvcmVjYXN0LmNvbTAUBgNVHREE -DTALggl0ZXN0LXJvb3QwGwYDVR0fBBQwEjAQoA6gDIYKZ29vZ2xlLmNvbTANBgkq -hkiG9w0BAQsFAAOCAQEAjG7km+QsIuW7KNY3h8YHJZhdr+tIx57k9tUR4w1+d8QI -t44FTdCYdN8n89lsFK9bONZatd0LY3qqcOARE2ni0Hg/zV9u8TTVKTKAOOx8zBd1 -TnwzhXb8mssqnXK9lcECexuWf/s5lkyHjcWOuzNVI0PohrX9tGZwdzsZEgH4Y49i -o8I9DD+uBHknwByRLXSDmgggwgOYsyTg/IfYoHlLHDD3CaOpkCvUCZvM9bI7nrlx -2GByQ/WDT4ArAHcf+Z1iaSIbV6WG6QWoPsu2/WKybcuN2fznaXtJMwgRl50BUv2h -DU3c2oZTc0mPYGft6U8mVwLqfYTcEduGidTLAQPE5w== ------END CERTIFICATE-----`; - -export const issuerPemBundle = ` ------BEGIN CERTIFICATE----- -MIIDRTCCAi2gAwIBAgIUdKagCL6TnN5xLkwhPbNY8JEcY0YwDQYJKoZIhvcNAQEL -BQAwGzEZMBcGA1UEAxMQd3d3LnRlc3QtaW50LmNvbTAeFw0yMzAxMDkxOTA1NTBa -Fw0yMzAyMTAxOTA2MjBaMBsxGTAXBgNVBAMTEHd3dy50ZXN0LWludC5jb20wggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfd5o9JfyRAXH+E1vE2U0xjSqs -A/cxDqsDXRHBnNJvzAa+7gPKXCDQZbr6chjxLXpP6Bv2/O+dZHq1fo/f6q9PDDGW -JYIluwbACpe7W1UB7q9xFkZg85yQsNYokGZlwv/AMGpFBxDwVlNGL+4fxvFTv7uF -mIlDzSIPrzByyCrqAFMNNqNwlAerDt/C6DMZae/rTGXIXsTfUpxPy21bzkeA+70I -YCV1ffK8UnAeBYNUJ+v8+XgTQ5KhRyQ+fscUkO3T2s6f3O9Q2sWxswkf2YmZB+V1 -cTZ5w6hqiuFdBXz7GRnACi1/gbWbaExQTJRplArFwIHka7dqJh8tYkXDjai3AgMB -AAGjgYAwfjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E -FgQU68/xXIgvsleKkuA8clK/6YslB/IwHwYDVR0jBBgwFoAU68/xXIgvsleKkuA8 -clK/6YslB/IwGwYDVR0RBBQwEoIQd3d3LnRlc3QtaW50LmNvbTANBgkqhkiG9w0B -AQsFAAOCAQEAWSff0BH3SJv/XqwN/flqc1CVzOios72/IJ+KBBv0AzFCZ8wJPi+c -hH1bw7tqi01Bgh595TctogDFN1b6pjN+jrlIP4N+FF9Moj79Q+jHQMnuJomyPuI7 -i07vqUcxgSmvEBBWOWS+/vxe6TfWDg18nyPf127CWQN8IHTo1f/GavX+XmRve6XT -EWoqcQshEk9i87oqCbaT7B40jgjTAd1r4Cc6P4s1fAGPt9e9eqMj13kTyVDNuCoD -FSZYalrlkASpg+c9oDQIh2MikGQINXHv/zIEHOW93siKMWeA4ni6phHtMg/p5eJt -SxnVZsSzj8QLy2uwX1AADR0QUvJzMxptyA== ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAn3eaPSX8kQFx/hNbxNlNMY0qrAP3MQ6rA10RwZzSb8wGvu4D -ylwg0GW6+nIY8S16T+gb9vzvnWR6tX6P3+qvTwwxliWCJbsGwAqXu1tVAe6vcRZG -YPOckLDWKJBmZcL/wDBqRQcQ8FZTRi/uH8bxU7+7hZiJQ80iD68wcsgq6gBTDTaj -cJQHqw7fwugzGWnv60xlyF7E31KcT8ttW85HgPu9CGAldX3yvFJwHgWDVCfr/Pl4 -E0OSoUckPn7HFJDt09rOn9zvUNrFsbMJH9mJmQfldXE2ecOoaorhXQV8+xkZwAot -f4G1m2hMUEyUaZQKxcCB5Gu3aiYfLWJFw42otwIDAQABAoIBADC+vZ4Ne4vTtkWl -Izsj9Y29Chs0xx3uzuWjUGcvib/0zOcWGICF8t3hCuu9btRiQ24jlFDGdnRVH5FV -E6OtuFLgdlPgOU1RQzn2wvTZcT26+VQHLBI8xVIRTBVwNmzK06Sq6AEbrNjaenAM -/KwoAuLHzAmFXAgmr0++DIA5oayPWyi5IoyFO7EoRv79Xz5LWfu5j8CKOFXmI5MT -vEVYM6Gb2xHRa2Ng0SJ4VzwC09GcXlHKRAz+CubJuncvjbcM/EryvexozKkUq4XA -KqGr9xxdZ4XDlo3Rj9S9P9JaOin0I1mwwz6p+iwMF0zr+/ldjE4oPBdB1PUgSJ7j -2CZcS1kCgYEAwIZ3UsMIXqkMlkMz/7nu2sqzV3EgQjY5QRoz98ligKg4fhYKz+K4 -yXvJrRyLkwEBaPdLppCZbs4xsuuv3jiqUHV5n7sfpUA5HVKkKh6XY7jnszbqV732 -iB1mQVEjzM92/amew2hDKLGQDW0nglrg6uV+bx0Lnp6Glahr8NOAyk0CgYEA1Ar3 -jTqTkU+NQX7utlxx0HPVL//JH/erp/Gnq9fN8dZhK/yjwX5savUlNHpgePoXf1pE -lgi21/INQsvp7O2AUKuj96k+jBHQ0SS58AQGFv8iNDkLE57N74vCO6+Xdi1rHj/Y -7jglr00box/7SOmvb4SZz2o0jm0Ejsg2M0aBuRMCgYEAgTB6F34qOqMDgD1eQka5 -QfXs/Es8E1Ihf08e+jIXuC+poOoXnUINL56ySUizXBS7pnzzNbUoUFNqxB4laF/r -4YvC7m15ocED0mpnIKBghBlK2VaLUA93xAS+XiwdcszwkuzkTUnEbyUfffL2JSHo -dZdEDTmXV3wW4Ywfyn2Sma0CgYAeNNG/FLEg6iw9QE/ROqob/+RGyjFklGunqQ0x -tbRo1xlQotTRI6leMz3xk91aXoYqZjmPBf7GFH0/Hr1cOxkkZM8e4MVAPul4Ybr7 -LheP/xhoSBgD24OKtGYfCoyRETdJP98vUGBN8LYXLt8lK+UKBeHDYmXKRE156ZuP -AmRIcQKBgFvp+xMoyAsBeOlTjVDZ0mTnFh1yp8f7N3yXdHPpFShwjXjlqLmLO5RH -mZAvaH0Ux/wCfvwHhdC46jBrs9S4zLBvj3+44NYOzvz2dBWP/5MuXgzFe30h9Yd0 -zUlyEaWm0jY2Ylzax8ECKRL0td2bv36vxOYtTax8MSB15szsnPJ+ ------END RSA PRIVATE KEY----- -`; - -export const csr = `-----BEGIN CERTIFICATE REQUEST----- -MIICdDCCAVwCAQAwDjEMMAoGA1UEAxMDbG9sMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEA4Dz2b/nAP/M6bqyk5mctqqYAAcoME//xPBy0wREHuZ776Pu4 -l45kDL3dPXiY8U2P9pn8WIr2KpLK6oWUfSsiG2P082bpWDL20UymkWqDhhrA4unf -ZRq68UIDbcetlLw15YKnlNdvNZ7Qr8Se8KV0YGR/wFqI7QfS6VE3lhxZWEBUayI0 -egqOuDbXAcZTON1AZ92/F+WFSbc43iYdDk16XfAPFKhtvLr6zQQuzebAb7HG04Hc -GhRskixxyJ8XY6XUplfsa1HcpUXE4f1GeUvq3g6ltVCSJ0p7qI9FFjV4t+DCLVVV -LnwHUi9Vzz6i2wjMt7P6+gHR+RrOWBgRMn38fwIDAQABoCEwHwYJKoZIhvcNAQkO -MRIwEDAOBgNVHREEBzAFggNsb2wwDQYJKoZIhvcNAQELBQADggEBAAm3AHQ1ctdV -8HCrMOXGVLgI2cB1sFd6VYVxPBxIk812Y4wyO8Q6POE5VZNTIgMcSeIaFu5lgHNL -Peeb54F+zEa+OJYkcWgCAX5mY/0HoML4p2bxFTSjllSpcX7ktjq4IEIY/LRpqSgc -jgZHHRwanFfkeIOhN4Q5qJWgBPNhDAcNPE7T0M/4mxqYDqMSJvMYmC67hq1UOOug -/QVDUDJRC1C0aDw9if+DbG/bt1V6HpMQhDIEUjzfu4zG8pcag3cJpOA8JhW1hnG0 -XA2ZOCA7s34/szr2FczXtIoKiYmv3UzPyO9/4mc0Q2+/nR4CG8NU9WW/XJCne9ID -elRplAzrMF4= ------END CERTIFICATE REQUEST-----`; - -export const csr2 = `-----BEGIN CERTIFICATE REQUEST----- -MIIChDCCAWwCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQCuW9C58M1wO0vdGmtLcJbbCkKyfsHJJae1j4LL -xdGqs1j9UKD66UALSzZEeMCBdtTNNzThAgYJqCSA5swqpbRf6WZ3K/X7oHbfcrHi -SAm8v/0QsJDF5Rphiy6wyggaoaHEsbSp83kYy9r+h48vFW5Dr8UvJTsp5kdRn31L -bTHr56iqOaHQbu6hDj4Ompg/0OElPH1tV2X947o8timR+L89utZzR+d8x/eeTdPl -H7TEkMEomRvt7NTRHGYRsm3Gzq4AA6PalzIxzwJrNgXfJDutNn/QwcVd5sImwYCO -GaHsOvGfc02w+Vqqva9EOEQSr6B90kA+vc30I6uSiugzV9TFAgMBAAGgKTAnBgkq -hkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEB -CwUAA4IBAQAjm6JTU7axU6TzLlXlOp7hZ4+nep2/8vvJ9EOXzL8x/qtTTizctdG9 -Op70gywoUxAS2tatwa4fmW9DbA2eGiLU+Ibj/5b0Veq5DQdp1Qg3MLBP/+AcM/7m -rrgA9MhkpQahXCj4vXo6NeXYaTh6Jo/s8C9h3WxTD6ptDMiaPFcEuWcx0e3AjjH0 -pe7k9/MfB2wLfQ7+5wee/tCFWZN4tk8YfjQeQA1extXYKM/f8eu3Z/wjbbMOVpwb -xst+VTY7X9T8cU/hjDEoNG677meI+W5MgiwX0rxTpoz991fqr3vp7PELYj3GMyii -D1YfvqXieNij4UrduRqCXj1m8SVZlM+X ------END CERTIFICATE REQUEST-----`; - -export const componentPemBundle = `-----BEGIN CERTIFICATE----- -MIIDGjCCAgKgAwIBAgIUFvnhb2nQ8+KNS3SzjlfYDMHGIRgwDQYJKoZIhvcNAQEL -BQAwDTELMAkGA1UEAxMCZmEwHhcNMTgwMTEwMTg1NDI5WhcNMTgwMjExMTg1NDU5 -WjANMQswCQYDVQQDEwJmYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AN2VtBn6EMlA4aYre/xoKHxlgNDxJnfSQWfs6yF/K201qPnt4QF9AXChatbmcKVn -OaURq+XEJrGVgF/u2lSos3NRZdhWVe8o3/sOetsGxcrd0gXAieOSmkqJjp27bYdl -uY3WsxhyiPvdfS6xz39OehsK/YCB6qCzwB4eEfSKqbkvfDL9sLlAiOlaoHC9pczf -6/FANKp35UDwInSwmq5vxGbnWk9zMkh5Jq6hjOWHZnVc2J8J49PYvkIM8uiHDgOE -w71T2xM5plz6crmZnxPCOcTKIdF7NTEP2lUfiqc9lONV9X1Pi4UclLPHJf5bwTmn -JaWgbKeY+IlF61/mgxzhC7cCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFLDtc6+HZN2lv60JSDAZq3+IHoq7MB8GA1Ud -IwQYMBaAFLDtc6+HZN2lv60JSDAZq3+IHoq7MA0GA1UdEQQGMASCAmZhMA0GCSqG -SIb3DQEBCwUAA4IBAQDVt6OddTV1MB0UvF5v4zL1bEB9bgXvWx35v/FdS+VGn/QP -cC2c4ZNukndyHhysUEPdqVg4+up1aXm4eKXzNmGMY/ottN2pEhVEWQyoIIA1tH0e -8Kv/bysYpHZKZuoGg5+mdlHS2p2Dh2bmYFyBLJ8vaeP83NpTs2cNHcmEvWh/D4UN -UmYDODRN4qh9xYruKJ8i89iMGQfbdcq78dCC4JwBIx3bysC8oF4lqbTYoYNVTnAi -LVqvLdHycEOMlqV0ecq8uMLhPVBalCmIlKdWNQFpXB0TQCsn95rCCdi7ZTsYk5zv -Q4raFvQrZth3Cz/X5yPTtQL78oBYrmHzoQKDFJ2z ------END CERTIFICATE-----`; - -// for parse-pki-cert tests: -// certificate contains all allowable params -export const loadedCert = `-----BEGIN CERTIFICATE-----\nMIIE7TCCA9WgAwIBAgIULcrWXSz3/kG81EgBo0A4Zt+ZgkYwDQYJKoZIhvcNAQEL\nBQAwga0xDzANBgNVBAYTBkZyYW5jZTESMBAGA1UECBMJQ2hhbXBhZ25lMQ4wDAYD\nVQQHEwVQYXJpczETMBEGA1UECRMKMjM0IHNlc2FtZTEPMA0GA1UEERMGMTIzNDU2\nMQ8wDQYDVQQKEwZXaWRnZXQxEDAOBgNVBAsTB0ZpbmFuY2UxGDAWBgNVBAMTD2Nv\nbW1vbi1uYW1lLmNvbTETMBEGA1UEBRMKY2VyZWFsMTI5MjAeFw0yMzAyMDMxNzI3\nMzNaFw0yMzAzMDcxNzI4MDNaMIGtMQ8wDQYDVQQGEwZGcmFuY2UxEjAQBgNVBAgT\nCUNoYW1wYWduZTEOMAwGA1UEBxMFUGFyaXMxEzARBgNVBAkTCjIzNCBzZXNhbWUx\nDzANBgNVBBETBjEyMzQ1NjEPMA0GA1UEChMGV2lkZ2V0MRAwDgYDVQQLEwdGaW5h\nbmNlMRgwFgYDVQQDEw9jb21tb24tbmFtZS5jb20xEzARBgNVBAUTCmNlcmVhbDEy\nOTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8NO7LXHp28SzOqmQv\nns4fGogKydEklWG4JEN3pM+k9nTyEgA8DFhtSLvcqF0cqhEydw4FVU+LEUGySUer\nmM4VNl9qglFBgmYE8TNgWkUw9ZP6MNgx13I8zXTXOIDj0iwXks02x8451oPbqqdq\nXsCc4vSP7BPwQOjc0C56c54zyRC1zFm9jlh+As0QinuYcjFjVabCku6JSYc4kunh\nz7derU9cURUxB5/ja9zC7jGS8tg4XUWdUkbj1O/krEWfjQx9Kj8aEU1gFfAvW/Bd\nIqgAlHATYN6i8HDmAmdGty9zLht9wUgnAtVh3lK3939h/rI0qCLV6N/RjCC7csnz\n9I67AgMBAAGjggEBMIH+MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/\nAgERMB0GA1UdDgQWBBSSdKle0wMGy0jvPmcoDanGduhLqzAfBgNVHSMEGDAWgBSS\ndKle0wMGy0jvPmcoDanGduhLqzAuBgNVHR4BAf8EJDAioCAwDoIMZG5zbmFtZTEu\nY29tMA6CDGRzbm5hbWUyLmNvbTBoBgNVHREEYTBfoB0GCCsBBAEFCQIGoBEMD3Nv\nbWUtdXRmLXN0cmluZ4IIYWx0bmFtZTGCCGFsdG5hbWUyhwTAngEmhxASNA/SViEA\nAQCJAAAAAEUAhgh0ZXN0dXJpMYYIdGVzdHVyaTIwDQYJKoZIhvcNAQELBQADggEB\nAAQukDwIg01QLQK2MQqjePNZlJleKLMK9LiabyGqc7u4bgmX3gYIrH7uopvO5cIv\nvqxcVBATQ6ez29t5MagzDu1+vnwE8fQhRoe0sp5TRLiGSlBJf53+0Wb3vbaOT0Fx\n/FFK0f2wHqYv3h/CTxu8YxDY4DwCRTPJ2KfTvT85BXtTUlzKIp1ytALSKcz0Owoe\neQPtQUdi8UHef8uHuWbk7DftMXojXbCqtHQdS3Rrl9zyc+Ds67flb5hKEseQZRgw\ntPtAIxhjSfZPTjl/3aasCBikESdeS8IOxIXL1bGun0xWnIBBc9uRe8hpdPjZj7Eh\nIt7ucIzFep0DLWCeQrAHeqo=\n-----END CERTIFICATE-----`; -// use_pss = true -export const pssTrueCert = `-----BEGIN CERTIFICATE-----\nMIIDqTCCAl2gAwIBAgIUVY2PTRZl1t/fjfyEwrG4HvGjYekwQQYJKoZIhvcNAQEK\nMDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEF\nAKIDAgEgMBoxGDAWBgNVBAMTD2NvbW1vbi1uYW1lLmNvbTAeFw0yMzAxMjEwMTA3\nNDBaFw0yMzAyMjIwMTA4MTBaMBoxGDAWBgNVBAMTD2NvbW1vbi1uYW1lLmNvbTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANlG6DuZ4B6Tv8u8rI+pUvQv\n2E7dFCu+i1YAbEJSnuAQ9XFUG5Uf3uHB8AFrOKRBAaFdBV4hKvBpfvMj3jl93d0b\nHdHeIM+sancDwpexpLvSW4yDpbIhAnkYzbUYgZyEAJeIgq/4ufT77TCK8XIzDywD\nhXZtDJkc6w3mm6hiqEQXLKnDQTfKLK8Fbsq4OuQ4vO5VIJrVZ1gKemDs7W/9WIzp\n0iSjzcIfWnUy1Dpk+AF8HhXok8CbhHfOGgbQZ6DcXOIJeb4XarJ9sgLJNAuhdcHR\ngP0TkPiOewbBG9Ish1p3F+pkI3vjQk4cghmilAuEkMc2NCNNy6q1bwSELVQnMiMC\nAwEAAaN/MH0wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFAsrMoFu6tt1pybxx9ln6w5QK/2tMB8GA1UdIwQYMBaAFAsrMoFu6tt1pybx\nx9ln6w5QK/2tMBoGA1UdEQQTMBGCD2NvbW1vbi1uYW1lLmNvbTBBBgkqhkiG9w0B\nAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQC\nAQUAogMCASADggEBAFh+PMwEmxaZR6OtfB0Uvw2vA7Oodmm3W0bYjQlEz8U+Q+JZ\ncIPa4VnRy1QALmKbPCbRApA/gcWzIwtzo1JhLtcDINg2Tl0nj4WvgpIvj0/lQNMq\nmwP7G/K4PyJTv3+y5XwVfepZAZITB0w5Sg5dLC6HP8AGVIaeb3hGNHYvPlE+pbT+\njL0xxzFjOorWoy5fxbWoVyVv9iZ4j0zRnbkYHIi3d8g56VV6Rbyw4WJt6p87lmQ8\n0wbiJTtuew/0Rpuc3PEcR9XfB5ct8bvaGGTSTwh6JQ33ohKKAKjbBNmhBDSP1thQ\n2mTkms/mbDRaTiQKHZx25TmOlLN5Ea1TSS0K6yw=\n-----END CERTIFICATE-----`; -// only has common name -export const skeletonCert = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgIUVQy58VgdVpAK9c8SfS31idSv6FUwDQYJKoZIhvcNAQEL\nBQAwGjEYMBYGA1UEAxMPY29tbW9uLW5hbWUuY29tMB4XDTIzMDEyMTAxMjAyOVoX\nDTIzMDIyMjAxMjA1OVowGjEYMBYGA1UEAxMPY29tbW9uLW5hbWUuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2UboO5ngHpO/y7ysj6lS9C/YTt0U\nK76LVgBsQlKe4BD1cVQblR/e4cHwAWs4pEEBoV0FXiEq8Gl+8yPeOX3d3Rsd0d4g\nz6xqdwPCl7Gku9JbjIOlsiECeRjNtRiBnIQAl4iCr/i59PvtMIrxcjMPLAOFdm0M\nmRzrDeabqGKoRBcsqcNBN8osrwVuyrg65Di87lUgmtVnWAp6YOztb/1YjOnSJKPN\nwh9adTLUOmT4AXweFeiTwJuEd84aBtBnoNxc4gl5vhdqsn2yAsk0C6F1wdGA/ROQ\n+I57BsEb0iyHWncX6mQje+NCThyCGaKUC4SQxzY0I03LqrVvBIQtVCcyIwIDAQAB\no38wfTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU\nCysygW7q23WnJvHH2WfrDlAr/a0wHwYDVR0jBBgwFoAUCysygW7q23WnJvHH2Wfr\nDlAr/a0wGgYDVR0RBBMwEYIPY29tbW9uLW5hbWUuY29tMA0GCSqGSIb3DQEBCwUA\nA4IBAQDPco+FIHXczf0HTwFAmIVu4HKaeIwDsVPxoUqqWEix8AyCsB5uqpKZasby\nedlrdBohM4dnoV+VmV0de04y95sdo3Ot60hm/czLog3tHg4o7AmfA7saS+5hCL1M\nCJWqoJHRFo0hOWJHpLJRWz5DqRZWspASoVozLOYyjRD+tNBjO5hK4FtaG6eri38t\nOpTt7sdInVODlntpNuuCVprPpHGj4kPOcViQULoFQq5fwyadpdjqSXmEGlt0to5Y\nMbTb4Jhj0HywgO53BUUmMzzY9idXh/8A7ThrM5LtqhxaYHLVhyeo+5e0mgiXKp+n\nQ8Uh4TNNTCvOUlAHycZNaxYTlEPn\n-----END CERTIFICATE-----`; -// contains unsupported subject and extension OIDs -export const unsupportedOids = `-----BEGIN CERTIFICATE-----\nMIIEjDCCA3SgAwIBAgIUD4EeORgh/i+ZZFOk8KsGKQPWsoIwDQYJKoZIhvcNAQEL\nBQAwgZIxMTAvBgNVBAMMKGZhbmN5LWNlcnQtdW5zdXBwb3J0ZWQtc3Viai1hbmQt\nZXh0LW9pZHMxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZLYW5zYXMxDzANBgNVBAcM\nBlRvcGVrYTESMBAGA1UECgwJQWNtZSwgSW5jMRowGAYJKoZIhvcNAQkBFgtmb29A\nYmFyLmNvbTAeFw0yMzAxMjMxODQ3MjNaFw0zMzAxMjAxODQ3MjNaMIGSMTEwLwYD\nVQQDDChmYW5jeS1jZXJ0LXVuc3VwcG9ydGVkLXN1YmotYW5kLWV4dC1vaWRzMQsw\nCQYDVQQGEwJVUzEPMA0GA1UECAwGS2Fuc2FzMQ8wDQYDVQQHDAZUb3Bla2ExEjAQ\nBgNVBAoMCUFjbWUsIEluYzEaMBgGCSqGSIb3DQEJARYLZm9vQGJhci5jb20wggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDyYH5qS7krfZ2tA5uZsY2qXbTb\ntGNG1BsyDhZ/qqVlQybjDsHJZwNUbpfhBcCLaKyAwH1R9n54NOOOn6bYgfKWTgy3\nL7224YDAqYe7Y/GPjgI2MRvRfn6t2xzQxtJ0l0k8LeyNcwhiqYLQyOOfDdc127fm\nW40r2nmhLpH0i9e2I/YP1HQ+ldVgVBqeUTntgVSBfrQF56v9mAcvvHEa5sdHqmX4\nJ2lhWTnx9jqb7NZxCem76BlX1Gt5TpP3Ym2ZFVQI9fuPK4O8JVhk1KBCmIgR3Ft+\nPpFUs/c41EMunKJNzveYrInSDScaC6voIJpK23nMAiM1HckLfUUc/4UojD+VAgMB\nAAGjgdcwgdQwHQYDVR0OBBYEFH7tt4enejKTZtYjUKUUx6PXyzlgMB8GA1UdIwQY\nMBaAFH7tt4enejKTZtYjUKUUx6PXyzlgMA4GA1UdDwEB/wQEAwIFoDAgBgNVHSUB\nAf8EFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBCjBM\nBgNVHREERTBDhwTAngEmhgx1cmlTdXBwb3J0ZWSCEWRucy1OYW1lU3VwcG9ydGVk\noBoGAyoDBKATDBFleGFtcGxlIG90aGVybmFtZTANBgkqhkiG9w0BAQsFAAOCAQEA\nP6ckVJgbcJue+MK3RVDuG+Mh7dl89ynC7NwpQFRjLVZQuoMHZT/dcLlVeFejVXu5\nR+IPLmQU6NV7JAmy4zGap8awf12QTy3g410ecrSF94WWlu8bPoekfUnnP+kfzLPH\nCUAkRKxWDSRKX5C8cMMxacVBBaBIayuusLcHkHmxLLDw34PFzyz61gtZOJq7JYnD\nhU9YsNh6bCDmnBDBsDMOI7h8lBRQwTiWVoSD9YNVvFiY29YvFbJQGdh+pmBtf7E+\n1B/0t5NbvqlQSbhMM0QgYFhuCxr3BGNob7kRjgW4i+oh+Nc5ptA5q70QMaYudqRS\nd8SYWhRdxmH3qcHNPcR1iw==\n-----END CERTIFICATE-----`; -// unsupportedPem is same cert as above, formatted differently -export const unsupportedPem = ` ------BEGIN CERTIFICATE----- -MIIEjDCCA3SgAwIBAgIUD4EeORgh/i+ZZFOk8KsGKQPWsoIwDQYJKoZIhvcNAQEL -BQAwgZIxMTAvBgNVBAMMKGZhbmN5LWNlcnQtdW5zdXBwb3J0ZWQtc3Viai1hbmQt -ZXh0LW9pZHMxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZLYW5zYXMxDzANBgNVBAcM -BlRvcGVrYTESMBAGA1UECgwJQWNtZSwgSW5jMRowGAYJKoZIhvcNAQkBFgtmb29A -YmFyLmNvbTAeFw0yMzAxMjMxODQ3MjNaFw0zMzAxMjAxODQ3MjNaMIGSMTEwLwYD -VQQDDChmYW5jeS1jZXJ0LXVuc3VwcG9ydGVkLXN1YmotYW5kLWV4dC1vaWRzMQsw -CQYDVQQGEwJVUzEPMA0GA1UECAwGS2Fuc2FzMQ8wDQYDVQQHDAZUb3Bla2ExEjAQ -BgNVBAoMCUFjbWUsIEluYzEaMBgGCSqGSIb3DQEJARYLZm9vQGJhci5jb20wggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDyYH5qS7krfZ2tA5uZsY2qXbTb -tGNG1BsyDhZ/qqVlQybjDsHJZwNUbpfhBcCLaKyAwH1R9n54NOOOn6bYgfKWTgy3 -L7224YDAqYe7Y/GPjgI2MRvRfn6t2xzQxtJ0l0k8LeyNcwhiqYLQyOOfDdc127fm -W40r2nmhLpH0i9e2I/YP1HQ+ldVgVBqeUTntgVSBfrQF56v9mAcvvHEa5sdHqmX4 -J2lhWTnx9jqb7NZxCem76BlX1Gt5TpP3Ym2ZFVQI9fuPK4O8JVhk1KBCmIgR3Ft+ -PpFUs/c41EMunKJNzveYrInSDScaC6voIJpK23nMAiM1HckLfUUc/4UojD+VAgMB -AAGjgdcwgdQwHQYDVR0OBBYEFH7tt4enejKTZtYjUKUUx6PXyzlgMB8GA1UdIwQY -MBaAFH7tt4enejKTZtYjUKUUx6PXyzlgMA4GA1UdDwEB/wQEAwIFoDAgBgNVHSUB -Af8EFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBCjBM -BgNVHREERTBDhwTAngEmhgx1cmlTdXBwb3J0ZWSCEWRucy1OYW1lU3VwcG9ydGVk -oBoGAyoDBKATDBFleGFtcGxlIG90aGVybmFtZTANBgkqhkiG9w0BAQsFAAOCAQEA -P6ckVJgbcJue+MK3RVDuG+Mh7dl89ynC7NwpQFRjLVZQuoMHZT/dcLlVeFejVXu5 -R+IPLmQU6NV7JAmy4zGap8awf12QTy3g410ecrSF94WWlu8bPoekfUnnP+kfzLPH -CUAkRKxWDSRKX5C8cMMxacVBBaBIayuusLcHkHmxLLDw34PFzyz61gtZOJq7JYnD -hU9YsNh6bCDmnBDBsDMOI7h8lBRQwTiWVoSD9YNVvFiY29YvFbJQGdh+pmBtf7E+ -1B/0t5NbvqlQSbhMM0QgYFhuCxr3BGNob7kRjgW4i+oh+Nc5ptA5q70QMaYudqRS -d8SYWhRdxmH3qcHNPcR1iw== ------END CERTIFICATE-----`; -export const certWithoutCN = `-----BEGIN CERTIFICATE-----\nMIIDUDCCAjigAwIBAgIUEUpM5i7XMd/imZkR9XvonMaqPyYwDQYJKoZIhvcNAQEL\nBQAwHDEaMBgGCSqGSIb3DQEJARYLZm9vQGJhci5jb20wHhcNMjMwMTIzMjMyODEw\nWhcNMzMwMTIwMjMyODEwWjAcMRowGAYJKoZIhvcNAQkBFgtmb29AYmFyLmNvbTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPGSdeqLICZcoUzxk88F8Tp+\nVNI+mS74L8pHyb9ZNZfeXPo0E9L5pi+KKI7rkxAtBGUecG1ENSxDDK9p6XZhWHSU\nZ6bdjOsjcIlfiM+1hhtDclIVxIDnz2Jt1/Vmnm8DXwdwVATWiFLTnfm288deNwsT\npl0ehAR3BadkZvteC6t+giEw/4qm1/FP53GEBOQeUWJDZRvtL37rdx4joFv3cR4w\nV0dukOjc5AGXtIOorO145OSZj8s7RsW3pfGcFUcOg7/flDxfK1UqFflQa7veLvKa\nWE/fOMyB/711QjSkTuQ5Rw3Rf9Fr2pqVJQgElTIW1SKaX5EJTB9mtGB34UqUXtsC\nAwEAAaOBiTCBhjAdBgNVHQ4EFgQUyhFP/fm+798mErPD5VQvEaAZQrswHwYDVR0j\nBBgwFoAUyhFP/fm+798mErPD5VQvEaAZQrswDgYDVR0PAQH/BAQDAgWgMCAGA1Ud\nJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEK\nMA0GCSqGSIb3DQEBCwUAA4IBAQCishzVkhuSAtqxgsZdYzBs3GpakGIio5zReW27\n6dk96hYCbbe4K3DtcFbRD1B8t6aTJlHxkFRaOWErSXu9WP3fUhIDNRE64Qsrg1zk\n3Km430qBlorXmTp6xhYHQfY5bn5rT2YY7AmaYIlIFxRhod43i5GDbBP+e+d/vTqR\nv1AJflYofeR4LeATP64B6a4R+QQVoxI43+pyH3ka+nRHwJBR9h8SMtJoqBy7x9pl\nYlBDa8lSn05doA3+e03VIzitvBBWI4oX1XB0tShSLk6YJXayIwe0ZNVvfYLIRKCp\nb4DUwChYzG/FwFSssUAqzVFhu3i+uU3Z47bsLVm0R5m7hLiZ\n-----END CERTIFICATE-----`; - -// CROSS-SIGNING: -export const newCSR = { - common_name: 'Short-Lived Int R1', - csr: `-----BEGIN CERTIFICATE REQUEST-----\nMIICYjCCAUoCAQAwHTEbMBkGA1UEAxMSU2hvcnQtTGl2ZWQgSW50IFIxMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqsvFU7lzt06n1w6BL+Waf9zd+Z3G\n90Kv0HAksoLaWYinhkxNIUTU8ar9HLa2WV4EoJbNq91Hn+jFc2SYEXtRV+jm0kEh\nz4C4AoQ4D0s83JcYNssiNbVA04wa5ovD0iA/pzwVz8TnJSfAAuZ3vXFlyIbQ3ESo\nzt9hGjo/JOpoBh67E7xkuzw4lnC2rXGHdh9pk1Di+wqREnKU4nuhDLnTC/LL+Mkm\n07F1aMAW3Z/PWFmmsDJHMhZnaYo2LGCwU4A0U1ED0XpwflobVbkzZDmCXOPEI8UX\nG6VcL36zWnzEQnlZKN91MAa+s0E4z40KHKVSblSkjYD1K6n0y787ic2mDwIDAQAB\noAAwDQYJKoZIhvcNAQELBQADggEBAFQtiJaRfaQS3jHr7aFeszB/JmDRQiOoML3g\nhA3EcVd2rvDjiqikwD9EFdLTJyYJfb+9yiKDJqB7Fw2GPSrFxrd+jC9qZRI3VEWK\n8VdflLbruc1FcqJcE/0z2hWa11eud1bMLq8U6AfxNHL4r4ukrp2D5elrdsrDnhZj\nwMi3FtEFd4RZVaWZYVmWcQTeH7Zz/LYwkVDgBuvC+SOCaNNo/dCurkAAoxw8obBj\n1FS2F/3oHQxMui8vS8j6sMWMPZ5D3Q0xSC3HBUNoI2ZC77Mxn9yfj6ianUXKOOlf\nQMRaPBVajxZm9ovV64QKr+7HK7W7U/fNEqvoKBUDCqEuWmSsxMk=\n-----END CERTIFICATE REQUEST-----`, -}; - -export const oldParentIssuerCert = `-----BEGIN CERTIFICATE-----\nMIIDKzCCAhOgAwIBAgIUMCEF+bzBC4NQIWjE1sv/RbnYfUgwDQYJKoZIhvcNAQEL\nBQAwHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgxMB4XDTIzMDEyNTAwMjQz\nM1oXDTIzMDIyNjAwMjUwM1owHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgx\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0zrpJvjWcBV49Eor+zfh\nGW40xH6PcPSpzWGCCFiMPFwKrBSjuGRwwkLsXU7u2P15jIV/IU2kPS+WOW+EIe0x\ns5X2SoujZGOmM6du/6HIo9lz9yjb5G1SHdv/e65Q45QWb6wQcuO4axffvPzmAU9L\nQcunEF4g3rCz4cHYumi0osybbwR45z+8owNhykdbu7AwV0Cyz3C/lT1wxDxbFr0Y\n1NEjQ8AF4oRzqkmGoLp6ixDxp8zMpOlKWWYem1mx0RbqlwLP7khiS5YKi8+j8aog\nOhHA/W4i+ihrBzkv4GtOSdkhJz5qacifydUXtJ7SmvYs9Fi+hFgw61sw23ywbr3+\nywIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUQsdYFMtsNMYNDIhZHMd77kcLLi8wHwYDVR0jBBgwFoAUQsdYFMtsNMYN\nDIhZHMd77kcLLi8wDQYJKoZIhvcNAQELBQADggEBAFNKTnNUzjZGHpXVK9Go8k/i\nVMNBktjGp58z+EN32TJnq/tOW1eVswUmq71S3R16Iho4XZDZVchuK+zhqSwlAmgM\no1vs6L5IJ0rVZcLZpysxFtawlbA362zBOX0F7tqStdEeBWaXw6J+MQ26xAPgHjXo\nc3fqqNWGbrOPt1uFoXWD+0Bg8M90a7OT0ijubh/PcuCe1yF9G2BqRQruB05gZiHl\n0NGbUka1ntD/lxYfLeSnp+FHJVDrcAHwPhKQS8HHr/ZBjKEGY8In+JIi/KBV/M8b\nGeW2k5odl6r2UIR6PWSei1WKKHe09WzO7rGJaN6uKLP14c0nSF3/q+AQY3m+tPY=\n-----END CERTIFICATE-----\n`; -export const parentIssuerCert = `-----BEGIN CERTIFICATE-----\nMIIDKzCCAhOgAwIBAgIUBxLeuD3K0hF5dGpaEgZqytTN3lswDQYJKoZIhvcNAQEL\nBQAwHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgyMB4XDTIzMDEyNTAwMjQz\nM1oXDTIzMDIyNjAwMjUwM1owHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgy\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuqkwN4m5dFLwFi0iYs4r\nTO4HWzloF4yCOaNfVksh1cOafVu9vjFwOWgHZFe6h8BOn6biKdFtvGTyIzlMHe5t\nyFmec9pfjX243bH9Ev4n2RTMKs818g9LdoZT6SI7DxHEu3yuHBg9TM87+GB+dA1V\nkRsK5hgtNCSMdgFSljM169sYbNilpk8M7O2hr+AmgRi0c1nUEPCe4JAr0Zv8iweJ\ntFRVHiQJXD9WIVxaWVxqWFsHoXseZS7H76RSdf4jNfENmBguHZMAPhtqlc/pMan8\nu0IJETWjWENn+WYC7DnnfQtNqyebU2LdT3oKO8tELqITygjT2tCS1Zavmsy69VY0\nYwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUxgchIBo+1F++IFW0F586I5QDFGYwHwYDVR0jBBgwFoAUxgchIBo+1F++\nIFW0F586I5QDFGYwDQYJKoZIhvcNAQELBQADggEBAI6DdnW8q/FqGqk/Y0k7iUrZ\nYkfMRlss6uBTzLev53eXqFIJ3+EFVfV+ohDEedlYYm2QCELzQcJSR7Q2I22PQj8X\nTO0yqk6LOCMv/4yiDhF4D+haiDU4joq5GX1dpFdlNSQ5fJmnLKu8HYbOhbwUo4ns\n4yGzIMulZR1Zqf/HGEOCYPDQ0ZHucmHn7uGhmV+kgYGoKVEZ8XxfmyNPKuwTAUHL\nfInPJZtbxXTVmiWWy3iraeI4XcUvaD0JtVnsVphYrqrSZ60DjgFsjiyenxePGHXf\nYXV9HIS6OXlvWhJKlSINOTv9fAa+e+JtK7frdvxJNHoTG34PiGXfOV2swTvLJQo=\n-----END CERTIFICATE-----\n`; -export const intIssuerCert = `-----BEGIN CERTIFICATE-----\nMIIDKzCCAhOgAwIBAgIUPt5VyO6gyA4hVaMkdpNyBlP+I64wDQYJKoZIhvcNAQEL\nBQAwHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgxMB4XDTIzMDEyNTAwMjQz\nM1oXDTIzMDIyNjAwMjUwM1owHTEbMBkGA1UEAxMSU2hvcnQtTGl2ZWQgSW50IFIx\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqsvFU7lzt06n1w6BL+Wa\nf9zd+Z3G90Kv0HAksoLaWYinhkxNIUTU8ar9HLa2WV4EoJbNq91Hn+jFc2SYEXtR\nV+jm0kEhz4C4AoQ4D0s83JcYNssiNbVA04wa5ovD0iA/pzwVz8TnJSfAAuZ3vXFl\nyIbQ3ESozt9hGjo/JOpoBh67E7xkuzw4lnC2rXGHdh9pk1Di+wqREnKU4nuhDLnT\nC/LL+Mkm07F1aMAW3Z/PWFmmsDJHMhZnaYo2LGCwU4A0U1ED0XpwflobVbkzZDmC\nXOPEI8UXG6VcL36zWnzEQnlZKN91MAa+s0E4z40KHKVSblSkjYD1K6n0y787ic2m\nDwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUkBK+oGpo5DNj2pCKoUE08WFOxQUwHwYDVR0jBBgwFoAUQsdYFMtsNMYN\nDIhZHMd77kcLLi8wDQYJKoZIhvcNAQELBQADggEBAIf4Bp/NYftiN8LmQrVzPWAe\nc4Bxm/NFFtkwQEvFhndMN68MUyXa5yxAdnYAHN+fRpYPxbjoZNXjW/jx3Kjft44r\ntyNGrrkjR80TI9FbL53nN7hLtZQdizsQD0Wype4Q1JOIxYw2Wd5Hr/PVPrJZ3PGg\nwNeI5IRu/cVbVT/vkRaHqYSwpa+V2cZTaEk6h62KPaKu3ui+omoeitU6qXHOysXQ\nrdGkJl/x831sIKmN0dMiGeoJdHGAr/E2f3ijKbVPsjIxZbm2SSumldOFYWn9cNYD\nI6sizFH976Wpde/GRIvBIzJnlK3xgfy0D9AUvwKyt75PVEnshc9tlhxoSVlKaUE=\n-----END CERTIFICATE-----\n`; -export const newlySignedCert = `-----BEGIN CERTIFICATE-----\nMIIDKzCCAhOgAwIBAgIUKapKK5Coau2sfIJgqA9jcC6BkWIwDQYJKoZIhvcNAQEL\nBQAwHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgyMB4XDTIzMDEyNTIyMjky\nNVoXDTIzMDIyNjIyMjk1NVowHTEbMBkGA1UEAxMSU2hvcnQtTGl2ZWQgSW50IFIx\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqsvFU7lzt06n1w6BL+Wa\nf9zd+Z3G90Kv0HAksoLaWYinhkxNIUTU8ar9HLa2WV4EoJbNq91Hn+jFc2SYEXtR\nV+jm0kEhz4C4AoQ4D0s83JcYNssiNbVA04wa5ovD0iA/pzwVz8TnJSfAAuZ3vXFl\nyIbQ3ESozt9hGjo/JOpoBh67E7xkuzw4lnC2rXGHdh9pk1Di+wqREnKU4nuhDLnT\nC/LL+Mkm07F1aMAW3Z/PWFmmsDJHMhZnaYo2LGCwU4A0U1ED0XpwflobVbkzZDmC\nXOPEI8UXG6VcL36zWnzEQnlZKN91MAa+s0E4z40KHKVSblSkjYD1K6n0y787ic2m\nDwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUkBK+oGpo5DNj2pCKoUE08WFOxQUwHwYDVR0jBBgwFoAUxgchIBo+1F++\nIFW0F586I5QDFGYwDQYJKoZIhvcNAQELBQADggEBAJaems1vgEjxgb3d1y9PYxzN\nLZbuf/+0+BCVa9k4bEsbuhXhEecFdIi2OKS6fabeoEOF97Gvqrgc+LEpNsU6lIRA\nkJ/nHe0CD2hf0aBQsGsOllYy/4QnrPlbowb4KizPknEMWdGcvfnlzzOJzo4/UuMk\nMZ9vn2GrINzfml/sLocOzP/MsPd8bBhXI2Emh2O9tJ4+zeHLhEzcM1gdNk8pp+wP\nEOks0EcN4UBkpEnDZcDTJVgp9XpWy19EEGqsxjBq6rlpIvPW8XHoH1jZSGY1KWBJ\nRGtDcGugwTxO9jYHz/a1qu4BVt5FFcb0L3IOvcr+3QCCeiJQHcVY8QRbO9M4AQk=\n-----END CERTIFICATE-----\n`; -// both certs generated with key type ed25519 -export const unsupportedSignatureRoot = `-----BEGIN CERTIFICATE-----\nMIIBXTCCAQ+gAwIBAgIUcp9CkzsU5Pkv2ZJO8Gp+tJrzuJYwBQYDK2VwMBIxEDAO\nBgNVBAMTB215LXJvb3QwHhcNMjMwNzE4MTYyNzQ3WhcNMjMwODE5MTYyODE3WjAS\nMRAwDgYDVQQDEwdteS1yb290MCowBQYDK2VwAyEAmZ+By07QvgAEX1HRjhltJlgK\nA8il2LYUpH0uw7f2lXCjdzB1MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD\nAQH/MB0GA1UdDgQWBBTAcYaOaiKhDmYqSe6vg/lAtYspkDAfBgNVHSMEGDAWgBTA\ncYaOaiKhDmYqSe6vg/lAtYspkDASBgNVHREECzAJggdteS1yb290MAUGAytlcANB\nAG9xXZnKNEXRyfa91hm9S80PwlwIMh4MkWetwfPBn3M74cHzDK1okANmweca4RRq\nQHDPT7shx3CuosvL2Ori/ws=\n-----END CERTIFICATE-----`; -export const unsupportedSignatureInt = `-----BEGIN CERTIFICATE-----\nMIICfTCCAWWgAwIBAgIUei2XIhhsP1/ytDciEGfA1C7t/sMwDQYJKoZIhvcNAQEL\nBQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjMwNzE4MTg1NDA3WhcNMjMw\nODE5MTg1NDM3WjASMRAwDgYDVQQDEwdpbnQtY3NyMCowBQYDK2VwAyEAa9vHnJA3\nnzA/fYiTUg8EhomjMtVp5O2c01nQRXEv72OjgcAwgb0wDgYDVR0PAQH/BAQDAgEG\nMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFGtjjUwrRGmFmYBHrUE38tSxvVM3\nMB8GA1UdIwQYMBaAFNng9+uArFyIUcD23XdvCSIfYiDPMEYGCCsGAQUFBwEBBDow\nODAZBggrBgEFBQcwAoYNaGFzaGljb3JwLmNvbTAbBggrBgEFBQcwAoYPdmF1bHRw\ncm9qZWN0LmlvMBIGA1UdEQQLMAmCB2ludC1jc3IwDQYJKoZIhvcNAQELBQADggEB\nAAOSNgZjesJG4BgLU8jQmOO7n6W8WcR+dT+ELDC1nLlEZ2BJCDSXXUX8AihIHKxn\nA9W4slABUacyJlAZo/o/wcxyfbA6PUXmHnoqEPZ3zXMwuLN/iRW7/uQvI6TIwnpH\nXETFARLmK8cfGgbhi24STkHTF4ljczkOab7sTUQTHELlo+F2gNtmgnyaBFCGUYor\nX1pkMBcBa9BWRsfhy8E+tBVVUrNNUddwzC/5nMLqT8XqENMndDoG7eeT9Ex6otZy\nzURkcq09FtcmyY2RBYkV4UzyHN7cESMIk/J33ZCNAfHaDGuOqTy5nYU5fTtjJcit\nwEcWiSesrKPCletBpuMpgiU=\n-----END CERTIFICATE-----\n`; diff --git a/ui/tests/helpers/pki/workflow.js b/ui/tests/helpers/pki/workflow.js deleted file mode 100644 index 5fdd05973..000000000 --- a/ui/tests/helpers/pki/workflow.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { SELECTORS as ROLEFORM } from './pki-role-form'; -import { SELECTORS as GENERATECERT } from './pki-role-generate'; -import { SELECTORS as KEYFORM } from './pki-key-form'; -import { SELECTORS as KEYPAGES } from './page/pki-keys'; -import { SELECTORS as ISSUERDETAILS } from './pki-issuer-details'; -import { SELECTORS as CONFIGURATION } from './pki-configure-create'; -import { SELECTORS as DELETE } from './pki-delete-all-issuers'; -import { SELECTORS as TIDY } from './page/pki-tidy-form'; -import { SELECTORS as CONFIGEDIT } from './page/pki-configuration-edit'; -import { SELECTORS as GENROOT } from './pki-generate-root'; - -export const SELECTORS = { - breadcrumbContainer: '[data-test-breadcrumbs]', - breadcrumbs: '[data-test-breadcrumbs] li', - overviewBreadcrumb: '[data-test-breadcrumbs] li:nth-of-type(2) > a', - pageTitle: '[data-test-pki-role-page-title]', - alertBanner: '[data-test-alert-banner="alert"]', - emptyState: '[data-test-component="empty-state"]', - emptyStateTitle: '[data-test-empty-state-title]', - emptyStateLink: '.empty-state-actions a', - emptyStateMessage: '[data-test-empty-state-message]', - // TABS - overviewTab: '[data-test-secret-list-tab="Overview"]', - rolesTab: '[data-test-secret-list-tab="Roles"]', - issuersTab: '[data-test-secret-list-tab="Issuers"]', - certsTab: '[data-test-secret-list-tab="Certificates"]', - keysTab: '[data-test-secret-list-tab="Keys"]', - tidyTab: '[data-test-secret-list-tab="Tidy"]', - configTab: '[data-test-secret-list-tab="Configuration"]', - // ROLES - deleteRoleButton: '[data-test-pki-role-delete]', - generateCertLink: '[data-test-pki-role-generate-cert]', - signCertLink: '[data-test-pki-role-sign-cert]', - editRoleLink: '[data-test-pki-role-edit-link]', - createRoleLink: '[data-test-pki-role-create-link]', - roleForm: { - ...ROLEFORM, - }, - generateCertForm: { - ...GENERATECERT, - }, - // KEYS - keyForm: { - ...KEYFORM, - }, - keyPages: { - ...KEYPAGES, - }, - // ISSUERS - issuerListItem: (id) => `[data-test-issuer-list="${id}"]`, - importIssuerLink: '[data-test-generate-issuer="import"]', - generateIssuerDropdown: '[data-test-issuer-generate-dropdown]', - generateIssuerRoot: '[data-test-generate-issuer="root"]', - generateIssuerIntermediate: '[data-test-generate-issuer="intermediate"]', - issuerPopupMenu: '[data-test-popup-menu-trigger]', - issuerPopupDetails: '[data-test-popup-menu-details] a', - issuerDetails: { - title: '[data-test-pki-issuer-page-title]', - ...ISSUERDETAILS, - }, - // CONFIGURATION - configuration: { - title: '[data-test-pki-configuration-page-title]', - emptyState: '[data-test-configuration-empty-state]', - pkiBetaBanner: '[data-test-pki-configuration-banner]', - pkiBetaBannerLink: '[data-test-pki-configuration-banner] a', - ...CONFIGURATION, - ...DELETE, - ...TIDY, - ...GENROOT, - }, - // EDIT CONFIGURATION - configEdit: { - ...CONFIGEDIT, - }, -}; diff --git a/ui/tests/helpers/policy-generator/pki.js b/ui/tests/helpers/policy-generator/pki.js deleted file mode 100644 index 30e7926cf..000000000 --- a/ui/tests/helpers/policy-generator/pki.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { singularize } from 'ember-inflector'; - -export const adminPolicy = (mountPath) => { - return ` - path "${mountPath}/*" { - capabilities = ["create", "read", "update", "delete", "list"] - }, - `; -}; - -// keys require singularized paths for GET -export const readerPolicy = (mountPath, resource) => { - return ` - path "${mountPath}/${resource}" { - capabilities = ["read", "list"] - }, - path "${mountPath}/${resource}/*" { - capabilities = ["read", "list"] - }, - path "${mountPath}/${singularize(resource)}" { - capabilities = ["read", "list"] - }, - path "${mountPath}/${singularize(resource)}/*" { - capabilities = ["read", "list"] - }, - `; -}; -export const updatePolicy = (mountPath, resource) => { - return ` - path "${mountPath}/${resource}" { - capabilities = ["read", "list"] - }, - path "${mountPath}/${resource}/*" { - capabilities = ["read", "update"] - }, - path "${mountPath}/${singularize(resource)}/*" { - capabilities = ["read", "update"] - }, - path "${mountPath}/issue/*" { - capabilities = ["update"] - }, - path "${mountPath}/generate/*" { - capabilities = ["update"] - }, - path "${mountPath}/import" { - capabilities = ["update"] - }, - path "${mountPath}/sign/*" { - capabilities = ["update"] - }, - `; -}; diff --git a/ui/tests/helpers/poll-cluster.js b/ui/tests/helpers/poll-cluster.js deleted file mode 100644 index d45549160..000000000 --- a/ui/tests/helpers/poll-cluster.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { settled } from '@ember/test-helpers'; - -export async function pollCluster(owner) { - const store = owner.lookup('service:store'); - await store.peekAll('cluster').firstObject.reload(); - await settled(); -} diff --git a/ui/tests/helpers/stubs.js b/ui/tests/helpers/stubs.js deleted file mode 100644 index 887fb82d1..000000000 --- a/ui/tests/helpers/stubs.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export function capabilitiesStub(requestPath, capabilitiesArray) { - // sample of capabilitiesArray: ['read', 'update'] - return { - [requestPath]: capabilitiesArray, - capabilities: capabilitiesArray, - request_id: '40f7e44d-af5c-9b60-bd20-df72eb17e294', - lease_id: '', - renewable: false, - lease_duration: 0, - data: { - [requestPath]: capabilitiesArray, - capabilities: capabilitiesArray, - }, - wrap_info: null, - warnings: null, - auth: null, - }; -} - -export const noopStub = (response) => { - return function () { - return [response, { 'Content-Type': 'application/json' }, JSON.stringify({})]; - }; -}; - -/** - * allowAllCapabilitiesStub mocks the response from capabilities-self - * that allows the user to do any action (root user) - * Example usage assuming setupMirage(hooks) was called: - * this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub(['read'])); - */ -export function allowAllCapabilitiesStub(capabilitiesList = ['root']) { - return function (_, { requestBody }) { - const { paths } = JSON.parse(requestBody); - const specificCapabilities = paths.reduce((obj, path) => { - return { - ...obj, - [path]: capabilitiesList, - }; - }, {}); - return { - ...specificCapabilities, - capabilities: capabilitiesList, - request_id: 'mirage-795dc9e1-0321-9ac6-71fc', - lease_id: '', - renewable: false, - lease_duration: 0, - data: { ...specificCapabilities, capabilities: capabilitiesList }, - wrap_info: null, - warnings: null, - auth: null, - }; - }; -} diff --git a/ui/tests/helpers/wait-for-error.js b/ui/tests/helpers/wait-for-error.js deleted file mode 100644 index e8c9ba189..000000000 --- a/ui/tests/helpers/wait-for-error.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { waitUntil } from '@ember/test-helpers'; -import Ember from 'ember'; - -export default function waitForError(opts) { - const orig = Ember.onerror; - - let error = null; - Ember.onerror = (err) => { - error = err; - }; - - return waitUntil(() => error, opts).finally(() => { - Ember.onerror = orig; - }); -} diff --git a/ui/tests/index.html b/ui/tests/index.html deleted file mode 100644 index fbed991d9..000000000 --- a/ui/tests/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - Vault Tests - - - - {{content-for "head"}} {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} {{content-for "test-head-footer"}} - - - {{content-for "body"}} {{content-for "test-body"}} - -
-
-
-
- -
-
- - - - - - - - {{content-for "body-footer"}} {{content-for "test-body-footer"}} - - diff --git a/ui/tests/integration/.gitkeep b/ui/tests/integration/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/tests/integration/components/alert-inline-test.js b/ui/tests/integration/components/alert-inline-test.js deleted file mode 100644 index 5fee1b1d7..000000000 --- a/ui/tests/integration/components/alert-inline-test.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, settled, find, waitUntil } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | alert-inline', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set('message', 'some very important alert'); - this.set('type', 'warning'); - }); - - test('it renders alert message with correct class args', async function (assert) { - await render(hbs` - - `); - assert.dom('[data-test-inline-error-message]').hasText('some very important alert'); - assert - .dom('[data-test-inline-alert]') - .hasAttribute('class', 'message-inline padding-top is-marginless size-small'); - }); - - test('it yields to block text', async function (assert) { - await render(hbs` - - A much more important alert - - `); - assert.dom('[data-test-inline-error-message]').hasText('A much more important alert'); - }); - - test('it renders correctly for type=danger', async function (assert) { - this.set('type', 'danger'); - await render(hbs` - - `); - assert - .dom('[data-test-inline-error-message]') - .hasAttribute('class', 'has-text-danger', 'has danger text'); - assert.dom('[data-test-icon="x-square-fill"]').exists('danger icon exists'); - }); - - test('it renders correctly for type=warning', async function (assert) { - await render(hbs` - - `); - assert.dom('[data-test-inline-error-message]').doesNotHaveAttribute('class', 'does not have styled text'); - assert.dom('[data-test-icon="alert-triangle-fill"]').exists('warning icon exists'); - }); - - test('it mimics loading when message changes', async function (assert) { - await render(hbs` - - `); - assert - .dom('[data-test-inline-error-message]') - .hasText('some very important alert', 'it renders original message'); - - this.set('message', 'some changed alert!!!'); - await waitUntil(() => find('[data-test-icon="loading"]')); - assert.ok(find('[data-test-icon="loading"]'), 'it shows loading icon when message changes'); - await settled(); - assert - .dom('[data-test-inline-error-message]') - .hasText('some changed alert!!!', 'it shows updated message'); - }); -}); diff --git a/ui/tests/integration/components/alert-popup-test.js b/ui/tests/integration/components/alert-popup-test.js deleted file mode 100644 index 989e6b06d..000000000 --- a/ui/tests/integration/components/alert-popup-test.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'vault/tests/helpers'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { click } from '@ember/test-helpers'; - -module('Integration | Component | alert-popup', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set('message', 'some very important alert'); - this.set('type', 'warning'); - this.set('close', () => this.set('closed', true)); - }); - - test('it renders the alert popup input', async function (assert) { - await render(hbs` - - `); - - assert.dom(this.element).hasText('Warning some very important alert'); - }); - - test('it invokes the close action', async function (assert) { - assert.expect(1); - - await render(hbs` - - `); - await click('.close-button'); - - assert.true(this.closed); - }); - - test('it renders the alert popup with different colors based on types', async function (assert) { - await render(hbs` - - `); - - assert.dom('.message').hasClass('is-highlight'); - - this.set('type', 'info'); - - await render(hbs` - - `); - - assert.dom('.message').hasClass('is-info'); - - this.set('type', 'danger'); - - await render(hbs` - - `); - - assert.dom('.message').hasClass('is-danger'); - }); -}); diff --git a/ui/tests/integration/components/auth-config-form/options-test.js b/ui/tests/integration/components/auth-config-form/options-test.js deleted file mode 100644 index edad9d815..000000000 --- a/ui/tests/integration/components/auth-config-form/options-test.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { resolve } from 'rsvp'; -import EmberObject from '@ember/object'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, settled } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import { create } from 'ember-cli-page-object'; -import authConfigForm from 'vault/tests/pages/components/auth-config-form/options'; - -const component = create(authConfigForm); - -module('Integration | Component | auth-config-form options', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.owner.lookup('service:flash-messages').registerTypes(['success']); - this.router = this.owner.lookup('service:router'); - this.router.reopen({ - transitionTo() { - return { - followRedirects() { - return resolve(); - }, - }; - }, - replaceWith() { - return resolve(); - }, - }); - }); - - test('it submits data correctly', async function (assert) { - assert.expect(1); - const model = EmberObject.create({ - tune() { - return resolve(); - }, - config: { - serialize() { - return {}; - }, - }, - }); - sinon.spy(model.config, 'serialize'); - this.set('model', model); - await render(hbs`{{auth-config-form/options model=this.model}}`); - component.save(); - return settled().then(() => { - assert.ok(model.config.serialize.calledOnce); - }); - }); -}); diff --git a/ui/tests/integration/components/auth-form-test.js b/ui/tests/integration/components/auth-form-test.js deleted file mode 100644 index ed329bc04..000000000 --- a/ui/tests/integration/components/auth-form-test.js +++ /dev/null @@ -1,354 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { later, _cancelTimers as cancelTimers } from '@ember/runloop'; -import EmberObject from '@ember/object'; -import { resolve } from 'rsvp'; -import Service from '@ember/service'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, settled } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import Pretender from 'pretender'; -import { create } from 'ember-cli-page-object'; -import authForm from '../../pages/components/auth-form'; -import { validate } from 'uuid'; - -const component = create(authForm); - -const workingAuthService = Service.extend({ - authenticate() { - return resolve({}); - }, - handleError() {}, - setLastFetch() {}, -}); - -const routerService = Service.extend({ - transitionTo() { - return { - followRedirects() { - return resolve(); - }, - }; - }, -}); - -module('Integration | Component | auth form', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:router', routerService); - this.router = this.owner.lookup('service:router'); - }); - - const CSP_ERR_TEXT = `Error This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.`; - test('it renders error on CSP violation', async function (assert) { - assert.expect(2); - this.set('cluster', EmberObject.create({ standby: true })); - this.set('selectedAuth', 'token'); - await render(hbs`{{auth-form cluster=this.cluster selectedAuth=this.selectedAuth}}`); - assert.false(component.errorMessagePresent, false); - this.owner.lookup('service:csp-event').events.addObject({ violatedDirective: 'connect-src' }); - await settled(); - assert.strictEqual(component.errorText, CSP_ERR_TEXT); - }); - - test('it renders with vault style errors', async function (assert) { - assert.expect(1); - const server = new Pretender(function () { - this.get('/v1/auth/**', () => { - return [ - 400, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - errors: ['Not allowed'], - }), - ]; - }); - this.get('/v1/sys/internal/ui/mounts', this.passthrough); - }); - - this.set('cluster', EmberObject.create({})); - this.set('selectedAuth', 'token'); - await render(hbs`{{auth-form cluster=this.cluster selectedAuth=this.selectedAuth}}`); - return component.login().then(() => { - assert.strictEqual(component.errorText, 'Error Authentication failed: Not allowed'); - server.shutdown(); - }); - }); - - test('it renders AdapterError style errors', async function (assert) { - assert.expect(1); - const server = new Pretender(function () { - this.get('/v1/auth/**', () => { - return [400, { 'Content-Type': 'application/json' }]; - }); - this.get('/v1/sys/internal/ui/mounts', this.passthrough); - }); - - this.set('cluster', EmberObject.create({})); - this.set('selectedAuth', 'token'); - await render(hbs`{{auth-form cluster=this.cluster selectedAuth=this.selectedAuth}}`); - // returns null because test does not return details of failed network request. On the app it will return the details of the error instead of null. - return component.login().then(() => { - assert.strictEqual(component.errorText, 'Error Authentication failed: null'); - server.shutdown(); - }); - }); - - test('it renders no tabs when no methods are passed', async function (assert) { - const methods = { - 'approle/': { - type: 'approle', - }, - }; - const server = new Pretender(function () { - this.get('/v1/sys/internal/ui/mounts', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify({ data: { auth: methods } })]; - }); - }); - await render(hbs``); - - assert.strictEqual(component.tabs.length, 0, 'renders a tab for every backend'); - server.shutdown(); - }); - - test('it renders all the supported methods and Other tab when methods are present', async function (assert) { - const methods = { - 'foo/': { - type: 'userpass', - }, - 'approle/': { - type: 'approle', - }, - }; - const server = new Pretender(function () { - this.get('/v1/sys/internal/ui/mounts', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify({ data: { auth: methods } })]; - }); - }); - - this.set('cluster', EmberObject.create({})); - await render(hbs`{{auth-form cluster=this.cluster }}`); - - assert.strictEqual(component.tabs.length, 2, 'renders a tab for userpass and Other'); - assert.strictEqual(component.tabs.objectAt(0).name, 'foo', 'uses the path in the label'); - assert.strictEqual(component.tabs.objectAt(1).name, 'Other', 'second tab is the Other tab'); - server.shutdown(); - }); - - test('it renders the description', async function (assert) { - const methods = { - 'approle/': { - type: 'userpass', - description: 'app description', - }, - }; - const server = new Pretender(function () { - this.get('/v1/sys/internal/ui/mounts', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify({ data: { auth: methods } })]; - }); - }); - this.set('cluster', EmberObject.create({})); - await render(hbs`{{auth-form cluster=this.cluster }}`); - - assert.strictEqual( - component.descriptionText, - 'app description', - 'renders a description for auth methods' - ); - server.shutdown(); - }); - - test('it calls authenticate with the correct path', async function (assert) { - this.owner.unregister('service:auth'); - this.owner.register('service:auth', workingAuthService); - this.auth = this.owner.lookup('service:auth'); - const authSpy = sinon.spy(this.auth, 'authenticate'); - const methods = { - 'foo/': { - type: 'userpass', - }, - }; - const server = new Pretender(function () { - this.get('/v1/sys/internal/ui/mounts', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify({ data: { auth: methods } })]; - }); - }); - - this.set('cluster', EmberObject.create({})); - this.set('selectedAuth', 'foo/'); - await render(hbs`{{auth-form cluster=this.cluster selectedAuth=this.selectedAuth}}`); - await component.login(); - - await settled(); - assert.ok(authSpy.calledOnce, 'a call to authenticate was made'); - const { data } = authSpy.getCall(0).args[0]; - assert.strictEqual(data.path, 'foo', 'uses the id for the path'); - authSpy.restore(); - server.shutdown(); - }); - - test('it renders no tabs when no supported methods are present in passed methods', async function (assert) { - const methods = { - 'approle/': { - type: 'approle', - }, - }; - const server = new Pretender(function () { - this.get('/v1/sys/internal/ui/mounts', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify({ data: { auth: methods } })]; - }); - }); - this.set('cluster', EmberObject.create({})); - await render(hbs``); - - server.shutdown(); - assert.strictEqual(component.tabs.length, 0, 'renders a tab for every backend'); - }); - - test('it makes a request to unwrap if passed a wrappedToken and logs in', async function (assert) { - this.owner.register('service:auth', workingAuthService); - this.auth = this.owner.lookup('service:auth'); - const authSpy = sinon.spy(this.auth, 'authenticate'); - const server = new Pretender(function () { - this.post('/v1/sys/wrapping/unwrap', () => { - return [ - 200, - { 'content-type': 'application/json' }, - JSON.stringify({ - auth: { - client_token: '12345', - }, - }), - ]; - }); - }); - - const wrappedToken = '54321'; - this.set('wrappedToken', wrappedToken); - this.set('cluster', EmberObject.create({})); - await render(hbs``); - later(() => cancelTimers(), 50); - await settled(); - assert.strictEqual( - server.handledRequests[0].url, - '/v1/sys/wrapping/unwrap', - 'makes call to unwrap the token' - ); - assert.strictEqual( - server.handledRequests[0].requestHeaders['X-Vault-Token'], - wrappedToken, - 'uses passed wrapped token for the unwrap' - ); - assert.ok(authSpy.calledOnce, 'a call to authenticate was made'); - server.shutdown(); - authSpy.restore(); - }); - - test('it shows an error if unwrap errors', async function (assert) { - const server = new Pretender(function () { - this.post('/v1/sys/wrapping/unwrap', () => { - return [ - 400, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - errors: ['There was an error unwrapping!'], - }), - ]; - }); - }); - - this.set('wrappedToken', '54321'); - await render(hbs`{{auth-form cluster=this.cluster wrappedToken=this.wrappedToken}}`); - later(() => cancelTimers(), 50); - - await settled(); - assert.strictEqual( - component.errorText, - 'Error Token unwrap failed: There was an error unwrapping!', - 'shows the error' - ); - server.shutdown(); - }); - - test('it should retain oidc role when mount path is changed', async function (assert) { - assert.expect(1); - - const auth_url = 'http://dev-foo-bar.com'; - const server = new Pretender(function () { - this.post('/v1/auth/:path/oidc/auth_url', (req) => { - const { role, redirect_uri } = JSON.parse(req.requestBody); - const goodRequest = - req.params.path === 'foo-oidc' && - role === 'foo' && - redirect_uri.includes('/auth/foo-oidc/oidc/callback'); - - return [ - goodRequest ? 200 : 400, - { 'Content-Type': 'application/json' }, - JSON.stringify( - goodRequest ? { data: { auth_url } } : { errors: [`role "${role}" could not be found`] } - ), - ]; - }); - this.get('/v1/sys/internal/ui/mounts', this.passthrough); - }); - - window.open = (url) => { - assert.strictEqual(url, auth_url, 'auth_url is returned when required params are passed'); - }; - - this.owner.lookup('service:router').reopen({ - urlFor(route, { auth_path }) { - return `/auth/${auth_path}/oidc/callback`; - }, - }); - - this.set('cluster', EmberObject.create({})); - await render(hbs``); - - await component.selectMethod('oidc'); - await component.oidcRole('foo'); - await component.oidcMoreOptions(); - await component.oidcMountPath('foo-oidc'); - await component.login(); - - server.shutdown(); - }); - - test('it should set nonce value as uuid for okta method type', async function (assert) { - assert.expect(1); - - const server = new Pretender(function () { - this.post('/v1/auth/okta/login/foo', (req) => { - const { nonce } = JSON.parse(req.requestBody); - assert.true(validate(nonce), 'Nonce value passed as uuid for okta login'); - return [ - 200, - { 'content-type': 'application/json' }, - JSON.stringify({ - auth: { - client_token: '12345', - }, - }), - ]; - }); - this.get('/v1/sys/internal/ui/mounts', this.passthrough); - }); - - this.set('cluster', EmberObject.create({})); - await render(hbs``); - - await component.selectMethod('okta'); - await component.username('foo'); - await component.password('bar'); - await component.login(); - - server.shutdown(); - }); -}); diff --git a/ui/tests/integration/components/auth-jwt-test.js b/ui/tests/integration/components/auth-jwt-test.js deleted file mode 100644 index 494c86a6b..000000000 --- a/ui/tests/integration/components/auth-jwt-test.js +++ /dev/null @@ -1,257 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { _cancelTimers as cancelTimers } from '@ember/runloop'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, settled, waitUntil } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import Pretender from 'pretender'; -import { resolve } from 'rsvp'; -import { create } from 'ember-cli-page-object'; -import form from '../../pages/components/auth-jwt'; -import { ERROR_WINDOW_CLOSED, ERROR_MISSING_PARAMS, ERROR_JWT_LOGIN } from 'vault/components/auth-jwt'; -import { fakeWindow, buildMessage } from '../../helpers/oidc-window-stub'; - -const component = create(form); -const windows = []; - -fakeWindow.reopen({ - init() { - this._super(...arguments); - windows.push(this); - }, - open() { - return fakeWindow.create(); - }, - close() { - windows.forEach((w) => w.trigger('close')); - }, -}); - -const OIDC_AUTH_RESPONSE = { - auth: { - client_token: 'token', - }, -}; - -const renderIt = async (context, path = 'jwt') => { - const handler = (data, e) => { - if (e && e.preventDefault) e.preventDefault(); - return resolve(); - }; - const fake = fakeWindow.create(); - context.set('window', fake); - context.set('handler', sinon.spy(handler)); - context.set('roleName', ''); - context.set('selectedAuthPath', path); - await render(hbs` - - `); -}; -module('Integration | Component | auth jwt', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.openSpy = sinon.spy(fakeWindow.proto(), 'open'); - this.owner.lookup('service:router').reopen({ - urlFor() { - return 'http://example.com'; - }, - }); - this.server = new Pretender(function () { - this.get('/v1/auth/:path/oidc/callback', function () { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify(OIDC_AUTH_RESPONSE)]; - }); - this.post('/v1/auth/:path/oidc/auth_url', (request) => { - const { role } = JSON.parse(request.requestBody); - if (['test', 'okta', 'bar'].includes(role)) { - const auth_url = role === 'test' ? 'http://example.com' : role === 'okta' ? 'http://okta.com' : ''; - return [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - data: { auth_url }, - }), - ]; - } - const errors = role === 'foo' ? ['role "foo" could not be found'] : [ERROR_JWT_LOGIN]; - return [400, { 'Content-Type': 'application/json' }, JSON.stringify({ errors })]; - }); - }); - }); - - hooks.afterEach(function () { - this.openSpy.restore(); - this.server.shutdown(); - }); - - test('it renders the yield', async function (assert) { - await render(hbs`Hello!`); - assert.strictEqual(component.yieldContent, 'Hello!', 'yields properly'); - }); - - test('jwt: it renders and makes auth_url requests', async function (assert) { - await renderIt(this); - await settled(); - assert.ok(component.jwtPresent, 'renders jwt field'); - assert.ok(component.rolePresent, 'renders jwt field'); - assert.strictEqual(this.server.handledRequests.length, 1, 'request to the default path is made'); - assert.strictEqual(this.server.handledRequests[0].url, '/v1/auth/jwt/oidc/auth_url'); - this.set('selectedAuthPath', 'foo'); - await settled(); - assert.strictEqual(this.server.handledRequests.length, 2, 'a second request was made'); - assert.strictEqual( - this.server.handledRequests[1].url, - '/v1/auth/foo/oidc/auth_url', - 'requests when path is set' - ); - }); - - test('jwt: it calls passed action on login', async function (assert) { - await renderIt(this); - await component.login(); - assert.ok(this.handler.calledOnce); - }); - - test('oidc: test role: it renders', async function (assert) { - await renderIt(this); - await settled(); - this.set('selectedAuthPath', 'foo'); - await component.role('test'); - await settled(); - assert.notOk(component.jwtPresent, 'does not show jwt input for OIDC type login'); - assert.strictEqual(component.loginButtonText, 'Sign in with OIDC Provider'); - - await component.role('okta'); - // 1 for initial render, 1 for each time role changed = 3 - assert.strictEqual(this.server.handledRequests.length, 4, 'fetches the auth_url when the path changes'); - assert.strictEqual( - component.loginButtonText, - 'Sign in with Okta', - 'recognizes auth methods with certain urls' - ); - }); - - test('oidc: it calls window.open popup window on login', async function (assert) { - await renderIt(this); - this.set('selectedAuthPath', 'foo'); - await component.role('test'); - component.login(); - await waitUntil(() => { - return this.openSpy.calledOnce; - }); - cancelTimers(); - const call = this.openSpy.getCall(0); - assert.deepEqual( - call.args, - ['http://example.com', 'vaultOIDCWindow', 'width=500,height=600,resizable,scrollbars=yes,top=0,left=0'], - 'called with expected args' - ); - }); - - test('oidc: it calls error handler when popup is closed', async function (assert) { - await renderIt(this); - this.set('selectedAuthPath', 'foo'); - await component.role('test'); - component.login(); - await waitUntil(() => { - return this.openSpy.calledOnce; - }); - this.window.close(); - await settled(); - assert.strictEqual(this.error, ERROR_WINDOW_CLOSED, 'calls onError with error string'); - }); - - test('oidc: shows error when message posted with state key, wrong params', async function (assert) { - await renderIt(this); - this.set('selectedAuthPath', 'foo'); - await component.role('test'); - component.login(); - await waitUntil(() => { - return this.openSpy.calledOnce; - }); - this.window.trigger( - 'message', - buildMessage({ data: { source: 'oidc-callback', state: 'state', foo: 'bar' } }) - ); - cancelTimers(); - assert.strictEqual(this.error, ERROR_MISSING_PARAMS, 'calls onError with params missing error'); - }); - - test('oidc: storage event fires with state key, correct params', async function (assert) { - await renderIt(this); - this.set('selectedAuthPath', 'foo'); - await component.role('test'); - component.login(); - await waitUntil(() => { - return this.openSpy.calledOnce; - }); - this.window.trigger('message', buildMessage()); - await settled(); - assert.ok(this.handler.withArgs(null, null, 'token').calledOnce, 'calls the onSubmit handler with token'); - }); - - test('oidc: fails silently when event origin does not match window origin', async function (assert) { - await renderIt(this); - this.set('selectedAuthPath', 'foo'); - await component.role('test'); - component.login(); - await waitUntil(() => { - return this.openSpy.calledOnce; - }); - this.window.trigger('message', buildMessage({ origin: 'http://hackerz.com' })); - cancelTimers(); - await settled(); - assert.notOk(this.handler.called, 'should not call the submit handler'); - }); - - test('oidc: fails silently when event is not trusted', async function (assert) { - await renderIt(this); - this.set('selectedAuthPath', 'foo'); - await component.role('test'); - component.login(); - await waitUntil(() => { - return this.openSpy.calledOnce; - }); - this.window.trigger('message', buildMessage({ isTrusted: false })); - cancelTimers(); - await settled(); - assert.notOk(this.handler.called, 'should not call the submit handler'); - }); - - test('oidc: it should trigger error callback when role is not found', async function (assert) { - await renderIt(this, 'oidc'); - await component.role('foo'); - await component.login(); - assert.strictEqual( - this.error, - 'Invalid role. Please try again.', - 'Error message is returned when role is not found' - ); - }); - - test('oidc: it should trigger error callback when role is returned without auth_url', async function (assert) { - await renderIt(this, 'oidc'); - await component.role('bar'); - await component.login(); - assert.strictEqual( - this.error, - 'Missing auth_url. Please check that allowed_redirect_uris for the role include this mount path.', - 'Error message is returned when role is returned without auth_url' - ); - }); -}); diff --git a/ui/tests/integration/components/autocomplete-input-test.js b/ui/tests/integration/components/autocomplete-input-test.js deleted file mode 100644 index 81ebb9dc3..000000000 --- a/ui/tests/integration/components/autocomplete-input-test.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { click, fillIn, triggerEvent, typeIn, render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | autocomplete-input', function (hooks) { - setupRenderingTest(hooks); - - test('it should render label', async function (assert) { - await render( - hbs` - ` - ); - - assert.dom('label').doesNotExist('Label is hidden when not provided'); - this.setProperties({ - label: 'Some label', - subText: 'Some description', - }); - assert.dom('label').hasText('Some label', 'Label renders'); - assert.dom('[data-test-label-subtext]').hasText('Some description', 'Sub text renders'); - }); - - test('it should function as standard input', async function (assert) { - assert.expect(3); - const changeValue = 'foo bar'; - this.value = 'test'; - this.placeholder = 'text goes here'; - this.onChange = (value) => assert.strictEqual(value, changeValue, 'Value sent in onChange callback'); - - await render( - hbs` - ` - ); - - assert.dom('input').hasAttribute('placeholder', this.placeholder, 'Input placeholder renders'); - assert.dom('input').hasValue(this.value, 'Initial input value renders'); - await fillIn('input', changeValue); - }); - - test('it should trigger dropdown', async function (assert) { - await render( - hbs` - ` - ); - - await typeIn('input', '$'); - await triggerEvent('input', 'input', { data: '$' }); // simulate InputEvent for data prop with character pressed - assert.dom('.autocomplete-input-option').doesNotExist('Trigger does not open dropdown with no options'); - - this.set('options', [ - { label: 'Foo', value: '$foo' }, - { label: 'Bar', value: 'bar' }, - ]); - await triggerEvent('input', 'input', { data: '$' }); - const options = this.element.querySelectorAll('.autocomplete-input-option'); - options.forEach((o, index) => { - assert.dom(o).hasText(this.options[index].label, 'Label renders for option'); - }); - - await click(options[0]); - assert.dom('input').isFocused('Focus is returned to input after selecting option'); - assert - .dom('input') - .hasValue('$foo', 'Value is updated correctly. Trigger character is not prepended to value.'); - - await typeIn('input', '-$'); - await triggerEvent('input', 'input', { data: '$' }); - await click('.autocomplete-input-option:last-child'); - assert - .dom('input') - .hasValue('$foo-$bar', 'Value is updated correctly. Trigger character is prepended to option.'); - assert.strictEqual(this.value, '$foo-$bar', 'Value prop is updated correctly onChange'); - }); -}); diff --git a/ui/tests/integration/components/b64-toggle-test.js b/ui/tests/integration/components/b64-toggle-test.js deleted file mode 100644 index be20dba87..000000000 --- a/ui/tests/integration/components/b64-toggle-test.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click, find } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | b64 toggle', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - await render(hbs`{{b64-toggle}}`); - assert.dom('button').exists({ count: 1 }); - }); - - test('it toggles encoding on the passed string', async function (assert) { - this.set('value', 'value'); - await render(hbs`{{b64-toggle value=this.value}}`); - await click('button'); - assert.strictEqual(this.value, btoa('value'), 'encodes to base64'); - await click('button'); - assert.strictEqual(this.value, 'value', 'decodes from base64'); - }); - - test('it toggles encoding starting with base64', async function (assert) { - this.set('value', btoa('value')); - await render(hbs`{{b64-toggle value=this.value initialEncoding='base64'}}`); - assert.ok(find('button').textContent.includes('Decode'), 'renders as on when in b64 mode'); - await click('button'); - assert.strictEqual(this.value, 'value', 'decodes from base64'); - }); - - test('it detects changes to value after encoding', async function (assert) { - this.set('value', btoa('value')); - await render(hbs`{{b64-toggle value=this.value initialEncoding='base64'}}`); - assert.ok(find('button').textContent.includes('Decode'), 'renders as on when in b64 mode'); - this.set('value', btoa('value') + '='); - assert.ok(find('button').textContent.includes('Encode'), 'toggles off since value has changed'); - this.set('value', btoa('value')); - assert.ok( - find('button').textContent.includes('Decode'), - 'toggles on since value is equal to the original' - ); - }); - - test('it does not toggle when the value is empty', async function (assert) { - this.set('value', ''); - await render(hbs`{{b64-toggle value=this.value}}`); - await click('button'); - assert.ok(find('button').textContent.includes('Encode')); - }); -}); diff --git a/ui/tests/integration/components/box-radio-test.js b/ui/tests/integration/components/box-radio-test.js deleted file mode 100644 index e3671955e..000000000 --- a/ui/tests/integration/components/box-radio-test.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import sinon from 'sinon'; -import { render, click } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | box-radio', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set('type', 'aws'); - this.set('displayName', 'An Option'); - this.set('mountType', ''); - this.set('disabled', false); - }); - - test('it renders', async function (assert) { - const spy = sinon.spy(); - this.set('onRadioChange', spy); - await render(hbs``); - - assert.dom(this.element).hasText('An Option', 'shows the display name of the option'); - assert.dom('.tooltip').doesNotExist('tooltip does not exist when disabled is false'); - await click('[data-test-mount-type="aws"]'); - assert.ok(spy.calledOnce, 'calls the radio change function when option clicked'); - }); - - test('it renders correctly when disabled', async function (assert) { - const spy = sinon.spy(); - this.set('onRadioChange', spy); - await render(hbs``); - - assert.dom(this.element).hasText('An Option', 'shows the display name of the option'); - assert.dom('.ember-basic-dropdown-trigger').exists('tooltip exists'); - await click('[data-test-mount-type="aws"]'); - assert.ok(spy.notCalled, 'does not call the radio change function when option is clicked'); - }); -}); diff --git a/ui/tests/integration/components/calendar-widget-test.js b/ui/tests/integration/components/calendar-widget-test.js deleted file mode 100644 index 9d48b7b1f..000000000 --- a/ui/tests/integration/components/calendar-widget-test.js +++ /dev/null @@ -1,257 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click } from '@ember/test-helpers'; -import sinon from 'sinon'; -import hbs from 'htmlbars-inline-precompile'; -import calendarDropdown from 'vault/tests/pages/components/calendar-widget'; -import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters'; -import { subMonths, subYears } from 'date-fns'; -import timestamp from 'core/utils/timestamp'; - -module('Integration | Component | calendar-widget', function (hooks) { - setupRenderingTest(hooks); - - hooks.before(function () { - sinon.stub(timestamp, 'now').callsFake(() => new Date('2018-04-03T14:15:30')); - }); - hooks.beforeEach(function () { - const CURRENT_DATE = timestamp.now(); - this.set('currentDate', CURRENT_DATE); - this.set('calendarStartDate', subMonths(CURRENT_DATE, 12)); - this.set('calendarEndDate', CURRENT_DATE); - this.set('startTimestamp', subMonths(CURRENT_DATE, 12).toISOString()); - this.set('endTimestamp', CURRENT_DATE.toISOString()); - this.set('handleClientActivityQuery', sinon.spy()); - }); - hooks.after(function () { - timestamp.now.restore(); - }); - - test('it renders and disables correct months when start date is 12 months ago', async function (assert) { - assert.expect(14); - await render(hbs` - - `); - assert - .dom('[data-test-calendar-widget-trigger]') - .hasText(`Apr 2017 - Apr 2018`, 'renders and formats start and end dates'); - await calendarDropdown.openCalendar(); - assert.ok(calendarDropdown.showsCalendar, 'renders the calendar component'); - // assert months in current year are disabled/enabled correctly - const enabledMonths = ['January', 'February', 'March', 'April']; - ARRAY_OF_MONTHS.forEach(function (month) { - if (enabledMonths.includes(month)) { - assert - .dom(`[data-test-calendar-month="${month}"]`) - .doesNotHaveClass('is-readOnly', `${month} is enabled`); - } else { - assert.dom(`[data-test-calendar-month="${month}"]`).hasClass('is-readOnly', `${month} is read only`); - } - }); - }); - - test('it renders and disables months before start timestamp', async function (assert) { - await render(hbs` - - `); - - await calendarDropdown.openCalendar(); - assert.dom('[data-test-next-year]').isDisabled('Future year is disabled'); - await calendarDropdown.clickPreviousYear(); - assert - .dom('[data-test-display-year]') - .hasText(`${subYears(this.currentDate, 1).getFullYear()}`, 'shows the previous year'); - assert.dom('[data-test-previous-year]').isDisabled('disables previous year'); - - // assert months in previous year are disabled/enabled correctly - const disabledMonths = ['January', 'February', 'March']; - ARRAY_OF_MONTHS.forEach(function (month) { - if (disabledMonths.includes(month)) { - assert.dom(`[data-test-calendar-month="${month}"]`).hasClass('is-readOnly', `${month} is read only`); - } else { - assert - .dom(`[data-test-calendar-month="${month}"]`) - .doesNotHaveClass('is-readOnly', `${month} is enabled`); - } - }); - }); - - test('it calls parent callback with correct arg when clicking "Current billing period"', async function (assert) { - await render(hbs` - - `); - await calendarDropdown.menuToggle(); - await calendarDropdown.clickCurrentBillingPeriod(); - assert.propEqual( - this.handleClientActivityQuery.args[0][0], - { dateType: 'reset' }, - 'it calls parent function with reset dateType' - ); - }); - - test('it calls parent callback with correct arg when clicking "Current month"', async function (assert) { - await render(hbs` - - `); - await calendarDropdown.menuToggle(); - await calendarDropdown.clickCurrentMonth(); - assert.propEqual( - this.handleClientActivityQuery.args[0][0], - { dateType: 'currentMonth' }, - 'it calls parent function with currentMoth dateType' - ); - }); - - test('it calls parent callback with correct arg when selecting a month', async function (assert) { - await render(hbs` - - `); - await calendarDropdown.openCalendar(); - await click(`[data-test-calendar-month="April"`); - assert.propEqual( - this.handleClientActivityQuery.lastCall.lastArg, - { - dateType: 'endDate', - monthIdx: 3, - monthName: 'April', - year: 2018, - }, - 'it calls parent function with end date (current) month/year' - ); - - await calendarDropdown.openCalendar(); - await calendarDropdown.clickPreviousYear(); - await click(`[data-test-calendar-month="March"]`); - assert.propEqual( - this.handleClientActivityQuery.lastCall.lastArg, - { - dateType: 'endDate', - monthIdx: 2, - monthName: 'March', - year: 2017, - }, - 'it calls parent function with selected start date month/year' - ); - }); - - test('it disables correct months when start date 6 months ago', async function (assert) { - this.set('calendarStartDate', subMonths(this.currentDate, 6)); // Nov 3, 2017 - this.set('startTimestamp', subMonths(this.currentDate, 6).toISOString()); - await render(hbs` - - `); - - await calendarDropdown.openCalendar(); - assert.dom('[data-test-next-year]').isDisabled('Future year is disabled'); - - // Check start year disables correct months - await calendarDropdown.clickPreviousYear(); - assert.dom('[data-test-previous-year]').isDisabled('previous year is disabled'); - const prevYearEnabled = ['October', 'November', 'December']; - ARRAY_OF_MONTHS.forEach(function (month) { - if (prevYearEnabled.includes(month)) { - assert - .dom(`[data-test-calendar-month="${month}"]`) - .doesNotHaveClass('is-readOnly', `${month} is enabled`); - } else { - assert.dom(`[data-test-calendar-month="${month}"]`).hasClass('is-readOnly', `${month} is read only`); - } - }); - - // Check end year disables correct months - await click('[data-test-next-year]'); - const currYearEnabled = ['January', 'February', 'March', 'April']; - ARRAY_OF_MONTHS.forEach(function (month) { - if (currYearEnabled.includes(month)) { - assert - .dom(`[data-test-calendar-month="${month}"]`) - .doesNotHaveClass('is-readOnly', `${month} is enabled`); - } else { - assert.dom(`[data-test-calendar-month="${month}"]`).hasClass('is-readOnly', `${month} is read only`); - } - }); - }); - - test('it disables correct months when start date 36 months ago', async function (assert) { - this.set('calendarStartDate', subMonths(this.currentDate, 36)); // April 3 2015 - this.set('startTimestamp', subMonths(this.currentDate, 36).toISOString()); - await render(hbs` - - `); - - await calendarDropdown.openCalendar(); - assert.dom('[data-test-next-year]').isDisabled('Future year is disabled'); - - for (const year of [2017, 2016, 2015]) { - await calendarDropdown.clickPreviousYear(); - assert.dom('[data-test-display-year]').hasText(year.toString()); - } - - assert.dom('[data-test-previous-year]').isDisabled('previous year is disabled'); - assert.dom('[data-test-next-year]').isEnabled('next year is enabled'); - - assert.dom('.calendar-widget .is-readOnly').exists('Some months disabled'); - - const disabledMonths = ['January', 'February', 'March']; - ARRAY_OF_MONTHS.forEach(function (month) { - if (disabledMonths.includes(month)) { - assert.dom(`[data-test-calendar-month="${month}"]`).hasClass('is-readOnly', `${month} is read only`); - } else { - assert - .dom(`[data-test-calendar-month="${month}"]`) - .doesNotHaveClass('is-readOnly', `${month} is enabled`); - } - }); - - await click('[data-test-next-year]'); - assert.dom('.calendar-widget .is-readOnly').doesNotExist('All months enabled for 2016'); - await click('[data-test-next-year]'); - assert.dom('.calendar-widget .is-readOnly').doesNotExist('All months enabled for 2017'); - await click('[data-test-next-year]'); - assert.dom('.calendar-widget .is-readOnly').exists('Some months disabled for 2018'); - - const enabledMonths = ['January', 'February', 'March', 'April']; - ARRAY_OF_MONTHS.forEach(function (month) { - if (enabledMonths.includes(month)) { - assert - .dom(`[data-test-calendar-month="${month}"]`) - .doesNotHaveClass('is-readOnly', `${month} is enabled`); - } else { - assert.dom(`[data-test-calendar-month="${month}"]`).hasClass('is-readOnly', `${month} is read only`); - } - }); - }); -}); diff --git a/ui/tests/integration/components/checkbox-grid-test.js b/ui/tests/integration/components/checkbox-grid-test.js deleted file mode 100644 index f887c2937..000000000 --- a/ui/tests/integration/components/checkbox-grid-test.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import Sinon from 'sinon'; - -module('Integration | Component | checkbox-grid', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.name = 'fooBar'; - this.label = 'Foo bar'; - this.fields = [ - { key: 'abc', label: 'All Bears Cry' }, - { key: 'def', label: 'Dark Eel Feelings' }, - ]; - - this.onChange = Sinon.spy(); - }); - - test('it renders with minimum inputs', async function (assert) { - const changeSpy = Sinon.spy(); - this.set('onChange', changeSpy); - await render( - hbs`` - ); - - assert.dom('[data-test-checkbox]').exists({ count: 2 }, 'One checkbox is rendered for each field'); - assert.dom('[data-test-checkbox]').isNotChecked('no fields are checked by default'); - await click('[data-test-checkbox="abc"]'); - assert.ok(changeSpy.calledOnceWithExactly('fooBar', ['abc'])); - }); - - test('it renders with values set', async function (assert) { - const changeSpy = Sinon.spy(); - this.set('onChange', changeSpy); - this.set('currentValue', ['abc']); - await render( - hbs`` - ); - - assert.dom('[data-test-checkbox]').exists({ count: 2 }, 'One checkbox is rendered for each field'); - assert.dom('[data-test-checkbox="abc"]').isChecked('abc field is checked on load'); - assert.dom('[data-test-checkbox="def"]').isNotChecked('def field is unchecked on load'); - await click('[data-test-checkbox="abc"]'); - assert.ok(changeSpy.calledOnceWithExactly('fooBar', []), 'Sends correct payload when unchecking'); - await click('[data-test-checkbox="def"]'); - await click('[data-test-checkbox="abc"]'); - assert.ok( - changeSpy.calledWithExactly('fooBar', ['def', 'abc']), - 'sends correct payload with multiple checked' - ); - }); -}); diff --git a/ui/tests/integration/components/chevron-test.js b/ui/tests/integration/components/chevron-test.js deleted file mode 100644 index fc07e1844..000000000 --- a/ui/tests/integration/components/chevron-test.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import waitForError from 'vault/tests/helpers/wait-for-error'; - -module('Integration | Component | chevron', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); - - await render(hbs``); - assert.dom('.flight-icon').exists('renders'); - - await render(hbs``); - assert.dom('.flight-icon').hasClass('hs-icon-button-right', 'renders'); - - await render(hbs``); - assert.dom('.flight-icon').doesNotHaveClass('hs-icon-button-right', 'renders'); - - const promise = waitForError(); - render(hbs``); - const err = await promise; - - assert.ok( - err.message.includes('The direction property of Chevron'), - 'asserts about unsupported direction' - ); - }); -}); diff --git a/ui/tests/integration/components/clients/attribution-test.js b/ui/tests/integration/components/clients/attribution-test.js deleted file mode 100644 index bd83a26e5..000000000 --- a/ui/tests/integration/components/clients/attribution-test.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { endOfMonth, formatRFC3339 } from 'date-fns'; -import { click } from '@ember/test-helpers'; -import subMonths from 'date-fns/subMonths'; -import timestamp from 'core/utils/timestamp'; - -module('Integration | Component | clients/attribution', function (hooks) { - setupRenderingTest(hooks); - - hooks.before(function () { - sinon.stub(timestamp, 'now').callsFake(() => new Date('2018-04-03T14:15:30')); - }); - hooks.beforeEach(function () { - const mockNow = timestamp.now(); - this.mockNow = mockNow; - this.set('startTimestamp', formatRFC3339(subMonths(mockNow, 6))); - this.set('timestamp', formatRFC3339(mockNow)); - this.set('selectedNamespace', null); - this.set('chartLegend', [ - { label: 'entity clients', key: 'entity_clients' }, - { label: 'non-entity clients', key: 'non_entity_clients' }, - ]); - this.set('totalUsageCounts', { clients: 15, entity_clients: 10, non_entity_clients: 5 }); - this.set('totalClientAttribution', [ - { label: 'second', clients: 10, entity_clients: 7, non_entity_clients: 3 }, - { label: 'first', clients: 5, entity_clients: 3, non_entity_clients: 2 }, - ]); - this.set('totalMountsData', { clients: 5, entity_clients: 3, non_entity_clients: 2 }); - this.set('namespaceMountsData', [ - { label: 'auth1/', clients: 3, entity_clients: 2, non_entity_clients: 1 }, - { label: 'auth2/', clients: 2, entity_clients: 1, non_entity_clients: 1 }, - ]); - }); - hooks.after(function () { - timestamp.now.restore(); - }); - - test('it renders empty state with no data', async function (assert) { - await render(hbs` - - - `); - - assert.dom('[data-test-component="empty-state"]').exists(); - assert.dom('[data-test-empty-state-title]').hasText('No data found'); - assert.dom('[data-test-attribution-description]').hasText('There is a problem gathering data'); - assert.dom('[data-test-attribution-export-button]').doesNotExist(); - assert.dom('[data-test-attribution-timestamp]').doesNotHaveTextContaining('Updated'); - }); - - test('it renders with data for namespaces', async function (assert) { - await render(hbs` - - - `); - - assert.dom('[data-test-component="empty-state"]').doesNotExist(); - assert.dom('[data-test-horizontal-bar-chart]').exists('chart displays'); - assert.dom('[data-test-attribution-export-button]').exists(); - assert - .dom('[data-test-attribution-description]') - .hasText( - 'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.' - ); - assert - .dom('[data-test-attribution-subtext]') - .hasText( - 'The total clients in the namespace for this date range. This number is useful for identifying overall usage volume.' - ); - assert.dom('[data-test-top-attribution]').includesText('namespace').includesText('second'); - assert.dom('[data-test-attribution-clients]').includesText('namespace').includesText('10'); - }); - - test('it renders two charts and correct text for single, historical month', async function (assert) { - this.start = formatRFC3339(subMonths(this.mockNow, 1)); - this.end = formatRFC3339(subMonths(endOfMonth(this.mockNow), 1)); - await render(hbs` - - - `); - assert - .dom('[data-test-attribution-description]') - .includesText( - 'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.', - 'renders correct auth attribution description' - ); - assert - .dom('[data-test-chart-container="total-clients"] .chart-description') - .includesText( - 'The total clients in the namespace for this month. This number is useful for identifying overall usage volume.', - 'renders total monthly namespace text' - ); - assert - .dom('[data-test-chart-container="new-clients"] .chart-description') - .includesText( - 'The new clients in the namespace for this month. This aids in understanding which namespaces create and use new clients.', - 'renders new monthly namespace text' - ); - this.set('selectedNamespace', 'second'); - - assert - .dom('[data-test-attribution-description]') - .includesText( - 'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.', - 'renders correct auth attribution description' - ); - assert - .dom('[data-test-chart-container="total-clients"] .chart-description') - .includesText( - 'The total clients used by the auth method for this month. This number is useful for identifying overall usage volume.', - 'renders total monthly auth method text' - ); - assert - .dom('[data-test-chart-container="new-clients"] .chart-description') - .includesText( - 'The new clients used by the auth method for this month. This aids in understanding which auth methods create and use new clients.', - 'renders new monthly auth method text' - ); - }); - - test('it renders single chart for current month', async function (assert) { - await render(hbs` - - - `); - assert - .dom('[data-test-chart-container="single-chart"]') - .exists('renders single chart with total clients'); - assert - .dom('[data-test-attribution-subtext]') - .hasTextContaining('this month', 'renders total monthly namespace text'); - }); - - test('it renders single chart and correct text for for date range', async function (assert) { - await render(hbs` - - - `); - - assert - .dom('[data-test-chart-container="single-chart"]') - .exists('renders single chart with total clients'); - assert - .dom('[data-test-attribution-subtext]') - .hasTextContaining('date range', 'renders total monthly namespace text'); - }); - - test('it renders with data for selected namespace auth methods for a date range', async function (assert) { - this.set('selectedNamespace', 'second'); - await render(hbs` - - - `); - - assert.dom('[data-test-component="empty-state"]').doesNotExist(); - assert.dom('[data-test-horizontal-bar-chart]').exists('chart displays'); - assert.dom('[data-test-attribution-export-button]').exists(); - assert - .dom('[data-test-attribution-description]') - .hasText( - 'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.' - ); - assert - .dom('[data-test-attribution-subtext]') - .hasText( - 'The total clients used by the auth method for this date range. This number is useful for identifying overall usage volume.' - ); - assert.dom('[data-test-top-attribution]').includesText('auth method').includesText('auth1/'); - assert.dom('[data-test-attribution-clients]').includesText('auth method').includesText('3'); - }); - - test('it renders modal', async function (assert) { - await render(hbs` - - - `); - await click('[data-test-attribution-export-button]'); - assert.dom('.modal.is-active .title').hasText('Export attribution data', 'modal appears to export csv'); - assert.dom('.modal.is-active').includesText('June 2022 - December 2022'); - }); -}); diff --git a/ui/tests/integration/components/clients/config-test.js b/ui/tests/integration/components/clients/config-test.js deleted file mode 100644 index 9d81bda34..000000000 --- a/ui/tests/integration/components/clients/config-test.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, find, click, fillIn } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; - -module('Integration | Component | client count config', function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.router = this.owner.lookup('service:router'); - this.transitionStub = sinon.stub(this.router, 'transitionTo'); - const store = this.owner.lookup('service:store'); - this.createModel = (enabled = 'enable', reporting_enabled = false, minimum_retention_months = 24) => { - store.pushPayload('clients/config', { - modelName: 'clients/config', - id: 'foo', - data: { - enabled, - reporting_enabled, - minimum_retention_months, - retention_months: 24, - }, - }); - this.model = store.peekRecord('clients/config', 'foo'); - }; - }); - - test('it shows the table with the correct rows by default', async function (assert) { - this.createModel(); - - await render(hbs``); - - assert.dom('[data-test-clients-config-table]').exists('Clients config table exists'); - const rows = document.querySelectorAll('.info-table-row'); - assert.strictEqual(rows.length, 2, 'renders 2 info table rows'); - assert.ok( - find('[data-test-row-value="Usage data collection"]').textContent.includes('On'), - 'Enabled value matches model' - ); - assert.ok( - find('[data-test-row-value="Retention period"]').textContent.includes('24'), - 'Retention period value matches model' - ); - }); - - test('it should function in edit mode when reporting is disabled', async function (assert) { - assert.expect(13); - - this.server.put('/sys/internal/counters/config', (schema, req) => { - const { enabled, retention_months } = JSON.parse(req.requestBody); - const expected = { enabled: 'enable', retention_months: 24 }; - assert.deepEqual(expected, { enabled, retention_months }, 'Correct data sent in PUT request'); - return {}; - }); - - this.createModel('disable'); - - await render(hbs` - - - `); - - assert.dom('[data-test-input="enabled"]').isNotChecked('Data collection checkbox is not checked'); - assert - .dom('label[for="enabled"]') - .hasText('Data collection is off', 'Correct label renders when data collection is off'); - assert.dom('[data-test-input="retentionMonths"]').hasValue('24', 'Retention months render'); - - await click('[data-test-input="enabled"]'); - await fillIn('[data-test-input="retentionMonths"]', -3); - await click('[data-test-clients-config-save]'); - assert - .dom('[data-test-inline-error-message]') - .hasText( - 'Retention period must be greater than or equal to 24.', - 'Validation error shows for incorrect retention period' - ); - - await fillIn('[data-test-input="retentionMonths"]', 24); - await click('[data-test-clients-config-save]'); - assert.dom('.modal.is-active').exists('Modal renders'); - assert - .dom('[data-test-modal-title] span') - .hasText('Turn usage tracking on?', 'Correct modal title renders'); - assert.dom('[data-test-clients-config-modal="on"]').exists('Correct modal description block renders'); - - await click('[data-test-clients-config-modal="continue"]'); - assert.ok( - this.transitionStub.calledWith('vault.cluster.clients.config'), - 'Route transitions correctly on save success' - ); - - await click('[data-test-input="enabled"]'); - await click('[data-test-clients-config-save]'); - assert.dom('.modal.is-active').exists('Modal renders'); - assert - .dom('[data-test-modal-title] span') - .hasText('Turn usage tracking off?', 'Correct modal title renders'); - assert.dom('[data-test-clients-config-modal="off"]').exists('Correct modal description block renders'); - - await click('[data-test-clients-config-modal="cancel"]'); - assert.dom('.modal.is-active').doesNotExist('Modal is hidden on cancel'); - }); - - test('it should function in edit mode when reporting is enabled', async function (assert) { - assert.expect(6); - - this.server.put('/sys/internal/counters/config', (schema, req) => { - const { enabled, retention_months } = JSON.parse(req.requestBody); - const expected = { enabled: 'enable', retention_months: 48 }; - assert.deepEqual(expected, { enabled, retention_months }, 'Correct data sent in PUT request'); - return {}; - }); - - this.createModel('enable', true, 24); - - await render(hbs` - - - `); - - assert.dom('[data-test-input="enabled"]').isChecked('Data collection input is checked'); - assert - .dom('[data-test-input="enabled"]') - .isDisabled('Data collection input disabled when reporting is enabled'); - assert - .dom('label[for="enabled"]') - .hasText('Data collection is on', 'Correct label renders when data collection is on'); - assert.dom('[data-test-input="retentionMonths"]').hasValue('24', 'Retention months render'); - - await fillIn('[data-test-input="retentionMonths"]', 5); - await click('[data-test-clients-config-save]'); - assert - .dom('[data-test-inline-error-message]') - .hasText( - 'Retention period must be greater than or equal to 24.', - 'Validation error shows for incorrect retention period' - ); - - await fillIn('[data-test-input="retentionMonths"]', 48); - await click('[data-test-clients-config-save]'); - }); - - test('it should not show modal when data collection is not changed', async function (assert) { - assert.expect(1); - - this.server.put('/sys/internal/counters/config', (schema, req) => { - const { enabled, retention_months } = JSON.parse(req.requestBody); - const expected = { enabled: 'enable', retention_months: 24 }; - assert.deepEqual(expected, { enabled, retention_months }, 'Correct data sent in PUT request'); - return {}; - }); - - this.createModel(); - - await render(hbs` - - - `); - await fillIn('[data-test-input="retentionMonths"]', 24); - await click('[data-test-clients-config-save]'); - }); -}); diff --git a/ui/tests/integration/components/clients/horizontal-bar-chart-test.js b/ui/tests/integration/components/clients/horizontal-bar-chart-test.js deleted file mode 100644 index a7ed7d737..000000000 --- a/ui/tests/integration/components/clients/horizontal-bar-chart-test.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { findAll, render, triggerEvent } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | clients/horizontal-bar-chart', function (hooks) { - setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set('chartLegend', [ - { label: 'entity clients', key: 'entity_clients' }, - { label: 'non-entity clients', key: 'non_entity_clients' }, - ]); - }); - - test('it renders chart and tooltip', async function (assert) { - const totalObject = { clients: 5, entity_clients: 2, non_entity_clients: 3 }; - const dataArray = [ - { label: 'second', clients: 3, entity_clients: 1, non_entity_clients: 2 }, - { label: 'first', clients: 2, entity_clients: 1, non_entity_clients: 1 }, - ]; - this.set('totalCounts', totalObject); - this.set('totalClientAttribution', dataArray); - - await render(hbs` - `); - - assert.dom('[data-test-horizontal-bar-chart]').exists(); - const dataBars = findAll('[data-test-horizontal-bar-chart] rect.data-bar'); - const actionBars = findAll('[data-test-horizontal-bar-chart] rect.action-bar'); - - assert.strictEqual(actionBars.length, dataArray.length, 'renders correct number of hover bars'); - assert.strictEqual(dataBars.length, dataArray.length * 2, 'renders correct number of data bars'); - - const textLabels = this.element.querySelectorAll('[data-test-horizontal-bar-chart] .tick text'); - const textTotals = this.element.querySelectorAll('[data-test-horizontal-bar-chart] text.total-value'); - textLabels.forEach((label, index) => { - assert.dom(label).hasText(dataArray[index].label, 'label renders correct text'); - }); - textTotals.forEach((label, index) => { - assert.dom(label).hasText(`${dataArray[index].clients}`, 'total value renders correct number'); - }); - for (const [i, bar] of actionBars.entries()) { - const percent = Math.round((dataArray[i].clients / totalObject.clients) * 100); - await triggerEvent(bar, 'mouseover'); - const tooltip = document.querySelector('.ember-modal-dialog'); - assert.dom(tooltip).includesText(`${percent}%`, 'tooltip renders correct percentage'); - } - }); - - test('it renders data with a large range', async function (assert) { - const totalObject = { clients: 5929393, entity_clients: 1391997, non_entity_clients: 4537396 }; - const dataArray = [ - { label: 'second', clients: 5929093, entity_clients: 1391896, non_entity_clients: 4537100 }, - { label: 'first', clients: 300, entity_clients: 101, non_entity_clients: 296 }, - ]; - this.set('totalCounts', totalObject); - this.set('totalClientAttribution', dataArray); - - await render(hbs` - `); - - assert.dom('[data-test-horizontal-bar-chart]').exists(); - const dataBars = findAll('[data-test-horizontal-bar-chart] rect.data-bar'); - const actionBars = findAll('[data-test-horizontal-bar-chart] rect.action-bar'); - - assert.strictEqual(actionBars.length, dataArray.length, 'renders correct number of hover bars'); - assert.strictEqual(dataBars.length, dataArray.length * 2, 'renders correct number of data bars'); - - for (const [i, bar] of actionBars.entries()) { - const percent = Math.round((dataArray[i].clients / totalObject.clients) * 100); - await triggerEvent(bar, 'mouseover'); - const tooltip = document.querySelector('.ember-modal-dialog'); - assert.dom(tooltip).includesText(`${percent}%`, 'tooltip renders correct percentage'); - } - }); -}); diff --git a/ui/tests/integration/components/clients/line-chart-test.js b/ui/tests/integration/components/clients/line-chart-test.js deleted file mode 100644 index 0d918e5ee..000000000 --- a/ui/tests/integration/components/clients/line-chart-test.js +++ /dev/null @@ -1,226 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { setupRenderingTest } from 'ember-qunit'; -import { find, render, findAll, triggerEvent } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { format, formatRFC3339, subMonths } from 'date-fns'; -import { formatChartDate } from 'core/utils/date-formatters'; -import timestamp from 'core/utils/timestamp'; - -module('Integration | Component | clients/line-chart', function (hooks) { - setupRenderingTest(hooks); - hooks.before(function () { - sinon.stub(timestamp, 'now').callsFake(() => new Date('2018-04-03T14:15:30')); - }); - hooks.beforeEach(function () { - this.set('xKey', 'foo'); - this.set('yKey', 'bar'); - this.set('dataset', [ - { - foo: 1, - bar: 4, - }, - { - foo: 2, - bar: 8, - }, - { - foo: 3, - bar: 14, - }, - { - foo: 4, - bar: 10, - }, - ]); - }); - hooks.after(function () { - timestamp.now.restore(); - }); - - test('it renders', async function (assert) { - await render(hbs` -
- -
- `); - - assert.dom('[data-test-line-chart]').exists('Chart is rendered'); - assert - .dom('[data-test-line-chart="plot-point"]') - .exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`); - - findAll('[data-test-line-chart="x-axis-labels"] text').forEach((e, i) => { - assert - .dom(e) - .hasText(`${this.dataset[i][this.xKey]}`, `renders x-axis label: ${this.dataset[i][this.xKey]}`); - }); - assert.dom(find('[data-test-line-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`); - }); - - test('it renders upgrade data', async function (assert) { - const now = timestamp.now(); - this.set('dataset', [ - { - foo: format(subMonths(now, 4), 'M/yy'), - bar: 4, - }, - { - foo: format(subMonths(now, 3), 'M/yy'), - bar: 8, - }, - { - foo: format(subMonths(now, 2), 'M/yy'), - bar: 14, - }, - { - foo: format(subMonths(now, 1), 'M/yy'), - bar: 10, - }, - ]); - this.set('upgradeData', [ - { - id: '1.10.1', - previousVersion: '1.9.2', - timestampInstalled: formatRFC3339(subMonths(now, 2)), - }, - ]); - await render(hbs` -
- -
- `); - assert.dom('[data-test-line-chart]').exists('Chart is rendered'); - assert - .dom('[data-test-line-chart="plot-point"]') - .exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`); - assert - .dom(find(`[data-test-line-chart="upgrade-${this.dataset[2][this.xKey]}"]`)) - .hasStyle({ opacity: '1' }, `upgrade data point ${this.dataset[2][this.xKey]} has yellow highlight`); - }); - - test('it renders tooltip', async function (assert) { - const now = timestamp.now(); - const tooltipData = [ - { - month: format(subMonths(now, 4), 'M/yy'), - clients: 4, - new_clients: { - clients: 0, - }, - }, - { - month: format(subMonths(now, 3), 'M/yy'), - clients: 8, - new_clients: { - clients: 4, - }, - }, - { - month: format(subMonths(now, 2), 'M/yy'), - clients: 14, - new_clients: { - clients: 6, - }, - }, - { - month: format(subMonths(now, 1), 'M/yy'), - clients: 20, - new_clients: { - clients: 4, - }, - }, - ]; - this.set('dataset', tooltipData); - this.set('upgradeData', [ - { - id: '1.10.1', - previousVersion: '1.9.2', - timestampInstalled: formatRFC3339(subMonths(now, 2)), - }, - ]); - await render(hbs` -
- -
- `); - - const tooltipHoverCircles = findAll('[data-test-line-chart] circle.hover-circle'); - for (const [i, bar] of tooltipHoverCircles.entries()) { - await triggerEvent(bar, 'mouseover'); - const tooltip = document.querySelector('.ember-modal-dialog'); - const { month, clients, new_clients } = tooltipData[i]; - assert - .dom(tooltip) - .includesText( - `${formatChartDate(month)} ${clients} total clients ${new_clients.clients} new clients`, - `tooltip text is correct for ${month}` - ); - } - }); - - test('it fails gracefully when upgradeData is an object', async function (assert) { - this.set('upgradeData', { some: 'object' }); - await render(hbs` -
- -
- `); - - assert - .dom('[data-test-line-chart="plot-point"]') - .exists({ count: this.dataset.length }, 'chart still renders when upgradeData is not an array'); - }); - - test('it fails gracefully when upgradeData has incorrect key names', async function (assert) { - this.set('upgradeData', [{ incorrect: 'key names' }]); - await render(hbs` -
- -
- `); - - assert - .dom('[data-test-line-chart="plot-point"]') - .exists({ count: this.dataset.length }, 'chart still renders when upgradeData has incorrect keys'); - }); - - test('it renders empty state when no dataset', async function (assert) { - await render(hbs` -
- -
- `); - - assert.dom('[data-test-component="empty-state"]').exists('renders empty state when no data'); - assert - .dom('[data-test-empty-state-subtext]') - .hasText( - `this is a custom message to explain why you're not seeing a line chart`, - 'custom message renders' - ); - }); -}); diff --git a/ui/tests/integration/components/clients/monthly-usage-test.js b/ui/tests/integration/components/clients/monthly-usage-test.js deleted file mode 100644 index 8f77ee4ca..000000000 --- a/ui/tests/integration/components/clients/monthly-usage-test.js +++ /dev/null @@ -1,1487 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { formatRFC3339 } from 'date-fns'; -import { findAll } from '@ember/test-helpers'; -import { calculateAverage } from 'vault/utils/chart-helpers'; -import { formatNumber } from 'core/helpers/format-number'; -import timestamp from 'core/utils/timestamp'; - -module('Integration | Component | clients/monthly-usage', function (hooks) { - setupRenderingTest(hooks); - const DATASET = [ - { - month: '8/21', - timestamp: '2021-08-01T00:00:00Z', - counts: null, - namespaces: [], - new_clients: { - month: '8/21', - namespaces: [], - }, - namespaces_by_key: {}, - }, - { - month: '9/21', - clients: 19251, - entity_clients: 10713, - non_entity_clients: 8538, - namespaces: [ - { - label: 'root', - clients: 4852, - entity_clients: 3108, - non_entity_clients: 1744, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1598, - entity_clients: 687, - non_entity_clients: 911, - }, - { - label: 'path-1', - clients: 1429, - entity_clients: 981, - non_entity_clients: 448, - }, - { - label: 'path-4-with-over-18-characters', - clients: 965, - entity_clients: 720, - non_entity_clients: 245, - }, - { - label: 'path-2', - clients: 860, - entity_clients: 720, - non_entity_clients: 140, - }, - ], - }, - { - label: 'test-ns-2/', - clients: 4702, - entity_clients: 3057, - non_entity_clients: 1645, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1686, - entity_clients: 926, - non_entity_clients: 760, - }, - { - label: 'path-4-with-over-18-characters', - clients: 1525, - entity_clients: 789, - non_entity_clients: 736, - }, - { - label: 'path-2', - clients: 905, - entity_clients: 849, - non_entity_clients: 56, - }, - { - label: 'path-1', - clients: 586, - entity_clients: 493, - non_entity_clients: 93, - }, - ], - }, - { - label: 'test-ns-1/', - clients: 4569, - entity_clients: 1871, - non_entity_clients: 2698, - mounts: [ - { - label: 'path-4-with-over-18-characters', - clients: 1534, - entity_clients: 619, - non_entity_clients: 915, - }, - { - label: 'path-3-with-over-18-characters', - clients: 1528, - entity_clients: 589, - non_entity_clients: 939, - }, - { - label: 'path-1', - clients: 828, - entity_clients: 612, - non_entity_clients: 216, - }, - { - label: 'path-2', - clients: 679, - entity_clients: 51, - non_entity_clients: 628, - }, - ], - }, - { - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 3771, - entity_clients: 2029, - non_entity_clients: 1742, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1249, - entity_clients: 793, - non_entity_clients: 456, - }, - { - label: 'path-1', - clients: 1046, - entity_clients: 444, - non_entity_clients: 602, - }, - { - label: 'path-2', - clients: 930, - entity_clients: 277, - non_entity_clients: 653, - }, - { - label: 'path-4-with-over-18-characters', - clients: 546, - entity_clients: 515, - non_entity_clients: 31, - }, - ], - }, - { - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 1357, - entity_clients: 648, - non_entity_clients: 709, - mounts: [ - { - label: 'path-1', - clients: 613, - entity_clients: 23, - non_entity_clients: 590, - }, - { - label: 'path-3-with-over-18-characters', - clients: 543, - entity_clients: 465, - non_entity_clients: 78, - }, - { - label: 'path-2', - clients: 146, - entity_clients: 141, - non_entity_clients: 5, - }, - { - label: 'path-4-with-over-18-characters', - clients: 55, - entity_clients: 19, - non_entity_clients: 36, - }, - ], - }, - ], - namespaces_by_key: { - root: { - month: '9/21', - clients: 4852, - entity_clients: 3108, - non_entity_clients: 1744, - new_clients: { - month: '9/21', - label: 'root', - clients: 2525, - entity_clients: 1315, - non_entity_clients: 1210, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1598, - entity_clients: 687, - non_entity_clients: 911, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1055, - entity_clients: 257, - non_entity_clients: 798, - }, - }, - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 1429, - entity_clients: 981, - non_entity_clients: 448, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 543, - entity_clients: 340, - non_entity_clients: 203, - }, - }, - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 965, - entity_clients: 720, - non_entity_clients: 245, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 136, - entity_clients: 7, - non_entity_clients: 129, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 860, - entity_clients: 720, - non_entity_clients: 140, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 791, - entity_clients: 711, - non_entity_clients: 80, - }, - }, - }, - }, - 'test-ns-2/': { - month: '9/21', - clients: 4702, - entity_clients: 3057, - non_entity_clients: 1645, - new_clients: { - month: '9/21', - label: 'test-ns-2/', - clients: 1537, - entity_clients: 662, - non_entity_clients: 875, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1686, - entity_clients: 926, - non_entity_clients: 760, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 520, - entity_clients: 13, - non_entity_clients: 507, - }, - }, - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 1525, - entity_clients: 789, - non_entity_clients: 736, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 499, - entity_clients: 197, - non_entity_clients: 302, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 905, - entity_clients: 849, - non_entity_clients: 56, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 398, - entity_clients: 370, - non_entity_clients: 28, - }, - }, - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 586, - entity_clients: 493, - non_entity_clients: 93, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 120, - entity_clients: 82, - non_entity_clients: 38, - }, - }, - }, - }, - 'test-ns-1/': { - month: '9/21', - clients: 4569, - entity_clients: 1871, - non_entity_clients: 2698, - new_clients: { - month: '9/21', - label: 'test-ns-1/', - clients: 2712, - entity_clients: 879, - non_entity_clients: 1833, - }, - mounts_by_key: { - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 1534, - entity_clients: 619, - non_entity_clients: 915, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 740, - entity_clients: 39, - non_entity_clients: 701, - }, - }, - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1528, - entity_clients: 589, - non_entity_clients: 939, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1250, - entity_clients: 536, - non_entity_clients: 714, - }, - }, - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 828, - entity_clients: 612, - non_entity_clients: 216, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 463, - entity_clients: 283, - non_entity_clients: 180, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 679, - entity_clients: 51, - non_entity_clients: 628, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 259, - entity_clients: 21, - non_entity_clients: 238, - }, - }, - }, - }, - 'test-ns-2-with-namespace-length-over-18-characters/': { - month: '9/21', - clients: 3771, - entity_clients: 2029, - non_entity_clients: 1742, - new_clients: { - month: '9/21', - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 2087, - entity_clients: 902, - non_entity_clients: 1185, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1249, - entity_clients: 793, - non_entity_clients: 456, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 472, - entity_clients: 260, - non_entity_clients: 212, - }, - }, - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 1046, - entity_clients: 444, - non_entity_clients: 602, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 775, - entity_clients: 349, - non_entity_clients: 426, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 930, - entity_clients: 277, - non_entity_clients: 653, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 632, - entity_clients: 90, - non_entity_clients: 542, - }, - }, - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 546, - entity_clients: 515, - non_entity_clients: 31, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 208, - entity_clients: 203, - non_entity_clients: 5, - }, - }, - }, - }, - 'test-ns-1-with-namespace-length-over-18-characters/': { - month: '9/21', - clients: 1357, - entity_clients: 648, - non_entity_clients: 709, - new_clients: { - month: '9/21', - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 560, - entity_clients: 189, - non_entity_clients: 371, - }, - mounts_by_key: { - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 613, - entity_clients: 23, - non_entity_clients: 590, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 318, - entity_clients: 12, - non_entity_clients: 306, - }, - }, - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 543, - entity_clients: 465, - non_entity_clients: 78, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 126, - entity_clients: 89, - non_entity_clients: 37, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 146, - entity_clients: 141, - non_entity_clients: 5, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 76, - entity_clients: 75, - non_entity_clients: 1, - }, - }, - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 55, - entity_clients: 19, - non_entity_clients: 36, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 40, - entity_clients: 13, - non_entity_clients: 27, - }, - }, - }, - }, - }, - new_clients: { - month: '9/21', - clients: 9421, - entity_clients: 3947, - non_entity_clients: 5474, - namespaces: [ - { - label: 'test-ns-1/', - clients: 2712, - entity_clients: 879, - non_entity_clients: 1833, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1250, - entity_clients: 536, - non_entity_clients: 714, - }, - { - label: 'path-4-with-over-18-characters', - clients: 740, - entity_clients: 39, - non_entity_clients: 701, - }, - { - label: 'path-1', - clients: 463, - entity_clients: 283, - non_entity_clients: 180, - }, - { - label: 'path-2', - clients: 259, - entity_clients: 21, - non_entity_clients: 238, - }, - ], - }, - { - label: 'root', - clients: 2525, - entity_clients: 1315, - non_entity_clients: 1210, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1055, - entity_clients: 257, - non_entity_clients: 798, - }, - { - label: 'path-2', - clients: 791, - entity_clients: 711, - non_entity_clients: 80, - }, - { - label: 'path-1', - clients: 543, - entity_clients: 340, - non_entity_clients: 203, - }, - { - label: 'path-4-with-over-18-characters', - clients: 136, - entity_clients: 7, - non_entity_clients: 129, - }, - ], - }, - { - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 2087, - entity_clients: 902, - non_entity_clients: 1185, - mounts: [ - { - label: 'path-1', - clients: 775, - entity_clients: 349, - non_entity_clients: 426, - }, - { - label: 'path-2', - clients: 632, - entity_clients: 90, - non_entity_clients: 542, - }, - { - label: 'path-3-with-over-18-characters', - clients: 472, - entity_clients: 260, - non_entity_clients: 212, - }, - { - label: 'path-4-with-over-18-characters', - clients: 208, - entity_clients: 203, - non_entity_clients: 5, - }, - ], - }, - { - label: 'test-ns-2/', - clients: 1537, - entity_clients: 662, - non_entity_clients: 875, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 520, - entity_clients: 13, - non_entity_clients: 507, - }, - { - label: 'path-4-with-over-18-characters', - clients: 499, - entity_clients: 197, - non_entity_clients: 302, - }, - { - label: 'path-2', - clients: 398, - entity_clients: 370, - non_entity_clients: 28, - }, - { - label: 'path-1', - clients: 120, - entity_clients: 82, - non_entity_clients: 38, - }, - ], - }, - { - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 560, - entity_clients: 189, - non_entity_clients: 371, - mounts: [ - { - label: 'path-1', - clients: 318, - entity_clients: 12, - non_entity_clients: 306, - }, - { - label: 'path-3-with-over-18-characters', - clients: 126, - entity_clients: 89, - non_entity_clients: 37, - }, - { - label: 'path-2', - clients: 76, - entity_clients: 75, - non_entity_clients: 1, - }, - { - label: 'path-4-with-over-18-characters', - clients: 40, - entity_clients: 13, - non_entity_clients: 27, - }, - ], - }, - ], - }, - }, - { - month: '10/21', - clients: 19417, - entity_clients: 10105, - non_entity_clients: 9312, - namespaces: [ - { - label: 'root', - clients: 4835, - entity_clients: 2364, - non_entity_clients: 2471, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1797, - entity_clients: 883, - non_entity_clients: 914, - }, - { - label: 'path-1', - clients: 1501, - entity_clients: 663, - non_entity_clients: 838, - }, - { - label: 'path-2', - clients: 1461, - entity_clients: 800, - non_entity_clients: 661, - }, - { - label: 'path-4-with-over-18-characters', - clients: 76, - entity_clients: 18, - non_entity_clients: 58, - }, - ], - }, - { - label: 'test-ns-2/', - clients: 4027, - entity_clients: 1692, - non_entity_clients: 2335, - mounts: [ - { - label: 'path-4-with-over-18-characters', - clients: 1223, - entity_clients: 820, - non_entity_clients: 403, - }, - { - label: 'path-3-with-over-18-characters', - clients: 1110, - entity_clients: 111, - non_entity_clients: 999, - }, - { - label: 'path-1', - clients: 1034, - entity_clients: 462, - non_entity_clients: 572, - }, - { - label: 'path-2', - clients: 660, - entity_clients: 299, - non_entity_clients: 361, - }, - ], - }, - { - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 3924, - entity_clients: 2132, - non_entity_clients: 1792, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1411, - entity_clients: 765, - non_entity_clients: 646, - }, - { - label: 'path-2', - clients: 1205, - entity_clients: 382, - non_entity_clients: 823, - }, - { - label: 'path-1', - clients: 884, - entity_clients: 850, - non_entity_clients: 34, - }, - { - label: 'path-4-with-over-18-characters', - clients: 424, - entity_clients: 135, - non_entity_clients: 289, - }, - ], - }, - { - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 3639, - entity_clients: 2314, - non_entity_clients: 1325, - mounts: [ - { - label: 'path-1', - clients: 1062, - entity_clients: 781, - non_entity_clients: 281, - }, - { - label: 'path-4-with-over-18-characters', - clients: 1021, - entity_clients: 609, - non_entity_clients: 412, - }, - { - label: 'path-2', - clients: 849, - entity_clients: 426, - non_entity_clients: 423, - }, - { - label: 'path-3-with-over-18-characters', - clients: 707, - entity_clients: 498, - non_entity_clients: 209, - }, - ], - }, - { - label: 'test-ns-1/', - clients: 2992, - entity_clients: 1603, - non_entity_clients: 1389, - mounts: [ - { - label: 'path-1', - clients: 1140, - entity_clients: 480, - non_entity_clients: 660, - }, - { - label: 'path-4-with-over-18-characters', - clients: 1058, - entity_clients: 651, - non_entity_clients: 407, - }, - { - label: 'path-2', - clients: 575, - entity_clients: 416, - non_entity_clients: 159, - }, - { - label: 'path-3-with-over-18-characters', - clients: 219, - entity_clients: 56, - non_entity_clients: 163, - }, - ], - }, - ], - namespaces_by_key: { - root: { - month: '10/21', - clients: 4835, - entity_clients: 2364, - non_entity_clients: 2471, - new_clients: { - month: '10/21', - label: 'root', - clients: 1732, - entity_clients: 586, - non_entity_clients: 1146, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 1797, - entity_clients: 883, - non_entity_clients: 914, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 907, - entity_clients: 192, - non_entity_clients: 715, - }, - }, - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 1501, - entity_clients: 663, - non_entity_clients: 838, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 276, - entity_clients: 202, - non_entity_clients: 74, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 1461, - entity_clients: 800, - non_entity_clients: 661, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 502, - entity_clients: 189, - non_entity_clients: 313, - }, - }, - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 76, - entity_clients: 18, - non_entity_clients: 58, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 47, - entity_clients: 3, - non_entity_clients: 44, - }, - }, - }, - }, - 'test-ns-2/': { - month: '10/21', - clients: 4027, - entity_clients: 1692, - non_entity_clients: 2335, - new_clients: { - month: '10/21', - label: 'test-ns-2/', - clients: 2301, - entity_clients: 678, - non_entity_clients: 1623, - }, - mounts_by_key: { - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 1223, - entity_clients: 820, - non_entity_clients: 403, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 602, - entity_clients: 212, - non_entity_clients: 390, - }, - }, - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 1110, - entity_clients: 111, - non_entity_clients: 999, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 440, - entity_clients: 7, - non_entity_clients: 433, - }, - }, - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 1034, - entity_clients: 462, - non_entity_clients: 572, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 980, - entity_clients: 454, - non_entity_clients: 526, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 660, - entity_clients: 299, - non_entity_clients: 361, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 279, - entity_clients: 5, - non_entity_clients: 274, - }, - }, - }, - }, - 'test-ns-2-with-namespace-length-over-18-characters/': { - month: '10/21', - clients: 3924, - entity_clients: 2132, - non_entity_clients: 1792, - new_clients: { - month: '10/21', - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 1561, - entity_clients: 1225, - non_entity_clients: 336, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 1411, - entity_clients: 765, - non_entity_clients: 646, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 948, - entity_clients: 660, - non_entity_clients: 288, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 1205, - entity_clients: 382, - non_entity_clients: 823, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 305, - entity_clients: 289, - non_entity_clients: 16, - }, - }, - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 884, - entity_clients: 850, - non_entity_clients: 34, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 230, - entity_clients: 207, - non_entity_clients: 23, - }, - }, - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 424, - entity_clients: 135, - non_entity_clients: 289, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 78, - entity_clients: 69, - non_entity_clients: 9, - }, - }, - }, - }, - 'test-ns-1-with-namespace-length-over-18-characters/': { - month: '10/21', - clients: 3639, - entity_clients: 2314, - non_entity_clients: 1325, - new_clients: { - month: '10/21', - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 1245, - entity_clients: 710, - non_entity_clients: 535, - }, - mounts_by_key: { - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 1062, - entity_clients: 781, - non_entity_clients: 281, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 288, - entity_clients: 63, - non_entity_clients: 225, - }, - }, - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 1021, - entity_clients: 609, - non_entity_clients: 412, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 440, - entity_clients: 323, - non_entity_clients: 117, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 849, - entity_clients: 426, - non_entity_clients: 423, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 339, - entity_clients: 308, - non_entity_clients: 31, - }, - }, - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 707, - entity_clients: 498, - non_entity_clients: 209, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 178, - entity_clients: 16, - non_entity_clients: 162, - }, - }, - }, - }, - 'test-ns-1/': { - month: '10/21', - clients: 2992, - entity_clients: 1603, - non_entity_clients: 1389, - new_clients: { - month: '10/21', - label: 'test-ns-1/', - clients: 820, - entity_clients: 356, - non_entity_clients: 464, - }, - mounts_by_key: { - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 1140, - entity_clients: 480, - non_entity_clients: 660, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 239, - entity_clients: 30, - non_entity_clients: 209, - }, - }, - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 1058, - entity_clients: 651, - non_entity_clients: 407, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 256, - entity_clients: 63, - non_entity_clients: 193, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 575, - entity_clients: 416, - non_entity_clients: 159, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 259, - entity_clients: 245, - non_entity_clients: 14, - }, - }, - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 219, - entity_clients: 56, - non_entity_clients: 163, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 66, - entity_clients: 18, - non_entity_clients: 48, - }, - }, - }, - }, - }, - new_clients: { - month: '10/21', - clients: 7659, - entity_clients: 3555, - non_entity_clients: 4104, - namespaces: [ - { - label: 'test-ns-2/', - clients: 2301, - entity_clients: 678, - non_entity_clients: 1623, - mounts: [ - { - label: 'path-1', - clients: 980, - entity_clients: 454, - non_entity_clients: 526, - }, - { - label: 'path-4-with-over-18-characters', - clients: 602, - entity_clients: 212, - non_entity_clients: 390, - }, - { - label: 'path-3-with-over-18-characters', - clients: 440, - entity_clients: 7, - non_entity_clients: 433, - }, - { - label: 'path-2', - clients: 279, - entity_clients: 5, - non_entity_clients: 274, - }, - ], - }, - { - label: 'root', - clients: 1732, - entity_clients: 586, - non_entity_clients: 1146, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 907, - entity_clients: 192, - non_entity_clients: 715, - }, - { - label: 'path-2', - clients: 502, - entity_clients: 189, - non_entity_clients: 313, - }, - { - label: 'path-1', - clients: 276, - entity_clients: 202, - non_entity_clients: 74, - }, - { - label: 'path-4-with-over-18-characters', - clients: 47, - entity_clients: 3, - non_entity_clients: 44, - }, - ], - }, - { - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 1561, - entity_clients: 1225, - non_entity_clients: 336, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 948, - entity_clients: 660, - non_entity_clients: 288, - }, - { - label: 'path-2', - clients: 305, - entity_clients: 289, - non_entity_clients: 16, - }, - { - label: 'path-1', - clients: 230, - entity_clients: 207, - non_entity_clients: 23, - }, - { - label: 'path-4-with-over-18-characters', - clients: 78, - entity_clients: 69, - non_entity_clients: 9, - }, - ], - }, - { - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 1245, - entity_clients: 710, - non_entity_clients: 535, - mounts: [ - { - label: 'path-4-with-over-18-characters', - clients: 440, - entity_clients: 323, - non_entity_clients: 117, - }, - { - label: 'path-2', - clients: 339, - entity_clients: 308, - non_entity_clients: 31, - }, - { - label: 'path-1', - clients: 288, - entity_clients: 63, - non_entity_clients: 225, - }, - { - label: 'path-3-with-over-18-characters', - clients: 178, - entity_clients: 16, - non_entity_clients: 162, - }, - ], - }, - { - label: 'test-ns-1/', - clients: 820, - entity_clients: 356, - non_entity_clients: 464, - mounts: [ - { - label: 'path-2', - clients: 259, - entity_clients: 245, - non_entity_clients: 14, - }, - { - label: 'path-4-with-over-18-characters', - clients: 256, - entity_clients: 63, - non_entity_clients: 193, - }, - { - label: 'path-1', - clients: 239, - entity_clients: 30, - non_entity_clients: 209, - }, - { - label: 'path-3-with-over-18-characters', - clients: 66, - entity_clients: 18, - non_entity_clients: 48, - }, - ], - }, - ], - }, - }, - ]; - hooks.before(function () { - sinon.stub(timestamp, 'now').callsFake(() => new Date('2018-04-03T14:15:30')); - }); - hooks.beforeEach(function () { - this.set('timestamp', formatRFC3339(timestamp.now())); - this.set('isDateRange', true); - this.set('chartLegend', [ - { label: 'entity clients', key: 'entity_clients' }, - { label: 'non-entity clients', key: 'non_entity_clients' }, - ]); - this.set('byMonthActivityData', DATASET); - }); - hooks.after(function () { - timestamp.now.restore(); - }); - - test('it renders empty state with no data', async function (assert) { - await render(hbs` - - - `); - assert.dom('[data-test-monthly-usage]').exists('monthly usage component renders'); - assert.dom('[data-test-component="empty-state"]').exists(); - assert.dom('[data-test-empty-state-subtext]').hasText('No data to display'); - assert.dom('[data-test-monthly-usage-average-total] p.data-details').hasText('0', 'average total is 0'); - assert.dom('[data-test-monthly-usage-average-new] p.data-details').hasText('0', 'average new is 0'); - assert.dom('[data-test-vertical-bar-chart]').doesNotExist('vertical bar chart does not render'); - assert.dom('[data-test-monthly-usage-legend]').doesNotExist('legend does not exist'); - assert.dom('[data-test-monthly-usage-timestamp]').exists('renders timestamp'); - }); - - test('it renders with month over month activity data', async function (assert) { - const expectedTotal = formatNumber([calculateAverage(DATASET, 'clients')]); - const expectedNew = formatNumber([ - calculateAverage( - DATASET?.map((d) => d.new_clients), - 'clients' - ), - ]); - await render(hbs` - - - `); - assert.dom('[data-test-monthly-usage]').exists('monthly usage component renders'); - assert.dom('[data-test-component="empty-state"]').doesNotExist(); - assert.dom('[data-test-vertical-bar-chart]').exists('vertical bar chart displays'); - assert.dom('[data-test-monthly-usage-legend]').exists('renders vertical bar chart legend'); - assert.dom('[data-test-monthly-usage-timestamp]').exists('renders timestamp'); - - findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => { - assert.dom(e).hasText(`${DATASET[i].month}`, `renders x-axis label: ${DATASET[i].month}`); - }); - assert - .dom('[data-test-vertical-chart="data-bar"]') - .exists( - { count: DATASET.filter((m) => m.counts !== null).length * 2 }, - 'renders correct number of data bars' - ); - assert - .dom('[data-test-monthly-usage-average-total] p.data-details') - .hasText(`${expectedTotal}`, `renders correct total average ${expectedTotal}`); - assert - .dom('[data-test-monthly-usage-average-new] p.data-details') - .hasText(`${expectedNew}`, `renders correct new average ${expectedNew}`); - }); -}); diff --git a/ui/tests/integration/components/clients/running-total-test.js b/ui/tests/integration/components/clients/running-total-test.js deleted file mode 100644 index 868cc260a..000000000 --- a/ui/tests/integration/components/clients/running-total-test.js +++ /dev/null @@ -1,1596 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { formatRFC3339 } from 'date-fns'; -import { findAll } from '@ember/test-helpers'; -import { calculateAverage } from 'vault/utils/chart-helpers'; -import { formatNumber } from 'core/helpers/format-number'; -import timestamp from 'core/utils/timestamp'; - -module('Integration | Component | clients/running-total', function (hooks) { - setupRenderingTest(hooks); - const MONTHLY_ACTIVITY = [ - { - month: '8/21', - timestamp: '2021-08-01T00:00:00Z', - counts: null, - namespaces: [], - new_clients: { - month: '8/21', - namespaces: [], - }, - namespaces_by_key: {}, - }, - { - month: '9/21', - clients: 19251, - entity_clients: 10713, - non_entity_clients: 8538, - namespaces: [ - { - label: 'root', - clients: 4852, - entity_clients: 3108, - non_entity_clients: 1744, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1598, - entity_clients: 687, - non_entity_clients: 911, - }, - { - label: 'path-1', - clients: 1429, - entity_clients: 981, - non_entity_clients: 448, - }, - { - label: 'path-4-with-over-18-characters', - clients: 965, - entity_clients: 720, - non_entity_clients: 245, - }, - { - label: 'path-2', - clients: 860, - entity_clients: 720, - non_entity_clients: 140, - }, - ], - }, - { - label: 'test-ns-2/', - clients: 4702, - entity_clients: 3057, - non_entity_clients: 1645, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1686, - entity_clients: 926, - non_entity_clients: 760, - }, - { - label: 'path-4-with-over-18-characters', - clients: 1525, - entity_clients: 789, - non_entity_clients: 736, - }, - { - label: 'path-2', - clients: 905, - entity_clients: 849, - non_entity_clients: 56, - }, - { - label: 'path-1', - clients: 586, - entity_clients: 493, - non_entity_clients: 93, - }, - ], - }, - { - label: 'test-ns-1/', - clients: 4569, - entity_clients: 1871, - non_entity_clients: 2698, - mounts: [ - { - label: 'path-4-with-over-18-characters', - clients: 1534, - entity_clients: 619, - non_entity_clients: 915, - }, - { - label: 'path-3-with-over-18-characters', - clients: 1528, - entity_clients: 589, - non_entity_clients: 939, - }, - { - label: 'path-1', - clients: 828, - entity_clients: 612, - non_entity_clients: 216, - }, - { - label: 'path-2', - clients: 679, - entity_clients: 51, - non_entity_clients: 628, - }, - ], - }, - { - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 3771, - entity_clients: 2029, - non_entity_clients: 1742, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1249, - entity_clients: 793, - non_entity_clients: 456, - }, - { - label: 'path-1', - clients: 1046, - entity_clients: 444, - non_entity_clients: 602, - }, - { - label: 'path-2', - clients: 930, - entity_clients: 277, - non_entity_clients: 653, - }, - { - label: 'path-4-with-over-18-characters', - clients: 546, - entity_clients: 515, - non_entity_clients: 31, - }, - ], - }, - { - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 1357, - entity_clients: 648, - non_entity_clients: 709, - mounts: [ - { - label: 'path-1', - clients: 613, - entity_clients: 23, - non_entity_clients: 590, - }, - { - label: 'path-3-with-over-18-characters', - clients: 543, - entity_clients: 465, - non_entity_clients: 78, - }, - { - label: 'path-2', - clients: 146, - entity_clients: 141, - non_entity_clients: 5, - }, - { - label: 'path-4-with-over-18-characters', - clients: 55, - entity_clients: 19, - non_entity_clients: 36, - }, - ], - }, - ], - namespaces_by_key: { - root: { - month: '9/21', - clients: 4852, - entity_clients: 3108, - non_entity_clients: 1744, - new_clients: { - month: '9/21', - label: 'root', - clients: 2525, - entity_clients: 1315, - non_entity_clients: 1210, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1598, - entity_clients: 687, - non_entity_clients: 911, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1055, - entity_clients: 257, - non_entity_clients: 798, - }, - }, - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 1429, - entity_clients: 981, - non_entity_clients: 448, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 543, - entity_clients: 340, - non_entity_clients: 203, - }, - }, - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 965, - entity_clients: 720, - non_entity_clients: 245, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 136, - entity_clients: 7, - non_entity_clients: 129, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 860, - entity_clients: 720, - non_entity_clients: 140, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 791, - entity_clients: 711, - non_entity_clients: 80, - }, - }, - }, - }, - 'test-ns-2/': { - month: '9/21', - clients: 4702, - entity_clients: 3057, - non_entity_clients: 1645, - new_clients: { - month: '9/21', - label: 'test-ns-2/', - clients: 1537, - entity_clients: 662, - non_entity_clients: 875, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1686, - entity_clients: 926, - non_entity_clients: 760, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 520, - entity_clients: 13, - non_entity_clients: 507, - }, - }, - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 1525, - entity_clients: 789, - non_entity_clients: 736, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 499, - entity_clients: 197, - non_entity_clients: 302, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 905, - entity_clients: 849, - non_entity_clients: 56, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 398, - entity_clients: 370, - non_entity_clients: 28, - }, - }, - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 586, - entity_clients: 493, - non_entity_clients: 93, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 120, - entity_clients: 82, - non_entity_clients: 38, - }, - }, - }, - }, - 'test-ns-1/': { - month: '9/21', - clients: 4569, - entity_clients: 1871, - non_entity_clients: 2698, - new_clients: { - month: '9/21', - label: 'test-ns-1/', - clients: 2712, - entity_clients: 879, - non_entity_clients: 1833, - }, - mounts_by_key: { - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 1534, - entity_clients: 619, - non_entity_clients: 915, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 740, - entity_clients: 39, - non_entity_clients: 701, - }, - }, - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1528, - entity_clients: 589, - non_entity_clients: 939, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1250, - entity_clients: 536, - non_entity_clients: 714, - }, - }, - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 828, - entity_clients: 612, - non_entity_clients: 216, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 463, - entity_clients: 283, - non_entity_clients: 180, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 679, - entity_clients: 51, - non_entity_clients: 628, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 259, - entity_clients: 21, - non_entity_clients: 238, - }, - }, - }, - }, - 'test-ns-2-with-namespace-length-over-18-characters/': { - month: '9/21', - clients: 3771, - entity_clients: 2029, - non_entity_clients: 1742, - new_clients: { - month: '9/21', - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 2087, - entity_clients: 902, - non_entity_clients: 1185, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 1249, - entity_clients: 793, - non_entity_clients: 456, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 472, - entity_clients: 260, - non_entity_clients: 212, - }, - }, - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 1046, - entity_clients: 444, - non_entity_clients: 602, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 775, - entity_clients: 349, - non_entity_clients: 426, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 930, - entity_clients: 277, - non_entity_clients: 653, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 632, - entity_clients: 90, - non_entity_clients: 542, - }, - }, - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 546, - entity_clients: 515, - non_entity_clients: 31, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 208, - entity_clients: 203, - non_entity_clients: 5, - }, - }, - }, - }, - 'test-ns-1-with-namespace-length-over-18-characters/': { - month: '9/21', - clients: 1357, - entity_clients: 648, - non_entity_clients: 709, - new_clients: { - month: '9/21', - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 560, - entity_clients: 189, - non_entity_clients: 371, - }, - mounts_by_key: { - 'path-1': { - month: '9/21', - label: 'path-1', - clients: 613, - entity_clients: 23, - non_entity_clients: 590, - new_clients: { - month: '9/21', - label: 'path-1', - clients: 318, - entity_clients: 12, - non_entity_clients: 306, - }, - }, - 'path-3-with-over-18-characters': { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 543, - entity_clients: 465, - non_entity_clients: 78, - new_clients: { - month: '9/21', - label: 'path-3-with-over-18-characters', - clients: 126, - entity_clients: 89, - non_entity_clients: 37, - }, - }, - 'path-2': { - month: '9/21', - label: 'path-2', - clients: 146, - entity_clients: 141, - non_entity_clients: 5, - new_clients: { - month: '9/21', - label: 'path-2', - clients: 76, - entity_clients: 75, - non_entity_clients: 1, - }, - }, - 'path-4-with-over-18-characters': { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 55, - entity_clients: 19, - non_entity_clients: 36, - new_clients: { - month: '9/21', - label: 'path-4-with-over-18-characters', - clients: 40, - entity_clients: 13, - non_entity_clients: 27, - }, - }, - }, - }, - }, - new_clients: { - month: '9/21', - clients: 9421, - entity_clients: 3947, - non_entity_clients: 5474, - namespaces: [ - { - label: 'test-ns-1/', - clients: 2712, - entity_clients: 879, - non_entity_clients: 1833, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1250, - entity_clients: 536, - non_entity_clients: 714, - }, - { - label: 'path-4-with-over-18-characters', - clients: 740, - entity_clients: 39, - non_entity_clients: 701, - }, - { - label: 'path-1', - clients: 463, - entity_clients: 283, - non_entity_clients: 180, - }, - { - label: 'path-2', - clients: 259, - entity_clients: 21, - non_entity_clients: 238, - }, - ], - }, - { - label: 'root', - clients: 2525, - entity_clients: 1315, - non_entity_clients: 1210, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1055, - entity_clients: 257, - non_entity_clients: 798, - }, - { - label: 'path-2', - clients: 791, - entity_clients: 711, - non_entity_clients: 80, - }, - { - label: 'path-1', - clients: 543, - entity_clients: 340, - non_entity_clients: 203, - }, - { - label: 'path-4-with-over-18-characters', - clients: 136, - entity_clients: 7, - non_entity_clients: 129, - }, - ], - }, - { - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 2087, - entity_clients: 902, - non_entity_clients: 1185, - mounts: [ - { - label: 'path-1', - clients: 775, - entity_clients: 349, - non_entity_clients: 426, - }, - { - label: 'path-2', - clients: 632, - entity_clients: 90, - non_entity_clients: 542, - }, - { - label: 'path-3-with-over-18-characters', - clients: 472, - entity_clients: 260, - non_entity_clients: 212, - }, - { - label: 'path-4-with-over-18-characters', - clients: 208, - entity_clients: 203, - non_entity_clients: 5, - }, - ], - }, - { - label: 'test-ns-2/', - clients: 1537, - entity_clients: 662, - non_entity_clients: 875, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 520, - entity_clients: 13, - non_entity_clients: 507, - }, - { - label: 'path-4-with-over-18-characters', - clients: 499, - entity_clients: 197, - non_entity_clients: 302, - }, - { - label: 'path-2', - clients: 398, - entity_clients: 370, - non_entity_clients: 28, - }, - { - label: 'path-1', - clients: 120, - entity_clients: 82, - non_entity_clients: 38, - }, - ], - }, - { - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 560, - entity_clients: 189, - non_entity_clients: 371, - mounts: [ - { - label: 'path-1', - clients: 318, - entity_clients: 12, - non_entity_clients: 306, - }, - { - label: 'path-3-with-over-18-characters', - clients: 126, - entity_clients: 89, - non_entity_clients: 37, - }, - { - label: 'path-2', - clients: 76, - entity_clients: 75, - non_entity_clients: 1, - }, - { - label: 'path-4-with-over-18-characters', - clients: 40, - entity_clients: 13, - non_entity_clients: 27, - }, - ], - }, - ], - }, - }, - { - month: '10/21', - clients: 19417, - entity_clients: 10105, - non_entity_clients: 9312, - namespaces: [ - { - label: 'root', - clients: 4835, - entity_clients: 2364, - non_entity_clients: 2471, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1797, - entity_clients: 883, - non_entity_clients: 914, - }, - { - label: 'path-1', - clients: 1501, - entity_clients: 663, - non_entity_clients: 838, - }, - { - label: 'path-2', - clients: 1461, - entity_clients: 800, - non_entity_clients: 661, - }, - { - label: 'path-4-with-over-18-characters', - clients: 76, - entity_clients: 18, - non_entity_clients: 58, - }, - ], - }, - { - label: 'test-ns-2/', - clients: 4027, - entity_clients: 1692, - non_entity_clients: 2335, - mounts: [ - { - label: 'path-4-with-over-18-characters', - clients: 1223, - entity_clients: 820, - non_entity_clients: 403, - }, - { - label: 'path-3-with-over-18-characters', - clients: 1110, - entity_clients: 111, - non_entity_clients: 999, - }, - { - label: 'path-1', - clients: 1034, - entity_clients: 462, - non_entity_clients: 572, - }, - { - label: 'path-2', - clients: 660, - entity_clients: 299, - non_entity_clients: 361, - }, - ], - }, - { - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 3924, - entity_clients: 2132, - non_entity_clients: 1792, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 1411, - entity_clients: 765, - non_entity_clients: 646, - }, - { - label: 'path-2', - clients: 1205, - entity_clients: 382, - non_entity_clients: 823, - }, - { - label: 'path-1', - clients: 884, - entity_clients: 850, - non_entity_clients: 34, - }, - { - label: 'path-4-with-over-18-characters', - clients: 424, - entity_clients: 135, - non_entity_clients: 289, - }, - ], - }, - { - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 3639, - entity_clients: 2314, - non_entity_clients: 1325, - mounts: [ - { - label: 'path-1', - clients: 1062, - entity_clients: 781, - non_entity_clients: 281, - }, - { - label: 'path-4-with-over-18-characters', - clients: 1021, - entity_clients: 609, - non_entity_clients: 412, - }, - { - label: 'path-2', - clients: 849, - entity_clients: 426, - non_entity_clients: 423, - }, - { - label: 'path-3-with-over-18-characters', - clients: 707, - entity_clients: 498, - non_entity_clients: 209, - }, - ], - }, - { - label: 'test-ns-1/', - clients: 2992, - entity_clients: 1603, - non_entity_clients: 1389, - mounts: [ - { - label: 'path-1', - clients: 1140, - entity_clients: 480, - non_entity_clients: 660, - }, - { - label: 'path-4-with-over-18-characters', - clients: 1058, - entity_clients: 651, - non_entity_clients: 407, - }, - { - label: 'path-2', - clients: 575, - entity_clients: 416, - non_entity_clients: 159, - }, - { - label: 'path-3-with-over-18-characters', - clients: 219, - entity_clients: 56, - non_entity_clients: 163, - }, - ], - }, - ], - namespaces_by_key: { - root: { - month: '10/21', - clients: 4835, - entity_clients: 2364, - non_entity_clients: 2471, - new_clients: { - month: '10/21', - label: 'root', - clients: 1732, - entity_clients: 586, - non_entity_clients: 1146, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 1797, - entity_clients: 883, - non_entity_clients: 914, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 907, - entity_clients: 192, - non_entity_clients: 715, - }, - }, - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 1501, - entity_clients: 663, - non_entity_clients: 838, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 276, - entity_clients: 202, - non_entity_clients: 74, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 1461, - entity_clients: 800, - non_entity_clients: 661, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 502, - entity_clients: 189, - non_entity_clients: 313, - }, - }, - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 76, - entity_clients: 18, - non_entity_clients: 58, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 47, - entity_clients: 3, - non_entity_clients: 44, - }, - }, - }, - }, - 'test-ns-2/': { - month: '10/21', - clients: 4027, - entity_clients: 1692, - non_entity_clients: 2335, - new_clients: { - month: '10/21', - label: 'test-ns-2/', - clients: 2301, - entity_clients: 678, - non_entity_clients: 1623, - }, - mounts_by_key: { - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 1223, - entity_clients: 820, - non_entity_clients: 403, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 602, - entity_clients: 212, - non_entity_clients: 390, - }, - }, - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 1110, - entity_clients: 111, - non_entity_clients: 999, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 440, - entity_clients: 7, - non_entity_clients: 433, - }, - }, - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 1034, - entity_clients: 462, - non_entity_clients: 572, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 980, - entity_clients: 454, - non_entity_clients: 526, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 660, - entity_clients: 299, - non_entity_clients: 361, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 279, - entity_clients: 5, - non_entity_clients: 274, - }, - }, - }, - }, - 'test-ns-2-with-namespace-length-over-18-characters/': { - month: '10/21', - clients: 3924, - entity_clients: 2132, - non_entity_clients: 1792, - new_clients: { - month: '10/21', - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 1561, - entity_clients: 1225, - non_entity_clients: 336, - }, - mounts_by_key: { - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 1411, - entity_clients: 765, - non_entity_clients: 646, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 948, - entity_clients: 660, - non_entity_clients: 288, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 1205, - entity_clients: 382, - non_entity_clients: 823, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 305, - entity_clients: 289, - non_entity_clients: 16, - }, - }, - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 884, - entity_clients: 850, - non_entity_clients: 34, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 230, - entity_clients: 207, - non_entity_clients: 23, - }, - }, - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 424, - entity_clients: 135, - non_entity_clients: 289, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 78, - entity_clients: 69, - non_entity_clients: 9, - }, - }, - }, - }, - 'test-ns-1-with-namespace-length-over-18-characters/': { - month: '10/21', - clients: 3639, - entity_clients: 2314, - non_entity_clients: 1325, - new_clients: { - month: '10/21', - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 1245, - entity_clients: 710, - non_entity_clients: 535, - }, - mounts_by_key: { - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 1062, - entity_clients: 781, - non_entity_clients: 281, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 288, - entity_clients: 63, - non_entity_clients: 225, - }, - }, - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 1021, - entity_clients: 609, - non_entity_clients: 412, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 440, - entity_clients: 323, - non_entity_clients: 117, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 849, - entity_clients: 426, - non_entity_clients: 423, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 339, - entity_clients: 308, - non_entity_clients: 31, - }, - }, - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 707, - entity_clients: 498, - non_entity_clients: 209, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 178, - entity_clients: 16, - non_entity_clients: 162, - }, - }, - }, - }, - 'test-ns-1/': { - month: '10/21', - clients: 2992, - entity_clients: 1603, - non_entity_clients: 1389, - new_clients: { - month: '10/21', - label: 'test-ns-1/', - clients: 820, - entity_clients: 356, - non_entity_clients: 464, - }, - mounts_by_key: { - 'path-1': { - month: '10/21', - label: 'path-1', - clients: 1140, - entity_clients: 480, - non_entity_clients: 660, - new_clients: { - month: '10/21', - label: 'path-1', - clients: 239, - entity_clients: 30, - non_entity_clients: 209, - }, - }, - 'path-4-with-over-18-characters': { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 1058, - entity_clients: 651, - non_entity_clients: 407, - new_clients: { - month: '10/21', - label: 'path-4-with-over-18-characters', - clients: 256, - entity_clients: 63, - non_entity_clients: 193, - }, - }, - 'path-2': { - month: '10/21', - label: 'path-2', - clients: 575, - entity_clients: 416, - non_entity_clients: 159, - new_clients: { - month: '10/21', - label: 'path-2', - clients: 259, - entity_clients: 245, - non_entity_clients: 14, - }, - }, - 'path-3-with-over-18-characters': { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 219, - entity_clients: 56, - non_entity_clients: 163, - new_clients: { - month: '10/21', - label: 'path-3-with-over-18-characters', - clients: 66, - entity_clients: 18, - non_entity_clients: 48, - }, - }, - }, - }, - }, - new_clients: { - month: '10/21', - clients: 7659, - entity_clients: 3555, - non_entity_clients: 4104, - namespaces: [ - { - label: 'test-ns-2/', - clients: 2301, - entity_clients: 678, - non_entity_clients: 1623, - mounts: [ - { - label: 'path-1', - clients: 980, - entity_clients: 454, - non_entity_clients: 526, - }, - { - label: 'path-4-with-over-18-characters', - clients: 602, - entity_clients: 212, - non_entity_clients: 390, - }, - { - label: 'path-3-with-over-18-characters', - clients: 440, - entity_clients: 7, - non_entity_clients: 433, - }, - { - label: 'path-2', - clients: 279, - entity_clients: 5, - non_entity_clients: 274, - }, - ], - }, - { - label: 'root', - clients: 1732, - entity_clients: 586, - non_entity_clients: 1146, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 907, - entity_clients: 192, - non_entity_clients: 715, - }, - { - label: 'path-2', - clients: 502, - entity_clients: 189, - non_entity_clients: 313, - }, - { - label: 'path-1', - clients: 276, - entity_clients: 202, - non_entity_clients: 74, - }, - { - label: 'path-4-with-over-18-characters', - clients: 47, - entity_clients: 3, - non_entity_clients: 44, - }, - ], - }, - { - label: 'test-ns-2-with-namespace-length-over-18-characters/', - clients: 1561, - entity_clients: 1225, - non_entity_clients: 336, - mounts: [ - { - label: 'path-3-with-over-18-characters', - clients: 948, - entity_clients: 660, - non_entity_clients: 288, - }, - { - label: 'path-2', - clients: 305, - entity_clients: 289, - non_entity_clients: 16, - }, - { - label: 'path-1', - clients: 230, - entity_clients: 207, - non_entity_clients: 23, - }, - { - label: 'path-4-with-over-18-characters', - clients: 78, - entity_clients: 69, - non_entity_clients: 9, - }, - ], - }, - { - label: 'test-ns-1-with-namespace-length-over-18-characters/', - clients: 1245, - entity_clients: 710, - non_entity_clients: 535, - mounts: [ - { - label: 'path-4-with-over-18-characters', - clients: 440, - entity_clients: 323, - non_entity_clients: 117, - }, - { - label: 'path-2', - clients: 339, - entity_clients: 308, - non_entity_clients: 31, - }, - { - label: 'path-1', - clients: 288, - entity_clients: 63, - non_entity_clients: 225, - }, - { - label: 'path-3-with-over-18-characters', - clients: 178, - entity_clients: 16, - non_entity_clients: 162, - }, - ], - }, - { - label: 'test-ns-1/', - clients: 820, - entity_clients: 356, - non_entity_clients: 464, - mounts: [ - { - label: 'path-2', - clients: 259, - entity_clients: 245, - non_entity_clients: 14, - }, - { - label: 'path-4-with-over-18-characters', - clients: 256, - entity_clients: 63, - non_entity_clients: 193, - }, - { - label: 'path-1', - clients: 239, - entity_clients: 30, - non_entity_clients: 209, - }, - { - label: 'path-3-with-over-18-characters', - clients: 66, - entity_clients: 18, - non_entity_clients: 48, - }, - ], - }, - ], - }, - }, - ]; - const NEW_ACTIVITY = MONTHLY_ACTIVITY.map((d) => d.new_clients); - const TOTAL_USAGE_COUNTS = { - clients: 38668, - entity_clients: 20818, - non_entity_clients: 17850, - }; - hooks.before(function () { - sinon.stub(timestamp, 'now').callsFake(() => new Date('2018-04-03T14:15:30')); - }); - hooks.beforeEach(function () { - this.set('timestamp', formatRFC3339(timestamp.now())); - this.set('chartLegend', [ - { label: 'entity clients', key: 'entity_clients' }, - { label: 'non-entity clients', key: 'non_entity_clients' }, - ]); - }); - hooks.after(function () { - timestamp.now.restore(); - }); - - test('it renders with full monthly activity data', async function (assert) { - this.set('byMonthActivityData', MONTHLY_ACTIVITY); - this.set('totalUsageCounts', TOTAL_USAGE_COUNTS); - const expectedTotalEntity = formatNumber([TOTAL_USAGE_COUNTS.entity_clients]); - const expectedTotalNonEntity = formatNumber([TOTAL_USAGE_COUNTS.non_entity_clients]); - const expectedNewEntity = formatNumber([calculateAverage(NEW_ACTIVITY, 'entity_clients')]); - const expectedNewNonEntity = formatNumber([calculateAverage(NEW_ACTIVITY, 'non_entity_clients')]); - - await render(hbs` - - - `); - - assert.dom('[data-test-running-total]').exists('running total component renders'); - assert.dom('[data-test-line-chart]').exists('line chart renders'); - assert.dom('[data-test-vertical-bar-chart]').exists('vertical bar chart renders'); - assert.dom('[data-test-running-total-legend]').exists('legend renders'); - assert.dom('[data-test-running-total-timestamp]').exists('renders timestamp'); - assert - .dom('[data-test-running-total-entity] p.data-details') - .hasText(`${expectedTotalEntity}`, `renders correct total average ${expectedTotalEntity}`); - assert - .dom('[data-test-running-total-nonentity] p.data-details') - .hasText(`${expectedTotalNonEntity}`, `renders correct new average ${expectedTotalNonEntity}`); - assert - .dom('[data-test-running-new-entity] p.data-details') - .hasText(`${expectedNewEntity}`, `renders correct total average ${expectedNewEntity}`); - assert - .dom('[data-test-running-new-nonentity] p.data-details') - .hasText(`${expectedNewNonEntity}`, `renders correct new average ${expectedNewNonEntity}`); - - // assert line chart is correct - findAll('[data-test-line-chart="x-axis-labels"] text').forEach((e, i) => { - assert - .dom(e) - .hasText( - `${MONTHLY_ACTIVITY[i].month}`, - `renders x-axis labels for line chart: ${MONTHLY_ACTIVITY[i].month}` - ); - }); - assert - .dom('[data-test-line-chart="plot-point"]') - .exists( - { count: MONTHLY_ACTIVITY.filter((m) => m.counts !== null).length }, - 'renders correct number of plot points' - ); - - // assert bar chart is correct - findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => { - assert - .dom(e) - .hasText( - `${MONTHLY_ACTIVITY[i].month}`, - `renders x-axis labels for bar chart: ${MONTHLY_ACTIVITY[i].month}` - ); - }); - assert - .dom('[data-test-vertical-chart="data-bar"]') - .exists( - { count: MONTHLY_ACTIVITY.filter((m) => m.counts !== null).length * 2 }, - 'renders correct number of data bars' - ); - }); - - test('it renders with no new monthly data', async function (assert) { - const monthlyWithoutNew = MONTHLY_ACTIVITY.map((d) => ({ ...d, new_clients: { month: d.month } })); - this.set('byMonthActivityData', monthlyWithoutNew); - this.set('totalUsageCounts', TOTAL_USAGE_COUNTS); - const expectedTotalEntity = formatNumber([TOTAL_USAGE_COUNTS.entity_clients]); - const expectedTotalNonEntity = formatNumber([TOTAL_USAGE_COUNTS.non_entity_clients]); - - await render(hbs` - - - `); - assert.dom('[data-test-running-total]').exists('running total component renders'); - assert.dom('[data-test-line-chart]').exists('line chart renders'); - assert.dom('[data-test-vertical-bar-chart]').doesNotExist('vertical bar chart does not render'); - assert.dom('[data-test-running-total-legend]').doesNotExist('legend does not render'); - assert.dom('[data-test-component="empty-state"]').exists('renders empty state'); - assert.dom('[data-test-empty-state-title]').hasText('No new clients'); - assert.dom('[data-test-running-total-timestamp]').exists('renders timestamp'); - assert - .dom('[data-test-running-total-entity] p.data-details') - .hasText(`${expectedTotalEntity}`, `renders correct total average ${expectedTotalEntity}`); - assert - .dom('[data-test-running-total-nonentity] p.data-details') - .hasText(`${expectedTotalNonEntity}`, `renders correct new average ${expectedTotalNonEntity}`); - assert - .dom('[data-test-running-new-entity] p.data-details') - .doesNotExist('new client counts does not exist'); - assert - .dom('[data-test-running-new-nonentity] p.data-details') - .doesNotExist('average new client counts does not exist'); - }); - - test('it renders with single historical month data', async function (assert) { - const singleMonth = MONTHLY_ACTIVITY[MONTHLY_ACTIVITY.length - 1]; - const singleMonthNew = NEW_ACTIVITY[NEW_ACTIVITY.length - 1]; - this.set('singleMonth', [singleMonth]); - const expectedTotalClients = formatNumber([singleMonth.clients]); - const expectedTotalEntity = formatNumber([singleMonth.entity_clients]); - const expectedTotalNonEntity = formatNumber([singleMonth.non_entity_clients]); - const expectedNewClients = formatNumber([singleMonthNew.clients]); - const expectedNewEntity = formatNumber([singleMonthNew.entity_clients]); - const expectedNewNonEntity = formatNumber([singleMonthNew.non_entity_clients]); - - await render(hbs` - - - `); - assert.dom('[data-test-running-total]').exists('running total component renders'); - assert.dom('[data-test-line-chart]').doesNotExist('line chart does not render'); - assert.dom('[data-test-vertical-bar-chart]').doesNotExist('vertical bar chart does not render'); - assert.dom('[data-test-running-total-legend]').doesNotExist('legend does not render'); - assert.dom('[data-test-running-total-timestamp]').doesNotExist('renders timestamp'); - assert.dom('[data-test-stat-text-container]').exists({ count: 6 }, 'renders stat text containers'); - assert - .dom('[data-test-new] [data-test-stat-text-container="New clients"] div.stat-value') - .hasText(`${expectedNewClients}`, `renders correct total new clients: ${expectedNewClients}`); - assert - .dom('[data-test-new] [data-test-stat-text-container="Entity clients"] div.stat-value') - .hasText(`${expectedNewEntity}`, `renders correct total new entity: ${expectedNewEntity}`); - assert - .dom('[data-test-new] [data-test-stat-text-container="Non-entity clients"] div.stat-value') - .hasText(`${expectedNewNonEntity}`, `renders correct total new non-entity: ${expectedNewNonEntity}`); - assert - .dom('[data-test-total] [data-test-stat-text-container="Total monthly clients"] div.stat-value') - .hasText(`${expectedTotalClients}`, `renders correct total clients: ${expectedTotalClients}`); - assert - .dom('[data-test-total] [data-test-stat-text-container="Entity clients"] div.stat-value') - .hasText(`${expectedTotalEntity}`, `renders correct total entity: ${expectedTotalEntity}`); - assert - .dom('[data-test-total] [data-test-stat-text-container="Non-entity clients"] div.stat-value') - .hasText(`${expectedTotalNonEntity}`, `renders correct total non-entity: ${expectedTotalNonEntity}`); - }); -}); diff --git a/ui/tests/integration/components/clients/usage-stats-test.js b/ui/tests/integration/components/clients/usage-stats-test.js deleted file mode 100644 index 8f3c9ecec..000000000 --- a/ui/tests/integration/components/clients/usage-stats-test.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | clients/usage-stats', function (hooks) { - setupRenderingTest(hooks); - - test('it renders defaults', async function (assert) { - await render(hbs``); - - assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts even with no data passed'); - assert.dom('[data-test-stat-text="total-clients"]').exists('Total clients exists'); - assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('-', 'renders dash when no data'); - assert.dom('[data-test-stat-text="entity-clients"]').exists('Entity clients exists'); - assert - .dom('[data-test-stat-text="entity-clients"] .stat-value') - .hasText('-', 'renders dash when no data'); - assert.dom('[data-test-stat-text="non-entity-clients"]').exists('Non entity clients exists'); - assert - .dom('[data-test-stat-text="non-entity-clients"] .stat-value') - .hasText('-', 'renders dash when no data'); - assert - .dom('a') - .hasAttribute('href', 'https://developer.hashicorp.com/vault/tutorials/monitoring/usage-metrics'); - }); - - test('it renders with data', async function (assert) { - this.set('counts', { - clients: 17, - entity_clients: 7, - non_entity_clients: 10, - }); - await render(hbs``); - - assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts even with no data passed'); - assert.dom('[data-test-stat-text="total-clients"]').exists('Total clients exists'); - assert - .dom('[data-test-stat-text="total-clients"] .stat-value') - .hasText('17', 'Total clients shows passed value'); - assert.dom('[data-test-stat-text="entity-clients"]').exists('Entity clients exists'); - assert - .dom('[data-test-stat-text="entity-clients"] .stat-value') - .hasText('7', 'entity clients shows passed value'); - assert.dom('[data-test-stat-text="non-entity-clients"]').exists('Non entity clients exists'); - assert - .dom('[data-test-stat-text="non-entity-clients"] .stat-value') - .hasText('10', 'non entity clients shows passed value'); - }); -}); diff --git a/ui/tests/integration/components/clients/vertical-bar-chart-test.js b/ui/tests/integration/components/clients/vertical-bar-chart-test.js deleted file mode 100644 index dbac9e741..000000000 --- a/ui/tests/integration/components/clients/vertical-bar-chart-test.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, findAll, find, triggerEvent } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | clients/vertical-bar-chart', function (hooks) { - setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set('chartLegend', [ - { label: 'entity clients', key: 'entity_clients' }, - { label: 'non-entity clients', key: 'non_entity_clients' }, - ]); - }); - - test('it renders chart and tooltip for total clients', async function (assert) { - const barChartData = [ - { month: 'january', clients: 141, entity_clients: 91, non_entity_clients: 50, new_clients: 5 }, - { month: 'february', clients: 251, entity_clients: 101, non_entity_clients: 150, new_clients: 5 }, - ]; - this.set('barChartData', barChartData); - - await render(hbs` -
- -
- `); - const tooltipHoverBars = findAll('[data-test-vertical-bar-chart] rect.tooltip-rect'); - assert.dom('[data-test-vertical-bar-chart]').exists('renders chart'); - assert - .dom('[data-test-vertical-chart="data-bar"]') - .exists({ count: barChartData.length * 2 }, 'renders correct number of bars'); // multiply length by 2 because bars are stacked - - assert.dom(find('[data-test-vertical-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`); - findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => { - assert.dom(e).hasText(`${barChartData[i].month}`, `renders x-axis label: ${barChartData[i].month}`); - }); - - for (const [i, bar] of tooltipHoverBars.entries()) { - await triggerEvent(bar, 'mouseover'); - const tooltip = document.querySelector('.ember-modal-dialog'); - assert - .dom(tooltip) - .includesText( - `${barChartData[i].clients} total clients ${barChartData[i].entity_clients} entity clients ${barChartData[i].non_entity_clients} non-entity clients`, - 'tooltip text is correct' - ); - } - }); - - test('it renders chart and tooltip for new clients', async function (assert) { - const barChartData = [ - { month: 'january', entity_clients: 91, non_entity_clients: 50, clients: 0 }, - { month: 'february', entity_clients: 101, non_entity_clients: 150, clients: 110 }, - ]; - this.set('barChartData', barChartData); - - await render(hbs` -
- -
- `); - - const tooltipHoverBars = findAll('[data-test-vertical-bar-chart] rect.tooltip-rect'); - assert.dom('[data-test-vertical-bar-chart]').exists('renders chart'); - assert - .dom('[data-test-vertical-chart="data-bar"]') - .exists({ count: barChartData.length * 2 }, 'renders correct number of bars'); // multiply length by 2 because bars are stacked - - assert.dom(find('[data-test-vertical-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`); - findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => { - assert.dom(e).hasText(`${barChartData[i].month}`, `renders x-axis label: ${barChartData[i].month}`); - }); - - for (const [i, bar] of tooltipHoverBars.entries()) { - await triggerEvent(bar, 'mouseover'); - const tooltip = document.querySelector('.ember-modal-dialog'); - assert - .dom(tooltip) - .includesText( - `${barChartData[i].clients} new clients ${barChartData[i].entity_clients} entity clients ${barChartData[i].non_entity_clients} non-entity clients`, - 'tooltip text is correct' - ); - } - }); - - test('it renders empty state when no dataset', async function (assert) { - await render(hbs` -
- -
- `); - - assert.dom('[data-test-component="empty-state"]').exists('renders empty state when no data'); - assert - .dom('[data-test-empty-state-subtext]') - .hasText( - `this is a custom message to explain why you're not seeing a vertical bar chart`, - 'custom message renders' - ); - }); -}); diff --git a/ui/tests/integration/components/confirm-action-test.js b/ui/tests/integration/components/confirm-action-test.js deleted file mode 100644 index 9602d62bc..000000000 --- a/ui/tests/integration/components/confirm-action-test.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; - -module('Integration | Component | confirm-action', function (hooks) { - setupRenderingTest(hooks); - - test('it renders and on click shows the correct icon', async function (assert) { - const confirmAction = sinon.spy(); - this.set('onConfirm', confirmAction); - await render(hbs` - - DELETE - - `); - assert.dom('[data-test-icon="chevron-down"]').exists('Icon is pointing down'); - await click('[data-test-confirm-action-trigger="true"]'); - assert.dom('[data-test-icon="chevron-up"]').exists('Icon is now pointing up'); - assert.dom('[data-test-confirm-action-title]').hasText('Delete this?'); - }); - - test('it closes the confirmation modal on successful delete', async function (assert) { - const confirmAction = sinon.spy(); - this.set('onConfirm', confirmAction); - await render(hbs` - - DELETE - - `); - await click('[data-test-confirm-action-trigger="true"]'); - await click('[data-test-confirm-cancel-button="true"]'); - // assert that after CANCEL the icon button is pointing down. - assert.dom('[data-test-icon="chevron-down"]').exists('Icon is pointing down after clicking cancel'); - // open the modal again to test the DELETE action - await click('[data-test-confirm-action-trigger="true"]'); - await click('[data-test-confirm-button="true"]'); - assert - .dom('[data-test-icon="chevron-down"]') - .exists('Icon is pointing down after executing the Delete action'); - assert.true(confirmAction.called, 'calls the action when Delete is pressed'); - assert - .dom('[data-test-confirm-action-title]') - .doesNotExist('it has closed the confirm content and does not show the title'); - }); -}); diff --git a/ui/tests/integration/components/confirm-test.js b/ui/tests/integration/components/confirm-test.js deleted file mode 100644 index f5406c0f7..000000000 --- a/ui/tests/integration/components/confirm-test.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; - -module('Integration | Component | Confirm', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set('id', 'foo'); - this.set('title', 'Are you sure?'); - this.set('message', 'You will not be able to recover this item later.'); - this.set('triggerText', 'Click me!'); - this.set('onConfirm', sinon.spy()); - }); - - test('it renders', async function (assert) { - await render(hbs` - - - - `); - - assert.dom('.confirm-wrapper').exists(); - assert.dom('.confirm').containsText(this.triggerText); - }); - - test('does not show the confirmation message until it is triggered', async function (assert) { - await render(hbs` - - - - `); - assert.dom('.confirm-overlay').doesNotContainText(this.message); - - await click('[data-test-confirm-action-trigger]'); - - assert.dom('.confirm-overlay').containsText(this.title); - assert.dom('.confirm-overlay').containsText(this.message); - }); - - test('it calls onConfirm when the confirm button is clicked', async function (assert) { - await render(hbs` - - - - `); - await click('[data-test-confirm-action-trigger]'); - await click('[data-test-confirm-button=true]'); - - assert.ok(this.onConfirm.calledOnce); - }); - - test('it shows only the active triggers message', async function (assert) { - await render(hbs` - - - - - `); - - await click(`[data-test-confirm-action-trigger=${this.id}]`); - assert.dom('.confirm-overlay').containsText(this.title); - assert.dom('.confirm-overlay').containsText(this.message); - - await click('[data-test-confirm-cancel-button]'); - - await click("[data-test-confirm-action-trigger='bar']"); - assert.dom('.confirm-overlay').containsText('Wow'); - assert.dom('.confirm-overlay').containsText('Bazinga!'); - }); -}); diff --git a/ui/tests/integration/components/confirmation-modal-test.js b/ui/tests/integration/components/confirmation-modal-test.js deleted file mode 100644 index f11dbd621..000000000 --- a/ui/tests/integration/components/confirmation-modal-test.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import sinon from 'sinon'; -import { click, fillIn, render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | confirmation-modal', function (hooks) { - setupRenderingTest(hooks); - - test('it renders with disabled confirmation button until input matches', async function (assert) { - const confirmAction = sinon.spy(); - const closeAction = sinon.spy(); - this.set('onConfirm', confirmAction); - this.set('onClose', closeAction); - await render(hbs` - - - `); - - assert.dom('[data-test-confirm-button]').isDisabled(); - assert.dom('[data-test-modal-div]').hasAttribute('class', 'modal is-highlight is-active'); - assert.dom('[data-test-confirm-button]').hasText('Plz Continue', 'Confirm button has specified value'); - assert - .dom('[data-test-modal-title]') - .hasStyle({ color: 'rgb(160, 125, 2)' }, 'title exists with warning header'); - await fillIn('[data-test-confirmation-modal-input="Confirmation Modal"]', 'Destructive Thing'); - assert.dom('[data-test-confirm-button="Confirmation Modal"]').isNotDisabled(); - - await click('[data-test-cancel-button]'); - assert.true(closeAction.called, 'executes passed in onClose function'); - await click('[data-test-confirm-button]'); - assert.true(confirmAction.called, 'executes passed in onConfirm function'); - }); -}); diff --git a/ui/tests/integration/components/console/log-command-test.js b/ui/tests/integration/components/console/log-command-test.js deleted file mode 100644 index c610a9446..000000000 --- a/ui/tests/integration/components/console/log-command-test.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | console/log command', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - const commandText = 'list this/path'; - this.set('content', commandText); - - await render(hbs``); - assert.dom('.console-ui-command').includesText(commandText); - }); -}); diff --git a/ui/tests/integration/components/console/log-error-test.js b/ui/tests/integration/components/console/log-error-test.js deleted file mode 100644 index 608761a8c..000000000 --- a/ui/tests/integration/components/console/log-error-test.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | console/log error', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - const errorText = 'Error deleting at: sys/foo. URL: v1/sys/foo Code: 404'; - this.set('content', errorText); - await render(hbs`{{console/log-error content=this.content}}`); - assert.dom('pre').includesText(errorText); - }); -}); diff --git a/ui/tests/integration/components/console/log-json-test.js b/ui/tests/integration/components/console/log-json-test.js deleted file mode 100644 index 3304ba50c..000000000 --- a/ui/tests/integration/components/console/log-json-test.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, find } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | console/log json', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.codeMirror = this.owner.lookup('service:code-mirror'); - }); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - const objectContent = { one: 'two', three: 'four', seven: { five: 'six' }, eight: [5, 6] }; - const expectedText = JSON.stringify(objectContent, null, 2); - - this.set('content', objectContent); - - await render(hbs`{{console/log-json content=this.content}}`); - const instance = find('[data-test-component=code-mirror-modifier]').innerText; - assert.strictEqual(instance, expectedText); - }); -}); diff --git a/ui/tests/integration/components/console/log-list-test.js b/ui/tests/integration/components/console/log-list-test.js deleted file mode 100644 index 7d5a444cc..000000000 --- a/ui/tests/integration/components/console/log-list-test.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | console/log list', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - const listContent = { keys: ['one', 'two'] }; - const expectedText = 'Keys one two'; - - this.set('content', listContent); - - await render(hbs`{{console/log-list content=this.content}}`); - - assert.dom('pre').includesText(`${expectedText}`); - }); -}); diff --git a/ui/tests/integration/components/console/log-object-test.js b/ui/tests/integration/components/console/log-object-test.js deleted file mode 100644 index c90b5473a..000000000 --- a/ui/tests/integration/components/console/log-object-test.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import { stringifyObjectValues } from 'vault/components/console/log-object'; - -module('Integration | Component | console/log object', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - const objectContent = { one: 'two', three: 'four', seven: { five: 'six' }, eight: [5, 6] }; - const data = { one: 'two', three: 'four', seven: { five: 'six' }, eight: [5, 6] }; - stringifyObjectValues(data); - const expectedText = 'Key Value one two three four seven {"five":"six"} eight [5,6]'; - this.set('content', objectContent); - - await render(hbs`{{console/log-object content=this.content}}`); - assert.dom('pre').includesText(expectedText); - }); -}); diff --git a/ui/tests/integration/components/console/log-text-test.js b/ui/tests/integration/components/console/log-text-test.js deleted file mode 100644 index 5203db513..000000000 --- a/ui/tests/integration/components/console/log-text-test.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | console/log text', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - const text = 'Success! You did a thing!'; - this.set('content', text); - - await render(hbs`{{console/log-text content=this.content}}`); - - assert.dom('pre').includesText(text); - }); -}); diff --git a/ui/tests/integration/components/console/ui-panel-test.js b/ui/tests/integration/components/console/ui-panel-test.js deleted file mode 100644 index 9275d43e9..000000000 --- a/ui/tests/integration/components/console/ui-panel-test.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, settled } from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import uiPanel from 'vault/tests/pages/components/console/ui-panel'; -import hbs from 'htmlbars-inline-precompile'; - -const component = create(uiPanel); - -module('Integration | Component | console/ui panel', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - await render(hbs`{{console/ui-panel}}`); - - assert.ok(component.hasInput); - }); - - test('it clears console input on enter', async function (assert) { - await render(hbs`{{console/ui-panel}}`); - - await component.runCommands('list this/thing/here'); - await settled(); - assert.strictEqual(component.consoleInputValue, '', 'empties input field on enter'); - }); - - test('it clears the log when using clear command', async function (assert) { - await render(hbs`{{console/ui-panel}}`); - - await component.runCommands(['list this/thing/here', 'list this/other/thing', 'read another/thing']); - await settled(); - assert.notEqual(component.logOutput, '', 'there is output in the log'); - - await component.runCommands('clear'); - await settled(); - await component.up(); - await settled(); - assert.strictEqual(component.logOutput, '', 'clears the output log'); - assert.strictEqual( - component.consoleInputValue, - 'clear', - 'populates console input with previous command on up after enter' - ); - }); - - test('it adds command to history on enter', async function (assert) { - await render(hbs`{{console/ui-panel}}`); - - await component.runCommands('list this/thing/here'); - await settled(); - await component.up(); - await settled(); - assert.strictEqual( - component.consoleInputValue, - 'list this/thing/here', - 'populates console input with previous command on up after enter' - ); - await component.down(); - await settled(); - assert.strictEqual(component.consoleInputValue, '', 'populates console input with next command on down'); - }); - - test('it cycles through history with more than one command', async function (assert) { - await render(hbs`{{console/ui-panel}}`); - - await component.runCommands(['list this/thing/here', 'read that/thing/there', 'qwerty']); - await settled(); - await component.up(); - await settled(); - assert.strictEqual( - component.consoleInputValue, - 'qwerty', - 'populates console input with previous command on up after enter' - ); - await component.up(); - await settled(); - assert.strictEqual( - component.consoleInputValue, - 'read that/thing/there', - 'populates console input with previous command on up' - ); - await component.up(); - await settled(); - assert.strictEqual( - component.consoleInputValue, - 'list this/thing/here', - 'populates console input with previous command on up' - ); - await component.up(); - await settled(); - assert.strictEqual( - component.consoleInputValue, - 'qwerty', - 'populates console input with initial command if cycled through all previous commands' - ); - await component.down(); - await settled(); - assert.strictEqual( - component.consoleInputValue, - '', - 'clears console input if down pressed after history is on most recent command' - ); - }); -}); diff --git a/ui/tests/integration/components/control-group-success-test.js b/ui/tests/integration/components/control-group-success-test.js deleted file mode 100644 index 7e9fb8256..000000000 --- a/ui/tests/integration/components/control-group-success-test.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { later, run, _cancelTimers as cancelTimers } from '@ember/runloop'; -import { resolve } from 'rsvp'; -import Service from '@ember/service'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, settled } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import { create } from 'ember-cli-page-object'; -import controlGroupSuccess from '../../pages/components/control-group-success'; - -const component = create(controlGroupSuccess); - -const controlGroupService = Service.extend({ - deleteControlGroupToken: sinon.stub(), - markTokenForUnwrap: sinon.stub(), -}); - -const storeService = Service.extend({ - adapterFor() { - return { - toolAction() { - return resolve({ data: { foo: 'bar' } }); - }, - }; - }, -}); - -module('Integration | Component | control group success', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - run(() => { - this.owner.unregister('service:store'); - this.owner.register('service:control-group', controlGroupService); - this.controlGroup = this.owner.lookup('service:control-group'); - this.owner.register('service:store', storeService); - this.router = this.owner.lookup('service:router'); - this.router.reopen({ - transitionTo: sinon.stub().returns(resolve()), - }); - }); - }); - - const MODEL = { - approved: false, - requestPath: 'foo/bar', - id: 'accessor', - requestEntity: { id: 'requestor', name: 'entity8509' }, - reload: sinon.stub(), - }; - test('render with saved token', async function (assert) { - assert.expect(3); - const response = { - uiParams: { url: '/foo' }, - token: 'token', - }; - this.set('model', MODEL); - this.set('response', response); - await render(hbs`{{control-group-success model=this.model controlGroupResponse=this.response }}`); - assert.ok(component.showsNavigateMessage, 'shows unwrap message'); - await component.navigate(); - later(() => cancelTimers(), 50); - return settled().then(() => { - assert.ok(this.controlGroup.markTokenForUnwrap.calledOnce, 'marks token for unwrap'); - assert.ok(this.router.transitionTo.calledOnce, 'calls router transition'); - }); - }); - - test('render without token', async function (assert) { - assert.expect(2); - this.set('model', MODEL); - await render(hbs`{{control-group-success model=this.model}}`); - assert.ok(component.showsUnwrapForm, 'shows unwrap form'); - await component.token('token'); - component.unwrap(); - later(() => cancelTimers(), 50); - return settled().then(() => { - assert.ok(component.showsJsonViewer, 'shows unwrapped data'); - }); - }); -}); diff --git a/ui/tests/integration/components/control-group-test.js b/ui/tests/integration/components/control-group-test.js deleted file mode 100644 index b5adc8441..000000000 --- a/ui/tests/integration/components/control-group-test.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Service from '@ember/service'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import { create } from 'ember-cli-page-object'; -import controlGroup from '../../pages/components/control-group'; - -const component = create(controlGroup); - -const controlGroupService = Service.extend({ - init() { - this._super(...arguments); - this.set('wrapInfo', null); - }, - wrapInfoForAccessor() { - return this.wrapInfo; - }, -}); - -const authService = Service.extend(); - -module('Integration | Component | control group', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:auth', authService); - this.owner.register('service:control-group', controlGroupService); - this.controlGroup = this.owner.lookup('service:controlGroup'); - this.auth = this.owner.lookup('service:auth'); - }); - - const setup = (modelData = {}, authData = {}) => { - const modelDefaults = { - approved: false, - requestPath: 'foo/bar', - id: 'accessor', - requestEntity: { id: 'requestor', name: 'entity8509' }, - reload: sinon.stub(), - }; - const authDataDefaults = { entity_id: 'requestor' }; - - return { - model: { - ...modelDefaults, - ...modelData, - }, - authData: { - ...authDataDefaults, - ...authData, - }, - }; - }; - - test('requestor rendering', async function (assert) { - const { model, authData } = setup(); - this.set('model', model); - this.set('auth.authData', authData); - await render(hbs`{{control-group model=this.model}}`); - assert.ok(component.showsAccessorCallout, 'shows accessor callout'); - assert.strictEqual(component.bannerPrefix, 'Locked'); - assert.strictEqual(component.bannerText, 'The path you requested is locked by a Control Group'); - assert.strictEqual(component.requestorText, `You are requesting access to ${model.requestPath}`); - assert.false(component.showsTokenText, 'does not show token message when there is no token'); - assert.ok(component.showsRefresh, 'shows refresh button'); - assert.ok(component.authorizationText, 'Awaiting authorization.'); - }); - - test('requestor rendering: with token', async function (assert) { - const { model, authData } = setup(); - this.set('model', model); - this.set('auth.authData', authData); - this.set('controlGroup.wrapInfo', { token: 'token' }); - await render(hbs`{{control-group model=this.model}}`); - assert.true(component.showsTokenText, 'shows token message'); - assert.strictEqual(component.token, 'token', 'shows token value'); - }); - - test('requestor rendering: some approvals', async function (assert) { - const { model, authData } = setup({ authorizations: [{ name: 'manager 1' }, { name: 'manager 2' }] }); - this.set('model', model); - this.set('auth.authData', authData); - await render(hbs`{{control-group model=this.model}}`); - assert.ok(component.authorizationText, 'Already approved by manager 1, manager 2'); - }); - - test('requestor rendering: approved with no token', async function (assert) { - const { model, authData } = setup({ approved: true }); - this.set('model', model); - this.set('auth.authData', authData); - await render(hbs`{{control-group model=this.model}}`); - - assert.strictEqual(component.bannerPrefix, 'Success!'); - assert.strictEqual(component.bannerText, 'You have been given authorization'); - assert.false(component.showsTokenText, 'does not show token message when there is no token'); - assert.notOk(component.showsRefresh, 'does not shows refresh button'); - assert.ok(component.showsSuccessComponent, 'renders control group success'); - }); - - test('requestor rendering: approved with token', async function (assert) { - const { model, authData } = setup({ approved: true }); - this.set('model', model); - this.set('auth.authData', authData); - this.set('controlGroup.wrapInfo', { token: 'token' }); - await render(hbs`{{control-group model=this.model}}`); - assert.true(component.showsTokenText, 'shows token'); - assert.notOk(component.showsRefresh, 'does not shows refresh button'); - assert.ok(component.showsSuccessComponent, 'renders control group success'); - }); - - test('authorizer rendering', async function (assert) { - const { model, authData } = setup({ canAuthorize: true }, { entity_id: 'manager' }); - - this.set('model', model); - this.set('auth.authData', authData); - await render(hbs`{{control-group model=this.model}}`); - - assert.strictEqual(component.bannerPrefix, 'Locked'); - assert.strictEqual( - component.bannerText, - 'Someone is requesting access to a path locked by a Control Group' - ); - assert.strictEqual( - component.requestorText, - `${model.requestEntity.name} is requesting access to ${model.requestPath}` - ); - assert.false(component.showsTokenText, 'does not show token message when there is no token'); - - assert.ok(component.showsAuthorize, 'shows authorize button'); - }); - - test('authorizer rendering:authorized', async function (assert) { - const { model, authData } = setup( - { canAuthorize: true, authorizations: [{ id: 'manager', name: 'manager' }] }, - { entity_id: 'manager' } - ); - - this.set('model', model); - this.set('auth.authData', authData); - await render(hbs`{{control-group model=this.model}}`); - - assert.strictEqual(component.bannerPrefix, 'Thanks!'); - assert.strictEqual(component.bannerText, 'You have given authorization'); - assert.ok(component.showsBackLink, 'back link is visible'); - }); - - test('authorizer rendering: authorized and success', async function (assert) { - const { model, authData } = setup( - { approved: true, canAuthorize: true, authorizations: [{ id: 'manager', name: 'manager' }] }, - { entity_id: 'manager' } - ); - - this.set('model', model); - this.set('auth.authData', authData); - await render(hbs`{{control-group model=this.model}}`); - - assert.strictEqual(component.bannerPrefix, 'Thanks!'); - assert.strictEqual(component.bannerText, 'You have given authorization'); - assert.ok(component.showsBackLink, 'back link is visible'); - assert.strictEqual( - component.requestorText, - `${model.requestEntity.name} is authorized to access ${model.requestPath}` - ); - assert.notOk(component.showsSuccessComponent, 'does not render control group success'); - }); - - test('third-party: success', async function (assert) { - const { model, authData } = setup( - { approved: true, canAuthorize: true, authorizations: [{ id: 'foo', name: 'foo' }] }, - { entity_id: 'manager' } - ); - - this.set('model', model); - this.set('auth.authData', authData); - await render(hbs`{{control-group model=this.model}}`); - assert.strictEqual(component.bannerPrefix, 'Success!'); - assert.strictEqual(component.bannerText, 'This Control Group has been authorized'); - assert.ok(component.showsBackLink, 'back link is visible'); - assert.notOk(component.showsSuccessComponent, 'does not render control group success'); - }); -}); diff --git a/ui/tests/integration/components/database-role-edit-test.js b/ui/tests/integration/components/database-role-edit-test.js deleted file mode 100644 index 15cfe3c74..000000000 --- a/ui/tests/integration/components/database-role-edit-test.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { capabilitiesStub } from 'vault/tests/helpers/stubs'; - -module('Integration | Component | database-role-edit', function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - this.store.pushPayload('database-role', { - modelName: 'database/role', - database: ['my-mongodb-database'], - backend: 'database', - type: 'static', - name: 'my-static-role', - id: 'my-static-role', - }); - this.store.pushPayload('database-role', { - modelName: 'database/role', - database: ['my-mongodb-database'], - backend: 'database', - type: 'dynamic', - name: 'my-dynamic-role', - id: 'my-dynamic-role', - }); - this.modelStatic = this.store.peekRecord('database/role', 'my-static-role'); - this.modelDynamic = this.store.peekRecord('database/role', 'my-dynamic-role'); - }); - - test('it should show Get credentials button when a user has the correct policy', async function (assert) { - this.server.post('/sys/capabilities-self', capabilitiesStub('database/static-creds/my-role', ['read'])); - await render(hbs``); - assert.dom('[data-test-database-role-creds="static"]').exists('Get credentials button exists'); - }); - - test('it should show Generate credentials button when a user has the correct policy', async function (assert) { - this.server.post('/sys/capabilities-self', capabilitiesStub('database/creds/my-role', ['read'])); - await render(hbs``); - assert.dom('[data-test-database-role-creds="dynamic"]').exists('Generate credentials button exists'); - }); -}); diff --git a/ui/tests/integration/components/database-role-setting-form-test.js b/ui/tests/integration/components/database-role-setting-form-test.js deleted file mode 100644 index fb624ced1..000000000 --- a/ui/tests/integration/components/database-role-setting-form-test.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import EmberObject from '@ember/object'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -const testCases = [ - { - // default case should show all possible fields for each type - pluginType: '', - staticRoleFields: ['name', 'username', 'rotation_period', 'rotation_statements'], - dynamicRoleFields: [ - 'name', - 'default_ttl', - 'max_ttl', - 'creation_statements', - 'revocation_statements', - 'rollback_statements', - 'renew_statements', - ], - }, - { - pluginType: 'elasticsearch-database-plugin', - staticRoleFields: ['username', 'rotation_period'], - dynamicRoleFields: ['creation_statement', 'default_ttl', 'max_ttl'], - }, - { - pluginType: 'mongodb-database-plugin', - staticRoleFields: ['username', 'rotation_period'], - dynamicRoleFields: ['creation_statement', 'revocation_statement', 'default_ttl', 'max_ttl'], - statementsHidden: true, - }, - { - pluginType: 'mssql-database-plugin', - staticRoleFields: ['username', 'rotation_period'], - dynamicRoleFields: ['creation_statements', 'revocation_statements', 'default_ttl', 'max_ttl'], - }, - { - pluginType: 'mysql-database-plugin', - staticRoleFields: ['username', 'rotation_period'], - dynamicRoleFields: ['creation_statements', 'revocation_statements', 'default_ttl', 'max_ttl'], - }, - { - pluginType: 'mysql-aurora-database-plugin', - staticRoleFields: ['username', 'rotation_period'], - dynamicRoleFields: ['creation_statements', 'revocation_statements', 'default_ttl', 'max_ttl'], - }, - { - pluginType: 'mysql-rds-database-plugin', - staticRoleFields: ['username', 'rotation_period'], - dynamicRoleFields: ['creation_statements', 'revocation_statements', 'default_ttl', 'max_ttl'], - }, - { - pluginType: 'mysql-legacy-database-plugin', - staticRoleFields: ['username', 'rotation_period'], - dynamicRoleFields: ['creation_statements', 'revocation_statements', 'default_ttl', 'max_ttl'], - }, - { - pluginType: 'vault-plugin-database-oracle', - staticRoleFields: ['username', 'rotation_period'], - dynamicRoleFields: ['creation_statements', 'revocation_statements', 'default_ttl', 'max_ttl'], - }, -]; - -// used to calculate checks that fields do NOT show up -const ALL_ATTRS = [ - { name: 'default_ttl', type: 'string', options: {} }, - { name: 'max_ttl', type: 'string', options: {} }, - { name: 'username', type: 'string', options: {} }, - { name: 'rotation_period', type: 'string', options: {} }, - { name: 'creation_statements', type: 'string', options: {} }, - { name: 'creation_statement', type: 'string', options: {} }, - { name: 'revocation_statements', type: 'string', options: {} }, - { name: 'revocation_statement', type: 'string', options: {} }, - { name: 'rotation_statements', type: 'string', options: {} }, - { name: 'rollback_statements', type: 'string', options: {} }, - { name: 'renew_statements', type: 'string', options: {} }, -]; -const getFields = (nameArray) => { - const show = ALL_ATTRS.filter((attr) => nameArray.indexOf(attr.name) >= 0); - const hide = ALL_ATTRS.filter((attr) => nameArray.indexOf(attr.name) < 0); - return { show, hide }; -}; - -module('Integration | Component | database-role-setting-form', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set( - 'model', - EmberObject.create({ - // attrs is not its own set value b/c ember hates arrays as args - attrs: ALL_ATTRS, - }) - ); - }); - - test('it shows empty states when no roleType passed in', async function (assert) { - await render(hbs``); - assert.dom('[data-test-component="empty-state"]').exists({ count: 2 }, 'Two empty states exist'); - }); - - test('it shows appropriate fields based on roleType and db plugin', async function (assert) { - this.set('roleType', 'static'); - this.set('dbType', ''); - await render(hbs` - - `); - assert.dom('[data-test-component="empty-state"]').doesNotExist('Does not show empty states'); - for (const testCase of testCases) { - const staticFields = getFields(testCase.staticRoleFields); - const dynamicFields = getFields(testCase.dynamicRoleFields); - this.set('dbType', testCase.pluginType); - this.set('roleType', 'static'); - staticFields.show.forEach((attr) => { - assert - .dom(`[data-test-input="${attr.name}"]`) - .exists( - `${attr.name} attribute exists on static role for ${testCase.pluginType || 'default'} db type` - ); - }); - staticFields.hide.forEach((attr) => { - assert - .dom(`[data-test-input="${attr.name}"]`) - .doesNotExist( - `${attr.name} attribute does not exist on static role for ${ - testCase.pluginType || 'default' - } db type` - ); - }); - if (testCase.statementsHidden) { - assert - .dom('[data-test-statements-section]') - .doesNotExist(`Statements section is hidden for static ${testCase.pluginType} role`); - } - this.set('roleType', 'dynamic'); - dynamicFields.show.forEach((attr) => { - assert - .dom(`[data-test-input="${attr.name}"]`) - .exists( - `${attr.name} attribute exists on dynamic role for ${testCase.pluginType || 'default'} db type` - ); - }); - dynamicFields.hide.forEach((attr) => { - assert - .dom(`[data-test-input="${attr.name}"]`) - .doesNotExist( - `${attr.name} attribute does not exist on dynamic role for ${ - testCase.pluginType || 'default' - } db type` - ); - }); - } - }); -}); diff --git a/ui/tests/integration/components/date-dropdown-test.js b/ui/tests/integration/components/date-dropdown-test.js deleted file mode 100644 index eaa8f4781..000000000 --- a/ui/tests/integration/components/date-dropdown-test.js +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { setupRenderingTest } from 'ember-qunit'; -import { click, render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters'; -import timestamp from 'core/utils/timestamp'; - -const SELECTORS = { - monthDropdown: '[data-test-popup-menu-trigger="month"]', - specificMonth: (m) => `[data-test-dropdown-month="${m}"]`, - yearDropdown: '[data-test-popup-menu-trigger="year"]', - specificYear: (y) => `[data-test-dropdown-year="${y}"]`, - - submitButton: '[data-test-date-dropdown-submit]', - cancelButton: '[data-test-date-dropdown-cancel]', - monthOptions: '[data-test-month-list] button', -}; - -module('Integration | Component | date-dropdown', function (hooks) { - setupRenderingTest(hooks); - - hooks.before(function () { - sinon.stub(timestamp, 'now').callsFake(() => new Date('2018-04-03T14:15:30')); - }); - hooks.after(function () { - timestamp.now.restore(); - }); - - test('it renders dropdown', async function (assert) { - await render(hbs` -
- -
- `); - assert.dom(SELECTORS.submitButton).hasText('Submit', 'button renders default text'); - assert.dom(SELECTORS.cancelButton).doesNotExist('it does not render cancel button by default'); - }); - - test('it fires off cancel callback', async function (assert) { - assert.expect(2); - const onCancel = () => { - assert.ok('fires onCancel callback'); - }; - this.set('onCancel', onCancel); - await render(hbs` -
- -
- `); - assert.dom(SELECTORS.submitButton).hasText('Save', 'button renders passed in text'); - await click(SELECTORS.cancelButton); - }); - - test('it renders dropdown and selects month and year', async function (assert) { - assert.expect(26); - const parentAction = (args) => { - assert.propEqual( - args, - { - dateType: 'start', - monthIdx: 1, - monthName: 'February', - year: 2016, - }, - 'sends correct args to parent' - ); - }; - this.set('parentAction', parentAction); - - await render(hbs` -
- -
- `); - assert.dom(SELECTORS.submitButton).isDisabled('button is disabled when no month or year selected'); - - await click(SELECTORS.monthDropdown); - - assert.dom(SELECTORS.monthOptions).exists({ count: 12 }, 'dropdown has 12 months'); - ARRAY_OF_MONTHS.forEach((month) => { - assert.dom(SELECTORS.specificMonth(month)).hasText(`${month}`, `dropdown includes ${month}`); - }); - - await click(SELECTORS.specificMonth('February')); - assert.dom(SELECTORS.monthDropdown).hasText('February', 'dropdown shows selected month'); - assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting month'); - - await click(SELECTORS.yearDropdown); - - assert.dom('[data-test-dropdown-year]').exists({ count: 5 }, 'dropdown has 5 years'); - for (const year of [2018, 2017, 2016, 2015, 2014]) { - assert.dom(SELECTORS.specificYear(year)).exists(); - } - - await click('[data-test-dropdown-year="2016"]'); - assert.dom(SELECTORS.yearDropdown).hasText(`2016`, `dropdown shows selected year`); - assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting year'); - assert.dom(SELECTORS.submitButton).isNotDisabled('button enabled when month and year selected'); - - await click(SELECTORS.submitButton); - }); - - test('selecting month first: current year enabled when current month selected', async function (assert) { - assert.expect(5); - await render(hbs` -
- -
- `); - // select current month - await click(SELECTORS.monthDropdown); - await click(SELECTORS.specificMonth('January')); - await click(SELECTORS.yearDropdown); - // all years should be selectable - for (const year of [2018, 2017, 2016, 2015, 2014]) { - assert.dom(SELECTORS.specificYear(year)).isNotDisabled(`year ${year} is selectable`); - } - }); - - test('selecting month first: it disables current year when future months selected', async function (assert) { - assert.expect(5); - await render(hbs` -
- -
- `); - - // select future month - await click(SELECTORS.monthDropdown); - await click(SELECTORS.specificMonth('June')); - await click(SELECTORS.yearDropdown); - - assert.dom(SELECTORS.specificYear(2018)).isDisabled(`current year is disabled`); - // previous years should be selectable - for (const year of [2017, 2016, 2015, 2014]) { - assert.dom(SELECTORS.specificYear(year)).isNotDisabled(`year ${year} is selectable`); - } - }); - - test('selecting year first: it disables future months when current year selected', async function (assert) { - assert.expect(12); - await render(hbs` -
- -
- `); - await click(SELECTORS.yearDropdown); - await click(SELECTORS.specificYear(2018)); - await click(SELECTORS.monthDropdown); - - const expectedSelectable = ['January', 'February', 'March', 'April']; - ARRAY_OF_MONTHS.forEach((month) => { - if (expectedSelectable.includes(month)) { - assert.dom(SELECTORS.specificMonth(month)).isNotDisabled(`${month} is selectable for current year`); - } else { - assert.dom(SELECTORS.specificMonth(month)).isDisabled(`${month} is disabled for current year`); - } - }); - }); - - test('selecting year first: it enables all months when past year is selected', async function (assert) { - assert.expect(12); - await render(hbs` -
- -
- `); - - await click(SELECTORS.yearDropdown); - await click(SELECTORS.specificYear(2017)); - await click(SELECTORS.monthDropdown); - - ARRAY_OF_MONTHS.forEach((month) => { - assert.dom(SELECTORS.specificMonth(month)).isNotDisabled(`${month} is selectable for previous year`); - }); - }); -}); diff --git a/ui/tests/integration/components/diff-version-selector-test.js b/ui/tests/integration/components/diff-version-selector-test.js deleted file mode 100644 index 82f126d08..000000000 --- a/ui/tests/integration/components/diff-version-selector-test.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import EmberObject from '@ember/object'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -const VERSIONS = [ - { - version: 2, - }, - { - version: 1, - }, -]; - -module('Integration | Component | diff-version-selector', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - this.set( - 'model', - EmberObject.create({ - currentVersion: 2, - versions: VERSIONS, - }) - ); - await render(hbs``); - const leftSideVersion = document - .querySelector('[data-test-popup-menu-trigger="left-version"]') - .innerText.trim(); - assert.strictEqual(leftSideVersion, 'Version 2', 'left side toolbar defaults to currentVersion'); - - await click('[data-test-popup-menu-trigger="left-version"]'); - - assert.dom('[data-test-leftSide-version="1"]').exists('leftside shows both versions'); - assert.dom('[data-test-leftSide-version="2"]').exists('leftside shows both versions'); - }); -}); diff --git a/ui/tests/integration/components/download-button-test.js b/ui/tests/integration/components/download-button-test.js deleted file mode 100644 index 7dff1d21b..000000000 --- a/ui/tests/integration/components/download-button-test.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { click, render, resetOnerror, setupOnerror } from '@ember/test-helpers'; -import { isPresent } from 'ember-cli-page-object'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; - -const SELECTORS = { - button: '[data-test-download-button]', - icon: '[data-test-icon="download"]', -}; -module('Integration | Component | download button', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - const downloadService = this.owner.lookup('service:download'); - this.downloadSpy = sinon.stub(downloadService, 'miscExtension'); - - this.data = 'my data to download'; - this.filename = 'my special file'; - this.extension = 'csv'; - }); - - test('it renders', async function (assert) { - await render(hbs` - - - Download - - `); - assert.dom(SELECTORS.button).hasClass('button'); - assert.ok(isPresent(SELECTORS.icon), 'renders yielded icon'); - assert.dom(SELECTORS.button).hasTextContaining('Download', 'renders yielded text'); - }); - - test('it downloads with defaults when only passed @data arg', async function (assert) { - assert.expect(3); - - await render(hbs` - - Download - - `); - await click(SELECTORS.button); - const [filename, content, extension] = this.downloadSpy.getCall(0).args; - assert.ok(filename.includes('Z'), 'filename defaults to ISO string'); - assert.strictEqual(content, this.data, 'called with correct data'); - assert.strictEqual(extension, 'txt', 'called with default extension'); - }); - - test('it calls download service with passed in args', async function (assert) { - assert.expect(3); - - await render(hbs` - - Download - - `); - - await click(SELECTORS.button); - const [filename, content, extension] = this.downloadSpy.getCall(0).args; - assert.ok(filename.includes(`${this.filename}-`), 'filename added to ISO string'); - assert.strictEqual(content, this.data, 'called with correct data'); - assert.strictEqual(extension, this.extension, 'called with passed in extension'); - }); - - test('it sets download content with arg passed to fetchData', async function (assert) { - assert.expect(3); - this.fetchData = () => 'this is fetched data from a parent function'; - await render(hbs` - - Download - - `); - - await click(SELECTORS.button); - const [filename, content, extension] = this.downloadSpy.getCall(0).args; - assert.ok(filename.includes('Z'), 'filename defaults to ISO string'); - assert.strictEqual(content, this.fetchData(), 'called with fetched data'); - assert.strictEqual(extension, 'txt', 'called with default extension'); - }); - - test('it throws error when both data and fetchData are passed as args', async function (assert) { - assert.expect(1); - setupOnerror((error) => { - assert.strictEqual( - error.message, - 'Assertion Failed: Only pass either @data or @fetchData, passing both means @data will be overwritten by the return value of @fetchData', - 'throws error with incorrect args' - ); - }); - this.fetchData = () => 'this is fetched data from a parent function'; - await render(hbs` - - `); - resetOnerror(); - }); -}); diff --git a/ui/tests/integration/components/edit-form-kmip-role-test.js b/ui/tests/integration/components/edit-form-kmip-role-test.js deleted file mode 100644 index a949ca2e8..000000000 --- a/ui/tests/integration/components/edit-form-kmip-role-test.js +++ /dev/null @@ -1,238 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { later, run, _cancelTimers as cancelTimers } from '@ember/runloop'; -import { resolve } from 'rsvp'; -import EmberObject, { computed } from '@ember/object'; -import Service from '@ember/service'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { click, render, settled } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import { setupEngine } from 'ember-engines/test-support'; -import { COMPUTEDS } from 'vault/models/kmip/role'; - -const flash = Service.extend({ - success: sinon.stub(), -}); -const namespace = Service.extend({}); - -const fieldToCheckbox = (field) => ({ name: field, type: 'boolean' }); - -const createModel = (options) => { - const model = EmberObject.extend(COMPUTEDS, { - /* eslint-disable ember/avoid-leaking-state-in-ember-objects */ - newFields: [ - 'role', - 'operationActivate', - 'operationAddAttribute', - 'operationAll', - 'operationCreate', - 'operationDestroy', - 'operationDiscoverVersion', - 'operationGet', - 'operationGetAttributes', - 'operationLocate', - 'operationNone', - 'operationRekey', - 'operationRevoke', - 'tlsClientKeyBits', - 'tlsClientKeyType', - 'tlsClientTtl', - ], - fields: computed('operationFields', function () { - return this.operationFields.map(fieldToCheckbox); - }), - destroyRecord() { - return resolve(); - }, - save() { - return resolve(); - }, - rollbackAttributes() {}, - }); - return model.create({ - ...options, - }); -}; - -module('Integration | Component | edit form kmip role', function (hooks) { - setupRenderingTest(hooks); - setupEngine(hooks, 'kmip'); - - hooks.beforeEach(function () { - this.context = { owner: this.engine }; // this.engine set by setupEngine - run(() => { - this.engine.unregister('service:flash-messages'); - this.engine.register('service:flash-messages', flash); - this.engine.register('service:namespace', namespace); - }); - }); - - test('it renders: new model', async function (assert) { - assert.expect(3); - const model = createModel({ isNew: true }); - this.set('model', model); - this.onSave = ({ model }) => { - assert.false(model.operationNone, 'callback fires with operationNone as false'); - assert.true(model.operationAll, 'callback fires with operationAll as true'); - }; - await render(hbs``, this.context); - - assert.dom('[data-test-input="operationAll"]').isChecked('sets operationAll'); - await click('[data-test-edit-form-submit]'); - }); - - test('it renders: operationAll', async function (assert) { - assert.expect(3); - const model = createModel({ operationAll: true }); - this.set('model', model); - this.onSave = ({ model }) => { - assert.false(model.operationNone, 'callback fires with operationNone as false'); - assert.true(model.operationAll, 'callback fires with operationAll as true'); - }; - await render(hbs``, this.context); - assert.dom('[data-test-input="operationAll"]').isChecked('sets operationAll'); - await click('[data-test-edit-form-submit]'); - }); - - test('it renders: operationNone', async function (assert) { - assert.expect(2); - const model = createModel({ operationNone: true, operationAll: undefined }); - this.set('model', model); - - this.onSave = ({ model }) => { - assert.true(model.operationNone, 'callback fires with operationNone as true'); - }; - await render(hbs``, this.context); - assert.dom('[data-test-input="operationNone"]').isNotChecked('sets operationNone'); - await click('[data-test-edit-form-submit]'); - }); - - test('it renders: choose operations', async function (assert) { - assert.expect(3); - const model = createModel({ operationGet: true }); - this.set('model', model); - this.onSave = ({ model }) => { - assert.false(model.operationNone, 'callback fires with operationNone as false'); - }; - await render(hbs``, this.context); - - assert.dom('[data-test-input="operationNone"]').isChecked('sets operationNone'); - assert.dom('[data-test-input="operationAll"]').isNotChecked('sets operationAll'); - await click('[data-test-edit-form-submit]'); - }); - - test('it saves operationNone=true when unchecking operationAll box', async function (assert) { - assert.expect(15); - const model = createModel({ isNew: true }); - this.set('model', model); - this.onSave = ({ model }) => { - assert.true(model.operationNone, 'callback fires with operationNone as true'); - assert.false(model.operationAll, 'callback fires with operationAll as false'); - }; - - await render(hbs``, this.context); - await click('[data-test-input="operationAll"]'); - for (const field of model.fields) { - const { name } = field; - if (name === 'operationNone') continue; - assert.dom(`[data-test-input="${name}"]`).isNotChecked(`${name} is unchecked`); - } - - assert.dom('[data-test-input="operationAll"]').isNotChecked('sets operationAll'); - assert - .dom('[data-test-input="operationNone"]') - .isChecked('operationNone toggle is true which means allow operations'); - await click('[data-test-edit-form-submit]'); - }); - - const savingTests = [ - [ - 'setting operationAll', - { operationNone: true, operationGet: true }, - 'operationNone', - { - operationAll: true, - operationNone: false, - operationGet: true, - }, - { - operationGet: null, - operationNone: false, - }, - 5, - ], - [ - 'setting operationNone', - { operationAll: true, operationCreate: true }, - 'operationNone', - { - operationAll: false, - operationNone: true, - operationCreate: true, - }, - { - operationNone: true, - operationCreate: null, - operationAll: false, - }, - 6, - ], - - [ - 'setting choose, and selecting an additional item', - { operationAll: true, operationGet: true, operationCreate: true }, - 'operationAll,operationDestroy', - { - operationAll: false, - operationCreate: true, - operationGet: true, - }, - { - operationGet: true, - operationCreate: true, - operationDestroy: true, - operationAll: false, - }, - 7, - ], - ]; - for (const testCase of savingTests) { - const [name, initialState, displayClicks, stateBeforeSave, stateAfterSave, assertionCount] = testCase; - test(name, async function (assert) { - assert.expect(assertionCount); - const model = createModel(initialState); - this.set('model', model); - const clickTargets = displayClicks.split(','); - await render(hbs``, this.context); - - for (const clickTarget of clickTargets) { - await click(`label[for=${clickTarget}]`); - } - for (const beforeStateKey of Object.keys(stateBeforeSave)) { - assert.strictEqual( - model.get(beforeStateKey), - stateBeforeSave[beforeStateKey], - `sets ${beforeStateKey}` - ); - } - - click('[data-test-edit-form-submit]'); - - later(() => cancelTimers(), 50); - return settled().then(() => { - for (const afterStateKey of Object.keys(stateAfterSave)) { - assert.strictEqual( - model.get(afterStateKey), - stateAfterSave[afterStateKey], - `sets ${afterStateKey} on save` - ); - } - }); - }); - } -}); diff --git a/ui/tests/integration/components/edit-form-test.js b/ui/tests/integration/components/edit-form-test.js deleted file mode 100644 index ee4f9ebf7..000000000 --- a/ui/tests/integration/components/edit-form-test.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { later, run, _cancelTimers as cancelTimers } from '@ember/runloop'; -import { resolve } from 'rsvp'; -import EmberObject from '@ember/object'; -import Service from '@ember/service'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, settled } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import { create } from 'ember-cli-page-object'; -import editForm from 'vault/tests/pages/components/edit-form'; - -const component = create(editForm); - -const flash = Service.extend({ - success: sinon.stub(), -}); - -const createModel = (canDelete = true) => { - return EmberObject.create({ - fields: [ - { name: 'one', type: 'string' }, - { name: 'two', type: 'boolean' }, - ], - canDelete, - destroyRecord() { - return resolve(); - }, - save() { - return resolve(); - }, - rollbackAttributes() {}, - }); -}; - -module('Integration | Component | edit form', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - run(() => { - this.owner.unregister('service:flash-messages'); - this.owner.register('service:flash-messages', flash); - }); - }); - - test('it renders', async function (assert) { - this.set('model', createModel()); - await render(hbs`{{edit-form model=this.model}}`); - - assert.ok(component.fields.length, 2); - }); - - test('it calls flash message fns on save', async function (assert) { - assert.expect(4); - const onSave = () => { - return resolve(); - }; - this.set('model', createModel()); - this.set('onSave', onSave); - const saveSpy = sinon.spy(this, 'onSave'); - - await render(hbs`{{edit-form model=this.model onSave=this.onSave}}`); - - component.submit(); - later(() => cancelTimers(), 50); - return settled().then(() => { - assert.ok(saveSpy.calledOnce, 'calls passed onSave'); - assert.strictEqual(saveSpy.getCall(0).args[0].saveType, 'save'); - assert.deepEqual(saveSpy.getCall(0).args[0].model, this.model, 'passes model to onSave'); - const flash = this.owner.lookup('service:flash-messages'); - assert.strictEqual(flash.success.callCount, 1, 'calls flash message success'); - }); - }); -}); diff --git a/ui/tests/integration/components/empty-state-test.js b/ui/tests/integration/components/empty-state-test.js deleted file mode 100644 index ab367522a..000000000 --- a/ui/tests/integration/components/empty-state-test.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | empty-state', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); - - await render(hbs`{{empty-state}}`); - - assert.dom(this.element).hasText(''); - - // Template block usage: - await render(hbs` - {{#empty-state - title="Empty State Title" - message="This is the empty state message" - }} - Actions Link - {{/empty-state}} - `); - - assert.dom('.empty-state-title').hasText('Empty State Title', 'renders empty state title'); - assert - .dom('.empty-state-message') - .hasText('This is the empty state message', 'renders empty state message'); - assert.dom('.empty-state-actions').hasText('Actions Link', 'renders empty state actions'); - }); -}); diff --git a/ui/tests/integration/components/features-selection-test.js b/ui/tests/integration/components/features-selection-test.js deleted file mode 100644 index ca49cd68e..000000000 --- a/ui/tests/integration/components/features-selection-test.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import featuresSelection from 'vault/tests/pages/components/wizard/features-selection'; -import hbs from 'htmlbars-inline-precompile'; -import Service from '@ember/service'; - -const component = create(featuresSelection); - -const permissionsService = Service.extend({ - hasPermission(path) { - // This enables the Secrets and Authentication wizard items and disables the others. - const allowedPaths = ['sys/mounts/example', 'sys/auth', 'sys/auth/foo', 'sys/wrapping/wrap']; - if (allowedPaths.includes(path)) { - return true; - } - return false; - }, -}); - -module('Integration | Component | features-selection', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:permissions', permissionsService); - }); - - test('it disables and enables wizard items according to user permissions', async function (assert) { - assert.expect(4); - const enabled = { Secrets: true, Authentication: true, Policies: false, Tools: false }; - await render(hbs``); - - component.wizardItems.forEach((i) => { - assert.strictEqual( - i.hasDisabledTooltip, - !enabled[i.text], - 'shows a tooltip only when the wizard item is not enabled' - ); - }); - }); - - test('it disables the start button if no wizard items are checked', async function (assert) { - await render(hbs``); - assert.true(component.hasDisabledStartButton); - }); - - test('it enables the start button when user has permission and wizard items are checked', async function (assert) { - await render(hbs``); - await component.selectSecrets(); - assert.false(component.hasDisabledStartButton); - }); -}); diff --git a/ui/tests/integration/components/form-error-test.js b/ui/tests/integration/components/form-error-test.js deleted file mode 100644 index 4ce3fa573..000000000 --- a/ui/tests/integration/components/form-error-test.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | form-error', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); - - await render(hbs`{{form-error}}`); - - assert.dom(this.element).hasText(''); - - // Template block usage: - await render(hbs` - {{#form-error}} - template block text - {{/form-error}} - `); - - assert.dom(this.element).hasText('template block text'); - }); -}); diff --git a/ui/tests/integration/components/form-field-label-test.js b/ui/tests/integration/components/form-field-label-test.js deleted file mode 100644 index d254227ee..000000000 --- a/ui/tests/integration/components/form-field-label-test.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { click } from '@ember/test-helpers'; - -module('Integration | Component | form-field-label', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - this.setProperties({ - label: 'Test Label', - helpText: null, - subText: null, - docLink: null, - }); - - await render(hbs` - - `); - - assert.dom('label').hasAttribute('for', 'some-input', 'Attributes passed to label element'); - assert.dom('label').hasText(this.label, 'Label text renders'); - assert.dom('[data-test-help-text]').doesNotExist('Help text hidden when not provided'); - assert.dom('.sub-text').doesNotExist('Sub text hidden when not provided'); - this.setProperties({ - helpText: 'More info', - subText: 'Some description', - }); - await click('[data-test-tool-tip-trigger]'); - assert.dom('[data-test-help-text]').hasText(this.helpText, 'Help text renders in tooltip'); - assert.dom('.sub-text').hasText(this.subText, 'Sub text renders'); - assert.dom('a').doesNotExist('docLink hidden when not provided'); - this.set('docLink', '/doc/path'); - assert.dom('.sub-text').includesText('See our documentation for help', 'Doc link text renders'); - assert - .dom('a') - .hasAttribute('href', 'https://developer.hashicorp.com' + this.docLink, 'Doc link renders'); - }); -}); diff --git a/ui/tests/integration/components/form-field-test.js b/ui/tests/integration/components/form-field-test.js deleted file mode 100644 index 0848251fc..000000000 --- a/ui/tests/integration/components/form-field-test.js +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import EmberObject from '@ember/object'; -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click, fillIn } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import { create } from 'ember-cli-page-object'; -import sinon from 'sinon'; -import formFields from '../../pages/components/form-field'; - -const component = create(formFields); - -module('Integration | Component | form field', function (hooks) { - setupRenderingTest(hooks); - - const createAttr = (name, type, options) => { - return { - name, - type, - options, - }; - }; - - const setup = async function (attr) { - // ember sets model attrs from the defaultValue key, mimicking that behavior here - const model = EmberObject.create({ [attr.name]: attr.options?.defaultValue }); - const spy = sinon.spy(); - this.set('onChange', spy); - this.set('model', model); - this.set('attr', attr); - await render(hbs``); - return [model, spy]; - }; - - test('it renders', async function (assert) { - const model = EmberObject.create({}); - this.attr = { name: 'foo' }; - this.model = model; - await render(hbs``); - assert.strictEqual(component.fields.objectAt(0).labelText[0], 'Foo', 'renders a label'); - assert.notOk(component.hasInput, 'renders only the label'); - }); - - test('it renders: string', async function (assert) { - const [model, spy] = await setup.call(this, createAttr('foo', 'string', { defaultValue: 'default' })); - assert.strictEqual(component.fields.objectAt(0).labelText[0], 'Foo', 'renders a label'); - assert.strictEqual(component.fields.objectAt(0).inputValue, 'default', 'renders default value'); - assert.ok(component.hasInput, 'renders input for string'); - await component.fields.objectAt(0).input('bar').change(); - - assert.strictEqual(model.get('foo'), 'bar'); - assert.ok(spy.calledWith('foo', 'bar'), 'onChange called with correct args'); - }); - - test('it renders: boolean', async function (assert) { - const [model, spy] = await setup.call(this, createAttr('foo', 'boolean', { defaultValue: false })); - assert.strictEqual(component.fields.objectAt(0).labelText[0], 'Foo', 'renders a label'); - assert.notOk(component.fields.objectAt(0).inputChecked, 'renders default value'); - assert.ok(component.hasCheckbox, 'renders a checkbox for boolean'); - await component.fields.objectAt(0).clickLabel(); - - assert.true(model.get('foo')); - assert.ok(spy.calledWith('foo', true), 'onChange called with correct args'); - }); - - test('it renders: number', async function (assert) { - const [model, spy] = await setup.call(this, createAttr('foo', 'number', { defaultValue: 5 })); - assert.strictEqual(component.fields.objectAt(0).labelText[0], 'Foo', 'renders a label'); - assert.strictEqual(component.fields.objectAt(0).inputValue, '5', 'renders default value'); - assert.ok(component.hasInput, 'renders input for number'); - await component.fields.objectAt(0).input(8).change(); - - assert.strictEqual(model.get('foo'), '8'); - assert.ok(spy.calledWith('foo', '8'), 'onChange called with correct args'); - }); - - test('it renders: object', async function (assert) { - await setup.call(this, createAttr('foo', 'object')); - assert.strictEqual(component.fields.objectAt(0).labelText[0], 'Foo', 'renders a label'); - assert.ok(component.hasJSONEditor, 'renders the json editor'); - }); - - test('it renders: string as json with clear button', async function (assert) { - await setup.call(this, createAttr('foo', 'string', { editType: 'json', allowReset: true })); - assert.strictEqual(component.fields.objectAt(0).labelText[0], 'Foo', 'renders a label'); - assert.ok(component.hasJSONEditor, 'renders the json editor'); - assert.ok(component.hasJSONClearButton, 'renders button that will clear the JSON value'); - }); - - test('it renders: editType textarea', async function (assert) { - const [model, spy] = await setup.call( - this, - createAttr('foo', 'string', { defaultValue: 'goodbye', editType: 'textarea' }) - ); - assert.strictEqual(component.fields.objectAt(0).labelText[0], 'Foo', 'renders a label'); - assert.ok(component.hasTextarea, 'renders a textarea'); - assert.strictEqual(component.fields.objectAt(0).textareaValue, 'goodbye', 'renders default value'); - await component.fields.objectAt(0).textarea('hello'); - - assert.strictEqual(model.get('foo'), 'hello'); - assert.ok(spy.calledWith('foo', 'hello'), 'onChange called with correct args'); - }); - - test('it renders: editType file', async function (assert) { - await setup.call(this, createAttr('foo', 'string', { editType: 'file' })); - assert.ok(component.hasTextFile, 'renders the text-file component'); - await click('[data-test-text-toggle]'); - await fillIn('[data-test-text-file-textarea]', 'hello world'); - assert.dom('[data-test-text-file-textarea]').hasClass('masked-font'); - await click('[data-test-button="toggle-masked"]'); - assert.dom('[data-test-text-file-textarea]').doesNotHaveClass('masked-font'); - }); - - test('it renders: editType ttl', async function (assert) { - const [model, spy] = await setup.call(this, createAttr('foo', null, { editType: 'ttl' })); - assert.ok(component.hasTTLPicker, 'renders the ttl-picker component'); - await component.fields.objectAt(0).toggleTtl(); - await component.fields.objectAt(0).select('h').change(); - await component.fields.objectAt(0).ttlTime('3'); - const expectedSeconds = `${3 * 3600}s`; - assert.strictEqual(model.get('foo'), expectedSeconds); - assert.ok(spy.calledWith('foo', expectedSeconds), 'onChange called with correct args'); - }); - - test('it renders: editType ttl without toggle', async function (assert) { - const [model, spy] = await setup.call( - this, - createAttr('foo', null, { editType: 'ttl', hideToggle: true }) - ); - await component.fields.objectAt(0).select('h').change(); - await component.fields.objectAt(0).ttlTime('3'); - const expectedSeconds = `${3 * 3600}s`; - assert.strictEqual(model.get('foo'), expectedSeconds); - assert.ok(spy.calledWith('foo', expectedSeconds), 'onChange called with correct args'); - }); - - test('it renders: radio buttons for possible values', async function (assert) { - const [model, spy] = await setup.call( - this, - createAttr('foo', null, { editType: 'radio', possibleValues: ['SHA1', 'SHA256'] }) - ); - assert.ok(component.hasRadio, 'renders radio buttons'); - const selectedValue = 'SHA256'; - await component.selectRadioInput(selectedValue); - assert.strictEqual(model.get('foo'), selectedValue); - assert.ok(spy.calledWith('foo', selectedValue), 'onChange called with correct args'); - }); - - test('it renders: editType stringArray', async function (assert) { - const [model, spy] = await setup.call(this, createAttr('foo', 'string', { editType: 'stringArray' })); - assert.ok(component.hasStringList, 'renders the string-list component'); - - await component.fields.objectAt(0).textarea('array').change(); - assert.deepEqual(model.get('foo'), ['array'], 'sets the value on the model'); - assert.deepEqual(spy.args[0], ['foo', ['array']], 'onChange called with correct args'); - }); - - test('it renders: sensitive', async function (assert) { - const [model, spy] = await setup.call(this, createAttr('password', 'string', { sensitive: true })); - assert.ok(component.hasMaskedInput, 'renders the masked-input component'); - await component.fields.objectAt(0).textarea('secret'); - assert.strictEqual(model.get('password'), 'secret'); - assert.ok(spy.calledWith('password', 'secret'), 'onChange called with correct args'); - }); - - test('it uses a passed label', async function (assert) { - await setup.call(this, createAttr('foo', 'string', { label: 'Not Foo' })); - assert.strictEqual( - component.fields.objectAt(0).labelText[0], - 'Not Foo', - 'renders the label from options' - ); - }); - - test('it renders a help tooltip', async function (assert) { - await setup.call(this, createAttr('foo', 'string', { helpText: 'Here is some help text' })); - await component.tooltipTrigger(); - assert.ok(component.hasTooltip, 'renders the tooltip component'); - }); - - test('it should not expand and toggle ttl when default 0s value is present', async function (assert) { - assert.expect(2); - - this.setProperties({ - model: EmberObject.create({ foo: '0s' }), - attr: createAttr('foo', null, { editType: 'ttl' }), - onChange: () => {}, - }); - - await render(hbs``); - assert - .dom('[data-test-toggle-input="Foo"]') - .isNotChecked('Toggle is initially unchecked when given default value'); - assert.dom('[data-test-ttl-picker-group="Foo"]').doesNotExist('Ttl input is hidden'); - }); - - test('it should toggle and expand ttl when initial non default value is provided', async function (assert) { - assert.expect(2); - - this.setProperties({ - model: EmberObject.create({ foo: '1s' }), - attr: createAttr('foo', null, { editType: 'ttl' }), - onChange: () => {}, - }); - - await render(hbs``); - assert.dom('[data-test-toggle-input="Foo"]').isChecked('Toggle is initially checked when given value'); - assert.dom('[data-test-ttl-value="Foo"]').hasValue('1', 'Ttl input displays with correct value'); - }); - - test('it should show validation warning', async function (assert) { - const model = this.owner.lookup('service:store').createRecord('auth-method'); - model.path = 'foo bar'; - this.validations = model.validate().state; - this.setProperties({ - model, - attr: createAttr('path', 'string'), - onChange: () => {}, - }); - - await render( - hbs`` - ); - assert.dom('[data-test-validation-warning]').exists('Validation warning renders'); - }); -}); diff --git a/ui/tests/integration/components/get-credentials-card-test.js b/ui/tests/integration/components/get-credentials-card-test.js deleted file mode 100644 index 82a128f9e..000000000 --- a/ui/tests/integration/components/get-credentials-card-test.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import Service from '@ember/service'; -import { click, find, render, typeIn } from '@ember/test-helpers'; -import { selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; - -const TITLE = 'Get Credentials'; -const SEARCH_LABEL = 'Role to use'; - -const storeService = Service.extend({ - query(modelType) { - return new Promise((resolve, reject) => { - switch (modelType) { - case 'database/role': - resolve([{ id: 'my-role', backend: 'database' }]); - break; - default: - reject({ httpStatus: 404, message: 'not found' }); - break; - } - reject({ httpStatus: 404, message: 'not found' }); - }); - }, -}); - -module('Integration | Component | get-credentials-card', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.router = this.owner.lookup('service:router'); - this.router.transitionTo = sinon.stub(); - - this.owner.unregister('service:store'); - this.owner.register('service:store', storeService); - this.set('title', TITLE); - this.set('searchLabel', SEARCH_LABEL); - }); - - hooks.afterEach(function () { - this.router.transitionTo.reset(); - }); - - test('it shows a disabled button when no item is selected', async function (assert) { - await render(hbs``); - assert.dom('[data-test-get-credentials]').isDisabled(); - assert.dom('[data-test-get-credentials]').hasText('Get credentials', 'Button has default text'); - }); - - test('it shows button that can be clicked to credentials route when an item is selected', async function (assert) { - const models = ['database/role']; - this.set('models', models); - await render( - hbs`` - ); - assert - .dom('[data-test-component="search-select"]#search-input-role') - .exists('renders search select component by default'); - assert - .dom('[data-test-component="search-select"]#search-input-role') - .hasText('Search for a role...', 'renders placeholder text passed to search select'); - await clickTrigger(); - await selectChoose('', 'my-role'); - assert.dom('[data-test-get-credentials]').isEnabled(); - await click('[data-test-get-credentials]'); - assert.propEqual( - this.router.transitionTo.lastCall.args, - ['vault.cluster.secrets.backend.credentials', 'my-role'], - 'transitionTo is called with correct route and role name' - ); - }); - - test('it renders input search field when renderInputSearch=true and shows placeholder text', async function (assert) { - await render( - hbs`` - ); - assert - .dom('[data-test-component="search-select"]') - .doesNotExist('does not render search select component'); - assert.strictEqual( - find('[data-test-search-roles] input').placeholder, - 'secret/', - 'renders placeholder text passed to search input' - ); - await typeIn('[data-test-search-roles] input', 'test'); - assert.dom('[data-test-get-credentials]').isEnabled('submit button enables after typing input text'); - assert.dom('[data-test-get-credentials]').hasText('View secret', 'Button has view secret CTA'); - await click('[data-test-get-credentials]'); - assert.propEqual( - this.router.transitionTo.lastCall.args, - ['vault.cluster.secrets.backend.show', 'test'], - 'transitionTo is called with correct route and secret name' - ); - }); - - test('it prefills input if initialValue has value', async function (assert) { - await render( - hbs`` - ); - assert - .dom('[data-test-component="search-select"]') - .doesNotExist('does not render search select component'); - assert.dom('[data-test-search-roles] input').hasValue('hello/', 'pre-fills search input'); - assert.dom('[data-test-get-credentials]').isEnabled('submit button is enabled at render'); - assert.dom('[data-test-get-credentials]').hasText('View list', 'Button has list CTA'); - await typeIn('[data-test-search-roles] input', 'test'); - assert - .dom('[data-test-get-credentials]') - .hasText('View secret', 'Button has view secret CTA after input'); - await click('[data-test-get-credentials]'); - assert.propEqual( - this.router.transitionTo.lastCall.args, - ['vault.cluster.secrets.backend.show', 'hello/test'], - 'transitionTo is called with correct route and secret name' - ); - }); - - test('it goes to list route if input ends in / and type=secret', async function (assert) { - await render( - hbs`` - ); - assert - .dom('[data-test-component="search-select"]') - .doesNotExist('does not render search select component'); - await typeIn('[data-test-search-roles] input', 'test/'); - assert.dom('[data-test-get-credentials]').hasText('View list', 'submit button has list CTA'); - assert.dom('[data-test-get-credentials]').isEnabled('submit button is enabled at render'); - await click('[data-test-get-credentials]'); - assert.propEqual( - this.router.transitionTo.lastCall.args, - ['vault.cluster.secrets.backend.list', 'test/'], - 'transitionTo is called with correct route and secret name' - ); - }); -}); diff --git a/ui/tests/integration/components/hover-copy-button-test.js b/ui/tests/integration/components/hover-copy-button-test.js deleted file mode 100644 index 818987890..000000000 --- a/ui/tests/integration/components/hover-copy-button-test.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, settled } from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import hbs from 'htmlbars-inline-precompile'; -import copyButton from 'vault/tests/pages/components/hover-copy-button'; -const component = create(copyButton); - -module('Integration | Component | hover copy button', function (hooks) { - setupRenderingTest(hooks); - - // ember-cli-clipboard helpers don't like the new style - test('it shows success message in tooltip', async function (assert) { - await render(hbs` -
- -
- `); - await component.focusContainer(); - await settled(); - assert.ok(component.buttonIsVisible); - await component.mouseEnter(); - await settled(); - assert.strictEqual(component.tooltipText, 'Copy', 'shows copy'); - }); - - test('it has the correct class when alwaysShow is true', async function (assert) { - await render(hbs` - - `); - await render(hbs`{{hover-copy-button alwaysShow=true copyValue=this.copyValue}}`); - assert.ok(component.buttonIsVisible); - assert.ok(component.wrapperClass.includes('hover-copy-button-static')); - }); -}); diff --git a/ui/tests/integration/components/icon-test.js b/ui/tests/integration/components/icon-test.js deleted file mode 100644 index 53263f861..000000000 --- a/ui/tests/integration/components/icon-test.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import waitForError from 'vault/tests/helpers/wait-for-error'; - -module('Integration | Component | icon', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - await render(hbs``); - assert.dom('.i-con').exists('renders'); - - await render(hbs``); - assert.dom('.vault-logo').exists('inlines the SVG'); - assert.dom('.hs-icon').hasClass('hs-icon-l', 'Default hs class applied'); - - await render(hbs`