]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homectl: Add flags to edit blob directories
authorAdrian Vovk <adrianvovk@gmail.com>
Wed, 10 Jan 2024 22:25:00 +0000 (17:25 -0500)
committerLuca Boccassi <bluca@debian.org>
Mon, 19 Feb 2024 11:18:11 +0000 (11:18 +0000)
This makes it possible to edit blob directories using homectl. The
following syntax is available:

* `--blob-directory=/path/somewhere`: Replaces the entire blob directory
with the contents of /path/somewhere

* `--blob-directory=foobar=/path/somewhere`: Replaces just the file
foobar in the blob directory with the contents of /path/somewhere

* `--blob-directory=foobar=`: Deletes the file foobar from the blob
directory

* `--blob-directory=`: Resets all previous flags

* `--avatar=`, etc: Shortcuts for `--blob-directory=FILENAME=` for the
known files in the blob directory

man/homectl.xml
shell-completion/bash/homectl
src/home/homectl.c

index 0e79f82e0f273229f92c7f305f0770bf44f725e1..c6663e09e058a41636ea28a1ee040462189b3a29 100644 (file)
         <xi:include href="version-info.xml" xpointer="v247"/></listitem>
       </varlistentry>
 
+    <varlistentry>
+        <term><option>--blob=</option><replaceable>PATH</replaceable></term>
+        <term><option>-b</option> <replaceable>PATH</replaceable></term>
+        <term><option>--blob=</option><replaceable>FILENAME</replaceable>=<replaceable>PATH</replaceable></term>
+        <term><option>-b</option> <replaceable>FILENAME</replaceable>=<replaceable>PATH</replaceable></term>
+
+        <listitem><para>Accepts either a directory path, or a file name followed by a file path. If just a
+        directory path is specified, then the user's entire blob directory is replaced the specified path.
+        Note that this replacement is performed before per-file manipulations are applied, which means these per-file
+        manipulations will be applied on top of the specified directory. If a filename and file path are specified, then
+        the single specified blob file will be overwritten with the specified path. If completely blank, the entire blob
+        directory is emptied out (which also resets all previous blob-related flags up to this point). If a filename is
+        specified but the corresponding path is blank, that single file will be deleted from the blob directory. All changes
+        are performed in temporary copies of the specified files in directories, which means that the originals specified on
+        the command line are not modified. See <ulink url="https://systemd.io/USER_RECORD_BLOB_DIRS">User Record Blob Directories</ulink>
+        for more information about blob directories.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/></listitem>
+      </varlistentry>
+
+    <varlistentry>
+        <term><option>--avatar=</option><replaceable>PATH</replaceable></term>
+        <term><option>--login-background=</option><replaceable>PATH</replaceable></term>
+
+        <listitem><para>Accept a file path. If set, the specifified file is used to overwrite the
+        corresponding file in the user's blob directory. If blank, the corresponding file is deleted
+        from the blob directory. Essentially, these options are shortcuts to
+        <option>--blob=</option><replaceable>FILENAME</replaceable>=<replaceable>PATH</replaceable>
+        for the known filenames defined in
+        <ulink url="https://systemd.io/USER_RECORD_BLOB_DIRS">User Record Blob Directories</ulink>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><option>--locked=</option><replaceable>BOOLEAN</replaceable></term>
 
index a9a77d474bb564f65b365c7fe08034f4df5bce37..3bb84fedb26803e80eb38749ce1720c337a57120 100644 (file)
@@ -106,7 +106,10 @@ _homectl() {
                         --cifs-service
                         --stop-delay
                         --kill-processes
-                        --auto-login'
+                        --auto-login
+                     -b --blob
+                        --avatar
+                        --login-background'
     )
 
     if __contains_word "$prev" ${OPTS[ARG]}; then
@@ -117,7 +120,7 @@ _homectl() {
             --machine|-M)
                 comps=$( __get_machines )
                 ;;
-            --identity|--image-path)
+            --identity|--image-path|--avatar|--login-background)
                 comps=$(compgen -A file -- "$cur" )
                 compopt -o filenames
                 ;;
@@ -130,7 +133,7 @@ _homectl() {
             --locked|--enforce-password-policy|--password-change-now|--luks-discard|--luks-offline-discard|--nosuid|--nodev|--noexec|--kill-processes|--auto-login)
                 comps='yes no'
                 ;;
-            -d|--home-dir|--skel)
+            -d|--home-dir|--skel|-b|--blob)
                 comps=$(compgen -A directory -- "$cur" )
                 compopt -o dirnames
                 ;;
index 34c62f1edb59c8b1321941ac4c7092d3c5c51182..5357e791a5fc675bf9d08331d2e319f0e7965062 100644 (file)
@@ -12,7 +12,9 @@
 #include "cap-list.h"
 #include "capability-util.h"
 #include "cgroup-util.h"
