diff --git a/README.md b/README.md index d279d76..9d8ca08 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ It supports various backends including: - [Google Sheets](#google-sheets) - [SOPS](https://github.com/getsops/sops)-encrypted files - Terraform State +- 1Password - 1Password Connect - [Doppler](https://doppler.com/) - CredHub(Coming soon) @@ -220,6 +221,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](#1password) - [1Password Connect](#1password-connect) - [Doppler](#doppler) - [Pulumi State](#pulumi-state) @@ -664,6 +666,28 @@ Examples: - `ref+gitlab://gitlab.com/11111/password` - `ref+gitlab://my-gitlab.org/11111/password?ssl_verify=true&scheme=https` +### 1Password + +For this provider to work a working [service account token](https://developer.1password.com/docs/service-accounts/get-started/) is required. +The following env var has to be configured: +- `OP_SERVICE_ACCOUNT_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). + +See [Secret reference syntax](https://developer.1password.com/docs/cli/secrets-reference-syntax/) for more information. + +*Caution: vals-expressions are parsed as URIs. For the 1Password provider the host component of the URI identifies the vault. 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+op://VAULT_NAME/ITEM_NAME/FIELD_NAME` +- `ref+op://VAULT_ID/ITEM_NAME/FIELD_NAME` +- `ref+op://VAULT_NAME/ITEM_NAME/[SECTION_NAME/]FIELD_NAME` + ### 1Password Connect For this provider to work you require a working and accessible [1Password connect server](https://developer.1password.com/docs/connect). diff --git a/go.mod b/go.mod index a6b72e0..0fa83ac 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( cloud.google.com/go/secretmanager v1.13.0 cloud.google.com/go/storage v1.41.0 github.com/1Password/connect-sdk-go v1.5.3 + github.com/1password/onepassword-sdk-go v0.1.0-beta.7 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.1.0 @@ -38,6 +39,7 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect cloud.google.com/go/longrunning v0.5.7 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/extism/go-sdk v1.2.0 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/errors v0.22.0 // indirect @@ -45,9 +47,11 @@ require ( github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/strfmt v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect + github.com/tetratelabs/wazero v1.7.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect ) diff --git a/go.sum b/go.sum index 9aac0b8..c9cbeea 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg= filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE= 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/1password/onepassword-sdk-go v0.1.0-beta.7 h1:gBF2LhDTzGqwFZk2a1GZoA+8Hz7J406W7uNk84uV5I4= +github.com/1password/onepassword-sdk-go v0.1.0-beta.7/go.mod h1:yxhCMMeJs6seB45snoI2IwNvFZIuhC4xsJioZ2vnmHI= github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= @@ -178,6 +180,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/extism/go-sdk v1.2.0 h1:A0DnIMthdP8h6K9NbRpRs1PIXHOUlb/t/TZWk5eUzx4= +github.com/extism/go-sdk v1.2.0/go.mod h1:xUfKSEQndAvHBc1Ohdre0e+UdnRzUpVfbA8QLcx4fbY= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -220,6 +224,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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 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= @@ -438,6 +444,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tetratelabs/wazero v1.7.1 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8= +github.com/tetratelabs/wazero v1.7.1/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= diff --git a/pkg/providers/onepassword/onepassword.go b/pkg/providers/onepassword/onepassword.go new file mode 100644 index 0000000..0ec9159 --- /dev/null +++ b/pkg/providers/onepassword/onepassword.go @@ -0,0 +1,53 @@ +package onepassword + +import ( + "context" + "fmt" + "os" + + "github.com/1password/onepassword-sdk-go" + + "github.com/helmfile/vals/pkg/api" +) + +type provider struct { + client *onepassword.Client +} + +// New creates a new 1Password provider +func New(cfg api.StaticConfig) *provider { + p := &provider{} + + return p +} + +// Get secret string from 1Password +func (p *provider) GetString(key string) (string, error) { + var err error + + ctx := context.Background() + token := os.Getenv("OP_SERVICE_ACCOUNT_TOKEN") + + client, err := onepassword.NewClient( + ctx, + onepassword.WithServiceAccountToken(token), + onepassword.WithIntegrationInfo("Vals op integration", "v1.0.0"), + ) + if err != nil { + return "", fmt.Errorf("storage.NewClient: %v", err) + } + + p.client = client + + prefixedKey := fmt.Sprintf("op://%s", key) + item, err := p.client.Secrets.Resolve(ctx, prefixedKey) + if err != nil { + return "", fmt.Errorf("error retrieving item: %v", err) + } + + return item, nil +} + +func (p *provider) GetStringMap(key string) (map[string]interface{}, error) { + return nil, fmt.Errorf("path fragment is not supported for 1password provider") +} diff --git a/pkg/stringprovider/stringprovider.go b/pkg/stringprovider/stringprovider.go index c00b9b0..98d4506 100644 --- a/pkg/stringprovider/stringprovider.go +++ b/pkg/stringprovider/stringprovider.go @@ -17,6 +17,7 @@ import ( "github.com/helmfile/vals/pkg/providers/hcpvaultsecrets" "github.com/helmfile/vals/pkg/providers/httpjson" "github.com/helmfile/vals/pkg/providers/k8s" + "github.com/helmfile/vals/pkg/providers/onepassword" "github.com/helmfile/vals/pkg/providers/onepasswordconnect" "github.com/helmfile/vals/pkg/providers/pulumi" "github.com/helmfile/vals/pkg/providers/s3" @@ -60,6 +61,8 @@ func New(l *log.Logger, provider api.StaticConfig) (api.LazyLoadedStringProvider return azurekeyvault.New(provider), nil case "gitlab": return gitlab.New(provider), nil + case "onepassword": + return onepassword.New(provider), nil case "onepasswordconnect": return onepasswordconnect.New(provider), nil case "doppler": diff --git a/vals.go b/vals.go index 2ccb825..a76f158 100644 --- a/vals.go +++ b/vals.go @@ -37,6 +37,7 @@ import ( "github.com/helmfile/vals/pkg/providers/hcpvaultsecrets" "github.com/helmfile/vals/pkg/providers/httpjson" "github.com/helmfile/vals/pkg/providers/k8s" + "github.com/helmfile/vals/pkg/providers/onepassword" "github.com/helmfile/vals/pkg/providers/onepasswordconnect" "github.com/helmfile/vals/pkg/providers/pulumi" "github.com/helmfile/vals/pkg/providers/s3" @@ -90,6 +91,7 @@ const ( ProviderTFStateRemote = "tfstateremote" ProviderAzureKeyVault = "azurekeyvault" ProviderEnvSubst = "envsubst" + ProviderOnePassword = "op" ProviderOnePasswordConnect = "onepasswordconnect" ProviderDoppler = "doppler" ProviderPulumiStateAPI = "pulumistateapi" @@ -246,6 +248,9 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) { case ProviderEnvSubst: p := envsubst.New(conf) return p, nil + case ProviderOnePassword: + p := onepassword.New(conf) + return p, nil case ProviderOnePasswordConnect: p := onepasswordconnect.New(conf) return p, nil diff --git a/vals_onepassword_test.go b/vals_onepassword_test.go new file mode 100644 index 0000000..b8eb38a --- /dev/null +++ b/vals_onepassword_test.go @@ -0,0 +1,60 @@ +package vals + +import ( + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestValues_OnePassword_EvalTemplate(t *testing.T) { + // TODO + // 1. Create vault and item for testing and a service account + // op vault create vals-test + // op item create --vault vals-test --title=vals-test username=foo@bar.org password=secret --category=login + // op service-account create "Vals Test Service Account" --expires-in 24h --vault vals-test:read_items + + // 2. Set up the new service account token as environment variable: + // export OP_SERVICE_ACCOUNT_TOKEN=ops_xxxxxxxxx + if os.Getenv("SKIP_TESTS") != "" { + t.Skip("Skipping tests") + } + + type testcase struct { + template map[string]interface{} + expected map[string]interface{} + } + vaultName := "vals-test" + itemName := "vals-test" + + testcases := []testcase{ + { + template: map[string]interface{}{ + "foo": "FOO", + "username": fmt.Sprintf("ref+op://%s/%s/username", vaultName, itemName), + "password": fmt.Sprintf("ref+op://%s/%s/password", vaultName, itemName), + }, + expected: map[string]interface{}{ + "foo": "FOO", + "username": "foo@bar.org", + "password": "secret", + }, + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + vals, err := Eval(tc.template) + if err != nil { + t.Fatalf("%v", err) + } + + diff := cmp.Diff(tc.expected, vals) + if diff != "" { + t.Errorf("unxpected diff: %s", diff) + } + }) + } +}