summaryrefslogtreecommitdiffstats
path: root/src/home/homectl.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 03:50:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 03:50:40 +0000
commitfc53809803cd2bc2434e312b19a18fa36776da12 (patch)
treeb4b43bd6538f51965ce32856e9c053d0f90919c8 /src/home/homectl.c
parentAdding upstream version 255.5. (diff)
downloadsystemd-fc53809803cd2bc2434e312b19a18fa36776da12.tar.xz
systemd-fc53809803cd2bc2434e312b19a18fa36776da12.zip
Adding upstream version 256.upstream/256
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/home/homectl.c')
-rw-r--r--src/home/homectl.c1094
1 files changed, 907 insertions, 187 deletions
diff --git a/src/home/homectl.c b/src/home/homectl.c
index a6951c8..d9321a2 100644
--- a/src/home/homectl.c
+++ b/src/home/homectl.c
@@ -12,6 +12,9 @@
#include "cap-list.h"
#include "capability-util.h"
#include "cgroup-util.h"
+#include "copy.h"
+#include "creds-util.h"
+#include "dirent-util.h"
#include "dns-domain.h"
#include "env-util.h"
#include "fd-util.h"
@@ -19,6 +22,7 @@
#include "format-table.h"
#include "fs-util.h"
#include "glyph-util.h"
+#include "hashmap.h"
#include "home-util.h"
#include "homectl-fido2.h"
#include "homectl-pkcs11.h"
@@ -35,16 +39,21 @@
#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 "rm-rf.h"
#include "spawn-polkit-agent.h"
#include "terminal-util.h"
-#include "uid-alloc-range.h"
+#include "tmpfile-util.h"
+#include "uid-classification.h"
#include "user-record.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;
@@ -52,6 +61,7 @@ static bool arg_legend = true;
static bool arg_ask_password = true;
static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
static const char *arg_host = NULL;
+static bool arg_offline = false;
static const char *arg_identity = NULL;
static JsonVariant *arg_identity_extra = NULL;
static JsonVariant *arg_identity_extra_privileged = NULL;
@@ -80,6 +90,10 @@ 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 char *arg_blob_dir = NULL;
+static bool arg_blob_clear = false;
+static Hashmap *arg_blob_files = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
@@ -89,6 +103,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_blob_dir, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_blob_files, hashmap_freep);
static const BusLocator *bus_mgr;
@@ -102,7 +118,10 @@ static bool identity_properties_specified(void) {
!strv_isempty(arg_identity_filter) ||
!strv_isempty(arg_identity_filter_rlimits) ||
!strv_isempty(arg_pkcs11_token_uri) ||
- !strv_isempty(arg_fido2_device);
+ !strv_isempty(arg_fido2_device) ||
+ arg_blob_dir ||
+ arg_blob_clear ||
+ !hashmap_isempty(arg_blob_files);
}
static int acquire_bus(sd_bus **bus) {
@@ -184,7 +203,7 @@ static int list_homes(int argc, char *argv[], void *userdata) {
if (r < 0)
return bus_log_parse_error(r);
- if (table_get_rows(table) > 1 || !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
+ if (!table_isempty(table) || !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
r = table_set_sort(table, (size_t) 0);
if (r < 0)
return table_log_sort_error(r);
@@ -194,11 +213,11 @@ static int list_homes(int argc, char *argv[], void *userdata) {
return r;
}
- if (arg_legend && (arg_json_format_flags & JSON_FORMAT_OFF)) {
- if (table_get_rows(table) > 1)
- printf("\n%zu home areas listed.\n", table_get_rows(table) - 1);
- else
+ if (arg_legend && !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
+ if (table_isempty(table))
printf("No home areas.\n");
+ else
+ printf("\n%zu home areas listed.\n", table_get_rows(table) - 1);
}
return 0;
@@ -243,14 +262,14 @@ static int acquire_existing_password(
user_name) < 0)
return log_oom();
- r = ask_password_auto(question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "home-password",
- /* credential_name= */ "home.password",
- USEC_INFINITY,
- flags,
- &password);
+ AskPasswordRequest req = {
+ .message = question,
+ .icon = "user-home",
+ .keyring = "home-password",
+ .credential = "home.password",
+ };
+
+ r = ask_password_auto(&req, USEC_INFINITY, flags, &password);
if (r == -EUNATCH) { /* EUNATCH is returned if no password was found and asking interactively was
* disabled via the flags. Not an error for us. */
log_debug_errno(r, "No passwords acquired.");
@@ -301,14 +320,14 @@ static int acquire_recovery_key(
if (asprintf(&question, "Please enter recovery key for user %s:", user_name) < 0)
return log_oom();
- r = ask_password_auto(question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "home-recovery-key",
- /* credential_name= */ "home.recovery-key",
- USEC_INFINITY,
- flags,
- &recovery_key);
+ AskPasswordRequest req = {
+ .message = question,
+ .icon = "user-home",
+ .keyring = "home-recovery-key",
+ .credential = "home.recovery-key",
+ };
+
+ r = ask_password_auto(&req, USEC_INFINITY, flags, &recovery_key);
if (r == -EUNATCH) { /* EUNATCH is returned if no recovery key was found and asking interactively was
* disabled via the flags. Not an error for us. */
log_debug_errno(r, "No recovery keys acquired.");
@@ -355,15 +374,14 @@ static int acquire_token_pin(
if (asprintf(&question, "Please enter security token PIN for user %s:", user_name) < 0)
return log_oom();
- r = ask_password_auto(
- question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "token-pin",
- /* credential_name= */ "home.token-pin",
- USEC_INFINITY,
- flags,
- &pin);
+ AskPasswordRequest req = {
+ .message = question,
+ .icon = "user-home",
+ .keyring = "token-pin",
+ .credential = "home.token-pin",
+ };
+
+ r = ask_password_auto(&req, USEC_INFINITY, flags, &pin);
if (r == -EUNATCH) { /* EUNATCH is returned if no PIN was found and asking interactively was disabled
* via the flags. Not an error for us. */
log_debug_errno(r, "No security token PINs acquired.");
@@ -735,7 +753,6 @@ static int inspect_home(int argc, char *argv[], void *userdata) {
r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", *i);
} else
r = bus_call_method(bus, bus_mgr, "GetUserRecordByUID", &error, &reply, "u", (uint32_t) uid);
-
if (r < 0) {
log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
if (ret == 0)
@@ -1092,7 +1109,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 +1119,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 +1167,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;
@@ -1191,19 +1223,22 @@ static int acquire_new_password(
_cleanup_free_ char *question = NULL;
if (--i == 0)
- return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up:");
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up.");
if (asprintf(&question, "Please enter new password for user %s:", user_name) < 0)
return log_oom();
+ AskPasswordRequest req = {
+ .message = question,
+ .icon = "user-home",
+ .keyring = "home-password",
+ .credential = "home.new-password",
+ };
+
r = ask_password_auto(
- question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "home-password",
- /* credential_name= */ "home.new-password",
+ &req,
USEC_INFINITY,
- 0, /* no caching, we want to collect a new password here after all */
+ /* flags= */ 0, /* no caching, we want to collect a new password here after all */
&first);
if (r < 0)
return log_error_errno(r, "Failed to acquire password: %m");
@@ -1212,14 +1247,12 @@ static int acquire_new_password(
if (asprintf(&question, "Please enter new password for user %s (repeat):", user_name) < 0)
return log_oom();
+ req.message = question;
+
r = ask_password_auto(
- question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "home-password",
- /* credential_name= */ "home.new-password",
+ &req,
USEC_INFINITY,
- 0, /* no caching */
+ /* flags= */ 0, /* no caching */
&second);
if (r < 0)
return log_error_errno(r, "Failed to acquire password: %m");
@@ -1247,47 +1280,145 @@ static int acquire_new_password(
}
}
-static int create_home(int argc, char *argv[], void *userdata) {
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+static int acquire_merged_blob_dir(UserRecord *hr, bool existing, Hashmap **ret) {
+ _cleanup_free_ char *sys_blob_path = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
+ _cleanup_closedir_ DIR *d = NULL;
+ const char *src_blob_path, *filename;
+ void *fd_ptr;
int r;
- r = acquire_bus(&bus);
- if (r < 0)
- return r;
+ assert(ret);
- (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+ HASHMAP_FOREACH_KEY(fd_ptr, filename, arg_blob_files) {
+ _cleanup_free_ char *filename_dup = NULL;
+ _cleanup_close_ int fd_dup = -EBADF;
- if (argc >= 2) {
- /* If a username was specified, use it */
+ filename_dup = strdup(filename);
+ if (!filename_dup)
+ return log_oom();
- if (valid_user_group_name(argv[1], 0))
- r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
+ if (PTR_TO_FD(fd_ptr) != -EBADF) {
+ fd_dup = fcntl(PTR_TO_FD(fd_ptr), F_DUPFD_CLOEXEC, 3);
+ if (fd_dup < 0)
+ return log_error_errno(errno, "Failed to duplicate fd of %s: %m", filename);
+ }
+
+ r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, filename_dup, FD_TO_PTR(fd_dup));
+ if (r < 0)
+ return r;
+ TAKE_PTR(filename_dup); /* Ownership transferred to hashmap */
+ TAKE_FD(fd_dup);
+ }
+
+ if (arg_blob_dir)
+ src_blob_path = arg_blob_dir;
+ else if (existing && !arg_blob_clear) {
+ if (hr->blob_directory)
+ src_blob_path = hr->blob_directory;
else {
- _cleanup_free_ char *un = NULL, *rr = NULL;
+ /* This isn't technically a correct thing to do for generic user records,
+ * so anyone looking at this code for reference shouldn't replicate it.
+ * However, since homectl is tied to homed, this is OK. This adds robustness
+ * for situations where the user record is coming directly from the CLI and
+ * thus doesn't have a blobDirectory set */
+
+ sys_blob_path = path_join(home_system_blob_dir(), hr->user_name);
+ if (!sys_blob_path)
+ return log_oom();
- /* 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]);
+ src_blob_path = sys_blob_path;
+ }
+ } else
+ goto nodir; /* Shortcut: no dir to merge with, so just return copy of arg_blob_files */
- 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");
- }
+ d = opendir(src_blob_path);
+ if (!d)
+ return log_error_errno(errno, "Failed to open %s: %m", src_blob_path);
- r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
+ FOREACH_DIRENT_ALL(de, d, return log_error_errno(errno, "Failed to read %s: %m", src_blob_path)) {
+ _cleanup_free_ char *name = NULL;
+ _cleanup_close_ int fd = -EBADF;
+
+ if (dot_or_dot_dot(de->d_name))
+ continue;
+
+ if (hashmap_contains(blobs, de->d_name))
+ continue; /* arg_blob_files should override the base dir */
+
+ if (!suitable_blob_filename(de->d_name)) {
+ log_warning("File %s in blob directory %s has an invalid filename. Skipping.", de->d_name, src_blob_path);
+ continue;
}
+
+ name = strdup(de->d_name);
+ if (!name)
+ return log_oom();
+
+ fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open %s in %s: %m", de->d_name, src_blob_path);
+
+ r = fd_verify_regular(fd);
+ if (r < 0) {
+ log_warning_errno(r, "Entry %s in blob directory %s is not a regular file. Skipping.", de->d_name, src_blob_path);
+ continue;
+ }
+
+ r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, name, FD_TO_PTR(fd));
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 r;
+ TAKE_PTR(name); /* Ownership transferred to hashmap */
+ TAKE_FD(fd);
+ }
+
+nodir:
+ *ret = TAKE_PTR(blobs);
+ return 0;
+}
+
+static int bus_message_append_blobs(sd_bus_message *m, Hashmap *blobs) {
+ const char *filename;
+ void *fd_ptr;
+ int r;
+
+ assert(m);
+
+ r = sd_bus_message_open_container(m, 'a', "{sh}");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH_KEY(fd_ptr, filename, blobs) {
+ int fd = PTR_TO_FD(fd_ptr);
+
+ if (fd == -EBADF) /* File marked for deletion */
+ continue;
+
+ r = sd_bus_message_append(m, "{sh}", filename, fd);
+ if (r < 0)
+ return r;
}
- r = acquire_new_home_record(&hr);
+ return sd_bus_message_close_container(m);
+}
+
+static int create_home_common(JsonVariant *input) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ r = acquire_new_home_record(input, &hr);
+ if (r < 0)
+ return r;
+
+ r = acquire_merged_blob_dir(hr, false, &blobs);
if (r < 0)
return r;
@@ -1337,7 +1468,7 @@ static int create_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return log_error_errno(r, "Failed to format user record: %m");
- r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHome");
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHomeEx");
if (r < 0)
return bus_log_create_error(r);
@@ -1347,6 +1478,14 @@ static int create_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return bus_log_create_error(r);
+ r = bus_message_append_blobs(m, blobs);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "t", UINT64_C(0));
+ 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) {
if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) {
@@ -1374,6 +1513,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.", 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;
@@ -1467,7 +1641,7 @@ static int acquire_updated_home_record(
reply = sd_bus_message_unref(reply);
- r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature"));
+ r = 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");
}
@@ -1537,7 +1711,9 @@ static int update_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL, *secret = NULL;
_cleanup_free_ char *buffer = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
const char *username;
+ uint64_t flags = 0;
int r;
if (argc >= 2)
@@ -1570,18 +1746,25 @@ static int update_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return r;
+ r = acquire_merged_blob_dir(hr, true, &blobs);
+ if (r < 0)
+ return r;
+
/* If we do multiple operations, let's output things more verbosely, since otherwise the repeated
* authentication might be confusing. */
if (arg_and_resize || arg_and_change_password)
log_info("Updating home directory.");
+ if (arg_offline)
+ flags |= SD_HOMED_UPDATE_OFFLINE;
+
for (;;) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
_cleanup_free_ char *formatted = NULL;
- r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHome");
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHomeEx");
if (r < 0)
return bus_log_create_error(r);
@@ -1595,6 +1778,14 @@ static int update_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return bus_log_create_error(r);
+ r = bus_message_append_blobs(m, blobs);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "t", flags);
+ 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) {
if (arg_and_change_password &&
@@ -2131,6 +2322,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,12 +2562,14 @@ 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"
" --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"
@@ -2205,6 +2582,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"
@@ -2222,7 +2601,7 @@ static int help(int argc, char *argv[], void *userdata) {
" --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 language\n"
+ " --language=LOCALE Set preferred languages\n"
" --ssh-authorized-keys=KEYS\n"
" Specify SSH public keys\n"
" --pkcs11-token-uri=URI URI to PKCS#11 security token containing\n"
@@ -2239,7 +2618,13 @@ static int help(int argc, char *argv[], void *userdata) {
" Whether to require user verification to unlock\n"
" the account\n"
" --recovery-key=BOOL Add a recovery key\n"
- "\n%4$sAccount Management User Record Properties:%5$s\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"
@@ -2322,6 +2707,9 @@ static int help(int argc, char *argv[], void *userdata) {
" --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(),
@@ -2334,12 +2722,14 @@ static int help(int argc, char *argv[], void *userdata) {
}
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_EMAIL_ADDRESS,
ARG_DISK_SIZE,
@@ -2397,6 +2787,8 @@ static int parse_argv(int argc, char *argv[]) {
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,
@@ -2412,98 +2804,108 @@ static int parse_argv(int argc, char *argv[]) {
ARG_FIDO2_CRED_ALG,
ARG_CAPABILITY_BOUNDING_SET,
ARG_CAPABILITY_AMBIENT_SET,
+ ARG_PROMPT_NEW_USER,
+ ARG_AVATAR,
+ ARG_LOGIN_BACKGROUND,
};
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 },
- { "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 },
- { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS },
- { "location", required_argument, NULL, ARG_LOCATION },
- { "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 },
- { "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 },
- { "and-resize", required_argument, NULL, ARG_AND_RESIZE },
- { "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD },
- { "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 },
+ { "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 },
+ { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS },
+ { "location", required_argument, NULL, ARG_LOCATION },
+ { "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 },
+ { "and-resize", required_argument, NULL, ARG_AND_RESIZE },
+ { "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD },
+ { "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 },
{}
};
@@ -2515,7 +2917,7 @@ static int parse_argv(int argc, char *argv[]) {
for (;;) {
int c;
- c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL);
+ c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPE", options, NULL);
if (c < 0)
break;
@@ -2539,6 +2941,10 @@ static int parse_argv(int argc, char *argv[]) {
arg_ask_password = false;
break;
+ case ARG_OFFLINE:
+ arg_offline = true;
+ break;
+
case 'H':
arg_transport = BUS_TRANSPORT_REMOTE;
arg_host = optarg;
@@ -2609,7 +3015,7 @@ static int parse_argv(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", optarg);
if (r == 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain: %m", optarg);
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain.", optarg);
r = json_variant_set_field_string(&arg_identity_extra, "realm", optarg);
if (r < 0)
@@ -2622,7 +3028,9 @@ static int parse_argv(int argc, char *argv[]) {
case ARG_CIFS_USER_NAME:
case ARG_CIFS_DOMAIN:
case ARG_CIFS_EXTRA_MOUNT_OPTIONS:
- case ARG_LUKS_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" :
@@ -2632,6 +3040,8 @@ static int parse_argv(int argc, char *argv[]) {
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);
@@ -2754,15 +3164,15 @@ static int parse_argv(int argc, char *argv[]) {
r = rlimit_parse(l, eq + 1, &rl);
if (r < 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse resource limit value: %s", eq + 1);
+ return log_error_errno(r, "Failed to parse resource limit value: %s", eq + 1);
r = rl.rlim_cur == RLIM_INFINITY ? json_variant_new_null(&jcur) : json_variant_new_unsigned(&jcur, rl.rlim_cur);
if (r < 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate current integer: %m");
+ return log_error_errno(r, "Failed to allocate current integer: %m");
r = rl.rlim_max == RLIM_INFINITY ? json_variant_new_null(&jmax) : json_variant_new_unsigned(&jmax, rl.rlim_max);
if (r < 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate maximum integer: %m");
+ return log_error_errno(r, "Failed to allocate maximum integer: %m");
t = strjoin("RLIMIT_", rlimit_to_string(l));
if (!t)
@@ -2906,26 +3316,46 @@ static int parse_argv(int argc, char *argv[]) {
break;
- case ARG_LANGUAGE:
- if (isempty(optarg)) {
- r = drop_from_identity("language");
+ case ARG_LANGUAGE: {
+ const char *p = optarg;
+
+ if (isempty(p)) {
+ r = drop_from_identity("preferredLanguage");
+ if (r < 0)
+ return r;
+
+ r = drop_from_identity("additionalLanguages");
if (r < 0)
return r;
+ arg_languages = strv_free(arg_languages);
break;
}
- if (!locale_is_valid(optarg))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg);
+ for (;;) {
+ _cleanup_free_ char *word = NULL;
- if (locale_is_installed(optarg) <= 0)
- log_warning("Locale '%s' is not installed, accepting anyway.", optarg);
+ r = extract_first_word(&p, &word, ",:", 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse locale list: %m");
+ if (r == 0)
+ break;
- r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", optarg);
- if (r < 0)
- return log_error_errno(r, "Failed to set preferredLanguage field: %m");
+ if (!locale_is_valid(word))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", word);
+
+ if (locale_is_installed(word) <= 0)
+ log_warning("Locale '%s' is not installed, accepting anyway.", word);
+
+ r = strv_consume(&arg_languages, TAKE_PTR(word));
+ if (r < 0)
+ return log_oom();
+
+ strv_uniq(arg_languages);
+ }
break;
+ }
case ARG_NOSUID:
case ARG_NODEV:
@@ -3788,6 +4218,82 @@ static int parse_argv(int argc, char *argv[]) {
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, false, &arg_blob_dir);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse path %s: %m", optarg);
+ 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, false, &path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse path %s: %m", eq + 1);
+ } 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();
+
+ r = parse_path_argument(optarg, false, &path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse path %s: %m", optarg);
+ }
+
+ 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 (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 */
+
+ 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);
+
+ break;
+ }
+
case '?':
return -EINVAL;
@@ -3802,6 +4308,25 @@ static int parse_argv(int argc, char *argv[]) {
if (arg_disk_size != UINT64_MAX || arg_disk_size_relative != UINT64_MAX)
arg_and_resize = true;
+ if (!strv_isempty(arg_languages)) {
+ char **additional;
+
+ r = 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");
+
+ additional = strv_skip(arg_languages, 1);
+ if (!strv_isempty(additional)) {
+ r = 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 1;
}
@@ -3835,6 +4360,197 @@ static int redirect_bus_mgr(void) {
return 0;
}
+static bool is_fallback_shell(const char *p) {
+ const char *q;
+
+ if (!p)
+ return false;
+
+ if (p[0] == '-') {
+ /* Skip over login shell dash */
+ p++;
+
+ if (streq(p, "ystemd-home-fallback-shell")) /* maybe the dash was used to override the binary name? */
+ return true;
+ }
+
+ q = strrchr(p, '/'); /* Skip over path */
+ if (q)
+ p = q + 1;
+
+ 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;
+
+ 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_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ if (n_tries >= 5)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to activate home dir, even after %u tries.", n_tries);
+
+ /* 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));
+
+ r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON identity: %m");
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG|USER_RECORD_PERMISSIVE);
+ if (r < 0)
+ return r;
+
+ if (!hr->use_fallback) /* Nice! We are done, fallback logic not necessary */
+ break;
+
+ if (!secret) {
+ r = acquire_passed_secrets(hr->user_name, &secret);
+ 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);
+
+ r = sd_bus_message_append(m, "s", NULL); /* empty user string means: our calling user */
+ 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) {
+ 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.");
+
+ r = handle_generic_user_record_error(hr->user_name, secret, &error, r, false);
+ if (r < 0)
+ return r;
+
+ sd_bus_error_free(&error);
+ } else
+ break;
+ }
+
+ /* Try again */
+ hr = user_record_unref(hr);
+ }
+
+ 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));
+
+ 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");
+
+ if (unsetenv("XDG_SESSION_INCOMPLETE") < 0) /* Unset the 'incomplete' env var */
+ return log_error_errno(errno, "Failed to unset $XDG_SESSION_INCOMPLETE: %m");
+ }
+
+ /* 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);
+
+ assert(!hr->use_fallback);
+ assert_se(shell = user_record_shell(hr));
+ assert_se(hd = user_record_home_directory(hr));
+
+ /* 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);
+
+ 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 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);
+
+ /* 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] == '-')
+ argv0[0] = '-';
+
+ 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[]) {
static const Verb verbs[] = {
{ "help", VERB_ANY, VERB_ANY, 0, help },
@@ -3854,6 +4570,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 },
{}
};
@@ -3865,6 +4582,9 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return r;
+ if (is_fallback_shell(argv[0]))
+ return fallback_shell(argc, argv);
+
r = parse_argv(argc, argv);
if (r <= 0)
return r;