user-record/home: add concept of alias names for accounts (#36041)

This commit is contained in:
Lennart Poettering
2025-01-21 12:57:52 +01:00
committed by GitHub
24 changed files with 405 additions and 116 deletions
-11
View File
@@ -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.
+8
View File
@@ -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.
+10
View File
@@ -226,6 +226,16 @@
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--alias=<replaceable>NAME</replaceable><optional>,<replaceable>NAME…</replaceable></optional></option></term>
<listitem><para>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.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--email-address=<replaceable>EMAIL</replaceable></option></term>
+20
View File
@@ -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);
+2
View File
@@ -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)
+52 -10
View File
@@ -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;
+5 -2
View File
@@ -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;
+27 -1
View File
@@ -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;
+5
View File
@@ -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 */
+55 -21
View File
@@ -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);
+41 -9
View File
@@ -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;
}
+2
View File
@@ -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);
+19 -13
View File
@@ -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) {
+47 -32
View File
@@ -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;
+11 -12
View File
@@ -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");
+13
View File
@@ -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);
+2
View File
@@ -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);
+7
View File
@@ -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;
+26 -1
View File
@@ -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;
}
+3
View File
@@ -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_;
+3 -2
View File
@@ -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)
+22
View File
@@ -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);
+2 -2
View File
@@ -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);
+23
View File
@@ -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