/* SPDX-License-Identifier: LGPL-2.1+ */

#include <getopt.h>

#include "sd-bus.h"

#include "ask-password-api.h"
#include "bus-common-errors.h"
#include "bus-error.h"
#include "bus-locator.h"
#include "cgroup-util.h"
#include "dns-domain.h"
#include "env-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-table.h"
#include "home-util.h"
#include "homectl-fido2.h"
#include "homectl-pkcs11.h"
#include "homectl-recovery-key.h"
#include "locale-util.h"
#include "main-func.h"
#include "memory-util.h"
#include "pager.h"
#include "parse-util.h"
#include "path-util.h"
#include "pkcs11-util.h"
#include "pretty-print.h"
#include "process-util.h"
#include "pwquality-util.h"
#include "rlimit-util.h"
#include "spawn-polkit-agent.h"
#include "terminal-util.h"
#include "user-record-pwquality.h"
#include "user-record-show.h"
#include "user-record-util.h"
#include "user-record.h"
#include "user-util.h"
#include "verbs.h"

static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static bool arg_ask_password = true;
static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
static const char *arg_host = NULL;
static const char *arg_identity = NULL;
static JsonVariant *arg_identity_extra = NULL;
static JsonVariant *arg_identity_extra_privileged = NULL;
static JsonVariant *arg_identity_extra_this_machine = NULL;
static JsonVariant *arg_identity_extra_rlimits = NULL;
static char **arg_identity_filter = NULL; /* this one is also applied to 'privileged' and 'thisMachine' subobjects */
static char **arg_identity_filter_rlimits = NULL;
static uint64_t arg_disk_size = UINT64_MAX;
static uint64_t arg_disk_size_relative = UINT64_MAX;
static char **arg_pkcs11_token_uri = NULL;
static char **arg_fido2_device = NULL;
static bool arg_recovery_key = false;
static bool arg_json = false;
static JsonFormatFlags arg_json_format_flags = 0;
static bool arg_and_resize = false;
static bool arg_and_change_password = false;
static enum {
        EXPORT_FORMAT_FULL,          /* export the full record */
        EXPORT_FORMAT_STRIPPED,      /* strip "state" + "binding", but leave signature in place */
        EXPORT_FORMAT_MINIMAL,       /* also strip signature */
} arg_export_format = EXPORT_FORMAT_FULL;

STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_privileged, json_variant_unrefp);
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, json_variant_unrefp);
STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep);

static const BusLocator *bus_mgr;

static bool identity_properties_specified(void) {
        return
                arg_identity ||
                !json_variant_is_blank_object(arg_identity_extra) ||
                !json_variant_is_blank_object(arg_identity_extra_privileged) ||
                !json_variant_is_blank_object(arg_identity_extra_this_machine) ||
                !json_variant_is_blank_object(arg_identity_extra_rlimits) ||
                !strv_isempty(arg_identity_filter) ||
                !strv_isempty(arg_identity_filter_rlimits) ||
                !strv_isempty(arg_pkcs11_token_uri) ||
                !strv_isempty(arg_fido2_device);
}

static int acquire_bus(sd_bus **bus) {
        int r;

        assert(bus);

        if (*bus)
                return 0;

        r = bus_connect_transport(arg_transport, arg_host, false, bus);
        if (r < 0)
                return log_error_errno(r, "Failed to connect to bus: %m");

        (void) sd_bus_set_allow_interactive_authorization(*bus, arg_ask_password);

        return 0;
}

static int list_homes(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        _cleanup_(table_unrefp) Table *table = NULL;
        int r;

        (void) pager_open(arg_pager_flags);

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        r = bus_call_method(bus, bus_mgr, "ListHomes", &error, &reply, NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to list homes: %s", bus_error_message(&error, r));

        table = table_new("name", "uid", "gid", "state", "realname", "home", "shell");
        if (!table)
                return log_oom();

        r = sd_bus_message_enter_container(reply, 'a', "(susussso)");
        if (r < 0)
                return bus_log_parse_error(r);

        for (;;) {
                const char *name, *state, *realname, *home, *shell, *color;
                TableCell *cell;
                uint32_t uid, gid;

                r = sd_bus_message_read(reply, "(susussso)", &name, &uid, &state, &gid, &realname, &home, &shell, NULL);
                if (r < 0)
                        return bus_log_parse_error(r);
                if (r == 0)
                        break;

                r = table_add_many(table,
                                   TABLE_STRING, name,
                                   TABLE_UID, uid,
                                   TABLE_GID, gid);
                if (r < 0)
                        return table_log_add_error(r);


                r = table_add_cell(table, &cell, TABLE_STRING, state);
                if (r < 0)
                        return table_log_add_error(r);

                color = user_record_state_color(state);
                if (color)
                        (void) table_set_color(table, cell, color);

                r = table_add_many(table,
                                   TABLE_STRING, strna(empty_to_null(realname)),
                                   TABLE_STRING, home,
                                   TABLE_STRING, strna(empty_to_null(shell)));
                if (r < 0)
                        return table_log_add_error(r);
        }

        r = sd_bus_message_exit_container(reply);
        if (r < 0)
                return bus_log_parse_error(r);

        if (table_get_rows(table) > 1 || arg_json) {
                r = table_set_sort(table, (size_t) 0, (size_t) -1);
                if (r < 0)
                        return table_log_sort_error(r);

                table_set_header(table, arg_legend);

                if (arg_json)
                        r = table_print_json(table, stdout, arg_json_format_flags);
                else
                        r = table_print(table, NULL);
                if (r < 0)
                        return table_log_print_error(r);
        }

        if (arg_legend && !arg_json) {
                if (table_get_rows(table) > 1)
                        printf("\n%zu home areas listed.\n", table_get_rows(table) - 1);
                else
                        printf("No home areas.\n");
        }

        return 0;
}

static int acquire_existing_password(const char *user_name, UserRecord *hr, bool emphasize_current) {
        _cleanup_(strv_free_erasep) char **password = NULL;
        _cleanup_free_ char *question = NULL;
        char *e;
        int r;

        assert(user_name);
        assert(hr);

        e = getenv("PASSWORD");
        if (e) {
                /* People really shouldn't use environment variables for passing passwords. We support this
                 * only for testing purposes, and do not document the behaviour, so that people won't
                 * actually use this outside of testing. */

                r = user_record_set_password(hr, STRV_MAKE(e), true);
                if (r < 0)
                        return log_error_errno(r, "Failed to store password: %m");

                string_erase(e);

                if (unsetenv("PASSWORD") < 0)
                        return log_error_errno(errno, "Failed to unset $PASSWORD: %m");

                return 0;
        }

        if (asprintf(&question, emphasize_current ?
                     "Please enter current password for user %s:" :
                     "Please enter password for user %s:",
                     user_name) < 0)
                return log_oom();

        r = ask_password_auto(question, "user-home", NULL, "home-password", USEC_INFINITY, ASK_PASSWORD_ACCEPT_CACHED|ASK_PASSWORD_PUSH_CACHE, &password);
        if (r < 0)
                return log_error_errno(r, "Failed to acquire password: %m");

        r = user_record_set_password(hr, password, true);
        if (r < 0)
                return log_error_errno(r, "Failed to store password: %m");

        return 0;
}

static int acquire_token_pin(const char *user_name, UserRecord *hr) {
        _cleanup_(strv_free_erasep) char **pin = NULL;
        _cleanup_free_ char *question = NULL;
        char *e;
        int r;

        assert(user_name);
        assert(hr);

        e = getenv("PIN");
        if (e) {
                r = user_record_set_token_pin(hr, STRV_MAKE(e), false);
                if (r < 0)
                        return log_error_errno(r, "Failed to store token PIN: %m");

                string_erase(e);

                if (unsetenv("PIN") < 0)
                        return log_error_errno(errno, "Failed to unset $PIN: %m");

                return 0;
        }

        if (asprintf(&question, "Please enter security token PIN for user %s:", user_name) < 0)
                return log_oom();

        /* We never cache or use cached PINs, since usually there are only very few attempts allowed before the PIN is blocked */
        r = ask_password_auto(question, "user-home", NULL, "token-pin", USEC_INFINITY, 0, &pin);
        if (r < 0)
                return log_error_errno(r, "Failed to acquire security token PIN: %m");

        r = user_record_set_token_pin(hr, pin, false);
        if (r < 0)
                return log_error_errno(r, "Failed to store security token PIN: %m");

        return 0;
}

static int handle_generic_user_record_error(
                const char *user_name,
                UserRecord *hr,
                const sd_bus_error *error,
                int ret,
                bool emphasize_current_password) {
        int r;

        assert(user_name);
        assert(hr);

        if (sd_bus_error_has_name(error, BUS_ERROR_HOME_ABSENT))
                return log_error_errno(SYNTHETIC_ERRNO(EREMOTE),
                                       "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", user_name);

        else if (sd_bus_error_has_name(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT))
                return log_error_errno(SYNTHETIC_ERRNO(ETOOMANYREFS),
                                       "Too frequent unsuccessful login attempts for user %s, try again later.", user_name);

        else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD)) {

                if (!strv_isempty(hr->password))
                        log_notice("Password incorrect or not sufficient, please try again.");

                r = acquire_existing_password(user_name, hr, emphasize_current_password);
                if (r < 0)
                        return r;

        } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) {

                if (strv_isempty(hr->password))
                        log_notice("Security token not inserted, please enter password.");
                else
                        log_notice("Password incorrect or not sufficient, and configured security token not inserted, please try again.");

                r = acquire_existing_password(user_name, hr, emphasize_current_password);
                if (r < 0)
                        return r;

        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_NEEDED)) {

                r = acquire_token_pin(user_name, hr);
                if (r < 0)
                        return r;

        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED)) {

                log_notice("%s%sPlease authenticate physically on security token.",
                           emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "",
                           emoji_enabled() ? " " : "");

                r = user_record_set_pkcs11_protected_authentication_path_permitted(hr, true);
                if (r < 0)
                        return log_error_errno(r, "Failed to set PKCS#11 protected authentication path permitted flag: %m");

        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED)) {

                log_notice("%s%sAuthentication requires presence verification on security token.",
                           emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "",
                           emoji_enabled() ? " " : "");

                r = user_record_set_fido2_user_presence_permitted(hr, true);
                if (r < 0)
                        return log_error_errno(r, "Failed to set FIDO2 user presence permitted flag: %m");

        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_LOCKED))
                return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Security token PIN is locked, please unlock it first. (Hint: Removal and re-insertion might suffice.)");

        else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) {

                log_notice("Security token PIN incorrect, please try again.");

                r = acquire_token_pin(user_name, hr);
                if (r < 0)
                        return r;

        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT)) {

                log_notice("Security token PIN incorrect, please try again (only a few tries left!).");

                r = acquire_token_pin(user_name, hr);
                if (r < 0)
                        return r;

        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT)) {

                log_notice("Security token PIN incorrect, please try again (only one try left!).");

                r = acquire_token_pin(user_name, hr);
                if (r < 0)
                        return r;
        } else
                return log_error_errno(ret, "Operation on home %s failed: %s", user_name, bus_error_message(error, ret));

        return 0;
}

