diff --git a/executor.go b/executor.go index 267fb16..dcd9abb 100644 --- a/executor.go +++ b/executor.go @@ -3,11 +3,14 @@ package ansible import ( "context" "crypto/rand" + "crypto/sha256" "encoding/base64" + "encoding/binary" "errors" "io" "io/fs" "maps" + mathrand "math/rand" "os" "path" "path/filepath" @@ -3628,7 +3631,7 @@ func (e *Executor) lookupValue(expr string, host string, task *Task) (any, bool) return value, true } case "password": - if value, ok := e.lookupPassword(arg); ok { + if value, ok := e.lookupPassword(arg, host, task); ok { return value, true } 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.path = e.resolveLookupPasswordValue(spec.path, host, task) + spec.seed = e.resolveLookupPasswordValue(spec.seed, host, task) 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 + if resolvedPath != "/dev/null" { + 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 { return "", false } @@ -3774,10 +3781,28 @@ func (e *Executor) lookupPassword(arg string) (string, bool) { 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 { path string length int chars string + seed string } func parsePasswordLookupSpec(arg string) passwordLookupSpec { @@ -3799,6 +3824,8 @@ func parsePasswordLookupSpec(arg string) passwordLookupSpec { if chars := passwordLookupCharset(value); chars != "" { spec.chars = chars } + case "seed": + spec.seed = value } continue } @@ -3848,7 +3875,7 @@ func passwordLookupCharset(value string) string { return chars.String() } -func generatePassword(length int, chars string) (string, error) { +func generatePassword(length int, chars, seed string) (string, error) { if length <= 0 { length = 20 } @@ -3856,6 +3883,17 @@ func generatePassword(length int, chars string) (string, error) { 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) if _, err := rand.Read(buf); err != nil { return "", coreerr.E("Executor.lookupPassword", "generate password", err) diff --git a/executor_extra_test.go b/executor_extra_test.go index c1b7d1e..213bb14 100644 --- a/executor_extra_test.go +++ b/executor_extra_test.go @@ -947,6 +947,19 @@ func TestExecutorExtra_HandleLookup_Good_PasswordLookupCreatesFile(t *testing.T) 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) { dir := t.TempDir() require.NoError(t, writeTestFile(joinPath(dir, "defaults", "common.yml"), []byte("common: true\n"), 0644))