From fd0dd2d4bce00b69f8badab1a71b8929e392af5c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 21:56:14 +0100 Subject: [PATCH] userdbctl: optionally show user/group data from JSON filerather than from system --- man/userdbctl.xml | 19 ++++++ shell-completion/bash/userdbctl | 2 +- src/userdb/userdbctl.c | 76 +++++++++++++++++++++-- test/units/TEST-74-AUX-UTILS.userdbctl.sh | 9 +++ 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/man/userdbctl.xml b/man/userdbctl.xml index f7b0c1d9ebb..2db590fa721 100644 --- a/man/userdbctl.xml +++ b/man/userdbctl.xml @@ -243,6 +243,19 @@ + + + + + When used with the user or group command, read + the user definition in JSON format from the specified file, instead of querying it from the + system. If the path is specified as -, reads the JSON data from standard + input. This is useful to validate and introspect JSON user or group records quickly, and check how + they would be interpreted on the local system. + + + + @@ -263,6 +276,9 @@ List all known users records or show details of one or more specified user records. Use to tweak output mode. + If used in conjuntion with the user record data is read in JSON + format from the specified file instead of querying it from the system. For details see above. + @@ -272,6 +288,9 @@ List all known group records or show details of one or more specified group records. Use to tweak the output mode. + If used in conjuntion with the group record data is read in JSON + format from the specified file instead of querying it from the system. For details see above. + diff --git a/shell-completion/bash/userdbctl b/shell-completion/bash/userdbctl index da1419b1753..b068fe81fdb 100644 --- a/shell-completion/bash/userdbctl +++ b/shell-completion/bash/userdbctl @@ -44,7 +44,7 @@ _userdbctl () { [STANDALONE]='-h --help --version --no-pager --no-legend -j -N --chain -z --fuzzy -I -S -R -B' [ARG]='--output -s --service --with-nss --synthesize --with-dropin --with-varlink - --multiplexer --json --uid-min --uid-max --disposition --boundaries' + --multiplexer --json --uid-min --uid-max --disposition --boundaries --from-file' ) if __contains_word "$prev" ${OPTS[ARG]}; then diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c index 81007c4e50a..8ba12afbb74 100644 --- a/src/userdb/userdbctl.c +++ b/src/userdb/userdbctl.c @@ -45,8 +45,10 @@ static uid_t arg_uid_min = 0; static uid_t arg_uid_max = UID_INVALID-1; static bool arg_fuzzy = false; static bool arg_boundaries = true; +static sd_json_variant *arg_from_file = NULL; STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_from_file, sd_json_variant_unrefp); static const char *user_disposition_to_color(UserDisposition d) { assert(d >= 0); @@ -380,7 +382,7 @@ static int display_user(int argc, char *argv[], void *userdata) { int ret = 0, r; if (arg_output < 0) - arg_output = argc > 1 && !arg_fuzzy ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; if (arg_output == OUTPUT_TABLE) { table = table_new(" ", "name", "disposition", "uid", "gid", "realname", "home", "shell", "order"); @@ -402,7 +404,23 @@ static int display_user(int argc, char *argv[], void *userdata) { .uid_max = arg_uid_max, }; - if (argc > 1 && !arg_fuzzy) + if (arg_from_file) { + if (argc > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No argument expected when invoked with --from-file=, refusing."); + + _cleanup_(user_record_unrefp) UserRecord *ur = user_record_new(); + if (!ur) + return log_oom(); + + r = user_record_load(ur, arg_from_file, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG); + if (r < 0) + return r; + + r = show_user(ur, table); + if (r < 0) + return r; + + } else if (argc > 1 && !arg_fuzzy) STRV_FOREACH(i, argv + 1) { _cleanup_(user_record_unrefp) UserRecord *ur = NULL; @@ -706,7 +724,7 @@ static int display_group(int argc, char *argv[], void *userdata) { int ret = 0, r; if (arg_output < 0) - arg_output = argc > 1 && !arg_fuzzy ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; if (arg_output == OUTPUT_TABLE) { table = table_new(" ", "name", "disposition", "gid", "description", "order"); @@ -727,7 +745,23 @@ static int display_group(int argc, char *argv[], void *userdata) { .gid_max = arg_uid_max, }; - if (argc > 1 && !arg_fuzzy) + if (arg_from_file) { + if (argc > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No argument expected when invoked with --from-file=, refusing."); + + _cleanup_(group_record_unrefp) GroupRecord *gr = group_record_new(); + if (!gr) + return log_oom(); + + r = group_record_load(gr, arg_from_file, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG); + if (r < 0) + return r; + + r = show_group(gr, table); + if (r < 0) + return r; + + } else if (argc > 1 && !arg_fuzzy) STRV_FOREACH(i, argv + 1) { _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; @@ -888,6 +922,9 @@ static int display_memberships(int argc, char *argv[], void *userdata) { _cleanup_(table_unrefp) Table *table = NULL; int ret = 0, r; + if (arg_from_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing memberships, refusing."); + if (arg_output < 0) arg_output = OUTPUT_TABLE; @@ -982,6 +1019,9 @@ static int display_services(int argc, char *argv[], void *userdata) { _cleanup_closedir_ DIR *d = NULL; int r; + if (arg_from_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing services, refusing."); + d = opendir("/run/systemd/userdb/"); if (!d) { if (errno == ENOENT) { @@ -1048,6 +1088,9 @@ static int ssh_authorized_keys(int argc, char *argv[], void *userdata) { assert(argc >= 2); + if (arg_from_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing SSH authorized keys, refusing."); + if (arg_chain) { /* If --chain is specified, the rest of the command line is the chain command */ @@ -1167,6 +1210,7 @@ static int help(int argc, char *argv[], void *userdata) { " -R Equivalent to --disposition=regular\n" " --boundaries=BOOL Show/hide UID/GID range boundaries in output\n" " -B Equivalent to --boundaries=no\n" + " -F --from-file=PATH Read JSON record from file\n" "\nSee the %s for details.\n", program_invocation_short_name, ansi_highlight(), @@ -1215,6 +1259,7 @@ static int parse_argv(int argc, char *argv[]) { { "fuzzy", no_argument, NULL, 'z' }, { "disposition", required_argument, NULL, ARG_DISPOSITION }, { "boundaries", required_argument, NULL, ARG_BOUNDARIES }, + { "from-file", required_argument, NULL, 'F' }, {} }; @@ -1245,7 +1290,7 @@ static int parse_argv(int argc, char *argv[]) { int c; c = getopt_long(argc, argv, - arg_chain ? "+hjs:NISRzB" : "hjs:NISRzB", /* When --chain was used disable parsing of further switches */ + arg_chain ? "+hjs:NISRzBF:" : "hjs:NISRzBF:", /* When --chain was used disable parsing of further switches */ options, NULL); if (c < 0) break; @@ -1420,6 +1465,24 @@ static int parse_argv(int argc, char *argv[]) { arg_boundaries = false; break; + case 'F': { + if (isempty(optarg)) { + arg_from_file = sd_json_variant_unref(arg_from_file); + break; + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + const char *fn = streq(optarg, "-") ? NULL : optarg; + unsigned line = 0; + r = sd_json_parse_file(fn ? NULL : stdin, fn ?: "", SD_JSON_PARSE_SENSITIVE, &v, &line, /* reterr_column= */ NULL); + if (r < 0) + return log_syntax(/* unit= */ NULL, LOG_ERR, fn ?: "", line, r, "JSON parse failure."); + + sd_json_variant_unref(arg_from_file); + arg_from_file = TAKE_PTR(v); + break; + } + case '?': return -EINVAL; @@ -1435,6 +1498,9 @@ static int parse_argv(int argc, char *argv[]) { if (arg_disposition_mask == UINT64_MAX) arg_disposition_mask = USER_DISPOSITION_MASK_ALL; + if (arg_from_file) + arg_boundaries = false; + return 1; } diff --git a/test/units/TEST-74-AUX-UTILS.userdbctl.sh b/test/units/TEST-74-AUX-UTILS.userdbctl.sh index b0b7472d617..55927778492 100755 --- a/test/units/TEST-74-AUX-UTILS.userdbctl.sh +++ b/test/units/TEST-74-AUX-UTILS.userdbctl.sh @@ -37,3 +37,12 @@ assert_eq "$(userdbctl user 0 -j | jq -r .userName)" root assert_eq "$(userdbctl user 2147352576 -j | jq -r .userName)" foreign-0 assert_eq "$(userdbctl user 2147352577 -j | jq -r .userName)" foreign-1 assert_eq "$(userdbctl user 2147418110 -j | jq -r .userName)" foreign-65534 + +# Make sure that -F shows same data as if we'd ask directly +userdbctl user root -j | userdbctl -F- user | cmp - <(userdbctl user root) +userdbctl user systemd-network -j | userdbctl -F- user | cmp - <(userdbctl user systemd-network) +userdbctl user 65534 -j | userdbctl -F- user | cmp - <(userdbctl user 65534) + +userdbctl group root -j | userdbctl -F- group | cmp - <(userdbctl group root) +userdbctl group systemd-network -j | userdbctl -F- group | cmp - <(userdbctl group systemd-network) +userdbctl group 65534 -j | userdbctl -F- group | cmp - <(userdbctl group 65534) -- 2.47.3