Merge pull request #30226 from poettering/homed-fallback-shell

homed: allow logging into home areas via ssh without unlocking them locally first
This commit is contained in:
Lennart Poettering
2024-02-14 17:08:04 +01:00
committed by GitHub
29 changed files with 781 additions and 111 deletions
+2 -10
View File
@@ -1195,16 +1195,8 @@ Features:
passwords, not just the first. i.e. if there are multiple defined, prefer
unlocked over locked and prefer non-empty over empty.
* homed: while a home dir is not activated generate slightly different NSS
records for it, that reports the home dir as "/" and the shell as some binary
provided by us. Then, when an SSH login happens and SSH permits it our binary
is invoked. This binary can then talk to homed and activate the homedir if
it's not around yet, prompting the user for a password. Once that succeeded
we'll switch to the real user record, i.e. home dir and shell, and our tool
exec()s the latter. Net effect: ssh'ing into a homed account will just work:
we'll neatly prompt for the homedir's password if its needed. –– Building on
this we could take this even further: since this tool will potentially have
access to the client's ssh-agent (if ssh-agent forwarding is enabled) we
* homed: if the homed shell fallback thing has access to an SSH agent, try to
use it to unlock home dir (if ssh-agent forwarding is enabled). We
could implement SSH unlocking of a homedir with that: when enrolling a new
ssh pubkey in a user record we'd ask the ssh-agent to sign some random value
with the privkey, then use that as luks key to unlock the home dir. Will not
+14
View File
@@ -914,6 +914,20 @@ itself.
`fileSystemType` → The file system type backing the home directory: a short
string, such as "btrfs", "ext4", "xfs".
`fallbackShell`, `fallbackHomeDirectory` → These fields have the same contents
and format as the `shell` and `homeDirectory` fields (see above). When the
`useFallback` field (see below) is set to true, the data from these fields
should override the fields of the same name without the `fallback` prefix.
`useFallback` → A boolean that allows choosing between the regular `shell` and
`homeDirectory` fields or the fallback fields of the same name (see above). If
`true` the fallback fields should be used in place of the regular fields, if
`false` or unset the regular fields should be used. This mechanism is used for
enable subsystems such as SSH to allow logins into user accounts, whose homed
directories need further unlocking (because the SSH native authentication
cannot release a suitabable disk encryption key), which the fallback shell
provides.
## Fields in the `signature` section
As mentioned, the `signature` section of the user record may contain one or
+36 -10
View File
@@ -63,9 +63,10 @@ node /org/freedesktop/home1 {
out b incomplete,
out o bus_path);
ListHomes(out a(susussso) home_areas);
@org.freedesktop.systemd1.Privileged("true")
ActivateHome(in s user_name,
in s secret);
ActivateHomeIfReferenced(in s user_name,
in s secret);
@org.freedesktop.systemd1.Privileged("true")
DeactivateHome(in s user_name);
RegisterHome(in s user_record);
@@ -100,6 +101,10 @@ node /org/freedesktop/home1 {
in b please_suspend,
out h send_fd);
@org.freedesktop.systemd1.Privileged("true")
RefHomeUnrestricted(in s user_name,
in b please_suspend,
out h send_fd);
@org.freedesktop.systemd1.Privileged("true")
ReleaseHome(in s user_name);
InhibitSuspendHome(in s user_name,
out h send_fd);
@@ -136,6 +141,8 @@ node /org/freedesktop/home1 {
<variablelist class="dbus-method" generated="True" extra-ref="ActivateHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ActivateHomeIfReferenced()"/>
<variablelist class="dbus-method" generated="True" extra-ref="DeactivateHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RegisterHome()"/>
@@ -166,6 +173,8 @@ node /org/freedesktop/home1 {
<variablelist class="dbus-method" generated="True" extra-ref="RefHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RefHomeUnrestricted()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ReleaseHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="InhibitSuspendHome()"/>
@@ -224,6 +233,11 @@ node /org/freedesktop/home1 {
<classname>org.freedesktop.home1.Home</classname> interface documented below, but may be called on the
manager object and takes a user name as additional argument, instead.</para>
<para><function>ActivateHomeIfReferenced()</function> is identical to
<function>ActivateHome()</function>. However, the call only succeeds if the home directory is currently
referenced. Useful in conjunction with <function>RefHomeUnrestricted()</function>, which allows
creating a reference to a home directory even if the home directory is not active.</para>
<para><function>DeactivateHome()</function> deactivates (i.e. unmounts) the home directory of the
specified user. It is equivalent to the <function>Deactivate()</function> method on the
<classname>org.freedesktop.home1.Home</classname> interface documented below.</para>
@@ -355,6 +369,10 @@ node /org/freedesktop/home1 {
<function>Ref()</function> on the <classname>org.freedesktop.home1.Home</classname>
interface.</para>
<para><function>RefHomeUnrestricted()</function> is identical to <function>RefHome()</function> but
succeeds even if the home area is not active currently. This is useful on conjunction with
<function>ActivateHomeIfReferenced()</function>.</para>
<para><function>ReleaseHome()</function> releases a home directory again, if all file descriptors
referencing it are already closed, that where acquired through <function>AcquireHome()</function> or
<function>RefHome()</function>. Note that this call does not actually cause the deactivation of the
@@ -409,8 +427,8 @@ node /org/freedesktop/home1 {
node /org/freedesktop/home1/home {
interface org.freedesktop.home1.Home {
methods:
@org.freedesktop.systemd1.Privileged("true")
Activate(in s secret);
ActivateIfReferenced(in s secret);
@org.freedesktop.systemd1.Privileged("true")
Deactivate();
Unregister();
@@ -436,6 +454,9 @@ node /org/freedesktop/home1/home {
Ref(in b please_suspend,
out h send_fd);
@org.freedesktop.systemd1.Privileged("true")
RefUnrestricted(in b please_suspend,
out h send_fd);
@org.freedesktop.systemd1.Privileged("true")
Release();
InhibitSuspend(out h send_fd);
properties:
@@ -467,6 +488,8 @@ node /org/freedesktop/home1/home {
<variablelist class="dbus-method" generated="True" extra-ref="Activate()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ActivateIfReferenced()"/>
<variablelist class="dbus-method" generated="True" extra-ref="Deactivate()"/>
<variablelist class="dbus-method" generated="True" extra-ref="Unregister()"/>
@@ -493,6 +516,8 @@ node /org/freedesktop/home1/home {
<variablelist class="dbus-method" generated="True" extra-ref="Ref()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RefUnrestricted()"/>
<variablelist class="dbus-method" generated="True" extra-ref="Release()"/>
<variablelist class="dbus-method" generated="True" extra-ref="InhibitSuspend()"/>
@@ -512,12 +537,13 @@ node /org/freedesktop/home1/home {
<refsect2>
<title>Methods</title>
<para><function>Activate()</function>, <function>Deactivate()</function>,
<function>Unregister()</function>, <function>Realize()</function>, <function>Remove()</function>,
<function>Fixate()</function>, <function>Authenticate()</function>, <function>Update()</function>,
<function>Resize()</function>, <function>ChangePassword()</function>, <function>Lock()</function>,
<function>Unlock()</function>, <function>Acquire()</function>, <function>Ref()</function>,
<function>Release()</function>, <function>InhibitSuspend()</function> operate like their matching counterparts on the
<para><function>Activate()</function>, <function>ActivateIfReferenced()</function>,
<function>Deactivate()</function>, <function>Unregister()</function>, <function>Realize()</function>,
<function>Remove()</function>, <function>Fixate()</function>, <function>Authenticate()</function>,
<function>Update()</function>, <function>Resize()</function>, <function>ChangePassword()</function>,
<function>Lock()</function>, <function>Unlock()</function>, <function>Acquire()</function>,
<function>Ref()</function>, <function>RefUnrestricted()</function>, <function>Release()</function>,
<function>InhibitSuspend()</function> operate like their matching counterparts on the
<classname>org.freedesktop.home1.Manager</classname> interface (see above). The main difference is that
they are methods of the home directory objects, and hence carry no additional user name
parameter. Which of the two flavors of methods to call depends on the handles to the user known on the
@@ -549,11 +575,11 @@ node /org/freedesktop/home1/home {
<title>History</title>
<refsect2>
<title>The Manager Object</title>
<para><function>InhibitSuspendHome()</function> was added in version 256.</para>
<para><function>InhibitSuspendHome()</function>, <function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function> wer added in version 256.</para>
</refsect2>
<refsect2>
<title>Home Objects</title>
<para><function>InhibitSuspend()</function> was added in version 256.</para>
<para><function>InhibitSuspend()</function>, <function>ActivateIfReferenced()</function> and <function>RefUnrestricted()</function> were added in version 256.</para>
</refsect2>
</refsect1>
+10 -1
View File
@@ -1146,6 +1146,7 @@ node /org/freedesktop/login1/session/1 {
TakeControl(in b force);
ReleaseControl();
SetType(in s type);
SetClass(in s class);
SetDisplay(in s display);
SetTTY(in h tty_fd);
TakeDevice(in u major,
@@ -1202,7 +1203,6 @@ node /org/freedesktop/login1/session/1 {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly u Audit = ...;
readonly s Type = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s Class = '...';
readonly b Active = ...;
readonly s State = '...';
@@ -1243,6 +1243,8 @@ node /org/freedesktop/login1/session/1 {
<variablelist class="dbus-method" generated="True" extra-ref="SetType()"/>
<variablelist class="dbus-method" generated="True" extra-ref="SetClass()"/>
<variablelist class="dbus-method" generated="True" extra-ref="SetDisplay()"/>
<variablelist class="dbus-method" generated="True" extra-ref="SetTTY()"/>
@@ -1343,6 +1345,12 @@ node /org/freedesktop/login1/session/1 {
connection. This should help prevent a session from entering an inconsistent state, for example if the
controller crashes. The only argument <varname>type</varname> is the new session type.</para>
<para><function>SetClass()</function> allows the caller to change the class of the session dynamically.
It may only be called by session's owening user. Currently, this call may be exclusively used to change
the class from <literal>user-incomplete</literal> to <literal>user</literal>. The call is synchronous,
and will return only once the user's service manager has successfully been started, if necessary. The
only argument <varname>type</varname> is the new session type.</para>
<para><function>SetDisplay()</function> allows the display name of the graphical session to be changed. This is
useful if the display server is started as part of the session. It can only be called by session's current
controller. If <function>TakeControl()</function> has not been called, this method will fail. The only argument
@@ -1579,6 +1587,7 @@ node /org/freedesktop/login1/session/1 {
<title>Session Objects</title>
<para><function>SetDisplay()</function> was added in version 252.</para>
<para><function>SetTTY()</function> was added in version 254.</para>
<para><function>SetClass()</function> was added in version 256.</para>
</refsect2>
</refsect1>
</refentry>
+4
View File
@@ -115,6 +115,10 @@
<entry><constant>user-early</constant></entry>
<entry>Similar to <literal>user</literal> but sessions of this class are not ordered after <filename>systemd-user-sessions.service</filename>, i.e. may be started before regular sessions are allowed to be established. This session class is the default for sessions of the root user that would otherwise qualify for the <constant>user</constant> class, see above. (Added in v256.)</entry>
</row>
<row>
<entry><constant>user-incomplete</constant></entry>
<entry>Similar to <literal>user</literal> but for sessions which are not fully set up yet, i.e. have no home directory mounted ot similar. This is used by <citerefentry><refentrytitle>systemd-homed.service</refentrytitle><manvolnum>8</manvolnum></citerefentry> to allow users to log in via <command>ssh</command> before their home directory is mounted, delaying the mount until the user provided the unlock password. Sessions of this class are upgraded to the regular <constant>user</constant> class once the home directory is activated.</entry>
</row>
<row>
<entry><constant>greeter</constant></entry>
<entry>Similar to <literal>user</literal> but for sessions that are spawned by a display manager ephemerally and which prompt the user for login credentials.</entry>
+4 -3
View File
@@ -216,9 +216,10 @@
<para><function>sd_session_get_class()</function> may be used to determine the class of the session
identified by the specified session identifier. The returned string is one of <literal>user</literal>,
<literal>user-early</literal>, <literal>greeter</literal>, <literal>lock-screen</literal>,
<literal>background</literal>, <literal>background-light</literal>, <literal>manager</literal> or
<literal>manager-early</literal> and needs to be freed with the libc <citerefentry
<literal>user-early</literal>, <literal>user-incomplete</literal>, <literal>greeter</literal>,
<literal>lock-screen</literal>, <literal>background</literal>, <literal>background-light</literal>,
<literal>manager</literal> or <literal>manager-early</literal> and needs to be freed with the libc
<citerefentry
project='man-pages'><refentrytitle>free</refentrytitle><manvolnum>3</manvolnum></citerefentry> call after
use.</para>
+194 -1
View File
@@ -740,7 +740,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)
@@ -4094,6 +4093,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 },
@@ -4125,6 +4315,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;
+51 -17
View File
@@ -144,15 +144,31 @@ int bus_home_method_activate(
_cleanup_(user_record_unrefp) UserRecord *secret = NULL;
Home *h = ASSERT_PTR(userdata);
bool if_referenced;
int r;
assert(message);
if_referenced = endswith(sd_bus_message_get_member(message), "IfReferenced");
r = bus_verify_polkit_async_full(
message,
"org.freedesktop.home1.activate-home",
/* details= */ NULL,
/* interctive= */ false,
h->uid,
&h->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
r = bus_message_read_secret(message, &secret, error);
if (r < 0)
return r;
r = home_activate(h, secret, error);
r = home_activate(h, if_referenced, secret, error);
if (r < 0)
return r;
@@ -620,30 +636,38 @@ int bus_home_method_ref(
_cleanup_close_ int fd = -EBADF;
Home *h = ASSERT_PTR(userdata);
HomeState state;
int please_suspend, r;
bool unrestricted;
assert(message);
/* In unrestricted mode we'll add a reference to the home even if it's not active */
unrestricted = strstr(sd_bus_message_get_member(message), "Unrestricted");
r = sd_bus_message_read(message, "b", &please_suspend);
if (r < 0)
return r;
state = home_get_state(h);
switch (state) {
case HOME_ABSENT:
return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
case HOME_UNFIXATED:
case HOME_INACTIVE:
case HOME_DIRTY:
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
case HOME_LOCKED:
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
default:
if (HOME_STATE_IS_ACTIVE(state))
break;
if (!unrestricted) {
HomeState state;
return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
state = home_get_state(h);
switch (state) {
case HOME_ABSENT:
return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
case HOME_UNFIXATED:
case HOME_INACTIVE:
case HOME_DIRTY:
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
case HOME_LOCKED:
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
default:
if (HOME_STATE_IS_ACTIVE(state))
break;
return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
}
}
fd = home_create_fifo(h, please_suspend ? HOME_FIFO_PLEASE_SUSPEND : HOME_FIFO_DONT_SUSPEND);
@@ -814,7 +838,12 @@ const sd_bus_vtable home_vtable[] = {
SD_BUS_ARGS("s", secret),
SD_BUS_NO_RESULT,
bus_home_method_activate,
SD_BUS_VTABLE_SENSITIVE),
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("ActivateIfReferenced",
SD_BUS_ARGS("s", secret),
SD_BUS_NO_RESULT,
bus_home_method_activate,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD("Deactivate", NULL, NULL, bus_home_method_deactivate, 0),
SD_BUS_METHOD("Unregister", NULL, NULL, bus_home_method_unregister, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("Realize",
@@ -865,6 +894,11 @@ const sd_bus_vtable home_vtable[] = {
SD_BUS_RESULT("h", send_fd),
bus_home_method_ref,
0),
SD_BUS_METHOD_WITH_ARGS("RefUnrestricted",
SD_BUS_ARGS("b", please_suspend),
SD_BUS_RESULT("h", send_fd),
bus_home_method_ref,
0),
SD_BUS_METHOD("Release", NULL, NULL, bus_home_method_release, 0),
SD_BUS_METHOD_WITH_ARGS("InhibitSuspend",
SD_BUS_NO_ARGS,
+7 -1
View File
@@ -1383,12 +1383,15 @@ static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_sta
return 0;
}
int home_activate(Home *h, UserRecord *secret, sd_bus_error *error) {
int home_activate(Home *h, bool if_referenced, UserRecord *secret, sd_bus_error *error) {
int r;
assert(h);
assert(secret);
if (if_referenced && !home_is_referenced(h))
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_REFERENCED, "Home %s is currently not referenced.", h->user_name);
switch (home_get_state(h)) {
case HOME_UNFIXATED:
return home_fixate_internal(h, secret, HOME_FIXATING_FOR_ACTIVATION, error);
@@ -2567,6 +2570,9 @@ int home_augment_status(
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("state", JSON_BUILD_STRING(home_state_to_string(state))),
JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.Home")),
JSON_BUILD_PAIR("useFallback", JSON_BUILD_BOOLEAN(!HOME_STATE_IS_ACTIVE(state))),
JSON_BUILD_PAIR("fallbackShell", JSON_BUILD_CONST_STRING(BINDIR "/systemd-home-fallback-shell")),
JSON_BUILD_PAIR("fallbackHomeDirectory", JSON_BUILD_CONST_STRING("/")),
JSON_BUILD_PAIR_CONDITION(disk_size != UINT64_MAX, "diskSize", JSON_BUILD_UNSIGNED(disk_size)),
JSON_BUILD_PAIR_CONDITION(disk_usage != UINT64_MAX, "diskUsage", JSON_BUILD_UNSIGNED(disk_usage)),
JSON_BUILD_PAIR_CONDITION(disk_free != UINT64_MAX, "diskFree", JSON_BUILD_UNSIGNED(disk_free)),
+1 -1
View File
@@ -190,7 +190,7 @@ int home_save_record(Home *h);
int home_unlink_record(Home *h);
int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error);
int home_activate(Home *h, UserRecord *secret, sd_bus_error *error);
int home_activate(Home *h, bool if_referenced, UserRecord *secret, sd_bus_error *error);
int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error);
int home_deactivate(Home *h, bool force, sd_bus_error *error);
int home_create(Home *h, UserRecord *secret, sd_bus_error *error);
+11 -1
View File
@@ -737,7 +737,12 @@ static const sd_bus_vtable manager_vtable[] = {
SD_BUS_ARGS("s", user_name, "s", secret),
SD_BUS_NO_RESULT,
method_activate_home,
SD_BUS_VTABLE_SENSITIVE),
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("ActivateHomeIfReferenced",
SD_BUS_ARGS("s", user_name, "s", secret),
SD_BUS_NO_RESULT,
method_activate_home,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("DeactivateHome",
SD_BUS_ARGS("s", user_name),
SD_BUS_NO_RESULT,
@@ -843,6 +848,11 @@ static const sd_bus_vtable manager_vtable[] = {
SD_BUS_RESULT("h", send_fd),
method_ref_home,
0),
SD_BUS_METHOD_WITH_ARGS("RefHomeUnrestricted",
SD_BUS_ARGS("s", user_name, "b", please_suspend),
SD_BUS_RESULT("h", send_fd),
method_ref_home,
0),
SD_BUS_METHOD_WITH_ARGS("ReleaseHome",
SD_BUS_ARGS("s", user_name),
SD_BUS_NO_RESULT,
+4
View File
@@ -137,4 +137,8 @@ if conf.get('ENABLE_HOMED') == 1
install_data('homed.conf',
install_dir : pkgconfigfiledir)
endif
meson.add_install_script(sh, '-c',
ln_s.format(bindir / 'homectl',
bindir / 'systemd-home-fallback-shell'))
endif
+16
View File
@@ -57,6 +57,10 @@
send_interface="org.freedesktop.home1.Manager"
send_member="ActivateHome"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="ActivateHomeIfReferenced"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="DeactivateHome"/>
@@ -117,6 +121,10 @@
send_interface="org.freedesktop.home1.Manager"
send_member="RefHome"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="RefHomeUnrestricted"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="ReleaseHome"/>
@@ -143,6 +151,10 @@
send_interface="org.freedesktop.home1.Home"
send_member="Activate"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
send_member="ActivateIfReferenced"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
send_member="Deactivate"/>
@@ -195,6 +207,10 @@
send_interface="org.freedesktop.home1.Home"
send_member="Ref"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
send_member="RefUnrestricted"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
send_member="Release"/>
+10
View File
@@ -78,4 +78,14 @@
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
<action id="org.freedesktop.home1.activate-home">
<description gettext-domain="systemd">Activate a home area</description>
<message gettext-domain="systemd">Authentication is required to activate a user's home area.</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
+109 -32
View File
@@ -23,6 +23,7 @@
typedef enum AcquireHomeFlags {
ACQUIRE_MUST_AUTHENTICATE = 1 << 0,
ACQUIRE_PLEASE_SUSPEND = 1 << 1,
ACQUIRE_REF_ANYWAY = 1 << 2,
} AcquireHomeFlags;
static int parse_argv(
@@ -499,7 +500,7 @@ static int acquire_home(
PamBusData **bus_data) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL;
bool do_auth = FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE), home_not_active = false, home_locked = false;
bool do_auth = FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE), home_not_active = false, home_locked = false, unrestricted = false;
_cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
_cleanup_close_ int acquired_fd = -EBADF;
_cleanup_free_ char *fd_field = NULL;
@@ -510,13 +511,27 @@ static int acquire_home(
assert(handle);
/* This acquires a reference to a home directory in one of two ways: if please_authenticate is true,
* then we'll call AcquireHome() after asking the user for a password. Otherwise it tries to call
* RefHome() and if that fails queries the user for a password and uses AcquireHome().
/* This acquires a reference to a home directory in the following ways:
*
* The idea is that the PAM authentication hook sets please_authenticate and thus always
* authenticates, while the other PAM hooks unset it so that they can a ref of their own without
* authentication if possible, but with authentication if necessary. */
* 1. If please_authenticate is false, it tries to call RefHome() first — which
* will get us a reference to the home without authentication (which will work for homes that are
* not encrypted, or that already are activated). If this works, we are done. Yay!
*
* 2. Otherwise, we'll call AcquireHome() — which will try to activate the home getting us a
* reference. If this works, we are done. Yay!
*
* 3. if ref_anyway, we'll call RefHomeUnrestricted() — which will give us a reference in any case
* (even if the activation failed!).
*
* The idea is that please_authenticate is set to false for the PAM session hooks (since for those
* authentication doesn't matter), and true for the PAM authentication hooks (since for those
* authentication is essential). And ref_anyway should be set if we are pretty sure that we can later
* activate the home directory via our fallback shell logic, and hence are OK if we can't activate
* things here. Usecase for that are SSH logins where SSH does the authentication and thus only the
* session hooks are called. But from the session hooks SSH doesn't allow asking questions, hence we
* simply allow the login attempt to continue but then invoke our fallback shell that will prompt the
* user for the missing unlock credentials, and then chainload the real shell.
*/
r = pam_get_user(handle, &username, NULL);
if (r != PAM_SUCCESS)
@@ -546,16 +561,16 @@ static int acquire_home(
return r;
/* Implement our own retry loop here instead of relying on the PAM client's one. That's because it
* might happen that the record we stored on the host does not match the encryption password of
* the LUKS image in case the image was used in a different system where the password was
* changed. In that case it will happen that the LUKS password and the host password are
* different, and we handle that by collecting and passing multiple passwords in that case. Hence we
* treat bad passwords as a request to collect one more password and pass the new all all previously
* used passwords again. */
* might happen that the record we stored on the host does not match the encryption password of the
* LUKS image in case the image was used in a different system where the password was changed. In
* that case it will happen that the LUKS password and the host password are different, and we handle
* that by collecting and passing multiple passwords in that case. Hence we treat bad passwords as a
* request to collect one more password and pass the new all all previously used passwords again. */
for (;;) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
const char *method = NULL;
if (do_auth && !secret) {
const char *cached_password = NULL;
@@ -579,7 +594,14 @@ static int acquire_home(
}
}
r = bus_message_new_method_call(bus, &m, bus_home_mgr, do_auth ? "AcquireHome" : "RefHome");
if (do_auth)
method = "AcquireHome"; /* If we shall authenticate no matter what */
else if (unrestricted)
method = "RefHomeUnrestricted"; /* If we shall get a ref no matter what */
else
method = "RefHome"; /* If we shall get a ref (if possible) */
r = bus_message_new_method_call(bus, &m, bus_home_mgr, method);
if (r < 0)
return pam_bus_log_create_error(handle, r);
@@ -599,15 +621,16 @@ static int acquire_home(
r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply);
if (r < 0) {
if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE))
if (sd_bus_error_has_names(&error, BUS_ERROR_HOME_NOT_ACTIVE, BUS_ERROR_HOME_BUSY)) {
/* Only on RefHome(): We can't access the home directory currently, unless
* it's unlocked with a password. Hence, let's try this again, this time with
* authentication. */
home_not_active = true;
else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED))
do_auth = true;
} else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED)) {
home_locked = true; /* Similar */
else {
do_auth = true;
} else {
r = handle_generic_user_record_error(handle, ur->user_name, secret, r, &error, debug);
if (r == PAM_CONV_ERR) {
/* Password/PIN prompts will fail in certain environments, for example when
@@ -615,18 +638,25 @@ static int acquire_home(
* per-service PAM logic. In that case, print a friendly message and accept
* failure. */
if (home_not_active)
(void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently not active, please log in locally first."), ur->user_name);
if (home_locked)
(void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently locked, please unlock locally first."), ur->user_name);
if (!FLAGS_SET(flags, ACQUIRE_REF_ANYWAY)) {
if (home_not_active)
(void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently not active, please log in locally first."), ur->user_name);
if (home_locked)
(void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently locked, please unlock locally first."), ur->user_name);
if (FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) || debug)
pam_syslog(handle, FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) ? LOG_ERR : LOG_DEBUG, "Failed to prompt for password/prompt.");
if (FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) || debug)
pam_syslog(handle, FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) ? LOG_ERR : LOG_DEBUG, "Failed to prompt for password/prompt.");
return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR;
}
if (r != PAM_SUCCESS)
return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR;
}
/* ref_anyway is true, hence let's now get a ref no matter what. */
unrestricted = true;
do_auth = false;
} else if (r != PAM_SUCCESS)
return r;
else
do_auth = true; /* The issue was dealt with, some more information was collected. Let's try to authenticate, again. */
}
} else {
int fd;
@@ -648,9 +678,6 @@ static int acquire_home(
return pam_syslog_pam_error(handle, LOG_ERR, PAM_MAXTRIES,
"Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
}
/* Try again, this time with authentication if we didn't do that before. */
do_auth = true;
}
/* Later PAM modules may need the auth token, but only during pam_authenticate. */
@@ -674,7 +701,19 @@ static int acquire_home(
return r;
}
pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name);
/* If we didn't actually manage to unlock the home directory, then we rely on the fallback-shell to
* unlock it for us. But until that happens we don't want that logind spawns the per-user service
* manager for us (since it would see an inaccessible home directory). Hence set an environment
* variable that pam_systemd looks for). */
if (unrestricted) {
r = pam_putenv(handle, "XDG_SESSION_INCOMPLETE=1");
if (r != PAM_SUCCESS)
return pam_syslog_pam_error(handle, LOG_WARNING, r, "Failed to set XDG_SESSION_INCOMPLETE= environment variable: @PAMERR@");
pam_syslog(handle, LOG_NOTICE, "Home for user %s acquired in incomplete mode, requires later activation.", ur->user_name);
} else
pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name);
return PAM_SUCCESS;
}
@@ -729,6 +768,36 @@ _public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int sm_flags, int arg
return PAM_SUCCESS;
}
static int fallback_shell_can_work(
pam_handle_t *handle,
AcquireHomeFlags *flags) {
const char *tty = NULL, *display = NULL;
int r;
assert(handle);
assert(flags);
r = pam_get_item_many(
handle,
PAM_TTY, &tty,
PAM_XDISPLAY, &display);
if (r != PAM_SUCCESS)
return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get PAM items: @PAMERR@");
/* The fallback shell logic only works on TTY logins, hence only allow it if there's no X11 display
* set, and a TTY field is set that is neither "cron" (which is what crond sets, god knows why) not
* contains a colon (which is what various graphical X11 logins do). Note that ssh sets the tty to
* "ssh" here, which we allow (I mean, ssh is after all the primary reason we do all this). */
if (isempty(display) &&
tty &&
!strchr(tty, ':') &&
!streq(tty, "cron"))
*flags |= ACQUIRE_REF_ANYWAY; /* Allow login even if we can only ref, not activate */
return PAM_SUCCESS;
}
_public_ PAM_EXTERN int pam_sm_open_session(
pam_handle_t *handle,
int sm_flags,
@@ -753,6 +822,10 @@ _public_ PAM_EXTERN int pam_sm_open_session(
pam_debug_syslog(handle, debug, "pam-systemd-homed session start");
r = fallback_shell_can_work(handle, &flags);
if (r != PAM_SUCCESS)
return r;
r = acquire_home(handle, flags, debug, &d);
if (r == PAM_USER_UNKNOWN) /* Not managed by us? Don't complain. */
return PAM_SUCCESS;
@@ -854,7 +927,11 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt(
pam_debug_syslog(handle, debug, "pam-systemd-homed account management");
r = acquire_home(handle, flags, debug, NULL);
r = fallback_shell_can_work(handle, &flags);
if (r != PAM_SUCCESS)
return r;
r = acquire_home(handle, flags, debug, /* bus_data= */ NULL);
if (r != PAM_SUCCESS)
return r;
@@ -147,6 +147,7 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = {
SD_BUS_ERROR_MAP(BUS_ERROR_HOME_CANT_AUTHENTICATE, EKEYREVOKED),
SD_BUS_ERROR_MAP(BUS_ERROR_HOME_IN_USE, EADDRINUSE),
SD_BUS_ERROR_MAP(BUS_ERROR_REBALANCE_NOT_NEEDED, EALREADY),
SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_REFERENCED, EBADR),
SD_BUS_ERROR_MAP_END
};
@@ -152,5 +152,6 @@
#define BUS_ERROR_HOME_CANT_AUTHENTICATE "org.freedesktop.home1.HomeCantAuthenticate"
#define BUS_ERROR_HOME_IN_USE "org.freedesktop.home1.HomeInUse"
#define BUS_ERROR_REBALANCE_NOT_NEEDED "org.freedesktop.home1.RebalanceNotNeeded"
#define BUS_ERROR_HOME_NOT_REFERENCED "org.freedesktop.home1.HomeNotReferenced"
BUS_ERROR_MAP_ELF_USE(bus_common_errors);
+10 -6
View File
@@ -4038,22 +4038,26 @@ const BusObjectImplementation manager_object = {
&user_object),
};
static int session_jobs_reply(Session *s, uint32_t jid, const char *unit, const char *result) {
static void session_jobs_reply(Session *s, uint32_t jid, const char *unit, const char *result) {
assert(s);
assert(unit);
if (!s->started)
return 0;
return;
if (result && !streq(result, "done")) {
_cleanup_(sd_bus_error_free) sd_bus_error e = SD_BUS_ERROR_NULL;
sd_bus_error_setf(&e, BUS_ERROR_JOB_FAILED,
"Job %u for unit '%s' failed with '%s'", jid, unit, result);
return session_send_create_reply(s, &e);
(void) session_send_create_reply(s, &e);
(void) session_send_upgrade_reply(s, &e);
return;
}
return session_send_create_reply(s, NULL);
(void) session_send_create_reply(s, /* error= */ NULL);
(void) session_send_upgrade_reply(s, /* error= */ NULL);
}
int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *error) {
@@ -4089,7 +4093,7 @@ int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *err
if (session) {
if (streq_ptr(path, session->scope_job)) {
session->scope_job = mfree(session->scope_job);
(void) session_jobs_reply(session, id, unit, result);
session_jobs_reply(session, id, unit, result);
session_save(session);
user_save(session->user);
@@ -4106,7 +4110,7 @@ int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *err
LIST_FOREACH(sessions_by_user, s, user->sessions)
/* Don't propagate user service failures to the client */
(void) session_jobs_reply(s, id, unit, /* error = */ NULL);
session_jobs_reply(s, id, unit, /* error = */ NULL /* don't propagate user service failures to the client */);
user_save(user);
}
+81 -1
View File
@@ -404,6 +404,62 @@ static int method_set_type(sd_bus_message *message, void *userdata, sd_bus_error
return sd_bus_reply_method_return(message, NULL);
}
static int method_set_class(sd_bus_message *message, void *userdata, sd_bus_error *error) {
_cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL;
Session *s = ASSERT_PTR(userdata);
SessionClass class;
const char *c;
uid_t uid;
int r;
assert(message);
r = sd_bus_message_read(message, "s", &c);
if (r < 0)
return r;
class = session_class_from_string(c);
if (class < 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
"Invalid session class '%s'", c);
/* For now, we'll allow only upgrades user-incomplete → user */
if (class != SESSION_USER)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
"Class may only be set to 'user', refusing.");
if (s->class == SESSION_USER) /* No change, shortcut */
return sd_bus_reply_method_return(message, NULL);
if (s->class != SESSION_USER_INCOMPLETE)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
"Only sessions with class 'user-incomplete' may change class, refusing.");
if (s->upgrade_message)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
"Set session class operation already in progress, refsuing.");
r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds);
if (r < 0)
return r;
r = sd_bus_creds_get_euid(creds, &uid);
if (r < 0)
return r;
if (uid != 0 && uid != s->user->user_record->uid)
return sd_bus_error_set(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may change its class");
session_set_class(s, class);
sd_bus_message_unref(s->upgrade_message);
s->upgrade_message = sd_bus_message_ref(message);
r = session_send_upgrade_reply(s, /* error= */ NULL);
if (r < 0)
return r;
return 1;
}
static int method_set_display(sd_bus_message *message, void *userdata, sd_bus_error *error) {
Session *s = ASSERT_PTR(userdata);
const char *display;
@@ -875,6 +931,25 @@ int session_send_create_reply(Session *s, sd_bus_error *error) {
false);
}
int session_send_upgrade_reply(Session *s, sd_bus_error *error) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *c = NULL;
assert(s);
if (!s->upgrade_message)
return 0;
if (!sd_bus_error_is_set(error) && !session_ready(s))
return 0;
c = TAKE_PTR(s->upgrade_message);
if (error)
return sd_bus_reply_method_error(c, error);
session_save(s);
return sd_bus_reply_method_return(c, NULL);
}
static const sd_bus_vtable session_vtable[] = {
SD_BUS_VTABLE_START(0),
@@ -895,7 +970,7 @@ static const sd_bus_vtable session_vtable[] = {
SD_BUS_PROPERTY("Leader", "u", bus_property_get_pid, offsetof(Session, leader.pid), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Audit", "u", NULL, offsetof(Session, audit_id), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Type", "s", property_get_type, offsetof(Session, type), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("Class", "s", property_get_class, offsetof(Session, class), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Class", "s", property_get_class, offsetof(Session, class), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("Active", "b", property_get_active, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("State", "s", property_get_state, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("IdleHint", "b", property_get_idle_hint, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
@@ -953,6 +1028,11 @@ static const sd_bus_vtable session_vtable[] = {
SD_BUS_NO_RESULT,
method_set_type,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("SetClass",
SD_BUS_ARGS("s", class),
SD_BUS_NO_RESULT,
method_set_class,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("SetDisplay",
SD_BUS_ARGS("s", display),
SD_BUS_NO_RESULT,
+1
View File
@@ -16,6 +16,7 @@ int session_send_lock(Session *s, bool lock);
int session_send_lock_all(Manager *m, bool lock);
int session_send_create_reply(Session *s, sd_bus_error *error);
int session_send_upgrade_reply(Session *s, sd_bus_error *error);
int bus_session_method_activate(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_session_method_lock(sd_bus_message *message, void *userdata, sd_bus_error *error);
+16
View File
@@ -187,6 +187,7 @@ Session* session_free(Session *s) {
session_reset_leader(s, /* keep_fdstore = */ true);
sd_bus_message_unref(s->create_message);
sd_bus_message_unref(s->upgrade_message);
free(s->tty);
free(s->display);
@@ -1210,6 +1211,20 @@ void session_set_type(Session *s, SessionType t) {
(void) session_send_changed(s, "Type", NULL);
}
void session_set_class(Session *s, SessionClass c) {
assert(s);
if (s->class == c)
return;
s->class = c;
(void) session_save(s);
(void) session_send_changed(s, "Class", NULL);
/* This class change might mean we need the per-user session manager now. Try to start it */
user_start_service_manager(s->user);
}
int session_set_display(Session *s, const char *display) {
int r;
@@ -1648,6 +1663,7 @@ DEFINE_STRING_TABLE_LOOKUP(session_type, SessionType);
static const char* const session_class_table[_SESSION_CLASS_MAX] = {
[SESSION_USER] = "user",
[SESSION_USER_EARLY] = "user-early",
[SESSION_USER_INCOMPLETE] = "user-incomplete",
[SESSION_GREETER] = "greeter",
[SESSION_LOCK_SCREEN] = "lock-screen",
[SESSION_BACKGROUND] = "background",
+5 -2
View File
@@ -22,6 +22,7 @@ typedef enum SessionState {
typedef enum SessionClass {
SESSION_USER, /* A regular user session */
SESSION_USER_EARLY, /* A user session, that is not ordered after systemd-user-sessions.service (i.e. for root) */
SESSION_USER_INCOMPLETE, /* A user session that is only half-way set up and doesn't pull in the service manager, and can be upgraded to a full user session later */
SESSION_GREETER, /* A login greeter pseudo-session */
SESSION_LOCK_SCREEN, /* A lock screen */
SESSION_BACKGROUND, /* Things like cron jobs, which are non-interactive */
@@ -37,7 +38,7 @@ typedef enum SessionClass {
#define SESSION_CLASS_IS_EARLY(class) IN_SET((class), SESSION_USER_EARLY, SESSION_MANAGER_EARLY)
/* Which session classes want their own scope units? (all of them, except the manager, which comes in its own service unit already */
#define SESSION_CLASS_WANTS_SCOPE(class) IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER, SESSION_LOCK_SCREEN, SESSION_BACKGROUND, SESSION_BACKGROUND_LIGHT)
#define SESSION_CLASS_WANTS_SCOPE(class) IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_USER_INCOMPLETE, SESSION_GREETER, SESSION_LOCK_SCREEN, SESSION_BACKGROUND, SESSION_BACKGROUND_LIGHT)
/* Which session classes want their own per-user service manager? */
#define SESSION_CLASS_WANTS_SERVICE_MANAGER(class) IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER, SESSION_LOCK_SCREEN, SESSION_BACKGROUND)
@@ -145,7 +146,8 @@ struct Session {
bool was_active:1;
sd_bus_message *create_message;
sd_bus_message *create_message; /* The D-Bus message used to create the session, which we haven't responded to yet */
sd_bus_message *upgrade_message; /* The D-Bus message used to upgrade the session class user-incomplete → user, wich we haven't responded to yet */
/* Set up when a client requested to release the session via the bus */
sd_event_source *timer_event_source;
@@ -178,6 +180,7 @@ int session_set_idle_hint(Session *s, bool b);
int session_get_locked_hint(Session *s);
int session_set_locked_hint(Session *s, bool b);
void session_set_type(Session *s, SessionType t);
void session_set_class(Session *s, SessionClass c);
int session_set_display(Session *s, const char *display);
int session_set_tty(Session *s, const char *tty);
int session_create_fifo(Session *s);
+4
View File
@@ -334,6 +334,10 @@
send_interface="org.freedesktop.login1.Session"
send_member="SetType"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.login1.Session"
send_member="SetClass"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.login1.Session"
send_member="TakeDevice"/>
+29 -1
View File
@@ -525,6 +525,26 @@ static const char* getenv_harder(pam_handle_t *handle, const char *key, const ch
return fallback;
}
static bool getenv_harder_bool(pam_handle_t *handle, const char *key, bool fallback) {
const char *v;
int r;
assert(handle);
assert(key);
v = getenv_harder(handle, key, NULL);
if (isempty(v))
return fallback;
r = parse_boolean(v);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Boolean environment variable value of '%s' is not valid: %s", key, v);
return fallback;
}
return r;
}
static int update_environment(pam_handle_t *handle, const char *key, const char *value) {
int r;
@@ -903,7 +923,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
int session_fd = -EBADF, existing, r;
bool debug = false, remote;
bool debug = false, remote, incomplete;
uint32_t vtnr = 0;
uid_t original_uid;
@@ -944,6 +964,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
type = getenv_harder(handle, "XDG_SESSION_TYPE", type_pam);
class = getenv_harder(handle, "XDG_SESSION_CLASS", class_pam);
desktop = getenv_harder(handle, "XDG_SESSION_DESKTOP", desktop_pam);
incomplete = getenv_harder_bool(handle, "XDG_SESSION_INCOMPLETE", false);
if (streq_ptr(service, "systemd-user")) {
/* If we detect that we are running in the "systemd-user" PAM stack, then let's patch the class to
@@ -1009,6 +1030,13 @@ _public_ PAM_EXTERN int pam_sm_open_session(
((IN_SET(user_record_disposition(ur), USER_INTRINSIC, USER_SYSTEM, USER_DYNAMIC) &&
streq(type, "tty")) ? "user-early" : "user");
if (incomplete) {
if (streq(class, "user"))
class = "user-incomplete";
else
pam_syslog_pam_error(handle, LOG_WARNING, 0, "PAM session of class '%s' is incomplete, which is not supported, ignoring.", class);
}
remote = !isempty(remote_host) && !is_localhost(remote_host);
r = pam_get_data(handle, "systemd.memory_max", (const void **)&memory_max);
+16 -4
View File
@@ -204,8 +204,14 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) {
printf(" Real Name: %s\n", hr->real_name);
hd = user_record_home_directory(hr);
if (hd)
printf(" Directory: %s\n", hd);
if (hd) {
printf(" Directory: %s", hd);
if (hr->fallback_home_directory && hr->use_fallback)
printf(" %s(fallback)%s", ansi_highlight_yellow(), ansi_normal());
printf("\n");
}
storage = user_record_storage(hr);
if (storage >= 0) /* Let's be political, and clarify which storage we like, and which we don't. About CIFS we don't complain. */
@@ -223,8 +229,14 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) {
printf(" Removable: %s\n", yes_no(b));
shell = user_record_shell(hr);
if (shell)
printf(" Shell: %s\n", shell);
if (shell) {
printf(" Shell: %s", shell);
if (hr->fallback_shell && hr->use_fallback)
printf(" %s(fallback)%s", ansi_highlight_yellow(), ansi_normal());
printf("\n");
}
if (hr->email_address)
printf(" Email: %s\n", hr->email_address);
+52 -20
View File
@@ -167,6 +167,9 @@ static UserRecord* user_record_free(UserRecord *h) {
free(h->home_directory);
free(h->home_directory_auto);
free(h->fallback_shell);
free(h->fallback_home_directory);
strv_free(h->member_of);
strv_free(h->capability_bounding_set);
strv_free(h->capability_ambient_set);
@@ -1325,23 +1328,26 @@ static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDisp
static int dispatch_status(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
static const JsonDispatch status_dispatch_table[] = {
{ "diskUsage", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_usage), 0 },
{ "diskFree", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_free), 0 },
{ "diskSize", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 },
{ "diskCeiling", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_ceiling), 0 },
{ "diskFloor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_floor), 0 },
{ "state", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, state), JSON_SAFE },
{ "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE },
{ "signedLocally", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, signed_locally), 0 },
{ "goodAuthenticationCounter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, good_authentication_counter), 0 },
{ "badAuthenticationCounter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, bad_authentication_counter), 0 },
{ "lastGoodAuthenticationUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, last_good_authentication_usec), 0 },
{ "lastBadAuthenticationUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, last_bad_authentication_usec), 0 },
{ "rateLimitBeginUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, ratelimit_begin_usec), 0 },
{ "rateLimitCount", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, ratelimit_count), 0 },
{ "removable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, removable), 0 },
{ "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 },
{ "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
{ "diskUsage", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_usage), 0 },
{ "diskFree", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_free), 0 },
{ "diskSize", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 },
{ "diskCeiling", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_ceiling), 0 },
{ "diskFloor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_floor), 0 },
{ "state", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, state), JSON_SAFE },
{ "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE },
{ "signedLocally", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, signed_locally), 0 },
{ "goodAuthenticationCounter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, good_authentication_counter), 0 },
{ "badAuthenticationCounter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, bad_authentication_counter), 0 },
{ "lastGoodAuthenticationUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, last_good_authentication_usec), 0 },
{ "lastBadAuthenticationUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, last_bad_authentication_usec), 0 },
{ "rateLimitBeginUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, ratelimit_begin_usec), 0 },
{ "rateLimitCount", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, ratelimit_count), 0 },
{ "removable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, removable), 0 },
{ "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 },
{ "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
{ "fallbackShell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, fallback_shell), 0 },
{ "fallbackHomeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, fallback_home_directory), 0 },
{ "useFallback", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, use_fallback), 0 },
{},
};
@@ -1752,7 +1758,7 @@ mode_t user_record_access_mode(UserRecord *h) {
return h->access_mode != MODE_INVALID ? h->access_mode : 0700;
}
const char* user_record_home_directory(UserRecord *h) {
static const char *user_record_home_directory_real(UserRecord *h) {
assert(h);
if (h->home_directory)
@@ -1767,6 +1773,15 @@ const char* user_record_home_directory(UserRecord *h) {
return "/";
}
const char* user_record_home_directory(UserRecord *h) {
assert(h);
if (h->use_fallback && h->fallback_home_directory)
return h->fallback_home_directory;
return user_record_home_directory_real(h);
}
const char *user_record_image_path(UserRecord *h) {
assert(h);
@@ -1775,7 +1790,9 @@ const char *user_record_image_path(UserRecord *h) {
if (h->image_path_auto)
return h->image_path_auto;
return IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT) ? user_record_home_directory(h) : NULL;
/* For some storage types the image is the home directory itself. (But let's ignore the fallback logic for it) */
return IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT) ?
user_record_home_directory_real(h) : NULL;
}
const char *user_record_cifs_user_name(UserRecord *h) {
@@ -1792,7 +1809,7 @@ unsigned long user_record_mount_flags(UserRecord *h) {
(h->nodev ? MS_NODEV : 0);
}
const char *user_record_shell(UserRecord *h) {
static const char *user_record_shell_real(UserRecord *h) {
assert(h);
if (h->shell)
@@ -1807,6 +1824,21 @@ const char *user_record_shell(UserRecord *h) {
return NOLOGIN;
}
const char *user_record_shell(UserRecord *h) {
const char *shell;
assert(h);
shell = user_record_shell_real(h);
/* Return fallback shall if we are told so — except if the primary shell is already a nologin shell,
* then let's not risk anything. */
if (h->use_fallback && h->fallback_shell)
return is_nologin_shell(shell) ? NOLOGIN : h->fallback_shell;
return shell;
}
const char *user_record_real_name(UserRecord *h) {
assert(h);
+6
View File
@@ -293,6 +293,10 @@ typedef struct UserRecord {
char *home_directory;
char *home_directory_auto; /* when none is set explicitly, this is where we place the implicit home directory */
/* fallback shell and home dir */
char *fallback_shell;
char *fallback_home_directory;
uid_t uid;
gid_t gid;
@@ -322,6 +326,8 @@ typedef struct UserRecord {
uint64_t disk_ceiling;
uint64_t disk_floor;
bool use_fallback; /* if true → use fallback_shell + fallback_home_directory instead of the regular ones */
char *state;
char *service;
int signed_locally;
+3
View File
@@ -21,6 +21,9 @@ test_append_files() {
install_btrfs
generate_module_dependencies
fi
inst_binary ssh
inst_binary sshd
inst_binary ssh-keygen
}
do_test "$@"
+83
View File
@@ -314,6 +314,89 @@ for opt in json multiplexer output synthesize with-dropin with-nss with-varlink;
(! userdbctl "--$opt=foo" "--$opt=''" "--$opt=🐱")
done
# FIXME: sshd seems to crash inside asan currently, skip the actual ssh test hence
if command -v ssh &> /dev/null && command -v sshd &> /dev/null && ! [[ -v ASAN_OPTIONS ]]; then
at_exit() {
systemctl stop mysshserver.socket
rm -f /tmp/homed.id_rsa /run/systemd/system/mysshserver.socket /run/systemd/system/mysshserver@.service
systemctl daemon-reload
homectl remove homedsshtest ||:
mv /etc/pam.d/sshd.save46 mv /etc/pam.d/sshd
}
trap at_exit EXIT
# Test that SSH logins work with delayed unlocking
ssh-keygen -N '' -C '' -t rsa -f /tmp/homed.id_rsa
NEWPASSWORD=hunter4711 homectl create \
--disk-size=min \
--luks-discard=yes \
--luks-pbkdf-type=pbkdf2 \
--luks-pbkdf-time-cost=1ms \
--enforce-password-policy=no \
--ssh-authorized-keys=@/tmp/homed.id_rsa.pub \
--stop-delay=0 \
homedsshtest
mkdir -p /etc/ssh
test -f /etc/ssh/ssh_host_rsa_key || ssh-keygen -t rsa -C '' -N '' -f /etc/ssh/ssh_host_rsa_key
# ssh wants this dir around, but distros cannot agree on a common name for it, let's just create all that are aware of distros use
mkdir -p /usr/share/empty.sshd /var/empty /var/empty/sshd
mv /etc/pam.d/sshd /etc/pam.d/sshd.save46
cat > /etc/pam.d/sshd <<EOF
auth sufficient pam_unix.so nullok
auth sufficient pam_systemd_home.so
auth required pam_deny.so
account sufficient pam_systemd_home.so
account sufficient pam_unix.so
account required pam_permit.so
session optional pam_systemd_home.so
session optional pam_systemd.so
session required pam_unix.so
EOF
cat >> /etc/ssh/sshd_config <<EOF
AuthorizedKeysCommand /usr/bin/userdbctl ssh-authorized-keys %u
AuthorizedKeysCommandUser root
UsePAM yes
AcceptEnv PASSWORD
LogLevel DEBUG3
EOF
cat > /run/systemd/system/mysshserver.socket <<EOF
[Socket]
ListenStream=4711
Accept=yes
EOF
cat > /run/systemd/system/mysshserver@.service <<EOF
[Service]
ExecStart=-/usr/sbin/sshd -i -d -e
StandardInput=socket
StandardOutput=socket
StandardError=journal
EOF
systemctl daemon-reload
systemctl start mysshserver.socket
userdbctl user -j homedsshtest
ssh -t -t -4 -p 4711 -i /tmp/homed.id_rsa -o "SetEnv PASSWORD=hunter4711" -o "StrictHostKeyChecking no" homedsshtest@localhost echo zzz | tail -n 1 | tr -d '\r' > /tmp/homedsshtest.out
cat /tmp/homedsshtest.out
test "$(cat /tmp/homedsshtest.out)" = "zzz"
rm /tmp/homedsshtest.out
ssh -t -t -4 -p 4711 -i /tmp/homed.id_rsa -o "SetEnv PASSWORD=hunter4711" -o "StrictHostKeyChecking no" homedsshtest@localhost env
wait_for_state homedsshtest inactive
homectl remove homedsshtest
fi
systemd-analyze log-level info
touch /testok