From b752901de38611c9c292e9c626fb80f816f86e10 Mon Sep 17 00:00:00 2001 From: Antoine Miquel Date: Sun, 21 Jan 2024 03:02:01 +0100 Subject: [PATCH] Feat/conjur (#195) * feat: add conjur provider Signed-off-by: Antoine Miquel Signed-off-by: antoinemiquel --- README.md | 19 ++++ go.mod | 6 ++ go.sum | 13 +++ pkg/providers/conjur/conjur.go | 91 ++++++++++++++++++ pkg/providers/conjur/conjur_test.go | 134 +++++++++++++++++++++++++++ pkg/stringprovider/stringprovider.go | 3 + vals.go | 5 + 7 files changed, 271 insertions(+) create mode 100644 pkg/providers/conjur/conjur.go create mode 100644 pkg/providers/conjur/conjur_test.go diff --git a/README.md b/README.md index 87ad0f8..73bc0ff 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ It supports various backends including: - CredHub(Coming soon) - Pulumi State - Kubernetes +- Conjur - Use `vals eval -f refs.yaml` to replace all the `ref`s in the file to actual values and secrets. - Use `vals exec -f env.yaml -- ` to populate envvars and execute the command. @@ -220,6 +221,7 @@ Please see the [relevant unit test cases](https://github.com/helmfile/vals/blob/ - [Doppler](#doppler) - [Pulumi State](#pulumi-state) - [Kubernetes](#kubernetes) +- [Conjur](#conjur) Please see [pkg/providers](https://github.com/helmfile/vals/tree/master/pkg/providers) for the implementations of all the providers. The package names corresponds to the URI schemes. @@ -748,6 +750,23 @@ Examples: > NOTE: This provider only supports kind "Secret" or "ConfigMap" in apiVersion "v1" at this time. +### Conjur + +This provider retrieves the value of secrets stored in [Conjur](https://www.conjur.org/). +It's based on the https://github.com/cyberark/conjur-api-go lib. + +The following env vars have to be configured: +- `CONJUR_APPLIANCE_URL` +- `CONJUR_ACCOUNT` +- `CONJUR_AUTHN_LOGIN` +- `CONJUR_AUTHN_API_KEY` + +- `ref+conjur://PATH/TO/VARIABLE[?address=CONJUR_APPLIANCE_URL&account=CONJUR_ACCOUNT&login=CONJUR_AUTHN_LOGIN&apikey=CONJUR_AUTHN_API_KEY]/CONJUR_SECRET_ID` + +Example: + +- `ref+conjur://branch/variable_name` + ## Advanced Usages ### Discriminating config and secrets diff --git a/go.mod b/go.mod index 93a38e6..75f7b54 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/alessio/shellescape v1.4.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 // indirect @@ -70,10 +71,13 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 // indirect github.com/aws/smithy-go v1.14.2 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cyberark/conjur-api-go v0.11.1 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect @@ -84,6 +88,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.1.0 // indirect @@ -143,6 +148,7 @@ require ( github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/urfave/cli v1.22.14 // indirect + github.com/zalando/go-keyring v0.2.3-0.20230503081219-17db2e5354bd // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.17.0 // indirect diff --git a/go.sum b/go.sum index d5712df..8914416 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCv github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/a8m/envsubst v1.3.0 h1:GmXKmVssap0YtlU3E230W98RWtWCyIZzjtf1apWWyAg= github.com/a8m/envsubst v1.3.0/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= @@ -119,6 +121,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 h1:CQBFElb0LS8RojMJlxRSo/HXipvT github.com/aws/aws-sdk-go-v2/service/sts v1.21.5/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU= github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ= github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= @@ -140,6 +144,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyberark/conjur-api-go v0.11.1 h1:vjaMkw0geJsA+ikMM6UDLg4VLFQWKo/B0i9IWlOQ1f0= +github.com/cyberark/conjur-api-go v0.11.1/go.mod h1:n1p46Hj9l8wkZjM17cVYdfcatyPboWyioLGlC0QszCs= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -185,6 +193,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= @@ -417,6 +427,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/go-keyring v0.2.3-0.20230503081219-17db2e5354bd h1:D+eeEnOlWcMXbwZ5X3oy68nHafBtGcj1jMKFHtVdybY= +github.com/zalando/go-keyring v0.2.3-0.20230503081219-17db2e5354bd/go.mod h1:sI3evg9Wvpw3+n4SqplGSJUMwtDeROfD4nsFz4z9PG0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= @@ -483,6 +495,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/providers/conjur/conjur.go b/pkg/providers/conjur/conjur.go new file mode 100644 index 0000000..cd09a3d --- /dev/null +++ b/pkg/providers/conjur/conjur.go @@ -0,0 +1,91 @@ +package conjur + +import ( + "fmt" + "os" + + "github.com/cyberark/conjur-api-go/conjurapi" + "github.com/cyberark/conjur-api-go/conjurapi/authn" + + "github.com/helmfile/vals/pkg/api" + "github.com/helmfile/vals/pkg/log" +) + +type provider struct { + client *conjurapi.Client + log *log.Logger + + Address string + Account string + Login string + Apikey string +} + +func New(l *log.Logger, cfg api.StaticConfig) *provider { + p := &provider{ + log: l, + } + p.Address = cfg.String("address") + if p.Address == "" { + p.Address = os.Getenv("CONJUR_APPLIANCE_URL") + } + p.Account = cfg.String("account") + if p.Account == "" { + p.Account = os.Getenv("CONJUR_ACCOUNT") + } + p.Login = cfg.String("login") + if p.Login == "" { + p.Login = os.Getenv("CONJUR_AUTHN_LOGIN") + } + p.Apikey = cfg.String("apikey") + if p.Apikey == "" { + p.Apikey = os.Getenv("CONJUR_AUTHN_API_KEY") + } + + return p +} + +func (p *provider) GetString(varId string) (string, error) { + cli, err := p.ensureClient() + if err != nil { + return "", fmt.Errorf("cannot create Conjur Client: %v", err) + } + + secretValue, err := cli.RetrieveSecret(varId) + if err != nil { + return "", fmt.Errorf("no variable found for path %q", varId) + } + + return string(secretValue), nil +} + +func (p *provider) GetStringMap(path string) (map[string]interface{}, error) { + return nil, fmt.Errorf("this provider does not support values from URI fragments") +} + +func (p *provider) ensureClient() (*conjurapi.Client, error) { + if p.client == nil { + config, err := conjurapi.LoadConfig() + if err != nil { + p.log.Debugf("conjur: cannot get conjur config") + return nil, err + } + + config.ApplianceURL = p.Address + config.Account = p.Account + + cli, err := conjurapi.NewClientFromKey(config, + authn.LoginPair{ + Login: p.Login, + APIKey: p.Apikey, + }, + ) + if err != nil { + p.log.Debugf("conjur: connection failed") + return nil, err + } + + p.client = cli + } + return p.client, nil +} diff --git a/pkg/providers/conjur/conjur_test.go b/pkg/providers/conjur/conjur_test.go new file mode 100644 index 0000000..0f7aa72 --- /dev/null +++ b/pkg/providers/conjur/conjur_test.go @@ -0,0 +1,134 @@ +package conjur + +import ( + "testing" + + "github.com/cyberark/conjur-api-go/conjurapi" + "github.com/stretchr/testify/assert" + + "github.com/helmfile/vals/pkg/config" + "github.com/helmfile/vals/pkg/log" +) + +func Test_New(t *testing.T) { + testsConfig := []struct { + name string + options map[string]interface{} + envVars map[string]string + want *provider + }{ + { + name: "onlyConf", + options: map[string]interface{}{ + "address": "http://127.0.0.1", + "account": "myAccount", + "login": "user", + "apikey": "pass", + }, + envVars: map[string]string{}, + want: &provider{ + log: log.New(log.Config{}), + Address: "http://127.0.0.1", + Account: "myAccount", + Login: "user", + Apikey: "pass", + }, + }, + { + name: "onlyEnvVars", + options: map[string]interface{}{}, + envVars: map[string]string{ + "CONJUR_APPLIANCE_URL": "http://127.0.0.1", + "CONJUR_ACCOUNT": "myAccount", + "CONJUR_AUTHN_LOGIN": "user", + "CONJUR_AUTHN_API_KEY": "pass", + }, + want: &provider{ + log: log.New(log.Config{}), + Address: "http://127.0.0.1", + Account: "myAccount", + Login: "user", + Apikey: "pass", + }, + }, + { + name: "mixConfigAndEnvVars", + options: map[string]interface{}{ + "address": "http://127.0.0.1", + "account": "myAccount", + "login": "user", + "apikey": "pass", + }, + envVars: map[string]string{ + "CONJUR_APPLIANCE_URL": "http://192.168.0.10", + "CONJUR_ACCOUNT": "myAccount2", + "CONJUR_AUTHN_LOGIN": "user2", + "CONJUR_AUTHN_API_KEY": "pass2", + }, + want: &provider{ + log: log.New(log.Config{}), + Address: "http://127.0.0.1", + Account: "myAccount", + Login: "user", + Apikey: "pass", + }, + }, + } + + for _, tt := range testsConfig { + t.Run(tt.name, func(t *testing.T) { + conf := config.Map(tt.options) + logger := log.New(log.Config{}) + + for k, v := range tt.envVars { + t.Setenv(k, v) + } + + p := New(logger, conf) + + assert.EqualValues(t, tt.want, p) + + // cleanup envVars + for k := range tt.envVars { + t.Setenv(k, "") + } + }) + } +} + +func Test_GetStringMap(t *testing.T) { + options := map[string]interface{}{ + "address": "http://127.0.0.1", + "account": "myAccount", + "login": "user", + "apikey": "pass", + } + conf := config.Map(options) + logger := log.New(log.Config{}) + + p := New(logger, conf) + + mapRes, err := p.GetStringMap("somePath") + + assert.Empty(t, mapRes) + assert.Error(t, err) +} + +func Test_ensureClient(t *testing.T) { + options := map[string]interface{}{ + "address": "http://127.0.0.1", + "account": "myAccount", + "login": "user", + "apikey": "pass", + } + conf := config.Map(options) + logger := log.New(log.Config{}) + + p := New(logger, conf) + p.client = &conjurapi.Client{} + + cli, err := p.ensureClient() + + assert.Equal(t, p.client, cli) + assert.NoError(t, err) +} diff --git a/pkg/stringprovider/stringprovider.go b/pkg/stringprovider/stringprovider.go index cb8e0af..bd5b0a9 100644 --- a/pkg/stringprovider/stringprovider.go +++ b/pkg/stringprovider/stringprovider.go @@ -8,6 +8,7 @@ import ( "github.com/helmfile/vals/pkg/providers/awskms" "github.com/helmfile/vals/pkg/providers/awssecrets" "github.com/helmfile/vals/pkg/providers/azurekeyvault" + "github.com/helmfile/vals/pkg/providers/conjur" "github.com/helmfile/vals/pkg/providers/doppler" "github.com/helmfile/vals/pkg/providers/gcpsecrets" "github.com/helmfile/vals/pkg/providers/gcs" @@ -67,6 +68,8 @@ func New(l *log.Logger, provider api.StaticConfig) (api.LazyLoadedStringProvider return gkms.New(l, provider), nil case "k8s": return k8s.New(l, provider) + case "conjur": + return conjur.New(l, provider), nil } return nil, fmt.Errorf("failed initializing string provider from config: %v", provider) diff --git a/vals.go b/vals.go index 8d48df2..5d43ac3 100644 --- a/vals.go +++ b/vals.go @@ -23,6 +23,7 @@ import ( "github.com/helmfile/vals/pkg/providers/awskms" "github.com/helmfile/vals/pkg/providers/awssecrets" "github.com/helmfile/vals/pkg/providers/azurekeyvault" + "github.com/helmfile/vals/pkg/providers/conjur" "github.com/helmfile/vals/pkg/providers/doppler" "github.com/helmfile/vals/pkg/providers/echo" "github.com/helmfile/vals/pkg/providers/envsubst" @@ -91,6 +92,7 @@ const ( ProviderPulumiStateAPI = "pulumistateapi" ProviderGKMS = "gkms" ProviderK8s = "k8s" + ProviderConjur = "conjur" ) var ( @@ -252,6 +254,9 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) { return p, nil case ProviderK8s: return k8s.New(r.logger, conf) + case ProviderConjur: + p := conjur.New(r.logger, conf) + return p, nil } return nil, fmt.Errorf("no provider registered for scheme %q", scheme) }