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/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 1a1fc32..871223b 100644
--- a/libmalcontent/manager.c
+++ b/libmalcontent/manager.c
@@ -28,8 +28,10 @@
#include
#include
#include
+#include
#include "libmalcontent/app-filter-private.h"
+#include "libmalcontent/session-limits-private.h"
G_DEFINE_QUARK (MctManagerError, mct_manager_error)
@@ -865,4 +867,240 @@ 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);
+}
diff --git a/libmalcontent/manager.h b/libmalcontent/manager.h
index 2402b1a..641c577 100644
--- a/libmalcontent/manager.h
+++ b/libmalcontent/manager.h
@@ -97,6 +97,7 @@ 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)
@@ -135,4 +136,19 @@ 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);
+
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..de390fd
--- /dev/null
+++ b/libmalcontent/session-limits.c
@@ -0,0 +1,191 @@
+/* -*- 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;
+}
diff --git a/libmalcontent/session-limits.h b/libmalcontent/session-limits.h
new file mode 100644
index 0000000..ba29ca0
--- /dev/null
+++ b/libmalcontent/session-limits.h
@@ -0,0 +1,59 @@
+/* -*- 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);
+
+G_END_DECLS