Пример #1
0
    def parse_authz(self):
        self.log.debug("Parsing authz security policy %s", self.authz_file)

        self.authz = UnicodeConfigParser()
        try:
            self.authz.read(self.authz_file)
        except ParsingError as e:
            self.log.error("Error parsing authz permission policy file: %s",
                           to_unicode(e))
            raise ConfigurationError()
        groups = {}
        if self.authz.has_section('groups'):
            for group, users in self.authz.items('groups'):
                groups[group] = to_list(users)

        self.groups_by_user = {}

        def add_items(group, items):
            for item in items:
                if item.startswith('@'):
                    add_items(group, groups[item[1:]])
                else:
                    self.groups_by_user.setdefault(item, set()).add(group)

        for group, users in groups.iteritems():
            add_items('@' + group, users)

        self.authz_mtime = os.path.getmtime(self.authz_file)
Пример #2
0
    def parse_authz(self):
        self.log.debug("Parsing authz security policy %s", self.authz_file)

        if not self.authz_file:
            self.log.error("The `[authz_policy] authz_file` configuration "
                           "option in trac.ini is empty or not defined.")
            raise ConfigurationError()
        try:
            authz_mtime = os.path.getmtime(self.authz_file)
        except OSError as e:
            self.log.error("Error parsing authz permission policy file: %s",
                           exception_to_unicode(e))
            raise ConfigurationError()

        self.authz = UnicodeConfigParser(ignorecase_option=False)
        try:
            self.authz.read(self.authz_file)
        except configparser.ParsingError as e:
            self.log.error("Error parsing authz permission policy file: %s",
                           exception_to_unicode(e))
            raise ConfigurationError()
        groups = {}
        if self.authz.has_section('groups'):
            for group, users in self.authz.items('groups'):
                groups[group] = to_list(users)

        self.groups_by_user = {}

        def add_items(group, items):
            for item in items:
                if item.startswith('@'):
                    add_items(group, groups[item[1:]])
                else:
                    self.groups_by_user.setdefault(item, set()).add(group)

        for group, users in groups.items():
            add_items('@' + group, users)

        all_actions = set(PermissionSystem(self.env).get_actions())
        authz_basename = os.path.basename(self.authz_file)
        for section in self.authz.sections():
            if section == 'groups':
                continue
            for user, actions in self.authz.items(section):
                for action in to_list(actions):
                    if action.startswith('!'):
                        action = action[1:]
                    if action not in all_actions:
                        self.log.warning(
                            "The action %s in the [%s] section "
                            "of %s is not a valid action.", action, section,
                            authz_basename)
        self.authz_mtime = authz_mtime
Пример #3
0
 def setUp(self):
     self.tempdir = mkdtemp()
     self.filename = os.path.join(self.tempdir, 'config.ini')
     _write(self.filename, [
         u'[ä]', u'öption = ÿ',
         u'[ä]', u'optīon = 1.1',
         u'[č]', u'ôption = ž',
         u'[č]', u'optïon = 1',
         u'[ė]', u'optioñ = true',
     ])
     self.parser = UnicodeConfigParser()
     self._read()
Пример #4
0
    def parse_authz(self):
        self.log.debug("Parsing authz security policy %s", self.authz_file)

        self.authz = UnicodeConfigParser()
        try:
            self.authz.read(self.authz_file)
        except ParsingError as e:
            self.log.error("Error parsing authz permission policy file: %s", to_unicode(e))
            raise ConfigurationError()
        groups = {}
        if self.authz.has_section("groups"):
            for group, users in self.authz.items("groups"):
                groups[group] = to_list(users)

        self.groups_by_user = {}

        def add_items(group, items):
            for item in items:
                if item.startswith("@"):
                    add_items(group, groups[item[1:]])
                else:
                    self.groups_by_user.setdefault(item, set()).add(group)

        for group, users in groups.iteritems():
            add_items("@" + group, users)

        self.authz_mtime = os.path.getmtime(self.authz_file)
