Rename project from eos-parental-controls to malcontent
Rename the library from libeos-parental-controls to libmalcontent, and the client from eos-parental-controls-client to malcontent-client. This was done using the following mechanical edits, and no other changes: ``` git search-replace -f EPC///MCT git search-replace -f Epc///Mct git search-replace -f epc///mct git search-replace -f eos_parental_controls///malcontent git search-replace -f eos-parental-controls///malcontent git search-replace -f EosParentalControls///Malcontent git search-replace -f 'eos\\-parental\\-controls///malcontent' git search-replace -f 'Since: 0.1.0///Since: 0.2.0' ``` Note that the accounts-service extension interface has *not* been renamed, as that would revert people’s parental controls settings in existing deployments. Signed-off-by: Philip Withnall <withnall@endlessm.com>
This commit is contained in:
parent
c9889a3ce7
commit
03436eacf5
15 changed files with 630 additions and 630 deletions
120
malcontent-client/docs/malcontent-client.8
Normal file
120
malcontent-client/docs/malcontent-client.8
Normal file
|
@ -0,0 +1,120 @@
|
|||
.\" Manpage for malcontent\-client.
|
||||
.\" Documentation is under the same licence as the malcontent
|
||||
.\" package.
|
||||
.TH man 8 "03 Oct 2018" "1.0" "malcontent\-client man page"
|
||||
.\"
|
||||
.SH NAME
|
||||
.IX Header "NAME"
|
||||
malcontent\-client — Parental Controls Access Utility
|
||||
.\"
|
||||
.SH SYNOPSIS
|
||||
.IX Header "SYNOPSIS"
|
||||
.\"
|
||||
\fBmalcontent\-client get [\-q] [\-n] [\fPUSER\fB]
|
||||
.PP
|
||||
\fBmalcontent\-client check [\-q] [\-n] [\fPUSER\fB] \fPPATH\fB
|
||||
.\"
|
||||
.SH DESCRIPTION
|
||||
.IX Header "DESCRIPTION"
|
||||
.\"
|
||||
\fBmalcontent\-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"
|
||||
.\"
|
||||
\fBmalcontent\-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"
|
||||
.\"
|
||||
\fBmalcontent\-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 \fBmalcontent\-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.
|
320
malcontent-client/malcontent-client.py
Normal file
320
malcontent-client/malcontent-client.py
Normal file
|
@ -0,0 +1,320 @@
|
|||
#!/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('Malcontent', '0') # noqa
|
||||
from gi.repository import Malcontent, 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."""
|
||||
return Malcontent.get_app_filter(
|
||||
connection=None, user_id=user_id,
|
||||
allow_interactive_authorization=interactive, cancellable=None)
|
||||
|
||||
|
||||
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 __set_app_filter(user_id, app_filter, interactive):
|
||||
"""Set 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."""
|
||||
Malcontent.set_app_filter(
|
||||
connection=None, user_id=user_id, app_filter=app_filter,
|
||||
allow_interactive_authorization=interactive, cancellable=None)
|
||||
|
||||
|
||||
def __set_app_filter_or_error(user_id, app_filter, interactive):
|
||||
"""Wrapper around __set_app_filter() which prints an error and raises
|
||||
SystemExit, rather than an internal exception."""
|
||||
try:
|
||||
__set_app_filter(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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
oars_value_mapping = {
|
||||
Malcontent.AppFilterOarsValue.UNKNOWN: "unknown",
|
||||
Malcontent.AppFilterOarsValue.NONE: "none",
|
||||
Malcontent.AppFilterOarsValue.MILD: "mild",
|
||||
Malcontent.AppFilterOarsValue.MODERATE: "moderate",
|
||||
Malcontent.AppFilterOarsValue.INTENSE: "intense",
|
||||
}
|
||||
|
||||
|
||||
def __oars_value_to_string(value):
|
||||
"""Convert an Malcontent.AppFilterOarsValue to a human-readable
|
||||
string."""
|
||||
try:
|
||||
return oars_value_mapping[value]
|
||||
except KeyError:
|
||||
return "invalid (OARS value {})".format(value)
|
||||
|
||||
|
||||
def __oars_value_from_string(value_str):
|
||||
"""Convert a human-readable string to an
|
||||
Malcontent.AppFilterOarsValue."""
|
||||
for k, v in oars_value_mapping.items():
|
||||
if v == value_str:
|
||||
return k
|
||||
raise KeyError('Unknown OARS value ‘{}’'.format(value_str))
|
||||
|
||||
|
||||
def command_get(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)
|
||||
|
||||
print('App filter for user {} retrieved:'.format(user_id))
|
||||
|
||||
sections = app_filter.get_oars_sections()
|
||||
for section in sections:
|
||||
value = app_filter.get_oars_value(section)
|
||||
print(' {}: {}'.format(section, oars_value_mapping[value]))
|
||||
if not sections:
|
||||
print(' (No OARS values)')
|
||||
|
||||
if app_filter.is_user_installation_allowed():
|
||||
print('App installation is allowed to user repository')
|
||||
else:
|
||||
print('App installation is disallowed to user repository')
|
||||
|
||||
if app_filter.is_system_installation_allowed():
|
||||
print('App installation is allowed to system repository')
|
||||
else:
|
||||
print('App installation is disallowed to system repository')
|
||||
|
||||
|
||||
def command_check(user, path, quiet=False, interactive=True):
|
||||
"""Check the given path or flatpak ref 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)
|
||||
|
||||
if path.startswith('app/') and path.count('/') < 3:
|
||||
# Flatpak app ID
|
||||
path = path[4:]
|
||||
is_allowed = app_filter.is_flatpak_app_allowed(path)
|
||||
noun = 'Flatpak app ID'
|
||||
elif path.startswith('app/') or path.startswith('runtime/'):
|
||||
# Flatpak ref
|
||||
is_allowed = app_filter.is_flatpak_ref_allowed(path)
|
||||
noun = 'Flatpak ref'
|
||||
else:
|
||||
# File system path
|
||||
path = os.path.abspath(path)
|
||||
is_allowed = app_filter.is_path_allowed(path)
|
||||
noun = 'Path'
|
||||
|
||||
if is_allowed:
|
||||
print('{} {} is allowed by app filter for user {}'.format(
|
||||
noun, path, user_id))
|
||||
return
|
||||
else:
|
||||
print('{} {} is not allowed by app filter for user {}'.format(
|
||||
noun, path, user_id))
|
||||
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)
|
||||
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)))
|
||||
|
||||
|
||||
def command_set(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()
|
||||
builder.set_allow_user_installation(allow_user_installation)
|
||||
builder.set_allow_system_installation(allow_system_installation)
|
||||
|
||||
for arg in app_filter_args:
|
||||
if '=' in arg:
|
||||
[section, value_str] = arg.split('=', 2)
|
||||
try:
|
||||
value = __oars_value_from_string(value_str)
|
||||
except KeyError:
|
||||
print('Unknown OARS value ‘{}’'.format(value_str),
|
||||
file=sys.stderr)
|
||||
raise SystemExit(EXIT_INVALID_OPTION)
|
||||
builder.set_oars_value(section, value)
|
||||
elif arg.startswith('app/') or arg.startswith('runtime/'):
|
||||
builder.blacklist_flatpak_ref(arg)
|
||||
else:
|
||||
builder.blacklist_path(arg)
|
||||
app_filter = builder.end()
|
||||
|
||||
__set_app_filter_or_error(user_id, app_filter, interactive)
|
||||
|
||||
print('App filter for user {} set'.format(user_id))
|
||||
|
||||
|
||||
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')
|
||||
|
||||
# ‘oars-section’ command
|
||||
parser_oars_section = subparsers.add_parser('oars-section',
|
||||
parents=[common_parser],
|
||||
help='get the value of a '
|
||||
'given OARS section')
|
||||
parser_oars_section.set_defaults(function=command_oars_section)
|
||||
parser_oars_section.add_argument('user', default='', nargs='?',
|
||||
help='user ID or username to get the '
|
||||
'OARS filter for (default: current '
|
||||
'user)')
|
||||
parser_oars_section.add_argument('section', help='OARS section to get')
|
||||
|
||||
# ‘set’ command
|
||||
parser_set = subparsers.add_parser('set', parents=[common_parser],
|
||||
help='set current parental controls '
|
||||
'settings')
|
||||
parser_set.set_defaults(function=command_set)
|
||||
parser_set.add_argument('user', default='', nargs='?',
|
||||
help='user ID or username to get the app filter '
|
||||
'for (default: current user)')
|
||||
parser_set.add_argument('--allow-user-installation',
|
||||
dest='allow_user_installation',
|
||||
action='store_true',
|
||||
help='allow installation to the user flatpak '
|
||||
'repo in general')
|
||||
parser_set.add_argument('--disallow-user-installation',
|
||||
dest='allow_user_installation',
|
||||
action='store_false',
|
||||
help='unconditionally disallow installation to '
|
||||
'the user flatpak repo')
|
||||
parser_set.add_argument('--allow-system-installation',
|
||||
dest='allow_system_installation',
|
||||
action='store_true',
|
||||
help='allow installation to the system flatpak '
|
||||
'repo in general')
|
||||
parser_set.add_argument('--disallow-system-installation',
|
||||
dest='allow_system_installation',
|
||||
action='store_false',
|
||||
help='unconditionally disallow installation to '
|
||||
'the system flatpak repo')
|
||||
parser_set.add_argument('app_filter_args', nargs='*',
|
||||
help='paths to blacklist and OARS section=value '
|
||||
'pairs to store')
|
||||
parser_set.set_defaults(allow_user_installation=True,
|
||||
allow_system_installation=False)
|
||||
|
||||
# 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()
|
11
malcontent-client/meson.build
Normal file
11
malcontent-client/meson.build
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Python program
|
||||
install_data('malcontent-client.py',
|
||||
install_dir: bindir,
|
||||
install_mode: 'rwxr-xr-x',
|
||||
rename: ['malcontent-client'],
|
||||
)
|
||||
|
||||
# Documentation
|
||||
install_man('docs/malcontent-client.8')
|
||||
|
||||
# TODO subdir('tests')
|
Loading…
Add table
Add a link
Reference in a new issue