From fe0c597774fbefa714c85a26b35ea9ee2b43d4c6 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 28 Sep 2018 10:11:11 +0200 Subject: [PATCH] libeos-parental-controls: Initial implementation of library This allows the app filter to be queried, and includes all the basic parts of a shared library. Introspection and unit tests are to follow. Signed-off-by: Philip Withnall https://phabricator.endlessm.com/T23859 --- libeos-parental-controls/app-filter.c | 384 ++++++++++++++++++++++++++ libeos-parental-controls/app-filter.h | 84 ++++++ libeos-parental-controls/meson.build | 45 +++ meson.build | 74 ++++- 4 files changed, 586 insertions(+), 1 deletion(-) create mode 100644 libeos-parental-controls/app-filter.c create mode 100644 libeos-parental-controls/app-filter.h create mode 100644 libeos-parental-controls/meson.build diff --git a/libeos-parental-controls/app-filter.c b/libeos-parental-controls/app-filter.c new file mode 100644 index 0000000..7650a7b --- /dev/null +++ b/libeos-parental-controls/app-filter.c @@ -0,0 +1,384 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2018 Endless Mobile, Inc. + * + * This library 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Authors: + * - Philip Withnall + */ + +#include "config.h" + +#include +#include +#include +#include +#include + + +G_DEFINE_QUARK (EpcAppFilterError, epc_app_filter_error) + +/** + * EpcAppFilterListType: + * @EPC_APP_FILTER_LIST_BLACKLIST: Any program in the list is not allowed to + * be run. + * @EPC_APP_FILTER_LIST_WHITELIST: Any program not in the list is not allowed + * to be run. + * + * Different semantics for interpreting an application list. + * + * Since: 0.1.0 + */ +typedef enum +{ + EPC_APP_FILTER_LIST_BLACKLIST, + EPC_APP_FILTER_LIST_WHITELIST, +} EpcAppFilterListType; + +struct _EpcAppFilter +{ + gint ref_count; + + uid_t user_id; + + gchar **app_list; /* (owned) (array zero-terminated=1) */ + EpcAppFilterListType app_list_type; +}; + +G_DEFINE_BOXED_TYPE (EpcAppFilter, epc_app_filter, + epc_app_filter_ref, epc_app_filter_unref) + +/** + * epc_app_filter_ref: + * @filter: (transfer none): an #EpcAppFilter + * + * Increment the reference count of @filter, and return the same pointer to it. + * + * Returns: (transfer full): the same pointer as @filter + * Since: 0.1.0 + */ +EpcAppFilter * +epc_app_filter_ref (EpcAppFilter *filter) +{ + g_return_val_if_fail (filter != NULL, NULL); + g_return_val_if_fail (filter->ref_count >= 1, NULL); + g_return_val_if_fail (filter->ref_count <= G_MAXINT - 1, NULL); + + filter->ref_count++; + return filter; +} + +/** + * epc_app_filter_unref: + * @filter: (transfer full): an #EpcAppFilter + * + * Decrement the reference count of @filter. If the reference count reaches + * zero, free the @filter and all its resources. + * + * Since: 0.1.0 + */ +void +epc_app_filter_unref (EpcAppFilter *filter) +{ + g_return_if_fail (filter != NULL); + g_return_if_fail (filter->ref_count >= 1); + + filter->ref_count--; + + if (filter->ref_count <= 0) + { + g_strfreev (filter->app_list); + g_free (filter); + } +} + +/** + * epc_app_filter_get_user_id: + * @filter: an #EpcAppFilter + * + * Get the user ID of the user this #EpcAppFilter is for. + * + * Returns: user ID of the relevant user + * Since: 0.1.0 + */ +uid_t +epc_app_filter_get_user_id (EpcAppFilter *filter) +{ + g_return_val_if_fail (filter != NULL, FALSE); + g_return_val_if_fail (filter->ref_count >= 1, FALSE); + + return filter->user_id; +} + +/** + * epc_app_filter_is_path_allowed: + * @filter: an #EpcAppFilter + * @path: absolute path of a program to check + * + * Check whether the program at @path is allowed to be run according to this + * app filter. @path will be canonicalised without doing any I/O. + * + * Returns: %TRUE if the user this @filter corresponds to is allowed to run the + * program at @path according to the @filter policy; %FALSE otherwise + * Since: 0.1.0 + */ +gboolean +epc_app_filter_is_path_allowed (EpcAppFilter *filter, + const gchar *path) +{ + g_return_val_if_fail (filter != NULL, FALSE); + g_return_val_if_fail (filter->ref_count >= 1, FALSE); + g_return_val_if_fail (path != NULL, FALSE); + g_return_val_if_fail (g_path_is_absolute (path), FALSE); + + g_autofree gchar *canonical_path = g_canonicalize_filename (path, "/"); + gboolean path_in_list = g_strv_contains ((const gchar * const *) filter->app_list, + canonical_path); + + switch (filter->app_list_type) + { + case EPC_APP_FILTER_LIST_BLACKLIST: + return !path_in_list; + case EPC_APP_FILTER_LIST_WHITELIST: + return path_in_list; + default: + g_assert_not_reached (); + } +} + +/* Check if @error is a D-Bus remote error mataching @expected_error_name. */ +static gboolean +bus_remote_error_matches (const GError *error, + const gchar *expected_error_name) +{ + g_autofree gchar *error_name = NULL; + + if (!g_dbus_error_is_remote_error (error)) + return FALSE; + + error_name = g_dbus_error_get_remote_error (error); + + return g_str_equal (error_name, expected_error_name); +} + +/* Convert a #GDBusError into a #EpcAppFilter error. */ +static GError * +bus_error_to_app_filter_error (const GError *bus_error, + uid_t user_id) +{ + if (g_error_matches (bus_error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED) || + bus_remote_error_matches (bus_error, "org.freedesktop.Accounts.Error.PermissionDenied")) + return g_error_new (EPC_APP_FILTER_ERROR, EPC_APP_FILTER_ERROR_PERMISSION_DENIED, + _("Not allowed to query app filter data for user %u"), + user_id); + else if (g_error_matches (bus_error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) + return g_error_new (EPC_APP_FILTER_ERROR, EPC_APP_FILTER_ERROR_INVALID_USER, + _("User %u does not exist"), user_id); + else + return g_error_copy (bus_error); +} + +static void get_bus_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data); +static void get_app_filter (GDBusConnection *connection, + GTask *task); +static void get_app_filter_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data); + +typedef struct +{ + uid_t user_id; + gboolean allow_interactive_authorization; +} GetAppFilterData; + +static void +get_app_filter_data_free (GetAppFilterData *data) +{ + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetAppFilterData, get_app_filter_data_free) + +/** + * epc_get_app_filter_async: + * @connection: (nullable): a #GDBusConnection to the system bus, or %NULL to + * use the default + * @user_id: ID of the user to query, typically coming from getuid() + * @allow_interactive_authorization: %TRUE to allow interactive polkit + * authorization dialogues to be displayed during the call; %FALSE otherwise + * @callback: a #GAsyncReadyCallback + * @cancellable: (nullable): a #GCancellable, or %NULL + * @user_data: user data to pass to @callback + * + * Asynchronously get a snapshot of the app filter settings for the given + * @user_id. + * + * @connection should be a connection to the system bus, where accounts-service + * runs. It’s provided mostly for testing purposes, or to allow an existing + * connection to be re-used. Pass %NULL to use the default connection. + * + * On failure, an #EpcAppFilterError, a #GDBusError or a #GIOError will be + * returned. + * + * Since: 0.1.0 + */ +void +epc_get_app_filter_async (GDBusConnection *connection, + uid_t user_id, + gboolean allow_interactive_authorization, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GDBusConnection) connection_owned = NULL; + g_autoptr(GTask) task = NULL; + g_autoptr(GetAppFilterData) data = NULL; + + g_return_if_fail (connection == NULL || G_IS_DBUS_CONNECTION (connection)); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_source_tag (task, epc_get_app_filter_async); + + data = g_new0 (GetAppFilterData, 1); + data->user_id = user_id; + data->allow_interactive_authorization = allow_interactive_authorization; + g_task_set_task_data (task, g_steal_pointer (&data), + (GDestroyNotify) get_app_filter_data_free); + + if (connection == NULL) + g_bus_get (G_BUS_TYPE_SYSTEM, cancellable, + get_bus_cb, g_steal_pointer (&task)); + else + get_app_filter (connection, g_steal_pointer (&task)); +} + +static void +get_bus_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GDBusConnection) connection = NULL; + g_autoptr(GError) local_error = NULL; + + connection = g_bus_get_finish (result, &local_error); + + if (local_error != NULL) + g_task_return_error (task, g_steal_pointer (&local_error)); + else + get_app_filter (connection, g_steal_pointer (&task)); +} + +static void +get_app_filter (GDBusConnection *connection, + GTask *task) +{ + g_autofree gchar *object_path = NULL; + GCancellable *cancellable; + + GetAppFilterData *data = g_task_get_task_data (task); + cancellable = g_task_get_cancellable (task); + object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%u", + data->user_id); + g_dbus_connection_call (connection, + "org.freedesktop.Accounts", + object_path, + "org.freedesktop.DBus.Properties", + "GetAll", + g_variant_new ("(s)", "com.endlessm.ParentalControls.AppFilter"), + G_VARIANT_TYPE ("(a{sv})"), + data->allow_interactive_authorization + ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION + : G_DBUS_CALL_FLAGS_NONE, + -1, /* timeout, ms */ + cancellable, + get_app_filter_cb, + g_steal_pointer (&task)); +} + +static void +get_app_filter_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection = G_DBUS_CONNECTION (obj); + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GVariant) result_variant = NULL; + g_autoptr(GVariant) properties = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(EpcAppFilter) app_filter = NULL; + gboolean is_whitelist; + g_auto(GStrv) app_list = NULL; + + GetAppFilterData *data = g_task_get_task_data (task); + result_variant = g_dbus_connection_call_finish (connection, result, &local_error); + + if (local_error != NULL) + { + g_autoptr(GError) app_filter_error = bus_error_to_app_filter_error (local_error, + data->user_id); + g_task_return_error (task, g_steal_pointer (&app_filter_error)); + return; + } + + /* Extract the properties we care about. They may be silently omitted from the + * results if we don’t have permission to access them. */ + properties = g_variant_get_child_value (result_variant, 0); + if (!g_variant_lookup (properties, "app-filter", "(b^as)", + &is_whitelist, &app_list)) + { + g_task_return_new_error (task, EPC_APP_FILTER_ERROR, + EPC_APP_FILTER_ERROR_PERMISSION_DENIED, + _("Not allowed to query app filter data for user %u"), + data->user_id); + return; + } + + /* Success. Create an #EpcAppFilter object to contain the results. */ + app_filter = g_new0 (EpcAppFilter, 1); + app_filter->ref_count = 1; + app_filter->user_id = data->user_id; + app_filter->app_list = g_steal_pointer (&app_list); + app_filter->app_list_type = + is_whitelist ? EPC_APP_FILTER_LIST_WHITELIST : EPC_APP_FILTER_LIST_BLACKLIST; + + g_task_return_pointer (task, g_steal_pointer (&app_filter), + (GDestroyNotify) epc_app_filter_unref); +} + +/** + * epc_get_app_filter_finish: + * @result: a #GAsyncResult + * @error: return location for a #GError, or %NULL + * + * Finish an asynchronous operation to get the app filter for a user, started + * with epc_get_app_filter_async(). + * + * Returns: (transfer full): app filter for the queried user + * Since: 0.1.0 + */ +EpcAppFilter * +epc_get_app_filter_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, NULL), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} diff --git a/libeos-parental-controls/app-filter.h b/libeos-parental-controls/app-filter.h new file mode 100644 index 0000000..e5b7a7d --- /dev/null +++ b/libeos-parental-controls/app-filter.h @@ -0,0 +1,84 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2018 Endless Mobile, Inc. + * + * This library 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Authors: + * - Philip Withnall + */ + +#pragma once + +#include +#include +#include + +G_BEGIN_DECLS + +/** + * EpcAppFilterError: + * @EPC_APP_FILTER_ERROR_INVALID_USER: Given user ID doesn’t exist + * @EPC_APP_FILTER_ERROR_PERMISSION_DENIED: Not authorized to query the app + * filter for the given user + * + * Errors which can be returned by epc_get_app_filter_async(). + * + * Since: 0.1.0 + */ +typedef enum +{ + EPC_APP_FILTER_ERROR_INVALID_USER, + EPC_APP_FILTER_ERROR_PERMISSION_DENIED, +} EpcAppFilterError; + +GQuark epc_app_filter_error_quark (void); +#define EPC_APP_FILTER_ERROR epc_app_filter_error_quark () + +/** + * EpcAppFilter: + * + * #EpcAppFilter is an opaque, immutable structure which contains a snapshot of + * the app filtering settings for a user at a given time. This includes a list + * of apps which are explicitly banned or allowed to be run by that user. + * + * Typically, app filter settings can only be changed by the administrator, and + * are read-only for non-administrative users. The precise policy is set using + * polkit. + * + * Since: 0.1.0 + */ +typedef struct _EpcAppFilter EpcAppFilter; +GType epc_app_filter_get_type (void); + +EpcAppFilter *epc_app_filter_ref (EpcAppFilter *filter); +void epc_app_filter_unref (EpcAppFilter *filter); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (EpcAppFilter, epc_app_filter_unref) + +uid_t epc_app_filter_get_user_id (EpcAppFilter *filter); +gboolean epc_app_filter_is_path_allowed (EpcAppFilter *filter, + const gchar *path); + +void epc_get_app_filter_async (GDBusConnection *connection, + uid_t user_id, + gboolean allow_interactive_authorization, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +EpcAppFilter *epc_get_app_filter_finish (GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/libeos-parental-controls/meson.build b/libeos-parental-controls/meson.build new file mode 100644 index 0000000..263fe0d --- /dev/null +++ b/libeos-parental-controls/meson.build @@ -0,0 +1,45 @@ +libeos_parental_controls_api_version = '0' +libeos_parental_controls_api_name = 'eos-parental-controls-' + libeos_parental_controls_api_version +libeos_parental_controls_sources = [ + 'app-filter.c', +] +libeos_parental_controls_headers = [ + 'app-filter.h', +] + +libeos_parental_controls_public_deps = [ + dependency('gio-2.0', version: '>= 2.44'), + dependency('glib-2.0', version: '>= 2.54.2'), + dependency('gobject-2.0', version: '>= 2.54'), +] + +# FIXME: Would be good to use subdir here: https://github.com/mesonbuild/meson/issues/2969 +libeos_parental_controls_include_subdir = join_paths(libeos_parental_controls_api_name, 'libeos-parental-controls') + +libeos_parental_controls = library(libeos_parental_controls_api_name, + libeos_parental_controls_sources + libeos_parental_controls_headers, + dependencies: libeos_parental_controls_public_deps, + include_directories: root_inc, + install: true, + version: meson.project_version(), + soversion: libeos_parental_controls_api_version, +) +libeos_parental_controls_dep = declare_dependency( + link_with: libeos_parental_controls, + include_directories: root_inc, +) + +# Public library bits. +install_headers(libeos_parental_controls_headers, + subdir: libeos_parental_controls_include_subdir, +) + +pkgconfig.generate( + libraries: [ libeos_parental_controls ], + subdirs: libeos_parental_controls_api_name, + version: meson.project_version(), + name: 'libeos-parental-controls', + filebase: libeos_parental_controls_api_name, + description: 'Library providing access to parental control settings.', + requires: libeos_parental_controls_public_deps, +) \ No newline at end of file diff --git a/meson.build b/meson.build index ace0b52..fae8a04 100644 --- a/meson.build +++ b/meson.build @@ -29,4 +29,76 @@ polkit_gobject = dependency('polkit-gobject-1') polkitpolicydir = polkit_gobject.get_pkgconfig_variable('policydir', define_variable: ['prefix', prefix]) -subdir('accounts-service') \ No newline at end of file +config_h = configuration_data() +config_h.set_quoted('GETTEXT_PACKAGE', meson.project_name()) +configure_file( + output: 'config.h', + configuration: config_h, +) +root_inc = include_directories('.') + +# Enable warning flags +test_c_args = [ + '-fno-strict-aliasing', + '-fstack-protector-strong', + '-Waggregate-return', + '-Wall', + '-Wunused', + '-Warray-bounds', + '-Wcast-align', + '-Wclobbered', + '-Wno-declaration-after-statement', + '-Wduplicated-branches', + '-Wduplicated-cond', + '-Wempty-body', + '-Wformat=2', + '-Wformat-nonliteral', + '-Wformat-security', + '-Wformat-signedness', + '-Wignored-qualifiers', + '-Wimplicit-function-declaration', + '-Wincompatible-pointer-types', + '-Wincompatible-pointer-types-discards-qualifiers', + '-Winit-self', + '-Wint-conversion', + '-Wlogical-op', + '-Wmisleading-indentation', + '-Wmissing-declarations', + '-Wmissing-format-attribute', + '-Wmissing-include-dirs', + '-Wmissing-noreturn', + '-Wmissing-parameter-type', + '-Wmissing-prototypes', + '-Wnested-externs', + '-Wno-error=cpp', + '-Wno-discarded-qualifiers', + '-Wno-missing-field-initializers', + '-Wno-suggest-attribute=format', + '-Wno-unused-parameter', + '-Wnull-dereference', + '-Wold-style-definition', + '-Woverflow', + '-Woverride-init', + '-Wparentheses', + '-Wpointer-arith', + '-Wredundant-decls', + '-Wreturn-type', + '-Wshadow', + '-Wsign-compare', + '-Wstrict-aliasing=2', + '-Wstrict-prototypes', + '-Wswitch-default', + '-Wswitch-enum', + '-Wtype-limits', + '-Wundef', + '-Wuninitialized', + '-Wunused-but-set-variable', + '-Wunused-result', + '-Wunused-variable', + '-Wwrite-strings' +] +cc = meson.get_compiler('c') +add_project_arguments(cc.get_supported_arguments(test_c_args), language: 'c') + +subdir('accounts-service') +subdir('libeos-parental-controls') \ No newline at end of file