From e6258f37ac886c43bf3bca1f823ed728fcf59fad Mon Sep 17 00:00:00 2001 From: William Petit Date: Fri, 29 Jul 2022 17:47:11 +0200 Subject: [PATCH] feat: ansible-vault values auto retagging in yaml format Using the YAML encoder, Formidable is now capable of detecting et retagging ansible-vault [1] encrypted values. You can use the query parameter 'ansible_vault=no' to disable this behavior. [1] https://docs.ansible.com/ansible/latest/user_guide/vault.html --- Makefile | 9 +- internal/data/format/yaml/encoder_handler.go | 61 ++++++++++++- internal/data/format/yaml/encoder_test.go | 88 +++++++++++++++++++ internal/data/format/yaml/testdata/vault.txt | 1 + .../blocks/form_input_string.html.tmpl | 8 +- 5 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 internal/data/format/yaml/encoder_test.go create mode 100644 internal/data/format/yaml/testdata/vault.txt diff --git a/Makefile b/Makefile index e366eda..10ad314 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ GORELEASER_VERSION ?= v1.8.3 GORELEASER_ARGS ?= --auto-snapshot --rm-dist GITCHLOG_ARGS ?= SHELL := /bin/bash +RUN_INSTALL_TESTS ?= yes .PHONY: help help: ## Display this help @@ -15,8 +16,12 @@ help: ## Display this help watch: deps ## Watching updated files - live reload ( set -o allexport && source .env && set +o allexport && go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest ) -.PHONY: help -test: test-go test-install-script ## Executing tests +.PHONY: test +test: test-go ## Executing tests + +ifeq ($(RUN_INSTALL_TESTS), yes) +test: test-install-script +endif test-go: deps ( set -o allexport && source .env && set +o allexport && go test -v -race -count=1 $(GOTEST_ARGS) ./... ) diff --git a/internal/data/format/yaml/encoder_handler.go b/internal/data/format/yaml/encoder_handler.go index 809b11e..e68bbce 100644 --- a/internal/data/format/yaml/encoder_handler.go +++ b/internal/data/format/yaml/encoder_handler.go @@ -6,12 +6,15 @@ import ( "net/url" "path" "path/filepath" + "strings" "forge.cadoles.com/wpetit/formidable/internal/data/format" "github.com/pkg/errors" "gopkg.in/yaml.v3" ) +const YAMLTagAnsibleVaultValuesQueryParam = "ansible_vault" + type EncoderHandler struct{} func (d *EncoderHandler) Match(url *url.URL) bool { @@ -22,17 +25,69 @@ func (d *EncoderHandler) Match(url *url.URL) bool { } func (d *EncoderHandler) Encode(url *url.URL, data interface{}) (io.Reader, error) { - var buf bytes.Buffer + var output bytes.Buffer - encoder := yaml.NewEncoder(&buf) + encoder := yaml.NewEncoder(&output) if err := encoder.Encode(data); err != nil { return nil, errors.WithStack(err) } - return &buf, nil + if shouldTransformAnsibleVault(url) { + if err := tagAnsibleVaultValues(&output); err != nil { + return nil, errors.WithStack(err) + } + } + + return &output, nil } func NewEncoderHandler() *EncoderHandler { return &EncoderHandler{} } + +func shouldTransformAnsibleVault(url *url.URL) bool { + return !url.Query().Has(YAMLTagAnsibleVaultValuesQueryParam) || url.Query().Get(YAMLTagAnsibleVaultValuesQueryParam) == "yes" +} + +func tagAnsibleVaultValues(buf *bytes.Buffer) error { + decoder := yaml.NewDecoder(buf) + + var node yaml.Node + + if err := decoder.Decode(&node); err != nil { + return errors.WithStack(err) + } + + walkNodeTree(&node, func(node *yaml.Node) { + isAnsibleVaultNode := node.Tag == "!!str" && strings.HasPrefix(strings.TrimSpace(node.Value), "$ANSIBLE_VAULT") + if !isAnsibleVaultNode { + return + } + + node.Tag = "!vault" + node.Style = yaml.LiteralStyle | yaml.TaggedStyle + }) + + buf.Reset() + + encoder := yaml.NewEncoder(buf) + + if err := encoder.Encode(node.Content[0]); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func walkNodeTree(node *yaml.Node, fn func(node *yaml.Node)) { + fn(node) + + if node.Content == nil { + return + } + + for _, sub := range node.Content { + walkNodeTree(sub, fn) + } +} diff --git a/internal/data/format/yaml/encoder_test.go b/internal/data/format/yaml/encoder_test.go new file mode 100644 index 0000000..5979fdc --- /dev/null +++ b/internal/data/format/yaml/encoder_test.go @@ -0,0 +1,88 @@ +package yaml + +import ( + "fmt" + "io" + "net/url" + "os" + "os/exec" + "strings" + "testing" + + "github.com/pkg/errors" + yaml "gopkg.in/yaml.v3" +) + +// YAML string containing an ansible-vault encrypted variable +const ansibleVaultYAML = ` +unencrypted: foo +encrypted: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 63393636613562663937383964323839376239663230366130386566393131313963386265303632 + 3133356532346437653338343032303732646530303431660a383862353766326334306138613734 + 36313438626564623435373365616531353533663765663335616134656430323134323537336661 + 3437653863343331370a393136653735643333373962633631663539653664313936303964303866 + 3933 +` + +func TestEncoderAnsibleVault(t *testing.T) { + _, err := exec.LookPath("ansible") + if err != nil { + t.Skip("The 'ansible' command seems not to be available on this system. Skipping.") + + return + } + + var data interface{} + + if err := yaml.Unmarshal([]byte(ansibleVaultYAML), &data); err != nil { + t.Fatal(errors.WithStack(err)) + } + + encoder := NewEncoderHandler() + + url, err := url.Parse("stdout://local.yml?ansible_vault=yes") + if err != nil { + t.Fatal(errors.WithStack(err)) + } + + reader, err := encoder.Encode(url, data) + if err != nil { + t.Fatal(errors.WithStack(err)) + } + + temp, err := os.CreateTemp(os.TempDir(), "formidable_test_*.yml") + if err != nil { + t.Fatal(errors.WithStack(err)) + } + + defer func() { + if err := os.Remove(temp.Name()); err != nil { + panic(errors.WithStack(err)) + } + }() + + t.Logf("Writing encoded YAML content in file '%s'...", temp.Name()) + + if _, err := io.Copy(temp, reader); err != nil { + t.Fatal(errors.WithStack(err)) + } + + args := []string{ + "localhost", + "-m", "debug", + "--vault-password-file", "./testdata/vault.txt", + "-e", fmt.Sprintf("@%s", temp.Name()), + "-a", "var=encrypted", + } + + t.Logf("Running command 'ansible %s'", strings.Join(args, " ")) + + cmd := exec.Command("ansible", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + t.Fatal(errors.WithStack(err)) + } +} diff --git a/internal/data/format/yaml/testdata/vault.txt b/internal/data/format/yaml/testdata/vault.txt new file mode 100644 index 0000000..b7e8e2b --- /dev/null +++ b/internal/data/format/yaml/testdata/vault.txt @@ -0,0 +1 @@ +formidable \ No newline at end of file diff --git a/internal/server/template/blocks/form_input_string.html.tmpl b/internal/server/template/blocks/form_input_string.html.tmpl index f7750d9..7bd5275 100644 --- a/internal/server/template/blocks/form_input_string.html.tmpl +++ b/internal/server/template/blocks/form_input_string.html.tmpl @@ -1,9 +1,13 @@ {{define "form_input_string"}} {{ $fullProperty := getFullProperty .Parent .Property }} {{ $value := getValue .Defaults .Values $fullProperty }} - + value="{{ $value }}" /> */}} + {{end}} \ No newline at end of file