diff options
Diffstat (limited to '')
-rw-r--r-- | plugins/malcontent/gs-plugin-malcontent.c | 480 | ||||
-rw-r--r-- | plugins/malcontent/gs-plugin-malcontent.h | 22 | ||||
-rw-r--r-- | plugins/malcontent/meson.build | 14 |
3 files changed, 516 insertions, 0 deletions
diff --git a/plugins/malcontent/gs-plugin-malcontent.c b/plugins/malcontent/gs-plugin-malcontent.c new file mode 100644 index 0000000..d8825a9 --- /dev/null +++ b/plugins/malcontent/gs-plugin-malcontent.c @@ -0,0 +1,480 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018-2019 Endless Mobile + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <gnome-software.h> +#include <libmalcontent/malcontent.h> +#include <string.h> +#include <math.h> + +#include "gs-plugin-malcontent.h" +#include "gs-plugin-private.h" + +/* + * SECTION: + * Adds the %GS_APP_QUIRK_PARENTAL_FILTER and + * %GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE quirks to applications if they + * contravene the effective user’s current parental controls policy. + * + * Specifically, %GS_APP_QUIRK_PARENTAL_FILTER will be added if an app’s OARS + * rating is too extreme for the current parental controls OARS policy. + * %GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE will be added if the app is listed on + * the current parental controls blocklist. + * + * Parental controls policy is loaded using libmalcontent. + * + * This plugin is ordered after flatpak and appstream as it uses OARS data from + * them. + * + * Limiting access to applications by not allowing them to be launched by + * gnome-software is only one part of a wider approach to parental controls. + * In order to guarantee users do not have access to applications they shouldn’t + * have access to, an LSM (such as AppArmor) needs to be used. That complements, + * rather than substitutes for, filtering in user visible UIs. + */ + +struct _GsPluginMalcontent { + GsPlugin parent; + + GMutex mutex; /* protects @app_filter **/ + MctManager *manager; /* (owned) */ + gulong manager_app_filter_changed_id; + MctAppFilter *app_filter; /* (mutex) (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsPluginMalcontent, gs_plugin_malcontent, GS_TYPE_PLUGIN) + +/* Convert an #MctAppFilterOarsValue to an #AsContentRatingValue. This is + * actually a trivial cast, since the types are defined the same; but throw in + * a static assertion to be sure. */ +static AsContentRatingValue +convert_app_filter_oars_value (MctAppFilterOarsValue filter_value) +{ + G_STATIC_ASSERT (AS_CONTENT_RATING_VALUE_LAST == MCT_APP_FILTER_OARS_VALUE_INTENSE + 1); + + return (AsContentRatingValue) filter_value; +} + +static gboolean +app_is_expected_to_have_content_rating (GsApp *app) +{ + if (gs_app_has_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE)) + return FALSE; + + switch (gs_app_get_kind (app)) { + case AS_COMPONENT_KIND_ADDON: + case AS_COMPONENT_KIND_CODEC: + case AS_COMPONENT_KIND_DRIVER: + case AS_COMPONENT_KIND_FIRMWARE: + case AS_COMPONENT_KIND_FONT: + case AS_COMPONENT_KIND_GENERIC: + case AS_COMPONENT_KIND_INPUT_METHOD: + case AS_COMPONENT_KIND_LOCALIZATION: + case AS_COMPONENT_KIND_OPERATING_SYSTEM: + case AS_COMPONENT_KIND_RUNTIME: + case AS_COMPONENT_KIND_REPOSITORY: + return FALSE; + case AS_COMPONENT_KIND_UNKNOWN: + case AS_COMPONENT_KIND_DESKTOP_APP: + case AS_COMPONENT_KIND_WEB_APP: + case AS_COMPONENT_KIND_CONSOLE_APP: + default: + break; + } + + return TRUE; +} + +/* Check whether the OARS rating for @app is as, or less, extreme than the + * user’s preferences in @app_filter. If so (i.e. if the app is suitable for + * this user to use), return %TRUE; otherwise return %FALSE. + * + * The #AsContentRating in @app may be %NULL if no OARS ratings are provided for + * the app. If so, we have to assume the most restrictive ratings. However, if + * @rating is provided but is empty, we assume that every section in it has + * value %AS_CONTENT_RATING_VALUE_NONE. See + * https://github.com/hughsie/oars/blob/HEAD/specification/oars-1.1.md */ +static gboolean +app_is_content_rating_appropriate (GsApp *app, MctAppFilter *app_filter) +{ + g_autoptr(AsContentRating) rating = gs_app_dup_content_rating (app); /* (nullable) */ + g_autofree const gchar **oars_sections = mct_app_filter_get_oars_sections (app_filter); + AsContentRatingValue default_rating_value; + + if (rating == NULL && !app_is_expected_to_have_content_rating (app)) { + /* Some apps, such as flatpak runtimes, are not expected to have + * content ratings. */ + return TRUE; + } else if (rating == NULL) { + g_debug ("No OARS ratings provided for ‘%s’: assuming most extreme", + gs_app_get_unique_id (app)); + default_rating_value = AS_CONTENT_RATING_VALUE_INTENSE; + } else { + default_rating_value = AS_CONTENT_RATING_VALUE_NONE; + } + + for (gsize i = 0; oars_sections[i] != NULL; i++) { + AsContentRatingValue rating_value; + MctAppFilterOarsValue filter_value; + + filter_value = mct_app_filter_get_oars_value (app_filter, oars_sections[i]); + + if (rating != NULL) + rating_value = as_content_rating_get_value (rating, oars_sections[i]); + else + rating_value = AS_CONTENT_RATING_VALUE_UNKNOWN; + + if (rating_value == AS_CONTENT_RATING_VALUE_UNKNOWN) + rating_value = default_rating_value; + + if (filter_value == MCT_APP_FILTER_OARS_VALUE_UNKNOWN) + continue; + else if (convert_app_filter_oars_value (filter_value) < rating_value) + return FALSE; + } + + return TRUE; +} + +static gboolean +app_is_parentally_blocklisted (GsApp *app, MctAppFilter *app_filter) +{ + const gchar *desktop_id; + g_autoptr(GAppInfo) appinfo = NULL; + + desktop_id = gs_app_get_id (app); + if (desktop_id == NULL) + return FALSE; + appinfo = G_APP_INFO (gs_utils_get_desktop_app_info (desktop_id)); + if (appinfo == NULL) + return FALSE; + + return !mct_app_filter_is_appinfo_allowed (app_filter, appinfo); +} + +static gboolean +app_set_parental_quirks (GsPluginMalcontent *self, + GsApp *app, + MctAppFilter *app_filter) +{ + /* note that both quirks can be set on an app at the same time, and they + * have slightly different meanings */ + gboolean filtered = FALSE; + + /* check the OARS ratings to see if this app should be installable */ + if (!app_is_content_rating_appropriate (app, app_filter)) { + g_debug ("Filtering ‘%s’: app OARS rating is too extreme for this user", + gs_app_get_unique_id (app)); + gs_app_add_quirk (app, GS_APP_QUIRK_PARENTAL_FILTER); + filtered = TRUE; + } else { + gs_app_remove_quirk (app, GS_APP_QUIRK_PARENTAL_FILTER); + } + + /* check the app blocklist to see if this app should be launchable */ + if (app_is_parentally_blocklisted (app, app_filter)) { + g_debug ("Filtering ‘%s’: app is blocklisted for this user", + gs_app_get_unique_id (app)); + gs_app_add_quirk (app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE); + filtered = TRUE; + } else { + gs_app_remove_quirk (app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE); + } + + return filtered; +} + +static void +reload_app_filter_async (GsPluginMalcontent *self, + gboolean interactive, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + /* Refresh the app filter. This causes a D-Bus request. */ + mct_manager_get_app_filter_async (self->manager, + getuid (), + interactive ? MCT_GET_APP_FILTER_FLAGS_INTERACTIVE : MCT_GET_APP_FILTER_FLAGS_NONE, + cancellable, + callback, + user_data); +} + +static gboolean +reload_app_filter_finish (GsPluginMalcontent *self, + GAsyncResult *result, + GError **error) +{ + g_autoptr(MctAppFilter) new_app_filter = NULL; + g_autoptr(MctAppFilter) old_app_filter = NULL; + + new_app_filter = mct_manager_get_app_filter_finish (self->manager, + result, + error); + + /* on failure, keep the old app filter around since it might be more + * useful than nothing */ + if (new_app_filter == NULL) + return FALSE; + + { + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex); + old_app_filter = g_steal_pointer (&self->app_filter); + self->app_filter = g_steal_pointer (&new_app_filter); + } + + return TRUE; +} + +static void reload_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +app_filter_changed_cb (MctManager *manager, + guint64 user_id, + gpointer user_data) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (user_data); + + if (user_id != getuid ()) + return; + + /* The user’s app filter has changed, which means that different + * apps could be filtered from before. Reload everything to be + * sure of re-filtering correctly. */ + g_debug ("Reloading due to app filter changing for user %" G_GUINT64_FORMAT, user_id); + reload_app_filter_async (self, FALSE, NULL, reload_cb, g_object_ref (self)); +} + +static void +reload_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GsPluginMalcontent) self = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + if (reload_app_filter_finish (self, result, &local_error)) + gs_plugin_reload (GS_PLUGIN (self)); + else + g_warning ("Failed to reload changed app filter: %s", local_error->message); +} + +static void +gs_plugin_malcontent_init (GsPluginMalcontent *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + /* need application IDs and content ratings */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "flatpak"); + + /* set plugin name; it’s not a loadable plugin, but this is descriptive and harmless */ + gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Malcontent"); +} + +static void get_app_filter_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_malcontent_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_malcontent_setup_async); + + self->manager = mct_manager_new (gs_plugin_get_system_bus_connection (plugin)); + self->manager_app_filter_changed_id = g_signal_connect (self->manager, + "app-filter-changed", + (GCallback) app_filter_changed_cb, + self); + + mct_manager_get_app_filter_async (self->manager, getuid (), + /* FIXME: Should this be unconditionally interactive? */ + MCT_GET_APP_FILTER_FLAGS_INTERACTIVE, cancellable, + get_app_filter_cb, + g_steal_pointer (&task)); +} + +static void +get_app_filter_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginMalcontent *self = g_task_get_source_object (task); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex); + g_autoptr(GError) local_error = NULL; + + self->app_filter = mct_manager_get_app_filter_finish (self->manager, result, &local_error); + if (self->app_filter == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_malcontent_setup_finish (GsPlugin *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +refine_app_locked (GsPluginMalcontent *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* not valid */ + if (gs_app_get_id (app) == NULL) + return TRUE; + + /* Filter by various parental filters. The filter can’t be %NULL, + * otherwise setup() would have failed and the plugin would have been + * disabled. */ + g_assert (self->app_filter != NULL); + + app_set_parental_quirks (self, app, self->app_filter); + + return TRUE; +} + +static void +gs_plugin_malcontent_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (plugin); + g_autoptr(GTask) task = NULL; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex); + g_autoptr(GError) local_error = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_malcontent_refine_async); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app_locked (self, app, flags, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_malcontent_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void refresh_metadata_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_malcontent_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_malcontent_refresh_metadata_async); + + reload_app_filter_async (self, + (flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE), + cancellable, + refresh_metadata_cb, + g_steal_pointer (&task)); +} + +static void +refresh_metadata_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginMalcontent *self = g_task_get_source_object (task); + g_autoptr(GError) local_error = NULL; + + if (reload_app_filter_finish (self, result, &local_error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&local_error)); +} + +static gboolean +gs_plugin_malcontent_refresh_metadata_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_malcontent_dispose (GObject *object) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (object); + + g_clear_pointer (&self->app_filter, mct_app_filter_unref); + if (self->manager != NULL && self->manager_app_filter_changed_id != 0) { + g_signal_handler_disconnect (self->manager, + self->manager_app_filter_changed_id); + self->manager_app_filter_changed_id = 0; + } + g_clear_object (&self->manager); + + G_OBJECT_CLASS (gs_plugin_malcontent_parent_class)->dispose (object); +} + +static void +gs_plugin_malcontent_class_init (GsPluginMalcontentClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_malcontent_dispose; + + plugin_class->setup_async = gs_plugin_malcontent_setup_async; + plugin_class->setup_finish = gs_plugin_malcontent_setup_finish; + plugin_class->refine_async = gs_plugin_malcontent_refine_async; + plugin_class->refine_finish = gs_plugin_malcontent_refine_finish; + plugin_class->refresh_metadata_async = gs_plugin_malcontent_refresh_metadata_async; + plugin_class->refresh_metadata_finish = gs_plugin_malcontent_refresh_metadata_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_MALCONTENT; +} diff --git a/plugins/malcontent/gs-plugin-malcontent.h b/plugins/malcontent/gs-plugin-malcontent.h new file mode 100644 index 0000000..11f9a4a --- /dev/null +++ b/plugins/malcontent/gs-plugin-malcontent.h @@ -0,0 +1,22 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_MALCONTENT (gs_plugin_malcontent_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginMalcontent, gs_plugin_malcontent, GS, PLUGIN_MALCONTENT, GsPlugin) + +G_END_DECLS diff --git a/plugins/malcontent/meson.build b/plugins/malcontent/meson.build new file mode 100644 index 0000000..149fde6 --- /dev/null +++ b/plugins/malcontent/meson.build @@ -0,0 +1,14 @@ +c_args = ['-DG_LOG_DOMAIN="GsPluginMalcontent"'] + +shared_module( + 'gs_plugin_malcontent', + sources : 'gs-plugin-malcontent.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : c_args, + dependencies : [ plugin_libs, malcontent ], +) |