]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Merge pull request #31173 from yuwata/network-route-check-conflict
authorYu Watanabe <watanabe.yu+github@gmail.com>
Wed, 14 Feb 2024 23:12:42 +0000 (08:12 +0900)
committerGitHub <noreply@github.com>
Wed, 14 Feb 2024 23:12:42 +0000 (08:12 +0900)
network/route: check if existing route can be updated

50 files changed:
TODO
docs/USER_RECORD.md
man/org.freedesktop.home1.xml
man/org.freedesktop.login1.xml
man/pam_systemd.xml
man/sd_session_is_active.xml
man/systemctl.xml
man/systemd.exec.xml
src/boot/bootctl-reboot-to-firmware.c
src/boot/bootctl-reboot-to-firmware.h
src/boot/bootctl-status.c
src/boot/bootctl-status.h
src/boot/bootctl.c
src/boot/bootctl.h
src/home/homectl.c
src/home/homed-home-bus.c
src/home/homed-home.c
src/home/homed-home.h
src/home/homed-manager-bus.c
src/home/meson.build
src/home/org.freedesktop.home1.conf
src/home/org.freedesktop.home1.policy
src/home/pam_systemd_home.c
src/libsystemd/sd-bus/bus-common-errors.c
src/libsystemd/sd-bus/bus-common-errors.h
src/login/logind-dbus.c
src/login/logind-session-dbus.c
src/login/logind-session-dbus.h
src/login/logind-session.c
src/login/logind-session.h
src/login/org.freedesktop.login1.conf
src/login/pam_systemd.c
src/shared/bootspec.c
src/shared/bootspec.h
src/shared/efi-loader.c
src/shared/meson.build
src/shared/user-record-show.c
src/shared/user-record.c
src/shared/user-record.h
src/shared/varlink-io.systemd.BootControl.c [new file with mode: 0644]
src/shared/varlink-io.systemd.BootControl.h [new file with mode: 0644]
src/systemctl/systemctl-util.c
src/test/test-varlink-idl.c
test/TEST-46-HOMED/test.sh
test/test-functions
test/units/testsuite-46.sh
test/units/testsuite-74.bootctl.sh
units/meson.build
units/systemd-bootctl.socket [new file with mode: 0644]
units/systemd-bootctl@.service.in [new file with mode: 0644]

diff --git a/TODO b/TODO
index c0260c7b037538756348841a78d53a78278d2400..a64a6a0c1c29adc03dcaa9ea4d3c44736532cb52 100644 (file)
--- a/TODO
+++ b/TODO
@@ -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
index aba45c39f42bc1b9f4b13592ee7b1fe5dc6542b8..50ea58ac3182b9edae452ccfb46f56d808cb99a2 100644 (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
index 7c604d2e0c1d0c22810b0ec3d28a6e46d354fbe9..b1bb6587dc449a881d280587e744c518500596e9 100644 (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>
 
index 8b84f227ff1ff7ea19e7c98511a08fb0715c9418..dffd16e325639f254c0502775ed50d6b10915960 100644 (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>
index 1a841df13bf19441a0ce0a9908a11080f1d32f18..dcd104907a51ff2692a3a2dccf5809e4cc04b01b 100644 (file)
                 <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>
index 747fab4c68ff2ceb7e99b8614a490e1d14409923..1cbef64e003c14f2008e94e5bc8ea0555ff773bb 100644 (file)
 
     <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>
 
index 4a84df343b7d826cb23dbc26cb786110d643d733..98844783b01c1db4f82bbc156da3337d661fbf36 100644 (file)
@@ -2289,7 +2289,10 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
             </listitem>
             <listitem>
               <para>when a <command>stop</command>-ped, <command>disable</command>-d, or <command>mask</command>-ed
-              unit still has active triggering units.</para>
+              unit still has active triggering units,</para>
+            </listitem>
+            <listitem>
+              <para>when a unit file is changed and requires <command>daemon-reload</command>.</para>
             </listitem>
           </itemizedlist>
           </para>
index 300255e03885c4acaecb9041443edcf4200be07b..dac19d6537022de05df097aaf9083b7f49e924dd 100644 (file)
@@ -1237,9 +1237,9 @@ CapabilityBoundingSet=~CAP_B CAP_C</programlisting>
       <varlistentry>
         <term><varname>IgnoreSIGPIPE=</varname></term>
 
-        <listitem><para>Takes a boolean argument. If true, causes <constant>SIGPIPE</constant> to be ignored in the
-        executed process. Defaults to true because <constant>SIGPIPE</constant> generally is useful only in shell
-        pipelines.</para></listitem>
+        <listitem><para>Takes a boolean argument. If true, <constant>SIGPIPE</constant> is ignored in the
+        executed process. Defaults to true since <constant>SIGPIPE</constant> is generally only useful in
+        shell pipelines.</para></listitem>
       </varlistentry>
 
     </variablelist>
index 91f259768c00e0e888d01164a509fe2ba0252798..cdb04f804505ced28f0d01be5805222377229d54 100644 (file)
@@ -2,6 +2,7 @@
 
 #include "bootctl-reboot-to-firmware.h"
 #include "efi-api.h"
+#include "errno-util.h"
 #include "parse-util.h"
 
 int verb_reboot_to_firmware(int argc, char *argv[], void *userdata) {
@@ -17,7 +18,7 @@ int verb_reboot_to_firmware(int argc, char *argv[], void *userdata) {
                         puts("supported");
                         return 1; /* recognizable error #1 */
                 }
-                if (r == -EOPNOTSUPP) {
+                if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) {
                         puts("not supported");
                         return 2; /* recognizable error #2 */
                 }
@@ -36,3 +37,39 @@ int verb_reboot_to_firmware(int argc, char *argv[], void *userdata) {
                 return 0;
         }
 }
+
+int vl_method_set_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+        static const JsonDispatch dispatch_table[] = {
+                { "state", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, 0, 0 },
+                {}
+        };
+        bool b;
+        int r;
+
+        r = varlink_dispatch(link, parameters, dispatch_table, &b);
+        if (r != 0)
+                return r;
+
+        r = efi_set_reboot_to_firmware(b);
+        if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
+                return varlink_error(link, "io.systemd.BootControl.RebootToFirmwareNotSupported", NULL);
+        if (r < 0)
+                return r;
+
+        return varlink_reply(link, NULL);
+}
+
+int vl_method_get_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+        int r;
+
+        if (json_variant_elements(parameters) > 0)
+                return varlink_error_invalid_parameter(link, parameters);
+
+        r = efi_get_reboot_to_firmware();
+        if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
+                return varlink_error(link, "io.systemd.BootControl.RebootToFirmwareNotSupported", NULL);
+        if (r < 0)
+                return r;
+
+        return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_BOOLEAN("state", r)));
+}
index 0ca4b2c3a30ded150db86636e81f0fd24898c8ce..fb8a2485b33c65ae5eac01b4e3726b20f9d75ad0 100644 (file)
@@ -1,3 +1,8 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include "varlink.h"
+
 int verb_reboot_to_firmware(int argc, char *argv[], void *userdata);
