From ad03f2d5f0d7f87b775357e5a2727dbcbc973fce Mon Sep 17 00:00:00 2001 From: Adrian Vovk Date: Wed, 24 Apr 2024 18:12:54 -0400 Subject: [PATCH 1/3] user-record: Introduce selfModifiable fields Allows the system administrator to configure what fields the user is allowed to edit about themself, along with hard-coded defaults. --- docs/USER_RECORD.md | 13 +++++- src/shared/user-record-show.c | 29 +++++++++++++ src/shared/user-record.c | 82 +++++++++++++++++++++++++++++++++++ src/shared/user-record.h | 8 ++++ 4 files changed, 131 insertions(+), 1 deletion(-) diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 0268cc1230..911fceb03f 100644 --- a/docs/USER_RECORD.md +++ b/docs/USER_RECORD.md @@ -597,6 +597,17 @@ The salt to pass to the FIDO2 device is found in `fido2HmacSalt`. The only supported recovery key type at the moment is `modhex64`, for details see the description of `recoveryKey` below. An account may have any number of recovery keys defined, and the array should have one entry for each. +`selfModifiableFields` → An array of strings, each corresponding to a field name that can appear +in the `regular` or `perMachine` sections. The user may be allowed to edit any field in this list +without authenticating as an administrator. Note that the user will only be allowed to edit fields +in `perMachine` sections that match the machine the user is performing the edit from. + +`selfModifiableBlobs` → Similar to `selfModifiableFields`, but it lists blobs that the user +is allowed to edit. + +`selfModifiablePrivileged` → Similar to `selfModifiableFields`, but it lists fields in +the `privileged` section that the user is allowed to edit. + `privileged` → An object, which contains the fields of the `privileged` section of the user record, see below. @@ -754,7 +765,7 @@ All other fields that may be used in this section are identical to the equally n `autoLogin`, `preferredSessionType`, `preferredSessionLauncher`, `stopDelayUSec`, `killProcesses`, `passwordChangeMinUSec`, `passwordChangeMaxUSec`, `passwordChangeWarnUSec`, `passwordChangeInactiveUSec`, `passwordChangeNow`, `pkcs11TokenUri`, -`fido2HmacCredential`. +`fido2HmacCredential`, `selfModifiableFields`, `selfModifiableBlobs`, `selfModifiablePrivileged`. ## Fields in the `binding` section diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c index 45ef48bab6..e6de0cd002 100644 --- a/src/shared/user-record-show.c +++ b/src/shared/user-record-show.c @@ -28,6 +28,25 @@ const char* user_record_state_color(const char *state) { return NULL; } +static void dump_self_modifiable(const char *heading, char **field, const char **value) { + assert(heading); + + /* Helper function for printing the various self_modifiable_* fields from the user record */ + + if (strv_isempty((char**) value)) + /* Case 1: the array is explicitly set to be empty by the administrator */ + printf("%13s %sDisabled by Administrator%s\n", heading, ansi_highlight_red(), ansi_normal()); + else if (!field) + /* Case 2: we have values, but the field is NULL. This means that we're using the defaults. + * We list them anyways, because they're security-sensitive to the administrator */ + STRV_FOREACH(i, value) + printf("%13s %s%s%s\n", i == value ? heading : "", ansi_grey(), *i, ansi_normal()); + else + /* Case 3: we have a list provided by the administrator */ + STRV_FOREACH(i, value) + printf("%13s %s\n", i == value ? heading : "", *i); +} + void user_record_show(UserRecord *hr, bool show_full_group_info) { _cleanup_strv_free_ char **langs = NULL; const char *hd, *ip, *shell; @@ -585,6 +604,16 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { if (hr->service) printf(" Service: %s\n", hr->service); + + dump_self_modifiable("Self Modify:", + hr->self_modifiable_fields, + user_record_self_modifiable_fields(hr)); + dump_self_modifiable("(Blobs)", + hr->self_modifiable_blobs, + user_record_self_modifiable_blobs(hr)); + dump_self_modifiable("(Privileged)", + hr->self_modifiable_privileged, + user_record_self_modifiable_privileged(hr)); } void group_record_show(GroupRecord *gr, bool show_full_user_info) { diff --git a/src/shared/user-record.c b/src/shared/user-record.c index b03a38eb18..35512cbf51 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -207,6 +207,10 @@ static UserRecord* user_record_free(UserRecord *h) { for (size_t i = 0; i < h->n_recovery_key; i++) recovery_key_done(h->recovery_key + i); + strv_free(h->self_modifiable_fields); + strv_free(h->self_modifiable_blobs); + strv_free(h->self_modifiable_privileged); + sd_json_variant_unref(h->json); return mfree(h); @@ -1300,6 +1304,9 @@ static int dispatch_per_machine(const char *name, sd_json_variant *variant, sd_j { "passwordChangeNow", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 }, { "pkcs11TokenUri", SD_JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 }, { "fido2HmacCredential", SD_JSON_VARIANT_ARRAY, dispatch_fido2_hmac_credential_array, 0, 0 }, + { "selfModifiableFields", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserRecord, self_modifiable_fields), SD_JSON_STRICT }, + { "selfModifiableBlobs", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserRecord, self_modifiable_blobs), SD_JSON_STRICT }, + { "selfModifiablePrivileged", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserRecord, self_modifiable_privileged), SD_JSON_STRICT }, {}, }; @@ -1646,6 +1653,9 @@ int user_record_load(UserRecord *h, sd_json_variant *v, UserRecordLoadFlags load { "pkcs11TokenUri", SD_JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 }, { "fido2HmacCredential", SD_JSON_VARIANT_ARRAY, dispatch_fido2_hmac_credential_array, 0, 0 }, { "recoveryKeyType", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserRecord, recovery_key_type), 0 }, + { "selfModifiableFields", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserRecord, self_modifiable_fields), SD_JSON_STRICT }, + { "selfModifiableBlobs", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserRecord, self_modifiable_blobs), SD_JSON_STRICT }, + { "selfModifiablePrivileged", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserRecord, self_modifiable_privileged), SD_JSON_STRICT }, { "secret", SD_JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 }, { "privileged", SD_JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 }, @@ -2156,6 +2166,78 @@ int user_record_languages(UserRecord *h, char ***ret) { return 0; } +const char** user_record_self_modifiable_fields(UserRecord *h) { + /* As a rule of thumb: a setting is safe if it cannot be used by a + * user to give themselves some unfair advantage over other users on + * a given system. */ + static const char *const default_fields[] = { + /* For display purposes */ + "realName", + "emailAddress", /* Just the $EMAIL env var */ + "iconName", + "location", + + /* Basic account settings */ + "shell", + "umask", + "environment", + "timeZone", + "preferredLanguage", + "additionalLanguages", + "preferredSessionLauncher", + "preferredSessionType", + + /* Authentication methods */ + "pkcs11TokenUri", + "fido2HmacCredential", + "recoveryKeyType", + + "lastChangeUSec", /* Necessary to be able to change record at all */ + "lastPasswordChangeUSec", /* Ditto, but for authentication methods */ + NULL + }; + + assert(h); + + /* Note that we intentionally distinguish between NULL and an empty array here */ + return (const char**) h->self_modifiable_fields ?: (const char**) default_fields; +} + +const char** user_record_self_modifiable_blobs(UserRecord *h) { + static const char *const default_blobs[] = { + /* For display purposes */ + "avatar", + "login-background", + NULL + }; + + assert(h); + + /* Note that we intentionally distinguish between NULL and an empty array here */ + return (const char**) h->self_modifiable_blobs ?: (const char**) default_blobs; +} + +const char** user_record_self_modifiable_privileged(UserRecord *h) { + static const char *const default_fields[] = { + /* For display purposes */ + "passwordHint", + + /* Authentication methods */ + "hashedPassword" + "pkcs11EncryptedKey", + "fido2HmacSalt", + "recoveryKey", + + "sshAuthorizedKeys", /* Basically just ~/.ssh/authorized_keys */ + NULL + }; + + assert(h); + + /* Note that we intentionally distinguish between NULL and an empty array here */ + return (const char**) h->self_modifiable_privileged ?: (const char**) default_fields; +} + uint64_t user_record_ratelimit_next_try(UserRecord *h) { assert(h); diff --git a/src/shared/user-record.h b/src/shared/user-record.h index 0443820890..acbb8eca73 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -383,6 +383,10 @@ typedef struct UserRecord { char **capability_bounding_set; char **capability_ambient_set; + char **self_modifiable_fields; /* fields a user can change about themself w/o auth */ + char **self_modifiable_blobs; + char **self_modifiable_privileged; + sd_json_variant *json; } UserRecord; @@ -431,6 +435,10 @@ uint64_t user_record_capability_bounding_set(UserRecord *h); uint64_t user_record_capability_ambient_set(UserRecord *h); 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_build_image_path(UserStorage storage, const char *user_name_and_realm, char **ret); bool user_record_equal(UserRecord *a, UserRecord *b); From a192250eda1e5cc1f8fc799cf9b85d37e7fa0519 Mon Sep 17 00:00:00 2001 From: Adrian Vovk Date: Thu, 1 Feb 2024 22:53:09 -0500 Subject: [PATCH 2/3] homed: Allow user to change parts of their record 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 | 34 +++++- src/home/homed-home-bus.h | 1 - src/home/homed-home.c | 9 -- src/home/org.freedesktop.home1.policy | 10 ++ src/shared/user-record.c | 170 +++++++++++++++++++++++++- src/shared/user-record.h | 1 + 6 files changed, 207 insertions(+), 18 deletions(-) diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c index 290e05ac6b..80e2773447 100644 --- a/src/home/homed-home-bus.c +++ b/src/home/homed-home-bus.c @@ -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; diff --git a/src/home/homed-home-bus.h b/src/home/homed-home-bus.h index 1644bc8066..8d4ddf909f 100644 --- a/src/home/homed-home-bus.h +++ b/src/home/homed-home-bus.h @@ -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); diff --git a/src/home/homed-home.c b/src/home/homed-home.c index 664397e55e..32691e4f81 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -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) { diff --git a/src/home/org.freedesktop.home1.policy b/src/home/org.freedesktop.home1.policy index 3b19ed3ed5..d3317772ac 100644 --- a/src/home/org.freedesktop.home1.policy +++ b/src/home/org.freedesktop.home1.policy @@ -49,6 +49,16 @@ + + Update your home area + Authentication is required to update your home area. + + auth_admin_keep + auth_admin_keep + yes + + + Resize a home area Authentication is required to resize a user's home area. diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 35512cbf51..47fd5cc311 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -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); diff --git a/src/shared/user-record.h b/src/shared/user-record.h index acbb8eca73..b539b3f55e 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -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); From 6a2d4a233dd10cbcfe931e5c63193641f2dab4f3 Mon Sep 17 00:00:00 2001 From: Adrian Vovk Date: Wed, 24 Apr 2024 19:41:03 -0400 Subject: [PATCH 3/3] test: Test user record selfModifiable behavior --- src/test/meson.build | 1 + src/test/test-user-record.c | 101 ++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/test/test-user-record.c diff --git a/src/test/meson.build b/src/test/meson.build index 2157e7c1f3..9f74a7b56a 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -183,6 +183,7 @@ simple_tests += files( 'test-umask-util.c', 'test-unaligned.c', 'test-unit-file.c', + 'test-user-record.c', 'test-user-util.c', 'test-utf8.c', 'test-verbs.c', diff --git a/src/test/test-user-record.c b/src/test/test-user-record.c new file mode 100644 index 0000000000..3a7e8e28af --- /dev/null +++ b/src/test/test-user-record.c @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "json-util.h" +#include "macro.h" +#include "tests.h" +#include "user-record.h" + +#define USER(ret, ...) \ + ({ \ + typeof(ret) _r = (ret); \ + user_record_unref(*_r); \ + assert_se(user_record_build((ret), SD_JSON_BUILD_OBJECT(__VA_ARGS__)) >= 0); \ + 0; \ + }) + +TEST(self_changes) { + _cleanup_(user_record_unrefp) UserRecord *curr = NULL, *new = NULL; + + /* not allowlisted */ + USER(&curr, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 11111)); + USER(&new, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 99999)); + assert_se(!user_record_self_changes_allowed(curr, new)); + + /* manually allowlisted */ + USER(&curr, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 11111), + SD_JSON_BUILD_PAIR_ARRAY("selfModifiableFields", SD_JSON_BUILD_STRING("notInHardCodedList"))); + USER(&new, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_ARRAY("selfModifiableFields", SD_JSON_BUILD_STRING("notInHardCodedList")), + /* change in order shouldn't affect things */ + SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 99999)); + assert_se(user_record_self_changes_allowed(curr, new)); + + /* default allowlisted */ + USER(&curr, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_STRING("realName", "Old Name")); + USER(&new, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_STRING("realName", "New Name")); + assert_se(user_record_self_changes_allowed(curr, new)); + + /* introduced new default allowlisted */ + USER(&curr, + SD_JSON_BUILD_PAIR_STRING("userName", "test")); + USER(&new, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_STRING("realName", "New Name")); + assert_se(user_record_self_changes_allowed(curr, new)); + + /* introduced new not allowlisted */ + USER(&curr, + SD_JSON_BUILD_PAIR_STRING("userName", "test")); + USER(&new, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 99999)); + assert_se(!user_record_self_changes_allowed(curr, new)); + + /* privileged section: default allowlisted */ + USER(&curr, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_OBJECT("privileged", + SD_JSON_BUILD_PAIR_STRING("passwordHint", "Old Hint"))); + USER(&new, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_OBJECT("privileged", + SD_JSON_BUILD_PAIR_STRING("passwordHint", "New Hint"))); + assert_se(user_record_self_changes_allowed(curr, new)); + + /* privileged section: not allowlisted */ + USER(&curr, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_OBJECT("privileged", + SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 11111))); + USER(&new, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_OBJECT("privileged", + SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 99999))); + assert_se(!user_record_self_changes_allowed(curr, new)); + + /* privileged section: manually allowlisted */ + USER(&curr, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_ARRAY("selfModifiablePrivileged", SD_JSON_BUILD_STRING("notInHardCodedList")), + SD_JSON_BUILD_PAIR_OBJECT("privileged", + SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 11111))); + USER(&new, + SD_JSON_BUILD_PAIR_STRING("userName", "test"), + SD_JSON_BUILD_PAIR_ARRAY("selfModifiablePrivileged", SD_JSON_BUILD_STRING("notInHardCodedList")), + SD_JSON_BUILD_PAIR_OBJECT("privileged", + SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 99999))); + assert_se(user_record_self_changes_allowed(curr, new)); +} + +DEFINE_TEST_MAIN(LOG_INFO);