diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0163820 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage.* diff --git a/README.md b/README.md index 40e774e..c44e61e 100644 --- a/README.md +++ b/README.md @@ -113,15 +113,18 @@ joao flush NAME [--dry-run] [--redact] joao fetch NAME [--dry-run] # check for differences between local and remote items joao diff PATH [--cache] -# initialize a new joao repo -joao repo init [PATH] -# list the item names within prefix -joao repo list [PREFIX] # print the repo config root -joao repo root -# -joao repo status -joao repo filter clean FILE -joao repo filter diff PATH OLD_FILE OLD_SHA OLD_MODE NEW_FILE NEW_SHA NEW_MODE -joao repo filter smudge FILE +# tbd +# initialize a new joao repo +# joao repo init [PATH] +# list the item names within prefix +# joao repo list [PREFIX] +# joao repo root +# joao repo status +# joao repo filter clean FILE +# joao repo filter diff PATH OLD_FILE OLD_SHA OLD_MODE NEW_FILE NEW_SHA NEW_MODE +# joao repo filter smudge FILE + +# get instructions to run as a vault plugin: +joao vault server --help ``` diff --git a/cmd/diff.go b/cmd/diff.go index 48f87b7..11be67c 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -3,17 +3,12 @@ package cmd import ( - "git.rob.mx/nidito/chinampa" "git.rob.mx/nidito/chinampa/pkg/command" "git.rob.mx/nidito/joao/pkg/config" "github.com/sirupsen/logrus" ) -func init() { - chinampa.Register(diffCommand) -} - -var diffCommand = (&command.Command{ +var Diff = &command.Command{ Path: []string{"diff"}, Summary: "Shows differences between local and remote configs", Description: `Fetches remote and compares against local, ignoring comments but respecting order.`, @@ -57,4 +52,4 @@ var diffCommand = (&command.Command{ logrus.Info("Done") return nil }, -}).SetBindings() +} diff --git a/cmd/fetch.go b/cmd/fetch.go index 05edaa8..c5843f1 100644 --- a/cmd/fetch.go +++ b/cmd/fetch.go @@ -7,17 +7,12 @@ import ( "io/fs" "os" - "git.rob.mx/nidito/chinampa" "git.rob.mx/nidito/chinampa/pkg/command" "git.rob.mx/nidito/joao/pkg/config" "github.com/sirupsen/logrus" ) -func init() { - chinampa.Register(fetchCommand) -} - -var fetchCommand = (&command.Command{ +var Fetch = &command.Command{ Path: []string{"fetch"}, Summary: "fetches configuration values from 1Password", Description: `Fetches secrets for local ﹅CONFIG﹅ files from 1Password.`, @@ -79,4 +74,4 @@ var fetchCommand = (&command.Command{ logrus.Info("Done") return nil }, -}).SetBindings() +} diff --git a/cmd/flush.go b/cmd/flush.go index 196a544..d3c7f24 100644 --- a/cmd/flush.go +++ b/cmd/flush.go @@ -5,18 +5,13 @@ package cmd import ( "fmt" - "git.rob.mx/nidito/chinampa" "git.rob.mx/nidito/chinampa/pkg/command" opclient "git.rob.mx/nidito/joao/internal/op-client" "git.rob.mx/nidito/joao/pkg/config" "github.com/sirupsen/logrus" ) -func init() { - chinampa.Register(flushCommand) -} - -var flushCommand = (&command.Command{ +var Flush = &command.Command{ Path: []string{"flush"}, Summary: "flush configuration values to 1Password", Description: `Creates or updates existing items for every ﹅CONFIG﹅ file provided. Does not delete 1Password items.`, @@ -58,4 +53,4 @@ var flushCommand = (&command.Command{ logrus.Info("Done") return nil }, -}).SetBindings() +} diff --git a/cmd/get.go b/cmd/get.go index 1948e2e..50b5649 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -7,17 +7,12 @@ import ( "fmt" "strings" - "git.rob.mx/nidito/chinampa" "git.rob.mx/nidito/chinampa/pkg/command" "git.rob.mx/nidito/joao/pkg/config" "gopkg.in/yaml.v3" ) -func init() { - chinampa.Register(gCommand) -} - -var gCommand = (&command.Command{ +var Get = &command.Command{ Path: []string{"get"}, Summary: "retrieves configuration", Description: ` @@ -60,10 +55,12 @@ looks at the filesystem or remotely, using 1password (over the CLI if available, "redacted": { Description: "Do not print secret values", Type: "bool", + Default: false, }, "remote": { Description: "Get values from 1password", Type: "bool", + Default: false, }, }, Action: func(cmd *command.Command) error { @@ -136,4 +133,4 @@ looks at the filesystem or remotely, using 1password (over the CLI if available, _, err = cmd.Cobra.OutOrStdout().Write(bytes) return err }, -}).SetBindings() +} diff --git a/cmd/get_test.go b/cmd/get_test.go new file mode 100644 index 0000000..de0b80e --- /dev/null +++ b/cmd/get_test.go @@ -0,0 +1,284 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package cmd_test + +import ( + "bytes" + "os" + "path" + "runtime" + "strings" + "testing" + + . "git.rob.mx/nidito/joao/cmd" + "github.com/spf13/cobra" +) + +func fromProjectRoot() string { + _, filename, _, _ := runtime.Caller(0) + dir := path.Join(path.Dir(filename), "../") + if err := os.Chdir(dir); err != nil { + panic(err) + } + wd, _ := os.Getwd() + return wd +} + +func TestGetRedacted(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", true, "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--redacted"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected, err := os.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file: %s", err) + } + + got := out.String() + if strings.TrimSpace(got) != strings.ReplaceAll(strings.TrimSpace(string(expected)), " very secret", "") { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetNormal(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", false, "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", "."}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected, err := os.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file: %s", err) + } + + got := out.String() + if strings.TrimSpace(got) != strings.TrimSpace(string(expected)) { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetPath(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", false, "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", "nested.secret"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := "very secret" + got := out.String() + if strings.TrimSpace(got) != strings.TrimSpace(expected) { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetPathCollection(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", false, "") + cmd.Flags().StringP("output", "o", "yaml", "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", "nested", "--output", "yaml"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := `bool: true +int: 1 +secret: very secret +string: quem` + + got := out.String() + if strings.TrimSpace(got) != expected { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetDiff(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", false, "") + cmd.Flags().StringP("output", "o", "diff-yaml", "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--output", "diff-yaml"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := `_config: !!joao + name: some:test + vault: example +bool: false +int: 1 +list: + - one + - two + - three +nested: + bool: true + int: 1 + secret: !!secret very secret + string: quem +secret: !!secret very secret +string: "pato"` + + got := out.String() + if strings.TrimSpace(got) != expected { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetJSON(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", false, "") + cmd.Flags().StringP("output", "o", "json", "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--output", "json"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"secret":"very secret","string":"quem"},"secret":"very secret","string":"pato"}` + + got := out.String() + if strings.TrimSpace(got) != expected { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetJSONPathScalar(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", false, "") + cmd.Flags().StringP("output", "o", "json", "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", "nested.secret", "--output", "json"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := `very secret` + + got := out.String() + if strings.TrimSpace(got) != expected { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetJSONPathCollection(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", false, "") + cmd.Flags().StringP("output", "o", "json", "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", "nested", "--output", "json"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := `{"bool":true,"int":1,"secret":"very secret","string":"quem"}` + + got := out.String() + if strings.TrimSpace(got) != expected { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetJSONRedacted(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", true, "") + cmd.Flags().StringP("output", "o", "json", "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--output", "json", "--redacted"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"secret":"","string":"quem"},"secret":"","string":"pato"}` + + got := out.String() + if strings.TrimSpace(got) != expected { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetJSONOP(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().StringP("output", "o", "op", "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--output", "op"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := `{"id":"","title":"some:test","vault":{"id":"example"},"category":"PASSWORD","sections":[{"id":"~annotations","label":"~annotations"},{"id":"nested","label":"nested"}],"fields":[{"id":"password","type":"CONCEALED","purpose":"PASSWORD","label":"password","value":"56615e9be5f0ce5f97d5b446faaa1d39f95a13a1ea8326ae933c3d29eb29735c"},{"id":"notesPlain","type":"STRING","purpose":"NOTES","label":"notesPlain","value":"flushed by joao"},{"id":"~annotations.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"int","value":"int"},{"id":"int","type":"STRING","label":"int","value":"1"},{"id":"string","type":"STRING","label":"string","value":"pato"},{"id":"~annotations.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"bool","value":"bool"},{"id":"bool","type":"STRING","label":"bool","value":"false"},{"id":"~annotations.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"secret","value":"secret"},{"id":"secret","type":"CONCEALED","label":"secret","value":"very secret"},{"id":"nested.string","section":{"id":"nested"},"type":"STRING","label":"string","value":"quem"},{"id":"~annotations.nested.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.int","value":"int"},{"id":"nested.int","section":{"id":"nested"},"type":"STRING","label":"int","value":"1"},{"id":"~annotations.nested.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.secret","value":"secret"},{"id":"nested.secret","section":{"id":"nested"},"type":"CONCEALED","label":"secret","value":"very secret"},{"id":"~annotations.nested.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.bool","value":"bool"},{"id":"nested.bool","section":{"id":"nested"},"type":"STRING","label":"bool","value":"true"},{"id":"list.0","section":{"id":"list"},"type":"STRING","label":"0","value":"one"},{"id":"list.1","section":{"id":"list"},"type":"STRING","label":"1","value":"two"},{"id":"list.2","section":{"id":"list"},"type":"STRING","label":"2","value":"three"}],"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z"}` + + got := out.String() + if strings.TrimSpace(got) != expected { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} diff --git a/cmd/set.go b/cmd/set.go index 14720a8..a6d708d 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -8,18 +8,13 @@ import ( "os" "strings" - "git.rob.mx/nidito/chinampa" "git.rob.mx/nidito/chinampa/pkg/command" opclient "git.rob.mx/nidito/joao/internal/op-client" "git.rob.mx/nidito/joao/pkg/config" "github.com/sirupsen/logrus" ) -func init() { - chinampa.Register(setCommand) -} - -var setCommand = (&command.Command{ +var Set = &command.Command{ Path: []string{"set"}, Summary: "updates configuration values", Description: ` @@ -141,4 +136,4 @@ Will read values from stdin (or ﹅--from﹅ a file) and store it at the ﹅PATH logrus.Info("Done") return err }, -}).SetBindings() +} diff --git a/cmd/vault-plugin.go b/cmd/vault-plugin.go new file mode 100644 index 0000000..9bec0fb --- /dev/null +++ b/cmd/vault-plugin.go @@ -0,0 +1,92 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package cmd + +import ( + "git.rob.mx/nidito/chinampa/pkg/command" + "git.rob.mx/nidito/joao/internal/vault" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/plugin" +) + +var Plugin = &command.Command{ + Path: []string{"vault", "server"}, + Summary: "Starts a vault-joao-plugin server", + Description: `Runs ﹅joao﹅ as a vault plugin. + +You'll need to install ﹅joao﹅ in the machine running ﹅vault﹅ to ﹅plugin_directory﹅ as specified by vault's config. The installed ﹅joao﹅ executable needs to be executable for the user running vault only. + +### Configuration +﹅﹅﹅sh +export VAULT_PLUGIN_DIR=/var/lib/vault/plugins +chmod 700 "$VAULT_PLUGIN_DIR/joao" +export PLUGIN_SHA="$(openssl dgst -sha256 -hex "$VAULT_PLUGIN_DIR/joao" | awk '{print $2}')" +export VERSION="$($VAULT_PLUGIN_DIR/joao --version)" + +# register +vault plugin register -sha256="$PLUGIN_SHA" -command=joao -args="vault,server" -version="$VERSION" secret joao + +# configure, add ﹅vault﹅ to set a default vault for querying +vault write config/1password "host=$OP_CONNECT_HOST" "token=$OP_CONNECT_TOKEN" # vault=my-default-vault + +if !vault plugin list secret | grep -c -m1 '^joao ' >/dev/null; then + # first time, let's enable the secrets backend + vault secrets enable --path=config joao +else + # updating from a previous version + vault secrets tune -plugin-version="$VERSION" config/ + vault plugin reload -plugin joao +fi +﹅﹅﹅ + +### Vault API + +﹅﹅﹅sh +# VAULT is optional if configured with a default ﹅vault﹅. See above + +# vault read config/tree/[VAULT/]ITEM +vault read config/tree/service:api +vault read config/tree/prod/service:api + +# vault list config/trees/[VAULT/] +vault list config/trees +vault list config/trees/prod +﹅﹅﹅ +`, + Options: command.Options{ + "ca-cert": { + Type: command.ValueTypeString, + Description: "See https://pkg.go.dev/github.com/hashicorp/vault/api#TLSConfig", + }, + "ca-path": { + Type: command.ValueTypeString, + Description: "See https://pkg.go.dev/github.com/hashicorp/vault/api#TLSConfig", + }, + "client-cert": { + Type: command.ValueTypeString, + Description: "See https://pkg.go.dev/github.com/hashicorp/vault/api#TLSConfig", + }, + "client-key": { + Type: command.ValueTypeString, + Description: "See https://pkg.go.dev/github.com/hashicorp/vault/api#TLSConfig", + }, + "tls-skip-verify": { + Type: command.ValueTypeBoolean, + Description: "See https://pkg.go.dev/github.com/hashicorp/vault/api#TLSConfig", + Default: false, + }, + }, + Action: func(cmd *command.Command) error { + return plugin.ServeMultiplex(&plugin.ServeOpts{ + BackendFactoryFunc: vault.Factory, + TLSProviderFunc: api.VaultPluginTLSProvider(&api.TLSConfig{ + CACert: cmd.Options["ca-cert"].ToString(), + CAPath: cmd.Options["ca-path"].ToString(), + ClientCert: cmd.Options["client-cert"].ToString(), + ClientKey: cmd.Options["client-key"].ToString(), + TLSServerName: "", + Insecure: cmd.Options["tls-skip-verify"].ToValue().(bool), + }), + }) + }, +} diff --git a/docs/letter-to-secret-santa.md b/docs/letter-to-secret-santa.md index e7adf98..38a2263 100644 --- a/docs/letter-to-secret-santa.md +++ b/docs/letter-to-secret-santa.md @@ -27,8 +27,8 @@ smtp: There's many entries in that tree, and the SMTP password value would be addressed with: - `joao get api/config.prod.yaml smtp.password` -- `op item get op://prod/api/smtp.password` -- `vault kv read config/kv/prod/api/smtp.password` +- `op item get op://prod/api/smtp/password` +- `vault kv read -field=smtp.password config/tree/prod/api` ## Source of truth is hard diff --git a/go.mod b/go.mod index 9ee8751..64630f2 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,16 @@ go 1.18 // replace git.rob.mx/nidito/chinampa => /Users/roberto/src/chinampa require ( - git.rob.mx/nidito/chinampa v0.0.0-20221231055324-8ea5f42ef848 + git.rob.mx/nidito/chinampa v0.0.0-20230102065449-d9b257e145ce github.com/1Password/connect-sdk-go v1.5.0 github.com/alessio/shellescape v1.4.1 - github.com/hashicorp/errwrap v1.1.0 github.com/hashicorp/go-hclog v1.4.0 github.com/hashicorp/vault/api v1.8.2 github.com/hashicorp/vault/sdk v0.6.2 - github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/jellydator/ttlcache/v3 v3.0.1 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.6.1 - golang.org/x/crypto v0.4.0 + golang.org/x/crypto v0.5.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -30,18 +29,19 @@ require ( github.com/dlclark/regexp2 v1.7.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/color v1.13.0 // indirect - github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.4.8 // indirect - github.com/hashicorp/go-retryablehttp v0.7.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.2 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect @@ -56,7 +56,7 @@ require ( github.com/leodido/go-urn v1.2.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/microcosm-cc/bluemonday v1.0.21 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -80,12 +80,13 @@ require ( github.com/yuin/goldmark v1.5.3 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect go.uber.org/atomic v1.10.0 // indirect - golang.org/x/net v0.4.0 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/term v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/net v0.5.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.4.0 // indirect + golang.org/x/term v0.4.0 // indirect + golang.org/x/text v0.6.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect + google.golang.org/genproto v0.0.0-20230109162033-3c3c17ce83e6 // indirect google.golang.org/grpc v1.51.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect diff --git a/go.sum b/go.sum index c50a0cd..d294938 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -git.rob.mx/nidito/chinampa v0.0.0-20221231055324-8ea5f42ef848 h1:Nvyo7qK6oVLWQ2aHRtQ5AAMcVEue51Wr+hxBF4OzMkE= -git.rob.mx/nidito/chinampa v0.0.0-20221231055324-8ea5f42ef848/go.mod h1:jZwWmhBRfjJjp2jwM/+jIGgfWLQPudgAah+wKCKjBfk= +git.rob.mx/nidito/chinampa v0.0.0-20230102065449-d9b257e145ce h1:fKG3wUdPgsviY2mE79vhXL4CalNdvhkL6vDAtdyVt0I= +git.rob.mx/nidito/chinampa v0.0.0-20230102065449-d9b257e145ce/go.mod h1:obhWsLkUIlKJyhfa7uunrSs2O44JBqsegSAtAvY2LRM= github.com/1Password/connect-sdk-go v1.5.0 h1:F0WJcLSzGg3iXEDY49/ULdszYKsQLGTzn+2cyYXqiyk= github.com/1Password/connect-sdk-go v1.5.0/go.mod h1:TdynFeyvaRoackENbJ8RfJokH+WAowAu1MLmUbdMq6s= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -60,8 +60,9 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= @@ -92,7 +93,6 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= @@ -109,8 +109,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM= github.com/hashicorp/go-plugin v1.4.8/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= -github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= +github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmkiSY5xuju57czJ/IJQ= @@ -141,6 +141,8 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jellydator/ttlcache/v3 v3.0.1 h1:cHgCSMS7TdQcoprXnWUptJZzyFsqs18Lt8VVhRuZYVU= +github.com/jellydator/ttlcache/v3 v3.0.1/go.mod h1:WwTaEmcXQ3MTjOm4bsZoDFiCu/hMvNWLO1w67RXz6h4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -169,8 +171,9 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= @@ -209,8 +212,6 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -278,12 +279,13 @@ github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGj go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -292,6 +294,7 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -300,12 +303,14 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -326,23 +331,24 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= @@ -351,8 +357,8 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70= -google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230109162033-3c3c17ce83e6 h1:uUn6GsgKK2eCI0bWeRMgRCcqDaQXYDuB+5tXA5Xeg/8= +google.golang.org/genproto v0.0.0-20230109162033-3c3c17ce83e6/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/internal/op-client/connect.go b/internal/op-client/connect.go index 4dc43f1..9e3e031 100644 --- a/internal/op-client/connect.go +++ b/internal/op-client/connect.go @@ -37,31 +37,6 @@ func NewConnect(host, token string) *Connect { return &Connect{client: client} } -// func (b *Connect) getVaultId(vaultIdentifier string) (string, error) { -// if !IsValidClientUUID(vaultIdentifier) { -// vaults, err := b.client.GetVaultsByTitle(vaultIdentifier) -// if err != nil { -// return "", err -// } - -// if len(vaults) == 0 { -// return "", fmt.Errorf("no vaults found with identifier %q", vaultIdentifier) -// } - -// oldestVault := vaults[0] -// if len(vaults) > 1 { -// for _, returnedVault := range vaults { -// if returnedVault.CreatedAt.Before(oldestVault.CreatedAt) { -// oldestVault = returnedVault -// } -// } -// logrus.Infof("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultIdentifier, oldestVault.ID) -// } -// vaultIdentifier = oldestVault.ID -// } -// return vaultIdentifier, nil -// } - func (b *Connect) Get(vault, name string) (*op.Item, error) { return b.client.GetItem(name, vault) } diff --git a/internal/op-client/mock/opconnect.go b/internal/op-client/mock/opconnect.go new file mode 100644 index 0000000..4e025f8 --- /dev/null +++ b/internal/op-client/mock/opconnect.go @@ -0,0 +1,208 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package mock + +import ( + "fmt" + "math/rand" + "time" + + "github.com/1Password/connect-sdk-go/connect" + "github.com/1Password/connect-sdk-go/onepassword" +) + +const Host = "http://localhost:8080" +const Token = "test_token" + +var randPool = []rune("0123456789abcdefghijklmnopqrstuvwxyz") +var items = map[string]*onepassword.Item{} + +func Add(item *onepassword.Item) *onepassword.Item { + item.ID = itemID() + items[item.ID] = item + return item +} + +func Update(item *onepassword.Item) *onepassword.Item { + items[item.ID] = item + return item +} + +func Clear() { + items = map[string]*onepassword.Item{} +} + +func Delete(key string) { + delete(items, key) +} + +var ( + Vaults = []onepassword.Vault{ + { + ID: "aabbccddeeffgghhiijjkkllmm", + Name: "Zeroth Vault", + }, + { + ID: "00011122233344455566677788", + Name: "First Vault", + }, + } +) + +type Client struct{} + +func (m *Client) GetVaults() ([]onepassword.Vault, error) { + return Vaults, nil +} + +func (m *Client) GetVault(uuid string) (*onepassword.Vault, error) { + for _, v := range Vaults { + if v.Name == uuid || v.ID == uuid { + return &v, nil + } + } + + return nil, nil +} + +func (m *Client) GetVaultByUUID(uuid string) (*onepassword.Vault, error) { + return m.GetVault(uuid) +} + +func (m *Client) GetVaultByTitle(title string) (*onepassword.Vault, error) { + return m.GetVault(title) +} + +func (m *Client) GetVaultsByTitle(uuid string) ([]onepassword.Vault, error) { + res := []onepassword.Vault{} + for _, v := range Vaults { + if v.Name == uuid || v.ID == uuid { + res = append(res, v) + } + } + + return res, nil +} + +func (m *Client) GetItems(vaultQuery string) ([]onepassword.Item, error) { + res := []onepassword.Item{} + for _, item := range items { + if item.Vault.ID == vaultQuery { + res = append(res, *item) + } + } + return res, nil +} + +func (m *Client) GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error) { + return get(itemQuery, vaultQuery) +} + +func (m *Client) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) { + return get(uuid, vaultQuery) +} + +func (m *Client) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) { + return get(title, vaultQuery) +} + +func (m *Client) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) { + res := []onepassword.Item{} + for _, v := range items { + if v.Title == title { + res = append(res, *v) + } + } + + return res, nil +} + +func (m *Client) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { + item.CreatedAt = time.Now() + item.Vault.ID = vaultQuery + return Add(item), nil +} + +func (m *Client) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { + return Update(item), nil +} + +func (m *Client) DeleteItem(item *onepassword.Item, vaultQuery string) error { + return deleteItem(item, vaultQuery) +} + +func (m *Client) DeleteItemByID(itemUUID string, vaultQuery string) error { + item, err := get(itemUUID, vaultQuery) + if err != nil { + return err + } + return deleteItem(item, vaultQuery) +} + +func (m *Client) DeleteItemByTitle(title string, vaultQuery string) error { + item, err := get(title, vaultQuery) + if err != nil { + return err + } + return deleteItem(item, vaultQuery) +} + +func (m *Client) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) { + return nil, nil +} + +func (m *Client) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) { + return nil, nil +} + +func (m *Client) GetFileContent(file *onepassword.File) ([]byte, error) { + return nil, nil +} + +func (m *Client) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) { + return "", nil +} + +func (m *Client) LoadStructFromItemByUUID(config any, itemUUID string, vaultQuery string) error { + return nil +} + +func (m *Client) LoadStructFromItemByTitle(config any, itemTitle string, vaultQuery string) error { + return nil +} + +func (m *Client) LoadStructFromItem(config any, itemQuery string, vaultQuery string) error { + return nil +} + +func (m *Client) LoadStruct(config any) error { + return nil +} + +func itemID() string { + b := make([]rune, 26) + for i := range b { + b[i] = randPool[rand.Intn(len(randPool))] // nolint: gosec + } + return string(b) +} + +func get(itemUUID, vaultUUID string) (*onepassword.Item, error) { + for _, item := range items { + if (item.ID == itemUUID || item.Title == itemUUID) && item.Vault.ID == vaultUUID { + return item, nil + } + } + + return nil, fmt.Errorf("could not retrieve item with id %s in vault %s", itemUUID, vaultUUID) +} + +func deleteItem(item *onepassword.Item, vaultUUID string) error { + if item.Vault.ID != vaultUUID { + return fmt.Errorf("could not delete item: %s: not found in vault %s", item.Title, vaultUUID) + } + Delete(item.ID) + return nil +} + +var _ connect.Client = &Client{} diff --git a/internal/vault/backend.go b/internal/vault/backend.go new file mode 100644 index 0000000..e46fc39 --- /dev/null +++ b/internal/vault/backend.go @@ -0,0 +1,190 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package vault + +import ( + "context" + "fmt" + "net/http" + "time" + + "git.rob.mx/nidito/joao/internal/vault/middleware" + "git.rob.mx/nidito/joao/pkg/version" + "github.com/1Password/connect-sdk-go/connect" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" + ttlcache "github.com/jellydator/ttlcache/v3" +) + +const ( + userAgent = "joao/%s" +) + +type backend struct { + *framework.Backend + configCache *ttlcache.Cache[string, string] + client *connect.Client +} + +var ConnectClientFactory func(s logical.Storage) (connect.Client, error) = onePasswordConnectClient + +// Factory returns a new backend as logical.Backend. +func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { + b := Backend() + if err := b.Setup(ctx, conf); err != nil { + return nil, err + } + return b, nil +} + +type clientCallback func(client connect.Client, r *logical.Request, fd *framework.FieldData) (*logical.Response, error) + +func withClient(b *backend, callback clientCallback) framework.OperationFunc { + return func(ctx context.Context, r *logical.Request, fd *framework.FieldData) (*logical.Response, error) { + client, err := b.Client(r.Storage) + if err != nil { + return nil, fmt.Errorf("plugin is not configured: %s", err) + } + + return callback(client, r, fd) + } +} + +func itemPattern(name string) string { + return fmt.Sprintf("(?P<%s>\\w(([\\w-.:]+)?\\w)?)", name) +} + +func optionalVaultPattern(suffix string) string { + return fmt.Sprintf("(?P([\\w:]+)%s)?", suffix) +} + +func Backend() *backend { + var b = &backend{ + configCache: ttlcache.New( + ttlcache.WithTTL[string, string](5 * time.Minute), + ), + } + + b.Backend = &framework.Backend{ + BackendType: logical.TypeCredential, + Help: "joao reads configuration entries from 1Password Connect", + PathsSpecial: &logical.Paths{ + SealWrapStorage: []string{ + middleware.ConfigPath, + }, + }, + Paths: framework.PathAppend( + []*framework.Path{ + { + Pattern: middleware.ConfigPath, + HelpSynopsis: "Configures the connection to a 1Password Connect Server", + HelpDescription: "Provide a `host` and `token`, with an optional default `vault` to query 1Password Connect at", + Fields: map[string]*framework.FieldSchema{ + "host": { + Type: framework.TypeString, + Description: "The address for the 1Password Connect server", + }, + "token": { + Type: framework.TypeString, + Description: "A 1Password Connect token", + }, + "vault": { + Type: framework.TypeString, + Description: "An optional vault id or name to use for queries", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: middleware.ReadConfig, + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: func(ctx context.Context, r *logical.Request, fd *framework.FieldData) (*logical.Response, error) { + res, err := middleware.WriteConfig(ctx, r, fd) + if err != nil { + return nil, err + } + + b.client = nil + if _, err := b.Client(r.Storage); err != nil { + return nil, err + } + return res, nil + }, + }, + }, + }, + { + Pattern: "trees/" + optionalVaultPattern(""), + HelpSynopsis: `List configuration trees`, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: withClient(b, middleware.ListTrees), + Summary: "List available entries", + }, + }, + Fields: map[string]*framework.FieldSchema{ + "vault": { + Type: framework.TypeString, + Description: "Specifies the id of the vault to list from.", + Required: true, + }, + }, + }, + { + Pattern: "tree/" + optionalVaultPattern("/") + itemPattern("id"), + HelpSynopsis: `Returns a configuration tree`, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: withClient(b, middleware.ReadTree), + Summary: "Retrieve nested key values from specified item", + }, + }, + Fields: map[string]*framework.FieldSchema{ + "id": { + Type: framework.TypeString, + Description: "The item name or id to read", + Required: true, + }, + "vault": { + Type: framework.TypeString, + Description: "The vault name or id to read from", + Required: true, + }, + }, + }, + }, + ), + Secrets: []*framework.Secret{}, + } + + return b +} + +func (b *backend) Client(s logical.Storage) (connect.Client, error) { + if b.client != nil { + return *b.client, nil + } + + client, err := ConnectClientFactory(s) + if err != nil { + return nil, err + } + b.client = &client + return client, nil +} + +func onePasswordConnectClient(s logical.Storage) (connect.Client, error) { + config, err := middleware.ConfigFromStorage(context.Background(), s) + if err != nil { + return nil, fmt.Errorf("error retrieving config for client: %w", err) + } + + if config == nil { + return nil, fmt.Errorf("no config set for backend, write host, token and vault to [mount]/1password") + } + + http.DefaultClient.Timeout = 15 * time.Second + client := connect.NewClientWithUserAgent(config.Host, config.Token, fmt.Sprintf(userAgent, version.Version)) + + return client, nil +} diff --git a/internal/vault/backend_test.go b/internal/vault/backend_test.go new file mode 100644 index 0000000..2b7e117 --- /dev/null +++ b/internal/vault/backend_test.go @@ -0,0 +1,79 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package vault_test + +import ( + "context" + "testing" + + "git.rob.mx/nidito/joao/internal/op-client/mock" + "git.rob.mx/nidito/joao/internal/vault" + "git.rob.mx/nidito/joao/internal/vault/middleware" + "github.com/1Password/connect-sdk-go/connect" + "github.com/hashicorp/vault/sdk/logical" +) + +func init() { + vault.ConnectClientFactory = func(s logical.Storage) (connect.Client, error) { + return &mock.Client{}, nil + } +} + +func TestConfiguredBackend(t *testing.T) { + b, reqStorage := getBackend(t) + + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: middleware.ConfigPath, + Storage: reqStorage, + }) + + if err != nil && resp.IsError() { + t.Fatalf("Unexpected error with config set: %s => %v", err, resp) + } + + if resp.Data["token"] != mock.Token { + t.Errorf("Found unknown token: %s", resp.Data["token"]) + } + + if resp.Data["host"] != mock.Host { + t.Errorf("Found unknown host: %s", resp.Data["host"]) + } + + if resp.Data["vault"] != mock.Vaults[0].ID { + t.Errorf("Found unknown vault: %s", resp.Data["vault"]) + } +} + +func TestUnconfiguredBackend(t *testing.T) { + b, reqStorage := getUnconfiguredBackend(t) + + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: middleware.ConfigPath, + Storage: reqStorage, + }) + + if err != nil && resp.IsError() { + t.Fatalf("Unexpected error with unconfigured: %s => %v", err, resp) + } + + if resp != nil { + t.Fatalf("Found a response where none was expected: %v", resp) + } + + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "tree/someItem", + Storage: reqStorage, + }) + + if err == nil && !resp.IsError() { + t.Fatalf("Expected error with no config set: %v", resp) + } + + expected := middleware.ErrorNoVaultProvided.Error() + if actual := err.Error(); actual != expected { + t.Fatalf("unconfigured client threw wrong error: \nwanted: %s\ngot: %s", expected, actual) + } +} diff --git a/internal/vault/config_test.go b/internal/vault/config_test.go new file mode 100644 index 0000000..4a0c02e --- /dev/null +++ b/internal/vault/config_test.go @@ -0,0 +1,95 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package vault_test + +import ( + "context" + "testing" + + "git.rob.mx/nidito/joao/internal/op-client/mock" + "github.com/hashicorp/vault/sdk/logical" +) + +func TestConfigEmpty(t *testing.T) { + b, reqStorage := getUnconfiguredBackend(t) + + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "1password", + Storage: reqStorage, + }) + + if err != nil { + t.Fatalf("Could not issue request: %s", err) + } + + if resp != nil { + t.Fatalf("got response, expected none %v", resp) + } +} + +func TestConfigDefault(t *testing.T) { + b, reqStorage := getBackend(t) + + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "1password", + Storage: reqStorage, + }) + + if err != nil { + t.Fatalf("Could not issue request: %s", err) + } + + if resp.IsError() { + t.Fatalf("get request threw error: %s", resp.Error()) + } + + if len(resp.Data) == 0 { + t.Fatal("got no response, expected something!") + } + + mapsEqual(t, resp.Data, map[string]any{"host": mock.Host, "token": mock.Token, "vault": mock.Vaults[0].ID}) +} + +func TestConfigUpdate(t *testing.T) { + b, reqStorage := getBackend(t) + expected := map[string]any{ + "host": "mira", + "token": "un", + "vault": "salmón", + } + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "1password", + Data: expected, + Storage: reqStorage, + }) + + if err != nil { + t.Fatalf("Could not issue update request: %s", err) + } + + if resp != nil && resp.IsError() { + t.Fatal(resp.Error()) + } + + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "1password", + Storage: reqStorage, + }) + if err != nil { + t.Fatalf("Could not issue read after update request: %s", err) + } + + if resp.IsError() { + t.Fatalf("get after update request threw error: %s", resp.Error()) + } + + if len(resp.Data) == 0 { + t.Fatal("got no response on get after update, expected something!") + } + + mapsEqual(t, resp.Data, expected) +} diff --git a/internal/vault/helper_test.go b/internal/vault/helper_test.go new file mode 100644 index 0000000..2b4c99c --- /dev/null +++ b/internal/vault/helper_test.go @@ -0,0 +1,82 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package vault_test + +import ( + "context" + "encoding/json" + "testing" + "time" + + "git.rob.mx/nidito/joao/internal/op-client/mock" + "git.rob.mx/nidito/joao/internal/vault" + "git.rob.mx/nidito/joao/internal/vault/middleware" + "github.com/1Password/connect-sdk-go/connect" + hclog "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/logical" +) + +func mapsEqual(t *testing.T, actual, expected map[string]any) { + for key, want := range expected { + if have, ok := actual[key]; !ok || want != have { + t.Fail() + t.Errorf(`field mismatch for "%v". \nwanted: %v\ngot: %v"`, key, want, have) + } + } + if t.Failed() { + t.FailNow() + } +} + +func testConfig() *logical.BackendConfig { + config := logical.TestBackendConfig() + config.StorageView = new(logical.InmemStorage) + config.Logger = hclog.NewNullLogger() + config.System = &logical.StaticSystemView{ + DefaultLeaseTTLVal: 1 * time.Hour, + MaxLeaseTTLVal: 2 * time.Hour, + } + return config +} + +func getBackend(tb testing.TB) (logical.Backend, logical.Storage) { + tb.Helper() + cfg := testConfig() + ctx := context.Background() + + data, err := json.Marshal(map[string]string{"host": mock.Host, "token": mock.Token, "vault": mock.Vaults[0].ID}) + if err != nil { + tb.Fatalf("Could not serialize config for client: %s", err) + } + + _ = cfg.StorageView.Put(ctx, &logical.StorageEntry{ + Key: middleware.ConfigPath, + Value: data, + }) + + setOnePassswordConnectMocks() + b, err := vault.Factory(context.Background(), cfg) + + if err != nil { + tb.Fatal(err) + } + return b, cfg.StorageView +} + +func getUnconfiguredBackend(tb testing.TB) (logical.Backend, logical.Storage) { + tb.Helper() + cfg := testConfig() + setOnePassswordConnectMocks() + b, err := vault.Factory(context.Background(), cfg) + + if err != nil { + tb.Fatal(err) + } + return b, cfg.StorageView +} + +func setOnePassswordConnectMocks() { + vault.ConnectClientFactory = func(s logical.Storage) (connect.Client, error) { + return &mock.Client{}, nil + } +} diff --git a/internal/vault/middleware/config.go b/internal/vault/middleware/config.go new file mode 100644 index 0000000..8109862 --- /dev/null +++ b/internal/vault/middleware/config.go @@ -0,0 +1,82 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package middleware + +import ( + "context" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +const ( + ConfigPath = "1password" +) + +type Config struct { + Host string `json:"host"` + Token string `json:"token"` + Vault string `json:"vault"` +} + +func ConfigFromStorage(ctx context.Context, s logical.Storage) (*Config, error) { + entry, err := s.Get(ctx, ConfigPath) + if err != nil || entry == nil { + return nil, err + } + + var config Config + if err := entry.DecodeJSON(&config); err != nil { + return nil, err + } + + return &config, nil +} + +func ReadConfig(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + cfg, err := ConfigFromStorage(ctx, req.Storage) + if err != nil || cfg == nil { + return nil, err + } + + return &logical.Response{ + Data: map[string]any{ + "host": cfg.Host, + "token": cfg.Token, + "vault": cfg.Vault, + }, + }, nil +} + +func WriteConfig(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + existing, err := ConfigFromStorage(ctx, req.Storage) + if err != nil { + return nil, err + } + if existing == nil { + existing = &Config{} + } + + if host, ok := data.GetOk("host"); ok { + existing.Host = host.(string) + } + + if token, ok := data.GetOk("token"); ok { + existing.Token = token.(string) + } + + if opVault, ok := data.GetOk("vault"); ok { + existing.Vault = opVault.(string) + } + + entry, err := logical.StorageEntryJSON(ConfigPath, existing) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(ctx, entry); err != nil { + return nil, err + } + + return nil, nil +} diff --git a/internal/vault/middleware/tree.go b/internal/vault/middleware/tree.go new file mode 100644 index 0000000..740c160 --- /dev/null +++ b/internal/vault/middleware/tree.go @@ -0,0 +1,82 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package middleware + +import ( + "context" + "fmt" + "strings" + + "git.rob.mx/nidito/joao/pkg/config" + "github.com/1Password/connect-sdk-go/connect" + "gopkg.in/yaml.v3" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +var ErrorNoVaultProvided = fmt.Errorf("no vault has been specified, provide one reading from MOUNT/tree/VAULT/ITEM, or configure a default writing to MOUNT/1password") + +func vaultName(data *framework.FieldData, storage logical.Storage) (vault string, err error) { + if vaultI, ok := data.GetOk("vault"); ok { + vault := strings.TrimSuffix(vaultI.(string), "/") + if vault != "" { + return vault, nil + } + } + + config, err := ConfigFromStorage(context.Background(), storage) + if err != nil { + return "", fmt.Errorf("could not get config from storage: %w", err) + } + + if config != nil && config.Vault != "" { + return config.Vault, nil + } + + return "", ErrorNoVaultProvided +} + +func ReadTree(client connect.Client, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + vault, err := vaultName(data, req.Storage) + if err != nil { + return nil, err + } + + item, err := client.GetItem(data.Get("id").(string), vault) + if err != nil { + return nil, fmt.Errorf("could not retrieve item: %w", err) + } + + tree := config.NewEntry("root", yaml.MappingNode) + + if err := tree.FromOP(item.Fields); err != nil { + return nil, err + } + + return &logical.Response{ + Data: tree.AsMap().(map[string]any), + }, nil +} + +func ListTrees(client connect.Client, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + vault, err := vaultName(data, req.Storage) + if err != nil { + return nil, err + } + + items, err := client.GetItems(vault) + if err != nil { + return nil, fmt.Errorf("could not list items: %w", err) + } + + retMap := map[string]any{} + retList := []string{} + for _, item := range items { + key := fmt.Sprintf("%s %s", item.Title, item.ID) + retMap[key] = item.ID + retList = append(retList, key) + } + + return logical.ListResponseWithInfo(retList, retMap), nil +} diff --git a/internal/vault/tree_test.go b/internal/vault/tree_test.go new file mode 100644 index 0000000..ce925d0 --- /dev/null +++ b/internal/vault/tree_test.go @@ -0,0 +1,271 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package vault_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "git.rob.mx/nidito/joao/internal/op-client/mock" + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/hashicorp/vault/sdk/logical" +) + +func getTestBackendWithConfig(t *testing.T) (logical.Backend, logical.Storage) { + t.Helper() + return getBackend(t) +} + +func TestReadEntry(t *testing.T) { + b, reqStorage := getTestBackendWithConfig(t) + mock.Clear() + item := mock.Add(generateConfigItem("service:test")) + expected := map[string]any{ + "boolean": false, + "integer": 42, + "list": []string{"first item", "second item"}, + "nested": map[string]any{ + "boolean": true, + "integer": 42, + "string": "this is a string", + }, + } + expectedJSON, _ := json.Marshal(expected) + + t.Run("with default vault", func(t *testing.T) { + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: fmt.Sprintf("tree/%v", item.Title), + Storage: reqStorage, + }) + + if err != nil { + t.Fatal("read request failed:", err) + } + + if resp == nil { + t.Fatal("Item missing") + } + + if resp.IsError() { + t.Fatal(resp.Error()) + } + + gotJSON, _ := json.Marshal(resp.Data) + + if string(expectedJSON) != string(gotJSON) { + t.Fatalf("unexpectedJSON response.\nwanted: %s\ngot: %s", string(expectedJSON), string(gotJSON)) + } + }) + + t.Run("with explicit vault", func(t *testing.T) { + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: fmt.Sprintf("tree/%s/%s", item.Vault.ID, item.Title), + Storage: reqStorage, + }) + + if err != nil { + t.Fatal("read request failed:", err) + } + + if resp == nil { + t.Fatal("Item missing") + } + + if resp.IsError() { + t.Fatal(resp.Error()) + } + + gotJSON, _ := json.Marshal(resp.Data) + + if string(expectedJSON) != string(gotJSON) { + t.Fatalf("unexpectedJSON response.\nwanted: %s\ngot: %s", string(expectedJSON), string(gotJSON)) + } + }) +} + +func TestListEntries(t *testing.T) { + b, reqStorage := getTestBackendWithConfig(t) + mock.Clear() + item := mock.Add(generateConfigItem("service:test")) + + expected := map[string]any{ + "keys": []string{ + "service:test " + item.ID, + }, + "key_info": map[string]string{ + "service:test " + item.ID: item.ID, + }, + } + + expectedJSON, _ := json.Marshal(expected) + t.Run("with default vault", func(t *testing.T) { + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ListOperation, + Path: "trees/", + Storage: reqStorage, + }) + + if err != nil { + t.Fatal(err) + } + + if resp.IsError() { + t.Fatal(resp.Error()) + } + + gotJSON, _ := json.Marshal(resp.Data) + + if string(expectedJSON) != string(gotJSON) { + t.Fatalf("unexpectedJSON response.\nwanted: %s\ngot: %s", string(expectedJSON), string(gotJSON)) + } + }) + + t.Run("with explicit vault", func(t *testing.T) { + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ListOperation, + Path: "trees/" + item.Vault.ID, + Storage: reqStorage, + }) + + if err != nil { + t.Fatal(err) + } + + if resp.IsError() { + t.Fatal(resp.Error()) + } + + gotJSON, _ := json.Marshal(resp.Data) + + if string(expectedJSON) != string(gotJSON) { + t.Fatalf("unexpectedJSON response.\nwanted: %s\ngot: %s", string(expectedJSON), string(gotJSON)) + } + }) + + t.Run("with explicit unknown vault", func(t *testing.T) { + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ListOperation, + Path: "trees/asdf", + Storage: reqStorage, + }) + + if err != nil { + t.Fatal(err) + } + + if resp.IsError() { + t.Fatal(resp.Error()) + } + + gotJSON, _ := json.Marshal(resp.Data) + + if string(gotJSON) != "{}" { + t.Fatalf("unexpectedJSON response, wanted %s, got %s", "{}", string(gotJSON)) + } + }) +} + +func generateConfigItem(title string) *onepassword.Item { + return &onepassword.Item{ + Category: "password", + Title: title, + Vault: onepassword.ItemVault{ + ID: mock.Vaults[0].ID, + }, + Fields: []*onepassword.ItemField{ + { + ID: "nested.string", + Type: "STRING", + Section: &onepassword.ItemSection{ID: "nested", Label: "nested"}, + Label: "string", + Value: "this is a string", + }, + { + ID: "nested.boolean", + Type: "STRING", + Section: &onepassword.ItemSection{ID: "nested", Label: "nested"}, + Label: "boolean", + Value: "true", + }, + { + ID: "nested.integer", + Type: "STRING", + Section: &onepassword.ItemSection{ID: "nested", Label: "nested"}, + Label: "integer", + Value: "42", + }, + { + ID: "list.0", + Type: "STRING", + Section: &onepassword.ItemSection{ID: "list", Label: "list"}, + Label: "0", + Value: "first item", + }, + { + ID: "list.1", + Type: "STRING", + Section: &onepassword.ItemSection{ID: "list", Label: "list"}, + Label: "1", + Value: "second item", + }, + { + ID: "boolean", + Type: "STRING", + Label: "boolean", + Value: "false", + }, + { + ID: "integer", + Type: "STRING", + Label: "integer", + Value: "42", + }, + { + ID: "~annotations.integer", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "integer", + Value: "int", + }, + { + ID: "~annotations.boolean", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "boolean", + Value: "bool", + }, + { + ID: "~annotations.nested.integer", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "nested.integer", + Value: "int", + }, + { + ID: "~annotations.nested.boolean", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "nested.boolean", + Value: "bool", + }, + }, + Sections: []*onepassword.ItemSection{ + { + ID: "~annotations", + Label: "~annotations", + }, + { + ID: "nested", + Label: "nested", + }, + { + ID: "list", + Label: "list", + }, + }, + } +} diff --git a/main.go b/main.go index ce6ac16..3067523 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( "git.rob.mx/nidito/chinampa" "git.rob.mx/nidito/chinampa/pkg/runtime" - _ "git.rob.mx/nidito/joao/cmd" + "git.rob.mx/nidito/joao/cmd" "git.rob.mx/nidito/joao/pkg/version" "github.com/sirupsen/logrus" ) @@ -24,11 +24,25 @@ func main() { logrus.Debug("Debugging enabled") } + chinampa.Register(cmd.Get) + chinampa.Register(cmd.Set) + chinampa.Register(cmd.Diff) + chinampa.Register(cmd.Fetch) + chinampa.Register(cmd.Flush) + chinampa.Register(cmd.Plugin) + if err := chinampa.Execute(chinampa.Config{ - Name: "joao", - Summary: "Helps organize config for roberto", - Description: `﹅joao﹅ makes yaml, json, 1password and vault play along nicely.`, - Version: version.Version, + Name: "joao", + Summary: "A very WIP configuration manager", + Description: `﹅joao﹅ makes yaml, json, 1Password and Hashicorp Vault play along nicely. + +Keeps config entries encoded as YAML in the filesystem, backs it up to 1Password, and syncs scrubbed copies to git. Robots consume entries via 1Password Connect + Vault. + +Schema for configuration and non-secret values live along the code, and are pushed to remote origins. Secrets can optionally and temporally be flushed to disk for editing or other sorts of operations. Git filters are available to prevent secrets from being pushed to remotes. Secrets are grouped into files, and every file gets its own 1Password item. + +Secret values are specified using the ﹅!!secret﹅ YAML tag. +`, + Version: version.Version, }); err != nil { logrus.Errorf("total failure: %s", err) os.Exit(2) diff --git a/pkg/config/config.go b/pkg/config/config.go index 19f50eb..a02b248 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -180,7 +180,11 @@ func (cfg *Config) DiffRemote(path string, stdout io.Writer, stderr io.Writer) e diff.Stdout = stdout diff.Stderr = stderr - diff.Run() + + if err := diff.Run(); err != nil { + return fmt.Errorf("diff could not run: %w", err) + } + if diff.ProcessState.ExitCode() > 2 { return fmt.Errorf("diff exited with exit code %d", diff.ProcessState.ExitCode()) } diff --git a/pkg/config/entry.go b/pkg/config/entry.go index 4619603..c0f99ff 100644 --- a/pkg/config/entry.go +++ b/pkg/config/entry.go @@ -255,10 +255,13 @@ func (e *Entry) FromOP(fields []*op.ItemField) error { valueStr := data[label] var style yaml.Style var tag string + kind := "" if annotations[label] == "secret" { style = yaml.TaggedStyle tag = YAMLTypeSecret + } else if k, ok := annotations[label]; ok { + kind = "!!" + k } path := strings.Split(label, ".") @@ -272,6 +275,7 @@ func (e *Entry) FromOP(fields []*op.ItemField) error { existing.Tag = tag existing.Kind = yaml.ScalarNode existing.Path = path + existing.Type = kind break } @@ -281,6 +285,7 @@ func (e *Entry) FromOP(fields []*op.ItemField) error { Value: valueStr, Style: style, Tag: tag, + Type: kind, } if isNumeric(key) { // logrus.Debugf("hydrating sequence value at %s", path) diff --git a/test.yaml b/test.yaml new file mode 100644 index 0000000..25cd8a8 --- /dev/null +++ b/test.yaml @@ -0,0 +1,18 @@ +_config: !!joao + name: some:test + vault: example +# not sorted on purpose +int: 1 # line +# foot +string: "pato" +bool: false +secret: !!secret very secret +nested: + string: quem + int: 1 + secret: !!secret very secret + bool: true +list: + - one + - two + - three