From fe0c597774fbefa714c85a26b35ea9ee2b43d4c6 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 28 Sep 2018 10:11:11 +0200 Subject: [PATCH 1/4] 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 From 9b8cef7697c8812c26e76a7fc58c107f74230ec8 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 2 Oct 2018 15:56:39 +0100 Subject: [PATCH 2/4] build: Add a bug URI in a FIXME comment Signed-off-by: Philip Withnall https://phabricator.endlessm.com/T23859 --- meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/meson.build b/meson.build index fae8a04..3b8ea59 100644 --- a/meson.build +++ b/meson.build @@ -19,6 +19,7 @@ prefix = get_option('prefix') datadir = join_paths(prefix, get_option('datadir')) # FIXME: This isn’t exposed in accountsservice.pc +# See https://gitlab.freedesktop.org/accountsservice/accountsservice/merge_requests/16 accountsserviceinterfacesdir = join_paths(datadir, 'accountsservice', 'interfaces') dbus = dependency('dbus-1') From 1235c275eb87d382ea6be1f5a0b720ab79ffa575 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 2 Oct 2018 15:57:04 +0100 Subject: [PATCH 3/4] build: Add gobject-introspection support Verified to all be introspectable. Signed-off-by: Philip Withnall https://phabricator.endlessm.com/T23859 --- libeos-parental-controls/meson.build | 11 +++++++++++ meson.build | 1 + 2 files changed, 12 insertions(+) diff --git a/libeos-parental-controls/meson.build b/libeos-parental-controls/meson.build index 263fe0d..d801dd9 100644 --- a/libeos-parental-controls/meson.build +++ b/libeos-parental-controls/meson.build @@ -42,4 +42,15 @@ pkgconfig.generate( filebase: libeos_parental_controls_api_name, description: 'Library providing access to parental control settings.', requires: libeos_parental_controls_public_deps, +) + +gnome.generate_gir(libeos_parental_controls, + sources: libeos_parental_controls_sources + libeos_parental_controls_headers, + nsversion: libeos_parental_controls_api_version, + namespace: 'EosParentalControls', + symbol_prefix: 'epc_', + identifier_prefix: 'Epc', + export_packages: 'libeos-parental-controls', + includes: ['GObject-2.0', 'Gio-2.0'], + install: true, ) \ No newline at end of file diff --git a/meson.build b/meson.build index 3b8ea59..0bd1ef2 100644 --- a/meson.build +++ b/meson.build @@ -9,6 +9,7 @@ project('eos-parental-controls', 'c', ] ) +gnome = import('gnome') i18n = import('i18n') pkgconfig = import('pkgconfig') From 63d229e6537d6677cc3b4d9ad206f01526ab7434 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 2 Oct 2018 17:03:33 +0100 Subject: [PATCH 4/4] eos-parental-controls-client: Add simple client program This allows querying of the parental controls for a given user (or the current user). Includes documentation but no tests yet. Signed-off-by: Philip Withnall https://phabricator.endlessm.com/T23859 --- .../docs/eos-parental-controls-client.8 | 120 ++++++++++++ .../eos-parental-controls-client.py | 174 ++++++++++++++++++ eos-parental-controls-client/meson.build | 11 ++ meson.build | 2 + 4 files changed, 307 insertions(+) create mode 100644 eos-parental-controls-client/docs/eos-parental-controls-client.8 create mode 100644 eos-parental-controls-client/eos-parental-controls-client.py create mode 100644 eos-parental-controls-client/meson.build diff --git a/eos-parental-controls-client/docs/eos-parental-controls-client.8 b/eos-parental-controls-client/docs/eos-parental-controls-client.8 new file mode 100644 index 0000000..99910b5 --- /dev/null +++ b/eos-parental-controls-client/docs/eos-parental-controls-client.8 @@ -0,0 +1,120 @@ +.\" Manpage for eos\-parental\-controls\-client. +.\" Documentation is under the same licence as the eos\-parental\-controls +.\" package. +.TH man 8 "03 Oct 2018" "1.0" "eos\-parental\-controls\-client man page" +.\" +.SH NAME +.IX Header "NAME" +eos\-parental\-controls\-client — Parental Controls Access Utility +.\" +.SH SYNOPSIS +.IX Header "SYNOPSIS" +.\" +\fBeos\-parental\-controls\-client get [\-q] [\-n] [\fPUSER\fB] +.PP +\fBeos\-parental\-controls\-client check [\-q] [\-n] [\fPUSER\fB] \fPPATH\fB +.\" +.SH DESCRIPTION +.IX Header "DESCRIPTION" +.\" +\fBeos\-parental\-controls\-client\fP is a utility for querying and updating the +parental controls settings for users on the system. It will typically require +adminstrator access to do anything more than query the current user’s parental +controls. +.PP +It communicates with accounts-service, which stores parental controls data. +.PP +Its first argument is a command to run. Currently, the only supported commands +are \fBget\fP and \fBcheck\fP. +.\" +.SH \fBget\fP OPTIONS +.IX Header "get OPTIONS" +.\" +.IP "\fBUSER\fP" +Username or ID of the user to get the app filter for. If not specified, the +current user will be used by default. +.\" +.IP "\fB\-q\fP, \fB\-\-quiet\fP" +Only output error messages, and no informational messages, as the operation +progresses. (Default: Output informational messages.) +.\" +.IP "\fB\-n\fP, \fB\-\-no\-interactive\fP" +Do not allow interactive authorization with polkit. If this is needed to +complete the operation, the operation will fail. (Default: Allow interactive +authorization.) +.\" +.SH \fBcheck\fP OPTIONS +.IX Header "check OPTIONS" +.\" +.IP "\fBUSER\fP" +Username or ID of the user to get the app filter for. If not specified, the +current user will be used by default. +.\" +.IP "\fBPATH\fP" +Path to a program to check against the app filter, to see if it can be run by +the specified user. +.\" +.IP "\fB\-q\fP, \fB\-\-quiet\fP" +Only output error messages, and no informational messages, as the operation +progresses. (Default: Output informational messages.) +.\" +.IP "\fB\-n\fP, \fB\-\-no\-interactive\fP" +Do not allow interactive authorization with polkit. If this is needed to +complete the operation, the operation will fail. (Default: Allow interactive +authorization.) +.\" +.SH "ENVIRONMENT" +.IX Header "ENVIRONMENT" +.\" +\fBeos\-parental\-controls\-client\fP supports the standard GLib environment +variables for debugging. These variables are \fBnot\fP intended to be used in +production: +.\" +.IP \fI$G_MESSAGES_DEBUG\fP 4 +.IX Item "$G_MESSAGES_DEBUG" +This variable can contain one or more debug domain names to display debug output +for. The value \fIall\fP will enable all debug output. The default is for no +debug output to be enabled. +.\" +.SH "EXIT STATUS" +.IX Header "EXIT STATUS" +.\" +\fBeos\-parental\-controls\-client\fP may return one of several error codes if it +encounters problems. +.\" +.IP "0" 4 +.IX Item "0" +No problems occurred. The utility ran and successfully queried the app filter. +If running the \fBcheck\fP command, the given path was allowed to be run by the +given user. +.\" +.IP "1" 4 +.IX Item "1" +An invalid option was passed to \fBeos\-parental\-controls\-client\fP on +startup. +.\" +.IP "2" 4 +.IX Item "2" +The current user was not authorized to query the app filter for the given user. +.\" +.IP "3" 4 +.IX Item "3" +If running the \fBcheck\fP command, the given path was \fInot\fP allowed to be +run by the given user. +.\" +.SH BUGS +.IX Header "BUGS" +.\" +Any bugs which are found should be reported on the project website: +.br +\fIhttps://support.endlessm.com/\fP +.\" +.SH AUTHOR +.IX Header "AUTHOR" +.\" +Endless Mobile, Inc. +.\" +.SH COPYRIGHT +.IX Header "COPYRIGHT" +.\" +Copyright © 2018 Endless Mobile, Inc. diff --git a/eos-parental-controls-client/eos-parental-controls-client.py b/eos-parental-controls-client/eos-parental-controls-client.py new file mode 100644 index 0000000..4436513 --- /dev/null +++ b/eos-parental-controls-client/eos-parental-controls-client.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# 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 + +import argparse +import os +import pwd +import sys +import gi +gi.require_version('EosParentalControls', '0') # noqa +from gi.repository import EosParentalControls, GLib + + +# Exit codes, which are a documented part of the API. +EXIT_SUCCESS = 0 +EXIT_INVALID_OPTION = 1 +EXIT_PERMISSION_DENIED = 2 +EXIT_PATH_NOT_ALLOWED = 3 + + +def __get_app_filter(user_id, interactive): + """Get the app filter for `user_id` off the bus. + + If `interactive` is `True`, interactive polkit authorisation dialogues will + be allowed. An exception will be raised on failure.""" + app_filter = None + exception = None + + def __get_cb(obj, result, user_data): + nonlocal app_filter, exception + try: + app_filter = EosParentalControls.get_app_filter_finish(result) + except Exception as e: + exception = e + + EosParentalControls.get_app_filter_async( + connection=None, user_id=user_id, + allow_interactive_authorization=interactive, cancellable=None, + callback=__get_cb, user_data=None) + + context = GLib.MainContext.default() + while not app_filter and not exception: + context.iteration(True) + + if exception: + raise exception + return app_filter + + +def __get_app_filter_or_error(user_id, interactive): + """Wrapper around __get_app_filter() which prints an error and raises + SystemExit, rather than an internal exception.""" + try: + return __get_app_filter(user_id, interactive) + except GLib.Error as e: + print('Error getting app filter for user {}: {}'.format( + user_id, e.message), file=sys.stderr) + raise SystemExit(EXIT_PERMISSION_DENIED) + + +def __lookup_user_id(user): + """Convert a command-line specified username or ID into a user ID. If + `user` is empty, use the current user ID. + + Raise KeyError if lookup fails.""" + if user == '': + return os.getuid() + elif user.isdigit(): + return int(user) + else: + return pwd.getpwnam(user).pw_uid + + +def __lookup_user_id_or_error(user): + """Wrapper around __lookup_user_id() which prints an error and raises + SystemExit, rather than an internal exception.""" + try: + return __lookup_user_id(user) + except KeyError: + print('Error getting ID for username {}'.format(user), file=sys.stderr) + raise SystemExit(EXIT_INVALID_OPTION) + + +def command_get(user, quiet=False, interactive=True): + """Get the app filter for the given user.""" + user_id = __lookup_user_id_or_error(user) + __get_app_filter_or_error(user_id, interactive) + + print('App filter for user {} retrieved'.format(user_id)) + + +def command_check(user, path, quiet=False, interactive=True): + """Check the given path is runnable by the given user, according to their + app filter.""" + user_id = __lookup_user_id_or_error(user) + app_filter = __get_app_filter_or_error(user_id, interactive) + + path = os.path.abspath(path) + + if app_filter.is_path_allowed(path): + print('Path {} is allowed by app filter for user {}'.format( + path, user_id)) + return + else: + print('Path {} is not allowed by app filter for user {}'.format( + path, user_id)) + raise SystemExit(EXIT_PATH_NOT_ALLOWED) + + +def main(): + # Parse command line arguments + parser = argparse.ArgumentParser( + description='Query and update parental controls.') + subparsers = parser.add_subparsers(metavar='command', + help='command to run (default: ‘get’)') + parser.set_defaults(function=command_get) + parser.add_argument('-q', '--quiet', action='store_true', + help='output no informational messages') + parser.set_defaults(quiet=False) + + # Common options for the subcommands which might need authorisation. + common_parser = argparse.ArgumentParser(add_help=False) + group = common_parser.add_mutually_exclusive_group() + group.add_argument('-n', '--no-interactive', dest='interactive', + action='store_false', + help='do not allow interactive polkit authorization ' + 'dialogues') + group.add_argument('--interactive', dest='interactive', + action='store_true', + help='opposite of --no-interactive') + common_parser.set_defaults(interactive=True) + + # ‘get’ command + parser_get = subparsers.add_parser('get', parents=[common_parser], + help='get current parental controls ' + 'settings') + parser_get.set_defaults(function=command_get) + parser_get.add_argument('user', default='', nargs='?', + help='user ID or username to get the app filter ' + 'for (default: current user)') + + # ‘check’ command + parser_check = subparsers.add_parser('check', parents=[common_parser], + help='check whether a path is ' + 'allowed by app filter') + parser_check.set_defaults(function=command_check) + parser_check.add_argument('user', default='', nargs='?', + help='user ID or username to get the app filter ' + 'for (default: current user)') + parser_check.add_argument('path', + help='path to a program to check') + + # Parse the command line arguments and run the subcommand. + args = parser.parse_args() + args_dict = dict((k, v) for k, v in vars(args).items() if k != 'function') + args.function(**args_dict) + + +if __name__ == '__main__': + main() diff --git a/eos-parental-controls-client/meson.build b/eos-parental-controls-client/meson.build new file mode 100644 index 0000000..c06732a --- /dev/null +++ b/eos-parental-controls-client/meson.build @@ -0,0 +1,11 @@ +# Python program +install_data('eos-parental-controls-client.py', + install_dir: bindir, + install_mode: 'rwxr-xr-x', + rename: ['eos-parental-controls-client'], +) + +# Documentation +install_man('docs/eos-parental-controls-client.8') + +# TODO subdir('tests') \ No newline at end of file diff --git a/meson.build b/meson.build index 0bd1ef2..daeb3b2 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,7 @@ meson_make_symlink = join_paths(meson.source_root(), 'tools', 'meson-make-symlin po_dir = join_paths(meson.source_root(), 'po') prefix = get_option('prefix') +bindir = join_paths(prefix, get_option('bindir')) datadir = join_paths(prefix, get_option('datadir')) # FIXME: This isn’t exposed in accountsservice.pc @@ -103,4 +104,5 @@ cc = meson.get_compiler('c') add_project_arguments(cc.get_supported_arguments(test_c_args), language: 'c') subdir('accounts-service') +subdir('eos-parental-controls-client') subdir('libeos-parental-controls') \ No newline at end of file