Пример #5
0
    def enable_authz_permpolicy(self, authz_content, filename=None):
        """Enables the Authz permissions policy. The `authz_content` will
        be written to `filename`, and may be specified in a triple-quoted
        string.::

           [wiki:WikiStart@*]
           * = WIKI_VIEW
           [wiki:PrivatePage@*]
           john = WIKI_VIEW
           * = !WIKI_VIEW

        `authz_content` may also be a dictionary of dictionaries specifying
        the sections and key/value pairs of each section, however this form
        should only be used when the order of the entries in the file is not
        important, as the order cannot be known.::

           {
            'wiki:WikiStart@*': {'*': 'WIKI_VIEW'},
            'wiki:PrivatePage@*': {'john': 'WIKI_VIEW', '*': '!WIKI_VIEW'},
           }

        The `filename` parameter is optional, and if omitted a filename will
        be generated by computing a hash of `authz_content`, prefixed with
        "authz-".
        """
        if filename is None:
            filename = 'authz-' + \
                       hashlib.md5(str(authz_content)).hexdigest()[:9]
        env = self.get_trac_environment()
        authz_file = os.path.join(env.conf_dir, filename)
        if os.path.exists(authz_file):
            wait_for_file_mtime_change(authz_file)
        if isinstance(authz_content, basestring):
            authz_content = [
                line.strip() + '\n'
                for line in authz_content.strip().splitlines()
            ]
            authz_content = ['# -*- coding: utf-8 -*-\n'] + authz_content
            create_file(authz_file, authz_content)
        else:
            parser = UnicodeConfigParser()
            for section, options in authz_content.items():
                parser.add_section(section)
                for key, value in options.items():
                    parser.set(section, key, value)
            with open(authz_file, 'w') as f:
                parser.write(f)
        permission_policies = env.config.get('trac', 'permission_policies')
        env.config.set('trac', 'permission_policies',
                       'AuthzPolicy, ' + permission_policies)
        env.config.set('authz_policy', 'authz_file', authz_file)
        env.config.set('components', 'tracopt.perm.authz_policy.*', 'enabled')
        env.config.save()
Пример #6
0
 def setUp(self):
     self.tempdir = tempfile.mkdtemp()
     self.filename = os.path.join(self.tempdir, 'config.ini')
     _write(self.filename, [
         u'[ä]', u'öption = ÿ',
         u'[ä]', u'optīon = 1.1',
         u'[č]', u'ôption = ž',
         u'[č]', u'optïon = 1',
         u'[ė]', u'optioñ = true',
     ])
     self.parser = UnicodeConfigParser()
     self._read()
Пример #7
0
    def test_templates_need_update_true(self):
        """Templates need to be updated."""
        self.env.config.set('notification', 'ticket_subject_template',
                            '$prefix #$ticket.id: $summary')
        self.env.config.set('notification', 'batch_subject_template',
                            '$prefix Batch modify: $tickets_descr')
        self.env.config.save()

        db45.do_upgrade(self.env, None, None)

        self.assertIn(('INFO', 'Replaced value of [notification] '
                       'ticket_subject_template: $prefix #$ticket.id: '
                       '$summary -> ${prefix} #${ticket.id}: ${summary}'),
                      self.env.log_messages)
        self.assertIn(('INFO', 'Replaced value of [notification] '
                       'batch_subject_template: $prefix Batch modify: '
                       '$tickets_descr -> ${prefix} Batch modify: '
                       '${tickets_descr}'), self.env.log_messages)
        parser = UnicodeConfigParser()
        parser.read(self.env.config.filename)
        self.assertEqual('${prefix} #${ticket.id}: ${summary}',
                         parser.get('notification', 'ticket_subject_template'))
        self.assertEqual('${prefix} Batch modify: ${tickets_descr}',
                         parser.get('notification', 'batch_subject_template'))
        self.assertTrue(self._backup_file_exists())
Пример #8
0
def parse(authz_file, modules):
    """Parse a Subversion authorization file.

    Return a dict of modules, each containing a dict of paths, each containing
    a dict mapping users to permissions. Only modules contained in `modules`
    are retained.
    """
    parser = UnicodeConfigParser(ignorecase_option=False)
    parser.read(authz_file)

    groups = {}
    aliases = {}
    sections = {}
    for section in parser.sections():
        if section == 'groups':
            for name, value in parser.items(section):
                groups.setdefault(name, set()).update(to_list(value))
        elif section == 'aliases':
            for name, value in parser.items(section):
                aliases[name] = value.strip()
        else:
            for name, value in parser.items(section):
                parts = section.split(':', 1)
                module, path = parts[0] if len(parts) > 1 else '', parts[-1]
                if module in modules:
                    sections.setdefault((module, path), []) \
                            .append((name, value))

    def resolve(subject, done):
        if subject.startswith('@'):
            done.add(subject)
            for members in groups[subject[1:]] - done:
                for each in resolve(members, done):
                    yield each
        elif subject.startswith('&'):
            yield aliases[subject[1:]]
        else:
            yield subject

    authz = {}
    for (module, path), items in sections.iteritems():
        section = authz.setdefault(module, {}).setdefault(path, {})
        for subject, perms in items:
            readable = 'r' in perms
            # Ordering isn't significant; any entry could grant permission
            section.update((user, readable)
                           for user in resolve(subject, set())
                           if not section.get(user))
    return authz
