Accept NSS aliases for canonicalized user records (#42452)

This PR fixes userdb lookups for NSS users that are resolved through an
alias but returned with a canonical user name.

Some NSS providers, such as SSSD, can successfully resolve a user by an
alias-like name, for example a Kerberos/AD UPN (for example
testuser@example.test) while returning a passwd record with the
canonical login name.

The original lookup name was not preserved. Later, the userdb worker
checked whether the returned record matched the requested name with
user_record_matches_user_name(). Since the requested name was
testuser@example.test, but the record only contained testuser, the
lookup was rejected as:
`io.systemd.UserDatabase.ConflictingRecordFound`

This also caused pam_systemd to fail opening sessions for such users
with:
`pam_systemd(...:session): Failed to get user record`

In my case, this broke graphical logins for Samba users logging in with
a UPN on systems where SSSD canonicalizes the NSS result.

My solution preserves the requested name as a UserRecord alias when an
NSS lookup by name succeeds but the returned pw_name differs from the
requested name.
This allows user_record_matches_user_name() to accept the canonicalized
NSS result instead of treating it as a conflicting record.
The patch does not invent new aliases. It only records the name that NSS
itself already accepted and resolved successfully.

I'm not an expert on working with systemd, so I'm asking for a review of
my PR.
This commit is contained in:
Lennart Poettering
2026-06-19 22:14:03 +02:00
committed by GitHub
11 changed files with 288 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;
+2
View File
@@ -114,6 +114,7 @@ simple_tests += files(
'test-fstab-util.c',
'test-glob-util.c',
'test-gpt.c',
'test-group-record-nss.c',
'test-gunicode.c',
'test-hash-funcs.c',
'test-hexdecoct.c',
@@ -221,6 +222,7 @@ simple_tests += files(
'test-unaligned.c',
'test-unit-file.c',
'test-user-record.c',
'test-user-record-nss.c',
'test-user-util.c',
'test-utf8.c',
'test-verbs.c',
+93
View File
@@ -0,0 +1,93 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <grp.h>
#include <string.h>
#include "sd-json.h"
#include "group-record.h"
#include "strv.h"
#include "tests.h"
#include "user-record-nss.h"
TEST(nss_group_alias) {
static const struct group grp = {
.gr_name = (char*) "domain users",
.gr_gid = 1000,
};
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
sd_json_variant *aliases;
ASSERT_OK(nss_group_to_group_record(&grp, NULL, "domain users@example.test", &g));
ASSERT_TRUE(group_record_matches_group_name(g, "domain users@example.test"));
ASSERT_TRUE(strv_contains(g->aliases, "domain users@example.test"));
aliases = ASSERT_NOT_NULL(sd_json_variant_by_key(g->json, "aliases"));
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_index(aliases, 0)), "domain users@example.test");
}
TEST(nss_group_invalid_alias) {
static const struct group grp = {
.gr_name = (char*) "domain users",
.gr_gid = 1000,
};
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
ASSERT_OK(nss_group_to_group_record(&grp, NULL, "domain/users", &g));
ASSERT_FALSE(group_record_matches_group_name(g, "domain/users"));
ASSERT_TRUE(strv_isempty(g->aliases));
ASSERT_NULL(sd_json_variant_by_key(g->json, "aliases"));
}
TEST(nss_group_alias_realm) {
static const struct group grp = {
.gr_name = (char*) "domain users",
.gr_gid = 1000,
};
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
ASSERT_OK(nss_group_to_group_record(&grp, NULL, "domain-users", &g));
g->realm = ASSERT_NOT_NULL(strdup("example.test"));
ASSERT_TRUE(group_record_matches_group_name(g, "domain-users@example.test"));
}
TEST(nss_group_alias_same_as_canonical_noop) {
static const struct group grp = {
.gr_name = (char*) "domain users",
.gr_gid = 1000,
};
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
ASSERT_OK(nss_group_to_group_record(&grp, NULL, "domain users", &g));
ASSERT_TRUE(strv_isempty(g->aliases));
ASSERT_NULL(sd_json_variant_by_key(g->json, "aliases"));
}
TEST(nss_group_null_alias_noop) {
static const struct group grp = {
.gr_name = (char*) "domain users",
.gr_gid = 1000,
};
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
ASSERT_OK(nss_group_to_group_record(&grp, NULL, NULL, &g));
ASSERT_TRUE(strv_isempty(g->aliases));
ASSERT_NULL(sd_json_variant_by_key(g->json, "aliases"));
}
TEST(nss_group_alias_fuzzy_match) {
static const struct group grp = {
.gr_name = (char*) "canonical group",
.gr_gid = 1000,
};
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
UserDBMatch match = USERDB_MATCH_NULL;
ASSERT_OK(nss_group_to_group_record(&grp, NULL, "external-group", &g));
match.fuzzy_names = ASSERT_NOT_NULL(strv_new("ternal"));
ASSERT_TRUE(group_record_match(g, &match));
userdb_match_done(&match);
}
DEFINE_TEST_MAIN(LOG_INFO);
+117
View File
@@ -0,0 +1,117 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <pwd.h>
#include <string.h>
#include "sd-json.h"
#include "strv.h"
#include "tests.h"
#include "user-record.h"
#include "user-record-nss.h"
TEST(nss_user_alias) {
static const struct passwd pwd = {
.pw_name = (char*) "testuser",
.pw_uid = 1000,
.pw_gid = 1000,
.pw_gecos = (char*) "testuser",
.pw_dir = (char*) "/home/testuser",
.pw_shell = (char*) "/bin/bash",
};
_cleanup_(user_record_unrefp) UserRecord *u = NULL;
sd_json_variant *aliases;
ASSERT_OK(nss_passwd_to_user_record(&pwd, NULL, "testuser@example.test", &u));
ASSERT_TRUE(user_record_matches_user_name(u, "testuser@example.test"));
ASSERT_TRUE(strv_contains(u->aliases, "testuser@example.test"));
aliases = ASSERT_NOT_NULL(sd_json_variant_by_key(u->json, "aliases"));
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_index(aliases, 0)), "testuser@example.test");
}
TEST(nss_user_invalid_alias) {
static const struct passwd pwd = {
.pw_name = (char*) "testuser",
.pw_uid = 1000,
.pw_gid = 1000,
.pw_gecos = (char*) "testuser",
.pw_dir = (char*) "/home/testuser",
.pw_shell = (char*) "/bin/bash",
};
_cleanup_(user_record_unrefp) UserRecord *u = NULL;
ASSERT_OK(nss_passwd_to_user_record(&pwd, NULL, "testuser/bad", &u));
ASSERT_FALSE(user_record_matches_user_name(u, "testuser/bad"));
ASSERT_TRUE(strv_isempty(u->aliases));
ASSERT_NULL(sd_json_variant_by_key(u->json, "aliases"));
}
TEST(nss_user_alias_realm) {
static const struct passwd pwd = {
.pw_name = (char*) "testuser",
.pw_uid = 1000,
.pw_gid = 1000,
.pw_gecos = (char*) "testuser",
.pw_dir = (char*) "/home/testuser",
.pw_shell = (char*) "/bin/bash",
};
_cleanup_(user_record_unrefp) UserRecord *u = NULL;
ASSERT_OK(nss_passwd_to_user_record(&pwd, NULL, "testuser-short", &u));
u->realm = ASSERT_NOT_NULL(strdup("example.test"));
ASSERT_TRUE(user_record_matches_user_name(u, "testuser-short@example.test"));
}
TEST(nss_user_alias_same_as_canonical_noop) {
static const struct passwd pwd = {
.pw_name = (char*) "testuser",
.pw_uid = 1000,
.pw_gid = 1000,
.pw_gecos = (char*) "testuser",
.pw_dir = (char*) "/home/testuser",
.pw_shell = (char*) "/bin/bash",
};
_cleanup_(user_record_unrefp) UserRecord *u = NULL;
ASSERT_OK(nss_passwd_to_user_record(&pwd, NULL, "testuser", &u));
ASSERT_TRUE(strv_isempty(u->aliases));
ASSERT_NULL(sd_json_variant_by_key(u->json, "aliases"));
}
TEST(nss_user_null_alias_noop) {
static const struct passwd pwd = {
.pw_name = (char*) "testuser",
.pw_uid = 1000,
.pw_gid = 1000,
.pw_gecos = (char*) "testuser",
.pw_dir = (char*) "/home/testuser",
.pw_shell = (char*) "/bin/bash",
};
_cleanup_(user_record_unrefp) UserRecord *u = NULL;
ASSERT_OK(nss_passwd_to_user_record(&pwd, NULL, NULL, &u));
ASSERT_TRUE(strv_isempty(u->aliases));
ASSERT_NULL(sd_json_variant_by_key(u->json, "aliases"));
}
TEST(nss_user_alias_fuzzy_match) {
static const struct passwd pwd = {
.pw_name = (char*) "canonical",
.pw_uid = 1000,
.pw_gid = 1000,
.pw_gecos = (char*) "canonical",
.pw_dir = (char*) "/home/canonical",
.pw_shell = (char*) "/bin/bash",
};
_cleanup_(user_record_unrefp) UserRecord *u = NULL;
UserDBMatch match = USERDB_MATCH_NULL;
ASSERT_OK(nss_passwd_to_user_record(&pwd, NULL, "external-login", &u));
match.fuzzy_names = ASSERT_NOT_NULL(strv_new("ternal"));
ASSERT_TRUE(user_record_match(u, &match));
userdb_match_done(&match);
}
DEFINE_TEST_MAIN(LOG_INFO);
+11 -7
View File
@@ -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);