diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 426edbf..f2740ec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,7 +5,7 @@ before_script: libxml2-devel dbus-daemon glib2-devel dbus-devel gobject-introspection-devel gettext-devel polkit-devel polkit-gnome git - lcov + lcov pam-devel - export LANG=C.UTF-8 stages: diff --git a/accounts-service/com.endlessm.ParentalControls.SessionLimits.xml b/accounts-service/com.endlessm.ParentalControls.SessionLimits.xml new file mode 100644 index 0000000..6dbba64 --- /dev/null +++ b/accounts-service/com.endlessm.ParentalControls.SessionLimits.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accounts-service/com.endlessm.ParentalControls.policy.in b/accounts-service/com.endlessm.ParentalControls.policy.in index ff09f2d..bdeb76a 100644 --- a/accounts-service/com.endlessm.ParentalControls.policy.in +++ b/accounts-service/com.endlessm.ParentalControls.policy.in @@ -39,4 +39,44 @@ auth_admin_keep - \ No newline at end of file + + + Change your own session limits + Authentication is required to change your session limits. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Read your own session limits + Authentication is required to read your session limits. + + yes + yes + yes + + + + + Change another user’s session limits + Authentication is required to change another user’s session limits. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Read another user’s session limits + Authentication is required to read another user’s session limits. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + diff --git a/accounts-service/com.endlessm.ParentalControls.rules b/accounts-service/com.endlessm.ParentalControls.rules index e630bcf..b3bf998 100644 --- a/accounts-service/com.endlessm.ParentalControls.rules +++ b/accounts-service/com.endlessm.ParentalControls.rules @@ -23,7 +23,9 @@ polkit.addRule(function(action, subject) { /* Allow administrators to read parental controls (for any account) without * needing an additional polkit authorisation dialogue. */ if ((action.id == "com.endlessm.ParentalControls.AppFilter.ReadOwn" || - action.id == "com.endlessm.ParentalControls.AppFilter.ReadAny") && + action.id == "com.endlessm.ParentalControls.AppFilter.ReadAny" || + action.id == "com.endlessm.ParentalControls.SessionLimits.ReadOwn" || + action.id == "com.endlessm.ParentalControls.SessionLimits.ReadAny") && subject.active && subject.local && subject.isInGroup("sudo")) { return polkit.Result.YES; diff --git a/accounts-service/meson.build b/accounts-service/meson.build index c2b61fe..0a304ae 100644 --- a/accounts-service/meson.build +++ b/accounts-service/meson.build @@ -6,11 +6,19 @@ i18n.merge_file('com.endlessm.ParentalControls.policy', install_dir: polkitpolicydir, ) -install_data('com.endlessm.ParentalControls.AppFilter.xml', - install_dir: dbusinterfacesdir) -meson.add_install_script(meson_make_symlink, - join_paths(dbusinterfacesdir, 'com.endlessm.ParentalControls.AppFilter.xml'), - join_paths(accountsserviceinterfacesdir, 'com.endlessm.ParentalControls.AppFilter.xml')) +dbus_interfaces = [ + 'com.endlessm.ParentalControls.AppFilter', + 'com.endlessm.ParentalControls.SessionLimits', +] + +foreach dbus_interface: dbus_interfaces + filename = dbus_interface + '.xml' + install_data(filename, + install_dir: dbusinterfacesdir) + meson.add_install_script(meson_make_symlink, + join_paths(dbusinterfacesdir, filename), + join_paths(accountsserviceinterfacesdir, filename)) +endforeach install_data('com.endlessm.ParentalControls.rules', install_dir: join_paths(get_option('datadir'), 'polkit-1', 'rules.d')) diff --git a/libmalcontent/app-filter.c b/libmalcontent/app-filter.c index 266b3d6..3599424 100644 --- a/libmalcontent/app-filter.c +++ b/libmalcontent/app-filter.c @@ -33,7 +33,12 @@ #include "libmalcontent/app-filter-private.h" -G_DEFINE_QUARK (MctAppFilterError, mct_app_filter_error) +/* FIXME: Eventually deprecate these compatibility fallbacks. */ +GQuark +mct_app_filter_error_quark (void) +{ + return mct_manager_error_quark (); +} /* struct _MctAppFilter is defined in app-filter-private.h */ diff --git a/libmalcontent/app-filter.h b/libmalcontent/app-filter.h index 263b4ee..cc92e23 100644 --- a/libmalcontent/app-filter.h +++ b/libmalcontent/app-filter.h @@ -29,31 +29,6 @@ G_BEGIN_DECLS -/** - * MctAppFilterError: - * @MCT_APP_FILTER_ERROR_INVALID_USER: Given user ID doesn’t exist - * @MCT_APP_FILTER_ERROR_PERMISSION_DENIED: Not authorized to query the app - * filter for the given user - * @MCT_APP_FILTER_ERROR_INVALID_DATA: The data stored in the app filter for - * a user is inconsistent or invalid - * @MCT_APP_FILTER_ERROR_DISABLED: App filtering is disabled for all users (Since: 0.3.0) - * - * Errors relating to #MctAppFilter instances, which can be returned by - * mct_manager_get_app_filter_async() (for example). - * - * Since: 0.2.0 - */ -typedef enum -{ - MCT_APP_FILTER_ERROR_INVALID_USER, - MCT_APP_FILTER_ERROR_PERMISSION_DENIED, - MCT_APP_FILTER_ERROR_INVALID_DATA, - MCT_APP_FILTER_ERROR_DISABLED, -} MctAppFilterError; - -GQuark mct_app_filter_error_quark (void); -#define MCT_APP_FILTER_ERROR mct_app_filter_error_quark () - /** * MctAppFilterOarsValue: * @MCT_APP_FILTER_OARS_VALUE_UNKNOWN: Unknown value for the given @@ -197,4 +172,16 @@ void mct_app_filter_builder_set_allow_user_installation (MctAppFilterBuilder * void mct_app_filter_builder_set_allow_system_installation (MctAppFilterBuilder *builder, gboolean allow_system_installation); +#include + +/* FIXME: Eventually deprecate these compatibility fallbacks. */ +typedef MctManagerError MctAppFilterError; +#define MCT_APP_FILTER_ERROR_INVALID_USER MCT_MANAGER_ERROR_INVALID_USER +#define MCT_APP_FILTER_ERROR_PERMISSION_DENIED MCT_MANAGER_ERROR_PERMISSION_DENIED +#define MCT_APP_FILTER_ERROR_INVALID_DATA MCT_MANAGER_ERROR_INVALID_DATA +#define MCT_APP_FILTER_ERROR_DISABLED MCT_MANAGER_ERROR_DISABLED + +GQuark mct_app_filter_error_quark (void); +#define MCT_APP_FILTER_ERROR mct_app_filter_error_quark () + G_END_DECLS diff --git a/libmalcontent/malcontent.h b/libmalcontent/malcontent.h index d3f2865..2d523be 100644 --- a/libmalcontent/malcontent.h +++ b/libmalcontent/malcontent.h @@ -24,3 +24,4 @@ #include #include +#include diff --git a/libmalcontent/manager.c b/libmalcontent/manager.c index 5f9c76b..eef42fb 100644 --- a/libmalcontent/manager.c +++ b/libmalcontent/manager.c @@ -28,8 +28,13 @@ #include #include #include +#include #include "libmalcontent/app-filter-private.h" +#include "libmalcontent/session-limits-private.h" + + +G_DEFINE_QUARK (MctManagerError, mct_manager_error) /** * MctManager: @@ -298,19 +303,19 @@ bus_remote_error_matches (const GError *error, return g_str_equal (error_name, expected_error_name); } -/* Convert a #GDBusError into a #MctAppFilter error. */ +/* Convert a #GDBusError into a #MctManagerError. */ static GError * -bus_error_to_app_filter_error (const GError *bus_error, - uid_t user_id) +bus_error_to_manager_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, + return g_error_new (MCT_MANAGER_ERROR, MCT_MANAGER_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, + return g_error_new (MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_USER, _("User %u does not exist"), (guint) user_id); else return g_error_copy (bus_error); @@ -346,8 +351,8 @@ accounts_find_user_by_id (GDBusConnection *connection, &local_error); if (local_error != NULL) { - g_autoptr(GError) app_filter_error = bus_error_to_app_filter_error (local_error, - user_id); + g_autoptr(GError) app_filter_error = bus_error_to_manager_error (local_error, + user_id); g_propagate_error (error, g_steal_pointer (&app_filter_error)); return NULL; } @@ -373,7 +378,7 @@ accounts_find_user_by_id (GDBusConnection *connection, MctAppFilter * mct_manager_get_app_filter (MctManager *self, uid_t user_id, - MctGetAppFilterFlags flags, + MctManagerGetValueFlags flags, GCancellable *cancellable, GError **error) { @@ -394,7 +399,7 @@ mct_manager_get_app_filter (MctManager *self, g_return_val_if_fail (error == NULL || *error == NULL, NULL); object_path = accounts_find_user_by_id (self->connection, user_id, - (flags & MCT_GET_APP_FILTER_FLAGS_INTERACTIVE), + (flags & MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE), cancellable, error); if (object_path == NULL) return NULL; @@ -407,7 +412,7 @@ mct_manager_get_app_filter (MctManager *self, "GetAll", g_variant_new ("(s)", "com.endlessm.ParentalControls.AppFilter"), G_VARIANT_TYPE ("(a{sv})"), - (flags & MCT_GET_APP_FILTER_FLAGS_INTERACTIVE) + (flags & MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ @@ -415,24 +420,23 @@ mct_manager_get_app_filter (MctManager *self, &local_error); if (local_error != NULL) { - g_autoptr(GError) app_filter_error = NULL; + g_autoptr(GError) manager_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 doesn’t 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")); + manager_error = g_error_new_literal (MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_DISABLED, + _("App filtering is globally disabled")); } else { - app_filter_error = bus_error_to_app_filter_error (local_error, - user_id); + manager_error = bus_error_to_manager_error (local_error, user_id); } - g_propagate_error (error, g_steal_pointer (&app_filter_error)); + g_propagate_error (error, g_steal_pointer (&manager_error)); return NULL; } @@ -442,8 +446,8 @@ mct_manager_get_app_filter (MctManager *self, 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, + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_PERMISSION_DENIED, _("Not allowed to query app filter data for user %u"), (guint) user_id); return NULL; @@ -462,8 +466,8 @@ mct_manager_get_app_filter (MctManager *self, 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, + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_INVALID_DATA, _("OARS filter for user %u has an unrecognized kind ‘%s’"), (guint) user_id, content_rating_kind); return NULL; @@ -505,7 +509,7 @@ static void get_app_filter_thread_cb (GTask *task, typedef struct { uid_t user_id; - MctGetAppFilterFlags flags; + MctManagerGetValueFlags flags; } GetAppFilterData; static void @@ -528,7 +532,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetAppFilterData, get_app_filter_data_free) * Asynchronously get a snapshot of the app filter settings for the given * @user_id. * - * On failure, an #MctAppFilterError, a #GDBusError or a #GIOError will be + * On failure, an #MctManagerError, a #GDBusError or a #GIOError will be * returned. * * Since: 0.3.0 @@ -536,7 +540,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetAppFilterData, get_app_filter_data_free) void mct_manager_get_app_filter_async (MctManager *self, uid_t user_id, - MctGetAppFilterFlags flags, + MctManagerGetValueFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) @@ -623,7 +627,7 @@ gboolean mct_manager_set_app_filter (MctManager *self, uid_t user_id, MctAppFilter *app_filter, - MctSetAppFilterFlags flags, + MctManagerSetValueFlags flags, GCancellable *cancellable, GError **error) { @@ -637,7 +641,6 @@ mct_manager_set_app_filter (MctManager *self, 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_autoptr(GDBusConnection) connection = NULL; g_return_val_if_fail (MCT_IS_MANAGER (self), FALSE); g_return_val_if_fail (app_filter != NULL, FALSE); @@ -646,7 +649,7 @@ mct_manager_set_app_filter (MctManager *self, g_return_val_if_fail (error == NULL || *error == NULL, FALSE); object_path = accounts_find_user_by_id (self->connection, user_id, - (flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE), + (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE), cancellable, error); if (object_path == NULL) return FALSE; @@ -668,7 +671,7 @@ mct_manager_set_app_filter (MctManager *self, "AppFilter", g_steal_pointer (&app_filter_variant)), G_VARIANT_TYPE ("()"), - (flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE) + (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ @@ -676,7 +679,7 @@ mct_manager_set_app_filter (MctManager *self, &local_error); if (local_error != NULL) { - g_propagate_error (error, bus_error_to_app_filter_error (local_error, user_id)); + g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } @@ -691,7 +694,7 @@ mct_manager_set_app_filter (MctManager *self, "OarsFilter", g_steal_pointer (&oars_filter_variant)), G_VARIANT_TYPE ("()"), - (flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE) + (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ @@ -699,7 +702,7 @@ mct_manager_set_app_filter (MctManager *self, &local_error); if (local_error != NULL) { - g_propagate_error (error, bus_error_to_app_filter_error (local_error, user_id)); + g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } @@ -714,7 +717,7 @@ mct_manager_set_app_filter (MctManager *self, "AllowUserInstallation", g_steal_pointer (&allow_user_installation_variant)), G_VARIANT_TYPE ("()"), - (flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE) + (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ @@ -722,7 +725,7 @@ mct_manager_set_app_filter (MctManager *self, &local_error); if (local_error != NULL) { - g_propagate_error (error, bus_error_to_app_filter_error (local_error, user_id)); + g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } @@ -737,7 +740,7 @@ mct_manager_set_app_filter (MctManager *self, "AllowSystemInstallation", g_steal_pointer (&allow_system_installation_variant)), G_VARIANT_TYPE ("()"), - (flags & MCT_SET_APP_FILTER_FLAGS_INTERACTIVE) + (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE, -1, /* timeout, ms */ @@ -745,7 +748,7 @@ mct_manager_set_app_filter (MctManager *self, &local_error); if (local_error != NULL) { - g_propagate_error (error, bus_error_to_app_filter_error (local_error, user_id)); + g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); return FALSE; } @@ -761,7 +764,7 @@ typedef struct { uid_t user_id; MctAppFilter *app_filter; /* (owned) */ - MctSetAppFilterFlags flags; + MctManagerSetValueFlags flags; } SetAppFilterData; static void @@ -786,7 +789,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (SetAppFilterData, set_app_filter_data_free) * 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. * - * On failure, an #MctAppFilterError, a #GDBusError or a #GIOError will be + * On failure, an #MctManagerError, a #GDBusError or a #GIOError will be * returned. The user’s app filter settings will be left in an undefined state. * * Since: 0.3.0 @@ -795,7 +798,7 @@ void mct_manager_set_app_filter_async (MctManager *self, uid_t user_id, MctAppFilter *app_filter, - MctSetAppFilterFlags flags, + MctManagerSetValueFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) @@ -864,4 +867,469 @@ mct_manager_set_app_filter_finish (MctManager *self, g_return_val_if_fail (error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean (G_TASK (result), error); -} \ No newline at end of file +} + +/** + * mct_manager_get_session_limits: + * @self: a #MctManager + * @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_manager_get_session_limits_async(). + * + * Returns: (transfer full): session limits for the queried user + * Since: 0.5.0 + */ +MctSessionLimits * +mct_manager_get_session_limits (MctManager *self, + uid_t user_id, + MctManagerGetValueFlags 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(MctSessionLimits) session_limits = NULL; + guint32 limit_type; + guint32 daily_start_time, daily_end_time; + + g_return_val_if_fail (MCT_IS_MANAGER (self), NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + object_path = accounts_find_user_by_id (self->connection, user_id, + (flags & MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE), + cancellable, error); + if (object_path == NULL) + return NULL; + + result_variant = + g_dbus_connection_call_sync (self->connection, + "org.freedesktop.Accounts", + object_path, + "org.freedesktop.DBus.Properties", + "GetAll", + g_variant_new ("(s)", "com.endlessm.ParentalControls.SessionLimits"), + G_VARIANT_TYPE ("(a{sv})"), + (flags & MCT_MANAGER_GET_VALUE_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) manager_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 doesn’t have the com.endlessm.ParentalControls.SessionLimits + * extension interface installed. */ + manager_error = g_error_new_literal (MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_DISABLED, + _("Session limits are globally disabled")); + } + else + { + manager_error = bus_error_to_manager_error (local_error, user_id); + } + + g_propagate_error (error, g_steal_pointer (&manager_error)); + return NULL; + } + + /* 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, "LimitType", "u", + &limit_type)) + { + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_PERMISSION_DENIED, + _("Not allowed to query session limits data for user %u"), + (guint) user_id); + return NULL; + } + + /* Check that the limit type is something we support. */ + G_STATIC_ASSERT (sizeof (limit_type) >= sizeof (MctSessionLimitsType)); + + if ((guint) limit_type > MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE) + { + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_INVALID_DATA, + _("Session limit for user %u has an unrecognized type ‘%u’"), + (guint) user_id, limit_type); + return NULL; + } + + if (!g_variant_lookup (properties, "DailySchedule", "(uu)", + &daily_start_time, &daily_end_time)) + { + /* Default value. */ + daily_start_time = 0; + daily_end_time = 24 * 60 * 60; + } + + if (daily_start_time >= daily_end_time || + daily_end_time > 24 * 60 * 60) + { + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_INVALID_DATA, + _("Session limit for user %u has invalid daily schedule %u–%u"), + (guint) user_id, daily_start_time, daily_end_time); + return NULL; + } + + /* Success. Create an #MctSessionLimits object to contain the results. */ + session_limits = g_new0 (MctSessionLimits, 1); + session_limits->ref_count = 1; + session_limits->user_id = user_id; + session_limits->limit_type = limit_type; + session_limits->daily_start_time = daily_start_time; + session_limits->daily_end_time = daily_end_time; + + return g_steal_pointer (&session_limits); +} + +static void get_session_limits_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +typedef struct +{ + uid_t user_id; + MctManagerGetValueFlags flags; +} GetSessionLimitsData; + +static void +get_session_limits_data_free (GetSessionLimitsData *data) +{ + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetSessionLimitsData, get_session_limits_data_free) + +/** + * mct_manager_get_session_limits_async: + * @self: a #MctManager + * @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 session limit settings for the given + * @user_id. + * + * On failure, an #MctManagerError, a #GDBusError or a #GIOError will be + * returned via mct_manager_get_session_limits_finish(). + * + * Since: 0.5.0 + */ +void +mct_manager_get_session_limits_async (MctManager *self, + uid_t user_id, + MctManagerGetValueFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GetSessionLimitsData) data = NULL; + + g_return_if_fail (MCT_IS_MANAGER (self)); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, mct_manager_get_session_limits_async); + + data = g_new0 (GetSessionLimitsData, 1); + data->user_id = user_id; + data->flags = flags; + g_task_set_task_data (task, g_steal_pointer (&data), + (GDestroyNotify) get_session_limits_data_free); + + g_task_run_in_thread (task, get_session_limits_thread_cb); +} + +static void +get_session_limits_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr(MctSessionLimits) limits = NULL; + MctManager *manager = MCT_MANAGER (source_object); + GetSessionLimitsData *data = task_data; + g_autoptr(GError) local_error = NULL; + + limits = mct_manager_get_session_limits (manager, 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 (&limits), + (GDestroyNotify) mct_session_limits_unref); +} + +/** + * mct_manager_get_session_limits_finish: + * @self: a #MctManager + * @result: a #GAsyncResult + * @error: return location for a #GError, or %NULL + * + * Finish an asynchronous operation to get the session limits for a user, + * started with mct_manager_get_session_limits_async(). + * + * Returns: (transfer full): session limits for the queried user + * Since: 0.5.0 + */ +MctSessionLimits * +mct_manager_get_session_limits_finish (MctManager *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (MCT_IS_MANAGER (self), NULL); + g_return_val_if_fail (g_task_is_valid (result, self), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +/** + * mct_manager_set_session_limits: + * @self: a #MctManager + * @user_id: ID of the user to set the limits for, typically coming from getuid() + * @session_limits: (transfer none): the session limits 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_manager_set_session_limits_async(). + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 0.5.0 + */ +gboolean +mct_manager_set_session_limits (MctManager *self, + uid_t user_id, + MctSessionLimits *session_limits, + MctManagerSetValueFlags flags, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *object_path = NULL; + g_autoptr(GVariant) limit_variant = NULL; + const gchar *limit_property_name = NULL; + g_autoptr(GVariant) limit_type_variant = NULL; + g_autoptr(GVariant) limit_result_variant = NULL; + g_autoptr(GVariant) limit_type_result_variant = NULL; + g_autoptr(GError) local_error = NULL; + + g_return_val_if_fail (MCT_IS_MANAGER (self), FALSE); + g_return_val_if_fail (session_limits != NULL, FALSE); + g_return_val_if_fail (session_limits->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); + + object_path = accounts_find_user_by_id (self->connection, user_id, + (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE), + cancellable, error); + if (object_path == NULL) + return FALSE; + + switch (session_limits->limit_type) + { + case MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE: + limit_variant = g_variant_new ("(uu)", + session_limits->daily_start_time, + session_limits->daily_end_time); + limit_property_name = "DailySchedule"; + break; + case MCT_SESSION_LIMITS_TYPE_NONE: + limit_variant = NULL; + limit_property_name = NULL; + break; + default: + g_assert_not_reached (); + } + + limit_type_variant = g_variant_new_uint32 (session_limits->limit_type); + + if (limit_property_name != NULL) + { + /* Change the details of the new limit first, so that all the properties are + * correct by the time the limit type is changed over. */ + limit_result_variant = + g_dbus_connection_call_sync (self->connection, + "org.freedesktop.Accounts", + object_path, + "org.freedesktop.DBus.Properties", + "Set", + g_variant_new ("(ssv)", + "com.endlessm.ParentalControls.SessionLimits", + limit_property_name, + g_steal_pointer (&limit_variant)), + G_VARIANT_TYPE ("()"), + (flags & MCT_MANAGER_SET_VALUE_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_manager_error (local_error, user_id)); + return FALSE; + } + } + + limit_type_result_variant = + g_dbus_connection_call_sync (self->connection, + "org.freedesktop.Accounts", + object_path, + "org.freedesktop.DBus.Properties", + "Set", + g_variant_new ("(ssv)", + "com.endlessm.ParentalControls.SessionLimits", + "LimitType", + g_steal_pointer (&limit_type_variant)), + G_VARIANT_TYPE ("()"), + (flags & MCT_MANAGER_SET_VALUE_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_manager_error (local_error, user_id)); + return FALSE; + } + + return TRUE; +} + +static void set_session_limits_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +typedef struct +{ + uid_t user_id; + MctSessionLimits *session_limits; /* (owned) */ + MctManagerSetValueFlags flags; +} SetSessionLimitsData; + +static void +set_session_limits_data_free (SetSessionLimitsData *data) +{ + mct_session_limits_unref (data->session_limits); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SetSessionLimitsData, set_session_limits_data_free) + +/** + * mct_manager_set_session_limits_async: + * @self: a #MctManager + * @user_id: ID of the user to set the limits for, typically coming from getuid() + * @session_limits: (transfer none): the session limits 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 session limits settings for the given @user_id to the + * given @session_limits instance. + * + * On failure, an #MctManagerError, a #GDBusError or a #GIOError will be + * returned via mct_manager_set_session_limits_finish(). The user’s session + * limits settings will be left in an undefined state. + * + * Since: 0.5.0 + */ +void +mct_manager_set_session_limits_async (MctManager *self, + uid_t user_id, + MctSessionLimits *session_limits, + MctManagerSetValueFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(SetSessionLimitsData) data = NULL; + + g_return_if_fail (MCT_IS_MANAGER (self)); + g_return_if_fail (session_limits != NULL); + g_return_if_fail (session_limits->ref_count >= 1); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, mct_manager_set_session_limits_async); + + data = g_new0 (SetSessionLimitsData, 1); + data->user_id = user_id; + data->session_limits = mct_session_limits_ref (session_limits); + data->flags = flags; + g_task_set_task_data (task, g_steal_pointer (&data), + (GDestroyNotify) set_session_limits_data_free); + + g_task_run_in_thread (task, set_session_limits_thread_cb); +} + +static void +set_session_limits_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + gboolean success; + MctManager *manager = MCT_MANAGER (source_object); + SetSessionLimitsData *data = task_data; + g_autoptr(GError) local_error = NULL; + + success = mct_manager_set_session_limits (manager, data->user_id, + data->session_limits, 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_manager_set_session_limits_finish: + * @self: a #MctManager + * @result: a #GAsyncResult + * @error: return location for a #GError, or %NULL + * + * Finish an asynchronous operation to set the session limits for a user, + * started with mct_manager_set_session_limits_async(). + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 0.5.0 + */ +gboolean +mct_manager_set_session_limits_finish (MctManager *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (MCT_IS_MANAGER (self), FALSE); + g_return_val_if_fail (g_task_is_valid (result, self), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/libmalcontent/manager.h b/libmalcontent/manager.h index 841ba8a..f1bf1ce 100644 --- a/libmalcontent/manager.h +++ b/libmalcontent/manager.h @@ -25,43 +25,79 @@ #include #include #include -#include G_BEGIN_DECLS /** - * MctGetAppFilterFlags: - * @MCT_GET_APP_FILTER_FLAGS_NONE: No flags set. - * @MCT_GET_APP_FILTER_FLAGS_INTERACTIVE: Allow interactive polkit dialogs when - * requesting authorization. + * MctManagerGetValueFlags: + * @MCT_MANAGER_GET_VALUE_FLAGS_NONE: No flags set. + * @MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE: Allow interactive polkit dialogs + * when requesting authorization. * - * Flags to control the behaviour of mct_manager_get_app_filter() and - * mct_manager_get_app_filter_async(). + * Flags to control the behaviour of getter functions like + * mct_manager_get_app_filter() and mct_manager_get_app_filter_async(). * - * Since: 0.3.0 + * Since: 0.5.0 */ typedef enum { - MCT_GET_APP_FILTER_FLAGS_NONE = 0, - MCT_GET_APP_FILTER_FLAGS_INTERACTIVE, -} MctGetAppFilterFlags; + MCT_MANAGER_GET_VALUE_FLAGS_NONE = 0, + MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE = (1 << 0), +} MctManagerGetValueFlags; + +/* FIXME: Eventually deprecate these compatibility fallbacks. */ +typedef MctManagerGetValueFlags MctGetAppFilterFlags; +#define MCT_GET_APP_FILTER_FLAGS_NONE MCT_MANAGER_GET_VALUE_FLAGS_NONE +#define MCT_GET_APP_FILTER_FLAGS_INTERACTIVE MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE /** - * MctSetAppFilterFlags: - * @MCT_SET_APP_FILTER_FLAGS_NONE: No flags set. - * @MCT_SET_APP_FILTER_FLAGS_INTERACTIVE: Allow interactive polkit dialogs when - * requesting authorization. + * MctManagerSetValueFlags: + * @MCT_MANAGER_SET_VALUE_FLAGS_NONE: No flags set. + * @MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE: Allow interactive polkit dialogs + * when requesting authorization. * - * Flags to control the behaviour of mct_manager_set_app_filter() and - * mct_manager_set_app_filter_async(). + * Flags to control the behaviour of setter functions like + * mct_manager_set_app_filter() and mct_manager_set_app_filter_async(). * - * Since: 0.3.0 + * Since: 0.5.0 */ typedef enum { - MCT_SET_APP_FILTER_FLAGS_NONE = 0, - MCT_SET_APP_FILTER_FLAGS_INTERACTIVE, -} MctSetAppFilterFlags; + MCT_MANAGER_SET_VALUE_FLAGS_NONE = 0, + MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE = (1 << 0), +} MctManagerSetValueFlags; + +/* FIXME: Eventually deprecate these compatibility fallbacks. */ +typedef MctManagerSetValueFlags MctSetAppFilterFlags; +#define MCT_SET_APP_FILTER_FLAGS_NONE MCT_MANAGER_SET_VALUE_FLAGS_NONE +#define MCT_SET_APP_FILTER_FLAGS_INTERACTIVE MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE + +/** + * MctManagerError: + * @MCT_MANAGER_ERROR_INVALID_USER: Given user ID doesn’t exist + * @MCT_MANAGER_ERROR_PERMISSION_DENIED: Not authorized to query properties of + * the given user + * @MCT_MANAGER_ERROR_INVALID_DATA: The data stored in a property of the given + * user is inconsistent or invalid + * @MCT_MANAGER_ERROR_DISABLED: Parental controls are disabled for all users + * + * Errors relating to get/set operations on an #MctManager instance. + * + * Since: 0.5.0 + */ +typedef enum +{ + MCT_MANAGER_ERROR_INVALID_USER, + MCT_MANAGER_ERROR_PERMISSION_DENIED, + MCT_MANAGER_ERROR_INVALID_DATA, + MCT_MANAGER_ERROR_DISABLED, +} MctManagerError; + +GQuark mct_manager_error_quark (void); +#define MCT_MANAGER_ERROR mct_manager_error_quark () + +#include +#include #define MCT_TYPE_MANAGER mct_manager_get_type () G_DECLARE_FINAL_TYPE (MctManager, mct_manager, MCT, MANAGER, GObject) @@ -70,12 +106,12 @@ MctManager *mct_manager_new (GDBusConnection *connection); MctAppFilter *mct_manager_get_app_filter (MctManager *self, uid_t user_id, - MctGetAppFilterFlags flags, + MctManagerGetValueFlags flags, GCancellable *cancellable, GError **error); void mct_manager_get_app_filter_async (MctManager *self, uid_t user_id, - MctGetAppFilterFlags flags, + MctManagerGetValueFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data); @@ -86,13 +122,13 @@ MctAppFilter *mct_manager_get_app_filter_finish (MctManager *self, gboolean mct_manager_set_app_filter (MctManager *self, uid_t user_id, MctAppFilter *app_filter, - MctSetAppFilterFlags flags, + MctManagerSetValueFlags flags, GCancellable *cancellable, GError **error); void mct_manager_set_app_filter_async (MctManager *self, uid_t user_id, MctAppFilter *app_filter, - MctSetAppFilterFlags flags, + MctManagerSetValueFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data); @@ -100,4 +136,36 @@ gboolean mct_manager_set_app_filter_finish (MctManager *self, GAsyncResult *result, GError **error); +MctSessionLimits *mct_manager_get_session_limits (MctManager *self, + uid_t user_id, + MctManagerGetValueFlags flags, + GCancellable *cancellable, + GError **error); +void mct_manager_get_session_limits_async (MctManager *self, + uid_t user_id, + MctManagerGetValueFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +MctSessionLimits *mct_manager_get_session_limits_finish (MctManager *self, + GAsyncResult *result, + GError **error); + +gboolean mct_manager_set_session_limits (MctManager *self, + uid_t user_id, + MctSessionLimits *session_limits, + MctManagerSetValueFlags flags, + GCancellable *cancellable, + GError **error); +void mct_manager_set_session_limits_async (MctManager *self, + uid_t user_id, + MctSessionLimits *session_limits, + MctManagerSetValueFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean mct_manager_set_session_limits_finish (MctManager *self, + GAsyncResult *result, + GError **error); + G_END_DECLS diff --git a/libmalcontent/meson.build b/libmalcontent/meson.build index b06b086..e592f11 100644 --- a/libmalcontent/meson.build +++ b/libmalcontent/meson.build @@ -3,14 +3,17 @@ libmalcontent_api_name = 'malcontent-' + libmalcontent_api_version libmalcontent_sources = [ 'app-filter.c', 'manager.c', + 'session-limits.c', ] libmalcontent_headers = [ 'app-filter.h', 'malcontent.h', 'manager.h', + 'session-limits.h', ] libmalcontent_private_headers = [ 'app-filter-private.h', + 'session-limits-private.h', ] libmalcontent_public_deps = [ diff --git a/libmalcontent/session-limits-private.h b/libmalcontent/session-limits-private.h new file mode 100644 index 0000000..88f6c84 --- /dev/null +++ b/libmalcontent/session-limits-private.h @@ -0,0 +1,62 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2019 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 +#include + +G_BEGIN_DECLS + +/** + * MctSessionLimitsType: + * @MCT_SESSION_LIMITS_TYPE_NONE: No session limits are imposed. + * @MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE: Sessions are limited to between a + * pair of given times each day. + * + * Types of session limit which can be imposed on an account. Additional types + * may be added in future. + * + * Since: 0.5.0 + */ +typedef enum +{ + /* these values are used in the com.endlessm.ParentalControls.SessionLimits + * D-Bus interface, so must not be changed */ + MCT_SESSION_LIMITS_TYPE_NONE = 0, + MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE = 1, +} MctSessionLimitsType; + +struct _MctSessionLimits +{ + gint ref_count; + + uid_t user_id; + + MctSessionLimitsType limit_type; + guint daily_start_time; /* seconds since midnight */ + guint daily_end_time; /* seconds since midnight */ +}; + +G_END_DECLS diff --git a/libmalcontent/session-limits.c b/libmalcontent/session-limits.c new file mode 100644 index 0000000..be4373f --- /dev/null +++ b/libmalcontent/session-limits.c @@ -0,0 +1,451 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2019 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 + +#include "libmalcontent/session-limits-private.h" + + +/* struct _MctSessionLimits is defined in session-limits-private.h */ + +G_DEFINE_BOXED_TYPE (MctSessionLimits, mct_session_limits, + mct_session_limits_ref, mct_session_limits_unref) + +/** + * mct_session_limits_ref: + * @limits: (transfer none): an #MctSessionLimits + * + * Increment the reference count of @limits, and return the same pointer to it. + * + * Returns: (transfer full): the same pointer as @limits + * Since: 0.5.0 + */ +MctSessionLimits * +mct_session_limits_ref (MctSessionLimits *limits) +{ + g_return_val_if_fail (limits != NULL, NULL); + g_return_val_if_fail (limits->ref_count >= 1, NULL); + g_return_val_if_fail (limits->ref_count <= G_MAXINT - 1, NULL); + + limits->ref_count++; + return limits; +} + +/** + * mct_session_limits_unref: + * @limits: (transfer full): an #MctSessionLimits + * + * Decrement the reference count of @limits. If the reference count reaches + * zero, free the @limits and all its resources. + * + * Since: 0.5.0 + */ +void +mct_session_limits_unref (MctSessionLimits *limits) +{ + g_return_if_fail (limits != NULL); + g_return_if_fail (limits->ref_count >= 1); + + limits->ref_count--; + + if (limits->ref_count <= 0) + { + g_free (limits); + } +} + +/** + * mct_session_limits_get_user_id: + * @limits: an #MctSessionLimits + * + * Get the user ID of the user this #MctSessionLimits is for. + * + * Returns: user ID of the relevant user + * Since: 0.5.0 + */ +uid_t +mct_session_limits_get_user_id (MctSessionLimits *limits) +{ + g_return_val_if_fail (limits != NULL, (uid_t) -1); + g_return_val_if_fail (limits->ref_count >= 1, (uid_t) -1); + + return limits->user_id; +} + +/** + * mct_session_limits_check_time_remaining: + * @limits: an #MctSessionLimits + * @now_usecs: current time as microseconds since the Unix epoch (UTC), + * typically queried using g_get_real_time() + * @time_remaining_secs_out: (out) (optional): return location for the number + * of seconds remaining before the user’s session has to end, if limits are + * in force + * @time_limit_enabled_out: (out) (optional): return location for whether time + * limits are enabled for this user + * + * Check whether the user has time remaining in which they are allowed to use + * the computer, assuming that @now_usecs is the current time, and applying the + * session limit policy from @limits to it. + * + * This will return whether the user is allowed to use the computer now; further + * information about the policy and remaining time is provided in + * @time_remaining_secs_out and @time_limit_enabled_out. + * + * Returns: %TRUE if the user this @limits corresponds to is allowed to be in + * an active session at the given time; %FALSE otherwise + * Since: 0.5.0 + */ +gboolean +mct_session_limits_check_time_remaining (MctSessionLimits *limits, + guint64 now_usecs, + guint64 *time_remaining_secs_out, + gboolean *time_limit_enabled_out) +{ + guint64 time_remaining_secs; + gboolean time_limit_enabled; + gboolean user_allowed_now; + g_autoptr(GDateTime) now_dt = NULL; + guint64 now_time_of_day_secs; + + g_return_val_if_fail (limits != NULL, FALSE); + g_return_val_if_fail (limits->ref_count >= 1, FALSE); + + /* Helper calculations. */ + now_dt = g_date_time_new_from_unix_utc (now_usecs / G_USEC_PER_SEC); + if (now_dt == NULL) + { + time_remaining_secs = 0; + time_limit_enabled = TRUE; + user_allowed_now = FALSE; + goto out; + } + + now_time_of_day_secs = ((g_date_time_get_hour (now_dt) * 60 + + g_date_time_get_minute (now_dt)) * 60 + + g_date_time_get_second (now_dt)); + + /* Work out the limits. */ + switch (limits->limit_type) + { + case MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE: + user_allowed_now = (now_time_of_day_secs >= limits->daily_start_time && + now_time_of_day_secs < limits->daily_end_time); + time_remaining_secs = user_allowed_now ? (limits->daily_end_time - now_time_of_day_secs) : 0; + time_limit_enabled = TRUE; + + g_debug ("%s: Daily schedule limit allowed in %u–%u (now is %" + G_GUINT64_FORMAT "); %" G_GUINT64_FORMAT " seconds remaining", + G_STRFUNC, limits->daily_start_time, limits->daily_end_time, + now_time_of_day_secs, time_remaining_secs); + + break; + case MCT_SESSION_LIMITS_TYPE_NONE: + default: + user_allowed_now = TRUE; + time_remaining_secs = G_MAXUINT64; + time_limit_enabled = FALSE; + + g_debug ("%s: No limit enabled", G_STRFUNC); + + break; + } + +out: + /* Postconditions. */ + g_assert (!user_allowed_now || time_remaining_secs > 0); + g_assert (user_allowed_now || time_remaining_secs == 0); + g_assert (time_limit_enabled || time_remaining_secs == G_MAXUINT64); + + /* Output. */ + if (time_remaining_secs_out != NULL) + *time_remaining_secs_out = time_remaining_secs; + if (time_limit_enabled_out != NULL) + *time_limit_enabled_out = time_limit_enabled; + + return user_allowed_now; +} + +/* + * Actual implementation of #MctSessionLimitsBuilder. + * + * All members are %NULL if un-initialised, cleared, or ended. + */ +typedef struct +{ + MctSessionLimitsType limit_type; + + /* Which member is used is determined by @limit_type: */ + union + { + struct + { + guint start_time; /* seconds since midnight */ + guint end_time; /* seconds since midnight */ + } daily_schedule; + }; + + /*< private >*/ + gpointer padding[10]; +} MctSessionLimitsBuilderReal; + +G_STATIC_ASSERT (sizeof (MctSessionLimitsBuilderReal) == + sizeof (MctSessionLimitsBuilder)); +G_STATIC_ASSERT (__alignof__ (MctSessionLimitsBuilderReal) == + __alignof__ (MctSessionLimitsBuilder)); + +G_DEFINE_BOXED_TYPE (MctSessionLimitsBuilder, mct_session_limits_builder, + mct_session_limits_builder_copy, mct_session_limits_builder_free) + +/** + * mct_session_limits_builder_init: + * @builder: an uninitialised #MctSessionLimitsBuilder + * + * Initialise the given @builder so it can be used to construct a new + * #MctSessionLimits. @builder must have been allocated on the stack, and must + * not already be initialised. + * + * Construct the #MctSessionLimits by calling methods on @builder, followed by + * mct_session_limits_builder_end(). To abort construction, use + * mct_session_limits_builder_clear(). + * + * Since: 0.5.0 + */ +void +mct_session_limits_builder_init (MctSessionLimitsBuilder *builder) +{ + MctSessionLimitsBuilder local_builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder; + + g_return_if_fail (_builder != NULL); + g_return_if_fail (_builder->limit_type == MCT_SESSION_LIMITS_TYPE_NONE); + + memcpy (builder, &local_builder, sizeof (local_builder)); +} + +/** + * mct_session_limits_builder_clear: + * @builder: an #MctSessionLimitsBuilder + * + * Clear @builder, freeing any internal state in it. This will not free the + * top-level storage for @builder itself, which is assumed to be allocated on + * the stack. + * + * If called on an already-cleared #MctSessionLimitsBuilder, this function is + * idempotent. + * + * Since: 0.5.0 + */ +void +mct_session_limits_builder_clear (MctSessionLimitsBuilder *builder) +{ + MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder; + + g_return_if_fail (_builder != NULL); + + /* Nothing to free here for now. */ + _builder->limit_type = MCT_SESSION_LIMITS_TYPE_NONE; +} + +/** + * mct_session_limits_builder_new: + * + * Construct a new #MctSessionLimitsBuilder on the heap. This is intended for + * language bindings. The returned builder must eventually be freed with + * mct_session_limits_builder_free(), but can be cleared zero or more times with + * mct_session_limits_builder_clear() first. + * + * Returns: (transfer full): a new heap-allocated #MctSessionLimitsBuilder + * Since: 0.5.0 + */ +MctSessionLimitsBuilder * +mct_session_limits_builder_new (void) +{ + g_autoptr(MctSessionLimitsBuilder) builder = NULL; + + builder = g_new0 (MctSessionLimitsBuilder, 1); + mct_session_limits_builder_init (builder); + + return g_steal_pointer (&builder); +} + +/** + * mct_session_limits_builder_copy: + * @builder: an #MctSessionLimitsBuilder + * + * Copy the given @builder to a newly-allocated #MctSessionLimitsBuilder on the + * heap. This is safe to use with cleared, stack-allocated + * #MctSessionLimitsBuilders. + * + * Returns: (transfer full): a copy of @builder + * Since: 0.5.0 + */ +MctSessionLimitsBuilder * +mct_session_limits_builder_copy (MctSessionLimitsBuilder *builder) +{ + MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder; + g_autoptr(MctSessionLimitsBuilder) copy = NULL; + MctSessionLimitsBuilderReal *_copy; + + g_return_val_if_fail (builder != NULL, NULL); + + copy = mct_session_limits_builder_new (); + _copy = (MctSessionLimitsBuilderReal *) copy; + + mct_session_limits_builder_clear (copy); + _copy->limit_type = _builder->limit_type; + + switch (_builder->limit_type) + { + case MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE: + _copy->daily_schedule.start_time = _builder->daily_schedule.start_time; + _copy->daily_schedule.end_time = _builder->daily_schedule.end_time; + break; + case MCT_SESSION_LIMITS_TYPE_NONE: + default: + break; + } + + return g_steal_pointer (©); +} + +/** + * mct_session_limits_builder_free: + * @builder: a heap-allocated #MctSessionLimitsBuilder + * + * Free an #MctSessionLimitsBuilder originally allocated using + * mct_session_limits_builder_new(). This must not be called on stack-allocated + * builders initialised using mct_session_limits_builder_init(). + * + * Since: 0.5.0 + */ +void +mct_session_limits_builder_free (MctSessionLimitsBuilder *builder) +{ + g_return_if_fail (builder != NULL); + + mct_session_limits_builder_clear (builder); + g_free (builder); +} + +/** + * mct_session_limits_builder_end: + * @builder: an initialised #MctSessionLimitsBuilder + * + * Finish constructing an #MctSessionLimits with the given @builder, and return + * it. The #MctSessionLimitsBuilder will be cleared as if + * mct_session_limits_builder_clear() had been called. + * + * Returns: (transfer full): a newly constructed #MctSessionLimits + * Since: 0.5.0 + */ +MctSessionLimits * +mct_session_limits_builder_end (MctSessionLimitsBuilder *builder) +{ + MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder; + g_autoptr(MctSessionLimits) session_limits = NULL; + + g_return_val_if_fail (_builder != NULL, NULL); + + /* Build the #MctSessionLimits. */ + session_limits = g_new0 (MctSessionLimits, 1); + session_limits->ref_count = 1; + session_limits->user_id = -1; + session_limits->limit_type = _builder->limit_type; + + switch (_builder->limit_type) + { + case MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE: + session_limits->daily_start_time = _builder->daily_schedule.start_time; + session_limits->daily_end_time = _builder->daily_schedule.end_time; + break; + case MCT_SESSION_LIMITS_TYPE_NONE: + default: + /* Defaults: */ + session_limits->daily_start_time = 0; + session_limits->daily_end_time = 24 * 60 * 60; + break; + } + + mct_session_limits_builder_clear (builder); + + return g_steal_pointer (&session_limits); +} + +/** + * mct_session_limits_builder_set_none: + * @builder: an initialised #MctSessionLimitsBuilder + * + * Unset any session limits currently set in the @builder. + * + * Since: 0.5.0 + */ +void +mct_session_limits_builder_set_none (MctSessionLimitsBuilder *builder) +{ + MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder; + + g_return_if_fail (_builder != NULL); + + /* This will need to free other limit types’ data first in future. */ + _builder->limit_type = MCT_SESSION_LIMITS_TYPE_NONE; +} + +/** + * mct_session_limits_builder_set_daily_schedule: + * @builder: an initialised #MctSessionLimitsBuilder + * @start_time_secs: number of seconds since midnight when the user’s session + * can first start + * @end_time_secs: number of seconds since midnight when the user’s session can + * last end + * + * Set the session limits in @builder to be a daily schedule, where sessions are + * allowed between @start_time_secs and @end_time_secs every day. + * @start_time_secs and @end_time_secs are given as offsets from the start of + * the day, in seconds. @end_time_secs must be greater than @start_time_secs. + * @end_time_secs must be at most `24 * 60 * 60`. + * + * This will overwrite any other session limits. + * + * Since: 0.5.0 + */ +void +mct_session_limits_builder_set_daily_schedule (MctSessionLimitsBuilder *builder, + guint start_time_secs, + guint end_time_secs) +{ + MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder; + + g_return_if_fail (_builder != NULL); + g_return_if_fail (start_time_secs < end_time_secs); + g_return_if_fail (end_time_secs <= 24 * 60 * 60); + + /* This will need to free other limit types’ data first in future. */ + _builder->limit_type = MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE; + _builder->daily_schedule.start_time = start_time_secs; + _builder->daily_schedule.end_time = end_time_secs; +} diff --git a/libmalcontent/session-limits.h b/libmalcontent/session-limits.h new file mode 100644 index 0000000..1ac356c --- /dev/null +++ b/libmalcontent/session-limits.h @@ -0,0 +1,122 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2019 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 + +/** + * MctSessionLimits: + * + * #MctSessionLimits is an opaque, immutable structure which contains a snapshot + * of the session limits settings for a user at a given time. This includes + * whether session limits are being enforced, and the limit policy — for + * example, the times of day when a user is allowed to use the computer. + * + * Typically, session limits 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.5.0 + */ +typedef struct _MctSessionLimits MctSessionLimits; +GType mct_session_limits_get_type (void); + +MctSessionLimits *mct_session_limits_ref (MctSessionLimits *limits); +void mct_session_limits_unref (MctSessionLimits *limits); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MctSessionLimits, mct_session_limits_unref) + +uid_t mct_session_limits_get_user_id (MctSessionLimits *limits); +gboolean mct_session_limits_check_time_remaining (MctSessionLimits *limits, + guint64 now_usecs, + guint64 *time_remaining_secs_out, + gboolean *time_limit_enabled_out); + +/** + * MctSessionLimitsBuilder: + * + * #MctSessionLimitsBuilder is a stack-allocated mutable structure used to build + * an #MctSessionLimits instance. Use mct_session_limits_builder_init(), various + * method calls to set properties of the session limits, and then + * mct_session_limits_builder_end(), to construct an #MctSessionLimits. + * + * Since: 0.5.0 + */ +typedef struct +{ + /*< private >*/ + guint u0; + guint u1; + guint u2; + gpointer p0[10]; +} MctSessionLimitsBuilder; + +GType mct_session_limits_builder_get_type (void); + +/** + * MCT_SESSION_LIMITS_BUILDER_INIT: + * + * Initialise a stack-allocated #MctSessionLimitsBuilder instance at declaration + * time. + * + * This is typically used with g_auto(): + * |[ + * g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + * ]| + * + * Since: 0.5.0 + */ +#define MCT_SESSION_LIMITS_BUILDER_INIT() \ + { \ + 0, /* MCT_SESSION_LIMITS_TYPE_NONE */ \ + 0, \ + 0, \ + /* padding: */ \ + { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } \ + } + +void mct_session_limits_builder_init (MctSessionLimitsBuilder *builder); +void mct_session_limits_builder_clear (MctSessionLimitsBuilder *builder); + +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (MctSessionLimitsBuilder, + mct_session_limits_builder_clear) + +MctSessionLimitsBuilder *mct_session_limits_builder_new (void); +MctSessionLimitsBuilder *mct_session_limits_builder_copy (MctSessionLimitsBuilder *builder); +void mct_session_limits_builder_free (MctSessionLimitsBuilder *builder); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MctSessionLimitsBuilder, mct_session_limits_builder_free) + +MctSessionLimits *mct_session_limits_builder_end (MctSessionLimitsBuilder *builder); + +void mct_session_limits_builder_set_none (MctSessionLimitsBuilder *builder); + +void mct_session_limits_builder_set_daily_schedule (MctSessionLimitsBuilder *builder, + guint start_time_secs, + guint end_time_secs); + +G_END_DECLS diff --git a/libmalcontent/tests/app-filter.c b/libmalcontent/tests/app-filter.c index 5b7d284..7ce4abc 100644 --- a/libmalcontent/tests/app-filter.c +++ b/libmalcontent/tests/app-filter.c @@ -594,7 +594,7 @@ test_app_filter_bus_get (BusFixture *fixture, mct_manager_get_app_filter_async (fixture->manager, fixture->valid_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, async_result_cb, &result); while (result == NULL) @@ -605,7 +605,7 @@ test_app_filter_bus_get (BusFixture *fixture, { app_filter = mct_manager_get_app_filter (fixture->manager, fixture->valid_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, &local_error); } @@ -650,7 +650,7 @@ test_app_filter_bus_get_whitelist (BusFixture *fixture, app_filter = mct_manager_get_app_filter (fixture->manager, fixture->valid_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, &local_error); g_assert_no_error (local_error); @@ -705,7 +705,7 @@ test_app_filter_bus_get_all_oars_values (BusFixture *fixture, app_filter = mct_manager_get_app_filter (fixture->manager, fixture->valid_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, &local_error); g_assert_no_error (local_error); @@ -753,7 +753,7 @@ test_app_filter_bus_get_defaults (BusFixture *fixture, app_filter = mct_manager_get_app_filter (fixture->manager, fixture->valid_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, &local_error); g_assert_no_error (local_error); @@ -785,7 +785,7 @@ test_app_filter_bus_get_error_invalid_user (BusFixture *fixture, mct_manager_get_app_filter_async (fixture->manager, fixture->missing_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, async_result_cb, &result); /* Handle the FindUserById() call and claim the user doesn’t exist. */ @@ -809,7 +809,7 @@ test_app_filter_bus_get_error_invalid_user (BusFixture *fixture, &local_error); g_assert_error (local_error, - MCT_APP_FILTER_ERROR, MCT_APP_FILTER_ERROR_INVALID_USER); + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_USER); g_assert_null (app_filter); } @@ -831,7 +831,7 @@ test_app_filter_bus_get_error_permission_denied (BusFixture *fixture, mct_manager_get_app_filter_async (fixture->manager, fixture->valid_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, async_result_cb, &result); /* Handle the FindUserById() call. */ @@ -866,7 +866,7 @@ test_app_filter_bus_get_error_permission_denied (BusFixture *fixture, &local_error); g_assert_error (local_error, - MCT_APP_FILTER_ERROR, MCT_APP_FILTER_ERROR_PERMISSION_DENIED); + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED); g_assert_null (app_filter); } @@ -888,7 +888,7 @@ test_app_filter_bus_get_error_permission_denied_missing (BusFixture *fixture, mct_manager_get_app_filter_async (fixture->manager, fixture->valid_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, async_result_cb, &result); /* Handle the FindUserById() call. */ @@ -924,7 +924,7 @@ test_app_filter_bus_get_error_permission_denied_missing (BusFixture *fixture, &local_error); g_assert_error (local_error, - MCT_APP_FILTER_ERROR, MCT_APP_FILTER_ERROR_PERMISSION_DENIED); + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED); g_assert_null (app_filter); } @@ -943,7 +943,7 @@ test_app_filter_bus_get_error_unknown (BusFixture *fixture, mct_manager_get_app_filter_async (fixture->manager, fixture->valid_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, async_result_cb, &result); /* Handle the FindUserById() call and return a bogus error. */ @@ -991,7 +991,7 @@ test_app_filter_bus_get_error_disabled (BusFixture *fixture, mct_manager_get_app_filter_async (fixture->manager, fixture->valid_uid, - MCT_GET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, async_result_cb, &result); /* Handle the FindUserById() call. */ @@ -1027,7 +1027,7 @@ test_app_filter_bus_get_error_disabled (BusFixture *fixture, &local_error); g_assert_error (local_error, - MCT_APP_FILTER_ERROR, MCT_APP_FILTER_ERROR_DISABLED); + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_DISABLED); g_assert_null (app_filter); } @@ -1184,7 +1184,7 @@ test_app_filter_bus_set (BusFixture *fixture, mct_manager_set_app_filter_async (fixture->manager, fixture->valid_uid, app_filter, - MCT_SET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, async_result_cb, &result); while (result == NULL) @@ -1196,7 +1196,7 @@ test_app_filter_bus_set (BusFixture *fixture, { success = mct_manager_set_app_filter (fixture->manager, fixture->valid_uid, app_filter, - MCT_SET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, &local_error); } @@ -1225,7 +1225,7 @@ test_app_filter_bus_set_error_invalid_user (BusFixture *fixture, mct_manager_set_app_filter_async (fixture->manager, fixture->missing_uid, app_filter, - MCT_SET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, async_result_cb, &result); /* Handle the FindUserById() call and claim the user doesn’t exist. */ @@ -1249,7 +1249,7 @@ test_app_filter_bus_set_error_invalid_user (BusFixture *fixture, &local_error); g_assert_error (local_error, - MCT_APP_FILTER_ERROR, MCT_APP_FILTER_ERROR_INVALID_USER); + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_USER); g_assert_false (success); } @@ -1282,11 +1282,11 @@ test_app_filter_bus_set_error_permission_denied (BusFixture *fixture, success = mct_manager_set_app_filter (fixture->manager, fixture->valid_uid, app_filter, - MCT_SET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, &local_error); g_assert_error (local_error, - MCT_APP_FILTER_ERROR, MCT_APP_FILTER_ERROR_PERMISSION_DENIED); + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED); g_assert_false (success); } @@ -1320,7 +1320,7 @@ test_app_filter_bus_set_error_unknown (BusFixture *fixture, success = mct_manager_set_app_filter (fixture->manager, fixture->valid_uid, app_filter, - MCT_SET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, &local_error); g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_DBUS_ERROR); @@ -1363,7 +1363,7 @@ test_app_filter_bus_set_error_invalid_property (BusFixture *fixture, success = mct_manager_set_app_filter (fixture->manager, fixture->valid_uid, app_filter, - MCT_SET_APP_FILTER_FLAGS_NONE, NULL, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, &local_error); g_assert_error (local_error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS); diff --git a/libmalcontent/tests/meson.build b/libmalcontent/tests/meson.build index a8a815a..610bc35 100644 --- a/libmalcontent/tests/meson.build +++ b/libmalcontent/tests/meson.build @@ -33,9 +33,14 @@ accounts_service_iface_c = custom_target( '@INPUT@'], ) +accounts_service_extension_ifaces = [ + join_paths(meson.source_root(), 'accounts-service', 'com.endlessm.ParentalControls.AppFilter.xml'), + join_paths(meson.source_root(), 'accounts-service', 'com.endlessm.ParentalControls.SessionLimits.xml'), +] + accounts_service_extension_iface_h = custom_target( 'accounts-service-extension-iface.h', - input: ['com.endlessm.ParentalControls.AppFilter.xml'], + input: accounts_service_extension_ifaces, output: ['accounts-service-extension-iface.h'], command: [gdbus_codegen, '--interface-info-header', @@ -44,7 +49,7 @@ accounts_service_extension_iface_h = custom_target( ) accounts_service_extension_iface_c = custom_target( 'accounts-service-extension-iface.c', - input: ['com.endlessm.ParentalControls.AppFilter.xml'], + input: accounts_service_extension_ifaces, output: ['accounts-service-extension-iface.c'], command: [gdbus_codegen, '--interface-info-body', @@ -59,6 +64,12 @@ test_programs = [ accounts_service_extension_iface_h, accounts_service_extension_iface_c, ], deps], + ['session-limits', [ + accounts_service_iface_h, + accounts_service_iface_c, + accounts_service_extension_iface_h, + accounts_service_extension_iface_c, + ], deps], ] installed_tests_metadir = join_paths(datadir, 'installed-tests', diff --git a/libmalcontent/tests/session-limits.c b/libmalcontent/tests/session-limits.c new file mode 100644 index 0000000..2559f3a --- /dev/null +++ b/libmalcontent/tests/session-limits.c @@ -0,0 +1,1197 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2019 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 +#include +#include +#include "accounts-service-iface.h" +#include "accounts-service-extension-iface.h" + + +/* Helper function to convert a constant time in seconds to microseconds, + * avoiding issues with integer constants being too small for the multiplication + * by using explicit typing. */ +static guint64 +usec (guint64 sec) +{ + return sec * G_USEC_PER_SEC; +} + +/* Test that the #GType definitions for various types work. */ +static void +test_session_limits_types (void) +{ + g_type_ensure (mct_session_limits_get_type ()); + g_type_ensure (mct_session_limits_builder_get_type ()); +} + +/* Test that ref() and unref() work on an #MctSessionLimits. */ +static void +test_session_limits_refs (void) +{ + g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + g_autoptr(MctSessionLimits) limits = NULL; + + /* Use an empty #MctSessionLimits. */ + limits = mct_session_limits_builder_end (&builder); + + g_assert_nonnull (limits); + + /* Call check_time_remaining() to check that the limits object hasn’t been + * finalised. */ + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (0), NULL, NULL)); + mct_session_limits_ref (limits); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (0), NULL, NULL)); + mct_session_limits_unref (limits); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (0), NULL, NULL)); + + /* Final ref is dropped by g_autoptr(). */ +} + +/* Check error handling when passing an invalid time for @now_usecs to + * mct_session_limits_check_time_remaining(). */ +static void +test_session_limits_check_time_remaining_invalid_time (void) +{ + g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + g_autoptr(MctSessionLimits) limits = NULL; + guint64 time_remaining_secs; + gboolean time_limit_enabled; + + /* Use an empty #MctSessionLimits. */ + limits = mct_session_limits_builder_end (&builder); + + /* Pass an invalid time to mct_session_limits_check_time_remaining(). */ + g_assert_false (mct_session_limits_check_time_remaining (limits, G_MAXUINT64, &time_remaining_secs, &time_limit_enabled)); + g_assert_cmpuint (time_remaining_secs, ==, 0); + g_assert_true (time_limit_enabled); +} + +/* Fixture for tests which use an #MctSessionLimitsBuilder. The builder can + * either be heap- or stack-allocated. @builder will always be a valid pointer + * to it. + */ +typedef struct +{ + MctSessionLimitsBuilder *builder; + MctSessionLimitsBuilder stack_builder; +} BuilderFixture; + +static void +builder_set_up_stack (BuilderFixture *fixture, + gconstpointer test_data) +{ + mct_session_limits_builder_init (&fixture->stack_builder); + fixture->builder = &fixture->stack_builder; +} + +static void +builder_tear_down_stack (BuilderFixture *fixture, + gconstpointer test_data) +{ + mct_session_limits_builder_clear (&fixture->stack_builder); + fixture->builder = NULL; +} + +static void +builder_set_up_stack2 (BuilderFixture *fixture, + gconstpointer test_data) +{ + MctSessionLimitsBuilder local_builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + memcpy (&fixture->stack_builder, &local_builder, sizeof (local_builder)); + fixture->builder = &fixture->stack_builder; +} + +static void +builder_tear_down_stack2 (BuilderFixture *fixture, + gconstpointer test_data) +{ + mct_session_limits_builder_clear (&fixture->stack_builder); + fixture->builder = NULL; +} + +static void +builder_set_up_heap (BuilderFixture *fixture, + gconstpointer test_data) +{ + fixture->builder = mct_session_limits_builder_new (); +} + +static void +builder_tear_down_heap (BuilderFixture *fixture, + gconstpointer test_data) +{ + g_clear_pointer (&fixture->builder, mct_session_limits_builder_free); +} + +/* Test building a non-empty #MctSessionLimits using an + * #MctSessionLimitsBuilder. */ +static void +test_session_limits_builder_non_empty (BuilderFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(MctSessionLimits) limits = NULL; + g_autofree const gchar **sections = NULL; + + mct_session_limits_builder_set_daily_schedule (fixture->builder, 100, 8 * 60 * 60); + + limits = mct_session_limits_builder_end (fixture->builder); + + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (0), NULL, NULL)); + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (99), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (100), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (8 * 60 * 60 - 1), NULL, NULL)); + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (8 * 60 * 60), NULL, NULL)); +} + +/* Test building an empty #MctSessionLimits using an #MctSessionLimitsBuilder. */ +static void +test_session_limits_builder_empty (BuilderFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(MctSessionLimits) limits = NULL; + g_autofree const gchar **sections = NULL; + + limits = mct_session_limits_builder_end (fixture->builder); + + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (0), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (99), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (100), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (8 * 60 * 60 - 1), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (8 * 60 * 60), NULL, NULL)); +} + +/* Check that copying a cleared #MctSessionLimitsBuilder works, and the copy can + * then be initialised and used to build a limits object. */ +static void +test_session_limits_builder_copy_empty (void) +{ + g_autoptr(MctSessionLimitsBuilder) builder = mct_session_limits_builder_new (); + g_autoptr(MctSessionLimitsBuilder) builder_copy = NULL; + g_autoptr(MctSessionLimits) limits = NULL; + + mct_session_limits_builder_clear (builder); + builder_copy = mct_session_limits_builder_copy (builder); + + mct_session_limits_builder_init (builder_copy); + mct_session_limits_builder_set_daily_schedule (builder_copy, 100, 8 * 60 * 60); + limits = mct_session_limits_builder_end (builder_copy); + + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (0), NULL, NULL)); + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (99), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (100), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (8 * 60 * 60 - 1), NULL, NULL)); + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (8 * 60 * 60), NULL, NULL)); +} + +/* Check that copying a filled #MctSessionLimitsBuilder works, and the copy can + * be used to build a limits object. */ +static void +test_session_limits_builder_copy_full (void) +{ + g_autoptr(MctSessionLimitsBuilder) builder = mct_session_limits_builder_new (); + g_autoptr(MctSessionLimitsBuilder) builder_copy = NULL; + g_autoptr(MctSessionLimits) limits = NULL; + + mct_session_limits_builder_set_daily_schedule (builder, 100, 8 * 60 * 60); + builder_copy = mct_session_limits_builder_copy (builder); + limits = mct_session_limits_builder_end (builder_copy); + + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (0), NULL, NULL)); + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (99), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (100), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (8 * 60 * 60 - 1), NULL, NULL)); + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (8 * 60 * 60), NULL, NULL)); +} + +/* Check that overriding an already-set limit in a #MctSessionLimitsBuilder + * removes all trace of it. In this test, override with a ‘none’ limit. */ +static void +test_session_limits_builder_override_none (void) +{ + g_autoptr(MctSessionLimitsBuilder) builder = mct_session_limits_builder_new (); + g_autoptr(MctSessionLimits) limits = NULL; + + /* Set up some schedule. */ + mct_session_limits_builder_set_daily_schedule (builder, 100, 8 * 60 * 60); + + /* Override it. */ + mct_session_limits_builder_set_none (builder); + limits = mct_session_limits_builder_end (builder); + + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (0), NULL, NULL)); +} + +/* Check that overriding an already-set limit in a #MctSessionLimitsBuilder + * removes all trace of it. In this test, override with a ‘daily schedule’ + * limit. */ +static void +test_session_limits_builder_override_daily_schedule (void) +{ + g_autoptr(MctSessionLimitsBuilder) builder = mct_session_limits_builder_new (); + g_autoptr(MctSessionLimits) limits = NULL; + + /* Set up some schedule. */ + mct_session_limits_builder_set_daily_schedule (builder, 100, 8 * 60 * 60); + + /* Override it. */ + mct_session_limits_builder_set_daily_schedule (builder, 200, 7 * 60 * 60); + limits = mct_session_limits_builder_end (builder); + + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (150), NULL, NULL)); + g_assert_true (mct_session_limits_check_time_remaining (limits, usec (4 * 60 * 60), NULL, NULL)); + g_assert_false (mct_session_limits_check_time_remaining (limits, usec (7 * 60 * 60 + 30 * 60), NULL, NULL)); +} + +/* Fixture for tests which interact with the accountsservice over D-Bus. The + * D-Bus service is mocked up using @queue, which allows us to reply to D-Bus + * calls from the code under test from within the test process. + * + * It exports one user object (for UID 500) and the manager object. The method + * return values from UID 500 are up to the test in question, so it could be an + * administrator, or non-administrator, have a restrictive or permissive app + * limits, etc. + */ +typedef struct +{ + GtDBusQueue *queue; /* (owned) */ + uid_t valid_uid; + uid_t missing_uid; + MctManager *manager; /* (owned) */ +} BusFixture; + +static void +bus_set_up (BusFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *object_path = NULL; + + fixture->valid_uid = 500; /* arbitrarily chosen */ + fixture->missing_uid = 501; /* must be different from valid_uid and not exported */ + fixture->queue = gt_dbus_queue_new (); + + gt_dbus_queue_connect (fixture->queue, &local_error); + g_assert_no_error (local_error); + + gt_dbus_queue_own_name (fixture->queue, "org.freedesktop.Accounts"); + + object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%u", fixture->valid_uid); + gt_dbus_queue_export_object (fixture->queue, + object_path, + (GDBusInterfaceInfo *) &com_endlessm_parental_controls_session_limits_interface, + &local_error); + g_assert_no_error (local_error); + + gt_dbus_queue_export_object (fixture->queue, + "/org/freedesktop/Accounts", + (GDBusInterfaceInfo *) &org_freedesktop_accounts_interface, + &local_error); + g_assert_no_error (local_error); + + fixture->manager = mct_manager_new (gt_dbus_queue_get_client_connection (fixture->queue)); +} + +static void +bus_tear_down (BusFixture *fixture, + gconstpointer test_data) +{ + g_clear_object (&fixture->manager); + gt_dbus_queue_disconnect (fixture->queue, TRUE); + g_clear_pointer (&fixture->queue, gt_dbus_queue_free); +} + +/* Helper #GAsyncReadyCallback which returns the #GAsyncResult in its @user_data. */ +static void +async_result_cb (GObject *obj, + GAsyncResult *result, + gpointer user_data) +{ + GAsyncResult **result_out = (GAsyncResult **) user_data; + + g_assert_null (*result_out); + *result_out = g_object_ref (result); +} + +/* Generic mock accountsservice implementation which returns the properties + * given in #GetSessionLimitsData.properties if queried for a UID matching + * #GetSessionLimitsData.expected_uid. Intended to be used for writing + * ‘successful’ mct_manager_get_session_limits() tests returning a variety of + * values. */ +typedef struct +{ + uid_t expected_uid; + const gchar *properties; +} GetSessionLimitsData; + +/* This is run in a worker thread. */ +static void +get_session_limits_server_cb (GtDBusQueue *queue, + gpointer user_data) +{ + const GetSessionLimitsData *data = user_data; + g_autoptr(GDBusMethodInvocation) invocation1 = NULL; + g_autoptr(GDBusMethodInvocation) invocation2 = NULL; + g_autofree gchar *object_path = NULL; + g_autoptr(GVariant) properties_variant = NULL; + + /* Handle the FindUserById() call. */ + gint64 user_id; + invocation1 = + gt_dbus_queue_assert_pop_message (queue, + "/org/freedesktop/Accounts", + "org.freedesktop.Accounts", + "FindUserById", "(x)", &user_id); + g_assert_cmpint (user_id, ==, data->expected_uid); + + object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%u", (uid_t) user_id); + g_dbus_method_invocation_return_value (invocation1, g_variant_new ("(o)", object_path)); + + /* Handle the Properties.GetAll() call and return some arbitrary, valid values + * for the given user. */ + const gchar *property_interface; + invocation2 = + gt_dbus_queue_assert_pop_message (queue, + object_path, + "org.freedesktop.DBus.Properties", + "GetAll", "(&s)", &property_interface); + g_assert_cmpstr (property_interface, ==, "com.endlessm.ParentalControls.SessionLimits"); + + properties_variant = g_variant_ref_sink (g_variant_new_parsed (data->properties)); + g_dbus_method_invocation_return_value (invocation2, + g_variant_new_tuple (&properties_variant, 1)); +} + +/* Test that getting an #MctSessionLimits from the mock D-Bus service works. The + * @test_data is a boolean value indicating whether to do the call + * synchronously (%FALSE) or asynchronously (%TRUE). + * + * The mock D-Bus replies are generated in get_session_limits_server_cb(), which + * is used for both synchronous and asynchronous calls. */ +static void +test_session_limits_bus_get (BusFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(MctSessionLimits) session_limits = NULL; + g_autoptr(GError) local_error = NULL; + guint64 time_remaining_secs; + gboolean time_limit_enabled; + gboolean test_async = GPOINTER_TO_UINT (test_data); + const GetSessionLimitsData get_session_limits_data = + { + .expected_uid = fixture->valid_uid, + .properties = "{" + "'LimitType': <@u 1>," + "'DailySchedule': <(@u 100, @u 8000)>" + "}" + }; + + gt_dbus_queue_set_server_func (fixture->queue, get_session_limits_server_cb, + (gpointer) &get_session_limits_data); + + if (test_async) + { + g_autoptr(GAsyncResult) result = NULL; + + mct_manager_get_session_limits_async (fixture->manager, + fixture->valid_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, + async_result_cb, &result); + + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + session_limits = mct_manager_get_session_limits_finish (fixture->manager, result, &local_error); + } + else + { + session_limits = mct_manager_get_session_limits (fixture->manager, + fixture->valid_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, + &local_error); + } + + g_assert_no_error (local_error); + g_assert_nonnull (session_limits); + + /* Check the session limits properties. */ + g_assert_cmpuint (mct_session_limits_get_user_id (session_limits), ==, fixture->valid_uid); + g_assert_false (mct_session_limits_check_time_remaining (session_limits, usec (0), + &time_remaining_secs, &time_limit_enabled)); + g_assert_true (time_limit_enabled); + g_assert_true (mct_session_limits_check_time_remaining (session_limits, usec (2000), + &time_remaining_secs, &time_limit_enabled)); + g_assert_cmpuint (time_remaining_secs, ==, 8000 - 2000); + g_assert_true (time_limit_enabled); +} + +/* Test that getting an #MctSessionLimits from the mock D-Bus service works. The + * @test_data is a boolean value indicating whether to do the call + * synchronously (%FALSE) or asynchronously (%TRUE). + * + * The mock D-Bus replies are generated in get_session_limits_server_cb(), which + * is used for both synchronous and asynchronous calls. */ +static void +test_session_limits_bus_get_none (BusFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(MctSessionLimits) session_limits = NULL; + g_autoptr(GError) local_error = NULL; + guint64 time_remaining_secs; + gboolean time_limit_enabled; + gboolean test_async = GPOINTER_TO_UINT (test_data); + const GetSessionLimitsData get_session_limits_data = + { + .expected_uid = fixture->valid_uid, + .properties = "{" + "'LimitType': <@u 0>," + "'DailySchedule': <(@u 0, @u 86400)>" + "}" + }; + + gt_dbus_queue_set_server_func (fixture->queue, get_session_limits_server_cb, + (gpointer) &get_session_limits_data); + + if (test_async) + { + g_autoptr(GAsyncResult) result = NULL; + + mct_manager_get_session_limits_async (fixture->manager, + fixture->valid_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, + async_result_cb, &result); + + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + session_limits = mct_manager_get_session_limits_finish (fixture->manager, result, &local_error); + } + else + { + session_limits = mct_manager_get_session_limits (fixture->manager, + fixture->valid_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, + &local_error); + } + + g_assert_no_error (local_error); + g_assert_nonnull (session_limits); + + /* Check the session limits properties. */ + g_assert_cmpuint (mct_session_limits_get_user_id (session_limits), ==, fixture->valid_uid); + g_assert_true (mct_session_limits_check_time_remaining (session_limits, usec (0), + &time_remaining_secs, &time_limit_enabled)); + g_assert_false (time_limit_enabled); + g_assert_true (mct_session_limits_check_time_remaining (session_limits, usec (2000), + &time_remaining_secs, &time_limit_enabled)); + g_assert_false (time_limit_enabled); +} + +/* Test that mct_manager_get_session_limits() returns an appropriate error if the + * mock D-Bus service reports that the given user cannot be found. + * + * The mock D-Bus replies are generated inline. */ +static void +test_session_limits_bus_get_error_invalid_user (BusFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(GAsyncResult) result = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(GDBusMethodInvocation) invocation = NULL; + g_autofree gchar *error_message = NULL; + g_autoptr(MctSessionLimits) session_limits = NULL; + + mct_manager_get_session_limits_async (fixture->manager, + fixture->missing_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, + async_result_cb, &result); + + /* Handle the FindUserById() call and claim the user doesn’t exist. */ + gint64 user_id; + invocation = + gt_dbus_queue_assert_pop_message (fixture->queue, + "/org/freedesktop/Accounts", + "org.freedesktop.Accounts", + "FindUserById", "(x)", &user_id); + g_assert_cmpint (user_id, ==, fixture->missing_uid); + + error_message = g_strdup_printf ("Failed to look up user with uid %u.", fixture->missing_uid); + g_dbus_method_invocation_return_dbus_error (invocation, + "org.freedesktop.Accounts.Error.Failed", + error_message); + + /* Get the get_session_limits() result. */ + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + session_limits = mct_manager_get_session_limits_finish (fixture->manager, result, + &local_error); + + g_assert_error (local_error, + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_USER); + g_assert_null (session_limits); +} + +/* Test that mct_manager_get_session_limits() returns an appropriate error if the + * mock D-Bus service reports that the properties of the given user can’t be + * accessed due to permissions. + * + * The mock D-Bus replies are generated inline. */ +static void +test_session_limits_bus_get_error_permission_denied (BusFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(GAsyncResult) result = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(GDBusMethodInvocation) invocation1 = NULL; + g_autoptr(GDBusMethodInvocation) invocation2 = NULL; + g_autofree gchar *object_path = NULL; + g_autoptr(MctSessionLimits) session_limits = NULL; + + mct_manager_get_session_limits_async (fixture->manager, + fixture->valid_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, + async_result_cb, &result); + + /* Handle the FindUserById() call. */ + gint64 user_id; + invocation1 = + gt_dbus_queue_assert_pop_message (fixture->queue, + "/org/freedesktop/Accounts", + "org.freedesktop.Accounts", + "FindUserById", "(x)", &user_id); + g_assert_cmpint (user_id, ==, fixture->valid_uid); + + object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%u", (uid_t) user_id); + g_dbus_method_invocation_return_value (invocation1, g_variant_new ("(o)", object_path)); + + /* Handle the Properties.GetAll() call and return a permission denied error. */ + const gchar *property_interface; + invocation2 = + gt_dbus_queue_assert_pop_message (fixture->queue, + object_path, + "org.freedesktop.DBus.Properties", + "GetAll", "(&s)", &property_interface); + g_assert_cmpstr (property_interface, ==, "com.endlessm.ParentalControls.SessionLimits"); + + g_dbus_method_invocation_return_dbus_error (invocation2, + "org.freedesktop.Accounts.Error.PermissionDenied", + "Not authorized"); + + /* Get the get_session_limits() result. */ + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + session_limits = mct_manager_get_session_limits_finish (fixture->manager, result, + &local_error); + + g_assert_error (local_error, + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED); + g_assert_null (session_limits); +} + +/* Test that mct_manager_get_session_limits() returns an appropriate error if + * the mock D-Bus service replies with no session limits properties (implying + * that it hasn’t sent the property values because of permissions). + * + * The mock D-Bus replies are generated inline. */ +static void +test_session_limits_bus_get_error_permission_denied_missing (BusFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(GAsyncResult) result = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(GDBusMethodInvocation) invocation1 = NULL; + g_autoptr(GDBusMethodInvocation) invocation2 = NULL; + g_autofree gchar *object_path = NULL; + g_autoptr(MctSessionLimits) session_limits = NULL; + + mct_manager_get_session_limits_async (fixture->manager, + fixture->valid_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, + async_result_cb, &result); + + /* Handle the FindUserById() call. */ + gint64 user_id; + invocation1 = + gt_dbus_queue_assert_pop_message (fixture->queue, + "/org/freedesktop/Accounts", + "org.freedesktop.Accounts", + "FindUserById", "(x)", &user_id); + g_assert_cmpint (user_id, ==, fixture->valid_uid); + + object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%u", (uid_t) user_id); + g_dbus_method_invocation_return_value (invocation1, g_variant_new ("(o)", object_path)); + + /* Handle the Properties.GetAll() call and return an empty array due to not + * having permission to access the properties. The code actually keys off the + * presence of the LimitType property, since that was the first one to be + * added. */ + const gchar *property_interface; + invocation2 = + gt_dbus_queue_assert_pop_message (fixture->queue, + object_path, + "org.freedesktop.DBus.Properties", + "GetAll", "(&s)", &property_interface); + g_assert_cmpstr (property_interface, ==, "com.endlessm.ParentalControls.SessionLimits"); + + g_dbus_method_invocation_return_value (invocation2, g_variant_new ("(a{sv})", NULL)); + + /* Get the get_session_limits() result. */ + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + session_limits = mct_manager_get_session_limits_finish (fixture->manager, result, + &local_error); + + g_assert_error (local_error, + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED); + g_assert_null (session_limits); +} + +/* Test that mct_manager_get_session_limits() returns an error if the mock D-Bus + * service reports an unrecognised error. + * + * The mock D-Bus replies are generated inline. */ +static void +test_session_limits_bus_get_error_unknown (BusFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(GAsyncResult) result = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(GDBusMethodInvocation) invocation = NULL; + g_autoptr(MctSessionLimits) session_limits = NULL; + + mct_manager_get_session_limits_async (fixture->manager, + fixture->valid_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, + async_result_cb, &result); + + /* Handle the FindUserById() call and return a bogus error. */ + gint64 user_id; + invocation = + gt_dbus_queue_assert_pop_message (fixture->queue, + "/org/freedesktop/Accounts", + "org.freedesktop.Accounts", + "FindUserById", "(x)", &user_id); + g_assert_cmpint (user_id, ==, fixture->valid_uid); + + g_dbus_method_invocation_return_dbus_error (invocation, + "org.freedesktop.Accounts.Error.NewAndInterestingError", + "This is a fake error message " + "which libmalcontent " + "will never have seen before, " + "but must still handle correctly"); + + /* Get the get_session_limits() result. */ + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + session_limits = mct_manager_get_session_limits_finish (fixture->manager, result, + &local_error); + + /* We don’t actually care what error is actually used here. */ + g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_DBUS_ERROR); + g_assert_null (session_limits); +} + +/* Test that mct_manager_get_session_limits() returns an error if the mock D-Bus + * service reports an unknown interface, which means that parental controls are + * not installed properly. + * + * The mock D-Bus replies are generated inline. */ +static void +test_session_limits_bus_get_error_disabled (BusFixture *fixture, + gconstpointer test_data) +{ + g_autoptr(GAsyncResult) result = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(GDBusMethodInvocation) invocation1 = NULL; + g_autoptr(GDBusMethodInvocation) invocation2 = NULL; + g_autofree gchar *object_path = NULL; + g_autoptr(MctSessionLimits) session_limits = NULL; + + mct_manager_get_session_limits_async (fixture->manager, + fixture->valid_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, NULL, + async_result_cb, &result); + + /* Handle the FindUserById() call. */ + gint64 user_id; + invocation1 = + gt_dbus_queue_assert_pop_message (fixture->queue, + "/org/freedesktop/Accounts", + "org.freedesktop.Accounts", + "FindUserById", "(x)", &user_id); + g_assert_cmpint (user_id, ==, fixture->valid_uid); + + object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%u", (uid_t) user_id); + g_dbus_method_invocation_return_value (invocation1, g_variant_new ("(o)", object_path)); + + /* Handle the Properties.GetAll() call and return an InvalidArgs error. */ + const gchar *property_interface; + invocation2 = + gt_dbus_queue_assert_pop_message (fixture->queue, + object_path, + "org.freedesktop.DBus.Properties", + "GetAll", "(&s)", &property_interface); + g_assert_cmpstr (property_interface, ==, "com.endlessm.ParentalControls.SessionLimits"); + + g_dbus_method_invocation_return_dbus_error (invocation2, + "org.freedesktop.DBus.Error.InvalidArgs", + "No such interface " + "“com.endlessm.ParentalControls.SessionLimits”"); + + /* Get the get_session_limits() result. */ + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + session_limits = mct_manager_get_session_limits_finish (fixture->manager, result, + &local_error); + + g_assert_error (local_error, + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_DISABLED); + g_assert_null (session_limits); +} + +/* Generic mock accountsservice implementation which handles properties being + * set on a mock User object, and compares their values to the given + * `expected_*` ones. + * + * If @error_index is non-negative, it gives the index of a Set() call to return + * the given @dbus_error_name and @dbus_error_message from, rather than + * accepting the property value from the caller. If @error_index is negative, + * all Set() calls will be accepted. */ +typedef struct +{ + uid_t expected_uid; + + const gchar * const *expected_properties; + + /* All GVariants in text format: */ + const gchar *expected_limit_type_value; /* (nullable) */ + const gchar *expected_daily_schedule_value; /* (nullable) */ + + gint error_index; /* -1 to return no error */ + const gchar *dbus_error_name; /* NULL to return no error */ + const gchar *dbus_error_message; /* NULL to return no error */ +} SetSessionLimitsData; + +static const gchar * +set_session_limits_data_get_expected_property_value (const SetSessionLimitsData *data, + const gchar *property_name) +{ + if (g_str_equal (property_name, "LimitType")) + return data->expected_limit_type_value; + else if (g_str_equal (property_name, "DailySchedule")) + return data->expected_daily_schedule_value; + else + g_assert_not_reached (); +} + +/* This is run in a worker thread. */ +static void +set_session_limits_server_cb (GtDBusQueue *queue, + gpointer user_data) +{ + const SetSessionLimitsData *data = user_data; + g_autoptr(GDBusMethodInvocation) find_invocation = NULL; + g_autofree gchar *object_path = NULL; + + g_assert ((data->error_index == -1) == (data->dbus_error_name == NULL)); + g_assert ((data->dbus_error_name == NULL) == (data->dbus_error_message == NULL)); + + /* Handle the FindUserById() call. */ + gint64 user_id; + find_invocation = + gt_dbus_queue_assert_pop_message (queue, + "/org/freedesktop/Accounts", + "org.freedesktop.Accounts", + "FindUserById", "(x)", &user_id); + g_assert_cmpint (user_id, ==, data->expected_uid); + + object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%u", (uid_t) user_id); + g_dbus_method_invocation_return_value (find_invocation, g_variant_new ("(o)", object_path)); + + /* Handle the Properties.Set() calls. */ + gsize i; + + for (i = 0; data->expected_properties[i] != NULL; i++) + { + const gchar *property_interface; + const gchar *property_name; + g_autoptr(GVariant) property_value = NULL; + g_autoptr(GDBusMethodInvocation) property_invocation = NULL; + g_autoptr(GVariant) expected_property_value = NULL; + + property_invocation = + gt_dbus_queue_assert_pop_message (queue, + object_path, + "org.freedesktop.DBus.Properties", + "Set", "(&s&sv)", &property_interface, + &property_name, &property_value); + g_assert_cmpstr (property_interface, ==, "com.endlessm.ParentalControls.SessionLimits"); + g_assert_cmpstr (property_name, ==, data->expected_properties[i]); + + if (data->error_index >= 0 && (gsize) data->error_index == i) + { + g_dbus_method_invocation_return_dbus_error (property_invocation, + data->dbus_error_name, + data->dbus_error_message); + break; + } + else + { + expected_property_value = g_variant_new_parsed (set_session_limits_data_get_expected_property_value (data, property_name)); + g_assert_cmpvariant (property_value, expected_property_value); + + g_dbus_method_invocation_return_value (property_invocation, NULL); + } + } +} + +/* Test that setting an #MctSessionLimits on the mock D-Bus service works. The + * @test_data is a boolean value indicating whether to do the call + * synchronously (%FALSE) or asynchronously (%TRUE). + * + * The mock D-Bus replies are generated in set_session_limits_server_cb(), which + * is used for both synchronous and asynchronous calls. */ +static void +test_session_limits_bus_set (BusFixture *fixture, + gconstpointer test_data) +{ + gboolean success; + g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + g_autoptr(MctSessionLimits) session_limits = NULL; + g_autoptr(GError) local_error = NULL; + gboolean test_async = GPOINTER_TO_UINT (test_data); + const gchar *expected_properties[] = + { + "DailySchedule", + "LimitType", + NULL + }; + const SetSessionLimitsData set_session_limits_data = + { + .expected_uid = fixture->valid_uid, + .expected_properties = expected_properties, + .expected_limit_type_value = "@u 1", + .expected_daily_schedule_value = "(@u 100, @u 4000)", + .error_index = -1, + }; + + /* Build a session limits object. */ + mct_session_limits_builder_set_daily_schedule (&builder, 100, 4000); + + session_limits = mct_session_limits_builder_end (&builder); + + /* Set the mock service function and set the limits. */ + gt_dbus_queue_set_server_func (fixture->queue, set_session_limits_server_cb, + (gpointer) &set_session_limits_data); + + if (test_async) + { + g_autoptr(GAsyncResult) result = NULL; + + mct_manager_set_session_limits_async (fixture->manager, + fixture->valid_uid, session_limits, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, + async_result_cb, &result); + + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + success = mct_manager_set_session_limits_finish (fixture->manager, result, + &local_error); + } + else + { + success = mct_manager_set_session_limits (fixture->manager, + fixture->valid_uid, session_limits, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, + &local_error); + } + + g_assert_no_error (local_error); + g_assert_true (success); +} + +/* Test that mct_manager_set_session_limits() returns an appropriate error if + * the mock D-Bus service reports that the given user cannot be found. + * + * The mock D-Bus replies are generated inline. */ +static void +test_session_limits_bus_set_error_invalid_user (BusFixture *fixture, + gconstpointer test_data) +{ + gboolean success; + g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + g_autoptr(MctSessionLimits) session_limits = NULL; + g_autoptr(GAsyncResult) result = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(GDBusMethodInvocation) invocation = NULL; + g_autofree gchar *error_message = NULL; + + /* Use the default session limits. */ + session_limits = mct_session_limits_builder_end (&builder); + + mct_manager_set_session_limits_async (fixture->manager, + fixture->missing_uid, session_limits, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, + async_result_cb, &result); + + /* Handle the FindUserById() call and claim the user doesn’t exist. */ + gint64 user_id; + invocation = + gt_dbus_queue_assert_pop_message (fixture->queue, + "/org/freedesktop/Accounts", + "org.freedesktop.Accounts", + "FindUserById", "(x)", &user_id); + g_assert_cmpint (user_id, ==, fixture->missing_uid); + + error_message = g_strdup_printf ("Failed to look up user with uid %u.", fixture->missing_uid); + g_dbus_method_invocation_return_dbus_error (invocation, + "org.freedesktop.Accounts.Error.Failed", + error_message); + + /* Get the set_session_limits() result. */ + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + success = mct_manager_set_session_limits_finish (fixture->manager, result, + &local_error); + + g_assert_error (local_error, + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_USER); + g_assert_false (success); +} + +/* Test that mct_manager_set_session_limits() returns an appropriate error if the + * mock D-Bus service replies with a permission denied error when setting + * properties. + * + * The mock D-Bus replies are generated in set_session_limits_server_cb(). */ +static void +test_session_limits_bus_set_error_permission_denied (BusFixture *fixture, + gconstpointer test_data) +{ + gboolean success; + g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + g_autoptr(MctSessionLimits) session_limits = NULL; + g_autoptr(GError) local_error = NULL; + const gchar *expected_properties[] = + { + "LimitType", + NULL + }; + const SetSessionLimitsData set_session_limits_data = + { + .expected_uid = fixture->valid_uid, + .expected_properties = expected_properties, + .error_index = 0, + .dbus_error_name = "org.freedesktop.Accounts.Error.PermissionDenied", + .dbus_error_message = "Not authorized", + }; + + /* Use the default session limits. */ + session_limits = mct_session_limits_builder_end (&builder); + + gt_dbus_queue_set_server_func (fixture->queue, set_session_limits_server_cb, + (gpointer) &set_session_limits_data); + + success = mct_manager_set_session_limits (fixture->manager, + fixture->valid_uid, session_limits, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, + &local_error); + + g_assert_error (local_error, + MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED); + g_assert_false (success); +} + +/* Test that mct_manager_set_session_limits() returns an error if the mock D-Bus + * service reports an unrecognised error. + * + * The mock D-Bus replies are generated in set_session_limits_server_cb(). */ +static void +test_session_limits_bus_set_error_unknown (BusFixture *fixture, + gconstpointer test_data) +{ + gboolean success; + g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + g_autoptr(MctSessionLimits) session_limits = NULL; + g_autoptr(GError) local_error = NULL; + const gchar *expected_properties[] = + { + "LimitType", + NULL + }; + const SetSessionLimitsData set_session_limits_data = + { + .expected_uid = fixture->valid_uid, + .expected_properties = expected_properties, + .error_index = 0, + .dbus_error_name = "org.freedesktop.Accounts.Error.NewAndInterestingError", + .dbus_error_message = "This is a fake error message which " + "libmalcontent will never have seen " + "before, but must still handle correctly", + }; + + /* Use the default session limits. */ + session_limits = mct_session_limits_builder_end (&builder); + + gt_dbus_queue_set_server_func (fixture->queue, set_session_limits_server_cb, + (gpointer) &set_session_limits_data); + + success = mct_manager_set_session_limits (fixture->manager, + fixture->valid_uid, session_limits, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, + &local_error); + + g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_DBUS_ERROR); + g_assert_false (success); +} + +/* Test that mct_manager_set_session_limits() returns an error if the mock D-Bus + * service reports an InvalidArgs error with a given one of its Set() calls. + * + * @test_data contains a property index encoded with GINT_TO_POINTER(), + * indicating which Set() call to return the error on, since the calls are made + * in series. + * + * The mock D-Bus replies are generated in set_session_limits_server_cb(). */ +static void +test_session_limits_bus_set_error_invalid_property (BusFixture *fixture, + gconstpointer test_data) +{ + gboolean success; + g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + g_autoptr(MctSessionLimits) session_limits = NULL; + g_autoptr(GError) local_error = NULL; + const gchar *expected_properties[] = + { + "DailySchedule", + "LimitType", + NULL + }; + const SetSessionLimitsData set_session_limits_data = + { + .expected_uid = fixture->valid_uid, + .expected_properties = expected_properties, + .expected_limit_type_value = "@u 1", + .expected_daily_schedule_value = "(@u 100, @u 3000)", + .error_index = GPOINTER_TO_INT (test_data), + .dbus_error_name = "org.freedesktop.DBus.Error.InvalidArgs", + .dbus_error_message = "Mumble mumble something wrong with the limits value", + }; + + /* Build a session limits object. */ + mct_session_limits_builder_set_daily_schedule (&builder, 100, 3000); + + session_limits = mct_session_limits_builder_end (&builder); + + gt_dbus_queue_set_server_func (fixture->queue, set_session_limits_server_cb, + (gpointer) &set_session_limits_data); + + success = mct_manager_set_session_limits (fixture->manager, + fixture->valid_uid, session_limits, + MCT_MANAGER_SET_VALUE_FLAGS_NONE, NULL, + &local_error); + + g_assert_error (local_error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS); + g_assert_false (success); +} + +int +main (int argc, + char **argv) +{ + setlocale (LC_ALL, ""); + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/session-limits/types", test_session_limits_types); + g_test_add_func ("/session-limits/refs", test_session_limits_refs); + g_test_add_func ("/session-limits/check-time-remaining/invalid-time", + test_session_limits_check_time_remaining_invalid_time); + + g_test_add ("/session-limits/builder/stack/non-empty", BuilderFixture, NULL, + builder_set_up_stack, test_session_limits_builder_non_empty, + builder_tear_down_stack); + g_test_add ("/session-limits/builder/stack/empty", BuilderFixture, NULL, + builder_set_up_stack, test_session_limits_builder_empty, + builder_tear_down_stack); + g_test_add ("/session-limits/builder/stack2/non-empty", BuilderFixture, NULL, + builder_set_up_stack2, test_session_limits_builder_non_empty, + builder_tear_down_stack2); + g_test_add ("/session-limits/builder/stack2/empty", BuilderFixture, NULL, + builder_set_up_stack2, test_session_limits_builder_empty, + builder_tear_down_stack2); + g_test_add ("/session-limits/builder/heap/non-empty", BuilderFixture, NULL, + builder_set_up_heap, test_session_limits_builder_non_empty, + builder_tear_down_heap); + g_test_add ("/session-limits/builder/heap/empty", BuilderFixture, NULL, + builder_set_up_heap, test_session_limits_builder_empty, + builder_tear_down_heap); + g_test_add_func ("/session-limits/builder/copy/empty", + test_session_limits_builder_copy_empty); + g_test_add_func ("/session-limits/builder/copy/full", + test_session_limits_builder_copy_full); + g_test_add_func ("/session-limits/builder/override/none", + test_session_limits_builder_override_none); + g_test_add_func ("/session-limits/builder/override/daily-schedule", + test_session_limits_builder_override_daily_schedule); + + g_test_add ("/session-limits/bus/get/async", BusFixture, GUINT_TO_POINTER (TRUE), + bus_set_up, test_session_limits_bus_get, bus_tear_down); + g_test_add ("/session-limits/bus/get/sync", BusFixture, GUINT_TO_POINTER (FALSE), + bus_set_up, test_session_limits_bus_get, bus_tear_down); + g_test_add ("/session-limits/bus/get/none", BusFixture, NULL, + bus_set_up, test_session_limits_bus_get_none, bus_tear_down); + + g_test_add ("/session-limits/bus/get/error/invalid-user", BusFixture, NULL, + bus_set_up, test_session_limits_bus_get_error_invalid_user, bus_tear_down); + g_test_add ("/session-limits/bus/get/error/permission-denied", BusFixture, NULL, + bus_set_up, test_session_limits_bus_get_error_permission_denied, bus_tear_down); + g_test_add ("/session-limits/bus/get/error/permission-denied-missing", BusFixture, NULL, + bus_set_up, test_session_limits_bus_get_error_permission_denied_missing, bus_tear_down); + g_test_add ("/session-limits/bus/get/error/unknown", BusFixture, NULL, + bus_set_up, test_session_limits_bus_get_error_unknown, bus_tear_down); + g_test_add ("/session-limits/bus/get/error/disabled", BusFixture, NULL, + bus_set_up, test_session_limits_bus_get_error_disabled, bus_tear_down); + + g_test_add ("/session-limits/bus/set/async", BusFixture, GUINT_TO_POINTER (TRUE), + bus_set_up, test_session_limits_bus_set, bus_tear_down); + g_test_add ("/session-limits/bus/set/sync", BusFixture, GUINT_TO_POINTER (FALSE), + bus_set_up, test_session_limits_bus_set, bus_tear_down); + + g_test_add ("/session-limits/bus/set/error/invalid-user", BusFixture, NULL, + bus_set_up, test_session_limits_bus_set_error_invalid_user, bus_tear_down); + g_test_add ("/session-limits/bus/set/error/permission-denied", BusFixture, NULL, + bus_set_up, test_session_limits_bus_set_error_permission_denied, bus_tear_down); + g_test_add ("/session-limits/bus/set/error/unknown", BusFixture, NULL, + bus_set_up, test_session_limits_bus_set_error_unknown, bus_tear_down); + g_test_add ("/session-limits/bus/set/error/invalid-property/daily-schedule", + BusFixture, GINT_TO_POINTER (0), bus_set_up, + test_session_limits_bus_set_error_invalid_property, bus_tear_down); + g_test_add ("/session-limits/bus/set/error/invalid-property/limit-type", + BusFixture, GINT_TO_POINTER (1), bus_set_up, + test_session_limits_bus_set_error_invalid_property, bus_tear_down); + + return g_test_run (); +} diff --git a/malcontent-client/docs/malcontent-client.8 b/malcontent-client/docs/malcontent-client.8 index c6bb70b..cfc3c6d 100644 --- a/malcontent-client/docs/malcontent-client.8 +++ b/malcontent-client/docs/malcontent-client.8 @@ -10,9 +10,9 @@ malcontent\-client — Parental Controls Access Utility .SH SYNOPSIS .IX Header "SYNOPSIS" .\" -\fBmalcontent\-client get [\-q] [\-n] [\fPUSER\fB] +\fBmalcontent\-client get\-app\-filter [\-q] [\-n] [\fPUSER\fB] .PP -\fBmalcontent\-client check [\-q] [\-n] [\fPUSER\fB] \fPARG\fB +\fBmalcontent\-client check\-app\-filter [\-q] [\-n] [\fPUSER\fB] \fPARG\fB .\" .SH DESCRIPTION .IX Header "DESCRIPTION" @@ -25,10 +25,13 @@ controls. 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. +are \fBget\-app\-filter\fP and \fBcheck\-app\-filter\fP. +.PP +The command line API and output format are unstable and likely to change in +future versions of \fBmalcontent\-client\fP. .\" -.SH \fBget\fP OPTIONS -.IX Header "get OPTIONS" +.SH \fBget\-app\-filter\fP OPTIONS +.IX Header "get\-app\-filter OPTIONS" .\" .IP "\fBUSER\fP" Username or ID of the user to get the app filter for. If not specified, the @@ -43,8 +46,8 @@ 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" +.SH \fBcheck\-app\-filter\fP OPTIONS +.IX Header "check\-app\-filter OPTIONS" .\" .IP "\fBUSER\fP" Username or ID of the user to get the app filter for. If not specified, the @@ -85,8 +88,8 @@ 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, content type or flatpak ref -was allowed for the given user. +If running the \fBcheck\-app\-filter\fP command, the given path, content type or +flatpak ref was allowed for the given user. .\" .IP "1" 4 .IX Item "1" @@ -99,8 +102,17 @@ 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, content type or flatpak ref -was \fInot\fP allowed for the given user. +If running the \fBcheck\-app\-filter\fP command, the given path, content type or +flatpak ref was \fInot\fP allowed for the given user. +.\" +.IP "4" 4 +.IX Item "4" +Malcontent is disabled at the system level, and hence parental controls are +not enabled or enforced. +.\" +.IP "5" 4 +.IX Item "5" +An operation failed and no more specific error information is available. .\" .SH BUGS .IX Header "BUGS" diff --git a/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index 5be8eb5..2c53018 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -17,6 +17,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import argparse +import datetime import os import pwd import sys @@ -30,6 +31,25 @@ EXIT_SUCCESS = 0 EXIT_INVALID_OPTION = 1 EXIT_PERMISSION_DENIED = 2 EXIT_PATH_NOT_ALLOWED = 3 +EXIT_DISABLED = 4 +EXIT_FAILED = 5 + + +def __manager_error_to_exit_code(error): + if error.matches(Malcontent.manager_error_quark(), + Malcontent.ManagerError.INVALID_USER): + return EXIT_INVALID_OPTION + elif error.matches(Malcontent.manager_error_quark(), + Malcontent.ManagerError.PERMISSION_DENIED): + return EXIT_PERMISSION_DENIED + elif error.matches(Malcontent.manager_error_quark(), + Malcontent.ManagerError.INVALID_DATA): + return EXIT_INVALID_OPTION + elif error.matches(Malcontent.manager_error_quark(), + Malcontent.ManagerError.DISABLED): + return EXIT_DISABLED + + return EXIT_FAILED def __get_app_filter(user_id, interactive): @@ -38,9 +58,9 @@ def __get_app_filter(user_id, interactive): If `interactive` is `True`, interactive polkit authorisation dialogues will be allowed. An exception will be raised on failure.""" if interactive: - flags = Malcontent.GetAppFilterFlags.INTERACTIVE + flags = Malcontent.ManagerGetValueFlags.INTERACTIVE else: - flags = Malcontent.GetAppFilterFlags.NONE + flags = Malcontent.ManagerGetValueFlags.NONE connection = Gio.bus_get_sync(Gio.BusType.SYSTEM) manager = Malcontent.Manager.new(connection) @@ -57,7 +77,35 @@ def __get_app_filter_or_error(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) + raise SystemExit(__manager_error_to_exit_code(e)) + + +def __get_session_limits(user_id, interactive): + """Get the session limits for `user_id` off the bus. + + If `interactive` is `True`, interactive polkit authorisation dialogues will + be allowed. An exception will be raised on failure.""" + if interactive: + flags = Malcontent.ManagerGetValueFlags.INTERACTIVE + else: + flags = Malcontent.ManagerGetValueFlags.NONE + + connection = Gio.bus_get_sync(Gio.BusType.SYSTEM) + manager = Malcontent.Manager.new(connection) + return manager.get_session_limits( + user_id=user_id, + flags=flags, cancellable=None) + + +def __get_session_limits_or_error(user_id, interactive): + """Wrapper around __get_session_limits() which prints an error and raises + SystemExit, rather than an internal exception.""" + try: + return __get_session_limits(user_id, interactive) + except GLib.Error as e: + print('Error getting session limits for user {}: {}'.format( + user_id, e.message), file=sys.stderr) + raise SystemExit(__manager_error_to_exit_code(e)) def __set_app_filter(user_id, app_filter, interactive): @@ -66,9 +114,9 @@ def __set_app_filter(user_id, app_filter, interactive): If `interactive` is `True`, interactive polkit authorisation dialogues will be allowed. An exception will be raised on failure.""" if interactive: - flags = Malcontent.GetAppFilterFlags.INTERACTIVE + flags = Malcontent.ManagerSetValueFlags.INTERACTIVE else: - flags = Malcontent.GetAppFilterFlags.NONE + flags = Malcontent.ManagerSetValueFlags.NONE connection = Gio.bus_get_sync(Gio.BusType.SYSTEM) manager = Malcontent.Manager.new(connection) @@ -85,29 +133,34 @@ def __set_app_filter_or_error(user_id, app_filter, interactive): except GLib.Error as e: print('Error setting app filter for user {}: {}'.format( user_id, e.message), file=sys.stderr) - raise SystemExit(EXIT_PERMISSION_DENIED) + raise SystemExit(__manager_error_to_exit_code(e)) -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. +def __lookup_user_id(user_id_or_username): + """Convert a command-line specified username or ID into a + (user ID, username) tuple, looking up the component which isn’t specified. + If `user_id_or_username` is empty, use the current user ID. Raise KeyError if lookup fails.""" - if user == '': - return os.getuid() - elif user.isdigit(): - return int(user) + if user_id_or_username == '': + user_id = os.getuid() + return (user_id, pwd.getpwuid(user_id).pw_name) + elif user_id_or_username.isdigit(): + user_id = int(user_id_or_username) + return (user_id, pwd.getpwuid(user_id).pw_name) else: - return pwd.getpwnam(user).pw_uid + username = user_id_or_username + return (pwd.getpwnam(username).pw_uid, username) -def __lookup_user_id_or_error(user): +def __lookup_user_id_or_error(user_id_or_username): """Wrapper around __lookup_user_id() which prints an error and raises SystemExit, rather than an internal exception.""" try: - return __lookup_user_id(user) + return __lookup_user_id(user_id_or_username) except KeyError: - print('Error getting ID for username {}'.format(user), file=sys.stderr) + print('Error getting ID for username {}'.format(user_id_or_username), + file=sys.stderr) raise SystemExit(EXIT_INVALID_OPTION) @@ -138,12 +191,12 @@ def __oars_value_from_string(value_str): raise KeyError('Unknown OARS value ‘{}’'.format(value_str)) -def command_get(user, quiet=False, interactive=True): +def command_get_app_filter(user, quiet=False, interactive=True): """Get the app filter for the given user.""" - user_id = __lookup_user_id_or_error(user) + (user_id, username) = __lookup_user_id_or_error(user) app_filter = __get_app_filter_or_error(user_id, interactive) - print('App filter for user {} retrieved:'.format(user_id)) + print('App filter for user {} retrieved:'.format(username)) sections = app_filter.get_oars_sections() for section in sections: @@ -163,12 +216,30 @@ def command_get(user, quiet=False, interactive=True): print('App installation is disallowed to system repository') +def command_get_session_limits(user, now=None, quiet=False, interactive=True): + """Get the session limits for the given user.""" + (user_id, username) = __lookup_user_id_or_error(user) + session_limits = __get_session_limits_or_error(user_id, interactive) + + (user_allowed_now, time_remaining_secs, time_limit_enabled) = \ + session_limits.check_time_remaining(now.timestamp() * GLib.USEC_PER_SEC) + + if not time_limit_enabled: + print('Session limits are not enabled for user {}'.format(username)) + elif user_allowed_now: + print('Session limits are enabled for user {}, and they have {} ' + 'seconds remaining'.format(username, time_remaining_secs)) + else: + print('Session limits are enabled for user {}, and they have no time ' + 'remaining'.format(username)) + + def command_monitor(user, quiet=False, interactive=True): """Monitor app filter changes for the given user.""" if user == '': - filter_user_id = 0 + (filter_user_id, filter_username) = (0, '') else: - filter_user_id = __lookup_user_id_or_error(user) + (filter_user_id, filter_username) = __lookup_user_id_or_error(user) apply_filter = (user != '') def _on_app_filter_changed(manager, changed_user_id): @@ -181,7 +252,7 @@ def command_monitor(user, quiet=False, interactive=True): if apply_filter: print('Monitoring app filter changes for ' - 'user ID {}'.format(filter_user_id)) + 'user {}'.format(filter_username)) else: print('Monitoring app filter changes for all users') @@ -213,10 +284,10 @@ def is_valid_content_type(arg): parts[0] != '' and parts[1] != '') -def command_check(user, arg, quiet=False, interactive=True): +def command_check_app_filter(user, arg, quiet=False, interactive=True): """Check the given path, content type or flatpak ref is runnable by the given user, according to their app filter.""" - user_id = __lookup_user_id_or_error(user) + (user_id, username) = __lookup_user_id_or_error(user) app_filter = __get_app_filter_or_error(user_id, interactive) is_maybe_flatpak_id = arg.startswith('app/') and arg.count('/') < 3 @@ -259,31 +330,32 @@ def command_check(user, arg, quiet=False, interactive=True): if is_allowed: if not quiet: print('{} {} is allowed by app filter for user {}'.format( - noun, arg, user_id)) + noun, arg, username)) return else: if not quiet: print('{} {} is not allowed by app filter for user {}'.format( - noun, arg, user_id)) + noun, arg, username)) raise SystemExit(EXIT_PATH_NOT_ALLOWED) def command_oars_section(user, section, quiet=False, interactive=True): """Get the value of the given OARS section for the given user, according to their OARS filter.""" - user_id = __lookup_user_id_or_error(user) + (user_id, username) = __lookup_user_id_or_error(user) app_filter = __get_app_filter_or_error(user_id, interactive) value = app_filter.get_oars_value(section) print('OARS section ‘{}’ for user {} has value ‘{}’'.format( - section, user_id, __oars_value_to_string(value))) + section, username, __oars_value_to_string(value))) -def command_set(user, allow_user_installation=True, - allow_system_installation=False, app_filter_args=None, - quiet=False, interactive=True): +def command_set_app_filter(user, allow_user_installation=True, + allow_system_installation=False, + app_filter_args=None, quiet=False, + interactive=True): """Set the app filter for the given user.""" - user_id = __lookup_user_id_or_error(user) + (user_id, username) = __lookup_user_id_or_error(user) builder = Malcontent.AppFilterBuilder.new() builder.set_allow_user_installation(allow_user_installation) builder.set_allow_system_installation(allow_system_installation) @@ -327,7 +399,7 @@ def command_set(user, allow_user_installation=True, __set_app_filter_or_error(user_id, app_filter, interactive) if not quiet: - print('App filter for user {} set'.format(user_id)) + print('App filter for user {} set'.format(username)) def main(): @@ -335,8 +407,9 @@ def main(): 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) + help='command to run (default: ' + '‘get-app-filter’)') + parser.set_defaults(function=command_get_app_filter, user='') parser.add_argument('-q', '--quiet', action='store_true', help='output no informational messages') parser.set_defaults(quiet=False) @@ -353,14 +426,34 @@ def main(): 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)') + # ‘get-app-filter’ command + parser_get_app_filter = \ + subparsers.add_parser('get-app-filter', + parents=[common_parser], + help='get current app filter settings') + parser_get_app_filter.set_defaults(function=command_get_app_filter) + parser_get_app_filter.add_argument('user', default='', nargs='?', + help='user ID or username to get the ' + 'app filter for (default: current ' + 'user)') + + # ‘get-session-limits’ command + parser_get_session_limits = \ + subparsers.add_parser('get-session-limits', + parents=[common_parser], + help='get current session limit settings') + parser_get_session_limits.set_defaults(function=command_get_session_limits) + parser_get_session_limits.add_argument('user', default='', nargs='?', + help='user ID or username to get ' + 'the session limits for (default: ' + 'current user)') + parser_get_session_limits.add_argument( + '--now', + metavar='yyyy-mm-ddThh:mm:ssZ', + type=lambda d: datetime.datetime.strptime(d, '%Y-%m-%dT%H:%M:%S%z'), + default=datetime.datetime.now(), + help='date/time to use as the value for ‘now’ (default: wall clock ' + 'time)') # ‘monitor’ command parser_monitor = subparsers.add_parser('monitor', @@ -371,18 +464,19 @@ def main(): help='user ID or username to monitor the app ' 'filter for (default: all users)') - # ‘check’ command - parser_check = subparsers.add_parser('check', parents=[common_parser], - help='check whether a path, content ' - 'type or flatpak ref 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('arg', - help='path to a program, content type or ' - 'flatpak ref to check') + # ‘check-app-filter’ command + parser_check_app_filter = \ + subparsers.add_parser('check-app-filter', parents=[common_parser], + help='check whether a path, content type or ' + 'flatpak ref is allowed by app filter') + parser_check_app_filter.set_defaults(function=command_check_app_filter) + parser_check_app_filter.add_argument('user', default='', nargs='?', + help='user ID or username to get the ' + 'app filter for (default: ' + 'current user)') + parser_check_app_filter.add_argument('arg', + help='path to a program, content ' + 'type or flatpak ref to check') # ‘oars-section’ command parser_oars_section = subparsers.add_parser('oars-section', @@ -396,40 +490,43 @@ def main(): 'user)') parser_oars_section.add_argument('section', help='OARS section to get') - # ‘set’ command - parser_set = subparsers.add_parser('set', parents=[common_parser], - help='set current parental controls ' - 'settings') - parser_set.set_defaults(function=command_set) - parser_set.add_argument('user', default='', nargs='?', - help='user ID or username to get the app filter ' - 'for (default: current user)') - parser_set.add_argument('--allow-user-installation', - dest='allow_user_installation', - action='store_true', - help='allow installation to the user flatpak ' - 'repo in general') - parser_set.add_argument('--disallow-user-installation', - dest='allow_user_installation', - action='store_false', - help='unconditionally disallow installation to ' - 'the user flatpak repo') - parser_set.add_argument('--allow-system-installation', - dest='allow_system_installation', - action='store_true', - help='allow installation to the system flatpak ' - 'repo in general') - parser_set.add_argument('--disallow-system-installation', - dest='allow_system_installation', - action='store_false', - help='unconditionally disallow installation to ' - 'the system flatpak repo') - parser_set.add_argument('app_filter_args', nargs='*', - help='paths, content types or flatpak refs to ' - 'blacklist and OARS section=value ' - 'pairs to store') - parser_set.set_defaults(allow_user_installation=True, - allow_system_installation=False) + # ‘set-app-filter’ command + parser_set_app_filter = \ + subparsers.add_parser('set-app-filter', parents=[common_parser], + help='set current app filter settings') + parser_set_app_filter.set_defaults(function=command_set_app_filter) + parser_set_app_filter.add_argument('user', default='', nargs='?', + help='user ID or username to set the ' + 'app filter for (default: current ' + 'user)') + parser_set_app_filter.add_argument('--allow-user-installation', + dest='allow_user_installation', + action='store_true', + help='allow installation to the user ' + 'flatpak repo in general') + parser_set_app_filter.add_argument('--disallow-user-installation', + dest='allow_user_installation', + action='store_false', + help='unconditionally disallow ' + 'installation to the user flatpak ' + 'repo') + parser_set_app_filter.add_argument('--allow-system-installation', + dest='allow_system_installation', + action='store_true', + help='allow installation to the system ' + 'flatpak repo in general') + parser_set_app_filter.add_argument('--disallow-system-installation', + dest='allow_system_installation', + action='store_false', + help='unconditionally disallow ' + 'installation to the system ' + 'flatpak repo') + parser_set_app_filter.add_argument('app_filter_args', nargs='*', + help='paths, content types or flatpak ' + 'refs to blacklist and OARS ' + 'section=value pairs to store') + parser_set_app_filter.set_defaults(allow_user_installation=True, + allow_system_installation=False) # Parse the command line arguments and run the subcommand. args = parser.parse_args() diff --git a/meson.build b/meson.build index 8bf62d8..1e37a5a 100644 --- a/meson.build +++ b/meson.build @@ -19,12 +19,19 @@ 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')) +libdir = join_paths(prefix, get_option('libdir')) libexecdir = join_paths(prefix, get_option('libexecdir')) # FIXME: This isn’t exposed in accountsservice.pc # See https://gitlab.freedesktop.org/accountsservice/accountsservice/merge_requests/16 accountsserviceinterfacesdir = join_paths(datadir, 'accountsservice', 'interfaces') +# FIXME: pam.pc doesn’t exist +pamlibdir = get_option('pamlibdir') +if pamlibdir == '' + pamlibdir = join_paths(prefix, libdir.split('/')[-1], 'security') +endif + dbus = dependency('dbus-1') dbusinterfacesdir = dbus.get_pkgconfig_variable('interfaces_dir', define_variable: ['datadir', datadir]) @@ -120,4 +127,5 @@ test_env = [ subdir('accounts-service') subdir('malcontent-client') -subdir('libmalcontent') \ No newline at end of file +subdir('libmalcontent') +subdir('pam') diff --git a/meson_options.txt b/meson_options.txt index 96a517d..06329d4 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,4 +3,9 @@ option( type: 'boolean', value: false, description: 'enable installed tests' -) \ No newline at end of file +) +option( + 'pamlibdir', + type: 'string', + description: 'directory for PAM modules' +) diff --git a/pam/meson.build b/pam/meson.build new file mode 100644 index 0000000..8d3992d --- /dev/null +++ b/pam/meson.build @@ -0,0 +1,24 @@ +libpam = cc.find_library('pam', required: true) +libpam_misc = cc.find_library('pam_misc', required: true) + +pam_malcontent = shared_library('pam_malcontent', + files('pam_malcontent.c'), + name_prefix: '', + link_args: [ + '-shared', + '-Wl,--version-script=' + join_paths(meson.current_source_dir(), 'pam_malcontent.sym'), + ], + dependencies: [ + dependency('gio-2.0', version: '>= 2.44'), + dependency('glib-2.0', version: '>= 2.54.2'), + dependency('gobject-2.0', version: '>= 2.54'), + libmalcontent_dep, + libpam, + libpam_misc, + ], + link_depends: files('pam_malcontent.sym'), + include_directories: root_inc, + install: true, + install_dir: pamlibdir) + +subdir('tests') diff --git a/pam/pam_malcontent.c b/pam/pam_malcontent.c new file mode 100644 index 0000000..018b303 --- /dev/null +++ b/pam/pam_malcontent.c @@ -0,0 +1,206 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2019 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" + +#define PAM_SM_ACCOUNT + +#include +#include +#include +#include +#include +#include +#include +#include + + +/* Example usage: + * + * Here’s an example of a PAM file which uses `pam_malcontent.so`. Note + * that `pam_malcontent.so` must be listed before `pam_systemd.so`, and it must + * have type `account`. + * + * ``` + * auth sufficient pam_unix.so nullok try_first_pass + * auth required pam_deny.so + * + * account required pam_nologin.so + * account sufficient pam_unix.so + * account required pam_permit.so + * -account required pam_malcontent.so + * + * password sufficient pam_unix.so nullok sha512 shadow try_first_pass try_authtok + * password required pam_deny.so + * + * -session optional pam_keyinit.so revoke + * -session optional pam_loginuid.so + * -session optional pam_systemd.so + * session sufficient pam_unix.so + * ``` +*/ + +/* @pw_out is (transfer none) (out) (not optional) */ +static int +get_user_data (pam_handle_t *handle, + const char **username_out, + const struct passwd **pw_out) +{ + const char *username = NULL; + struct passwd *pw = NULL; + int r; + + g_return_val_if_fail (handle != NULL, PAM_AUTH_ERR); + g_return_val_if_fail (username_out != NULL, PAM_AUTH_ERR); + g_return_val_if_fail (pw_out != NULL, PAM_AUTH_ERR); + + r = pam_get_user (handle, &username, NULL); + if (r != PAM_SUCCESS) + { + pam_syslog (handle, LOG_ERR, "Failed to get user name."); + return r; + } + + if (username == NULL || *username == '\0') + { + pam_syslog (handle, LOG_ERR, "User name not valid."); + return PAM_AUTH_ERR; + } + + pw = pam_modutil_getpwnam (handle, username); + if (pw == NULL) + { + pam_syslog (handle, LOG_ERR, "Failed to get user data."); + return PAM_USER_UNKNOWN; + } + + *pw_out = pw; + *username_out = username; + + return PAM_SUCCESS; +} + +static void +runtime_max_sec_free (pam_handle_t *handle, + void *data, + int error_status) +{ + g_return_if_fail (data != NULL); + + g_free (data); +} + +PAM_EXTERN int +pam_sm_acct_mgmt (pam_handle_t *handle, + int flags, + int argc, + const char **argv) +{ + int retval; + const char *username = NULL; + const struct passwd *pw = NULL; + g_autoptr(GDBusConnection) connection = NULL; + g_autoptr(MctManager) manager = NULL; + g_autoptr(MctSessionLimits) limits = NULL; + g_autoptr(GError) local_error = NULL; + g_autofree gchar *runtime_max_sec_str = NULL; + guint64 now = g_get_real_time (); + guint64 time_remaining_secs = 0; + gboolean time_limit_enabled = FALSE; + + /* Look up the user data from the handle. */ + retval = get_user_data (handle, &username, &pw); + if (retval != PAM_SUCCESS) + { + /* The error has already been logged. */ + return retval; + } + + if (pw->pw_uid == 0) + { + /* Always allow root, to avoid a situation where this PAM module prevents + * all users logging in with no way of recovery. */ + pam_info (handle, _("User ‘%s’ has no time limits enabled"), "root"); + return PAM_SUCCESS; + } + + /* Connect to the system bus. */ + connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &local_error); + if (connection == NULL) + { + pam_error (handle, + _("Error getting session limits for user ‘%s’: %s"), + username, local_error->message); + return PAM_SERVICE_ERR; + } + + /* Get the time limits on this user’s session usage. */ + manager = mct_manager_new (connection); + limits = mct_manager_get_session_limits (manager, pw->pw_uid, + MCT_MANAGER_GET_VALUE_FLAGS_NONE, + NULL, &local_error); + + if (limits == NULL) + { + if (g_error_matches (local_error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_DISABLED)) + { + return PAM_SUCCESS; + } + else + { + pam_error (handle, + _("Error getting session limits for user ‘%s’: %s"), + username, local_error->message); + return PAM_SERVICE_ERR; + } + } + + /* Check if there’s time left. */ + if (!mct_session_limits_check_time_remaining (limits, now, &time_remaining_secs, + &time_limit_enabled)) + { + pam_error (handle, _("User ‘%s’ has no time remaining"), username); + return PAM_AUTH_ERR; + } + + if (!time_limit_enabled) + { + pam_info (handle, _("User ‘%s’ has no time limits enabled"), username); + return PAM_SUCCESS; + } + + /* Propagate the remaining time to the `pam_systemd.so` module, which will + * end the user’s session when it runs out. */ + runtime_max_sec_str = g_strdup_printf ("%" G_GUINT64_FORMAT, time_remaining_secs); + retval = pam_set_data (handle, "systemd.runtime_max_sec", + g_steal_pointer (&runtime_max_sec_str), runtime_max_sec_free); + + if (retval != PAM_SUCCESS) + { + pam_error (handle, _("Error setting time limit on login session: %s"), + pam_strerror (handle, retval)); + return retval; + } + + return PAM_SUCCESS; +} diff --git a/pam/pam_malcontent.sym b/pam/pam_malcontent.sym new file mode 100644 index 0000000..c1d6cc7 --- /dev/null +++ b/pam/pam_malcontent.sym @@ -0,0 +1,26 @@ +/* + * Copyright © 2019 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 + */ + +{ +global: + pam_sm_acct_mgmt; +local: *; +}; diff --git a/pam/tests/meson.build b/pam/tests/meson.build new file mode 100644 index 0000000..0560dcb --- /dev/null +++ b/pam/tests/meson.build @@ -0,0 +1,48 @@ +deps = [ + dependency('glib-2.0', version: '>= 2.60.0'), + cc.find_library('dl'), +] + +envs = test_env + [ + 'G_TEST_SRCDIR=' + meson.current_source_dir(), + 'G_TEST_BUILDDIR=' + meson.current_build_dir(), +] + +test_programs = [ + ['pam_malcontent', [], deps], +] + +installed_tests_metadir = join_paths(datadir, 'installed-tests', + 'libmalcontent-' + libmalcontent_api_version) +installed_tests_execdir = join_paths(libexecdir, 'installed-tests', + 'libmalcontent-' + libmalcontent_api_version) + +foreach program: test_programs + test_conf = configuration_data() + test_conf.set('installed_tests_dir', installed_tests_execdir) + test_conf.set('program', program[0]) + + configure_file( + input: test_template, + output: program[0] + '.test', + install: enable_installed_tests, + install_dir: installed_tests_metadir, + configuration: test_conf, + ) + + exe = executable( + program[0], + [program[0] + '.c'] + program[1], + dependencies: program[2], + include_directories: root_inc, + install: enable_installed_tests, + install_dir: installed_tests_execdir, + ) + + test( + program[0], + exe, + env: envs, + args: ['--tap'], + ) +endforeach diff --git a/pam/tests/pam_malcontent.c b/pam/tests/pam_malcontent.c new file mode 100644 index 0000000..871516b --- /dev/null +++ b/pam/tests/pam_malcontent.c @@ -0,0 +1,62 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2019 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 +#include +#include +#include + +/* Test that the `pam_malcontent.so` module can be loaded using dlopen() and + * that it exports the appropriate symbols for PAM to be able to use it. */ +static void +test_pam_malcontent_dlopen (void) +{ + g_autofree gchar *module_path = NULL; + void *handle; + int retval; + void *fn; + + module_path = g_test_build_filename (G_TEST_BUILT, "..", "pam_malcontent.so", NULL); + + /* Check the module can be loaded. */ + handle = dlopen (module_path, RTLD_NOW); + g_assert_nonnull (handle); + + /* Check the appropriate symbols exist. */ + fn = dlsym (handle, "pam_sm_acct_mgmt"); + g_assert_nonnull (fn); + + retval = dlclose (handle); + g_assert_cmpint (retval, ==, 0); +} + +int +main (int argc, + char **argv) +{ + setlocale (LC_ALL, ""); + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/pam_malcontent/dlopen", test_pam_malcontent_dlopen); + + return g_test_run (); +} diff --git a/po/POTFILES.in b/po/POTFILES.in index 3c3e360..fd575ff 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,3 +1,5 @@ # List of source files containing translatable strings. # Please keep this file sorted alphabetically. -accounts-service/com.endlessm.ParentalControls.policy \ No newline at end of file +accounts-service/com.endlessm.ParentalControls.policy +libmalcontent/manager.c +pam/pam_malcontent.c