diff options
Diffstat (limited to 'src/plugins/acl/acl-backend-vfile.c')
-rw-r--r-- | src/plugins/acl/acl-backend-vfile.c | 659 |
1 files changed, 659 insertions, 0 deletions
diff --git a/src/plugins/acl/acl-backend-vfile.c b/src/plugins/acl/acl-backend-vfile.c new file mode 100644 index 0000000..3ee0af9 --- /dev/null +++ b/src/plugins/acl/acl-backend-vfile.c @@ -0,0 +1,659 @@ +/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "array.h" +#include "istream.h" +#include "nfs-workarounds.h" +#include "mail-storage-private.h" +#include "acl-global-file.h" +#include "acl-cache.h" +#include "acl-backend-vfile.h" + +#include <fcntl.h> +#include <unistd.h> +#include <sys/stat.h> + +#define ACL_ESTALE_RETRY_COUNT NFS_ESTALE_RETRY_COUNT +#define ACL_VFILE_DEFAULT_CACHE_SECS 30 + +static struct acl_backend *acl_backend_vfile_alloc(void) +{ + struct acl_backend_vfile *backend; + pool_t pool; + + pool = pool_alloconly_create("ACL backend", 512); + backend = p_new(pool, struct acl_backend_vfile, 1); + backend->backend.pool = pool; + return &backend->backend; +} + +static int +acl_backend_vfile_init(struct acl_backend *_backend, const char *data) +{ + struct acl_backend_vfile *backend = + (struct acl_backend_vfile *)_backend; + struct stat st; + const char *const *tmp; + + tmp = t_strsplit(data, ":"); + backend->global_path = p_strdup_empty(_backend->pool, *tmp); + backend->cache_secs = ACL_VFILE_DEFAULT_CACHE_SECS; + + if (*tmp != NULL) + tmp++; + for (; *tmp != NULL; tmp++) { + if (str_begins(*tmp, "cache_secs=")) { + if (str_to_uint(*tmp + 11, &backend->cache_secs) < 0) { + i_error("acl vfile: Invalid cache_secs value: %s", + *tmp + 11); + return -1; + } + } else { + i_error("acl vfile: Unknown parameter: %s", *tmp); + return -1; + } + } + if (backend->global_path != NULL) { + if (stat(backend->global_path, &st) < 0) { + if (errno != ENOENT) { + i_error("acl vfile: stat(%s) failed: %m", + backend->global_path); + return -1; + } + } else if (!S_ISDIR(st.st_mode)) { + _backend->global_file = + acl_global_file_init(backend->global_path, backend->cache_secs, + _backend->debug); + } + } + if (_backend->debug) { + if (backend->global_path == NULL) + i_debug("acl vfile: Global ACLs disabled"); + else if (_backend->global_file != NULL) { + i_debug("acl vfile: Global ACL file: %s", + backend->global_path); + } else { + i_debug("acl vfile: Global ACL legacy directory: %s", + backend->global_path); + } + } + + _backend->cache = + acl_cache_init(_backend, + sizeof(struct acl_backend_vfile_validity)); + return 0; +} + +static void acl_backend_vfile_deinit(struct acl_backend *_backend) +{ + struct acl_backend_vfile *backend = + (struct acl_backend_vfile *)_backend; + + if (backend->acllist_pool != NULL) { + array_free(&backend->acllist); + pool_unref(&backend->acllist_pool); + } + if (_backend->global_file != NULL) + acl_global_file_deinit(&_backend->global_file); + pool_unref(&backend->backend.pool); +} + +static const char * +acl_backend_vfile_get_local_dir(struct acl_backend *backend, + const char *name, const char *vname) +{ + struct mail_namespace *ns = mailbox_list_get_namespace(backend->list); + struct mailbox_list *list = ns->list; + struct mail_storage *storage; + enum mailbox_list_path_type type; + const char *dir, *inbox; + + if (*name == '\0') + name = NULL; + + if (backend->globals_only) + return NULL; + + /* ACL files are very important. try to keep them among the main + mail files. that's not possible though with a) if the mailbox is + a file or b) if the mailbox path doesn't point to filesystem. */ + if (mailbox_list_get_storage(&list, vname, &storage) < 0) + return NULL; + i_assert(list == ns->list); + + type = mail_storage_get_acl_list_path_type(storage); + if (name == NULL) { + if (!mailbox_list_get_root_path(list, type, &dir)) + return NULL; + } else { + if (mailbox_list_get_path(list, name, type, &dir) <= 0) + return NULL; + } + + /* verify that the directory isn't same as INBOX's directory. + this is mainly for Maildir. */ + if (name == NULL && + mailbox_list_get_path(list, "INBOX", + MAILBOX_LIST_PATH_TYPE_MAILBOX, &inbox) > 0 && + strcmp(inbox, dir) == 0) { + /* can't have default ACLs with this setup */ + return NULL; + } + return dir; +} + +static struct acl_object * +acl_backend_vfile_object_init(struct acl_backend *_backend, + const char *name) +{ + struct acl_backend_vfile *backend = + (struct acl_backend_vfile *)_backend; + struct acl_object_vfile *aclobj; + const char *dir, *vname, *error; + + aclobj = i_new(struct acl_object_vfile, 1); + aclobj->aclobj.backend = _backend; + aclobj->aclobj.name = i_strdup(name); + + T_BEGIN { + if (*name == '\0' || + mailbox_list_is_valid_name(_backend->list, name, &error)) { + vname = *name == '\0' ? "" : + mailbox_list_get_vname(_backend->list, name); + + dir = acl_backend_vfile_get_local_dir(_backend, name, vname); + aclobj->local_path = dir == NULL ? NULL : + i_strconcat(dir, "/"ACL_FILENAME, NULL); + if (backend->global_path != NULL && + _backend->global_file == NULL) { + aclobj->global_path = + i_strconcat(backend->global_path, "/", vname, NULL); + } + } else { + /* Invalid mailbox name, just use the default + global ACL files */ + } + } T_END; + return &aclobj->aclobj; +} + +static const char * +get_parent_mailbox(struct acl_backend *backend, const char *name) +{ + const char *p; + + p = strrchr(name, mailbox_list_get_hierarchy_sep(backend->list)); + return p == NULL ? NULL : t_strdup_until(name, p); +} + +static int +acl_backend_vfile_exists(struct acl_backend_vfile *backend, const char *path, + struct acl_vfile_validity *validity) +{ + struct stat st; + + if (validity->last_check + (time_t)backend->cache_secs > ioloop_time) { + /* use the cached value */ + return validity->last_mtime != ACL_VFILE_VALIDITY_MTIME_NOTFOUND ? 1 : 0; + } + + validity->last_check = ioloop_time; + if (stat(path, &st) < 0) { + if (errno == ENOENT || errno == ENOTDIR) { + validity->last_mtime = ACL_VFILE_VALIDITY_MTIME_NOTFOUND; + return 0; + } + if (errno == EACCES) { + validity->last_mtime = ACL_VFILE_VALIDITY_MTIME_NOACCESS; + return 1; + } + i_error("stat(%s) failed: %m", path); + return -1; + } + validity->last_mtime = st.st_mtime; + validity->last_size = st.st_size; + return 1; +} + +static bool +acl_backend_vfile_has_acl(struct acl_backend *_backend, const char *name) +{ + struct acl_backend_vfile *backend = + (struct acl_backend_vfile *)_backend; + struct acl_backend_vfile_validity *old_validity, new_validity; + const char *global_path, *vname; + int ret; + + old_validity = acl_cache_get_validity(_backend->cache, name); + if (old_validity != NULL) + new_validity = *old_validity; + else + i_zero(&new_validity); + + /* The caller wants to stop whenever a parent mailbox exists, even if + it has no ACL file. Also, if a mailbox doesn't exist then it can't + have a local ACL file. First check if there's a matching global ACL. + If not, check if the mailbox exists. */ + vname = *name == '\0' ? "" : + mailbox_list_get_vname(_backend->list, name); + struct mailbox *box = + mailbox_alloc(_backend->list, vname, + MAILBOX_FLAG_READONLY | MAILBOX_FLAG_IGNORE_ACLS); + if (backend->global_path == NULL) { + /* global ACLs disabled */ + ret = 0; + } else if (_backend->global_file != NULL) { + /* check global ACL file */ + ret = acl_global_file_refresh(_backend->global_file); + if (ret == 0 && acl_global_file_have_any(_backend->global_file, box->vname)) + ret = 1; + } else { + /* check global ACL directory */ + global_path = t_strconcat(backend->global_path, "/", name, NULL); + ret = acl_backend_vfile_exists(backend, global_path, + &new_validity.global_validity); + } + + if (ret != 0) { + /* error / global ACL found */ + } else if (mailbox_open(box) == 0) { + /* mailbox exists */ + ret = 1; + } else { + enum mail_error error; + const char *errstr = + mailbox_get_last_internal_error(box, &error); + if (error == MAIL_ERROR_NOTFOUND) + ret = 0; + else { + e_error(box->event, "acl: Failed to open mailbox: %s", + errstr); + ret = -1; + } + } + + acl_cache_set_validity(_backend->cache, name, &new_validity); + mailbox_free(&box); + return ret > 0; +} + +static struct acl_object * +acl_backend_vfile_object_init_parent(struct acl_backend *backend, + const char *child_name) +{ + const char *parent; + + /* stop at the first parent that + a) has global ACL file + b) has local ACL file + c) exists */ + while ((parent = get_parent_mailbox(backend, child_name)) != NULL) { + if (acl_backend_vfile_has_acl(backend, parent)) + break; + child_name = parent; + } + if (parent == NULL) { + /* use the root */ + parent = acl_backend_get_default_object(backend)->name; + } + return acl_backend_vfile_object_init(backend, parent); +} + +static void acl_backend_vfile_object_deinit(struct acl_object *_aclobj) +{ + struct acl_object_vfile *aclobj = (struct acl_object_vfile *)_aclobj; + + i_free(aclobj->local_path); + i_free(aclobj->global_path); + + if (array_is_created(&aclobj->aclobj.rights)) + array_free(&aclobj->aclobj.rights); + pool_unref(&aclobj->aclobj.rights_pool); + i_free(aclobj->aclobj.name); + i_free(aclobj); +} + +static int +acl_backend_vfile_read(struct acl_object *aclobj, bool global, const char *path, + struct acl_vfile_validity *validity, bool try_retry, + bool *is_dir_r) +{ + struct istream *input; + struct stat st; + struct acl_rights rights; + const char *line, *error; + unsigned int linenum; + int fd, ret = 0; + + *is_dir_r = FALSE; + + fd = nfs_safe_open(path, O_RDONLY); + if (fd == -1) { + if (errno == ENOENT || errno == ENOTDIR) { + if (aclobj->backend->debug) + i_debug("acl vfile: file %s not found", path); + validity->last_mtime = ACL_VFILE_VALIDITY_MTIME_NOTFOUND; + } else if (errno == EACCES) { + if (aclobj->backend->debug) + i_debug("acl vfile: no access to file %s", + path); + + acl_object_remove_all_access(aclobj); + validity->last_mtime = ACL_VFILE_VALIDITY_MTIME_NOACCESS; + } else { + i_error("open(%s) failed: %m", path); + return -1; + } + + validity->last_size = 0; + validity->last_read_time = ioloop_time; + return 1; + } + + if (fstat(fd, &st) < 0) { + if (errno == ESTALE && try_retry) { + i_close_fd(&fd); + return 0; + } + + i_error("fstat(%s) failed: %m", path); + i_close_fd(&fd); + return -1; + } + if (S_ISDIR(st.st_mode)) { + /* we opened a directory. */ + *is_dir_r = TRUE; + i_close_fd(&fd); + return 0; + } + + if (aclobj->backend->debug) + i_debug("acl vfile: reading file %s", path); + + input = i_stream_create_fd(fd, SIZE_MAX); + i_stream_set_return_partial_line(input, TRUE); + linenum = 0; + while ((line = i_stream_read_next_line(input)) != NULL) { + linenum++; + if (line[0] == '\0' || line[0] == '#') + continue; + T_BEGIN { + ret = acl_rights_parse_line(line, aclobj->rights_pool, + &rights, &error); + rights.global = global; + if (ret < 0) { + i_error("ACL file %s line %u: %s", + path, linenum, error); + } else { + array_push_back(&aclobj->rights, &rights); + } + } T_END; + if (ret < 0) + break; + } + + if (ret < 0) { + /* parsing failure */ + } else if (input->stream_errno != 0) { + if (input->stream_errno == ESTALE && try_retry) + ret = 0; + else { + ret = -1; + i_error("read(%s) failed: %s", path, + i_stream_get_error(input)); + } + } else { + if (fstat(fd, &st) < 0) { + if (errno == ESTALE && try_retry) + ret = 0; + else { + ret = -1; + i_error("fstat(%s) failed: %m", path); + } + } else { + ret = 1; + validity->last_read_time = ioloop_time; + validity->last_mtime = st.st_mtime; + validity->last_size = st.st_size; + } + } + + i_stream_unref(&input); + if (close(fd) < 0) { + if (errno == ESTALE && try_retry) + return 0; + + i_error("close(%s) failed: %m", path); + return -1; + } + return ret; +} + +static int +acl_backend_vfile_read_with_retry(struct acl_object *aclobj, + bool global, const char *path, + struct acl_vfile_validity *validity) +{ + unsigned int i; + int ret; + bool is_dir; + + if (path == NULL) + return 0; + + for (i = 0;; i++) { + ret = acl_backend_vfile_read(aclobj, global, path, validity, + i < ACL_ESTALE_RETRY_COUNT, + &is_dir); + if (ret != 0) + break; + + if (is_dir) { + /* opened a directory. use dir/.DEFAULT instead */ + path = t_strconcat(path, "/.DEFAULT", NULL); + } else { + /* ESTALE - try again */ + } + } + + return ret <= 0 ? -1 : 0; +} + +static bool +acl_vfile_validity_has_changed(struct acl_backend_vfile *backend, + const struct acl_vfile_validity *validity, + const struct stat *st) +{ + if (st->st_mtime == validity->last_mtime && + st->st_size == validity->last_size) { + /* same timestamp, but if it was modified within the + same second we want to refresh it again later (but + do it only after a couple of seconds so we don't + keep re-reading it all the time within those + seconds) */ + time_t cache_secs = backend->cache_secs; + + if (validity->last_read_time != 0 && + (st->st_mtime < validity->last_read_time - cache_secs || + ioloop_time - validity->last_read_time <= cache_secs)) + return FALSE; + } + return TRUE; +} + +static int +acl_backend_vfile_refresh(struct acl_object *aclobj, const char *path, + struct acl_vfile_validity *validity) +{ + struct acl_backend_vfile *backend = + (struct acl_backend_vfile *)aclobj->backend; + struct stat st; + int ret; + + if (validity == NULL) + return 1; + if (path == NULL || + validity->last_check + (time_t)backend->cache_secs > ioloop_time) + return 0; + + validity->last_check = ioloop_time; + ret = stat(path, &st); + if (ret == 0 && S_ISDIR(st.st_mode)) { + /* it's a directory. use dir/.DEFAULT instead */ + path = t_strconcat(path, "/.DEFAULT", NULL); + ret = stat(path, &st); + } + + if (ret < 0) { + if (errno == ENOENT || errno == ENOTDIR) { + /* if the file used to exist, we have to re-read it */ + return validity->last_mtime != ACL_VFILE_VALIDITY_MTIME_NOTFOUND ? 1 : 0; + } + if (errno == EACCES) + return validity->last_mtime != ACL_VFILE_VALIDITY_MTIME_NOACCESS ? 1 : 0; + i_error("stat(%s) failed: %m", path); + return -1; + } + return acl_vfile_validity_has_changed(backend, validity, &st) ? 1 : 0; +} + +int acl_backend_vfile_object_get_mtime(struct acl_object *aclobj, + time_t *mtime_r) +{ + struct acl_backend_vfile_validity *validity; + + validity = acl_cache_get_validity(aclobj->backend->cache, aclobj->name); + if (validity == NULL) + return -1; + + if (validity->local_validity.last_mtime != 0) + *mtime_r = validity->local_validity.last_mtime; + else if (validity->global_validity.last_mtime != 0) + *mtime_r = validity->global_validity.last_mtime; + else + *mtime_r = 0; + return 0; +} + +static int +acl_backend_global_file_refresh(struct acl_object *_aclobj, + struct acl_vfile_validity *validity) +{ + struct acl_backend_vfile *backend = + (struct acl_backend_vfile *)_aclobj->backend; + struct stat st; + + if (acl_global_file_refresh(_aclobj->backend->global_file) < 0) + return -1; + + acl_global_file_last_stat(_aclobj->backend->global_file, &st); + if (validity == NULL) + return 1; + return acl_vfile_validity_has_changed(backend, validity, &st) ? 1 : 0; +} + +static int acl_backend_vfile_object_refresh_cache(struct acl_object *_aclobj) +{ + struct acl_object_vfile *aclobj = (struct acl_object_vfile *)_aclobj; + struct acl_backend_vfile *backend = + (struct acl_backend_vfile *)_aclobj->backend; + struct acl_backend_vfile_validity *old_validity; + struct acl_backend_vfile_validity validity; + time_t mtime; + int ret; + + old_validity = acl_cache_get_validity(_aclobj->backend->cache, + _aclobj->name); + ret = _aclobj->backend->global_file != NULL ? + acl_backend_global_file_refresh(_aclobj, old_validity == NULL ? NULL : + &old_validity->global_validity) : + acl_backend_vfile_refresh(_aclobj, aclobj->global_path, + old_validity == NULL ? NULL : + &old_validity->global_validity); + if (ret == 0) { + ret = acl_backend_vfile_refresh(_aclobj, aclobj->local_path, + old_validity == NULL ? NULL : + &old_validity->local_validity); + } + if (ret <= 0) + return ret; + + /* either global or local ACLs changed, need to re-read both */ + if (!array_is_created(&_aclobj->rights)) { + _aclobj->rights_pool = + pool_alloconly_create("acl rights", 256); + i_array_init(&_aclobj->rights, 16); + } else { + array_clear(&_aclobj->rights); + p_clear(_aclobj->rights_pool); + } + + i_zero(&validity); + if (_aclobj->backend->global_file != NULL) { + struct stat st; + + acl_object_add_global_acls(_aclobj); + acl_global_file_last_stat(_aclobj->backend->global_file, &st); + validity.global_validity.last_read_time = ioloop_time; + validity.global_validity.last_mtime = st.st_mtime; + validity.global_validity.last_size = st.st_size; + } else { + if (acl_backend_vfile_read_with_retry(_aclobj, TRUE, aclobj->global_path, + &validity.global_validity) < 0) + return -1; + } + if (acl_backend_vfile_read_with_retry(_aclobj, FALSE, aclobj->local_path, + &validity.local_validity) < 0) + return -1; + + acl_rights_sort(_aclobj); + /* update cache only after we've successfully read everything */ + acl_object_rebuild_cache(_aclobj); + acl_cache_set_validity(_aclobj->backend->cache, + _aclobj->name, &validity); + + if (acl_backend_vfile_object_get_mtime(_aclobj, &mtime) == 0) + acl_backend_vfile_acllist_verify(backend, _aclobj->name, mtime); + return 0; +} + +static int acl_backend_vfile_object_last_changed(struct acl_object *_aclobj, + time_t *last_changed_r) +{ + struct acl_backend_vfile_validity *old_validity; + + *last_changed_r = 0; + + old_validity = acl_cache_get_validity(_aclobj->backend->cache, + _aclobj->name); + if (old_validity == NULL) { + if (acl_backend_vfile_object_refresh_cache(_aclobj) < 0) + return -1; + old_validity = acl_cache_get_validity(_aclobj->backend->cache, + _aclobj->name); + if (old_validity == NULL) + return 0; + } + *last_changed_r = old_validity->local_validity.last_mtime; + return 0; +} + +struct acl_backend_vfuncs acl_backend_vfile = { + acl_backend_vfile_alloc, + acl_backend_vfile_init, + acl_backend_vfile_deinit, + acl_backend_vfile_nonowner_iter_init, + acl_backend_vfile_nonowner_iter_next, + acl_backend_vfile_nonowner_iter_deinit, + acl_backend_vfile_nonowner_lookups_rebuild, + acl_backend_vfile_object_init, + acl_backend_vfile_object_init_parent, + acl_backend_vfile_object_deinit, + acl_backend_vfile_object_refresh_cache, + acl_backend_vfile_object_update, + acl_backend_vfile_object_last_changed, + acl_default_object_list_init, + acl_default_object_list_next, + acl_default_object_list_deinit +}; |