static int activate_home(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        int r, ret = 0;
        char **i;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        STRV_FOREACH(i, strv_skip(argv, 1)) {
                _cleanup_(user_record_unrefp) UserRecord *secret = NULL;

                secret = user_record_new();
                if (!secret)
                        return log_oom();

                for (;;) {
                        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                        r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHome");
                        if (r < 0)
                                return bus_log_create_error(r);

                        r = sd_bus_message_append(m, "s", *i);
                        if (r < 0)
                                return bus_log_create_error(r);

                        r = bus_message_append_secret(m, secret);
                        if (r < 0)
                                return bus_log_create_error(r);

                        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                        if (r < 0) {
                                r = handle_generic_user_record_error(*i, secret, &error, r, false);
                                if (r < 0) {
                                        if (ret == 0)
                                                ret = r;

                                        break;
                                }
                        } else
                                break;
                }
        }

        return ret;
}

static int deactivate_home(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        int r, ret = 0;
        char **i;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        STRV_FOREACH(i, strv_skip(argv, 1)) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                r = bus_message_new_method_call(bus, &m, bus_mgr, "DeactivateHome");
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_message_append(m, "s", *i);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                if (r < 0) {
                        log_error_errno(r, "Failed to deactivate user home: %s", bus_error_message(&error, r));
                        if (ret == 0)
                                ret = r;
                }
        }

        return ret;
}

static void dump_home_record(UserRecord *hr) {
        int r;

        assert(hr);

        if (hr->incomplete) {
                fflush(stdout);
                log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", hr->user_name);
        }

        if (arg_json) {
                _cleanup_(user_record_unrefp) UserRecord *stripped = NULL;

                if (arg_export_format == EXPORT_FORMAT_STRIPPED)
                        r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &stripped);
                else if (arg_export_format == EXPORT_FORMAT_MINIMAL)
                        r = user_record_clone(hr, USER_RECORD_EXTRACT_SIGNABLE, &stripped);
                else
                        r = 0;
                if (r < 0)
                        log_warning_errno(r, "Failed to strip user record, ignoring: %m");
                if (stripped)
                        hr = stripped;

                json_variant_dump(hr->json, arg_json_format_flags, stdout, NULL);
        } else
                user_record_show(hr, true);
}

static char **mangle_user_list(char **list, char ***ret_allocated) {
        _cleanup_free_ char *myself = NULL;
        char **l;

        if (!strv_isempty(list)) {
                *ret_allocated = NULL;
                return list;
        }

        myself = getusername_malloc();
        if (!myself)
                return NULL;

        l = new(char*, 2);
        if (!l)
                return NULL;

        l[0] = TAKE_PTR(myself);
        l[1] = NULL;

        *ret_allocated = l;
        return l;
}

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;
        int r, ret = 0;
        char **items, **i;

        (void) pager_open(arg_pager_flags);

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        items = mangle_user_list(strv_skip(argv, 1), &mangled_list);
        if (!items)
                return log_oom();

        STRV_FOREACH(i, items) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
                _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
                _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
                const char *json;
                int incomplete;
                uid_t uid;

                r = parse_uid(*i, &uid);
                if (r < 0) {
                        if (!valid_user_group_name(*i, 0)) {
                                log_error("Invalid user name '%s'.", *i);
                                if (ret == 0)
                                        ret = -EINVAL;

                                continue;
                        }

                        r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", *i);
                } else
                        r = bus_call_method(bus, bus_mgr, "GetUserRecordByUID", &error, &reply, "u", (uint32_t) uid);

                if (r < 0) {
                        log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
                        if (ret == 0)
                                ret = r;

                        continue;
                }

                r = sd_bus_message_read(reply, "sbo", &json, &incomplete, NULL);
                if (r < 0) {
                        bus_log_parse_error(r);
                        if (ret == 0)
                                ret = r;

                        continue;
                }

                r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
                if (r < 0) {
                        log_error_errno(r, "Failed to parse JSON identity: %m");
                        if (ret == 0)
                                ret = r;

                        continue;
                }

                hr = user_record_new();
                if (!hr)
                        return log_oom();

                r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG);
                if (r < 0) {
                        if (ret == 0)
                                ret = r;

                        continue;
                }

                hr->incomplete = incomplete;
                dump_home_record(hr);
        }

        return ret;
}

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;
        int r, ret = 0;
        char **i, **items;

        items = mangle_user_list(strv_skip(argv, 1), &mangled_list);
        if (!items)
                return log_oom();

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);

        STRV_FOREACH(i, items) {
                _cleanup_(user_record_unrefp) UserRecord *secret = NULL;

                secret = user_record_new();
                if (!secret)
                        return log_oom();

                for (;;) {
                        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                        r = bus_message_new_method_call(bus, &m, bus_mgr, "AuthenticateHome");
                        if (r < 0)
                                return bus_log_create_error(r);

                        r = sd_bus_message_append(m, "s", *i);
                        if (r < 0)
                                return bus_log_create_error(r);

                        r = bus_message_append_secret(m, secret);
                        if (r < 0)
                                return bus_log_create_error(r);

                        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                        if (r < 0) {
                                r = handle_generic_user_record_error(*i, secret, &error, r, false);
                                if (r < 0) {
                                        if (ret == 0)
                                                ret = r;

                                        break;
                                }
                        } else
                                break;
                }
        }

        return ret;
}

static int update_last_change(JsonVariant **v, bool with_password, bool override) {
        JsonVariant *c;
        usec_t n;
        int r;

        assert(v);

        n = now(CLOCK_REALTIME);

        c = json_variant_by_key(*v, "lastChangeUSec");
        if (c) {
                uintmax_t u;

                if (!override)
                        goto update_password;

                if (!json_variant_is_unsigned(c))
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec field is not an unsigned integer, refusing.");

                u = json_variant_unsigned(c);
                if (u >= n)
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec is from the future, can't update.");
        }

        r = json_variant_set_field_unsigned(v, "lastChangeUSec", n);
        if (r < 0)
                return log_error_errno(r, "Failed to update lastChangeUSec: %m");

update_password:
        if (!with_password)
                return 0;

        c = json_variant_by_key(*v, "lastPasswordChangeUSec");
        if (c) {
                uintmax_t u;

                if (!override)
                        return 0;

                if (!json_variant_is_unsigned(c))
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastPasswordChangeUSec field is not an unsigned integer, refusing.");

                u = json_variant_unsigned(c);
                if (u >= n)
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastPasswordChangeUSec is from the future, can't update.");
        }

        r = json_variant_set_field_unsigned(v, "lastPasswordChangeUSec", n);
        if (r < 0)
                return log_error_errno(r, "Failed to update lastPasswordChangeUSec: %m");

        return 1;
}

static int apply_identity_changes(JsonVariant **_v) {
        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
        int r;

        assert(_v);

        v = json_variant_ref(*_v);

        r = json_variant_filter(&v, arg_identity_filter);
        if (r < 0)
                return log_error_errno(r, "Failed to filter identity: %m");

        r = json_variant_merge(&v, arg_identity_extra);
        if (r < 0)
                return log_error_errno(r, "Failed to merge identities: %m");

        if (arg_identity_extra_this_machine || !strv_isempty(arg_identity_filter)) {
                _cleanup_(json_variant_unrefp) JsonVariant *per_machine = NULL, *mmid = NULL;
                char mids[SD_ID128_STRING_MAX];
                sd_id128_t mid;

                r = sd_id128_get_machine(&mid);
                if (r < 0)
                        return log_error_errno(r, "Failed to acquire machine ID: %m");

                r = json_variant_new_string(&mmid, sd_id128_to_string(mid, mids));
                if (r < 0)
                        return log_error_errno(r, "Failed to allocate matchMachineId object: %m");

                per_machine = json_variant_ref(json_variant_by_key(v, "perMachine"));
                if (per_machine) {
                        _cleanup_(json_variant_unrefp) JsonVariant *npm = NULL, *add = NULL;
                        _cleanup_free_ JsonVariant **array = NULL;
                        JsonVariant *z;
                        size_t i = 0;

                        if (!json_variant_is_array(per_machine))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine field is not an array, refusing.");

                        array = new(JsonVariant*, json_variant_elements(per_machine) + 1);
                        if (!array)
                                return log_oom();

                        JSON_VARIANT_ARRAY_FOREACH(z, per_machine) {
                                JsonVariant *u;

                                if (!json_variant_is_object(z))
                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine entry is not an object, refusing.");

                                array[i++] = z;

                                u = json_variant_by_key(z, "matchMachineId");
                                if (!u)
                                        continue;

                                if (!json_variant_equal(u, mmid))
                                        continue;

                                r = json_variant_merge(&add, z);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to merge perMachine entry: %m");

                                i--;
                        }

                        r = json_variant_filter(&add, arg_identity_filter);
                        if (r < 0)
                                return log_error_errno(r, "Failed to filter perMachine: %m");

                        r = json_variant_merge(&add, arg_identity_extra_this_machine);
                        if (r < 0)
                                return log_error_errno(r, "Failed to merge in perMachine fields: %m");

                        if (arg_identity_filter_rlimits || arg_identity_extra_rlimits) {
                                _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL;

                                rlv = json_variant_ref(json_variant_by_key(add, "resourceLimits"));

                                r = json_variant_filter(&rlv, arg_identity_filter_rlimits);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to filter resource limits: %m");

                                r = json_variant_merge(&rlv, arg_identity_extra_rlimits);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to set resource limits: %m");

                                if (json_variant_is_blank_object(rlv)) {
                                        r = json_variant_filter(&add, STRV_MAKE("resourceLimits"));
                                        if (r < 0)
                                                return log_error_errno(r, "Failed to drop resource limits field from identity: %m");
                                } else {
                                        r = json_variant_set_field(&add, "resourceLimits", rlv);
                                        if (r < 0)
                                                return log_error_errno(r, "Failed to update resource limits of identity: %m");
                                }
                        }

                        if (!json_variant_is_blank_object(add)) {
                                r = json_variant_set_field(&add, "matchMachineId", mmid);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to set matchMachineId field: %m");

                                array[i++] = add;
                        }

                        r = json_variant_new_array(&npm, array, i);
                        if (r < 0)
                                return log_error_errno(r, "Failed to allocate new perMachine array: %m");

                        json_variant_unref(per_machine);
                        per_machine = TAKE_PTR(npm);
                } else {
                        _cleanup_(json_variant_unrefp) JsonVariant *item = json_variant_ref(arg_identity_extra_this_machine);

                        if (arg_identity_extra_rlimits) {
                                r = json_variant_set_field(&item, "resourceLimits", arg_identity_extra_rlimits);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to update resource limits of identity: %m");
                        }

                        r = json_variant_set_field(&item, "matchMachineId", mmid);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set matchMachineId field: %m");

                        r = json_variant_append_array(&per_machine, item);
                        if (r < 0)
                                return log_error_errno(r, "Failed to append to perMachine array: %m");
                }

                r = json_variant_set_field(&v, "perMachine", per_machine);
                if (r < 0)
                        return log_error_errno(r, "Failed to update per machine record: %m");
        }

        if (arg_identity_extra_privileged || arg_identity_filter) {
                _cleanup_(json_variant_unrefp) JsonVariant *privileged = NULL;

                privileged = json_variant_ref(json_variant_by_key(v, "privileged"));

                r = json_variant_filter(&privileged, arg_identity_filter);
                if (r < 0)
                        return log_error_errno(r, "Failed to filter identity (privileged part): %m");

                r = json_variant_merge(&privileged, arg_identity_extra_privileged);
                if (r < 0)
                        return log_error_errno(r, "Failed to merge identities (privileged part): %m");

                if (json_variant_is_blank_object(privileged)) {
                        r = json_variant_filter(&v, STRV_MAKE("privileged"));
                        if (r < 0)
                                return log_error_errno(r, "Failed to drop privileged part from identity: %m");
                } else {
                        r = json_variant_set_field(&v, "privileged", privileged);
                        if (r < 0)
                                return log_error_errno(r, "Failed to update privileged part of identity: %m");
                }
        }

        if (arg_identity_filter_rlimits) {
                _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL;

                rlv = json_variant_ref(json_variant_by_key(v, "resourceLimits"));

                r = json_variant_filter(&rlv, arg_identity_filter_rlimits);
                if (r < 0)
                        return log_error_errno(r, "Failed to filter resource limits: %m");

                /* Note that we only filter resource limits here, but don't apply them. We do that in the perMachine section */

                if (json_variant_is_blank_object(rlv)) {
                        r = json_variant_filter(&v, STRV_MAKE("resourceLimits"));
                        if (r < 0)
                                return log_error_errno(r, "Failed to drop resource limits field from identity: %m");
                } else {
                        r = json_variant_set_field(&v, "resourceLimits", rlv);
                        if (r < 0)
                                return log_error_errno(r, "Failed to update resource limits of identity: %m");
                }
        }

        json_variant_unref(*_v);
        *_v = TAKE_PTR(v);

        return 0;
}

