user-util: rework get_user_creds()

Let's fold get_user_creds_clean() into get_user_creds(), and introduce a
flags argument for it to select "clean" behaviour. This flags parameter
also learns to other new flags:

- USER_CREDS_SYNTHESIZE_FALLBACK: in this mode the user records for
  root/nobody are only synthesized as fallback. Normally, the synthesized
  records take precedence over what is in the user database.  With this
  flag set this is reversed, and the user database takes precedence, and
  the synthesized records are only used if they are missing there. This
  flag should be set in cases where doing NSS is deemed safe, and where
  there's interest in knowing the correct shell, for example if the
  admin changed root's shell to zsh or suchlike.

- USER_CREDS_ALLOW_MISSING: if set, and a UID/GID is specified by
  numeric value, and there's no user/group record for it accept it
  anyway. This allows us to fix #9767

This then also ports all users to set the most appropriate flags.

Fixes: #9767

[zj: remove one isempty() call]
This commit is contained in:
Lennart Poettering 2018-08-02 18:36:47 +02:00 committed by Zbigniew Jędrzejewski-Szmek
parent 163a035aa6
commit fafff8f1ff
21 changed files with 146 additions and 106 deletions

View File

@ -87,17 +87,31 @@ char *getusername_malloc(void) {
return uid_to_name(getuid());
}
int get_user_creds(
static inline bool is_nologin_shell(const char *shell) {
return PATH_IN_SET(shell,
/* 'nologin' is the friendliest way to disable logins for a user account. It prints a nice
* message and exits. Different distributions place the binary at different places though,
* hence let's list them all. */
"/bin/nologin",
"/sbin/nologin",
"/usr/bin/nologin",
"/usr/sbin/nologin",
/* 'true' and 'false' work too for the same purpose, but are less friendly as they don't do
* any message printing. Different distributions place the binary at various places but at
* least not in the 'sbin' directory. */
"/bin/false",
"/usr/bin/false",
"/bin/true",
"/usr/bin/true");
}
static int synthesize_user_creds(
const char **username,
uid_t *uid, gid_t *gid,
const char **home,
const char **shell) {
struct passwd *p;
uid_t u;
assert(username);
assert(*username);
const char **shell,
UserCredsFlags flags) {
/* We enforce some special rules for uid=0 and uid=65534: in order to avoid NSS lookups for root we hardcode
* their user record data. */
@ -129,32 +143,85 @@ int get_user_creds(
*gid = GID_NOBODY;
if (home)
*home = "/";
*home = FLAGS_SET(flags, USER_CREDS_CLEAN) ? NULL : "/";
if (shell)
*shell = "/sbin/nologin";
*shell = FLAGS_SET(flags, USER_CREDS_CLEAN) ? NULL : "/sbin/nologin";
return 0;
}
return -ENOMEDIUM;
}
int get_user_creds(
const char **username,
uid_t *uid, gid_t *gid,
const char **home,
const char **shell,
UserCredsFlags flags) {
uid_t u = UID_INVALID;
struct passwd *p;
int r;
assert(username);
assert(*username);
if (!FLAGS_SET(flags, USER_CREDS_SYNTHESIZE_FALLBACK) ||
(!home && !shell)) {
/* So here's the deal: normally, we'll try to synthesize all records we can synthesize, and override
* the user database with that. However, if the user specifies USER_CREDS_SYNTHESIZE_FALLBACK then the
* user database will override the synthetic records instead except if the user is only interested in
* the UID and/or GID (but not the home directory, or the shell), in which case we'll always override
* the user database (i.e. the USER_CREDS_SYNTHESIZE_FALLBACK flag has no effect in this case). Why?
* Simply because there are valid usecase where the user might change the home directory or the shell
* of the relevant users, but changing the UID/GID mappings for them is something we explicitly don't
* support. */
r = synthesize_user_creds(username, uid, gid, home, shell, flags);
if (r >= 0)
return 0;
if (r != -ENOMEDIUM) /* not a username we can synthesize */
return r;
}
if (parse_uid(*username, &u) >= 0) {
errno = 0;
p = getpwuid(u);
/* If there are multiple users with the same id, make
* sure to leave $USER to the configured value instead
* of the first occurrence in the database. However if
* the uid was configured by a numeric uid, then let's
* pick the real username from /etc/passwd. */
/* If there are multiple users with the same id, make sure to leave $USER to the configured value
* instead of the first occurrence in the database. However if the uid was configured by a numeric uid,
* then let's pick the real username from /etc/passwd. */
if (p)
*username = p->pw_name;
else if (FLAGS_SET(flags, USER_CREDS_ALLOW_MISSING) && !gid && !home && !shell) {
/* If the specified user is a numeric UID and it isn't in the user database, and the caller
* passed USER_CREDS_ALLOW_MISSING and was only interested in the UID, then juts return that
* and don't complain. */
if (uid)
*uid = u;
return 0;
}
} else {
errno = 0;
p = getpwnam(*username);
}
if (!p) {
r = errno > 0 ? -errno : -ESRCH;
if (!p)
return errno > 0 ? -errno : -ESRCH;
/* If the user requested that we only synthesize as fallback, do so now */
if (FLAGS_SET(flags, USER_CREDS_SYNTHESIZE_FALLBACK)) {
if (synthesize_user_creds(username, uid, gid, home, shell, flags) >= 0)
return 0;
}
return r;
}
if (uid) {
if (!uid_is_valid(p->pw_uid))
@ -170,66 +237,30 @@ int get_user_creds(
*gid = p->pw_gid;
}
if (home)
*home = p->pw_dir;
if (home) {
if (FLAGS_SET(flags, USER_CREDS_CLEAN) && empty_or_root(p->pw_dir))
*home = NULL;
else
*home = p->pw_dir;
}
if (shell)
*shell = p->pw_shell;
if (shell) {
if (FLAGS_SET(flags, USER_CREDS_CLEAN) && (isempty(p->pw_shell) || is_nologin_shell(p->pw_shell)))
*shell = NULL;
else
*shell = p->pw_shell;
}
return 0;
}
static inline bool is_nologin_shell(const char *shell) {
return PATH_IN_SET(shell,
/* 'nologin' is the friendliest way to disable logins for a user account. It prints a nice
* message and exits. Different distributions place the binary at different places though,
* hence let's list them all. */
"/bin/nologin",
"/sbin/nologin",
"/usr/bin/nologin",
"/usr/sbin/nologin",
/* 'true' and 'false' work too for the same purpose, but are less friendly as they don't do
* any message printing. Different distributions place the binary at various places but at
* least not in the 'sbin' directory. */
"/bin/false",
"/usr/bin/false",
"/bin/true",
"/usr/bin/true");
}
int get_user_creds_clean(
const char **username,
uid_t *uid, gid_t *gid,
const char **home,
const char **shell) {
int r;
/* Like get_user_creds(), but resets home/shell to NULL if they don't contain anything relevant. */
r = get_user_creds(username, uid, gid, home, shell);
if (r < 0)
return r;
if (shell &&
(isempty(*shell) || is_nologin_shell(*shell)))
*shell = NULL;
if (home && empty_or_root(*home))
*home = NULL;
return 0;
}
int get_group_creds(const char **groupname, gid_t *gid) {
int get_group_creds(const char **groupname, gid_t *gid, UserCredsFlags flags) {
struct group *g;
gid_t id;
assert(groupname);
/* We enforce some special rules for gid=0: in order to avoid
* NSS lookups for root we hardcode its data. */
/* We enforce some special rules for gid=0: in order to avoid NSS lookups for root we hardcode its data. */
if (STR_IN_SET(*groupname, "root", "0")) {
*groupname = "root";
@ -256,6 +287,12 @@ int get_group_creds(const char **groupname, gid_t *gid) {
if (g)
*groupname = g->gr_name;
else if (FLAGS_SET(flags, USER_CREDS_ALLOW_MISSING)) {
if (gid)
*gid = id;
return 0;
}
} else {
errno = 0;
g = getgrnam(*groupname);
@ -391,7 +428,7 @@ int in_group(const char *name) {
int r;
gid_t gid;
r = get_group_creds(&name, &gid);
r = get_group_creds(&name, &gid, 0);
if (r < 0)
return r;

View File

@ -25,9 +25,14 @@ static inline int parse_gid(const char *s, gid_t *ret_gid) {
char* getlogname_malloc(void);
char* getusername_malloc(void);
int get_user_creds(const char **username, uid_t *uid, gid_t *gid, const char **home, const char **shell);
int get_user_creds_clean(const char **username, uid_t *uid, gid_t *gid, const char **home, const char **shell);
int get_group_creds(const char **groupname, gid_t *gid);
typedef enum UserCredsFlags {
USER_CREDS_SYNTHESIZE_FALLBACK = 1 << 0, /* if set, only synthesize user records if database lacks them. Normally we bypass the userdb entirely for the records we can synthesize */
USER_CREDS_ALLOW_MISSING = 1 << 1, /* if a numeric UID string is resolved, be OK if there's no record for it */
USER_CREDS_CLEAN = 1 << 2, /* try to clean up shell and home fields with invalid data */
} UserCredsFlags;
int get_user_creds(const char **username, uid_t *uid, gid_t *gid, const char **home, const char **shell, UserCredsFlags flags);
int get_group_creds(const char **groupname, gid_t *gid, UserCredsFlags flags);
char* uid_to_name(uid_t uid);
char* gid_to_name(gid_t gid);

View File

@ -921,7 +921,7 @@ static int get_fixed_user(const ExecContext *c, const char **user,
* (i.e. are "/" or "/bin/nologin"). */
name = c->user;
r = get_user_creds_clean(&name, uid, gid, home, shell);
r = get_user_creds(&name, uid, gid, home, shell, USER_CREDS_CLEAN);
if (r < 0)
return r;
@ -939,7 +939,7 @@ static int get_fixed_group(const ExecContext *c, const char **group, gid_t *gid)
return 0;
name = c->group;
r = get_group_creds(&name, gid);
r = get_group_creds(&name, gid, 0);
if (r < 0)
return r;
@ -1011,7 +1011,7 @@ static int get_supplementary_groups(const ExecContext *c, const char *user,
return -E2BIG;
g = *i;
r = get_group_creds(&g, l_gids+k);
r = get_group_creds(&g, l_gids+k, 0);
if (r < 0)
return r;

View File

@ -1934,7 +1934,7 @@ static int socket_chown(Socket *s, pid_t *_pid) {
if (!isempty(s->user)) {
const char *user = s->user;
r = get_user_creds(&user, &uid, &gid, NULL, NULL);
r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0);
if (r < 0) {
log_unit_error_errno(UNIT(s), r, "Failed to resolve user %s: %m", user);
_exit(EXIT_USER);
@ -1944,7 +1944,7 @@ static int socket_chown(Socket *s, pid_t *_pid) {
if (!isempty(s->group)) {
const char *group = s->group;
r = get_group_creds(&group, &gid);
r = get_group_creds(&group, &gid, 0);
if (r < 0) {
log_unit_error_errno(UNIT(s), r, "Failed to resolve group %s: %m", group);
_exit(EXIT_GROUP);

View File

@ -672,7 +672,7 @@ static int change_uid_gid(const char *context[]) {
if (uid <= SYSTEM_UID_MAX) {
const char *user = "systemd-coredump";
r = get_user_creds(&user, &uid, &gid, NULL, NULL);
r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0);
if (r < 0) {
log_warning_errno(r, "Cannot resolve %s user. Proceeding to dump core as root: %m", user);
uid = gid = 0;

View File

@ -912,7 +912,7 @@ static int show_user(int argc, char *argv[], void *userdata) {
const char *path = NULL;
uid_t uid;
r = get_user_creds((const char**) (argv+i), &uid, NULL, NULL, NULL);
r = get_user_creds((const char**) (argv+i), &uid, NULL, NULL, NULL, 0);
if (r < 0)
return log_error_errno(r, "Failed to look up user %s: %m", argv[i]);
@ -1108,7 +1108,7 @@ static int enable_linger(int argc, char *argv[], void *userdata) {
if (isempty(argv[i]))
uid = UID_INVALID;
else {
r = get_user_creds((const char**) (argv+i), &uid, NULL, NULL, NULL);
r = get_user_creds((const char**) (argv+i), &uid, NULL, NULL, NULL, 0);
if (r < 0)
return log_error_errno(r, "Failed to look up user %s: %m", argv[i]);
}
@ -1143,7 +1143,7 @@ static int terminate_user(int argc, char *argv[], void *userdata) {
for (i = 1; i < argc; i++) {
uid_t uid;
r = get_user_creds((const char**) (argv+i), &uid, NULL, NULL, NULL);
r = get_user_creds((const char**) (argv+i), &uid, NULL, NULL, NULL, 0);
if (r < 0)
return log_error_errno(r, "Failed to look up user %s: %m", argv[i]);
@ -1180,7 +1180,7 @@ static int kill_user(int argc, char *argv[], void *userdata) {
for (i = 1; i < argc; i++) {
uid_t uid;
r = get_user_creds((const char**) (argv+i), &uid, NULL, NULL, NULL);
r = get_user_creds((const char**) (argv+i), &uid, NULL, NULL, NULL, 0);
if (r < 0)
return log_error_errno(r, "Failed to look up user %s: %m", argv[i]);

View File

@ -152,7 +152,7 @@ int manager_add_user_by_name(Manager *m, const char *name, User **_user) {
assert(m);
assert(name);
r = get_user_creds(&name, &uid, &gid, NULL, NULL);
r = get_user_creds(&name, &uid, &gid, NULL, NULL, 0);
if (r < 0)
return r;

View File

@ -117,7 +117,7 @@ static int do_mount(const char *user) {
gid_t gid;
int r;
r = get_user_creds(&user, &uid, &gid, NULL, NULL);
r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0);
if (r < 0)
return log_error_errno(r,
r == -ESRCH ? "No such user \"%s\"" :
@ -141,7 +141,7 @@ static int do_umount(const char *user) {
/* The user may be already removed. So, first try to parse the string by parse_uid(),
* and if it fails, fallback to get_user_creds().*/
if (parse_uid(user, &uid) < 0) {
r = get_user_creds(&user, &uid, NULL, NULL, NULL);
r = get_user_creds(&user, &uid, NULL, NULL, NULL, 0);
if (r < 0)
return log_error_errno(r,
r == -ESRCH ? "No such user \"%s\"" :

View File

@ -212,7 +212,7 @@ static int parse_argv(int argc, char *argv[]) {
case ARG_OWNER: {
const char *user = optarg;
r = get_user_creds(&user, &arg_uid, &arg_gid, NULL, NULL);
r = get_user_creds(&user, &arg_uid, &arg_gid, NULL, NULL, 0);
if (r < 0)
return log_error_errno(r,
r == -EBADMSG ? "UID or GID of user %s are invalid."

View File

@ -75,10 +75,9 @@ static int netdev_tuntap_add(NetDev *netdev, struct ifreq *ifr) {
assert(t);
if (t->user_name) {
user = t->user_name;
r = get_user_creds(&user, &uid, NULL, NULL, NULL);
r = get_user_creds(&user, &uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING);
if (r < 0)
return log_netdev_error_errno(netdev, r, "Cannot resolve user name %s: %m", t->user_name);
@ -87,10 +86,9 @@ static int netdev_tuntap_add(NetDev *netdev, struct ifreq *ifr) {
}
if (t->group_name) {
group = t->group_name;
r = get_group_creds(&group, &gid);
r = get_group_creds(&group, &gid, USER_CREDS_ALLOW_MISSING);
if (r < 0)
return log_netdev_error_errno(netdev, r, "Cannot resolve group name %s: %m", t->group_name);

View File

@ -28,7 +28,7 @@ int main(int argc, char *argv[]) {
goto out;
}
r = get_user_creds(&user, &uid, &gid, NULL, NULL);
r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0);
if (r < 0) {
log_error_errno(r, "Cannot resolve user name %s: %m", user);
goto out;

View File

@ -103,7 +103,7 @@ static int parse_argv(int argc, char *argv[]) {
case ARG_UID: {
const char *u = optarg;
r = get_user_creds(&u, &arg_uid, &arg_gid, NULL, NULL);
r = get_user_creds(&u, &arg_uid, &arg_gid, NULL, NULL, 0);
if (r == -ESRCH) /* If the user doesn't exist, then accept it anyway as numeric */
r = parse_uid(u, &arg_uid);
if (r < 0)

View File

@ -37,7 +37,7 @@ int main(int argc, char *argv[]) {
goto finish;
}
r = get_user_creds(&user, &uid, &gid, NULL, NULL);
r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0);
if (r < 0) {
log_error_errno(r, "Cannot resolve user name %s: %m", user);
goto finish;

View File

@ -1232,7 +1232,7 @@ static int start_transient_scope(
if (arg_exec_group) {
gid_t gid;
r = get_group_creds(&arg_exec_group, &gid);
r = get_group_creds(&arg_exec_group, &gid, 0);
if (r < 0)
return log_error_errno(r, "Failed to resolve group %s: %m", arg_exec_group);
@ -1245,7 +1245,7 @@ static int start_transient_scope(
uid_t uid;
gid_t gid;
r = get_user_creds_clean(&arg_exec_user, &uid, &gid, &home, &shell);
r = get_user_creds(&arg_exec_user, &uid, &gid, &home, &shell, USER_CREDS_CLEAN|USER_CREDS_SYNTHESIZE_FALLBACK);
if (r < 0)
return log_error_errno(r, "Failed to resolve user %s: %m", arg_exec_user);

View File

@ -219,7 +219,7 @@ static int condition_test_user(Condition *c) {
return streq(c->parameter, "root");
u = c->parameter;
r = get_user_creds(&u, &id, NULL, NULL, NULL);
r = get_user_creds(&u, &id, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING);
if (r < 0)
return 0;

View File

@ -32,7 +32,7 @@ static void test_add_acls_for_user(void) {
if (getuid() == 0) {
const char *nobody = NOBODY_USER_NAME;
r = get_user_creds(&nobody, &uid, NULL, NULL, NULL);
r = get_user_creds(&nobody, &uid, NULL, NULL, NULL, 0);
if (r < 0)
uid = 0;
} else

View File

@ -9,7 +9,7 @@ int main(int argc, char *argv[]) {
int r;
const char* name = argv[1] ?: NOBODY_USER_NAME;
r = get_user_creds(&name, &uid, NULL, NULL, NULL);
r = get_user_creds(&name, &uid, NULL, NULL, NULL, 0);
if (r < 0) {
log_full_errno(r == -ESRCH ? LOG_NOTICE : LOG_ERR,
r, "Failed to resolve \"%s\": %m", name);

View File

@ -159,7 +159,7 @@ static void test_get_user_creds_one(const char *id, const char *name, uid_t uid,
log_info("/* %s(\"%s\", \"%s\", "UID_FMT", "GID_FMT", \"%s\", \"%s\") */",
__func__, id, name, uid, gid, home, shell);
r = get_user_creds(&id, &ruid, &rgid, &rhome, &rshell);
r = get_user_creds(&id, &ruid, &rgid, &rhome, &rshell, 0);
log_info_errno(r, "got \"%s\", "UID_FMT", "GID_FMT", \"%s\", \"%s\": %m",
id, ruid, rgid, strnull(rhome), strnull(rshell));
if (!synthesize_nobody() && streq(name, NOBODY_USER_NAME)) {
@ -180,7 +180,7 @@ static void test_get_group_creds_one(const char *id, const char *name, gid_t gid
log_info("/* %s(\"%s\", \"%s\", "GID_FMT") */", __func__, id, name, gid);
r = get_group_creds(&id, &rgid);
r = get_group_creds(&id, &rgid, 0);
log_info_errno(r, "got \"%s\", "GID_FMT": %m", id, rgid);
if (!synthesize_nobody() && streq(name, NOBODY_GROUP_NAME)) {
log_info("(skipping detailed tests because nobody is not synthesized)");

View File

@ -107,7 +107,7 @@ int main(int argc, char *argv[]) {
gid = getegid();
if (uid_current == 0) {
r = get_user_creds(&user, &uid, &gid, NULL, NULL);
r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0);
if (r < 0) {
log_error_errno(r, "Cannot resolve user name %s: %m", user);
goto finish;

View File

@ -2704,7 +2704,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer, bool
if (!isempty(user) && !streq(user, "-")) {
const char *u = user;
r = get_user_creds(&u, &i.uid, NULL, NULL, NULL);
r = get_user_creds(&u, &i.uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING);
if (r < 0) {
*invalid_config = true;
return log_error_errno(r, "[%s:%u] Unknown user '%s'.", fname, line, user);
@ -2716,7 +2716,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer, bool
if (!isempty(group) && !streq(group, "-")) {
const char *g = group;
r = get_group_creds(&g, &i.gid);
r = get_group_creds(&g, &i.gid, USER_CREDS_ALLOW_MISSING);
if (r < 0) {
*invalid_config = true;
log_error("[%s:%u] Unknown group '%s'.", fname, line, group);

View File

@ -481,7 +481,7 @@ static uid_t add_uid(struct udev_rules *rules, const char *owner) {
return uid;
}
}
r = get_user_creds(&owner, &uid, NULL, NULL, NULL);
r = get_user_creds(&owner, &uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING);
if (r < 0)
log_unknown_owner(r, "user", owner);
@ -524,7 +524,7 @@ static gid_t add_gid(struct udev_rules *rules, const char *group) {
return gid;
}
}
r = get_group_creds(&group, &gid);
r = get_group_creds(&group, &gid, USER_CREDS_ALLOW_MISSING);
if (r < 0)
log_unknown_owner(r, "group", group);
@ -2110,7 +2110,7 @@ void udev_rules_apply_to_event(struct udev_rules *rules,
event->owner_final = true;
udev_event_apply_format(event, rules_str(rules, cur->key.value_off), owner, sizeof(owner), false);
event->owner_set = true;
r = get_user_creds(&ow, &event->uid, NULL, NULL, NULL);
r = get_user_creds(&ow, &event->uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING);
if (r < 0) {
log_unknown_owner(r, "user", owner);
event->uid = 0;
@ -2131,7 +2131,7 @@ void udev_rules_apply_to_event(struct udev_rules *rules,
event->group_final = true;
udev_event_apply_format(event, rules_str(rules, cur->key.value_off), group, sizeof(group), false);
event->group_set = true;
r = get_group_creds(&gr, &event->gid);
r = get_group_creds(&gr, &event->gid, USER_CREDS_ALLOW_MISSING);
if (r < 0) {
log_unknown_owner(r, "group", group);
event->gid = 0;