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

Christian Boltz apparmor at cboltz.de
Fri Aug 12 20:47:07 UTC 2016


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 ]

--- 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



Regards,

Christian Boltz
-- 
you could be correct in that bugzilla may not be useful in predicting
either when the bug will be resolved, or the weather next month.
so, maybe subscribe to [opensuse-crystal_ball] is the best bet.
[DenverD in opensuse-factory]
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 819 bytes
Desc: This is a digitally signed message part.
URL: <https://lists.ubuntu.com/archives/apparmor/attachments/20160812/15cf2622/attachment.pgp>


More information about the AppArmor mailing list