]> git.ipfire.org Git - thirdparty/systemd.git/blobdiff - src/home/homectl.c
Merge pull request #30284 from YHNdnzj/fstab-wantedby-defaultdeps
[thirdparty/systemd.git] / src / home / homectl.c
index fd71e98eef6dab4d5bc9face0508dc17b9eeb680..f2fe90c75b0412c63c5500dcf3a062930e73c8b8 100644 (file)
@@ -9,7 +9,10 @@
 #include "bus-common-errors.h"
 #include "bus-error.h"
 #include "bus-locator.h"
+#include "cap-list.h"
+#include "capability-util.h"
 #include "cgroup-util.h"
+#include "creds-util.h"
 #include "dns-domain.h"
 #include "env-util.h"
 #include "fd-util.h"
 #include "pager.h"
 #include "parse-argument.h"
 #include "parse-util.h"
+#include "password-quality-util.h"
 #include "path-util.h"
 #include "percent-util.h"
 #include "pkcs11-util.h"
 #include "pretty-print.h"
+#include "proc-cmdline.h"
 #include "process-util.h"
-#include "pwquality-util.h"
+#include "recurse-dir.h"
 #include "rlimit-util.h"
 #include "spawn-polkit-agent.h"
 #include "terminal-util.h"
 #include "uid-alloc-range.h"
-#include "user-record-pwquality.h"
+#include "user-record.h"
+#include "user-record-password-quality.h"
 #include "user-record-show.h"
 #include "user-record-util.h"
-#include "user-record.h"
 #include "user-util.h"
+#include "userdb.h"
 #include "verbs.h"
 
 static PagerFlags arg_pager_flags = 0;
@@ -76,6 +82,9 @@ static enum {
         EXPORT_FORMAT_STRIPPED,      /* strip "state" + "binding", but leave signature in place */
         EXPORT_FORMAT_MINIMAL,       /* also strip signature */
 } arg_export_format = EXPORT_FORMAT_FULL;
+static uint64_t arg_capability_bounding_set = UINT64_MAX;
+static uint64_t arg_capability_ambient_set = UINT64_MAX;
+static bool arg_prompt_new_user = false;
 
 STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
 STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