+#include "copy.h"
 #include "creds-util.h"
+#include "dirent-util.h"
 #include "dns-domain.h"
 #include "env-util.h"
 #include "fd-util.h"
@@ -20,6 +22,7 @@
 #include "format-table.h"
 #include "fs-util.h"
 #include "glyph-util.h"
+#include "hashmap.h"
 #include "home-util.h"
 #include "homectl-fido2.h"
 #include "homectl-pkcs11.h"
 #include "process-util.h"
 #include "recurse-dir.h"
 #include "rlimit-util.h"
+#include "rm-rf.h"
 #include "spawn-polkit-agent.h"
 #include "terminal-util.h"
+#include "tmpfile-util.h"
 #include "uid-classification.h"
 #include "user-record.h"
 #include "user-record-password-quality.h"
@@ -85,6 +90,9 @@ static enum {
 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 char *arg_blob_dir = NULL;
+static bool arg_blob_clear = false;
+static Hashmap *arg_blob_files = NULL;
 
 STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
 STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
@@ -94,6 +102,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_blob_dir, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_blob_files, hashmap_freep);
 
 static const BusLocator *bus_mgr;
 
@@ -107,7 +117,10 @@ static bool identity_properties_specified(void) {
                 !strv_isempty(arg_identity_filter) ||
                 !strv_isempty(arg_identity_filter_rlimits) ||
                 !strv_isempty(arg_pkcs11_token_uri) ||
-                !strv_isempty(arg_fido2_device);
+                !strv_isempty(arg_fido2_device) ||
+                arg_blob_dir ||
+                arg_blob_clear ||
+                !hashmap_isempty(arg_blob_files);
 }
 
 static int acquire_bus(sd_bus **bus) {
@@ -1266,9 +1279,132 @@ static int acquire_new_password(
         }
 }
 
+static int acquire_merged_blob_dir(UserRecord *hr, bool existing, Hashmap **ret) {
+        _cleanup_free_ char *sys_blob_path = NULL;
+        _cleanup_hashmap_free_ Hashmap *blobs = NULL;
+        _cleanup_closedir_ DIR *d = NULL;
+        const char *src_blob_path, *filename;
+        void *fd_ptr;
+        int r;
+
+        assert(ret);
+
+        HASHMAP_FOREACH_KEY(fd_ptr, filename, arg_blob_files) {
+                _cleanup_free_ char *filename_dup = NULL;
+                _cleanup_close_ int fd_dup = -EBADF;
+
+                filename_dup = strdup(filename);
+                if (!filename_dup)
+                        return log_oom();
+
+                if (PTR_TO_FD(fd_ptr) != -EBADF) {
+                        fd_dup = fcntl(PTR_TO_FD(fd_ptr), F_DUPFD_CLOEXEC, 3);
+                        if (fd_dup < 0)
+                                return log_error_errno(errno, "Failed to duplicate fd of %s: %m", filename);
+                }
+
+                r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, filename_dup, FD_TO_PTR(fd_dup));
+                if (r < 0)
+                        return r;
+                TAKE_PTR(filename_dup); /* Ownership transferred to hashmap */
+                TAKE_FD(fd_dup);
+        }
+
+        if (arg_blob_dir)
+                src_blob_path = arg_blob_dir;
+        else if (existing && !arg_blob_clear) {
+                if (hr->blob_directory)
+                        src_blob_path = hr->blob_directory;
+                else {
+                        /* This isn't technically a correct thing to do for generic user records,
+                         * so anyone looking at this code for reference shouldn't replicate it.
+                         * However, since homectl is tied to homed, this is OK. This adds robustness
+                         * for situations where the user record is coming directly from the CLI and
+                         * thus doesn't have a blobDirectory set */
+
+                        sys_blob_path = path_join(home_system_blob_dir(), hr->user_name);
+                        if (!sys_blob_path)
+                                return log_oom();
+
+                        src_blob_path = sys_blob_path;
+                }
+        } else
+                goto nodir; /* Shortcut: no dir to merge with, so just return copy of arg_blob_files */
+
+        d = opendir(src_blob_path);
+        if (!d)
+                return log_error_errno(errno, "Failed to open %s: %m", src_blob_path);
+
+        FOREACH_DIRENT_ALL(de, d, return log_error_errno(errno, "Failed to read %s: %m", src_blob_path)) {
+                _cleanup_free_ char *name = NULL;
+                _cleanup_close_ int fd = -EBADF;
+
+                if (dot_or_dot_dot(de->d_name))
+                        continue;
+
+                if (hashmap_contains(blobs, de->d_name))
+                        continue; /* arg_blob_files should override the base dir */
+
+                if (!suitable_blob_filename(de->d_name)) {
+                        log_warning("File %s in blob directory %s has an invalid filename. Skipping.", de->d_name, src_blob_path);
+                        continue;
+                }
+
+                name = strdup(de->d_name);
+                if (!name)
+                        return log_oom();
+
+                fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+                if (fd < 0)
+                        return log_error_errno(errno, "Failed to open %s in %s: %m", de->d_name, src_blob_path);
+
+                r = fd_verify_regular(fd);
+                if (r < 0) {
+                        log_warning_errno(r, "Entry %s in blob directory %s is not a regular file. Skipping.", de->d_name, src_blob_path);
+                        continue;
+                }
+
+                r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, name, FD_TO_PTR(fd));
+                if (r < 0)
+                        return r;
+                TAKE_PTR(name); /* Ownership transferred to hashmap */
+                TAKE_FD(fd);
+        }
+
+nodir:
+        *ret = TAKE_PTR(blobs);
+        return 0;
+}
+
+static int bus_message_append_blobs(sd_bus_message *m, Hashmap *blobs) {
+        const char *filename;
+        void *fd_ptr;
+        int r;
+
+        assert(m);
+
+        r = sd_bus_message_open_container(m, 'a', "{sh}");
+        if (r < 0)
+                return r;
+
+        HASHMAP_FOREACH_KEY(fd_ptr, filename, blobs) {
+                int fd = PTR_TO_FD(fd_ptr);
+
+                if (fd == -EBADF) /* File marked for deletion */
+                        continue;
+
+                r = sd_bus_message_append(m, "{sh}", filename, fd);
+                if (r < 0)
+                        return r;
+        }
+
+        return sd_bus_message_close_container(m);
+}
+
 static int create_home_common(JsonVariant *input) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        _cleanup_hashmap_free_ Hashmap *blobs = NULL;
         int r;
 
         r = acquire_bus(&bus);
