diff --git a/cmd/root.go b/cmd/root.go index 96b46c89..b57ef317 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,7 +8,7 @@ import ( func NewRootCommand() *cobra.Command { var command = &cobra.Command{ Use: "argocd-vault-plugin", - Short: "This is a plugin to replace with Vault secrets", + Short: "This is a plugin to replace with Vault secrets", Run: func(cmd *cobra.Command, args []string) { cmd.HelpFunc()(cmd, args) }, diff --git a/docs/cmd/avp.md b/docs/cmd/avp.md index d8916235..857ed32c 100644 --- a/docs/cmd/avp.md +++ b/docs/cmd/avp.md @@ -1,4 +1,4 @@ -This is a plugin to replace with Vault secrets +This is a plugin to replace 's with Vault secrets ``` argocd-vault-plugin [flags] diff --git a/pkg/auth/ibmsecretsmanager/iam.go b/pkg/auth/ibmsecretsmanager/iam.go index cc47beae..4b7e6902 100644 --- a/pkg/auth/ibmsecretsmanager/iam.go +++ b/pkg/auth/ibmsecretsmanager/iam.go @@ -7,8 +7,8 @@ import ( "net/http" "net/url" "strings" - "time" + "github.com/IBM/argocd-vault-plugin/pkg/types" "github.com/IBM/argocd-vault-plugin/pkg/utils" "github.com/hashicorp/vault/api" ) @@ -16,12 +16,14 @@ import ( // IAMAuth is a struct for working with SecretManager that uses IAM type IAMAuth struct { APIKey string + Client types.HTTPClient } // NewIAMAuth initializes a new IAMAuth with api key -func NewIAMAuth(apikey string) *IAMAuth { +func NewIAMAuth(apikey string, client types.HTTPClient) *IAMAuth { iamAuth := &IAMAuth{ APIKey: apikey, + Client: client, } return iamAuth @@ -29,7 +31,7 @@ func NewIAMAuth(apikey string) *IAMAuth { // Authenticate authenticates with Vault using App Role and returns a token func (i *IAMAuth) Authenticate(vaultClient *api.Client) error { - accessToken, err := getAccessToken(i.APIKey) + accessToken, err := getAccessToken(i.APIKey, i.Client) if err != nil { return err } @@ -52,7 +54,7 @@ func (i *IAMAuth) Authenticate(vaultClient *api.Client) error { return nil } -func getAccessToken(apikey string) (string, error) { +func getAccessToken(apikey string, client types.HTTPClient) (string, error) { // Set url values to be added to the request urlValues := url.Values{} urlValues.Set("grant_type", "urn:ibm:params:oauth:grant-type:apikey") @@ -66,12 +68,8 @@ func getAccessToken(apikey string) (string, error) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") - var httpClient = &http.Client{ - Timeout: 10 * time.Second, - } - // Perform http request - res, err := httpClient.Do(req) + res, err := client.Do(req) if err != nil { return "", err } diff --git a/pkg/auth/ibmsecretsmanager/iam_test.go b/pkg/auth/ibmsecretsmanager/iam_test.go index 84a8074c..26d06533 100644 --- a/pkg/auth/ibmsecretsmanager/iam_test.go +++ b/pkg/auth/ibmsecretsmanager/iam_test.go @@ -1 +1,40 @@ package ibmsecretsmanager_test + +import ( + "bytes" + "io/ioutil" + "net/http" + "testing" + + "github.com/IBM/argocd-vault-plugin/pkg/auth/ibmsecretsmanager" + "github.com/IBM/argocd-vault-plugin/pkg/helpers" +) + +// MockClient is the mock client +type MockClient struct{} + +// Do is the mock client's `Do` func +func (m *MockClient) Do(req *http.Request) (*http.Response, error) { + json := `{"access_token":"123"}` + + // create a new reader with that JSON + r := ioutil.NopCloser(bytes.NewReader([]byte(json))) + return &http.Response{ + StatusCode: 200, + Body: r, + }, nil +} + +// Need to find a way to mock GitHub Auth within Vault +func TestIBMAuth(t *testing.T) { + cluster := helpers.CreateTestAuthVault(t) + defer cluster.Cleanup() + + c := &MockClient{} + ibm := ibmsecretsmanager.NewIAMAuth("abc", c) + + err := ibm.Authenticate(cluster.Cores[0].Client) + if err != nil { + t.Fatalf("expected no errors but got: %s", err) + } +} diff --git a/pkg/auth/vault/github.go b/pkg/auth/vault/github.go index 97c2c9f2..a0bccb0a 100644 --- a/pkg/auth/vault/github.go +++ b/pkg/auth/vault/github.go @@ -29,7 +29,6 @@ func (g *GithubAuth) Authenticate(vaultClient *api.Client) error { if err != nil { return err } - // If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping. err = utils.SetToken(vaultClient, data.Auth.ClientToken) if err != nil { diff --git a/pkg/auth/vault/github_test.go b/pkg/auth/vault/github_test.go index c41f401f..e8241f63 100644 --- a/pkg/auth/vault/github_test.go +++ b/pkg/auth/vault/github_test.go @@ -1,16 +1,21 @@ package vault_test +import ( + "testing" + + "github.com/IBM/argocd-vault-plugin/pkg/auth/vault" + "github.com/IBM/argocd-vault-plugin/pkg/helpers" +) + // Need to find a way to mock GitHub Auth within Vault -// func TestGithubLogin(t *testing.T) { -// cluster, role, secret := helpers.CreateTestVault(t) -// defer cluster.Cleanup() -// -// github := auth.Github{ -// AccessToken: "test", -// } -// -// err := github.Authenticate(cluster.Cores[0].Client) -// if err != nil { -// t.Fatalf("expected no errors but got: %s", err) -// } -// } +func TestGithubLogin(t *testing.T) { + cluster := helpers.CreateTestAuthVault(t) + defer cluster.Cleanup() + + github := vault.NewGithubAuth("123") + + err := github.Authenticate(cluster.Cores[0].Client) + if err != nil { + t.Fatalf("expected no errors but got: %s", err) + } +} diff --git a/pkg/auth/vault/kubernetes_test.go b/pkg/auth/vault/kubernetes_test.go index 72cca848..ebcd7995 100644 --- a/pkg/auth/vault/kubernetes_test.go +++ b/pkg/auth/vault/kubernetes_test.go @@ -1,14 +1,59 @@ package vault_test -// Need to find a way to mock k8s Auth within Vault -// func TestGithubLogin(t *testing.T) { -// cluster, role, secret := helpers.CreateTestVault(t) -// defer cluster.Cleanup() -// -// k8s := vault.NewK8sAuth("", "", "") -// -// err := k8s.Authenticate(cluster.Cores[0].Client) -// if err != nil { -// t.Fatalf("expected no errors but got: %s", err) -// } -// } +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/IBM/argocd-vault-plugin/pkg/auth/vault" + "github.com/IBM/argocd-vault-plugin/pkg/helpers" +) + +const saPath = "/tmp/avp/kubernetes.io/serviceaccount" + +func writeK8sToken() error { + err := os.MkdirAll(saPath, os.ModePerm) + if err != nil { + return fmt.Errorf("Could not create directory: %s", err.Error()) + } + + data := []byte("123456") + err = ioutil.WriteFile(filepath.Join(saPath, "token"), data, 0644) + if err != nil { + return err + } + return nil +} + +func removeK8sToken() error { + err := os.RemoveAll("/tmp/avp") + if err != nil { + return err + } + return nil +} + +// Need to find a way to mock GitHub Auth within Vault +func TestKubernetesAuth(t *testing.T) { + cluster := helpers.CreateTestAuthVault(t) + defer cluster.Cleanup() + + err := writeK8sToken() + if err != nil { + t.Fatalf("error writing token: %s", err) + } + + k8s := vault.NewK8sAuth("role", "", string(filepath.Join(saPath, "token"))) + + err = k8s.Authenticate(cluster.Cores[0].Client) + if err != nil { + t.Fatalf("expected no errors but got: %s", err) + } + + err = removeK8sToken() + if err != nil { + fmt.Println(err) + } +} diff --git a/pkg/backends/ibmsecretsmanager_test.go b/pkg/backends/ibmsecretsmanager_test.go index 4e74c3c0..89898e9f 100644 --- a/pkg/backends/ibmsecretsmanager_test.go +++ b/pkg/backends/ibmsecretsmanager_test.go @@ -2,21 +2,29 @@ package backends_test import ( "fmt" + "net/http" "reflect" "testing" + "github.com/IBM/argocd-vault-plugin/pkg/auth/ibmsecretsmanager" "github.com/IBM/argocd-vault-plugin/pkg/backends" "github.com/IBM/argocd-vault-plugin/pkg/helpers" ) +// MockClient is the mock client +type MockClient struct{} + +// Do is the mock client's `Do` func +func (m *MockClient) Do(req *http.Request) (*http.Response, error) { + return &http.Response{}, nil +} + func TestSecretsManagerGetSecrets(t *testing.T) { ln, client, _ := helpers.CreateTestVault(t) defer ln.Close() - sm := backends.IBMSecretsManager{ - IBMCloudAPIKey: "token", - VaultClient: client, - } + iamAuth := ibmsecretsmanager.NewIAMAuth("token", &MockClient{}) + sm := backends.NewIBMSecretsManagerBackend(iamAuth, client) expected := map[string]interface{}{ "secret": "value", @@ -37,10 +45,8 @@ func TestSecretsmanagerGetSecretsFail(t *testing.T) { ln, client, _ := helpers.CreateTestVault(t) defer ln.Close() - sm := backends.IBMSecretsManager{ - IBMCloudAPIKey: "token", - VaultClient: client, - } + iamAuth := ibmsecretsmanager.NewIAMAuth("token", &MockClient{}) + sm := backends.NewIBMSecretsManagerBackend(iamAuth, client) _, err := sm.GetSecrets("secret/ibm/arbitrary/groups/3", map[string]string{}) diff --git a/pkg/config/config.go b/pkg/config/config.go index d5999f3d..ae5ed19c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,6 +16,7 @@ import ( "github.com/IBM/argocd-vault-plugin/pkg/backends" "github.com/IBM/argocd-vault-plugin/pkg/kube" "github.com/IBM/argocd-vault-plugin/pkg/types" + "github.com/IBM/argocd-vault-plugin/pkg/utils" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/secretsmanager" @@ -100,7 +101,7 @@ func New(v *viper.Viper, co *Options) (*Config, error) { switch authType { case types.IAMAuth: if v.IsSet(types.EnvAvpIBMAPIKey) { - auth = ibmsecretsmanager.NewIAMAuth(v.GetString(types.EnvAvpIBMAPIKey)) + auth = ibmsecretsmanager.NewIAMAuth(v.GetString(types.EnvAvpIBMAPIKey), utils.DefaultHttpClient()) } else { return nil, fmt.Errorf("%s for iam authentication cannot be empty", types.EnvAvpIBMAPIKey) } diff --git a/pkg/helpers/test_helpers.go b/pkg/helpers/test_helpers.go index fa6429c0..1fa9e11a 100644 --- a/pkg/helpers/test_helpers.go +++ b/pkg/helpers/test_helpers.go @@ -228,6 +228,56 @@ func CreateTestAppRoleVault(t *testing.T) (*vault.TestCluster, string, string) { return cluster, roleID, secretID } +// CreateTestGithubVault initializes a new test vault with AppRole and Kv v2 +func CreateTestAuthVault(t *testing.T) *vault.TestCluster { + t.Helper() + + coreConfig := &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "github": Factory, + "kubernetes": Factory, + "ibmcloud": Factory, + }, + } + + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: http.Handler, + }) + + cluster.Start() + + vault.TestWaitActive(t, cluster.Cores[0].Core) + + client := cluster.Cores[0].Client + + client.Sys().Mount("kv", &api.MountInput{ + Type: "kv", + Options: map[string]string{ + "version": "2", + }, + }) + + if err := client.Sys().EnableAuthWithOptions("github", &api.EnableAuthOptions{ + Type: "github", + }); err != nil { + t.Fatal(err) + } + + if err := client.Sys().EnableAuthWithOptions("kubernetes", &api.EnableAuthOptions{ + Type: "kubernetes", + }); err != nil { + t.Fatal(err) + } + + if err := client.Sys().EnableAuthWithOptions("ibmcloud", &api.EnableAuthOptions{ + Type: "ibmcloud", + }); err != nil { + t.Fatal(err) + } + + return cluster +} + type MockVault struct { GetSecretsCalled bool Data map[string]interface{} diff --git a/pkg/helpers/vault_plugin_mock.go b/pkg/helpers/vault_plugin_mock.go new file mode 100644 index 00000000..b200fda5 --- /dev/null +++ b/pkg/helpers/vault_plugin_mock.go @@ -0,0 +1,77 @@ +package helpers + +import ( + "context" + "errors" + "time" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +func Factory(ctx context.Context, c *logical.BackendConfig) (logical.Backend, error) { + b := Backend(c) + if err := b.Setup(ctx, c); err != nil { + return nil, err + } + return b, nil +} + +type backend struct { + *framework.Backend +} + +func Backend(c *logical.BackendConfig) *backend { + var b backend + + b.Backend = &framework.Backend{ + BackendType: logical.TypeCredential, + AuthRenew: b.pathAuthRenew, + PathsSpecial: &logical.Paths{ + Unauthenticated: []string{"login"}, + }, + Paths: []*framework.Path{ + &framework.Path{ + Pattern: "login", + Fields: map[string]*framework.FieldSchema{ + "token": &framework.FieldSchema{ + Type: framework.TypeString, + }, + }, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathAuthLogin, + }, + }, + }, + } + + return &b +} + +func (b *backend) pathAuthLogin(_ context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return &logical.Response{ + Auth: &logical.Auth{ + InternalData: map[string]interface{}{ + "secret_value": "abcd1234", + }, + LeaseOptions: logical.LeaseOptions{ + TTL: 30 * time.Hour, + MaxTTL: 60 * time.Hour, + Renewable: true, + }, + }, + }, nil +} + +func (b *backend) pathAuthRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + if req.Auth == nil { + return nil, errors.New("request auth was nil") + } + + secretValue := req.Auth.InternalData["secret_value"].(string) + if secretValue != "abcd1234" { + return nil, errors.New("internal data does not match") + } + + return framework.LeaseExtend(30*time.Hour, 60*time.Hour, b.System())(ctx, req, d) +} diff --git a/pkg/types/types.go b/pkg/types/types.go index a9e31e65..5b05851c 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -1,6 +1,8 @@ package types import ( + "net/http" + "github.com/hashicorp/vault/api" ) @@ -14,3 +16,8 @@ type Backend interface { type AuthType interface { Authenticate(*api.Client) error } + +// HTTPClient interface +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 503a00c9..31367d8f 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -1,13 +1,16 @@ package utils_test import ( + "crypto/tls" "encoding/json" "fmt" "io/ioutil" + "net/http" "os" "path/filepath" "reflect" "testing" + "time" "github.com/IBM/argocd-vault-plugin/pkg/helpers" "github.com/IBM/argocd-vault-plugin/pkg/utils" @@ -105,3 +108,21 @@ func TestSetToken(t *testing.T) { t.Fatal(err) } } + +func TestDefaultHTTPClient(t *testing.T) { + expectedClient := &http.Client{ + Timeout: 60 * time.Second, + Transport: &http.Transport{ + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + }, + } + + client := utils.DefaultHttpClient() + + if !reflect.DeepEqual(client, expectedClient) { + t.Errorf("expected: %v, got: %v.", expectedClient, client) + } +}