userdbctl: optionally show user/group data from JSON filerather than from system

This commit is contained in:
Lennart Poettering
2025-03-07 18:13:36 +01:00
parent 6a2097dd78
commit fd0dd2d4bc
4 changed files with 100 additions and 6 deletions
+19
View File
@@ -243,6 +243,19 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--from-file=PATH</option></term>
<term><option>-f</option></term>
<listitem><para>When used with the <command>user</command> or <command>group</command> 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 <literal>-</literal>, 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.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="no-pager" />
<xi:include href="standard-options.xml" xpointer="no-legend" />
<xi:include href="standard-options.xml" xpointer="help" />
@@ -263,6 +276,9 @@
<listitem><para>List all known users records or show details of one or more specified user
records. Use <option>--output=</option> to tweak output mode.</para>
<para>If used in conjuntion with <option>--from-file=</option> the user record data is read in JSON
format from the specified file instead of querying it from the system. For details see above.</para>
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
@@ -272,6 +288,9 @@
<listitem><para>List all known group records or show details of one or more specified group
records. Use <option>--output=</option> to tweak the output mode.</para>
<para>If used in conjuntion with <option>--from-file=</option> the group record data is read in JSON
format from the specified file instead of querying it from the system. For details see above.</para>
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
+1 -1
View File
@@ -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
+71 -5
View File
@@ -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 ?: "<stdin>", SD_JSON_PARSE_SENSITIVE, &v, &line, /* reterr_column= */ NULL);
if (r < 0)
return log_syntax(/* unit= */ NULL, LOG_ERR, fn ?: "<stdin>", 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;
}
@@ -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)