]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homed: Allow user to change parts of their record
authorAdrian Vovk <adrianvovk@gmail.com>
Fri, 2 Feb 2024 03:53:09 +0000 (22:53 -0500)
committerLuca Boccassi <luca.boccassi@gmail.com>
Fri, 1 Nov 2024 10:41:46 +0000 (10:41 +0000)
This allows an unprivileged user that is active at the console to change
the fields that are in the selfModifiable allowlists (introduced in a
previous commit) without authenticating as a system administrator.

Administrators can disable this behavior per-user by setting the
relevant selfModifiable allowlists, or system-wide by changing the
policy of the org.freedesktop.home1.update-home-by-owner Polkit action.

src/home/homed-home-bus.c
src/home/homed-home-bus.h
src/home/homed-home.c
src/home/org.freedesktop.home1.policy
src/shared/user-record.c
src/shared/user-record.h

index 290e05ac6b24f1ae4f0ba9c3b6faeeda455f5d3e..80e2773447fda970898bbc429214ae2cd080dc47 100644 (file)
@@ -55,7 +55,7 @@ static int property_get_state(
         return sd_bus_message_append(reply, "s", home_state_to_string(home_get_state(h)));
 }
 
-int bus_home_client_is_trusted(Home *h, sd_bus_message *message) {
+static int bus_home_client_is_trusted(Home *h, sd_bus_message *message, bool strict) {
         _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL;
         uid_t euid;
         int r;
@@ -73,7 +73,7 @@ int bus_home_client_is_trusted(Home *h, sd_bus_message *message) {
         if (r < 0)
                 return r;
 
-        return euid == 0 || h->uid == euid;
+        return (!strict && euid == 0) || h->uid == euid;
 }
 
 static int home_verify_polkit_async(
@@ -117,7 +117,7 @@ int bus_home_get_record_json(
         assert(h);
         assert(ret);
 
-        trusted = bus_home_client_is_trusted(h, message);
+        trusted = bus_home_client_is_trusted(h, message, /* strict= */ false);
         if (trusted < 0) {
                 log_warning_errno(trusted, "Failed to determine whether client is trusted, assuming untrusted.");
                 trusted = false;
@@ -423,7 +423,7 @@ int bus_home_update_record(
                 Hashmap *blobs,
                 uint64_t flags,
                 sd_bus_error *error) {
-        int r;
+        int r, relax_access;
 
         assert(h);
         assert(message);
@@ -436,10 +436,32 @@ int bus_home_update_record(
         if ((flags & ~SD_HOMED_UPDATE_FLAGS_ALL) != 0)
                 return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid flags provided.");
 
+        if (blobs) {
+                const char *failed = NULL;
+                r = user_record_ensure_blob_manifest(hr, blobs, &failed);
+                if (r == -EINVAL)
+                        return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest.");
+                if (r < 0)
+                        return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed));
+        }
+
+        relax_access = user_record_self_changes_allowed(h->record, hr);
+        if (relax_access < 0) {
+                log_warning_errno(relax_access, "Failed to determine if changes to user record are permitted, assuming not: %m");
+                relax_access = false;
+        } else if (relax_access) {
+                relax_access = bus_home_client_is_trusted(h, message, /* strict= */ true);
+                if (relax_access < 0) {
+                        log_warning_errno(relax_access, "Failed to determine whether client is trusted, assuming not: %m");
+                        relax_access = false;
+                }
+        }
+
         r = home_verify_polkit_async(
                         h,
                         message,
-                        "org.freedesktop.home1.update-home",
+                        relax_access ? "org.freedesktop.home1.update-home-by-owner"
+                                     : "org.freedesktop.home1.update-home",
                         UID_INVALID,
                         error);
         if (r < 0)
@@ -561,7 +583,7 @@ int bus_home_method_change_password(
                         h,
                         message,
                         "org.freedesktop.home1.passwd-home",
-                        h->uid,
+                        h->uid, /* Always let a user change their own password. Safe b/c homework will always re-check password */
                         error);
         if (r < 0)
                 return r;
index 1644bc8066a428b191e635b7462adf7174e8b3f1..8d4ddf909f3e5df7c180fd2d9305fdae9ecd40ec 100644 (file)
@@ -6,7 +6,6 @@
 #include "bus-object.h"
 #include "homed-home.h"
 
-int bus_home_client_is_trusted(Home *h, sd_bus_message *message);
 int bus_home_get_record_json(Home *h, sd_bus_message *message, char **ret, bool *ret_incomplete);
 
 int bus_home_method_activate(sd_bus_message *message, void *userdata, sd_bus_error *error);
index 664397e55e01a983a7c939ad87d58beff38a90bf..32691e4f815387e1079cb0d8c02528522fcd975a 100644 (file)
@@ -1727,15 +1727,6 @@ static int home_update_internal(
                 secret = saved_secret;
         }
 
-        if (blobs) {
-                const char *failed = NULL;
-                r = user_record_ensure_blob_manifest(hr, blobs, &failed);
-                if (r == -EINVAL)
-                        return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest.");
-                if (r < 0)
-                        return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed));
-        }
-
         r = manager_verify_user_record(h->manager, hr);
         switch (r) {
 
index 3b19ed3ed5fc0a99e55964ff09b8111280c8aead..d3317772acb45c34112767eefe04513e2e33fd72 100644 (file)
                 </defaults>
         </action>
 
+        <action id="org.freedesktop.home1.update-home-by-owner">
+                <description gettext-domain="systemd">Update your home area</description>
+                <message gettext-domain="systemd">Authentication is required to update your home area.</message>
+                <defaults>
+                        <allow_any>auth_admin_keep</allow_any>
+                        <allow_inactive>auth_admin_keep</allow_inactive>
+                        <allow_active>yes</allow_active>
+                </defaults>
+        </action>
+
         <action id="org.freedesktop.home1.resize-home">
                 <description gettext-domain="systemd">Resize a home area</description>
                 <message gettext-domain="systemd">Authentication is required to resize a user's home area.</message>
index 35512cbf51a47f14ce4674038dcce520273672fb..47fd5cc31180cd738eb00c4ef587709f0c5fcf7c 100644 (file)
@@ -1475,7 +1475,6 @@ int user_group_record_mangle(
 
         assert(v);
         assert(ret_variant);
-        assert(ret_mask);
 
         /* Note that this function is shared with the group record parser, hence we try to be generic in our
          * log message wording here, to cover both cases. */
@@ -1563,7 +1562,8 @@ int user_group_record_mangle(
         else
                 *ret_variant = sd_json_variant_ref(v);
 
-        *ret_mask = m;
+        if (ret_mask)
+                *ret_mask = m;
         return 0;
 }
 
@@ -2238,6 +2238,172 @@ const char** user_record_self_modifiable_privileged(UserRecord *h) {
         return (const char**) h->self_modifiable_privileged ?: (const char**) default_fields;
 }
 
+static int remove_self_modifiable_json_fields_common(UserRecord *current, sd_json_variant **target) {
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL, *blobs = NULL;
+        char **allowed;
+        int r;
+
+        assert(current);
+        assert(target);
+
+        if (!sd_json_variant_is_object(*target))
+                return -EINVAL;
+
+        v = sd_json_variant_ref(*target);
+
+        /* Handle basic fields */
+        allowed = (char**) user_record_self_modifiable_fields(current);
+        r = sd_json_variant_filter(&v, allowed);
+        if (r < 0)
+                return r;
+
+        /* Handle blobs */
+        blobs = sd_json_variant_ref(sd_json_variant_by_key(v, "blobManifest"));
+        if (blobs) {
+                /* The blobManifest contains the sha256 hashes of the blobs,
+                 * which are enforced by the service managing the user. So, by
+                 * comparing the blob manifests like this, we're actually comparing
+                 * the contents of the blob directories & files */
+
+                allowed = (char**) user_record_self_modifiable_blobs(current);
+                r = sd_json_variant_filter(&blobs, allowed);
+                if (r < 0)
+                        return r;
+
+                if (sd_json_variant_is_blank_object(blobs))
+                        r = sd_json_variant_filter(&v, STRV_MAKE("blobManifest"));
+                else
+                        r = sd_json_variant_set_field(&v, "blobManifest", blobs);
+                if (r < 0)
+                        return r;
+        }
+
+        JSON_VARIANT_REPLACE(*target, TAKE_PTR(v));
+        return 0;
+}
+
+static int remove_self_modifiable_json_fields(UserRecord *current, UserRecord *h, sd_json_variant **ret) {
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL, *privileged = NULL;
+        sd_json_variant *per_machine;
+        char **allowed;
+        int r;
+
+        assert(current);
+        assert(h);
+        assert(ret);
+
+        r = user_group_record_mangle(h->json, USER_RECORD_EXTRACT_SIGNABLE|USER_RECORD_PERMISSIVE, &v, NULL);
+        if (r < 0)
+                return r;
+
+        /* Handle the regular section */
+        r = remove_self_modifiable_json_fields_common(current, &v);
+        if (r < 0)
+                return r;
+
+        /* Handle the perMachine section */
+        per_machine = sd_json_variant_by_key(v, "perMachine");
+        if (per_machine) {
+                _cleanup_(sd_json_variant_unrefp) sd_json_variant *new_per_machine = NULL;
+                sd_json_variant *e;
+
+                if (!sd_json_variant_is_array(per_machine))
+                        return -EINVAL;
+
+                JSON_VARIANT_ARRAY_FOREACH(e, per_machine) {
+                        _cleanup_(sd_json_variant_unrefp) sd_json_variant *z = NULL;
+
+                        if (!sd_json_variant_is_object(e))
+                                return -EINVAL;
+
+                        r = per_machine_match(e, 0);
+                        if (r < 0)
+                                return r;
+                        if (r == 0) {
+                                /* It's only permissible to change anything inside of matching perMachine sections */
+                                r = sd_json_variant_append_array(&new_per_machine, e);
+                                if (r < 0)
+                                        return r;
+                                continue;
+                        }
+
+                        z = sd_json_variant_ref(e);
+
+                        r = remove_self_modifiable_json_fields_common(current, &z);
+                        if (r < 0)
+                                return r;
+
+                        if (!sd_json_variant_is_blank_object(z)) {
+                                r = sd_json_variant_append_array(&new_per_machine, z);
+                                if (r < 0)
+                                        return r;
+                        }
+                }
+
+                if (sd_json_variant_is_blank_array(new_per_machine))
+                        r = sd_json_variant_filter(&v, STRV_MAKE("perMachine"));
+                else
+                        r = sd_json_variant_set_field(&v, "perMachine", new_per_machine);
+                if (r < 0)
+                        return r;
+        }
+
+        /* Handle the privileged section */
+        privileged = sd_json_variant_ref(sd_json_variant_by_key(v, "privileged"));
+        if (privileged) {
+                allowed = (char**) user_record_self_modifiable_privileged(current);
+                r = sd_json_variant_filter(&privileged, allowed);
+                if (r < 0)
+                        return r;
+
+                if (sd_json_variant_is_blank_object(privileged))
+                        r = sd_json_variant_filter(&v, STRV_MAKE("privileged"));
+                else
+                        r = sd_json_variant_set_field(&v, "privileged", privileged);
+                if (r < 0)
+                        return r;
+        }
+
+        JSON_VARIANT_REPLACE(*ret, TAKE_PTR(v));
+        return 0;
+}
+
+int user_record_self_changes_allowed(UserRecord *current, UserRecord *incoming) {
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *vc = NULL, *vi = NULL;
+        int r;
+
+        assert(current);
+        assert(incoming);
+
+        /* We remove the fields that the user is allowed to change and then
+         * compare the resulting JSON records. If they are not equal, that
+         * means a disallowed field has been changed and thus we should
+         * require administrator permission to apply the changes. */
+
+        r = remove_self_modifiable_json_fields(current, current, &vc);
+        if (r < 0)
+                return r;
+
+        /* Note that we use `current` as the source of the allowlist, and not
+         * `incoming`. This prevents the user from adding fields. Consider a
+         * scenario that would've been possible if we had messed up this check:
+         *
+         * 1) A user starts out with no group memberships and no custom allowlist.
+         *    Thus, this user is not an administrator, and the `memberOf` and
+         *    `selfModifiableFields` fields are unset in their record.
+         * 2) This user crafts a request to add the following to their record:
+         *    { "memberOf": ["wheel"], "selfModifiableFields": ["memberOf", "selfModifiableFields"] }
+         * 3) We remove the `mebmerOf` and `selfModifiabileFields` fields from `incoming`
+         * 4) `current` and `incoming` compare as equal, so we let the change happen
+         * 5) the user has granted themselves administrator privileges
+         */
+        r = remove_self_modifiable_json_fields(current, incoming, &vi);
+        if (r < 0)
+                return r;
+
+        return sd_json_variant_equal(vc, vi);
+}
+
 uint64_t user_record_ratelimit_next_try(UserRecord *h) {
         assert(h);
 
index acbb8eca735db97daf819a4acce441ea1888d40f..b539b3f55e37643432a4cfa3863f4641198ec6da 100644 (file)
@@ -438,6 +438,7 @@ int user_record_languages(UserRecord *h, char ***ret);
 const char **user_record_self_modifiable_fields(UserRecord *h);
 const char **user_record_self_modifiable_blobs(UserRecord *h);
 const char **user_record_self_modifiable_privileged(UserRecord *h);
+int user_record_self_changes_allowed(UserRecord *current, UserRecord *new);
 
 int user_record_build_image_path(UserStorage storage, const char *user_name_and_realm, char **ret);