summaryrefslogtreecommitdiffstats
path: root/plugins/malcontent
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--plugins/malcontent/gs-plugin-malcontent.c343
-rw-r--r--plugins/malcontent/meson.build17
2 files changed, 360 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..d474168
--- /dev/null
+++ b/plugins/malcontent/gs-plugin-malcontent.c
@@ -0,0 +1,343 @@
+/* -*- 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>
+
+/*
+ * 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 GsPluginData {
+ GMutex mutex; /* protects @app_filter **/
+ MctManager *manager; /* (owned) */
+ gulong manager_app_filter_changed_id;
+ MctAppFilter *app_filter; /* (mutex) (owned) (nullable) */
+};
+
+/* 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_APP_KIND_ADDON:
+ case AS_APP_KIND_CODEC:
+ case AS_APP_KIND_DRIVER:
+ case AS_APP_KIND_FIRMWARE:
+ case AS_APP_KIND_FONT:
+ case AS_APP_KIND_GENERIC:
+ case AS_APP_KIND_INPUT_METHOD:
+ case AS_APP_KIND_LOCALIZATION:
+ case AS_APP_KIND_OS_UPDATE:
+ case AS_APP_KIND_OS_UPGRADE:
+ case AS_APP_KIND_RUNTIME:
+ case AS_APP_KIND_SOURCE:
+ return FALSE;
+ case AS_APP_KIND_UNKNOWN:
+ case AS_APP_KIND_DESKTOP:
+ case AS_APP_KIND_WEB_APP:
+ case AS_APP_KIND_SHELL_EXTENSION:
+ case AS_APP_KIND_CONSOLE:
+ 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/master/specification/oars-1.1.md */
+static gboolean
+app_is_content_rating_appropriate (GsApp *app, MctAppFilter *app_filter)
+{
+ AsContentRating *rating = gs_app_get_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 (GsPlugin *plugin, 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 MctAppFilter *
+query_app_filter (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ return mct_manager_get_app_filter (priv->manager, getuid (),
+ MCT_GET_APP_FILTER_FLAGS_INTERACTIVE, cancellable,
+ error);
+}
+
+static gboolean
+reload_app_filter (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(MctAppFilter) new_app_filter = NULL;
+ g_autoptr(MctAppFilter) old_app_filter = NULL;
+
+ /* Refresh the app filter. This blocks on a D-Bus request. */
+ new_app_filter = query_app_filter (plugin, cancellable, 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 (&priv->mutex);
+ old_app_filter = g_steal_pointer (&priv->app_filter);
+ priv->app_filter = g_steal_pointer (&new_app_filter);
+ }
+
+ return TRUE;
+}
+
+static void
+app_filter_changed_cb (MctManager *manager,
+ guint64 user_id,
+ gpointer user_data)
+{
+ GsPlugin *plugin = GS_PLUGIN (user_data);
+ g_autoptr(GError) error_local = NULL;
+
+ 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);
+ if (reload_app_filter (plugin, NULL, &error_local))
+ gs_plugin_reload (plugin);
+ else
+ g_warning ("Failed to reload changed app filter: %s", error_local->message);
+}
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+ gs_plugin_alloc_data (plugin, sizeof (GsPluginData));
+
+ /* 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");
+}
+
+gboolean
+gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+ g_autoptr(GDBusConnection) system_bus = NULL;
+
+ system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, cancellable, error);
+ if (system_bus == NULL)
+ return FALSE;
+
+ priv->manager = mct_manager_new (system_bus);
+ priv->manager_app_filter_changed_id = g_signal_connect (priv->manager,
+ "app-filter-changed",
+ (GCallback) app_filter_changed_cb,
+ plugin);
+ priv->app_filter = query_app_filter (plugin, cancellable, error);
+
+ return (priv->app_filter != NULL);
+}
+
+static gboolean
+refine_app_locked (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ /* 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 (priv->app_filter != NULL);
+
+ app_set_parental_quirks (plugin, app, priv->app_filter);
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ if (!refine_app_locked (plugin, app, flags, cancellable, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_refresh (GsPlugin *plugin,
+ guint cache_age,
+ GCancellable *cancellable,
+ GError **error)
+{
+ return reload_app_filter (plugin, cancellable, error);
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ g_clear_pointer (&priv->app_filter, mct_app_filter_unref);
+ if (priv->manager != NULL && priv->manager_app_filter_changed_id != 0) {
+ g_signal_handler_disconnect (priv->manager,
+ priv->manager_app_filter_changed_id);
+ priv->manager_app_filter_changed_id = 0;
+ }
+ g_clear_object (&priv->manager);
+} \ No newline at end of file
diff --git a/plugins/malcontent/meson.build b/plugins/malcontent/meson.build
new file mode 100644
index 0000000..ea0740a
--- /dev/null
+++ b/plugins/malcontent/meson.build
@@ -0,0 +1,17 @@
+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 ],
+ link_with : [
+ libgnomesoftware,
+ ],
+) \ No newline at end of file