static int add_disposition(JsonVariant **v) {
        int r;

        assert(v);

        if (json_variant_by_key(*v, "disposition"))
                return 0;

        /* Set the disposition to regular, if not configured explicitly */
        r = json_variant_set_field_string(v, "disposition", "regular");
        if (r < 0)
                return log_error_errno(r, "Failed to set disposition field: %m");

        return 1;
}

static int acquire_new_home_record(UserRecord **ret) {
        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
        char **i;
        int r;

        assert(ret);

        if (arg_identity) {
                unsigned line, column;

                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);
        }

        r = apply_identity_changes(&v);
        if (r < 0)
                return r;

        r = add_disposition(&v);
        if (r < 0)
                return r;

        STRV_FOREACH(i, arg_pkcs11_token_uri) {
                r = identity_add_pkcs11_key_data(&v, *i);
                if (r < 0)
                        return r;
        }

        STRV_FOREACH(i, arg_fido2_device) {
                r = identity_add_fido2_parameters(&v, *i);
                if (r < 0)
                        return r;
        }

        if (arg_recovery_key) {
                r = identity_add_recovery_key(&v);
                if (r < 0)
                        return r;
        }

        r = update_last_change(&v, true, false);
        if (r < 0)
                return r;

        if (DEBUG_LOGGING)
                json_variant_dump(v, JSON_FORMAT_PRETTY, NULL, NULL);

        hr = user_record_new();
        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);
        if (r < 0)
                return r;

        *ret = TAKE_PTR(hr);
        return 0;
}

static int acquire_new_password(
                const char *user_name,
                UserRecord *hr,
                bool suggest,
                char **ret) {

        unsigned i = 5;
        char *e;
        int r;

        assert(user_name);
        assert(hr);

        e = getenv("NEWPASSWORD");
        if (e) {
                _cleanup_(erase_and_freep) char *copy = NULL;

                /* As above, this is not for use, just for testing */

                if (ret) {
                        copy = strdup(e);
                        if (!copy)
                                return log_oom();
                }

                r = user_record_set_password(hr, STRV_MAKE(e), /* prepend = */ true);
                if (r < 0)
                        return log_error_errno(r, "Failed to store password: %m");

                string_erase(e);

                if (unsetenv("NEWPASSWORD") < 0)
                        return log_error_errno(errno, "Failed to unset $NEWPASSWORD: %m");

                if (ret)
                        *ret = TAKE_PTR(copy);

                return 0;
        }

        if (suggest)
                (void) suggest_passwords();

        for (;;) {
                _cleanup_(strv_free_erasep) char **first = NULL, **second = NULL;
                _cleanup_free_ char *question = NULL;

                if (--i == 0)
                        return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up:");

                if (asprintf(&question, "Please enter new password for user %s:", user_name) < 0)
                        return log_oom();

                r = ask_password_auto(question, "user-home", NULL, "home-password", USEC_INFINITY, 0, &first);
                if (r < 0)
                        return log_error_errno(r, "Failed to acquire password: %m");

                question = mfree(question);
                if (asprintf(&question, "Please enter new password for user %s (repeat):", user_name) < 0)
                        return log_oom();

                r = ask_password_auto(question, "user-home", NULL, "home-password", USEC_INFINITY, 0, &second);
                if (r < 0)
                        return log_error_errno(r, "Failed to acquire password: %m");

                if (strv_equal(first, second)) {
                        _cleanup_(erase_and_freep) char *copy = NULL;

                        if (ret) {
                                copy = strdup(first[0]);
                                if (!copy)
                                        return log_oom();
                        }

                        r = user_record_set_password(hr, first, /* prepend = */ true);
                        if (r < 0)
                                return log_error_errno(r, "Failed to store password: %m");

                        if (ret)
                                *ret = TAKE_PTR(copy);

                        return 0;
                }

                log_error("Password didn't match, try again.");
        }
}

static int create_home(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
        int r;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        (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);
        if (r < 0)
                return r;

        /* If the JSON record carries no plain text password (besides the recovery key), then let's query it
         * manually. */
        if (strv_length(hr->password) <= arg_recovery_key) {

                if (strv_isempty(hr->hashed_password)) {
                        _cleanup_(erase_and_freep) char *new_password = NULL;

                        /* No regular (i.e. non-PKCS#11) hashed passwords set in the record, let's fix that. */
                        r = acquire_new_password(hr->user_name, hr, /* suggest = */ true, &new_password);
                        if (r < 0)
                                return r;

                        r = user_record_make_hashed_password(hr, STRV_MAKE(new_password), /* extend = */ false);
                        if (r < 0)
                                return log_error_errno(r, "Failed to hash password: %m");
                } else {
                        /* There's a hash password set in the record, acquire the unhashed version of it. */
                        r = acquire_existing_password(hr->user_name, hr, /* emphasize_current= */ false);
                        if (r < 0)
                                return r;
                }
        }

        if (hr->enforce_password_policy == 0) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;

                /* If password quality enforcement is disabled, let's at least warn client side */

                r = user_record_quality_check_password(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));
        }

        for (;;) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
                _cleanup_(erase_and_freep) char *formatted = NULL;

                r = json_variant_format(hr->json, 0, &formatted);
                if (r < 0)
                        return log_error_errno(r, "Failed to format user record: %m");

                r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHome");
                if (r < 0)
                        return bus_log_create_error(r);

                (void) sd_bus_message_sensitive(m);

                r = sd_bus_message_append(m, "s", formatted);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                if (r < 0) {
                        if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) {
                                _cleanup_(erase_and_freep) char *new_password = NULL;

                                log_error_errno(r, "%s", bus_error_message(&error, r));
                                log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)");

                                r = acquire_new_password(hr->user_name, hr, /* suggest = */ false, &new_password);
                                if (r < 0)
                                        return r;

                                r = user_record_make_hashed_password(hr, STRV_MAKE(new_password), /* extend = */ false);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to hash passwords: %m");
                        } else {
                                r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false);
                                if (r < 0)
                                        return r;
                        }
                } else
                        break; /* done */
        }

        return 0;
}

static int remove_home(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        int r, ret = 0;
        char **i;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);

        STRV_FOREACH(i, strv_skip(argv, 1)) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                r = bus_message_new_method_call(bus, &m, bus_mgr, "RemoveHome");
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_message_append(m, "s", *i);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                if (r < 0) {
                        log_error_errno(r, "Failed to remove home: %s", bus_error_message(&error, r));
                        if (ret == 0)
                                ret = r;
                }
        }

        return ret;
}

static int acquire_updated_home_record(
                sd_bus *bus,
                const char *username,
                UserRecord **ret) {

        _cleanup_(json_variant_unrefp) JsonVariant *json = NULL;
        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
        char **i;
        int r;

        assert(ret);

        if (arg_identity) {
                unsigned line, column;
                JsonVariant *un;

                r = json_parse_file(
                                streq(arg_identity, "-") ? stdin : NULL,
                                streq(arg_identity, "-") ? "<stdin>" : arg_identity, JSON_PARSE_SENSITIVE, &json, &line, &column);
                if (r < 0)
                        return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);

                un = json_variant_by_key(json, "userName");
                if (un) {
                        if (!json_variant_is_string(un) || (username && !streq(json_variant_string(un), username)))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match.");
                } else {
                        if (!username)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified.");

                        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");
                }

        } else {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
                int incomplete;
                const char *text;

                if (!identity_properties_specified())
                        return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified.");

                r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", username);
                if (r < 0)
                        return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r));

                r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL);
                if (r < 0)
                        return bus_log_parse_error(r);

                if (incomplete)
                        return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record.");

                r = json_parse(text, JSON_PARSE_SENSITIVE, &json, NULL, NULL);
                if (r < 0)
                        return log_error_errno(r, "Failed to parse JSON identity: %m");

                reply = sd_bus_message_unref(reply);

                r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature"));
                if (r < 0)
                        return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
        }

        r = apply_identity_changes(&json);
        if (r < 0)
                return r;

        STRV_FOREACH(i, arg_pkcs11_token_uri) {
                r = identity_add_pkcs11_key_data(&json, *i);
                if (r < 0)
                        return r;
        }

        STRV_FOREACH(i, arg_fido2_device) {
                r = identity_add_fido2_parameters(&json, *i);
                if (r < 0)
                        return r;
        }

        /* If the user supplied a full record, then add in lastChange, but do not override. Otherwise always
         * override. */
        r = update_last_change(&json, arg_pkcs11_token_uri || arg_fido2_device, !arg_identity);
        if (r < 0)
                return r;

        if (DEBUG_LOGGING)
                json_variant_dump(json, JSON_FORMAT_PRETTY, NULL, NULL);

        hr = user_record_new();
        if (!hr)
                return log_oom();

        r = user_record_load(hr, json, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG);
        if (r < 0)
                return r;

        *ret = TAKE_PTR(hr);
        return 0;
}

static int home_record_reset_human_interaction_permission(UserRecord *hr) {
        int r;

        assert(hr);

        /* When we execute multiple operations one after the other, let's reset the permission to ask the
         * user each time, so that if interaction is necessary we will be told so again and thus can print a
         * nice message to the user, telling the user so. */

        r = user_record_set_pkcs11_protected_authentication_path_permitted(hr, -1);
        if (r < 0)
                return log_error_errno(r, "Failed to reset PKCS#11 protected authentication path permission flag: %m");

        r = user_record_set_fido2_user_presence_permitted(hr, -1);
        if (r < 0)
                return log_error_errno(r, "Failed to reset FIDO2 user presence permission flag: %m");

        return 0;
}

