dir: Support filtering app installs/upgrades by user’s OARS settings

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 <withnall@endlessm.com>
This commit is contained in:
Philip Withnall 2019-06-26 14:45:04 +01:00 committed by Alexander Larsson
parent 8bd8bdcbcc
commit 9758968cc4
6 changed files with 405 additions and 0 deletions

View File

@ -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) \

View File

@ -42,15 +42,22 @@
#include "libglnx/libglnx.h"
#include "flatpak-error.h"
#include <ostree.h>
#include <polkit/polkit.h>
#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 <libmalcontent/malcontent.h>
#include "flatpak-parental-controls-private.h"
#endif
#ifdef HAVE_LIBSYSTEMD
#define SD_JOURNAL_SUPPRESS_LOCATION
#include <systemd/sd-journal.h>
@ -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 users 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. Thats 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 cant pass any details to this
* polkit check, since it could be run by the user or by the system helper,
* and non-root users cant 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;

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Authors:
* Philip Withnall <withnall@endlessm.com>
*/
#if !defined(__FLATPAK_H_INSIDE__) && !defined(FLATPAK_COMPILATION)
#error "Only <flatpak.h> can be included directly."
#endif
#ifndef __FLATPAK_PARENTAL_CONTROLS_PRIVATE_H__
#define __FLATPAK_PARENTAL_CONTROLS_PRIVATE_H__
#include <libmalcontent/app-filter.h>
#include <glib.h>
gboolean flatpak_oars_check_rating (GHashTable *content_rating,
const gchar *content_rating_type,
MctAppFilter *filter);
#endif /* __FLATPAK_PARENTAL_CONTROLS_PRIVATE_H__ */

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Authors:
* Philip Withnall <withnall@endlessm.com>
*/
#include "config.h"
#include <glib.h>
#include <gio/gio.h>
#include <libmalcontent/app-filter.h>
#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 <content_attribute/> 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: users parental controls settings
*
* Check whether the OARS rating in @content_rating is as, or less, extreme than
* the users 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 <content_rating/> 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 <content_rating/> 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;
}

View File

@ -235,5 +235,62 @@
</defaults>
</action>
<action id="org.freedesktop.Flatpak.override-parental-controls">
<!-- SECURITY:
- Authorisation to actually install software is controlled by
org.freedesktop.Flatpak.app-install.
- This action is checked after app-install, as it can only be done
once the apps data (including its content rating) has been
downloaded.
- This action is checked to see if the installation should be allowed
based on whether the app being installed has extreme content.
- It is checked only if an app has too extreme content for the user
who is trying to install it (in which case, the app is unsafe).
- Typically, normal users will need admin permission to install apps
with extreme content; admins will be able to install it without
additional checks.
- In order to configure the policy so that admins can install safe and
unsafe software anywhere without authorisation, and non-admins can
install safe software in their user or system dirs without
authorisation, but need authorisation to install unsafe software
anywhere:
* Unconditionally return `yes` from `app-install`.
* Return `auth_admin` from `override-parental-controls` for users
not in `@privileged_group@`, and `yes` for users in it.
* Set the malcontent `is-{user,system}-installation-allowed`
properties of all non-admins parental controls policies to true.
- In order to configure the policy so that admins can install safe and
unsafe software anywhere without authorisation, and non-admins can
install safe software in their user dir without authorisation, but
need authorisation to install safe software in the system dir or to
install unsafe software anywhere:
* Unconditionally return `yes` from `app-install`.
* Return `auth_admin` from `override-parental-controls` for users
not in `@privileged_group@`, and `yes` for users in it.
* Set the malcontent `is-user-installation-allowed` property of all
non-admins parental controls policies to true.
* Set the malcontent `is-system-installation-allowed` property of
all non-admins parental controls policies to false.
- In order to configure the policy so that all users (including
admins) can install safe software anywhere without authorisation,
but need authorisation to install unsafe software anywhere (i.e.
applying parental controls to admins too):
* Unconditionally return `yes` from `app-install`.
* Unconditionally return `auth_admin` from `override-parental-controls`.
* Set the malcontent `is-user-installation-allowed` property of all
users parental controls policies to true.
* Set the malcontent `is-system-installation-allowed` property of
all users parental controls policies to true.
-->
<description>Override parental controls</description>
<message>Authentication is required to install software which is restricted by your parental controls policy</message>
<icon_name>package-x-generic</icon_name>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>

View File

@ -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;
});