[apparmor] [patch] [06/38] Add FileRule and FileRuleset

Seth Arnold seth.arnold at canonical.com
Wed Sep 14 06:37:48 UTC 2016


On Fri, Aug 12, 2016 at 10:47:07PM +0200, Christian Boltz wrote:
> Hello,
> 
> $subject.
> 
> 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.)
> 
> 
> [ 06-add-FileRule.diff ]

Acked-by: Seth Arnold <seth.arnold at canonical.com>

Thanks

> 
> --- utils/apparmor/rule/file.py	2016-01-20 21:59:12.806060698 +0100
> +++ utils/apparmor/rule/file.py	2016-01-20 20:44:55.781788850 +0100
> @@ -0,0 +1,355 @@
> +# ----------------------------------------------------------------------
> +#    Copyright (C) 2016 Christian Boltz <apparmor at 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) and not deny:
> +            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
> 
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 473 bytes
Desc: not available
URL: <https://lists.ubuntu.com/archives/apparmor/attachments/20160913/0b67eaf8/attachment-0001.pgp>


More information about the AppArmor mailing list