/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright © 2019 Endless Mobile, Inc. * * 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.1 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 * * Authors: * - Philip Withnall */ #include "config.h" #include #include #include #include #include #include #include #include #include "libmalcontent/app-filter-private.h" #include "libmalcontent/session-limits-private.h" G_DEFINE_QUARK (MctManagerError, mct_manager_error) /** * MctManager: * * #MctManager is a top-level management object which is used to query and * monitor #MctAppFilters for different users. * * Since: 0.3.0 */ struct _MctManager { GObject parent_instance; GDBusConnection *connection; /* (owned) */ guint user_changed_id; }; G_DEFINE_TYPE (MctManager, mct_manager, G_TYPE_OBJECT) typedef enum { PROP_CONNECTION = 1, } MctManagerProperty; static GParamSpec *props[PROP_CONNECTION + 1] = { NULL, }; static void mct_manager_init (MctManager *self) { /* Nothing to do here. */ } static void mct_manager_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *spec) { MctManager *self = MCT_MANAGER (object); switch ((MctManagerProperty) property_id) { case PROP_CONNECTION: g_value_set_object (value, self->connection); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec); break; } } static void mct_manager_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *spec) { MctManager *self = MCT_MANAGER (object); switch ((MctManagerProperty) property_id) { case PROP_CONNECTION: /* Construct-only. May be %NULL. */ g_assert (self->connection == NULL); self->connection = g_value_dup_object (value); g_assert (self->connection != NULL); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec); break; } } static void _mct_manager_user_changed_cb (GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data); static void mct_manager_constructed (GObject *object) { MctManager *self = MCT_MANAGER (object); /* Chain up. */ G_OBJECT_CLASS (mct_manager_parent_class)->constructed (object); /* Connect to notifications from AccountsService. */ g_assert (self->connection != NULL); self->user_changed_id = g_dbus_connection_signal_subscribe (self->connection, "org.freedesktop.Accounts", /* sender */ "org.freedesktop.Accounts.User", /* interface name */ "Changed", /* signal name */ NULL, /* object path */ NULL, /* arg0 */ G_DBUS_SIGNAL_FLAGS_NONE, _mct_manager_user_changed_cb, self, NULL); } static void mct_manager_dispose (GObject *object) { MctManager *self = MCT_MANAGER (object); if (self->user_changed_id != 0 && self->connection != NULL) { g_dbus_connection_signal_unsubscribe (self->connection, self->user_changed_id); self->user_changed_id = 0; } g_clear_object (&self->connection); G_OBJECT_CLASS (mct_manager_parent_class)->dispose (object); } static void mct_manager_class_init (MctManagerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructed = mct_manager_constructed; object_class->dispose = mct_manager_dispose; object_class->get_property = mct_manager_get_property; object_class->set_property = mct_manager_set_property; /** * MctManager:connection: * * A connection to the system bus, where accounts-service runs. It’s provided * mostly for testing purposes, or to allow an existing connection to be * re-used. * * Since: 0.3.0 */ props[PROP_CONNECTION] = g_param_spec_object ("connection", "D-Bus Connection", "A connection to the system bus.", G_TYPE_DBUS_CONNECTION, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); /** * MctManager::app-filter-changed: * @self: a #MctManager * @user_id: UID of the user whose app filter has changed * * Emitted when the app filter stored for a user changes. * The new app filter for the user should be requested again from * the #MctManager instance. * * Since: 0.3.0 */ g_signal_new ("app-filter-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT64); } /** * mct_manager_new: * @connection: (transfer none): a #GDBusConnection to use * * Create a new #MctManager. * * Returns: (transfer full): a new #MctManager * Since: 0.3.0 */ MctManager * mct_manager_new (GDBusConnection *connection) { g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL); return g_object_new (MCT_TYPE_MANAGER, "connection", connection, NULL); } static void _mct_manager_user_changed_cb (GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { MctManager *manager = MCT_MANAGER (user_data); g_autoptr(GError) local_error = NULL; const gchar *uid_str; guint64 uid; g_assert (g_str_equal (interface_name, "org.freedesktop.Accounts.User")); g_assert (g_str_equal (signal_name, "Changed")); /* Extract the UID from the object path. This is a bit hacky, but probably * better than depending on libaccountsservice just for this. */ if (!g_str_has_prefix (object_path, "/org/freedesktop/Accounts/User")) return; uid_str = object_path + strlen ("/org/freedesktop/Accounts/User"); if (!g_ascii_string_to_unsigned (uid_str, 10, 0, G_MAXUINT64, &uid, &local_error)) { g_warning ("Error converting object path ‘%s’ to user ID: %s", object_path, local_error->message); g_clear_error (&local_error); } g_signal_emit_by_name (manager, "app-filter-changed", uid); } /** * _mct_app_filter_build_app_filter_variant: * @filter: an #MctAppFilter * * Build a #GVariant which contains the app filter from @filter, in the format * used for storing it in AccountsService. * * Returns: (transfer floating): a new, floating #GVariant containing the app * filter * Since: 0.2.0 */ static GVariant * _mct_app_filter_build_app_filter_variant (MctAppFilter *filter) { g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(bas)")); g_return_val_if_fail (filter != NULL, NULL); g_return_val_if_fail (filter->ref_count >= 1, NULL); g_variant_builder_add (&builder, "b", (filter->app_list_type == MCT_APP_FILTER_LIST_WHITELIST)); g_variant_builder_open (&builder, G_VARIANT_TYPE ("as")); for (gsize i = 0; filter->app_list[i] != NULL; i++) g_variant_builder_add (&builder, "s", filter->app_list[i]); g_variant_builder_close (&builder); return g_variant_builder_end (&builder); } /* Check if @error is a D-Bus remote error matching @expected_error_name. */ static gboolean bus_remote_error_matches (const GError *error, const gchar *expected_error_name) { g_autofree gchar *error_name = NULL; if (!g_dbus_error_is_remote_error (error)) return FALSE; error_name = g_dbus_error_get_remote_error (error); return g_str_equal (error_name, expected_error_name); } /* Convert a #GDBusError into a #MctManagerError. */ static GError * bus_error_to_manager_error (const GError *bus_error, uid_t user_id) { if (g_error_matches (bus_error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED) || bus_remote_error_matches (bus_error, "org.freedesktop.Accounts.Error.PermissionDenied")) return g_error_new (MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED, _("Not allowed to query app filter data for user %u"), (guint) user_id); else if (g_error_matches (bus_error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD) || bus_remote_error_matches (bus_error, "org.freedesktop.Accounts.Error.Failed")) return g_error_new (MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_USER, _("User %u does not exist"), (guint) user_id); else return g_error_copy (bus_error); } /* Find the object path for the given @user_id on the accountsservice D-Bus * interface, by calling its FindUserById() method. This is a synchronous, * blocking function. */ static gchar * accounts_find_user_by_id (GDBusConnection *connection, uid_t user_id, gboolean allow_interactive_authorization, GCancellable *cancellable, GError **error) { g_autofree gchar *object_path = NULL; g_autoptr(GVariant) result_variant = NULL; g_autoptr(GError) local_error = NULL; result_variant = g_dbus_connection_call_sync (connection, "org.freedesktop.Accounts", "/org/freedesktop/Accounts", "org.freedesktop.Accounts", "FindUserById", g_variant_new ("(x)", (gint64) user_id), G_VARIANT_TYPE ("(o)"), allow_interactive_authorization ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ cancellable, &local_error); if (local_error != NULL) { g_autoptr(GError) app_filter_error = bus_error_to_manager_error (local_error, user_id); g_propagate_error (error, g_steal_pointer (&app_filter_error)); return NULL; } g_variant_get (result_variant, "(o)", &object_path); return g_steal_pointer (&object_path); } /** * mct_manager_get_app_filter: * @self: a #MctManager * @user_id: ID of the user to query, typically coming from getuid() * @flags: flags to affect the behaviour of the call * @cancellable: (nullable): a #GCancellable, or %NULL * @error: return location for a #GError, or %NULL * * Synchronous version of mct_manager_get_app_filter_async(). * * Returns: (transfer full): app filter for the queried user * Since: 0.3.0 */ MctAppFilter * mct_manager_get_app_filter (MctManager *self, uid_t user_id, MctManagerGetValueFlags flags, GCancellable *cancellable, GError **error) { g_autofree gchar *object_path = NULL; g_autoptr(GVariant) result_variant = NULL; g_autoptr(GVariant) properties = NULL; g_autoptr(GError) local_error = NULL; g_autoptr(MctAppFilter) app_filter = NULL; gboolean is_whitelist; g_auto(GStrv) app_list = NULL; const gchar *content_rating_kind; g_autoptr(GVariant) oars_variant = NULL; gboolean allow_user_installation; gboolean allow_system_installation; g_return_val_if_fail (MCT_IS_MANAGER (self), NULL); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); object_path = accounts_find_user_by_id (self->connection, user_id, (flags & MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE), cancellable, error); if (object_path == NULL) return NULL; result_variant = g_dbus_connection_call_sync (self->connection, "org.freedesktop.Accounts", object_path, "org.freedesktop.DBus.Properties", "GetAll", g_variant_new ("(s)", "com.endlessm.ParentalControls.AppFilter"), G_VARIANT_TYPE ("(a{sv})"), (flags & MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ cancellable, &local_error); if (local_error != NULL) { g_autoptr(GError) manager_error = NULL; if (g_error_matches (local_error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS)) { /* o.fd.D.GetAll() will return InvalidArgs errors if * accountsservice doesn’t have the com.endlessm.ParentalControls.AppFilter * extension interface installed. */ manager_error = g_error_new_literal (MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_DISABLED, _("App filtering is globally disabled")); } else { manager_error = bus_error_to_manager_error (local_error, user_id); } g_propagate_error (error, g_steal_pointer (&manager_error)); return NULL; } /* Extract the properties we care about. They may be silently omitted from the * results if we don’t have permission to access them. */ properties = g_variant_get_child_value (result_variant, 0); if (!g_variant_lookup (properties, "AppFilter", "(b^as)", &is_whitelist, &app_list)) { g_set_error (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED, _("Not allowed to query app filter data for user %u"), (guint) user_id); return NULL; } if (!g_variant_lookup (properties, "OarsFilter", "(&s@a{ss})", &content_rating_kind, &oars_variant)) { /* Default value. */ content_rating_kind = "oars-1.1"; oars_variant = g_variant_new ("a{ss}", NULL); } /* Check that the OARS filter is in a format we support. Currently, that’s * only oars-1.0 and oars-1.1. */ if (!g_str_equal (content_rating_kind, "oars-1.0") && !g_str_equal (content_rating_kind, "oars-1.1")) { g_set_error (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_DATA, _("OARS filter for user %u has an unrecognized kind ‘%s’"), (guint) user_id, content_rating_kind); return NULL; } if (!g_variant_lookup (properties, "AllowUserInstallation", "b", &allow_user_installation)) { /* Default value. */ allow_user_installation = TRUE; } if (!g_variant_lookup (properties, "AllowSystemInstallation", "b", &allow_system_installation)) { /* Default value. */ allow_system_installation = FALSE; } /* Success. Create an #MctAppFilter object to contain the results. */ app_filter = g_new0 (MctAppFilter, 1); app_filter->ref_count = 1; app_filter->user_id = user_id; app_filter->app_list = g_steal_pointer (&app_list); app_filter->app_list_type = is_whitelist ? MCT_APP_FILTER_LIST_WHITELIST : MCT_APP_FILTER_LIST_BLACKLIST; app_filter->oars_ratings = g_steal_pointer (&oars_variant); app_filter->allow_user_installation = allow_user_installation; app_filter->allow_system_installation = allow_system_installation; return g_steal_pointer (&app_filter); } static void get_app_filter_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable); typedef struct { uid_t user_id; MctManagerGetValueFlags flags; } GetAppFilterData; static void get_app_filter_data_free (GetAppFilterData *data) { g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetAppFilterData, get_app_filter_data_free) /** * mct_manager_get_app_filter_async: * @self: a #MctManager * @user_id: ID of the user to query, typically coming from getuid() * @flags: flags to affect the behaviour of the call * @cancellable: (nullable): a #GCancellable, or %NULL * @callback: a #GAsyncReadyCallback * @user_data: user data to pass to @callback * * Asynchronously get a snapshot of the app filter settings for the given * @user_id. * * On failure, an #MctManagerError, a #GDBusError or a #GIOError will be * returned. * * Since: 0.3.0 */ void mct_manager_get_app_filter_async (MctManager *self, uid_t user_id, MctManagerGetValueFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; g_autoptr(GetAppFilterData) data = NULL; g_return_if_fail (MCT_IS_MANAGER (self)); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, mct_manager_get_app_filter_async); data = g_new0 (GetAppFilterData, 1); data->user_id = user_id; data->flags = flags; g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) get_app_filter_data_free); g_task_run_in_thread (task, get_app_filter_thread_cb); } static void get_app_filter_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { g_autoptr(MctAppFilter) filter = NULL; MctManager *manager = MCT_MANAGER (source_object); GetAppFilterData *data = task_data; g_autoptr(GError) local_error = NULL; filter = mct_manager_get_app_filter (manager, data->user_id, data->flags, cancellable, &local_error); if (local_error != NULL) g_task_return_error (task, g_steal_pointer (&local_error)); else g_task_return_pointer (task, g_steal_pointer (&filter), (GDestroyNotify) mct_app_filter_unref); } /** * mct_manager_get_app_filter_finish: * @self: a #MctManager * @result: a #GAsyncResult * @error: return location for a #GError, or %NULL * * Finish an asynchronous operation to get the app filter for a user, started * with mct_manager_get_app_filter_async(). * * Returns: (transfer full): app filter for the queried user * Since: 0.3.0 */ MctAppFilter * mct_manager_get_app_filter_finish (MctManager *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (MCT_IS_MANAGER (self), NULL); g_return_val_if_fail (g_task_is_valid (result, self), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); return g_task_propagate_pointer (G_TASK (result), error); } /** * mct_manager_set_app_filter: * @self: a #MctManager * @user_id: ID of the user to set the filter for, typically coming from getuid() * @app_filter: (transfer none): the app filter to set for the user * @flags: flags to affect the behaviour of the call * @cancellable: (nullable): a #GCancellable, or %NULL * @error: return location for a #GError, or %NULL * * Synchronous version of mct_manager_set_app_filter_async(). * * Returns: %TRUE on success, %FALSE otherwise * Since: 0.3.0 */ gboolean mct_manager_set_app_filter (MctManager *self, uid_t user_id, MctAppFilter *app_filter, MctManagerSetValueFlags flags, GCancellable *cancellable, GError **error) { g_autofree gchar *object_path = NULL; g_autoptr(GVariant) app_filter_variant = NULL; g_autoptr(GVariant) oars_filter_variant = NULL; g_autoptr(GVariant) allow_user_installation_variant = NULL; g_autoptr(GVariant) allow_system_installation_variant = NULL; g_autoptr(GVariant) app_filter_result_variant = NULL; g_autoptr(GVariant) oars_filter_result_variant = NULL; g_autoptr(GVariant) allow_user_installation_result_variant = NULL; g_autoptr(GVariant) allow_system_installation_result_variant = NULL; g_autoptr(GError) local_error = NULL; g_return_val_if_fail (MCT_IS_MANAGER (self), FALSE); g_return_val_if_fail (app_filter != NULL, FALSE); g_return_val_if_fail (app_filter->ref_count >= 1, FALSE); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); object_path = accounts_find_user_by_id (self->connection, user_id, (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE), cancellable, error); if (object_path == NULL) return FALSE; app_filter_variant = _mct_app_filter_build_app_filter_variant (app_filter); oars_filter_variant = g_variant_new ("(s@a{ss})", "oars-1.1", app_filter->oars_ratings); allow_user_installation_variant = g_variant_new_boolean (app_filter->allow_user_installation); allow_system_installation_variant = g_variant_new_boolean (app_filter->allow_system_installation); app_filter_result_variant = g_dbus_connection_call_sync (self->connection, "org.freedesktop.Accounts", object_path, "org.freedesktop.DBus.Properties", "Set", g_variant_new ("(ssv)", "com.endlessm.ParentalControls.AppFilter", "AppFilter", g_steal_pointer (&app_filter_variant)), G_VARIANT_TYPE ("()"), (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ cancellable, &local_error); if (local_error != NULL) { g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } oars_filter_result_variant = g_dbus_connection_call_sync (self->connection, "org.freedesktop.Accounts", object_path, "org.freedesktop.DBus.Properties", "Set", g_variant_new ("(ssv)", "com.endlessm.ParentalControls.AppFilter", "OarsFilter", g_steal_pointer (&oars_filter_variant)), G_VARIANT_TYPE ("()"), (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ cancellable, &local_error); if (local_error != NULL) { g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } allow_user_installation_result_variant = g_dbus_connection_call_sync (self->connection, "org.freedesktop.Accounts", object_path, "org.freedesktop.DBus.Properties", "Set", g_variant_new ("(ssv)", "com.endlessm.ParentalControls.AppFilter", "AllowUserInstallation", g_steal_pointer (&allow_user_installation_variant)), G_VARIANT_TYPE ("()"), (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ cancellable, &local_error); if (local_error != NULL) { g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } allow_system_installation_result_variant = g_dbus_connection_call_sync (self->connection, "org.freedesktop.Accounts", object_path, "org.freedesktop.DBus.Properties", "Set", g_variant_new ("(ssv)", "com.endlessm.ParentalControls.AppFilter", "AllowSystemInstallation", g_steal_pointer (&allow_system_installation_variant)), G_VARIANT_TYPE ("()"), (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ cancellable, &local_error); if (local_error != NULL) { g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } return TRUE; } static void set_app_filter_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable); typedef struct { uid_t user_id; MctAppFilter *app_filter; /* (owned) */ MctManagerSetValueFlags flags; } SetAppFilterData; static void set_app_filter_data_free (SetAppFilterData *data) { mct_app_filter_unref (data->app_filter); g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (SetAppFilterData, set_app_filter_data_free) /** * mct_manager_set_app_filter_async: * @self: a #MctManager * @user_id: ID of the user to set the filter for, typically coming from getuid() * @app_filter: (transfer none): the app filter to set for the user * @flags: flags to affect the behaviour of the call * @cancellable: (nullable): a #GCancellable, or %NULL * @callback: a #GAsyncReadyCallback * @user_data: user data to pass to @callback * * Asynchronously set the app filter settings for the given @user_id to the * given @app_filter instance. This will set all fields of the app filter. * * On failure, an #MctManagerError, a #GDBusError or a #GIOError will be * returned. The user’s app filter settings will be left in an undefined state. * * Since: 0.3.0 */ void mct_manager_set_app_filter_async (MctManager *self, uid_t user_id, MctAppFilter *app_filter, MctManagerSetValueFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; g_autoptr(SetAppFilterData) data = NULL; g_return_if_fail (MCT_IS_MANAGER (self)); g_return_if_fail (app_filter != NULL); g_return_if_fail (app_filter->ref_count >= 1); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, mct_manager_set_app_filter_async); data = g_new0 (SetAppFilterData, 1); data->user_id = user_id; data->app_filter = mct_app_filter_ref (app_filter); data->flags = flags; g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) set_app_filter_data_free); g_task_run_in_thread (task, set_app_filter_thread_cb); } static void set_app_filter_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { gboolean success; MctManager *manager = MCT_MANAGER (source_object); SetAppFilterData *data = task_data; g_autoptr(GError) local_error = NULL; success = mct_manager_set_app_filter (manager, data->user_id, data->app_filter, data->flags, cancellable, &local_error); if (local_error != NULL) g_task_return_error (task, g_steal_pointer (&local_error)); else g_task_return_boolean (task, success); } /** * mct_manager_set_app_filter_finish: * @self: a #MctManager * @result: a #GAsyncResult * @error: return location for a #GError, or %NULL * * Finish an asynchronous operation to set the app filter for a user, started * with mct_manager_set_app_filter_async(). * * Returns: %TRUE on success, %FALSE otherwise * Since: 0.3.0 */ gboolean mct_manager_set_app_filter_finish (MctManager *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (MCT_IS_MANAGER (self), FALSE); g_return_val_if_fail (g_task_is_valid (result, self), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean (G_TASK (result), error); } /** * mct_manager_get_session_limits: * @self: a #MctManager * @user_id: ID of the user to query, typically coming from getuid() * @flags: flags to affect the behaviour of the call * @cancellable: (nullable): a #GCancellable, or %NULL * @error: return location for a #GError, or %NULL * * Synchronous version of mct_manager_get_session_limits_async(). * * Returns: (transfer full): session limits for the queried user * Since: 0.5.0 */ MctSessionLimits * mct_manager_get_session_limits (MctManager *self, uid_t user_id, MctManagerGetValueFlags flags, GCancellable *cancellable, GError **error) { g_autofree gchar *object_path = NULL; g_autoptr(GVariant) result_variant = NULL; g_autoptr(GVariant) properties = NULL; g_autoptr(GError) local_error = NULL; g_autoptr(MctSessionLimits) session_limits = NULL; guint32 limit_type; guint32 daily_start_time, daily_end_time; g_return_val_if_fail (MCT_IS_MANAGER (self), NULL); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); object_path = accounts_find_user_by_id (self->connection, user_id, (flags & MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE), cancellable, error); if (object_path == NULL) return NULL; result_variant = g_dbus_connection_call_sync (self->connection, "org.freedesktop.Accounts", object_path, "org.freedesktop.DBus.Properties", "GetAll", g_variant_new ("(s)", "com.endlessm.ParentalControls.SessionLimits"), G_VARIANT_TYPE ("(a{sv})"), (flags & MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ cancellable, &local_error); if (local_error != NULL) { g_autoptr(GError) manager_error = NULL; if (g_error_matches (local_error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS)) { /* o.fd.D.GetAll() will return InvalidArgs errors if * accountsservice doesn’t have the com.endlessm.ParentalControls.SessionLimits * extension interface installed. */ manager_error = g_error_new_literal (MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_DISABLED, _("Session limits are globally disabled")); } else { manager_error = bus_error_to_manager_error (local_error, user_id); } g_propagate_error (error, g_steal_pointer (&manager_error)); return NULL; } /* Extract the properties we care about. They may be silently omitted from the * results if we don’t have permission to access them. */ properties = g_variant_get_child_value (result_variant, 0); if (!g_variant_lookup (properties, "LimitType", "u", &limit_type)) { g_set_error (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED, _("Not allowed to query session limits data for user %u"), (guint) user_id); return NULL; } /* Check that the limit type is something we support. */ G_STATIC_ASSERT (sizeof (limit_type) >= sizeof (MctSessionLimitsType)); if ((guint) limit_type > MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE) { g_set_error (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_DATA, _("Session limit for user %u has an unrecognized type ‘%u’"), (guint) user_id, limit_type); return NULL; } if (!g_variant_lookup (properties, "DailySchedule", "(uu)", &daily_start_time, &daily_end_time)) { /* Default value. */ daily_start_time = 0; daily_end_time = 24 * 60 * 60; } if (daily_start_time >= daily_end_time || daily_end_time > 24 * 60 * 60) { g_set_error (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_DATA, _("Session limit for user %u has invalid daily schedule %u–%u"), (guint) user_id, daily_start_time, daily_end_time); return NULL; } /* Success. Create an #MctSessionLimits object to contain the results. */ session_limits = g_new0 (MctSessionLimits, 1); session_limits->ref_count = 1; session_limits->user_id = user_id; session_limits->limit_type = limit_type; session_limits->daily_start_time = daily_start_time; session_limits->daily_end_time = daily_end_time; return g_steal_pointer (&session_limits); } static void get_session_limits_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable); typedef struct { uid_t user_id; MctManagerGetValueFlags flags; } GetSessionLimitsData; static void get_session_limits_data_free (GetSessionLimitsData *data) { g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetSessionLimitsData, get_session_limits_data_free) /** * mct_manager_get_session_limits_async: * @self: a #MctManager * @user_id: ID of the user to query, typically coming from getuid() * @flags: flags to affect the behaviour of the call * @cancellable: (nullable): a #GCancellable, or %NULL * @callback: a #GAsyncReadyCallback * @user_data: user data to pass to @callback * * Asynchronously get a snapshot of the session limit settings for the given * @user_id. * * On failure, an #MctManagerError, a #GDBusError or a #GIOError will be * returned via mct_manager_get_session_limits_finish(). * * Since: 0.5.0 */ void mct_manager_get_session_limits_async (MctManager *self, uid_t user_id, MctManagerGetValueFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; g_autoptr(GetSessionLimitsData) data = NULL; g_return_if_fail (MCT_IS_MANAGER (self)); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, mct_manager_get_session_limits_async); data = g_new0 (GetSessionLimitsData, 1); data->user_id = user_id; data->flags = flags; g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) get_session_limits_data_free); g_task_run_in_thread (task, get_session_limits_thread_cb); } static void get_session_limits_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { g_autoptr(MctSessionLimits) limits = NULL; MctManager *manager = MCT_MANAGER (source_object); GetSessionLimitsData *data = task_data; g_autoptr(GError) local_error = NULL; limits = mct_manager_get_session_limits (manager, data->user_id, data->flags, cancellable, &local_error); if (local_error != NULL) g_task_return_error (task, g_steal_pointer (&local_error)); else g_task_return_pointer (task, g_steal_pointer (&limits), (GDestroyNotify) mct_session_limits_unref); } /** * mct_manager_get_session_limits_finish: * @self: a #MctManager * @result: a #GAsyncResult * @error: return location for a #GError, or %NULL * * Finish an asynchronous operation to get the session limits for a user, * started with mct_manager_get_session_limits_async(). * * Returns: (transfer full): session limits for the queried user * Since: 0.5.0 */ MctSessionLimits * mct_manager_get_session_limits_finish (MctManager *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (MCT_IS_MANAGER (self), NULL); g_return_val_if_fail (g_task_is_valid (result, self), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); return g_task_propagate_pointer (G_TASK (result), error); } /** * mct_manager_set_session_limits: * @self: a #MctManager * @user_id: ID of the user to set the limits for, typically coming from getuid() * @session_limits: (transfer none): the session limits to set for the user * @flags: flags to affect the behaviour of the call * @cancellable: (nullable): a #GCancellable, or %NULL * @error: return location for a #GError, or %NULL * * Synchronous version of mct_manager_set_session_limits_async(). * * Returns: %TRUE on success, %FALSE otherwise * Since: 0.5.0 */ gboolean mct_manager_set_session_limits (MctManager *self, uid_t user_id, MctSessionLimits *session_limits, MctManagerSetValueFlags flags, GCancellable *cancellable, GError **error) { g_autofree gchar *object_path = NULL; g_autoptr(GVariant) limit_variant = NULL; const gchar *limit_property_name = NULL; g_autoptr(GVariant) limit_type_variant = NULL; g_autoptr(GVariant) limit_result_variant = NULL; g_autoptr(GVariant) limit_type_result_variant = NULL; g_autoptr(GError) local_error = NULL; g_return_val_if_fail (MCT_IS_MANAGER (self), FALSE); g_return_val_if_fail (session_limits != NULL, FALSE); g_return_val_if_fail (session_limits->ref_count >= 1, FALSE); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); object_path = accounts_find_user_by_id (self->connection, user_id, (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE), cancellable, error); if (object_path == NULL) return FALSE; switch (session_limits->limit_type) { case MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE: limit_variant = g_variant_new ("(uu)", session_limits->daily_start_time, session_limits->daily_end_time); limit_property_name = "DailySchedule"; break; case MCT_SESSION_LIMITS_TYPE_NONE: limit_variant = NULL; limit_property_name = NULL; break; default: g_assert_not_reached (); } limit_type_variant = g_variant_new_uint32 (session_limits->limit_type); if (limit_property_name != NULL) { /* Change the details of the new limit first, so that all the properties are * correct by the time the limit type is changed over. */ limit_result_variant = g_dbus_connection_call_sync (self->connection, "org.freedesktop.Accounts", object_path, "org.freedesktop.DBus.Properties", "Set", g_variant_new ("(ssv)", "com.endlessm.ParentalControls.SessionLimits", limit_property_name, g_steal_pointer (&limit_variant)), G_VARIANT_TYPE ("()"), (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ cancellable, &local_error); if (local_error != NULL) { g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } } limit_type_result_variant = g_dbus_connection_call_sync (self->connection, "org.freedesktop.Accounts", object_path, "org.freedesktop.DBus.Properties", "Set", g_variant_new ("(ssv)", "com.endlessm.ParentalControls.SessionLimits", "LimitType", g_steal_pointer (&limit_type_variant)), G_VARIANT_TYPE ("()"), (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ cancellable, &local_error); if (local_error != NULL) { g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } return TRUE; } static void set_session_limits_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable); typedef struct { uid_t user_id; MctSessionLimits *session_limits; /* (owned) */ MctManagerSetValueFlags flags; } SetSessionLimitsData; static void set_session_limits_data_free (SetSessionLimitsData *data) { mct_session_limits_unref (data->session_limits); g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (SetSessionLimitsData, set_session_limits_data_free) /** * mct_manager_set_session_limits_async: * @self: a #MctManager * @user_id: ID of the user to set the limits for, typically coming from getuid() * @session_limits: (transfer none): the session limits to set for the user * @flags: flags to affect the behaviour of the call * @cancellable: (nullable): a #GCancellable, or %NULL * @callback: a #GAsyncReadyCallback * @user_data: user data to pass to @callback * * Asynchronously set the session limits settings for the given @user_id to the * given @session_limits instance. * * On failure, an #MctManagerError, a #GDBusError or a #GIOError will be * returned via mct_manager_set_session_limits_finish(). The user’s session * limits settings will be left in an undefined state. * * Since: 0.5.0 */ void mct_manager_set_session_limits_async (MctManager *self, uid_t user_id, MctSessionLimits *session_limits, MctManagerSetValueFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; g_autoptr(SetSessionLimitsData) data = NULL; g_return_if_fail (MCT_IS_MANAGER (self)); g_return_if_fail (session_limits != NULL); g_return_if_fail (session_limits->ref_count >= 1); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, mct_manager_set_session_limits_async); data = g_new0 (SetSessionLimitsData, 1); data->user_id = user_id; data->session_limits = mct_session_limits_ref (session_limits); data->flags = flags; g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) set_session_limits_data_free); g_task_run_in_thread (task, set_session_limits_thread_cb); } static void set_session_limits_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { gboolean success; MctManager *manager = MCT_MANAGER (source_object); SetSessionLimitsData *data = task_data; g_autoptr(GError) local_error = NULL; success = mct_manager_set_session_limits (manager, data->user_id, data->session_limits, data->flags, cancellable, &local_error); if (local_error != NULL) g_task_return_error (task, g_steal_pointer (&local_error)); else g_task_return_boolean (task, success); } /** * mct_manager_set_session_limits_finish: * @self: a #MctManager * @result: a #GAsyncResult * @error: return location for a #GError, or %NULL * * Finish an asynchronous operation to set the session limits for a user, * started with mct_manager_set_session_limits_async(). * * Returns: %TRUE on success, %FALSE otherwise * Since: 0.5.0 */ gboolean mct_manager_set_session_limits_finish (MctManager *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (MCT_IS_MANAGER (self), FALSE); g_return_val_if_fail (g_task_is_valid (result, self), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean (G_TASK (result), error); } G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (sd_journal, sd_journal_close) /* TODO Docs */ MctSessionHistory * mct_manager_get_session_history (MctManager *self, uid_t user_id, GDateTime *start, GDateTime *end, MctManagerGetValueFlags flags, GCancellable *cancellable, GError **error) { g_auto(sd_journal) *journal = NULL; int ret; const gchar * const match_message_ids[] = { "MESSAGE_ID=8d45620c1a4348dbb17410da57c60c66", /* new session */ "MESSAGE_ID=3354939424b4456d9802ca8333ed424a", /* session ended */ "MESSAGE_ID=6bbd95ee977941e497c48be27c254128", /* entering sleep */ "MESSAGE_ID=8811e6df2a8e40f58a94cea26f8ebf14", /* leaving sleep */ "MESSAGE_ID=b07a249cd024414a82dd00cd181378ff", /* startup complete */ "MESSAGE_ID=98268866d1d54a499c4e98921d93bc40", /* starting to shut down */ /* TODO: need to add messages to gnome-shell for when the user locks and unlocks the screen, * including logging in and out and switching user */ }; const size_t message_id_len = sizeof (match_message_ids[0]); g_autoptr(GPtrArray) active_periods = g_ptr_array_new_with_free_func (NULL); /* TODO */ g_return_val_if_fail (MCT_IS_MANAGER (self), NULL); g_return_val_if_fail (start != NULL, NULL); g_return_val_if_fail (end != NULL, NULL); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); ret = sd_journal_open (&journal, SD_JOURNAL_SYSTEM); if (ret < 0) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Error opening journal: %s", g_strerror (-ret)); return NULL; } /* TODO: work out what data threshold is needed and set with sd_journal_set_data_threshold() */ /* Add matches. */ for (gsize i = 0; i < G_N_ELEMENTS (match_message_ids); i++) { ret = sd_journal_add_match (journal, "MESSAGE_ID=TODO", 0); if (ret < 0) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, /* TODO More specific error codes throughout */ "Error applying matches in journal: %s", g_strerror (-ret)); return NULL; } } ret = sd_journal_seek_realtime_usec (journal, (uint64_t) g_date_time_to_unix (start) * G_USEC_PER_SEC); if (ret < 0) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, /* TODO More specific error codes throughout */ "Error finding start entry in journal: %s", g_strerror (-ret)); return NULL; } while (TRUE) { uint64_t entry_realtime = 0; const void *data = NULL; size_t data_len = 0; ret = sd_journal_next (journal); if (ret < 0) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Error seeking journal: %s", g_strerror (-ret)); return NULL; } else if (ret == 0) { /* Reached EOF. */ break; } ret = sd_journal_get_realtime_usec (journal, &entry_realtime); if (ret < 0) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Error reading journal: %s", g_strerror (-ret)); return NULL; } if (entry_realtime / G_USEC_PER_SEC >= g_date_time_to_unix (end)) { /* Reached the end of the search range. */ break; } /* Check the type of the record. */ ret = sd_journal_get_data (journal, "MESSAGE_ID", &data, &data_len); if (ret < 0) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Error reading journal: %s", g_strerror (-ret)); return NULL; } if (data_len != message_id_len) { g_debug ("Unexpected field %s, ignoring", data); continue; } /* TODO: need to filter by uid */ if (memcmp (data, "MESSAGE_ID=8d45620c1a4348dbb17410da57c60c66", message_id_len) == 0) { /* New session. */ g_ptr_array_add (active_periods, TODO); } /* TODO */ "MESSAGE_ID=3354939424b4456d9802ca8333ed424a", /* session ended */ "MESSAGE_ID=6bbd95ee977941e497c48be27c254128", /* entering sleep */ "MESSAGE_ID=8811e6df2a8e40f58a94cea26f8ebf14", /* leaving sleep */ "MESSAGE_ID=b07a249cd024414a82dd00cd181378ff", /* startup complete */ "MESSAGE_ID=98268866d1d54a499c4e98921d93bc40", /* starting to shut down */ } }