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/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-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..47fd5cc311 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 }, {}, }; @@ -1468,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. */ @@ -1556,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; } @@ -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,244 @@ 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; +} + +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 0443820890..b539b3f55e 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,11 @@ 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_self_changes_allowed(UserRecord *current, UserRecord *new); + int user_record_build_image_path(UserStorage storage, const char *user_name_and_realm, char **ret); bool user_record_equal(UserRecord *a, UserRecord *b); 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);