]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homectl: add "firstboot" command
authorLennart Poettering <lennart@poettering.net>
Wed, 22 Nov 2023 09:58:14 +0000 (10:58 +0100)
committerLennart Poettering <lennart@poettering.net>
Mon, 18 Dec 2023 10:10:53 +0000 (11:10 +0100)
This extends what systemd-firstboot does and runs on first boots only
and either processes user records passed in via credentials to create,
or asks the user interactively to create one (only if no regular user
exists yet).

man/homectl.xml
man/kernel-command-line.xml
man/rules/meson.build
man/systemd.system-credentials.xml
src/home/homectl.c
units/meson.build
units/systemd-homed-firstboot.service [new file with mode: 0644]
units/systemd-homed.service.in

index 7fc7d5f01223c0a9daec021c0d931ec3622ba44a..7e26c941799e8219b86b184f65adaae9ae304ca8 100644 (file)
@@ -18,6 +18,7 @@
 
   <refnamediv>
     <refname>homectl</refname>
+    <refname>systemd-homed-firstboot.service</refname>
     <refpurpose>Create, remove, change or inspect home directories</refpurpose>
   </refnamediv>
 
 
         <xi:include href="version-info.xml" xpointer="v250"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><command>firstboot</command></term>
+
+        <listitem><para>This command is supposed to be invoked during the initial boot of the system. It
+        checks whether any regular home area exists so far, and if not queries the user interactively on the
+        console for user name and password and creates one. Alternatively, if one or more service credentials
+        whose name starts with <literal>home.create.</literal> are passed to the command (containing a user
+        record in JSON format) these users are automatically created at boot.</para>
+
+        <para>This command is invoked by the <filename>systemd-homed-firstboot.service</filename> service
+        unit.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/></listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Credentials</title>
+
+    <para>When invoked with the <command>firstboot</command> command, <command>homectl</command> supports the
+    service credentials logic as implemented by
+    <varname>ImportCredential=</varname>/<varname>LoadCredential=</varname>/<varname>SetCredential=</varname>
+    (see <citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>1</manvolnum></citerefentry> for
+    details). The following credentials are used when passed in:</para>
+
+    <variablelist class='system-credentials'>
+      <varlistentry>
+        <term><varname>home.create.*</varname></term>
+
+        <listitem><para>If one or more credentials whose names begin with <literal>home.create.</literal>,
+        followed by a valid UNIX username are passed, a new home area is created, one for each specified user
+        record.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/></listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Kernel Command Line</title>
+
+    <variablelist class='kernel-commandline-options'>
+      <varlistentry>
+        <term><varname>systemd.firstboot=</varname></term>
+
+        <listitem><para>This boolean will disable the effect of <command>homectl firstboot</command>
+        command. It's primarily interpreted by
+        <citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/></listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
index 6ac20ad2f4f51596fe60ff41312afd24ca6ba3b7..7a7b2b7deb45f5af065e33de29eef4323a77ea69 100644 (file)
 
         <listitem><para>Takes a boolean argument, defaults to on. If off,
         <citerefentry><refentrytitle>systemd-firstboot.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+        and
+        <citerefentry><refentrytitle>systemd-homed-firstboot.service</refentrytitle><manvolnum>1</manvolnum></citerefentry>
         will not query the user for basic system settings, even if the system boots up for the first time and
         the relevant settings are not initialized yet. Not to be confused with
         <varname>systemd.condition-first-boot=</varname> (see below), which overrides the result of the
index 5dc3e08896dcb6dae239123ce3463cdfc42a8fb0..3d63cf1131ac9a0beca92699b39dbbdd633a160a 100644 (file)
@@ -18,7 +18,7 @@ manpages = [
   'ENABLE_RESOLVE'],
  ['environment.d', '5', [], 'ENABLE_ENVIRONMENT_D'],
  ['file-hierarchy', '7', [], ''],
- ['homectl', '1', [], 'ENABLE_HOMED'],
+ ['homectl', '1', ['systemd-homed-firstboot.service'], 'ENABLE_HOMED'],
  ['homed.conf', '5', ['homed.conf.d'], 'ENABLE_HOMED'],
  ['hostname', '5', [], ''],
  ['hostnamectl', '1', [], 'ENABLE_HOSTNAMED'],
index f7f0df18aab3f60ce6b8da263bed7004cba435fa..2a2d03b29fd1daf364b6a699b78ec1eb54749c3a 100644 (file)
           <xi:include href="version-info.xml" xpointer="v254"/>
         </listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>home.create.*</varname></term>
