aa-notify: Enhanced Graphical User Interfaces

This commit is contained in:
Maxime Bélair 2024-08-13 16:58:25 +00:00 committed by John Johansen
parent d9e9bb1a55
commit ff1baf3851
12 changed files with 626 additions and 14 deletions

View file

@ -77,7 +77,8 @@ test-utils:
extends:
- .ubuntu-before_script
script:
- apt-get install --no-install-recommends -y libc6-dev libjs-jquery libjs-jquery-throttle-debounce libjs-jquery-isonscreen libjs-jquery-tablesorter flake8 python3-coverage python3-notify2 python3-psutil python3-setuptools
- apt-get install --no-install-recommends -y libc6-dev libjs-jquery libjs-jquery-throttle-debounce libjs-jquery-isonscreen libjs-jquery-tablesorter flake8 python3-coverage python3-notify2 python3-psutil python3-setuptools python3-tk python3-ttkthemes python3-gi
# See apparmor/apparmor#221
- make -C parser/tst gen_dbus
- make -C parser/tst gen_xtrans

View file

@ -354,6 +354,9 @@ The aa-notify tool's Python dependencies can be satisfied by installing the
following packages (Debian package names, other distros may vary):
* python3-notify2
* python3-psutil
* python3-tk
* python3-ttkthemes
* python3-gi
Perl is no longer needed since none of the utilities shipped to end users depend
on it anymore.

View file

@ -51,7 +51,7 @@ po/${NAME}.pot: ${TOOLS} ${PYMODULES}
.PHONY: install
install: ${MANPAGES} ${HTMLMANPAGES}
install -d ${CONFDIR}
install -m 644 logprof.conf severity.db notify.conf ${CONFDIR}
install -m 644 logprof.conf severity.db notify.conf default_unconfined.template ${CONFDIR}
install -d ${BINDIR}
# aa-easyprof is installed by python-tools-setup.py
install -m 755 $(filter-out aa-easyprof, ${TOOLS}) ${BINDIR}

View file

