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