diff --git a/src/home/homectl.c b/src/home/homectl.c index e9161dfb75..75339d93c8 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "sd-bus.h" @@ -30,6 +29,7 @@ #include "fs-util.h" #include "glyph-util.h" #include "hashmap.h" +#include "help-util.h" #include "hexdecoct.h" #include "home-util.h" #include "homectl-fido2.h" @@ -40,6 +40,7 @@ #include "libfido2-util.h" #include "locale-util.h" #include "main-func.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -50,7 +51,6 @@ #include "pkcs11-util.h" #include "plymouth-util.h" #include "polkit-agent.h" -#include "pretty-print.h" #include "proc-cmdline.h" #include "process-util.h" #include "prompt-util.h" @@ -178,6 +178,9 @@ static int acquire_bus(sd_bus **bus) { return 0; } +VERB_GROUP("Basic User Manipulation Commands"); +VERB(verb_list_homes, "list", /* argspec= */ NULL, VERB_ANY, 1, VERB_DEFAULT, + "List home areas"); static int verb_list_homes(int argc, char *argv[], uintptr_t _data, 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; @@ -627,85 +630,6 @@ static int acquire_passed_secrets(const char *user_name, UserRecord **ret) { return 0; } -static int verb_activate_home(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - int r, ret = 0; - - r = acquire_bus(&bus); - if (r < 0) - return r; - - STRV_FOREACH(i, strv_skip(argv, 1)) { - _cleanup_(user_record_unrefp) UserRecord *secret = NULL; - - r = acquire_passed_secrets(*i, &secret); - 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, "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, /* emphasize_current_password= */ false); - if (r < 0) { - if (ret == 0) - ret = r; - - break; - } - } else - break; - } - } - - return ret; -} - -static int verb_deactivate_home(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - int r, ret = 0; - - 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; @@ -775,6 +699,8 @@ static int inspect_home(sd_bus *bus, const char *name) { return 0; } +VERB(verb_inspect_homes, "inspect", "USER…", VERB_ANY, VERB_ANY, 0, + "Inspect a home area"); static int verb_inspect_homes(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r; @@ -799,65 +725,6 @@ static int verb_inspect_homes(int argc, char *argv[], uintptr_t _data, void *use } } -static int authenticate_home(sd_bus *bus, const char *name) { - _cleanup_(user_record_unrefp) UserRecord *secret = NULL; - int r; - - r = acquire_passed_secrets(name, &secret); - 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, "AuthenticateHome"); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_message_append(m, "s", name); - 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(name, secret, &error, r, false); - if (r >= 0) - continue; - } - return r; - } -} - -static int verb_authenticate_homes(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - int r; - - r = acquire_bus(&bus); - if (r < 0) - return r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - char **args = strv_skip(argv, 1); - if (args) { - STRV_FOREACH(arg, args) - RET_GATHER(r, authenticate_home(bus, *arg)); - - return r; - } else { - _cleanup_free_ char *myself = getusername_malloc(); - if (!myself) - return log_oom(); - - return authenticate_home(bus, myself); - } -} - static int update_last_change(sd_json_variant **v, bool with_password, bool override) { sd_json_variant *c; usec_t n; @@ -1557,6 +1424,8 @@ static int create_home_common(sd_json_variant *input, bool show_enforce_password return 0; } +VERB(verb_create_home, "create", "USER", VERB_ANY, 2, 0, + "Create a home area"); static int verb_create_home(int argc, char *argv[], uintptr_t _data, void *userdata) { int r; @@ -1592,182 +1461,6 @@ static int verb_create_home(int argc, char *argv[], uintptr_t _data, void *userd return create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ true); } -static int verb_adopt_home(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r, ret = 0; - - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - 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_message_unrefp) sd_bus_message *m = NULL; - r = bus_message_new_method_call(bus, &m, bus_mgr, "AdoptHome"); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_message_append(m, "st", *i, UINT64_C(0)); - if (r < 0) - return bus_log_create_error(r); - - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); - if (r < 0) { - log_error_errno(r, "Failed to adopt home: %s", bus_error_message(&error, r)); - if (ret == 0) - ret = r; - } - } - - return ret; -} - -static int register_home_common(sd_bus *bus, sd_json_variant *v) { - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *_bus = NULL; - int r; - - assert(v); - - if (!bus) { - r = acquire_bus(&_bus); - if (r < 0) - return r; - bus = _bus; - } - - _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; - r = bus_message_new_method_call(bus, &m, bus_mgr, "RegisterHome"); - if (r < 0) - return bus_log_create_error(r); - - _cleanup_free_ char *formatted = NULL; - r = sd_json_variant_format(v, /* flags= */ 0, &formatted); - if (r < 0) - return log_error_errno(r, "Failed to format JSON record: %m"); - - r = sd_bus_message_append(m, "s", formatted); - if (r < 0) - return bus_log_create_error(r); - - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); - if (r < 0) - return log_error_errno(r, "Failed to register home: %s", bus_error_message(&error, r)); - - return 0; -} - -static int register_home_one(sd_bus *bus, FILE *f, const char *path) { - int r; - - assert(bus); - assert(path); - - _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; - unsigned line = 0, column = 0; - r = sd_json_parse_file(f, path, SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE, &v, &line, &column); - if (r < 0) - return log_error_errno(r, "[%s:%u:%u] Failed to parse user record: %m", path, line, column); - - return register_home_common(bus, v); -} - -static int verb_register_home(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; - - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - r = acquire_bus(&bus); - if (r < 0) - return r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - if (arg_identity) { - if (argc > 1) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not accepting an arguments if --identity= is specified, refusing."); - - return register_home_one(bus, /* f= */ NULL, arg_identity); - } - - if (argc == 1 || (argc == 2 && streq(argv[1], "-"))) - return register_home_one(bus, /* f= */ stdin, ""); - - r = 0; - STRV_FOREACH(i, strv_skip(argv, 1)) { - if (streq(*i, "-")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing reading from standard input if multiple user records are specified."); - - RET_GATHER(r, register_home_one(bus, /* f= */ NULL, *i)); - } - - return r; -} - -static int verb_unregister_home(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; - - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - r = acquire_bus(&bus); - if (r < 0) - return r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - int ret = 0; - STRV_FOREACH(i, strv_skip(argv, 1)) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; - r = bus_message_new_method_call(bus, &m, bus_mgr, "UnregisterHome"); - 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); - - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, /* ret_reply= */ NULL); - if (r < 0) - RET_GATHER(ret, log_error_errno(r, "Failed to unregister home: %s", bus_error_message(&error, r))); - } - - return ret; -} - -static int verb_remove_home(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - int r, ret = 0; - - 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, @@ -1909,6 +1602,8 @@ static int home_record_reset_human_interaction_permission(UserRecord *hr) { return 0; } +VERB(verb_update_home, "update", "USER", VERB_ANY, 2, 0, + "Update a home area"); static int verb_update_home(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_(user_record_unrefp) UserRecord *hr = NULL, *secret = NULL; @@ -2086,6 +1781,8 @@ static int verb_update_home(int argc, char *argv[], uintptr_t _data, void *userd return 0; } +VERB(verb_passwd_home, "passwd", "USER", VERB_ANY, 2, 0, + "Change password of a home area"); static int verb_passwd_home(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(user_record_unrefp) UserRecord *old_secret = NULL, *new_secret = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; @@ -2203,6 +1900,8 @@ static int parse_disk_size(const char *t, uint64_t *ret) { return 0; } +VERB(verb_resize_home, "resize", "USER SIZE", 2, 3, 0, + "Resize a home area"); static int verb_resize_home(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_(user_record_unrefp) UserRecord *secret = NULL; @@ -2265,7 +1964,9 @@ static int verb_resize_home(int argc, char *argv[], uintptr_t _data, void *userd return 0; } -static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdata) { +VERB(verb_remove_home, "remove", "USER…", 2, VERB_ANY, 0, + "Remove a home area"); +static int verb_remove_home(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r, ret = 0; @@ -2273,11 +1974,13 @@ static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdat 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, "LockHome"); + r = bus_message_new_method_call(bus, &m, bus_mgr, "RemoveHome"); if (r < 0) return bus_log_create_error(r); @@ -2287,7 +1990,7 @@ static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdat 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)); + log_error_errno(r, "Failed to remove home: %s", bus_error_message(&error, r)); if (ret == 0) ret = r; } @@ -2296,7 +1999,10 @@ static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdat return ret; } -static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userdata) { +VERB_GROUP("Advanced User Manipulation Commands"); +VERB(verb_activate_home, "activate", "USER…", 2, VERB_ANY, 0, + "Activate a home area"); +static int verb_activate_home(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r, ret = 0; @@ -2315,7 +2021,7 @@ static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userd _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"); + r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHome"); if (r < 0) return bus_log_create_error(r); @@ -2329,7 +2035,7 @@ static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userd 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); + r = handle_generic_user_record_error(*i, secret, &error, r, /* emphasize_current_password= */ false); if (r < 0) { if (ret == 0) ret = r; @@ -2344,6 +2050,64 @@ static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userd return ret; } +VERB(verb_deactivate_home, "deactivate", "USER…", 2, VERB_ANY, 0, + "Deactivate a home area"); +static int verb_deactivate_home(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = 0; + + 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; +} + +VERB_NOARG(verb_deactivate_all_homes, "deactivate-all", + "Deactivate all active home areas"); +static int verb_deactivate_all_homes(int argc, char *argv[], uintptr_t _data, 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; +} + +VERB(verb_with_home, "with", "USER [COMMAND…]", 2, VERB_ANY, 0, + "Run shell or command with access to a home area"); static int verb_with_home(int argc, char *argv[], uintptr_t _data, 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; @@ -2467,6 +2231,580 @@ static int verb_with_home(int argc, char *argv[], uintptr_t _data, void *userdat return ret; } +static int authenticate_home(sd_bus *bus, const char *name) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + int r; + + r = acquire_passed_secrets(name, &secret); + 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, "AuthenticateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", name); + 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(name, secret, &error, r, false); + if (r >= 0) + continue; + } + return r; + } +} + +VERB(verb_authenticate_homes, "authenticate", "USER…", VERB_ANY, VERB_ANY, 0, + "Authenticate a home area"); +static int verb_authenticate_homes(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + char **args = strv_skip(argv, 1); + if (args) { + STRV_FOREACH(arg, args) + RET_GATHER(r, authenticate_home(bus, *arg)); + + return r; + } else { + _cleanup_free_ char *myself = getusername_malloc(); + if (!myself) + return log_oom(); + + return authenticate_home(bus, myself); + } +} + +VERB_GROUP("User Migration Commands"); +VERB(verb_adopt_home, "adopt", "PATH…", VERB_ANY, VERB_ANY, 0, + "Add an existing home area on this system"); +static int verb_adopt_home(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r, ret = 0; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + 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_message_unrefp) sd_bus_message *m = NULL; + r = bus_message_new_method_call(bus, &m, bus_mgr, "AdoptHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "st", *i, UINT64_C(0)); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to adopt home: %s", bus_error_message(&error, r)); + if (ret == 0) + ret = r; + } + } + + return ret; +} + +static int register_home_common(sd_bus *bus, sd_json_variant *v) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *_bus = NULL; + int r; + + assert(v); + + if (!bus) { + r = acquire_bus(&_bus); + if (r < 0) + return r; + bus = _bus; + } + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + r = bus_message_new_method_call(bus, &m, bus_mgr, "RegisterHome"); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_free_ char *formatted = NULL; + r = sd_json_variant_format(v, /* flags= */ 0, &formatted); + if (r < 0) + return log_error_errno(r, "Failed to format JSON record: %m"); + + r = sd_bus_message_append(m, "s", formatted); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to register home: %s", bus_error_message(&error, r)); + + return 0; +} + +static int register_home_one(sd_bus *bus, FILE *f, const char *path) { + int r; + + assert(bus); + assert(path); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + unsigned line = 0, column = 0; + r = sd_json_parse_file(f, path, SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse user record: %m", path, line, column); + + return register_home_common(bus, v); +} + +VERB(verb_register_home, "register", "PATH…", VERB_ANY, VERB_ANY, 0, + "Register a user record locally"); +static int verb_register_home(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (arg_identity) { + if (argc > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not accepting an arguments if --identity= is specified, refusing."); + + return register_home_one(bus, /* f= */ NULL, arg_identity); + } + + if (argc == 1 || (argc == 2 && streq(argv[1], "-"))) + return register_home_one(bus, /* f= */ stdin, ""); + + r = 0; + STRV_FOREACH(i, strv_skip(argv, 1)) { + if (streq(*i, "-")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing reading from standard input if multiple user records are specified."); + + RET_GATHER(r, register_home_one(bus, /* f= */ NULL, *i)); + } + + return r; +} + +VERB(verb_unregister_home, "unregister", "USER…", 2, VERB_ANY, 0, + "Unregister a user record locally"); +static int verb_unregister_home(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + int ret = 0; + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + r = bus_message_new_method_call(bus, &m, bus_mgr, "UnregisterHome"); + 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); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, /* ret_reply= */ NULL); + if (r < 0) + RET_GATHER(ret, log_error_errno(r, "Failed to unregister home: %s", bus_error_message(&error, r))); + } + + return ret; +} + +VERB_GROUP("Signing Keys Commands"); +VERB_NOARG(verb_list_signing_keys, "list-signing-keys", + "List home signing keys"); +static int verb_list_signing_keys(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + r = bus_call_method(bus, bus_mgr, "ListSigningKeys", &error, &reply, NULL); + if (r < 0) + return log_error_errno(r, "Failed to list signing keys: %s", bus_error_message(&error, r)); + + _cleanup_(table_unrefp) Table *table = table_new("name", "key"); + if (!table) + return log_oom(); + + r = sd_bus_message_enter_container(reply, 'a', "(sst)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + const char *name, *pem; + + r = sd_bus_message_read(reply, "(sst)", &name, &pem, NULL); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + _cleanup_free_ char *h = NULL; + if (!sd_json_format_enabled(arg_json_format_flags)) { + /* Let's decode the PEM key to DER (so that we lose prefix/suffix), then truncate it + * for display reasons. */ + + r = dlopen_libcrypto(LOG_DEBUG); + if (r < 0) + return r; + + _cleanup_(EVP_PKEY_freep) EVP_PKEY *key = NULL; + r = openssl_pubkey_from_pem(pem, SIZE_MAX, &key); + if (r < 0) + return log_error_errno(r, "Failed to parse PEM: %m"); + + _cleanup_free_ void *der = NULL; + int n = sym_i2d_PUBKEY(key, (unsigned char**) &der); + if (n < 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to encode key as DER."); + + ssize_t m = base64mem(der, MIN(n, 64), &h); + if (m < 0) + return log_oom(); + if (n > 64) /* check if we truncated the original version */ + if (!strextend(&h, glyph(GLYPH_ELLIPSIS))) + return log_oom(); + } + + r = table_add_many( + table, + TABLE_STRING, name, + TABLE_STRING, h ?: pem); + 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_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) { + r = table_set_sort(table, (size_t) 0); + if (r < 0) + return table_log_sort_error(r); + + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); + if (r < 0) + return r; + } + + if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) { + if (table_isempty(table)) + printf("No signing keys.\n"); + else + printf("\n%zu signing keys listed.\n", table_get_rows(table) - 1); + } + + return 0; +} + +VERB(verb_get_signing_key, "get-signing-key", "[NAME…]", VERB_ANY, VERB_ANY, 0, + "Get a named home signing key"); +static int verb_get_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + char **keys = argc >= 2 ? strv_skip(argv, 1) : STRV_MAKE("local.public"); + int ret = 0; + STRV_FOREACH(k, keys) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + r = bus_call_method(bus, bus_mgr, "GetSigningKey", &error, &reply, "s", *k); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to get signing key '%s': %s", *k, bus_error_message(&error, r))); + continue; + } + + const char *pem; + r = sd_bus_message_read(reply, "st", &pem, NULL); + if (r < 0) { + RET_GATHER(ret, bus_log_parse_error(r)); + continue; + } + + fputs(pem, stdout); + if (!endswith(pem, "\n")) + fputc('\n', stdout); + + fflush(stdout); + } + + return ret; +} + +static int add_signing_key_one(sd_bus *bus, const char *fn, FILE *key) { + int r; + + assert_se(bus); + assert_se(fn); + assert_se(key); + + _cleanup_free_ char *pem = NULL; + r = read_full_stream(key, &pem, /* ret_size= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to read key '%s': %m", fn); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = bus_call_method(bus, bus_mgr, "AddSigningKey", &error, /* ret_reply= */ NULL, "sst", fn, pem, UINT64_C(0)); + if (r < 0) + return log_error_errno(r, "Failed to add signing key '%s': %s", fn, bus_error_message(&error, r)); + + return 0; +} + +VERB(verb_add_signing_key, "add-signing-key", "FILE…", VERB_ANY, VERB_ANY, 0, + "Add home signing key"); +static int verb_add_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + int ret = EXIT_SUCCESS; + if (argc < 2 || streq(argv[1], "-")) { + if (!arg_key_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key name must be specified via --key-name= when reading key from standard input, refusing."); + + RET_GATHER(ret, add_signing_key_one(bus, arg_key_name, stdin)); + } else { + /* Refuse if more han one key is specified in combination with --key-name= */ + if (argc >= 3 && arg_key_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--key-name= is not supported if multiple signing keys are specified, refusing."); + + STRV_FOREACH(k, strv_skip(argv, 1)) { + + if (streq(*k, "-")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing to read from standard input if multiple keys are specified."); + + _cleanup_free_ char *fn = NULL; + if (!arg_key_name) { + r = path_extract_filename(*k, &fn); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to extract filename from path '%s': %m", *k)); + continue; + } + } + + _cleanup_fclose_ FILE *f = fopen(*k, "re"); + if (!f) { + RET_GATHER(ret, log_error_errno(errno, "Failed to open '%s': %m", *k)); + continue; + } + + RET_GATHER(ret, add_signing_key_one(bus, fn ?: arg_key_name, f)); + } + } + + return ret; +} + +static int add_signing_keys_from_credentials(void) { + int r; + + _cleanup_close_ int fd = open_credentials_dir(); + if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */ + return 0; + if (fd < 0) + return log_error_errno(fd, "Failed to open credentials directory: %m"); + + _cleanup_free_ DirectoryEntries *des = NULL; + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); + if (r < 0) + return log_error_errno(r, "Failed to enumerate credentials: %m"); + + int ret = 0; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + FOREACH_ARRAY(i, des->entries, des->n_entries) { + struct dirent *de = *i; + if (de->d_type != DT_REG) + continue; + + const char *e = startswith(de->d_name, "home.add-signing-key."); + if (!e) + continue; + + if (!filename_is_valid(e)) + continue; + + if (!bus) { + r = acquire_bus(&bus); + if (r < 0) + return r; + } + + _cleanup_fclose_ FILE *f = NULL; + r = xfopenat(fd, de->d_name, "re", O_NOFOLLOW, &f); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to open credential '%s': %m", de->d_name)); + continue; + } + + RET_GATHER(ret, add_signing_key_one(bus, e, f)); + } + + return ret; +} + +static int remove_signing_key_one(sd_bus *bus, const char *fn) { + int r; + + assert_se(bus); + assert_se(fn); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = bus_call_method(bus, bus_mgr, "RemoveSigningKey", &error, /* ret_reply= */ NULL, "st", fn, UINT64_C(0)); + if (r < 0) + return log_error_errno(r, "Failed to remove signing key '%s': %s", fn, bus_error_message(&error, r)); + + return 0; +} + +VERB(verb_remove_signing_key, "remove-signing-key", "NAME…", 2, VERB_ANY, 0, + "Remove home signing key"); +static int verb_remove_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = EXIT_SUCCESS; + STRV_FOREACH(k, strv_skip(argv, 1)) + RET_GATHER(r, remove_signing_key_one(bus, *k)); + + return r; +} + +VERB_GROUP("Lock/Unlock Commands"); +VERB(verb_lock_home, "lock", "USER…", 2, VERB_ANY, 0, + "Temporarily lock an active home area"); +static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = 0; + + 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; +} + +VERB(verb_unlock_home, "unlock", "USER…", 2, VERB_ANY, 0, + "Unlock a temporarily locked home area"); +static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = 0; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + r = acquire_passed_secrets(*i, &secret); + 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, "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(*i, secret, &error, r, false); + if (r < 0) { + if (ret == 0) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +VERB_NOARG(verb_lock_all_homes, "lock-all", + "Lock all suitable home areas"); static int verb_lock_all_homes(int argc, char *argv[], uintptr_t _data, 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; @@ -2488,27 +2826,9 @@ static int verb_lock_all_homes(int argc, char *argv[], uintptr_t _data, void *us return 0; } -static int verb_deactivate_all_homes(int argc, char *argv[], uintptr_t _data, 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; -} - +VERB_GROUP("Other Commands"); +VERB_NOARG(verb_rebalance, "rebalance", + "Rebalance free space between home areas"); static int verb_rebalance(int argc, char *argv[], uintptr_t _data, 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; @@ -2764,8 +3084,8 @@ static int create_interactively(void) { return 0; } -static int add_signing_keys_from_credentials(void); - +VERB_NOARG(verb_firstboot, "firstboot", + "Run first-boot home area creation wizard"); static int verb_firstboot(int argc, char *argv[], uintptr_t _data, void *userdata) { int r; @@ -3651,434 +3971,92 @@ static int parse_fido2_device_field(const char *arg) { } static int help(void) { - _cleanup_free_ char *link = NULL; + static const char* const vgroups[] = { + "Basic User Manipulation Commands", + "Advanced User Manipulation Commands", + "User Migration Commands", + "Signing Keys Commands", + "Lock/Unlock Commands", + "Other Commands", + }; + + static const char* const ogroups[] = { + NULL, + "General User Record Properties", + "Authentication User Record Properties", + "Blob Directory User Record Properties", + "Account Management User Record Properties", + "Password Policy User Record Properties", + "Resource Management User Record Properties", + "Storage User Record Properties", + "LUKS Storage User Record Properties", + "Mounting User Record Properties", + "CIFS User Record Properties", + "Login Behaviour User Record Properties", + }; + + Table *vtables[ELEMENTSOF(vgroups)] = {}; + CLEANUP_ELEMENTS(vtables, table_unref_array_clear); + Table *otables[ELEMENTSOF(ogroups)] = {}; + CLEANUP_ELEMENTS(otables, table_unref_array_clear); int r; + for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { + r = verbs_get_help_table_group(vgroups[i], &vtables[i]); + if (r < 0) + return r; + } + + for (size_t i = 0; i < ELEMENTSOF(ogroups); i++) { + r = option_parser_get_help_table_group(ogroups[i], &otables[i]); + if (r < 0) + return r; + } + + /* The two groups are not synchronized because the option table is very wide. */ + + assert_cc(ELEMENTSOF(vtables) == 6); + (void) table_sync_column_widths(0, vtables[0], vtables[1], vtables[2], + vtables[3], vtables[4], vtables[5]); + + assert_cc(ELEMENTSOF(otables) == 12); + (void) table_sync_column_widths(0, otables[0], otables[1], otables[2], otables[3], + otables[4], otables[5], otables[6], otables[7], + otables[8], otables[9], otables[10], otables[11]); + pager_open(arg_pager_flags); - r = terminal_urlify_man("homectl", "1", &link); - if (r < 0) - return log_oom(); + help_cmdline("[OPTIONS…] COMMAND …"); + help_abstract("Create, manipulate or inspect home directories."); - printf("%1$s [OPTIONS...] COMMAND ...\n\n" - "%2$sCreate, manipulate or inspect home directories.%3$s\n" - "\n%4$sBasic User Manipulation Commands:%5$s\n" - " list List home areas\n" - " inspect USER… Inspect a home area\n" - " create USER Create 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" - " remove USER… Remove a home area\n" - "\n%4$sAdvanced User Manipulation Commands:%5$s\n" - " activate USER… Activate a home area\n" - " deactivate USER… Deactivate a home area\n" - " deactivate-all Deactivate all active home areas\n" - " with USER [COMMAND…] Run shell or command with access to a home area\n" - " authenticate USER… Authenticate a home area\n" - "\n%4$sUser Migration Commands:%5$s\n" - " adopt PATH… Add an existing home area on this system\n" - " register PATH… Register a user record locally\n" - " unregister USER… Unregister a user record locally\n" - "\n%4$sSigning Keys Commands:%5$s\n" - " list-signing-keys List home signing keys\n" - " get-signing-key [NAME…] Get a named home signing key\n" - " add-signing-key FILE… Add home signing key\n" - " remove-signing-key NAME… Remove home signing key\n" - "\n%4$sLock/Unlock Commands:%5$s\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" - "\n%4$sOther Commands:%5$s\n" - " rebalance Rebalance free space between home areas\n" - " firstboot Run first-boot home area creation wizard\n" - "\n%4$sOptions:%5$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --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" - " --offline Don't update record embedded in home directory\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" - " --key-name=NAME Key name when adding a signing key\n" - " --seize=no Do not strip existing signatures of user record\n" - " when creating\n" - " --prompt-new-user firstboot: Query user interactively for user\n" - " to create\n" - " --prompt-groups=no In first-boot mode, don't prompt for auxiliary\n" - " group memberships\n" - " --prompt-shell=no In first-boot mode, don't prompt for shells\n" - " --chrome=no In first-boot mode, don't show colour bar at top\n" - " and bottom of terminal\n" - " --mute-console=yes In first-boot mode, tell kernel/PID 1 to not\n" - " write to the console while running\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" - " --alias=ALIAS Define alias usernames for this account\n" - " --email-address=EMAIL Email address for user\n" - " --location=LOCATION Set location of user on earth\n" - " --birth-date=[DATE] Set user birth date (YYYY-MM-DD)\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" - " --capability-bounding-set=CAPS\n" - " Bounding POSIX capability set\n" - " --capability-ambient-set=CAPS\n" - " Ambient POSIX capability set\n" - " --access-mode=MODE User home directory access mode\n" - " --umask=MODE Umask for user when logging in\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 languages\n" - " --default-area=AREA Select default area\n" - "\n%4$sAuthentication User Record Properties:%5$s\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" - " --fido2-with-client-pin=BOOL\n" - " Whether to require entering a PIN to unlock the\n" - " account\n" - " --fido2-with-user-presence=BOOL\n" - " Whether to require user presence to unlock the\n" - " account\n" - " --fido2-with-user-verification=BOOL\n" - " Whether to require user verification to unlock\n" - " the account\n" - " --recovery-key=BOOL Add a recovery key\n" - "\n%4$sBlob Directory User Record Properties:%5$s\n" - " -b --blob=[FILENAME=]PATH\n" - " Path to a replacement blob directory, or replace\n" - " an individual files in the blob directory.\n" - " --avatar=PATH Path to user avatar picture\n" - " --login-background=PATH Path to user login background picture\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 Same as --enforce-password-policy=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" - " --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" - " --tmp-limit=BYTES|PERCENT Set limit on /tmp/\n" - " --dev-shm-limit=BYTES|PERCENT\n" - " Set limit on /dev/shm/\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" - " --drop-caches=BOOL Whether to automatically drop caches on logout\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" - " --luks-sector-size=BYTES\n" - " Sector size for LUKS encryption in bytes\n" - " --luks-extra-mount-options=OPTIONS\n" - " LUKS extra mount options\n" - " --auto-resize-mode=MODE Automatically grow/shrink home on login/logout\n" - " --rebalance-weight=WEIGHT Weight while rebalancing\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" - " --cifs-extra-mount-options=OPTIONS\n" - " CIFS (Windows) extra mount options\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" - " --session-launcher=LAUNCHER\n" - " Preferred session launcher file\n" - " --session-type=TYPE Preferred session type\n" - "\nSee the %6$s for details.\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal(), - ansi_underline(), - ansi_normal(), - link); + for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { + help_section(vgroups[i]); + r = table_print_or_warn(vtables[i]); + if (r < 0) + return r; + } + + help_section("Options"); + r = table_print_or_warn(otables[0]); + if (r < 0) + return r; + + for (size_t i = 1; i < ELEMENTSOF(ogroups); i++) { + help_section(ogroups[i]); + r = table_print_or_warn(otables[i]); + if (r < 0) + return r; + } + + help_man_page_reference("homectl", "1"); return 0; } -static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) { - return help(); -} +VERB_COMMON_HELP_HIDDEN(help); -static int parse_argv(int argc, char *argv[]) { +static int parse_argv(int argc, char *argv[], char ***remaining_args) { _cleanup_strv_free_ char **arg_languages = NULL; - - enum { - ARG_VERSION = 0x100, - ARG_NO_PAGER, - ARG_NO_LEGEND, - ARG_NO_ASK_PASSWORD, - ARG_OFFLINE, - ARG_REALM, - ARG_ALIAS, - 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_BIRTH_DATE, - 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_CIFS_EXTRA_MOUNT_OPTIONS, - 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_FORCE_ITERATIONS, - ARG_LUKS_PBKDF_TIME_COST, - ARG_LUKS_PBKDF_MEMORY_COST, - ARG_LUKS_PBKDF_PARALLEL_THREADS, - ARG_LUKS_SECTOR_SIZE, - 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_SESSION_LAUNCHER, - ARG_SESSION_TYPE, - ARG_PKCS11_TOKEN_URI, - ARG_FIDO2_DEVICE, - ARG_FIDO2_WITH_PIN, - ARG_FIDO2_WITH_UP, - ARG_FIDO2_WITH_UV, - ARG_RECOVERY_KEY, - ARG_DROP_CACHES, - ARG_LUKS_EXTRA_MOUNT_OPTIONS, - ARG_AUTO_RESIZE_MODE, - ARG_REBALANCE_WEIGHT, - ARG_FIDO2_CRED_ALG, - ARG_CAPABILITY_BOUNDING_SET, - ARG_CAPABILITY_AMBIENT_SET, - ARG_PROMPT_NEW_USER, - ARG_AVATAR, - ARG_LOGIN_BACKGROUND, - ARG_TMP_LIMIT, - ARG_DEV_SHM_LIMIT, - ARG_DEFAULT_AREA, - ARG_KEY_NAME, - ARG_SEIZE, - ARG_MATCH, - ARG_PROMPT_SHELL, - ARG_PROMPT_GROUPS, - ARG_CHROME, - ARG_MUTE_CONSOLE, - }; - - 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 }, - { "offline", no_argument, NULL, ARG_OFFLINE }, - { "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 }, - { "alias", required_argument, NULL, ARG_ALIAS }, - { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS }, - { "location", required_argument, NULL, ARG_LOCATION }, - { "birth-date", required_argument, NULL, ARG_BIRTH_DATE }, - { "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-force-iterations", required_argument, NULL, ARG_LUKS_PBKDF_FORCE_ITERATIONS }, - { "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 }, - { "luks-sector-size", required_argument, NULL, ARG_LUKS_SECTOR_SIZE }, - { "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 }, - { "cifs-extra-mount-options", required_argument, NULL, ARG_CIFS_EXTRA_MOUNT_OPTIONS }, - { "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 }, - { "session-launcher", required_argument, NULL, ARG_SESSION_LAUNCHER, }, - { "session-type", required_argument, NULL, ARG_SESSION_TYPE, }, - { "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-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG }, - { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE }, - { "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN }, - { "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP }, - { "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV }, - { "recovery-key", required_argument, NULL, ARG_RECOVERY_KEY }, - { "drop-caches", required_argument, NULL, ARG_DROP_CACHES }, - { "luks-extra-mount-options", required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS }, - { "auto-resize-mode", required_argument, NULL, ARG_AUTO_RESIZE_MODE }, - { "rebalance-weight", required_argument, NULL, ARG_REBALANCE_WEIGHT }, - { "capability-bounding-set", required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET }, - { "capability-ambient-set", required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET }, - { "prompt-new-user", no_argument, NULL, ARG_PROMPT_NEW_USER }, - { "blob", required_argument, NULL, 'b' }, - { "avatar", required_argument, NULL, ARG_AVATAR }, - { "login-background", required_argument, NULL, ARG_LOGIN_BACKGROUND }, - { "tmp-limit", required_argument, NULL, ARG_TMP_LIMIT }, - { "dev-shm-limit", required_argument, NULL, ARG_DEV_SHM_LIMIT }, - { "default-area", required_argument, NULL, ARG_DEFAULT_AREA }, - { "key-name", required_argument, NULL, ARG_KEY_NAME }, - { "seize", required_argument, NULL, ARG_SEIZE }, - { "match", required_argument, NULL, ARG_MATCH }, - { "prompt-shell", required_argument, NULL, ARG_PROMPT_SHELL }, - { "prompt-groups", required_argument, NULL, ARG_PROMPT_GROUPS }, - { "chrome", required_argument, NULL, ARG_CHROME }, - { "mute-console", required_argument, NULL, ARG_MUTE_CONSOLE }, - {} - }; - int r; /* This points to one of arg_identity_extra, arg_identity_extra_this_machine, @@ -4088,6 +4066,7 @@ static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); + assert(remaining_args); /* Eventually we should probably turn this into a proper --dry-run option, but as long as it is not * hooked up everywhere let's make it an environment variable only. */ @@ -4097,489 +4076,59 @@ static int parse_argv(int argc, char *argv[]) { else if (r != -ENXIO) log_debug_errno(r, "Unable to parse $SYSTEMD_HOME_DRY_RUN, ignoring: %m"); - for (;;) { - int c; - - c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPENAT", options, NULL); - if (c < 0) - break; + OptionParser opts = { argc, argv }; + FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { - case 'h': + OPTION_COMMON_HELP: return help(); - case ARG_VERSION: + OPTION_COMMON_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 ARG_OFFLINE: + OPTION_LONG("offline", NULL, "Don't update record embedded in home directory"): arg_offline = true; break; - case 'H': + OPTION_COMMON_HOST: arg_transport = BUS_TRANSPORT_REMOTE; - arg_host = optarg; + arg_host = opts.arg; break; - case 'M': - r = parse_machine_argument(optarg, &arg_host, &arg_transport); + OPTION_COMMON_MACHINE: + r = parse_machine_argument(opts.arg, &arg_host, &arg_transport); if (r < 0) return r; break; - case 'I': - arg_identity = optarg; + OPTION('I', "identity", "PATH", "Read JSON identity from file"): + arg_identity = opts.arg; break; - case 'c': - if (!isempty(optarg) && !valid_gecos(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid GECOS field '%s'.", optarg); - - r = parse_string_field(match_identity ?: &arg_identity_extra, "realName", optarg); - if (r < 0) - return r; - break; - - case ARG_ALIAS: - r = parse_group_field(&arg_identity_extra, "aliases", optarg); - if (r < 0) - return r; - break; - - case 'd': - r = parse_home_directory_field(&arg_identity_extra, "homeDirectory", optarg); - if (r < 0) - return r; - break; - - case ARG_REALM: - r = parse_realm_field(&arg_identity_extra, "realm", optarg); - if (r < 0) - return r; - break; - - case ARG_EMAIL_ADDRESS: - case ARG_LOCATION: - case ARG_ICON_NAME: - case ARG_CIFS_USER_NAME: - case ARG_CIFS_DOMAIN: - case ARG_CIFS_EXTRA_MOUNT_OPTIONS: - case ARG_LUKS_EXTRA_MOUNT_OPTIONS: - case ARG_SESSION_LAUNCHER: - case ARG_SESSION_TYPE: { - 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_EXTRA_MOUNT_OPTIONS ? "cifsExtraMountOptions" : - c == ARG_LUKS_EXTRA_MOUNT_OPTIONS ? "luksExtraMountOptions" : - c == ARG_SESSION_LAUNCHER ? "preferredSessionLauncher" : - c == ARG_SESSION_TYPE ? "preferredSessionType" : - NULL; - assert(field); - - r = parse_string_field(match_identity ?: &arg_identity_extra, field, optarg); - if (r < 0) - return r; - break; - } - - case ARG_BIRTH_DATE: - if (isempty(optarg)) { - r = drop_from_identity("birthDate"); - if (r < 0) - return r; - } else { - r = parse_birth_date(optarg, /* ret= */ NULL); - if (r < 0) - return log_error_errno(r, "Invalid birth date (expected YYYY-MM-DD): %s", optarg); - - r = parse_string_field(&arg_identity_extra, "birthDate", optarg); - if (r < 0) - return r; - } - break; - - case ARG_CIFS_SERVICE: - if (!isempty(optarg)) { - r = parse_cifs_service(optarg, /* ret_host= */ NULL, /* ret_service= */ NULL, /* ret_path= */ NULL); - if (r < 0) - return log_error_errno(r, "Failed to validate CIFS service name: %s", optarg); - } - - r = parse_string_field(match_identity ?: &arg_identity_extra, "cifsService", optarg); - if (r < 0) - return r; - break; - - case ARG_PASSWORD_HINT: - r = parse_string_field(&arg_identity_extra_privileged, "passwordHint", optarg); - if (r < 0) - return r; - - string_erase(optarg); - break; - - case ARG_NICE: - r = parse_nice_field(match_identity ?: &arg_identity_extra, "niceLevel", optarg); - if (r < 0) - return r; - break; - - case ARG_RLIMIT: - r = parse_rlimit_field(&arg_identity_extra_rlimits, "resourceLimits", optarg); - if (r < 0) - return r; - break; - - case 'u': - r = parse_uid_field(&arg_identity_extra, "uid", optarg); - if (r < 0) - return r; - break; - - case 'k': - case ARG_IMAGE_PATH: { - const char *field = c == 'k' ? "skeletonDirectory" : "imagePath"; - - r = parse_path_field(match_identity ?: &arg_identity_extra_this_machine, field, optarg); - if (r < 0) - return r; - break; - } - - case 's': - if (!isempty(optarg) && !valid_shell(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Shell '%s' not valid.", optarg); - - r = parse_string_field(match_identity ?: &arg_identity_extra, "shell", optarg); - if (r < 0) - return r; - break; - - case ARG_SETENV: - r = parse_environment_field(match_identity ?: &arg_identity_extra, "environment", optarg); - if (r < 0) - return r; - break; - - case ARG_TIMEZONE: - if (!isempty(optarg) && !timezone_is_valid(optarg, LOG_DEBUG)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Timezone '%s' is not valid.", optarg); - - r = parse_string_field(match_identity ?: &arg_identity_extra, "timeZone", optarg); - if (r < 0) - return r; - break; - - case ARG_LANGUAGE: - r = parse_language_field(&arg_languages, optarg); - if (r < 0) - return r; - 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); - - r = parse_boolean_field(match_identity ?: &arg_identity_extra, field, optarg); - if (r < 0) - return r; - break; - } - - case 'P': - r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false); - if (r < 0) - return log_error_errno(r, "Failed to set %s field: %m", "enforcePasswordPolicy"); - break; - - case ARG_DISK_SIZE: - r = parse_disk_size_field(match_identity ?: &arg_identity_extra_this_machine, optarg); - if (r < 0) - return r; - break; - - case ARG_ACCESS_MODE: - r = parse_mode_field(&arg_identity_extra, "accessMode", optarg); - if (r < 0) - return r; - break; - - case ARG_LUKS_DISCARD: - case ARG_LUKS_OFFLINE_DISCARD: { - const char *field = c == ARG_LUKS_DISCARD ? "luksDiscard" : "luksOfflineDiscard"; - - r = parse_boolean_field(match_identity ?: &arg_identity_extra, field, optarg); - if (r < 0) - return r; - break; - } - - case ARG_LUKS_VOLUME_KEY_SIZE: - case ARG_LUKS_PBKDF_FORCE_ITERATIONS: - 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_FORCE_ITERATIONS ? "luksPbkdfForceIterations" : - c == ARG_LUKS_PBKDF_PARALLEL_THREADS ? "luksPbkdfParallelThreads" : - c == ARG_RATE_LIMIT_BURST ? "rateLimitBurst" : - NULL; - assert(field); - - r = parse_unsigned_field(match_identity ?: &arg_identity_extra, field, optarg); - if (r < 0) - return r; - break; - } - - case ARG_LUKS_SECTOR_SIZE: - r = parse_sector_size_field(match_identity ?: &arg_identity_extra, "luksSectorSize", optarg); - if (r < 0) - return r; - break; - - case ARG_UMASK: - r = parse_mode_field(match_identity ?: &arg_identity_extra, "umask", optarg); - if (r < 0) - return r; - break; - - case ARG_SSH_AUTHORIZED_KEYS: - r = parse_ssh_authorized_keys(&arg_identity_extra_privileged, "sshAuthorizedKeys", optarg); - if (r < 0) - return r; - - break; - - case ARG_NOT_BEFORE: - case ARG_NOT_AFTER: - case 'e': { - const char *field = c == ARG_NOT_BEFORE ? "notBeforeUSec" : "notAfterUSec"; - - r = parse_timestamp_field(match_identity ?: &arg_identity_extra, field, optarg); - if (r < 0) - return r; - break; - } - - case ARG_PASSWORD_CHANGE_MIN: - case ARG_PASSWORD_CHANGE_MAX: - case ARG_PASSWORD_CHANGE_WARN: - case ARG_PASSWORD_CHANGE_INACTIVE: { - const char *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); - - r = parse_time_field(match_identity ?: &arg_identity_extra, field, optarg); - if (r < 0) - return r; - 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); - - sd_json_variant **identity = - match_identity ?: - IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ? - &arg_identity_extra_this_machine : &arg_identity_extra; - - if (!string_is_safe(optarg, STRING_ALLOW_GLOBS)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Parameter for field %s not valid: %s", field, optarg); - - r = parse_string_field(identity, field, optarg); - if (r < 0) - return r; - 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; - assert(field); - - r = parse_time_field(match_identity ?: &arg_identity_extra, field, optarg); - if (r < 0) - return r; - break; - } - - case 'G': - r = parse_group_field(match_identity ?: &arg_identity_extra, "memberOf", optarg); - if (r < 0) - return r; - break; - - case ARG_TASKS_MAX: - r = parse_u64_field(match_identity ?: &arg_identity_extra, "tasksMax", optarg); - if (r < 0) - return r; - 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; - - r = parse_size_field(match_identity ?: &arg_identity_extra_this_machine, field, optarg); - if (r < 0) - return r; - break; - } - - case ARG_CPU_WEIGHT: - case ARG_IO_WEIGHT: { - const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" : - c == ARG_IO_WEIGHT ? "ioWeight" : - NULL; - - r = parse_weight_field(match_identity ?: &arg_identity_extra, field, optarg); - if (r < 0) - return r; - break; - } - - case ARG_PKCS11_TOKEN_URI: - r = parse_pkcs11_token_uri_field(optarg); + OPTION_COMMON_JSON: + r = parse_json_argument(opts.arg, &arg_json_format_flags); if (r <= 0) return r; break; - case ARG_FIDO2_CRED_ALG: - r = parse_fido2_algorithm(optarg, &arg_fido2_cred_alg); - if (r < 0) - return log_error_errno(r, "Failed to parse COSE algorithm: %s", optarg); - break; - - case ARG_FIDO2_DEVICE: - r = parse_fido2_device_field(optarg); - if (r <= 0) - return r; - break; - - case ARG_FIDO2_WITH_PIN: - r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL); - if (r < 0) - return r; - - SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r); - break; - - case ARG_FIDO2_WITH_UP: - r = parse_boolean_argument("--fido2-with-user-presence=", optarg, NULL); - if (r < 0) - return r; - - SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r); - break; - - case ARG_FIDO2_WITH_UV: - r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL); - if (r < 0) - return r; - - SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r); - break; - - case ARG_RECOVERY_KEY: - r = parse_boolean(optarg); - if (r < 0) - return log_error_errno(r, "Failed to parse --recovery-key= argument: %s", optarg); - arg_recovery_key = r; - - r = drop_from_identity("recoveryKey", "recoveryKeyType"); - if (r < 0) - return r; - break; - - case ARG_AUTO_RESIZE_MODE: - r = parse_auto_resize_mode_field(match_identity ?: &arg_identity_extra, - "autoResizeMode", optarg); - if (r < 0) - return r; - break; - - case ARG_REBALANCE_WEIGHT: - r = parse_rebalance_weight(match_identity ?: &arg_identity_extra, - "rebalanceWeight", optarg); - if (r < 0) - return r; - break; - - case 'j': + OPTION_COMMON_LOWERCASE_J: arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; break; - case ARG_JSON: - r = parse_json_argument(optarg, &arg_json_format_flags); - if (r <= 0) - return r; + OPTION_LONG("export-format", "FORMAT", + "Strip JSON inspection data (full, stripped, minimal)"): + if (streq(opts.arg, "help")) + return DUMP_STRING_TABLE(export_format, ExportFormat, _EXPORT_FORMAT_MAX); + + arg_export_format = export_format_from_string(opts.arg); + if (arg_export_format < 0) + return log_error_errno(arg_export_format, "Invalid export format: %s", opts.arg); break; - case 'E': + OPTION_SHORT('E', NULL, "Same as -j --export-format=stripped"): {} + OPTION_HELP_VERBATIM("-EE", "Same as -j --export-format=minimal"): if (arg_export_format == EXPORT_FORMAT_FULL) arg_export_format = EXPORT_FORMAT_STRIPPED; else if (arg_export_format == EXPORT_FORMAT_STRIPPED) @@ -4592,73 +4141,314 @@ static int parse_argv(int argc, char *argv[]) { arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; break; - case ARG_EXPORT_FORMAT: - if (streq(optarg, "help")) - return DUMP_STRING_TABLE(export_format, ExportFormat, _EXPORT_FORMAT_MAX); + OPTION_LONG("key-name", "NAME", "Key name when adding a signing key"): + if (!isempty(opts.arg) && !filename_is_valid(opts.arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Parameter for --key-name= not a valid filename: %s", opts.arg); - arg_export_format = export_format_from_string(optarg); - if (arg_export_format < 0) - return log_error_errno(arg_export_format, "Invalid export format: %s", optarg); - - break; - - case ARG_DROP_CACHES: - r = parse_boolean_field(match_identity ?: &arg_identity_extra, "dropCaches", optarg); + r = free_and_strdup_warn(&arg_key_name, empty_to_null(opts.arg)); if (r < 0) return r; break; - case ARG_CAPABILITY_AMBIENT_SET: - r = parse_capability_set_field(match_identity ?: &arg_identity_extra, - &arg_capability_ambient_set, - "capabilityAmbientSet", optarg); + OPTION_LONG("seize", "BOOL", + "Whether to strip existing signatures of user record when creating"): + r = parse_boolean_argument("--seize=", opts.arg, &arg_seize); if (r < 0) return r; break; - case ARG_CAPABILITY_BOUNDING_SET: - r = parse_capability_set_field(match_identity ?: &arg_identity_extra, - &arg_capability_bounding_set, - "capabilityBoundingSet", optarg); - if (r < 0) - return r; - break; - - case ARG_PROMPT_NEW_USER: + OPTION_LONG("prompt-new-user", NULL, + "firstboot: Query user interactively for user to create"): arg_prompt_new_user = true; break; - case 'b': - case ARG_AVATAR: - case ARG_LOGIN_BACKGROUND: { + OPTION_LONG("prompt-groups", "BOOL", + "In first-boot mode, don't prompt for auxiliary group memberships"): + r = parse_boolean_argument("--prompt-groups=", opts.arg, &arg_prompt_groups); + if (r < 0) + return r; + break; + + OPTION_LONG("prompt-shell", "BOOL", + "In first-boot mode, don't prompt for shells"): + r = parse_boolean_argument("--prompt-shell=", opts.arg, &arg_prompt_shell); + if (r < 0) + return r; + break; + + OPTION_LONG("chrome", "BOOL", + "In first-boot mode, don't show colour bar at top and bottom of terminal"): + r = parse_boolean_argument("--chrome=", opts.arg, &arg_chrome); + if (r < 0) + return r; + break; + + OPTION_LONG("mute-console", "BOOL", + "In first-boot mode, tell kernel/PID 1 to not write to the console while running"): + r = parse_boolean_argument("--mute-console=", opts.arg, &arg_mute_console); + if (r < 0) + return r; + break; + + OPTION_COMMON_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + OPTION_COMMON_NO_LEGEND: + arg_legend = false; + break; + + OPTION_COMMON_NO_ASK_PASSWORD: + arg_ask_password = false; + break; + + OPTION_GROUP("General User Record Properties"): {} + + OPTION('c', "real-name", "REALNAME", "Real name for user"): {} + OPTION_LONG("comment", "REALNAME", /* help= */ NULL): /* Compat alias to keep things in sync with useradd(8) */ + if (!isempty(opts.arg) && !valid_gecos(opts.arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid GECOS field '%s'.", opts.arg); + + r = parse_string_field(match_identity ?: &arg_identity_extra, "realName", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("realm", "REALM", "Realm to create user in"): + r = parse_realm_field(&arg_identity_extra, "realm", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("alias", "ALIAS", "Define alias usernames for this account"): + r = parse_group_field(&arg_identity_extra, "aliases", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("email-address", "EMAIL", "Email address for user"): + r = parse_string_field(match_identity ?: &arg_identity_extra, "emailAddress", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("location", "LOCATION", "Set location of user on earth"): + r = parse_string_field(match_identity ?: &arg_identity_extra, "location", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "birth-date", "DATE", + "Set user birth date (YYYY-MM-DD)"): + if (isempty(opts.arg)) { + r = drop_from_identity("birthDate"); + if (r < 0) + return r; + } else { + r = parse_birth_date(opts.arg, /* ret= */ NULL); + if (r < 0) + return log_error_errno(r, "Invalid birth date (expected YYYY-MM-DD): %s", opts.arg); + + r = parse_string_field(&arg_identity_extra, "birthDate", opts.arg); + if (r < 0) + return r; + } + break; + + OPTION_LONG("icon-name", "NAME", "Icon name for user"): + r = parse_string_field(match_identity ?: &arg_identity_extra, "iconName", opts.arg); + if (r < 0) + return r; + break; + + OPTION('d', "home-dir", "PATH", "Home directory"): /* Compatible with useradd(8) */ + r = parse_home_directory_field(&arg_identity_extra, "homeDirectory", opts.arg); + if (r < 0) + return r; + break; + + OPTION('u', "uid", "UID", "Numeric UID for user"): /* Compatible with useradd(8) */ + r = parse_uid_field(&arg_identity_extra, "uid", opts.arg); + if (r < 0) + return r; + break; + + OPTION('G', "member-of", "GROUP", "Add user to group"): {} + OPTION_LONG("groups", "GROUP", /* help= */ NULL): /* Compat alias to keep things in sync with useradd(8) */ + r = parse_group_field(match_identity ?: &arg_identity_extra, "memberOf", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("capability-bounding-set", "CAPS", "Bounding POSIX capability set"): + r = parse_capability_set_field(match_identity ?: &arg_identity_extra, + &arg_capability_bounding_set, + "capabilityBoundingSet", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("capability-ambient-set", "CAPS", "Ambient POSIX capability set"): + r = parse_capability_set_field(match_identity ?: &arg_identity_extra, + &arg_capability_ambient_set, + "capabilityAmbientSet", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("access-mode", "MODE", "User home directory access mode"): + r = parse_mode_field(&arg_identity_extra, "accessMode", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("umask", "MODE", "Umask for user when logging in"): + r = parse_mode_field(match_identity ?: &arg_identity_extra, "umask", opts.arg); + if (r < 0) + return r; + break; + + OPTION('k', "skel", "PATH", "Skeleton directory to use"): /* Compatible with useradd(8) */ + r = parse_path_field(match_identity ?: &arg_identity_extra_this_machine, "skeletonDirectory", opts.arg); + if (r < 0) + return r; + break; + + OPTION('s', "shell", "PATH", "Shell for account"): /* Compatible with useradd(8) */ + if (!isempty(opts.arg) && !valid_shell(opts.arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Shell '%s' not valid.", opts.arg); + + r = parse_string_field(match_identity ?: &arg_identity_extra, "shell", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("setenv", "VARIABLE[=VALUE]", "Set an environment variable at log-in"): + r = parse_environment_field(match_identity ?: &arg_identity_extra, "environment", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("timezone", "TIMEZONE", "Set a time-zone"): + if (!isempty(opts.arg) && !timezone_is_valid(opts.arg, LOG_DEBUG)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Timezone '%s' is not valid.", opts.arg); + + r = parse_string_field(match_identity ?: &arg_identity_extra, "timeZone", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("language", "LOCALE", "Set preferred languages"): + r = parse_language_field(&arg_languages, opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("default-area", "AREA", "Select default area"): + r = parse_filename_field(match_identity ?: &arg_identity_extra, "defaultArea", opts.arg); + if (r < 0) + return r; + break; + + OPTION_GROUP("Authentication User Record Properties"): {} + + OPTION_LONG("ssh-authorized-keys", "KEYS", "Specify SSH public keys"): + r = parse_ssh_authorized_keys(&arg_identity_extra_privileged, "sshAuthorizedKeys", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("pkcs11-token-uri", "URI", + "URI to PKCS#11 security token containing private key and matching X.509 certificate"): + r = parse_pkcs11_token_uri_field(opts.arg); + if (r <= 0) + return r; + break; + + OPTION_LONG("fido2-device", "PATH", + "Path to FIDO2 hidraw device with hmac-secret extension"): + r = parse_fido2_device_field(opts.arg); + if (r <= 0) + return r; + break; + + OPTION_LONG("fido2-with-client-pin", "BOOL", + "Whether to require entering a PIN to unlock the account"): + r = parse_boolean_argument("--fido2-with-client-pin=", opts.arg, NULL); + if (r < 0) + return r; + + SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r); + break; + + OPTION_LONG("fido2-with-user-presence", "BOOL", + "Whether to require user presence to unlock the account"): + r = parse_boolean_argument("--fido2-with-user-presence=", opts.arg, NULL); + if (r < 0) + return r; + + SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r); + break; + + OPTION_LONG("fido2-with-user-verification", "BOOL", + "Whether to require user verification to unlock the account"): + r = parse_boolean_argument("--fido2-with-user-verification=", opts.arg, NULL); + if (r < 0) + return r; + + SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r); + break; + + OPTION_LONG("recovery-key", "BOOL", "Add a recovery key"): + r = parse_boolean(opts.arg); + if (r < 0) + return log_error_errno(r, "Failed to parse --recovery-key= argument: %s", opts.arg); + arg_recovery_key = r; + + r = drop_from_identity("recoveryKey", "recoveryKeyType"); + if (r < 0) + return r; + break; + + OPTION_GROUP("Blob Directory User Record Properties"): {} + + OPTION('b', "blob", "[FILENAME=]PATH", + "Path to a replacement blob directory, or replace an individual files in the blob directory"): {} + OPTION_LONG("avatar", "PATH", "Path to user avatar picture"): {} + OPTION_LONG("login-background", "PATH", "Path to user login background picture"): { _cleanup_close_ int fd = -EBADF; _cleanup_free_ char *path = NULL, *filename = NULL; + const char *long_code = opts.opt->long_code; - if (c == 'b') { - char *eq; + if (streq(long_code, "blob")) { + const char *eq; - if (isempty(optarg)) { /* --blob= deletes everything, including existing blob dirs */ + if (isempty(opts.arg)) { /* --blob= deletes everything, including existing blob dirs */ hashmap_clear(arg_blob_files); arg_blob_dir = mfree(arg_blob_dir); arg_blob_clear = true; break; } - eq = strrchr(optarg, '='); + eq = strrchr(opts.arg, '='); if (!eq) { /* --blob=/some/path replaces the blob dir */ - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_blob_dir); + r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_blob_dir); if (r < 0) return r; break; } /* --blob=filename=/some/path replaces the file "filename" with /some/path */ - filename = strndup(optarg, eq - optarg); + filename = strndup(opts.arg, eq - opts.arg); if (!filename) return log_oom(); if (isempty(filename)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", optarg); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", opts.arg); if (!suitable_blob_filename(filename)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid blob filename: %s", filename); @@ -4666,17 +4456,11 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; } else { - const char *well_known_filename = - c == ARG_AVATAR ? "avatar" : - c == ARG_LOGIN_BACKGROUND ? "login-background" : - NULL; - assert(well_known_filename); - - filename = strdup(well_known_filename); + filename = strdup(long_code); if (!filename) return log_oom(); - r = parse_path_argument(optarg, /* suppress_root= */ false, &path); + r = parse_path_argument(opts.arg, /* suppress_root= */ false, &path); if (r < 0) return r; } @@ -4700,107 +4484,420 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_TMP_LIMIT: - case ARG_DEV_SHM_LIMIT: { - const char *field = - c == ARG_TMP_LIMIT ? "tmpLimit" : - c == ARG_DEV_SHM_LIMIT ? "devShmLimit" : - NULL; - const char *field_scale = - c == ARG_TMP_LIMIT ? "tmpLimitScale" : - c == ARG_DEV_SHM_LIMIT ? "devShmLimitScale" : - NULL; + OPTION_GROUP("Account Management User Record Properties"): {} - assert(field); - assert(field_scale); + OPTION_LONG("locked", "BOOL", "Set locked account state"): + r = parse_boolean_field(match_identity ?: &arg_identity_extra, "locked", opts.arg); + if (r < 0) + return r; + break; - r = parse_tmpfs_limit_field(match_identity ?: &arg_identity_extra, - field, field_scale, optarg); + OPTION_LONG("not-before", "TIMESTAMP", "Do not allow logins before"): {} + OPTION_LONG("not-after", "TIMESTAMP", "Do not allow logins after"): {} + OPTION_LONG("expiredate", "TIMESTAMP", /* help= */ NULL): /* Compat alias for -e to keep things in sync with useradd(8) */ { + const char *field = streq(opts.opt->long_code, "not-before") ? "notBeforeUSec" : "notAfterUSec"; + + r = parse_timestamp_field(match_identity ?: &arg_identity_extra, field, opts.arg); if (r < 0) return r; break; } - case ARG_DEFAULT_AREA: - r = parse_filename_field(match_identity ?: &arg_identity_extra, "defaultArea", optarg); + OPTION_SHORT('e', "TIMESTAMP", /* help= */ NULL): /* -e alias for --expiredate */ + r = parse_timestamp_field(match_identity ?: &arg_identity_extra, "notAfterUSec", opts.arg); if (r < 0) return r; break; - case ARG_KEY_NAME: - if (!isempty(optarg) && !filename_is_valid(optarg)) + OPTION_LONG("rate-limit-interval", "SECS", "Login rate-limit interval in seconds"): + r = parse_time_field(match_identity ?: &arg_identity_extra, "rateLimitIntervalUSec", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("rate-limit-burst", "NUMBER", "Login rate-limit attempts per interval"): + r = parse_unsigned_field(match_identity ?: &arg_identity_extra, "rateLimitBurst", opts.arg); + if (r < 0) + return r; + break; + + OPTION_GROUP("Password Policy User Record Properties"): {} + + OPTION_LONG("password-hint", "HINT", "Set Password hint"): + r = parse_string_field(&arg_identity_extra_privileged, "passwordHint", opts.arg); + if (r < 0) + return r; + + string_erase((char *) opts.arg); + break; + + OPTION_LONG("enforce-password-policy", "BOOL", + "Control whether to enforce system's password policy for this user"): + r = parse_boolean_field(match_identity ?: &arg_identity_extra, "enforcePasswordPolicy", opts.arg); + if (r < 0) + return r; + break; + + OPTION_SHORT('P', NULL, "Same as --enforce-password-policy=no"): + r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", "enforcePasswordPolicy"); + break; + + OPTION_LONG("password-change-now", "BOOL", + "Require the password to be changed on next login"): + r = parse_boolean_field(match_identity ?: &arg_identity_extra, "passwordChangeNow", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("password-change-min", "TIME", "Require minimum time between password changes"): {} + OPTION_LONG("password-change-max", "TIME", "Require maximum time between password changes"): {} + OPTION_LONG("password-change-warn", "TIME", "How much time to warn before password expiry"): {} + OPTION_LONG("password-change-inactive", "TIME", "How much time to block password after expiry"): { + const char *lc = opts.opt->long_code; + const char *field = + streq(lc, "password-change-min") ? "passwordChangeMinUSec" : + streq(lc, "password-change-max") ? "passwordChangeMaxUSec" : + streq(lc, "password-change-warn") ? "passwordChangeWarnUSec" : + streq(lc, "password-change-inactive") ? "passwordChangeInactiveUSec" : + NULL; + assert(field); + + r = parse_time_field(match_identity ?: &arg_identity_extra, field, opts.arg); + if (r < 0) + return r; + break; + } + + OPTION_GROUP("Resource Management User Record Properties"): {} + + OPTION_LONG("disk-size", "BYTES", "Size to assign the user on disk"): + r = parse_disk_size_field(match_identity ?: &arg_identity_extra_this_machine, opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("nice", "NICE", "Nice level for user"): + r = parse_nice_field(match_identity ?: &arg_identity_extra, "niceLevel", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("rlimit", "LIMIT=VALUE[:VALUE]", "Set resource limits"): + r = parse_rlimit_field(&arg_identity_extra_rlimits, "resourceLimits", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("tasks-max", "MAX", "Set maximum number of per-user tasks"): + r = parse_u64_field(match_identity ?: &arg_identity_extra, "tasksMax", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("memory-high", "BYTES", "Set high memory threshold in bytes"): + r = parse_size_field(match_identity ?: &arg_identity_extra_this_machine, "memoryHigh", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("memory-max", "BYTES", "Set maximum memory limit"): + r = parse_size_field(match_identity ?: &arg_identity_extra_this_machine, "memoryMax", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("cpu-weight", "WEIGHT", "Set CPU weight"): {} + OPTION_LONG("io-weight", "WEIGHT", "Set IO weight"): { + const char *field = streq(opts.opt->long_code, "cpu-weight") ? "cpuWeight" : "ioWeight"; + + r = parse_weight_field(match_identity ?: &arg_identity_extra, field, opts.arg); + if (r < 0) + return r; + break; + } + + OPTION_LONG("tmp-limit", "BYTES|PERCENT", "Set limit on /tmp/"): {} + OPTION_LONG("dev-shm-limit", "BYTES|PERCENT", "Set limit on /dev/shm/"): { + bool is_tmp = streq(opts.opt->long_code, "tmp-limit"); + const char *field = is_tmp ? "tmpLimit" : "devShmLimit"; + const char *field_scale = is_tmp ? "tmpLimitScale" : "devShmLimitScale"; + + r = parse_tmpfs_limit_field(match_identity ?: &arg_identity_extra, + field, field_scale, opts.arg); + if (r < 0) + return r; + break; + } + + OPTION_GROUP("Storage User Record Properties"): {} + + OPTION_LONG("storage", "STORAGE", + "Storage type to use (luks, fscrypt, directory, subvolume, cifs)"): + if (!string_is_safe(opts.arg, STRING_ALLOW_GLOBS)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Parameter for --key-name= not a valid filename: %s", optarg); + "Parameter for field %s not valid: %s", "storage", opts.arg); - r = free_and_strdup_warn(&arg_key_name, empty_to_null(optarg)); + r = parse_string_field(match_identity ?: &arg_identity_extra_this_machine, "storage", opts.arg); if (r < 0) return r; break; - case ARG_SEIZE: - r = parse_boolean_argument("--seize=", optarg, &arg_seize); + OPTION_LONG("image-path", "PATH", "Path to image file/directory"): + r = parse_path_field(match_identity ?: &arg_identity_extra_this_machine, "imagePath", opts.arg); if (r < 0) return r; break; - case ARG_MATCH: - if (streq(optarg, "any")) + OPTION_LONG("drop-caches", "BOOL", "Whether to automatically drop caches on logout"): + r = parse_boolean_field(match_identity ?: &arg_identity_extra, "dropCaches", opts.arg); + if (r < 0) + return r; + break; + + OPTION_GROUP("LUKS Storage User Record Properties"): {} + + OPTION_LONG("fs-type", "TYPE", + "File system type to use in case of luks storage (btrfs, ext4, xfs)"): + if (!string_is_safe(opts.arg, STRING_ALLOW_GLOBS)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Parameter for field %s not valid: %s", "fileSystemType", opts.arg); + + r = parse_string_field(match_identity ?: &arg_identity_extra_this_machine, "fileSystemType", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-discard", "BOOL", + "Whether to use 'discard' feature of file system when activated (mounted)"): {} + OPTION_LONG("luks-offline-discard", "BOOL", "Whether to trim file on logout"): { + const char *field = streq(opts.opt->long_code, "luks-discard") ? "luksDiscard" : "luksOfflineDiscard"; + + r = parse_boolean_field(match_identity ?: &arg_identity_extra, field, opts.arg); + if (r < 0) + return r; + break; + } + + OPTION_LONG("luks-cipher", "CIPHER", "Cipher to use for LUKS encryption"): + if (!string_is_safe(opts.arg, STRING_ALLOW_GLOBS)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Parameter for field %s not valid: %s", "luksCipher", opts.arg); + + r = parse_string_field(match_identity ?: &arg_identity_extra, "luksCipher", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-cipher-mode", "MODE", "Cipher mode to use for LUKS encryption"): + if (!string_is_safe(opts.arg, STRING_ALLOW_GLOBS)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Parameter for field %s not valid: %s", "luksCipherMode", opts.arg); + + r = parse_string_field(match_identity ?: &arg_identity_extra, "luksCipherMode", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-volume-key-size", "BITS", "Volume key size to use for LUKS encryption"): + r = parse_unsigned_field(match_identity ?: &arg_identity_extra, "luksVolumeKeySize", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-pbkdf-type", "TYPE", "Password-based Key Derivation Function to use"): + if (!string_is_safe(opts.arg, STRING_ALLOW_GLOBS)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Parameter for field %s not valid: %s", "luksPbkdfType", opts.arg); + + r = parse_string_field(match_identity ?: &arg_identity_extra, "luksPbkdfType", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-pbkdf-hash-algorithm", "ALG", "PBKDF hash algorithm to use"): + if (!string_is_safe(opts.arg, STRING_ALLOW_GLOBS)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Parameter for field %s not valid: %s", "luksPbkdfHashAlgorithm", opts.arg); + + r = parse_string_field(match_identity ?: &arg_identity_extra, "luksPbkdfHashAlgorithm", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-pbkdf-time-cost", "SECS", "Time cost for PBKDF in seconds"): + r = parse_time_field(match_identity ?: &arg_identity_extra, "luksPbkdfTimeCostUSec", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-pbkdf-memory-cost", "BYTES", "Memory cost for PBKDF in bytes"): + r = parse_size_field(match_identity ?: &arg_identity_extra_this_machine, "luksPbkdfMemoryCost", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-pbkdf-parallel-threads", "N", "Number of parallel threads for PKBDF"): + r = parse_unsigned_field(match_identity ?: &arg_identity_extra, "luksPbkdfParallelThreads", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-sector-size", "BYTES", "Sector size for LUKS encryption in bytes"): + r = parse_sector_size_field(match_identity ?: &arg_identity_extra, "luksSectorSize", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-extra-mount-options", "…", "LUKS extra mount options"): + r = parse_string_field(match_identity ?: &arg_identity_extra, "luksExtraMountOptions", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("luks-pbkdf-force-iterations", "NUMBER", /* help= */ NULL): + r = parse_unsigned_field(match_identity ?: &arg_identity_extra, "luksPbkdfForceIterations", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("auto-resize-mode", "MODE", + "Automatically grow/shrink home on login/logout"): + r = parse_auto_resize_mode_field(match_identity ?: &arg_identity_extra, + "autoResizeMode", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("rebalance-weight", "WEIGHT", "Weight while rebalancing"): + r = parse_rebalance_weight(match_identity ?: &arg_identity_extra, + "rebalanceWeight", opts.arg); + if (r < 0) + return r; + break; + + OPTION_GROUP("Mounting User Record Properties"): {} + + OPTION_LONG("nosuid", "BOOL", "Control the 'nosuid' flag of the home mount"): + r = parse_boolean_field(match_identity ?: &arg_identity_extra, "mountNoSuid", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("nodev", "BOOL", "Control the 'nodev' flag of the home mount"): + r = parse_boolean_field(match_identity ?: &arg_identity_extra, "mountNoDevices", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("noexec", "BOOL", "Control the 'noexec' flag of the home mount"): + r = parse_boolean_field(match_identity ?: &arg_identity_extra, "mountNoExecute", opts.arg); + if (r < 0) + return r; + break; + + OPTION_GROUP("CIFS User Record Properties"): {} + + OPTION_LONG("cifs-domain", "DOMAIN", "CIFS (Windows) domain"): + r = parse_string_field(match_identity ?: &arg_identity_extra, "cifsDomain", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("cifs-user-name", "USER", "CIFS (Windows) user name"): + r = parse_string_field(match_identity ?: &arg_identity_extra, "cifsUserName", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("cifs-service", "SERVICE", + "CIFS (Windows) service to mount as home area"): + if (!isempty(opts.arg)) { + r = parse_cifs_service(opts.arg, /* ret_host= */ NULL, /* ret_service= */ NULL, /* ret_path= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to validate CIFS service name: %s", opts.arg); + } + + r = parse_string_field(match_identity ?: &arg_identity_extra, "cifsService", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("cifs-extra-mount-options", "…", + "CIFS (Windows) extra mount options"): + r = parse_string_field(match_identity ?: &arg_identity_extra, "cifsExtraMountOptions", opts.arg); + if (r < 0) + return r; + break; + + OPTION_GROUP("Login Behaviour User Record Properties"): {} + + OPTION_LONG("stop-delay", "SECS", + "How long to leave user services running after logout"): + r = parse_time_field(match_identity ?: &arg_identity_extra, "stopDelayUSec", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("kill-processes", "BOOL", + "Whether to kill user processes when sessions terminate"): + r = parse_boolean_field(match_identity ?: &arg_identity_extra, "killProcesses", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("auto-login", "BOOL", "Try to log this user in automatically"): + r = parse_boolean_field(match_identity ?: &arg_identity_extra, "autoLogin", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("session-launcher", "LAUNCHER", "Preferred session launcher file"): + r = parse_string_field(match_identity ?: &arg_identity_extra, "preferredSessionLauncher", opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("session-type", "TYPE", "Preferred session type"): + r = parse_string_field(match_identity ?: &arg_identity_extra, "preferredSessionType", opts.arg); + if (r < 0) + return r; + break; + + /* Hidden options below */ + + OPTION_LONG("fido2-credential-algorithm", "ALG", /* help= */ NULL): + r = parse_fido2_algorithm(opts.arg, &arg_fido2_cred_alg); + if (r < 0) + return log_error_errno(r, "Failed to parse COSE algorithm: %s", opts.arg); + break; + + OPTION_LONG("match", "any|this|other|auto", /* help= */ NULL): + if (streq(opts.arg, "any")) match_identity = &arg_identity_extra; - else if (streq(optarg, "this")) + else if (streq(opts.arg, "this")) match_identity = &arg_identity_extra_this_machine; - else if (streq(optarg, "other")) + else if (streq(opts.arg, "other")) match_identity = &arg_identity_extra_other_machines; - else if (streq(optarg, "auto")) + else if (streq(opts.arg, "auto")) match_identity = NULL; else return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--machine= argument not understood. Refusing."); break; - case 'A': + OPTION_SHORT('A', NULL, /* help= */ NULL): match_identity = &arg_identity_extra; break; - case 'T': + + OPTION_SHORT('T', NULL, /* help= */ NULL): match_identity = &arg_identity_extra_this_machine; break; - case 'N': + + OPTION_SHORT('N', NULL, /* help= */ NULL): match_identity = &arg_identity_extra_other_machines; break; - - case ARG_PROMPT_SHELL: - r = parse_boolean_argument("--prompt-shell=", optarg, &arg_prompt_shell); - if (r < 0) - return r; - - break; - - case ARG_PROMPT_GROUPS: - r = parse_boolean_argument("--prompt-groups=", optarg, &arg_prompt_groups); - if (r < 0) - return r; - - break; - - case ARG_CHROME: - r = parse_boolean_argument("--chrome=", optarg, &arg_chrome); - if (r < 0) - return r; - - break; - - case ARG_MUTE_CONSOLE: - r = parse_boolean_argument("--mute-console=", optarg, &arg_mute_console); - if (r < 0) - return r; - - break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - } if (!strv_isempty(arg_languages)) { char **additional; @@ -4821,6 +4918,7 @@ static int parse_argv(int argc, char *argv[]) { } } + *remaining_args = option_parser_get_args(&opts); return 1; } @@ -5050,304 +5148,8 @@ static int fallback_shell(int argc, char *argv[]) { return log_error_errno(errno, "Failed to execute shell '%s': %m", shell); } -static int verb_list_signing_keys(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; - - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - r = acquire_bus(&bus); - if (r < 0) - return r; - - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - r = bus_call_method(bus, bus_mgr, "ListSigningKeys", &error, &reply, NULL); - if (r < 0) - return log_error_errno(r, "Failed to list signing keys: %s", bus_error_message(&error, r)); - - _cleanup_(table_unrefp) Table *table = table_new("name", "key"); - if (!table) - return log_oom(); - - r = sd_bus_message_enter_container(reply, 'a', "(sst)"); - if (r < 0) - return bus_log_parse_error(r); - - for (;;) { - const char *name, *pem; - - r = sd_bus_message_read(reply, "(sst)", &name, &pem, NULL); - if (r < 0) - return bus_log_parse_error(r); - if (r == 0) - break; - - _cleanup_free_ char *h = NULL; - if (!sd_json_format_enabled(arg_json_format_flags)) { - /* Let's decode the PEM key to DER (so that we lose prefix/suffix), then truncate it - * for display reasons. */ - - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - - _cleanup_(EVP_PKEY_freep) EVP_PKEY *key = NULL; - r = openssl_pubkey_from_pem(pem, SIZE_MAX, &key); - if (r < 0) - return log_error_errno(r, "Failed to parse PEM: %m"); - - _cleanup_free_ void *der = NULL; - int n = sym_i2d_PUBKEY(key, (unsigned char**) &der); - if (n < 0) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to encode key as DER."); - - ssize_t m = base64mem(der, MIN(n, 64), &h); - if (m < 0) - return log_oom(); - if (n > 64) /* check if we truncated the original version */ - if (!strextend(&h, glyph(GLYPH_ELLIPSIS))) - return log_oom(); - } - - r = table_add_many( - table, - TABLE_STRING, name, - TABLE_STRING, h ?: pem); - 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_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) { - r = table_set_sort(table, (size_t) 0); - if (r < 0) - return table_log_sort_error(r); - - r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); - if (r < 0) - return r; - } - - if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) { - if (table_isempty(table)) - printf("No signing keys.\n"); - else - printf("\n%zu signing keys listed.\n", table_get_rows(table) - 1); - } - - return 0; -} - -static int verb_get_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; - - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - r = acquire_bus(&bus); - if (r < 0) - return r; - - char **keys = argc >= 2 ? strv_skip(argv, 1) : STRV_MAKE("local.public"); - int ret = 0; - STRV_FOREACH(k, keys) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - r = bus_call_method(bus, bus_mgr, "GetSigningKey", &error, &reply, "s", *k); - if (r < 0) { - RET_GATHER(ret, log_error_errno(r, "Failed to get signing key '%s': %s", *k, bus_error_message(&error, r))); - continue; - } - - const char *pem; - r = sd_bus_message_read(reply, "st", &pem, NULL); - if (r < 0) { - RET_GATHER(ret, bus_log_parse_error(r)); - continue; - } - - fputs(pem, stdout); - if (!endswith(pem, "\n")) - fputc('\n', stdout); - - fflush(stdout); - } - - return ret; -} - -static int add_signing_key_one(sd_bus *bus, const char *fn, FILE *key) { - int r; - - assert_se(bus); - assert_se(fn); - assert_se(key); - - _cleanup_free_ char *pem = NULL; - r = read_full_stream(key, &pem, /* ret_size= */ NULL); - if (r < 0) - return log_error_errno(r, "Failed to read key '%s': %m", fn); - - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - r = bus_call_method(bus, bus_mgr, "AddSigningKey", &error, /* ret_reply= */ NULL, "sst", fn, pem, UINT64_C(0)); - if (r < 0) - return log_error_errno(r, "Failed to add signing key '%s': %s", fn, bus_error_message(&error, r)); - - return 0; -} - -static int verb_add_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; - - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - r = acquire_bus(&bus); - if (r < 0) - return r; - - int ret = EXIT_SUCCESS; - if (argc < 2 || streq(argv[1], "-")) { - if (!arg_key_name) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key name must be specified via --key-name= when reading key from standard input, refusing."); - - RET_GATHER(ret, add_signing_key_one(bus, arg_key_name, stdin)); - } else { - /* Refuse if more han one key is specified in combination with --key-name= */ - if (argc >= 3 && arg_key_name) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--key-name= is not supported if multiple signing keys are specified, refusing."); - - STRV_FOREACH(k, strv_skip(argv, 1)) { - - if (streq(*k, "-")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing to read from standard input if multiple keys are specified."); - - _cleanup_free_ char *fn = NULL; - if (!arg_key_name) { - r = path_extract_filename(*k, &fn); - if (r < 0) { - RET_GATHER(ret, log_error_errno(r, "Failed to extract filename from path '%s': %m", *k)); - continue; - } - } - - _cleanup_fclose_ FILE *f = fopen(*k, "re"); - if (!f) { - RET_GATHER(ret, log_error_errno(errno, "Failed to open '%s': %m", *k)); - continue; - } - - RET_GATHER(ret, add_signing_key_one(bus, fn ?: arg_key_name, f)); - } - } - - return ret; -} - -static int add_signing_keys_from_credentials(void) { - int r; - - _cleanup_close_ int fd = open_credentials_dir(); - if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */ - return 0; - if (fd < 0) - return log_error_errno(fd, "Failed to open credentials directory: %m"); - - _cleanup_free_ DirectoryEntries *des = NULL; - r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); - if (r < 0) - return log_error_errno(r, "Failed to enumerate credentials: %m"); - - int ret = 0; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - FOREACH_ARRAY(i, des->entries, des->n_entries) { - struct dirent *de = *i; - if (de->d_type != DT_REG) - continue; - - const char *e = startswith(de->d_name, "home.add-signing-key."); - if (!e) - continue; - - if (!filename_is_valid(e)) - continue; - - if (!bus) { - r = acquire_bus(&bus); - if (r < 0) - return r; - } - - _cleanup_fclose_ FILE *f = NULL; - r = xfopenat(fd, de->d_name, "re", O_NOFOLLOW, &f); - if (r < 0) { - RET_GATHER(ret, log_error_errno(r, "Failed to open credential '%s': %m", de->d_name)); - continue; - } - - RET_GATHER(ret, add_signing_key_one(bus, e, f)); - } - - return ret; -} - -static int remove_signing_key_one(sd_bus *bus, const char *fn) { - int r; - - assert_se(bus); - assert_se(fn); - - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - r = bus_call_method(bus, bus_mgr, "RemoveSigningKey", &error, /* ret_reply= */ NULL, "st", fn, UINT64_C(0)); - if (r < 0) - return log_error_errno(r, "Failed to remove signing key '%s': %s", fn, bus_error_message(&error, r)); - - return 0; -} - -static int verb_remove_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; - - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - r = acquire_bus(&bus); - if (r < 0) - return r; - - r = EXIT_SUCCESS; - STRV_FOREACH(k, strv_skip(argv, 1)) - RET_GATHER(r, remove_signing_key_one(bus, *k)); - - return r; -} - static int run(int argc, char *argv[]) { - static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, verb_help }, - { "list", VERB_ANY, 1, VERB_DEFAULT, verb_list_homes }, - { "activate", 2, VERB_ANY, 0, verb_activate_home }, - { "deactivate", 2, VERB_ANY, 0, verb_deactivate_home }, - { "inspect", VERB_ANY, VERB_ANY, 0, verb_inspect_homes }, - { "authenticate", VERB_ANY, VERB_ANY, 0, verb_authenticate_homes }, - { "create", VERB_ANY, 2, 0, verb_create_home }, - { "adopt", VERB_ANY, VERB_ANY, 0, verb_adopt_home }, - { "register", VERB_ANY, VERB_ANY, 0, verb_register_home }, - { "unregister", 2, VERB_ANY, 0, verb_unregister_home }, - { "remove", 2, VERB_ANY, 0, verb_remove_home }, - { "update", VERB_ANY, 2, 0, verb_update_home }, - { "passwd", VERB_ANY, 2, 0, verb_passwd_home }, - { "resize", 2, 3, 0, verb_resize_home }, - { "lock", 2, VERB_ANY, 0, verb_lock_home }, - { "unlock", 2, VERB_ANY, 0, verb_unlock_home }, - { "with", 2, VERB_ANY, 0, verb_with_home }, - { "lock-all", VERB_ANY, 1, 0, verb_lock_all_homes }, - { "deactivate-all", VERB_ANY, 1, 0, verb_deactivate_all_homes }, - { "rebalance", VERB_ANY, 1, 0, verb_rebalance }, - { "firstboot", VERB_ANY, 1, 0, verb_firstboot }, - { "list-signing-keys", VERB_ANY, 1, 0, verb_list_signing_keys }, - { "get-signing-key", VERB_ANY, VERB_ANY, 0, verb_get_signing_key }, - { "add-signing-key", VERB_ANY, VERB_ANY, 0, verb_add_signing_key }, - { "remove-signing-key", 2, VERB_ANY, 0, verb_remove_signing_key }, - {} - }; - + char **args = NULL; int r; log_setup(); @@ -5359,11 +5161,11 @@ static int run(int argc, char *argv[]) { if (is_fallback_shell(argv[0])) return fallback_shell(argc, argv); - r = parse_argv(argc, argv); + r = parse_argv(argc, argv, &args); if (r <= 0) return r; - return dispatch_verb(argc, argv, verbs, NULL); + return dispatch_verb_with_args(args, /* userdata= */ NULL); } DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/shared/options.c b/src/shared/options.c index ac36fe0765..1a24106fc5 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -510,7 +510,8 @@ int _option_parser_get_help_table_full( /* We indent the option string by two spaces. We could set the minimum cell width and * right-align for a similar result, but that'd be more work. This is only used for * display. */ - const char *prefix = opt->short_code != 0 ? " " : " "; + bool shift_left = opt->short_code != 0 || FLAGS_SET(opt->flags, OPTION_HELP_ENTRY_VERBATIM); + const char *prefix = shift_left ? " " : " "; _cleanup_free_ char *t = strjoin(prefix, s); if (!t) return log_oom();