]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
user-record-nss: preserve valid nss lookup aliases in records
authorArseniy Kostevich <faux@altlinux.org>
Wed, 3 Jun 2026 09:17:18 +0000 (12:17 +0300)
committerArseniy Kostevich <faux@altlinux.org>
Mon, 15 Jun 2026 13:54:35 +0000 (16:54 +0300)
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.

src/shared/group-record.c
src/shared/group-record.h
src/shared/user-record-nss.c
src/shared/user-record-nss.h
src/shared/user-record.c
src/shared/user-record.h
src/shared/userdb.c
src/userdb/userwork.c

index 85433b15374ef1ecc8eef2c6c2f3ab40cbdff3ee..361030f5a2719517268a29f8a534a759b6ab0670 100644 (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;
         }
 
index 04f3e079976530eba932b3acbf442bd76be4b5da..35968aa29fbb8434b64f9d389427ccf4768bf275 100644 (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;
index 96adbb21f1dcd98b118453e5d8e60c77092a6708..405ec2c4343de954b45bebe475ac4d5591149f67 100644 (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;
 
index 835b1d686baa42db6afaff46402512e8e32d951d..53681c317827fdfa5c42104d6f329da1272fb077 100644 (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);
index 50b30fb256037a3d6141273e8158b7447a10033c..3a005df08e5206217e25affa8f5e94d21871e024 100644 (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;
 }
index 8a20a5535dbc4e596b6d8a5dab69f84e1f3d0a42..aee78694a90fa74e60a111f8d442678cd6119a9a 100644 (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);
 
index 9891e0e7333920225732f15cdb52b60286f66000..14848150c5b47d622e95e8d46bc8e03313ab5c58 100644 (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;
 
index aa77cde86b353fa5e183f0031d81e33a59c086fe..fa06a49b260b4692a45e0212ab0781a65919c21c 100644 (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);