From 9758968cc4b5a3ed042943b8cf15c81deff29813 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 26 Jun 2019 14:45:04 +0100 Subject: [PATCH] =?UTF-8?q?dir:=20Support=20filtering=20app=20installs/upg?= =?UTF-8?q?rades=20by=20user=E2=80=99s=20OARS=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the user’s OARS filter to prevent installation or upgrade of apps which have more extreme content than the user is allowed to see. This uses libmalcontent to load the user’s enforced OARS filter, which describes the extremeness of each type of content the user is allowed to see. If an app they are trying to install exceeds the filter value in any OARS section, installation is disallowed and an error is returned. libmalcontent stores the parental controls policy per-user in accountsservice, which enforces access control on the policies. The app filter is also allowed to prevent app installation entirely, which overrides the OARS values. This is independent from the app-install polkit action, which determines whether an unprivileged user may install an app system-wide. Being stored in accountsservice, the new boolean is also easier to set per-user without having to programmatically write a polkit JS policy file which handles multiple users (and parse it back again). The parental controls checks are done at deploy time, either in the `flatpak` process (for user repositories) or in the `flatpak-system-helper` (for system repositories). The checks use content rating data extracted from the app’s AppData XML and stored in the `FlatpakDeploy` cache. The checks are passed through polkit (even for user repositories) so that users can get an admin override to install apps which would otherwise be too extreme. This uses the new `org.freedesktop.Flatpak.parental-controls` polkit rule. The checks have to be done at deploy time, as that’s when the AppData XML for the app is parsed. The downside of this arrangement is that an app must be entirely downloaded before the parental checks can be done. This won’t be much of an issue on normal desktops, however, since we can assume that gnome-software will check an app’s appropriateness before showing it to the user in the first place. Parental controls are not enforced for non-apps/runtimes, which includes the ostree-metadata and appstream/* refs. One thorny issue is that flatpak unit tests may be run in an environment with no system D-Bus available to connect to (a Jenkins instance, for example), which means the call to `mct_manager_get_app_filter()` in `flatpak_dir_check_parental_controls()` fails. So this commit skips the parental controls check if the system bus is unavailable and the environment variable `FLATPAK_SYSTEM_HELPER_ON_SESSION` is set, since the testlibrary already sets that variable so that the system-helper will be started on the session bus. The feature can be tested using something like: ``` $ malcontent-client set philip \ violence-realistic=none app/org.freedesktop.Bustle/x86_64/stable App filter for user 1000 set $ flatpak run org.freedesktop.Bustle error: Running app/org.freedesktop.Bustle/x86_64/stable is not allowed by the policy set by your administrator $ flatpak --user install flathub io.github.FreeDM error: Failed to install io.github.FreeDM: Installing app/io.github.FreeDM/x86_64/stable is not allowed by the policy set by your administrator ``` Includes work by André Magalhães and Umang Jain. Signed-off-by: Philip Withnall --- common/Makefile.am.inc | 9 + common/flatpak-dir.c | 155 ++++++++++++++++++ common/flatpak-parental-controls-private.h | 35 ++++ common/flatpak-parental-controls.c | 141 ++++++++++++++++ .../org.freedesktop.Flatpak.policy.in | 57 +++++++ .../org.freedesktop.Flatpak.rules.in | 8 + 6 files changed, 405 insertions(+) create mode 100644 common/flatpak-parental-controls-private.h create mode 100644 common/flatpak-parental-controls.c diff --git a/common/Makefile.am.inc b/common/Makefile.am.inc index bf39b965..bacf390a 100644 --- a/common/Makefile.am.inc +++ b/common/Makefile.am.inc @@ -149,6 +149,13 @@ libflatpak_common_la_SOURCES = \ common/valgrind-private.h \ $(NULL) +if HAVE_LIBMALCONTENT +libflatpak_common_la_SOURCES += \ + common/flatpak-parental-controls.c \ + common/flatpak-parental-controls-private.h \ + $(NULL) +endif + libflatpak_common_la_CFLAGS = \ -DFLATPAK_COMPILATION \ -DLIBEXECDIR=\"$(libexecdir)\" \ @@ -163,6 +170,7 @@ libflatpak_common_la_CFLAGS = \ $(LIBSECCOMP_CFLAGS) \ $(MALCONTENT_CFLAGS) \ $(OSTREE_CFLAGS) \ + $(POLKIT_CFLAGS) \ $(SOUP_CFLAGS) \ $(SYSTEMD_CFLAGS) \ $(XAUTH_CFLAGS) \ @@ -178,6 +186,7 @@ libflatpak_common_la_LIBADD = \ $(LIBSECCOMP_LIBS) \ $(MALCONTENT_LIBS) \ $(OSTREE_LIBS) \ + $(POLKIT_LIBS) \ $(SOUP_LIBS) \ $(SYSTEMD_LIBS) \ $(XAUTH_LIBS) \ diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c index 7644bf5c..2a89c4ef 100644 --- a/common/flatpak-dir.c +++ b/common/flatpak-dir.c @@ -42,15 +42,22 @@ #include "libglnx/libglnx.h" #include "flatpak-error.h" #include +#include #include "flatpak-dir-private.h" #include "flatpak-utils-base-private.h" #include "flatpak-oci-registry-private.h" +#include "flatpak-ref.h" #include "flatpak-run-private.h" #include "flatpak-appdata-private.h" #include "errno.h" +#ifdef HAVE_LIBMALCONTENT +#include +#include "flatpak-parental-controls-private.h" +#endif + #ifdef HAVE_LIBSYSTEMD #define SD_JOURNAL_SUPPRESS_LOCATION #include @@ -67,6 +74,18 @@ #define SYSCONF_REMOTES_DIR "remotes.d" #define SYSCONF_REMOTES_FILE_EXT ".flatpakrepo" +/* This uses a weird Auto prefix to avoid conflicts with later added polkit types. + */ +typedef PolkitAuthority AutoPolkitAuthority; +typedef PolkitAuthorizationResult AutoPolkitAuthorizationResult; +typedef PolkitDetails AutoPolkitDetails; +typedef PolkitSubject AutoPolkitSubject; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitAuthority, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitAuthorizationResult, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitDetails, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitSubject, g_object_unref) + static FlatpakOciRegistry *flatpak_dir_create_system_child_oci_registry (FlatpakDir *self, GLnxLockFile *file_lock, GError **error); @@ -7608,6 +7627,137 @@ apply_extra_data (FlatpakDir *self, return TRUE; } +/* Check the user’s parental controls allow installation of @ref by looking at + * its cached @deploy_data, which contains its content rating as extracted from + * its AppData when it was originally downloaded. That’s compared to the + * parental controls policy loaded from the #MctManager. + * + * If @ref should not be installed, an error is returned. */ +static gboolean +flatpak_dir_check_parental_controls (FlatpakDir *self, + const char *ref, + GVariant *deploy_data, + GCancellable *cancellable, + GError **error) +{ +#ifdef HAVE_LIBMALCONTENT + g_autoptr(GError) local_error = NULL; + const char *on_session = g_getenv ("FLATPAK_SYSTEM_HELPER_ON_SESSION"); + g_autoptr(GDBusConnection) dbus_connection = NULL; + g_autoptr(MctManager) manager = NULL; + g_autoptr(MctAppFilter) app_filter = NULL; + const char *content_rating_type; + g_autoptr(GHashTable) content_rating = NULL; + g_autoptr(AutoPolkitAuthority) authority = NULL; + g_autoptr(AutoPolkitDetails) details = NULL; + g_autoptr(AutoPolkitSubject) subject = NULL; + gint subject_uid; + g_autoptr(AutoPolkitAuthorizationResult) result = NULL; + gboolean authorized; + gboolean repo_installation_allowed, app_is_appropriate; + + /* The ostree-metadata and appstream/ branches should not have any parental + * controls restrictions. Similarly, for the moment, there is no point in + * restricting runtimes. */ + if (!g_str_has_prefix (ref, "app/")) + return TRUE; + + g_debug ("Getting parental controls details for %s from %s", + ref, flatpak_deploy_data_get_origin (deploy_data)); + + if (on_session != NULL) + { + /* FIXME: Instead of skipping the parental controls check in the test + * environment, make a mock service for it. + * https://github.com/flatpak/flatpak/issues/2993 */ + g_debug ("Skipping parental controls check for %s since the " + "system bus is unavailable in the test environment", ref); + return TRUE; + } + + dbus_connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, cancellable, &local_error); + if (dbus_connection == NULL) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return FALSE; + } + + if (self->user || self->source_pid == 0) + subject = polkit_unix_process_new_for_owner (getpid (), 0, getuid ()); + else + subject = polkit_unix_process_new_for_owner (self->source_pid, 0, -1); + + /* Get the parental controls for the invoking user. */ + subject_uid = polkit_unix_process_get_uid (POLKIT_UNIX_PROCESS (subject)); + if (subject_uid == -1) + { + g_set_error_literal (error, G_DBUS_ERROR, G_DBUS_ERROR_AUTH_FAILED, + "Failed to get subject UID"); + return FALSE; + } + + manager = mct_manager_new (dbus_connection); + app_filter = mct_manager_get_app_filter (manager, subject_uid, + MCT_GET_APP_FILTER_FLAGS_INTERACTIVE, + cancellable, &local_error); + if (g_error_matches (local_error, MCT_APP_FILTER_ERROR, MCT_APP_FILTER_ERROR_DISABLED)) + { + g_debug ("Skipping parental controls check for %s since parental " + "controls are disabled globally", ref); + return TRUE; + } + else if (local_error != NULL) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return FALSE; + } + + /* Check the content rating against the parental controls. If the app is + * allowed to be installed, return so immediately. */ + repo_installation_allowed = ((self->user && mct_app_filter_is_user_installation_allowed (app_filter)) || + (!self->user && mct_app_filter_is_system_installation_allowed (app_filter))); + + content_rating_type = flatpak_deploy_data_get_appdata_content_rating_type (deploy_data); + content_rating = flatpak_deploy_data_get_appdata_content_rating (deploy_data); + app_is_appropriate = flatpak_oars_check_rating (content_rating, content_rating_type, + app_filter); + + if (repo_installation_allowed && app_is_appropriate) + { + g_debug ("Parental controls policy satisfied for %s", ref); + return TRUE; + } + + /* Otherwise, check polkit to see if the admin is going to allow the user to + * override their parental controls policy. We can’t pass any details to this + * polkit check, since it could be run by the user or by the system helper, + * and non-root users can’t pass details to polkit checks. */ + authority = polkit_authority_get_sync (NULL, error); + if (authority == NULL) + return FALSE; + + result = polkit_authority_check_authorization_sync (authority, subject, + "org.freedesktop.Flatpak.override-parental-controls", + NULL, + POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, + cancellable, error); + if (result == NULL) + return FALSE; + + authorized = polkit_authorization_result_get_is_authorized (result); + + if (!authorized) + return flatpak_fail_error (error, FLATPAK_ERROR_PERMISSION_DENIED, + /* Translators: The placeholder is for an app ref. */ + _("Installing %s is not allowed by the policy set by your administrator"), + ref); + + g_debug ("Parental controls policy overridden by polkit for %s", ref); +#endif /* HAVE_LIBMALCONTENT */ + + return TRUE; +} + /* We create a deploy ref for the currently deployed version of all refs to avoid deployed commits being pruned when e.g. we pull --no-deploy. */ static gboolean @@ -8036,6 +8186,11 @@ flatpak_dir_deploy (FlatpakDir *self, installed_size, previous_ids); + /* Check the app is actually allowed to be used by this user. This can block + * on getting authorisation. */ + if (!flatpak_dir_check_parental_controls (self, ref, deploy_data, cancellable, error)) + return FALSE; + deploy_data_file = g_file_get_child (checkoutdir, "deploy"); if (!flatpak_variant_save (deploy_data_file, deploy_data, cancellable, error)) return FALSE; diff --git a/common/flatpak-parental-controls-private.h b/common/flatpak-parental-controls-private.h new file mode 100644 index 00000000..d1c361db --- /dev/null +++ b/common/flatpak-parental-controls-private.h @@ -0,0 +1,35 @@ +/* + * Copyright © 2018 Endless Mobile, Inc. + * + * This program 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, see . + * + * Authors: + * Philip Withnall + */ + +#if !defined(__FLATPAK_H_INSIDE__) && !defined(FLATPAK_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __FLATPAK_PARENTAL_CONTROLS_PRIVATE_H__ +#define __FLATPAK_PARENTAL_CONTROLS_PRIVATE_H__ + +#include +#include + +gboolean flatpak_oars_check_rating (GHashTable *content_rating, + const gchar *content_rating_type, + MctAppFilter *filter); + +#endif /* __FLATPAK_PARENTAL_CONTROLS_PRIVATE_H__ */ diff --git a/common/flatpak-parental-controls.c b/common/flatpak-parental-controls.c new file mode 100644 index 00000000..e1f4c77d --- /dev/null +++ b/common/flatpak-parental-controls.c @@ -0,0 +1,141 @@ +/* + * Copyright © 2018 Endless Mobile, Inc. + * + * This program 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, see . + * + * Authors: + * Philip Withnall + */ + +#include "config.h" + +#include +#include +#include + +#include "flatpak-parental-controls-private.h" + +/* + * See https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-content_rating + * for details of the appstream content rating specification. + * + * See https://hughsie.github.io/oars/ for details of OARS. Specifically, + * https://github.com/hughsie/oars/tree/master/specification/. + */ + +/* Convert an appstream value to #MctAppFilterOarsValue. + * https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-content_rating + */ +static MctAppFilterOarsValue +app_filter_oars_value_from_appdata (const gchar *appdata_value) +{ + g_return_val_if_fail (appdata_value != NULL, MCT_APP_FILTER_OARS_VALUE_UNKNOWN); + + if (g_str_equal (appdata_value, "intense")) + return MCT_APP_FILTER_OARS_VALUE_INTENSE; + else if (g_str_equal (appdata_value, "moderate")) + return MCT_APP_FILTER_OARS_VALUE_MODERATE; + else if (g_str_equal (appdata_value, "mild")) + return MCT_APP_FILTER_OARS_VALUE_MILD; + else if (g_str_equal (appdata_value, "none")) + return MCT_APP_FILTER_OARS_VALUE_NONE; + else if (g_str_equal (appdata_value, "unknown")) + return MCT_APP_FILTER_OARS_VALUE_UNKNOWN; + else + return MCT_APP_FILTER_OARS_VALUE_UNKNOWN; +} + +static const gchar * +app_filter_oars_value_to_string (MctAppFilterOarsValue oars_value) +{ + switch (oars_value) + { + case MCT_APP_FILTER_OARS_VALUE_UNKNOWN: return "unknown"; + case MCT_APP_FILTER_OARS_VALUE_INTENSE: return "intense"; + case MCT_APP_FILTER_OARS_VALUE_MODERATE: return "moderate"; + case MCT_APP_FILTER_OARS_VALUE_MILD: return "mild"; + case MCT_APP_FILTER_OARS_VALUE_NONE: return "none"; + default: return "unknown"; + } +} + +/** + * flatpak_oars_check_rating: + * @content_rating: (nullable) (transfer none): OARS ratings for the app, + * or %NULL if none are known + * @content_rating_type: (nullable): scheme used in @content_rating, such as + * `oars-1.0` or `oars-1.1`, or %NULL if @content_rating is %NULL + * @filter: user’s parental controls settings + * + * Check whether the OARS rating in @content_rating is as, or less, extreme than + * the user’s preferences in @filter. If so (i.e. if the app is suitable for + * this user to use), return %TRUE; otherwise return %FALSE. + * + * @content_rating may be %NULL if no OARS ratings are provided for the app. If + * so, we have to assume the most restrictive ratings. + * + * Returns: %TRUE if the app is safe to install, %FALSE otherwise + */ +gboolean +flatpak_oars_check_rating (GHashTable *content_rating, + const gchar *content_rating_type, + MctAppFilter *filter) +{ + const gchar * const supported_rating_types[] = { "oars-1.0", "oars-1.1", NULL }; + g_autofree const gchar **oars_sections = mct_app_filter_get_oars_sections (filter); + MctAppFilterOarsValue default_rating_value; + + if (content_rating_type != NULL && + !g_strv_contains (supported_rating_types, content_rating_type)) + return FALSE; + + /* If the app has a element, even if it has no OARS sections + * in it, use a default value of `none` for any missing sections. Otherwise, + * if the app has no element, use `unknown`. */ + if (content_rating != NULL) + default_rating_value = MCT_APP_FILTER_OARS_VALUE_NONE; + else + default_rating_value = MCT_APP_FILTER_OARS_VALUE_UNKNOWN; + + for (gsize i = 0; oars_sections[i] != NULL; i++) + { + MctAppFilterOarsValue rating_value; + MctAppFilterOarsValue filter_value = mct_app_filter_get_oars_value (filter, + oars_sections[i]); + const gchar *appdata_value; + + if (content_rating != NULL) + appdata_value = g_hash_table_lookup (content_rating, oars_sections[i]); + + if (appdata_value != NULL) + rating_value = app_filter_oars_value_from_appdata (appdata_value); + else + rating_value = default_rating_value; + + if (filter_value < rating_value || + (rating_value == MCT_APP_FILTER_OARS_VALUE_UNKNOWN && + filter_value != MCT_APP_FILTER_OARS_VALUE_UNKNOWN) || + (rating_value != MCT_APP_FILTER_OARS_VALUE_UNKNOWN && + filter_value == MCT_APP_FILTER_OARS_VALUE_UNKNOWN)) + { + g_debug ("%s: Comparing rating ‘%s’: app has ‘%s’ but policy has ‘%s’ unknown: OARS check failed", + G_STRFUNC, oars_sections[i], + app_filter_oars_value_to_string (rating_value), + app_filter_oars_value_to_string (filter_value)); + return FALSE; + } + } + + return TRUE; +} diff --git a/system-helper/org.freedesktop.Flatpak.policy.in b/system-helper/org.freedesktop.Flatpak.policy.in index 6c3c6d20..d68a5d53 100644 --- a/system-helper/org.freedesktop.Flatpak.policy.in +++ b/system-helper/org.freedesktop.Flatpak.policy.in @@ -235,5 +235,62 @@ + + + Override parental controls + Authentication is required to install software which is restricted by your parental controls policy + package-x-generic + + auth_admin + auth_admin + auth_admin + + + diff --git a/system-helper/org.freedesktop.Flatpak.rules.in b/system-helper/org.freedesktop.Flatpak.rules.in index be03f425..f01364c2 100644 --- a/system-helper/org.freedesktop.Flatpak.rules.in +++ b/system-helper/org.freedesktop.Flatpak.rules.in @@ -11,3 +11,11 @@ polkit.addRule(function(action, subject) { return polkit.Result.NOT_HANDLED; }); + +polkit.addRule(function(action, subject) { + if (action.id == "org.freedesktop.Flatpak.override-parental-controls") { + return polkit.Result.AUTH_ADMIN; + } + + return polkit.Result.NOT_HANDLED; +});