Пример #9
0
    def enable_authz_permpolicy(self, authz_content, filename=None):
        """Enables the Authz permissions policy. The `authz_content` will
        be written to `filename`, and may be specified in a triple-quoted
        string.::

           [wiki:WikiStart@*]
           * = WIKI_VIEW
           [wiki:PrivatePage@*]
           john = WIKI_VIEW
           * = !WIKI_VIEW

        `authz_content` may also be a dictionary of dictionaries specifying
        the sections and key/value pairs of each section, however this form
        should only be used when the order of the entries in the file is not
        important, as the order cannot be known.::

           {
            'wiki:WikiStart@*': {'*': 'WIKI_VIEW'},
            'wiki:PrivatePage@*': {'john': 'WIKI_VIEW', '*': '!WIKI_VIEW'},
           }

        The `filename` parameter is optional, and if omitted a filename will
        be generated by computing a hash of `authz_content`, prefixed with
        "authz-".
        """
        if filename is None:
            filename = 'authz-' + \
                       hashlib.md5(str(authz_content)).hexdigest()[:9]
        authz_file = os.path.join(self.tracdir, 'conf', filename)
        if os.path.exists(authz_file):
            wait_for_file_mtime_change(authz_file)
        if isinstance(authz_content, basestring):
            authz_content = [line.strip() + '\n'
                             for line in authz_content.strip().splitlines()]
            authz_content = ['# -*- coding: utf-8 -*-\n'] + authz_content
            create_file(authz_file, authz_content)
        else:
            parser = UnicodeConfigParser()
            for section, options in authz_content.items():
                parser.add_section(section)
                for key, value in options.items():
                    parser.set(section, key, value)
            with open(authz_file, 'w') as f:
                parser.write(f)
        env = self.get_trac_environment()
        permission_policies = env.config.get('trac', 'permission_policies')
        env.config.set('trac', 'permission_policies',
                       'AuthzPolicy, ' + permission_policies)
        env.config.set('authz_policy', 'authz_file', authz_file)
        env.config.set('components', 'tracopt.perm.authz_policy.*', 'enabled')
        env.config.save()
