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;
}
}
-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;
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;
+static int acquire_updated_home_record(
+ sd_bus *bus,
+ const char *username,
+ UserRecord **ret) {
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- r = acquire_bus(&bus);
- if (r < 0)
- return r;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ int r;
- (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+ assert(ret);
- 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);
+ if (arg_identity) {
+ unsigned line = 0, column = 0;
+ sd_json_variant *un;
- r = sd_bus_message_append(m, "st", *i, UINT64_C(0));
+ r = sd_json_parse_file(
+ streq(arg_identity, "-") ? stdin : NULL,
+ streq(arg_identity, "-") ? "<stdin>" : arg_identity,
+ SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE,
+ &json,
+ &line,
+ &column);
if (r < 0)
- return bus_log_create_error(r);
+ return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
- _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;
+ un = sd_json_variant_by_key(json, "userName");
+ if (un) {
+ if (!sd_json_variant_is_string(un) || (username && !streq(sd_json_variant_string(un), username)))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match.");
+ } else {
+ if (!username)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified.");
+
+ r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set userName field: %m");
}
- }
- return ret;
-}
+ } else {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ int incomplete;
+ const char *text;
-static int register_home_common(sd_bus *bus, sd_json_variant *v) {
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *_bus = NULL;
- int r;
+ if (!identity_properties_specified())
+ return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified.");
- assert(v);
+ r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", username);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r));
- if (!bus) {
- r = acquire_bus(&_bus);
+ r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL);
if (r < 0)
- return r;
- bus = _bus;
- }
+ return bus_log_parse_error(r);
- _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);
+ if (incomplete)
+ return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record.");
- _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_json_parse(
+ text,
+ SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE,
+ &json,
+ /* reterr_line= */ NULL,
+ /* reterr_column= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON identity: %m");
- r = sd_bus_message_append(m, "s", formatted);
- if (r < 0)
- return bus_log_create_error(r);
+ reply = sd_bus_message_unref(reply);
- _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));
+ r = sd_json_variant_filter(&json, STRV_MAKE("binding", "status", "signature", "blobManifest"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
+ }
- return 0;
-}
+ r = apply_identity_changes(&json);
+ if (r < 0)
+ return r;
-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, "<stdio>");
-
- 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,
- UserRecord **ret) {
-
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
- _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
- int r;
-
- assert(ret);
-
- if (arg_identity) {
- unsigned line = 0, column = 0;
- sd_json_variant *un;
-
- r = sd_json_parse_file(
- streq(arg_identity, "-") ? stdin : NULL,
- streq(arg_identity, "-") ? "<stdin>" : arg_identity,
- SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE,
- &json,
- &line,
- &column);
- if (r < 0)
- return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
-
- un = sd_json_variant_by_key(json, "userName");
- if (un) {
- if (!sd_json_variant_is_string(un) || (username && !streq(sd_json_variant_string(un), username)))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match.");
- } else {
- if (!username)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified.");
-
- r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username);
- if (r < 0)
- return log_error_errno(r, "Failed to set userName field: %m");
- }
-
- } else {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
- int incomplete;
- const char *text;
-
- if (!identity_properties_specified())
- return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified.");
-
- r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", username);
- if (r < 0)
- return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r));
-
- r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL);
- if (r < 0)
- return bus_log_parse_error(r);
-
- if (incomplete)
- return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record.");
-
- r = sd_json_parse(
- text,
- SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE,
- &json,
- /* reterr_line= */ NULL,
- /* reterr_column= */ NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to parse JSON identity: %m");
-
- reply = sd_bus_message_unref(reply);
-
- r = sd_json_variant_filter(&json, STRV_MAKE("binding", "status", "signature", "blobManifest"));
- if (r < 0)
- return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
- }
-
- r = apply_identity_changes(&json);
- if (r < 0)
- return r;
-
- STRV_FOREACH(i, arg_pkcs11_token_uri) {
- r = identity_add_pkcs11_key_data(&json, *i);
- if (r < 0)
- return r;
- }
+ STRV_FOREACH(i, arg_pkcs11_token_uri) {
+ r = identity_add_pkcs11_key_data(&json, *i);
+ if (r < 0)
+ return r;
+ }
STRV_FOREACH(i, arg_fido2_device) {
r = identity_add_fido2_parameters(&json, *i, arg_fido2_lock_with, arg_fido2_cred_alg);
return 0;
}
-static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+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;
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);
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;
}
return ret;
}
-static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+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;
_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);
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;
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 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;
+}
+
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;
return ret;
}
-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;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+static int authenticate_home(sd_bus *bus, const char *name) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
int r;
- r = acquire_bus(&bus);
+ r = acquire_passed_secrets(name, &secret);
if (r < 0)
return r;
- r = bus_message_new_method_call(bus, &m, bus_mgr, "LockAllHomes");
- if (r < 0)
- return bus_log_create_error(r);
+ 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 = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to lock all homes: %s", bus_error_message(&error, r));
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "AuthenticateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
- return 0;
-}
+ r = sd_bus_message_append(m, "s", name);
+ if (r < 0)
+ return bus_log_create_error(r);
-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 = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return bus_log_create_error(r);
- r = acquire_bus(&bus);
- if (r < 0)
+ 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;
-
- r = bus_message_new_method_call(bus, &m, bus_mgr, "DeactivateAllHomes");
- if (r < 0)
- return bus_log_create_error(r);
-
- r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to deactivate all homes: %s", bus_error_message(&error, r));
-
- return 0;
+ }
}
-static int 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;
+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;
if (r < 0)
return r;
- r = bus_message_new_method_call(bus, &m, bus_mgr, "Rebalance");
- if (r < 0)
- return bus_log_create_error(r);
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
- r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_REBALANCE_NOT_NEEDED))
- log_info("No homes needed rebalancing.");
- else
- return log_error_errno(r, "Failed to rebalance: %s", bus_error_message(&error, r));
- } else
- log_info("Completed rebalancing.");
+ char **args = strv_skip(argv, 1);
+ if (args) {
+ STRV_FOREACH(arg, args)
+ RET_GATHER(r, authenticate_home(bus, *arg));
- return 0;
-}
+ return r;
+ } else {
+ _cleanup_free_ char *myself = getusername_malloc();
+ if (!myself)
+ return log_oom();
-static int create_or_register_from_credentials(void) {
- int r;
+ return authenticate_home(bus, myself);
+ }
+}
- _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");
+static int verb_adopt_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+ int r, ret = 0;
- _cleanup_free_ DirectoryEntries *des = NULL;
- r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ r = acquire_bus(&bus);
if (r < 0)
- return log_error_errno(r, "Failed to enumerate credentials: %m");
+ return r;
- int ret = 0, n_processed = 0;
- FOREACH_ARRAY(i, des->entries, des->n_entries) {
- struct dirent *de = *i;
- if (de->d_type != DT_REG)
- continue;
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
- enum {
- OPERATION_CREATE,
- OPERATION_REGISTER,
- } op;
- const char *e;
- if ((e = startswith(de->d_name, "home.create.")))
- op = OPERATION_CREATE;
- else if ((e = startswith(de->d_name, "home.register.")))
- op = OPERATION_REGISTER;
- else
- continue;
+ 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);
- if (!valid_user_group_name(e, /* flags= */ 0)) {
- log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name);
- continue;
- }
+ r = sd_bus_message_append(m, "st", *i, UINT64_C(0));
+ if (r < 0)
+ return bus_log_create_error(r);
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *identity = NULL;
- unsigned line = 0, column = 0;
- r = sd_json_parse_file_at(
- /* f= */ NULL,
- fd,
- de->d_name,
- /* flags= */ SD_JSON_PARSE_MUST_BE_OBJECT,
- &identity,
- &line,
- &column);
+ _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_warning_errno(r, "[%s:%u:%u] Failed to parse user record in credential, ignoring: %m", de->d_name, line, column);
- continue;
+ log_error_errno(r, "Failed to adopt home: %s", bus_error_message(&error, r));
+ if (ret == 0)
+ ret = r;
}
+ }
- sd_json_variant *un = sd_json_variant_by_key(identity, "userName");
- if (un) {
- if (!sd_json_variant_is_string(un)) {
- log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name);
- continue;
- }
-
- if (!streq(sd_json_variant_string(un), e)) {
- log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, sd_json_variant_string(un), e);
- continue;
- }
- } else {
- r = sd_json_variant_set_field_string(&identity, "userName", e);
- if (r < 0)
- return log_warning_errno(r, "Failed to set userName field: %m");
- }
+ return ret;
+}
- log_notice("Processing user '%s' from credentials.", e);
+static int register_home_common(sd_bus *bus, sd_json_variant *v) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *_bus = NULL;
+ int r;
- if (op == OPERATION_CREATE)
- r = create_home_common(identity, /* show_enforce_password_policy_hint= */ false);
- else
- r = register_home_common(/* bus= */ NULL, identity);
- if (r >= 0)
- n_processed++;
+ assert(v);
- RET_GATHER(ret, r);
+ if (!bus) {
+ r = acquire_bus(&_bus);
+ if (r < 0)
+ return r;
+ bus = _bus;
}
- return ret < 0 ? ret : n_processed;
-}
-
-static int has_regular_user(void) {
- _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
- UserDBMatch match = USERDB_MATCH_NULL;
- int r;
+ _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);
- match.disposition_mask = INDEX_TO_MASK(uint64_t, USER_REGULAR);
+ _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 = userdb_all(&match, USERDB_SUPPRESS_SHADOW, &iterator);
+ r = sd_bus_message_append(m, "s", formatted);
if (r < 0)
- return log_error_errno(r, "Failed to create user enumerator: %m");
+ return bus_log_create_error(r);
- r = userdb_iterator_get(iterator, &match, /* ret= */ NULL);
- if (r == -ESRCH)
- return false;
+ _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 enumerate users: %m");
+ return log_error_errno(r, "Failed to register home: %s", bus_error_message(&error, r));
- return true;
+ return 0;
}
-static int username_is_ok(const char *name, void *userdata) {
+static int register_home_one(sd_bus *bus, FILE *f, const char *path) {
int r;
- assert(name);
-
- if (!valid_user_group_name(name, /* flags= */ 0)) {
- log_notice("Specified user name is not a valid UNIX user name, try again: %s", name);
- return false;
- }
+ assert(bus);
+ assert(path);
- r = userdb_by_name(name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL);
- if (r == -ESRCH)
- return true;
+ _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, "Failed to check if specified user '%s' already exists: %m", name);
+ return log_error_errno(r, "[%s:%u:%u] Failed to parse user record: %m", path, line, column);
- log_notice("Specified user '%s' exists already, try again.", name);
- return false;
+ return register_home_common(bus, v);
}
-static int create_interactively(void) {
- _cleanup_free_ char *username = NULL;
+static int verb_register_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
int r;
- if (!arg_prompt_new_user) {
- log_debug("Prompting for user creation was not requested.");
- return 0;
- }
-
- /* Needs to be called before mute_console or it will garble the screen */
- (void) plymouth_hide_splash();
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
- _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL;
- (void) mute_console(&mute_console_link);
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
- (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0);
+ if (arg_identity) {
+ if (argc > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not accepting an arguments if --identity= is specified, refusing.");
- if (arg_chrome)
- chrome_show("Create a User Account", /* bottom= */ NULL);
+ return register_home_one(bus, /* f= */ NULL, arg_identity);
+ }
- DEFER_VOID_CALL(chrome_hide);
+ if (argc == 1 || (argc == 2 && streq(argv[1], "-")))
+ return register_home_one(bus, /* f= */ stdin, "<stdio>");
- if (emoji_enabled()) {
- fputs(glyph(GLYPH_HOME), stdout);
- putchar(' ');
+ 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));
}
- printf("Please create your user account!\n\n");
- r = prompt_loop("Please enter user name to create",
- GLYPH_IDCARD,
- /* menu= */ NULL,
- /* accepted= */ NULL,
- /* ellipsize_percentage= */ 60,
- /* n_columns= */ 3,
- /* column_width= */ 20,
- username_is_ok,
- /* refresh= */ NULL,
- /* userdata= */ NULL,
- PROMPT_MAY_SKIP|PROMPT_SILENT_VALIDATE,
- &username);
+ 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;
- if (isempty(username))
- return 0;
- r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username);
+ (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_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 log_error_errno(r, "Failed to set userName field: %m");
+ return r;
- /* Let's not insist on a strong password in the firstboot interactive interface. Insisting on this is
- * really annoying, as the user cannot just invoke the tool again with "--enforce-password-policy=no"
- * because after all the tool is called from the boot process, and not from an interactive
- * shell. Moreover, when setting up an initial system we can assume the user owns it, and hence we
- * don't need to hard enforce some policy on password strength some organization or OS vendor
- * requires. Note that this just disables the *strict* enforcement of the password policy. Even with
- * this disabled we'll still tell the user in the UI that the password is too weak and suggest better
- * ones, even if we then accept the weak ones if the user insists, by repeating it. */
- r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false);
+ _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 set enforcePasswordPolicy field: %m");
+ return log_error_errno(r, "Failed to list signing keys: %s", bus_error_message(&error, r));
- if (arg_prompt_groups) {
- _cleanup_strv_free_ char **groups = NULL;
+ _cleanup_(table_unrefp) Table *table = table_new("name", "key");
+ if (!table)
+ return log_oom();
- putchar('\n');
+ r = sd_bus_message_enter_container(reply, 'a', "(sst)");
+ if (r < 0)
+ return bus_log_parse_error(r);
- r = prompt_groups(username, &groups);
+ for (;;) {
+ const char *name, *pem;
+
+ r = sd_bus_message_read(reply, "(sst)", &name, &pem, NULL);
if (r < 0)
- return r;
+ return bus_log_parse_error(r);
+ if (r == 0)
+ break;
- if (!strv_isempty(groups)) {
- r = sd_json_variant_set_field_strv(&arg_identity_extra, "memberOf", groups);
+ _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 log_error_errno(r, "Failed to set memberOf field: %m");
+ 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);
}
- if (arg_prompt_shell) {
- _cleanup_free_ char *shell = NULL;
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
- putchar('\n');
+ 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 = prompt_shell(username, &shell);
+ r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
if (r < 0)
return r;
-
- if (!isempty(shell)) {
- log_info("Selected %s as the shell for user %s", shell, username);
-
- r = sd_json_variant_set_field_string(&arg_identity_extra, "shell", shell);
- if (r < 0)
- return log_error_errno(r, "Failed to set shell field: %m");
- }
}
- putchar('\n');
-
- r = create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ false);
- 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);
+ }
- log_info("Successfully created account '%s'.", username);
return 0;
}
-static int add_signing_keys_from_credentials(void);
-
-static int verb_firstboot(int argc, char *argv[], uintptr_t _data, void *userdata) {
+static int verb_get_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
int r;
- /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
- * tool. */
-
- bool enabled;
- r = proc_cmdline_get_bool("systemd.firstboot", /* flags= */ 0, &enabled);
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ r = acquire_bus(&bus);
if (r < 0)
- return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
- if (r > 0 && !enabled) {
- log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
- arg_prompt_new_user = false;
- }
+ 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;
+ }
- RET_GATHER(ret, add_signing_keys_from_credentials());
-
- r = create_or_register_from_credentials();
- RET_GATHER(ret, r);
- bool existing_users = r > 0;
-
- r = getenv_bool("SYSTEMD_HOME_FIRSTBOOT_OVERRIDE");
- if (r == 0)
- return 0;
- if (r < 0) {
- if (r != -ENXIO)
- log_warning_errno(r, "Failed to parse $SYSTEMD_HOME_FIRSTBOOT_OVERRIDE, ignoring: %m");
+ const char *pem;
+ r = sd_bus_message_read(reply, "st", &pem, NULL);
+ if (r < 0) {
+ RET_GATHER(ret, bus_log_parse_error(r));
+ continue;
+ }
- if (!existing_users) {
- r = has_regular_user();
- if (r < 0)
- return r;
+ fputs(pem, stdout);
+ if (!endswith(pem, "\n"))
+ fputc('\n', stdout);
- existing_users = r > 0;
- }
- if (existing_users) {
- log_info("Regular user already present in user database, skipping interactive user creation.");
- return 0;
- }
+ fflush(stdout);
}
- RET_GATHER(ret, create_interactively());
return ret;
}
-#define drop_from_identity(...) _drop_from_identity(STRV_MAKE(__VA_ARGS__))
-
-static int _drop_from_identity(char **fields) {
+static int add_signing_key_one(sd_bus *bus, const char *fn, FILE *key) {
int r;
- /* If we are called to update an identity record and drop some field, let's keep track of what to
- * remove from the old record */
- r = strv_extend_strv(&arg_identity_filter, fields, /* filter_duplicates= */ true);
- if (r < 0)
- return log_oom();
-
- /* Let's also drop the field if it was previously set to a new value on the same command line */
- r = sd_json_variant_filter(&arg_identity_extra, fields);
- if (r < 0)
- return log_error_errno(r, "Failed to filter JSON identity data: %m");
+ assert_se(bus);
+ assert_se(fn);
+ assert_se(key);
- r = sd_json_variant_filter(&arg_identity_extra_this_machine, fields);
+ _cleanup_free_ char *pem = NULL;
+ r = read_full_stream(key, &pem, /* ret_size= */ NULL);
if (r < 0)
- return log_error_errno(r, "Failed to filter JSON identity data: %m");
+ return log_error_errno(r, "Failed to read key '%s': %m", fn);
- r = sd_json_variant_filter(&arg_identity_extra_privileged, fields);
+ _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 filter JSON identity data: %m");
+ return log_error_errno(r, "Failed to add signing key '%s': %s", fn, bus_error_message(&error, r));
return 0;
}
-static int parse_ssh_authorized_keys(sd_json_variant **identity, const char *field, const char *arg) {
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
- _cleanup_strv_free_ char **l = NULL, **add = NULL;
+static int verb_add_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
int r;
- assert(identity);
- assert(field);
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
- if (isempty(arg))
- return drop_from_identity(field);
+ 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.");
- if (arg[0] == '@') {
- /* If prefixed with '@', read from a file */
+ 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.");
- _cleanup_fclose_ FILE *f = fopen(arg + 1, "re");
- if (!f)
- return log_error_errno(errno, "Failed to open '%s': %m", arg + 1);
+ STRV_FOREACH(k, strv_skip(argv, 1)) {
- for (;;) {
- _cleanup_free_ char *line = NULL;
+ if (streq(*k, "-"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing to read from standard input if multiple keys are specified.");
- r = read_line(f, LONG_LINE_MAX, &line);
- if (r < 0)
- return log_error_errno(r, "Failed to read from '%s': %m", arg + 1);
- if (r == 0)
- break;
+ _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;
+ }
+ }
- if (isempty(line) || line[0] == '#')
+ _cleanup_fclose_ FILE *f = fopen(*k, "re");
+ if (!f) {
+ RET_GATHER(ret, log_error_errno(errno, "Failed to open '%s': %m", *k));
continue;
+ }
- r = strv_consume(&add, TAKE_PTR(line));
- if (r < 0)
- return log_oom();
+ RET_GATHER(ret, add_signing_key_one(bus, fn ?: arg_key_name, f));
}
- } else {
- /* Otherwise, assume it's a literal key. Let's do some superficial checks
- * before accepting it though. */
+ }
- if (string_has_cc(arg, NULL))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Authorized key contains control characters, refusing.");
- if (arg[0] == '#')
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?");
+ return ret;
+}
- add = strv_new(arg);
- if (!add)
- return log_oom();
- }
+static int add_signing_keys_from_credentials(void) {
+ int r;
- v = sd_json_variant_ref(sd_json_variant_by_key(*identity, field));
- if (v) {
- r = sd_json_variant_strv(v, &l);
- if (r < 0)
- return log_error_errno(r, "Failed to parse %s list: %m", field);
- }
+ _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");
- r = strv_extend_strv_consume(&l, TAKE_PTR(add), /* filter_duplicates= */ true);
+ _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_oom();
+ return log_error_errno(r, "Failed to enumerate credentials: %m");
- v = sd_json_variant_unref(v);
+ 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;
- r = sd_json_variant_new_array_strv(&v, l);
- if (r < 0)
- return log_oom();
+ const char *e = startswith(de->d_name, "home.add-signing-key.");
+ if (!e)
+ continue;
- r = sd_json_variant_set_field(identity, field, v);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
+ if (!filename_is_valid(e))
+ continue;
- return 0;
+ 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 parse_string_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int remove_signing_key_one(sd_bus *bus, const char *fn) {
int r;
- assert(identity);
- assert(field);
-
- if (isempty(arg))
- return drop_from_identity(field);
+ assert_se(bus);
+ assert_se(fn);
- r = sd_json_variant_set_field_string(identity, field, arg);
+ _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 set %s field: %m", field);
+ return log_error_errno(r, "Failed to remove signing key '%s': %s", fn, bus_error_message(&error, r));
+
return 0;
}
-static int parse_home_directory_field(sd_json_variant **identity, const char *field, const char *arg) {
- _cleanup_free_ char *hd = NULL;
+static int verb_remove_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
int r;
- assert(identity);
- assert(field);
-
- if (!isempty(arg)) {
- r = parse_path_argument(arg, /* suppress_root= */ false, &hd);
- if (r < 0)
- return r;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
- if (!valid_home(hd))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd);
- }
+ r = EXIT_SUCCESS;
+ STRV_FOREACH(k, strv_skip(argv, 1))
+ RET_GATHER(r, remove_signing_key_one(bus, *k));
- return parse_string_field(identity, field, hd);
+ return r;
}
-static int parse_realm_field(sd_json_variant **identity, const char *field, const char *arg) {
- int r;
+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;
- assert(identity);
- assert(field);
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
- if (!isempty(arg)) {
- r = dns_name_is_valid(arg);
+ 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 log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", arg);
- if (r == 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain.", arg);
+ 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 parse_string_field(identity, field, arg);
+ return ret;
}
-static int parse_path_field(sd_json_variant **identity, const char *field, const char *arg) {
- _cleanup_free_ char *v = NULL;
- int r;
+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;
- assert(identity);
- assert(field);
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
- if (!isempty(arg)) {
- r = parse_path_argument(arg, /* suppress_root= */ false, &v);
+ 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;
- }
- return parse_string_field(identity, field, v);
-}
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-static int parse_filename_field(sd_json_variant **identity, const char *field, const char *arg) {
- assert(identity);
- assert(field);
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "UnlockHome");
+ if (r < 0)
+ return bus_log_create_error(r);
- if (!isempty(arg) && !filename_is_valid(arg))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Parameter for %s field not a valid filename: %s", field, arg);
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
- return parse_string_field(identity, field, arg);
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ r = handle_generic_user_record_error(argv[1], secret, &error, r, false);
+ if (r < 0) {
+ if (ret == 0)
+ ret = r;
+
+ break;
+ }
+ } else
+ break;
+ }
+ }
+
+ return ret;
}
-static int parse_unsigned_field(sd_json_variant **identity, const char *field, const char *arg) {
+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;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
int r;
- assert(identity);
- assert(field);
-
- if (isempty(arg))
- return drop_from_identity(field);
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
- unsigned n;
- r = safe_atou(arg, &n);
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "LockAllHomes");
if (r < 0)
- return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+ return bus_log_create_error(r);
- r = sd_json_variant_set_field_unsigned(identity, field, n);
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
+ return log_error_errno(r, "Failed to lock all homes: %s", bus_error_message(&error, r));
+
return 0;
}
-static int parse_u64_field(sd_json_variant **identity, const char *field, const char *arg) {
+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;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
int r;
- assert(identity);
- assert(field);
-
- if (isempty(arg))
- return drop_from_identity(field);
-
- uint64_t n;
- r = safe_atou64(arg, &n);
+ r = acquire_bus(&bus);
if (r < 0)
- return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+ return r;
- r = sd_json_variant_set_field_unsigned(identity, field, n);
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "Rebalance");
if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, BUS_ERROR_REBALANCE_NOT_NEEDED))
+ log_info("No homes needed rebalancing.");
+ else
+ return log_error_errno(r, "Failed to rebalance: %s", bus_error_message(&error, r));
+ } else
+ log_info("Completed rebalancing.");
+
return 0;
}
-static int parse_size_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int create_or_register_from_credentials(void) {
int r;
- assert(identity);
- assert(field);
-
- if (isempty(arg))
- return drop_from_identity(field);
-
- uint64_t n;
- r = parse_size(arg, 1024, &n);
- if (r < 0)
- return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+ _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");
- r = sd_json_variant_set_field_unsigned(identity, field, n);
+ _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 set %s field: %m", field);
- return 0;
-}
+ return log_error_errno(r, "Failed to enumerate credentials: %m");
-static int parse_boolean_field(sd_json_variant **identity, const char *field, const char *arg) {
- int r;
+ int ret = 0, n_processed = 0;
+ FOREACH_ARRAY(i, des->entries, des->n_entries) {
+ struct dirent *de = *i;
+ if (de->d_type != DT_REG)
+ continue;
- assert(identity);
- assert(field);
+ enum {
+ OPERATION_CREATE,
+ OPERATION_REGISTER,
+ } op;
+ const char *e;
+ if ((e = startswith(de->d_name, "home.create.")))
+ op = OPERATION_CREATE;
+ else if ((e = startswith(de->d_name, "home.register.")))
+ op = OPERATION_REGISTER;
+ else
+ continue;
- if (isempty(arg))
- return drop_from_identity(field);
+ if (!valid_user_group_name(e, /* flags= */ 0)) {
+ log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name);
+ continue;
+ }
- r = parse_boolean(arg);
- if (r < 0)
- return log_error_errno(r, "Failed to parse boolean parameter %s: %s", field, arg);
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *identity = NULL;
+ unsigned line = 0, column = 0;
+ r = sd_json_parse_file_at(
+ /* f= */ NULL,
+ fd,
+ de->d_name,
+ /* flags= */ SD_JSON_PARSE_MUST_BE_OBJECT,
+ &identity,
+ &line,
+ &column);
+ if (r < 0) {
+ log_warning_errno(r, "[%s:%u:%u] Failed to parse user record in credential, ignoring: %m", de->d_name, line, column);
+ continue;
+ }
- r = sd_json_variant_set_field_boolean(identity, field, r > 0);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
- return 0;
-}
+ sd_json_variant *un = sd_json_variant_by_key(identity, "userName");
+ if (un) {
+ if (!sd_json_variant_is_string(un)) {
+ log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name);
+ continue;
+ }
-static int parse_mode_field(sd_json_variant **identity, const char *field, const char *arg) {
- int r;
+ if (!streq(sd_json_variant_string(un), e)) {
+ log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, sd_json_variant_string(un), e);
+ continue;
+ }
+ } else {
+ r = sd_json_variant_set_field_string(&identity, "userName", e);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to set userName field: %m");
+ }
- assert(identity);
- assert(field);
+ log_notice("Processing user '%s' from credentials.", e);
- if (isempty(arg))
- return drop_from_identity(field);
+ if (op == OPERATION_CREATE)
+ r = create_home_common(identity, /* show_enforce_password_policy_hint= */ false);
+ else
+ r = register_home_common(/* bus= */ NULL, identity);
+ if (r >= 0)
+ n_processed++;
- mode_t mode;
- r = parse_mode(arg, &mode);
- if (r < 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", arg);
+ RET_GATHER(ret, r);
+ }
- r = sd_json_variant_set_field_unsigned(identity, field, mode);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
- return 0;
+ return ret < 0 ? ret : n_processed;
}
-static int parse_timestamp_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int has_regular_user(void) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ UserDBMatch match = USERDB_MATCH_NULL;
int r;
- assert(identity);
- assert(field);
-
- if (isempty(arg))
- return drop_from_identity(field);
+ match.disposition_mask = INDEX_TO_MASK(uint64_t, USER_REGULAR);
- usec_t n;
- r = parse_timestamp(arg, &n);
+ r = userdb_all(&match, USERDB_SUPPRESS_SHADOW, &iterator);
if (r < 0)
- return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+ return log_error_errno(r, "Failed to create user enumerator: %m");
- r = sd_json_variant_set_field_unsigned(identity, field, n);
+ r = userdb_iterator_get(iterator, &match, /* ret= */ NULL);
+ if (r == -ESRCH)
+ return false;
if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
- return 0;
+ return log_error_errno(r, "Failed to enumerate users: %m");
+
+ return true;
}
-static int parse_time_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int username_is_ok(const char *name, void *userdata) {
int r;
- assert(identity);
- assert(field);
+ assert(name);
- if (isempty(arg))
- return drop_from_identity(field);
+ if (!valid_user_group_name(name, /* flags= */ 0)) {
+ log_notice("Specified user name is not a valid UNIX user name, try again: %s", name);
+ return false;
+ }
- usec_t n;
- r = parse_sec(arg, &n);
+ r = userdb_by_name(name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL);
+ if (r == -ESRCH)
+ return true;
if (r < 0)
- return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+ return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", name);
- r = sd_json_variant_set_field_unsigned(identity, field, n);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
- return 0;
+ log_notice("Specified user '%s' exists already, try again.", name);
+ return false;
}
-static int parse_uid_field(sd_json_variant **identity, const char *field, const char *arg) {
- uid_t uid;
+static int create_interactively(void) {
+ _cleanup_free_ char *username = NULL;
int r;
- assert(identity);
- assert(field);
-
- if (isempty(arg))
- return drop_from_identity(field);
+ if (!arg_prompt_new_user) {
+ log_debug("Prompting for user creation was not requested.");
+ return 0;
+ }
- r = parse_uid(arg, &uid);
- if (r < 0)
- return log_error_errno(r, "Failed to parse UID '%s'.", arg);
+ /* Needs to be called before mute_console or it will garble the screen */
+ (void) plymouth_hide_splash();
- const char *bad_range =
- uid_is_system(uid) ? "in system range" :
- uid_is_greeter(uid) ? "in greeter range" :
- uid_is_dynamic(uid) ? "in dynamic ragne" :
- uid == UID_NOBODY ? "nobody UID" : NULL;
- if (bad_range)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID "UID_FMT" is %s, refusing.", uid, bad_range);
+ _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL;
+ (void) mute_console(&mute_console_link);
- r = sd_json_variant_set_field_unsigned(identity, field, uid);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
- return 0;
-}
+ (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0);
-static int parse_nice_field(sd_json_variant **identity, const char *field, const char *arg) {
- int nc, r;
+ if (arg_chrome)
+ chrome_show("Create a User Account", /* bottom= */ NULL);
- assert(identity);
- assert(field);
+ DEFER_VOID_CALL(chrome_hide);
- if (isempty(arg))
- return drop_from_identity(field);
+ if (emoji_enabled()) {
+ fputs(glyph(GLYPH_HOME), stdout);
+ putchar(' ');
+ }
+ printf("Please create your user account!\n\n");
- r = parse_nice(arg, &nc);
+ r = prompt_loop("Please enter user name to create",
+ GLYPH_IDCARD,
+ /* menu= */ NULL,
+ /* accepted= */ NULL,
+ /* ellipsize_percentage= */ 60,
+ /* n_columns= */ 3,
+ /* column_width= */ 20,
+ username_is_ok,
+ /* refresh= */ NULL,
+ /* userdata= */ NULL,
+ PROMPT_MAY_SKIP|PROMPT_SILENT_VALIDATE,
+ &username);
if (r < 0)
- return log_error_errno(r, "Failed to parse nice level '%s': %m", arg);
+ return r;
+ if (isempty(username))
+ return 0;
- r = sd_json_variant_set_field_integer(identity, field, nc);
+ r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username);
if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
- return 0;
-}
+ return log_error_errno(r, "Failed to set userName field: %m");
-static int parse_auto_resize_mode_field(sd_json_variant **identity, const char *field, const char *arg) {
- int r;
+ /* Let's not insist on a strong password in the firstboot interactive interface. Insisting on this is
+ * really annoying, as the user cannot just invoke the tool again with "--enforce-password-policy=no"
+ * because after all the tool is called from the boot process, and not from an interactive
+ * shell. Moreover, when setting up an initial system we can assume the user owns it, and hence we
+ * don't need to hard enforce some policy on password strength some organization or OS vendor
+ * requires. Note that this just disables the *strict* enforcement of the password policy. Even with
+ * this disabled we'll still tell the user in the UI that the password is too weak and suggest better
+ * ones, even if we then accept the weak ones if the user insists, by repeating it. */
+ r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m");
- assert(identity);
- assert(field);
+ if (arg_prompt_groups) {
+ _cleanup_strv_free_ char **groups = NULL;
- if (!isempty(arg)) {
- r = auto_resize_mode_from_string(arg);
- if (r < 0)
- return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
- arg = auto_resize_mode_to_string(r);
- }
+ putchar('\n');
- return parse_string_field(identity, field, arg);
-}
+ r = prompt_groups(username, &groups);
+ if (r < 0)
+ return r;
-static int parse_rebalance_weight(sd_json_variant **identity, const char *field, const char *arg) {
- int r;
+ if (!strv_isempty(groups)) {
+ r = sd_json_variant_set_field_strv(&arg_identity_extra, "memberOf", groups);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set memberOf field: %m");
+ }
+ }
- assert(identity);
- assert(field);
+ if (arg_prompt_shell) {
+ _cleanup_free_ char *shell = NULL;
- if (isempty(arg))
- return drop_from_identity(field);
+ putchar('\n');
- uint64_t u;
- if (streq(arg, "off"))
- u = REBALANCE_WEIGHT_OFF;
- else {
- r = safe_atou64(arg, &u);
+ r = prompt_shell(username, &shell);
if (r < 0)
- return log_error_errno(r, "Failed to parse rebalance weight parameter: %s", arg);
+ return r;
- if (u < REBALANCE_WEIGHT_MIN || u > REBALANCE_WEIGHT_MAX)
- return log_error_errno(SYNTHETIC_ERRNO(ERANGE),
- "Rebalancing weight out of valid range %" PRIu64 "%s%" PRIu64 ": %s",
- REBALANCE_WEIGHT_MIN, glyph(GLYPH_ELLIPSIS), REBALANCE_WEIGHT_MAX,
- arg);
+ if (!isempty(shell)) {
+ log_info("Selected %s as the shell for user %s", shell, username);
+
+ r = sd_json_variant_set_field_string(&arg_identity_extra, "shell", shell);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set shell field: %m");
+ }
}
- /* Drop from per machine stuff and everywhere */
- r = drop_from_identity(field);
+ putchar('\n');
+
+ r = create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ false);
if (r < 0)
return r;
- r = sd_json_variant_set_field_unsigned(identity, field, u);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
+ log_info("Successfully created account '%s'.", username);
return 0;
}
-static int parse_rlimit_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int verb_firstboot(int argc, char *argv[], uintptr_t _data, void *userdata) {
int r;
- assert(identity);
- assert(field);
-
- if (isempty(arg)) {
- /* Remove all resource limits */
-
- r = drop_from_identity(field);
- if (r < 0)
- return r;
+ /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
+ * tool. */
- arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits);
- *identity = sd_json_variant_unref(*identity);
- return 0;
+ bool enabled;
+ r = proc_cmdline_get_bool("systemd.firstboot", /* flags= */ 0, &enabled);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
+ if (r > 0 && !enabled) {
+ log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
+ arg_prompt_new_user = false;
}
- const char *eq = strchr(arg, '=');
- if (!eq)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", arg);
-
- _cleanup_free_ char *s = strndup(arg, eq - arg);
- if (!s)
- return log_oom();
+ int ret = 0;
- int limit = rlimit_from_string_harder(s);
- if (limit < 0)
- return log_error_errno(limit, "Unknown resource limit type: %s", s);
+ RET_GATHER(ret, add_signing_keys_from_credentials());
- const char *rlimit_field = strjoina("RLIMIT_", rlimit_to_string(limit));
+ r = create_or_register_from_credentials();
+ RET_GATHER(ret, r);
+ bool existing_users = r > 0;
- if (isempty(eq + 1)) {
- /* Remove only the specific rlimit */
+ r = getenv_bool("SYSTEMD_HOME_FIRSTBOOT_OVERRIDE");
+ if (r == 0)
+ return 0;
+ if (r < 0) {
+ if (r != -ENXIO)
+ log_warning_errno(r, "Failed to parse $SYSTEMD_HOME_FIRSTBOOT_OVERRIDE, ignoring: %m");
- r = strv_extend(&arg_identity_filter_rlimits, rlimit_field);
- if (r < 0)
- return r;
+ if (!existing_users) {
+ r = has_regular_user();
+ if (r < 0)
+ return r;
- r = sd_json_variant_filter(identity, STRV_MAKE(rlimit_field));
- if (r < 0)
- return log_error_errno(r, "Failed to filter JSON identity data: %m");
- return 0;
+ existing_users = r > 0;
+ }
+ if (existing_users) {
+ log_info("Regular user already present in user database, skipping interactive user creation.");
+ return 0;
+ }
}
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *jcur = NULL, *jmax = NULL;
- struct rlimit rl;
+ RET_GATHER(ret, create_interactively());
+ return ret;
+}
- r = rlimit_parse(limit, eq + 1, &rl);
+#define drop_from_identity(...) _drop_from_identity(STRV_MAKE(__VA_ARGS__))
+
+static int _drop_from_identity(char **fields) {
+ int r;
+
+ /* If we are called to update an identity record and drop some field, let's keep track of what to
+ * remove from the old record */
+ r = strv_extend_strv(&arg_identity_filter, fields, /* filter_duplicates= */ true);
if (r < 0)
- return log_error_errno(r, "Failed to parse resource limit value: %s", eq + 1);
+ return log_oom();
- r = rl.rlim_cur == RLIM_INFINITY ? sd_json_variant_new_null(&jcur) : sd_json_variant_new_unsigned(&jcur, rl.rlim_cur);
+ /* Let's also drop the field if it was previously set to a new value on the same command line */
+ r = sd_json_variant_filter(&arg_identity_extra, fields);
if (r < 0)
- return log_error_errno(r, "Failed to allocate json variant: %m");
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
- r = rl.rlim_max == RLIM_INFINITY ? sd_json_variant_new_null(&jmax) : sd_json_variant_new_unsigned(&jmax, rl.rlim_max);
+ r = sd_json_variant_filter(&arg_identity_extra_this_machine, fields);
if (r < 0)
- return log_error_errno(r, "Failed to allocate json variant: %m");
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
- r = sd_json_variant_set_fieldbo(identity, rlimit_field,
- SD_JSON_BUILD_PAIR_VARIANT("cur", jcur),
- SD_JSON_BUILD_PAIR_VARIANT("max", jmax));
+ r = sd_json_variant_filter(&arg_identity_extra_privileged, fields);
if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", rlimit_field);
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
+
return 0;
}
-static int parse_disk_size_field(sd_json_variant **identity, const char *arg) {
+static int parse_ssh_authorized_keys(sd_json_variant **identity, const char *field, const char *arg) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ _cleanup_strv_free_ char **l = NULL, **add = NULL;
int r;
assert(identity);
+ assert(field);
- if (isempty(arg)) {
- r = drop_from_identity("diskSize", "diskSizeRelative", "rebalanceWeight");
- if (r < 0)
- return r;
+ if (isempty(arg))
+ return drop_from_identity(field);
- arg_disk_size = arg_disk_size_relative = UINT64_MAX;
- return 0;
- }
+ if (arg[0] == '@') {
+ /* If prefixed with '@', read from a file */
- r = parse_permyriad(arg);
- if (r < 0) {
- r = parse_disk_size(arg, &arg_disk_size);
- if (r < 0)
- return r;
+ _cleanup_fclose_ FILE *f = fopen(arg + 1, "re");
+ if (!f)
+ return log_error_errno(errno, "Failed to open '%s': %m", arg + 1);
- r = drop_from_identity("diskSizeRelative");
- if (r < 0)
- return r;
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
- r = sd_json_variant_set_field_unsigned(identity, "diskSize", arg_disk_size);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", "diskSize");
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read from '%s': %m", arg + 1);
+ if (r == 0)
+ break;
- arg_disk_size_relative = UINT64_MAX;
+ if (isempty(line) || line[0] == '#')
+ continue;
+
+ r = strv_consume(&add, TAKE_PTR(line));
+ if (r < 0)
+ return log_oom();
+ }
} else {
- /* Normalize to UINT32_MAX == 100% */
- arg_disk_size_relative = UINT32_SCALE_FROM_PERMYRIAD(r);
+ /* Otherwise, assume it's a literal key. Let's do some superficial checks
+ * before accepting it though. */
- r = drop_from_identity("diskSize");
- if (r < 0)
- return r;
+ if (string_has_cc(arg, NULL))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Authorized key contains control characters, refusing.");
+ if (arg[0] == '#')
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?");
- r = sd_json_variant_set_field_unsigned(identity, "diskSizeRelative", arg_disk_size_relative);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", "diskSizeRelative");
+ add = strv_new(arg);
+ if (!add)
+ return log_oom();
+ }
- arg_disk_size = UINT64_MAX;
+ v = sd_json_variant_ref(sd_json_variant_by_key(*identity, field));
+ if (v) {
+ r = sd_json_variant_strv(v, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s list: %m", field);
}
- /* Automatically turn off the rebalance logic if user configured a size explicitly */
- r = sd_json_variant_set_field_unsigned(identity, "rebalanceWeight", REBALANCE_WEIGHT_OFF);
+ r = strv_extend_strv_consume(&l, TAKE_PTR(add), /* filter_duplicates= */ true);
if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", "rebalanceWeight");
- return 0;
-}
+ return log_oom();
-static int parse_sector_size_field(sd_json_variant **identity, const char *field, const char *arg) {
- uint64_t ss;
- int r;
+ v = sd_json_variant_unref(v);
- assert(identity);
- assert(field);
-
- if (isempty(arg))
- return drop_from_identity(field);
-
- r = parse_sector_size(arg, &ss);
+ r = sd_json_variant_new_array_strv(&v, l);
if (r < 0)
- return r;
+ return log_oom();
- r = sd_json_variant_set_field_unsigned(identity, field, ss);
+ r = sd_json_variant_set_field(identity, field, v);
if (r < 0)
return log_error_errno(r, "Failed to set %s field: %m", field);
+
return 0;
}
-static int parse_weight_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int parse_string_field(sd_json_variant **identity, const char *field, const char *arg) {
int r;
assert(identity);
if (isempty(arg))
return drop_from_identity(field);
- uint64_t u;
- r = safe_atou64(arg, &u);
- if (r < 0)
- return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
-
- if (!CGROUP_WEIGHT_IS_OK(u))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Weight %" PRIu64 " is out of valid range for field %s.", u, field);
-
- r = sd_json_variant_set_field_unsigned(identity, field, u);
+ r = sd_json_variant_set_field_string(identity, field, arg);
if (r < 0)
return log_error_errno(r, "Failed to set %s field: %m", field);
return 0;
}
-static int parse_environment_field(sd_json_variant **identity, const char *field, const char *arg) {
- _cleanup_strv_free_ char **l = NULL;
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *ne = NULL;
+static int parse_home_directory_field(sd_json_variant **identity, const char *field, const char *arg) {
+ _cleanup_free_ char *hd = NULL;
int r;
assert(identity);
assert(field);
- if (isempty(arg))
- return drop_from_identity(field);
-
- sd_json_variant *e = sd_json_variant_by_key(*identity, field);
- if (e) {
- r = sd_json_variant_strv(e, &l);
+ if (!isempty(arg)) {
+ r = parse_path_argument(arg, /* suppress_root= */ false, &hd);
if (r < 0)
- return log_error_errno(r, "Failed to parse JSON environment field: %m");
+ return r;
+
+ if (!valid_home(hd))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd);
}
- r = strv_env_replace_strdup_passthrough(&l, arg);
- if (r < 0)
- return log_error_errno(r, "Cannot assign environment variable %s: %m", arg);
+ return parse_string_field(identity, field, hd);
+}
- strv_sort(l);
+static int parse_realm_field(sd_json_variant **identity, const char *field, const char *arg) {
+ int r;
- r = sd_json_variant_new_array_strv(&ne, l);
- if (r < 0)
- return log_error_errno(r, "Failed to allocate json list: %m");
+ assert(identity);
+ assert(field);
- r = sd_json_variant_set_field(identity, field, ne);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
- return 0;
+ if (!isempty(arg)) {
+ r = dns_name_is_valid(arg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", arg);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain.", arg);
+ }
+
+ return parse_string_field(identity, field, arg);
}
-static int parse_language_field(char ***languages, const char *arg) {
+static int parse_path_field(sd_json_variant **identity, const char *field, const char *arg) {
+ _cleanup_free_ char *v = NULL;
int r;
- assert(languages);
+ assert(identity);
+ assert(field);
- if (isempty(arg)) {
- r = drop_from_identity("preferredLanguage", "additionalLanguages");
+ if (!isempty(arg)) {
+ r = parse_path_argument(arg, /* suppress_root= */ false, &v);
if (r < 0)
return r;
-
- strv_freep(languages);
- return 0;
}
- for (const char *p = arg;;) {
- _cleanup_free_ char *word = NULL;
-
- r = extract_first_word(&p, &word, ",:", /* flags= */ 0);
- if (r < 0)
- return log_error_errno(r, "Failed to parse locale list: %m");
- if (r == 0)
- return 0;
-
- if (!locale_is_valid(word))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", word);
+ return parse_string_field(identity, field, v);
+}
- if (locale_is_installed(word) <= 0)
- log_warning("Locale '%s' is not installed, accepting anyway.", word);
+static int parse_filename_field(sd_json_variant **identity, const char *field, const char *arg) {
+ assert(identity);
+ assert(field);
- r = strv_consume(languages, TAKE_PTR(word));
- if (r < 0)
- return log_oom();
+ if (!isempty(arg) && !filename_is_valid(arg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Parameter for %s field not a valid filename: %s", field, arg);
- strv_uniq(*languages);
- }
+ return parse_string_field(identity, field, arg);
}
-static int parse_group_field(
- sd_json_variant **identity,
- const char *field,
- const char *arg) {
+static int parse_unsigned_field(sd_json_variant **identity, const char *field, const char *arg) {
int r;
assert(identity);
if (isempty(arg))
return drop_from_identity(field);
- for (const char *p = arg;;) {
- _cleanup_free_ char *word = NULL;
- _cleanup_strv_free_ char **list = NULL;
-
- r = extract_first_word(&p, &word, ",", /* flags= */ 0);
- if (r < 0)
- return log_error_errno(r, "Failed to parse group list: %m");
- if (r == 0)
- return 0;
-
- if (!valid_user_group_name(word, /* flags= */ 0))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word);
+ unsigned n;
+ r = safe_atou(arg, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *mo =
- sd_json_variant_ref(sd_json_variant_by_key(*identity, field));
+ r = sd_json_variant_set_field_unsigned(identity, field, n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
+}
- r = sd_json_variant_strv(mo, &list);
- if (r < 0)
- return log_error_errno(r, "Failed to parse group list: %m");
+static int parse_u64_field(sd_json_variant **identity, const char *field, const char *arg) {
+ int r;
- r = strv_extend(&list, word);
- if (r < 0)
- return log_oom();
+ assert(identity);
+ assert(field);
- strv_sort_uniq(list);
+ if (isempty(arg))
+ return drop_from_identity(field);
- mo = sd_json_variant_unref(mo);
- r = sd_json_variant_new_array_strv(&mo, list);
- if (r < 0)
- return log_error_errno(r, "Failed to allocate json list: %m");
+ uint64_t n;
+ r = safe_atou64(arg, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
- r = sd_json_variant_set_field(identity, field, mo);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
- }
+ r = sd_json_variant_set_field_unsigned(identity, field, n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
}
-static int parse_capability_set_field(
- sd_json_variant **identity,
- uint64_t *capability_set,
- const char *field,
- const char *arg) {
-
- _cleanup_strv_free_ char **l = NULL;
+static int parse_size_field(sd_json_variant **identity, const char *field, const char *arg) {
int r;
assert(identity);
- assert(capability_set);
assert(field);
- assert(arg);
-
- r = parse_capability_set(arg, CAP_MASK_UNSET, capability_set);
- if (r == 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capabilities in capability string '%s'.", arg);
- if (r < 0)
- return log_error_errno(r, "Failed to parse capability string '%s': %m", arg);
- if (*capability_set == CAP_MASK_UNSET)
+ if (isempty(arg))
return drop_from_identity(field);
- if (capability_set_to_strv(*capability_set, &l) < 0)
- return log_oom();
+ uint64_t n;
+ r = parse_size(arg, 1024, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
- r = sd_json_variant_set_field_strv(identity, field, l);
+ r = sd_json_variant_set_field_unsigned(identity, field, n);
if (r < 0)
return log_error_errno(r, "Failed to set %s field: %m", field);
return 0;
}
-static int parse_tmpfs_limit_field(
- sd_json_variant **identity,
- const char *field,
- const char *field_scale,
- const char *arg) {
+static int parse_boolean_field(sd_json_variant **identity, const char *field, const char *arg) {
int r;
assert(identity);
assert(field);
- assert(field_scale);
if (isempty(arg))
- return drop_from_identity(field, field_scale);
+ return drop_from_identity(field);
- r = parse_permyriad(arg);
- if (r < 0) {
- uint64_t u;
+ r = parse_boolean(arg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse boolean parameter %s: %s", field, arg);
- r = parse_size(arg, 1024, &u);
- if (r < 0)
- return log_error_errno(r, "Failed to parse %s/%s parameter: %s", field, field_scale, arg);
+ r = sd_json_variant_set_field_boolean(identity, field, r > 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
+}
- r = sd_json_variant_set_field_unsigned(identity, field, u);
- if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field);
+static int parse_mode_field(sd_json_variant **identity, const char *field, const char *arg) {
+ int r;
- return drop_from_identity(field_scale);
- }
+ assert(identity);
+ assert(field);
- r = sd_json_variant_set_field_unsigned(identity, field_scale, UINT32_SCALE_FROM_PERMYRIAD(r));
+ if (isempty(arg))
+ return drop_from_identity(field);
+
+ mode_t mode;
+ r = parse_mode(arg, &mode);
if (r < 0)
- return log_error_errno(r, "Failed to set %s field: %m", field_scale);
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", arg);
- return drop_from_identity(field);
+ r = sd_json_variant_set_field_unsigned(identity, field, mode);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
}
-static int parse_pkcs11_token_uri_field(const char *arg) {
+static int parse_timestamp_field(sd_json_variant **identity, const char *field, const char *arg) {
int r;
- assert(arg);
+ assert(identity);
+ assert(field);
- if (streq(arg, "list"))
- return pkcs11_list_tokens();
+ if (isempty(arg))
+ return drop_from_identity(field);
- /* If --pkcs11-token-uri= is specified we always drop everything old */
- r = drop_from_identity("pkcs11TokenUri", "pkcs11EncryptedKey");
+ usec_t n;
+ r = parse_timestamp(arg, &n);
if (r < 0)
- return r;
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
- if (isempty(arg)) {
- arg_pkcs11_token_uri = strv_free(arg_pkcs11_token_uri);
- return 1;
- }
+ r = sd_json_variant_set_field_unsigned(identity, field, n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
+}
- if (streq(arg, "auto")) {
- char *found;
- r = pkcs11_find_token_auto(&found);
- if (r < 0)
- return r;
+static int parse_time_field(sd_json_variant **identity, const char *field, const char *arg) {
+ int r;
- r = strv_consume(&arg_pkcs11_token_uri, found);
- } else {
- if (!pkcs11_uri_valid(arg))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", arg);
+ assert(identity);
+ assert(field);
- r = strv_extend(&arg_pkcs11_token_uri, arg);
- }
+ if (isempty(arg))
+ return drop_from_identity(field);
+
+ usec_t n;
+ r = parse_sec(arg, &n);
if (r < 0)
- return r;
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
- strv_uniq(arg_pkcs11_token_uri);
- return 1;
+ r = sd_json_variant_set_field_unsigned(identity, field, n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
}
-static int parse_fido2_device_field(const char *arg) {
+static int parse_uid_field(sd_json_variant **identity, const char *field, const char *arg) {
+ uid_t uid;
int r;
- assert(arg);
+ assert(identity);
+ assert(field);
- if (streq(arg, "list"))
- return fido2_list_devices();
+ if (isempty(arg))
+ return drop_from_identity(field);
- r = drop_from_identity("fido2HmacCredential", "fido2HmacSalt");
+ r = parse_uid(arg, &uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse UID '%s'.", arg);
+
+ const char *bad_range =
+ uid_is_system(uid) ? "in system range" :
+ uid_is_greeter(uid) ? "in greeter range" :
+ uid_is_dynamic(uid) ? "in dynamic ragne" :
+ uid == UID_NOBODY ? "nobody UID" : NULL;
+ if (bad_range)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID "UID_FMT" is %s, refusing.", uid, bad_range);
+
+ r = sd_json_variant_set_field_unsigned(identity, field, uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
+}
+
+static int parse_nice_field(sd_json_variant **identity, const char *field, const char *arg) {
+ int nc, r;
+
+ assert(identity);
+ assert(field);
+
+ if (isempty(arg))
+ return drop_from_identity(field);
+
+ r = parse_nice(arg, &nc);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse nice level '%s': %m", arg);
+
+ r = sd_json_variant_set_field_integer(identity, field, nc);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
+}
+
+static int parse_auto_resize_mode_field(sd_json_variant **identity, const char *field, const char *arg) {
+ int r;
+
+ assert(identity);
+ assert(field);
+
+ if (!isempty(arg)) {
+ r = auto_resize_mode_from_string(arg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+ arg = auto_resize_mode_to_string(r);
+ }
+
+ return parse_string_field(identity, field, arg);
+}
+
+static int parse_rebalance_weight(sd_json_variant **identity, const char *field, const char *arg) {
+ int r;
+
+ assert(identity);
+ assert(field);
+
+ if (isempty(arg))
+ return drop_from_identity(field);
+
+ uint64_t u;
+ if (streq(arg, "off"))
+ u = REBALANCE_WEIGHT_OFF;
+ else {
+ r = safe_atou64(arg, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse rebalance weight parameter: %s", arg);
+
+ if (u < REBALANCE_WEIGHT_MIN || u > REBALANCE_WEIGHT_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE),
+ "Rebalancing weight out of valid range %" PRIu64 "%s%" PRIu64 ": %s",
+ REBALANCE_WEIGHT_MIN, glyph(GLYPH_ELLIPSIS), REBALANCE_WEIGHT_MAX,
+ arg);
+ }
+
+ /* Drop from per machine stuff and everywhere */
+ r = drop_from_identity(field);
if (r < 0)
return r;
+ r = sd_json_variant_set_field_unsigned(identity, field, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
+}
+
+static int parse_rlimit_field(sd_json_variant **identity, const char *field, const char *arg) {
+ int r;
+
+ assert(identity);
+ assert(field);
+
if (isempty(arg)) {
- arg_fido2_device = strv_free(arg_fido2_device);
- return 1;
+ /* Remove all resource limits */
+
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits);
+ *identity = sd_json_variant_unref(*identity);
+ return 0;
}
- if (streq(arg, "auto")) {
- char *found;
- r = fido2_find_device_auto(&found);
+ const char *eq = strchr(arg, '=');
+ if (!eq)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", arg);
+
+ _cleanup_free_ char *s = strndup(arg, eq - arg);
+ if (!s)
+ return log_oom();
+
+ int limit = rlimit_from_string_harder(s);
+ if (limit < 0)
+ return log_error_errno(limit, "Unknown resource limit type: %s", s);
+
+ const char *rlimit_field = strjoina("RLIMIT_", rlimit_to_string(limit));
+
+ if (isempty(eq + 1)) {
+ /* Remove only the specific rlimit */
+
+ r = strv_extend(&arg_identity_filter_rlimits, rlimit_field);
if (r < 0)
return r;
- r = strv_consume(&arg_fido2_device, found);
- } else
- r = strv_extend(&arg_fido2_device, arg);
+ r = sd_json_variant_filter(identity, STRV_MAKE(rlimit_field));
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
+ return 0;
+ }
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *jcur = NULL, *jmax = NULL;
+ struct rlimit rl;
+
+ r = rlimit_parse(limit, eq + 1, &rl);
if (r < 0)
- return r;
+ return log_error_errno(r, "Failed to parse resource limit value: %s", eq + 1);
- strv_uniq(arg_fido2_device);
- return 1;
+ r = rl.rlim_cur == RLIM_INFINITY ? sd_json_variant_new_null(&jcur) : sd_json_variant_new_unsigned(&jcur, rl.rlim_cur);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate json variant: %m");
+
+ r = rl.rlim_max == RLIM_INFINITY ? sd_json_variant_new_null(&jmax) : sd_json_variant_new_unsigned(&jmax, rl.rlim_max);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate json variant: %m");
+
+ r = sd_json_variant_set_fieldbo(identity, rlimit_field,
+ SD_JSON_BUILD_PAIR_VARIANT("cur", jcur),
+ SD_JSON_BUILD_PAIR_VARIANT("max", jmax));
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", rlimit_field);
+ return 0;
}
-static int help(void) {
- _cleanup_free_ char *link = NULL;
+static int parse_disk_size_field(sd_json_variant **identity, const char *arg) {
int r;
- pager_open(arg_pager_flags);
+ assert(identity);
- r = terminal_urlify_man("homectl", "1", &link);
+ if (isempty(arg)) {
+ r = drop_from_identity("diskSize", "diskSizeRelative", "rebalanceWeight");
+ if (r < 0)
+ return r;
+
+ arg_disk_size = arg_disk_size_relative = UINT64_MAX;
+ return 0;
+ }
+
+ r = parse_permyriad(arg);
+ if (r < 0) {
+ r = parse_disk_size(arg, &arg_disk_size);
+ if (r < 0)
+ return r;
+
+ r = drop_from_identity("diskSizeRelative");
+ if (r < 0)
+ return r;
+
+ r = sd_json_variant_set_field_unsigned(identity, "diskSize", arg_disk_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", "diskSize");
+
+ arg_disk_size_relative = UINT64_MAX;
+ } else {
+ /* Normalize to UINT32_MAX == 100% */
+ arg_disk_size_relative = UINT32_SCALE_FROM_PERMYRIAD(r);
+
+ r = drop_from_identity("diskSize");
+ if (r < 0)
+ return r;
+
+ r = sd_json_variant_set_field_unsigned(identity, "diskSizeRelative", arg_disk_size_relative);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", "diskSizeRelative");
+
+ arg_disk_size = UINT64_MAX;
+ }
+
+ /* Automatically turn off the rebalance logic if user configured a size explicitly */
+ r = sd_json_variant_set_field_unsigned(identity, "rebalanceWeight", REBALANCE_WEIGHT_OFF);
if (r < 0)
- return log_oom();
+ return log_error_errno(r, "Failed to set %s field: %m", "rebalanceWeight");
+ return 0;
+}
- 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);
+static int parse_sector_size_field(sd_json_variant **identity, const char *field, const char *arg) {
+ uint64_t ss;
+ int r;
+
+ assert(identity);
+ assert(field);
+
+ if (isempty(arg))
+ return drop_from_identity(field);
+
+ r = parse_sector_size(arg, &ss);
+ if (r < 0)
+ return r;
+ r = sd_json_variant_set_field_unsigned(identity, field, ss);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
return 0;
}
-static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) {
- return help();
+static int parse_weight_field(sd_json_variant **identity, const char *field, const char *arg) {
+ int r;
+
+ assert(identity);
+ assert(field);
+
+ if (isempty(arg))
+ return drop_from_identity(field);
+
+ uint64_t u;
+ r = safe_atou64(arg, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+
+ if (!CGROUP_WEIGHT_IS_OK(u))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Weight %" PRIu64 " is out of valid range for field %s.", u, field);
+
+ r = sd_json_variant_set_field_unsigned(identity, field, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
}
-static int parse_argv(int argc, char *argv[]) {
- _cleanup_strv_free_ char **arg_languages = NULL;
+static int parse_environment_field(sd_json_variant **identity, const char *field, const char *arg) {
+ _cleanup_strv_free_ char **l = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *ne = NULL;
+ int r;
- 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 },
- {}
- };
+ assert(identity);
+ assert(field);
- int r;
+ if (isempty(arg))
+ return drop_from_identity(field);
- /* This points to one of arg_identity_extra, arg_identity_extra_this_machine,
- * arg_identity_extra_other_machines, in order to redirect changes on the next property being set to
- * this part of the identity, instead of the default. */
- sd_json_variant **match_identity = NULL;
+ sd_json_variant *e = sd_json_variant_by_key(*identity, field);
+ if (e) {
+ r = sd_json_variant_strv(e, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON environment field: %m");
+ }
- assert(argc >= 0);
- assert(argv);
+ r = strv_env_replace_strdup_passthrough(&l, arg);
+ if (r < 0)
+ return log_error_errno(r, "Cannot assign environment variable %s: %m", arg);
- /* 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. */
- r = getenv_bool("SYSTEMD_HOME_DRY_RUN");
- if (r >= 0)
- arg_dry_run = r;
- else if (r != -ENXIO)
- log_debug_errno(r, "Unable to parse $SYSTEMD_HOME_DRY_RUN, ignoring: %m");
+ strv_sort(l);
- for (;;) {
- int c;
+ r = sd_json_variant_new_array_strv(&ne, l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate json list: %m");
- c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPENAT", options, NULL);
- if (c < 0)
- break;
+ r = sd_json_variant_set_field(identity, field, ne);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
+}
- switch (c) {
+static int parse_language_field(char ***languages, const char *arg) {
+ int r;
- case 'h':
- return help();
+ assert(languages);
- case ARG_VERSION:
- return version();
+ if (isempty(arg)) {
+ r = drop_from_identity("preferredLanguage", "additionalLanguages");
+ if (r < 0)
+ return r;
- case ARG_NO_PAGER:
- arg_pager_flags |= PAGER_DISABLE;
- break;
+ strv_freep(languages);
+ return 0;
+ }
- case ARG_NO_LEGEND:
- arg_legend = false;
- break;
+ for (const char *p = arg;;) {
+ _cleanup_free_ char *word = NULL;
- case ARG_NO_ASK_PASSWORD:
- arg_ask_password = false;
- break;
+ r = extract_first_word(&p, &word, ",:", /* flags= */ 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse locale list: %m");
+ if (r == 0)
+ return 0;
- case ARG_OFFLINE:
- arg_offline = true;
- break;
+ if (!locale_is_valid(word))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", word);
- case 'H':
- arg_transport = BUS_TRANSPORT_REMOTE;
- arg_host = optarg;
- break;
+ if (locale_is_installed(word) <= 0)
+ log_warning("Locale '%s' is not installed, accepting anyway.", word);
- case 'M':
- r = parse_machine_argument(optarg, &arg_host, &arg_transport);
- if (r < 0)
- return r;
- break;
+ r = strv_consume(languages, TAKE_PTR(word));
+ if (r < 0)
+ return log_oom();
- case 'I':
- arg_identity = optarg;
- break;
+ strv_uniq(*languages);
+ }
+}
- case 'c':
- if (!isempty(optarg) && !valid_gecos(optarg))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Invalid GECOS field '%s'.", optarg);
+static int parse_group_field(
+ sd_json_variant **identity,
+ const char *field,
+ const char *arg) {
+ int r;
- r = parse_string_field(match_identity ?: &arg_identity_extra, "realName", optarg);
- if (r < 0)
- return r;
- break;
+ assert(identity);
+ assert(field);
- case ARG_ALIAS:
- r = parse_group_field(&arg_identity_extra, "aliases", optarg);
- if (r < 0)
- return r;
- break;
+ if (isempty(arg))
+ return drop_from_identity(field);
- case 'd':
- r = parse_home_directory_field(&arg_identity_extra, "homeDirectory", optarg);
- if (r < 0)
- return r;
- break;
+ for (const char *p = arg;;) {
+ _cleanup_free_ char *word = NULL;
+ _cleanup_strv_free_ char **list = NULL;
- case ARG_REALM:
- r = parse_realm_field(&arg_identity_extra, "realm", optarg);
- if (r < 0)
- return r;
- break;
+ r = extract_first_word(&p, &word, ",", /* flags= */ 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse group list: %m");
+ if (r == 0)
+ return 0;
- 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);
+ if (!valid_user_group_name(word, /* flags= */ 0))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word);
- r = parse_string_field(match_identity ?: &arg_identity_extra, field, optarg);
- if (r < 0)
- return r;
- break;
- }
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *mo =
+ sd_json_variant_ref(sd_json_variant_by_key(*identity, field));
- 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 = sd_json_variant_strv(mo, &list);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse group list: %m");
- r = parse_string_field(&arg_identity_extra, "birthDate", optarg);
- if (r < 0)
- return r;
- }
- break;
+ r = strv_extend(&list, word);
+ if (r < 0)
+ return log_oom();
- 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);
- }
+ strv_sort_uniq(list);
- r = parse_string_field(match_identity ?: &arg_identity_extra, "cifsService", optarg);
- if (r < 0)
- return r;
- break;
+ mo = sd_json_variant_unref(mo);
+ r = sd_json_variant_new_array_strv(&mo, list);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate json list: %m");
- case ARG_PASSWORD_HINT:
- r = parse_string_field(&arg_identity_extra_privileged, "passwordHint", optarg);
- if (r < 0)
- return r;
+ r = sd_json_variant_set_field(identity, field, mo);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ }
+}
- string_erase(optarg);
- break;
+static int parse_capability_set_field(
+ sd_json_variant **identity,
+ uint64_t *capability_set,
+ const char *field,
+ const char *arg) {
- case ARG_NICE:
- r = parse_nice_field(match_identity ?: &arg_identity_extra, "niceLevel", optarg);
- if (r < 0)
- return r;
- break;
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
- case ARG_RLIMIT:
- r = parse_rlimit_field(&arg_identity_extra_rlimits, "resourceLimits", optarg);
- if (r < 0)
- return r;
- break;
+ assert(identity);
+ assert(capability_set);
+ assert(field);
+ assert(arg);
- case 'u':
- r = parse_uid_field(&arg_identity_extra, "uid", optarg);
- if (r < 0)
- return r;
- break;
+ r = parse_capability_set(arg, CAP_MASK_UNSET, capability_set);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capabilities in capability string '%s'.", arg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse capability string '%s': %m", arg);
- case 'k':
- case ARG_IMAGE_PATH: {
- const char *field = c == 'k' ? "skeletonDirectory" : "imagePath";
+ if (*capability_set == CAP_MASK_UNSET)
+ return drop_from_identity(field);
- r = parse_path_field(match_identity ?: &arg_identity_extra_this_machine, field, optarg);
- if (r < 0)
- return r;
- break;
- }
+ if (capability_set_to_strv(*capability_set, &l) < 0)
+ return log_oom();
- case 's':
- if (!isempty(optarg) && !valid_shell(optarg))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Shell '%s' not valid.", optarg);
+ r = sd_json_variant_set_field_strv(identity, field, l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ return 0;
+}
- r = parse_string_field(match_identity ?: &arg_identity_extra, "shell", optarg);
- if (r < 0)
- return r;
- break;
+static int parse_tmpfs_limit_field(
+ sd_json_variant **identity,
+ const char *field,
+ const char *field_scale,
+ const char *arg) {
+ int r;
- case ARG_SETENV:
- r = parse_environment_field(match_identity ?: &arg_identity_extra, "environment", optarg);
- if (r < 0)
- return r;
- break;
+ assert(identity);
+ assert(field);
+ assert(field_scale);
- 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);
+ if (isempty(arg))
+ return drop_from_identity(field, field_scale);
- r = parse_string_field(match_identity ?: &arg_identity_extra, "timeZone", optarg);
- if (r < 0)
- return r;
- break;
+ r = parse_permyriad(arg);
+ if (r < 0) {
+ uint64_t u;
+
+ r = parse_size(arg, 1024, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s/%s parameter: %s", field, field_scale, arg);
+
+ r = sd_json_variant_set_field_unsigned(identity, field, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ return drop_from_identity(field_scale);
+ }
+
+ r = sd_json_variant_set_field_unsigned(identity, field_scale, UINT32_SCALE_FROM_PERMYRIAD(r));
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field_scale);
+
+ return drop_from_identity(field);
+}
+
+static int parse_pkcs11_token_uri_field(const char *arg) {
+ int r;
+
+ assert(arg);
+
+ if (streq(arg, "list"))
+ return pkcs11_list_tokens();
+
+ /* If --pkcs11-token-uri= is specified we always drop everything old */
+ r = drop_from_identity("pkcs11TokenUri", "pkcs11EncryptedKey");
+ if (r < 0)
+ return r;
+
+ if (isempty(arg)) {
+ arg_pkcs11_token_uri = strv_free(arg_pkcs11_token_uri);
+ return 1;
+ }
+
+ if (streq(arg, "auto")) {
+ char *found;
+ r = pkcs11_find_token_auto(&found);
+ if (r < 0)
+ return r;
+
+ r = strv_consume(&arg_pkcs11_token_uri, found);
+ } else {
+ if (!pkcs11_uri_valid(arg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", arg);
+
+ r = strv_extend(&arg_pkcs11_token_uri, arg);
+ }
+ if (r < 0)
+ return r;
+
+ strv_uniq(arg_pkcs11_token_uri);
+ return 1;
+}
+
+static int parse_fido2_device_field(const char *arg) {
+ int r;
+
+ assert(arg);
+
+ if (streq(arg, "list"))
+ return fido2_list_devices();
+
+ r = drop_from_identity("fido2HmacCredential", "fido2HmacSalt");
+ if (r < 0)
+ return r;
+
+ if (isempty(arg)) {
+ arg_fido2_device = strv_free(arg_fido2_device);
+ return 1;
+ }
+
+ if (streq(arg, "auto")) {
+ char *found;
+ r = fido2_find_device_auto(&found);
+ if (r < 0)
+ return r;
+
+ r = strv_consume(&arg_fido2_device, found);
+ } else
+ r = strv_extend(&arg_fido2_device, arg);
+ if (r < 0)
+ return r;
+
+ strv_uniq(arg_fido2_device);
+ return 1;
+}
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ pager_open(arg_pager_flags);
+
+ r = terminal_urlify_man("homectl", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] COMMAND ...\n\n"
+ "%2$sCreate, manipulate or inspect home directories.%3$s\n"
+ "\n%4$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);
+
+ return 0;
+}
+
+static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) {
+ return help();
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ _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 },
+ {}
+ };
- case ARG_LANGUAGE:
- r = parse_language_field(&arg_languages, optarg);
- if (r < 0)
- return r;
- break;
+ int r;
- 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);
+ /* This points to one of arg_identity_extra, arg_identity_extra_this_machine,
+ * arg_identity_extra_other_machines, in order to redirect changes on the next property being set to
+ * this part of the identity, instead of the default. */
+ sd_json_variant **match_identity = NULL;
- r = parse_boolean_field(match_identity ?: &arg_identity_extra, field, optarg);
- if (r < 0)
- return r;
- break;
- }
+ assert(argc >= 0);
+ assert(argv);
- 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;
+ /* 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. */
+ r = getenv_bool("SYSTEMD_HOME_DRY_RUN");
+ if (r >= 0)
+ arg_dry_run = r;
+ else if (r != -ENXIO)
+ log_debug_errno(r, "Unable to parse $SYSTEMD_HOME_DRY_RUN, ignoring: %m");
- case ARG_DISK_SIZE:
- r = parse_disk_size_field(match_identity ?: &arg_identity_extra_this_machine, optarg);
- if (r < 0)
- return r;
- break;
+ for (;;) {
+ int c;
- case ARG_ACCESS_MODE:
- r = parse_mode_field(&arg_identity_extra, "accessMode", optarg);
- if (r < 0)
- return r;
+ c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPENAT", options, NULL);
+ if (c < 0)
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;
- }
+ switch (c) {
- 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);
+ case 'h':
+ return help();
- r = parse_unsigned_field(match_identity ?: &arg_identity_extra, field, optarg);
- if (r < 0)
- return r;
- break;
- }
+ case ARG_VERSION:
+ return version();
- case ARG_LUKS_SECTOR_SIZE:
- r = parse_sector_size_field(match_identity ?: &arg_identity_extra, "luksSectorSize", optarg);
- if (r < 0)
- return r;
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
break;
- case ARG_UMASK:
- r = parse_mode_field(match_identity ?: &arg_identity_extra, "umask", optarg);
- if (r < 0)
- return r;
+ case ARG_NO_LEGEND:
+ arg_legend = false;
break;
- case ARG_SSH_AUTHORIZED_KEYS:
- r = parse_ssh_authorized_keys(&arg_identity_extra_privileged, "sshAuthorizedKeys", optarg);
- if (r < 0)
- return r;
-
+ case ARG_NO_ASK_PASSWORD:
+ arg_ask_password = false;
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;
+ case ARG_OFFLINE:
+ arg_offline = true;
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;
+ case 'H':
+ arg_transport = BUS_TRANSPORT_REMOTE;
+ arg_host = optarg;
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);
+ case 'M':
+ r = parse_machine_argument(optarg, &arg_host, &arg_transport);
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);
+ case 'I':
+ arg_identity = optarg;
+ break;
- r = parse_time_field(match_identity ?: &arg_identity_extra, field, optarg);
+ 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 'G':
- r = parse_group_field(match_identity ?: &arg_identity_extra, "memberOf", optarg);
+ case ARG_ALIAS:
+ r = parse_group_field(&arg_identity_extra, "aliases", optarg);
if (r < 0)
return r;
break;
- case ARG_TASKS_MAX:
- r = parse_u64_field(match_identity ?: &arg_identity_extra, "tasksMax", optarg);
+ case 'd':
+ r = parse_home_directory_field(&arg_identity_extra, "homeDirectory", 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);
+ case ARG_REALM:
+ r = parse_realm_field(&arg_identity_extra, "realm", 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;
+ 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_weight_field(match_identity ?: &arg_identity_extra, field, optarg);
+ r = parse_string_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);
- 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);
- 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);
+ r = parse_string_field(&arg_identity_extra, "birthDate", optarg);
+ if (r < 0)
+ return r;
+ }
break;
- case ARG_FIDO2_DEVICE:
- r = parse_fido2_device_field(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);
+ }
- case ARG_FIDO2_WITH_PIN:
- r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL);
+ r = parse_string_field(match_identity ?: &arg_identity_extra, "cifsService", optarg);
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);
+ case ARG_PASSWORD_HINT:
+ r = parse_string_field(&arg_identity_extra_privileged, "passwordHint", optarg);
if (r < 0)
return r;
- SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r);
+ string_erase(optarg);
break;
- case ARG_FIDO2_WITH_UV:
- r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL);
+ case ARG_NICE:
+ r = parse_nice_field(match_identity ?: &arg_identity_extra, "niceLevel", optarg);
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");
+ case ARG_RLIMIT:
+ r = parse_rlimit_field(&arg_identity_extra_rlimits, "resourceLimits", optarg);
if (r < 0)
return r;
break;
- case ARG_AUTO_RESIZE_MODE:
- r = parse_auto_resize_mode_field(match_identity ?: &arg_identity_extra,
- "autoResizeMode", optarg);
+ case 'u':
+ r = parse_uid_field(&arg_identity_extra, "uid", optarg);
if (r < 0)
return r;
break;
- case ARG_REBALANCE_WEIGHT:
- r = parse_rebalance_weight(match_identity ?: &arg_identity_extra,
- "rebalanceWeight", optarg);
+ 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 'j':
- arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
- break;
+ case 's':
+ if (!isempty(optarg) && !valid_shell(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Shell '%s' not valid.", optarg);
- case ARG_JSON:
- r = parse_json_argument(optarg, &arg_json_format_flags);
- if (r <= 0)
+ r = parse_string_field(match_identity ?: &arg_identity_extra, "shell", optarg);
+ if (r < 0)
return r;
-
- break;
-
- case 'E':
- if (arg_export_format == EXPORT_FORMAT_FULL)
- arg_export_format = EXPORT_FORMAT_STRIPPED;
- else if (arg_export_format == EXPORT_FORMAT_STRIPPED)
- arg_export_format = EXPORT_FORMAT_MINIMAL;
- else
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported.");
-
- arg_json_format_flags &= ~SD_JSON_FORMAT_OFF;
- if (arg_json_format_flags == 0)
- 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);
-
- 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);
+ case ARG_SETENV:
+ r = parse_environment_field(match_identity ?: &arg_identity_extra, "environment", optarg);
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);
+ 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_CAPABILITY_BOUNDING_SET:
- r = parse_capability_set_field(match_identity ?: &arg_identity_extra,
- &arg_capability_bounding_set,
- "capabilityBoundingSet", optarg);
+ case ARG_LANGUAGE:
+ r = parse_language_field(&arg_languages, optarg);
if (r < 0)
return r;
break;
- case ARG_PROMPT_NEW_USER:
- arg_prompt_new_user = true;
- break;
-
- case 'b':
- case ARG_AVATAR:
- case ARG_LOGIN_BACKGROUND: {
- _cleanup_close_ int fd = -EBADF;
- _cleanup_free_ char *path = NULL, *filename = NULL;
-
- if (c == 'b') {
- char *eq;
-
- if (isempty(optarg)) { /* --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, '=');
- if (!eq) { /* --blob=/some/path replaces the blob dir */
- r = parse_path_argument(optarg, /* 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);
- if (!filename)
- return log_oom();
-
- if (isempty(filename))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", optarg);
- if (!suitable_blob_filename(filename))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid blob filename: %s", filename);
-
- r = parse_path_argument(eq + 1, /* suppress_root= */ false, &path);
- 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);
- if (!filename)
- return log_oom();
+ 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_path_argument(optarg, /* suppress_root= */ false, &path);
- if (r < 0)
- return r;
- }
+ r = parse_boolean_field(match_identity ?: &arg_identity_extra, field, optarg);
+ if (r < 0)
+ return r;
+ break;
+ }
- if (path) {
- fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
- if (fd < 0)
- return log_error_errno(errno, "Failed to open %s: %m", path);
+ 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;
- if (fd_verify_regular(fd) < 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provided blob is not a regular file: %s", path);
- } else
- fd = -EBADF; /* Delete the file */
+ case ARG_DISK_SIZE:
+ r = parse_disk_size_field(match_identity ?: &arg_identity_extra_this_machine, optarg);
+ if (r < 0)
+ return r;
+ break;
- r = hashmap_ensure_put(&arg_blob_files, &blob_fd_hash_ops, filename, FD_TO_PTR(fd));
+ case ARG_ACCESS_MODE:
+ r = parse_mode_field(&arg_identity_extra, "accessMode", optarg);
if (r < 0)
- return log_error_errno(r, "Failed to map %s to %s in blob directory: %m", path, filename);
- TAKE_PTR(filename); /* hashmap takes ownership */
- TAKE_FD(fd);
+ 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_TMP_LIMIT:
- case ARG_DEV_SHM_LIMIT: {
+ 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_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;
-
+ 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);
- assert(field_scale);
- r = parse_tmpfs_limit_field(match_identity ?: &arg_identity_extra,
- field, field_scale, optarg);
+ r = parse_unsigned_field(match_identity ?: &arg_identity_extra, field, optarg);
if (r < 0)
return r;
break;
}
- case ARG_DEFAULT_AREA:
- r = parse_filename_field(match_identity ?: &arg_identity_extra, "defaultArea", optarg);
+ 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_KEY_NAME:
- if (!isempty(optarg) && !filename_is_valid(optarg))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Parameter for --key-name= not a valid filename: %s", optarg);
-
- r = free_and_strdup_warn(&arg_key_name, empty_to_null(optarg));
+ case ARG_UMASK:
+ r = parse_mode_field(match_identity ?: &arg_identity_extra, "umask", optarg);
if (r < 0)
return r;
break;
- case ARG_SEIZE:
- r = parse_boolean_argument("--seize=", optarg, &arg_seize);
+ case ARG_SSH_AUTHORIZED_KEYS:
+ r = parse_ssh_authorized_keys(&arg_identity_extra_privileged, "sshAuthorizedKeys", optarg);
if (r < 0)
return r;
- break;
- case ARG_MATCH:
- if (streq(optarg, "any"))
- match_identity = &arg_identity_extra;
- else if (streq(optarg, "this"))
- match_identity = &arg_identity_extra_this_machine;
- else if (streq(optarg, "other"))
- match_identity = &arg_identity_extra_other_machines;
- else if (streq(optarg, "auto"))
- match_identity = NULL;
- else
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--machine= argument not understood. Refusing.");
break;
- case 'A':
- match_identity = &arg_identity_extra;
- break;
- case 'T':
- match_identity = &arg_identity_extra_this_machine;
- break;
- case 'N':
- match_identity = &arg_identity_extra_other_machines;
- break;
+ case ARG_NOT_BEFORE:
+ case ARG_NOT_AFTER:
+ case 'e': {
+ const char *field = c == ARG_NOT_BEFORE ? "notBeforeUSec" : "notAfterUSec";
- case ARG_PROMPT_SHELL:
- r = parse_boolean_argument("--prompt-shell=", optarg, &arg_prompt_shell);
+ r = parse_timestamp_field(match_identity ?: &arg_identity_extra, field, optarg);
if (r < 0)
return r;
-
break;
+ }
- case ARG_PROMPT_GROUPS:
- r = parse_boolean_argument("--prompt-groups=", optarg, &arg_prompt_groups);
+ 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_CHROME:
- r = parse_boolean_argument("--chrome=", optarg, &arg_chrome);
+ 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 ARG_MUTE_CONSOLE:
- r = parse_boolean_argument("--mute-console=", optarg, &arg_mute_console);
+ 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 '?':
- return -EINVAL;
+ 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;
- default:
- assert_not_reached();
+ r = parse_size_field(match_identity ?: &arg_identity_extra_this_machine, field, optarg);
+ if (r < 0)
+ return r;
+ break;
}
- }
-
- if (!strv_isempty(arg_languages)) {
- char **additional;
- r = sd_json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", arg_languages[0]);
- if (r < 0)
- return log_error_errno(r, "Failed to update preferred language: %m");
+ case ARG_CPU_WEIGHT:
+ case ARG_IO_WEIGHT: {
+ const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" :
+ c == ARG_IO_WEIGHT ? "ioWeight" :
+ NULL;
- additional = strv_skip(arg_languages, 1);
- if (!strv_isempty(additional)) {
- r = sd_json_variant_set_field_strv(&arg_identity_extra, "additionalLanguages", additional);
- if (r < 0)
- return log_error_errno(r, "Failed to update additional language list: %m");
- } else {
- r = drop_from_identity("additionalLanguages");
+ r = parse_weight_field(match_identity ?: &arg_identity_extra, field, optarg);
if (r < 0)
return r;
+ break;
}
- }
-
- return 1;
-}
-
-static int redirect_bus_mgr(void) {
- const char *suffix;
-
- /* Talk to a different service if that's requested. (The same env var is also understood by homed, so
- * that it is relatively easily possible to invoke a second instance of homed for debug purposes and
- * have homectl talk to it, without colliding with the host version. This is handy when operating
- * from a homed-managed account.) */
-
- suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
- if (suffix) {
- static BusLocator locator = {
- .path = "/org/freedesktop/home1",
- .interface = "org.freedesktop.home1.Manager",
- };
- /* Yes, we leak this memory, but there's little point to collect this, given that we only do
- * this in a debug environment, do it only once, and the string shall live for out entire
- * process runtime. */
+ case ARG_PKCS11_TOKEN_URI:
+ r = parse_pkcs11_token_uri_field(optarg);
+ if (r <= 0)
+ return r;
+ break;
- locator.destination = strjoin("org.freedesktop.home1.", suffix);
- if (!locator.destination)
- return log_oom();
+ 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;
- bus_mgr = &locator;
- } else
- bus_mgr = bus_home_mgr;
+ case ARG_FIDO2_DEVICE:
+ r = parse_fido2_device_field(optarg);
+ if (r <= 0)
+ return r;
+ break;
- return 0;
-}
+ case ARG_FIDO2_WITH_PIN:
+ r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL);
+ if (r < 0)
+ return r;
-static bool is_fallback_shell(const char *p) {
- const char *q;
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r);
+ break;
- if (!p)
- return false;
+ case ARG_FIDO2_WITH_UP:
+ r = parse_boolean_argument("--fido2-with-user-presence=", optarg, NULL);
+ if (r < 0)
+ return r;
- if (p[0] == '-') {
- /* Skip over login shell dash */
- p++;
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r);
+ break;
- if (streq(p, "ystemd-home-fallback-shell")) /* maybe the dash was used to override the binary name? */
- return true;
- }
+ case ARG_FIDO2_WITH_UV:
+ r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL);
+ if (r < 0)
+ return r;
- q = strrchr(p, '/'); /* Skip over path */
- if (q)
- p = q + 1;
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r);
+ break;
- return streq(p, "systemd-home-fallback-shell");
-}
+ 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;
-static int fallback_shell(int argc, char *argv[]) {
- _cleanup_(user_record_unrefp) UserRecord *secret = NULL, *hr = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_strv_free_ char **l = NULL;
- _cleanup_free_ char *argv0 = NULL;
- const char *json, *hd, *shell;
- int r, incomplete;
+ r = drop_from_identity("recoveryKey", "recoveryKeyType");
+ if (r < 0)
+ return r;
+ break;
- /* So here's the deal: if users log into a system via ssh, and their homed-managed home directory
- * wasn't activated yet, SSH will permit the access but the home directory isn't actually available
- * yet. SSH doesn't allow us to ask authentication questions from the PAM session stack, and doesn't
- * run the PAM authentication stack (because it authenticates via its own key management, after
- * all). So here's our way to support this: homectl can be invoked as a multi-call binary under the
- * name "systemd-home-fallback-shell". If so, it will chainload a login shell, but first try to
- * unlock the home directory of the user it is invoked as. systemd-homed will then override the shell
- * listed in user records whose home directory is not activated yet with this pseudo-shell. Net
- * effect: one SSH auth succeeds this pseudo shell gets invoked, which will unlock the homedir
- * (possibly asking for a passphrase) and then chainload the regular shell. Once the login is
- * complete the user record will look like any other. */
+ case ARG_AUTO_RESIZE_MODE:
+ r = parse_auto_resize_mode_field(match_identity ?: &arg_identity_extra,
+ "autoResizeMode", optarg);
+ if (r < 0)
+ return r;
+ break;
- r = acquire_bus(&bus);
- if (r < 0)
- return r;
+ case ARG_REBALANCE_WEIGHT:
+ r = parse_rebalance_weight(match_identity ?: &arg_identity_extra,
+ "rebalanceWeight", optarg);
+ if (r < 0)
+ return r;
+ break;
- for (unsigned n_tries = 0;; n_tries++) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ case 'j':
+ arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
+ break;
- if (n_tries >= 5)
- return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
- "Failed to activate home dir, even after %u tries.", n_tries);
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
- /* Let's start by checking if this all is even necessary, i.e. if the useFallback boolean field is actually set. */
- r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", NULL); /* empty user string means: our calling user */
- if (r < 0)
- return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
+ break;
- r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
- if (r < 0)
- return bus_log_parse_error(r);
+ case 'E':
+ if (arg_export_format == EXPORT_FORMAT_FULL)
+ arg_export_format = EXPORT_FORMAT_STRIPPED;
+ else if (arg_export_format == EXPORT_FORMAT_STRIPPED)
+ arg_export_format = EXPORT_FORMAT_MINIMAL;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported.");
- r = sd_json_parse(json, SD_JSON_PARSE_SENSITIVE|SD_JSON_PARSE_MUST_BE_OBJECT, &v, /* reterr_line= */ NULL, /* reterr_column= */ NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to parse JSON identity: %m");
+ arg_json_format_flags &= ~SD_JSON_FORMAT_OFF;
+ if (arg_json_format_flags == 0)
+ arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
+ break;
- hr = user_record_new();
- if (!hr)
- return log_oom();
+ case ARG_EXPORT_FORMAT:
+ if (streq(optarg, "help"))
+ return DUMP_STRING_TABLE(export_format, ExportFormat, _EXPORT_FORMAT_MAX);
- r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG|USER_RECORD_PERMISSIVE);
- if (r < 0)
- return r;
+ 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);
- if (!hr->use_fallback) /* Nice! We are done, fallback logic not necessary */
break;
- if (!secret) {
- r = acquire_passed_secrets(hr->user_name, &secret);
+ case ARG_DROP_CACHES:
+ r = parse_boolean_field(match_identity ?: &arg_identity_extra, "dropCaches", optarg);
if (r < 0)
return r;
- }
-
- for (;;) {
- _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-
- r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHomeIfReferenced");
- if (r < 0)
- return bus_log_create_error(r);
+ break;
- r = sd_bus_message_append(m, "s", NULL); /* empty user string means: our calling user */
+ case ARG_CAPABILITY_AMBIENT_SET:
+ r = parse_capability_set_field(match_identity ?: &arg_identity_extra,
+ &arg_capability_ambient_set,
+ "capabilityAmbientSet", optarg);
if (r < 0)
- return bus_log_create_error(r);
+ return r;
+ break;
- r = bus_message_append_secret(m, secret);
+ 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 bus_log_create_error(r);
+ return r;
+ break;
- r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_REFERENCED))
- return log_error_errno(r, "Called without reference on home taken, can't operate.");
+ case ARG_PROMPT_NEW_USER:
+ arg_prompt_new_user = true;
+ break;
- r = handle_generic_user_record_error(hr->user_name, secret, &error, r, false);
- if (r < 0)
- return r;
+ case 'b':
+ case ARG_AVATAR:
+ case ARG_LOGIN_BACKGROUND: {
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_free_ char *path = NULL, *filename = NULL;
- sd_bus_error_free(&error);
- } else
- break;
- }
+ if (c == 'b') {
+ char *eq;
- /* Try again */
- hr = user_record_unref(hr);
- }
+ if (isempty(optarg)) { /* --blob= deletes everything, including existing blob dirs */
+ hashmap_clear(arg_blob_files);
+ arg_blob_dir = mfree(arg_blob_dir);
+ arg_blob_clear = true;
+ break;
+ }
- incomplete = getenv_bool("XDG_SESSION_INCOMPLETE"); /* pam_systemd_home reports this state via an environment variable to us. */
- if (incomplete < 0 && incomplete != -ENXIO)
- return log_error_errno(incomplete, "Failed to parse $XDG_SESSION_INCOMPLETE environment variable: %m");
- if (incomplete > 0) {
- /* We are still in an "incomplete" session here. Now upgrade it to a full one. This will make logind
- * start the user@.service instance for us. */
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- r = sd_bus_call_method(
- bus,
- "org.freedesktop.login1",
- "/org/freedesktop/login1/session/self",
- "org.freedesktop.login1.Session",
- "SetClass",
- &error,
- /* ret_reply= */ NULL,
- "s",
- "user");
- if (r < 0)
- return log_error_errno(r, "Failed to upgrade session: %s", bus_error_message(&error, r));
+ eq = strrchr(optarg, '=');
+ if (!eq) { /* --blob=/some/path replaces the blob dir */
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_blob_dir);
+ if (r < 0)
+ return r;
+ break;
+ }
- if (setenv("XDG_SESSION_CLASS", "user", /* overwrite= */ true) < 0) /* Update the XDG_SESSION_CLASS environment variable to match the above */
- return log_error_errno(errno, "Failed to set $XDG_SESSION_CLASS: %m");
+ /* --blob=filename=/some/path replaces the file "filename" with /some/path */
+ filename = strndup(optarg, eq - optarg);
+ if (!filename)
+ return log_oom();
- if (unsetenv("XDG_SESSION_INCOMPLETE") < 0) /* Unset the 'incomplete' env var */
- return log_error_errno(errno, "Failed to unset $XDG_SESSION_INCOMPLETE: %m");
- }
+ if (isempty(filename))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", optarg);
+ if (!suitable_blob_filename(filename))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid blob filename: %s", filename);
- /* We are going to invoke execv() soon. Let's be extra accurate and flush/close our bus connection
- * first, just to make sure anything queued is flushed out (though there shouldn't be anything) */
- bus = sd_bus_flush_close_unref(bus);
+ r = parse_path_argument(eq + 1, /* suppress_root= */ false, &path);
+ 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);
- assert(!hr->use_fallback);
- assert_se(shell = user_record_shell(hr));
- assert_se(hd = user_record_home_directory(hr));
+ filename = strdup(well_known_filename);
+ if (!filename)
+ return log_oom();
- /* Extra protection: avoid loops */
- if (is_fallback_shell(shell))
- return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Primary shell of '%s' is fallback shell, refusing loop.", hr->user_name);
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &path);
+ if (r < 0)
+ return r;
+ }
- if (chdir(hd) < 0)
- return log_error_errno(errno, "Failed to change directory to home directory '%s': %m", hd);
+ if (path) {
+ fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open %s: %m", path);
- if (setenv("SHELL", shell, /* overwrite= */ true) < 0)
- return log_error_errno(errno, "Failed to set $SHELL: %m");
+ if (fd_verify_regular(fd) < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provided blob is not a regular file: %s", path);
+ } else
+ fd = -EBADF; /* Delete the file */
- if (setenv("HOME", hd, /* overwrite= */ true) < 0)
- return log_error_errno(errno, "Failed to set $HOME: %m");
+ r = hashmap_ensure_put(&arg_blob_files, &blob_fd_hash_ops, filename, FD_TO_PTR(fd));
+ if (r < 0)
+ return log_error_errno(r, "Failed to map %s to %s in blob directory: %m", path, filename);
+ TAKE_PTR(filename); /* hashmap takes ownership */
+ TAKE_FD(fd);
- /* Paranoia: in case the client passed some passwords to us to help us unlock, unlock things now */
- FOREACH_STRING(ue, "PASSWORD", "NEWPASSWORD", "PIN")
- if (unsetenv(ue) < 0)
- return log_error_errno(errno, "Failed to unset $%s: %m", ue);
+ break;
+ }
- r = path_extract_filename(shell, &argv0);
- if (r < 0)
- return log_error_errno(r, "Unable to extract file name from '%s': %m", shell);
- if (r == O_DIRECTORY)
- return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Shell '%s' is a path to a directory, refusing.", shell);
+ 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;
- /* Invoke this as login shell, by setting argv[0][0] to '-' (unless we ourselves weren't called as login shell) */
- if (!argv || isempty(argv[0]) || argv[0][0] == '-') {
- _cleanup_free_ char *prefixed = strjoin("-", argv0);
- if (!prefixed)
- return log_oom();
+ assert(field);
+ assert(field_scale);
- free_and_replace(argv0, prefixed);
- }
+ r = parse_tmpfs_limit_field(match_identity ?: &arg_identity_extra,
+ field, field_scale, optarg);
+ if (r < 0)
+ return r;
+ break;
+ }
- l = strv_new(argv0);
- if (!l)
- return log_oom();
+ case ARG_DEFAULT_AREA:
+ r = parse_filename_field(match_identity ?: &arg_identity_extra, "defaultArea", optarg);
+ if (r < 0)
+ return r;
+ break;
- if (strv_extend_strv(&l, strv_skip(argv, 1), /* filter_duplicates= */ false) < 0)
- return log_oom();
+ case ARG_KEY_NAME:
+ if (!isempty(optarg) && !filename_is_valid(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Parameter for --key-name= not a valid filename: %s", optarg);
- execv(shell, l);
- return log_error_errno(errno, "Failed to execute shell '%s': %m", shell);
-}
+ r = free_and_strdup_warn(&arg_key_name, empty_to_null(optarg));
+ if (r < 0)
+ return r;
+ break;
-static int verb_list_signing_keys(int argc, char *argv[], uintptr_t _data, void *userdata) {
- int r;
+ case ARG_SEIZE:
+ r = parse_boolean_argument("--seize=", optarg, &arg_seize);
+ if (r < 0)
+ return r;
+ break;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- r = acquire_bus(&bus);
- if (r < 0)
- return r;
+ case ARG_MATCH:
+ if (streq(optarg, "any"))
+ match_identity = &arg_identity_extra;
+ else if (streq(optarg, "this"))
+ match_identity = &arg_identity_extra_this_machine;
+ else if (streq(optarg, "other"))
+ match_identity = &arg_identity_extra_other_machines;
+ else if (streq(optarg, "auto"))
+ match_identity = NULL;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--machine= argument not understood. Refusing.");
+ break;
- _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));
+ case 'A':
+ match_identity = &arg_identity_extra;
+ break;
+ case 'T':
+ match_identity = &arg_identity_extra_this_machine;
+ break;
+ case 'N':
+ match_identity = &arg_identity_extra_other_machines;
+ break;
- _cleanup_(table_unrefp) Table *table = table_new("name", "key");
- if (!table)
- return log_oom();
+ case ARG_PROMPT_SHELL:
+ r = parse_boolean_argument("--prompt-shell=", optarg, &arg_prompt_shell);
+ if (r < 0)
+ return r;
- r = sd_bus_message_enter_container(reply, 'a', "(sst)");
- if (r < 0)
- return bus_log_parse_error(r);
+ break;
- for (;;) {
- const char *name, *pem;
+ case ARG_PROMPT_GROUPS:
+ r = parse_boolean_argument("--prompt-groups=", optarg, &arg_prompt_groups);
+ if (r < 0)
+ return r;
- 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);
+ case ARG_CHROME:
+ r = parse_boolean_argument("--chrome=", optarg, &arg_chrome);
if (r < 0)
return r;
- _cleanup_(EVP_PKEY_freep) EVP_PKEY *key = NULL;
- r = openssl_pubkey_from_pem(pem, SIZE_MAX, &key);
+ break;
+
+ case ARG_MUTE_CONSOLE:
+ r = parse_boolean_argument("--mute-console=", optarg, &arg_mute_console);
if (r < 0)
- return log_error_errno(r, "Failed to parse PEM: %m");
+ return r;
- _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.");
+ break;
- 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();
- }
+ case '?':
+ return -EINVAL;
- r = table_add_many(
- table,
- TABLE_STRING, name,
- TABLE_STRING, h ?: pem);
- if (r < 0)
- return table_log_add_error(r);
+ default:
+ assert_not_reached();
+ }
}
- 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);
+ if (!strv_isempty(arg_languages)) {
+ char **additional;
- r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
+ r = sd_json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", arg_languages[0]);
if (r < 0)
- return r;
- }
+ return log_error_errno(r, "Failed to update preferred language: %m");
- 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);
+ additional = strv_skip(arg_languages, 1);
+ if (!strv_isempty(additional)) {
+ r = sd_json_variant_set_field_strv(&arg_identity_extra, "additionalLanguages", additional);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update additional language list: %m");
+ } else {
+ r = drop_from_identity("additionalLanguages");
+ if (r < 0)
+ return r;
+ }
}
- return 0;
+ return 1;
}
-static int verb_get_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
- int r;
+static int redirect_bus_mgr(void) {
+ const char *suffix;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- r = acquire_bus(&bus);
- if (r < 0)
- return r;
+ /* Talk to a different service if that's requested. (The same env var is also understood by homed, so
+ * that it is relatively easily possible to invoke a second instance of homed for debug purposes and
+ * have homectl talk to it, without colliding with the host version. This is handy when operating
+ * from a homed-managed account.) */
- 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;
- }
+ suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
+ if (suffix) {
+ static BusLocator locator = {
+ .path = "/org/freedesktop/home1",
+ .interface = "org.freedesktop.home1.Manager",
+ };
- const char *pem;
- r = sd_bus_message_read(reply, "st", &pem, NULL);
- if (r < 0) {
- RET_GATHER(ret, bus_log_parse_error(r));
- continue;
- }
+ /* Yes, we leak this memory, but there's little point to collect this, given that we only do
+ * this in a debug environment, do it only once, and the string shall live for out entire
+ * process runtime. */
- fputs(pem, stdout);
- if (!endswith(pem, "\n"))
- fputc('\n', stdout);
+ locator.destination = strjoin("org.freedesktop.home1.", suffix);
+ if (!locator.destination)
+ return log_oom();
- fflush(stdout);
- }
+ bus_mgr = &locator;
+ } else
+ bus_mgr = bus_home_mgr;
- return ret;
+ return 0;
}
-static int add_signing_key_one(sd_bus *bus, const char *fn, FILE *key) {
- int r;
+static bool is_fallback_shell(const char *p) {
+ const char *q;
- assert_se(bus);
- assert_se(fn);
- assert_se(key);
+ if (!p)
+ return false;
- _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);
+ if (p[0] == '-') {
+ /* Skip over login shell dash */
+ p++;
- _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));
+ if (streq(p, "ystemd-home-fallback-shell")) /* maybe the dash was used to override the binary name? */
+ return true;
+ }
- return 0;
-}
+ q = strrchr(p, '/'); /* Skip over path */
+ if (q)
+ p = q + 1;
-static int verb_add_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
- int r;
+ return streq(p, "systemd-home-fallback-shell");
+}
+static int fallback_shell(int argc, char *argv[]) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL, *hr = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+ _cleanup_free_ char *argv0 = NULL;
+ const char *json, *hd, *shell;
+ int r, incomplete;
+
+ /* So here's the deal: if users log into a system via ssh, and their homed-managed home directory
+ * wasn't activated yet, SSH will permit the access but the home directory isn't actually available
+ * yet. SSH doesn't allow us to ask authentication questions from the PAM session stack, and doesn't
+ * run the PAM authentication stack (because it authenticates via its own key management, after
+ * all). So here's our way to support this: homectl can be invoked as a multi-call binary under the
+ * name "systemd-home-fallback-shell". If so, it will chainload a login shell, but first try to
+ * unlock the home directory of the user it is invoked as. systemd-homed will then override the shell
+ * listed in user records whose home directory is not activated yet with this pseudo-shell. Net
+ * effect: one SSH auth succeeds this pseudo shell gets invoked, which will unlock the homedir
+ * (possibly asking for a passphrase) and then chainload the regular shell. Once the login is
+ * complete the user record will look like any other. */
+
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.");
+ for (unsigned n_tries = 0;; n_tries++) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
- 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.");
+ if (n_tries >= 5)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to activate home dir, even after %u tries.", n_tries);
- STRV_FOREACH(k, strv_skip(argv, 1)) {
+ /* Let's start by checking if this all is even necessary, i.e. if the useFallback boolean field is actually set. */
+ r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", NULL); /* empty user string means: our calling user */
+ if (r < 0)
+ return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
- if (streq(*k, "-"))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing to read from standard input if multiple keys are specified.");
+ r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
+ if (r < 0)
+ return bus_log_parse_error(r);
- _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;
- }
- }
+ r = sd_json_parse(json, SD_JSON_PARSE_SENSITIVE|SD_JSON_PARSE_MUST_BE_OBJECT, &v, /* reterr_line= */ NULL, /* reterr_column= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON identity: %m");
- _cleanup_fclose_ FILE *f = fopen(*k, "re");
- if (!f) {
- RET_GATHER(ret, log_error_errno(errno, "Failed to open '%s': %m", *k));
- continue;
- }
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
- RET_GATHER(ret, add_signing_key_one(bus, fn ?: arg_key_name, f));
- }
- }
+ r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG|USER_RECORD_PERMISSIVE);
+ if (r < 0)
+ return r;
- return ret;
-}
+ if (!hr->use_fallback) /* Nice! We are done, fallback logic not necessary */
+ break;
-static int add_signing_keys_from_credentials(void) {
- int r;
+ if (!secret) {
+ r = acquire_passed_secrets(hr->user_name, &secret);
+ if (r < 0)
+ return 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");
+ for (;;) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
- _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");
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHomeIfReferenced");
+ if (r < 0)
+ return bus_log_create_error(r);
- 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;
+ r = sd_bus_message_append(m, "s", NULL); /* empty user string means: our calling user */
+ if (r < 0)
+ return bus_log_create_error(r);
- const char *e = startswith(de->d_name, "home.add-signing-key.");
- if (!e)
- continue;
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return bus_log_create_error(r);
- if (!filename_is_valid(e))
- continue;
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_REFERENCED))
+ return log_error_errno(r, "Called without reference on home taken, can't operate.");
- if (!bus) {
- r = acquire_bus(&bus);
- if (r < 0)
- return r;
- }
+ r = handle_generic_user_record_error(hr->user_name, secret, &error, r, false);
+ 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;
+ sd_bus_error_free(&error);
+ } else
+ break;
}
- RET_GATHER(ret, add_signing_key_one(bus, e, f));
+ /* Try again */
+ hr = user_record_unref(hr);
}
- return ret;
-}
+ incomplete = getenv_bool("XDG_SESSION_INCOMPLETE"); /* pam_systemd_home reports this state via an environment variable to us. */
+ if (incomplete < 0 && incomplete != -ENXIO)
+ return log_error_errno(incomplete, "Failed to parse $XDG_SESSION_INCOMPLETE environment variable: %m");
+ if (incomplete > 0) {
+ /* We are still in an "incomplete" session here. Now upgrade it to a full one. This will make logind
+ * start the user@.service instance for us. */
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.login1",
+ "/org/freedesktop/login1/session/self",
+ "org.freedesktop.login1.Session",
+ "SetClass",
+ &error,
+ /* ret_reply= */ NULL,
+ "s",
+ "user");
+ if (r < 0)
+ return log_error_errno(r, "Failed to upgrade session: %s", bus_error_message(&error, r));
-static int remove_signing_key_one(sd_bus *bus, const char *fn) {
- int r;
+ if (setenv("XDG_SESSION_CLASS", "user", /* overwrite= */ true) < 0) /* Update the XDG_SESSION_CLASS environment variable to match the above */
+ return log_error_errno(errno, "Failed to set $XDG_SESSION_CLASS: %m");
- assert_se(bus);
- assert_se(fn);
+ if (unsetenv("XDG_SESSION_INCOMPLETE") < 0) /* Unset the 'incomplete' env var */
+ return log_error_errno(errno, "Failed to unset $XDG_SESSION_INCOMPLETE: %m");
+ }
- _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));
+ /* We are going to invoke execv() soon. Let's be extra accurate and flush/close our bus connection
+ * first, just to make sure anything queued is flushed out (though there shouldn't be anything) */
+ bus = sd_bus_flush_close_unref(bus);
- return 0;
-}
+ assert(!hr->use_fallback);
+ assert_se(shell = user_record_shell(hr));
+ assert_se(hd = user_record_home_directory(hr));
-static int verb_remove_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
- int r;
+ /* Extra protection: avoid loops */
+ if (is_fallback_shell(shell))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Primary shell of '%s' is fallback shell, refusing loop.", hr->user_name);
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- r = acquire_bus(&bus);
+ if (chdir(hd) < 0)
+ return log_error_errno(errno, "Failed to change directory to home directory '%s': %m", hd);
+
+ if (setenv("SHELL", shell, /* overwrite= */ true) < 0)
+ return log_error_errno(errno, "Failed to set $SHELL: %m");
+
+ if (setenv("HOME", hd, /* overwrite= */ true) < 0)
+ return log_error_errno(errno, "Failed to set $HOME: %m");
+
+ /* Paranoia: in case the client passed some passwords to us to help us unlock, unlock things now */
+ FOREACH_STRING(ue, "PASSWORD", "NEWPASSWORD", "PIN")
+ if (unsetenv(ue) < 0)
+ return log_error_errno(errno, "Failed to unset $%s: %m", ue);
+
+ r = path_extract_filename(shell, &argv0);
if (r < 0)
- return r;
+ return log_error_errno(r, "Unable to extract file name from '%s': %m", shell);
+ if (r == O_DIRECTORY)
+ return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Shell '%s' is a path to a directory, refusing.", shell);
- r = EXIT_SUCCESS;
- STRV_FOREACH(k, strv_skip(argv, 1))
- RET_GATHER(r, remove_signing_key_one(bus, *k));
+ /* Invoke this as login shell, by setting argv[0][0] to '-' (unless we ourselves weren't called as login shell) */
+ if (!argv || isempty(argv[0]) || argv[0][0] == '-') {
+ _cleanup_free_ char *prefixed = strjoin("-", argv0);
+ if (!prefixed)
+ return log_oom();
- return r;
+ free_and_replace(argv0, prefixed);
+ }
+
+ l = strv_new(argv0);
+ if (!l)
+ return log_oom();
+
+ if (strv_extend_strv(&l, strv_skip(argv, 1), /* filter_duplicates= */ false) < 0)
+ return log_oom();
+
+ execv(shell, l);
+ return log_error_errno(errno, "Failed to execute shell '%s': %m", shell);
}
static int run(int argc, char *argv[]) {