homectl: add "firstboot" command

This extends what systemd-firstboot does and runs on first boots only
and either processes user records passed in via credentials to create,
or asks the user interactively to create one (only if no regular user
exists yet).
This commit is contained in:
Lennart Poettering
2023-12-18 11:10:53 +01:00
parent 0a9c4a1082
commit 3ccadbce33
8 changed files with 342 additions and 35 deletions
+242 -33
View File
@@ -12,6 +12,7 @@
#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"
@@ -35,7 +36,9 @@
#include "percent-util.h"
#include "pkcs11-util.h"
#include "pretty-print.h"
#include "proc-cmdline.h"
#include "process-util.h"
#include "recurse-dir.h"
#include "rlimit-util.h"
#include "spawn-polkit-agent.h"
#include "terminal-util.h"
@@ -45,6 +48,7 @@
#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;
@@ -80,6 +84,7 @@ static enum {
} 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);
@@ -1092,7 +1097,7 @@ static int add_disposition(JsonVariant **v) {
return 1;
}
static int acquire_new_home_record(UserRecord **ret) {
static int acquire_new_home_record(JsonVariant *input, UserRecord **ret) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
int r;
@@ -1102,12 +1107,16 @@ static int acquire_new_home_record(UserRecord **ret) {
if (arg_identity) {
unsigned line, column;
if (input)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Two identity records specified, refusing.");
r = json_parse_file(
streq(arg_identity, "-") ? stdin : NULL,
streq(arg_identity, "-") ? "<stdin>" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column);
if (r < 0)
return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
}
} else
v = json_variant_ref(input);
r = apply_identity_changes(&v);
if (r < 0)
@@ -1258,7 +1267,7 @@ static int acquire_new_password(
}
}
static int create_home(int argc, char *argv[], void *userdata) {
static int create_home_common(JsonVariant *input) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
int r;
@@ -1269,36 +1278,7 @@ static int create_home(int argc, char *argv[], void *userdata) {
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
if (argc >= 2) {
/* If a username was specified, use it */
if (valid_user_group_name(argv[1], 0))
r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
else {
_cleanup_free_ char *un = NULL, *rr = NULL;
/* Before we consider the user name invalid, let's check if we can split it? */
r = split_user_name_realm(argv[1], &un, &rr);
if (r < 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
if (rr) {
r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
if (r < 0)
return log_error_errno(r, "Failed to set realm field: %m");
}
r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
}
if (r < 0)
return log_error_errno(r, "Failed to set userName field: %m");
} else {
/* If neither a username nor an identity have been specified we cannot operate. */
if (!arg_identity)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
}
r = acquire_new_home_record(&hr);
r = acquire_new_home_record(input, &hr);
if (r < 0)
return r;
@@ -1385,6 +1365,41 @@ static int create_home(int argc, char *argv[], void *userdata) {
return 0;
}
static int create_home(int argc, char *argv[], void *userdata) {
int r;
if (argc >= 2) {
/* If a username was specified, use it */
if (valid_user_group_name(argv[1], 0))
r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
else {
_cleanup_free_ char *un = NULL, *rr = NULL;
/* Before we consider the user name invalid, let's check if we can split it? */
r = split_user_name_realm(argv[1], &un, &rr);
if (r < 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
if (rr) {
r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
if (r < 0)
return log_error_errno(r, "Failed to set realm field: %m");
}
r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
}
if (r < 0)
return log_error_errno(r, "Failed to set userName field: %m");
} else {
/* If neither a username nor an identity have been specified we cannot operate. */
if (!arg_identity)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
}
return create_home_common(/* input= */ NULL);
}
static int remove_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
int r, ret = 0;
@@ -2142,6 +2157,190 @@ static int rebalance(int argc, char *argv[], void *userdata) {
return 0;
}
static int create_from_credentials(void) {
_cleanup_close_ int fd = -EBADF;
int ret = 0, n_created = 0, r;
fd = open_credentials_dir();
if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */
return 0;
if (fd < 0)
return log_error_errno(fd, "Failed to open credentials directory: %m");
_cleanup_free_ DirectoryEntries *des = NULL;
r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
if (r < 0)
return log_error_errno(r, "Failed to enumerate credentials: %m");
FOREACH_ARRAY(i, des->entries, des->n_entries) {
_cleanup_(json_variant_unrefp) JsonVariant *identity = NULL;
struct dirent *de = *i;
const char *e;
if (de->d_type != DT_REG)
continue;
e = startswith(de->d_name, "home.create.");
if (!e)
continue;
if (!valid_user_group_name(e, 0)) {
log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name);
continue;
}
r = json_parse_file_at(
/* f= */ NULL,
fd,
de->d_name,
/* flags= */ 0,
&identity,
/* ret_line= */ NULL,
/* ret_column= */ NULL);
if (r < 0) {
log_warning_errno(r, "Failed to parse user record in credential '%s', ignoring: %m", de->d_name);
continue;
}
JsonVariant *un;
un = json_variant_by_key(identity, "userName");
if (un) {
if (!json_variant_is_string(un)) {
log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name);
continue;
}
if (!streq(json_variant_string(un), e)) {
log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, json_variant_string(un), e);
continue;
}
} else {
r = json_variant_set_field_string(&identity, "userName", e);
if (r < 0)
return log_warning_errno(r, "Failed to set userName field: %m");
}
log_notice("Processing user '%s' from credentials.", e);
r = create_home_common(identity);
if (r >= 0)
n_created++;
RET_GATHER(ret, r);
}
return ret < 0 ? ret : n_created;
}
static int has_regular_user(void) {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
int r;
r = userdb_all(USERDB_SUPPRESS_SHADOW, &iterator);
if (r < 0)
return log_error_errno(r, "Failed to create user enumerator: %m");
for (;;) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
r = userdb_iterator_get(iterator, &ur);
if (r == -ESRCH)
break;
if (r < 0)
return log_error_errno(r, "Failed to enumerate users: %m");
if (user_record_disposition(ur) == USER_REGULAR)
return true;
}
return false;
}
static int create_interactively(void) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_free_ char *username = NULL;
int r;
if (!arg_prompt_new_user) {
log_debug("Prompting for user creation was not requested.");
return 0;
}
r = acquire_bus(&bus);
if (r < 0)
return r;
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
(void) reset_terminal_fd(STDIN_FILENO, /* switch_to_text= */ false);
for (;;) {
username = mfree(username);
r = ask_string(&username,
"%s Please enter user name to create (empty to skip): ",
special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET));
if (r < 0)
return log_error_errno(r, "Failed to query user for username: %m");
if (isempty(username)) {
log_info("No data entered, skipping.");
return 0;
}
if (!valid_user_group_name(username, /* flags= */ 0)) {
log_notice("Specified user name is not a valid UNIX user name, try again: %s", username);
continue;
}
r = userdb_by_name(username, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL);
if (r == -ESRCH)
break;
if (r < 0)
return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", username);
log_notice("Specified user '%s' exists already, try again.", username);
}
r = json_variant_set_field_string(&arg_identity_extra, "userName", username);
if (r < 0)
return log_error_errno(r, "Failed to set userName field: %m");
return create_home_common(/* input= */ NULL);
}
static int verb_firstboot(int argc, char *argv[], void *userdata) {
int r;
/* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
* tool. */
bool enabled;
r = proc_cmdline_get_bool("systemd.firstboot", /* flags = */ 0, &enabled);
if (r < 0)
return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
if (r > 0 && !enabled) {
log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
arg_prompt_new_user = false;
}
r = create_from_credentials();
if (r < 0)
return r;
if (r > 0) /* Already created users from credentials */
return 0;
r = has_regular_user();
if (r < 0)
return r;
if (r > 0) {
log_info("Regular user already present in user database, skipping user creation.");
return 0;
}
return create_interactively();
}
static int drop_from_identity(const char *field) {
int r;
@@ -2198,6 +2397,7 @@ static int help(int argc, char *argv[], void *userdata) {
" deactivate-all Deactivate all active home areas\n"
" rebalance Rebalance free space between home areas\n"
" with USER [COMMAND…] Run shell or command with access to a home area\n"
" firstboot Run first-boot home area creation wizard\n"
"\n%4$sOptions:%5$s\n"
" -h --help Show this help\n"
" --version Show package version\n"
@@ -2216,6 +2416,8 @@ static int help(int argc, char *argv[], void *userdata) {
" -E When specified once equals -j --export-format=\n"
" stripped, when specified twice equals\n"
" -j --export-format=minimal\n"
" --prompt-new-user firstboot: Query user interactively for user\n"
" to create\n"
"\n%4$sGeneral User Record Properties:%5$s\n"
" -c --real-name=REALNAME Real name for user\n"
" --realm=REALM Realm to create user in\n"
@@ -2423,6 +2625,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_FIDO2_CRED_ALG,
ARG_CAPABILITY_BOUNDING_SET,
ARG_CAPABILITY_AMBIENT_SET,
ARG_PROMPT_NEW_USER,
};
static const struct option options[] = {
@@ -2515,6 +2718,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "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 },
{}
};
@@ -3799,6 +4003,10 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
case ARG_PROMPT_NEW_USER:
arg_prompt_new_user = true;
break;
case '?':
return -EINVAL;
@@ -3865,6 +4073,7 @@ static int run(int argc, char *argv[]) {
{ "lock-all", VERB_ANY, 1, 0, lock_all_homes },
{ "deactivate-all", VERB_ANY, 1, 0, deactivate_all_homes },
{ "rebalance", VERB_ANY, 1, 0, rebalance },
{ "firstboot", VERB_ANY, 1, 0, verb_firstboot },
{}
};