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.
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/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/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/home/homectl.c b/src/home/homectl.c
index 104c35454b..b8b6aa3a86 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));
@@ -2764,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"
@@ -2908,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,
@@ -2999,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 },
@@ -3154,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;
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 f6dd27594f..ef30ea7eaf 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)
@@ -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;
@@ -175,7 +177,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)
@@ -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) {
diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c
index 624f1ced88..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);
@@ -115,7 +110,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.");
}
@@ -125,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);
@@ -144,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);
@@ -178,9 +179,42 @@ static int acquire_user_record(
if (r < 0)
return pam_bus_log_parse_error(handle, r);
+ fresh_data = true;
+ }
+
+ 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");
+
+ ur = user_record_new();
+ if (!ur)
+ return pam_log_oom(handle);
+
+ r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE);
+ if (r < 0)
+ 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 (!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.");
+
+ /* 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 */
- json_copy = strdup(json);
+ _cleanup_free_ char *json_copy = strdup(json);
if (!json_copy)
return pam_log_oom(handle);
@@ -196,35 +230,18 @@ static int acquire_user_record(
if (!json_copy)
return pam_log_oom(handle);
- generic_field = strjoin("systemd-user-record-", username);
+ _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@", homed_field);
+ "Failed to set PAM user record data '%s': @PAMERR@", generic_field);
TAKE_PTR(json_copy);
}
- r = sd_json_parse(json, SD_JSON_PARSE_SENSITIVE, &v, NULL, NULL);
- if (r < 0)
- return pam_syslog_errno(handle, LOG_ERR, r, "Failed to parse JSON user record: %m");
-
- ur = user_record_new();
- if (!ur)
- return pam_log_oom(handle);
-
- r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE);
- if (r < 0)
- 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))
- return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR,
- "Acquired user record does not match user name.");
-
if (ret_record)
*ret_record = TAKE_PTR(ur);
@@ -535,7 +552,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 +895,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 +964,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 +1072,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..7a457c5ac6 100644
--- a/src/login/pam_systemd.c
+++ b/src/login/pam_systemd.c
@@ -177,26 +177,25 @@ 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@");
-
if (isempty(username))
return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "User name not valid.");
/* 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@");
@@ -217,14 +216,14 @@ 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 {
_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");
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-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 88970425cc..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 },
@@ -2625,6 +2627,28 @@ 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;
+
+ 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;
+}
+
int suitable_blob_filename(const char *name) {
/* Enforces filename requirements as described in docs/USER_RECORD_BULK_DIRS.md */
return filename_is_valid(name) &&
@@ -2691,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 d3decdb5c1..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;
@@ -490,6 +491,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/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);
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);
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