mirror of
https://gitlab.com/apparmor/apparmor
synced 2025-08-31 06:16:03 +00:00
[06/38] Add FileRule and FileRuleset
These classes handle file rules, including file rules with leading perms, and are meant to replace lots of file rule code in aa.py and aa-mergeprof. Note: get_glob() and logprof_header_localvars() don't even look finalized and will be changed in a later patch. (Some other things will also be changed or added with later patches - but you probably won't notice them while reviewing this patch.) Acked-by: Seth Arnold <seth.arnold@canonical.com> Acked-by: Kshitij Gupta <kgupta8592@gmail.com> (with some suggestions for a follow-up patch) v1.1: remove 'and not deny' from a condition in split_perms() to get more helpful error messages for rules like "deny /foo pix," Acked-by: Steve Beattie <steve@nxnw.org>
This commit is contained in:
355
utils/apparmor/rule/file.py
Normal file
355
utils/apparmor/rule/file.py
Normal file
@@ -0,0 +1,355 @@
|
||||
# ----------------------------------------------------------------------
|
||||
# Copyright (C) 2016 Christian Boltz <apparmor@cboltz.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of version 2 of the GNU General Public
|
||||
# License as published by the Free Software Foundation.
|
||||
#
|
||||
# This program 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 General Public License for more details.
|
||||
#
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
from apparmor.regex import RE_PROFILE_FILE_ENTRY, strip_quotes
|
||||
from apparmor.common import AppArmorBug, AppArmorException, type_is_str
|
||||
from apparmor.rule import BaseRule, BaseRuleset, check_and_split_list, logprof_value_or_all, parse_modifiers, quote_if_needed
|
||||
|
||||
# setup module translations
|
||||
from apparmor.translations import init_translation
|
||||
_ = init_translation()
|
||||
|
||||
|
||||
allow_exec_transitions = ('ix', 'ux', 'Ux', 'px', 'Px', 'cx', 'Cx') # 2 chars - len relevant for split_perms()
|
||||
allow_exec_fallback_transitions = ('pix', 'Pix', 'cix', 'Cix', 'pux', 'PUx', 'cux', 'CUx') # 3 chars - len relevant for split_perms()
|
||||
deny_exec_transitions = ('x')
|
||||
file_permissions = ('m', 'r', 'w', 'a', 'l', 'k') # also defines the write order
|
||||
|
||||
|
||||
|
||||
class FileRule(BaseRule):
|
||||
'''Class to handle and store a single file rule'''
|
||||
|
||||
# Nothing external should reference this class, all external users
|
||||
# should reference the class field FileRule.ALL
|
||||
class __FileAll(object):
|
||||
pass
|
||||
|
||||
ALL = __FileAll
|
||||
|
||||
rule_name = 'file'
|
||||
|
||||
def __init__(self, path, perms, exec_perms, target, owner, file_keyword=False, leading_perms=False,
|
||||
audit=False, deny=False, allow_keyword=False, comment='', log_event=None):
|
||||
'''Initialize FileRule
|
||||
|
||||
Parameters:
|
||||
- path: string, AARE or FileRule.ALL
|
||||
- perms: string, set of chars or FileRule.ALL (must not contain exec mode)
|
||||
- exec_perms: None or string
|
||||
- target: string, AARE or FileRule.ALL
|
||||
- owner: bool
|
||||
- file_keyword: bool
|
||||
- leading_perms: bool
|
||||
'''
|
||||
|
||||
super(FileRule, self).__init__(audit=audit, deny=deny, allow_keyword=allow_keyword,
|
||||
comment=comment, log_event=log_event)
|
||||
|
||||
# rulepart partperms is_path log_event
|
||||
self.path, self.all_paths = self._aare_or_all(path, 'path', True, log_event)
|
||||
self.target, self.all_targets, = self._aare_or_all(target, 'target', False, log_event)
|
||||
|
||||
if type_is_str(perms):
|
||||
perms, tmp_exec_perms = split_perms(perms, deny)
|
||||
if tmp_exec_perms:
|
||||
raise AppArmorBug('perms must not contain exec perms')
|
||||
elif perms == None:
|
||||
perms = set()
|
||||
|
||||
self.perms, self.all_perms, unknown_items = check_and_split_list(perms, file_permissions, FileRule.ALL, 'FileRule', 'permissions', allow_empty_list=True)
|
||||
if unknown_items:
|
||||
raise AppArmorBug('Passed unknown perms to FileRule: %s' % str(unknown_items))
|
||||
if self.perms and 'a' in self.perms and 'w' in self.perms:
|
||||
raise AppArmorException("Conflicting permissions found: 'a' and 'w'")
|
||||
|
||||
if exec_perms is None:
|
||||
self.exec_perms = None
|
||||
elif type_is_str(exec_perms):
|
||||
if deny:
|
||||
if exec_perms != 'x':
|
||||
raise AppArmorException(_("file deny rules only allow to use 'x' as execute mode, but not %s" % exec_perms))
|
||||
else:
|
||||
if exec_perms == 'x':
|
||||
raise AppArmorException(_("Execute flag ('x') in file rule must specify the exec mode (ix, Px, Cx etc.)"))
|
||||
elif exec_perms not in allow_exec_transitions and exec_perms not in allow_exec_fallback_transitions:
|
||||
raise AppArmorBug('Unknown execute mode specified in file rule: %s' % exec_perms)
|
||||
self.exec_perms = exec_perms
|
||||
else:
|
||||
raise AppArmorBug('Passed unknown perms object to FileRule: %s' % str(perms))
|
||||
|
||||
if type(owner) is not bool:
|
||||
raise AppArmorBug('non-boolean value passed to owner flag')
|
||||
self.owner = owner
|
||||
|
||||
if type(file_keyword) is not bool:
|
||||
raise AppArmorBug('non-boolean value passed to file keyword flag')
|
||||
self.file_keyword = file_keyword
|
||||
|
||||
if type(leading_perms) is not bool:
|
||||
raise AppArmorBug('non-boolean value passed to leading permissions flag')
|
||||
self.leading_perms = leading_perms
|
||||
|
||||
# XXX subset
|
||||
|
||||
# check for invalid combinations (bare 'file,' vs. path rule)
|
||||
# if (self.all_paths and not self.all_perms) or (not self.all_paths and self.all_perms):
|
||||
# raise AppArmorBug('all_paths and all_perms must be equal')
|
||||
# elif
|
||||
if self.all_paths and (self.exec_perms or self.target):
|
||||
raise AppArmorBug('exec perms or target specified for bare file rule')
|
||||
|
||||
@classmethod
|
||||
def _match(cls, raw_rule):
|
||||
return RE_PROFILE_FILE_ENTRY.search(raw_rule)
|
||||
|
||||
@classmethod
|
||||
def _parse(cls, raw_rule):
|
||||
'''parse raw_rule and return FileRule'''
|
||||
|
||||
matches = cls._match(raw_rule)
|
||||
if not matches:
|
||||
raise AppArmorException(_("Invalid file rule '%s'") % raw_rule)
|
||||
|
||||
audit, deny, allow_keyword, comment = parse_modifiers(matches)
|
||||
|
||||
owner = bool(matches.group('owner'))
|
||||
|
||||
leading_perms = False
|
||||
|
||||
if matches.group('path'):
|
||||
path = strip_quotes(matches.group('path'))
|
||||
elif matches.group('path2'):
|
||||
path = strip_quotes(matches.group('path2'))
|
||||
leading_perms = True
|
||||
else:
|
||||
path = FileRule.ALL
|
||||
|
||||
if matches.group('perms'):
|
||||
perms = matches.group('perms')
|
||||
perms, exec_perms = split_perms(perms, deny)
|
||||
elif matches.group('perms2'):
|
||||
perms = matches.group('perms2')
|
||||
perms, exec_perms = split_perms(perms, deny)
|
||||
leading_perms = True
|
||||
else:
|
||||
perms = FileRule.ALL
|
||||
exec_perms = None
|
||||
|
||||
if matches.group('target'):
|
||||
target = strip_quotes(matches.group('target'))
|
||||
else:
|
||||
target = FileRule.ALL
|
||||
|
||||
file_keyword = bool(matches.group('file_keyword'))
|
||||
|
||||
return FileRule(path, perms, exec_perms, target, owner, file_keyword, leading_perms,
|
||||
audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment)
|
||||
|
||||
def get_clean(self, depth=0):
|
||||
'''return rule (in clean/default formatting)'''
|
||||
|
||||
space = ' ' * depth
|
||||
|
||||
if self.all_paths:
|
||||
path = ''
|
||||
elif self.path:
|
||||
path = quote_if_needed(self.path.regex)
|
||||
else:
|
||||
raise AppArmorBug('Empty path in file rule')
|
||||
|
||||
if self.all_perms:
|
||||
perms = ''
|
||||
else:
|
||||
perms = self._joint_perms()
|
||||
if not perms:
|
||||
raise AppArmorBug('Empty permissions in file rule')
|
||||
|
||||
if self.leading_perms:
|
||||
path_and_perms = '%s %s' % (perms, path)
|
||||
else:
|
||||
path_and_perms = '%s %s' % (path, perms)
|
||||
|
||||
if self.all_targets:
|
||||
target = ''
|
||||
elif self.target:
|
||||
target = ' -> %s' % quote_if_needed(self.target.regex)
|
||||
else:
|
||||
raise AppArmorBug('Empty exec target in file rule')
|
||||
|
||||
if self.owner:
|
||||
owner = 'owner '
|
||||
else:
|
||||
owner = ''
|
||||
|
||||
if self.file_keyword:
|
||||
file_keyword = 'file '
|
||||
else:
|
||||
file_keyword = ''
|
||||
|
||||
if self.all_paths and self.all_perms and not path and not perms and not target:
|
||||
return('%s%s%sfile,%s' % (space, self.modifiers_str(), owner, self.comment)) # plain 'file,' rule
|
||||
elif not self.all_paths and not self.all_perms and path and perms:
|
||||
return('%s%s%s%s%s%s,%s' % (space, self.modifiers_str(), file_keyword, owner, path_and_perms, target, self.comment))
|
||||
else:
|
||||
raise AppArmorBug('Invalid combination of path and perms in file rule - either specify path and perms, or none of them')
|
||||
|
||||
def _joint_perms(self):
|
||||
'''return the permissions as string'''
|
||||
perm_string = ''
|
||||
for perm in file_permissions:
|
||||
if perm in self.perms:
|
||||
perm_string = perm_string + perm
|
||||
|
||||
if self.exec_perms:
|
||||
perm_string = perm_string + self.exec_perms
|
||||
|
||||
return perm_string
|
||||
|
||||
def is_covered_localvars(self, other_rule):
|
||||
'''check if other_rule is covered by this rule object'''
|
||||
|
||||
if not self._is_covered_aare(self.path, self.all_paths, other_rule.path, other_rule.all_paths, 'path'):
|
||||
return False
|
||||
|
||||
# TODO: check 'a' vs. 'w'
|
||||
# perms can be empty if only exec_perms are specified, therefore disable the sanity check in _is_covered_list()...
|
||||
if not self._is_covered_list(self.perms, self.all_perms, other_rule.perms, other_rule.all_perms, 'perms', sanity_check=False):
|
||||
return False
|
||||
|
||||
# ... and do our own sanity check
|
||||
if not other_rule.perms and not other_rule.all_perms and not other_rule.exec_perms:
|
||||
raise AppArmorBug('No permission or exec permission specified in other file rule')
|
||||
|
||||
if not self.exec_perms and other_rule.exec_perms:
|
||||
return False
|
||||
|
||||
# TODO: handle fallback modes?
|
||||
if other_rule.exec_perms and self.exec_perms != other_rule.exec_perms:
|
||||
return False
|
||||
|
||||
# check exec_mode and target only if other_rule contains exec_perms or link permissions
|
||||
# (for mrwk permissions, the target is ignored anyway)
|
||||
if other_rule.exec_perms or (other_rule.perms and 'l' in other_rule.perms):
|
||||
if not self._is_covered_aare(self.target, self.all_targets, other_rule.target, other_rule.all_targets, 'target'):
|
||||
return False
|
||||
|
||||
# a different target means running with a different profile, therefore we have to be more strict than _is_covered_aare()
|
||||
# XXX should we enforce an exact match for a) exec and/or b) link target?
|
||||
if self.all_targets != other_rule.all_targets:
|
||||
return False
|
||||
|
||||
if self.owner and not other_rule.owner:
|
||||
return False
|
||||
|
||||
# no check for file_keyword and leading_perms - they are not relevant for is_covered()
|
||||
|
||||
# still here? -> then it is covered
|
||||
return True
|
||||
|
||||
|
||||
def is_equal_localvars(self, rule_obj, strict):
|
||||
'''compare if rule-specific variables are equal'''
|
||||
|
||||
if not type(rule_obj) == FileRule:
|
||||
raise AppArmorBug('Passed non-file rule: %s' % str(rule_obj))
|
||||
|
||||
if self.owner != rule_obj.owner:
|
||||
return False
|
||||
|
||||
if not self._is_equal_aare(self.path, self.all_paths, rule_obj.path, rule_obj.all_paths, 'path'):
|
||||
return False
|
||||
|
||||
if self.perms != rule_obj.perms:
|
||||
return False
|
||||
|
||||
if self.all_perms != rule_obj.all_perms:
|
||||
return False
|
||||
|
||||
if self.exec_perms != rule_obj.exec_perms:
|
||||
return False
|
||||
|
||||
if not self._is_equal_aare(self.target, self.all_targets, rule_obj.target, rule_obj.all_targets, 'target'):
|
||||
return False
|
||||
|
||||
if strict: # file_keyword and leading_perms are only cosmetics, but still a difference
|
||||
if self.file_keyword != rule_obj.file_keyword:
|
||||
return False
|
||||
|
||||
if self.leading_perms != rule_obj.leading_perms:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def logprof_header_localvars(self):
|
||||
if self.owner:
|
||||
owner = _('Yes')
|
||||
else:
|
||||
owner = _('No')
|
||||
|
||||
path = logprof_value_or_all(self.path, self.all_paths)
|
||||
perms = logprof_value_or_all(self.perms, self.all_perms)
|
||||
if self.exec_perms:
|
||||
perms = perms + self.exec_perms
|
||||
target = logprof_value_or_all(self.target, self.all_targets)
|
||||
|
||||
return [
|
||||
_('Owner only'), owner,
|
||||
_('Path'), path,
|
||||
_('Permissions'), perms,
|
||||
_('Target'), target,
|
||||
# file_keyword and leading_perms are not really relevant
|
||||
]
|
||||
|
||||
|
||||
class FileRuleset(BaseRuleset):
|
||||
'''Class to handle and store a collection of file rules'''
|
||||
|
||||
def get_glob(self, path_or_rule):
|
||||
'''Return the next possible glob. For file rules, that means removing owner or globbing the path'''
|
||||
# XXX only remove one part, not all
|
||||
return 'file,'
|
||||
|
||||
|
||||
def split_perms(perm_string, deny):
|
||||
'''parse permission string
|
||||
- perm_string: the permission string to parse
|
||||
- deny: True if this is a deny rule
|
||||
'''
|
||||
perms = set()
|
||||
exec_mode = None
|
||||
|
||||
while perm_string:
|
||||
if perm_string[0] in file_permissions:
|
||||
perms.add(perm_string[0])
|
||||
perm_string = perm_string[1:]
|
||||
elif perm_string[0] == 'x':
|
||||
if not deny:
|
||||
raise AppArmorException(_("'x' must be preceded by an exec qualifier (i, P, C or U)"))
|
||||
exec_mode = 'x'
|
||||
perm_string = perm_string[1:]
|
||||
elif perm_string.startswith(allow_exec_transitions):
|
||||
if exec_mode:
|
||||
raise AppArmorException(_('conflicting execute permissions found: %s and %s' % (exec_mode, perm_string[0:2])))
|
||||
exec_mode = perm_string[0:2]
|
||||
perm_string = perm_string[2:]
|
||||
elif perm_string.startswith(allow_exec_fallback_transitions):
|
||||
if exec_mode:
|
||||
raise AppArmorException(_('conflicting execute permissions found: %s and %s' % (exec_mode, perm_string[0:3])))
|
||||
exec_mode = perm_string[0:3]
|
||||
perm_string = perm_string[3:]
|
||||
else:
|
||||
raise AppArmorException(_('permission contains unknown character(s) %s' % perm_string))
|
||||
|
||||
return perms, exec_mode
|
Reference in New Issue
Block a user