From 5072f4268b89a71e47e59c434da0222f722c7f0e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 20 Jan 2025 10:31:09 +0100 Subject: [PATCH 01/10] strv: add strv_equal_ignore_order() helper --- src/basic/strv.c | 20 ++++++++++++++++++++ src/basic/strv.h | 2 ++ src/test/test-strv.c | 22 ++++++++++++++++++++++ 3 files changed, 44 insertions(+) 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/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); From 30de5691744781277f992a25afa268518f3fe711 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Jan 2025 14:15:26 +0100 Subject: [PATCH 02/10] pam: minor coding style tweaks --- src/home/pam_systemd_home.c | 7 ++----- src/login/pam_systemd.c | 13 ++++++------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index 624f1ced88..0d28e99ba2 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -115,7 +115,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."); } @@ -535,7 +534,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 +877,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 +946,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 +1054,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..9a0ec294f0 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -187,7 +187,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 valid."); @@ -224,7 +223,7 @@ static int acquire_user_record( _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"); From 8aacf0fee1a8e9503bc071d5557293b0f3af50a4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 3 Jan 2025 17:53:33 +0100 Subject: [PATCH 03/10] user-record: add helper that checks if a provided user name matches a record This ensures that user names can be specified either in the regular short syntax or with a realm appended, and both are accepted. (The latter of course only if the record actually defines a realm) --- src/home/homed-varlink.c | 4 ++-- src/home/pam_systemd_home.c | 2 +- src/login/pam_systemd.c | 2 +- src/shared/group-record.c | 13 +++++++++++++ src/shared/group-record.h | 2 ++ src/shared/user-record.c | 13 +++++++++++++ src/shared/user-record.h | 2 ++ src/shared/userdb-dropin.c | 5 +++-- src/userdb/userwork.c | 4 ++-- 9 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/home/homed-varlink.c b/src/home/homed-varlink.c index f6dd27594f..cfd46ea51a 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) @@ -175,7 +175,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) diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index 0d28e99ba2..fb61105295 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -220,7 +220,7 @@ 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."); diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 9a0ec294f0..dc8c727035 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -216,7 +216,7 @@ 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 { 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.c b/src/shared/user-record.c index 88970425cc..e63736a742 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -2625,6 +2625,19 @@ 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; + + 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) && diff --git a/src/shared/user-record.h b/src/shared/user-record.h index d3decdb5c1..48b97ce28a 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -490,6 +490,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/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); From e2e1f38f5a9d442d0a027986024f4ea75ce97d2f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Jan 2025 14:15:52 +0100 Subject: [PATCH 04/10] user-record: add support for alias user names to user record --- docs/USER_RECORD.md | 8 ++++++++ src/shared/user-record-show.c | 7 +++++++ src/shared/user-record.c | 14 +++++++++++++- src/shared/user-record.h | 1 + 4 files changed, 29 insertions(+), 1 deletion(-) 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/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 e63736a742..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 }, @@ -2635,6 +2637,15 @@ bool user_record_matches_user_name(const UserRecord *u, const char *user_name) { 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; } @@ -2704,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 48b97ce28a..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; From 1fb53bb561db11e72bb695d578f8e94042565822 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Jan 2025 14:14:08 +0100 Subject: [PATCH 05/10] pam_systemd_home: use right field name in error message --- src/home/pam_systemd_home.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index fb61105295..794438129e 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -202,7 +202,7 @@ static int acquire_user_record( 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); } From a642f9d2d37945917bf4200a1095a43e6e7b6ea7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Jan 2025 14:15:03 +0100 Subject: [PATCH 06/10] pam_systemd_home: support login with alias names + user names with realms This in particular makes sure that we normalize the user name and update it in the PAM session, once we acquire it. This means that if you have a user with name "a" and alias "b", and the user logs in as "b" they end up properly with "a" as user name set, as intended by the PAM gods. Moreover, if you have a user "c" in a ralm "d", they may log in by specifying "c" or "c@d", with equivalent results. --- src/home/pam_systemd_home.c | 90 ++++++++++++++++++++++--------------- src/login/pam_systemd.c | 8 ++-- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index 794438129e..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); @@ -124,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); @@ -143,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); @@ -177,37 +179,10 @@ static int acquire_user_record( if (r < 0) return pam_bus_log_parse_error(handle, r); - /* First copy: for the homed-specific data field, i.e. where we know the user record is from - * homed */ - json_copy = strdup(json); - if (!json_copy) - return pam_log_oom(handle); - - r = pam_set_data(handle, homed_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); - - /* Take a second copy: for the generic data field, the one which we share with - * pam_systemd. While we insist on only reusing homed records, pam_systemd is fine with homed - * and non-homed user records. */ - json_copy = strdup(json); - if (!json_copy) - return pam_log_oom(handle); - - 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@", generic_field); - - TAKE_PTR(json_copy); + fresh_data = true; } - r = sd_json_parse(json, SD_JSON_PARSE_SENSITIVE, &v, NULL, NULL); + 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"); @@ -224,6 +199,49 @@ static int acquire_user_record( 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 */ + _cleanup_free_ char *json_copy = strdup(json); + if (!json_copy) + return pam_log_oom(handle); + + r = pam_set_data(handle, homed_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); + + /* Take a second copy: for the generic data field, the one which we share with + * pam_systemd. While we insist on only reusing homed records, pam_systemd is fine with homed + * and non-homed user records. */ + json_copy = strdup(json); + if (!json_copy) + return pam_log_oom(handle); + + _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@", generic_field); + + TAKE_PTR(json_copy); + } + if (ret_record) *ret_record = TAKE_PTR(ur); diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index dc8c727035..7a457c5ac6 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -177,13 +177,11 @@ 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@"); @@ -192,10 +190,12 @@ static int acquire_user_record( /* 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@"); From 40fd0e0423ed9e4ae47d7f1adba83c0487d7decd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Jan 2025 14:08:51 +0100 Subject: [PATCH 07/10] homed: support user record aliases --- src/home/homectl.c | 12 +----- src/home/homed-home-bus.c | 7 +++- src/home/homed-home.c | 28 ++++++++++++- src/home/homed-home.h | 5 +++ src/home/homed-manager-bus.c | 76 ++++++++++++++++++++++++++---------- src/home/homed-manager.c | 50 +++++++++++++++++++----- src/home/homed-manager.h | 2 + src/home/homed-varlink.c | 28 +++++++------ 8 files changed, 154 insertions(+), 54 deletions(-) diff --git a/src/home/homectl.c b/src/home/homectl.c index 104c35454b..afea68622c 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)); 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 cfd46ea51a..ef30ea7eaf 100644 --- a/src/home/homed-varlink.c +++ b/src/home/homed-varlink.c @@ -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; @@ -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) { From 5cd7b455e0b2ee5991ff06a885c8bc4fe78c2225 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Jan 2025 14:01:15 +0100 Subject: [PATCH 08/10] homectl: add support for creating users with alias names --- man/homectl.xml | 10 ++++++++++ src/home/homectl.c | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) 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/home/homectl.c b/src/home/homectl.c index afea68622c..b8b6aa3a86 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -2756,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" @@ -2900,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, @@ -2991,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 }, @@ -3146,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; From 853e9b754a16c58f9fb147376af941ec679e65a6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Jan 2025 15:18:45 +0100 Subject: [PATCH 09/10] test: add test for homed alias and realm user resolution --- test/units/TEST-46-HOMED.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) 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 From 3d3f27cd9a9d7ba325f0d6acc8526c86b938f09c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Jan 2025 13:58:14 +0100 Subject: [PATCH 10/10] update TODO --- TODO | 11 ----------- 1 file changed, 11 deletions(-) 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.