Пример #10
0
class UnicodeParserTestCase(unittest.TestCase):

    def setUp(self):
        self.tempdir = tempfile.mkdtemp()
        self.filename = os.path.join(self.tempdir, 'config.ini')
        _write(self.filename, [
            u'[ä]', u'öption = ÿ',
            u'[ä]', u'optīon = 1.1',
            u'[č]', u'ôption = ž',
            u'[č]', u'optïon = 1',
            u'[ė]', u'optioñ = true',
        ])
        self.parser = UnicodeConfigParser()
        self._read()

    def tearDown(self):
        shutil.rmtree(self.tempdir)

    def _write(self):
        with open(self.filename, 'w') as f:
            self.parser.write(f)

    def _read(self):
        self.parser.read(self.filename)

    def test_sections(self):
        self.assertEqual([u'ä', u'č', u'ė'], self.parser.sections())

    def test_add_section(self):
        self.parser.add_section(u'ē')
        self._write()
        self.assertEqual(
            u'[ä]\n'
            u'öption = ÿ\n'
            u'optīon = 1.1\n\n'
            u'[č]\n'
            u'ôption = ž\n'
            u'optïon = 1\n\n'
            u'[ė]\n'
            u'optioñ = true\n\n'
            u'[ē]\n\n', _read(self.filename))

    def test_has_section(self):
        self.assertTrue(self.parser.has_section(u'ä'))
        self.assertTrue(self.parser.has_section(u'č'))
        self.assertTrue(self.parser.has_section(u'ė'))
        self.assertFalse(self.parser.has_section(u'î'))

    def test_options(self):
        self.assertEqual([u'öption', u'optīon'], self.parser.options(u'ä'))
        self.assertEqual([u'ôption', u'optïon'], self.parser.options(u'č'))

    def test_get(self):
        self.assertEqual(u'ÿ', self.parser.get(u'ä', u'öption'))
        self.assertEqual(u'ž', self.parser.get(u'č', u'ôption'))

    def test_items(self):
        self.assertEqual([(u'öption', u'ÿ'), (u'optīon', u'1.1')],
                          self.parser.items(u'ä'))
        self.assertEqual([(u'ôption', u'ž'), (u'optïon', u'1')],
                         self.parser.items(u'č'))

    def test_getint(self):
        self.assertEqual(1, self.parser.getint(u'č', u'optïon'))

    def test_getfloat(self):
        self.assertEqual(1.1, self.parser.getfloat(u'ä', u'optīon'))

    def test_getboolean(self):
        self.assertTrue(self.parser.getboolean(u'ė', u'optioñ'))

    def test_has_option(self):
        self.assertTrue(self.parser.has_option(u'ä', u'öption'))
        self.assertTrue(self.parser.has_option(u'ä', u'optīon'))
        self.assertTrue(self.parser.has_option(u'č', u'ôption'))
        self.assertTrue(self.parser.has_option(u'č', u'optïon'))
        self.assertTrue(self.parser.has_option(u'ė', u'optioñ'))
        self.assertFalse(self.parser.has_option(u'î', u'optioñ'))

    def test_set(self):
        self.parser.set(u'ä', u'öption', u'ù')
        self.parser.set(u'ė', u'optiœn', None)
        self._write()
        self.assertEqual(
            u'[ä]\n'
            u'öption = ù\n'
            u'optīon = 1.1\n\n'
            u'[č]\n'
            u'ôption = ž\n'
            u'optïon = 1\n\n'
            u'[ė]\n'
            u'optioñ = true\n'
            u'optiœn = \n\n', _read(self.filename))

    def test_remove_option(self):
        self.parser.remove_option(u'ä', u'öption')
        self.parser.remove_option(u'ė', u'optioñ')
        self._write()
        self.assertEqual(
            u'[ä]\n'
            u'optīon = 1.1\n\n'
            u'[č]\n'
            u'ôption = ž\n'
            u'optïon = 1\n\n'
            u'[ė]\n\n', _read(self.filename))

    def test_remove_section(self):
        self.parser.remove_section(u'ä')
        self.parser.remove_section(u'ė')
        self._write()
        self.assertEqual(
            u'[č]\n'
            u'ôption = ž\n'
            u'optïon = 1\n\n', _read(self.filename))
