From 631bbc31ed4aae7ef5da31de110a5577394df4ce Mon Sep 17 00:00:00 2001 From: sbene Date: Wed, 4 Sep 2024 20:15:19 +0200 Subject: [PATCH] feat: Enable multiple token caching and add option to disable caching completely Signed-off-by: sbene --- cmd/generate.go | 3 + cmd/generate_test.go | 50 ++++++++++++++ pkg/auth/vault/approle.go | 4 +- pkg/auth/vault/approle_test.go | 26 +++++++- pkg/auth/vault/github.go | 4 +- pkg/auth/vault/github_test.go | 4 +- pkg/auth/vault/kubernetes.go | 4 +- pkg/auth/vault/kubernetes_test.go | 4 +- pkg/auth/vault/userpass.go | 4 +- pkg/auth/vault/userpass_test.go | 26 +++++++- pkg/utils/util.go | 106 ++++++++++++++++++------------ pkg/utils/utils_test.go | 19 +++--- 12 files changed, 187 insertions(+), 67 deletions(-) diff --git a/cmd/generate.go b/cmd/generate.go index e6f63c10..a6b1845d 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -21,6 +21,7 @@ func NewGenerateCommand() *cobra.Command { const StdIn = "-" var configPath, secretName string var verboseOutput bool + var disableCache bool var command = &cobra.Command{ Use: "generate ", @@ -63,6 +64,7 @@ func NewGenerateCommand() *cobra.Command { v := viper.New() viper.Set("verboseOutput", verboseOutput) + viper.Set("disableCache", disableCache) cmdConfig, err := config.New(v, &config.Options{ SecretName: secretName, ConfigPath: configPath, @@ -116,5 +118,6 @@ func NewGenerateCommand() *cobra.Command { command.Flags().StringVarP(&configPath, "config-path", "c", "", "path to a file containing Vault configuration (YAML, JSON, envfile) to use") command.Flags().StringVarP(&secretName, "secret-name", "s", "", "name of a Kubernetes Secret in the argocd namespace containing Vault configuration data in the argocd namespace of your ArgoCD host (Only available when used in ArgoCD). The namespace can be overridden by using the format :") command.Flags().BoolVar(&verboseOutput, "verbose-sensitive-output", false, "enable verbose mode for detailed info to help with debugging. Includes sensitive data (credentials), logged to stderr") + command.Flags().BoolVar(&disableCache, "disable-token-cache", false, "disable the automatic token cache feature that store tokens locally") return command } diff --git a/cmd/generate_test.go b/cmd/generate_test.go index a99f8d3c..f428c690 100644 --- a/cmd/generate_test.go +++ b/cmd/generate_test.go @@ -2,12 +2,14 @@ package cmd import ( "bytes" + "fmt" "io" "os" "strings" "testing" "github.com/argoproj-labs/argocd-vault-plugin/pkg/helpers" + "github.com/argoproj-labs/argocd-vault-plugin/pkg/utils" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/vault" ) @@ -250,6 +252,54 @@ func TestMain(t *testing.T) { } }) + t.Run("will not create cache if disabled", func(t *testing.T) { + + // Purging token cache before launching this test + err := utils.PurgeTokenCache() + if err != nil { + t.Fatalf("fail to purge tocken cache: %s", err.Error()) + } + + // Starting the generate command with the --disable-token-cache flag + args := []string{ + "../fixtures/input/nonempty", + "--disable-token-cache", + } + cmd := NewGenerateCommand() + + b := bytes.NewBufferString("") + e := bytes.NewBufferString("") + cmd.SetArgs(args) + cmd.SetOut(b) + cmd.SetErr(e) + cmd.Execute() + out, err := io.ReadAll(b) // Read buffer to bytes + if err != nil { + t.Fatal(err) + } + stderr, err := io.ReadAll(e) // Read buffer to bytes + if err != nil { + t.Fatal(err) + } + + buf, err := os.ReadFile("../fixtures/output/all.yaml") + if err != nil { + t.Fatal(err) + } + + // We first check that the command was successful to make sure it reached the token caching part + expected := string(buf) + if string(out) != expected { + t.Fatalf("expected %s\n\nbut got\n\n%s\nerr: %s", expected, string(out), string(stderr)) + } + + // No cache is expected + _, err = utils.ReadExistingToken(fmt.Sprintf("approle_%s", roleid)) + if err == nil { + t.Fatalf("expected no cache but found one") + } + }) + os.Unsetenv("AVP_TYPE") os.Unsetenv("VAULT_ADDR") os.Unsetenv("AVP_AUTH_TYPE") diff --git a/pkg/auth/vault/approle.go b/pkg/auth/vault/approle.go index ceb4484f..913f2c66 100644 --- a/pkg/auth/vault/approle.go +++ b/pkg/auth/vault/approle.go @@ -33,7 +33,7 @@ func NewAppRoleAuth(roleID, secretID, mountPath string) *AppRoleAuth { // Authenticate authenticates with Vault using App Role and returns a token func (a *AppRoleAuth) Authenticate(vaultClient *api.Client) error { - err := utils.LoginWithCachedToken(vaultClient) + err := utils.LoginWithCachedToken(vaultClient, fmt.Sprintf("approle_%s", a.RoleID)) if err != nil { utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", err) } else { @@ -54,7 +54,7 @@ func (a *AppRoleAuth) Authenticate(vaultClient *api.Client) error { utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", data) // If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping. - err = utils.SetToken(vaultClient, data.Auth.ClientToken) + err = utils.SetToken(vaultClient, fmt.Sprintf("approle_%s", a.RoleID), data.Auth.ClientToken) if err != nil { utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", err) } diff --git a/pkg/auth/vault/approle_test.go b/pkg/auth/vault/approle_test.go index 9937253f..c3e57355 100644 --- a/pkg/auth/vault/approle_test.go +++ b/pkg/auth/vault/approle_test.go @@ -2,6 +2,7 @@ package vault_test import ( "bytes" + "fmt" "testing" "github.com/argoproj-labs/argocd-vault-plugin/pkg/auth/vault" @@ -20,7 +21,7 @@ func TestAppRoleLogin(t *testing.T) { t.Fatalf("expected no errors but got: %s", err) } - cachedToken, err := utils.ReadExistingToken() + cachedToken, err := utils.ReadExistingToken(fmt.Sprintf("approle_%s", roleID)) if err != nil { t.Fatalf("expected cached vault token but got: %s", err) } @@ -30,7 +31,7 @@ func TestAppRoleLogin(t *testing.T) { t.Fatalf("expected no errors but got: %s", err) } - newCachedToken, err := utils.ReadExistingToken() + newCachedToken, err := utils.ReadExistingToken(fmt.Sprintf("approle_%s", roleID)) if err != nil { t.Fatalf("expected cached vault token but got: %s", err) } @@ -38,4 +39,25 @@ func TestAppRoleLogin(t *testing.T) { if bytes.Compare(cachedToken, newCachedToken) != 0 { t.Fatalf("expected same token %s but got %s", cachedToken, newCachedToken) } + + // We create a new connection with a different approle and create a different cache + secondCluster, secondRoleID, secondSecretID := helpers.CreateTestAppRoleVault(t) + defer secondCluster.Cleanup() + + secondAppRole := vault.NewAppRoleAuth(secondRoleID, secondSecretID, "") + + err = secondAppRole.Authenticate(secondCluster.Cores[0].Client) + if err != nil { + t.Fatalf("expected no errors but got: %s", err) + } + + secondCachedToken, err := utils.ReadExistingToken(fmt.Sprintf("approle_%s", secondRoleID)) + if err != nil { + t.Fatalf("expected cached vault token but got: %s", err) + } + + // Both cache should be different + if bytes.Compare(cachedToken, secondCachedToken) == 0 { + t.Fatalf("expected different tokens but got %s", secondCachedToken) + } } diff --git a/pkg/auth/vault/github.go b/pkg/auth/vault/github.go index 82a23b8b..dfd6403e 100644 --- a/pkg/auth/vault/github.go +++ b/pkg/auth/vault/github.go @@ -32,7 +32,7 @@ func NewGithubAuth(token, mountPath string) *GithubAuth { // Authenticate authenticates with Vault and returns a token func (g *GithubAuth) Authenticate(vaultClient *api.Client) error { - err := utils.LoginWithCachedToken(vaultClient) + err := utils.LoginWithCachedToken(vaultClient, "github") if err != nil { utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", err) } else { @@ -52,7 +52,7 @@ func (g *GithubAuth) Authenticate(vaultClient *api.Client) error { utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", data) // If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping. - err = utils.SetToken(vaultClient, data.Auth.ClientToken) + err = utils.SetToken(vaultClient, "github", data.Auth.ClientToken) if err != nil { utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", err) } diff --git a/pkg/auth/vault/github_test.go b/pkg/auth/vault/github_test.go index ecedb197..2f98d529 100644 --- a/pkg/auth/vault/github_test.go +++ b/pkg/auth/vault/github_test.go @@ -21,7 +21,7 @@ func TestGithubLogin(t *testing.T) { t.Fatalf("expected no errors but got: %s", err) } - cachedToken, err := utils.ReadExistingToken() + cachedToken, err := utils.ReadExistingToken("github") if err != nil { t.Fatalf("expected cached vault token but got: %s", err) } @@ -31,7 +31,7 @@ func TestGithubLogin(t *testing.T) { t.Fatalf("expected no errors but got: %s", err) } - newCachedToken, err := utils.ReadExistingToken() + newCachedToken, err := utils.ReadExistingToken("github") if err != nil { t.Fatalf("expected cached vault token but got: %s", err) } diff --git a/pkg/auth/vault/kubernetes.go b/pkg/auth/vault/kubernetes.go index 9d6684d7..196404df 100644 --- a/pkg/auth/vault/kubernetes.go +++ b/pkg/auth/vault/kubernetes.go @@ -39,7 +39,7 @@ func NewK8sAuth(role, mountPath, tokenPath string) *K8sAuth { // Authenticate authenticates with Vault via K8s and returns a token func (k *K8sAuth) Authenticate(vaultClient *api.Client) error { - err := utils.LoginWithCachedToken(vaultClient) + err := utils.LoginWithCachedToken(vaultClient, "kubernetes") if err != nil { utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", err) } else { @@ -70,7 +70,7 @@ func (k *K8sAuth) Authenticate(vaultClient *api.Client) error { utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", data) // If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping. - err = utils.SetToken(vaultClient, data.Auth.ClientToken) + err = utils.SetToken(vaultClient, "kubernetes", data.Auth.ClientToken) if err != nil { utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", err) } diff --git a/pkg/auth/vault/kubernetes_test.go b/pkg/auth/vault/kubernetes_test.go index 77396dd5..fbabd133 100644 --- a/pkg/auth/vault/kubernetes_test.go +++ b/pkg/auth/vault/kubernetes_test.go @@ -53,7 +53,7 @@ func TestKubernetesAuth(t *testing.T) { t.Fatalf("expected no errors but got: %s", err) } - cachedToken, err := utils.ReadExistingToken() + cachedToken, err := utils.ReadExistingToken("kubernetes") if err != nil { t.Fatalf("expected cached vault token but got: %s", err) } @@ -63,7 +63,7 @@ func TestKubernetesAuth(t *testing.T) { t.Fatalf("expected no errors but got: %s", err) } - newCachedToken, err := utils.ReadExistingToken() + newCachedToken, err := utils.ReadExistingToken("kubernetes") if err != nil { t.Fatalf("expected cached vault token but got: %s", err) } diff --git a/pkg/auth/vault/userpass.go b/pkg/auth/vault/userpass.go index 23c1dfd2..027d20f9 100644 --- a/pkg/auth/vault/userpass.go +++ b/pkg/auth/vault/userpass.go @@ -33,7 +33,7 @@ func NewUserPassAuth(username, password, mountPath string) *UserPassAuth { // Authenticate authenticates with Vault using userpass and returns a token func (a *UserPassAuth) Authenticate(vaultClient *api.Client) error { - err := utils.LoginWithCachedToken(vaultClient) + err := utils.LoginWithCachedToken(vaultClient, fmt.Sprintf("userpass_%s", a.Username)) if err != nil { utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", err) } else { @@ -53,7 +53,7 @@ func (a *UserPassAuth) Authenticate(vaultClient *api.Client) error { utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", data) // If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping. - if err = utils.SetToken(vaultClient, data.Auth.ClientToken); err != nil { + if err = utils.SetToken(vaultClient, fmt.Sprintf("userpass_%s", a.Username), data.Auth.ClientToken); err != nil { utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", err) } diff --git a/pkg/auth/vault/userpass_test.go b/pkg/auth/vault/userpass_test.go index 0e67d76d..4cdc1bea 100644 --- a/pkg/auth/vault/userpass_test.go +++ b/pkg/auth/vault/userpass_test.go @@ -2,6 +2,7 @@ package vault_test import ( "bytes" + "fmt" "testing" "github.com/argoproj-labs/argocd-vault-plugin/pkg/auth/vault" @@ -19,7 +20,7 @@ func TestUserPassLogin(t *testing.T) { t.Fatalf("expected no errors but got: %s", err) } - cachedToken, err := utils.ReadExistingToken() + cachedToken, err := utils.ReadExistingToken(fmt.Sprintf("userpass_%s", username)) if err != nil { t.Fatalf("expected cached vault token but got: %s", err) } @@ -29,7 +30,7 @@ func TestUserPassLogin(t *testing.T) { t.Fatalf("expected no errors but got: %s", err) } - newCachedToken, err := utils.ReadExistingToken() + newCachedToken, err := utils.ReadExistingToken(fmt.Sprintf("userpass_%s", username)) if err != nil { t.Fatalf("expected cached vault token but got: %s", err) } @@ -37,4 +38,25 @@ func TestUserPassLogin(t *testing.T) { if bytes.Compare(cachedToken, newCachedToken) != 0 { t.Fatalf("expected same token %s but got %s", cachedToken, newCachedToken) } + + // We create a new connection with a different approle and create a different cache + secondCluster, secondUsername, secondPassword := helpers.CreateTestUserPassVault(t) + defer secondCluster.Cleanup() + + secondUserpass := vault.NewUserPassAuth(secondUsername, secondPassword, "") + + err = secondUserpass.Authenticate(secondCluster.Cores[0].Client) + if err != nil { + t.Fatalf("expected no errors but got: %s", err) + } + + secondCachedToken, err := utils.ReadExistingToken(fmt.Sprintf("userpass_%s", secondUsername)) + if err != nil { + t.Fatalf("expected cached vault token but got: %s", err) + } + + // Both cache should be different + if bytes.Compare(cachedToken, secondCachedToken) == 0 { + t.Fatalf("expected different tokens but got %s", secondCachedToken) + } } diff --git a/pkg/utils/util.go b/pkg/utils/util.go index cd6ad52a..899d5621 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -15,13 +15,28 @@ import ( "github.com/spf13/viper" ) -func ReadExistingToken() ([]byte, error) { +func PurgeTokenCache() error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + avpConfigFolderPath := filepath.Join(home, ".avp") + + err = os.RemoveAll(avpConfigFolderPath) + if err != nil { + return err + } + return nil +} + +func ReadExistingToken(identifier string) ([]byte, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } - avpConfigPath := filepath.Join(home, ".avp", "config.json") + avpConfigPath := filepath.Join(home, ".avp", fmt.Sprintf("%s_config.json", identifier)) if _, err := os.Stat(avpConfigPath); err != nil { return nil, err } @@ -42,62 +57,71 @@ func ReadExistingToken() ([]byte, error) { return byteValue, nil } -// LoginWithCachedToken takes a VaultType interface and tries to log in with the previously cached token, +// LoginWithCachedToken takes a VaultType interface, the auth types and an identifier of the token +// It then tries to log in with the matching previously cached token, // And sets the token in the client -func LoginWithCachedToken(vaultClient *api.Client) error { - byteValue, err := ReadExistingToken() - if err != nil { - return err - } +func LoginWithCachedToken(vaultClient *api.Client, identifier string) error { + if viper.GetBool("disableCache") { + return fmt.Errorf("Token cache feature is disabled") + } else { + byteValue, err := ReadExistingToken(identifier) + if err != nil { + return err + } - var result map[string]interface{} - err = json.Unmarshal([]byte(byteValue), &result) - if err != nil { - return err - } + var result map[string]interface{} + err = json.Unmarshal([]byte(byteValue), &result) + if err != nil { + return err + } - vaultClient.SetToken(result["vault_token"].(string)) - _, err = vaultClient.Auth().Token().LookupSelf() - if err != nil { - return err - } + vaultClient.SetToken(result["vault_token"].(string)) + _, err = vaultClient.Auth().Token().LookupSelf() + if err != nil { + return err + } - return nil + return nil + } } // SetToken attmepts to set the vault token on the vault api client // and then attempts to write that token to a file to be used later // If this method fails we do not want to stop the process -func SetToken(vaultClient *api.Client, token string) error { +func SetToken(vaultClient *api.Client, identifier string, token string) error { // We want to set the token first vaultClient.SetToken(token) - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("Could not access home directory: %s", err.Error()) - } + if viper.GetBool("disableCache") { + return fmt.Errorf("Token cache feature is disabled") + } else { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("Could not access home directory: %s", err.Error()) + } - path := filepath.Join(home, ".avp") - if _, err := os.Stat(path); os.IsNotExist(err) { - err := os.Mkdir(path, 0755) + path := filepath.Join(home, ".avp") + if _, err := os.Stat(path); os.IsNotExist(err) { + err := os.Mkdir(path, 0755) + if err != nil { + return fmt.Errorf("Could not create avp directory: %s", err.Error()) + } + } + + data := map[string]interface{}{ + "vault_token": token, + } + file, err := json.MarshalIndent(data, "", " ") if err != nil { - return fmt.Errorf("Could not create avp directory: %s", err.Error()) + return fmt.Errorf("Could not marshal token data: %s", err.Error()) + } + err = os.WriteFile(filepath.Join(path, fmt.Sprintf("%s_config.json", identifier)), file, 0644) + if err != nil { + return fmt.Errorf("Could not write token to file, will need to login to Vault on subsequent runs: %s", err.Error()) } - } - data := map[string]interface{}{ - "vault_token": token, + return nil } - file, err := json.MarshalIndent(data, "", " ") - if err != nil { - return fmt.Errorf("Could not marshal token data: %s", err.Error()) - } - err = os.WriteFile(filepath.Join(path, "config.json"), file, 0644) - if err != nil { - return fmt.Errorf("Could not write token to file, will need to login to Vault on subsequent runs: %s", err.Error()) - } - - return nil } func DefaultHttpClient() *http.Client { diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index bf42b3ac..9972b3ef 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -15,7 +15,7 @@ import ( "github.com/argoproj-labs/argocd-vault-plugin/pkg/utils" ) -func writeToken(token string) error { +func writeToken(identifier string, token string) error { home, err := os.UserHomeDir() if err != nil { return err @@ -26,7 +26,7 @@ func writeToken(token string) error { "vault_token": token, } file, _ := json.MarshalIndent(data, "", " ") - err = os.WriteFile(filepath.Join(path, "config.json"), file, 0644) + err = os.WriteFile(filepath.Join(path, fmt.Sprintf("%s_config.json", identifier)), file, 0644) if err != nil { return err } @@ -43,9 +43,9 @@ func removeToken() error { return nil } -func readToken() interface{} { +func readToken(identifier string) interface{} { home, _ := os.UserHomeDir() - path := filepath.Join(home, ".avp", "config.json") + path := filepath.Join(home, ".avp", fmt.Sprintf("%s_config.json", identifier)) dat, _ := os.ReadFile(path) var result map[string]interface{} json.Unmarshal([]byte(dat), &result) @@ -57,12 +57,12 @@ func TestCheckExistingToken(t *testing.T) { defer ln.Close() t.Run("will set token if valid", func(t *testing.T) { - err := writeToken(roottoken) + err := writeToken("test", roottoken) if err != nil { t.Fatal(err) } - err = utils.LoginWithCachedToken(client) + err = utils.LoginWithCachedToken(client, "test") if err != nil { t.Fatal(err) } @@ -82,25 +82,24 @@ func TestCheckExistingToken(t *testing.T) { ln, client, _ := helpers.CreateTestVault(t) defer ln.Close() - err := utils.LoginWithCachedToken(client) + err := utils.LoginWithCachedToken(client, "test") if err == nil { t.Fatal(err) } dir, _ := os.UserHomeDir() - expected := fmt.Sprintf("stat %s/.avp/config.json: no such file or directory", dir) + expected := fmt.Sprintf("stat %s/.avp/test_config.json: no such file or directory", dir) if err.Error() != expected { t.Errorf("expected: %s, got: %s.", expected, err.Error()) } }) - } func TestSetToken(t *testing.T) { cluster, _, _ := helpers.CreateTestAppRoleVault(t) defer cluster.Cleanup() - utils.SetToken(cluster.Cores[0].Client, "token") + utils.SetToken(cluster.Cores[0].Client, "test", "token") err := removeToken() if err != nil {