diff --git a/README.md b/README.md index 51233c9..4a2ca43 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ It supports various backends including: - [Google Sheets](#google-sheets) - [SOPS](https://github.com/mozilla/sops)-encrypted files - Terraform State +- 1Password Connect - CredHub(Coming soon) - Use `vals eval -f refs.yaml` to replace all the `ref`s in the file to actual values and secrets. @@ -210,6 +211,7 @@ Please see the [relevant unit test cases](https://github.com/helmfile/vals/blob/ - [Azure Key Vault](#azure-key-vault) - [EnvSubst](#envsubst) - [GitLab](#gitlab) +- [1Password Connect](#1password-connect) 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. @@ -627,6 +629,27 @@ Examples: - `ref+gitlab://gitlab.com/11111/password` - `ref+gitlab://my-gitlab.org/11111/password?ssl_verify=true&scheme=https` +### 1Password Connect + +For this provider to work you require a working and accessible [1Password connect server](https://developer.1password.com/docs/connect). +The following env vars have to be configured: +- `OP_CONNECT_HOST` +- `OP_CONNET_TOKEN` + +1Password is organized in vaults and items. +An item can have multiple fields with or without a section. Labels can be set on fields and sections. +Vaults, items, sections and labels can be accessed by ID or by label/name (and IDs and labels can be mixed and matched in one URL). + +If a section does not have a label the field is only accessible via the section ID. This does not hold true for some default fields which may have no section at all (e.g.username and password for a `Login` item). + +*Caution: vals-expressions are parsed as URIs. For the 1Password connect provider the host component of the URI identifies the vault (by ID or name). Therefore vaults containing certain characters not allowed in the host component (e.g. whitespaces, see [RFC-3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2) for details) can only be accessed by ID.* + +Examples: + +- `ref+onepasswordconnect://VAULT_ID/ITEM_ID#/[SECTION_ID.]FIELD_ID` +- `ref+onepasswordconnect://VAULT_LABEL/ITEM_LABEL#/[SECTION_LABEL.]FIELD_LABEL` +- `ref+onepasswordconnect://VAULT_LABEL/ITEM_ID#/[SECTION_LABEL.]FIELD_ID` + ## Advanced Usages ### Discriminating config and secrets diff --git a/go.mod b/go.mod index 167ec6a..84b475e 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( cloud.google.com/go/compute v1.7.0 // indirect cloud.google.com/go/iam v0.3.0 // indirect filippo.io/age v1.0.0 // indirect + github.com/1Password/connect-sdk-go v1.5.3 // indirect github.com/Azure/azure-sdk-for-go v63.3.0+incompatible // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.3.0 // indirect @@ -116,12 +117,15 @@ require ( github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect + github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.1+incompatible // indirect go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.9.0 // indirect diff --git a/go.sum b/go.sum index d4a517b..e843737 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/age v1.0.0 h1:V6q14n0mqYU3qKFkZ6oOaF9oXneOviS3ubXsSVBRSzc= filippo.io/age v1.0.0/go.mod h1:PaX+Si/Sd5G8LgfCwldsSba3H1DDQZhIhFGkhbHaBq8= +github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk= +github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs= github.com/Azure/azure-sdk-for-go v63.3.0+incompatible h1:INepVujzUrmArRZjDLHbtER+FkvCoEwyRCXGqOlmDII= github.com/Azure/azure-sdk-for-go v63.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY= @@ -493,6 +495,8 @@ github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DV github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/runc v1.1.0 h1:O9+X96OcDjkmmZyfaG996kV7yq8HsoU2h1XRRQcefG8= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -542,6 +546,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1037,6 +1045,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/pkg/providers/onepasswordconnect/onepasswordconnect.go b/pkg/providers/onepasswordconnect/onepasswordconnect.go new file mode 100644 index 0000000..4a13bac --- /dev/null +++ b/pkg/providers/onepasswordconnect/onepasswordconnect.go @@ -0,0 +1,91 @@ +package onepasswordconnect + +import ( + "errors" + "fmt" + "strings" + + "github.com/1Password/connect-sdk-go/connect" + "gopkg.in/yaml.v3" + + "github.com/helmfile/vals/pkg/api" +) + +type provider struct { + client connect.Client +} + +// New creates a new 1Password Connect provider +func New(cfg api.StaticConfig) *provider { + p := &provider{} + + return p +} + +// Get secret string from 1Password Connect +func (p *provider) GetString(key string) (string, error) { + var err error + + splits := strings.Split(key, "/") + if len(splits) < 2 { + return "", fmt.Errorf("invalid URI: %v", errors.New("vault or item missing")) + } + + client, err := connect.NewClientFromEnvironment() + if err != nil { + return "", fmt.Errorf("storage.NewClient: %v", err) + } + + p.client = client + + item, err := client.GetItem(splits[1], splits[0]) + if err != nil { + return "", fmt.Errorf("error retrieving item: %v", err) + } + + var data = make(map[string]string) + // fill map with all possible ID/Label combinations for value + for _, f := range item.Fields { + data[f.ID] = f.Value + // if no section on field (default fields on some item types) use value directly + if f.Section == nil { + data[f.Label] = f.Value + } else { + if f.Section.Label != "" { + var key = strings.Join([]string{f.Section.Label, f.Label}, ".") + data[key] = f.Value + key = strings.Join([]string{f.Section.Label, f.ID}, ".") + data[key] = f.Value + } + key = strings.Join([]string{f.Section.ID, f.Label}, ".") + data[key] = f.Value + key = strings.Join([]string{f.Section.ID, f.ID}, ".") + data[key] = f.Value + + } + } + var yamlData []byte + yamlData, err = yaml.Marshal(data) + if err != nil { + return "", fmt.Errorf("yaml.Marshal: %v", err) + } + + return (string)(yamlData), nil +} + +// Convert yaml to map interface and return the requested keys +func (p *provider) GetStringMap(key string) (map[string]interface{}, error) { + yamlData, err := p.GetString(key) + if err != nil { + fmt.Println(err) + return nil, err + } + + m := map[string]interface{}{} + + if err := yaml.Unmarshal([]byte(yamlData), &m); err != nil { + return nil, err + } + + return m, nil +} diff --git a/pkg/stringmapprovider/stringmapprovider.go b/pkg/stringmapprovider/stringmapprovider.go index 51eff27..05dd24d 100644 --- a/pkg/stringmapprovider/stringmapprovider.go +++ b/pkg/stringmapprovider/stringmapprovider.go @@ -9,6 +9,7 @@ import ( "github.com/helmfile/vals/pkg/providers/awssecrets" "github.com/helmfile/vals/pkg/providers/azurekeyvault" "github.com/helmfile/vals/pkg/providers/gcpsecrets" + "github.com/helmfile/vals/pkg/providers/onepasswordconnect" "github.com/helmfile/vals/pkg/providers/sops" "github.com/helmfile/vals/pkg/providers/ssm" "github.com/helmfile/vals/pkg/providers/vault" @@ -34,6 +35,8 @@ func New(l *log.Logger, provider api.StaticConfig) (api.LazyLoadedStringMapProvi return azurekeyvault.New(provider), nil case "awskms": return awskms.New(provider), nil + case "onepasswordconnect": + return onepasswordconnect.New(provider), nil } return nil, fmt.Errorf("failed initializing string-map provider from config: %v", provider) diff --git a/pkg/stringprovider/stringprovider.go b/pkg/stringprovider/stringprovider.go index 8a792c4..e212b6a 100644 --- a/pkg/stringprovider/stringprovider.go +++ b/pkg/stringprovider/stringprovider.go @@ -11,6 +11,7 @@ import ( "github.com/helmfile/vals/pkg/providers/gcpsecrets" "github.com/helmfile/vals/pkg/providers/gcs" "github.com/helmfile/vals/pkg/providers/gitlab" + "github.com/helmfile/vals/pkg/providers/onepasswordconnect" "github.com/helmfile/vals/pkg/providers/s3" "github.com/helmfile/vals/pkg/providers/sops" "github.com/helmfile/vals/pkg/providers/ssm" @@ -52,6 +53,8 @@ func New(l *log.Logger, provider api.StaticConfig) (api.LazyLoadedStringProvider return azurekeyvault.New(provider), nil case "gitlab": return gitlab.New(provider), nil + case "onepasswordconnect": + return onepasswordconnect.New(provider), nil } return nil, fmt.Errorf("failed initializing string provider from config: %v", provider) diff --git a/vals.go b/vals.go index 867def4..3bed7b4 100644 --- a/vals.go +++ b/vals.go @@ -30,6 +30,7 @@ import ( "github.com/helmfile/vals/pkg/providers/gcs" "github.com/helmfile/vals/pkg/providers/gitlab" "github.com/helmfile/vals/pkg/providers/googlesheets" + "github.com/helmfile/vals/pkg/providers/onepasswordconnect" "github.com/helmfile/vals/pkg/providers/s3" "github.com/helmfile/vals/pkg/providers/sops" "github.com/helmfile/vals/pkg/providers/ssm" @@ -62,25 +63,26 @@ const ( // secret cache size defaultCacheSize = 512 - ProviderVault = "vault" - ProviderS3 = "s3" - ProviderGCS = "gcs" - ProviderGitLab = "gitlab" - ProviderSSM = "awsssm" - ProviderKms = "awskms" - ProviderSecretsManager = "awssecrets" - ProviderSOPS = "sops" - ProviderEcho = "echo" - ProviderFile = "file" - ProviderGCPSecretManager = "gcpsecrets" - ProviderGoogleSheets = "googlesheets" - ProviderTFState = "tfstate" - ProviderTFStateGS = "tfstategs" - ProviderTFStateS3 = "tfstates3" - ProviderTFStateAzureRM = "tfstateazurerm" - ProviderTFStateRemote = "tfstateremote" - ProviderAzureKeyVault = "azurekeyvault" - ProviderEnvSubst = "envsubst" + ProviderVault = "vault" + ProviderS3 = "s3" + ProviderGCS = "gcs" + ProviderGitLab = "gitlab" + ProviderSSM = "awsssm" + ProviderKms = "awskms" + ProviderSecretsManager = "awssecrets" + ProviderSOPS = "sops" + ProviderEcho = "echo" + ProviderFile = "file" + ProviderGCPSecretManager = "gcpsecrets" + ProviderGoogleSheets = "googlesheets" + ProviderTFState = "tfstate" + ProviderTFStateGS = "tfstategs" + ProviderTFStateS3 = "tfstates3" + ProviderTFStateAzureRM = "tfstateazurerm" + ProviderTFStateRemote = "tfstateremote" + ProviderAzureKeyVault = "azurekeyvault" + ProviderEnvSubst = "envsubst" + ProviderOnePasswordConnect = "onepasswordconnect" ) var ( @@ -228,6 +230,9 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) { case ProviderEnvSubst: p := envsubst.New(conf) return p, nil + case ProviderOnePasswordConnect: + p := onepasswordconnect.New(conf) + return p, nil } return nil, fmt.Errorf("no provider registered for scheme %q", scheme) }