add vault plugin

test a little
This commit is contained in:
Roberto Hidalgo 2023-01-10 01:02:10 -06:00
parent 52f900eaa0
commit 6f163b5e22
25 changed files with 1580 additions and 111 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
coverage.*

View File

@ -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
```

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

284
cmd/get_test.go Normal file
View File

@ -0,0 +1,284 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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)
}
}

View File

@ -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()
}

92
cmd/vault-plugin.go Normal file
View File

@ -0,0 +1,92 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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),
}),
})
},
}

View File

@ -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

25
go.mod
View File

@ -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

48
go.sum
View File

@ -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=

View File

@ -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)
}

View File

@ -0,0 +1,208 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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{}

190
internal/vault/backend.go Normal file
View File

@ -0,0 +1,190 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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<vault>([\\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
}

View File

@ -0,0 +1,79 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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)
}
}

View File

@ -0,0 +1,95 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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)
}

View File

@ -0,0 +1,82 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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
}
}

View File

@ -0,0 +1,82 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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
}

View File

@ -0,0 +1,82 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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
}

271
internal/vault/tree_test.go Normal file
View File

@ -0,0 +1,271 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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",
},
},
}
}

24
main.go
View File

@ -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)

View File

@ -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())
}

View File

@ -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)

18
test.yaml Normal file
View File

@ -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