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
This commit is contained in:
wpetit 2022-07-29 17:47:11 +02:00 committed by Bornholm
parent bd70c1b91a
commit e6258f37ac
5 changed files with 160 additions and 7 deletions

View File

@ -7,6 +7,7 @@ GORELEASER_VERSION ?= v1.8.3
GORELEASER_ARGS ?= --auto-snapshot --rm-dist GORELEASER_ARGS ?= --auto-snapshot --rm-dist
GITCHLOG_ARGS ?= GITCHLOG_ARGS ?=
SHELL := /bin/bash SHELL := /bin/bash
RUN_INSTALL_TESTS ?= yes
.PHONY: help .PHONY: help
help: ## Display this help help: ## Display this help
@ -15,8 +16,12 @@ help: ## Display this help
watch: deps ## Watching updated files - live reload 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 ) ( set -o allexport && source .env && set +o allexport && go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest )
.PHONY: help .PHONY: test
test: test-go test-install-script ## Executing tests test: test-go ## Executing tests
ifeq ($(RUN_INSTALL_TESTS), yes)
test: test-install-script
endif
test-go: deps test-go: deps
( set -o allexport && source .env && set +o allexport && go test -v -race -count=1 $(GOTEST_ARGS) ./... ) ( set -o allexport && source .env && set +o allexport && go test -v -race -count=1 $(GOTEST_ARGS) ./... )

View File

@ -6,12 +6,15 @@ import (
"net/url" "net/url"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"forge.cadoles.com/wpetit/formidable/internal/data/format" "forge.cadoles.com/wpetit/formidable/internal/data/format"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
const YAMLTagAnsibleVaultValuesQueryParam = "ansible_vault"
type EncoderHandler struct{} type EncoderHandler struct{}
func (d *EncoderHandler) Match(url *url.URL) bool { 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) { 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 { if err := encoder.Encode(data); err != nil {
return nil, errors.WithStack(err) 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 { func NewEncoderHandler() *EncoderHandler {
return &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)
}
}

View File

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

View File

@ -0,0 +1 @@
formidable

View File

@ -1,9 +1,13 @@
{{define "form_input_string"}} {{define "form_input_string"}}
{{ $fullProperty := getFullProperty .Parent .Property }} {{ $fullProperty := getFullProperty .Parent .Property }}
{{ $value := getValue .Defaults .Values $fullProperty }} {{ $value := getValue .Defaults .Values $fullProperty }}
<input type="text" {{/* <input type="text"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
name="{{ $fullProperty }}" name="{{ $fullProperty }}"
id="{{ $fullProperty }}" id="{{ $fullProperty }}"
value="{{ $value }}" /> value="{{ $value }}" /> */}}
<textarea
name="{{ $fullProperty }}"
id="{{ $fullProperty }}"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">{{ $value }}</textarea>
{{end}} {{end}}