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.
This commit is contained in:
Arseniy Kostevich
2026-06-15 16:54:35 +03:00
parent b85a12dfb2
commit caf6d7b6b5
8 changed files with 76 additions and 21 deletions
+11 -1
View File
@@ -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;
}
+1
View File
@@ -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;
+30 -4
View File
@@ -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;
+2 -2
View File
@@ -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);
+18 -5
View File
@@ -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;
}
+1
View File
@@ -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);
+2 -2
View File
@@ -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;