static int update_home(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
        _cleanup_free_ char *buffer = NULL;
        const char *username;
        int r;

        if (argc >= 2)
                username = argv[1];
        else if (!arg_identity) {
                buffer = getusername_malloc();
                if (!buffer)
                        return log_oom();

                username = buffer;
        } else
                username = NULL;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);

        r = acquire_updated_home_record(bus, username, &hr);
        if (r < 0)
                return r;

        /* If we do multiple operations, let's output things more verbosely, since otherwise the repeated
         * authentication might be confusing. */

        if (arg_and_resize || arg_and_change_password)
                log_info("Updating home directory.");

        for (;;) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
                _cleanup_free_ char *formatted = NULL;

                r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHome");
                if (r < 0)
                        return bus_log_create_error(r);

                r = json_variant_format(hr->json, 0, &formatted);
                if (r < 0)
                        return log_error_errno(r, "Failed to format user record: %m");

                (void) sd_bus_message_sensitive(m);

                r = sd_bus_message_append(m, "s", formatted);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                if (r < 0) {
                        if (arg_and_change_password &&
                            sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))
                                /* In the generic handler we'd ask for a password in this case, but when
                                 * changing passwords that's not sufficient, as we need to acquire all keys
                                 * first. */
                                return log_error_errno(r, "Security token not inserted, refusing.");

                        r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false);
                        if (r < 0)
                                return r;
                } else
                        break;
        }

        if (arg_and_resize)
                log_info("Resizing home.");

        (void) home_record_reset_human_interaction_permission(hr);

        /* Also sync down disk size to underlying LUKS/fscrypt/quota */
        while (arg_and_resize) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                r = bus_message_new_method_call(bus, &m, bus_mgr, "ResizeHome");
                if (r < 0)
                        return bus_log_create_error(r);

                /* Specify UINT64_MAX as size, in which case the underlying disk size will just be synced */
                r = sd_bus_message_append(m, "st", hr->user_name, UINT64_MAX);
                if (r < 0)
                        return bus_log_create_error(r);

                r = bus_message_append_secret(m, hr);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                if (r < 0) {
                        if (arg_and_change_password &&
                            sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))
                                return log_error_errno(r, "Security token not inserted, refusing.");

                        r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false);
                        if (r < 0)
                                return r;
                } else
                        break;
        }

        if (arg_and_change_password)
                log_info("Synchronizing passwords and encryption keys.");

        (void) home_record_reset_human_interaction_permission(hr);

        /* Also sync down passwords to underlying LUKS/fscrypt */
        while (arg_and_change_password) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                r = bus_message_new_method_call(bus, &m, bus_mgr, "ChangePasswordHome");
                if (r < 0)
                        return bus_log_create_error(r);

                /* Specify an empty new secret, in which case the underlying LUKS/fscrypt password will just be synced */
                r = sd_bus_message_append(m, "ss", hr->user_name, "{}");
                if (r < 0)
                        return bus_log_create_error(r);

                r = bus_message_append_secret(m, hr);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                if (r < 0) {
                        if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))
                                return log_error_errno(r, "Security token not inserted, refusing.");

                        r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false);
                        if (r < 0)
                                return r;
                } else
                        break;
        }

        return 0;
}

static int passwd_home(int argc, char *argv[], void *userdata) {
        _cleanup_(user_record_unrefp) UserRecord *old_secret = NULL, *new_secret = NULL;
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        _cleanup_free_ char *buffer = NULL;
        const char *username;
        int r;

        if (arg_pkcs11_token_uri)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "To change the PKCS#11 security token use 'homectl update --pkcs11-token-uri=…'.");
        if (arg_fido2_device)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "To change the FIDO2 security token use 'homectl update --fido2-device=…'.");
        if (identity_properties_specified())
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The 'passwd' verb does not permit changing other record properties at the same time.");

        if (argc >= 2)
                username = argv[1];
        else {
                buffer = getusername_malloc();
                if (!buffer)
                        return log_oom();

                username = buffer;
        }

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);

        old_secret = user_record_new();
        if (!old_secret)
                return log_oom();

        new_secret = user_record_new();
        if (!new_secret)
                return log_oom();

        r = acquire_new_password(username, new_secret, /* suggest = */ true, NULL);
        if (r < 0)
                return r;

        for (;;) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                r = bus_message_new_method_call(bus, &m, bus_mgr, "ChangePasswordHome");
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_message_append(m, "s", username);
                if (r < 0)
                        return bus_log_create_error(r);

                r = bus_message_append_secret(m, new_secret);
                if (r < 0)
                        return bus_log_create_error(r);

                r = bus_message_append_secret(m, old_secret);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                if (r < 0) {
                        if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) {

                                log_error_errno(r, "%s", bus_error_message(&error, r));

                                r = acquire_new_password(username, new_secret, /* suggest = */ false, NULL);

                        } else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))

                                /* In the generic handler we'd ask for a password in this case, but when
                                 * changing passwords that's not sufficeint, as we need to acquire all keys
                                 * first. */
                                return log_error_errno(r, "Security token not inserted, refusing.");
                        else
                                r = handle_generic_user_record_error(username, old_secret, &error, r, true);
                        if (r < 0)
                                return r;
                } else
                        break;
        }

        return 0;
}

static int resize_home(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
        uint64_t ds = UINT64_MAX;
        int r;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);

        if (arg_disk_size_relative != UINT64_MAX ||
            (argc > 2 && parse_percent(argv[2]) >= 0))
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
                                               "Relative disk size specification currently not supported when resizing.");

        if (argc > 2) {
                r = parse_size(argv[2], 1024, &ds);
                if (r < 0)
                        return log_error_errno(r, "Failed to parse disk size parameter: %s", argv[2]);
        }

        if (arg_disk_size != UINT64_MAX) {
                if (ds != UINT64_MAX && ds != arg_disk_size)
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size specified twice and doesn't match, refusing.");

                ds = arg_disk_size;
        }

        secret = user_record_new();
        if (!secret)
                return log_oom();

        for (;;) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                r = bus_message_new_method_call(bus, &m, bus_mgr, "ResizeHome");
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_message_append(m, "st", argv[1], ds);
                if (r < 0)
                        return bus_log_create_error(r);

                r = bus_message_append_secret(m, secret);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                if (r < 0) {
                        r = handle_generic_user_record_error(argv[1], secret, &error, r, false);
                        if (r < 0)
                                return r;
                } else
                        break;
        }

        return 0;
}

static int lock_home(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        int r, ret = 0;
        char **i;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        STRV_FOREACH(i, strv_skip(argv, 1)) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                r = bus_message_new_method_call(bus, &m, bus_mgr, "LockHome");
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_message_append(m, "s", *i);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                if (r < 0) {
                        log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r));
                        if (ret == 0)
                                ret = r;
                }
        }

        return ret;
}

static int unlock_home(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        int r, ret = 0;
        char **i;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        STRV_FOREACH(i, strv_skip(argv, 1)) {
                _cleanup_(user_record_unrefp) UserRecord *secret = NULL;

                secret = user_record_new();
                if (!secret)
                        return log_oom();

                for (;;) {
                        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                        r = bus_message_new_method_call(bus, &m, bus_mgr, "UnlockHome");
                        if (r < 0)
                                return bus_log_create_error(r);

                        r = sd_bus_message_append(m, "s", *i);
                        if (r < 0)
                                return bus_log_create_error(r);

                        r = bus_message_append_secret(m, secret);
                        if (r < 0)
                                return bus_log_create_error(r);

                        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                        if (r < 0) {
                                r = handle_generic_user_record_error(argv[1], secret, &error, r, false);
                                if (r < 0) {
                                        if (ret == 0)
                                                ret = r;

                                        break;
                                }
                        } else
                                break;
                }
        }

        return ret;
}

static int with_home(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
        _cleanup_close_ int acquired_fd = -1;
        _cleanup_strv_free_ char **cmdline  = NULL;
        const char *home;
        int r, ret;
        pid_t pid;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        if (argc < 3) {
                _cleanup_free_ char *shell = NULL;

                /* If no command is specified, spawn a shell */
                r = get_shell(&shell);
                if (r < 0)
                        return log_error_errno(r, "Failed to acquire shell: %m");

                cmdline = strv_new(shell);
        } else
                cmdline = strv_copy(argv + 2);
        if (!cmdline)
                return log_oom();

        secret = user_record_new();
        if (!secret)
                return log_oom();

        for (;;) {
                r = bus_message_new_method_call(bus, &m, bus_mgr, "AcquireHome");
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_message_append(m, "s", argv[1]);
                if (r < 0)
                        return bus_log_create_error(r);

                r = bus_message_append_secret(m, secret);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_message_append(m, "b", /* please_suspend = */ getenv_bool("SYSTEMD_PLEASE_SUSPEND_HOME") > 0);
                if (r < 0)
                        return bus_log_create_error(r);

                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply);
                m = sd_bus_message_unref(m);
                if (r < 0) {
                        r = handle_generic_user_record_error(argv[1], secret, &error, r, false);
                        if (r < 0)
                                return r;

                        sd_bus_error_free(&error);
                } else {
                        int fd;

                        r = sd_bus_message_read(reply, "h", &fd);
                        if (r < 0)
                                return bus_log_parse_error(r);

                        acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
                        if (acquired_fd < 0)
                                return log_error_errno(errno, "Failed to duplicate acquired fd: %m");

                        reply = sd_bus_message_unref(reply);
                        break;
                }
        }

        r = bus_call_method(bus, bus_mgr, "GetHomeByName", &error, &reply, "s", argv[1]);
        if (r < 0)
                return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));

        r = sd_bus_message_read(reply, "usussso", NULL, NULL, NULL, NULL, &home, NULL, NULL);
        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);
        if (r < 0)
                return r;
        if (r == 0) {
                if (chdir(home) < 0) {
                        log_error_errno(errno, "Failed to change to directory %s: %m", home);
                        _exit(255);
                }

                execvp(cmdline[0], cmdline);
                log_error_errno(errno, "Failed to execute %s: %m", cmdline[0]);
                _exit(255);
        }

        ret = wait_for_terminate_and_check(cmdline[0], pid, WAIT_LOG_ABNORMAL);

        /* Close the fd that pings the home now. */
        acquired_fd = safe_close(acquired_fd);

        r = bus_message_new_method_call(bus, &m, bus_mgr, "ReleaseHome");
        if (r < 0)
                return bus_log_create_error(r);

        r = sd_bus_message_append(m, "s", argv[1]);
        if (r < 0)
                return bus_log_create_error(r);

        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
        if (r < 0) {
                if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY))
                        log_notice("Not deactivating home directory of %s, as it is still used.", argv[1]);
                else
                        return log_error_errno(r, "Failed to release user home: %s", bus_error_message(&error, r));
        }

        return ret;
}

static int lock_all_homes(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        int r;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        r = bus_message_new_method_call(bus, &m, bus_mgr, "LockAllHomes");
        if (r < 0)
                return bus_log_create_error(r);

        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to lock all homes: %s", bus_error_message(&error, r));

        return 0;
}

static int deactivate_all_homes(int argc, char *argv[], void *userdata) {
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        int r;

        r = acquire_bus(&bus);
        if (r < 0)
                return r;

        r = bus_message_new_method_call(bus, &m, bus_mgr, "DeactivateAllHomes");
        if (r < 0)
                return bus_log_create_error(r);

        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to deactivate all homes: %s", bus_error_message(&error, r));

        return 0;
}

static int drop_from_identity(const char *field) {
        int r;

        assert(field);

        /* If we are called to update an identity record and drop some field, let's keep track of what to
         * remove from the old record */
        r = strv_extend(&arg_identity_filter, field);
        if (r < 0)
                return log_oom();

        /* Let's also drop the field if it was previously set to a new value on the same command line */
        r = json_variant_filter(&arg_identity_extra, STRV_MAKE(field));
        if (r < 0)
                return log_error_errno(r, "Failed to filter JSON identity data: %m");

        r = json_variant_filter(&arg_identity_extra_this_machine, STRV_MAKE(field));
        if (r < 0)
                return log_error_errno(r, "Failed to filter JSON identity data: %m");

        r = json_variant_filter(&arg_identity_extra_privileged, STRV_MAKE(field));
        if (r < 0)
                return log_error_errno(r, "Failed to filter JSON identity data: %m");

        return 0;
}

