X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=elogind.git;a=blobdiff_plain;f=src%2Fsysusers%2Fsysusers.c;h=19568adf7a820581171b7ce723c8616ca175bafb;hp=53ff9509d85202df2c6354365b9efeac7af5f5c3;hb=e70bc43cdf75b36e7ad3d29e9a6f8ee1461e7d5e;hpb=1b99214789101976d6bbf75c351279584b071998;ds=sidebyside diff --git a/src/sysusers/sysusers.c b/src/sysusers/sysusers.c index 53ff9509d..19568adf7 100644 --- a/src/sysusers/sysusers.c +++ b/src/sysusers/sysusers.c @@ -24,6 +24,7 @@ #include #include #include +#include #include "util.h" #include "hashmap.h" @@ -34,10 +35,13 @@ #include "conf-files.h" #include "copy.h" #include "utf8.h" +#include "label.h" +#include "fileio-label.h" typedef enum ItemType { ADD_USER = 'u', ADD_GROUP = 'g', + ADD_MEMBER = 'm', } ItemType; typedef struct Item { ItemType type; @@ -53,12 +57,15 @@ typedef struct Item { bool gid_set:1; bool uid_set:1; - bool todo:1; + bool todo_user:1; + bool todo_group:1; } Item; static char *arg_root = NULL; static const char conf_file_dirs[] = + "/etc/sysusers.d\0" + "/run/sysusers.d\0" "/usr/local/lib/sysusers.d\0" "/usr/lib/sysusers.d\0" #ifdef HAVE_SPLIT_USR @@ -68,6 +75,7 @@ static const char conf_file_dirs[] = static Hashmap *users = NULL, *groups = NULL; static Hashmap *todo_uids = NULL, *todo_gids = NULL; +static Hashmap *members = NULL; static Hashmap *database_uid = NULL, *database_user = NULL; static Hashmap *database_gid = NULL, *database_group = NULL; @@ -187,8 +195,9 @@ static int load_group_database(void) { return 0; } -static int make_backup(const char *x) { - _cleanup_close_ int src = -1, dst = -1; +static int make_backup(const char *target, const char *x) { + _cleanup_close_ int src = -1; + _cleanup_fclose_ FILE *dst = NULL; char *backup, *temp; struct timespec ts[2]; struct stat st; @@ -205,30 +214,30 @@ static int make_backup(const char *x) { if (fstat(src, &st) < 0) return -errno; - temp = strappenda(x, ".XXXXXX"); - dst = mkostemp_safe(temp, O_WRONLY|O_CLOEXEC|O_NOCTTY); - if (dst < 0) - return dst; + r = fopen_temporary_label(target, x, &dst, &temp); + if (r < 0) + return r; - r = copy_bytes(src, dst); + r = copy_bytes(src, fileno(dst), (off_t) -1); if (r < 0) goto fail; + /* Don't fail on chmod() or chown(). If it stays owned by us + * and/or unreadable by others, then it isn't too bad... */ + + backup = strappenda(x, "-"); + /* Copy over the access mask */ - if (fchmod(dst, st.st_mode & 07777) < 0) { - r = -errno; - goto fail; - } + if (fchmod(fileno(dst), st.st_mode & 07777) < 0) + log_warning("Failed to change mode on %s: %m", backup); - /* Don't fail on chmod(). If it stays owned by us, then it - * isn't too bad... */ - fchown(dst, st.st_uid, st.st_gid); + if (fchown(fileno(dst), st.st_uid, st.st_gid)< 0) + log_warning("Failed to change ownership of %s: %m", backup); ts[0] = st.st_atim; ts[1] = st.st_mtim; - futimens(dst, ts); + futimens(fileno(dst), ts); - backup = strappenda(x, "-"); if (rename(temp, backup) < 0) goto fail; @@ -239,11 +248,62 @@ fail: return r; } +static int putgrent_with_members(const struct group *gr, FILE *group) { + char **a; + + assert(gr); + assert(group); + + a = hashmap_get(members, gr->gr_name); + if (a) { + _cleanup_strv_free_ char **l = NULL; + bool added = false; + char **i; + + l = strv_copy(gr->gr_mem); + if (!l) + return -ENOMEM; + + STRV_FOREACH(i, a) { + if (strv_find(l, *i)) + continue; + + if (strv_extend(&l, *i) < 0) + return -ENOMEM; + + added = true; + } + + if (added) { + struct group t; + + strv_uniq(l); + strv_sort(l); + + t = *gr; + t.gr_mem = l; + + errno = 0; + if (putgrent(&t, group) != 0) + return errno ? -errno : -EIO; + + return 1; + } + } + + errno = 0; + if (putgrent(gr, group) != 0) + return errno ? -errno : -EIO; + + return 0; +} + static int write_files(void) { _cleanup_fclose_ FILE *passwd = NULL, *group = NULL; _cleanup_free_ char *passwd_tmp = NULL, *group_tmp = NULL; const char *passwd_path = NULL, *group_path = NULL; + bool group_changed = false; Iterator iterator; Item *i; int r; @@ -251,11 +311,11 @@ static int write_files(void) { /* We don't patch /etc/shadow or /etc/gshadow here, since we * only create user accounts without passwords anyway. */ - if (hashmap_size(todo_gids) > 0) { + if (hashmap_size(todo_gids) > 0 || hashmap_size(members) > 0) { _cleanup_fclose_ FILE *original = NULL; group_path = fix_root("/etc/group"); - r = fopen_temporary(group_path, &group, &group_tmp); + r = fopen_temporary_label("/etc/group", group_path, &group, &group_tmp); if (r < 0) goto finish; @@ -279,7 +339,7 @@ static int write_files(void) { * duplicate entries. */ i = hashmap_get(groups, gr->gr_name); - if (i && i->todo) { + if (i && i->todo_group) { r = -EEXIST; goto finish; } @@ -289,10 +349,12 @@ static int write_files(void) { goto finish; } - if (putgrent(gr, group) < 0) { - r = -errno; + r = putgrent_with_members(gr, group); + if (r < 0) goto finish; - } + + if (r > 0) + group_changed = true; errno = 0; } @@ -313,10 +375,11 @@ static int write_files(void) { .gr_passwd = (char*) "x", }; - if (putgrent(&n, group) < 0) { - r = -errno; + r = putgrent_with_members(&n, group); + if (r < 0) goto finish; - } + + group_changed = true; } r = fflush_and_check(group); @@ -328,7 +391,7 @@ static int write_files(void) { _cleanup_fclose_ FILE *original = NULL; passwd_path = fix_root("/etc/passwd"); - r = fopen_temporary(passwd_path, &passwd, &passwd_tmp); + r = fopen_temporary_label("/etc/passwd", passwd_path, &passwd, &passwd_tmp); if (r < 0) goto finish; @@ -345,7 +408,7 @@ static int write_files(void) { while ((pw = fgetpwent(original))) { i = hashmap_get(users, pw->pw_name); - if (i && i->todo) { + if (i && i->todo_user) { r = -EEXIST; goto finish; } @@ -355,8 +418,9 @@ static int write_files(void) { goto finish; } + errno = 0; if (putpwent(pw, passwd) < 0) { - r = -errno; + r = errno ? -errno : -EIO; goto finish; } @@ -392,8 +456,9 @@ static int write_files(void) { n.pw_dir = (char*) "/"; } - if (putpwent(&n, passwd) < 0) { - r = -r; + errno = 0; + if (putpwent(&n, passwd) != 0) { + r = errno ? -errno : -EIO; goto finish; } } @@ -404,20 +469,20 @@ static int write_files(void) { } /* Make a backup of the old files */ - if (group) { - r = make_backup(group_path); + if (group && group_changed) { + r = make_backup("/etc/group", group_path); if (r < 0) goto finish; } if (passwd) { - r = make_backup(passwd_path); + r = make_backup("/etc/passwd", passwd_path); if (r < 0) goto finish; } /* And make the new files count */ - if (group) { + if (group && group_changed) { if (rename(group_tmp, group_path) < 0) { r = -errno; goto finish; @@ -437,15 +502,13 @@ static int write_files(void) { passwd_tmp = NULL; } - return 0; + r = 0; finish: - if (r < 0) { - if (passwd_tmp) - unlink(passwd_tmp); - if (group_tmp) - unlink(group_tmp); - } + if (passwd_tmp) + unlink(passwd_tmp); + if (group_tmp) + unlink(group_tmp); return r; } @@ -480,7 +543,7 @@ static int uid_is_ok(uid_t uid, const char *name) { p = getpwuid(uid); if (p) return 0; - if (errno != 0) + if (!IN_SET(errno, 0, ENOENT)) return -errno; errno = 0; @@ -488,7 +551,7 @@ static int uid_is_ok(uid_t uid, const char *name) { if (g) { if (!streq(g->gr_name, name)) return 0; - } else if (errno != 0) + } else if (!IN_SET(errno, 0, ENOENT)) return -errno; } @@ -573,7 +636,7 @@ static int add_user(Item *i) { z = hashmap_get(database_user, i->name); if (z) { log_debug("User %s already exists.", i->name); - i->uid = PTR_TO_GID(z); + i->uid = PTR_TO_UID(z); i->uid_set = true; return 0; } @@ -594,7 +657,7 @@ static int add_user(Item *i) { i->description = strdup(p->pw_gecos); return 0; } - if (errno != 0) { + if (!IN_SET(errno, 0, ENOENT)) { log_error("Failed to check if user %s already exists: %m", i->name); return -errno; } @@ -606,7 +669,7 @@ static int add_user(Item *i) { log_error("User %s already exists in shadow database, but not in user database.", i->name); return -EBADMSG; } - if (errno != 0) { + if (!IN_SET(errno, 0, ENOENT)) { log_error("Failed to check if user %s already exists in shadow database: %m", i->name); return -errno; } @@ -691,7 +754,7 @@ static int add_user(Item *i) { if (r < 0) return log_oom(); - i->todo = true; + i->todo_user = true; log_info("Creating user %s (%s) with uid " UID_FMT " and gid " GID_FMT ".", i->name, strna(i->description), i->uid, i->gid); return 0; @@ -719,14 +782,14 @@ static int gid_is_ok(gid_t gid) { g = getgrgid(gid); if (g) return 0; - if (errno != 0) + if (!IN_SET(errno, 0, ENOENT)) return -errno; errno = 0; p = getpwuid((uid_t) gid); if (p) return 0; - if (errno != 0) + if (!IN_SET(errno, 0, ENOENT)) return -errno; } @@ -760,7 +823,7 @@ static int add_group(Item *i) { i->gid_set = true; return 0; } - if (errno != 0) { + if (!IN_SET(errno, 0, ENOENT)) { log_error("Failed to check if group %s already exists: %m", i->name); return -errno; } @@ -844,7 +907,7 @@ static int add_group(Item *i) { if (r < 0) return log_oom(); - i->todo = true; + i->todo_group = true; log_info("Creating group %s with gid " GID_FMT ".", i->name, i->gid); return 0; @@ -889,9 +952,10 @@ static int process_item(Item *i) { return add_group(i); } - } - assert_not_reached("Unknown item type"); + default: + assert_not_reached("Unknown item type"); + } } static void item_free(Item *i) { @@ -908,6 +972,74 @@ static void item_free(Item *i) { DEFINE_TRIVIAL_CLEANUP_FUNC(Item*, item_free); +static int add_implicit(void) { + char *g, **l; + Iterator iterator; + int r; + + /* Implicitly create additional users and groups, if they were listed in "m" lines */ + + HASHMAP_FOREACH_KEY(l, g, members, iterator) { + Item *i; + char **m; + + i = hashmap_get(groups, g); + if (!i) { + _cleanup_(item_freep) Item *j = NULL; + + r = hashmap_ensure_allocated(&groups, string_hash_func, string_compare_func); + if (r < 0) + return log_oom(); + + j = new0(Item, 1); + if (!j) + return log_oom(); + + j->type = ADD_GROUP; + j->name = strdup(g); + if (!j->name) + return log_oom(); + + r = hashmap_put(groups, j->name, j); + if (r < 0) + return log_oom(); + + log_debug("Adding implicit group '%s' due to m line", j->name); + j = NULL; + } + + STRV_FOREACH(m, l) { + + i = hashmap_get(users, *m); + if (!i) { + _cleanup_(item_freep) Item *j = NULL; + + r = hashmap_ensure_allocated(&users, string_hash_func, string_compare_func); + if (r < 0) + return log_oom(); + + j = new0(Item, 1); + if (!j) + return log_oom(); + + j->type = ADD_USER; + j->name = strdup(*m); + if (!j->name) + return log_oom(); + + r = hashmap_put(users, j->name, j); + if (r < 0) + return log_oom(); + + log_debug("Adding implicit user '%s' due to m line", j->name); + j = NULL; + } + } + } + + return 0; +} + static bool item_equal(Item *a, Item *b) { assert(a); assert(b); @@ -969,6 +1101,9 @@ static bool valid_user_group_name(const char *u) { if ((size_t) (i-u) > (size_t) sz) return false; + if ((size_t) (i-u) > UT_NAMESIZE - 1) + return false; + return true; } @@ -977,7 +1112,11 @@ static bool valid_gecos(const char *d) { if (!utf8_is_valid(d)) return false; - if (strpbrk(d, ":\n")) + if (string_has_cc(d, NULL)) + return false; + + /* Colons are used as field separators, and hence not OK */ + if (strchr(d, ':')) return false; return true; @@ -993,7 +1132,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { {} }; - _cleanup_free_ char *action = NULL, *name = NULL, *id = NULL; + _cleanup_free_ char *action = NULL, *name = NULL, *id = NULL, *resolved_name = NULL; _cleanup_(item_freep) Item *i = NULL; Item *existing; Hashmap *h; @@ -1019,90 +1158,180 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { return -EINVAL; } - if (!IN_SET(action[0], ADD_USER, ADD_GROUP)) { + if (!IN_SET(action[0], ADD_USER, ADD_GROUP, ADD_MEMBER)) { log_error("[%s:%u] Unknown command command type '%c'.", fname, line, action[0]); return -EBADMSG; } - i = new0(Item, 1); - if (!i) - return log_oom(); - - i->type = action[0]; - - r = specifier_printf(name, specifier_table, NULL, &i->name); + r = specifier_printf(name, specifier_table, NULL, &resolved_name); if (r < 0) { log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name); return r; } - if (!valid_user_group_name(i->name)) { - log_error("[%s:%u] '%s' is not a valid user or group name.", fname, line, i->name); + if (!valid_user_group_name(resolved_name)) { + log_error("[%s:%u] '%s' is not a valid user or group name.", fname, line, resolved_name); return -EINVAL; } if (n >= 0) { n += strspn(buffer+n, WHITESPACE); - if (buffer[n] != 0 && (buffer[n] != '-' || buffer[n+1] != 0)) { - i->description = unquote(buffer+n, "\""); - if (!i->description) - return log_oom(); - if (!valid_gecos(i->description)) { - log_error("[%s:%u] '%s' is not a valid GECOS field.", fname, line, i->description); - return -EINVAL; - } - } + if (STR_IN_SET(buffer + n, "", "-")) + n = -1; } - if (id && !streq(id, "-")) { + switch (action[0]) { - if (path_is_absolute(id)) { - char *p; + case ADD_MEMBER: { + _cleanup_free_ char *resolved_id = NULL; + char **l; - p = strdup(id); - if (!p) - return log_oom(); + r = hashmap_ensure_allocated(&members, string_hash_func, string_compare_func); + if (r < 0) + return log_oom(); - path_kill_slashes(p); + /* Try to extend an existing member or group item */ - if (i->type == ADD_USER) - i->uid_path = p; - else - i->gid_path = p; + if (!id || streq(id, "-")) { + log_error("[%s:%u] Lines of type 'm' require a group name in the third field.", fname, line); + return -EINVAL; + } - } else if (i->type == ADD_USER) { - r = parse_uid(id, &i->uid); - if (r < 0) { - log_error("Failed to parse UID: %s", id); - return -EBADMSG; - } + r = specifier_printf(id, specifier_table, NULL, &resolved_id); + if (r < 0) { + log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name); + return r; + } - i->uid_set = true; + if (!valid_user_group_name(resolved_id)) { + log_error("[%s:%u] '%s' is not a valid user or group name.", fname, line, resolved_id); + return -EINVAL; + } + + if (n >= 0) { + log_error("[%s:%u] Lines of type 'm' don't take a GECOS field.", fname, line); + return -EINVAL; + } + + l = hashmap_get(members, resolved_id); + if (l) { + /* A list for this group name already exists, let's append to it */ + r = strv_push(&l, resolved_name); + if (r < 0) + return log_oom(); + + resolved_name = NULL; + assert_se(hashmap_update(members, resolved_id, l) >= 0); } else { - assert(i->type == ADD_GROUP); + /* No list for this group name exists yet, create one */ - r = parse_gid(id, &i->gid); + l = new0(char *, 2); + if (!l) + return -ENOMEM; + + l[0] = resolved_name; + l[1] = NULL; + + r = hashmap_put(members, resolved_id, l); if (r < 0) { - log_error("Failed to parse GID: %s", id); - return -EBADMSG; + free(l); + return log_oom(); } - i->gid_set = true; + resolved_id = resolved_name = NULL; } + + return 0; } - if (i->type == ADD_USER) { + case ADD_USER: r = hashmap_ensure_allocated(&users, string_hash_func, string_compare_func); + if (r < 0) + return log_oom(); + + i = new0(Item, 1); + if (!i) + return log_oom(); + + if (id && !streq(id, "-")) { + + if (path_is_absolute(id)) { + i->uid_path = strdup(id); + if (!i->uid_path) + return log_oom(); + + path_kill_slashes(i->uid_path); + + } else { + r = parse_uid(id, &i->uid); + if (r < 0) { + log_error("Failed to parse UID: %s", id); + return -EBADMSG; + } + + i->uid_set = true; + } + } + + if (n >= 0) { + i->description = unquote(buffer+n, "\""); + if (!i->description) + return log_oom(); + + if (!valid_gecos(i->description)) { + log_error("[%s:%u] '%s' is not a valid GECOS field.", fname, line, i->description); + return -EINVAL; + } + } + h = users; - } else { - assert(i->type == ADD_GROUP); + break; + + case ADD_GROUP: r = hashmap_ensure_allocated(&groups, string_hash_func, string_compare_func); + if (r < 0) + return log_oom(); + + if (n >= 0) { + log_error("[%s:%u] Lines of type 'g' don't take a GECOS field.", fname, line); + return -EINVAL; + } + + i = new0(Item, 1); + if (!i) + return log_oom(); + + if (id && !streq(id, "-")) { + + if (path_is_absolute(id)) { + i->gid_path = strdup(id); + if (!i->gid_path) + return log_oom(); + + path_kill_slashes(i->gid_path); + } else { + r = parse_gid(id, &i->gid); + if (r < 0) { + log_error("Failed to parse GID: %s", id); + return -EBADMSG; + } + + i->gid_set = true; + } + } + + h = groups; + break; + default: + return -EBADMSG; } - if (r < 0) - return log_oom(); + + i->type = action[0]; + i->name = resolved_name; + resolved_name = NULL; existing = hashmap_get(h, i->name); if (existing) { @@ -1115,10 +1344,8 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { } r = hashmap_put(h, i->name, i); - if (r < 0) { - log_error("Failed to insert item %s: %s", i->name, strerror(-r)); - return r; - } + if (r < 0) + return log_oom(); i = NULL; return 0; @@ -1165,42 +1392,6 @@ static int read_config_file(const char *fn, bool ignore_enoent) { return r; } -static int take_lock(void) { - - struct flock flock = { - .l_type = F_WRLCK, - .l_whence = SEEK_SET, - .l_start = 0, - .l_len = 0, - }; - - const char *path; - int fd, r; - - /* This is roughly the same as lckpwdf(), but not as awful. We - * don't want to use alarm() and signals, hence we implement - * our own trivial version of this. - * - * Note that shadow-utils also takes per-database locks in - * addition to lckpwdf(). However, we don't given that they - * are redundant as they they invoke lckpwdf() first and keep - * it during everything they do. The per-database locks are - * awfully racy, and thus we just won't do them. */ - - path = fix_root("/etc/.pwd.lock"); - fd = open(path, O_WRONLY|O_CREAT|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600); - if (fd < 0) - return -errno; - - r = fcntl(fd, F_SETLKW, &flock); - if (r < 0) { - safe_close(fd); - return -errno; - } - - return fd; -} - static void free_database(Hashmap *by_name, Hashmap *by_id) { char *name; @@ -1222,16 +1413,13 @@ static void free_database(Hashmap *by_name, Hashmap *by_id) { hashmap_free(by_id); } -static int help(void) { - +static void help(void) { printf("%s [OPTIONS...] [CONFIGURATION FILE...]\n\n" "Creates system user accounts.\n\n" " -h --help Show this help\n" " --version Show package version\n" - " --root=PATH Operate on an alternate filesystem root\n", - program_invocation_short_name); - - return 0; + " --root=PATH Operate on an alternate filesystem root\n" + , program_invocation_short_name); } static int parse_argv(int argc, char *argv[]) { @@ -1253,12 +1441,13 @@ static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) { + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) switch (c) { case 'h': - return help(); + help(); + return 0; case ARG_VERSION: puts(PACKAGE_STRING); @@ -1280,7 +1469,6 @@ static int parse_argv(int argc, char *argv[]) { default: assert_not_reached("Unhandled option"); } - } return 1; } @@ -1291,6 +1479,7 @@ int main(int argc, char *argv[]) { Iterator iterator; int r, k; Item *i; + char *n; r = parse_argv(argc, argv); if (r <= 0) @@ -1302,7 +1491,11 @@ int main(int argc, char *argv[]) { umask(0022); - r = 0; + r = label_init(NULL); + if (r < 0) { + log_error("SELinux setup failed: %s", strerror(-r)); + goto finish; + } if (optind < argc) { int j; @@ -1329,7 +1522,11 @@ int main(int argc, char *argv[]) { } } - lock = take_lock(); + r = add_implicit(); + if (r < 0) + goto finish; + + lock = take_password_lock(arg_root); if (lock < 0) { log_error("Failed to take lock: %s", strerror(-lock)); goto finish; @@ -1364,8 +1561,14 @@ finish: while ((i = hashmap_steal_first(users))) item_free(i); + while ((n = hashmap_first_key(members))) { + strv_free(hashmap_steal_first(members)); + free(n); + } + hashmap_free(groups); hashmap_free(users); + hashmap_free(members); hashmap_free(todo_uids); hashmap_free(todo_gids);