feat(ansible): support seeded password lookups
Some checks are pending
CI / test (push) Waiting to run
CI / auto-fix (push) Waiting to run
CI / auto-merge (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 14:51:44 +00:00
parent 28ef1f3d85
commit c276c343bc
2 changed files with 57 additions and 6 deletions

View file

@ -3,11 +3,14 @@ package ansible
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/binary"
"errors" "errors"
"io" "io"
"io/fs" "io/fs"
"maps" "maps"
mathrand "math/rand"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -3628,7 +3631,7 @@ func (e *Executor) lookupValue(expr string, host string, task *Task) (any, bool)
return value, true return value, true
} }
case "password": case "password":
if value, ok := e.lookupPassword(arg); ok { if value, ok := e.lookupPassword(arg, host, task); ok {
return value, true return value, true
} }
case "first_found": case "first_found":
@ -3746,18 +3749,22 @@ func firstFoundTerms(value any) ([]string, []string) {
} }
} }
func (e *Executor) lookupPassword(arg string) (string, bool) { func (e *Executor) lookupPassword(arg string, host string, task *Task) (string, bool) {
spec := parsePasswordLookupSpec(arg) spec := parsePasswordLookupSpec(arg)
spec.path = e.resolveLookupPasswordValue(spec.path, host, task)
spec.seed = e.resolveLookupPasswordValue(spec.seed, host, task)
if spec.path == "" { if spec.path == "" {
return "", false return "", false
} }
resolvedPath := e.resolveLocalPath(spec.path) resolvedPath := e.resolveLocalPath(spec.path)
if data, err := coreio.Local.Read(resolvedPath); err == nil { if resolvedPath != "/dev/null" {
return strings.TrimRight(data, "\r\n"), true if data, err := coreio.Local.Read(resolvedPath); err == nil {
return strings.TrimRight(data, "\r\n"), true
}
} }
password, err := generatePassword(spec.length, spec.chars) password, err := generatePassword(spec.length, spec.chars, spec.seed)
if err != nil { if err != nil {
return "", false return "", false
} }
@ -3774,10 +3781,28 @@ func (e *Executor) lookupPassword(arg string) (string, bool) {
return password, true return password, true
} }
func (e *Executor) resolveLookupPasswordValue(value string, host string, task *Task) string {
value = corexTrimSpace(value)
if value == "" {
return ""
}
if resolved, ok := e.lookupConditionValue(value, host, task, nil); ok {
return sprintf("%v", resolved)
}
if strings.Contains(value, "{{") {
return e.templateString(value, host, task)
}
return value
}
type passwordLookupSpec struct { type passwordLookupSpec struct {
path string path string
length int length int
chars string chars string
seed string
} }
func parsePasswordLookupSpec(arg string) passwordLookupSpec { func parsePasswordLookupSpec(arg string) passwordLookupSpec {
@ -3799,6 +3824,8 @@ func parsePasswordLookupSpec(arg string) passwordLookupSpec {
if chars := passwordLookupCharset(value); chars != "" { if chars := passwordLookupCharset(value); chars != "" {
spec.chars = chars spec.chars = chars
} }
case "seed":
spec.seed = value
} }
continue continue
} }
@ -3848,7 +3875,7 @@ func passwordLookupCharset(value string) string {
return chars.String() return chars.String()
} }
func generatePassword(length int, chars string) (string, error) { func generatePassword(length int, chars, seed string) (string, error) {
if length <= 0 { if length <= 0 {
length = 20 length = 20
} }
@ -3856,6 +3883,17 @@ func generatePassword(length int, chars string) (string, error) {
chars = passwordLookupCharset("ascii_letters,digits") chars = passwordLookupCharset("ascii_letters,digits")
} }
if seed != "" {
sum := sha256.Sum256([]byte(seed))
seedValue := int64(binary.LittleEndian.Uint64(sum[:8]))
r := mathrand.New(mathrand.NewSource(seedValue))
buf := make([]byte, length)
for i := range buf {
buf[i] = chars[r.Intn(len(chars))]
}
return string(buf), nil
}
buf := make([]byte, length) buf := make([]byte, length)
if _, err := rand.Read(buf); err != nil { if _, err := rand.Read(buf); err != nil {
return "", coreerr.E("Executor.lookupPassword", "generate password", err) return "", coreerr.E("Executor.lookupPassword", "generate password", err)

View file

@ -947,6 +947,19 @@ func TestExecutorExtra_HandleLookup_Good_PasswordLookupCreatesFile(t *testing.T)
assert.Len(t, content, 12) assert.Len(t, content, 12)
} }
func TestExecutorExtra_HandleLookup_Good_PasswordLookupHonoursSeed(t *testing.T) {
dir := t.TempDir()
e := NewExecutor(dir)
first := e.handleLookup("lookup('password', '/dev/null length=16 chars=ascii_lowercase seed=inventory_hostname')", "host1", nil)
second := e.handleLookup("lookup('password', '/dev/null length=16 chars=ascii_lowercase seed=inventory_hostname')", "host1", nil)
other := e.handleLookup("lookup('password', '/dev/null length=16 chars=ascii_lowercase seed=inventory_hostname')", "host2", nil)
assert.Len(t, first, 16)
assert.Equal(t, first, second)
assert.NotEqual(t, first, other)
}
func TestExecutorExtra_HandleLookup_Good_FirstFoundLookupReturnsFirstExistingPath(t *testing.T) { func TestExecutorExtra_HandleLookup_Good_FirstFoundLookupReturnsFirstExistingPath(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
require.NoError(t, writeTestFile(joinPath(dir, "defaults", "common.yml"), []byte("common: true\n"), 0644)) require.NoError(t, writeTestFile(joinPath(dir, "defaults", "common.yml"), []byte("common: true\n"), 0644))