Пример #11
0
class AuthzPolicy(Component):
    """Permission policy using an authz-like configuration file.

    Refer to SVN documentation for syntax of the authz file. Groups are
    supported.

    As the fine-grained permissions brought by this permission policy are
    often used in complement of the other permission policies (like the
    `DefaultPermissionPolicy`), there's no need to redefine all the
    permissions here. Only additional rights or restrictions should be added.

    === Installation ===
    Enabling this policy requires listing it in `trac.ini`::

      {{{
      [trac]
      permission_policies = AuthzPolicy, DefaultPermissionPolicy

      [authz_policy]
      authz_file = conf/authzpolicy.conf
      }}}

    This means that the `AuthzPolicy` permissions will be checked first, and
    only if no rule is found will the `DefaultPermissionPolicy` be used.


    === Configuration ===
    The `authzpolicy.conf` file is a `.ini` style configuration file.

     - Each section of the config is a glob pattern used to match against a
       Trac resource descriptor. These descriptors are in the form::

         {{{
         <realm>:<id>@<version>[/<realm>:<id>@<version> ...]
         }}}

       Resources are ordered left to right, from parent to child. If any
       component is inapplicable, `*` is substituted. If the version pattern is
       not specified explicitely, all versions (`@*`) is added implicitly

       Example: Match the WikiStart page::

         {{{
         [wiki:*]
         [wiki:WikiStart*]
         [wiki:WikiStart@*]
         [wiki:WikiStart]
         }}}

       Example: Match the attachment
       ``wiki:WikiStart@117/attachment/FOO.JPG@*`` on WikiStart::

         {{{
         [wiki:*]
         [wiki:WikiStart*]
         [wiki:WikiStart@*]
         [wiki:WikiStart@*/attachment/*]
         [wiki:WikiStart@117/attachment/FOO.JPG]
         }}}

     - Sections are checked against the current Trac resource '''IN ORDER''' of
       appearance in the configuration file. '''ORDER IS CRITICAL'''.

     - Once a section matches, the current username is matched, '''IN ORDER''',
       against the keys of the section. If a key is prefixed with a `@`, it is
       treated as a group. If a key is prefixed with a `!`, the permission is
       denied rather than granted. The username will match any of 'anonymous',
       'authenticated', <username> or '*', using normal Trac permission rules.

    Example configuration::

      {{{
      [groups]
      administrators = athomas

      [*/attachment:*]
      * = WIKI_VIEW, TICKET_VIEW

      [wiki:WikiStart@*]
      @administrators = WIKI_ADMIN
      anonymous = WIKI_VIEW
      * = WIKI_VIEW

      # Deny access to page templates
      [wiki:PageTemplates/*]
      * =

      # Match everything else
      [*]
      @administrators = TRAC_ADMIN
      anonymous = BROWSER_VIEW, CHANGESET_VIEW, FILE_VIEW, LOG_VIEW,
          MILESTONE_VIEW, POLL_VIEW, REPORT_SQL_VIEW, REPORT_VIEW,
          ROADMAP_VIEW, SEARCH_VIEW, TICKET_CREATE, TICKET_MODIFY,
          TICKET_VIEW, TIMELINE_VIEW,
          WIKI_CREATE, WIKI_MODIFY, WIKI_VIEW
      # Give authenticated users some extra permissions
      authenticated = REPO_SEARCH, XML_RPC
      }}}

    """
    implements(IPermissionPolicy)

    authz_file = PathOption('authz_policy', 'authz_file', '',
                            "Location of authz policy configuration file. "
                            "Non-absolute paths are relative to the "
                            "Environment `conf` directory.")

    def __init__(self):
        self.authz = None
        self.authz_mtime = None
        self.groups_by_user = {}

    # IPermissionPolicy methods

    def check_permission(self, action, username, resource, perm):
        if not self.authz_mtime or \
                os.path.getmtime(self.authz_file) != self.authz_mtime:
            self.parse_authz()
        resource_key = self.normalise_resource(resource)
        self.log.debug('Checking %s on %s', action, resource_key)
        permissions = self.authz_permissions(resource_key, username)
        if permissions is None:
            return None                 # no match, can't decide
        elif permissions == []:
            return False                # all actions are denied

        # FIXME: expand all permissions once for all
        ps = PermissionSystem(self.env)
        for deny, perms in groupby(permissions,
                                   key=lambda p: p.startswith('!')):
            if deny and action in ps.expand_actions(p[1:] for p in perms):
                return False            # action is explicitly denied
            elif action in ps.expand_actions(perms):
                return True             # action is explicitly granted

        return None                     # no match for action, can't decide

    # Internal methods

    def parse_authz(self):
        self.log.debug("Parsing authz security policy %s",
                       self.authz_file)

        if not self.authz_file:
            self.log.error("The `[authz_policy] authz_file` configuration "
                           "option in trac.ini is empty or not defined.")
            raise ConfigurationError()
        try:
            self.authz_mtime = os.path.getmtime(self.authz_file)
        except OSError as e:
            self.log.error("Error parsing authz permission policy file: %s",
                           exception_to_unicode(e))
            raise ConfigurationError()

        self.authz = UnicodeConfigParser(ignorecase_option=False)
        try:
            self.authz.read(self.authz_file)
        except ParsingError as e:
            self.log.error("Error parsing authz permission policy file: %s",
                           exception_to_unicode(e))
            raise ConfigurationError()
        groups = {}
        if self.authz.has_section('groups'):
            for group, users in self.authz.items('groups'):
                groups[group] = to_list(users)

        self.groups_by_user = {}

        def add_items(group, items):
            for item in items:
                if item.startswith('@'):
                    add_items(group, groups[item[1:]])
                else:
                    self.groups_by_user.setdefault(item, set()).add(group)

        for group, users in groups.iteritems():
            add_items('@' + group, users)

        all_actions = set(PermissionSystem(self.env).get_actions())
        authz_basename = os.path.basename(self.authz_file)
        for section in self.authz.sections():
            if section == 'groups':
                continue
            for _, actions in self.authz.items(section):
                for action in to_list(actions):
                    if action.startswith('!'):
                        action = action[1:]
                    if action not in all_actions:
                        self.log.warning("The action %s in the [%s] section "
                                         "of %s is not a valid action.",
                                         action, section, authz_basename)

    def normalise_resource(self, resource):
        def to_descriptor(resource):
            id = resource.id
            return '%s:%s@%s' % (resource.realm or '*',
                                 id if id is not None else '*',
                                 resource.version or '*')

        def flatten(resource):
            if not resource:
                return ['*:*@*']
            descriptor = to_descriptor(resource)
            if not resource.realm and resource.id is None:
                return [descriptor]
            # XXX Due to the mixed functionality in resource we can end up with
            # ticket, ticket:1, ticket:1@10. This code naively collapses all
            # subsets of the parent resource into one. eg. ticket:1@10
            parent = resource.parent
            while parent and resource.realm == parent.realm:
                parent = parent.parent
            if parent:
                return flatten(parent) + [descriptor]
            else:
                return [descriptor]

        return '/'.join(flatten(resource))

    def authz_permissions(self, resource_key, username):
        # TODO: Handle permission negation in sections. eg. "if in this
        # ticket, remove TICKET_MODIFY"
        if username and username != 'anonymous':
            valid_users = ['*', 'authenticated', 'anonymous', username]
        else:
            valid_users = ['*', 'anonymous']
        for resource_section in [a for a in self.authz.sections()
                                   if a != 'groups']:
            resource_glob = resource_section
            if '@' not in resource_glob:
                resource_glob += '@*'

            if fnmatchcase(resource_key, resource_glob):
                for who, permissions in self.authz.items(resource_section):
                    permissions = to_list(permissions)
                    if who in valid_users or \
                            who in self.groups_by_user.get(username, []):
                        self.log.debug("%s matched section %s for user %s",
                                       resource_key, resource_glob, username)
                        if isinstance(permissions, basestring):
                            return [permissions]
                        else:
                            return permissions
        return None