+        <listitem>
+          <para>Creates a home area for the specified user with the user record data passed in. For details see
+          <citerefentry><refentrytitle>homectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
+
+          <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
index 9a433252c2a21f03a0df99e3fcc4d28a7793d02b..f2fe90c75b0412c63c5500dcf3a062930e73c8b8 100644 (file)
@@ -12,6 +12,7 @@
 #include "cap-list.h"
 #include "capability-util.h"
 #include "cgroup-util.h"
+#include "creds-util.h"
 #include "dns-domain.h"
 #include "env-util.h"
 #include "fd-util.h"
@@ -35,7 +36,9 @@
 #include "percent-util.h"
 #include "pkcs11-util.h"
 #include "pretty-print.h"
+#include "proc-cmdline.h"
 #include "process-util.h"
+#include "recurse-dir.h"
 #include "rlimit-util.h"
 #include "spawn-polkit-agent.h"
 #include "terminal-util.h"
@@ -45,6 +48,7 @@
 #include "user-record-show.h"
 #include "user-record-util.h"
 #include "user-util.h"
+#include "userdb.h"
 #include "verbs.h"
 
 static PagerFlags arg_pager_flags = 0;
@@ -80,6 +84,7 @@ static enum {
 } arg_export_format = EXPORT_FORMAT_FULL;
 static uint64_t arg_capability_bounding_set = UINT64_MAX;
 static uint64_t arg_capability_ambient_set = UINT64_MAX;
+static bool arg_prompt_new_user = false;
 
 STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
 STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
@@ -1092,7 +1097,7 @@ static int add_disposition(JsonVariant **v) {
         return 1;
 }
 
