/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ /* gvfs - extensions for gio * * Copyright (C) 2015, 2016 Cosimo Cecchi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gvfsbackendadmin.h" #include "gvfsjobcreatemonitor.h" #include "gvfsjobenumerate.h" #include "gvfsjobopenforread.h" #include "gvfsjobopenforwrite.h" #include "gvfsjobqueryattributes.h" #include "gvfsjobqueryinfo.h" #include "gvfsjobread.h" #include "gvfsjobseekread.h" #include "gvfsjobseekwrite.h" #include "gvfsjobsetdisplayname.h" #include "gvfsjobwrite.h" #include "gvfsmonitor.h" struct _GVfsBackendAdmin { GVfsBackend parent_instance; GMutex polkit_mutex; PolkitAuthority *authority; }; struct _GVfsBackendAdminClass { GVfsBackendClass parent_class; }; G_DEFINE_TYPE(GVfsBackendAdmin, g_vfs_backend_admin, G_VFS_TYPE_BACKEND) static void do_finalize (GObject *object) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (object); g_clear_object (&self->authority); g_mutex_clear (&self->polkit_mutex); G_OBJECT_CLASS (g_vfs_backend_admin_parent_class)->finalize (object); } static gboolean check_permission (GVfsBackendAdmin *self, GVfsJob *job) { GVfsJobDBus *dbus_job = G_VFS_JOB_DBUS (job); GError *error = NULL; GDBusMethodInvocation *invocation; GDBusConnection *connection; GCredentials *credentials; pid_t pid; uid_t uid; PolkitSubject *subject; PolkitAuthorizationResult *result; gboolean is_authorized; invocation = dbus_job->invocation; connection = g_dbus_method_invocation_get_connection (invocation); credentials = g_dbus_connection_get_peer_credentials (connection); pid = g_credentials_get_unix_pid (credentials, &error); if (error != NULL) { g_vfs_job_failed_from_error (job, error); g_error_free (error); return FALSE; } uid = g_credentials_get_unix_user (credentials, &error); if (error != NULL) { g_vfs_job_failed_from_error (job, error); g_error_free (error); return FALSE; } /* Only one polkit dialog at a time */ g_mutex_lock (&self->polkit_mutex); subject = polkit_unix_process_new_for_owner (pid, 0, uid); result = polkit_authority_check_authorization_sync (self->authority, subject, "org.gtk.vfs.file-operations", NULL, POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, NULL, &error); g_object_unref (subject); g_mutex_unlock (&self->polkit_mutex); if (error != NULL) { g_vfs_job_failed_from_error (job, error); g_error_free (error); return FALSE; } is_authorized = polkit_authorization_result_get_is_authorized (result) || polkit_authorization_result_get_is_challenge (result); g_object_unref (result); if (!is_authorized) g_vfs_job_failed_literal (job, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, _("Permission denied")); return is_authorized; } static void complete_job (GVfsJob *job, GError *error) { if (error != NULL) { g_vfs_job_failed_from_error (job, error); g_error_free (error); return; } g_vfs_job_succeeded (job); } static void do_query_info (GVfsBackend *backend, GVfsJobQueryInfo *query_info_job, const char *filename, GFileQueryInfoFlags flags, GFileInfo *info, GFileAttributeMatcher *matcher) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (query_info_job); GError *error = NULL; GFile *file; GFileInfo *real_info; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); real_info = g_file_query_info (file, query_info_job->attributes, flags, job->cancellable, &error); g_object_unref (file); if (error != NULL) goto out; /* Override read/write flags, since the above call will use access() * to determine permissions, which does not honor our privileged * capabilities. */ g_file_info_set_attribute_boolean (real_info, G_FILE_ATTRIBUTE_ACCESS_CAN_READ, TRUE); g_file_info_set_attribute_boolean (real_info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, TRUE); g_file_info_set_attribute_boolean (real_info, G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE, TRUE); g_file_info_set_attribute_boolean (real_info, G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME, TRUE); g_file_info_copy_into (real_info, info); g_object_unref (real_info); out: complete_job (job, error); } static void do_close_write (GVfsBackend *backend, GVfsJobCloseWrite *close_write_job, GVfsBackendHandle handle) { GVfsJob *job = G_VFS_JOB (close_write_job); GOutputStream *stream = handle; GError *error = NULL; g_output_stream_close (stream, job->cancellable, &error); g_object_unref (stream); complete_job (job, error); } static void do_write (GVfsBackend *backend, GVfsJobWrite *write_job, GVfsBackendHandle handle, char *buffer, gsize buffer_size) { GVfsJob *job = G_VFS_JOB (write_job); GOutputStream *stream = handle; GError *error = NULL; gssize bytes_written; bytes_written = g_output_stream_write (stream, buffer, buffer_size, job->cancellable, &error); if (bytes_written > -1) g_vfs_job_write_set_written_size (write_job, bytes_written); complete_job (job, error); } static void set_open_for_write_attributes (GVfsJobOpenForWrite *open_write_job, GFileOutputStream *stream) { GSeekable *seekable = G_SEEKABLE (stream); g_vfs_job_open_for_write_set_handle (open_write_job, stream); g_vfs_job_open_for_write_set_can_seek (open_write_job, g_seekable_can_seek (seekable)); g_vfs_job_open_for_write_set_can_truncate (open_write_job, g_seekable_can_truncate (seekable)); } static void do_append_to (GVfsBackend *backend, GVfsJobOpenForWrite *open_write_job, const char *filename, GFileCreateFlags flags) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (open_write_job); GError *error = NULL; GFile *file; GFileOutputStream *stream; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); stream = g_file_append_to (file, flags, job->cancellable, &error); g_object_unref (file); if (error != NULL) goto out; set_open_for_write_attributes (open_write_job, stream); g_vfs_job_open_for_write_set_initial_offset (open_write_job, g_seekable_tell (G_SEEKABLE (stream))); out: complete_job (job, error); } static void do_create (GVfsBackend *backend, GVfsJobOpenForWrite *open_write_job, const char *filename, GFileCreateFlags flags) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (open_write_job); GError *error = NULL; GFile *file; GFileOutputStream *stream; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); stream = g_file_create (file, flags, job->cancellable, &error); g_object_unref (file); if (error != NULL) goto out; set_open_for_write_attributes (open_write_job, stream); out: complete_job (job, error); } static void do_replace (GVfsBackend *backend, GVfsJobOpenForWrite *open_write_job, const char *filename, const char *etag, gboolean make_backup, GFileCreateFlags flags) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (open_write_job); GError *error = NULL; GFile *file; GFileOutputStream *stream; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); stream = g_file_replace (file, etag, make_backup, flags, job->cancellable, &error); g_object_unref (file); if (error != NULL) goto out; set_open_for_write_attributes (open_write_job, stream); out: complete_job (job, error); } static void do_close_read (GVfsBackend *backend, GVfsJobCloseRead *close_read_job, GVfsBackendHandle handle) { GVfsJob *job = G_VFS_JOB (close_read_job); GInputStream *stream = handle; GError *error = NULL; g_input_stream_close (stream, job->cancellable, &error); g_object_unref (stream); complete_job (job, error); } static void do_read (GVfsBackend *backend, GVfsJobRead *read_job, GVfsBackendHandle handle, char *buffer, gsize bytes_requested) { GVfsJob *job = G_VFS_JOB (read_job); GInputStream *stream = handle; GError *error = NULL; gssize bytes; bytes = g_input_stream_read (stream, buffer, bytes_requested, job->cancellable, &error); if (bytes > -1) g_vfs_job_read_set_size (read_job, bytes); complete_job (job, error); } static void do_open_for_read (GVfsBackend *backend, GVfsJobOpenForRead *open_read_job, const char *filename) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (open_read_job); GError *error = NULL; GFile *file; GFileInputStream *stream; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); stream = g_file_read (file, job->cancellable, &error); g_object_unref (file); if (error != NULL) goto out; g_vfs_job_open_for_read_set_handle (open_read_job, stream); g_vfs_job_open_for_read_set_can_seek (open_read_job, g_seekable_can_seek (G_SEEKABLE (stream))); out: complete_job (job, error); } static void do_truncate (GVfsBackend *backend, GVfsJobTruncate *truncate_job, GVfsBackendHandle handle, goffset size) { GVfsJob *job = G_VFS_JOB (truncate_job); GSeekable *seekable = handle; GError *error = NULL; g_seekable_truncate (seekable, size, job->cancellable, &error); complete_job (job, error); } static void do_seek_on_read (GVfsBackend *backend, GVfsJobSeekRead *seek_read_job, GVfsBackendHandle handle, goffset offset, GSeekType type) { GVfsJob *job = G_VFS_JOB (seek_read_job); GSeekable *seekable = handle; GError *error = NULL; if (g_seekable_seek (seekable, offset, type, job->cancellable, &error)) g_vfs_job_seek_read_set_offset (seek_read_job, g_seekable_tell (seekable)); complete_job (job, error); } static void do_seek_on_write (GVfsBackend *backend, GVfsJobSeekWrite *seek_write_job, GVfsBackendHandle handle, goffset offset, GSeekType type) { GVfsJob *job = G_VFS_JOB (seek_write_job); GSeekable *seekable = handle; GError *error = NULL; if (g_seekable_seek (seekable, offset, type, job->cancellable, &error)) g_vfs_job_seek_write_set_offset (seek_write_job, g_seekable_tell (seekable)); complete_job (job, error); } static void do_enumerate (GVfsBackend *backend, GVfsJobEnumerate *enumerate_job, const char *filename, GFileAttributeMatcher *attribute_matcher, GFileQueryInfoFlags flags) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (enumerate_job); GError *error = NULL; GFile *file; GFileEnumerator *enumerator; GFileInfo *info; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); enumerator = g_file_enumerate_children (file, enumerate_job->attributes, flags, job->cancellable, &error); g_object_unref (file); if (error != NULL) goto out; while (TRUE) { if (!g_file_enumerator_iterate (enumerator, &info, NULL, job->cancellable, &error)) { g_object_unref (enumerator); goto out; } if (!info) break; g_vfs_job_enumerate_add_info (enumerate_job, g_object_ref (info)); } g_file_enumerator_close (enumerator, job->cancellable, &error); g_object_unref (enumerator); if (error != NULL) goto out; g_vfs_job_enumerate_done (enumerate_job); out: complete_job (job, error); } static void do_make_directory (GVfsBackend *backend, GVfsJobMakeDirectory *mkdir_job, const char *filename) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (mkdir_job); GError *error = NULL; GFile *file; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); g_file_make_directory (file, job->cancellable, &error); g_object_unref (file); complete_job (job, error); } static void do_make_symlink (GVfsBackend *backend, GVfsJobMakeSymlink *symlink_job, const char *filename, const char *symlink_value) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (symlink_job); GError *error = NULL; GFile *file; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); g_file_make_symbolic_link (file, symlink_value, job->cancellable, &error); g_object_unref (file); complete_job (job, error); } static void do_query_fs_info (GVfsBackend *backend, GVfsJobQueryFsInfo *query_info_job, const char *filename, GFileInfo *info, GFileAttributeMatcher *attribute_matcher) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (query_info_job); GError *error = NULL; GFile *file; char *attributes; GFileInfo *real_info; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); attributes = g_file_attribute_matcher_to_string (attribute_matcher); real_info = g_file_query_filesystem_info (file, attributes, job->cancellable, &error); g_object_unref (file); g_free (attributes); if (real_info != NULL) { g_file_info_copy_into (real_info, info); g_object_unref (real_info); } complete_job (job, error); } static void monitor_changed (GFileMonitor* monitor, GFile* file, GFile* other_file, GFileMonitorEvent event_type, GVfsMonitor *vfs_monitor) { char *file_path, *other_file_path; file_path = g_file_get_path (file); if (other_file) other_file_path = g_file_get_path (other_file); else other_file_path = NULL; g_vfs_monitor_emit_event (vfs_monitor, event_type, file_path, other_file_path); g_free (file_path); g_free (other_file_path); } static void create_dir_file_monitor (GVfsBackend *backend, GVfsJobCreateMonitor *monitor_job, const char *filename, GFileMonitorFlags flags, gboolean is_dir_monitor) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (monitor_job); GError *error = NULL; GFile *file; GFileMonitor *monitor; GVfsMonitor *vfs_monitor; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); if (is_dir_monitor) monitor = g_file_monitor_directory (file, flags, job->cancellable, &error); else monitor = g_file_monitor_file (file, flags, job->cancellable, &error); g_object_unref (file); if (error != NULL) goto out; vfs_monitor = g_vfs_monitor_new (backend); g_signal_connect (monitor, "changed", G_CALLBACK (monitor_changed), vfs_monitor); g_object_set_data_full (G_OBJECT (vfs_monitor), "real-monitor", monitor, (GDestroyNotify) g_object_unref); g_vfs_job_create_monitor_set_monitor (monitor_job, vfs_monitor); g_object_unref (vfs_monitor); out: complete_job (job, error); } static void do_create_dir_monitor (GVfsBackend *backend, GVfsJobCreateMonitor *job, const char *filename, GFileMonitorFlags flags) { create_dir_file_monitor (backend, job, filename, flags, TRUE); } static void do_create_file_monitor (GVfsBackend *backend, GVfsJobCreateMonitor *job, const char *filename, GFileMonitorFlags flags) { create_dir_file_monitor (backend, job, filename, flags, FALSE); } static void do_set_display_name (GVfsBackend *backend, GVfsJobSetDisplayName *display_name_job, const char *filename, const char *display_name) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (display_name_job); GError *error = NULL; GFile *file, *new_file; char *new_path; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); new_file = g_file_set_display_name (file, display_name, job->cancellable, &error); g_object_unref (file); if (error != NULL) goto out; new_path = g_file_get_path (new_file); g_vfs_job_set_display_name_set_new_path (display_name_job, new_path); g_free (new_path); g_object_unref (new_file); out: complete_job (job, error); } static void do_set_attribute (GVfsBackend *backend, GVfsJobSetAttribute *set_attribute_job, const char *filename, const char *attribute, GFileAttributeType type, gpointer value_p, GFileQueryInfoFlags flags) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (set_attribute_job); GError *error = NULL; GFile *file; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); g_file_set_attribute (file, attribute, type, value_p, flags, job->cancellable, &error); g_object_unref (file); complete_job (job, error); } static void do_delete (GVfsBackend *backend, GVfsJobDelete *delete_job, const char *filename) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (delete_job); GError *error = NULL; GFile *file; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); g_file_delete (file, job->cancellable, &error); g_object_unref (file); complete_job (job, error); } static void do_move (GVfsBackend *backend, GVfsJobMove *move_job, const char *source, const char *destination, GFileCopyFlags flags, GFileProgressCallback progress_callback, gpointer progress_callback_data) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (move_job); GError *error = NULL; GFile *src_file, *dst_file; if (!check_permission (self, job)) return; src_file = g_file_new_for_path (source); dst_file = g_file_new_for_path (destination); g_file_move (src_file, dst_file, flags, job->cancellable, progress_callback, progress_callback_data, &error); g_object_unref (src_file); g_object_unref (dst_file); complete_job (job, error); } static void do_query_settable_attributes (GVfsBackend *backend, GVfsJobQueryAttributes *query_job, const char *filename) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (query_job); GError *error = NULL; GFile *file; GFileAttributeInfoList *attr_list; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); attr_list = g_file_query_settable_attributes (file, job->cancellable, &error); g_object_unref (file); if (attr_list != NULL) { g_vfs_job_query_attributes_set_list (query_job, attr_list); g_file_attribute_info_list_unref (attr_list); } complete_job (job, error); } static void do_query_writable_namespaces (GVfsBackend *backend, GVfsJobQueryAttributes *query_job, const char *filename) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (query_job); GError *error = NULL; GFile *file; GFileAttributeInfoList *attr_list; if (!check_permission (self, job)) return; file = g_file_new_for_path (filename); attr_list = g_file_query_writable_namespaces (file, job->cancellable, &error); g_object_unref (file); if (attr_list != NULL) { g_vfs_job_query_attributes_set_list (query_job, attr_list); g_file_attribute_info_list_unref (attr_list); } complete_job (job, error); } static void do_mount (GVfsBackend *backend, GVfsJobMount *mount_job, GMountSpec *mount_spec, GMountSource *mount_source, gboolean is_automount) { GVfsBackendAdmin *self = G_VFS_BACKEND_ADMIN (backend); GVfsJob *job = G_VFS_JOB (mount_job); GError *error = NULL; self->authority = polkit_authority_get_sync (NULL, &error); complete_job (job, error); } static void g_vfs_backend_admin_class_init (GVfsBackendAdminClass * klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GVfsBackendClass *backend_class = G_VFS_BACKEND_CLASS (klass); object_class->finalize = do_finalize; backend_class->mount = do_mount; backend_class->open_for_read = do_open_for_read; backend_class->query_info = do_query_info; backend_class->read = do_read; backend_class->create = do_create; backend_class->append_to = do_append_to; backend_class->replace = do_replace; backend_class->write = do_write; backend_class->close_read = do_close_read; backend_class->close_write = do_close_write; backend_class->seek_on_read = do_seek_on_read; backend_class->seek_on_write = do_seek_on_write; backend_class->enumerate = do_enumerate; backend_class->truncate = do_truncate; backend_class->make_directory = do_make_directory; backend_class->make_symlink = do_make_symlink; backend_class->query_fs_info = do_query_fs_info; backend_class->create_dir_monitor = do_create_dir_monitor; backend_class->create_file_monitor = do_create_file_monitor; backend_class->set_display_name = do_set_display_name; backend_class->set_attribute = do_set_attribute; backend_class->delete = do_delete; backend_class->move = do_move; backend_class->query_settable_attributes = do_query_settable_attributes; backend_class->query_writable_namespaces = do_query_writable_namespaces; } static void g_vfs_backend_admin_init (GVfsBackendAdmin *self) { GVfsBackend *backend = G_VFS_BACKEND (self); GMountSpec *mount_spec; g_mutex_init (&self->polkit_mutex); g_vfs_backend_set_user_visible (backend, FALSE); mount_spec = g_mount_spec_new ("admin"); g_vfs_backend_set_mount_spec (backend, mount_spec); g_mount_spec_unref (mount_spec); g_vfs_backend_set_icon_name (backend, "folder"); g_vfs_backend_set_symbolic_icon_name (backend, "folder-symbolic"); } #define REQUIRED_CAPS (CAP_TO_MASK(CAP_FOWNER) | \ CAP_TO_MASK(CAP_DAC_OVERRIDE) | \ CAP_TO_MASK(CAP_DAC_READ_SEARCH)) static void acquire_caps (uid_t uid) { struct __user_cap_header_struct hdr; struct __user_cap_data_struct data; /* Tell kernel not clear capabilities when dropping root */ if (prctl (PR_SET_KEEPCAPS, 1, 0, 0, 0) < 0) g_error ("prctl(PR_SET_KEEPCAPS) failed"); /* Drop root uid, but retain the required permitted caps */ if (setuid (uid) < 0) g_error ("unable to drop privs"); memset (&hdr, 0, sizeof(hdr)); hdr.version = _LINUX_CAPABILITY_VERSION; /* Drop all non-require capabilities */ data.effective = REQUIRED_CAPS; data.permitted = REQUIRED_CAPS; data.inheritable = 0; if (capset (&hdr, &data) < 0) g_error ("capset failed"); } static char *session_address = NULL; static GOptionEntry entries[] = { { "address", 0, 0, G_OPTION_ARG_STRING, &session_address, "DBus session address", NULL }, { NULL } }; void g_vfs_backend_admin_pre_setup (int *argc, char **argv[]) { const char *pkexec_uid; uid_t uid; GError *error = NULL; GOptionContext *context; pkexec_uid = g_getenv ("PKEXEC_UID"); if (pkexec_uid == NULL) g_error ("gvfsd-admin must be executed under pkexec"); uid = strtol (pkexec_uid, NULL, 10); if ((errno == ERANGE && (uid == LONG_MAX || uid == LONG_MIN)) || (errno != 0 && uid == 0)) g_error ("Unable to convert PKEXEC_UID string to uid_t"); context = g_option_context_new (NULL); g_option_context_set_ignore_unknown_options (context, TRUE); g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); g_option_context_parse (context, argc, argv, &error); if (error != NULL) g_error ("Can't parse arguments: %s", error->message); acquire_caps (uid); g_setenv ("DBUS_SESSION_BUS_ADDRESS", session_address, TRUE); }