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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user