homed: add concept for "adopting" an existing homedir locally

Currently homed scans /home/ via inotify for new .home + .homedir/
popping up to register as local users. Let's also add an explicit way to
request this form of "adoption": a bus call that takes a path and that
makes a home dir activatable locally.

(Usecase: you cross boot between two systems – let's say your traditional
fedora and your ParticleOS – and want to use the same homedir from both:
simply mount the /home dir from the other somewhere, and then hit
"homectl adopt /somewhere/lennart.home" and you have the user locally
too).
This commit is contained in:
Lennart Poettering
2025-02-20 09:52:18 +01:00
parent ce94761deb
commit cbf9a1c888
9 changed files with 141 additions and 11 deletions

View File

@@ -1184,6 +1184,19 @@
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
<varlistentry>
<term><command>adopt</command> <replaceable>PATH</replaceable> [<replaceable>PATH</replaceable>…]</term>
<listitem><para>Adopts one or more existing home directories on the local system. Takes one or more paths to
<filename>*.home</filename> LUKS home directories or <filename>*.homedir/</filename> standalone home
directories or subvolumes previously created by <filename>systemd-homed</filename> and makes them
available locally for login. The referenced files are not moved. This is an alternative for moving
such home directories into <filename>/home/</filename> (where they would be picked up
automatically).</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><command>remove</command> <replaceable>USER</replaceable></term>

View File

@@ -70,6 +70,8 @@ node /org/freedesktop/home1 {
@org.freedesktop.systemd1.Privileged("true")
DeactivateHome(in s user_name);
RegisterHome(in s user_record);
AdoptHome(in s image_path,
in t flags);
UnregisterHome(in s user_name);
CreateHome(in s user_record);
CreateHomeEx(in s user_record,
@@ -160,6 +162,8 @@ node /org/freedesktop/home1 {
<variablelist class="dbus-method" generated="True" extra-ref="RegisterHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="AdoptHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="UnregisterHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="CreateHome()"/>
@@ -274,6 +278,12 @@ node /org/freedesktop/home1 {
is useful to register home directories locally that are not located where
<filename>systemd-homed.service</filename> would find them automatically.</para>
<para><function>AdoptHome()</function> also registers a new home directory locally. It takes a path to
a home directory itself, and will register it locally. This only works for <filename>*.home</filename>
and <filename>*.homedir/</filename> home directories. This operation is done automatically for all such
home areas showing up in <filename>/home/</filename>, but may be requested explicitly with this call for
directories elsewhere. The <varname>flags</varname> must be set to zero, currently.</para>
<para><function>UnregisterHome()</function> unregisters an existing home directory. It takes a user
name as argument and undoes what <function>RegisterHome()</function> does. It does not attempt to
remove the home directory itself, it just unregisters it with the local system. Note that if the home
@@ -633,9 +643,9 @@ node /org/freedesktop/home1/home {
<title>The Manager Object</title>
<para><function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function>,
<function>CreateHomeEx()</function>, and <function>UpdateHomeEx()</function> were added in version 256.</para>
<para><function>ListSigningKeys()</function>, <function>GetSigningKey()</function>,
<function>AddSigningKey()</function>, and <function>RemoveSigningKey()</function> were added in version
258.</para>
<para><function>AdoptHome()</function>, <function>ListSigningKeys()</function>,
<function>GetSigningKey()</function>, <function>AddSigningKey()</function>, and
<function>RemoveSigningKey()</function> were added in version 258.</para>
</refsect2>
<refsect2>
<title>Home Objects</title>

View File

@@ -173,7 +173,7 @@ _homectl() {
fi
local -A VERBS=(
[STANDALONE]='list lock-all'
[STANDALONE]='list lock-all adopt'
[CREATE]='create'
[NAMES]='activate deactivate inspect authenticate remove lock unlock'
[NAME]='update passwd'

View File

@@ -1564,6 +1564,38 @@ static int create_home(int argc, char *argv[], void *userdata) {
return create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ true);
}
static int verb_adopt_home(int argc, char *argv[], void *userdata) {
int r, ret = 0;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
r = acquire_bus(&bus);
if (r < 0)
return r;
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
STRV_FOREACH(i, strv_skip(argv, 1)) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
r = bus_message_new_method_call(bus, &m, bus_mgr, "AdoptHome");
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append(m, "st", *i, UINT64_C(0));
if (r < 0)
return bus_log_create_error(r);
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
if (r < 0) {
log_error_errno(r, "Failed to adopt home: %s", bus_error_message(&error, r));
if (ret == 0)
ret = r;
}
}
return ret;
}
static int remove_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
int r, ret = 0;
@@ -2808,6 +2840,7 @@ static int help(int argc, char *argv[], void *userdata) {
" inspect USER… Inspect a home area\n"
" authenticate USER… Authenticate a home area\n"
" create USER Create a home area\n"
" adopt PATH… Add an existing home area on this system\n"
" remove USER… Remove a home area\n"
" update USER Update a home area\n"
" passwd USER Change password of a home area\n"
@@ -5242,6 +5275,7 @@ static int run(int argc, char *argv[]) {
{ "inspect", VERB_ANY, VERB_ANY, 0, inspect_home },
{ "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home },
{ "create", VERB_ANY, 2, 0, create_home },
{ "adopt", VERB_ANY, VERB_ANY, 0, verb_adopt_home },
{ "remove", 2, VERB_ANY, 0, remove_home },
{ "update", VERB_ANY, 2, 0, update_home },
{ "passwd", VERB_ANY, 2, 0, passwd_home },

View File

@@ -519,6 +519,47 @@ static int method_register_home(
return sd_bus_reply_method_return(message, NULL);
}
static int method_adopt_home(
sd_bus_message *message,
void *userdata,
sd_bus_error *error) {
Manager *m = ASSERT_PTR(userdata);
int r;
assert(message);
const char *image_path = NULL;
uint64_t flags = 0;
r = sd_bus_message_read(message, "st", &image_path, &flags);
if (r < 0)
return r;
if (!path_is_absolute(image_path) || !path_is_safe(image_path))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Specified path is not absolute or not valid: %s", image_path);
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags field must be zero.");
r = bus_verify_polkit_async(
message,
"org.freedesktop.home1.create-home",
/* details= */ NULL,
&m->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
r = manager_adopt_home(m, image_path);
if (r == -EMEDIUMTYPE)
return sd_bus_error_setf(error, BUS_ERROR_UNRECOGNIZED_HOME_FORMAT, "Unrecognized format of home directory: %s", image_path);
if (r < 0)
return r;
return sd_bus_reply_method_return(message, NULL);
}
static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
return generic_home_method(userdata, message, bus_home_method_unregister, error);
}
@@ -1091,6 +1132,11 @@ static const sd_bus_vtable manager_vtable[] = {
SD_BUS_NO_RESULT,
method_register_home,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("AdoptHome",
SD_BUS_ARGS("s", image_path, "t", flags),
SD_BUS_NO_RESULT,
method_adopt_home,
SD_BUS_VTABLE_UNPRIVILEGED),
/* Remove the JSON record from homed, but don't remove actual $HOME */
SD_BUS_METHOD_WITH_ARGS("UnregisterHome",

View File

@@ -177,7 +177,7 @@ static int on_home_inotify(sd_event_source *s, const struct inotify_event *event
else if (FLAGS_SET(event->mask, IN_MOVED_TO))
log_debug("%s has been moved in, having a look.", j);
(void) manager_assess_image(m, -1, get_home_root(), event->name);
(void) manager_assess_image(m, /* dir_fd= */ -EBADF, get_home_root(), event->name);
(void) bus_manager_emit_auto_login_changed(m);
}
@@ -841,6 +841,10 @@ static int manager_assess_image(
assert(dir_path);
assert(dentry_name);
/* Maybe registers the specified .home or .homedir as a home we manage. Returns:
*
* -EMEDIUMTYPE: Not a dir with .homedir suffix or a file with .home suffix */
luks_suffix = endswith(dentry_name, ".home");
if (luks_suffix)
directory_suffix = NULL;
@@ -849,7 +853,7 @@ static int manager_assess_image(
/* Early filter out: by name */
if (!luks_suffix && !directory_suffix)
return 0;
return -EMEDIUMTYPE;
path = path_join(dir_path, dentry_name);
if (!path)
@@ -868,7 +872,7 @@ static int manager_assess_image(
_cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL;
if (!luks_suffix)
return 0;
return -EMEDIUMTYPE;
n = strndup(dentry_name, luks_suffix - dentry_name);
if (!n)
@@ -876,7 +880,7 @@ static int manager_assess_image(
r = split_user_name_realm(n, &user_name, &realm);
if (r == -EINVAL) /* Not the right format: ignore */
return 0;
return -EMEDIUMTYPE;
if (r < 0)
return log_error_errno(r, "Failed to split image name into user name/realm: %m");
@@ -889,7 +893,7 @@ static int manager_assess_image(
UserStorage storage;
if (!directory_suffix)
return 0;
return -EMEDIUMTYPE;
n = strndup(dentry_name, directory_suffix - dentry_name);
if (!n)
@@ -897,7 +901,7 @@ static int manager_assess_image(
r = split_user_name_realm(n, &user_name, &realm);
if (r == -EINVAL) /* Not the right format: ignore */
return 0;
return -EMEDIUMTYPE;
if (r < 0)
return log_error_errno(r, "Failed to split image name into user name/realm: %m");
@@ -939,7 +943,26 @@ static int manager_assess_image(
return manager_add_home_by_image(m, user_name, realm, path, NULL, storage, st.st_uid);
}
return 0;
return -EMEDIUMTYPE;
}
int manager_adopt_home(Manager *m, const char *path) {
int r;
assert(m);
assert(path);
_cleanup_free_ char *fn = NULL;
r = path_extract_filename(path, &fn);
if (r < 0)
return r;
_cleanup_free_ char *dir = NULL;
r = path_extract_directory(path, &dir);
if (r < 0)
return r;
return manager_assess_image(m, /* dir_fd= */ -EBADF, dir, fn);
}
int manager_enumerate_images(Manager *m) {

View File

@@ -87,6 +87,8 @@ int manager_reschedule_rebalance(Manager *m);
int manager_verify_user_record(Manager *m, UserRecord *hr);
int manager_adopt_home(Manager *m, const char *path);
int manager_acquire_key_pair(Manager *m);
int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error);

View File

@@ -151,6 +151,7 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = {
SD_BUS_ERROR_MAP(BUS_ERROR_REBALANCE_NOT_NEEDED, EALREADY),
SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_REFERENCED, EBADR),
SD_BUS_ERROR_MAP(BUS_ERROR_NO_SUCH_KEY, ENOKEY),
SD_BUS_ERROR_MAP(BUS_ERROR_UNRECOGNIZED_HOME_FORMAT, EMEDIUMTYPE),
SD_BUS_ERROR_MAP(BUS_ERROR_NO_UPDATE_CANDIDATE, EALREADY),

View File

@@ -157,6 +157,7 @@
#define BUS_ERROR_REBALANCE_NOT_NEEDED "org.freedesktop.home1.RebalanceNotNeeded"
#define BUS_ERROR_HOME_NOT_REFERENCED "org.freedesktop.home1.HomeNotReferenced"
#define BUS_ERROR_NO_SUCH_KEY "org.freedesktop.home1.NoSuchKey"
#define BUS_ERROR_UNRECOGNIZED_HOME_FORMAT "org.freedesktop.home1.UnrecognizedHomeFormat"
#define BUS_ERROR_NO_UPDATE_CANDIDATE "org.freedesktop.sysupdate1.NoCandidate"