Пример #12
0
class UnicodeParserTestCase(unittest.TestCase):

    def setUp(self):
        self.tempdir = mkdtemp()
        self.filename = os.path.join(self.tempdir, 'config.ini')
        _write(self.filename, [
            u'[ä]', u'öption = ÿ',
            u'[ä]', u'optīon = 1.1',
            u'[č]', u'ôption = ž',
            u'[č]', u'optïon = 1',
            u'[ė]', u'optioñ = true',
        ])
        self.parser = UnicodeConfigParser()
        self._read()

    def tearDown(self):
        rmtree(self.tempdir)

    def _write(self):
        with open(self.filename, 'w') as f:
            self.parser.write(f)

    def _read(self):
        self.parser.read(self.filename)

    def test_sections(self):
        self.assertEqual([u'ä', u'č', u'ė'], self.parser.sections())

    def test_add_section(self):
        self.parser.add_section(u'ē')
        self._write()
        self.assertEqual(
            u'[ä]\n'
            u'öption = ÿ\n'
            u'optīon = 1.1\n\n'
            u'[č]\n'
            u'ôption = ž\n'
            u'optïon = 1\n\n'
            u'[ė]\n'
            u'optioñ = true\n\n'
            u'[ē]\n\n', _read(self.filename))

    def test_has_section(self):
        self.assertTrue(self.parser.has_section(u'ä'))
        self.assertTrue(self.parser.has_section(u'č'))
        self.assertTrue(self.parser.has_section(u'ė'))
        self.assertFalse(self.parser.has_section(u'î'))

    def test_options(self):
        self.assertEqual([u'öption', u'optīon'], self.parser.options(u'ä'))
        self.assertEqual([u'ôption', u'optïon'], self.parser.options(u'č'))

    def test_get(self):
        self.assertEqual(u'ÿ', self.parser.get(u'ä', u'öption'))
        self.assertEqual(u'ž', self.parser.get(u'č', u'ôption'))

    def test_items(self):
        self.assertEqual([(u'öption', u'ÿ'), (u'optīon', u'1.1')],
                          self.parser.items(u'ä'))
        self.assertEqual([(u'ôption', u'ž'), (u'optïon', u'1')],
                         self.parser.items(u'č'))

    def test_getint(self):
        self.assertEqual(1, self.parser.getint(u'č', u'optïon'))

    def test_getfloat(self):
        self.assertEqual(1.1, self.parser.getfloat(u'ä', u'optīon'))

    def test_getboolean(self):
        self.assertTrue(self.parser.getboolean(u'ė', u'optioñ'))

    def test_has_option(self):
        self.assertTrue(self.parser.has_option(u'ä', u'öption'))
        self.assertTrue(self.parser.has_option(u'ä', u'optīon'))
        self.assertTrue(self.parser.has_option(u'č', u'ôption'))
        self.assertTrue(self.parser.has_option(u'č', u'optïon'))
        self.assertTrue(self.parser.has_option(u'ė', u'optioñ'))
        self.assertFalse(self.parser.has_option(u'î', u'optioñ'))

    def test_set(self):
        self.parser.set(u'ä', u'öption', u'ù')
        self.parser.set(u'ė', u'optiœn', None)
        self._write()
        self.assertEqual(
            u'[ä]\n'
            u'öption = ù\n'
            u'optīon = 1.1\n\n'
            u'[č]\n'
            u'ôption = ž\n'
            u'optïon = 1\n\n'
            u'[ė]\n'
            u'optioñ = true\n'
            u'optiœn = \n\n', _read(self.filename))

    def test_remove_option(self):
        self.parser.remove_option(u'ä', u'öption')
        self.parser.remove_option(u'ė', u'optioñ')
        self._write()
        self.assertEqual(
            u'[ä]\n'
            u'optīon = 1.1\n\n'
            u'[č]\n'
            u'ôption = ž\n'
            u'optïon = 1\n\n'
            u'[ė]\n\n', _read(self.filename))

    def test_remove_section(self):
        self.parser.remove_section(u'ä')
        self.parser.remove_section(u'ė')
        self._write()
        self.assertEqual(
            u'[č]\n'
            u'ôption = ž\n'
            u'optïon = 1\n\n', _read(self.filename))
