feat(ansible): support authorized_key path options

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 00:52:53 +00:00
parent ce60a583f3
commit 988c0e53ca
3 changed files with 78 additions and 8 deletions

View file

@ -1359,6 +1359,8 @@ func moduleAuthorizedKeyWithClient(_ *Executor, client sshRunner, args map[strin
key := getStringArg(args, "key", "")
state := getStringArg(args, "state", "present")
exclusive := getBoolArg(args, "exclusive", false)
manageDir := getBoolArg(args, "manage_dir", true)
pathArg := getStringArg(args, "path", "")
if user == "" || key == "" {
return nil, mockError("moduleAuthorizedKeyWithClient", "authorized_key: user and key required")
@ -1377,7 +1379,17 @@ func moduleAuthorizedKeyWithClient(_ *Executor, client sshRunner, args map[strin
}
}
authKeysPath := joinPath(home, ".ssh", "authorized_keys")
authKeysPath := pathArg
if authKeysPath == "" {
authKeysPath = joinPath(home, ".ssh", "authorized_keys")
} else if corexHasPrefix(authKeysPath, "~/") {
authKeysPath = joinPath(home, corexTrimPrefix(authKeysPath, "~/"))
} else if authKeysPath == "~" {
authKeysPath = home
}
if authKeysPath == "" {
authKeysPath = joinPath(home, ".ssh", "authorized_keys")
}
if state == "absent" {
// Remove the exact key line when present.
@ -1387,9 +1399,11 @@ func moduleAuthorizedKeyWithClient(_ *Executor, client sshRunner, args map[strin
return &TaskResult{Changed: true}, nil
}
// Ensure .ssh directory exists (best-effort)
_, _, _, _ = client.Run(context.Background(), sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
pathDir(authKeysPath), pathDir(authKeysPath), user, user, pathDir(authKeysPath)))
if manageDir {
// Ensure the parent directory exists (best-effort).
_, _, _, _ = client.Run(context.Background(), sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
pathDir(authKeysPath), pathDir(authKeysPath), user, user, pathDir(authKeysPath)))
}
if exclusive {
cmd := sprintf("printf '%%s\\n' %q > %q", key, authKeysPath)

View file

@ -2877,6 +2877,8 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client sshExecutorCl
key := getStringArg(args, "key", "")
state := getStringArg(args, "state", "present")
exclusive := getBoolArg(args, "exclusive", false)
manageDir := getBoolArg(args, "manage_dir", true)
pathArg := getStringArg(args, "path", "")
if user == "" || key == "" {
return nil, coreerr.E("Executor.moduleAuthorizedKey", "user and key required", nil)
@ -2895,7 +2897,17 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client sshExecutorCl
}
}
authKeysPath := joinPath(home, ".ssh", "authorized_keys")
authKeysPath := pathArg
if authKeysPath == "" {
authKeysPath = joinPath(home, ".ssh", "authorized_keys")
} else if corexHasPrefix(authKeysPath, "~/") {
authKeysPath = joinPath(home, corexTrimPrefix(authKeysPath, "~/"))
} else if authKeysPath == "~" {
authKeysPath = home
}
if authKeysPath == "" {
authKeysPath = joinPath(home, ".ssh", "authorized_keys")
}
if state == "absent" {
// Remove the exact key line when present.
@ -2905,9 +2917,11 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client sshExecutorCl
return &TaskResult{Changed: true}, nil
}
// Ensure .ssh directory exists (best-effort)
_, _, _, _ = client.Run(ctx, sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
pathDir(authKeysPath), pathDir(authKeysPath), user, user, pathDir(authKeysPath)))
if manageDir {
// Ensure the parent directory exists (best-effort).
_, _, _, _ = client.Run(ctx, sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
pathDir(authKeysPath), pathDir(authKeysPath), user, user, pathDir(authKeysPath)))
}
if exclusive {
cmd := sprintf("printf '%%s\\n' %q > %q", key, authKeysPath)

View file

@ -433,6 +433,48 @@ func TestModulesAdv_ModuleAuthorizedKey_Good_ExclusiveRewritesFile(t *testing.T)
assert.False(t, mock.hasExecuted(`grep -qF`))
}
func TestModulesAdv_ModuleAuthorizedKey_Good_CustomPath(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0)
mock.expectCommand(`mkdir -p "/srv/keys"`, "", "", 0)
mock.expectCommand(`grep -qF`, "", "", 1)
mock.expectCommand(`echo`, "", "", 0)
mock.expectCommand(`chmod 600`, "", "", 0)
result, err := moduleAuthorizedKeyWithClient(e, mock, map[string]any{
"user": "deploy",
"key": testKey,
"path": "/srv/keys/deploy_keys",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("/srv/keys/deploy_keys"))
assert.True(t, mock.hasExecuted(`mkdir -p "/srv/keys"`))
}
func TestModulesAdv_ModuleAuthorizedKey_Good_ManageDirDisabled(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0)
mock.expectCommand(`grep -qF`, "", "", 1)
mock.expectCommand(`echo`, "", "", 0)
mock.expectCommand(`chmod 600`, "", "", 0)
result, err := moduleAuthorizedKeyWithClient(e, mock, map[string]any{
"user": "deploy",
"key": testKey,
"manage_dir": false,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.False(t, mock.hasExecuted(`mkdir -p`))
}
func TestModulesAdv_ModuleAuthorizedKey_Good_RootUserFallback(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... admin@host"