diff --git a/TODO b/TODO index d2c451fb99..6ece980c4f 100644 --- a/TODO +++ b/TODO @@ -262,12 +262,6 @@ Features: suffix the escape sequence with one more decimal digit, because compilers think you might actually specify a value outside the 8bit range with that. -* homed: allow login via username + realm on getty/login prompt. Then rewrite - the user name in the PAM stack - -* homed/userdb: add "aliases" field to user record, which can alternatively be - used for logging in. Rewrite user name in the PAM stack once acquired. - * confext/sysext: instead of mounting the overlayfs directly on /etc/ + /usr/, insert an intermediary bind mount on itself there. This has the benefit that services where mount propagation from the root fs is off, an still have @@ -381,11 +375,6 @@ Features: * Clean up "reboot argument" handling, i.e. set it through some IPC service instead of directly via /run/, so that it can be sensible set remotely. -* userdb: add concept for user "aliases", to cover for cases where you can log - in under the name lennart@somenetworkfsserver, and it would automatically - generate a local user, and from the one both names can be used to allow - logins into the same account. - * systemd-tpm2-support: add a some logic that detects if system is in DA lockout mode, and queries the user for TPM recovery PIN then. diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 1a219fed41..a8e02b2c5e 100644 --- a/docs/USER_RECORD.md +++ b/docs/USER_RECORD.md @@ -226,6 +226,14 @@ This field is optional, when unset the user should not be considered part of any A user record with a realm set is never compatible (for the purpose of updates, see above) with a user record without one set, even if the `userName` field matches. +`aliases` → An array of strings, each being a valid UNIX user name. If +specified, a list of additional UNIX user names this record shall be known +under. These are *alias* names only, the name in `userName` is always the +primary name. Typically, a user record that carries this field shall be +retrievable and resolvable under every name listed here, pretty much everywhere +the primary user name is. If logging in is attempted via an alias name it +should be normalized to the primary name. + `blobDirectory` → The absolute path to a world-readable copy of the user's blob directory. See [Blob Directories](/USER_RECORD_BLOB_DIRS) for more details. diff --git a/man/homectl.xml b/man/homectl.xml index 4f251bb336..6a2be70030 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -226,6 +226,16 @@ + + + + Additional names for the user. Takes one or more valid UNIX user names, separated by + commas. May be used multiple times to define multiple aliases. An alias username may be specified + wherever the primary user name may be specified, and resolves to the same user record. + + + + diff --git a/src/basic/strv.c b/src/basic/strv.c index a92b1234a3..c9c4551cdc 100644 --- a/src/basic/strv.c +++ b/src/basic/strv.c @@ -861,6 +861,26 @@ int strv_compare(char * const *a, char * const *b) { return 0; } +bool strv_equal_ignore_order(char **a, char **b) { + + /* Just like strv_equal(), but doesn't care about the order of elements or about redundant entries + * (i.e. it's even ok if the number of entries in the array differ, as long as the difference just + * consists of repititions) */ + + if (a == b) + return true; + + STRV_FOREACH(i, a) + if (!strv_contains(b, *i)) + return false; + + STRV_FOREACH(i, b) + if (!strv_contains(a, *i)) + return false; + + return true; +} + void strv_print_full(char * const *l, const char *prefix) { STRV_FOREACH(s, l) printf("%s%s\n", strempty(prefix), *s); diff --git a/src/basic/strv.h b/src/basic/strv.h index 49ef19dcb5..86ba06f835 100644 --- a/src/basic/strv.h +++ b/src/basic/strv.h @@ -96,6 +96,8 @@ static inline bool strv_equal(char * const *a, char * const *b) { return strv_compare(a, b) == 0; } +bool strv_equal_ignore_order(char **a, char **b); + char** strv_new_internal(const char *x, ...) _sentinel_; char** strv_new_ap(const char *x, va_list ap); #define strv_new(...) strv_new_internal(__VA_ARGS__, NULL) diff --git a/src/home/homectl.c b/src/home/homectl.c index 104c35454b..b8b6aa3a86 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -750,17 +750,9 @@ static int inspect_home(int argc, char *argv[], void *userdata) { uid_t uid; r = parse_uid(*i, &uid); - if (r < 0) { - if (!valid_user_group_name(*i, 0)) { - log_error("Invalid user name '%s'.", *i); - if (ret == 0) - ret = -EINVAL; - - continue; - } - + if (r < 0) r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", *i); - } else + else r = bus_call_method(bus, bus_mgr, "GetUserRecordByUID", &error, &reply, "u", (uint32_t) uid); if (r < 0) { log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r)); @@ -2764,6 +2756,7 @@ static int help(int argc, char *argv[], void *userdata) { "\n%4$sGeneral User Record Properties:%5$s\n" " -c --real-name=REALNAME Real name for user\n" " --realm=REALM Realm to create user in\n" + " --alias=ALIAS Define alias usernames for this account\n" " --email-address=EMAIL Email address for user\n" " --location=LOCATION Set location of user on earth\n" " --icon-name=NAME Icon name for user\n" @@ -2908,6 +2901,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_NO_ASK_PASSWORD, ARG_OFFLINE, ARG_REALM, + ARG_ALIAS, ARG_EMAIL_ADDRESS, ARG_DISK_SIZE, ARG_ACCESS_MODE, @@ -2999,6 +2993,7 @@ static int parse_argv(int argc, char *argv[]) { { "real-name", required_argument, NULL, 'c' }, { "comment", required_argument, NULL, 'c' }, /* Compat alias to keep thing in sync with useradd(8) */ { "realm", required_argument, NULL, ARG_REALM }, + { "alias", required_argument, NULL, ARG_ALIAS }, { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS }, { "location", required_argument, NULL, ARG_LOCATION }, { "password-hint", required_argument, NULL, ARG_PASSWORD_HINT }, @@ -3154,6 +3149,53 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_ALIAS: { + if (isempty(optarg)) { + r = drop_from_identity("aliases"); + if (r < 0) + return r; + break; + } + + for (const char *p = optarg;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", 0); + if (r < 0) + return log_error_errno(r, "Failed to parse alias list: %m"); + if (r == 0) + break; + + if (!valid_user_group_name(word, 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid alias user name %s.", word); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *av = + sd_json_variant_ref(sd_json_variant_by_key(arg_identity_extra, "aliases")); + + _cleanup_strv_free_ char **list = NULL; + r = sd_json_variant_strv(av, &list); + if (r < 0) + return log_error_errno(r, "Failed to parse group list: %m"); + + r = strv_extend(&list, word); + if (r < 0) + return log_oom(); + + strv_sort_uniq(list); + + av = sd_json_variant_unref(av); + r = sd_json_variant_new_array_strv(&av, list); + if (r < 0) + return log_error_errno(r, "Failed to create alias list JSON: %m"); + + r = sd_json_variant_set_field(&arg_identity_extra, "aliases", av); + if (r < 0) + return log_error_errno(r, "Failed to update alias list: %m"); + } + + break; + } + case 'd': { _cleanup_free_ char *hd = NULL; diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c index 80e2773447..a3e6a32162 100644 --- a/src/home/homed-home-bus.c +++ b/src/home/homed-home-bus.c @@ -799,8 +799,11 @@ static int bus_home_object_find( if (parse_uid(e, &uid) >= 0) h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); - else - h = hashmap_get(m->homes_by_name, e); + else { + r = manager_get_home_by_name(m, e, &h); + if (r < 0) + return r; + } if (!h) return 0; diff --git a/src/home/homed-home.c b/src/home/homed-home.c index a6f99713dd..96477b3061 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -101,6 +101,7 @@ static int suitable_home_record(UserRecord *hr) { int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) { _cleanup_(home_freep) Home *home = NULL; _cleanup_free_ char *nm = NULL, *ns = NULL, *blob = NULL; + _cleanup_strv_free_ char **aliases = NULL; int r; assert(m); @@ -113,19 +114,29 @@ int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) { if (hashmap_contains(m->homes_by_name, hr->user_name)) return -EBUSY; + STRV_FOREACH(a, hr->aliases) + if (hashmap_contains(m->homes_by_name, *a)) + return -EBUSY; + if (hashmap_contains(m->homes_by_uid, UID_TO_PTR(hr->uid))) return -EBUSY; if (sysfs && hashmap_contains(m->homes_by_sysfs, sysfs)) return -EBUSY; - if (hashmap_size(m->homes_by_name) >= HOME_USERS_MAX) + if (hashmap_size(m->homes_by_uid) >= HOME_USERS_MAX) return -EUSERS; nm = strdup(hr->user_name); if (!nm) return -ENOMEM; + if (!strv_isempty(hr->aliases)) { + aliases = strv_copy(hr->aliases); + if (!aliases) + return -ENOMEM; + } + if (sysfs) { ns = strdup(sysfs); if (!ns) @@ -139,6 +150,7 @@ int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) { *home = (Home) { .manager = m, .user_name = TAKE_PTR(nm), + .aliases = TAKE_PTR(aliases), .uid = hr->uid, .state = _HOME_STATE_INVALID, .worker_stdout_fd = -EBADF, @@ -152,6 +164,12 @@ int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) { if (r < 0) return r; + STRV_FOREACH(a, home->aliases) { + r = hashmap_put(m->homes_by_name, *a, home); + if (r < 0) + return r; + } + r = hashmap_put(m->homes_by_uid, UID_TO_PTR(home->uid), home); if (r < 0) return r; @@ -197,6 +215,9 @@ Home *home_free(Home *h) { if (h->user_name) (void) hashmap_remove_value(h->manager->homes_by_name, h->user_name, h); + STRV_FOREACH(a, h->aliases) + (void) hashmap_remove_value(h->manager->homes_by_name, *a, h); + if (uid_is_valid(h->uid)) (void) hashmap_remove_value(h->manager->homes_by_uid, UID_TO_PTR(h->uid), h); @@ -218,6 +239,7 @@ Home *home_free(Home *h) { h->worker_event_source = sd_event_source_disable_unref(h->worker_event_source); safe_close(h->worker_stdout_fd); free(h->user_name); + strv_free(h->aliases); free(h->sysfs); h->ref_event_source_please_suspend = sd_event_source_disable_unref(h->ref_event_source_please_suspend); @@ -257,6 +279,10 @@ int home_set_record(Home *h, UserRecord *hr) { if (!user_record_compatible(h->record, hr)) return -EREMCHG; + /* For now do not allow changing list of aliases */ + if (!strv_equal_ignore_order(h->aliases, hr->aliases)) + return -EREMCHG; + if (!FLAGS_SET(hr->mask, USER_RECORD_REGULAR) || FLAGS_SET(hr->mask, USER_RECORD_SECRET)) return -EINVAL; diff --git a/src/home/homed-home.h b/src/home/homed-home.h index 8c92e39fe5..93689563d3 100644 --- a/src/home/homed-home.h +++ b/src/home/homed-home.h @@ -109,7 +109,12 @@ static inline bool HOME_STATE_MAY_RETRY_DEACTIVATE(HomeState state) { struct Home { Manager *manager; + + /* The fields this record can be looked up by. This is kinda redundant, as the same information is + * available in the .record field, but we keep separate copies of these keys to make memory + * management for the hashmaps easier. */ char *user_name; + char **aliases; uid_t uid; char *sysfs; /* When found via plugged in device, the sysfs path to it */ diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c index 08c917aee2..a08cc3803c 100644 --- a/src/home/homed-manager-bus.c +++ b/src/home/homed-manager-bus.c @@ -37,7 +37,7 @@ static int property_get_auto_login( if (r < 0) return r; - HASHMAP_FOREACH(h, m->homes_by_name) { + HASHMAP_FOREACH(h, m->homes_by_uid) { _cleanup_strv_free_ char **seats = NULL; _cleanup_free_ char *home_path = NULL; @@ -97,11 +97,9 @@ static int lookup_user_name( return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "Client's UID " UID_FMT " not managed.", uid); } else { - - if (!valid_user_group_name(user_name, 0)) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); - - h = hashmap_get(m->homes_by_name, user_name); + r = manager_get_home_by_name(m, user_name, &h); + if (r < 0) + return r; if (!h) return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); } @@ -342,6 +340,31 @@ static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bu return generic_home_method(userdata, message, bus_home_method_deactivate, error); } +static int check_for_conflicts(Manager *m, const char *name, sd_bus_error *error) { + int r; + + assert(m); + assert(name); + + Home *other = hashmap_get(m->homes_by_name, name); + if (other) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists already, refusing.", name); + + r = getpwnam_malloc(name, /* ret= */ NULL); + if (r >= 0) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists in the NSS user database, refusing.", name); + if (r != -ESRCH) + return r; + + r = getgrnam_malloc(name, /* ret= */ NULL); + if (r >= 0) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s conflicts with an NSS group by the same name, refusing.", name); + if (r != -ESRCH) + return r; + + return 0; +} + static int validate_and_allocate_home(Manager *m, UserRecord *hr, Hashmap *blobs, Home **ret, sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *signed_hr = NULL; bool signed_locally; @@ -356,21 +379,32 @@ static int validate_and_allocate_home(Manager *m, UserRecord *hr, Hashmap *blobs if (r < 0) return r; - other = hashmap_get(m->homes_by_name, hr->user_name); - if (other) - return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists already, refusing.", hr->user_name); - - r = getpwnam_malloc(hr->user_name, /* ret= */ NULL); - if (r >= 0) - return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists in the NSS user database, refusing.", hr->user_name); - if (r != -ESRCH) + r = check_for_conflicts(m, hr->user_name, error); + if (r < 0) return r; - r = getgrnam_malloc(hr->user_name, /* ret= */ NULL); - if (r >= 0) - return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s conflicts with an NSS group by the same name, refusing.", hr->user_name); - if (r != -ESRCH) - return r; + if (hr->realm) { + r = check_for_conflicts(m, user_record_user_name_and_realm(hr), error); + if (r < 0) + return r; + } + + STRV_FOREACH(a, hr->aliases) { + r = check_for_conflicts(m, *a, error); + if (r < 0) + return r; + + if (hr->realm) { + _cleanup_free_ char *alias_with_realm = NULL; + alias_with_realm = strjoin(*a, "@", hr->realm); + if (!alias_with_realm) + return -ENOMEM; + + r = check_for_conflicts(m, alias_with_realm, error); + if (r < 0) + return r; + } + } if (blobs) { const char *failed = NULL; @@ -637,7 +671,7 @@ static int method_lock_all_homes(sd_bus_message *message, void *userdata, sd_bus * for every suitable home we have and only when all of them completed we send a reply indicating * completion. */ - HASHMAP_FOREACH(h, m->homes_by_name) { + HASHMAP_FOREACH(h, m->homes_by_uid) { if (!home_shall_suspend(h)) continue; @@ -676,7 +710,7 @@ static int method_deactivate_all_homes(sd_bus_message *message, void *userdata, * systemd-homed.service itself since we want to allow restarting of it without tearing down all home * directories. */ - HASHMAP_FOREACH(h, m->homes_by_name) { + HASHMAP_FOREACH(h, m->homes_by_uid) { if (!o) { o = operation_new(OPERATION_DEACTIVATE_ALL, message); diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index a0d38eb701..447d8949cc 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -76,7 +76,6 @@ static bool uid_is_home(uid_t uid) { #define UID_CLAMP_INTO_HOME_RANGE(rnd) (((uid_t) (rnd) % (HOME_UID_MAX - HOME_UID_MIN + 1)) + HOME_UID_MIN) DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_uid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free); -DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_name_hash_ops, char, string_hash_func, string_compare_func, Home, home_free); DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_worker_pid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free); DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_sysfs_hash_ops, char, path_hash_func, path_compare, Home, home_free); @@ -192,7 +191,7 @@ static int on_home_inotify(sd_event_source *s, const struct inotify_event *event log_debug("%s has been moved away, revalidating.", j); h = hashmap_get(m->homes_by_name, n); - if (h) { + if (h && streq(h->user_name, n)) { manager_revalidate_image(m, h); (void) bus_manager_emit_auto_login_changed(m); } @@ -243,7 +242,7 @@ int manager_new(Manager **ret) { if (!m->homes_by_uid) return -ENOMEM; - m->homes_by_name = hashmap_new(&homes_by_name_hash_ops); + m->homes_by_name = hashmap_new(&string_hash_ops); if (!m->homes_by_name) return -ENOMEM; @@ -698,6 +697,11 @@ static int manager_add_home_by_image( if (h) { bool same; + if (!streq(h->user_name, user_name)) { + log_debug("Found an image for user %s which already is an alias for another user, skipping.", user_name); + return 0; /* Ignore images that would synthesize a user that conflicts with an alias of another user */ + } + if (h->state != HOME_UNFIXATED) { log_debug("Found an image for user %s which already has a record, skipping.", user_name); return 0; /* ignore images that synthesize a user we already have a record for */ @@ -1721,7 +1725,7 @@ int manager_gc_images(Manager *m) { } else { /* Gc all */ - HASHMAP_FOREACH(h, m->homes_by_name) + HASHMAP_FOREACH(h, m->homes_by_uid) manager_revalidate_image(m, h); } @@ -1741,12 +1745,14 @@ static int manager_gc_blob(Manager *m) { return log_error_errno(errno, "Failed to open %s: %m", home_system_blob_dir()); } - FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read system blob directory: %m")) - if (!hashmap_contains(m->homes_by_name, de->d_name)) { + FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read system blob directory: %m")) { + Home *found = hashmap_get(m->homes_by_name, de->d_name); + if (!found || !streq(found->user_name, de->d_name)) { r = rm_rf_at(dirfd(d), de->d_name, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); if (r < 0) log_warning_errno(r, "Failed to delete blob dir for missing user '%s', ignoring: %m", de->d_name); } + } return 0; } @@ -1841,7 +1847,7 @@ static bool manager_shall_rebalance(Manager *m) { if (IN_SET(m->rebalance_state, REBALANCE_PENDING, REBALANCE_SHRINKING, REBALANCE_GROWING)) return true; - HASHMAP_FOREACH(h, m->homes_by_name) + HASHMAP_FOREACH(h, m->homes_by_uid) if (home_shall_rebalance(h)) return true; @@ -1887,7 +1893,7 @@ static int manager_rebalance_calculate(Manager *m) { * (home dirs get 100 by default, i.e. 5x more). This weight * is not configurable, the per-home weights are. */ - HASHMAP_FOREACH(h, m->homes_by_name) { + HASHMAP_FOREACH(h, m->homes_by_uid) { statfs_f_type_t fstype; h->rebalance_pending = false; /* First, reset the flag, we only want it to be true for the * homes that qualify for rebalancing */ @@ -2024,7 +2030,7 @@ static int manager_rebalance_apply(Manager *m) { assert(m); - HASHMAP_FOREACH(h, m->homes_by_name) { + HASHMAP_FOREACH(h, m->homes_by_uid) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; if (!h->rebalance_pending) @@ -2265,3 +2271,29 @@ int manager_reschedule_rebalance(Manager *m) { return 1; } + +int manager_get_home_by_name(Manager *m, const char *user_name, Home **ret) { + assert(m); + assert(user_name); + + Home *h = hashmap_get(m->homes_by_name, user_name); + if (!h) { + /* Also search by username and realm. For that simply chop off realm, then look for the home, and verify it afterwards. */ + const char *realm = strrchr(user_name, '@'); + if (realm) { + _cleanup_free_ char *prefix = strndup(user_name, realm - user_name); + if (!prefix) + return -ENOMEM; + + Home *j; + j = hashmap_get(m->homes_by_name, prefix); + if (j && user_record_matches_user_name(j->record, user_name)) + h = j; + } + } + + if (ret) + *ret = h; + + return !!h; +} diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h index eadb0b155d..9fea621031 100644 --- a/src/home/homed-manager.h +++ b/src/home/homed-manager.h @@ -91,3 +91,5 @@ int manager_acquire_key_pair(Manager *m); int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error); int bus_manager_emit_auto_login_changed(Manager *m); + +int manager_get_home_by_name(Manager *m, const char *user_name, Home **ret); diff --git a/src/home/homed-varlink.c b/src/home/homed-varlink.c index f6dd27594f..ef30ea7eaf 100644 --- a/src/home/homed-varlink.c +++ b/src/home/homed-varlink.c @@ -62,7 +62,7 @@ static bool home_user_match_lookup_parameters(LookupParameters *p, Home *h) { assert(p); assert(h); - if (p->user_name && !streq(p->user_name, h->user_name)) + if (p->user_name && !user_record_matches_user_name(h->record, p->user_name)) return false; if (uid_is_valid(p->uid) && h->uid != p->uid) @@ -100,15 +100,17 @@ int vl_method_get_user_record(sd_varlink *link, sd_json_variant *parameters, sd_ if (uid_is_valid(p.uid)) h = hashmap_get(m->homes_by_uid, UID_TO_PTR(p.uid)); - else if (p.user_name) - h = hashmap_get(m->homes_by_name, p.user_name); - else { + else if (p.user_name) { + r = manager_get_home_by_name(m, p.user_name, &h); + if (r < 0) + return r; + } else { /* If neither UID nor name was specified, then dump all homes. Do so with varlink_notify() * for all entries but the last, so that clients can stream the results, and easily process * them piecemeal. */ - HASHMAP_FOREACH(h, m->homes_by_name) { + HASHMAP_FOREACH(h, m->homes_by_uid) { if (!home_user_match_lookup_parameters(&p, h)) continue; @@ -175,7 +177,7 @@ static bool home_group_match_lookup_parameters(LookupParameters *p, Home *h) { assert(p); assert(h); - if (p->group_name && !streq(h->user_name, p->group_name)) + if (p->group_name && !user_record_matches_user_name(h->record, p->group_name)) return false; if (gid_is_valid(p->gid) && h->uid != (uid_t) p->gid) @@ -212,11 +214,13 @@ int vl_method_get_group_record(sd_varlink *link, sd_json_variant *parameters, sd if (gid_is_valid(p.gid)) h = hashmap_get(m->homes_by_uid, UID_TO_PTR((uid_t) p.gid)); - else if (p.group_name) - h = hashmap_get(m->homes_by_name, p.group_name); - else { + else if (p.group_name) { + r = manager_get_home_by_name(m, p.group_name, &h); + if (r < 0) + return r; + } else { - HASHMAP_FOREACH(h, m->homes_by_name) { + HASHMAP_FOREACH(h, m->homes_by_uid) { if (!home_group_match_lookup_parameters(&p, h)) continue; @@ -279,7 +283,9 @@ int vl_method_get_memberships(sd_varlink *link, sd_json_variant *parameters, sd_ if (p.user_name) { const char *last = NULL; - h = hashmap_get(m->homes_by_name, p.user_name); + r = manager_get_home_by_name(m, p.user_name, &h); + if (r < 0) + return r; if (!h) return sd_varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); @@ -315,7 +321,7 @@ int vl_method_get_memberships(sd_varlink *link, sd_json_variant *parameters, sd_ } else if (p.group_name) { const char *last = NULL; - HASHMAP_FOREACH(h, m->homes_by_name) { + HASHMAP_FOREACH(h, m->homes_by_uid) { if (!strv_contains(h->record->member_of, p.group_name)) continue; @@ -340,7 +346,7 @@ int vl_method_get_memberships(sd_varlink *link, sd_json_variant *parameters, sd_ } else { const char *last_user_name = NULL, *last_group_name = NULL; - HASHMAP_FOREACH(h, m->homes_by_name) + HASHMAP_FOREACH(h, m->homes_by_uid) STRV_FOREACH(j, h->record->member_of) { if (last_user_name) { diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index 624f1ced88..95f719d912 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -102,11 +102,6 @@ static int acquire_user_record( UserRecord **ret_record, PamBusData **bus_data) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; - _cleanup_(user_record_unrefp) UserRecord *ur = NULL; - _cleanup_free_ char *homed_field = NULL; - const char *json = NULL; int r; assert(handle); @@ -115,7 +110,6 @@ static int acquire_user_record( r = pam_get_user(handle, &username, NULL); if (r != PAM_SUCCESS) return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get user name: @PAMERR@"); - if (isempty(username)) return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "User name not set."); } @@ -125,13 +119,19 @@ static int acquire_user_record( if (STR_IN_SET(username, "root", NOBODY_USER_NAME) || !valid_user_group_name(username, 0)) return PAM_USER_UNKNOWN; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + const char *json = NULL; + bool fresh_data; + /* We cache the user record in the PAM context. We use a field name that includes the username, since * clients might change the user name associated with a PAM context underneath us. Notably, 'sudo' * creates a single PAM context and first authenticates it with the user set to the originating user, * then updates the user for the destination user and issues the session stack with the same PAM * context. We thus must be prepared that the user record changes between calls and we keep any * caching separate. */ - homed_field = strjoin("systemd-home-user-record-", username); + _cleanup_free_ char *homed_field = strjoin("systemd-home-user-record-", username); if (!homed_field) return pam_log_oom(handle); @@ -144,9 +144,10 @@ static int acquire_user_record( * negative cache indicator) */ if (json == POINTER_MAX) return PAM_USER_UNKNOWN; + + fresh_data = false; } else { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_free_ char *generic_field = NULL, *json_copy = NULL; _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; r = pam_acquire_bus_connection(handle, "pam-systemd-home", debug, &bus, bus_data); @@ -178,9 +179,42 @@ static int acquire_user_record( if (r < 0) return pam_bus_log_parse_error(handle, r); + fresh_data = true; + } + + r = sd_json_parse(json, /* flags= */ 0, &v, NULL, NULL); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to parse JSON user record: %m"); + + ur = user_record_new(); + if (!ur) + return pam_log_oom(handle); + + r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to load user record: %m"); + + /* Safety check if cached record actually matches what we are looking for */ + if (!user_record_matches_user_name(ur, username)) + return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, + "Acquired user record does not match user name."); + + /* Update the 'username' pointer to point to our own record now. The pam_set_item() call below is + * going to invalidate the old version after all */ + username = ur->user_name; + + /* We passed all checks. Let's now make sure the rest of the PAM stack continues with the primary, + * normalized name of the user record (i.e. not an alias or so). */ + r = pam_set_item(handle, PAM_USER, ur->user_name); + if (r != PAM_SUCCESS) + return pam_syslog_pam_error(handle, LOG_ERR, r, + "Failed to update username PAM item to '%s': @PAMERR@", ur->user_name); + + /* Everything seems to be good, let's cache this data now */ + if (fresh_data) { /* First copy: for the homed-specific data field, i.e. where we know the user record is from * homed */ - json_copy = strdup(json); + _cleanup_free_ char *json_copy = strdup(json); if (!json_copy) return pam_log_oom(handle); @@ -196,35 +230,18 @@ static int acquire_user_record( if (!json_copy) return pam_log_oom(handle); - generic_field = strjoin("systemd-user-record-", username); + _cleanup_free_ char *generic_field = strjoin("systemd-user-record-", username); if (!generic_field) return pam_log_oom(handle); r = pam_set_data(handle, generic_field, json_copy, pam_cleanup_free); if (r != PAM_SUCCESS) return pam_syslog_pam_error(handle, LOG_ERR, r, - "Failed to set PAM user record data '%s': @PAMERR@", homed_field); + "Failed to set PAM user record data '%s': @PAMERR@", generic_field); TAKE_PTR(json_copy); } - r = sd_json_parse(json, SD_JSON_PARSE_SENSITIVE, &v, NULL, NULL); - if (r < 0) - return pam_syslog_errno(handle, LOG_ERR, r, "Failed to parse JSON user record: %m"); - - ur = user_record_new(); - if (!ur) - return pam_log_oom(handle); - - r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE); - if (r < 0) - return pam_syslog_errno(handle, LOG_ERR, r, "Failed to load user record: %m"); - - /* Safety check if cached record actually matches what we are looking for */ - if (!streq_ptr(username, ur->user_name)) - return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, - "Acquired user record does not match user name."); - if (ret_record) *ret_record = TAKE_PTR(ur); @@ -535,7 +552,6 @@ static int acquire_home( r = pam_get_user(handle, &username, NULL); if (r != PAM_SUCCESS) return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get user name: @PAMERR@"); - if (isempty(username)) return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "User name not set."); @@ -879,7 +895,6 @@ _public_ PAM_EXTERN int pam_sm_close_session( r = pam_get_user(handle, &username, NULL); if (r != PAM_SUCCESS) return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get user name: @PAMERR@"); - if (isempty(username)) return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "User name not set."); @@ -949,7 +964,7 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt( if (r != PAM_SUCCESS) return r; - r = acquire_user_record(handle, NULL, debug, &ur, NULL); + r = acquire_user_record(handle, /* username= */ NULL, debug, &ur, /* bus_data= */ NULL); if (r != PAM_SUCCESS) return r; @@ -1057,7 +1072,7 @@ _public_ PAM_EXTERN int pam_sm_chauthtok( pam_debug_syslog(handle, debug, "pam-systemd-homed account management"); - r = acquire_user_record(handle, NULL, debug, &ur, NULL); + r = acquire_user_record(handle, /* username= */ NULL, debug, &ur, /* bus_data= */ NULL); if (r != PAM_SUCCESS) return r; diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 893d03cdb6..7a457c5ac6 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -177,26 +177,25 @@ static int acquire_user_record( pam_handle_t *handle, UserRecord **ret_record) { - _cleanup_(user_record_unrefp) UserRecord *ur = NULL; - const char *username = NULL, *json = NULL; - _cleanup_free_ char *field = NULL; int r; assert(handle); + const char *username = NULL; r = pam_get_user(handle, &username, NULL); if (r != PAM_SUCCESS) return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get user name: @PAMERR@"); - if (isempty(username)) return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "User name not valid."); /* If pam_systemd_homed (or some other module) already acquired the user record we can reuse it * here. */ - field = strjoin("systemd-user-record-", username); + _cleanup_free_ char *field = strjoin("systemd-user-record-", username); if (!field) return pam_log_oom(handle); + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + const char *json = NULL; r = pam_get_data(handle, field, (const void**) &json); if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get PAM user record data: @PAMERR@"); @@ -217,14 +216,14 @@ static int acquire_user_record( return pam_syslog_errno(handle, LOG_ERR, r, "Failed to load user record: %m"); /* Safety check if cached record actually matches what we are looking for */ - if (!streq_ptr(username, ur->user_name)) + if (!user_record_matches_user_name(ur, username)) return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "Acquired user record does not match user name."); } else { _cleanup_free_ char *formatted = NULL; /* Request the record ourselves */ - r = userdb_by_name(username, 0, &ur); + r = userdb_by_name(username, /* flags= */ 0, &ur); if (r < 0) { pam_syslog_errno(handle, LOG_ERR, r, "Failed to get user record: %m"); return PAM_USER_UNKNOWN; @@ -1468,12 +1467,12 @@ _public_ PAM_EXTERN int pam_sm_close_session( if (parse_argv(handle, argc, argv, - NULL, - NULL, - NULL, + /* class= */ NULL, + /* type= */ NULL, + /* deskop= */ NULL, &debug, - NULL, - NULL) < 0) + /* default_capability_bounding_set */ NULL, + /* default_capability_ambient_set= */ NULL) < 0) return PAM_SESSION_ERR; pam_debug_syslog(handle, debug, "pam-systemd shutting down"); diff --git a/src/shared/group-record.c b/src/shared/group-record.c index eea60af334..3aa2665718 100644 --- a/src/shared/group-record.c +++ b/src/shared/group-record.c @@ -331,6 +331,19 @@ int group_record_clone(GroupRecord *h, UserRecordLoadFlags flags, GroupRecord ** return 0; } +bool group_record_matches_group_name(const GroupRecord *g, const char *group_name) { + assert(g); + assert(group_name); + + if (streq_ptr(g->group_name, group_name)) + return true; + + if (streq_ptr(g->group_name_and_realm_auto, group_name)) + return true; + + return false; +} + int group_record_match(GroupRecord *h, const UserDBMatch *match) { assert(h); assert(match); diff --git a/src/shared/group-record.h b/src/shared/group-record.h index a2cef81c8a..5705fe2511 100644 --- a/src/shared/group-record.h +++ b/src/shared/group-record.h @@ -47,3 +47,5 @@ int group_record_match(GroupRecord *h, const UserDBMatch *match); const char* group_record_group_name_and_realm(GroupRecord *h); UserDisposition group_record_disposition(GroupRecord *h); + +bool group_record_matches_group_name(const GroupRecord *g, const char *groupname); diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c index a828ffa0a7..acff25d071 100644 --- a/src/shared/user-record-show.c +++ b/src/shared/user-record-show.c @@ -65,6 +65,13 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { printf(" User name: %s\n", user_record_user_name_and_realm(hr)); + if (!strv_isempty(hr->aliases)) { + STRV_FOREACH(i, hr->aliases) + printf(i == hr->aliases ? + " Alias: %s" : ", %s", *i); + putchar('\n'); + } + if (hr->state) { const char *color; diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 88970425cc..9feac30933 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -140,6 +140,7 @@ static UserRecord* user_record_free(UserRecord *h) { free(h->user_name); free(h->realm); free(h->user_name_and_realm_auto); + strv_free(h->aliases); free(h->real_name); free(h->email_address); erase_and_free(h->password_hint); @@ -1538,6 +1539,7 @@ int user_record_load(UserRecord *h, sd_json_variant *v, UserRecordLoadFlags load static const sd_json_dispatch_field user_dispatch_table[] = { { "userName", SD_JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), SD_JSON_RELAX }, + { "aliases", SD_JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, aliases), SD_JSON_RELAX }, { "realm", SD_JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 0 }, { "blobDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), SD_JSON_STRICT }, { "blobManifest", SD_JSON_VARIANT_OBJECT, dispatch_blob_manifest, offsetof(UserRecord, blob_manifest), 0 }, @@ -2625,6 +2627,28 @@ int user_record_is_nobody(const UserRecord *u) { return u->uid == UID_NOBODY || STRPTR_IN_SET(u->user_name, NOBODY_USER_NAME, "nobody"); } +bool user_record_matches_user_name(const UserRecord *u, const char *user_name) { + assert(u); + assert(user_name); + + if (streq_ptr(u->user_name, user_name)) + return true; + + if (streq_ptr(u->user_name_and_realm_auto, user_name)) + return true; + + if (strv_contains(u->aliases, user_name)) + return true; + + const char *realm = strrchr(user_name, '@'); + if (realm && streq_ptr(realm+1, u->realm)) + STRV_FOREACH(a, u->aliases) + if (startswith(user_name, *a) == realm) + return true; + + return false; +} + int suitable_blob_filename(const char *name) { /* Enforces filename requirements as described in docs/USER_RECORD_BULK_DIRS.md */ return filename_is_valid(name) && @@ -2691,7 +2715,8 @@ int user_record_match(UserRecord *u, const UserDBMatch *match) { u->cifs_user_name, }; - if (!user_name_fuzzy_match(names, ELEMENTSOF(names), match->fuzzy_names)) + if (!user_name_fuzzy_match(names, ELEMENTSOF(names), match->fuzzy_names) && + !user_name_fuzzy_match((const char**) u->aliases, strv_length(u->aliases), match->fuzzy_names)) return false; } diff --git a/src/shared/user-record.h b/src/shared/user-record.h index d3decdb5c1..d80a46130a 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -239,6 +239,7 @@ typedef struct UserRecord { char *user_name; char *realm; char *user_name_and_realm_auto; /* the user_name field concatenated with '@' and the realm, if the latter is defined */ + char **aliases; char *real_name; char *email_address; char *password_hint; @@ -490,6 +491,8 @@ typedef struct UserDBMatch { bool user_name_fuzzy_match(const char *names[], size_t n_names, char **matches); int user_record_match(UserRecord *u, const UserDBMatch *match); +bool user_record_matches_user_name(const UserRecord *u, const char *username); + const char* user_storage_to_string(UserStorage t) _const_; UserStorage user_storage_from_string(const char *s) _pure_; diff --git a/src/shared/userdb-dropin.c b/src/shared/userdb-dropin.c index 9f027d7783..81fd5f3ebc 100644 --- a/src/shared/userdb-dropin.c +++ b/src/shared/userdb-dropin.c @@ -4,6 +4,7 @@ #include "fd-util.h" #include "fileio.h" #include "format-util.h" +#include "group-record.h" #include "path-util.h" #include "stdio-util.h" #include "user-util.h" @@ -87,7 +88,7 @@ static int load_user( if (r < 0) return r; - if (name && !streq_ptr(name, u->user_name)) + if (name && !user_record_matches_user_name(u, name)) return -EINVAL; if (uid_is_valid(uid) && uid != u->uid) @@ -231,7 +232,7 @@ static int load_group( if (r < 0) return r; - if (name && !streq_ptr(name, g->group_name)) + if (name && !group_record_matches_group_name(g, name)) return -EINVAL; if (gid_is_valid(gid) && gid != g->gid) diff --git a/src/test/test-strv.c b/src/test/test-strv.c index d641043c50..b1d30d73a5 100644 --- a/src/test/test-strv.c +++ b/src/test/test-strv.c @@ -1255,4 +1255,26 @@ TEST(strv_find_closest) { ASSERT_NULL(strv_find_closest(l, "sfajosajfosdjaofjdsaf")); } +TEST(strv_equal_ignore_order) { + + ASSERT_TRUE(strv_equal_ignore_order(NULL, NULL)); + ASSERT_TRUE(strv_equal_ignore_order(NULL, STRV_MAKE(NULL))); + ASSERT_TRUE(strv_equal_ignore_order(STRV_MAKE(NULL), NULL)); + ASSERT_TRUE(strv_equal_ignore_order(STRV_MAKE(NULL), STRV_MAKE(NULL))); + + ASSERT_FALSE(strv_equal_ignore_order(STRV_MAKE("foo"), NULL)); + ASSERT_FALSE(strv_equal_ignore_order(STRV_MAKE("foo"), STRV_MAKE(NULL))); + ASSERT_FALSE(strv_equal_ignore_order(NULL, STRV_MAKE("foo"))); + ASSERT_FALSE(strv_equal_ignore_order(STRV_MAKE(NULL), STRV_MAKE("foo"))); + ASSERT_TRUE(strv_equal_ignore_order(STRV_MAKE("foo"), STRV_MAKE("foo"))); + ASSERT_FALSE(strv_equal_ignore_order(STRV_MAKE("foo"), STRV_MAKE("foo", "bar"))); + ASSERT_FALSE(strv_equal_ignore_order(STRV_MAKE("foo", "bar"), STRV_MAKE("foo"))); + ASSERT_TRUE(strv_equal_ignore_order(STRV_MAKE("foo", "bar"), STRV_MAKE("foo", "bar"))); + ASSERT_TRUE(strv_equal_ignore_order(STRV_MAKE("bar", "foo"), STRV_MAKE("foo", "bar"))); + ASSERT_FALSE(strv_equal_ignore_order(STRV_MAKE("bar", "foo"), STRV_MAKE("foo", "bar", "quux"))); + ASSERT_FALSE(strv_equal_ignore_order(STRV_MAKE("bar", "foo", "quux"), STRV_MAKE("foo", "bar"))); + ASSERT_TRUE(strv_equal_ignore_order(STRV_MAKE("bar", "foo", "quux"), STRV_MAKE("quux", "foo", "bar"))); + ASSERT_TRUE(strv_equal_ignore_order(STRV_MAKE("bar", "foo"), STRV_MAKE("bar", "foo", "bar", "foo", "foo"))); +} + DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c index 1e36face40..dce60e2ebd 100644 --- a/src/userdb/userwork.c +++ b/src/userdb/userwork.c @@ -215,7 +215,7 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete } if ((uid_is_valid(p.uid) && hr->uid != p.uid) || - (p.user_name && !streq(hr->user_name, p.user_name))) + (p.user_name && !user_record_matches_user_name(hr, p.user_name))) return sd_varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); r = build_user_json(link, hr, &v); @@ -345,7 +345,7 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet } if ((uid_is_valid(p.gid) && g->gid != p.gid) || - (p.group_name && !streq(g->group_name, p.group_name))) + (p.group_name && !group_record_matches_group_name(g, p.group_name))) return sd_varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); r = build_group_json(link, g, &v); diff --git a/test/units/TEST-46-HOMED.sh b/test/units/TEST-46-HOMED.sh index e858b0c297..8de170a1c9 100755 --- a/test/units/TEST-46-HOMED.sh +++ b/test/units/TEST-46-HOMED.sh @@ -629,6 +629,29 @@ EOF wait_for_state homedsshtest inactive fi +NEWPASSWORD=hunter4711 homectl create aliastest --storage=directory --alias=aliastest2 --alias=aliastest3 --realm=myrealm + +homectl inspect aliastest +homectl inspect aliastest2 +homectl inspect aliastest3 +homectl inspect aliastest@myrealm +homectl inspect aliastest2@myrealm +homectl inspect aliastest3@myrealm + +userdbctl user aliastest +userdbctl user aliastest2 +userdbctl user aliastest3 +userdbctl user aliastest@myrealm +userdbctl user aliastest2@myrealm +userdbctl user aliastest3@myrealm + +getent passwd aliastest +getent passwd aliastest2 +getent passwd aliastest3 +getent passwd aliastest@myrealm +getent passwd aliastest2@myrealm +getent passwd aliastest3@myrealm + systemd-analyze log-level info touch /testok