Merge pull request #30150 from poettering/homectl-interactive
add "homectl firstboot" verb, that runs at first boot and can create a user, interactively or from creds
This commit is contained in:
@@ -1655,8 +1655,8 @@ static int run(int argc, char *argv[]) {
|
||||
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, terminating.");
|
||||
return 0; /* disabled */
|
||||
log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
|
||||
arg_prompt_locale = arg_prompt_keymap = arg_prompt_timezone = arg_prompt_hostname = arg_prompt_root_password = arg_prompt_root_shell = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+254
-34
@@ -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)
|
||||
@@ -1146,7 +1155,18 @@ static int acquire_new_home_record(UserRecord **ret) {
|
||||
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;
|
||||
|
||||
@@ -1247,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;
|
||||
@@ -1258,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;
|
||||
|
||||
@@ -1374,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;
|
||||
@@ -2131,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;
|
||||
|
||||
@@ -2187,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"
|
||||
@@ -2205,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"
|
||||
@@ -2412,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[] = {
|
||||
@@ -2504,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 },
|
||||
{}
|
||||
};
|
||||
|
||||
@@ -3788,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;
|
||||
|
||||
@@ -3854,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 },
|
||||
{}
|
||||
};
|
||||
|
||||
|
||||
@@ -100,6 +100,17 @@ int get_encrypted_credentials_dir(const char **ret) {
|
||||
return get_credentials_dir_internal("ENCRYPTED_CREDENTIALS_DIRECTORY", ret);
|
||||
}
|
||||
|
||||
int open_credentials_dir(void) {
|
||||
const char *d;
|
||||
int r;
|
||||
|
||||
r = get_credentials_dir(&d);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return RET_NERRNO(open(d, O_CLOEXEC|O_DIRECTORY));
|
||||
}
|
||||
|
||||
int read_credential(const char *name, void **ret, size_t *ret_size) {
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
const char *d;
|
||||
|
||||
@@ -31,6 +31,8 @@ bool credential_glob_valid(const char *s);
|
||||
int get_credentials_dir(const char **ret);
|
||||
int get_encrypted_credentials_dir(const char **ret);
|
||||
|
||||
int open_credentials_dir(void);
|
||||
|
||||
/* Where creds have been passed to the system */
|
||||
#define SYSTEM_CREDENTIALS_DIRECTORY "/run/credentials/@system"
|
||||
#define ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY "/run/credentials/@encrypted"
|
||||
|
||||
Reference in New Issue
Block a user