#include "cap-list.h"
#include "capability-util.h"
#include "cgroup-util.h"
+#include "creds-util.h"
#include "dns-domain.h"
#include "env-util.h"
#include "fd-util.h"
#include "pager.h"
#include "parse-argument.h"
#include "parse-util.h"
+#include "password-quality-util.h"
#include "path-util.h"
#include "percent-util.h"
#include "pkcs11-util.h"
#include "pretty-print.h"
+#include "proc-cmdline.h"
#include "process-util.h"
-#include "pwquality-util.h"
+#include "recurse-dir.h"
#include "rlimit-util.h"
#include "spawn-polkit-agent.h"
#include "terminal-util.h"
#include "uid-alloc-range.h"
#include "user-record.h"
-#include "user-record-pwquality.h"
+#include "user-record-password-quality.h"
#include "user-record-show.h"
#include "user-record-util.h"
#include "user-util.h"
+#include "userdb.h"
#include "verbs.h"
static PagerFlags arg_pager_flags = 0;
} arg_export_format = EXPORT_FORMAT_FULL;
static uint64_t arg_capability_bounding_set = UINT64_MAX;
static uint64_t arg_capability_ambient_set = UINT64_MAX;
+static bool arg_prompt_new_user = false;
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
bool emphasize_current,
AskPasswordFlags flags) {
- _cleanup_(strv_free_erasep) char **password = NULL;
+ _cleanup_strv_free_erase_ char **password = NULL;
_cleanup_(erase_and_freep) char *envpw = NULL;
_cleanup_free_ char *question = NULL;
int r;
UserRecord *hr,
AskPasswordFlags flags) {
- _cleanup_(strv_free_erasep) char **recovery_key = NULL;
+ _cleanup_strv_free_erase_ char **recovery_key = NULL;
_cleanup_(erase_and_freep) char *envpw = NULL;
_cleanup_free_ char *question = NULL;
int r;
UserRecord *hr,
AskPasswordFlags flags) {
- _cleanup_(strv_free_erasep) char **pin = NULL;
+ _cleanup_strv_free_erase_ char **pin = NULL;
_cleanup_(erase_and_freep) char *envpin = NULL;
_cleanup_free_ char *question = NULL;
int r;
static int inspect_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_(strv_freep) char **mangled_list = NULL;
+ _cleanup_strv_free_ char **mangled_list = NULL;
int r, ret = 0;
char **items;
static int authenticate_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_(strv_freep) char **mangled_list = NULL;
+ _cleanup_strv_free_ char **mangled_list = NULL;
int r, ret = 0;
char **items;
if (r < 0)
return log_error_errno(r, "Failed to filter identity: %m");
- r = json_variant_merge(&v, arg_identity_extra);
+ r = json_variant_merge_object(&v, arg_identity_extra);
if (r < 0)
return log_error_errno(r, "Failed to merge identities: %m");
if (!json_variant_equal(u, mmid))
continue;
- r = json_variant_merge(&add, z);
+ r = json_variant_merge_object(&add, z);
if (r < 0)
return log_error_errno(r, "Failed to merge perMachine entry: %m");
if (r < 0)
return log_error_errno(r, "Failed to filter perMachine: %m");
- r = json_variant_merge(&add, arg_identity_extra_this_machine);
+ r = json_variant_merge_object(&add, arg_identity_extra_this_machine);
if (r < 0)
return log_error_errno(r, "Failed to merge in perMachine fields: %m");
if (r < 0)
return log_error_errno(r, "Failed to filter resource limits: %m");
- r = json_variant_merge(&rlv, arg_identity_extra_rlimits);
+ r = json_variant_merge_object(&rlv, arg_identity_extra_rlimits);
if (r < 0)
return log_error_errno(r, "Failed to set resource limits: %m");
if (r < 0)
return log_error_errno(r, "Failed to filter identity (privileged part): %m");
- r = json_variant_merge(&privileged, arg_identity_extra_privileged);
+ r = json_variant_merge_object(&privileged, arg_identity_extra_privileged);
if (r < 0)
return log_error_errno(r, "Failed to merge identities (privileged part): %m");
return 1;
}
-static int acquire_new_home_record(UserRecord **ret) {
+static int acquire_new_home_record(JsonVariant *input, UserRecord **ret) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
int r;
if (arg_identity) {
unsigned line, column;
+ if (input)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Two identity records specified, refusing.");
+
r = json_parse_file(
streq(arg_identity, "-") ? stdin : NULL,
streq(arg_identity, "-") ? "<stdin>" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column);
if (r < 0)
return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
- }
+ } else
+ v = json_variant_ref(input);
r = apply_identity_changes(&v);
if (r < 0)
if (!hr)
return log_oom();
- r = user_record_load(hr, v, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG|USER_RECORD_PERMISSIVE);
+ r = user_record_load(
+ hr,
+ v,
+ USER_RECORD_REQUIRE_REGULAR|
+ USER_RECORD_ALLOW_SECRET|
+ USER_RECORD_ALLOW_PRIVILEGED|
+ USER_RECORD_ALLOW_PER_MACHINE|
+ USER_RECORD_STRIP_BINDING|
+ USER_RECORD_STRIP_STATUS|
+ USER_RECORD_STRIP_SIGNATURE|
+ USER_RECORD_LOG|
+ USER_RECORD_PERMISSIVE);
if (r < 0)
return r;
(void) suggest_passwords();
for (;;) {
- _cleanup_(strv_free_erasep) char **first = NULL, **second = NULL;
+ _cleanup_strv_free_erase_ char **first = NULL, **second = NULL;
_cleanup_free_ char *question = NULL;
if (--i == 0)
}
}
-static int create_home(int argc, char *argv[], void *userdata) {
+static int create_home_common(JsonVariant *input) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
int r;
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
- if (argc >= 2) {
- /* If a username was specified, use it */
-
- if (valid_user_group_name(argv[1], 0))
- r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
- else {
- _cleanup_free_ char *un = NULL, *rr = NULL;
-
- /* Before we consider the user name invalid, let's check if we can split it? */
- r = split_user_name_realm(argv[1], &un, &rr);
- if (r < 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
-
- if (rr) {
- r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
- if (r < 0)
- return log_error_errno(r, "Failed to set realm field: %m");
- }
-
- r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
- }
- if (r < 0)
- return log_error_errno(r, "Failed to set userName field: %m");
- } else {
- /* If neither a username nor an identity have been specified we cannot operate. */
- if (!arg_identity)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
- }
-
- r = acquire_new_home_record(&hr);
+ r = acquire_new_home_record(input, &hr);
if (r < 0)
return r;
/* If password quality enforcement is disabled, let's at least warn client side */
- r = user_record_quality_check_password(hr, hr, &error);
+ r = user_record_check_password_quality(hr, hr, &error);
if (r < 0)
log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", bus_error_message(&error, r));
}
return 0;
}
+static int create_home(int argc, char *argv[], void *userdata) {
+ int r;
+
+ if (argc >= 2) {
+ /* If a username was specified, use it */
+
+ if (valid_user_group_name(argv[1], 0))
+ r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
+ else {
+ _cleanup_free_ char *un = NULL, *rr = NULL;
+
+ /* Before we consider the user name invalid, let's check if we can split it? */
+ r = split_user_name_realm(argv[1], &un, &rr);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
+
+ if (rr) {
+ r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set realm field: %m");
+ }
+
+ r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to set userName field: %m");
+ } else {
+ /* If neither a username nor an identity have been specified we cannot operate. */
+ if (!arg_identity)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
+ }
+
+ return create_home_common(/* input= */ NULL);
+}
+
static int remove_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
int r, ret = 0;
if (r < 0)
return bus_log_parse_error(r);
- r = safe_fork("(with)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REOPEN_LOG, &pid);
+ r = safe_fork("(with)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REOPEN_LOG, &pid);
if (r < 0)
return r;
if (r == 0) {
return 0;
}
+static int create_from_credentials(void) {
+ _cleanup_close_ int fd = -EBADF;
+ int ret = 0, n_created = 0, r;
+
+ fd = open_credentials_dir();
+ if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */
+ return 0;
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to open credentials directory: %m");
+
+ _cleanup_free_ DirectoryEntries *des = NULL;
+ r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate credentials: %m");
+
+ FOREACH_ARRAY(i, des->entries, des->n_entries) {
+ _cleanup_(json_variant_unrefp) JsonVariant *identity = NULL;
+ struct dirent *de = *i;
+ const char *e;
+
+ if (de->d_type != DT_REG)
+ continue;
+
+ e = startswith(de->d_name, "home.create.");
+ if (!e)
+ continue;
+
+ if (!valid_user_group_name(e, 0)) {
+ log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name);
+ continue;
+ }
+
+ r = json_parse_file_at(
+ /* f= */ NULL,
+ fd,
+ de->d_name,
+ /* flags= */ 0,
+ &identity,
+ /* ret_line= */ NULL,
+ /* ret_column= */ NULL);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse user record in credential '%s', ignoring: %m", de->d_name);
+ continue;
+ }
+
+ JsonVariant *un;
+ un = json_variant_by_key(identity, "userName");
+ if (un) {
+ if (!json_variant_is_string(un)) {
+ log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name);
+ continue;
+ }
+
+ if (!streq(json_variant_string(un), e)) {
+ log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, json_variant_string(un), e);
+ continue;
+ }
+ } else {
+ r = json_variant_set_field_string(&identity, "userName", e);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to set userName field: %m");
+ }
+
+ log_notice("Processing user '%s' from credentials.", e);
+
+ r = create_home_common(identity);
+ if (r >= 0)
+ n_created++;
+
+ RET_GATHER(ret, r);
+ }
+
+ return ret < 0 ? ret : n_created;
+}
+
+static int has_regular_user(void) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ int r;
+
+ r = userdb_all(USERDB_SUPPRESS_SHADOW, &iterator);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create user enumerator: %m");
+
+ for (;;) {
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+
+ r = userdb_iterator_get(iterator, &ur);
+ if (r == -ESRCH)
+ break;
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate users: %m");
+
+ if (user_record_disposition(ur) == USER_REGULAR)
+ return true;
+ }
+
+ return false;
+}
+
+static int create_interactively(void) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_free_ char *username = NULL;
+ int r;
+
+ if (!arg_prompt_new_user) {
+ log_debug("Prompting for user creation was not requested.");
+ return 0;
+ }
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ (void) reset_terminal_fd(STDIN_FILENO, /* switch_to_text= */ false);
+
+ for (;;) {
+ username = mfree(username);
+
+ r = ask_string(&username,
+ "%s Please enter user name to create (empty to skip): ",
+ special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET));
+ if (r < 0)
+ return log_error_errno(r, "Failed to query user for username: %m");
+
+ if (isempty(username)) {
+ log_info("No data entered, skipping.");
+ return 0;
+ }
+
+ if (!valid_user_group_name(username, /* flags= */ 0)) {
+ log_notice("Specified user name is not a valid UNIX user name, try again: %s", username);
+ continue;
+ }
+
+ r = userdb_by_name(username, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL);
+ if (r == -ESRCH)
+ break;
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", username);
+
+ log_notice("Specified user '%s' exists already, try again.", username);
+ }
+
+ r = json_variant_set_field_string(&arg_identity_extra, "userName", username);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set userName field: %m");
+
+ return create_home_common(/* input= */ NULL);
+}
+
+static int verb_firstboot(int argc, char *argv[], void *userdata) {
+ int r;
+
+ /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
+ * tool. */
+
+ bool enabled;
+ r = proc_cmdline_get_bool("systemd.firstboot", /* flags = */ 0, &enabled);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
+ if (r > 0 && !enabled) {
+ log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
+ arg_prompt_new_user = false;
+ }
+
+ r = create_from_credentials();
+ if (r < 0)
+ return r;
+ if (r > 0) /* Already created users from credentials */
+ return 0;
+
+ r = has_regular_user();
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ log_info("Regular user already present in user database, skipping user creation.");
+ return 0;
+ }
+
+ return create_interactively();
+}
+
static int drop_from_identity(const char *field) {
int r;
" deactivate-all Deactivate all active home areas\n"
" rebalance Rebalance free space between home areas\n"
" with USER [COMMAND…] Run shell or command with access to a home area\n"
+ " firstboot Run first-boot home area creation wizard\n"
"\n%4$sOptions:%5$s\n"
" -h --help Show this help\n"
" --version Show package version\n"
" -E When specified once equals -j --export-format=\n"
" stripped, when specified twice equals\n"
" -j --export-format=minimal\n"
+ " --prompt-new-user firstboot: Query user interactively for user\n"
+ " to create\n"
"\n%4$sGeneral User Record Properties:%5$s\n"
" -c --real-name=REALNAME Real name for user\n"
" --realm=REALM Realm to create user in\n"
ARG_FIDO2_CRED_ALG,
ARG_CAPABILITY_BOUNDING_SET,
ARG_CAPABILITY_AMBIENT_SET,
+ ARG_PROMPT_NEW_USER,
};
static const struct option options[] = {
{ "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 },
{}
};
}
case ARG_RLIMIT: {
- _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *jcur = NULL, *jmax = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *jcur = NULL, *jmax = NULL;
_cleanup_free_ char *field = NULL, *t = NULL;
const char *eq;
struct rlimit rl;
if (r < 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate maximum integer: %m");
- r = json_build(&v,
- JSON_BUILD_OBJECT(
- JSON_BUILD_PAIR("cur", JSON_BUILD_VARIANT(jcur)),
- JSON_BUILD_PAIR("max", JSON_BUILD_VARIANT(jmax))));
- if (r < 0)
- return log_error_errno(r, "Failed to build resource limit: %m");
-
t = strjoin("RLIMIT_", rlimit_to_string(l));
if (!t)
return log_oom();
- r = json_variant_set_field(&arg_identity_extra_rlimits, t, v);
+ r = json_variant_set_fieldb(
+ &arg_identity_extra_rlimits, t,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("cur", JSON_BUILD_VARIANT(jcur)),
+ JSON_BUILD_PAIR("max", JSON_BUILD_VARIANT(jmax))));
if (r < 0)
return log_error_errno(r, "Failed to set %s field: %m", rlimit_to_string(l));
case ARG_SSH_AUTHORIZED_KEYS: {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
- _cleanup_(strv_freep) char **l = NULL, **add = NULL;
+ _cleanup_strv_free_ char **l = NULL, **add = NULL;
if (isempty(optarg)) {
r = drop_from_identity("sshAuthorizedKeys");
strv_uniq(arg_fido2_device);
break;
- case ARG_FIDO2_WITH_PIN: {
- bool lock_with_pin;
-
- r = parse_boolean_argument("--fido2-with-client-pin=", optarg, &lock_with_pin);
+ case ARG_FIDO2_WITH_PIN:
+ r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL);
if (r < 0)
return r;
- SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, lock_with_pin);
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r);
break;
- }
- case ARG_FIDO2_WITH_UP: {
- bool lock_with_up;
-
- r = parse_boolean_argument("--fido2-with-user-presence=", optarg, &lock_with_up);
+ case ARG_FIDO2_WITH_UP:
+ r = parse_boolean_argument("--fido2-with-user-presence=", optarg, NULL);
if (r < 0)
return r;
- SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, lock_with_up);
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r);
break;
- }
- case ARG_FIDO2_WITH_UV: {
- bool lock_with_uv;
-
- r = parse_boolean_argument("--fido2-with-user-verification=", optarg, &lock_with_uv);
+ case ARG_FIDO2_WITH_UV:
+ r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL);
if (r < 0)
return r;
- SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, lock_with_uv);
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r);
break;
- }
case ARG_RECOVERY_KEY:
r = parse_boolean(optarg);
break;
case ARG_DROP_CACHES: {
- bool drop_caches;
-
if (isempty(optarg)) {
r = drop_from_identity("dropCaches");
if (r < 0)
break;
}
- r = parse_boolean_argument("--drop-caches=", optarg, &drop_caches);
+ r = parse_boolean_argument("--drop-caches=", optarg, NULL);
if (r < 0)
return r;
break;
}
+ case ARG_PROMPT_NEW_USER:
+ arg_prompt_new_user = true;
+ break;
+
case '?':
return -EINVAL;
{ "lock-all", VERB_ANY, 1, 0, lock_all_homes },
{ "deactivate-all", VERB_ANY, 1, 0, deactivate_all_homes },
{ "rebalance", VERB_ANY, 1, 0, rebalance },
+ { "firstboot", VERB_ANY, 1, 0, verb_firstboot },
{}
};