From: Arseniy Kostevich Date: Wed, 3 Jun 2026 09:17:18 +0000 (+0300) Subject: user-record-nss: preserve valid nss lookup aliases in records X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=caf6d7b6b5f983dfbbc48ebce5b6f71861b485aa;p=thirdparty%2Fsystemd.git user-record-nss: preserve valid nss lookup aliases in records NSS lookups may resolve an alias-like name to a canonical user or group name. For example, SSSD can resolve a userPrincipalName such as "testuser@example.test" while returning a passwd record whose pw_name is the canonical login name "testuser". The NSS userdb worker currently converts such records using only the canonical name. As a result, the originally requested lookup name is lost, and lookups through the varlink/multiplexer path may fail with ConflictingRecordFound even though direct NSS lookup succeeds. Preserve the originally requested lookup name as an alias when it differs from the canonical passwd/group name and passes normal alias validation. Add the alias both to the parsed record fields and to the JSON representation so serialized records remain self-consistent. Extend group record loading and matching to understand aliases too. When both a name and UID/GID are specified in a userdb lookup, prefer the by-name path so alias-aware records can satisfy the subsequent consistency checks. This allows canonicalized NSS results to match the original lookup name correctly and fixes session setup for users logging in with NSS-resolvable aliases such as UPNs. --- diff --git a/src/shared/group-record.c b/src/shared/group-record.c index 85433b15374..361030f5a27 100644 --- a/src/shared/group-record.c +++ b/src/shared/group-record.c @@ -34,6 +34,7 @@ static GroupRecord *group_record_free(GroupRecord *g) { free(g->group_name); free(g->realm); free(g->group_name_and_realm_auto); + strv_free(g->aliases); free(g->description); strv_free(g->members); @@ -92,6 +93,7 @@ static int dispatch_per_machine(const char *name, sd_json_variant *variant, sd_j { "matchMachineId", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, { "matchHostname", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, { "gid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 }, + { "aliases", SD_JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, aliases), SD_JSON_RELAX }, { "members", SD_JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, members), SD_JSON_RELAX }, { "administrators", SD_JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), SD_JSON_RELAX }, {}, @@ -176,6 +178,7 @@ int group_record_load( static const sd_json_dispatch_field group_dispatch_table[] = { { "groupName", SD_JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(GroupRecord, group_name), SD_JSON_RELAX }, + { "aliases", SD_JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, aliases), SD_JSON_RELAX }, { "realm", SD_JSON_VARIANT_STRING, json_dispatch_realm, offsetof(GroupRecord, realm), 0 }, { "uuid", SD_JSON_VARIANT_STRING, sd_json_dispatch_id128, offsetof(GroupRecord, uuid), 0 }, { "description", SD_JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(GroupRecord, description), 0 }, @@ -345,6 +348,12 @@ bool group_record_matches_group_name(const GroupRecord *g, const char *group_nam if (streq_ptr(g->group_name_and_realm_auto, group_name)) return true; + if (strv_contains(g->aliases, group_name)) + return true; + + if (record_name_matches_alias_realm(group_name, (char * const*) g->aliases, g->realm)) + return true; + return false; } @@ -370,7 +379,8 @@ bool group_record_match(GroupRecord *h, const UserDBMatch *match) { h->description, }; - 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**) h->aliases, strv_length(h->aliases), match->fuzzy_names)) return false; } diff --git a/src/shared/group-record.h b/src/shared/group-record.h index 04f3e079976..35968aa29fb 100644 --- a/src/shared/group-record.h +++ b/src/shared/group-record.h @@ -12,6 +12,7 @@ typedef struct GroupRecord { char *group_name; char *realm; char *group_name_and_realm_auto; + char **aliases; sd_id128_t uuid; char *description; diff --git a/src/shared/user-record-nss.c b/src/shared/user-record-nss.c index 96adbb21f1d..405ec2c4343 100644 --- a/src/shared/user-record-nss.c +++ b/src/shared/user-record-nss.c @@ -8,6 +8,7 @@ #include "errno-util.h" #include "format-util.h" #include "group-record.h" +#include "json-util.h" #include "libcrypt-util.h" #include "log.h" #include "string-util.h" @@ -45,9 +46,23 @@ static int strv_extend_strv_utf8_only(char ***dst, char **src, bool filter_dupli return strv_extend_strv(dst, t, filter_duplicates); } +static int nss_add_valid_alias(char ***aliases, const char *name, const char *alias_name) { + assert(aliases); + assert(name); + + if (isempty(alias_name) || streq_ptr(alias_name, name)) + return 0; + + if (!valid_user_group_name(alias_name, VALID_USER_RELAX)) + return 0; + + return strv_extend(aliases, alias_name); +} + int nss_passwd_to_user_record( const struct passwd *pwd, const struct spwd *spwd, + const char *alias_name, UserRecord **ret) { _cleanup_(user_record_unrefp) UserRecord *hr = NULL; @@ -69,6 +84,10 @@ int nss_passwd_to_user_record( if (r < 0) return r; + r = nss_add_valid_alias(&hr->aliases, hr->user_name, alias_name); + if (r < 0) + return r; + /* Some bad NSS modules synthesize GECOS fields with embedded ":" or "\n" characters, which are not * something we can output in /etc/passwd compatible format, since these are record separators * there. We normally refuse that, but we need to maintain compatibility with arbitrary NSS modules, @@ -150,6 +169,7 @@ int nss_passwd_to_user_record( r = sd_json_buildo( &hr->json, SD_JSON_BUILD_PAIR_STRING("userName", hr->user_name), + JSON_BUILD_PAIR_STRV_NON_EMPTY("aliases", hr->aliases), SD_JSON_BUILD_PAIR_UNSIGNED("uid", hr->uid), SD_JSON_BUILD_PAIR_UNSIGNED("gid", user_record_gid(hr)), SD_JSON_BUILD_PAIR_CONDITION(!!hr->real_name, "realName", SD_JSON_BUILD_STRING(hr->real_name)), @@ -240,7 +260,7 @@ int nss_user_record_by_name( } else incomplete = true; - r = nss_passwd_to_user_record(result, sresult, ret); + r = nss_passwd_to_user_record(result, sresult, name, ret); if (r < 0) return r; @@ -274,7 +294,7 @@ int nss_user_record_by_uid( } else incomplete = true; - r = nss_passwd_to_user_record(result, sresult, ret); + r = nss_passwd_to_user_record(result, sresult, /* alias_name= */ NULL, ret); if (r < 0) return r; @@ -286,6 +306,7 @@ int nss_user_record_by_uid( int nss_group_to_group_record( const struct group *grp, const struct sgrp *sgrp, + const char *alias_name, GroupRecord **ret) { _cleanup_(group_record_unrefp) GroupRecord *g = NULL; @@ -307,6 +328,10 @@ int nss_group_to_group_record( if (!g->group_name) return -ENOMEM; + r = nss_add_valid_alias(&g->aliases, g->group_name, alias_name); + if (r < 0) + return r; + r = strv_extend_strv_utf8_only(&g->members, grp->gr_mem, false); if (r < 0) return r; @@ -332,6 +357,7 @@ int nss_group_to_group_record( r = sd_json_buildo( &g->json, SD_JSON_BUILD_PAIR_STRING("groupName", g->group_name), + JSON_BUILD_PAIR_STRV_NON_EMPTY("aliases", g->aliases), SD_JSON_BUILD_PAIR_UNSIGNED("gid", g->gid), SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->members), "members", SD_JSON_BUILD_STRV(g->members)), SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->hashed_password), "privileged", SD_JSON_BUILD_OBJECT(SD_JSON_BUILD_PAIR_STRV("hashedPassword", g->hashed_password))), @@ -412,7 +438,7 @@ int nss_group_record_by_name( } else incomplete = true; - r = nss_group_to_group_record(result, sresult, ret); + r = nss_group_to_group_record(result, sresult, name, ret); if (r < 0) return r; @@ -446,7 +472,7 @@ int nss_group_record_by_gid( } else incomplete = true; - r = nss_group_to_group_record(result, sresult, ret); + r = nss_group_to_group_record(result, sresult, /* alias_name= */ NULL, ret); if (r < 0) return r; diff --git a/src/shared/user-record-nss.h b/src/shared/user-record-nss.h index 835b1d686ba..53681c31782 100644 --- a/src/shared/user-record-nss.h +++ b/src/shared/user-record-nss.h @@ -5,13 +5,13 @@ /* Synthesize UserRecord and GroupRecord objects from NSS data */ -int nss_passwd_to_user_record(const struct passwd *pwd, const struct spwd *spwd, UserRecord **ret); +int nss_passwd_to_user_record(const struct passwd *pwd, const struct spwd *spwd, const char *alias_name, UserRecord **ret); int nss_spwd_for_passwd(const struct passwd *pwd, struct spwd *ret_spwd, char **ret_buffer); int nss_user_record_by_name(const char *name, bool with_shadow, UserRecord **ret); int nss_user_record_by_uid(uid_t uid, bool with_shadow, UserRecord **ret); -int nss_group_to_group_record(const struct group *grp, const struct sgrp *sgrp, GroupRecord **ret); +int nss_group_to_group_record(const struct group *grp, const struct sgrp *sgrp, const char *alias_name, GroupRecord **ret); int nss_sgrp_for_group(const struct group *grp, struct sgrp *ret_sgrp, char **ret_buffer); int nss_group_record_by_name(const char *name, bool with_shadow, GroupRecord **ret); diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 50b30fb2560..3a005df08e5 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -2775,11 +2775,24 @@ bool user_record_matches_user_name(const UserRecord *u, const char *user_name) { 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; + if (record_name_matches_alias_realm(user_name, (char * const*) u->aliases, u->realm)) + return true; + + return false; +} + +bool record_name_matches_alias_realm(const char *name, char * const *aliases, const char *realm) { + const char *suffix; + + assert(name); + + suffix = strrchr(name, '@'); + if (!suffix || !streq_ptr(suffix + 1, realm)) + return false; + + STRV_FOREACH(a, aliases) + if (startswith(name, *a) == suffix) + return true; return false; } diff --git a/src/shared/user-record.h b/src/shared/user-record.h index 8a20a5535db..aee78694a90 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -546,6 +546,7 @@ bool userdb_match_is_set(const UserDBMatch *match) _pure_; void userdb_match_done(UserDBMatch *match); +bool record_name_matches_alias_realm(const char *name, char * const *aliases, const char *realm); bool user_name_fuzzy_match(const char *names[], size_t n_names, char **matches); bool user_record_match(UserRecord *u, const UserDBMatch *match); diff --git a/src/shared/userdb.c b/src/shared/userdb.c index 9891e0e7333..14848150c5b 100644 --- a/src/shared/userdb.c +++ b/src/shared/userdb.c @@ -1124,7 +1124,7 @@ static int userdb_iterator_get_one(UserDBIterator *iterator, UserRecord **ret) { incomplete = true; } - r = nss_passwd_to_user_record(pw, r >= 0 ? &spwd : NULL, ret); + r = nss_passwd_to_user_record(pw, r >= 0 ? &spwd : NULL, /* alias_name= */ NULL, ret); if (r < 0) return r; @@ -1570,7 +1570,7 @@ static int groupdb_iterator_get_one(UserDBIterator *iterator, GroupRecord **ret) incomplete = true; } - r = nss_group_to_group_record(gr, r >= 0 ? &sgrp : NULL, ret); + r = nss_group_to_group_record(gr, r >= 0 ? &sgrp : NULL, /* alias_name= */ NULL, ret); if (r < 0) return r; diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c index aa77cde86b3..fa06a49b260 100644 --- a/src/userdb/userwork.c +++ b/src/userdb/userwork.c @@ -176,10 +176,12 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete if (r < 0) return r; - if (uid_is_valid(p.uid)) - r = userdb_by_uid(p.uid, &p.match, userdb_flags, &hr); - else if (p.name) + /* Prefer lookup by name if both name and UID are specified, so NSS-backed records preserve + * the requested lookup name as an alias before we check the UID below. */ + if (p.name) r = userdb_by_name(p.name, &p.match, userdb_flags, &hr); + else if (uid_is_valid(p.uid)) + r = userdb_by_uid(p.uid, &p.match, userdb_flags, &hr); else { _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; @@ -317,10 +319,12 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet if (r < 0) return r; - if (gid_is_valid(p.gid)) - r = groupdb_by_gid(p.gid, &p.match, userdb_flags, &g); - else if (p.name) + /* Prefer lookup by name if both name and GID are specified, so NSS-backed records preserve + * the requested lookup name as an alias before we check the GID below. */ + if (p.name) r = groupdb_by_name(p.name, &p.match, userdb_flags, &g); + else if (gid_is_valid(p.gid)) + r = groupdb_by_gid(p.gid, &p.match, userdb_flags, &g); else { _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; @@ -360,7 +364,7 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet return sd_varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL); } - if ((uid_is_valid(p.gid) && g->gid != p.gid) || + if ((gid_is_valid(p.gid) && g->gid != p.gid) || (p.name && !group_record_matches_group_name(g, p.name))) return sd_varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);