]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
user-record: Add blobDirectory and blobManifest
authorAdrian Vovk <adrianvovk@gmail.com>
Mon, 8 Jan 2024 23:11:43 +0000 (18:11 -0500)
committerLuca Boccassi <bluca@debian.org>
Mon, 19 Feb 2024 11:18:11 +0000 (11:18 +0000)
These fields are used to connect a JSON user record to its blob
directory, and to include the directory's contents in the record's
signature

docs/USER_RECORD.md
docs/USER_RECORD_BLOB_DIRS.md
src/shared/user-record-show.c
src/shared/user-record.c
src/shared/user-record.h

index 1479e53916a50d9d4c0d354f4ea9a3e32a2abac4..f28106797e0d76c9f30f3ff92814f42787ec4101 100644 (file)
@@ -234,6 +234,16 @@ optional, when unset the user should not be considered part of any realm. A
 user record with a realm set is never compatible (for the purpose of updates,
 see above) with a user record without one set, even if the `userName` field matches.
 
+`blobDirectory` → The absolute path to a world-readable copy of the user's blob
+directory. See [Blob Directories](USER_RECORD_BLOB_DIRS.md) for more details.
+
+`blobManifest` → An object, which maps valid blob directory filenames (see
+[Blob Directories](USER_RECORD_BLOB_DIRS.md) for requirements) to SHA256 hashes
+formatted as hex strings. This exists for the purpose of including the contents
+of the blob directory in the record's signature. Managers that support blob
+directories and utilize signed user records (like `systemd-homed`) should use
+this field to verify the contents of the blob directory whenever appropriate.
+
 `realName` → The real name of the user, a string. This should contain the
 user's real ("human") name, and corresponds loosely to the GECOS field of
 classic UNIX user records. When converting a `struct passwd` to a JSON user
@@ -758,7 +768,7 @@ These two are the only two fields specific to this section. All other fields
 that may be used in this section are identical to the equally named ones in the
 `regular` section (i.e. at the top-level object). Specifically, these are:
 
