feat(ansible): support password lookups
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
1c637a2199
commit
153bf5b863
2 changed files with 153 additions and 0 deletions
128
executor.go
128
executor.go
|
|
@ -2,6 +2,7 @@ package ansible
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
|
|
@ -3625,11 +3626,138 @@ func (e *Executor) lookupValue(expr string, host string, task *Task) (any, bool)
|
|||
if value, ok := e.lookupConditionValue(arg, host, task, nil); ok {
|
||||
return value, true
|
||||
}
|
||||
case "password":
|
||||
if value, ok := e.lookupPassword(arg); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (e *Executor) lookupPassword(arg string) (string, bool) {
|
||||
spec := parsePasswordLookupSpec(arg)
|
||||
if spec.path == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
resolvedPath := e.resolveLocalPath(spec.path)
|
||||
if data, err := coreio.Local.Read(resolvedPath); err == nil {
|
||||
return strings.TrimRight(data, "\r\n"), true
|
||||
}
|
||||
|
||||
password, err := generatePassword(spec.length, spec.chars)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if resolvedPath != "/dev/null" {
|
||||
if err := coreio.Local.EnsureDir(pathDir(resolvedPath)); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if err := coreio.Local.Write(resolvedPath, password); err != nil {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
return password, true
|
||||
}
|
||||
|
||||
type passwordLookupSpec struct {
|
||||
path string
|
||||
length int
|
||||
chars string
|
||||
}
|
||||
|
||||
func parsePasswordLookupSpec(arg string) passwordLookupSpec {
|
||||
spec := passwordLookupSpec{
|
||||
length: 20,
|
||||
chars: passwordLookupCharset("ascii_letters,digits"),
|
||||
}
|
||||
|
||||
fields := strings.Fields(arg)
|
||||
for _, field := range fields {
|
||||
key, value, ok := strings.Cut(field, "=")
|
||||
if ok {
|
||||
switch lower(strings.TrimSpace(key)) {
|
||||
case "length":
|
||||
if n, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && n > 0 {
|
||||
spec.length = n
|
||||
}
|
||||
case "chars":
|
||||
if chars := passwordLookupCharset(value); chars != "" {
|
||||
spec.chars = chars
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if spec.path == "" {
|
||||
spec.path = field
|
||||
}
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
func passwordLookupCharset(value string) string {
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var chars strings.Builder
|
||||
seen := make(map[rune]bool)
|
||||
appendChars := func(set string) {
|
||||
for _, r := range set {
|
||||
if seen[r] {
|
||||
continue
|
||||
}
|
||||
seen[r] = true
|
||||
chars.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
for _, token := range strings.Split(value, ",") {
|
||||
switch lower(strings.TrimSpace(token)) {
|
||||
case "ascii_letters":
|
||||
appendChars("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
case "ascii_lowercase":
|
||||
appendChars("abcdefghijklmnopqrstuvwxyz")
|
||||
case "ascii_uppercase":
|
||||
appendChars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
case "digits":
|
||||
appendChars("0123456789")
|
||||
case "hexdigits":
|
||||
appendChars("0123456789abcdefABCDEF")
|
||||
default:
|
||||
appendChars(token)
|
||||
}
|
||||
}
|
||||
|
||||
return chars.String()
|
||||
}
|
||||
|
||||
func generatePassword(length int, chars string) (string, error) {
|
||||
if length <= 0 {
|
||||
length = 20
|
||||
}
|
||||
if chars == "" {
|
||||
chars = passwordLookupCharset("ascii_letters,digits")
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", coreerr.E("Executor.lookupPassword", "generate password", err)
|
||||
}
|
||||
|
||||
output := make([]byte, length)
|
||||
for i, b := range buf {
|
||||
output[i] = chars[int(b)%len(chars)]
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// resolveLoop resolves loop items.
|
||||
func (e *Executor) resolveLoop(loop any, host string) []any {
|
||||
return e.resolveLoopWithTask(loop, host, nil)
|
||||
|
|
|
|||
|
|
@ -910,6 +910,31 @@ func TestExecutorExtra_HandleLookup_Good_FileGlobLookup(t *testing.T) {
|
|||
}), result)
|
||||
}
|
||||
|
||||
func TestExecutorExtra_HandleLookup_Good_PasswordLookupReadsExistingFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
passPath := joinPath(dir, "secrets", "app.pass")
|
||||
require.NoError(t, writeTestFile(passPath, []byte("s3cret\n"), 0600))
|
||||
|
||||
e := NewExecutor(dir)
|
||||
result := e.handleLookup("lookup('password', 'secrets/app.pass')", "", nil)
|
||||
|
||||
assert.Equal(t, "s3cret", result)
|
||||
}
|
||||
|
||||
func TestExecutorExtra_HandleLookup_Good_PasswordLookupCreatesFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
passPath := joinPath(dir, "secrets", "generated.pass")
|
||||
|
||||
e := NewExecutor(dir)
|
||||
result := e.handleLookup("lookup('password', 'secrets/generated.pass length=12 chars=digits')", "", nil)
|
||||
|
||||
require.Len(t, result, 12)
|
||||
content, err := readTestFile(passPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, result, string(content))
|
||||
assert.Len(t, content, 12)
|
||||
}
|
||||
|
||||
func TestExecutorExtra_RunTaskOnHost_Good_LoopFromFileGlobLookup(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, writeTestFile(joinPath(dir, "files", "a.txt"), []byte("alpha"), 0644))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue