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-03-07 18:14:25 +01:00
parent ce94761deb
commit cbf9a1c888
9 changed files with 141 additions and 11 deletions
+13
View File
@@ -1184,6 +1184,19 @@
<xi:include href="version-info.xml" xpointer="v245"/></listitem> <xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry> </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> <varlistentry>
<term><command>remove</command> <replaceable>USER</replaceable></term> <term><command>remove</command> <replaceable>USER</replaceable></term>
+13 -3
View File
@@ -70,6 +70,8 @@ node /org/freedesktop/home1 {
@org.freedesktop.systemd1.Privileged("true") @org.freedesktop.systemd1.Privileged("true")
DeactivateHome(in s user_name); DeactivateHome(in s user_name);
RegisterHome(in s user_record); RegisterHome(in s user_record);
AdoptHome(in s image_path,
in t flags);
UnregisterHome(in s user_name); UnregisterHome(in s user_name);
CreateHome(in s user_record); CreateHome(in s user_record);
CreateHomeEx(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="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="UnregisterHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="CreateHome()"/> <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 is useful to register home directories locally that are not located where
<filename>systemd-homed.service</filename> would find them automatically.</para> <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 <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 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 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> <title>The Manager Object</title>
<para><function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function>, <para><function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function>,
<function>CreateHomeEx()</function>, and <function>UpdateHomeEx()</function> were added in version 256.</para> <function>CreateHomeEx()</function>, and <function>UpdateHomeEx()</function> were added in version 256.</para>
<para><function>ListSigningKeys()</function>, <function>GetSigningKey()</function>, <para><function>AdoptHome()</function>, <function>ListSigningKeys()</function>,
<function>AddSigningKey()</function>, and <function>RemoveSigningKey()</function> were added in version <function>GetSigningKey()</function>, <function>AddSigningKey()</function>, and
258.</para> <function>RemoveSigningKey()</function> were added in version 258.</para>
</refsect2> </refsect2>
<refsect2> <refsect2>
<title>Home Objects</title> <title>Home Objects</title>
+1 -1
View File
@@ -173,7 +173,7 @@ _homectl() {
fi fi
local -A VERBS=( local -A VERBS=(
[STANDALONE]='list lock-all' [STANDALONE]='list lock-all adopt'
[CREATE]='create' [CREATE]='create'
[NAMES]='activate deactivate inspect authenticate remove lock unlock' [NAMES]='activate deactivate inspect authenticate remove lock unlock'
[NAME]='update passwd' [NAME]='update passwd'
+34
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); 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) { static int remove_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
int r, ret = 0; int r, ret = 0;
@@ -2808,6 +2840,7 @@ static int help(int argc, char *argv[], void *userdata) {
" inspect USER… Inspect a home area\n" " inspect USER… Inspect a home area\n"
" authenticate USER… Authenticate a home area\n" " authenticate USER… Authenticate a home area\n"
" create USER Create 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" " remove USER… Remove a home area\n"
" update USER Update a home area\n" " update USER Update a home area\n"
" passwd USER Change password of 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 }, { "inspect", VERB_ANY, VERB_ANY, 0, inspect_home },
{ "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home }, { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home },
{ "create", VERB_ANY, 2, 0, create_home }, { "create", VERB_ANY, 2, 0, create_home },
{ "adopt", VERB_ANY, VERB_ANY, 0, verb_adopt_home },
{ "remove", 2, VERB_ANY, 0, remove_home }, { "remove", 2, VERB_ANY, 0, remove_home },
{ "update", VERB_ANY, 2, 0, update_home }, { "update", VERB_ANY, 2, 0, update_home },
{ "passwd", VERB_ANY, 2, 0, passwd_home }, { "passwd", VERB_ANY, 2, 0, passwd_home },
+46
View File
@@ -519,6 +519,47 @@ static int method_register_home(
return sd_bus_reply_method_return(message, NULL); 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) { 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); 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, SD_BUS_NO_RESULT,
method_register_home, method_register_home,
SD_BUS_VTABLE_UNPRIVILEGED), 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 */ /* Remove the JSON record from homed, but don't remove actual $HOME */
SD_BUS_METHOD_WITH_ARGS("UnregisterHome", SD_BUS_METHOD_WITH_ARGS("UnregisterHome",
+30 -7
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)) else if (FLAGS_SET(event->mask, IN_MOVED_TO))
log_debug("%s has been moved in, having a look.", j); 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); (void) bus_manager_emit_auto_login_changed(m);
} }
@@ -841,6 +841,10 @@ static int manager_assess_image(
assert(dir_path); assert(dir_path);
assert(dentry_name); 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"); luks_suffix = endswith(dentry_name, ".home");
if (luks_suffix) if (luks_suffix)
directory_suffix = NULL; directory_suffix = NULL;
@@ -849,7 +853,7 @@ static int manager_assess_image(
/* Early filter out: by name */ /* Early filter out: by name */
if (!luks_suffix && !directory_suffix) if (!luks_suffix && !directory_suffix)
return 0; return -EMEDIUMTYPE;
path = path_join(dir_path, dentry_name); path = path_join(dir_path, dentry_name);
if (!path) if (!path)
@@ -868,7 +872,7 @@ static int manager_assess_image(
_cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL; _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL;
if (!luks_suffix) if (!luks_suffix)
return 0; return -EMEDIUMTYPE;
n = strndup(dentry_name, luks_suffix - dentry_name); n = strndup(dentry_name, luks_suffix - dentry_name);
if (!n) if (!n)
@@ -876,7 +880,7 @@ static int manager_assess_image(
r = split_user_name_realm(n, &user_name, &realm); r = split_user_name_realm(n, &user_name, &realm);
if (r == -EINVAL) /* Not the right format: ignore */ if (r == -EINVAL) /* Not the right format: ignore */
return 0; return -EMEDIUMTYPE;
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to split image name into user name/realm: %m"); 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; UserStorage storage;
if (!directory_suffix) if (!directory_suffix)
return 0; return -EMEDIUMTYPE;
n = strndup(dentry_name, directory_suffix - dentry_name); n = strndup(dentry_name, directory_suffix - dentry_name);
if (!n) if (!n)
@@ -897,7 +901,7 @@ static int manager_assess_image(
r = split_user_name_realm(n, &user_name, &realm); r = split_user_name_realm(n, &user_name, &realm);
if (r == -EINVAL) /* Not the right format: ignore */ if (r == -EINVAL) /* Not the right format: ignore */
return 0; return -EMEDIUMTYPE;
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to split image name into user name/realm: %m"); 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 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) { int manager_enumerate_images(Manager *m) {
+2
View File
@@ -87,6 +87,8 @@ int manager_reschedule_rebalance(Manager *m);
int manager_verify_user_record(Manager *m, UserRecord *hr); 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_acquire_key_pair(Manager *m);
int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error); int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error);
@@ -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_REBALANCE_NOT_NEEDED, EALREADY),
SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_REFERENCED, EBADR), 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_NO_SUCH_KEY, ENOKEY),
SD_BUS_ERROR_MAP(BUS_ERROR_UNRECOGNIZED_HOME_FORMAT, EMEDIUMTYPE),
SD_BUS_ERROR_MAP(BUS_ERROR_NO_UPDATE_CANDIDATE, EALREADY), SD_BUS_ERROR_MAP(BUS_ERROR_NO_UPDATE_CANDIDATE, EALREADY),
@@ -157,6 +157,7 @@
#define BUS_ERROR_REBALANCE_NOT_NEEDED "org.freedesktop.home1.RebalanceNotNeeded" #define BUS_ERROR_REBALANCE_NOT_NEEDED "org.freedesktop.home1.RebalanceNotNeeded"
#define BUS_ERROR_HOME_NOT_REFERENCED "org.freedesktop.home1.HomeNotReferenced" #define BUS_ERROR_HOME_NOT_REFERENCED "org.freedesktop.home1.HomeNotReferenced"
#define BUS_ERROR_NO_SUCH_KEY "org.freedesktop.home1.NoSuchKey" #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" #define BUS_ERROR_NO_UPDATE_CANDIDATE "org.freedesktop.sysupdate1.NoCandidate"