From a7947d56e6c2368d209eb18bfb932782486c81bb Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 4 Dec 2019 15:52:24 +0000 Subject: [PATCH 01/21] malcontent-client: Fix a minor typo in --help output Signed-off-by: Philip Withnall --- malcontent-client/malcontent-client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index 5be8eb5..e0c15d1 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -402,7 +402,7 @@ def main(): '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 ' + help='user ID or username to set the app filter ' 'for (default: current user)') parser_set.add_argument('--allow-user-installation', dest='allow_user_installation', From 2b180a9afd858a7e84ab182b72c1d654bd73ab55 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 4 Dec 2019 15:52:03 +0000 Subject: [PATCH 02/21] =?UTF-8?q?malcontent-client:=20Rename=20=E2=80=98ge?= =?UTF-8?q?t=E2=80=99=20command=20to=20=E2=80=98get-app-filter=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There’ll be other types of getting happening soon. Signed-off-by: Philip Withnall --- malcontent-client/docs/malcontent-client.8 | 8 +++---- malcontent-client/malcontent-client.py | 25 ++++++++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/malcontent-client/docs/malcontent-client.8 b/malcontent-client/docs/malcontent-client.8 index c6bb70b..e61a72b 100644 --- a/malcontent-client/docs/malcontent-client.8 +++ b/malcontent-client/docs/malcontent-client.8 @@ -10,7 +10,7 @@ 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 .\" @@ -25,10 +25,10 @@ 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\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 diff --git a/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index e0c15d1..7b7ac28 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -138,7 +138,7 @@ 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) app_filter = __get_app_filter_or_error(user_id, interactive) @@ -335,8 +335,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) parser.add_argument('-q', '--quiet', action='store_true', help='output no informational messages') parser.set_defaults(quiet=False) @@ -353,14 +354,16 @@ 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)') # ‘monitor’ command parser_monitor = subparsers.add_parser('monitor', From b3dbc07b927d6e44ba8bb9d84756f905fe8f7d37 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Thu, 16 Jan 2020 11:29:05 +0000 Subject: [PATCH 03/21] =?UTF-8?q?malcontent-client:=20Rename=20=E2=80=98se?= =?UTF-8?q?t=E2=80=99=20command=20to=20=E2=80=98set-app-filter=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since it operates only on the app filter. This doesn’t update the documentation because none has been written for this command yet. No compatibility fallback is provided. Signed-off-by: Philip Withnall --- malcontent-client/malcontent-client.py | 78 ++++++++++++++------------ 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index 7b7ac28..261286c 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -279,9 +279,10 @@ def command_oars_section(user, section, quiet=False, interactive=True): section, user_id, __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) builder = Malcontent.AppFilterBuilder.new() @@ -399,40 +400,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 set 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() From 983e3bfa39a4d19478bafcc463ec104d3b67b108 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Thu, 16 Jan 2020 11:00:52 +0000 Subject: [PATCH 04/21] =?UTF-8?q?malcontent-client:=20Rename=20=E2=80=98ch?= =?UTF-8?q?eck=E2=80=99=20command=20to=20=E2=80=98check-app-filter?= =?UTF-8?q?=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since it operates only on the app filter. This updates the documentation too. No compatibility fallback is provided. Signed-off-by: Philip Withnall --- malcontent-client/docs/malcontent-client.8 | 16 ++++++------- malcontent-client/malcontent-client.py | 27 +++++++++++----------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/malcontent-client/docs/malcontent-client.8 b/malcontent-client/docs/malcontent-client.8 index e61a72b..3f765ef 100644 --- a/malcontent-client/docs/malcontent-client.8 +++ b/malcontent-client/docs/malcontent-client.8 @@ -12,7 +12,7 @@ malcontent\-client — Parental Controls Access Utility .\" \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,7 +25,7 @@ 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\-app\-filter\fP and \fBcheck\fP. +are \fBget\-app\-filter\fP and \fBcheck\-app\-filter\fP. .\" .SH \fBget\-app\-filter\fP OPTIONS .IX Header "get\-app\-filter OPTIONS" @@ -43,8 +43,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 +85,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 +99,8 @@ 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. .\" .SH BUGS .IX Header "BUGS" diff --git a/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index 261286c..69899c6 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -213,7 +213,7 @@ 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) @@ -375,18 +375,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', From 305129589e0c03186a240a3e95730d837f1333be Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 15 Jan 2020 12:13:25 +0000 Subject: [PATCH 05/21] malcontent-client: Fix typo in use of flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This doesn’t actually change the behaviour, since the two types are equivalent. Signed-off-by: Philip Withnall --- malcontent-client/malcontent-client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index 69899c6..344d93e 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -66,9 +66,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.SetAppFilterFlags.INTERACTIVE else: - flags = Malcontent.GetAppFilterFlags.NONE + flags = Malcontent.SetAppFilterFlags.NONE connection = Gio.bus_get_sync(Gio.BusType.SYSTEM) manager = Malcontent.Manager.new(connection) From b2ffb160fc34e3bbdb32983ea5fcc23cbc4eeb67 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Thu, 16 Jan 2020 11:28:11 +0000 Subject: [PATCH 06/21] malcontent-client: Print usernames rather than user IDs in output This makes the output a little easier to interpret. Signed-off-by: Philip Withnall --- malcontent-client/malcontent-client.py | 51 ++++++++++++++------------ 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index 344d93e..af32296 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -88,26 +88,31 @@ def __set_app_filter_or_error(user_id, app_filter, interactive): raise SystemExit(EXIT_PERMISSION_DENIED) -def __lookup_user_id(user): - """Convert a command-line specified username or ID into a user ID. If - `user` is empty, use the current user ID. +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) @@ -140,10 +145,10 @@ def __oars_value_from_string(value_str): 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: @@ -166,9 +171,9 @@ def command_get_app_filter(user, quiet=False, interactive=True): 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 +186,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') @@ -216,7 +221,7 @@ def is_valid_content_type(arg): 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,24 +264,24 @@ def command_check_app_filter(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_app_filter(user, allow_user_installation=True, @@ -284,7 +289,7 @@ def command_set_app_filter(user, allow_user_installation=True, 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) @@ -328,7 +333,7 @@ def command_set_app_filter(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(): From c9d5713f83b9b518c3ecb8f5b4d09653706ed6f3 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 6 Dec 2019 16:41:51 +0000 Subject: [PATCH 07/21] po: Add missing file to POTFILES.in This has been translatable for a while. Signed-off-by: Philip Withnall --- po/POTFILES.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/po/POTFILES.in b/po/POTFILES.in index 3c3e360..bc95597 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,3 +1,4 @@ # 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 From 5f2d4046eac2b69a36035cb3bb4b33cb1e7d6567 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 6 Dec 2019 16:44:24 +0000 Subject: [PATCH 08/21] libmalcontent: Fix a minor typo in a comment Signed-off-by: Philip Withnall --- libmalcontent/manager.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libmalcontent/manager.c b/libmalcontent/manager.c index 5f9c76b..57bf78c 100644 --- a/libmalcontent/manager.c +++ b/libmalcontent/manager.c @@ -298,7 +298,7 @@ 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 #MctAppFilterError. */ static GError * bus_error_to_app_filter_error (const GError *bus_error, uid_t user_id) From 282cf9c66b59adc1838b239d19274f54dae2eddb Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 6 Dec 2019 16:45:59 +0000 Subject: [PATCH 09/21] libmalcontent: Set flag-like values for flag types Previously these flags were using automatically assigned enum values, which would have eventually resulted in having more than one bit set per flag. Fix that before it happens by explicitly assigning flag-like values. This was an oversight when they were first written. This introduces no functional changes because both enums only had one element so far. Signed-off-by: Philip Withnall --- libmalcontent/manager.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libmalcontent/manager.h b/libmalcontent/manager.h index 841ba8a..843d014 100644 --- a/libmalcontent/manager.h +++ b/libmalcontent/manager.h @@ -43,7 +43,7 @@ G_BEGIN_DECLS typedef enum { MCT_GET_APP_FILTER_FLAGS_NONE = 0, - MCT_GET_APP_FILTER_FLAGS_INTERACTIVE, + MCT_GET_APP_FILTER_FLAGS_INTERACTIVE = (1 << 0), } MctGetAppFilterFlags; /** @@ -60,7 +60,7 @@ typedef enum typedef enum { MCT_SET_APP_FILTER_FLAGS_NONE = 0, - MCT_SET_APP_FILTER_FLAGS_INTERACTIVE, + MCT_SET_APP_FILTER_FLAGS_INTERACTIVE = (1 << 0), } MctSetAppFilterFlags; #define MCT_TYPE_MANAGER mct_manager_get_type () From acf2738d566e0c77d9eceb0ce4d77b7c27175287 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 11 Dec 2019 15:29:12 +0000 Subject: [PATCH 10/21] libmalcontent: Rename flags types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we have a flag type for getting and for setting every type of value which can be stored on an `MctManager`, that will lead to a load of flag types which all look identical. Refactor the types so we only have one shared flags type for getters, and one for setters. Add compatibility defines so that this doesn’t break API. It’s not an ABI break because the flag member values don’t change. Signed-off-by: Philip Withnall --- libmalcontent/manager.c | 26 ++++++------ libmalcontent/manager.h | 58 +++++++++++++++----------- libmalcontent/tests/app-filter.c | 32 +++++++------- malcontent-client/malcontent-client.py | 8 ++-- 4 files changed, 67 insertions(+), 57 deletions(-) diff --git a/libmalcontent/manager.c b/libmalcontent/manager.c index 57bf78c..467357c 100644 --- a/libmalcontent/manager.c +++ b/libmalcontent/manager.c @@ -373,7 +373,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 +394,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 +407,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 */ @@ -505,7 +505,7 @@ static void get_app_filter_thread_cb (GTask *task, typedef struct { uid_t user_id; - MctGetAppFilterFlags flags; + MctManagerGetValueFlags flags; } GetAppFilterData; static void @@ -536,7 +536,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 +623,7 @@ gboolean mct_manager_set_app_filter (MctManager *self, uid_t user_id, MctAppFilter *app_filter, - MctSetAppFilterFlags flags, + MctManagerSetValueFlags flags, GCancellable *cancellable, GError **error) { @@ -646,7 +646,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 +668,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 */ @@ -691,7 +691,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 */ @@ -714,7 +714,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 */ @@ -737,7 +737,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 */ @@ -761,7 +761,7 @@ typedef struct { uid_t user_id; MctAppFilter *app_filter; /* (owned) */ - MctSetAppFilterFlags flags; + MctManagerSetValueFlags flags; } SetAppFilterData; static void @@ -795,7 +795,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) diff --git a/libmalcontent/manager.h b/libmalcontent/manager.h index 843d014..83e14fa 100644 --- a/libmalcontent/manager.h +++ b/libmalcontent/manager.h @@ -30,38 +30,48 @@ 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 = (1 << 0), -} 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 = (1 << 0), -} 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 #define MCT_TYPE_MANAGER mct_manager_get_type () G_DECLARE_FINAL_TYPE (MctManager, mct_manager, MCT, MANAGER, GObject) @@ -70,12 +80,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 +96,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); diff --git a/libmalcontent/tests/app-filter.c b/libmalcontent/tests/app-filter.c index 5b7d284..119e54a 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. */ @@ -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. */ @@ -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. */ @@ -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. */ @@ -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. */ @@ -1282,7 +1282,7 @@ 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, @@ -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/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index af32296..c9a2e15 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -38,9 +38,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) @@ -66,9 +66,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.SetAppFilterFlags.INTERACTIVE + flags = Malcontent.ManagerSetValueFlags.INTERACTIVE else: - flags = Malcontent.SetAppFilterFlags.NONE + flags = Malcontent.ManagerSetValueFlags.NONE connection = Gio.bus_get_sync(Gio.BusType.SYSTEM) manager = Malcontent.Manager.new(connection) From 300b5a624f8afde8d3ba250f5b6ff8912d0baa50 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 11 Dec 2019 15:46:14 +0000 Subject: [PATCH 11/21] libmalcontent: Move MctAppFilterError to MctManagerError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This isn’t an API break, as compatibility defines are in place; and the error code values are the same, so it shouldn’t be an ABI break. The string value of the error quark has changed, but nobody should be comparing that against a value which hasn’t come out of libmalcontent, so changing it should be OK. This is along the same lines as the previous commit: we don’t need one error domain per property of an `MctManager`, so reduce the potential for future duplication by renaming it now. Signed-off-by: Philip Withnall --- libmalcontent/app-filter.c | 7 ++++- libmalcontent/app-filter.h | 37 ++++++++--------------- libmalcontent/manager.c | 50 +++++++++++++++++--------------- libmalcontent/manager.h | 27 ++++++++++++++++- libmalcontent/tests/app-filter.c | 12 ++++---- 5 files changed, 76 insertions(+), 57 deletions(-) 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/manager.c b/libmalcontent/manager.c index 467357c..61d457e 100644 --- a/libmalcontent/manager.c +++ b/libmalcontent/manager.c @@ -31,6 +31,9 @@ #include "libmalcontent/app-filter-private.h" + +G_DEFINE_QUARK (MctManagerError, mct_manager_error) + /** * MctManager: * @@ -298,19 +301,19 @@ bus_remote_error_matches (const GError *error, return g_str_equal (error_name, expected_error_name); } -/* Convert a #GDBusError into a #MctAppFilterError. */ +/* 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 +349,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; } @@ -415,24 +418,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 +444,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 +464,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; @@ -528,7 +530,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 @@ -676,7 +678,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; } @@ -699,7 +701,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; } @@ -722,7 +724,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; } @@ -745,7 +747,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; } @@ -786,7 +788,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 diff --git a/libmalcontent/manager.h b/libmalcontent/manager.h index 83e14fa..2402b1a 100644 --- a/libmalcontent/manager.h +++ b/libmalcontent/manager.h @@ -25,7 +25,6 @@ #include #include #include -#include G_BEGIN_DECLS @@ -73,6 +72,32 @@ 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 + #define MCT_TYPE_MANAGER mct_manager_get_type () G_DECLARE_FINAL_TYPE (MctManager, mct_manager, MCT, MANAGER, GObject) diff --git a/libmalcontent/tests/app-filter.c b/libmalcontent/tests/app-filter.c index 119e54a..7ce4abc 100644 --- a/libmalcontent/tests/app-filter.c +++ b/libmalcontent/tests/app-filter.c @@ -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); } @@ -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); } @@ -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); } @@ -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); } @@ -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); } @@ -1286,7 +1286,7 @@ test_app_filter_bus_set_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_false (success); } From a54415aa2c34a3e31dba6c621fdb4b441b5764f9 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Thu, 16 Jan 2020 11:20:57 +0000 Subject: [PATCH 12/21] malcontent-client: Improve specificity of exit statuses Add a couple of missing exit statuses (and document them) and convert Malcontent errors to exit statuses more specifically. Signed-off-by: Philip Withnall --- malcontent-client/docs/malcontent-client.8 | 9 +++++++++ malcontent-client/malcontent-client.py | 23 ++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/malcontent-client/docs/malcontent-client.8 b/malcontent-client/docs/malcontent-client.8 index 3f765ef..917adb7 100644 --- a/malcontent-client/docs/malcontent-client.8 +++ b/malcontent-client/docs/malcontent-client.8 @@ -102,6 +102,15 @@ The current user was not authorized to query the app filter 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 c9a2e15..21e0377 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -30,6 +30,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): @@ -57,7 +76,7 @@ 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 __set_app_filter(user_id, app_filter, interactive): @@ -85,7 +104,7 @@ 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_id_or_username): From 9dcaa10253688c4e17971349ad8389c57205091f Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 6 Dec 2019 17:05:54 +0000 Subject: [PATCH 13/21] libmalcontent: Drop an unused variable Signed-off-by: Philip Withnall --- libmalcontent/manager.c | 1 - 1 file changed, 1 deletion(-) diff --git a/libmalcontent/manager.c b/libmalcontent/manager.c index 61d457e..1a1fc32 100644 --- a/libmalcontent/manager.c +++ b/libmalcontent/manager.c @@ -639,7 +639,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); From 5e49cb783136285959644d9fa6f4b863704f5cea Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 6 Dec 2019 16:41:08 +0000 Subject: [PATCH 14/21] libmalcontent: Add a SessionLimits interface for time-limited sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is another extension interface on accountsservice which stores information about time and usage limits on the user session. Currently, only a ‘daily schedule’ limit (or no limit) is supported, but additional types and combinations of limits can be supported in future. The daily schedule limit allows using the computer between a certain start time and end time each day (the same each day). The user will be kicked out of their session when the end time is reached, if they haven’t already logged out. This includes the getters for the new data, polkit rules for accessing it, and some documentation. Changes to `malcontent-client` to support session limits, setters, and unit tests will all follow. Signed-off-by: Philip Withnall --- ...ndlessm.ParentalControls.SessionLimits.xml | 50 ++++ .../com.endlessm.ParentalControls.policy.in | 42 ++- .../com.endlessm.ParentalControls.rules | 4 +- accounts-service/meson.build | 18 +- libmalcontent/malcontent.h | 1 + libmalcontent/manager.c | 240 +++++++++++++++++- libmalcontent/manager.h | 16 ++ libmalcontent/meson.build | 3 + libmalcontent/session-limits-private.h | 62 +++++ libmalcontent/session-limits.c | 191 ++++++++++++++ libmalcontent/session-limits.h | 59 +++++ 11 files changed, 678 insertions(+), 8 deletions(-) create mode 100644 accounts-service/com.endlessm.ParentalControls.SessionLimits.xml create mode 100644 libmalcontent/session-limits-private.h create mode 100644 libmalcontent/session-limits.c create mode 100644 libmalcontent/session-limits.h 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 From ee7ed7dc3598d4e6e9b972dcda0b09577d8abfb2 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 9 Dec 2019 20:37:48 +0000 Subject: [PATCH 15/21] libmalcontent: Add support for setting session limits Signed-off-by: Philip Withnall --- libmalcontent/manager.c | 229 +++++++++++++++++++++++++++++ libmalcontent/manager.h | 17 +++ libmalcontent/session-limits.c | 260 +++++++++++++++++++++++++++++++++ libmalcontent/session-limits.h | 63 ++++++++ 4 files changed, 569 insertions(+) diff --git a/libmalcontent/manager.c b/libmalcontent/manager.c index 871223b..eef42fb 100644 --- a/libmalcontent/manager.c +++ b/libmalcontent/manager.c @@ -1104,3 +1104,232 @@ mct_manager_get_session_limits_finish (MctManager *self, 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 641c577..f1bf1ce 100644 --- a/libmalcontent/manager.h +++ b/libmalcontent/manager.h @@ -151,4 +151,21 @@ MctSessionLimits *mct_manager_get_session_limits_finish (MctManager 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/session-limits.c b/libmalcontent/session-limits.c index de390fd..be4373f 100644 --- a/libmalcontent/session-limits.c +++ b/libmalcontent/session-limits.c @@ -189,3 +189,263 @@ out: 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 index ba29ca0..1ac356c 100644 --- a/libmalcontent/session-limits.h +++ b/libmalcontent/session-limits.h @@ -56,4 +56,67 @@ gboolean mct_session_limits_check_time_remaining (MctSessionLimits *limits, 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 From ec1af3ef5598e475e06ec8530bddcbfd3bc43e06 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 6 Dec 2019 17:06:21 +0000 Subject: [PATCH 16/21] tests: Add tests for SessionLimits interface This adds tests for the getter and setter for session limits, giving us 65.9% branch coverage (but that includes `g_return_if_fail()` and friends, which are impossible and pointless to test both sides of the branch). Signed-off-by: Philip Withnall --- libmalcontent/tests/meson.build | 15 +- libmalcontent/tests/session-limits.c | 1197 ++++++++++++++++++++++++++ 2 files changed, 1210 insertions(+), 2 deletions(-) create mode 100644 libmalcontent/tests/session-limits.c 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 (); +} From c02c56b3b51771fbf19ac0c5e11c52f50e1e96af Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 6 Dec 2019 17:07:19 +0000 Subject: [PATCH 17/21] malcontent-client: Add a `get-session-limits` command Signed-off-by: Philip Withnall --- malcontent-client/malcontent-client.py | 65 ++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index 21e0377..61072cb 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 @@ -79,6 +80,34 @@ def __get_app_filter_or_error(user_id, interactive): 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): """Set the app filter for `user_id` off the bus. @@ -187,6 +216,24 @@ def command_get_app_filter(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 == '': @@ -390,6 +437,24 @@ def main(): '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', help='monitor parental controls ' From e16759e0f7aec31d2ec9bad8f47e7dbc2d72e742 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Thu, 16 Jan 2020 13:26:21 +0000 Subject: [PATCH 18/21] malcontent-client: Fix error when running with no arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default value for the `user` argument wasn’t looked up, since parsing an empty command line doesn’t go through the `parser_get_app_filter` subparser. Signed-off-by: Philip Withnall --- malcontent-client/malcontent-client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malcontent-client/malcontent-client.py b/malcontent-client/malcontent-client.py index 61072cb..2c53018 100644 --- a/malcontent-client/malcontent-client.py +++ b/malcontent-client/malcontent-client.py @@ -409,7 +409,7 @@ def main(): subparsers = parser.add_subparsers(metavar='command', help='command to run (default: ' '‘get-app-filter’)') - parser.set_defaults(function=command_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) From bd7b17ffd450aa9da3c24cd173d070e7861e49f5 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 6 Dec 2019 17:09:57 +0000 Subject: [PATCH 19/21] pam: Add a `pam_malcontent.so` module to enforce time-limited sessions This involves adding a build-time dependency on PAM. Signed-off-by: Philip Withnall --- .gitlab-ci.yml | 2 +- meson.build | 10 +- meson_options.txt | 7 +- pam/meson.build | 22 +++++ pam/pam_malcontent.c | 206 +++++++++++++++++++++++++++++++++++++++++ pam/pam_malcontent.sym | 26 ++++++ po/POTFILES.in | 1 + 7 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 pam/meson.build create mode 100644 pam/pam_malcontent.c create mode 100644 pam/pam_malcontent.sym 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/meson.build b/meson.build index f4a05ba..38d757c 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..f6a516f --- /dev/null +++ b/pam/meson.build @@ -0,0 +1,22 @@ +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) 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/po/POTFILES.in b/po/POTFILES.in index bc95597..fd575ff 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -2,3 +2,4 @@ # Please keep this file sorted alphabetically. accounts-service/com.endlessm.ParentalControls.policy libmalcontent/manager.c +pam/pam_malcontent.c From 876c155efb8aa399ef92ceb258a4aafa6f708cfe Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 6 Dec 2019 17:10:16 +0000 Subject: [PATCH 20/21] tests: Add pam_malcontent.so tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These tests check that the built `pam_malcontent.so` module can be loaded using `dlopen()` and that it exports the right symbol. This should mean that PAM can load it and use it. Unfortunately, we can’t actually run the module, since PAM hard-codes its configuration path as being in `/etc`, and there seems to be no way to override that to load a dummy configuration from a test directory. So the only way to test the PAM module is to use a file system bind mount to fake `/etc` (which requires privileges); or to actually install it on your system and integrate it into your real PAM configuration. Neither of those are acceptable for a unit test. It might be possible to re-execute a test under `bwrap` (if installed) to achieve this, bind mounting a dummy `/etc/pam.d/dummy` service file into the subprocess’ mount namespace, and otherwise bind mounting `/` to `/`. It would need a mock malcontent D-Bus API to talk to. Something to experiment with another time. (See `_pam_init_handlers()` in https://github.com/linux-pam/linux-pam/blob/master/libpam/pam_handlers.c for details of how PAM modules are loaded.) Signed-off-by: Philip Withnall --- pam/meson.build | 2 ++ pam/tests/meson.build | 48 +++++++++++++++++++++++++++++ pam/tests/pam_malcontent.c | 62 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 pam/tests/meson.build create mode 100644 pam/tests/pam_malcontent.c diff --git a/pam/meson.build b/pam/meson.build index f6a516f..8d3992d 100644 --- a/pam/meson.build +++ b/pam/meson.build @@ -20,3 +20,5 @@ pam_malcontent = shared_library('pam_malcontent', include_directories: root_inc, install: true, install_dir: pamlibdir) + +subdir('tests') 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 (); +} From 0364346a4c5727dc425357b4aece8e9b1ddeb202 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 17 Jan 2020 11:24:32 +0000 Subject: [PATCH 21/21] docs: Mention that malcontent-client command line API is unstable It might be stable one day, but while the functionality of libmalcontent is growing, the command line tooling will continue to change. Signed-off-by: Philip Withnall --- malcontent-client/docs/malcontent-client.8 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/malcontent-client/docs/malcontent-client.8 b/malcontent-client/docs/malcontent-client.8 index 917adb7..cfc3c6d 100644 --- a/malcontent-client/docs/malcontent-client.8 +++ b/malcontent-client/docs/malcontent-client.8 @@ -26,6 +26,9 @@ 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\-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\-app\-filter\fP OPTIONS .IX Header "get\-app\-filter OPTIONS"