static int help(int argc, char *argv[], void *userdata) {
        _cleanup_free_ char *link = NULL;
        int r;

        (void) pager_open(arg_pager_flags);

        r = terminal_urlify_man("homectl", "1", &link);
        if (r < 0)
                return log_oom();

        printf("%1$s [OPTIONS...] COMMAND ...\n\n"
               "%2$sCreate, manipulate or inspect home directories.%3$s\n"
               "\n%4$sCommands:%5$s\n"
               "  list                        List home areas\n"
               "  activate USER…              Activate a home area\n"
               "  deactivate USER…            Deactivate a home area\n"
               "  inspect USER…               Inspect a home area\n"
               "  authenticate USER…          Authenticate a home area\n"
               "  create USER                 Create a home area\n"
               "  remove USER…                Remove a home area\n"
               "  update USER                 Update a home area\n"
               "  passwd USER                 Change password of a home area\n"
               "  resize USER SIZE            Resize a home area\n"
               "  lock USER…                  Temporarily lock an active home area\n"
               "  unlock USER…                Unlock a temporarily locked home area\n"
               "  lock-all                    Lock all suitable home areas\n"
               "  deactivate-all              Deactivate all active home areas\n"
               "  with USER [COMMAND…]        Run shell or command with access to a home area\n"
               "\n%4$sOptions:%5$s\n"
               "  -h --help                   Show this help\n"
               "     --version                Show package version\n"
               "     --no-pager               Do not pipe output into a pager\n"
               "     --no-legend              Do not show the headers and footers\n"
               "     --no-ask-password        Do not ask for system passwords\n"
               "  -H --host=[USER@]HOST       Operate on remote host\n"
               "  -M --machine=CONTAINER      Operate on local container\n"
               "     --identity=PATH          Read JSON identity from file\n"
               "     --json=FORMAT            Output inspection data in JSON (takes one of\n"
               "                              pretty, short, off)\n"
               "  -j                          Equivalent to --json=pretty (on TTY) or\n"
               "                              --json=short (otherwise)\n"
               "     --export-format=         Strip JSON inspection data (full, stripped,\n"
               "                              minimal)\n"
               "  -E                          When specified once equals -j --export-format=\n"
               "                              stripped, when specified twice equals\n"
               "                              -j --export-format=minimal\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"
               "     --email-address=EMAIL    Email address for user\n"
               "     --location=LOCATION      Set location of user on earth\n"
               "     --icon-name=NAME         Icon name for user\n"
               "  -d --home-dir=PATH          Home directory\n"
               "  -u --uid=UID                Numeric UID for user\n"
               "  -G --member-of=GROUP        Add user to group\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"
               "     --timezone=TIMEZONE      Set a time-zone\n"
               "     --language=LOCALE        Set preferred language\n"
               "     --ssh-authorized-keys=KEYS\n"
               "                              Specify SSH public keys\n"
               "     --pkcs11-token-uri=URI   URI to PKCS#11 security token containing\n"
               "                              private key and matching X.509 certificate\n"
               "     --fido2-device=PATH      Path to FIDO2 hidraw device with hmac-secret\n"
               "                              extension\n"
               "     --recovery-key=BOOL      Add a recovery key\n"
               "\n%4$sAccount Management User Record Properties:%5$s\n"
               "     --locked=BOOL            Set locked account state\n"
               "     --not-before=TIMESTAMP   Do not allow logins before\n"
               "     --not-after=TIMESTAMP    Do not allow logins after\n"
               "     --rate-limit-interval=SECS\n"
               "                              Login rate-limit interval in seconds\n"
               "     --rate-limit-burst=NUMBER\n"
               "                              Login rate-limit attempts per interval\n"
               "\n%4$sPassword Policy User Record Properties:%5$s\n"
               "     --password-hint=HINT     Set Password hint\n"
               "     --enforce-password-policy=BOOL\n"
               "                              Control whether to enforce system's password\n"
               "                              policy for this user\n"
               "  -P                          Equivalent to --enforce-password-password=no\n"
               "     --password-change-now=BOOL\n"
               "                              Require the password to be changed on next login\n"
               "     --password-change-min=TIME\n"
               "                              Require minimum time between password changes\n"
               "     --password-change-max=TIME\n"
               "                              Require maximum time between password changes\n"
               "     --password-change-warn=TIME\n"
               "                              How much time to warn before password expiry\n"
               "     --password-change-inactive=TIME\n"
               "                              How much time to block password after expiry\n"
               "\n%4$sResource Management User Record Properties:%5$s\n"
               "     --disk-size=BYTES        Size to assign the user on disk\n"
               "     --access-mode=MODE       User home directory access mode\n"
               "     --umask=MODE             Umask for user when logging in\n"
               "     --nice=NICE              Nice level for user\n"
               "     --rlimit=LIMIT=VALUE[:VALUE]\n"
               "                              Set resource limits\n"
               "     --tasks-max=MAX          Set maximum number of per-user tasks\n"
               "     --memory-high=BYTES      Set high memory threshold in bytes\n"
               "     --memory-max=BYTES       Set maximum memory limit\n"
               "     --cpu-weight=WEIGHT      Set CPU weight\n"
               "     --io-weight=WEIGHT       Set IO weight\n"
               "\n%4$sStorage User Record Properties:%5$s\n"
               "     --storage=STORAGE        Storage type to use (luks, fscrypt, directory,\n"
               "                              subvolume, cifs)\n"
               "     --image-path=PATH        Path to image file/directory\n"
               "\n%4$sLUKS Storage User Record Properties:%5$s\n"
               "     --fs-type=TYPE           File system type to use in case of luks\n"
               "                              storage (btrfs, ext4, xfs)\n"
               "     --luks-discard=BOOL      Whether to use 'discard' feature of file system\n"
               "                              when activated (mounted)\n"
               "     --luks-offline-discard=BOOL\n"
               "                              Whether to trim file on logout\n"
               "     --luks-cipher=CIPHER     Cipher to use for LUKS encryption\n"
               "     --luks-cipher-mode=MODE  Cipher mode to use for LUKS encryption\n"
               "     --luks-volume-key-size=BITS\n"
               "                              Volume key size to use for LUKS encryption\n"
               "     --luks-pbkdf-type=TYPE   Password-based Key Derivation Function to use\n"
               "     --luks-pbkdf-hash-algorithm=ALGORITHM\n"
               "                              PBKDF hash algorithm to use\n"
               "     --luks-pbkdf-time-cost=SECS\n"
               "                              Time cost for PBKDF in seconds\n"
               "     --luks-pbkdf-memory-cost=BYTES\n"
               "                              Memory cost for PBKDF in bytes\n"
               "     --luks-pbkdf-parallel-threads=NUMBER\n"
               "                              Number of parallel threads for PKBDF\n"
               "\n%4$sMounting User Record Properties:%5$s\n"
               "     --nosuid=BOOL            Control the 'nosuid' flag of the home mount\n"
               "     --nodev=BOOL             Control the 'nodev' flag of the home mount\n"
               "     --noexec=BOOL            Control the 'noexec' flag of the home mount\n"
               "\n%4$sCIFS User Record Properties:%5$s\n"
               "     --cifs-domain=DOMAIN     CIFS (Windows) domain\n"
               "     --cifs-user-name=USER    CIFS (Windows) user name\n"
               "     --cifs-service=SERVICE   CIFS (Windows) service to mount as home area\n"
               "\n%4$sLogin Behaviour User Record Properties:%5$s\n"
               "     --stop-delay=SECS        How long to leave user services running after\n"
               "                              logout\n"
               "     --kill-processes=BOOL    Whether to kill user processes when sessions\n"
               "                              terminate\n"
               "     --auto-login=BOOL        Try to log this user in automatically\n"
               "\nSee the %6$s for details.\n"
               , program_invocation_short_name
               , ansi_highlight(), ansi_normal()
               , ansi_underline(), ansi_normal()
               , link
        );

        return 0;
}

