From ad03f2d5f0d7f87b775357e5a2727dbcbc973fce Mon Sep 17 00:00:00 2001 From: Adrian Vovk Date: Wed, 24 Apr 2024 18:12:54 -0400 Subject: [PATCH] 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 0268cc1230b..911fceb03f5 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 45ef48bab67..e6de0cd002f 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 b03a38eb18a..35512cbf51a 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 0443820890c..acbb8eca735 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);