@@ -1281,6 +1417,10 @@ static int create_home_common(JsonVariant *input) {
         if (r < 0)
                 return r;
 
+        r = acquire_merged_blob_dir(hr, false, &blobs);
+        if (r < 0)
+                return r;
+
         /* If the JSON record carries no plain text password (besides the recovery key), then let's query it
          * manually. */
         if (strv_length(hr->password) <= arg_recovery_key) {
@@ -1327,7 +1467,7 @@ static int create_home_common(JsonVariant *input) {
                 if (r < 0)
                         return log_error_errno(r, "Failed to format user record: %m");
 
-                r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHome");
+                r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHomeEx");
                 if (r < 0)
                         return bus_log_create_error(r);
 
@@ -1337,6 +1477,14 @@ static int create_home_common(JsonVariant *input) {
                 if (r < 0)
                         return bus_log_create_error(r);
 
+                r = bus_message_append_blobs(m, blobs);
+                if (r < 0)
+                        return bus_log_create_error(r);
+
+                r = sd_bus_message_append(m, "t", 0);
+                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_LOW_PASSWORD_QUALITY)) {
@@ -1492,7 +1640,7 @@ static int acquire_updated_home_record(
 
                 reply = sd_bus_message_unref(reply);
 
-                r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature"));
+                r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature", "blobManifest"));
                 if (r < 0)
                         return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
         }
@@ -1562,6 +1710,7 @@ static int update_home(int argc, char *argv[], void *userdata) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         _cleanup_(user_record_unrefp) UserRecord *hr = NULL, *secret = NULL;
         _cleanup_free_ char *buffer = NULL;
+        _cleanup_hashmap_free_ Hashmap *blobs = NULL;
         const char *username;
         int r;
 
@@ -1595,6 +1744,10 @@ static int update_home(int argc, char *argv[], void *userdata) {
         if (r < 0)
                 return r;
 
+        r = acquire_merged_blob_dir(hr, true, &blobs);
+        if (r < 0)
+                return r;
+
         /* If we do multiple operations, let's output things more verbosely, since otherwise the repeated
          * authentication might be confusing. */
 
@@ -1606,7 +1759,7 @@ static int update_home(int argc, char *argv[], void *userdata) {
                 _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
                 _cleanup_free_ char *formatted = NULL;
 
-                r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHome");
+                r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHomeEx");
                 if (r < 0)
                         return bus_log_create_error(r);
 
@@ -1620,6 +1773,14 @@ static int update_home(int argc, char *argv[], void *userdata) {
                 if (r < 0)
                         return bus_log_create_error(r);
 
+                r = bus_message_append_blobs(m, blobs);
+                if (r < 0)
+                        return bus_log_create_error(r);
+
+                r = sd_bus_message_append(m, "t", 0);
+                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 (arg_and_change_password &&
@@ -2451,7 +2612,13 @@ static int help(int argc, char *argv[], void *userdata) {
                "                               Whether to require user verification to unlock\n"
                "                               the account\n"
                "     --recovery-key=BOOL       Add a recovery key\n"
-               "\n%4$sAccount Management User  Record Properties:%5$s\n"
+               "\n%4$sBlob Directory User Record Properties:%5$s\n"
+               "  -b --blob=[FILENAME=]PATH\n"
+               "                               Path to a replacement blob directory, or replace\n"
+               "                               an individual files in the blob directory.\n"
+               "     --avatar=PATH             Path to user avatar picture\n"
+               "     --login-background=PATH   Path to user login background picture\n"
+               "\n%4$sAccount Management User Record Properties:%5$s\n"
                "     --locked=BOOL             Set locked account state\n"
                "     --not-before=TIMESTAMP    Do not allow logins before\n"
                "     --not-after=TIMESTAMP     Do not allow logins after\n"
@@ -2626,6 +2793,8 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_CAPABILITY_BOUNDING_SET,
                 ARG_CAPABILITY_AMBIENT_SET,
                 ARG_PROMPT_NEW_USER,
+                ARG_AVATAR,
+                ARG_LOGIN_BACKGROUND,
         };
 
         static const struct option options[] = {
@@ -2719,6 +2888,9 @@ static int parse_argv(int argc, char *argv[]) {
                 { "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             },
+                { "blob",                        required_argument, NULL, 'b'                             },
+                { "avatar",                      required_argument, NULL, ARG_AVATAR                      },
+                { "login-background",            required_argument, NULL, ARG_LOGIN_BACKGROUND            },
                 {}
         };
 
@@ -2730,7 +2902,7 @@ static int parse_argv(int argc, char *argv[]) {
         for (;;) {
                 int c;
 
-                c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL);
+                c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPE", options, NULL);
                 if (c < 0)
                         break;
 
@@ -4027,6 +4199,78 @@ static int parse_argv(int argc, char *argv[]) {
                         arg_prompt_new_user = true;
                         break;
 
+                case 'b':
+                case ARG_AVATAR:
+                case ARG_LOGIN_BACKGROUND: {
+                        _cleanup_close_ int fd = -EBADF;
+                        _cleanup_free_ char *path = NULL, *filename = NULL;
+
+                        if (c == 'b') {
+                                char *eq;
+
+                                if (isempty(optarg)) { /* --blob= deletes everything, including existing blob dirs */
+                                        hashmap_clear(arg_blob_files);
+                                        arg_blob_dir = mfree(arg_blob_dir);
+                                        arg_blob_clear = true;
+                                        break;
+                                }
+
+                                eq = strrchr(optarg, '=');
+                                if (!eq) { /* --blob=/some/path replaces the blob dir */
+                                        r = parse_path_argument(optarg, false, &arg_blob_dir);
+                                        if (r < 0)
+                                                return log_error_errno(r, "Failed to parse path %s: %m", optarg);
+                                        break;
+                                }
+
+                                /* --blob=filename=/some/path replaces the file "filename" with /some/path */
+                                filename = strndup(optarg, eq - optarg);
+                                if (!filename)
+                                        return log_oom();
+
+                                if (isempty(filename))
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", optarg);
+                                if (!suitable_blob_filename(filename))
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid blob filename: %s", filename);
+
+                                r = parse_path_argument(eq + 1, false, &path);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to parse path %s: %m", eq + 1);
+                        } else {
+                                const char *well_known_filename =
+                                                  c == ARG_AVATAR ? "avatar" :
+                                        c == ARG_LOGIN_BACKGROUND ? "login-background" :
+                                                                    NULL;
+                                assert(well_known_filename);
+
+                                filename = strdup(well_known_filename);
+                                if (!filename)
+                                        return log_oom();
+
+                                r = parse_path_argument(optarg, false, &path);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to parse path %s: %m", optarg);
+                        }
+
+                        if (path) {
+                                fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+                                if (fd < 0)
+                                        return log_error_errno(errno, "Failed to open %s: %m", path);
+
+                                if (fd_verify_regular(fd) < 0)
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provided blob is not a regular file: %s", path);
+                        } else
+                                fd = -EBADF; /* Delete the file */
+
+                        r = hashmap_ensure_put(&arg_blob_files, &blob_fd_hash_ops, filename, FD_TO_PTR(fd));
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to map %s to %s in blob directory: %m", path, filename);
+                        TAKE_PTR(filename); /* hashmap takes ownership */
+                        TAKE_FD(fd);
+
+                        break;
+                }
+
                 case '?':
                         return -EINVAL;