@@ -109,7 +118,7 @@ static int acquire_bus(sd_bus **bus) {
         if (*bus)
                 return 0;
 
-        r = bus_connect_transport(arg_transport, arg_host, false, bus);
+        r = bus_connect_transport(arg_transport, arg_host, RUNTIME_SCOPE_SYSTEM, bus);
         if (r < 0)
                 return bus_log_connect_error(r, arg_transport);
 
@@ -206,7 +215,7 @@ static int acquire_existing_password(
                 bool emphasize_current,
                 AskPasswordFlags flags) {
 
-        _cleanup_(strv_free_erasep) char **password = NULL;
+        _cleanup_strv_free_erase_ char **password = NULL;
         _cleanup_(erase_and_freep) char *envpw = NULL;
         _cleanup_free_ char *question = NULL;
         int r;
@@ -267,7 +276,7 @@ static int acquire_recovery_key(
                 UserRecord *hr,
                 AskPasswordFlags flags) {
 
-        _cleanup_(strv_free_erasep) char **recovery_key = NULL;
+        _cleanup_strv_free_erase_ char **recovery_key = NULL;
         _cleanup_(erase_and_freep) char *envpw = NULL;
         _cleanup_free_ char *question = NULL;
         int r;
@@ -325,7 +334,7 @@ static int acquire_token_pin(
                 UserRecord *hr,
                 AskPasswordFlags flags) {
 
-        _cleanup_(strv_free_erasep) char **pin = NULL;
+        _cleanup_strv_free_erase_ char **pin = NULL;
         _cleanup_(erase_and_freep) char *envpin = NULL;
         _cleanup_free_ char *question = NULL;
         int r;
@@ -695,7 +704,7 @@ static char **mangle_user_list(char **list, char ***ret_allocated) {
 
 static int inspect_home(int argc, char *argv[], void *userdata) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        _cleanup_(strv_freep) char **mangled_list = NULL;
+        _cleanup_strv_free_ char **mangled_list = NULL;
         int r, ret = 0;
         char **items;
 
@@ -779,7 +788,7 @@ static int inspect_home(int argc, char *argv[], void *userdata) {
 
 static int authenticate_home(int argc, char *argv[], void *userdata) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        _cleanup_(strv_freep) char **mangled_list = NULL;
+        _cleanup_strv_free_ char **mangled_list = NULL;
         int r, ret = 0;
         char **items;
 
@@ -899,7 +908,7 @@ static int apply_identity_changes(JsonVariant **_v) {
         if (r < 0)
                 return log_error_errno(r, "Failed to filter identity: %m");
 
-        r = json_variant_merge(&v, arg_identity_extra);
+        r = json_variant_merge_object(&v, arg_identity_extra);
         if (r < 0)
                 return log_error_errno(r, "Failed to merge identities: %m");
 
@@ -944,7 +953,7 @@ static int apply_identity_changes(JsonVariant **_v) {
                                 if (!json_variant_equal(u, mmid))
                                         continue;
 
-                                r = json_variant_merge(&add, z);
+                                r = json_variant_merge_object(&add, z);
                                 if (r < 0)
                                         return log_error_errno(r, "Failed to merge perMachine entry: %m");
 
@@ -955,7 +964,7 @@ static int apply_identity_changes(JsonVariant **_v) {
                         if (r < 0)
                                 return log_error_errno(r, "Failed to filter perMachine: %m");
 
-                        r = json_variant_merge(&add, arg_identity_extra_this_machine);
+                        r = json_variant_merge_object(&add, arg_identity_extra_this_machine);
                         if (r < 0)
                                 return log_error_errno(r, "Failed to merge in perMachine fields: %m");
 
@@ -968,7 +977,7 @@ static int apply_identity_changes(JsonVariant **_v) {
                                 if (r < 0)
                                         return log_error_errno(r, "Failed to filter resource limits: %m");
 
-                                r = json_variant_merge(&rlv, arg_identity_extra_rlimits);
+                                r = json_variant_merge_object(&rlv, arg_identity_extra_rlimits);
                                 if (r < 0)
                                         return log_error_errno(r, "Failed to set resource limits: %m");
 
@@ -1029,7 +1038,7 @@ static int apply_identity_changes(JsonVariant **_v) {
                 if (r < 0)
                         return log_error_errno(r, "Failed to filter identity (privileged part): %m");
 
-                r = json_variant_merge(&privileged, arg_identity_extra_privileged);
+                r = json_variant_merge_object(&privileged, arg_identity_extra_privileged);
                 if (r < 0)
                         return log_error_errno(r, "Failed to merge identities (privileged part): %m");
 
@@ -1088,7 +1097,7 @@ static int add_disposition(JsonVariant **v) {
         return 1;
 }
 
-static int acquire_new_home_record(UserRecord **ret) {
+static int acquire_new_home_record(JsonVariant *input, UserRecord **ret) {
         _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
         _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
         int r;
@@ -1098,12 +1107,16 @@ static int acquire_new_home_record(UserRecord **ret) {
         if (arg_identity) {
                 unsigned line, column;
 
+                if (input)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Two identity records specified, refusing.");
+
                 r = json_parse_file(
                                 streq(arg_identity, "-") ? stdin : NULL,
                                 streq(arg_identity, "-") ? "<stdin>" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column);
                 if (r < 0)
                         return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
-        }
+        } else
+                v = json_variant_ref(input);
 
         r = apply_identity_changes(&v);
         if (r < 0)
@@ -1142,7 +1155,18 @@ static int acquire_new_home_record(UserRecord **ret) {
         if (!hr)
                 return log_oom();
 
-        r = user_record_load(hr, v, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG|USER_RECORD_PERMISSIVE);
+        r = user_record_load(
+                        hr,
+                        v,
+                        USER_RECORD_REQUIRE_REGULAR|
+                        USER_RECORD_ALLOW_SECRET|
+                        USER_RECORD_ALLOW_PRIVILEGED|
+                        USER_RECORD_ALLOW_PER_MACHINE|
+                        USER_RECORD_STRIP_BINDING|
+                        USER_RECORD_STRIP_STATUS|
+                        USER_RECORD_STRIP_SIGNATURE|
+                        USER_RECORD_LOG|
+                        USER_RECORD_PERMISSIVE);
         if (r < 0)
                 return r;
 
@@ -1183,7 +1207,7 @@ static int acquire_new_password(
                 (void) suggest_passwords();
 
         for (;;) {
-                _cleanup_(strv_free_erasep) char **first = NULL, **second = NULL;
+                _cleanup_strv_free_erase_ char **first = NULL, **second = NULL;
                 _cleanup_free_ char *question = NULL;
 
                 if (--i == 0)
@@ -1243,7 +1267,7 @@ static int acquire_new_password(
         }
 }
 
-static int create_home(int argc, char *argv[], void *userdata) {
+static int create_home_common(JsonVariant *input) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
         int r;
@@ -1254,36 +1278,7 @@ static int create_home(int argc, char *argv[], void *userdata) {
 
         (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
 
-        if (argc >= 2) {
-                /* If a username was specified, use it */
-
-                if (valid_user_group_name(argv[1], 0))
-                        r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
-                else {
-                        _cleanup_free_ char *un = NULL, *rr = NULL;
-
-                        /* Before we consider the user name invalid, let's check if we can split it? */
-                        r = split_user_name_realm(argv[1], &un, &rr);
-                        if (r < 0)
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
-
-                        if (rr) {
-                                r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
-                                if (r < 0)
-                                        return log_error_errno(r, "Failed to set realm field: %m");
-                        }
-
-                        r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
-                }
-                if (r < 0)
-                        return log_error_errno(r, "Failed to set userName field: %m");
-        } else {
-                /* If neither a username nor an identity have been specified we cannot operate. */
-                if (!arg_identity)
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
-        }
-
-        r = acquire_new_home_record(&hr);
+        r = acquire_new_home_record(input, &hr);
         if (r < 0)
                 return r;
 
@@ -1319,7 +1314,7 @@ static int create_home(int argc, char *argv[], void *userdata) {
 
                 /* If password quality enforcement is disabled, let's at least warn client side */
 
-                r = user_record_quality_check_password(hr, hr, &error);
+                r = user_record_check_password_quality(hr, hr, &error);
                 if (r < 0)
                         log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", bus_error_message(&error, r));
         }
@@ -1370,6 +1365,41 @@ static int create_home(int argc, char *argv[], void *userdata) {
         return 0;
 }
 
+static int create_home(int argc, char *argv[], void *userdata) {
+        int r;
+
+        if (argc >= 2) {
+                /* If a username was specified, use it */
+
+                if (valid_user_group_name(argv[1], 0))
+                        r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
+                else {
+                        _cleanup_free_ char *un = NULL, *rr = NULL;
+
+                        /* Before we consider the user name invalid, let's check if we can split it? */
+                        r = split_user_name_realm(argv[1], &un, &rr);
+                        if (r < 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
+
+                        if (rr) {
+                                r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to set realm field: %m");
+                        }
+
+                        r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
+                }
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set userName field: %m");
+        } else {
+                /* If neither a username nor an identity have been specified we cannot operate. */
+                if (!arg_identity)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
+        }
+
+        return create_home_common(/* input= */ NULL);
+}
+
 static int remove_home(int argc, char *argv[], void *userdata) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         int r, ret = 0;
@@ -2021,7 +2051,7 @@ static int with_home(int argc, char *argv[], void *userdata) {
         if (r < 0)
                 return bus_log_parse_error(r);
 
-        r = safe_fork("(with)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REOPEN_LOG, &pid);
+        r = safe_fork("(with)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REOPEN_LOG, &pid);
         if (r < 0)
                 return r;
         if (r == 0) {
@@ -2127,6 +2157,190 @@ static int rebalance(int argc, char *argv[], void *userdata) {
         return 0;
 }
 
+static int create_from_credentials(void) {
+        _cleanup_close_ int fd = -EBADF;
+        int ret = 0, n_created = 0, r;
+
+        fd = open_credentials_dir();
+        if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */
+                return 0;
+        if (fd < 0)
+                return log_error_errno(fd, "Failed to open credentials directory: %m");
+
+        _cleanup_free_ DirectoryEntries *des = NULL;
+        r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
+        if (r < 0)
+                return log_error_errno(r, "Failed to enumerate credentials: %m");
+
+        FOREACH_ARRAY(i, des->entries, des->n_entries) {
+                _cleanup_(json_variant_unrefp) JsonVariant *identity = NULL;
+                struct dirent *de = *i;
+                const char *e;
+
+                if (de->d_type != DT_REG)
+                        continue;
+
+                e = startswith(de->d_name, "home.create.");
+                if (!e)
+                        continue;
+
+                if (!valid_user_group_name(e, 0)) {
+                        log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name);
+                        continue;
+                }
+
+                r = json_parse_file_at(
+                                /* f= */ NULL,
+                                fd,
+                                de->d_name,
+                                /* flags= */ 0,
+                                &identity,
+                                /* ret_line= */ NULL,
+                                /* ret_column= */ NULL);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to parse user record in credential '%s', ignoring: %m", de->d_name);
+                        continue;
+                }
+
+                JsonVariant *un;
+                un = json_variant_by_key(identity, "userName");
+                if (un) {
+                        if (!json_variant_is_string(un)) {
+                                log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name);
+                                continue;
+                        }
+
+                        if (!streq(json_variant_string(un), e)) {
+                                log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, json_variant_string(un), e);
+                                continue;
+                        }
+                } else {
+                        r = json_variant_set_field_string(&identity, "userName", e);
+                        if (r < 0)
+                                return log_warning_errno(r, "Failed to set userName field: %m");
+                }
+
+                log_notice("Processing user '%s' from credentials.", e);
+
+                r = create_home_common(identity);
+                if (r >= 0)
+                        n_created++;
+
+                RET_GATHER(ret, r);
+        }
+
+        return ret < 0 ? ret : n_created;
+}
+
+static int has_regular_user(void) {
+        _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+        int r;
+
+        r = userdb_all(USERDB_SUPPRESS_SHADOW, &iterator);
+        if (r < 0)
+                return log_error_errno(r, "Failed to create user enumerator: %m");
+
+        for (;;) {
+                _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+
+                r = userdb_iterator_get(iterator, &ur);
+                if (r == -ESRCH)
+                        break;
+                if (r < 0)
+                        return log_error_errno(r, "Failed to enumerate users: %m");
+
+                if (user_record_disposition(ur) == USER_REGULAR)
+                        return true;
+        }
+
+        return false;
+}
+
+static int create_interactively(void) {
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        _cleanup_free_ char *username = NULL;
+        int r;
+
+        if (!arg_prompt_new_user) {
+                log_debug("Prompting for user creation was not requested.");
+                return 0;
+        }
+
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
+
+        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+        (void) reset_terminal_fd(STDIN_FILENO, /* switch_to_text= */ false);
+
+        for (;;) {
+                username = mfree(username);
+
+                r = ask_string(&username,
+                               "%s Please enter user name to create (empty to skip): ",
+                               special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to query user for username: %m");
+
+                if (isempty(username)) {
+                        log_info("No data entered, skipping.");
+                        return 0;
+                }
+
+                if (!valid_user_group_name(username, /* flags= */ 0)) {
+                        log_notice("Specified user name is not a valid UNIX user name, try again: %s", username);
+                        continue;
+                }
+
+                r = userdb_by_name(username, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL);
+                if (r == -ESRCH)
+                        break;
+                if (r < 0)
+                        return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", username);
+
+                log_notice("Specified user '%s' exists already, try again.", username);
+        }
+
+        r = json_variant_set_field_string(&arg_identity_extra, "userName", username);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set userName field: %m");
+
+        return create_home_common(/* input= */ NULL);
+}
+
+static int verb_firstboot(int argc, char *argv[], void *userdata) {
+        int r;
+
+        /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
+         * tool. */
+
+        bool enabled;
+        r = proc_cmdline_get_bool("systemd.firstboot", /* flags = */ 0, &enabled);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
+        if (r > 0 && !enabled) {
+                log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
+                arg_prompt_new_user = false;
+        }
+
+        r = create_from_credentials();
+        if (r < 0)
+                return r;
+        if (r > 0) /* Already created users from credentials */
+                return 0;
+
+        r = has_regular_user();
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                log_info("Regular user already present in user database, skipping user creation.");
+                return 0;
+        }
+
+        return create_interactively();
+}
+
 static int drop_from_identity(const char *field) {
         int r;
 
@@ -2183,6 +2397,7 @@ static int help(int argc, char *argv[], void *userdata) {
                "  deactivate-all               Deactivate all active home areas\n"
                "  rebalance                    Rebalance free space between home areas\n"
                "  with USER [COMMAND…]         Run shell or command with access to a home area\n"
+               "  firstboot                    Run first-boot home area creation wizard\n"
                "\n%4$sOptions:%5$s\n"
                "  -h --help                    Show this help\n"
                "     --version                 Show package version\n"
@@ -2201,6 +2416,8 @@ static int help(int argc, char *argv[], void *userdata) {
                "  -E                           When specified once equals -j --export-format=\n"
                "                               stripped, when specified twice equals\n"
                "                               -j --export-format=minimal\n"
+               "     --prompt-new-user         firstboot: Query user interactively for user\n"
+               "                               to create\n"
                "\n%4$sGeneral User Record Properties:%5$s\n"
                "  -c --real-name=REALNAME      Real name for user\n"
                "     --realm=REALM             Realm to create user in\n"
@@ -2210,6 +2427,10 @@ static int help(int argc, char *argv[], void *userdata) {
                "  -d --home-dir=PATH           Home directory\n"
                "  -u --uid=UID                 Numeric UID for user\n"
                "  -G --member-of=GROUP         Add user to group\n"
+               "     --capability-bounding-set=CAPS\n"
+               "                               Bounding POSIX capability set\n"
+               "     --capability-ambient-set=CAPS\n"
+               "                               Ambient POSIX capability set\n"
                "     --skel=PATH               Skeleton directory to use\n"
                "     --shell=PATH              Shell for account\n"
                "     --setenv=VARIABLE[=VALUE] Set an environment variable at log-in\n"
@@ -2402,6 +2623,9 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_AUTO_RESIZE_MODE,
                 ARG_REBALANCE_WEIGHT,
                 ARG_FIDO2_CRED_ALG,
+                ARG_CAPABILITY_BOUNDING_SET,
+                ARG_CAPABILITY_AMBIENT_SET,
+                ARG_PROMPT_NEW_USER,
         };
 
         static const struct option options[] = {
@@ -2492,6 +2716,9 @@ static int parse_argv(int argc, char *argv[]) {
                 { "luks-extra-mount-options",    required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS    },
                 { "auto-resize-mode",            required_argument, NULL, ARG_AUTO_RESIZE_MODE            },
                 { "rebalance-weight",            required_argument, NULL, ARG_REBALANCE_WEIGHT            },
+                { "capability-bounding-set",     required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET     },
+                { "capability-ambient-set",      required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET      },
+                { "prompt-new-user",             no_argument,       NULL, ARG_PROMPT_NEW_USER             },
                 {}
         };
 
@@ -2696,7 +2923,7 @@ static int parse_argv(int argc, char *argv[]) {
                 }
 
                 case ARG_RLIMIT: {
-                        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *jcur = NULL, *jmax = NULL;
+                        _cleanup_(json_variant_unrefp) JsonVariant *jcur = NULL, *jmax = NULL;
                         _cleanup_free_ char *field = NULL, *t = NULL;
                         const char *eq;
                         struct rlimit rl;
@@ -2752,18 +2979,15 @@ static int parse_argv(int argc, char *argv[]) {
                         if (r < 0)
                                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate maximum integer: %m");
 
-                        r = json_build(&v,
-                                       JSON_BUILD_OBJECT(
-                                                       JSON_BUILD_PAIR("cur", JSON_BUILD_VARIANT(jcur)),
-                                                       JSON_BUILD_PAIR("max", JSON_BUILD_VARIANT(jmax))));
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to build resource limit: %m");
-
                         t = strjoin("RLIMIT_", rlimit_to_string(l));
                         if (!t)
                                 return log_oom();
 
-                        r = json_variant_set_field(&arg_identity_extra_rlimits, t, v);
+                        r = json_variant_set_fieldb(
+                                        &arg_identity_extra_rlimits, t,
+                                        JSON_BUILD_OBJECT(
+                                                        JSON_BUILD_PAIR("cur", JSON_BUILD_VARIANT(jcur)),
+                                                        JSON_BUILD_PAIR("max", JSON_BUILD_VARIANT(jmax))));
                         if (r < 0)
                                 return log_error_errno(r, "Failed to set %s field: %m", rlimit_to_string(l));
 
@@ -3150,7 +3374,7 @@ static int parse_argv(int argc, char *argv[]) {
 
                 case ARG_SSH_AUTHORIZED_KEYS: {
                         _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
-                        _cleanup_(strv_freep) char **l = NULL, **add = NULL;
+                        _cleanup_strv_free_ char **l = NULL, **add = NULL;
 
                         if (isempty(optarg)) {
                                 r = drop_from_identity("sshAuthorizedKeys");
@@ -3564,38 +3788,29 @@ static int parse_argv(int argc, char *argv[]) {
                         strv_uniq(arg_fido2_device);
                         break;
 
-                case ARG_FIDO2_WITH_PIN: {
-                        bool lock_with_pin;
-
-                        r = parse_boolean_argument("--fido2-with-client-pin=", optarg, &lock_with_pin);
+                case ARG_FIDO2_WITH_PIN:
+                        r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL);
                         if (r < 0)
                                 return r;
 
-                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, lock_with_pin);
+                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r);
                         break;
-                }
 
-                case ARG_FIDO2_WITH_UP: {
-                        bool lock_with_up;
-
-                        r = parse_boolean_argument("--fido2-with-user-presence=", optarg, &lock_with_up);
+                case ARG_FIDO2_WITH_UP:
+                        r = parse_boolean_argument("--fido2-with-user-presence=", optarg, NULL);
                         if (r < 0)
                                 return r;
 
-                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, lock_with_up);
+                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r);
                         break;
-                }
 
-                case ARG_FIDO2_WITH_UV: {
-                        bool lock_with_uv;
-
-                        r = parse_boolean_argument("--fido2-with-user-verification=", optarg, &lock_with_uv);
+                case ARG_FIDO2_WITH_UV:
+                        r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL);
                         if (r < 0)
                                 return r;
 
-                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, lock_with_uv);
+                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r);
                         break;
-                }
 
                 case ARG_RECOVERY_KEY:
                         r = parse_boolean(optarg);
@@ -3638,6 +3853,7 @@ static int parse_argv(int argc, char *argv[]) {
                                 r = drop_from_identity("rebalanceWeight");
                                 if (r < 0)
                                         return r;
+                                break;
                         }
 
                         if (streq(optarg, "off"))
@@ -3714,15 +3930,14 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
 
                 case ARG_DROP_CACHES: {
-                        bool drop_caches;
-
                         if (isempty(optarg)) {
                                 r = drop_from_identity("dropCaches");
                                 if (r < 0)
                                         return r;
+                                break;
                         }
 
-                        r = parse_boolean_argument("--drop-caches=", optarg, &drop_caches);
+                        r = parse_boolean_argument("--drop-caches=", optarg, NULL);
                         if (r < 0)
                                 return r;
 
@@ -3733,6 +3948,65 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
                 }
 
+                case ARG_CAPABILITY_AMBIENT_SET:
+                case ARG_CAPABILITY_BOUNDING_SET: {
+                        _cleanup_strv_free_ char **l = NULL;
+                        bool subtract = false;
+                        uint64_t parsed, *which, updated;
+                        const char *p, *field;
+
+                        if (c == ARG_CAPABILITY_AMBIENT_SET) {
+                                which = &arg_capability_ambient_set;
+                                field = "capabilityAmbientSet";
+                        } else {
+                                assert(c == ARG_CAPABILITY_BOUNDING_SET);
+                                which = &arg_capability_bounding_set;
+                                field = "capabilityBoundingSet";
+                        }
+
+                        if (isempty(optarg)) {
+                                r = drop_from_identity(field);
+                                if (r < 0)
+                                        return r;
+
+                                *which = UINT64_MAX;
+                                break;
+                        }
+
+                        p = optarg;
+                        if (*p == '~') {
+                                subtract = true;
+                                p++;
+                        }
+
+                        r = capability_set_from_string(p, &parsed);
+                        if (r == 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capabilities in capability string '%s'.", p);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse capability string '%s': %m", p);
+
+                        if (*which == UINT64_MAX)
+                                updated = subtract ? all_capabilities() & ~parsed : parsed;
+                        else if (subtract)
+                                updated = *which & ~parsed;
+                        else
+                                updated = *which | parsed;
+
+                        if (capability_set_to_strv(updated, &l) < 0)
+                                return log_oom();
+
+                        r = json_variant_set_field_strv(&arg_identity_extra, field, l);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to set %s field: %m", field);
+
+                        *which = updated;
+                        break;
+                }
+
+                case ARG_PROMPT_NEW_USER:
+                        arg_prompt_new_user = true;
+                        break;
+
                 case '?':
                         return -EINVAL;
 
@@ -3799,6 +4073,7 @@ static int run(int argc, char *argv[]) {
                 { "lock-all",       VERB_ANY, 1,        0,            lock_all_homes       },
                 { "deactivate-all", VERB_ANY, 1,        0,            deactivate_all_homes },
                 { "rebalance",      VERB_ANY, 1,        0,            rebalance            },
+                { "firstboot",      VERB_ANY, 1,        0,            verb_firstboot       },
                 {}
         };