@ -39,17 +39,29 @@ import re
import sys
import time
import subprocess
import notify2
import psutil
import apparmor.aa as aa
import apparmor.ui as aaui
import apparmor.config as aaconfig
import apparmor.update_profile as update_profile
import LibAppArmor # C-library to parse one log line
from apparmor.common import DebugLogger, open_file_read
from apparmor.common import DebugLogger, open_file_read, AppArmorException
from apparmor.fail import enable_aa_exception_handler
from apparmor.notify import get_last_login_timestamp
from apparmor.translations import init_translation
from apparmor.logparser import ReadLog
from apparmor.gui import UsernsGUI, AddProfileGUI, ErrorGUI, ShowMoreGUI, set_interface_theme
from dbus import DBusException
import gi
from gi.repository import GLib
import threading
gi.require_version('GLib', '2.0')
def get_user_login():
@ -124,11 +136,23 @@ def notify_about_new_entries(logfile, filters, wait=0):
# Follow the logfile and stream notifications
# Rate limit to not show too many notifications
try:
# Before use, notify2 must be initialized and the DBUS channel
# should be opened using the non-root user. This step needs to
# be executed after the drop_privileges().
notify2.init('aa-notify', mainloop='glib')
except DBusException:
sys.exit(_('Cannot initialize notify2. Please check that your terminal can use a graphical interface'))
try:
thread = threading.Thread(target=start_glib_loop)
thread.daemon = True
thread.start()
for event in follow_apparmor_events(logfile, wait):
if not is_event_in_filter(event, filters):
continue
debug_logger.info(format_event(event, logfile))
yield (format_event(event, logfile))
yield event, format_event(event, logfile)
except PermissionError:
sys.exit(_("ERROR: Cannot read {}. Please check permissions.").format(logfile))
@ -396,6 +420,151 @@ def compile_filter_regex(filters):
return filters
def is_special_profile_userns(ev, special_profiles):
if not special_profiles or ev['profile'] not in special_profiles:
return False # We don't use special profiles or there is already a profile defined: we don't ask to add userns
if 'execpath' not in ev or not ev['execpath']:
ev['execpath'] = aa.find_executable(ev['comm'])
return True
def ask_for_user_ns_denied(path, name, profile_path):
gui = UsernsGUI(name, path)
ans = gui.show()
if ans in {'allow', 'deny'}:
update_profile_path = update_profile.__file__
local_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_unconfined.template')
if os.path.exists(local_template_path): # We are using local aa-notify -> we use local template
template_path = local_template_path
else:
template_path = aa.CONFDIR + '/default_unconfined.template'
command = ['pkexec', '--keep-cwd', update_profile_path, 'create_userns', template_path, name, path, profile_path, ans]
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
if e.returncode != 126: # return code 126 means the user cancelled the request
UsernsGUI.show_error_cannot_reload_profile(profile_path, e.returncode)
else:
debug_logger.debug('No action from the user for {}'.format(path))
def prompt_userns(ev, special_profiles):
"""If the user namespace creation denial was generated by an unconfined binary, displays a graphical notification.
Creates a new profile to allow userns if the user wants it. Returns whether a notification was displayed to the user
"""
if not is_special_profile_userns(ev, special_profiles):
return False
if ev['execpath'] is None:
UsernsGUI.show_error_cannot_find_execpath(ev['comm'], os.path.dirname(os.path.abspath(__file__)) + '/default_unconfined.template')
return True
aa.update_profiles()
if aa.get_profile_filename_from_profile_name(ev['comm']):
# There is already a profile with this name: we show an error to the user.
# We could use the full path as profile name like for the old profiles if we want to handle this case
# but if execpath is not supported by the kernel it could also mean that we inferred a bad path
# So we do nothing beyond showing this error.
ErrorGUI(
_('Application {0} tried to create an user namespace, but a profile already exists with this name.\n'
'This is likely because there is several binaries named {0} thus the path inferred by AppArmor ({1}) is not correct.\n'
'You should review your profiles (in {2}).').format(ev['comm'], ev['execpath'], aa.profile_dir),
False).show()
return True
profile_path = aa.get_profile_filename_from_profile_name(ev['comm'], True)
if not profile_path:
return False
ask_for_user_ns_denied(ev['execpath'], ev['comm'], profile_path)
return True
# TODO reuse more code from aa-logprof in callbacks
def cb_more_info(notification, action, _args):
(raw_ev, rl) = _args
notification.close()
parsed_event = rl.parse_record(raw_ev)
out = _('Operation denied by AppArmor\n\n')
for key, value in parsed_event.items():
if value:
out += '\t{} = {}\n'.format(_(key), value)
out += _('\nThe software that declined this operation is {}\n').format(parsed_event['profile'])
rule = rl.create_rule_from_ev(parsed_event)
if rule:
aa.update_profiles()
profile_path = aa.get_profile_filename_from_profile_name(parsed_event['profile'])
if profile_path:
out += _('If you want to allow this operation you can add the line below in profile {}\n').format(profile_path)
else:
out += _('However {0} is not in {1}\nIt is likely that the profile was not stored in {1} or was removed.\n').format(parsed_event['profile'], aa.profile_dir)
out += rule.get_clean()
else: # Should not happen
out += _('ERROR: Could not create rule from event.')
return
ShowMoreGUI(profile_path, out, profile_path is not None).show()
def cb_add_to_profile(notification, action, _args):
(raw_ev, rl, special_profiles) = _args
notification.close()
parsed_event = rl.parse_record(raw_ev)
rule = rl.create_rule_from_ev(parsed_event)
if not rule:
ErrorGUI.show(_('ERROR: Could not create rule from event.'))
return
aa.update_profiles()
if customized_message['userns']['cond'](parsed_event, special_profiles):
if parsed_event['execpath'] is None:
UsernsGUI.show_error_cannot_find_execpath(parsed_event['comm'], os.path.dirname(os.path.abspath(__file__)) + '/default_unconfined.template')
return
profile_path = aa.get_profile_filename_from_profile_name(parsed_event['comm'], True)
if not profile_path:
ErrorGUI(_('Cannot get profile path for {}.').format(parsed_event['comm']), False).show()
return
ask_for_user_ns_denied(parsed_event['execpath'], parsed_event['comm'], profile_path)
else:
try:
AddProfileGUI(rule.get_clean(), parsed_event['profile']).show()
except AppArmorException:
AddProfileGUI.show_error_cannot_find_profile(parsed_event['profile'])
customized_message = {
'userns': {
'cond': lambda ev, special_profiles: (ev['operation'] == 'userns_create' or ev['operation'] == 'capable') and is_special_profile_userns(ev, special_profiles),
'msg': 'Application {0} wants to create an user namespace which could be used to compromise your system\nDo you want to allow it next time {0} is run?'
}
}
def customize_notification_message(ev, msg, special_profiles):
if customized_message['userns']['cond'](ev, special_profiles):
msg = _(customized_message['userns']['msg']).format(ev['comm'])
return msg
def start_glib_loop():
loop = GLib.MainLoop()
loop.run()
def main():
"""Run aa-notify.
@ -432,6 +601,7 @@ def main():
parser.add_argument('-v', '--verbose', action='store_true', help=_('show messages with stats'))
parser.add_argument('-u', '--user', type=str, help=_('user to drop privileges to when not using sudo'))
parser.add_argument('-w', '--wait', type=int, metavar=('NUM'), help=_('wait NUM seconds before displaying notifications (with -p)'))
parser.add_argument('--prompt-filter', type=str, metavar=('PF'), help=_('kind of operations which display a popup prompt'))
parser.add_argument('--debug', action='store_true', help=_('debug mode'))
parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
@ -476,6 +646,15 @@ def main():
else: # fallback to env variable (or None if not set)
confdir = os.getenv('__AA_CONFDIR')
# Todo: add more kinds of notifications
supported_prompt_filter = {'userns'}
if args.prompt_filter:
args.prompt_filter = set(args.prompt_filter.strip().split(','))
unsupported = args.prompt_filter - supported_prompt_filter
if unsupported:
sys.exit(_('ERROR: using an unsupported prompt filter: {}\nSupported values: {}').format(', '.join(unsupported), ', '.join(supported_prompt_filter)))
aa.init_aa(confdir=confdir)
# Initialize aa.logfile
@ -511,6 +690,9 @@ def main():
- message_body
- message_footer
- use_group
- userns_special_profiles
- ignore_denied_capability
- interface_theme
- filter.profile,
- filter.operation,
- filter.name,
@ -524,6 +706,9 @@ def main():
# Warn about unknown keys in the config
allowed_config_keys = [
'use_group',
'userns_special_profiles',
'ignore_denied_capability',
'interface_theme',
'show_notifications',
'message_body',
'message_footer',
@ -598,6 +783,21 @@ def main():
# @TODO: Extend UI lib to have warning and error functions that
# can be used in an uniform way with both text and JSON output.
if 'userns_special_profiles' in config['']:
userns_special_profiles = config['']['userns_special_profiles'].strip().split(',')
else:
userns_special_profiles = ['unconfined'] # By default, unconfined is the only special profile
if 'ignore_denied_capability' in config['']:
ignore_denied_capability = config['']['ignore_denied_capability'].strip().split(',')
else:
ignore_denied_capability = ['sudo', 'su']
if 'interface_theme' in config['']:
set_interface_theme(config['']['interface_theme'].strip())
else:
set_interface_theme('ubuntu')
if args.file:
logfile = args.file
elif os.path.isfile('/var/run/auditd.pid') and os.path.isfile('/var/log/audit/audit.log'):
@ -628,12 +828,26 @@ def main():
if not args.user and os.getuid() == 0 and 'SUDO_USER' not in os.environ.keys():
sys.exit("ERROR: Cannot be started a real root user. Use --user to define what user to use.")
# Required to parse_record.
rl = ReadLog('', '', '')
# At this point this script needs to be able to read 'logfile' but once
# the for loop starts, privileges can be dropped since the file descriptor
# has been opened and access granted. Further reads of the file will not
# trigger any new permission checks.
# @TODO Plan to catch PermissionError here or..?
for message in notify_about_new_entries(logfile, filters, args.wait):
for (event, message) in notify_about_new_entries(logfile, filters, args.wait):
ev = rl.parse_record(event)
# @TODO redo special behaviours with a more regular function
# We ignore capability denials for binaries in ignore_denied_capability
if ev['operation'] == 'capable' and ev['comm'] in ignore_denied_capability:
continue
# Special behaivor for userns:
if args.prompt_filter and 'userns' in args.prompt_filter and customized_message['userns']['cond'](ev, userns_special_profiles):
if prompt_userns(ev, userns_special_profiles):
continue # Notification already displayed for this event, we go to the next one.
# Notifications should not be run as root, since root probably is
# the wrong desktop user and not the one getting the notifications.
@ -643,16 +857,17 @@ def main():
if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
os.environ['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=/run/user/{}/bus'.format(os.geteuid())
# Before use, notify2 must be initialized and the DBUS channel
# should be opened using the non-root user. This step needs to
# be executed after the drop_privileges().
notify2.init('AppArmor')
message = customize_notification_message(ev, message, userns_special_profiles)
n = notify2.Notification(
_('AppArmor notification'),
_('AppArmor security notice'),
message,
'gtk-dialog-warning'
)
n.add_action('clicked', 'Allow', cb_add_to_profile, (event, rl, userns_special_profiles))
n.add_action('more_clicked', 'Show More', cb_more_info, (event, rl))
n.show()
# When notification is sent, raise privileged back to root if the

View file

@ -1607,6 +1607,14 @@ def collapse_log(hashlog, ignore_null_profiles=True):
return log_dict
def update_profiles(ui_msg=False, skip_profiles=()):
reset_aa()
try:
read_profiles(ui_msg, skip_profiles)
except AppArmorException as e:
print(_("Error while loading profiles: {}").format(e))
def read_profiles(ui_msg=False, skip_profiles=()):
# we'll read all profiles from disk, so reset the storage first (autodep() might have created/stored
# a profile already, which would cause a 'Conflicting profile' error in attach_profile_data())

233
utils/apparmor/gui.py Normal file
View file

@ -0,0 +1,233 @@
import os
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.messagebox as messagebox
import tkinter.font as font
import subprocess
import ttkthemes
import apparmor.aa as aa
import apparmor.update_profile as update_profile
from apparmor.common import AppArmorException
from apparmor.translations import init_translation
_ = init_translation()
notification_custom_msg = {
'userns': _('Application {0} wants to create an user namespace which could be used to compromise your system\nDo you want to allow it next time {0} is run?\nApplication path is {1}')
}
global interface_theme
class GUI:
def __init__(self):
try:
self.master = tk.Tk()
except tk.TclError:
print(_('ERROR: Cannot initialize Tkinter. Please check that your terminal can use a graphical interface'))
os._exit(1)
style = ttkthemes.ThemedStyle(self.master)
style.theme_use(interface_theme)
self.bg_color = style.lookup('TLabel', 'background')
self.master.configure(background=self.bg_color)
self.label_frame = ttk.Frame(self.master, padding=(20, 10))
self.label_frame.pack()
self.button_frame = ttk.Frame(self.master, padding=(0, 10))
self.button_frame.pack()
class AddProfileGUI(GUI):
def __init__(self, rule, profile_name, dbg=None):
self.dbg = dbg
self.rule = rule
self.profile_name = profile_name
self.profile_path = aa.get_profile_filename_from_profile_name(profile_name)
if not self.profile_path:
raise AppArmorException('Cannot find profile for {}'.format(self.profile_name))
super().__init__()
self.master.title(_('AppArmor - Add rule to profile'))
self.profile_label = ttk.Label(self.label_frame, text=_('Profile for: {}').format(self.profile_name))
self.profile_label.pack()
self.entry_frame = ttk.Frame(self.master)
self.entry_frame.pack()
self.rule = tk.StringVar(value=self.rule)
self.rule_entry = ttk.Entry(self.entry_frame, font=font.nametofont("TkDefaultFont"), width=50, textvariable=self.rule)
self.rule_entry.pack(side=tk.LEFT)
self.button_frame = ttk.Frame(self.master, padding=(10, 10))
self.button_frame.pack()
self.add_to_profile_button = ttk.Button(self.button_frame, text=_('Add to Profile'), command=self.add_to_profile)
self.add_to_profile_button.pack(side=tk.LEFT)
self.show_profile_button = ttk.Button(self.button_frame, text=_('Show Current Profile'), command=lambda: open_with_default_editor(self.profile_path))
self.show_profile_button.pack(side=tk.LEFT)
self.cancel_button = ttk.Button(self.button_frame, text=_('Cancel'), command=self.master.destroy)
self.cancel_button.pack(side=tk.LEFT)
def add_to_profile(self):
if self.dbg:
self.dbg.debug('Adding rule \'{}\' to profile at: {}'.format(self.rule.get(), self.profile_name))
# We get update_profile.py through this import so that it works in all cases
update_profile_path = update_profile.__file__
command = ['pkexec', '--keep-cwd', update_profile_path, 'add_rule', self.rule.get(), self.profile_name]
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
if e.returncode != 126: # return code 126 means the user cancelled the request
ErrorGUI(_('Failed to add rule {} to {}\n Error code = {}').format(self.rule.get(), self.profile_name, e.returncode), False).show()
self.master.destroy()
def show(self):
self.master.mainloop()
@staticmethod
def show_error_cannot_find_profile(profile_name):
ErrorGUI(
_(
'Cannot find profile for {}\n\n'
'It is likely that the profile was not stored in {} or was removed.'
).format(profile_name, aa.profile_dir),
False
).show()
class ShowMoreGUI(GUI):
def __init__(self, profile_path, msg, profile_found=True):
self.profile_path = profile_path
self.msg = msg
self.profile_found = profile_found
super().__init__()
self.master.title(_('AppArmor - More info'))
self.label = tk.Label(self.label_frame, background=self.bg_color, text=self.msg, anchor='w', justify=tk.LEFT, wraplength=460)
self.label.pack()
if self.profile_found:
self.show_profile_button = ttk.Button(self.button_frame, text=_('Show Current Profile'), command=lambda: open_with_default_editor(self.profile_path))
self.show_profile_button.pack()
def show(self):
self.master.mainloop()
class UsernsGUI(GUI):
def __init__(self, name, path):
self.name = name
self.path = path
self.result = None
super().__init__()
self.master.title(_('AppArmor - User namespace creation restricted'))
label_text = notification_custom_msg['userns'].format(name, path)
self.label = ttk.Label(self.label_frame, text=label_text, wraplength=460)
self.label.pack()
link = ttk.Label(self.master, text=_('More information'), foreground='blue', cursor='hand2')
link.pack()
link.bind('<Button-1>', self.more_info)
self.add_policy_button = ttk.Button(self.master, text=_('Allow'), command=lambda: self.set_result("allow"))
self.add_policy_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
self.never_ask_button = ttk.Button(self.master, text=_('Deny'), command=lambda: self.set_result("deny"))
self.never_ask_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
self.do_nothing_button = ttk.Button(self.master, text=_('Do nothing'), command=self.master.destroy)
self.do_nothing_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
@staticmethod
def more_info(ev):
more_info_text = _("""
In Linux, user namespaces enable non-root users to perform certain privileged operations. This feature can be useful for several legitimate use cases.
However, this feature also introduces security risks, (e.g. privilege escalation exploits).
This dialog allows you to choose whether you want to enable user namespaces for this application.""")
messagebox.showinfo(
_('AppArmor: More info'),
more_info_text,
)
def set_result(self, result):
self.result = result
self.master.destroy()
def show(self):
self.master.mainloop()
return self.result
@staticmethod
def show_error_cannot_reload_profile(profile_path, error):
ErrorGUI(_('Failed to create or load profile {}\n Error code = {}').format(profile_path, error), False).show()
@staticmethod
def show_error_cannot_find_execpath(name, template_path):
ErrorGUI(
_(
'Application {0} wants to create an user namespace which could be used to compromise your system\n\n'
'However, apparmor cannot find {0}. If you want to allow it, please create a profile for it.\n\n'
'A profile template is in {1}\n Profiles are in {2}'
).format(name, template_path, aa.profile_dir),
False
).show()
class ErrorGUI(GUI):
def __init__(self, msg, is_fatal):
self.msg = msg
self.is_fatal = is_fatal
super().__init__()
self.master.title('AppArmor Error')
# Create label to display the error message
self.label = ttk.Label(self.label_frame, background=self.bg_color, text=self.msg, wraplength=460)
self.label.pack()
# Create a button to close the dialog
self.button = ttk.Button(self.button_frame, text="OK", command=self.destroy)
self.button.pack()
def destroy(self):
self.master.destroy()
if self.is_fatal:
os._exit(1)
def show(self):
self.master.mainloop()
if self.is_fatal:
os._exit(1)
def set_interface_theme(theme):
global interface_theme
interface_theme = theme
def open_with_default_editor(profile_path):
try:
default_app = subprocess.run(['xdg-mime', 'query', 'default', 'text/plain'], capture_output=True, text=True, check=True).stdout.strip()
subprocess.run(['gtk-launch', default_app, profile_path], check=True)
except subprocess.CalledProcessError:
ErrorGUI(_('Failed to launch default editor'), False).show()
except FileNotFoundError as e:
ErrorGUI(_('Failed to open file: {}').format(e), False).show()

View file

@ -0,0 +1,80 @@
#!/usr/bin/python3
import subprocess
import sys
# TODO: transform this script to a package to use local imports so that if called with ./aa-notify, we use ./apparmor.*
from apparmor import aa
from apparmor.logparser import ReadLog
from apparmor.translations import init_translation
_ = init_translation()
def create_userns(template_path, name, bin_path, profile_path, decision):
with open(template_path, 'r') as f:
profile_template = f.read()
rule = 'userns' if decision == 'allow' else 'audit deny userns'
profile = profile_template.format(rule=rule, name=name, path=bin_path)
with open(profile_path, 'w') as file:
file.write(profile)
try:
subprocess.run(['apparmor_parser', '-r', profile_path], check=True)
except subprocess.CalledProcessError:
exit(_('Cannot reload updated profile'))
def add_to_profile(rule, profile_name):
aa.init_aa()
aa.read_profiles()
rule_type, rule_class = ReadLog('', '', '').get_rule_type(rule)
rule_obj = rule_class.create_instance(rule)
if profile_name not in aa.aa or profile_name not in aa.aa[profile_name]:
exit(_('Cannot find {} in profiles').format(profile_name))
aa.aa[profile_name][profile_name][rule_type].add(rule_obj, cleanup=True)
# Save changes
aa.write_profile_ui_feedback(profile_name)
aa.reload_base(profile_name)
def usage(is_help):
print('This tool is a low level tool - do not use it directly')
print('{} create_userns <template_path> <name> <bin_path> <profile_path> <decision>'.format(sys.argv[0]))
print('{} add_rule <rule> <profile_name>'.format(sys.argv[0]))
if is_help:
exit(0)
else:
exit(1)
def main():
if len(sys.argv) > 1:
command = sys.argv[1]
else:
command = None # Handle the case where no command is provided
match command:
case 'create_userns':
if not len(sys.argv) == 7:
usage(False)
create_userns(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6])
case 'add_rule':
if not len(sys.argv) == 4:
usage(False)
add_to_profile(sys.argv[2], sys.argv[3])
case 'help':
usage(True)
case _:
usage(False)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="com.ubuntu.pkexec.aa-notify.modify_profile">
<description>AppArmor: modifying security profile</description>
<message>To modify an AppArmor security profile, you need to authenticate.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">{LIB_PATH}apparmor/update_profile.py</annotate>
<annotate key="org.freedesktop.policykit.exec.argv1">add_rule</annotate>
</action>
<action id="com.ubuntu.pkexec.aa-notify.create_userns">
<description>AppArmor: adding userns profile</description>
<message>To allow a program to use unprivileged user namespaces, you need to authenticate.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">{LIB_PATH}apparmor/update_profile.py</annotate>
<annotate key="org.freedesktop.policykit.exec.argv1">create_userns</annotate>
</action>
</policyconfig>

View file

@ -0,0 +1,14 @@
# This profile was auto-generated by AppArmor
# This profile allows everything and only exists to give the
# application the possibility to use unprivileged user namespaces
abi <abi/4.0>,
include <tunables/global>
profile {name} {path} flags=(unconfined) {{
{rule},
# Site-specific additions and overrides. See local/README for details.
include if exists <local/{name}>
}}

View file

@ -11,6 +11,15 @@
# Set to 'no' to disable AppArmor notifications globally
show_notifications="yes"
# Special profiles used to remove privileges for unconfined binaries using user namespaces. If unsure, leave as is.
userns_special_profiles="unconfined,unprivileged_userns"
# Theme to use for aa-notify GUI themes. See https://ttkthemes.readthedocs.io/en/latest/themes.html for available themes.
interface_theme="ubuntu"
# Binaries for which we ignore userns-related capability denials
ignore_denied_capability="sudo,su"
# OPTIONAL - restrict using aa-notify to users in the given group
# (if not set, everybody who has permissions to read the logfile can use it)
# use_group="admin"

View file

@ -55,6 +55,24 @@ class Install(_install):
for d in data:
self.copy_tree(d, os.path.join(prefix + "/usr/share/apparmor/easyprof", os.path.basename(d)))
# Make update_profile.py executable
update_profile_path = os.path.join(self.install_lib, 'apparmor/update_profile.py')
print('changing mode of {} to 755'.format(update_profile_path))
os.chmod(update_profile_path, 0o755)
pkexec_action_name = 'com.ubuntu.pkexec.aa-notify.policy'
print('Installing {} to /usr/share/polkit-1/actions/ mode 644'.format(pkexec_action_name))
with open(pkexec_action_name, 'r') as f:
polkit_template = f.read()
polkit = polkit_template.format(LIB_PATH=self.install_lib)
if not os.path.exists(prefix + '/usr/share/polkit-1/actions/'):
os.mkdir(prefix + '/usr/share/polkit-1/actions/')
with open(prefix + '/usr/share/polkit-1/actions/' + pkexec_action_name, 'w') as f:
f.write(polkit)
os.chmod(prefix + '/usr/share/polkit-1/actions/' + pkexec_action_name, 0o644)
if os.path.exists('staging'):
shutil.rmtree('staging')

View file

@ -184,10 +184,10 @@ class AANotifyTest(AANotifyBase):
expected_return_code = 0
expected_output_1 = \
'''usage: aa-notify [-h] [-p] [--display DISPLAY] [-f FILE] [-l] [-s NUM] [-v]
[-u USER] [-w NUM] [--debug] [--filter.profile PROFILE]
[--filter.operation OPERATION] [--filter.name NAME]
[--filter.denied DENIED] [--filter.family FAMILY]
[--filter.socket SOCKET]
[-u USER] [-w NUM] [--prompt-filter PF] [--debug]
[--filter.profile PROFILE] [--filter.operation OPERATION]
[--filter.name NAME] [--filter.denied DENIED]
[--filter.family FAMILY] [--filter.socket SOCKET]
Display AppArmor notifications or messages for DENIED entries.
''' # noqa: E128
@ -207,6 +207,7 @@ Display AppArmor notifications or messages for DENIED entries.
-u USER, --user USER user to drop privileges to when not using sudo
-w NUM, --wait NUM wait NUM seconds before displaying notifications (with
-p)
--prompt-filter PF kind of operations which display a popup prompt
--debug debug mode
Filtering options: