add vault plugin
test a little
This commit is contained in:
parent
52f900eaa0
commit
6f163b5e22
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
coverage.*
|
23
README.md
23
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
|
||||
```
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
11
cmd/get.go
11
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()
|
||||
}
|
||||
|
284
cmd/get_test.go
Normal file
284
cmd/get_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
92
cmd/vault-plugin.go
Normal 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),
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
@ -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
25
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
|
||||
|
48
go.sum
48
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=
|
||||
|
@ -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)
|
||||
}
|
||||
|
208
internal/op-client/mock/opconnect.go
Normal file
208
internal/op-client/mock/opconnect.go
Normal 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
190
internal/vault/backend.go
Normal 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
|
||||
}
|
79
internal/vault/backend_test.go
Normal file
79
internal/vault/backend_test.go
Normal 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)
|
||||
}
|
||||
}
|
95
internal/vault/config_test.go
Normal file
95
internal/vault/config_test.go
Normal 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)
|
||||
}
|
82
internal/vault/helper_test.go
Normal file
82
internal/vault/helper_test.go
Normal 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
|
||||
}
|
||||
}
|
82
internal/vault/middleware/config.go
Normal file
82
internal/vault/middleware/config.go
Normal 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
|
||||
}
|
82
internal/vault/middleware/tree.go
Normal file
82
internal/vault/middleware/tree.go
Normal 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
271
internal/vault/tree_test.go
Normal 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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
20
main.go
20
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,10 +24,24 @@ 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.`,
|
||||
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)
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user