static int parse_argv(int argc, char *argv[]) {

        enum {
                ARG_VERSION = 0x100,
                ARG_NO_PAGER,
                ARG_NO_LEGEND,
                ARG_NO_ASK_PASSWORD,
                ARG_REALM,
                ARG_EMAIL_ADDRESS,
                ARG_DISK_SIZE,
                ARG_ACCESS_MODE,
                ARG_STORAGE,
                ARG_FS_TYPE,
                ARG_IMAGE_PATH,
                ARG_UMASK,
                ARG_LUKS_DISCARD,
                ARG_LUKS_OFFLINE_DISCARD,
                ARG_JSON,
                ARG_SETENV,
                ARG_TIMEZONE,
                ARG_LANGUAGE,
                ARG_LOCKED,
                ARG_SSH_AUTHORIZED_KEYS,
                ARG_LOCATION,
                ARG_ICON_NAME,
                ARG_PASSWORD_HINT,
                ARG_NICE,
                ARG_RLIMIT,
                ARG_NOT_BEFORE,
                ARG_NOT_AFTER,
                ARG_LUKS_CIPHER,
                ARG_LUKS_CIPHER_MODE,
                ARG_LUKS_VOLUME_KEY_SIZE,
                ARG_NOSUID,
                ARG_NODEV,
                ARG_NOEXEC,
                ARG_CIFS_DOMAIN,
                ARG_CIFS_USER_NAME,
                ARG_CIFS_SERVICE,
                ARG_TASKS_MAX,
                ARG_MEMORY_HIGH,
                ARG_MEMORY_MAX,
                ARG_CPU_WEIGHT,
                ARG_IO_WEIGHT,
                ARG_LUKS_PBKDF_TYPE,
                ARG_LUKS_PBKDF_HASH_ALGORITHM,
                ARG_LUKS_PBKDF_TIME_COST,
                ARG_LUKS_PBKDF_MEMORY_COST,
                ARG_LUKS_PBKDF_PARALLEL_THREADS,
                ARG_RATE_LIMIT_INTERVAL,
                ARG_RATE_LIMIT_BURST,
                ARG_STOP_DELAY,
                ARG_KILL_PROCESSES,
                ARG_ENFORCE_PASSWORD_POLICY,
                ARG_PASSWORD_CHANGE_NOW,
                ARG_PASSWORD_CHANGE_MIN,
                ARG_PASSWORD_CHANGE_MAX,
                ARG_PASSWORD_CHANGE_WARN,
                ARG_PASSWORD_CHANGE_INACTIVE,
                ARG_EXPORT_FORMAT,
                ARG_AUTO_LOGIN,
                ARG_PKCS11_TOKEN_URI,
                ARG_FIDO2_DEVICE,
                ARG_RECOVERY_KEY,
                ARG_AND_RESIZE,
                ARG_AND_CHANGE_PASSWORD,
        };

        static const struct option options[] = {
                { "help",                        no_argument,       NULL, 'h'                             },
                { "version",                     no_argument,       NULL, ARG_VERSION                     },
                { "no-pager",                    no_argument,       NULL, ARG_NO_PAGER                    },
                { "no-legend",                   no_argument,       NULL, ARG_NO_LEGEND                   },
                { "no-ask-password",             no_argument,       NULL, ARG_NO_ASK_PASSWORD             },
                { "host",                        required_argument, NULL, 'H'                             },
                { "machine",                     required_argument, NULL, 'M'                             },
                { "identity",                    required_argument, NULL, 'I'                             },
                { "real-name",                   required_argument, NULL, 'c'                             },
                { "comment",                     required_argument, NULL, 'c'                             }, /* Compat alias to keep thing in sync with useradd(8) */
                { "realm",                       required_argument, NULL, ARG_REALM                       },
                { "email-address",               required_argument, NULL, ARG_EMAIL_ADDRESS               },
                { "location",                    required_argument, NULL, ARG_LOCATION                    },
                { "password-hint",               required_argument, NULL, ARG_PASSWORD_HINT               },
                { "icon-name",                   required_argument, NULL, ARG_ICON_NAME                   },
                { "home-dir",                    required_argument, NULL, 'd'                             }, /* Compatible with useradd(8) */
                { "uid",                         required_argument, NULL, 'u'                             }, /* Compatible with useradd(8) */
                { "member-of",                   required_argument, NULL, 'G'                             },
                { "groups",                      required_argument, NULL, 'G'                             }, /* Compat alias to keep thing in sync with useradd(8) */
                { "skel",                        required_argument, NULL, 'k'                             }, /* Compatible with useradd(8) */
                { "shell",                       required_argument, NULL, 's'                             }, /* Compatible with useradd(8) */
                { "setenv",                      required_argument, NULL, ARG_SETENV                      },
                { "timezone",                    required_argument, NULL, ARG_TIMEZONE                    },
                { "language",                    required_argument, NULL, ARG_LANGUAGE                    },
                { "locked",                      required_argument, NULL, ARG_LOCKED                      },
                { "not-before",                  required_argument, NULL, ARG_NOT_BEFORE                  },
                { "not-after",                   required_argument, NULL, ARG_NOT_AFTER                   },
                { "expiredate",                  required_argument, NULL, 'e'                             }, /* Compat alias to keep thing in sync with useradd(8) */
                { "ssh-authorized-keys",         required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS         },
                { "disk-size",                   required_argument, NULL, ARG_DISK_SIZE                   },
                { "access-mode",                 required_argument, NULL, ARG_ACCESS_MODE                 },
                { "umask",                       required_argument, NULL, ARG_UMASK                       },
                { "nice",                        required_argument, NULL, ARG_NICE                        },
                { "rlimit",                      required_argument, NULL, ARG_RLIMIT                      },
                { "tasks-max",                   required_argument, NULL, ARG_TASKS_MAX                   },
                { "memory-high",                 required_argument, NULL, ARG_MEMORY_HIGH                 },
                { "memory-max",                  required_argument, NULL, ARG_MEMORY_MAX                  },
                { "cpu-weight",                  required_argument, NULL, ARG_CPU_WEIGHT                  },
                { "io-weight",                   required_argument, NULL, ARG_IO_WEIGHT                   },
                { "storage",                     required_argument, NULL, ARG_STORAGE                     },
                { "image-path",                  required_argument, NULL, ARG_IMAGE_PATH                  },
                { "fs-type",                     required_argument, NULL, ARG_FS_TYPE                     },
                { "luks-discard",                required_argument, NULL, ARG_LUKS_DISCARD                },
                { "luks-offline-discard",        required_argument, NULL, ARG_LUKS_OFFLINE_DISCARD        },
                { "luks-cipher",                 required_argument, NULL, ARG_LUKS_CIPHER                 },
                { "luks-cipher-mode",            required_argument, NULL, ARG_LUKS_CIPHER_MODE            },
                { "luks-volume-key-size",        required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE        },
                { "luks-pbkdf-type",             required_argument, NULL, ARG_LUKS_PBKDF_TYPE             },
                { "luks-pbkdf-hash-algorithm",   required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM   },
                { "luks-pbkdf-time-cost",        required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST        },
                { "luks-pbkdf-memory-cost",      required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST      },
                { "luks-pbkdf-parallel-threads", required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS },
                { "nosuid",                      required_argument, NULL, ARG_NOSUID                      },
                { "nodev",                       required_argument, NULL, ARG_NODEV                       },
                { "noexec",                      required_argument, NULL, ARG_NOEXEC                      },
                { "cifs-user-name",              required_argument, NULL, ARG_CIFS_USER_NAME              },
                { "cifs-domain",                 required_argument, NULL, ARG_CIFS_DOMAIN                 },
                { "cifs-service",                required_argument, NULL, ARG_CIFS_SERVICE                },
                { "rate-limit-interval",         required_argument, NULL, ARG_RATE_LIMIT_INTERVAL         },
                { "rate-limit-burst",            required_argument, NULL, ARG_RATE_LIMIT_BURST            },
                { "stop-delay",                  required_argument, NULL, ARG_STOP_DELAY                  },
                { "kill-processes",              required_argument, NULL, ARG_KILL_PROCESSES              },
                { "enforce-password-policy",     required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY     },
                { "password-change-now",         required_argument, NULL, ARG_PASSWORD_CHANGE_NOW         },
                { "password-change-min",         required_argument, NULL, ARG_PASSWORD_CHANGE_MIN         },
                { "password-change-max",         required_argument, NULL, ARG_PASSWORD_CHANGE_MAX         },
                { "password-change-warn",        required_argument, NULL, ARG_PASSWORD_CHANGE_WARN        },
                { "password-change-inactive",    required_argument, NULL, ARG_PASSWORD_CHANGE_INACTIVE    },
                { "auto-login",                  required_argument, NULL, ARG_AUTO_LOGIN                  },
                { "json",                        required_argument, NULL, ARG_JSON                        },
                { "export-format",               required_argument, NULL, ARG_EXPORT_FORMAT               },
                { "pkcs11-token-uri",            required_argument, NULL, ARG_PKCS11_TOKEN_URI            },
                { "fido2-device",                required_argument, NULL, ARG_FIDO2_DEVICE                },
                { "recovery-key",                required_argument, NULL, ARG_RECOVERY_KEY                },
                { "and-resize",                  required_argument, NULL, ARG_AND_RESIZE                  },
                { "and-change-password",         required_argument, NULL, ARG_AND_CHANGE_PASSWORD         },
                {}
        };

        int r;

        assert(argc >= 0);
        assert(argv);

        for (;;) {
                int c;

                c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL);
                if (c < 0)
                        break;

                switch (c) {

                case 'h':
                        return help(0, NULL, NULL);

                case ARG_VERSION:
                        return version();

                case ARG_NO_PAGER:
                        arg_pager_flags |= PAGER_DISABLE;
                        break;

                case ARG_NO_LEGEND:
                        arg_legend = false;
                        break;

                case ARG_NO_ASK_PASSWORD:
                        arg_ask_password = false;
                        break;

                case 'H':
                        arg_transport = BUS_TRANSPORT_REMOTE;
                        arg_host = optarg;
                        break;

                case 'M':
                        arg_transport = BUS_TRANSPORT_MACHINE;
                        arg_host = optarg;
                        break;

                case 'I':
                        arg_identity = optarg;
                        break;

                case 'c':
                        if (isempty(optarg)) {
                                r = drop_from_identity("realName");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        if (!valid_gecos(optarg))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Real name '%s' not a valid GECOS field.", optarg);

                        r = json_variant_set_field_string(&arg_identity_extra, "realName", optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set realName field: %m");

                        break;

                case 'd': {
                        _cleanup_free_ char *hd = NULL;

                        if (isempty(optarg)) {
                                r = drop_from_identity("homeDirectory");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_path_argument_and_warn(optarg, false, &hd);
                        if (r < 0)
                                return r;

                        if (!valid_home(hd))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd);

                        r = json_variant_set_field_string(&arg_identity_extra, "homeDirectory", hd);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set homeDirectory field: %m");

                        break;
                }

                case ARG_REALM:
                        if (isempty(optarg)) {
                                r = drop_from_identity("realm");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = dns_name_is_valid(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", optarg);
                        if (r == 0)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain: %m", optarg);

                        r = json_variant_set_field_string(&arg_identity_extra, "realm", optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set realm field: %m");
                        break;

                case ARG_EMAIL_ADDRESS:
                case ARG_LOCATION:
                case ARG_ICON_NAME:
                case ARG_CIFS_USER_NAME:
                case ARG_CIFS_DOMAIN:
                case ARG_CIFS_SERVICE: {

                        const char *field =
                                c == ARG_EMAIL_ADDRESS ? "emailAddress" :
                                     c == ARG_LOCATION ? "location" :
                                    c == ARG_ICON_NAME ? "iconName" :
                               c == ARG_CIFS_USER_NAME ? "cifsUserName" :
                                  c == ARG_CIFS_DOMAIN ? "cifsDomain" :
                                 c == ARG_CIFS_SERVICE ? "cifsService" :
                                                         NULL;

                        assert(field);

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = json_variant_set_field_string(&arg_identity_extra, field, optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", field);

                        break;
                }

                case ARG_PASSWORD_HINT:
                        if (isempty(optarg)) {
                                r = drop_from_identity("passwordHint");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = json_variant_set_field_string(&arg_identity_extra_privileged, "passwordHint", optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set passwordHint field: %m");

                        string_erase(optarg);
                        break;

                case ARG_NICE: {
                        int nc;

                        if (isempty(optarg)) {
                                r = drop_from_identity("niceLevel");
                                if (r < 0)
                                        return r;
                                break;
                        }

                        r = parse_nice(optarg, &nc);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse nice level: %s", optarg);

                        r = json_variant_set_field_integer(&arg_identity_extra, "niceLevel", nc);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set niceLevel field: %m");

                        break;
                }

                case ARG_RLIMIT: {
                        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *jcur = NULL, *jmax = NULL;
                        _cleanup_free_ char *field = NULL, *t = NULL;
                        const char *eq;
                        struct rlimit rl;
                        int l;

                        if (isempty(optarg)) {
                                /* Remove all resource limits */

                                r = drop_from_identity("resourceLimits");
                                if (r < 0)
                                        return r;

                                arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits);
                                arg_identity_extra_rlimits = json_variant_unref(arg_identity_extra_rlimits);
                                break;
                        }

                        eq = strchr(optarg, '=');
                        if (!eq)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", optarg);

                        field = strndup(optarg, eq - optarg);
                        if (!field)
                                return log_oom();

                        l = rlimit_from_string_harder(field);
                        if (l < 0)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown resource limit type: %s", field);

                        if (isempty(eq + 1)) {
                                /* Remove only the specific rlimit */

                                r = strv_extend(&arg_identity_filter_rlimits, rlimit_to_string(l));
                                if (r < 0)
                                        return r;

                                r = json_variant_filter(&arg_identity_extra_rlimits, STRV_MAKE(field));
                                if (r < 0)
                                        return log_error_errno(r, "Failed to filter JSON identity data: %m");

                                break;
                        }

                        r = rlimit_parse(l, eq + 1, &rl);
                        if (r < 0)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse resource limit value: %s", eq + 1);

                        r = rl.rlim_cur == RLIM_INFINITY ? json_variant_new_null(&jcur) : json_variant_new_unsigned(&jcur, rl.rlim_cur);
                        if (r < 0)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate current integer: %m");

                        r = rl.rlim_max == RLIM_INFINITY ? json_variant_new_null(&jmax) : json_variant_new_unsigned(&jmax, rl.rlim_max);
                        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);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", rlimit_to_string(l));

                        break;
                }

                case 'u': {
                        uid_t uid;

                        if (isempty(optarg)) {
                                r = drop_from_identity("uid");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_uid(optarg, &uid);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse UID '%s'.", optarg);

                        if (uid_is_system(uid))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in system range, refusing.", uid);
                        if (uid_is_dynamic(uid))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in dynamic range, refusing.", uid);
                        if (uid == UID_NOBODY)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is nobody UID, refusing.", uid);

                        r = json_variant_set_field_unsigned(&arg_identity_extra, "uid", uid);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set realm field: %m");

                        break;
                }

                case 'k':
                case ARG_IMAGE_PATH: {
                        const char *field = c == 'k' ? "skeletonDirectory" : "imagePath";
                        _cleanup_free_ char *v = NULL;

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_path_argument_and_warn(optarg, false, &v);
                        if (r < 0)
                                return r;

                        r = json_variant_set_field_string(&arg_identity_extra_this_machine, field, v);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", v);

                        break;
                }

                case 's':
                        if (isempty(optarg)) {
                                r = drop_from_identity("shell");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        if (!valid_shell(optarg))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Shell '%s' not valid.", optarg);

                        r = json_variant_set_field_string(&arg_identity_extra, "shell", optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set shell field: %m");

                        break;

                case ARG_SETENV: {
                        _cleanup_free_ char **l = NULL, **k = NULL;
                        _cleanup_(json_variant_unrefp) JsonVariant *ne = NULL;
                        JsonVariant *e;

                        if (isempty(optarg)) {
                                r = drop_from_identity("environment");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        if (!env_assignment_is_valid(optarg))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Environment assignment '%s' not valid.", optarg);

                        e = json_variant_by_key(arg_identity_extra, "environment");
                        if (e) {
                                r = json_variant_strv(e, &l);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to parse JSON environment field: %m");
                        }

                        k = strv_env_set(l, optarg);
                        if (!k)
                                return log_oom();

                        strv_sort(k);

                        r = json_variant_new_array_strv(&ne, k);
                        if (r < 0)
                                return log_error_errno(r, "Failed to allocate environment list JSON: %m");

                        r = json_variant_set_field(&arg_identity_extra, "environment", ne);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set environment list: %m");

                        break;
                }

                case ARG_TIMEZONE:

                        if (isempty(optarg)) {
                                r = drop_from_identity("timeZone");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        if (!timezone_is_valid(optarg, LOG_DEBUG))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timezone '%s' is not valid.", optarg);

                        r = json_variant_set_field_string(&arg_identity_extra, "timeZone", optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set timezone field: %m");

                        break;

                case ARG_LANGUAGE:
                        if (isempty(optarg)) {
                                r = drop_from_identity("language");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        if (!locale_is_valid(optarg))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg);

                        if (locale_is_installed(optarg) <= 0)
                                log_warning("Locale '%s' is not installed, accepting anyway.", optarg);

                        r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set preferredLanguage field: %m");

                        break;

                case ARG_NOSUID:
                case ARG_NODEV:
                case ARG_NOEXEC:
                case ARG_LOCKED:
                case ARG_KILL_PROCESSES:
                case ARG_ENFORCE_PASSWORD_POLICY:
                case ARG_AUTO_LOGIN:
                case ARG_PASSWORD_CHANGE_NOW: {
                        const char *field =
                                                 c == ARG_LOCKED ? "locked" :
                                                 c == ARG_NOSUID ? "mountNoSuid" :
                                                  c == ARG_NODEV ? "mountNoDevices" :
                                                 c == ARG_NOEXEC ? "mountNoExecute" :
                                         c == ARG_KILL_PROCESSES ? "killProcesses" :
                                c == ARG_ENFORCE_PASSWORD_POLICY ? "enforcePasswordPolicy" :
                                             c == ARG_AUTO_LOGIN ? "autoLogin" :
                                    c == ARG_PASSWORD_CHANGE_NOW ? "passwordChangeNow" :
                                                                   NULL;

                        assert(field);

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse %s boolean: %m", field);

                        r = json_variant_set_field_boolean(&arg_identity_extra, field, r > 0);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", field);

                        break;
                }

                case 'P':
                        r = json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m");

                        break;

                case ARG_DISK_SIZE:
                        if (isempty(optarg)) {
                                r = drop_from_identity("diskSize");
                                if (r < 0)
                                        return r;

                                r = drop_from_identity("diskSizeRelative");
                                if (r < 0)
                                        return r;

                                arg_disk_size = arg_disk_size_relative = UINT64_MAX;
                                break;
                        }

                        r = parse_permille(optarg);
                        if (r < 0) {
                                r = parse_size(optarg, 1024, &arg_disk_size);
                                if (r < 0)
                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size '%s' not valid.", optarg);

                                r = drop_from_identity("diskSizeRelative");
                                if (r < 0)
                                        return r;

                                r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSize", arg_disk_size);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to set diskSize field: %m");

                                arg_disk_size_relative = UINT64_MAX;
                        } else {
                                /* Normalize to UINT32_MAX == 100% */
                                arg_disk_size_relative = (uint64_t) r * UINT32_MAX / 1000U;

                                r = drop_from_identity("diskSize");
                                if (r < 0)
                                        return r;

                                r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to set diskSizeRelative field: %m");

                                arg_disk_size = UINT64_MAX;
                        }

                        break;

                case ARG_ACCESS_MODE: {
                        mode_t mode;

                        if (isempty(optarg)) {
                                r = drop_from_identity("accessMode");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_mode(optarg, &mode);
                        if (r < 0)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", optarg);

                        r = json_variant_set_field_unsigned(&arg_identity_extra, "accessMode", mode);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set access mode field: %m");

                        break;
                }

                case ARG_LUKS_DISCARD:
                        if (isempty(optarg)) {
                                r = drop_from_identity("luksDiscard");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse --luks-discard= parameter: %s", optarg);

                        r = json_variant_set_field_boolean(&arg_identity_extra, "luksDiscard", r);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set discard field: %m");

                        break;

                case ARG_LUKS_OFFLINE_DISCARD:
                        if (isempty(optarg)) {
                                r = drop_from_identity("luksOfflineDiscard");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse --luks-offline-discard= parameter: %s", optarg);

                        r = json_variant_set_field_boolean(&arg_identity_extra, "luksOfflineDiscard", r);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set offline discard field: %m");

                        break;

                case ARG_LUKS_VOLUME_KEY_SIZE:
                case ARG_LUKS_PBKDF_PARALLEL_THREADS:
                case ARG_RATE_LIMIT_BURST: {
                        const char *field =
                                       c == ARG_LUKS_VOLUME_KEY_SIZE ? "luksVolumeKeySize" :
                                c == ARG_LUKS_PBKDF_PARALLEL_THREADS ? "luksPbkdfParallelThreads" :
                                           c == ARG_RATE_LIMIT_BURST ? "rateLimitBurst" : NULL;
                        unsigned n;

                        assert(field);

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;
                        }

                        r = safe_atou(optarg, &n);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg);

                        r = json_variant_set_field_unsigned(&arg_identity_extra, field, n);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", field);

                        break;
                }

                case ARG_UMASK: {
                        mode_t m;

                        if (isempty(optarg)) {
                                r = drop_from_identity("umask");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_mode(optarg, &m);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse umask: %m");

                        r = json_variant_set_field_integer(&arg_identity_extra, "umask", m);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set umask field: %m");

                        break;
                }

                case ARG_SSH_AUTHORIZED_KEYS: {
                        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
                        _cleanup_(strv_freep) char **l = NULL, **add = NULL;

                        if (isempty(optarg)) {
                                r = drop_from_identity("sshAuthorizedKeys");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        if (optarg[0] == '@') {
                                _cleanup_fclose_ FILE *f = NULL;

                                /* If prefixed with '@' read from a file */

                                f = fopen(optarg+1, "re");
                                if (!f)
                                        return log_error_errno(errno, "Failed to open '%s': %m", optarg+1);

                                for (;;) {
                                        _cleanup_free_ char *line = NULL;

                                        r = read_line(f, LONG_LINE_MAX, &line);
                                        if (r < 0)
                                                return log_error_errno(r, "Failed to read from '%s': %m", optarg+1);
                                        if (r == 0)
                                                break;

                                        if (isempty(line))
                                                continue;

                                        if (line[0] == '#')
                                                continue;

                                        r = strv_consume(&add, TAKE_PTR(line));
                                        if (r < 0)
                                                return log_oom();
                                }
                        } else {
                                /* Otherwise, assume it's a literal key. Let's do some superficial checks
                                 * before accept it though. */

                                if (string_has_cc(optarg, NULL))
                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Authorized key contains control characters, refusing.");
                                if (optarg[0] == '#')
                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?");

                                add = strv_new(optarg);
                                if (!add)
                                        return log_oom();
                        }

                        v = json_variant_ref(json_variant_by_key(arg_identity_extra_privileged, "sshAuthorizedKeys"));
                        if (v) {
                                r = json_variant_strv(v, &l);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to parse SSH authorized keys list: %m");
                        }

                        r = strv_extend_strv(&l, add, true);
                        if (r < 0)
                                return log_oom();

                        v = json_variant_unref(v);

                        r = json_variant_new_array_strv(&v, l);
                        if (r < 0)
                                return log_oom();

                        r = json_variant_set_field(&arg_identity_extra_privileged, "sshAuthorizedKeys", v);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set authorized keys: %m");

                        break;
                }

                case ARG_NOT_BEFORE:
                case ARG_NOT_AFTER:
                case 'e': {
                        const char *field;
                        usec_t n;

                        field =           c == ARG_NOT_BEFORE ? "notBeforeUSec" :
                                IN_SET(c, ARG_NOT_AFTER, 'e') ? "notAfterUSec" : NULL;

                        assert(field);

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;

                                break;
                        }

                        /* Note the minor discrepancy regarding -e parsing here: we support that for compat
                         * reasons, and in the original useradd(8) implementation it accepts dates in the
                         * format YYYY-MM-DD. Coincidentally, we accept dates formatted like that too, but
                         * with greater precision. */
                        r = parse_timestamp(optarg, &n);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse %s parameter: %m", field);

                        r = json_variant_set_field_unsigned(&arg_identity_extra, field, n);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", field);
                        break;
                }

                case ARG_PASSWORD_CHANGE_MIN:
                case ARG_PASSWORD_CHANGE_MAX:
                case ARG_PASSWORD_CHANGE_WARN:
                case ARG_PASSWORD_CHANGE_INACTIVE: {
                        const char *field;
                        usec_t n;

                        field =      c == ARG_PASSWORD_CHANGE_MIN ? "passwordChangeMinUSec" :
                                     c == ARG_PASSWORD_CHANGE_MAX ? "passwordChangeMaxUSec" :
                                    c == ARG_PASSWORD_CHANGE_WARN ? "passwordChangeWarnUSec" :
                                c == ARG_PASSWORD_CHANGE_INACTIVE ? "passwordChangeInactiveUSec" :
                                                                    NULL;

                        assert(field);

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_sec(optarg, &n);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse %s parameter: %m", field);

                        r = json_variant_set_field_unsigned(&arg_identity_extra, field, n);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", field);
                        break;
                }

                case ARG_STORAGE:
                case ARG_FS_TYPE:
                case ARG_LUKS_CIPHER:
                case ARG_LUKS_CIPHER_MODE:
                case ARG_LUKS_PBKDF_TYPE:
                case ARG_LUKS_PBKDF_HASH_ALGORITHM: {

                        const char *field =
                                                  c == ARG_STORAGE ? "storage" :
                                                  c == ARG_FS_TYPE ? "fileSystemType" :
                                              c == ARG_LUKS_CIPHER ? "luksCipher" :
                                         c == ARG_LUKS_CIPHER_MODE ? "luksCipherMode" :
                                          c == ARG_LUKS_PBKDF_TYPE ? "luksPbkdfType" :
                                c == ARG_LUKS_PBKDF_HASH_ALGORITHM ? "luksPbkdfHashAlgorithm" : NULL;

                        assert(field);

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;

                                break;
                        }

                        if (!string_is_safe(optarg))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for %s field not valid: %s", field, optarg);

                        r = json_variant_set_field_string(
                                        IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ?
                                        &arg_identity_extra_this_machine :
                                        &arg_identity_extra, field, optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", field);

                        break;
                }

                case ARG_LUKS_PBKDF_TIME_COST:
                case ARG_RATE_LIMIT_INTERVAL:
                case ARG_STOP_DELAY: {
                        const char *field =
                                c == ARG_LUKS_PBKDF_TIME_COST ? "luksPbkdfTimeCostUSec" :
                                 c == ARG_RATE_LIMIT_INTERVAL ? "rateLimitIntervalUSec" :
                                          c == ARG_STOP_DELAY ? "stopDelayUSec" :
                                                                NULL;
                        usec_t t;

                        assert(field);

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;

                                break;
                        }

                        r = parse_sec(optarg, &t);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse %s field: %s", field, optarg);

                        r = json_variant_set_field_unsigned(&arg_identity_extra, field, t);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", field);

                        break;
                }

                case 'G': {
                        const char *p = optarg;

                        if (isempty(p)) {
                                r = drop_from_identity("memberOf");
                                if (r < 0)
                                        return r;

                                break;
                        }

                        for (;;) {
                                _cleanup_(json_variant_unrefp) JsonVariant *mo = NULL;
                                _cleanup_strv_free_ char **list = NULL;
                                _cleanup_free_ char *word = NULL;

                                r = extract_first_word(&p, &word, ",", 0);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to parse group list: %m");
                                if (r == 0)
                                        break;

                                if (!valid_user_group_name(word, 0))
                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word);

                                mo = json_variant_ref(json_variant_by_key(arg_identity_extra, "memberOf"));

                                r = json_variant_strv(mo, &list);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to parse group list: %m");

                                r = strv_extend(&list, word);
                                if (r < 0)
                                        return log_oom();

                                strv_sort(list);
                                strv_uniq(list);

                                mo = json_variant_unref(mo);
                                r = json_variant_new_array_strv(&mo, list);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to create group list JSON: %m");

                                r = json_variant_set_field(&arg_identity_extra, "memberOf", mo);
                                if (r < 0)
                                        return log_error_errno(r, "Failed to update group list: %m");
                        }

                        break;
                }

                case ARG_TASKS_MAX: {
                        uint64_t u;

                        if (isempty(optarg)) {
                                r = drop_from_identity("tasksMax");
                                if (r < 0)
                                        return r;
                                break;
                        }

                        r = safe_atou64(optarg, &u);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse --tasks-max= parameter: %s", optarg);

                        r = json_variant_set_field_unsigned(&arg_identity_extra, "tasksMax", u);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set tasksMax field: %m");

                        break;
                }

                case ARG_MEMORY_MAX:
                case ARG_MEMORY_HIGH:
                case ARG_LUKS_PBKDF_MEMORY_COST: {
                        const char *field =
                                            c == ARG_MEMORY_MAX ? "memoryMax" :
                                           c == ARG_MEMORY_HIGH ? "memoryHigh" :
                                c == ARG_LUKS_PBKDF_MEMORY_COST ? "luksPbkdfMemoryCost" : NULL;

                        uint64_t u;

                        assert(field);

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;
                                break;
                        }

                        r = parse_size(optarg, 1024, &u);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg);

                        r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, field, u);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", field);

                        break;
                }

                case ARG_CPU_WEIGHT:
                case ARG_IO_WEIGHT: {
                        const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" :
                                            c == ARG_IO_WEIGHT ? "ioWeight" : NULL;
                        uint64_t u;

                        assert(field);

                        if (isempty(optarg)) {
                                r = drop_from_identity(field);
                                if (r < 0)
                                        return r;
                                break;
                        }

                        r = safe_atou64(optarg, &u);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse --cpu-weight=/--io-weight= parameter: %s", optarg);

                        if (!CGROUP_WEIGHT_IS_OK(u))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weight %" PRIu64 " is out of valid weight range.", u);

                        r = json_variant_set_field_unsigned(&arg_identity_extra, field, u);
                        if (r < 0)
                                return log_error_errno(r, "Failed to set %s field: %m", field);

                        break;
                }

                case ARG_PKCS11_TOKEN_URI: {
                        const char *p;

                        if (streq(optarg, "list"))
                                return list_pkcs11_tokens();

                        /* If --pkcs11-token-uri= is specified we always drop everything old */
                        FOREACH_STRING(p, "pkcs11TokenUri", "pkcs11EncryptedKey") {
                                r = drop_from_identity(p);
                                if (r < 0)
                                        return r;
                        }

                        if (isempty(optarg)) {
                                arg_pkcs11_token_uri = strv_free(arg_pkcs11_token_uri);
                                break;
                        }

                        if (streq(optarg, "auto")) {
                                _cleanup_free_ char *found = NULL;

                                r = find_pkcs11_token_auto(&found);
                                if (r < 0)
                                        return r;
                                r = strv_consume(&arg_pkcs11_token_uri, TAKE_PTR(found));
                        } else {
                                if (!pkcs11_uri_valid(optarg))
                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", optarg);

                                r = strv_extend(&arg_pkcs11_token_uri, optarg);
                        }
                        if (r < 0)
                                return r;

                        strv_uniq(arg_pkcs11_token_uri);
                        break;
                }

                case ARG_FIDO2_DEVICE: {
                        const char *p;

                        if (streq(optarg, "list"))
                                return list_fido2_devices();

                        FOREACH_STRING(p, "fido2HmacCredential", "fido2HmacSalt") {
                                r = drop_from_identity(p);
                                if (r < 0)
                                        return r;
                        }

                        if (isempty(optarg)) {
                                arg_fido2_device = strv_free(arg_fido2_device);
                                break;
                        }

                        if (streq(optarg, "auto")) {
                                _cleanup_free_ char *found = NULL;

                                r = find_fido2_auto(&found);
                                if (r < 0)
                                        return r;

                                r = strv_consume(&arg_fido2_device, TAKE_PTR(found));
                        } else
                                r = strv_extend(&arg_fido2_device, optarg);

                        if (r < 0)
                                return r;

                        strv_uniq(arg_fido2_device);
                        break;
                }

                case ARG_RECOVERY_KEY: {
                        const char *p;

                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse --recovery-key= argument: %s", optarg);

                        arg_recovery_key = r;

                        FOREACH_STRING(p, "recoveryKey", "recoveryKeyType") {
                                r = drop_from_identity(p);
                                if (r < 0)
                                        return r;
                        }

                        break;
                }

                case 'j':
                        arg_json = true;
                        arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
                        break;

                case ARG_JSON:
                        if (streq(optarg, "pretty")) {
                                arg_json = true;
                                arg_json_format_flags = JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO;
                        } else if (streq(optarg, "short")) {
                                arg_json = true;
                                arg_json_format_flags = JSON_FORMAT_NEWLINE;
                        } else if (streq(optarg, "off")) {
                                arg_json = false;
                                arg_json_format_flags = 0;
                        } else if (streq(optarg, "help")) {
                                puts("pretty\n"
                                     "short\n"
                                     "off");
                                return 0;
                        } else
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown argument to --json=: %s", optarg);

                        break;

                case 'E':
                        if (arg_export_format == EXPORT_FORMAT_FULL)
                                arg_export_format = EXPORT_FORMAT_STRIPPED;
                        else if (arg_export_format == EXPORT_FORMAT_STRIPPED)
                                arg_export_format = EXPORT_FORMAT_MINIMAL;
                        else
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported.");

                        arg_json = true;
                        if (arg_json_format_flags == 0)
                                arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
                        break;

                case ARG_EXPORT_FORMAT:
                        if (streq(optarg, "full"))
                                arg_export_format = EXPORT_FORMAT_FULL;
                        else if (streq(optarg, "stripped"))
                                arg_export_format = EXPORT_FORMAT_STRIPPED;
                        else if (streq(optarg, "minimal"))
                                arg_export_format = EXPORT_FORMAT_MINIMAL;
                        else if (streq(optarg, "help")) {
                                puts("full\n"
                                     "stripped\n"
                                     "minimal");
                                return 0;
                        }

                        break;

                case ARG_AND_RESIZE:
                        arg_and_resize = true;
                        break;

                case ARG_AND_CHANGE_PASSWORD:
                        arg_and_change_password = true;
                        break;

                case '?':
                        return -EINVAL;

                default:
                        assert_not_reached("Unhandled option");
                }
        }

        if (!strv_isempty(arg_pkcs11_token_uri) || !strv_isempty(arg_fido2_device))
                arg_and_change_password = true;

        if (arg_disk_size != UINT64_MAX || arg_disk_size_relative != UINT64_MAX)
                arg_and_resize = true;

        return 1;
}

static int redirect_bus_mgr(void) {
        const char *suffix;

        /* Talk to a different service if that's requested. (The same env var is also understood by homed, so
         * that it is relatively easily possible to invoke a second instance of homed for debug purposes and
         * have homectl talk to it, without colliding with the host version. This is handy when operating
         * from a homed-managed account.) */

        suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
        if (suffix) {
                static BusLocator locator = {
                        .path = "/org/freedesktop/home1",
                        .interface = "org.freedesktop.home1.Manager",
                };

                /* Yes, we leak this memory, but there's little point to collect this, given that we only do
                 * this in a debug environment, do it only once, and the string shall live for out entire
                 * process runtime. */

                locator.destination = strjoin("org.freedesktop.home1.", suffix);
                if (!locator.destination)
                        return log_oom();

                bus_mgr = &locator;
        } else
                bus_mgr = bus_home_mgr;

        return 0;
}

static int run(int argc, char *argv[]) {
        static const Verb verbs[] = {
                { "help",           VERB_ANY, VERB_ANY, 0,            help                 },
                { "list",           VERB_ANY, 1,        VERB_DEFAULT, list_homes           },
                { "activate",       2,        VERB_ANY, 0,            activate_home        },
                { "deactivate",     2,        VERB_ANY, 0,            deactivate_home      },
                { "inspect",        VERB_ANY, VERB_ANY, 0,            inspect_home         },
                { "authenticate",   VERB_ANY, VERB_ANY, 0,            authenticate_home    },
                { "create",         VERB_ANY, 2,        0,            create_home          },
                { "remove",         2,        VERB_ANY, 0,            remove_home          },
                { "update",         VERB_ANY, 2,        0,            update_home          },
                { "passwd",         VERB_ANY, 2,        0,            passwd_home          },
                { "resize",         2,        3,        0,            resize_home          },
                { "lock",           2,        VERB_ANY, 0,            lock_home            },
                { "unlock",         2,        VERB_ANY, 0,            unlock_home          },
                { "with",           2,        VERB_ANY, 0,            with_home            },
                { "lock-all",       VERB_ANY, 1,        0,            lock_all_homes       },
                { "deactivate-all", VERB_ANY, 1,        0,            deactivate_all_homes },
                {}
        };

        int r;

        log_setup_cli();

        r = redirect_bus_mgr();
        if (r < 0)
                return r;

        r = parse_argv(argc, argv);
        if (r <= 0)
                return r;

        return dispatch_verb(argc, argv, verbs, NULL);
}

DEFINE_MAIN_FUNCTION(run);
