diff --git a/man/userdbctl.xml b/man/userdbctl.xml
index f7b0c1d9eb..2db590fa72 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 da1419b175..b068fe81fd 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 81007c4e50..8ba12afbb7 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 b0b7472d61..5592777849 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)