-`iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`,
+`blobDirectory`, `blobManifest`, `iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`,
 `preferredLanguage`, `additionalLanguages`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`,
 `notAfterUSec`, `storage`, `diskSize`, `diskSizeRelative`, `skeletonDirectory`,
 `accessMode`, `tasksMax`, `memoryHigh`, `memoryMax`, `cpuWeight`, `ioWeight`,
@@ -810,9 +820,9 @@ The following fields are defined in the `binding` section. They all have an
 identical format and override their equally named counterparts in the `regular`
 and `perMachine` sections:
 
-`imagePath`, `homeDirectory`, `partitionUuid`, `luksUuid`, `fileSystemUuid`,
-`uid`, `gid`, `storage`, `fileSystemType`, `luksCipher`, `luksCipherMode`,
-`luksVolumeKeySize`.
+`blobDirectory`, `imagePath`, `homeDirectory`, `partitionUuid`, `luksUuid`,
+`fileSystemUuid`, `uid`, `gid`, `storage`, `fileSystemType`, `luksCipher`,
+`luksCipherMode`, `luksVolumeKeySize`.
 
 ## Fields in the `status` section
 
@@ -1102,6 +1112,7 @@ A fully featured user record associated with a home directory managed by
                         "fileSystemUuid" : "758e88c8-5851-4a2a-b88f-e7474279c111",
                         "gid" : 60232,
                         "homeDirectory" : "/home/grobie",
+                        "blobDirectory" : "/var/cache/systemd/homed/grobie/",
                         "imagePath" : "/home/grobie.home",
                         "luksCipher" : "aes",
                         "luksCipherMode" : "xts-plain64",
@@ -1112,6 +1123,10 @@ A fully featured user record associated with a home directory managed by
                         "uid" : 60232
                 }
         },
+        "blobManifest" : {
+                "avatar" : "c0636851d25a62d817ff7da4e081d1e646e42c74d0ecb53425f75fcf1ba43b52",
+                "login-background" : "da7ad0222a6edbc6cd095149c72d38d92fd3114f606e4b57469857ef47fade18"
+        },
         "disposition" : "regular",
         "enforcePasswordPolicy" : false,
         "lastChangeUSec" : 1565950024279735,
index d0fa759a6f91a2d93e3b811423a26c85cf9aecd0..8f5dd7914b071417734d61f55a0f13d34c54b5d5 100644 (file)
@@ -15,7 +15,8 @@ system.
 
 The JSON User Record specifies the location of the blob directory via the
 `blobDirectory` field. If the field is unset, then there is no blob directory
-and thus no blob files to look for. The blob directory is completely
+and thus no blob files to look for.  Note that `blobDirectory` can exist in the
+`regular`, `perMachine`, and `status` sections. The blob directory is completely
 owned and managed by the service that owns the rest of the user record (as
 specified in the `service` field).
 
index 97235bd07b5f76f760691d31ca33b0a3adbbddd3..086d344c01153d0ca62d4d40c74c7241ee016389 100644 (file)
@@ -3,8 +3,14 @@
 #include "cap-list.h"
 #include "format-util.h"
 #include "fs-util.h"
+#include "glyph-util.h"
+#include "hashmap.h"
+#include "hexdecoct.h"
+#include "path-util.h"
+#include "pretty-print.h"
 #include "process-util.h"
 #include "rlimit-util.h"
+#include "sha256.h"
 #include "strv.h"
 #include "terminal-util.h"
 #include "user-record-show.h"
@@ -213,6 +219,37 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) {
                 printf("\n");
         }
 
+        if (hr->blob_directory) {
+                _cleanup_free_ char **filenames = NULL;
+                size_t n_filenames = 0;
+
+                r = hashmap_dump_keys_sorted(hr->blob_manifest, (void***) &filenames, &n_filenames);
+                if (r < 0) {
+                        errno = -r;
+                        printf("   Blob Dir.: %s (can't iterate: %m)\n", hr->blob_directory);
+                } else
+                        printf("   Blob Dir.: %s\n", hr->blob_directory);
+
+                for (size_t i = 0; i < n_filenames; i++) {
+                        _cleanup_free_ char *path = NULL, *link = NULL, *hash = NULL;
+                        const char *filename = filenames[i];
+                        const uint8_t *hash_bytes = hashmap_get(hr->blob_manifest, filename);
+                        bool last = i == n_filenames - 1;
+
+                        path = path_join(hr->blob_directory, filename);
+                        if (path)
+                                (void) terminal_urlify_path(path, filename, &link);
+                        hash = hexmem(hash_bytes, SHA256_DIGEST_SIZE);
+
+                        printf("              %s %s %s(%s)%s\n",
+                               special_glyph(last ? SPECIAL_GLYPH_TREE_RIGHT : SPECIAL_GLYPH_TREE_BRANCH),
+                               link ?: filename,
+                               ansi_grey(),
+                               hash ?: "can't display hash",
+                               ansi_normal());
+                }
+        }
+
         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. */
                 printf("     Storage: %s%s\n", user_storage_to_string(storage),
index 38e5f01c2364424212000b6ca3de6176b2981a2a..fc39194ac55ecf747e69f6bffddd19e691318456 100644 (file)
 #include "path-util.h"
 #include "pkcs11-util.h"
 #include "rlimit-util.h"
+#include "sha256.h"
 #include "string-table.h"
 #include "strv.h"
 #include "uid-classification.h"
 #include "user-record.h"
 #include "user-util.h"
+#include "utf8.h"
 
 #define DEFAULT_RATELIMIT_BURST 30
 #define DEFAULT_RATELIMIT_INTERVAL_USEC (1*USEC_PER_MINUTE)
@@ -142,6 +144,9 @@ static UserRecord* user_record_free(UserRecord *h) {
         free(h->location);
         free(h->icon_name);
 
+        free(h->blob_directory);
+        hashmap_free(h->blob_manifest);
+
         free(h->shell);
 
         strv_free(h->environment);
@@ -1074,6 +1079,7 @@ static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispa
 static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
 
         static const JsonDispatch binding_dispatch_table[] = {
+                { "blobDirectory",     JSON_VARIANT_STRING,        json_dispatch_path,           offsetof(UserRecord, blob_directory),       0         },
                 { "imagePath",         JSON_VARIANT_STRING,        json_dispatch_image_path,     offsetof(UserRecord, image_path),           0         },
                 { "homeDirectory",     JSON_VARIANT_STRING,        json_dispatch_home_directory, offsetof(UserRecord, home_directory),       0         },
                 { "partitionUuid",     JSON_VARIANT_STRING,        json_dispatch_id128,          offsetof(UserRecord, partition_uuid),       0         },
@@ -1110,6 +1116,52 @@ static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatch
         return json_dispatch(m, binding_dispatch_table, flags, userdata);
 }
 
+static int dispatch_blob_manifest(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+        _cleanup_hashmap_free_ Hashmap *manifest = NULL;
+        Hashmap **ret = ASSERT_PTR(userdata);
+        JsonVariant *value;
+        const char *key;
+        int r;
+
+        if (!variant)
+                return 0;
+
+        if (!json_variant_is_object(variant))
+                return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name));
+
+        JSON_VARIANT_OBJECT_FOREACH(key, value, variant) {
+                _cleanup_free_ char *filename = NULL;
+                _cleanup_free_ uint8_t *hash = NULL;
+
+                if (!json_variant_is_string(value))
+                        return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Blob entry '%s' has invalid hash.", key);
+
+                if (!suitable_blob_filename(key))
+                        return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Blob entry '%s' has invalid filename.", key);
+
+                filename = strdup(key);
+                if (!filename)
+                        return json_log_oom(value, flags);
+
+                hash = malloc(SHA256_DIGEST_SIZE);
+                if (!hash)
+                        return json_log_oom(value, flags);
+
+                r = parse_sha256(json_variant_string(value), hash);
+                if (r < 0)
+                        return json_log(value, flags, r, "Blob entry '%s' has invalid hash: %s", filename, json_variant_string(value));
+
+                r = hashmap_ensure_put(&manifest, &path_hash_ops_free_free, filename, hash);
+                if (r < 0)
+                        return json_log(value, flags, r, "Failed to insert blob manifest entry '%s': %m", filename);
+                TAKE_PTR(filename); /* Ownership transfers to hashmap */
+                TAKE_PTR(hash);
+        }
+
+        hashmap_free_and_replace(*ret, manifest);
+        return 0;
+}
+
 int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags) {
         sd_id128_t mid;
         int r;
@@ -1226,6 +1278,8 @@ static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDisp
         static const JsonDispatch per_machine_dispatch_table[] = {
                 { "matchMachineId",             _JSON_VARIANT_TYPE_INVALID, NULL,                                 0,                                                   0         },
                 { "matchHostname",              _JSON_VARIANT_TYPE_INVALID, NULL,                                 0,                                                   0         },
+                { "blobDirectory",              JSON_VARIANT_STRING,        json_dispatch_path,                   offsetof(UserRecord, blob_directory),                0         },
+                { "blobManifest",               JSON_VARIANT_OBJECT,        dispatch_blob_manifest,               offsetof(UserRecord, blob_manifest),                 0         },
                 { "iconName",                   JSON_VARIANT_STRING,        json_dispatch_string,                 offsetof(UserRecord, icon_name),                     JSON_SAFE },
                 { "location",                   JSON_VARIANT_STRING,        json_dispatch_string,                 offsetof(UserRecord, location),                      0         },
                 { "shell",                      JSON_VARIANT_STRING,        json_dispatch_filename_or_path,       offsetof(UserRecord, shell),                         0         },
@@ -1560,6 +1614,8 @@ int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_fla
         static const JsonDispatch user_dispatch_table[] = {
                 { "userName",                   JSON_VARIANT_STRING,        json_dispatch_user_group_name,        offsetof(UserRecord, user_name),                     JSON_RELAX},
                 { "realm",                      JSON_VARIANT_STRING,        json_dispatch_realm,                  offsetof(UserRecord, realm),                         0         },
+                { "blobDirectory",              JSON_VARIANT_STRING,        json_dispatch_path,                   offsetof(UserRecord, blob_directory),                0         },
+                { "blobManifest",               JSON_VARIANT_OBJECT,        dispatch_blob_manifest,               offsetof(UserRecord, blob_manifest),                 0         },
                 { "realName",                   JSON_VARIANT_STRING,        json_dispatch_gecos,                  offsetof(UserRecord, real_name),                     0         },
                 { "emailAddress",               JSON_VARIANT_STRING,        json_dispatch_string,                 offsetof(UserRecord, email_address),                 JSON_SAFE },
                 { "iconName",                   JSON_VARIANT_STRING,        json_dispatch_string,                 offsetof(UserRecord, icon_name),                     JSON_SAFE },
@@ -2373,6 +2429,13 @@ int user_record_test_password_change_required(UserRecord *h) {
         return change_permitted ? 0 : -EROFS;
 }
 
+int suitable_blob_filename(const char *name) {
+        /* Enforces filename requirements as described in docs/USER_RECORD_BULK_DIRS.md */
+        return filename_is_valid(name) &&
+               in_charset(name, URI_UNRESERVED) &&
+               name[0] != '.';
+}
+
 static const char* const user_storage_table[_USER_STORAGE_MAX] = {
         [USER_CLASSIC]   = "classic",
         [USER_LUKS]      = "luks",
index ee63a5364c2836780e887cdb04eec9eb21f7fd93..1819f55489c68086cedad6040ae4cfc9c97c2481 100644 (file)
@@ -6,6 +6,7 @@
 
 #include "sd-id128.h"
 
+#include "hashmap.h"
 #include "json.h"
 #include "missing_resource.h"
 #include "time-util.h"
@@ -243,6 +244,9 @@ typedef struct UserRecord {
         char *icon_name;
         char *location;
 
+        char *blob_directory;
+        Hashmap *blob_manifest;
+
         UserDisposition disposition;
         uint64_t last_change_usec;
         uint64_t last_password_change_usec;
@@ -449,6 +453,9 @@ int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags);
 int per_machine_match(JsonVariant *entry, JsonDispatchFlags flags);
 int user_group_record_mangle(JsonVariant *v, UserRecordLoadFlags load_flags, JsonVariant **ret_variant, UserRecordMask *ret_mask);
 
+#define BLOB_DIR_MAX_SIZE (UINT64_C(64) * U64_MB)
+int suitable_blob_filename(const char *name);
+
 const char* user_storage_to_string(UserStorage t) _const_;
 UserStorage user_storage_from_string(const char *s) _pure_;