-static int acquire_new_home_record(UserRecord **ret) {
+static int acquire_new_home_record(JsonVariant *input, UserRecord **ret) {
         _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
         _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
         int r;
@@ -1102,12 +1107,16 @@ static int acquire_new_home_record(UserRecord **ret) {
         if (arg_identity) {
                 unsigned line, column;
 
+                if (input)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Two identity records specified, refusing.");
+
                 r = json_parse_file(
                                 streq(arg_identity, "-") ? stdin : NULL,
                                 streq(arg_identity, "-") ? "<stdin>" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column);
                 if (r < 0)
                         return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
-        }
+        } else
+                v = json_variant_ref(input);
 
         r = apply_identity_changes(&v);
         if (r < 0)
@@ -1258,7 +1267,7 @@ static int acquire_new_password(
         }
 }
 
-static int create_home(int argc, char *argv[], void *userdata) {
+static int create_home_common(JsonVariant *input) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
         int r;
@@ -1269,36 +1278,7 @@ static int create_home(int argc, char *argv[], void *userdata) {
 
         (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
 
-        if (argc >= 2) {
-                /* If a username was specified, use it */
-
-                if (valid_user_group_name(argv[1], 0))
-                        r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
-                else {
-                        _cleanup_free_ char *un = NULL, *rr = NULL;
-
-                        /* Before we consider the user name invalid, let's check if we can split it? */
-                        r = split_user_name_realm(argv[1], &un, &rr);
-                        if (r < 0)
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
-
-                        if (rr) {
-                                r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
-                                if (r < 0)
-                                        return log_error_errno(r, "Failed to set realm field: %m");
-                        }
-
-                        r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
-                }
-                if (r < 0)
-                        return log_error_errno(r, "Failed to set userName field: %m");
-        } else {
-                /* If neither a username nor an identity have been specified we cannot operate. */
-                if (!arg_identity)
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
-        }
-
-        r = acquire_new_home_record(&hr);
+        r = acquire_new_home_record(input, &hr);
         if (r < 0)
                 return r;
 
@@ -1385,6 +1365,41 @@ static int create_home(int argc, char *argv[], void *userdata) {
         return 0;
 }
 
+static int create_home(int argc, char *argv[], void *userdata) {
+        int r;
+
+        if (argc >= 2) {
+                /* If a username was specified, use it */
+
+                if (valid_user_group_name(argv[1], 0))
+                        r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
+                else {
+                        _cleanup_free_ char *un = NULL, *rr = NULL;
+
+                        /* Before we consider the user name invalid, let's check if we can split it? */
+                        r = split_user_name_realm(argv[1], &un, &rr);
+                        if (r < 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
+
+                        if (rr) {
+                                r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to set realm field: %m");
+                        }
+
+                        r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
+                }
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set userName field: %m");
+        } else {
+                /* If neither a username nor an identity have been specified we cannot operate. */
+                if (!arg_identity)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
+        }
+
+        return create_home_common(/* input= */ NULL);
+}
+
 static int remove_home(int argc, char *argv[], void *userdata) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         int r, ret = 0;
@@ -2142,6 +2157,190 @@ static int rebalance(int argc, char *argv[], void *userdata) {
         return 0;
 }
 
+static int create_from_credentials(void) {
+        _cleanup_close_ int fd = -EBADF;
+        int ret = 0, n_created = 0, r;
+
+        fd = open_credentials_dir();
+        if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */
+                return 0;
+        if (fd < 0)
+                return log_error_errno(fd, "Failed to open credentials directory: %m");
+
+        _cleanup_free_ DirectoryEntries *des = NULL;
+        r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
+        if (r < 0)
+                return log_error_errno(r, "Failed to enumerate credentials: %m");
+
+        FOREACH_ARRAY(i, des->entries, des->n_entries) {
+                _cleanup_(json_variant_unrefp) JsonVariant *identity = NULL;
+                struct dirent *de = *i;
+                const char *e;
+
+                if (de->d_type != DT_REG)
+                        continue;
+
+                e = startswith(de->d_name, "home.create.");
+                if (!e)
+                        continue;
+
+                if (!valid_user_group_name(e, 0)) {
+                        log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name);
+                        continue;
+                }
+
+                r = json_parse_file_at(
+                                /* f= */ NULL,
+                                fd,
+                                de->d_name,
+                                /* flags= */ 0,
+                                &identity,
+                                /* ret_line= */ NULL,
+                                /* ret_column= */ NULL);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to parse user record in credential '%s', ignoring: %m", de->d_name);
+                        continue;
+                }
+
+                JsonVariant *un;
+                un = json_variant_by_key(identity, "userName");
+                if (un) {
+                        if (!json_variant_is_string(un)) {
+                                log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name);
+                                continue;
+                        }
+
+                        if (!streq(json_variant_string(un), e)) {
+                                log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, json_variant_string(un), e);
+                                continue;
+                        }
+                } else {
+                        r = json_variant_set_field_string(&identity, "userName", e);
+                        if (r < 0)
+                                return log_warning_errno(r, "Failed to set userName field: %m");
+                }
+
+                log_notice("Processing user '%s' from credentials.", e);
+
+                r = create_home_common(identity);
+                if (r >= 0)
+                        n_created++;
+
+                RET_GATHER(ret, r);
+        }
+
+        return ret < 0 ? ret : n_created;
+}
+
+static int has_regular_user(void) {
+        _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+        int r;
+
+        r = userdb_all(USERDB_SUPPRESS_SHADOW, &iterator);
+        if (r < 0)
+                return log_error_errno(r, "Failed to create user enumerator: %m");
+
+        for (;;) {
+                _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+
+                r = userdb_iterator_get(iterator, &ur);
+                if (r == -ESRCH)
+                        break;
+                if (r < 0)
+                        return log_error_errno(r, "Failed to enumerate users: %m");
+
+                if (user_record_disposition(ur) == USER_REGULAR)
+                        return true;
+        }
+
+        return false;
+}
+
+static int create_interactively(void) {
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        _cleanup_free_ char *username = NULL;
+        int r;
+
+        if (!arg_prompt_new_user) {
+                log_debug("Prompting for user creation was not requested.");
+                return 0;
+        }
+
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
+
+        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+        (void) reset_terminal_fd(STDIN_FILENO, /* switch_to_text= */ false);
+
+        for (;;) {
+                username = mfree(username);
+
+                r = ask_string(&username,
+                               "%s Please enter user name to create (empty to skip): ",
+                               special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to query user for username: %m");
+
+                if (isempty(username)) {
+                        log_info("No data entered, skipping.");
+                        return 0;
+                }
+
+                if (!valid_user_group_name(username, /* flags= */ 0)) {
+                        log_notice("Specified user name is not a valid UNIX user name, try again: %s", username);
+                        continue;
+                }
+
+                r = userdb_by_name(username, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL);
+                if (r == -ESRCH)
+                        break;
+                if (r < 0)
+                        return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", username);
+
+                log_notice("Specified user '%s' exists already, try again.", username);
+        }
+
+        r = json_variant_set_field_string(&arg_identity_extra, "userName", username);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set userName field: %m");
+
+        return create_home_common(/* input= */ NULL);
+}
+
+static int verb_firstboot(int argc, char *argv[], void *userdata) {
+        int r;
+
+        /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
+         * tool. */
+
+        bool enabled;
+        r = proc_cmdline_get_bool("systemd.firstboot", /* flags = */ 0, &enabled);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
+        if (r > 0 && !enabled) {
+                log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
+                arg_prompt_new_user = false;
+        }
+
+        r = create_from_credentials();
+        if (r < 0)
+                return r;
+        if (r > 0) /* Already created users from credentials */
+                return 0;
+
+        r = has_regular_user();
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                log_info("Regular user already present in user database, skipping user creation.");
+                return 0;
+        }
+
+        return create_interactively();
+}
+
 static int drop_from_identity(const char *field) {
         int r;
 
@@ -2198,6 +2397,7 @@ static int help(int argc, char *argv[], void *userdata) {
                "  deactivate-all               Deactivate all active home areas\n"
                "  rebalance                    Rebalance free space between home areas\n"
                "  with USER [COMMAND…]         Run shell or command with access to a home area\n"
+               "  firstboot                    Run first-boot home area creation wizard\n"
                "\n%4$sOptions:%5$s\n"
                "  -h --help                    Show this help\n"
                "     --version                 Show package version\n"
@@ -2216,6 +2416,8 @@ static int help(int argc, char *argv[], void *userdata) {
                "  -E                           When specified once equals -j --export-format=\n"
                "                               stripped, when specified twice equals\n"
                "                               -j --export-format=minimal\n"
+               "     --prompt-new-user         firstboot: Query user interactively for user\n"
+               "                               to create\n"
                "\n%4$sGeneral User Record Properties:%5$s\n"
                "  -c --real-name=REALNAME      Real name for user\n"
                "     --realm=REALM             Realm to create user in\n"
@@ -2423,6 +2625,7 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_FIDO2_CRED_ALG,
                 ARG_CAPABILITY_BOUNDING_SET,
                 ARG_CAPABILITY_AMBIENT_SET,
+                ARG_PROMPT_NEW_USER,
         };
 
         static const struct option options[] = {
@@ -2515,6 +2718,7 @@ static int parse_argv(int argc, char *argv[]) {
                 { "rebalance-weight",            required_argument, NULL, ARG_REBALANCE_WEIGHT            },
                 { "capability-bounding-set",     required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET     },
                 { "capability-ambient-set",      required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET      },
+                { "prompt-new-user",             no_argument,       NULL, ARG_PROMPT_NEW_USER             },
                 {}
         };
 
@@ -3799,6 +4003,10 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
                 }
 
+                case ARG_PROMPT_NEW_USER:
+                        arg_prompt_new_user = true;
+                        break;
+
                 case '?':
                         return -EINVAL;
 
@@ -3865,6 +4073,7 @@ static int run(int argc, char *argv[]) {
                 { "lock-all",       VERB_ANY, 1,        0,            lock_all_homes       },
                 { "deactivate-all", VERB_ANY, 1,        0,            deactivate_all_homes },
                 { "rebalance",      VERB_ANY, 1,        0,            rebalance            },
+                { "firstboot",      VERB_ANY, 1,        0,            verb_firstboot       },
                 {}
         };
 
index e7bfb7f838f95246c8a81a6075d4721ce829b8ca..8542245239ac8bd42d9ed2bdb89e09591d8eeafd 100644 (file)
@@ -303,6 +303,10 @@ units = [
           'file' : 'systemd-homed-activate.service',
           'conditions' : ['ENABLE_HOMED'],
         },
+        {
+          'file' : 'systemd-homed-firstboot.service',
+          'conditions' : ['ENABLE_HOMED'],
+        },
         {
           'file' : 'systemd-homed.service.in',
           'conditions' : ['ENABLE_HOMED'],
diff --git a/units/systemd-homed-firstboot.service b/units/systemd-homed-firstboot.service
new file mode 100644 (file)
index 0000000..3615940
--- /dev/null
@@ -0,0 +1,28 @@
+#  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=First Boot Home Area Wizard
+Documentation=man:homectl(1)
+ConditionFirstBoot=yes
+After=home.mount systemd-homed.service
+Before=systemd-user-sessions.service first-boot-complete.target
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=homectl firstboot --prompt-new-user
+StandardOutput=tty
+StandardInput=tty
+StandardError=tty
+ImportCredential=home.*
+
+[Install]
+WantedBy=systemd-homed.service
+Also=systemd-homed.service
index e629048b97a78318bac93d159a2b8b8f5fa64c03..bfc3df1021e77b0674a50f0602f7313aa6725895 100644 (file)
@@ -39,4 +39,4 @@ TimeoutStopSec=3min
 [Install]
 WantedBy=multi-user.target
 Alias=dbus-org.freedesktop.home1.service
-Also=systemd-homed-activate.service systemd-userdbd.service
+Also=systemd-homed-activate.service systemd-userdbd.service systemd-homed-firstboot.service