+
+int vl_method_set_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
+int vl_method_get_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
index f8b57c1f9d474fbf5ae27f8e21b5d336501891f8..58b6276dd914dc0e438949a52c1578969871062c 100644 (file)
@@ -318,7 +318,13 @@ int verb_status(int argc, char *argv[], void *userdata) {
         dev_t esp_devid = 0, xbootldr_devid = 0;
         int r, k;
 
-        r = acquire_esp(/* unprivileged_mode= */ -1, /* graceful= */ false, NULL, NULL, NULL, &esp_uuid, &esp_devid);
+        r = acquire_esp(/* unprivileged_mode= */ -1,
+                        /* graceful= */ false,
+                        /* ret_part= */ NULL,
+                        /* ret_pstart= */ NULL,
+                        /* ret_psize= */ NULL,
+                        &esp_uuid,
+                        &esp_devid);
         if (arg_print_esp_path) {
                 if (r == -EACCES) /* If we couldn't acquire the ESP path, log about access errors (which is the only
                                    * error the find_esp_and_warn() won't log on its own) */
@@ -330,7 +336,10 @@ int verb_status(int argc, char *argv[], void *userdata) {
                 return 0;
         }
 
-        r = acquire_xbootldr(/* unprivileged_mode= */ -1, &xbootldr_uuid, &xbootldr_devid);
+        r = acquire_xbootldr(
+                        /* unprivileged_mode= */ -1,
+                        &xbootldr_uuid,
+                        &xbootldr_devid);
         if (arg_print_dollar_boot_path) {
                 if (r == -EACCES)
                         return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
@@ -825,3 +834,58 @@ int verb_list(int argc, char *argv[], void *userdata) {
 int verb_unlink(int argc, char *argv[], void *userdata) {
         return verb_list(argc, argv, userdata);
 }
+
+int vl_method_list_boot_entries(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+        _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
+        dev_t esp_devid = 0, xbootldr_devid = 0;
+        int r;
+
+        assert(link);
+
+        if (json_variant_elements(parameters) > 0)
+                return varlink_error_invalid_parameter(link, parameters);
+
+        r = acquire_esp(/* unprivileged_mode= */ false,
+                        /* graceful= */ false,
+                        /* ret_part= */ NULL,
+                        /* ret_pstart= */ NULL,
+                        /* ret_psize= */ NULL,
+                        /* ret_uuid=*/ NULL,
+                        &esp_devid);
+        if (r == -EACCES) /* We really need the ESP path for this call, hence also log about access errors */
+                return log_error_errno(r, "Failed to determine ESP location: %m");
+        if (r < 0)
+                return r;
+
+        r = acquire_xbootldr(
+                        /* unprivileged_mode= */ false,
+                        /* ret_uuid= */ NULL,
+                        &xbootldr_devid);
+        if (r == -EACCES)
+                return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
+        if (r < 0)
+                return r;
+
+        r = boot_config_load_and_select(&config, arg_esp_path, esp_devid, arg_xbootldr_path, xbootldr_devid);
+        if (r < 0)
+                return r;
+
+        _cleanup_(json_variant_unrefp) JsonVariant *previous = NULL;
+        for (size_t i = 0; i < config.n_entries; i++) {
+                if (previous) {
+                        r = varlink_notifyb(link, JSON_BUILD_OBJECT(
+                                                            JSON_BUILD_PAIR_VARIANT("entry", previous)));
+                        if (r < 0)
+                                return r;
+
+                        previous = json_variant_unref(previous);
+                }
+
+                r = boot_entry_to_json(&config, i, &previous);
+                if (r < 0)
+                        return r;
+        }
+
+        return varlink_replyb(link, JSON_BUILD_OBJECT(
+                                              JSON_BUILD_PAIR_CONDITION(previous, "entry", JSON_BUILD_VARIANT(previous))));
+}
index f7998a3303eb5cec2980664764d0606e5256f58f..6fd436513ba04ef76e872cdd27c633356a6603bc 100644 (file)
@@ -1,5 +1,9 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include "varlink.h"
+
 int verb_status(int argc, char *argv[], void *userdata);
 int verb_list(int argc, char *argv[], void *userdata);
 int verb_unlink(int argc, char *argv[], void *userdata);
+
+int vl_method_list_boot_entries(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
index f608e8cc8e0a917dc31e90b388c1e25cf9e7d995..bd10c08b82eaa1aad093996c4a42aeeab14d99ba 100644 (file)
@@ -22,6 +22,8 @@
 #include "parse-argument.h"
 #include "pretty-print.h"
 #include "utf8.h"
+#include "varlink.h"
+#include "varlink-io.systemd.BootControl.h"
 #include "verbs.h"
 #include "virt.h"
 
@@ -53,6 +55,7 @@ InstallSource arg_install_source = ARG_INSTALL_SOURCE_AUTO;
 char *arg_efi_boot_option_description = NULL;
 bool arg_dry_run = false;
 ImagePolicy *arg_image_policy = NULL;
+bool arg_varlink = false;
 
 STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep);
@@ -418,6 +421,14 @@ static int parse_argv(int argc, char *argv[]) {
         if (arg_dry_run && argv[optind] && !STR_IN_SET(argv[optind], "unlink", "cleanup"))
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--dry is only supported with --unlink or --cleanup");
 
+        r = varlink_invocation(VARLINK_ALLOW_ACCEPT);
+        if (r < 0)
+                return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
+        if (r > 0) {
+                arg_varlink = true;
+                arg_pager_flags |= PAGER_DISABLE;
+        }
+
         return 1;
 }
 
@@ -462,6 +473,34 @@ static int run(int argc, char *argv[]) {
         if (r <= 0)
                 return r;
 
+        if (arg_varlink) {
+                _cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL;
+
+                /* Invocation as Varlink service */
+
+                r = varlink_server_new(&varlink_server, VARLINK_SERVER_ROOT_ONLY);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to allocate Varlink server: %m");
+
+                r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_BootControl);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to add Varlink interface: %m");
+
+                r = varlink_server_bind_method_many(
+                                varlink_server,
+                                "io.systemd.BootControl.ListBootEntries",     vl_method_list_boot_entries,
+                                "io.systemd.BootControl.SetRebootToFirmware", vl_method_set_reboot_to_firmware,
+                                "io.systemd.BootControl.GetRebootToFirmware", vl_method_get_reboot_to_firmware);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to bind Varlink methods: %m");
+
+                r = varlink_server_loop_auto(varlink_server);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to run Varlink event loop: %m");
+
+                return EXIT_SUCCESS;
+        }
+
         if (arg_print_root_device > 0) {
                 _cleanup_free_ char *path = NULL;
                 dev_t devno;
index e395b3324ad803d1b1a4c010965ab19a9962ec03..25cb5166ce75244d5618c71eda2d70488a4db0f4 100644 (file)
@@ -36,6 +36,7 @@ extern InstallSource arg_install_source;
 extern char *arg_efi_boot_option_description;
 extern bool arg_dry_run;
 extern ImagePolicy *arg_image_policy;
+extern bool arg_varlink;
 
 static inline const char *arg_dollar_boot_path(void) {
         /* $BOOT shall be the XBOOTLDR partition if it exists, and otherwise the ESP */
index 72a24e7156265058a95e6b436ae3521dba19c89c..34c62f1edb59c8b1321941ac4c7092d3c5c51182 100644 (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;
index 30f5735443ac6214b3a10bbb70600c3e712f103a..b459cadcbfc2b795bd8ac1cc88ffef15e04b4912 100644 (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,
index 8b0df9175ef2b5f7a4cea9e8eba09195a44977e1..e1422475dceef8d34f1e518e4ae96a2c910ced96 100644 (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)),
index b8b004642294628db7bd2c68ebcfe32a280ca79c..6c069ab5f0269d48c9186a2d97a81af0a2151a61 100644 (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);
index c613eed4d5634218283815f25f7a4314e341e7e3..c8e232f4257c91846c0e92b8e028f4cd5f72e463 100644 (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,
index 09831dee4529afa2678cbc22e99cece02b753c9b..c6fc5f3cea5c6c4bbb1a8080449b6a05cdbc7e4b 100644 (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
index 6d13535f957ebcf41c596f78de0cff9b7b1ff969..d2c4b9dd290d12af9311eecb5c9e685df0c8807a 100644 (file)
                        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"/>
                        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"/>
                        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"/>
                        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"/>
index 2ac710d66c6101ab0a11b24134a1ee75079e3ed5..be32b2e8e4e198dd7256f110d0c7a4b420f9bf73 100644 (file)
                         <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>
index 7460aebc567ef770856535768cf3809b8b15cbe7..21caa5a0589372e488d3debe1f143511f6813284 100644 (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;
+                                        }
 
-                                        return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR;
-                                }
-                                if (r != PAM_SUCCESS)
+                                        /* 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;
 
index efdd6539ccebd017523f92d2b18878870ab64112..e44795b1d3033f1ca035f17ba8ceb2197ad77ca8 100644 (file)
@@ -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
 };
index 2961ee4a9ecaba87902803cdba114752e8585045..36f53dbde14b75dac7652eb443d67e614a83b019 100644 (file)
 #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);
index 199c7f9e9b8c754ee2b5b602088fe528855012cc..933b69542a1c884f87532d6c661ddded330fe977 100644 (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);
                 }
index 7217b8147642c188d959c77d826bff94cf3f22e5..bd8618ba5057bbcf7ee5d7fef3d32c90153ae73e 100644 (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,
index 751ca86c0dbcfe74e93bef6e68e604b1714e7079..a02656293821532dbf2c5d8a6b3210e18e71510b 100644 (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);
index 69dc52ad66ce0d025c081cd5270d38abc1bc265e..0a2f04b021d442cd7b860d549ec7db97061607c1 100644 (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",
index 6a0fb5430359f4fbb8895c66f7f786a4cf84d3d2..b36d4221411b9e3e824a9da23b1de7928e9c9931 100644 (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, which 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);
index e49496fce840be9deb22f9dbf3942b510b6d7ca8..9b59e9ce556c88f331d48dff2446cbbad5948636 100644 (file)
                        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"/>
index 197729dd345d047d6cebc720c7c24aa592c0ac77..0e67d063a4cbe700deaba18896d46bed35f26b0a 100644 (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);
index b0bf94c7daefcd4e8cc15956d6c514ea9b899cd9..df45c670007c24f27a05ec415ea97c13f57850ea 100644 (file)
@@ -1668,6 +1668,63 @@ int show_boot_entry(
         return -status;
 }
 
+int boot_entry_to_json(const BootConfig *c, size_t i, JsonVariant **ret) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        const BootEntry *e;
+        int r;
+
+        assert(c);
+        assert(ret);
+
+        if (i >= c->n_entries) {
+                *ret = NULL;
+                return 0;
+        }
+
+        e = c->entries + i;
+
+        r = json_variant_merge_objectb(
+                        &v, JSON_BUILD_OBJECT(
+                                        JSON_BUILD_PAIR("type", JSON_BUILD_STRING(boot_entry_type_json_to_string(e->type))),
+                                        JSON_BUILD_PAIR_CONDITION(e->id, "id", JSON_BUILD_STRING(e->id)),
+                                        JSON_BUILD_PAIR_CONDITION(e->path, "path", JSON_BUILD_STRING(e->path)),
+                                        JSON_BUILD_PAIR_CONDITION(e->root, "root", JSON_BUILD_STRING(e->root)),
+                                        JSON_BUILD_PAIR_CONDITION(e->title, "title", JSON_BUILD_STRING(e->title)),
+                                        JSON_BUILD_PAIR_CONDITION(boot_entry_title(e), "showTitle", JSON_BUILD_STRING(boot_entry_title(e))),
+                                        JSON_BUILD_PAIR_CONDITION(e->sort_key, "sortKey", JSON_BUILD_STRING(e->sort_key)),
+                                        JSON_BUILD_PAIR_CONDITION(e->version, "version", JSON_BUILD_STRING(e->version)),
+                                        JSON_BUILD_PAIR_CONDITION(e->machine_id, "machineId", JSON_BUILD_STRING(e->machine_id)),
+                                        JSON_BUILD_PAIR_CONDITION(e->architecture, "architecture", JSON_BUILD_STRING(e->architecture)),
+                                        JSON_BUILD_PAIR_CONDITION(e->kernel, "linux", JSON_BUILD_STRING(e->kernel)),
+                                        JSON_BUILD_PAIR_CONDITION(e->efi, "efi", JSON_BUILD_STRING(e->efi)),
+                                        JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->initrd), "initrd", JSON_BUILD_STRV(e->initrd)),
+                                        JSON_BUILD_PAIR_CONDITION(e->device_tree, "devicetree", JSON_BUILD_STRING(e->device_tree)),
+                                        JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->device_tree_overlay), "devicetreeOverlay", JSON_BUILD_STRV(e->device_tree_overlay))));
+        if (r < 0)
+                return log_oom();
+
+        /* Sanitizers (only memory sanitizer?) do not like function call with too many
+         * arguments and trigger false positive warnings. Let's not add too many json objects
+         * at once. */
+        r = json_variant_merge_objectb(
+                        &v, JSON_BUILD_OBJECT(
+                                        JSON_BUILD_PAIR("isReported", JSON_BUILD_BOOLEAN(e->reported_by_loader)),
+                                        JSON_BUILD_PAIR_CONDITION(e->tries_left != UINT_MAX, "triesLeft", JSON_BUILD_UNSIGNED(e->tries_left)),
+                                        JSON_BUILD_PAIR_CONDITION(e->tries_done != UINT_MAX, "triesDone", JSON_BUILD_UNSIGNED(e->tries_done)),
+                                        JSON_BUILD_PAIR_CONDITION(c->default_entry >= 0, "isDefault", JSON_BUILD_BOOLEAN(i == (size_t) c->default_entry)),
+                                        JSON_BUILD_PAIR_CONDITION(c->selected_entry >= 0, "isSelected", JSON_BUILD_BOOLEAN(i == (size_t) c->selected_entry))));
+
+        if (r < 0)
+                return log_oom();
+
+        r = json_cmdline(e, &c->global_addons, &v);
+        if (r < 0)
+                return log_oom();
+
+        *ret = TAKE_PTR(v);
+        return 1;
+}
+
 int show_boot_entries(const BootConfig *config, JsonFormatFlags json_format) {
         int r;
 
@@ -1677,44 +1734,9 @@ int show_boot_entries(const BootConfig *config, JsonFormatFlags json_format) {
                 _cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
 
                 for (size_t i = 0; i < config->n_entries; i++) {
-                        const BootEntry *e = config->entries + i;
                         _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
 
-                        r = json_variant_merge_objectb(
-                                        &v, JSON_BUILD_OBJECT(
-                                                       JSON_BUILD_PAIR("type", JSON_BUILD_STRING(boot_entry_type_json_to_string(e->type))),
-                                                       JSON_BUILD_PAIR_CONDITION(e->id, "id", JSON_BUILD_STRING(e->id)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->path, "path", JSON_BUILD_STRING(e->path)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->root, "root", JSON_BUILD_STRING(e->root)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->title, "title", JSON_BUILD_STRING(e->title)),
-                                                       JSON_BUILD_PAIR_CONDITION(boot_entry_title(e), "showTitle", JSON_BUILD_STRING(boot_entry_title(e))),
-                                                       JSON_BUILD_PAIR_CONDITION(e->sort_key, "sortKey", JSON_BUILD_STRING(e->sort_key)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->version, "version", JSON_BUILD_STRING(e->version)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->machine_id, "machineId", JSON_BUILD_STRING(e->machine_id)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->architecture, "architecture", JSON_BUILD_STRING(e->architecture)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->kernel, "linux", JSON_BUILD_STRING(e->kernel)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->efi, "efi", JSON_BUILD_STRING(e->efi)),
-                                                       JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->initrd), "initrd", JSON_BUILD_STRV(e->initrd)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->device_tree, "devicetree", JSON_BUILD_STRING(e->device_tree)),
-                                                       JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->device_tree_overlay), "devicetreeOverlay", JSON_BUILD_STRV(e->device_tree_overlay))));
-                        if (r < 0)
-                                return log_oom();
-
-                        r = json_cmdline(e, &config->global_addons, &v);
-                        if (r < 0)
-                                return log_oom();
-
-                        /* Sanitizers (only memory sanitizer?) do not like function call with too many
-                         * arguments and trigger false positive warnings. Let's not add too many json objects
-                         * at once. */
-                        r = json_variant_merge_objectb(
-                                        &v, JSON_BUILD_OBJECT(
-                                                       JSON_BUILD_PAIR("isReported", JSON_BUILD_BOOLEAN(e->reported_by_loader)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->tries_left != UINT_MAX, "triesLeft", JSON_BUILD_UNSIGNED(e->tries_left)),
-                                                       JSON_BUILD_PAIR_CONDITION(e->tries_done != UINT_MAX, "triesDone", JSON_BUILD_UNSIGNED(e->tries_done)),
-                                                       JSON_BUILD_PAIR_CONDITION(config->default_entry >= 0, "isDefault", JSON_BUILD_BOOLEAN(i == (size_t) config->default_entry)),
-                                                       JSON_BUILD_PAIR_CONDITION(config->selected_entry >= 0, "isSelected", JSON_BUILD_BOOLEAN(i == (size_t) config->selected_entry))));
-
+                        r = boot_entry_to_json(config, i, &v);
                         if (r < 0)
                                 return log_oom();
 
@@ -1723,8 +1745,7 @@ int show_boot_entries(const BootConfig *config, JsonFormatFlags json_format) {
                                 return log_oom();
                 }
 
-                json_variant_dump(array, json_format | JSON_FORMAT_EMPTY_ARRAY, NULL, NULL);
-
+                return json_variant_dump(array, json_format | JSON_FORMAT_EMPTY_ARRAY, NULL, NULL);
         } else {
                 for (size_t n = 0; n < config->n_entries; n++) {
                         r = show_boot_entry(
index 8289325b9e313dd1cab7152206a0c525c577539e..9ae88d78aae532f9d4687bd5a9e9b702acad4a3a 100644 (file)
@@ -143,3 +143,5 @@ int show_boot_entries(
                 JsonFormatFlags json_format);
 
 int boot_filename_extract_tries(const char *fname, char **ret_stripped, unsigned *ret_tries_left, unsigned *ret_tries_done);
+
+int boot_entry_to_json(const BootConfig *c, size_t i, JsonVariant **ret);
index 758aaa13c169ac09ae037ae7114d73aeca5c0ef1..7d6bda924a9a0479b46849dc7121cb12a1c7525a 100644 (file)
@@ -102,7 +102,8 @@ int efi_loader_get_entries(char ***ret) {
         if (r < 0)
                 return r;
 
-        /* The variable contains a series of individually NUL terminated UTF-16 strings. */
+        /* The variable contains a series of individually NUL terminated UTF-16 strings. We gracefully
+         * consider the final NUL byte optional (i.e. the last string may or may not end in a NUL byte).*/
 
         for (size_t i = 0, start = 0;; i++) {
                 _cleanup_free_ char *decoded = NULL;
@@ -116,6 +117,11 @@ int efi_loader_get_entries(char ***ret) {
                 if (!end && entries[i] != 0)
                         continue;
 
+                /* Empty string at the end of variable? That's the trailer, we are done (i.e. we have a final
+                 * NUL terminator). */
+                if (end && start == i)
+                        break;
+
                 /* We reached the end of a string, let's decode it into UTF-8 */
                 decoded = utf16_to_utf8(entries + start, (i - start) * sizeof(char16_t));
                 if (!decoded)
@@ -128,7 +134,8 @@ int efi_loader_get_entries(char ***ret) {
                 } else
                         log_debug("Ignoring invalid loader entry '%s'.", decoded);
 
-                /* We reached the end of the variable */
+                /* Exit the loop if we reached the end of the variable (i.e. we do not have a final NUL
+                 * terminator) */
                 if (end)
                         break;
 
index 81de6708f01d6f3668fa1fd320d4c8ca96bf8166..fe0c9c1f2fa203fadd2b60a00fb44393c1bf993f 100644 (file)
@@ -174,6 +174,7 @@ shared_sources = files(
         'varlink.c',
         'varlink-idl.c',
         'varlink-io.systemd.c',
+        'varlink-io.systemd.BootControl.c',
         'varlink-io.systemd.Credentials.c',
         'varlink-io.systemd.Hostname.c',
         'varlink-io.systemd.Journal.c',
index ac0c7a776ab3e0ac3236a0f8083cd75fb34d34f4..97235bd07b5f76f760691d31ca33b0a3adbbddd3 100644 (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);
index 966abc5c42142cfa3aad407bba23b1330fe032fa..38e5f01c2364424212000b6ca3de6176b2981a2a 100644 (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);
 
index 0afbc796d2809ec16e6c67026f0f6bc655a7ca2a..ee63a5364c2836780e887cdb04eec9eb21f7fd93 100644 (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;
diff --git a/src/shared/varlink-io.systemd.BootControl.c b/src/shared/varlink-io.systemd.BootControl.c
new file mode 100644 (file)
index 0000000..500e072
--- /dev/null
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "varlink-io.systemd.BootControl.h"
+
+static VARLINK_DEFINE_ENUM_TYPE(
+                BootEntryType,
+                VARLINK_DEFINE_ENUM_VALUE(type1),
+                VARLINK_DEFINE_ENUM_VALUE(type2),
+                VARLINK_DEFINE_ENUM_VALUE(loader),
+                VARLINK_DEFINE_ENUM_VALUE(auto));
+
+static VARLINK_DEFINE_STRUCT_TYPE(
+                BootEntry,
+                VARLINK_DEFINE_FIELD_BY_TYPE(type, BootEntryType, 0),
+                VARLINK_DEFINE_FIELD(id, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(path, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(root, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(title, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(showTitle, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(sortKey, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(version, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(machineId, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(architecture, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(options, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(linux, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(efi, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(initrd, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY),
+                VARLINK_DEFINE_FIELD(devicetree, VARLINK_STRING, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(devicetreeOverlay, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY),
+                VARLINK_DEFINE_FIELD(isReported, VARLINK_BOOL, 0),
+                VARLINK_DEFINE_FIELD(triesLeft, VARLINK_INT, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(triesDone, VARLINK_INT, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(isDefault, VARLINK_BOOL, VARLINK_NULLABLE),
+                VARLINK_DEFINE_FIELD(isSelected, VARLINK_BOOL, VARLINK_NULLABLE));
+
+static VARLINK_DEFINE_METHOD(
+                ListBootEntries,
+                VARLINK_DEFINE_OUTPUT_BY_TYPE(entry, BootEntry, VARLINK_NULLABLE));
+
+static VARLINK_DEFINE_METHOD(
+                SetRebootToFirmware,
+                VARLINK_DEFINE_INPUT(state, VARLINK_BOOL, 0));
+
+static VARLINK_DEFINE_METHOD(
+                GetRebootToFirmware,
+                VARLINK_DEFINE_OUTPUT(state, VARLINK_BOOL, 0));
+
+static VARLINK_DEFINE_ERROR(
+                RebootToFirmwareNotSupported);
+
+VARLINK_DEFINE_INTERFACE(
+                io_systemd_BootControl,
+                "io.systemd.BootControl",
+                &vl_type_BootEntryType,
+                &vl_type_BootEntry,
+                &vl_method_ListBootEntries,
+                &vl_method_SetRebootToFirmware,
+                &vl_method_GetRebootToFirmware,
+                &vl_error_RebootToFirmwareNotSupported);
diff --git a/src/shared/varlink-io.systemd.BootControl.h b/src/shared/varlink-io.systemd.BootControl.h
new file mode 100644 (file)
index 0000000..fa72b70
--- /dev/null
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "varlink-idl.h"
+
+extern const VarlinkInterface vl_interface_io_systemd_BootControl;
index f37401d917c1779cb915d2ed8dbbba7fda439309..de6f53aafe64a789fde630ca1c4c8b90b049860d 100644 (file)
@@ -434,6 +434,9 @@ int need_daemon_reload(sd_bus *bus, const char *unit) {
 void warn_unit_file_changed(const char *unit) {
         assert(unit);
 
+        if (arg_no_warn)
+                return;
+
         log_warning("Warning: The unit file, source configuration file or drop-ins of %s changed on disk. Run 'systemctl%s daemon-reload' to reload units.",
                     unit,
                     arg_runtime_scope == RUNTIME_SCOPE_SYSTEM ? "" : " --user");
index e5708b73b546a13352b11129db1aba19d381bb92..d80fd705293cb7744ed5989e579e3185a5dc4cb3 100644 (file)
@@ -8,6 +8,7 @@
 #include "varlink.h"
 #include "varlink-idl.h"
 #include "varlink-io.systemd.h"
+#include "varlink-io.systemd.BootControl.h"
 #include "varlink-io.systemd.Credentials.h"
 #include "varlink-io.systemd.Journal.h"
 #include "varlink-io.systemd.ManagedOOM.h"
@@ -152,6 +153,8 @@ TEST(parse_format) {
         print_separator();
         test_parse_format_one(&vl_interface_io_systemd_Credentials);
         print_separator();
+        test_parse_format_one(&vl_interface_io_systemd_BootControl);
+        print_separator();
         test_parse_format_one(&vl_interface_xyz_test);
 }
 
index 3bf3891380ca28c61a7898747da4109d8d09c67a..923e00229e6982e2e91aa17f65c2b9ba887f106e 100755 (executable)
@@ -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 "$@"
index 80fdcd26b9485d0b1b00128fd4dabe855c427b39..ce7e03d7457ebc80697ec8bf1b2f04debac8f925 100644 (file)
@@ -1378,10 +1378,14 @@ install_rpm() {
 install_suse_systemd() {
     local pkgs
 
+    dinfo "Install basic filesystem structure"
+    install_rpm filesystem
+
     dinfo "Install SUSE systemd"
 
     pkgs=(
         systemd
+        systemd-boot
         systemd-container
         systemd-coredump
         systemd-experimental
index a77683b479ecdf1bee5ee7059f103613f22380cf..26e71afecd7bda9e162e09b87c98fbc206060d3f 100755 (executable)
@@ -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
index 61373b506e4bc7c1a2d1adf315f5ca497007df94..133006e40a45895b0d4af12bf15154536bcc578e 100755 (executable)
@@ -13,6 +13,11 @@ if ! command -v bootctl >/dev/null; then
     exit 0
 fi
 
+if [[ ! -d /usr/lib/systemd/boot/efi ]]; then
+    echo "sd-boot is not installed, skipping."
+    exit 0
+fi
+
 # shellcheck source=test/units/util.sh
 . "$(dirname "$0")"/util.sh
 
@@ -258,4 +263,13 @@ EOF
     SYSTEMD_RELAX_ESP_CHECKS=yes SYSTEMD_RELAX_XBOOTLDR_CHECKS=yes basic_tests --root "${IMAGE_DIR}/root"
 }
 
+testcase_bootctl_varlink() {
+    varlinkctl call --collect /run/systemd/io.systemd.BootControl io.systemd.BootControl.ListBootEntries '{}'
+
+    # We have no UEFI in the test environment, hence just check that this fails cleanly
+    ( SYSTEMD_LOG_TARGET=console varlinkctl call --json=short /run/systemd/io.systemd.BootControl io.systemd.BootControl.GetRebootToFirmware '{}' 2>&1 || true ) | grep -q io.systemd.BootControl.RebootToFirmwareNotSupported
+    ( SYSTEMD_LOG_TARGET=console varlinkctl call --json=short /run/systemd/io.systemd.BootControl io.systemd.BootControl.SetRebootToFirmware '{"state":true}' 2>&1 || true ) | grep -q io.systemd.BootControl.RebootToFirmwareNotSupported
+    ( SYSTEMD_LOG_TARGET=console varlinkctl call --json=short /run/systemd/io.systemd.BootControl io.systemd.BootControl.SetRebootToFirmware '{"state":false}' 2>&1 || true ) | grep -q io.systemd.BootControl.RebootToFirmwareNotSupported
+}
+
 run_testcases
index 0c971ef0bc4b5c8d004c7bee7b7a63b2c667c5be..936ebf783745ea97d4ab6fe274131b1ef47d47a1 100644 (file)
@@ -267,6 +267,15 @@ units = [
           'file' : 'systemd-boot-update.service',
           'conditions' : ['ENABLE_BOOTLOADER'],
         },
+        {
+          'file' : 'systemd-bootctl@.service.in',
+          'conditions' : ['ENABLE_BOOTLOADER'],
+        },
+        {
+          'file' : 'systemd-bootctl.socket',
+          'conditions' : ['ENABLE_BOOTLOADER'],
+          'symlinks' : ['sockets.target.wants/'],
+        },
         {
           'file' : 'systemd-confext.service',
           'conditions' : ['ENABLE_SYSEXT'],
diff --git a/units/systemd-bootctl.socket b/units/systemd-bootctl.socket
new file mode 100644 (file)
index 0000000..2b26d7e
--- /dev/null
@@ -0,0 +1,21 @@
+#  SPDX-License-Identifier: LGPL-2.1-or-later
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[Unit]
+Description=Boot Control (Varlink)
+Documentation=man:bootctl(1)
+DefaultDependencies=no
+After=local-fs.target
+Before=sockets.target
+
+[Socket]
+ListenStream=/run/systemd/io.systemd.BootControl
+FileDescriptorName=varlink
+SocketMode=0600
+Accept=yes
diff --git a/units/systemd-bootctl@.service.in b/units/systemd-bootctl@.service.in
new file mode 100644 (file)
index 0000000..d1c3ded
--- /dev/null
@@ -0,0 +1,20 @@
+#  SPDX-License-Identifier: LGPL-2.1-or-later
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[Unit]
+Description=Boot Control (Varlink)
+Documentation=man:bootctl(1)
+DefaultDependencies=no
+Conflicts=shutdown.target
+After=local-fs.target
+Before=shutdown.target
+
+[Service]
+Environment=LISTEN_FDNAMES=varlink
+ExecStart={{BINDIR}}/bootctl