diff --git a/eos-parental-controls-client/docs/eos-parental-controls-client.8 b/eos-parental-controls-client/docs/eos-parental-controls-client.8 new file mode 100644 index 0000000..99910b5 --- /dev/null +++ b/eos-parental-controls-client/docs/eos-parental-controls-client.8 @@ -0,0 +1,120 @@ +.\" Manpage for eos\-parental\-controls\-client. +.\" Documentation is under the same licence as the eos\-parental\-controls +.\" package. +.TH man 8 "03 Oct 2018" "1.0" "eos\-parental\-controls\-client man page" +.\" +.SH NAME +.IX Header "NAME" +eos\-parental\-controls\-client — Parental Controls Access Utility +.\" +.SH SYNOPSIS +.IX Header "SYNOPSIS" +.\" +\fBeos\-parental\-controls\-client get [\-q] [\-n] [\fPUSER\fB] +.PP +\fBeos\-parental\-controls\-client check [\-q] [\-n] [\fPUSER\fB] \fPPATH\fB +.\" +.SH DESCRIPTION +.IX Header "DESCRIPTION" +.\" +\fBeos\-parental\-controls\-client\fP is a utility for querying and updating the +parental controls settings for users on the system. It will typically require +adminstrator access to do anything more than query the current user’s parental +controls. +.PP +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. +.\" +.SH \fBget\fP OPTIONS +.IX Header "get OPTIONS" +.\" +.IP "\fBUSER\fP" +Username or ID of the user to get the app filter for. If not specified, the +current user will be used by default. +.\" +.IP "\fB\-q\fP, \fB\-\-quiet\fP" +Only output error messages, and no informational messages, as the operation +progresses. (Default: Output informational messages.) +.\" +.IP "\fB\-n\fP, \fB\-\-no\-interactive\fP" +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" +.\" +.IP "\fBUSER\fP" +Username or ID of the user to get the app filter for. If not specified, the +current user will be used by default. +.\" +.IP "\fBPATH\fP" +Path to a program to check against the app filter, to see if it can be run by +the specified user. +.\" +.IP "\fB\-q\fP, \fB\-\-quiet\fP" +Only output error messages, and no informational messages, as the operation +progresses. (Default: Output informational messages.) +.\" +.IP "\fB\-n\fP, \fB\-\-no\-interactive\fP" +Do not allow interactive authorization with polkit. If this is needed to +complete the operation, the operation will fail. (Default: Allow interactive +authorization.) +.\" +.SH "ENVIRONMENT" +.IX Header "ENVIRONMENT" +.\" +\fBeos\-parental\-controls\-client\fP supports the standard GLib environment +variables for debugging. These variables are \fBnot\fP intended to be used in +production: +.\" +.IP \fI$G_MESSAGES_DEBUG\fP 4 +.IX Item "$G_MESSAGES_DEBUG" +This variable can contain one or more debug domain names to display debug output +for. The value \fIall\fP will enable all debug output. The default is for no +debug output to be enabled. +.\" +.SH "EXIT STATUS" +.IX Header "EXIT STATUS" +.\" +\fBeos\-parental\-controls\-client\fP may return one of several error codes if it +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 was allowed to be run by the +given user. +.\" +.IP "1" 4 +.IX Item "1" +An invalid option was passed to \fBeos\-parental\-controls\-client\fP on +startup. +.\" +.IP "2" 4 +.IX Item "2" +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 was \fInot\fP allowed to be +run by the given user. +.\" +.SH BUGS +.IX Header "BUGS" +.\" +Any bugs which are found should be reported on the project website: +.br +\fIhttps://support.endlessm.com/\fP +.\" +.SH AUTHOR +.IX Header "AUTHOR" +.\" +Endless Mobile, Inc. +.\" +.SH COPYRIGHT +.IX Header "COPYRIGHT" +.\" +Copyright © 2018 Endless Mobile, Inc. diff --git a/eos-parental-controls-client/eos-parental-controls-client.py b/eos-parental-controls-client/eos-parental-controls-client.py new file mode 100644 index 0000000..4436513 --- /dev/null +++ b/eos-parental-controls-client/eos-parental-controls-client.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © 2018 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 + +import argparse +import os +import pwd +import sys +import gi +gi.require_version('EosParentalControls', '0') # noqa +from gi.repository import EosParentalControls, GLib + + +# Exit codes, which are a documented part of the API. +EXIT_SUCCESS = 0 +EXIT_INVALID_OPTION = 1 +EXIT_PERMISSION_DENIED = 2 +EXIT_PATH_NOT_ALLOWED = 3 + + +def __get_app_filter(user_id, interactive): + """Get the app filter for `user_id` off the bus. + + If `interactive` is `True`, interactive polkit authorisation dialogues will + be allowed. An exception will be raised on failure.""" + app_filter = None + exception = None + + def __get_cb(obj, result, user_data): + nonlocal app_filter, exception + try: + app_filter = EosParentalControls.get_app_filter_finish(result) + except Exception as e: + exception = e + + EosParentalControls.get_app_filter_async( + connection=None, user_id=user_id, + allow_interactive_authorization=interactive, cancellable=None, + callback=__get_cb, user_data=None) + + context = GLib.MainContext.default() + while not app_filter and not exception: + context.iteration(True) + + if exception: + raise exception + return app_filter + + +def __get_app_filter_or_error(user_id, interactive): + """Wrapper around __get_app_filter() which prints an error and raises + SystemExit, rather than an internal exception.""" + try: + return __get_app_filter(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) + + +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. + + Raise KeyError if lookup fails.""" + if user == '': + return os.getuid() + elif user.isdigit(): + return int(user) + else: + return pwd.getpwnam(user).pw_uid + + +def __lookup_user_id_or_error(user): + """Wrapper around __lookup_user_id() which prints an error and raises + SystemExit, rather than an internal exception.""" + try: + return __lookup_user_id(user) + except KeyError: + print('Error getting ID for username {}'.format(user), file=sys.stderr) + raise SystemExit(EXIT_INVALID_OPTION) + + +def command_get(user, quiet=False, interactive=True): + """Get the app filter for the given user.""" + user_id = __lookup_user_id_or_error(user) + __get_app_filter_or_error(user_id, interactive) + + print('App filter for user {} retrieved'.format(user_id)) + + +def command_check(user, path, quiet=False, interactive=True): + """Check the given path is runnable by the given user, according to their + app filter.""" + user_id = __lookup_user_id_or_error(user) + app_filter = __get_app_filter_or_error(user_id, interactive) + + path = os.path.abspath(path) + + if app_filter.is_path_allowed(path): + print('Path {} is allowed by app filter for user {}'.format( + path, user_id)) + return + else: + print('Path {} is not allowed by app filter for user {}'.format( + path, user_id)) + raise SystemExit(EXIT_PATH_NOT_ALLOWED) + + +def main(): + # Parse command line arguments + 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) + parser.add_argument('-q', '--quiet', action='store_true', + help='output no informational messages') + parser.set_defaults(quiet=False) + + # Common options for the subcommands which might need authorisation. + common_parser = argparse.ArgumentParser(add_help=False) + group = common_parser.add_mutually_exclusive_group() + group.add_argument('-n', '--no-interactive', dest='interactive', + action='store_false', + help='do not allow interactive polkit authorization ' + 'dialogues') + group.add_argument('--interactive', dest='interactive', + action='store_true', + 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)') + + # ‘check’ command + parser_check = subparsers.add_parser('check', parents=[common_parser], + help='check whether a path 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('path', + help='path to a program to check') + + # Parse the command line arguments and run the subcommand. + args = parser.parse_args() + args_dict = dict((k, v) for k, v in vars(args).items() if k != 'function') + args.function(**args_dict) + + +if __name__ == '__main__': + main() diff --git a/eos-parental-controls-client/meson.build b/eos-parental-controls-client/meson.build new file mode 100644 index 0000000..c06732a --- /dev/null +++ b/eos-parental-controls-client/meson.build @@ -0,0 +1,11 @@ +# Python program +install_data('eos-parental-controls-client.py', + install_dir: bindir, + install_mode: 'rwxr-xr-x', + rename: ['eos-parental-controls-client'], +) + +# Documentation +install_man('docs/eos-parental-controls-client.8') + +# TODO subdir('tests') \ No newline at end of file diff --git a/meson.build b/meson.build index 0bd1ef2..daeb3b2 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,7 @@ meson_make_symlink = join_paths(meson.source_root(), 'tools', 'meson-make-symlin 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')) # FIXME: This isn’t exposed in accountsservice.pc @@ -103,4 +104,5 @@ cc = meson.get_compiler('c') add_project_arguments(cc.get_supported_arguments(test_c_args), language: 'c') subdir('accounts-service') +subdir('eos-parental-controls-client') subdir('libeos-parental-controls') \ No newline at end of file