From 58dc261e55c56b389bc257d6c9c270fd945e2487 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 12 Mar 2019 18:12:48 +0000 Subject: [PATCH 1/4] details: Hide add/remove shortcut buttons for parentally filtered apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There’s no point in being able to add a shortcut to the desktop for an app if you are not allowed to launch it. Signed-off-by: Philip Withnall --- src/gs-details-page.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/gs-details-page.c b/src/gs-details-page.c index f2be29da3..62c28bcad 100644 --- a/src/gs-details-page.c +++ b/src/gs-details-page.c @@ -200,6 +200,11 @@ gs_details_page_update_shortcut_button (GsDetailsPage *self) if (gs_app_get_kind (self->app) != AS_APP_KIND_DESKTOP) return; + /* Leave the button hidden if the app can’t be launched by the current + * user. */ + if (gs_app_has_quirk (self->app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE)) + return; + /* only consider the shortcut button if the app is installed */ switch (gs_app_get_state (self->app)) { case AS_APP_STATE_INSTALLED: -- GitLab From 9301d237b6945c683d8d04dfd7c492257d0ca144 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 12 Mar 2019 18:16:20 +0000 Subject: [PATCH 2/4] malcontent: Add new plugin for restricting access to apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a new plugin which loads the user’s parental controls settings (comprising a maximum OARS rating for apps to show, and a blacklist of apps to not allow launching) and use them to filter which apps are shown. The OARS rating filters non-installed apps so the user can only install apps which are age-appropriate for them. The blacklist filters installed apps so that the user cannot run inappropriate apps installed for other users. These are implemented using the parental controls quirks. The UI already pays attention to these quirks, and hides various buttons. This adds an optional dependency on libmalcontent (https://gitlab.freedesktop.org/pwithnall/malcontent). It is part of a wider approach to implementing parental controls, which includes filtering in the `flatpak` command, in gnome-shell and in the control center. Signed-off-by: Philip Withnall --- meson.build | 4 + meson_options.txt | 1 + plugins/malcontent/gs-plugin-malcontent.c | 304 ++++++++++++++++++++++ plugins/malcontent/meson.build | 17 ++ plugins/meson.build | 3 + 5 files changed, 329 insertions(+) create mode 100644 plugins/malcontent/gs-plugin-malcontent.c create mode 100644 plugins/malcontent/meson.build diff --git a/meson.build b/meson.build index ac137e816..36e117d70 100644 --- a/meson.build +++ b/meson.build @@ -167,6 +167,10 @@ if get_option('flatpak') flatpak = dependency('flatpak', version : '>= 1.0.4') endif +if get_option('malcontent') + malcontent = dependency('malcontent-0', version: '>= 0.3.0') +endif + if get_option('rpm_ostree') libdnf = dependency('libdnf') ostree = dependency('ostree-1') diff --git a/meson_options.txt b/meson_options.txt index 14174f07c..74af14ba0 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -8,6 +8,7 @@ option('polkit', type : 'boolean', value : true, description : 'enable PolKit su option('eos_updater', type : 'boolean', value : false, description : 'enable eos-updater support') option('fwupd', type : 'boolean', value : true, description : 'enable fwupd support') option('flatpak', type : 'boolean', value : true, description : 'enable Flatpak support') +option('malcontent', type : 'boolean', value : true, description : 'enable parental controls support using libmalcontent') option('rpm_ostree', type : 'boolean', value : false, description : 'enable rpm-ostree support') option('shell_extensions', type : 'boolean', value : true, description : 'enable shell extensions support') option('odrs', type : 'boolean', value : true, description : 'enable ODRS support') diff --git a/plugins/malcontent/gs-plugin-malcontent.c b/plugins/malcontent/gs-plugin-malcontent.c new file mode 100644 index 000000000..68d08544a --- /dev/null +++ b/plugins/malcontent/gs-plugin-malcontent.c @@ -0,0 +1,304 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2018-2019 Endless Mobile + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include + +#include +#include +#include +#include +#include + +/* + * 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 blacklist. + * + * 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. */ +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); + + 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)); + } + + 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_INTENSE; + + if (rating_value == AS_CONTENT_RATING_VALUE_UNKNOWN || + 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_blacklisted (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 blacklist to see if this app should be launchable */ + if (app_is_parentally_blacklisted (app, app_filter)) { + g_debug ("Filtering ‘%s’: app is blacklisted 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 void +app_filter_changed_cb (MctManager *manager, + guint64 user_id, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + + if (user_id == getuid ()) { + /* 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); + gs_plugin_reload (plugin); + } +} + +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); +} + +gboolean +gs_plugin_refine_app (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_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + g_assert (priv->app_filter != NULL); + + app_set_parental_quirks (plugin, app, priv->app_filter); + + return TRUE; + } +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + 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; +} + +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 000000000..ea0740ad6 --- /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 diff --git a/plugins/meson.build b/plugins/meson.build index 2eebff024..29cbd4efc 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -27,6 +27,9 @@ endif if get_option('gudev') subdir('modalias') endif +if get_option('malcontent') + subdir('malcontent') +endif if get_option('odrs') subdir('odrs') endif -- GitLab From eafede5387747c99aa96b0f0b896518bd0b4fd8a Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 30 Apr 2019 12:57:03 +0100 Subject: [PATCH 3/4] core: Unconditionally set content rating data This is needed for parental controls. Signed-off-by: Philip Withnall --- plugins/core/gs-appstream.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/core/gs-appstream.c b/plugins/core/gs-appstream.c index 7647157ea..def0001ec 100644 --- a/plugins/core/gs-appstream.c +++ b/plugins/core/gs-appstream.c @@ -736,7 +736,7 @@ gs_appstream_refine_app (GsPlugin *plugin, gs_app_set_scope (app, as_app_scope_from_string (tmp)); /* set content rating */ - if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING) { + if (TRUE) { if (!gs_appstream_refine_app_content_ratings (plugin, app, component, error)) return FALSE; } -- GitLab From 54a02cefa6e28bfe698bb125adb3752966a17006 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 28 Aug 2019 11:14:23 +0100 Subject: [PATCH 4/4] build: Disable malcontent plugin by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Otherwise it adds a new feature to gnome-software 3.32, and we‘re past the feature freeze. Signed-off-by: Philip Withnall --- meson_options.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson_options.txt b/meson_options.txt index 74af14ba0..ef46e5ce1 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -8,7 +8,7 @@ option('polkit', type : 'boolean', value : true, description : 'enable PolKit su option('eos_updater', type : 'boolean', value : false, description : 'enable eos-updater support') option('fwupd', type : 'boolean', value : true, description : 'enable fwupd support') option('flatpak', type : 'boolean', value : true, description : 'enable Flatpak support') -option('malcontent', type : 'boolean', value : true, description : 'enable parental controls support using libmalcontent') +option('malcontent', type : 'boolean', value : false, description : 'enable parental controls support using libmalcontent') option('rpm_ostree', type : 'boolean', value : false, description : 'enable rpm-ostree support') option('shell_extensions', type : 'boolean', value : true, description : 'enable shell extensions support') option('odrs', type : 'boolean', value : true, description : 'enable ODRS support') -- GitLab