Add a manual test to exercise homectl prompt logic (#41954)

This commit is contained in:
Lennart Poettering
2026-05-06 09:21:17 +02:00
committed by GitHub
5 changed files with 390 additions and 256 deletions
+244
View File
@@ -0,0 +1,244 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <stdio.h>
#include <unistd.h>
#include "alloc-util.h"
#include "bitfield.h"
#include "chase.h"
#include "glyph-util.h"
#include "group-record.h"
#include "homectl-prompts.h"
#include "log.h"
#include "parse-util.h"
#include "prompt-util.h"
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "user-util.h"
#include "userdb.h"
static int acquire_group_list(char ***ret) {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
_cleanup_strv_free_ char **groups = NULL;
UserDBMatch match = USERDB_MATCH_NULL;
int r;
assert(ret);
match.disposition_mask = INDEXES_TO_MASK(uint64_t, USER_REGULAR, USER_SYSTEM);
r = groupdb_all(&match, USERDB_SUPPRESS_SHADOW, &iterator);
if (r == -ENOLINK)
log_debug_errno(r, "No groups found. (Didn't check via Varlink.)");
else if (r == -ESRCH)
log_debug_errno(r, "No groups found.");
else if (r < 0)
return log_debug_errno(r, "Failed to enumerate groups, ignoring: %m");
else
for (;;) {
_cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
r = groupdb_iterator_get(iterator, &match, &gr);
if (r == -ESRCH)
break;
if (r < 0)
return log_debug_errno(r, "Failed to acquire next group: %m");
if (group_record_disposition(gr) == USER_REGULAR) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
/* Filter groups here that belong to a specific user, and are named like them */
UserDBMatch user_match = USERDB_MATCH_NULL;
user_match.disposition_mask = INDEX_TO_MASK(uint64_t, USER_REGULAR);
r = userdb_by_name(gr->group_name, &user_match, USERDB_SUPPRESS_SHADOW, &ur);
if (r < 0 && r != -ESRCH)
return log_debug_errno(r, "Failed to check if matching user exists for group '%s': %m", gr->group_name);
if (r >= 0 && user_record_gid(ur) == gr->gid)
continue;
}
r = strv_extend(&groups, gr->group_name);
if (r < 0)
return log_oom();
}
strv_sort(groups);
*ret = TAKE_PTR(groups);
return !!*ret;
}
static int group_completion_callback(const char *key, GetCompletionsFlags flags, char ***ret_list, void *userdata) {
char ***available = userdata;
int r;
if (!*available) {
r = acquire_group_list(available);
if (r < 0)
log_debug_errno(r, "Failed to enumerate available groups, ignoring: %m");
}
_cleanup_strv_free_ char **l = strv_copy(*available);
if (!l)
return -ENOMEM;
if (!FLAGS_SET(flags, GET_COMPLETIONS_PRESELECT)) {
r = strv_extend(&l, "list");
if (r < 0)
return r;
}
*ret_list = TAKE_PTR(l);
return 0;
}
int prompt_groups(const char *username, char ***ret_groups) {
int r;
assert(username);
assert(ret_groups);
_cleanup_strv_free_ char **available = NULL, **groups = NULL;
for (;;) {
strv_sort_uniq(groups);
if (!strv_isempty(groups)) {
_cleanup_free_ char *j = strv_join(groups, ", ");
if (!j)
return log_oom();
log_info("Currently selected groups: %s", j);
}
_cleanup_free_ char *s = NULL;
r = ask_string_full(
&s,
group_completion_callback,
&available,
"%s Please enter an auxiliary group for user %s (empty to continue, \"list\" to list available groups): ",
glyph(GLYPH_LABEL),
username);
if (r < 0)
return log_error_errno(r, "Failed to query user for auxiliary group: %m");
if (isempty(s))
break;
if (streq(s, "list")) {
if (!available) {
r = acquire_group_list(&available);
if (r < 0)
log_warning_errno(r, "Failed to enumerate available groups, ignoring: %m");
if (r == 0)
log_notice("Did not find any available groups");
if (r <= 0)
continue;
}
r = show_menu(available,
/* n_columns= */ 3,
/* column_width= */ 20,
/* ellipsize_percentage= */ 60,
/* grey_prefix= */ NULL,
/* with_numbers= */ true);
if (r < 0)
return log_error_errno(r, "Failed to show menu: %m");
putchar('\n');
continue;
}
if (!strv_isempty(available)) {
unsigned u;
r = safe_atou(s, &u);
if (r >= 0) {
if (u <= 0 || u > strv_length(available)) {
log_error("Specified entry number out of range.");
continue;
}
log_info("Selected '%s'.", available[u-1]);
r = strv_extend(&groups, available[u-1]);
if (r < 0)
return log_oom();
continue;
}
}
if (!valid_user_group_name(s, /* flags= */ 0)) {
log_notice("Specified group name is not a valid UNIX group name, try again: %s", s);
continue;
}
r = groupdb_by_name(s, /* match= */ NULL, USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, /* ret= */ NULL);
if (r == -ESRCH) {
log_notice("Specified auxiliary group does not exist, try again: %s", s);
continue;
}
if (r < 0)
return log_error_errno(r, "Failed to check if specified group '%s' already exists: %m", s);
log_info("Selected '%s'.", s);
r = strv_extend(&groups, s);
if (r < 0)
return log_oom();
}
*ret_groups = TAKE_PTR(groups);
return 0;
}
static int shell_is_ok(const char *path, void *userdata) {
int r;
assert(path);
if (!valid_shell(path)) {
log_error("String '%s' is not a valid path to a shell, refusing.", path);
return false;
}
r = chase_and_access(path, /* root= */ NULL, CHASE_MUST_BE_REGULAR, X_OK, /* ret_path= */ NULL);
if (r == -ENOENT) {
log_error_errno(r, "Shell '%s' does not exist, try again.", path);
return false;
}
if (ERRNO_IS_NEG_PRIVILEGE(r)) {
log_error_errno(r, "File '%s' is not executable, try again.", path);
return false;
}
if (r < 0)
return log_error_errno(r, "Failed to check if shell '%s' exists and is executable: %m", path);
return true;
}
int prompt_shell(const char *username, char **ret_shell) {
assert(username);
assert(ret_shell);
_cleanup_free_ char *q = strjoin("Please enter the shell to use for user ", username);
if (!q)
return log_oom();
return prompt_loop(
q,
GLYPH_SHELL,
/* menu= */ NULL,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 0,
/* n_columns= */ 3,
/* column_width= */ 20,
shell_is_ok,
/* refresh= */ NULL,
/* userdata= */ NULL,
PROMPT_MAY_SKIP|PROMPT_SILENT_VALIDATE,
ret_shell);
}
+5
View File
@@ -0,0 +1,5 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
int prompt_groups(const char *username, char ***ret_groups);
int prompt_shell(const char *username, char **ret_shell);
+25 -256
View File
@@ -16,7 +16,6 @@
#include "capability-list.h"
#include "capability-util.h"
#include "cgroup-util.h"
#include "chase.h"
#include "creds-util.h"
#include "crypto-util.h"
#include "dirent-util.h"
@@ -35,6 +34,7 @@
#include "home-util.h"
#include "homectl-fido2.h"
#include "homectl-pkcs11.h"
#include "homectl-prompts.h"
#include "homectl-recovery-key.h"
#include "json-util.h"
#include "libfido2-util.h"
@@ -2643,245 +2643,6 @@ static int has_regular_user(void) {
return true;
}
static int acquire_group_list(char ***ret) {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
_cleanup_strv_free_ char **groups = NULL;
UserDBMatch match = USERDB_MATCH_NULL;
int r;
assert(ret);
match.disposition_mask = INDEXES_TO_MASK(uint64_t, USER_REGULAR, USER_SYSTEM);
r = groupdb_all(&match, USERDB_SUPPRESS_SHADOW, &iterator);
if (r == -ENOLINK)
log_debug_errno(r, "No groups found. (Didn't check via Varlink.)");
else if (r == -ESRCH)
log_debug_errno(r, "No groups found.");
else if (r < 0)
return log_debug_errno(r, "Failed to enumerate groups, ignoring: %m");
else
for (;;) {
_cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
r = groupdb_iterator_get(iterator, &match, &gr);
if (r == -ESRCH)
break;
if (r < 0)
return log_debug_errno(r, "Failed to acquire next group: %m");
if (group_record_disposition(gr) == USER_REGULAR) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
/* Filter groups here that belong to a specific user, and are named like them */
UserDBMatch user_match = USERDB_MATCH_NULL;
user_match.disposition_mask = INDEX_TO_MASK(uint64_t, USER_REGULAR);
r = userdb_by_name(gr->group_name, &user_match, USERDB_SUPPRESS_SHADOW, &ur);
if (r < 0 && r != -ESRCH)
return log_debug_errno(r, "Failed to check if matching user exists for group '%s': %m", gr->group_name);
if (r >= 0 && user_record_gid(ur) == gr->gid)
continue;
}
r = strv_extend(&groups, gr->group_name);
if (r < 0)
return log_oom();
}
strv_sort(groups);
*ret = TAKE_PTR(groups);
return !!*ret;
}
static int group_completion_callback(const char *key, GetCompletionsFlags flags, char ***ret_list, void *userdata) {
char ***available = userdata;
int r;
if (!*available) {
r = acquire_group_list(available);
if (r < 0)
log_debug_errno(r, "Failed to enumerate available groups, ignoring: %m");
}
_cleanup_strv_free_ char **l = strv_copy(*available);
if (!l)
return -ENOMEM;
if (!FLAGS_SET(flags, GET_COMPLETIONS_PRESELECT)) {
r = strv_extend(&l, "list");
if (r < 0)
return r;
}
*ret_list = TAKE_PTR(l);
return 0;
}
static int prompt_groups(const char *username, char ***ret_groups) {
int r;
assert(username);
assert(ret_groups);
if (!arg_prompt_groups) {
*ret_groups = NULL;
return 0;
}
putchar('\n');
_cleanup_strv_free_ char **available = NULL, **groups = NULL;
for (;;) {
strv_sort_uniq(groups);
if (!strv_isempty(groups)) {
_cleanup_free_ char *j = strv_join(groups, ", ");
if (!j)
return log_oom();
log_info("Currently selected groups: %s", j);
}
_cleanup_free_ char *s = NULL;
r = ask_string_full(
&s,
group_completion_callback,
&available,
"%s Please enter an auxiliary group for user %s (empty to continue, \"list\" to list available groups): ",
glyph(GLYPH_LABEL),
username);
if (r < 0)
return log_error_errno(r, "Failed to query user for auxiliary group: %m");
if (isempty(s))
break;
if (streq(s, "list")) {
if (!available) {
r = acquire_group_list(&available);
if (r < 0)
log_warning_errno(r, "Failed to enumerate available groups, ignoring: %m");
if (r == 0)
log_notice("Did not find any available groups");
if (r <= 0)
continue;
}
r = show_menu(available,
/* n_columns= */ 3,
/* column_width= */ 20,
/* ellipsize_percentage= */ 60,
/* grey_prefix= */ NULL,
/* with_numbers= */ true);
if (r < 0)
return log_error_errno(r, "Failed to show menu: %m");
putchar('\n');
continue;
};
if (!strv_isempty(available)) {
unsigned u;
r = safe_atou(s, &u);
if (r >= 0) {
if (u <= 0 || u > strv_length(available)) {
log_error("Specified entry number out of range.");
continue;
}
log_info("Selected '%s'.", available[u-1]);
r = strv_extend(&groups, available[u-1]);
if (r < 0)
return log_oom();
continue;
}
}
if (!valid_user_group_name(s, /* flags= */ 0)) {
log_notice("Specified group name is not a valid UNIX group name, try again: %s", s);
continue;
}
r = groupdb_by_name(s, /* match= */ NULL, USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, /* ret= */ NULL);
if (r == -ESRCH) {
log_notice("Specified auxiliary group does not exist, try again: %s", s);
continue;
}
if (r < 0)
return log_error_errno(r, "Failed to check if specified group '%s' already exists: %m", s);
log_info("Selected '%s'.", s);
r = strv_extend(&groups, s);
if (r < 0)
return log_oom();
}
*ret_groups = TAKE_PTR(groups);
return 0;
}
static int shell_is_ok(const char *path, void *userdata) {
int r;
assert(path);
if (!valid_shell(path)) {
log_error("String '%s' is not a valid path to a shell, refusing.", path);
return false;
}
r = chase_and_access(path, /* root= */ NULL, CHASE_MUST_BE_REGULAR, X_OK, /* ret_path= */ NULL) >= 0;
if (r == -ENOENT) {
log_error_errno(r, "Shell '%s' does not exist, try again.", path);
return false;
}
if (ERRNO_IS_NEG_PRIVILEGE(r)) {
log_error_errno(r, "File '%s' is not executable, try again.", path);
return false;
}
if (r < 0)
return log_error_errno(r, "Failed to check if shell '%s' exists and is executable: %m", path);
return true;
}
static int prompt_shell(const char *username, char **ret_shell) {
assert(username);
assert(ret_shell);
if (!arg_prompt_shell) {
*ret_shell = NULL;
return 0;
}
putchar('\n');
_cleanup_free_ char *q = strjoin("Please enter the shell to use for user ", username);
if (!q)
return log_oom();
return prompt_loop(
q,
GLYPH_SHELL,
/* menu= */ NULL,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 0,
/* n_columns= */ 3,
/* column_width= */ 20,
shell_is_ok,
/* refresh= */ NULL,
/* userdata= */ NULL,
PROMPT_MAY_SKIP|PROMPT_SILENT_VALIDATE,
ret_shell);
}
static int username_is_ok(const char *name, void *userdata) {
int r;
@@ -2963,30 +2724,38 @@ static int create_interactively(void) {
if (r < 0)
return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m");
_cleanup_strv_free_ char **groups = NULL;
r = prompt_groups(username, &groups);
if (r < 0)
return r;
if (arg_prompt_groups) {
_cleanup_strv_free_ char **groups = NULL;
if (!strv_isempty(groups)) {
strv_sort_uniq(groups);
putchar('\n');
r = sd_json_variant_set_field_strv(&arg_identity_extra, "memberOf", groups);
r = prompt_groups(username, &groups);
if (r < 0)
return log_error_errno(r, "Failed to set memberOf field: %m");
return r;
if (!strv_isempty(groups)) {
r = sd_json_variant_set_field_strv(&arg_identity_extra, "memberOf", groups);
if (r < 0)
return log_error_errno(r, "Failed to set memberOf field: %m");
}
}
_cleanup_free_ char *shell = NULL;
r = prompt_shell(username, &shell);
if (r < 0)
return r;
if (arg_prompt_shell) {
_cleanup_free_ char *shell = NULL;
if (!isempty(shell)) {
log_info("Selected %s as the shell for user %s", shell, username);
putchar('\n');
r = sd_json_variant_set_field_string(&arg_identity_extra, "shell", shell);
r = prompt_shell(username, &shell);
if (r < 0)
return log_error_errno(r, "Failed to set shell field: %m");
return r;
if (!isempty(shell)) {
log_info("Selected %s as the shell for user %s", shell, username);
r = sd_json_variant_set_field_string(&arg_identity_extra, "shell", shell);
if (r < 0)
return log_error_errno(r, "Failed to set shell field: %m");
}
}
putchar('\n');
+10
View File
@@ -51,6 +51,10 @@ homectl_sources = files(
'homectl.c',
)
homectl_extract = files(
'homectl-prompts.c',
)
pam_systemd_home_sources = files(
'home-util.c',
'pam_systemd_home.c',
@@ -85,6 +89,7 @@ executables += [
'name' : 'homectl',
'public' : true,
'sources' : homectl_sources,
'extract' : homectl_extract,
'objects' : ['systemd-homed'],
'dependencies' : [
libdl,
@@ -93,6 +98,11 @@ executables += [
threads,
],
},
test_template + {
'sources' : files('test-homectl-prompts.c'),
'objects' : ['homectl'],
'type' : 'manual',
},
test_template + {
'sources' : files('test-homed-regression-31896.c'),
'type' : 'manual',
+106
View File
@@ -0,0 +1,106 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "format-table.h"
#include "help-util.h"
#include "homectl-prompts.h"
#include "main-func.h"
#include "options.h"
#include "string-util.h"
#include "strv.h"
#include "tests.h"
#include "verbs.h"
static int help(void) {
_cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL;
int r;
r = option_parser_get_help_table(&options);
if (r < 0)
return r;
r = verbs_get_help_table(&verbs);
if (r < 0)
return r;
(void) table_sync_column_widths(0, options, verbs);
help_cmdline("[OPTIONS...] VERB [USERNAME]");
help_abstract("Exercise homectl prompt functions in isolation.");
help_section("Verbs");
r = table_print_or_warn(verbs);
if (r < 0)
return r;
help_section("Options");
return table_print_or_warn(options);
}
VERB(verb_groups, "groups", "[USER]", VERB_ANY, 2, 0, "Select groups");
static int verb_groups(int argc, char *argv[], uintptr_t _data, void *userdata) {
assert(argv);
const char *username = argv[1] ?: "test";
int r;
_cleanup_strv_free_ char **t = NULL;
r = prompt_groups(username, &t);
if (r < 0)
return r;
_cleanup_free_ char *s = ASSERT_PTR(strv_join(t, ", "));
log_info("groups: %s → %s", username, s);
return 0;
}
VERB(verb_shell, "shell", "[USER]", VERB_ANY, 2, 0, "Select shell");
static int verb_shell(int argc, char *argv[], uintptr_t _data, void *userdata) {
assert(argv);
const char *username = argv[1] ?: "test";
int r;
_cleanup_free_ char *s = NULL;
r = prompt_shell(username, &s);
if (r < 0)
return r;
log_info("shell: %s → %s", username, strnull(s));
return 0;
}
static int parse_argv(int argc, char **argv, char ***remaining_args) {
assert(argc >= 0);
assert(argv);
assert(remaining_args);
OptionParser opts = { argc, argv };
FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
}
*remaining_args = option_parser_get_args(&opts);
return 1;
}
static int run(int argc, char **argv) {
int r;
test_setup_logging(LOG_DEBUG);
char **args = NULL;
r = parse_argv(argc, argv, &args);
if (r <= 0)
return r;
return dispatch_verb_with_args(args, /* userdata= */ NULL);
}
DEFINE_MAIN_FUNCTION(run);