Пример #13
0
class AuthzPolicy(Component):
    """Permission policy using an authz-like configuration file.

    Refer to SVN documentation for syntax of the authz file. Groups are
    supported.

    As the fine-grained permissions brought by this permission policy are
    often used in complement of the other permission policies (like the
    `DefaultPermissionPolicy`), there's no need to redefine all the
    permissions here. Only additional rights or restrictions should be added.

    === Installation ===

    Enabling this policy requires listing it in `trac.ini:
    {{{
    [trac]
    permission_policies = AuthzPolicy, DefaultPermissionPolicy

    [authz_policy]
    authz_file = conf/authzpolicy.conf
    }}}

    This means that the `AuthzPolicy` permissions will be checked first, and
    only if no rule is found will the `DefaultPermissionPolicy` be used.


    === Configuration ===
    The `authzpolicy.conf` file is a `.ini` style configuration file.

     - Each section of the config is a glob pattern used to match against a
       Trac resource descriptor. These descriptors are in the form:
       {{{
       <realm>:<id>@<version>[/<realm>:<id>@<version> ...]
       }}}
       Resources are ordered left to right, from parent to child. If any
       component is inapplicable, `*` is substituted. If the version pattern is
       not specified explicitely, all versions (`@*`) is added implicitly

       Example: Match the WikiStart page
       {{{
       [wiki:*]
       [wiki:WikiStart*]
       [wiki:WikiStart@*]
       [wiki:WikiStart]
       }}}

       Example: Match the attachment `wiki:WikiStart@117/attachment/FOO.JPG@*`
       on WikiStart
       {{{
       [wiki:*]
       [wiki:WikiStart*]
       [wiki:WikiStart@*]
       [wiki:WikiStart@*/attachment/*]
       [wiki:WikiStart@117/attachment/FOO.JPG]
       }}}

     - Sections are checked against the current Trac resource '''IN ORDER''' of
       appearance in the configuration file. '''ORDER IS CRITICAL'''.

     - Once a section matches, the current username is matched, '''IN ORDER''',
       against the keys of the section. If a key is prefixed with a `@`, it is
       treated as a group. If a key is prefixed with a `!`, the permission is
       denied rather than granted. The username will match any of 'anonymous',
       'authenticated', <username> or '*', using normal Trac permission rules.

    Example configuration:
    {{{
    [groups]
    administrators = athomas

    [*/attachment:*]
    * = WIKI_VIEW, TICKET_VIEW

    [wiki:WikiStart@*]
    @administrators = WIKI_ADMIN
    anonymous = WIKI_VIEW
    * = WIKI_VIEW

    # Deny access to page templates
    [wiki:PageTemplates/*]
    * =

    # Match everything else
    [*]
    @administrators = TRAC_ADMIN
    anonymous = BROWSER_VIEW, CHANGESET_VIEW, FILE_VIEW, LOG_VIEW,
        MILESTONE_VIEW, POLL_VIEW, REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_VIEW,
        SEARCH_VIEW, TICKET_CREATE, TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW,
        WIKI_CREATE, WIKI_MODIFY, WIKI_VIEW
    # Give authenticated users some extra permissions
    authenticated = REPO_SEARCH, XML_RPC
    }}}
    """

    implements(IPermissionPolicy)

    authz_file = PathOption(
        "authz_policy",
        "authz_file",
        "",
        "Location of authz policy configuration file. "
        "Non-absolute paths are relative to the "
        "Environment `conf` directory.",
    )

    authz = None
    authz_mtime = None

    def __init__(self):
        if not self.authz_file:
            self.log.error(
                "The `[authz_policy] authz_file` configuration " "option in trac.ini is empty or not defined."
            )
            raise ConfigurationError()

        try:
            os.stat(self.authz_file)
        except OSError as e:
            self.log.error("Error parsing authz permission policy file: %s", to_unicode(e))
            raise ConfigurationError()
        self.groups_by_user = {}

    # IPermissionPolicy methods

    def check_permission(self, action, username, resource, perm):
        if not self.authz_mtime or os.path.getmtime(self.authz_file) != self.authz_mtime:
            self.parse_authz()
        resource_key = self.normalise_resource(resource)
        self.log.debug("Checking %s on %s", action, resource_key)
        permissions = self.authz_permissions(resource_key, username)
        if permissions is None:
            return None  # no match, can't decide
        elif permissions == []:
            return False  # all actions are denied

        # FIXME: expand all permissions once for all
        ps = PermissionSystem(self.env)
        for deny, perms in groupby(permissions, key=lambda p: p.startswith("!")):
            if deny and action in ps.expand_actions(p[1:] for p in perms):
                return False  # action is explicitly denied
            elif action in ps.expand_actions(perms):
                return True  # action is explicitly granted

        return None  # no match for action, can't decide

    # Internal methods

    def parse_authz(self):
        self.log.debug("Parsing authz security policy %s", self.authz_file)

        self.authz = UnicodeConfigParser()
        try:
            self.authz.read(self.authz_file)
        except ParsingError as e:
            self.log.error("Error parsing authz permission policy file: %s", to_unicode(e))
            raise ConfigurationError()
        groups = {}
        if self.authz.has_section("groups"):
            for group, users in self.authz.items("groups"):
                groups[group] = to_list(users)

        self.groups_by_user = {}

        def add_items(group, items):
            for item in items:
                if item.startswith("@"):
                    add_items(group, groups[item[1:]])
                else:
                    self.groups_by_user.setdefault(item, set()).add(group)

        for group, users in groups.iteritems():
            add_items("@" + group, users)

        self.authz_mtime = os.path.getmtime(self.authz_file)

    def normalise_resource(self, resource):
        def to_descriptor(resource):
            id = resource.id
            return "%s:%s@%s" % (resource.realm or "*", id if id is not None else "*", resource.version or "*")

        def flatten(resource):
            if not resource:
                return ["*:*@*"]
            descriptor = to_descriptor(resource)
            if not resource.realm and resource.id is None:
                return [descriptor]
            # XXX Due to the mixed functionality in resource we can end up with
            # ticket, ticket:1, ticket:1@10. This code naively collapses all
            # subsets of the parent resource into one. eg. ticket:1@10
            parent = resource.parent
            while parent and resource.realm == parent.realm:
                parent = parent.parent
            if parent:
                return flatten(parent) + [descriptor]
            else:
                return [descriptor]

        return "/".join(flatten(resource))

    def authz_permissions(self, resource_key, username):
        # TODO: Handle permission negation in sections. eg. "if in this
        # ticket, remove TICKET_MODIFY"
        if username and username != "anonymous":
            valid_users = ["*", "authenticated", username]
        else:
            valid_users = ["*", "anonymous"]
        for resource_section in [a for a in self.authz.sections() if a != "groups"]:
            resource_glob = resource_section
            if "@" not in resource_glob:
                resource_glob += "@*"

            if fnmatchcase(resource_key, resource_glob):
                for who, permissions in self.authz.items(resource_section):
                    permissions = to_list(permissions)
                    if who in valid_users or who in self.groups_by_user.get(username, []):
                        self.log.debug("%s matched section %s for user %s", resource_key, resource_glob, username)
                        if isinstance(permissions, basestring):
                            return [permissions]
                        else:
                            return permissions
        return None