]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
userdb: Add userdb.user.* and userdb.group.* credentials
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 13 Mar 2025 14:22:34 +0000 (15:22 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Tue, 18 Mar 2025 21:46:10 +0000 (22:46 +0100)
Let's allow providing extra userdb users and groups via credentials.
Similarly to systemd-udev-load-credentials.service, we ship
systemd-userdb-load-credentials.service which transform the JSON
user/group records provided via the corresponding credentials to static
userdb dropins in /etc/userdb.

Replaces #33811

man/systemd.system-credentials.xml
man/userdbctl.xml
src/nspawn/nspawn-bind-user.c
src/shared/group-record.c
src/shared/group-record.h
src/shared/user-record.c
src/shared/user-record.h
src/userdb/userdbctl.c
units/meson.build
units/systemd-userdb-load-credentials.service [new file with mode: 0644]
units/systemd-userdbd.service.in

index 59819865487b733ec50d5134aff0f0f5ec1e1af3..73c36c19153d647f24d6b6db0be11cbb071c5659 100644 (file)
           <xi:include href="version-info.xml" xpointer="v257"/>
         </listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>userdb.user.*</varname></term>
+        <term><varname>userdb.group.*</varname></term>
+
+        <listitem>
+          <para>Configure JSON user and group records. Read by
+          <filename>systemd-userdb-load-credentials.service</filename>, which invokes
+          <command>userdbctl load-credentials</command>. These credentials directly translate to
+          matching
+          <ulink url="https://systemd.io/USER_RECORD">JSON User</ulink> and
+          <ulink url="https://systemd.io/GROUP_RECORD">JSON Group</ulink> records. Example: the contents of a
+          credential <filename>userdb.user.foobar</filename> will be copied into a file
+          <filename>/etc/userdb/foobar.user</filename>, and
+          <filename>userdb.group.foobar</filename> will be copied into a file
+          <filename>/etc/userdb/foobar.group</filename>. Symlinks for the uid/gid will also be created in
+          <filename>/etc/userdb/</filename>, as well as the corresponding<filename>.membership</filename>
+          files. See
+          <citerefentry><refentrytitle>systemd-userdb</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
+          <citerefentry><refentrytitle>nss-systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>, and
+          <citerefentry><refentrytitle>userdbctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+          for details.</para>
+
+          <para>Any passed user records must contain uid and gid fields. Any passed group records must
+          contain a gid field. For both user and group records, the credential suffix (for
+          <literal>userdb.user.foobar</literal> the suffix is <literal>foobar</literal>) must match the user
+          or group name field from the user or group record.</para>
+
+          <para>Note that the records are created in <filename>/etc/userdb/</filename>
+          (<filename>/etc/passwd</filename> and <filename>/etc/group</filename> are not modified).</para>
+
+          <xi:include href="version-info.xml" xpointer="v258"/>
+        </listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
index d967783a2af096345ca5864c23901b7c092c8d84..77d0b1ffaa08435ee710c760a44832c2e467cb52 100644 (file)
 
         <xi:include href="version-info.xml" xpointer="v245"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><command>load-credentials</command></term>
+        <listitem>
+          <para>When specified, the following credentials are used when passed in:</para>
+
+          <variablelist>
+            <varlistentry>
+              <term><varname>userdb.user.*</varname></term>
+              <term><varname>userdb.group.*</varname></term>
+              <listitem>
+                <para>These credentials should contain valid
+                <ulink url="https://systemd.io/USER_RECORD">JSON User</ulink> and
+                <ulink url="https://systemd.io/GROUP_RECORD">JSON Group</ulink> records. For each matching
+                credential, various files are created in <filename>/etc/userdb/</filename>, implementing the
+                interface described in
+                <citerefentry><refentrytitle>nss-systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+                Any passed user records must contain uid and gid fields. Any passed group records must
+                contain a gid field. For both user and group records, the credential suffix (for
+                <literal>userdb.user.foobar</literal> the suffix is <literal>foobar</literal>) must match the
+                user or group name encoded in the record.</para>
+
+                <xi:include href="version-info.xml" xpointer="v258"/>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+
+          <xi:include href="version-info.xml" xpointer="v258"/>
+        </listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
index 373a05af3e914ae5dc90fa61d495d87b0b5f28d7..3e402ac33a93032c0fea6c4816c000f5ac325681 100644 (file)
@@ -369,18 +369,10 @@ int bind_user_setup(
                 const char *root) {
 
         static const UserRecordLoadFlags strip_flags = /* Removes privileged info */
-                USER_RECORD_REQUIRE_REGULAR|
-                USER_RECORD_STRIP_PRIVILEGED|
-                USER_RECORD_ALLOW_PER_MACHINE|
-                USER_RECORD_ALLOW_BINDING|
-                USER_RECORD_ALLOW_SIGNATURE|
+                USER_RECORD_LOAD_MASK_PRIVILEGED|
                 USER_RECORD_PERMISSIVE;
         static const UserRecordLoadFlags shadow_flags = /* Extracts privileged info */
-                USER_RECORD_STRIP_REGULAR|
-                USER_RECORD_ALLOW_PRIVILEGED|
-                USER_RECORD_STRIP_PER_MACHINE|
-                USER_RECORD_STRIP_BINDING|
-                USER_RECORD_STRIP_SIGNATURE|
+                USER_RECORD_EXTRACT_PRIVILEGED|
                 USER_RECORD_EMPTY_OK|
                 USER_RECORD_PERMISSIVE;
         int r;
index 04989eb63aa5e0971ea7270cac2f419cf33ed782..d4acc1a2620cb4f3f0631e1c72cd9e1bd723a53f 100644 (file)
@@ -372,3 +372,15 @@ bool group_record_match(GroupRecord *h, const UserDBMatch *match) {
 
         return true;
 }
+
+bool group_record_is_root(const GroupRecord *g) {
+        assert(g);
+
+        return g->gid == 0 || streq_ptr(g->group_name, "root");
+}
+
+bool group_record_is_nobody(const GroupRecord *g) {
+        assert(g);
+
+        return g->gid == GID_NOBODY || STRPTR_IN_SET(g->group_name, NOBODY_GROUP_NAME, "nobody");
+}
index ee8d39957798b7c48f3414fd52026b502f28e44e..95e70cf2694fcb43f2605a7913d0d22e5fff4923 100644 (file)
@@ -49,3 +49,6 @@ const char* group_record_group_name_and_realm(GroupRecord *h);
 UserDisposition group_record_disposition(GroupRecord *h);
 
 bool group_record_matches_group_name(const GroupRecord *g, const char *groupname);
+
+bool group_record_is_root(const GroupRecord *g);
+bool group_record_is_nobody(const GroupRecord *g);
index 4ce48066f9751d621cf78766489bc498c13b7b67..63d47ec9b1bc4c37989e0cbbd1cbc9560ab0257f 100644 (file)
@@ -2717,13 +2717,13 @@ int user_record_test_password_change_required(UserRecord *h) {
         return change_permitted ? 0 : -EROFS;
 }
 
-int user_record_is_root(const UserRecord *u) {
+bool user_record_is_root(const UserRecord *u) {
         assert(u);
 
         return u->uid == 0 || streq_ptr(u->user_name, "root");
 }
 
-int user_record_is_nobody(const UserRecord *u) {
+bool user_record_is_nobody(const UserRecord *u) {
         assert(u);
 
         return u->uid == UID_NOBODY || STRPTR_IN_SET(u->user_name, NOBODY_USER_NAME, "nobody");
index 24dc1310ef99b8d208ed8994a872b4301dd09acc..139651714b013de0b138ea65cf4ff7ee21857e57 100644 (file)
@@ -132,6 +132,18 @@ typedef enum UserRecordLoadFlags {
                                           USER_RECORD_STRIP_BINDING |
                                           USER_RECORD_STRIP_STATUS,
 
+        USER_RECORD_LOAD_MASK_PRIVILEGED = USER_RECORD_REQUIRE_REGULAR|
+                                           USER_RECORD_STRIP_PRIVILEGED|
+                                           USER_RECORD_ALLOW_PER_MACHINE|
+                                           USER_RECORD_ALLOW_BINDING|
+                                           USER_RECORD_ALLOW_SIGNATURE,
+
+        USER_RECORD_EXTRACT_PRIVILEGED   = USER_RECORD_STRIP_REGULAR|
+                                           USER_RECORD_ALLOW_PRIVILEGED|
+                                           USER_RECORD_STRIP_PER_MACHINE|
+                                           USER_RECORD_STRIP_BINDING|
+                                           USER_RECORD_STRIP_SIGNATURE,
+
         /* Whether to log about loader errors beyond LOG_DEBUG */
         USER_RECORD_LOG                 = 1U << 28,
 
@@ -477,8 +489,8 @@ int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask);
 int user_record_test_blocked(UserRecord *h);
 int user_record_test_password_change_required(UserRecord *h);
 
-int user_record_is_root(const UserRecord *u);
-int user_record_is_nobody(const UserRecord *u);
+bool user_record_is_root(const UserRecord *u);
+bool user_record_is_nobody(const UserRecord *u);
 
 /* The following six are user by group-record.c, that's why we export them here */
 int json_dispatch_realm(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
index 2e5a7fc3821c0019679213f0cad60fa4f16a1429..2bc2604318666c4f35067f847dd7f6e6979f7902 100644 (file)
@@ -4,22 +4,28 @@
 
 #include "bitfield.h"
 #include "build.h"
+#include "copy.h"
+#include "creds-util.h"
 #include "dirent-util.h"
 #include "errno-list.h"
 #include "escape.h"
 #include "fd-util.h"
+#include "fileio.h"
 #include "format-table.h"
 #include "format-util.h"
 #include "main-func.h"
+#include "mkdir-label.h"
 #include "pager.h"
 #include "parse-argument.h"
 #include "parse-util.h"
 #include "pretty-print.h"
+#include "recurse-dir.h"
 #include "socket-util.h"
 #include "strv.h"
 #include "terminal-util.h"
 #include "uid-classification.h"
 #include "uid-range.h"
+#include "umask-util.h"
 #include "user-record-show.h"
 #include "user-util.h"
 #include "userdb.h"
@@ -1164,6 +1170,306 @@ static int ssh_authorized_keys(int argc, char *argv[], void *userdata) {
         return r;
 }
 
+static int load_credential_one(int credential_dir_fd, const char *name, int userdb_dir_fd) {
+        int r;
+
+        assert(credential_dir_fd >= 0);
+        assert(name);
+        assert(userdb_dir_fd >= 0);
+
+        const char *user = startswith(name, "userdb.user.");
+        const char *group = startswith(name, "userdb.group.");
+        if (!user && !group)
+                return 0;
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        unsigned line = 0, column = 0;
+        r = sd_json_parse_file_at(NULL, credential_dir_fd, name, SD_JSON_PARSE_SENSITIVE, &v, &line, &column);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse credential '%s' as JSON at %u:%u: %m", name, line, column);
+
+        _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *ur_stripped = NULL, *ur_privileged = NULL;
+        _cleanup_(group_record_unrefp) GroupRecord *gr = NULL, *gr_stripped = NULL, *gr_privileged = NULL;
+        _cleanup_free_ char *fn = NULL, *link = NULL;
+
+        if (user) {
+                ur = user_record_new();
+                if (!ur)
+                        return log_oom();
+
+                r = user_record_load(ur, v, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG);
+                if (r < 0)
+                        return r;
+
+                if (user_record_is_root(ur))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'root' user from credentials is not supported.");
+                if (user_record_is_nobody(ur))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'nobody' user from credentials is not supported.");
+
+                if (!streq_ptr(user, ur->user_name))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Credential suffix '%s' does not match user record name '%s'",
+                                               user, strna(ur->user_name));
+
+                if (!uid_is_valid(ur->uid))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "JSON user record missing uid field");
+
+                if (!gid_is_valid(user_record_gid(ur)))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "JSON user record missing gid field");
+
+                _cleanup_(user_record_unrefp) UserRecord *m = NULL;
+                r = userdb_by_name(ur->user_name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m);
+                if (r >= 0) {
+                        if (m->uid != ur->uid)
+                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
+                                                       "Cannot create user %s from credential %s as it already exists with UID " UID_FMT " instead of " UID_FMT,
+                                                       ur->user_name, name, m->uid, ur->uid);
+
+                        log_info("User with name %s and UID " UID_FMT " already exists, not creating user from credential %s", ur->user_name, ur->uid, name);
+                        return 0;
+                }
+                if (r != -ESRCH)
+                        return log_error_errno(r, "Failed to check if user with name %s already exists: %m", ur->user_name);
+
+                m = user_record_unref(m);
+                r = userdb_by_uid(ur->uid, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m);
+                if (r >= 0) {
+                        if (!streq_ptr(ur->user_name, m->user_name))
+                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
+                                                       "Cannot create user %s from credential %s as UID " UID_FMT " is already assigned to user %s",
+                                                       ur->user_name, name, ur->uid, m->user_name);
+
+                        log_info("User with name %s and UID " UID_FMT " already exists, not creating user from credential %s", ur->user_name, ur->uid, name);
+                        return 0;
+                }
+                if (r != -ESRCH)
+                        return log_error_errno(r, "Failed to check if user with UID " UID_FMT " already exists: %m", ur->uid);
+
+                r = user_record_clone(ur, USER_RECORD_LOAD_MASK_PRIVILEGED|USER_RECORD_LOG, &ur_stripped);
+                if (r < 0)
+                        return r;
+
+                r = user_record_clone(ur, USER_RECORD_EXTRACT_PRIVILEGED|USER_RECORD_EMPTY_OK|USER_RECORD_LOG, &ur_privileged);
+                if (r < 0)
+                        return r;
+
+                fn = strjoin(ur->user_name, ".user");
+                if (!fn)
+                        return log_oom();
+
+                if (asprintf(&link, UID_FMT ".user", ur->uid) < 0)
+                        return log_oom();
+        } else {
+                assert(group);
+
+                gr = group_record_new();
+                if (!gr)
+                        return log_oom();
+
+                r = group_record_load(gr, v, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG);
+                if (r < 0)
+                        return r;
+
+                if (group_record_is_root(gr))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'root' group from credentials is not supported.");
+                if (group_record_is_nobody(gr))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'nobody' group from credentials is not supported.");
+
+                if (!streq_ptr(group, gr->group_name))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Credential suffix '%s' does not match group record name '%s'",
+                                               group, strna(gr->group_name));
+
+                if (!gid_is_valid(gr->gid))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "JSON group record missing gid field");
+
+                _cleanup_(group_record_unrefp) GroupRecord *m = NULL;
+                r = groupdb_by_name(gr->group_name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m);
+                if (r >= 0) {
+                        if (m->gid != gr->gid)
+                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
+                                                       "Cannot create group %s from credential %s as it already exists with GID " GID_FMT " instead of " GID_FMT,
+                                                       gr->group_name, name, m->gid, gr->gid);
+
+                        log_info("Group with name %s and GID " GID_FMT " already exists, not creating group from credential %s", gr->group_name, gr->gid, name);
+                        return 0;
+                }
+                if (r != -ESRCH)
+                        return log_error_errno(r, "Failed to check if group with name %s already exists: %m", gr->group_name);
+
+                m = group_record_unref(m);
+                r = groupdb_by_gid(gr->gid, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m);
+                if (r >= 0) {
+                        if (!streq_ptr(gr->group_name, m->group_name))
+                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
+                                                       "Cannot create group %s from credential %s as GID " GID_FMT " is already assigned to group %s",
+                                                       gr->group_name, name, gr->gid, m->group_name);
+
+                        log_info("Group with name %s and GID " GID_FMT " already exists, not creating group from credential %s", gr->group_name, gr->gid, name);
+                        return 0;
+                }
+                if (r != -ESRCH)
+                        return log_error_errno(r, "Failed to check if group with GID " GID_FMT " already exists: %m", gr->gid);
+
+                r = group_record_clone(gr, USER_RECORD_LOAD_MASK_PRIVILEGED|USER_RECORD_LOG, &gr_stripped);
+                if (r < 0)
+                        return r;
+
+                r = group_record_clone(gr, USER_RECORD_EXTRACT_PRIVILEGED|USER_RECORD_EMPTY_OK|USER_RECORD_LOG, &gr_privileged);
+                if (r < 0)
+                        return r;
+
+                fn = strjoin(gr->group_name, ".group");
+                if (!fn)
+                        return log_oom();
+
+                if (asprintf(&link, GID_FMT ".group", gr->gid) < 0)
+                        return log_oom();
+        }
+
+        if (!filename_is_valid(fn))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "Passed credential '%s' would result in invalid filename '%s'.",
+                                       name, fn);
+
+        _cleanup_free_ char *formatted = NULL;
+        r = sd_json_variant_format(ur ? ur_stripped->json : gr_stripped->json, SD_JSON_FORMAT_NEWLINE, &formatted);
+        if (r < 0)
+                return log_error_errno(r, "Failed to format JSON record: %m");
+
+        r = write_string_file_at(userdb_dir_fd, fn, formatted, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write JSON record to /etc/userdb/%s: %m", fn);
+
+        if (symlinkat(fn, userdb_dir_fd, link) < 0)
+                return log_error_errno(errno, "Failed to create symlink from %s to %s", link, fn);
+
+        log_info("Installed /etc/userdb/%s from credential.", fn);
+
+        if ((ur && !sd_json_variant_is_blank_object(ur_privileged->json)) ||
+            (gr && !sd_json_variant_is_blank_object(gr_privileged->json))) {
+                fn = mfree(fn);
+                fn = strjoin(ur ? ur->user_name : gr->group_name, ur ? ".user-privileged" : ".group-privileged");
+                if (!fn)
+                        return log_oom();
+
+                formatted = mfree(formatted);
+                r = sd_json_variant_format(ur ? ur_privileged->json : gr_privileged->json, SD_JSON_FORMAT_NEWLINE, &formatted);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to format JSON record: %m");
+
+                r = write_string_file_at(userdb_dir_fd, fn, formatted, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MODE_0600);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to write JSON record to /etc/userdb/%s: %m", fn);
+
+                link = mfree(link);
+
+                if (ur) {
+                        if (asprintf(&link, UID_FMT ".user-privileged", ur->uid) < 0)
+                                return log_oom();
+                } else {
+                        if (asprintf(&link, GID_FMT ".group-privileged", gr->gid) < 0)
+                                return log_oom();
+                }
+
+                if (symlinkat(fn, userdb_dir_fd, link) < 0)
+                        return log_error_errno(errno, "Failed to create symlink from %s to %s", link, fn);
+
+                log_info("Installed /etc/userdb/%s from credential.", fn);
+        }
+
+        if (ur)
+                STRV_FOREACH(g, ur->member_of) {
+                        _cleanup_free_ char *membership = strjoin(ur->user_name, ":", *g);
+                        if (!membership)
+                                return log_oom();
+
+                        _cleanup_close_ int fd = openat(userdb_dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644);
+                        if (fd < 0)
+                                return log_error_errno(errno, "Failed to create %s: %m", membership);
+
+                        log_info("Installed /etc/userdb/%s from credential.", membership);
+                }
+        else
+                STRV_FOREACH(u, gr->members) {
+                        _cleanup_free_ char *membership = strjoin(*u, ":", gr->group_name);
+                        if (!membership)
+                                return log_oom();
+
+                        _cleanup_close_ int fd = openat(userdb_dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644);
+                        if (fd < 0)
+                                return log_error_errno(errno, "Failed to create %s: %m", membership);
+
+                        log_info("Installed /etc/userdb/%s from credential.", membership);
+                }
+
+        if (ur && user_record_disposition(ur) == USER_REGULAR) {
+                const char *hd = user_record_home_directory(ur);
+
+                r = RET_NERRNO(access(hd, F_OK));
+                if (r < 0) {
+                        if (r != -ENOENT)
+                                return log_error_errno(r, "Failed to check if %s exists: %m", hd);
+
+                        WITH_UMASK(0000) {
+                                r = mkdir_parents(hd, 0755);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to create parent directories of %s: %m", hd);
+
+                                if (mkdir(hd, 0700) < 0 && errno != EEXIST)
+                                        return log_error_errno(errno, "Failed to create %s: %m", hd);
+                        }
+
+                        if (chown(hd, ur->uid, user_record_gid(ur)) < 0)
+                                return log_error_errno(errno, "Failed to chown %s: %m", hd);
+
+                        r = copy_tree(user_record_skeleton_directory(ur), hd, ur->uid, user_record_gid(ur),
+                                      COPY_REFLINK|COPY_MERGE, /* denylist= */ NULL, /* subvolumes= */NULL);
+                        if (r < 0 && r != -ENOENT)
+                                return log_error_errno(r, "Failed to copy skeleton directory to %s: %m", hd);
+                }
+        }
+
+        return 0;
+}
+
+static int load_credentials(int argc, char *argv[], void *userdata) {
+        int r;
+
+        _cleanup_close_ int credential_dir_fd = open_credentials_dir();
+        if (IN_SET(credential_dir_fd, -ENXIO, -ENOENT)) {
+                /* Credential env var not set, or dir doesn't exist. */
+                log_debug("No credentials found.");
+                return 0;
+        }
+        if (credential_dir_fd < 0)
+                return log_error_errno(credential_dir_fd, "Failed to open credentials directory: %m");
+
+        _cleanup_free_ DirectoryEntries *des = NULL;
+        r = readdir_all(credential_dir_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");
+
+        _cleanup_close_ int userdb_dir_fd = xopenat_full(
+                AT_FDCWD, "/etc/userdb",
+                /* open_flags= */ O_DIRECTORY|O_CREAT|O_CLOEXEC,
+                /* xopen_flags= */ XO_LABEL,
+                /* mode= */ 0755);
+        if (userdb_dir_fd < 0)
+                return log_error_errno(userdb_dir_fd, "Failed to open '/etc/userdb/': %m");
+
+        FOREACH_ARRAY(i, des->entries, des->n_entries) {
+                struct dirent *de = *i;
+
+                if (de->d_type != DT_REG)
+                        continue;
+
+                RET_GATHER(r, load_credential_one(credential_dir_fd, de->d_name, userdb_dir_fd));
+        }
+
+        return r;
+}
+
 static int help(int argc, char *argv[], void *userdata) {
         _cleanup_free_ char *link = NULL;
         int r;
@@ -1183,6 +1489,7 @@ static int help(int argc, char *argv[], void *userdata) {
                "  groups-of-user [USER…]     Show groups the specified users are members of\n"
                "  services                   Show enabled database services\n"
                "  ssh-authorized-keys USER   Show SSH authorized keys for user\n"
+               "  load-credentials           Write static user/group records from credentials\n"
                "\nOptions:\n"
                "  -h --help                  Show this help\n"
                "     --version               Show package version\n"
@@ -1512,10 +1819,8 @@ static int run(int argc, char *argv[]) {
                 { "users-in-group",      VERB_ANY, VERB_ANY, 0,            display_memberships },
                 { "groups-of-user",      VERB_ANY, VERB_ANY, 0,            display_memberships },
                 { "services",            VERB_ANY, 1,        0,            display_services    },
-
-                /* This one is a helper for sshd_config's AuthorizedKeysCommand= setting, it's not a
-                 * user-facing verb and thus should not appear in man pages or --help texts. */
                 { "ssh-authorized-keys", 2,        VERB_ANY, 0,            ssh_authorized_keys },
+                { "load-credentials",    VERB_ANY, 1,        0,            load_credentials    },
                 {}
         };
 
index b29bed068f67a3e56c5634b1818c201a09f6f1ec..1b92ccf8b226838bb984cae915c10c4e04ececbf 100644 (file)
@@ -813,6 +813,10 @@ units = [
           'conditions' : ['HAVE_PAM'],
           'symlinks' : ['multi-user.target.wants/'],
         },
+        {
+          'file' : 'systemd-userdb-load-credentials.service',
+          'conditions' : ['ENABLE_USERDB'],
+        },
         {
           'file' : 'systemd-userdbd.service.in',
           'conditions' : ['ENABLE_USERDB'],
diff --git a/units/systemd-userdb-load-credentials.service b/units/systemd-userdb-load-credentials.service
new file mode 100644 (file)
index 0000000..5bcd6d1
--- /dev/null
@@ -0,0 +1,31 @@
+#  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=Load JSON user/group Records from Credentials
+Documentation=man:systemd-userdb(8)
+Documentation=man:systemd.system-credentials(7)
+
+DefaultDependencies=no
+Before=systemd-user-sessions.service nss-user-lookup.target
+After=local-fs.target
+Conflicts=shutdown.target
+Before=shutdown.target
+
+ConditionPathExists=!/etc/initrd-release
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=userdbctl load-credentials
+ImportCredential=userdb.user.*
+ImportCredential=userdb.group.*
+
+[Install]
+WantedBy=sysinit.target
index 1c092654b99cfcd07ddf9dae1bba7e577b48a9ea..e853bb0c6fcd165f59d2b3c8a34601e516877030 100644 (file)
@@ -13,6 +13,7 @@ Documentation=man:systemd-userdbd.service(8)
 Requires=systemd-userdbd.socket
 After=systemd-userdbd.socket
 Before=sysinit.target
+Wants=systemd-userdb-load-credentials.service
 DefaultDependencies=no
 
 [Service]