/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" #include "array.h" #include "bsearch-insert-pos.h" #include "ioloop.h" #include "str.h" #include "strescape.h" #include "file-dotlock.h" #include "ostream.h" #include "mail-storage.h" #include "acl-cache.h" #include "acl-backend-vfile.h" #include #include static struct dotlock_settings dotlock_set = { .timeout = 30, .stale_timeout = 120 }; static int acl_backend_vfile_update_begin(struct acl_object_vfile *aclobj, struct dotlock **dotlock_r) { struct acl_object *_aclobj = &aclobj->aclobj; struct mailbox_permissions perm; int fd; if (aclobj->local_path == NULL) { i_error("Can't update acl object '%s': No local acl file path", aclobj->aclobj.name); return -1; } /* first lock the ACL file */ mailbox_list_get_permissions(_aclobj->backend->list, _aclobj->name, &perm); fd = file_dotlock_open_group(&dotlock_set, aclobj->local_path, 0, perm.file_create_mode, perm.file_create_gid, perm.file_create_gid_origin, dotlock_r); if (fd == -1) { i_error("file_dotlock_open(%s) failed: %m", aclobj->local_path); return -1; } /* locked successfully, re-read the existing file to make sure we don't lose any changes. */ acl_cache_flush(_aclobj->backend->cache, _aclobj->name); if (_aclobj->backend->v.object_refresh_cache(_aclobj) < 0) { file_dotlock_delete(dotlock_r); return -1; } return fd; } static bool vfile_object_modify_right(struct acl_object *aclobj, unsigned int idx, const struct acl_rights_update *update) { struct acl_rights *right; bool c1, c2; right = array_idx_modifiable(&aclobj->rights, idx); c1 = acl_right_names_modify(aclobj->rights_pool, &right->rights, update->rights.rights, update->modify_mode); c2 = acl_right_names_modify(aclobj->rights_pool, &right->neg_rights, update->rights.neg_rights, update->neg_modify_mode); if (right->rights == NULL && right->neg_rights == NULL) { /* this identifier no longer exists */ array_delete(&aclobj->rights, idx, 1); c1 = TRUE; } return c1 || c2; } static bool vfile_object_add_right(struct acl_object *aclobj, unsigned int idx, const struct acl_rights_update *update) { struct acl_rights right; bool c1, c2; if (update->modify_mode == ACL_MODIFY_MODE_REMOVE && update->neg_modify_mode == ACL_MODIFY_MODE_REMOVE) { /* nothing to do */ return FALSE; } i_zero(&right); right.id_type = update->rights.id_type; right.identifier = p_strdup(aclobj->rights_pool, update->rights.identifier); c1 = acl_right_names_modify(aclobj->rights_pool, &right.rights, update->rights.rights, update->modify_mode); c2 = acl_right_names_modify(aclobj->rights_pool, &right.neg_rights, update->rights.neg_rights, update->neg_modify_mode); if (c1 || c2) { array_insert(&aclobj->rights, idx, &right, 1); return TRUE; } return FALSE; } static void vfile_write_right(string_t *dest, const struct acl_rights *right, bool neg) { const char *const *rights = neg ? right->neg_rights : right->rights; if (neg) str_append_c(dest,'-'); acl_rights_write_id(dest, right); if (strchr(str_c(dest), ' ') != NULL) T_BEGIN { /* need to escape it */ const char *escaped = t_strdup(str_escape(str_c(dest))); str_truncate(dest, 0); str_printfa(dest, "\"%s\"", escaped); } T_END; str_append_c(dest, ' '); acl_right_names_write(dest, rights); str_append_c(dest, '\n'); } static int acl_backend_vfile_update_write(struct acl_object *aclobj, int fd, const char *path) { struct ostream *output; string_t *str; const struct acl_rights *rights; unsigned int i, count; int ret = 0; output = o_stream_create_fd_file(fd, 0, FALSE); o_stream_cork(output); str = str_new(default_pool, 256); /* rights are sorted with globals at the end, so we can stop at the first global */ rights = array_get(&aclobj->rights, &count); for (i = 0; i < count && !rights[i].global; i++) { if (rights[i].rights != NULL) { vfile_write_right(str, &rights[i], FALSE); o_stream_nsend(output, str_data(str), str_len(str)); str_truncate(str, 0); } if (rights[i].neg_rights != NULL) { vfile_write_right(str, &rights[i], TRUE); o_stream_nsend(output, str_data(str), str_len(str)); str_truncate(str, 0); } } str_free(&str); if (o_stream_finish(output) < 0) { i_error("write(%s) failed: %s", path, o_stream_get_error(output)); ret = -1; } o_stream_destroy(&output); /* we really don't want to lose ACL files' contents, so fsync() always before renaming */ if (fsync(fd) < 0) { i_error("fsync(%s) failed: %m", path); ret = -1; } return ret; } static void acl_backend_vfile_update_cache(struct acl_object *_aclobj, int fd) { struct acl_backend_vfile_validity *validity; struct stat st; if (fstat(fd, &st) < 0) { /* we'll just recalculate or fail it later */ acl_cache_flush(_aclobj->backend->cache, _aclobj->name); return; } validity = acl_cache_get_validity(_aclobj->backend->cache, _aclobj->name); validity->local_validity.last_read_time = ioloop_time; validity->local_validity.last_mtime = st.st_mtime; validity->local_validity.last_size = st.st_size; } int acl_backend_vfile_object_update(struct acl_object *_aclobj, const struct acl_rights_update *update) { 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 *validity; struct dotlock *dotlock; struct utimbuf ut; time_t orig_mtime; const char *path; unsigned int i; int fd; bool changed; /* global ACLs can't be updated here */ i_assert(!update->rights.global); fd = acl_backend_vfile_update_begin(aclobj, &dotlock); if (fd == -1) return -1; if (!array_bsearch_insert_pos(&_aclobj->rights, &update->rights, acl_rights_cmp, &i)) changed = vfile_object_add_right(_aclobj, i, update); else changed = vfile_object_modify_right(_aclobj, i, update); if (!changed) { file_dotlock_delete(&dotlock); return 0; } validity = acl_cache_get_validity(_aclobj->backend->cache, _aclobj->name); orig_mtime = validity->local_validity.last_mtime; /* ACLs were really changed, write the new ones */ path = file_dotlock_get_lock_path(dotlock); if (acl_backend_vfile_update_write(_aclobj, fd, path) < 0) { file_dotlock_delete(&dotlock); acl_cache_flush(_aclobj->backend->cache, _aclobj->name); return -1; } if (orig_mtime < update->last_change && update->last_change != 0) { /* set mtime to last_change, if it's higher than the file's original mtime. if original mtime is higher, then we're merging some changes and it's better for the mtime to get updated. */ ut.actime = ioloop_time; ut.modtime = update->last_change; if (utime(path, &ut) < 0) i_error("utime(%s) failed: %m", path); } acl_backend_vfile_update_cache(_aclobj, fd); if (file_dotlock_replace(&dotlock, 0) < 0) { acl_cache_flush(_aclobj->backend->cache, _aclobj->name); return -1; } /* make sure dovecot-acl-list gets updated if we changed any lookup rights. */ if (acl_rights_has_nonowner_lookup_changes(&update->rights) || update->modify_mode == ACL_MODIFY_MODE_REPLACE || update->modify_mode == ACL_MODIFY_MODE_CLEAR) (void)acl_backend_vfile_acllist_rebuild(backend); return 0; }