diff --git a/mock_ssh_test.go b/mock_ssh_test.go index 64d41b0..8f11c5d 100644 --- a/mock_ssh_test.go +++ b/mock_ssh_test.go @@ -1431,6 +1431,7 @@ func modulePipWithClient(_ *Executor, client sshRunner, args map[string]any) (*T func moduleUserWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) { name := getStringArg(args, "name", "") state := getStringArg(args, "state", "present") + appendGroups := getBoolArg(args, "append", false) if name == "" { return nil, mockError("moduleUserWithClient", "user: name required") @@ -1443,38 +1444,50 @@ func moduleUserWithClient(_ *Executor, client sshRunner, args map[string]any) (* } // Build useradd/usermod command - var opts []string + var addOpts []string + var modOpts []string if uid := getStringArg(args, "uid", ""); uid != "" { - opts = append(opts, "-u", uid) + addOpts = append(addOpts, "-u", uid) + modOpts = append(modOpts, "-u", uid) } if group := getStringArg(args, "group", ""); group != "" { - opts = append(opts, "-g", group) + addOpts = append(addOpts, "-g", group) + modOpts = append(modOpts, "-g", group) } if groups := normalizeStringArgs(args["groups"]); len(groups) > 0 { - opts = append(opts, "-G", join(",", groups)) + addOpts = append(addOpts, "-G", join(",", groups)) + if appendGroups { + modOpts = append(modOpts, "-a") + } + modOpts = append(modOpts, "-G", join(",", groups)) } if home := getStringArg(args, "home", ""); home != "" { - opts = append(opts, "-d", home) + addOpts = append(addOpts, "-d", home) + modOpts = append(modOpts, "-d", home) } if shell := getStringArg(args, "shell", ""); shell != "" { - opts = append(opts, "-s", shell) + addOpts = append(addOpts, "-s", shell) + modOpts = append(modOpts, "-s", shell) } if getBoolArg(args, "system", false) { - opts = append(opts, "-r") + addOpts = append(addOpts, "-r") + modOpts = append(modOpts, "-r") } if getBoolArg(args, "create_home", true) { - opts = append(opts, "-m") + addOpts = append(addOpts, "-m") + modOpts = append(modOpts, "-m") } // Try usermod first, then useradd - optsStr := joinStrings(opts, " ") + addOptsStr := joinStrings(addOpts, " ") + modOptsStr := joinStrings(modOpts, " ") var cmd string - if optsStr == "" { + if addOptsStr == "" { cmd = sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name) } else { cmd = sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s", - name, optsStr, name, optsStr, name) + name, modOptsStr, name, addOptsStr, name) } stdout, stderr, rc, err := client.Run(context.Background(), cmd) diff --git a/modules.go b/modules.go index 423fc2b..8970dec 100644 --- a/modules.go +++ b/modules.go @@ -1388,6 +1388,7 @@ func (e *Executor) moduleSystemd(ctx context.Context, client sshExecutorClient, func (e *Executor) moduleUser(ctx context.Context, client sshExecutorClient, args map[string]any) (*TaskResult, error) { name := getStringArg(args, "name", "") state := getStringArg(args, "state", "present") + appendGroups := getBoolArg(args, "append", false) if name == "" { return nil, coreerr.E("Executor.moduleUser", "name required", nil) @@ -1400,38 +1401,50 @@ func (e *Executor) moduleUser(ctx context.Context, client sshExecutorClient, arg } // Build useradd/usermod command - var opts []string + var addOpts []string + var modOpts []string if uid := getStringArg(args, "uid", ""); uid != "" { - opts = append(opts, "-u", uid) + addOpts = append(addOpts, "-u", uid) + modOpts = append(modOpts, "-u", uid) } if group := getStringArg(args, "group", ""); group != "" { - opts = append(opts, "-g", group) + addOpts = append(addOpts, "-g", group) + modOpts = append(modOpts, "-g", group) } if groups := normalizeStringArgs(args["groups"]); len(groups) > 0 { - opts = append(opts, "-G", join(",", groups)) + addOpts = append(addOpts, "-G", join(",", groups)) + if appendGroups { + modOpts = append(modOpts, "-a") + } + modOpts = append(modOpts, "-G", join(",", groups)) } if home := getStringArg(args, "home", ""); home != "" { - opts = append(opts, "-d", home) + addOpts = append(addOpts, "-d", home) + modOpts = append(modOpts, "-d", home) } if shell := getStringArg(args, "shell", ""); shell != "" { - opts = append(opts, "-s", shell) + addOpts = append(addOpts, "-s", shell) + modOpts = append(modOpts, "-s", shell) } if getBoolArg(args, "system", false) { - opts = append(opts, "-r") + addOpts = append(addOpts, "-r") + modOpts = append(modOpts, "-r") } if getBoolArg(args, "create_home", true) { - opts = append(opts, "-m") + addOpts = append(addOpts, "-m") + modOpts = append(modOpts, "-m") } // Try usermod first, then useradd - optsStr := join(" ", opts) + addOptsStr := join(" ", addOpts) + modOptsStr := join(" ", modOpts) var cmd string - if optsStr == "" { + if addOptsStr == "" { cmd = sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name) } else { cmd = sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s", - name, optsStr, name, optsStr, name) + name, modOptsStr, name, addOptsStr, name) } stdout, stderr, rc, err := client.Run(ctx, cmd) diff --git a/modules_adv_test.go b/modules_adv_test.go index 680e573..611af3f 100644 --- a/modules_adv_test.go +++ b/modules_adv_test.go @@ -77,6 +77,24 @@ func TestModulesAdv_ModuleUser_Good_GroupListInput(t *testing.T) { assert.True(t, mock.containsSubstring("-G docker,sudo")) } +func TestModulesAdv_ModuleUser_Good_AppendSupplementaryGroups(t *testing.T) { + e, mock := newTestExecutorWithMock("host1") + mock.expectCommand(`id deploy >/dev/null 2>&1 && usermod -a -G docker,sudo deploy \|\| useradd -G docker,sudo deploy`, "", "", 0) + + result, err := e.moduleUser(context.Background(), mock, map[string]any{ + "name": "deploy", + "groups": []any{"docker", "sudo"}, + "append": true, + "create_home": false, + }) + + require.NoError(t, err) + assert.True(t, result.Changed) + assert.False(t, result.Failed) + assert.True(t, mock.hasExecuted(`usermod -a -G docker,sudo deploy`)) + assert.True(t, mock.hasExecuted(`useradd -G docker,sudo deploy`)) +} + func TestModulesAdv_ModuleUser_Good_RemoveUser(t *testing.T) { e, mock := newTestExecutorWithMock("host1") mock.expectCommand(`userdel -r deploy`, "", "", 0)