]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
userdbctl: optionally show user/group data from JSON filerather than from system
authorLennart Poettering <lennart@poettering.net>
Wed, 19 Feb 2025 20:56:14 +0000 (21:56 +0100)
committerLennart Poettering <lennart@poettering.net>
Fri, 7 Mar 2025 17:13:36 +0000 (18:13 +0100)
man/userdbctl.xml
shell-completion/bash/userdbctl
src/userdb/userdbctl.c
test/units/TEST-74-AUX-UTILS.userdbctl.sh

index f7b0c1d9ebb984a9fd563f2bd0176c935436e4df..2db590fa7218475506ac42d0830afdf97b6b420f 100644 (file)
         <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" />
         <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>
 
         <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>
 
index da1419b17536a51d025156d44d3646d887652cfd..b068fe81fdba4bb7444d3f584951cf8b386c6890 100644 (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
index 81007c4e50ab54c327d52411f6114da0ab46e82e..8ba12afbb747949466da806237e794e0bf75f404 100644 (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;
 }
 
index b0b7472d617ec2384b546519a485987400910927..55927778492902eeb071f232e207d7d2d48071ec 100755 (executable)
@@ -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)