libmalcontent: Factor getting/setting app filter into a manager

Create a new MctManager object which is used as the anchor for getting
or setting MctAppFilters.

This changes the API naming around quite a bit, but doesn’t really
change its behaviour or functionality — see the tests for examples of
how little things change.

This is one step on the way to emitting a signal (from MctManager) when
a user’s parental controls change.

Signed-off-by: Philip Withnall <withnall@endlessm.com>

https://gitlab.freedesktop.org/pwithnall/malcontent/issues/1
This commit is contained in:
Philip Withnall 2019-04-24 12:44:50 +01:00
parent 388dedbff2
commit 68ebe8b568
8 changed files with 1074 additions and 829 deletions

View file

@ -29,39 +29,12 @@
#include <gio/gio.h>
#include <libmalcontent/app-filter.h>
#include "libmalcontent/app-filter-private.h"
G_DEFINE_QUARK (MctAppFilterError, mct_app_filter_error)
/**
* MctAppFilterListType:
* @MCT_APP_FILTER_LIST_BLACKLIST: Any program in the list is not allowed to
* be run.
* @MCT_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.2.0
*/
typedef enum
{
MCT_APP_FILTER_LIST_BLACKLIST,
MCT_APP_FILTER_LIST_WHITELIST,
} MctAppFilterListType;
struct _MctAppFilter
{
gint ref_count;
uid_t user_id;
gchar **app_list; /* (owned) (array zero-terminated=1) */
MctAppFilterListType app_list_type;
GVariant *oars_ratings; /* (type a{ss}) (owned non-floating) */
gboolean allow_user_installation;
gboolean allow_system_installation;
};
/* struct _MctAppFilter is defined in app-filter-private.h */
G_DEFINE_BOXED_TYPE (MctAppFilter, mct_app_filter,
mct_app_filter_ref, mct_app_filter_unref)
@ -453,639 +426,6 @@ mct_app_filter_is_system_installation_allowed (MctAppFilter *filter)
return filter->allow_system_installation;
}
/**
* _mct_app_filter_build_app_filter_variant:
* @filter: an #MctAppFilter
*
* Build a #GVariant which contains the app filter from @filter, in the format
* used for storing it in AccountsService.
*
* Returns: (transfer floating): a new, floating #GVariant containing the app
* filter
* Since: 0.2.0
*/
static GVariant *
_mct_app_filter_build_app_filter_variant (MctAppFilter *filter)
{
g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(bas)"));
g_return_val_if_fail (filter != NULL, NULL);
g_return_val_if_fail (filter->ref_count >= 1, NULL);
g_variant_builder_add (&builder, "b",
(filter->app_list_type == MCT_APP_FILTER_LIST_WHITELIST));
g_variant_builder_open (&builder, G_VARIANT_TYPE ("as"));
for (gsize i = 0; filter->app_list[i] != NULL; i++)
g_variant_builder_add (&builder, "s", filter->app_list[i]);
g_variant_builder_close (&builder);
return g_variant_builder_end (&builder);
}
/* Check if @error is a D-Bus remote error matching @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 #MctAppFilter 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 (MCT_APP_FILTER_ERROR, MCT_APP_FILTER_ERROR_PERMISSION_DENIED,
_("Not allowed to query app filter data for user %u"),
(guint) user_id);
else if (g_error_matches (bus_error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD) ||
bus_remote_error_matches (bus_error, "org.freedesktop.Accounts.Error.Failed"))
return g_error_new (MCT_APP_FILTER_ERROR, MCT_APP_FILTER_ERROR_INVALID_USER,
_("User %u does not exist"), (guint) user_id);
else
return g_error_copy (bus_error);
}
/* Find the object path for the given @user_id on the accountsservice D-Bus
* interface, by calling its FindUserById() method. This is a synchronous,
* blocking function. */
static gchar *
accounts_find_user_by_id (GDBusConnection *connection,
uid_t user_id,
gboolean allow_interactive_authorization,
GCancellable *cancellable,
GError **error)
{
g_autofree gchar *object_path = NULL;
g_autoptr(GVariant) result_variant = NULL;
g_autoptr(GError) local_error = NULL;
result_variant =
g_dbus_connection_call_sync (connection,
"org.freedesktop.Accounts",
"/org/freedesktop/Accounts",
"org.freedesktop.Accounts",
"FindUserById",
g_variant_new ("(x)", (gint64) user_id),
G_VARIANT_TYPE ("(o)"),
allow_interactive_authorization
? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION
: G_DBUS_CALL_FLAGS_NONE,
-1, /* timeout, ms */
cancellable,
&local_error);
if (local_error != NULL)
{
g_autoptr(GError) app_filter_error = bus_error_to_app_filter_error (local_error,
user_id);
g_propagate_error (error, g_steal_pointer (&app_filter_error));
return NULL;
}
g_variant_get (result_variant, "(o)", &object_path);
return g_steal_pointer (&object_path);
}
/**
* mct_get_app_filter:
* @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()
* @flags: flags to affect the behaviour of the call
* @cancellable: (nullable): a #GCancellable, or %NULL
* @error: return location for a #GError, or %NULL
*
* Synchronous version of mct_get_app_filter_async().
*
* Returns: (transfer full): app filter for the queried user
* Since: 0.3.0
*/
MctAppFilter *
mct_get_app_filter (GDBusConnection *connection,
uid_t user_id,
MctGetAppFilterFlags flags,
GCancellable *cancellable,
GError **error)
{
g_autofree gchar *object_path = NULL;
g_autoptr(GVariant) result_variant = NULL;
g_autoptr(GVariant) properties = NULL;
g_autoptr(GError) local_error = NULL;
g_autoptr(MctAppFilter) app_filter = NULL;
gboolean is_whitelist;
g_auto(GStrv) app_list = NULL;
const gchar *content_rating_kind;
g_autoptr(GVariant) oars_variant = NULL;
gboolean allow_user_installation;
gboolean allow_system_installation;
g_return_val_if_fail (connection == NULL || G_IS_DBUS_CONNECTION (connection), NULL);
g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
if (connection == NULL)
connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, cancellable, error);
if (connection == NULL)
return NULL;
object_path = accounts_find_user_by_id (connection, user_id,
(flags & MCT_GET_APP_FILTER_FLAGS_INTERACTIVE),
cancellable, error);
if (object_path == NULL)
return NULL;
result_variant =
g_dbus_connection_call_sync (connection,
"org.freedesktop.Accounts",
object_path,
"org.freedesktop.DBus.Properties",
"GetAll",
g_variant_new ("(s)", "com.endlessm.ParentalControls.AppFilter"),
G_VARIANT_TYPE ("(a{sv})"),
(flags & MCT_GET_APP_FILTER_FLAGS_INTERACTIVE)
? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION
: G_DBUS_CALL_FLAGS_NONE,
-1, /* timeout, ms */
cancellable,
&local_error);
if (local_error != NULL)
{
g_autoptr(GError) app_filter_error = NULL;
if (g_error_matches (local_error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS))
{
/* o.fd.D.GetAll() will return InvalidArgs errors if
* accountsservice doesnt have the com.endlessm.ParentalControls.AppFilter
* extension interface installed. */
app_filter_error = g_error_new_literal (MCT_APP_FILTER_ERROR,
MCT_APP_FILTER_ERROR_DISABLED,
_("App filtering is globally disabled"));
}
else
{
app_filter_error = bus_error_to_app_filter_error (local_error,
user_id);
}
g_propagate_error (error, g_steal_pointer (&app_filter_error));
return NULL;
}
/* Extract the properties we care about. They may be silently omitted from the
* results if we dont have permission to access them. */
properties = g_variant_get_child_value (result_variant, 0);
if (!g_variant_lookup (properties, "AppFilter", "(b^as)",
&is_whitelist, &app_list))
{
g_set_error (error, MCT_APP_FILTER_ERROR,
MCT_APP_FILTER_ERROR_PERMISSION_DENIED,
_("Not allowed to query app filter data for user %u"),
(guint) user_id);
return NULL;
}
if (!g_variant_lookup (properties, "OarsFilter", "(&s@a{ss})",
&content_rating_kind, &oars_variant))
{
/* Default value. */
content_rating_kind = "oars-1.1";
oars_variant = g_variant_new ("a{ss}", NULL);
}
/* Check that the OARS filter is in a format we support. Currently, thats
* only oars-1.0 and oars-1.1. */
if (!g_str_equal (content_rating_kind, "oars-1.0") &&
!g_str_equal (content_rating_kind, "oars-1.1"))
{
g_set_error (error, MCT_APP_FILTER_ERROR,
MCT_APP_FILTER_ERROR_INVALID_DATA,
_("OARS filter for user %u has an unrecognized kind %s"),
(guint) user_id, content_rating_kind);
return NULL;
}
if (!g_variant_lookup (properties, "AllowUserInstallation", "b",
&allow_user_installation))
{
/* Default value. */
allow_user_installation = TRUE;
}
if (!g_variant_lookup (properties, "AllowSystemInstallation", "b",
&allow_system_installation))
{
/* Default value. */
allow_system_installation = FALSE;
}
/* Success. Create an #MctAppFilter object to contain the results. */
app_filter = g_new0 (MctAppFilter, 1);
app_filter->ref_count = 1;
app_filter->user_id = user_id;
app_filter->app_list = g_steal_pointer (&app_list);
app_filter->app_list_type =
is_whitelist ? MCT_APP_FILTER_LIST_WHITELIST : MCT_APP_FILTER_LIST_BLACKLIST;
app_filter->oars_ratings = g_steal_pointer (&oars_variant);
app_filter->allow_user_installation = allow_user_installation;
app_filter->allow_system_installation = allow_system_installation;
return g_steal_pointer (&app_filter);
}
static void get_app_filter_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable);
typedef struct
{
GDBusConnection *connection; /* (nullable) (owned) */
uid_t user_id;
MctGetAppFilterFlags flags;
} GetAppFilterData;
static void
get_app_filter_data_free (GetAppFilterData *data)
{
g_clear_object (&data->connection);
g_free (data);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetAppFilterData, get_app_filter_data_free)
/**
* mct_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()
* @flags: flags to affect the behaviour of the call
* @cancellable: (nullable): a #GCancellable, or %NULL
* @callback: a #GAsyncReadyCallback
* @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. Its 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 #MctAppFilterError, a #GDBusError or a #GIOError will be
* returned.
*
* Since: 0.3.0
*/
void
mct_get_app_filter_async (GDBusConnection *connection,
uid_t user_id,
MctGetAppFilterFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
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, mct_get_app_filter_async);
data = g_new0 (GetAppFilterData, 1);
data->connection = (connection != NULL) ? g_object_ref (connection) : NULL;
data->user_id = user_id;
data->flags = flags;
g_task_set_task_data (task, g_steal_pointer (&data),
(GDestroyNotify) get_app_filter_data_free);
g_task_run_in_thread (task, get_app_filter_thread_cb);
}
static void
get_app_filter_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
g_autoptr(MctAppFilter) filter = NULL;
GetAppFilterData *data = task_data;
g_autoptr(GError) local_error = NULL;
filter = mct_get_app_filter (data->connection, data->user_id,
data->flags,
cancellable, &local_error);
if (local_error != NULL)
g_task_return_error (task, g_steal_pointer (&local_error));
else
g_task_return_pointer (task, g_steal_pointer (&filter),
(GDestroyNotify) mct_app_filter_unref);
}
/**
* mct_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 mct_get_app_filter_async().
*
* Returns: (transfer full): app filter for the queried user
* Since: 0.2.0
*/
MctAppFilter *
mct_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);
}
/**
* mct_set_app_filter:
* @connection: (nullable): a #GDBusConnection to the system bus, or %NULL to
* use the default
* @user_id: ID of the user to set the filter for, typically coming from getuid()
* @app_filter: (transfer none): the app filter to set for the user
* @flags: flags to affect the behaviour of the call
* @cancellable: (nullable): a #GCancellable, or %NULL
* @error: return location for a #GError, or %NULL
*
* Synchronous version of mct_set_app_filter_async().
*
* Returns: %TRUE on success, %FALSE otherwise
* Since: 0.3.0
*/
gboolean
mct_set_app_filter (GDBusConnection *connection,
uid_t user_id,
MctAppFilter *app_filter,
MctSetAppFilterFlags flags,
GCancellable *cancellable,
GError **error)
{
g_autofree gchar *object_path = NULL;
g_autoptr(GVariant) app_filter_variant = NULL;
g_autoptr(GVariant) oars_filter_variant = NULL;
g_autoptr(GVariant) allow_user_installation_variant = NULL;
g_autoptr(GVariant) allow_system_installation_variant = NULL;
g_autoptr(GVariant) app_filter_result_variant = NULL;
g_autoptr(GVariant) oars_filter_result_variant = NULL;
g_autoptr(GVariant) allow_user_installation_result_variant = NULL;
g_autoptr(GVariant) allow_system_installation_result_variant = NULL;
g_autoptr(GError) local_error = NULL;
g_return_val_if_fail (connection == NULL || G_IS_DBUS_CONNECTION (connection), FALSE);
g_return_val_if_fail (app_filter != NULL, FALSE);
g_return_val_if_fail (app_filter->ref_count >= 1, FALSE);
g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
if (connection == NULL)
connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, cancellable, error);
if (connection == NULL)
return FALSE;
object_path = accounts_find_user_by_id (connection, user_id,
(flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE),
cancellable, error);
if (object_path == NULL)
return FALSE;
app_filter_variant = _mct_app_filter_build_app_filter_variant (app_filter);
oars_filter_variant = g_variant_new ("(s@a{ss})", "oars-1.1",
app_filter->oars_ratings);
allow_user_installation_variant = g_variant_new_boolean (app_filter->allow_user_installation);
allow_system_installation_variant = g_variant_new_boolean (app_filter->allow_system_installation);
app_filter_result_variant =
g_dbus_connection_call_sync (connection,
"org.freedesktop.Accounts",
object_path,
"org.freedesktop.DBus.Properties",
"Set",
g_variant_new ("(ssv)",
"com.endlessm.ParentalControls.AppFilter",
"AppFilter",
g_steal_pointer (&app_filter_variant)),
G_VARIANT_TYPE ("()"),
(flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE)
? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION
: G_DBUS_CALL_FLAGS_NONE,
-1, /* timeout, ms */
cancellable,
&local_error);
if (local_error != NULL)
{
g_propagate_error (error, bus_error_to_app_filter_error (local_error, user_id));
return FALSE;
}
oars_filter_result_variant =
g_dbus_connection_call_sync (connection,
"org.freedesktop.Accounts",
object_path,
"org.freedesktop.DBus.Properties",
"Set",
g_variant_new ("(ssv)",
"com.endlessm.ParentalControls.AppFilter",
"OarsFilter",
g_steal_pointer (&oars_filter_variant)),
G_VARIANT_TYPE ("()"),
(flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE)
? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION
: G_DBUS_CALL_FLAGS_NONE,
-1, /* timeout, ms */
cancellable,
&local_error);
if (local_error != NULL)
{
g_propagate_error (error, bus_error_to_app_filter_error (local_error, user_id));
return FALSE;
}
allow_user_installation_result_variant =
g_dbus_connection_call_sync (connection,
"org.freedesktop.Accounts",
object_path,
"org.freedesktop.DBus.Properties",
"Set",
g_variant_new ("(ssv)",
"com.endlessm.ParentalControls.AppFilter",
"AllowUserInstallation",
g_steal_pointer (&allow_user_installation_variant)),
G_VARIANT_TYPE ("()"),
(flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE)
? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION
: G_DBUS_CALL_FLAGS_NONE,
-1, /* timeout, ms */
cancellable,
&local_error);
if (local_error != NULL)
{
g_propagate_error (error, bus_error_to_app_filter_error (local_error, user_id));
return FALSE;
}
allow_system_installation_result_variant =
g_dbus_connection_call_sync (connection,
"org.freedesktop.Accounts",
object_path,
"org.freedesktop.DBus.Properties",
"Set",
g_variant_new ("(ssv)",
"com.endlessm.ParentalControls.AppFilter",
"AllowSystemInstallation",
g_steal_pointer (&allow_system_installation_variant)),
G_VARIANT_TYPE ("()"),
(flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE)
? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION
: G_DBUS_CALL_FLAGS_NONE,
-1, /* timeout, ms */
cancellable,
&local_error);
if (local_error != NULL)
{
g_propagate_error (error, bus_error_to_app_filter_error (local_error, user_id));
return FALSE;
}
return TRUE;
}
static void set_app_filter_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable);
typedef struct
{
GDBusConnection *connection; /* (nullable) (owned) */
uid_t user_id;
MctAppFilter *app_filter; /* (owned) */
MctSetAppFilterFlags flags;
} SetAppFilterData;
static void
set_app_filter_data_free (SetAppFilterData *data)
{
g_clear_object (&data->connection);
mct_app_filter_unref (data->app_filter);
g_free (data);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (SetAppFilterData, set_app_filter_data_free)
/**
* mct_set_app_filter_async:
* @connection: (nullable): a #GDBusConnection to the system bus, or %NULL to
* use the default
* @user_id: ID of the user to set the filter for, typically coming from getuid()
* @app_filter: (transfer none): the app filter to set for the user
* @flags: flags to affect the behaviour of the call
* @cancellable: (nullable): a #GCancellable, or %NULL
* @callback: a #GAsyncReadyCallback
* @user_data: user data to pass to @callback
*
* Asynchronously set the app filter settings for the given @user_id to the
* given @app_filter instance. This will set all fields of the app filter.
*
* @connection should be a connection to the system bus, where accounts-service
* runs. Its 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 #MctAppFilterError, a #GDBusError or a #GIOError will be
* returned. The users app filter settings will be left in an undefined state.
*
* Since: 0.3.0
*/
void
mct_set_app_filter_async (GDBusConnection *connection,
uid_t user_id,
MctAppFilter *app_filter,
MctSetAppFilterFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autoptr(GTask) task = NULL;
g_autoptr(SetAppFilterData) data = NULL;
g_return_if_fail (connection == NULL || G_IS_DBUS_CONNECTION (connection));
g_return_if_fail (app_filter != NULL);
g_return_if_fail (app_filter->ref_count >= 1);
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, mct_set_app_filter_async);
data = g_new0 (SetAppFilterData, 1);
data->connection = (connection != NULL) ? g_object_ref (connection) : NULL;
data->user_id = user_id;
data->app_filter = mct_app_filter_ref (app_filter);
data->flags = flags;
g_task_set_task_data (task, g_steal_pointer (&data),
(GDestroyNotify) set_app_filter_data_free);
g_task_run_in_thread (task, set_app_filter_thread_cb);
}
static void
set_app_filter_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
gboolean success;
SetAppFilterData *data = task_data;
g_autoptr(GError) local_error = NULL;
success = mct_set_app_filter (data->connection, data->user_id,
data->app_filter, data->flags,
cancellable, &local_error);
if (local_error != NULL)
g_task_return_error (task, g_steal_pointer (&local_error));
else
g_task_return_boolean (task, success);
}
/**
* mct_set_app_filter_finish:
* @result: a #GAsyncResult
* @error: return location for a #GError, or %NULL
*
* Finish an asynchronous operation to set the app filter for a user, started
* with mct_set_app_filter_async().
*
* Returns: %TRUE on success, %FALSE otherwise
* Since: 0.2.0
*/
gboolean
mct_set_app_filter_finish (GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE);
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
return g_task_propagate_boolean (G_TASK (result), error);
}
/*
* Actual implementation of #MctAppFilterBuilder.
*