Homed update policy: user changing own settings (#31153)
Rework of #30109 to deal with changes in #30840 and discussed changes to behavior Depends on and includes #30840 Fixes https://github.com/systemd/systemd/issues/34268
This commit is contained in:
+12
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -49,6 +49,16 @@
|
||||
</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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+250
-2
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user