/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ #include "auth-common.h" #if defined (USERDB_PASSWD_FILE) || defined(PASSDB_PASSWD_FILE) #include "userdb.h" #include "db-passwd-file.h" #include "array.h" #include "buffer.h" #include "istream.h" #include "hash.h" #include "str.h" #include "eacces-error.h" #include "ioloop.h" #include #include #include #include #define PARSE_TIME_STARTUP_WARN_SECS 60 #define PARSE_TIME_RELOAD_WARN_SECS 10 static struct db_passwd_file *passwd_files; static void ATTR_NULL(3) passwd_file_add(struct passwd_file *pw, const char *username, const char *pass, const char *const *args) { /* args = uid, gid, user info, home dir, shell, extra_fields */ struct passwd_user *pu; const char *extra_fields = NULL; char *user; size_t len; if (hash_table_lookup(pw->users, username) != NULL) { e_error(pw->event, "User %s exists more than once", username); return; } pu = p_new(pw->pool, struct passwd_user, 1); user = p_strdup(pw->pool, username); len = pass == NULL ? 0 : strlen(pass); if (len > 4 && pass[0] != '{' && pass[0] != '$' && pass[len-1] == ']' && pass[len-4] == '[') { /* password[type] - we're being libpam-pwdfile compatible here. it uses 13 = DES and 34 = MD5. For backwards compatibility with ourself, we have also 56 = Digest-MD5. */ int num = (pass[len-3] - '0') * 10 + (pass[len-2] - '0'); pass = t_strndup(pass, len-4); if (num == 34) { pu->password = p_strconcat(pw->pool, "{PLAIN-MD5}", pass, NULL); } else if (num == 56) { pu->password = p_strconcat(pw->pool, "{DIGEST-MD5}", pass, NULL); if (strlen(pu->password) != 32 + 12) { e_error(pw->event, "User %s " "has invalid password", username); return; } } else { pu->password = p_strconcat(pw->pool, "{CRYPT}", pass, NULL); } } else { pu->password = p_strdup(pw->pool, pass); } pu->uid = (uid_t)-1; pu->gid = (gid_t)-1; if (*args == NULL) ; else if (!pw->db->userdb || **args == '\0') { args++; } else { pu->uid = userdb_parse_uid(NULL, *args); if (pu->uid == 0 || pu->uid == (uid_t)-1) { e_error(pw->event, "User %s has invalid UID '%s'", username, *args); return; } args++; } if (*args == NULL) { if (pw->db->userdb_warn_missing) { e_error(pw->event, "User %s is missing userdb info", username); } /* don't allow userdb lookups */ pu->uid = 0; pu->gid = 0; } else if (!pw->db->userdb || **args == '\0') args++; else { pu->gid = userdb_parse_gid(NULL, *args); if (pu->gid == 0 || pu->gid == (gid_t)-1) { e_error(pw->event, "User %s has invalid GID '%s'", username, *args); return; } args++; } /* user info */ if (*args != NULL) args++; /* home */ if (*args != NULL) { if (pw->db->userdb) pu->home = p_strdup_empty(pw->pool, *args); args++; } /* shell */ if (*args != NULL) args++; if (*args != NULL && **args == '\0') { /* old format, this field is empty and next field may contain MAIL */ args++; if (*args != NULL && **args != '\0' && pw->db->userdb) { extra_fields = t_strconcat("userdb_mail=", t_strarray_join(args, ":"), NULL); } } else if (*args != NULL) { /* new format, contains a space separated list of extra fields */ extra_fields = t_strarray_join(args, ":"); } if (extra_fields != NULL) { pu->extra_fields = p_strsplit_spaces(pw->pool, extra_fields, " "); } hash_table_insert(pw->users, user, pu); } static struct passwd_file * passwd_file_new(struct db_passwd_file *db, const char *expanded_path) { struct passwd_file *pw; pw = i_new(struct passwd_file, 1); pw->db = db; pw->path = i_strdup(expanded_path); pw->fd = -1; pw->event = event_create(db->event); event_set_append_log_prefix(pw->event, t_strdup_printf("passwd-file %s:", pw->path)); if (hash_table_is_created(db->files)) hash_table_insert(db->files, pw->path, pw); return pw; } static int passwd_file_open(struct passwd_file *pw, bool startup, const char **error_r) { const char *no_args = NULL; struct istream *input; const char *line; struct stat st; time_t start_time, end_time; unsigned int time_secs; int fd; fd = open(pw->path, O_RDONLY); if (fd == -1) { if (errno == EACCES) *error_r = eacces_error_get("open", pw->path); else { *error_r = t_strdup_printf("open(%s) failed: %m", pw->path); } return -1; } if (fstat(fd, &st) != 0) { *error_r = t_strdup_printf("fstat(%s) failed: %m", pw->path); i_close_fd(&fd); return -1; } pw->fd = fd; pw->stamp = st.st_mtime; pw->size = st.st_size; pw->pool = pool_alloconly_create(MEMPOOL_GROWING"passwd_file", 10240); hash_table_create(&pw->users, pw->pool, 0, str_hash, strcmp); start_time = time(NULL); input = i_stream_create_fd(pw->fd, SIZE_MAX); i_stream_set_return_partial_line(input, TRUE); while ((line = i_stream_read_next_line(input)) != NULL) { if (*line == '\0' || *line == ':' || *line == '#') continue; /* no username or comment */ T_BEGIN { const char *const *args = t_strsplit(line, ":"); if (args[1] != NULL) { /* at least username+password */ passwd_file_add(pw, args[0], args[1], args+2); } else { /* only username */ passwd_file_add(pw, args[0], NULL, &no_args); } } T_END; } i_stream_destroy(&input); end_time = time(NULL); time_secs = end_time - start_time; if ((time_secs > PARSE_TIME_STARTUP_WARN_SECS && startup) || (time_secs > PARSE_TIME_RELOAD_WARN_SECS && !startup)) { e_warning(pw->event, "Reading %u users took %u secs", hash_table_count(pw->users), time_secs); } else { e_debug(pw->event, "Read %u users in %u secs", hash_table_count(pw->users), time_secs); } return 0; } static void passwd_file_close(struct passwd_file *pw) { i_close_fd_path(&pw->fd, pw->path); hash_table_destroy(&pw->users); pool_unref(&pw->pool); } static void passwd_file_free(struct passwd_file *pw) { if (hash_table_is_created(pw->db->files)) hash_table_remove(pw->db->files, pw->path); passwd_file_close(pw); event_unref(&pw->event); i_free(pw->path); i_free(pw); } static int passwd_file_sync(struct auth_request *request, struct passwd_file *pw) { struct stat st; const char *error; if (pw->last_sync_time == ioloop_time) return hash_table_is_created(pw->users) ? 1 : -1; pw->last_sync_time = ioloop_time; if (stat(pw->path, &st) < 0) { /* with variables don't give hard errors, or errors about nonexistent files */ int ret = -1; if (errno == EACCES) { e_error(authdb_event(request), "%s", eacces_error_get("stat", pw->path)); } else if (errno == ENOENT) { auth_request_log_info(request, "passwd-file", "missing passwd file: %s", pw->path); ret = 0; } else { e_error(authdb_event(request), "stat(%s) failed: %m", pw->path); } if (pw->db->default_file != pw) passwd_file_free(pw); return ret; } if (st.st_mtime != pw->stamp || st.st_size != pw->size) { passwd_file_close(pw); if (passwd_file_open(pw, FALSE, &error) < 0) { e_error(authdb_event(request), "%s", error); return -1; } } return 1; } static struct db_passwd_file *db_passwd_file_find(const char *path) { struct db_passwd_file *f; for (f = passwd_files; f != NULL; f = f->next) { if (strcmp(f->path, path) == 0) return f; } return NULL; } static void db_passwd_file_set_userdb(struct db_passwd_file *db) { db->userdb = TRUE; /* warn about missing userdb fields only when there aren't any other userdbs. */ db->userdb_warn_missing = array_is_created(&global_auth_settings->userdbs) && array_count(&global_auth_settings->userdbs) == 1; } struct db_passwd_file * db_passwd_file_init(const char *path, bool userdb, bool debug) { struct db_passwd_file *db; const char *p; bool percents = FALSE; db = db_passwd_file_find(path); if (db != NULL) { db->refcount++; if (userdb) db_passwd_file_set_userdb(db); return db; } db = i_new(struct db_passwd_file, 1); db->refcount = 1; if (userdb) db_passwd_file_set_userdb(db); db->event = event_create(auth_event); event_set_forced_debug(db->event, debug); for (p = path; *p != '\0'; p++) { if (*p == '%' && p[1] != '\0') { if (var_get_key(++p) == '%') percents = TRUE; else db->vars = TRUE; } } if (percents && !db->vars) { /* just extra escaped % chars. remove them. */ struct var_expand_table empty_table[1] = { { .key = '\0' }, }; string_t *dest; const char *error; dest = t_str_new(256); if (var_expand(dest, path, empty_table, &error) <= 0) i_unreached(); path = str_c(dest); } db->path = i_strdup(path); if (db->vars) { hash_table_create(&db->files, default_pool, 0, str_hash, strcmp); } else { db->default_file = passwd_file_new(db, path); } db->next = passwd_files; passwd_files = db; return db; } void db_passwd_file_parse(struct db_passwd_file *db) { const char *error; if (db->default_file != NULL && db->default_file->stamp == 0) { /* no variables, open the file immediately */ if (passwd_file_open(db->default_file, TRUE, &error) < 0) e_error(db->default_file->event, "%s", error); } } void db_passwd_file_unref(struct db_passwd_file **_db) { struct db_passwd_file *db = *_db; struct db_passwd_file **p; struct hash_iterate_context *iter; char *path; struct passwd_file *file; *_db = NULL; i_assert(db->refcount >= 0); if (--db->refcount > 0) return; for (p = &passwd_files; *p != NULL; p = &(*p)->next) { if (*p == db) { *p = db->next; break; } } if (db->default_file != NULL) passwd_file_free(db->default_file); else { iter = hash_table_iterate_init(db->files); while (hash_table_iterate(iter, db->files, &path, &file)) passwd_file_free(file); hash_table_iterate_deinit(&iter); hash_table_destroy(&db->files); } event_unref(&db->event); i_free(db->path); i_free(db); } static const char * path_fix(const char *path, const struct auth_request *auth_request ATTR_UNUSED) { const char *p; p = strchr(path, '/'); if (p == NULL) return path; /* most likely this is an invalid request. just cut off the '/' and everything after it. */ return t_strdup_until(path, p); } int db_passwd_file_lookup(struct db_passwd_file *db, struct auth_request *request, const char *username_format, struct passwd_user **user_r) { struct passwd_file *pw; string_t *username, *dest; const char *error; int ret; if (!db->vars) pw = db->default_file; else { dest = t_str_new(256); if (auth_request_var_expand(dest, db->path, request, path_fix, &error) <= 0) { e_error(authdb_event(request), "Failed to expand passwd-file path %s: %s", db->path, error); return -1; } pw = hash_table_lookup(db->files, str_c(dest)); if (pw == NULL) { /* doesn't exist yet. create lookup for it. */ pw = passwd_file_new(db, str_c(dest)); } } if ((ret = passwd_file_sync(request, pw)) <= 0) { /* pw may be freed now */ return ret; } username = t_str_new(256); if (auth_request_var_expand(username, username_format, request, auth_request_str_escape, &error) <= 0) { e_error(authdb_event(request), "Failed to expand username_format=%s: %s", username_format, error); return -1; } e_debug(authdb_event(request), "lookup: user=%s file=%s", str_c(username), pw->path); *user_r = hash_table_lookup(pw->users, str_c(username)); if (*user_r == NULL) { auth_request_log_unknown_user(request, AUTH_SUBSYS_DB); return 0; } return 1; } #endif