diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -108,6 +108,7 @@
         trac.ticket.web_ui = trac.ticket.web_ui
         trac.timeline = trac.timeline.web_ui
         trac.versioncontrol.admin = trac.versioncontrol.admin
+        trac.versioncontrol.svn_authz = trac.versioncontrol.svn_authz
         trac.versioncontrol.svn_fs = trac.versioncontrol.svn_fs
         trac.versioncontrol.svn_prop = trac.versioncontrol.svn_prop
         trac.versioncontrol.web_ui = trac.versioncontrol.web_ui
diff --git a/trac/env.py b/trac/env.py
--- a/trac/env.py
+++ b/trac/env.py
@@ -307,7 +307,7 @@
         
         @param authname: user name for authorization
         """
-        return RepositoryManager(self).get_repository(reponame, authname)
+        return RepositoryManager(self).get_repository(reponame)
 
     def create(self, options=[]):
         """Create the basic directory structure of the environment, initialize
diff --git a/trac/versioncontrol/api.py b/trac/versioncontrol/api.py
--- a/trac/versioncontrol/api.py
+++ b/trac/versioncontrol/api.py
@@ -318,7 +318,7 @@
                 if is_default(reponame):
                     reponame = ''
                 try:
-                    repo = self.get_repository(reponame, req.authname)
+                    repo = self.get_repository(reponame)
                     if repo:
                         repo.sync()
                 except TracError, e:
@@ -422,7 +422,7 @@
                     if prio >= 0)
         return list(types)
     
-    def get_repositories_by_dir(self, directory, authname):
+    def get_repositories_by_dir(self, directory):
         """Retrieve the repositories based on the given directory.
 
            :param directory: the key for identifying the repositories.
@@ -435,7 +435,7 @@
             if dir:
                 dir = os.path.join(os.path.normcase(dir), '')
                 if dir.startswith(directory):
-                    repos = self.get_repository(reponame, authname)
+                    repos = self.get_repository(reponame)
                     if repos:
                         repositories.append(repos)
         return repositories
@@ -459,13 +459,12 @@
             db.commit()
         return id
     
-    def get_repository(self, reponame, authname):
+    def get_repository(self, reponame):
         """Retrieve the appropriate Repository for the given name.
 
            :param reponame: the key for specifying the repository.
                             If no name is given, take the default 
                             repository.
-           :param authname: deprecated (use fine grained permissions)
            :return: if no corresponding repository was defined, 
                     simply return `None`.
         """
@@ -499,7 +498,7 @@
         finally:
             self._lock.release()
 
-    def get_repository_by_path(self, path, authname):
+    def get_repository_by_path(self, path):
         """Retrieve a matching Repository for the given path.
         
         :param path: the eventually scoped repository-scoped path
@@ -519,7 +518,7 @@
             path = path[length:]
         else:
             reponame = ''
-        return (reponame, self.get_repository(reponame, authname),
+        return (reponame, self.get_repository(reponame),
                 path.rstrip('/') or '/')
 
     def get_default_repository(self, context):
@@ -549,12 +548,12 @@
                         self._all_repositories[reponame] = info
         return self._all_repositories
     
-    def get_real_repositories(self, authname):
+    def get_real_repositories(self):
         """Return a set of all real repositories (i.e. excluding aliases)."""
         repositories = set()
         for reponame in self.get_all_repositories():
             try:
-                repos = self.get_repository(reponame, authname)
+                repos = self.get_repository(reponame)
                 if repos is not None:
                     repositories.add(repos)
             except TracError:
@@ -572,7 +571,7 @@
             self._lock.release()
         self.config.touch()     # Force environment reload
  
-    def notify(self, event, reponame, revs, authname):
+    def notify(self, event, reponame, revs):
         """Notify repositories and change listeners about repository events.
         
         The supported events are the names of the methods defined in the
@@ -594,7 +593,7 @@
             else:
                 base = reponame
         if base:
-            repositories = [r for r in self.get_real_repositories(authname)
+            repositories = [r for r in self.get_real_repositories()
                             if r.get_base() == base]
         if not repositories:
             self.log.warn("Found no repositories matching '%s' base.",
@@ -676,7 +675,7 @@
 class Repository(object):
     """Base class for a repository provided by a version control system."""
 
-    def __init__(self, name, params, authz, log):
+    def __init__(self, name, params, log):
         """Initialize a repository.
         
            :param name: a unique name identifying the repository, usually a
@@ -686,14 +685,12 @@
                           the name of the repository under the key "name" and
                           the surrogate key that identifies the repository in
                           the database under the key "id".
-           :param authz: a repository authorizer (deprecated).
            :param log: a logger instance.
         """
         self.name = name
         self.params = params
         self.reponame = params['name']
         self.id = params['id']
-        self.authz = authz or Authorizer()
         self.log = log
 
     def close(self):
@@ -771,12 +768,11 @@
         """
         rev = self.youngest_rev
         while rev:
-            if self.authz.has_permission_for_changeset(rev):
-                chgset = self.get_changeset(rev)
-                if chgset.date < start:
-                    return
-                if chgset.date < stop:
-                    yield chgset
+            chgset = self.get_changeset(rev)
+            if chgset.date < start:
+                return
+            if chgset.date < stop:
+                yield chgset
             rev = self.previous_rev(rev)
 
     def has_node(self, path, rev=None):
@@ -1038,35 +1034,7 @@
 
 
 
-class PermissionDenied(PermissionError):
-    """Exception raised by an authorizer.
-
-    This exception is raise if the user has insufficient permissions
-    to view a specific part of the repository.
-    """
-    def __str__(self):
-        return self.action
-
-
-class Authorizer(object):
-    """Controls the view access to parts of the repository.
-    
-    Base class for authorizers that are responsible to granting or denying
-    access to view certain parts of a repository.
-    """
-
-    def assert_permission(self, path):
-        if not self.has_permission(path):
-            raise PermissionDenied(_('Insufficient permissions to access '
-                                     '%(path)s', path=path))
-
-    def assert_permission_for_changeset(self, rev):
-        if not self.has_permission_for_changeset(rev):
-            raise PermissionDenied(_('Insufficient permissions to access '
-                                     'changeset %(id)s', id=rev))
-
-    def has_permission(self, path):
-        return True
-
-    def has_permission_for_changeset(self, rev):
-        return True
+# Note: Since Trac 0.12, Exception PermissionDenied class is gone,
+# and class Authorizer is gone as well.
+#
+# Fine-grained permissions are now handled via normal permission policies.
diff --git a/trac/versioncontrol/cache.py b/trac/versioncontrol/cache.py
--- a/trac/versioncontrol/cache.py
+++ b/trac/versioncontrol/cache.py
@@ -22,8 +22,7 @@
 from trac.core import TracError
 from trac.util.datefmt import utc, to_timestamp
 from trac.util.translation import _
-from trac.versioncontrol import Changeset, Node, Repository, Authorizer, \
-                                NoSuchChangeset
+from trac.versioncontrol import Changeset, Node, Repository, NoSuchChangeset
 
 
 _kindmap = {'D': Node.DIRECTORY, 'F': Node.FILE}
@@ -43,14 +42,14 @@
 
     scope = property(lambda self: self.repos.scope)
     
-    def __init__(self, env, repos, authz, log):
+    def __init__(self, env, repos, log):
         self.env = env
         self.repos = repos
         self.metadata = CacheProxy(self.__class__.__module__ + '.'
                                    + self.__class__.__name__ + '.metadata:'
                                    + str(self.repos.id), self._metadata,
                                    self.env)
-        Repository.__init__(self, repos.name, repos.params, authz, log)
+        Repository.__init__(self, repos.name, repos.params, log)
 
     def close(self):
         self.repos.close()
@@ -65,8 +64,7 @@
         return self.repos.get_path_url(path, rev)
 
     def get_changeset(self, rev):
-        return CachedChangeset(self.repos, self.normalize_rev(rev),
-                               self.env, self.authz)
+        return CachedChangeset(self.repos, self.normalize_rev(rev), self.env)
 
     def get_changeset_uid(self, rev):
         return self.repos.get_changeset_uid(rev)
@@ -81,8 +79,7 @@
                         to_timestamp(stop)))
         for rev, in cursor:
             try:
-                if self.authz.has_permission_for_changeset(rev):
-                    yield self.get_changeset(rev)
+                yield self.get_changeset(rev)
             except NoSuchChangeset:
                 pass # skip changesets currently being resync'ed
 
@@ -220,70 +217,63 @@
             # 1. prepare for resyncing
             #    (there still might be a race condition at this point)
 
-            authz = self.repos.authz
-            self.repos.authz = Authorizer() # remove permission checking
-
             kindmap = dict(zip(_kindmap.values(), _kindmap.keys()))
             actionmap = dict(zip(_actionmap.values(), _actionmap.keys()))
 
-            try:
-                while next_youngest is not None:
-                    
-                    # 1.1 Attempt to resync the 'revision' table
-                    self.log.info("Trying to sync revision [%s]" %
-                                  next_youngest)
-                    cset = self.repos.get_changeset(next_youngest)
-                    try:
-                        cursor.execute("INSERT INTO revision "
-                                       " (repos,rev,time,author,message) "
-                                       "VALUES (%s,%s,%s,%s,%s)",
-                                       (self.id, str(next_youngest),
-                                        to_timestamp(cset.date),
-                                        cset.author, cset.message))
-                    except Exception, e: # *another* 1.1. resync attempt won 
-                        self.log.warning('Revision %s already cached: %s' %
-                                         (next_youngest, e))
-                        # also potentially in progress, so keep ''previous''
-                        # notion of 'youngest'
-                        self.repos.clear(youngest_rev=youngest)
-                        db.rollback()
-                        return
+            while next_youngest is not None:
+                
+                # 1.1 Attempt to resync the 'revision' table
+                self.log.info("Trying to sync revision [%s]" %
+                              next_youngest)
+                cset = self.repos.get_changeset(next_youngest)
+                try:
+                    cursor.execute("INSERT INTO revision "
+                                   " (repos,rev,time,author,message) "
+                                   "VALUES (%s,%s,%s,%s,%s)",
+                                   (self.id, str(next_youngest),
+                                    to_timestamp(cset.date),
+                                    cset.author, cset.message))
+                except Exception, e: # *another* 1.1. resync attempt won 
+                    self.log.warning('Revision %s already cached: %s' %
+                                     (next_youngest, e))
+                    # also potentially in progress, so keep ''previous''
+                    # notion of 'youngest'
+                    self.repos.clear(youngest_rev=youngest)
+                    db.rollback()
+                    return
 
-                    # 1.2. now *only* one process was able to get there
-                    #      (i.e. there *shouldn't* be any race condition here)
+                # 1.2. now *only* one process was able to get there
+                #      (i.e. there *shouldn't* be any race condition here)
 
-                    for path, kind, action, bpath, brev in cset.get_changes():
-                        self.log.debug("Caching node change in [%s]: %s"
-                                       % (next_youngest,
-                                          (path,kind,action,bpath,brev)))
-                        kind = kindmap[kind]
-                        action = actionmap[action]
-                        cursor.execute("INSERT INTO node_change "
-                                       " (repos,rev,path,node_type,"
-                                       "  change_type,base_path,base_rev) "
-                                       "VALUES (%s,%s,%s,%s,%s,%s,%s)",
-                                       (self.id, str(next_youngest),
-                                        path, kind, action, bpath, brev))
+                for path, kind, action, bpath, brev in cset.get_changes():
+                    self.log.debug("Caching node change in [%s]: %s"
+                                   % (next_youngest,
+                                      (path,kind,action,bpath,brev)))
+                    kind = kindmap[kind]
+                    action = actionmap[action]
+                    cursor.execute("INSERT INTO node_change "
+                                   " (repos,rev,path,node_type,"
+                                   "  change_type,base_path,base_rev) "
+                                   "VALUES (%s,%s,%s,%s,%s,%s,%s)",
+                                   (self.id, str(next_youngest),
+                                    path, kind, action, bpath, brev))
 
-                    # 1.3. iterate (1.1 should always succeed now)
-                    youngest = next_youngest                    
-                    next_youngest = self.repos.next_rev(next_youngest)
+                # 1.3. iterate (1.1 should always succeed now)
+                youngest = next_youngest                    
+                next_youngest = self.repos.next_rev(next_youngest)
 
-                    # 1.4. update 'youngest_rev' metadata 
-                    #      (minimize possibility of failures at point 0.)
-                    cursor.execute("UPDATE repository SET value=%s "
-                                   "WHERE id=%s AND name=%s",
-                                   (str(youngest), self.id,
-                                    CACHE_YOUNGEST_REV))
-                    self.metadata.invalidate(db)
-                    db.commit()
+                # 1.4. update 'youngest_rev' metadata 
+                #      (minimize possibility of failures at point 0.)
+                cursor.execute("UPDATE repository SET value=%s "
+                               "WHERE id=%s AND name=%s",
+                               (str(youngest), self.id,
+                                CACHE_YOUNGEST_REV))
+                self.metadata.invalidate(db)
+                db.commit()
 
-                    # 1.5. provide some feedback
-                    if feedback:
-                        feedback(youngest)
-            finally:
-                # 3. restore permission checking (after 1.)
-                self.repos.authz = authz
+                # 1.5. provide some feedback
+                if feedback:
+                    feedback(youngest)
 
     def get_node(self, path, rev=None):
         return self.repos.get_node(path, self.normalize_rev(rev))
@@ -397,10 +387,9 @@
 
 class CachedChangeset(Changeset):
 
-    def __init__(self, repos, rev, env, authz):
+    def __init__(self, repos, rev, env):
         self.repos = repos
         self.env = env
-        self.authz = authz
         db = self.env.get_db_cnx()
         cursor = db.cursor()
         cursor.execute("SELECT time,author,message FROM revision "
@@ -422,10 +411,6 @@
                        "FROM node_change WHERE repos=%s AND rev=%s "
                        "ORDER BY path", (self.repos.id, str(self.rev)))
         for path, kind, change, base_path, base_rev in cursor:
-            if not self.authz.has_permission(posixpath.join(self.scope,
-                                                            path.strip('/'))):
-                # FIXME: what about the base_path?
-                continue
             kind = _kindmap[kind]
             change = _actionmap[change]
             yield path, kind, change, base_path, base_rev
diff --git a/trac/versioncontrol/svn_authz.py b/trac/versioncontrol/svn_authz.py
--- a/trac/versioncontrol/svn_authz.py
+++ b/trac/versioncontrol/svn_authz.py
@@ -18,34 +18,17 @@
 
 import os.path
 
-from trac.config import Option
+from trac.config import Option, PathOption
 from trac.core import *
-from trac.versioncontrol import Authorizer
+from trac.perm import IPermissionPolicy
+from trac.resource import Resource
+from trac.util import read_file
+from trac.util.compat import all
+from trac.util.text import to_unicode
+from trac.util.translation import _
+from trac.versioncontrol.api import RepositoryManager
 
 
-class SvnAuthzOptions(Component):
-
-    authz_file = Option('trac', 'authz_file', '',
-        """Path to Subversion
-        [http://svnbook.red-bean.com/en/1.1/ch06s04.html#svn-ch-6-sect-4.4.2 authorization (authz) file]
-        """)
-
-    authz_module_name = Option('trac', 'authz_module_name', '',
-        """The module prefix used in the authz_file.""")
-
-
-def SubversionAuthorizer(env, repos, authname):
-    authz_file = env.config.get('trac', 'authz_file')
-    if not authz_file:
-        return Authorizer()
-    if not os.path.isabs(authz_file):
-        authz_file = os.path.join(env.path, authz_file)
-    if not os.path.exists(authz_file):
-        env.log.error('[trac] authz_file (%s) does not exist.' % authz_file)
-
-    module_name = env.config.get('trac', 'authz_module_name')
-    return RealSubversionAuthorizer(repos, authname, module_name, authz_file)
-
 def parent_iter(path):
     path = path.strip('/')
     if path:
@@ -56,114 +39,150 @@
     while 1:
         yield path
         if path == '/':
-            raise StopIteration()
+            return
         path = path[:-1]
         yield path
         idx = path.rfind('/')
         path = path[:idx + 1]
 
 
-class RealSubversionAuthorizer(Authorizer):
-    """FIXME: this should become a IPermissionPolicy, of course.
+class ParseError(Exception):
+    """Exception thrown for parse errors in authz files"""
 
-    `check_permission(username, action, resource)` should be able to
-    replace `has_permission(path)` when resource is a `('source', path)`
-    and `has_permission_for_changeset` when resource is a `('changeset', rev)`.
+
+def parse(authz):
+    """Parse a Subversion authorization file.
+    
+    Return a dict of modules, each containing a dict of paths, each containing
+    a dict mapping users to permissions.
+    """
+    groups = {}
+    aliases = {}
+    sections = {}
+    section = None
+    lineno = 0
+    for line in authz.splitlines():
+        lineno += 1
+        line = to_unicode(line.strip())
+        if not line or line.startswith('#'):
+            continue
+        if line.startswith('[') and line.endswith(']'):
+            section = line[1:-1]
+            continue
+        if section is None:
+            raise ParseError(_('Line %(lineno)d: Entry before first '
+                               'section header', lineno=lineno))
+        parts = line.split('=', 1)
+        if len(parts) != 2:
+            raise ParseError(_('Line %(lineno)d: Invalid entry',
+                               lineno=lineno))
+        name, value = parts
+        name = name.strip()
+        if section == 'groups':
+            group = groups.setdefault(name, set())
+            group.update(each.strip() for each in value.split(','))
+        elif section == 'aliases':
+            aliases[name] = value.strip()
+        else:
+            sections.setdefault(section, []).append((name.strip(), 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 name, items in sections.iteritems():
+        parts = name.split(':', 1)
+        module = authz.setdefault(len(parts) > 1 and parts[0] or '', {})
+        section = module.setdefault(parts[-1], {})
+        for subject, perms in items:
+            for user in resolve(subject, set()):
+                section.setdefault(user, 'r' in perms)  # The first match wins
+    
+    return authz
+        
+
+        
+class AuthzPermissionPolicy(Component):
+    """Permission policy for `source:` resources using a Subversion authz
+    file.
     """
 
-    auth_name = ''
-    module_name = ''
-    conf_authz = None
+    implements(IPermissionPolicy)
+    
+    authz_file = PathOption('trac', 'authz_file', '',
+        """Path to the Subversion
+        [http://svnbook.red-bean.com/en/1.1/ch06s04.html#svn-ch-6-sect-4.4.2 authorization (authz) file]
+        """)
 
-    def __init__(self, repos, auth_name, module_name, cfg_file, cfg_fp=None):
-        self.repos = repos
-        self.auth_name = auth_name
-        self.module_name = module_name
-                                
-        from ConfigParser import ConfigParser
-        self.conf_authz = ConfigParser()
-        if cfg_fp:
-            self.conf_authz.readfp(cfg_fp, cfg_file)
-        elif cfg_file:
-            self.conf_authz.read(cfg_file)
+    authz_module_name = Option('trac', 'authz_module_name', '',
+        """The module prefix used in the `authz_file` for the default
+        repository.
+        """)
 
-        self.groups = self._groups()
+    _mtime = 0
+    
+    # IPermissionPolicy methods
 
-    def has_permission(self, path):
-        if path is None:
-            return 1
-
-        for p in parent_iter(path):
-            if self.module_name:
-                for perm in self._get_section(self.module_name + ':' + p):
+    def check_permission(self, action, username, resource, perm):
+#        print "*** perm=%r resource=%r" % (action, resource)
+        if resource is None:
+            return True
+        if action == 'FILE_VIEW' and resource.realm == 'source':
+            authz = self._authz
+            modules = [resource.parent.id or self.authz_module_name]
+            if modules[0]:
+                modules.append('')
+            for p in parent_iter(resource.id):
+                for module in modules:
+                    section = authz.get(module, {}).get(p, {})
+                    perm = section.get(username)
                     if perm is not None:
                         return perm
-            for perm in self._get_section(p):
-                if perm is not None:
-                    return perm
+                    perm = section.get('*')
+                    if perm is not None:
+                        return perm
+            return False
 
-        return 0
+    @property
+    def _authz(self):
+        # TODO: Inefficient. Check only e.g. once per request
+        mtime = os.path.getmtime(self.authz_file)
+        if mtime > self._mtime or not hasattr(self, '_authz_cache'):
+            self.log.debug('Parsing authz file %s')
+            self._mtime = mtime
+            # TODO: Handle parse errors
+            self._authz_cache = parse(read_file(self.authz_file))
+        return self._authz_cache
 
-    def has_permission_for_changeset(self, rev):
-        changeset = self.repos.get_changeset(rev)
-        for change in changeset.get_changes():
-            # the repository checks permissions for each change, so just check
-            # if any changes can be accessed
-            return 1
-        return 0
 
-    # Internal API
+class ChangesetPermissionPolicy(Component):
+    """Default permission policy for changesets.
+    
+    This permission policy denies access to changesets containing at least
+    one file, and where access to all files is denied.
+    """
 
-    def _groups(self):
-        if not self.conf_authz.has_section('groups'):
-            return []
-
-        grp_parents = {}
-        usr_grps = []
-
-        for group in self.conf_authz.options('groups'):
-            for member in self.conf_authz.get('groups', group).split(','):
-                member = member.strip()
-                if member == self.auth_name:
-                    usr_grps.append(group)
-                elif member.startswith('@'):
-                    grp_parents.setdefault(member[1:], []).append(group)
-
-        expanded = {}
-
-        def expand_group(group):
-            if group in expanded:
-                return
-            expanded[group] = True
-            for parent in grp_parents.get(group, []):
-                expand_group(parent)
-
-        for g in usr_grps:
-            expand_group(g)
-
-        # expand groups
-        return expanded.keys()
-
-    def _get_section(self, section):
-        if not self.conf_authz.has_section(section):
+    implements(IPermissionPolicy)
+    
+    def check_permission(self, action, username, resource, perm):
+        if resource is None:
             return
-
-        yield self._get_permission(section, self.auth_name)
-
-        group_perm = None
-        for g in self.groups:
-            p = self._get_permission(section, '@' + g)
-            if p is not None:
-                group_perm = p
-
-            if group_perm:
-                yield 1
-
-        yield group_perm
-
-        yield self._get_permission(section, '*')
-
-    def _get_permission(self, section, subject):
-        if self.conf_authz.has_option(section, subject):
-            return 'r' in self.conf_authz.get(section, subject)
-        return None
+        if action == 'CHANGESET_VIEW' and resource.realm == 'changeset':
+            rm = RepositoryManager(self.env)
+            repos = rm.get_repository(resource.parent.id)
+            changes = list(repos.get_changeset(resource.id).get_changes())
+            if changes:
+                source = Resource('source', version=resource.id,
+                                  parent=resource.parent)
+                if all('FILE_VIEW' not in perm(source(id=change[0]))
+                       for change in changes):
+                    return False
+    
\ No newline at end of file
diff --git a/trac/versioncontrol/svn_fs.py b/trac/versioncontrol/svn_fs.py
--- a/trac/versioncontrol/svn_fs.py
+++ b/trac/versioncontrol/svn_fs.py
@@ -54,7 +54,6 @@
                                 IRepositoryConnector, \
                                 NoSuchChangeset, NoSuchNode
 from trac.versioncontrol.cache import CachedRepository
-from trac.versioncontrol.svn_authz import SubversionAuthorizer
 from trac.util import embedded_numbers
 from trac.util.text import exception_to_unicode, to_unicode
 from trac.util.translation import _
@@ -280,17 +279,12 @@
             self._version = self._get_version()
             self.env.systeminfo.append(('Subversion', self._version))
         params.update(tags=self.tags, branches=self.branches)
-        fs_repos = SubversionRepository(dir, params, None, self.log)
+        fs_repos = SubversionRepository(dir, params, self.log)
         if type == 'direct-svnfs':
             repos = fs_repos
         else:
-            repos = CachedRepository(self.env, fs_repos, None, self.log)
+            repos = CachedRepository(self.env, fs_repos, self.log)
             repos.has_linear_changesets = True
-        # FIXME: convert SubversionAuthorizer to a PermissionPolicy
-        if 'authname' in params:
-            authz = SubversionAuthorizer(self.env, weakref.proxy(repos),
-                                         params['authname'])
-            repos.authz = fs_repos.authz = authz
         return repos
 
     def _get_version(self):
@@ -305,7 +299,7 @@
 class SubversionRepository(Repository):
     """Repository implementation based on the svn.fs API."""
 
-    def __init__(self, path, params, authz, log):
+    def __init__(self, path, params, log):
         self.log = log
         self.pool = Pool()
         
@@ -335,7 +329,7 @@
         self.base = 'svn:%s:%s' % (self.uuid, _from_svn(root_path_utf8))
         name = 'svn:%s:%s' % (self.uuid, self.path)
 
-        Repository.__init__(self, name, params, authz, log)
+        Repository.__init__(self, name, params, log)
 
         # if root_path_utf8 is shorter than the path_utf8, the difference is
         # this scope (which always starts with a '/')
@@ -430,16 +424,13 @@
     
     def get_changeset(self, rev):
         rev = self.normalize_rev(rev)
-        return SubversionChangeset(rev, self.authz, self.scope,
-                                   self.fs_ptr, self.pool)
+        return SubversionChangeset(rev, self.scope, self.fs_ptr, self.pool)
 
     def get_changeset_uid(self, rev):
         return (self.uuid, rev)
 
     def get_node(self, path, rev=None):
         path = path or ''
-        self.authz.assert_permission(posixpath.join(self.scope,
-                                                    path.strip('/')))
         if path and path[-1] == '/':
             path = path[:-1]
 
@@ -490,8 +481,6 @@
                 if rev < end:
                     break
                 path = _from_svn(path_utf8)
-                if not self.authz.has_permission(path):
-                    break
                 yield path, rev
         del tmp1
         del tmp2
@@ -669,7 +658,6 @@
     def __init__(self, path, rev, repos, pool=None, parent_root=None):
         self.repos = repos
         self.fs_ptr = repos.fs_ptr
-        self.authz = repos.authz
         self.scope = repos.scope
         self._scoped_path_utf8 = _to_svn(self.scope, path)
         self.pool = Pool(pool)
@@ -719,9 +707,6 @@
         entries = fs.dir_entries(self.root, self._scoped_path_utf8, pool())
         for item in entries.keys():
             path = posixpath.join(self.path, _from_svn(item))
-            if not self.authz.has_permission(posixpath.join(self.scope,
-                                                            path.strip('/'))):
-                continue
             yield SubversionNode(path, self._requested_rev, self.repos,
                                  self.pool, self.root)
 
@@ -846,9 +831,8 @@
 
 class SubversionChangeset(Changeset):
 
-    def __init__(self, rev, authz, scope, fs_ptr, pool=None):
+    def __init__(self, rev, scope, fs_ptr, pool=None):
         self.rev = rev
-        self.authz = authz
         self.scope = scope
         self.fs_ptr = fs_ptr
         self.pool = Pool(pool)
@@ -896,8 +880,7 @@
             path = _from_svn(path_utf8)
 
             # Filtering on `path`
-            if not (_is_path_within_scope(self.scope, path) and
-                    self.authz.has_permission(path)):
+            if not _is_path_within_scope(self.scope, path):
                 continue
 
             path_utf8 = change.path
@@ -907,8 +890,7 @@
             base_rev = change.base_rev
 
             # Ensure `base_path` is within the scope
-            if not (_is_path_within_scope(self.scope, base_path) and
-                    self.authz.has_permission(base_path)):
+            if not _is_path_within_scope(self.scope, base_path):
                 base_path, base_rev = None, -1
 
             # Determine the action
diff --git a/trac/versioncontrol/templates/dir_entries.html b/trac/versioncontrol/templates/dir_entries.html
--- a/trac/versioncontrol/templates/dir_entries.html
+++ b/trac/versioncontrol/templates/dir_entries.html
@@ -6,7 +6,9 @@
     <xi:include href="macros.html" />
   </py:if>
   <py:for each="idx, entry in enumerate(dir.entries)">
-    <py:with vars="change = dir.changes[entry.rev]">
+    <py:with vars="change = dir.changes[entry.rev];
+                   chgset_context = change and context('changeset', change.rev, parent=repos_resource);
+                   chgset_view = change and 'CHANGESET_VIEW' in perm(chgset_context.resource)">
       <tr class="${idx % 2 and 'even' or 'odd'}">
         <td class="name">
           <a class="$entry.kind" title="View ${entry.kind.capitalize()}"
@@ -18,20 +20,21 @@
           <a title="View Revision Log" href="${href.log(reponame, entry.path, rev=rev)}">$entry.rev</a>
           <a title="View Changeset" class="chgset" href="${href.changeset(change.rev, reponame)}">&nbsp;</a>
         </td>
-        <td class="age" style="${change and dir.timerange and 'border-color: rgb(%s,%s,%s)' %
+        <td class="age" style="${chgset_view and dir.timerange and 'border-color: rgb(%s,%s,%s)' %
                                  dir.colorize_age(dir.timerange.relative(change.date)) or None}">
-          ${change and dateinfo(change.date) or '-'}
+          ${('&ndash;', dateinfo(change.date))[chgset_view]}
         </td>
-        <td class="change">
-          <span class="author" py:if="change">${authorinfo(change.author)}:</span>
-          <span class="change" py:choose=""
-                py:with="chgset_context = context('changeset', change.rev, parent=repos_resource)">
-            <py:when test="not change or 'CHANGESET_VIEW' not in perm(chgset_context.resource)">-</py:when>
-            <py:when test="wiki_format_messages">
-              ${change and wiki_to_oneliner(chgset_context, change.message, shorten=True)}
-            </py:when>
-            <py:otherwise>${change and shorten_line(change.message)}</py:otherwise>
-          </span>
+        <td class="change" py:choose="">
+          <py:when test="chgset_view">
+            <span class="author">${authorinfo(change.author)}:</span>
+            <span class="change" py:choose="">
+              <py:when test="wiki_format_messages">
+                ${wiki_to_oneliner(chgset_context, change.message, shorten=True)}
+              </py:when>
+              <py:otherwise>${shorten_line(change.message)}</py:otherwise>
+            </span>
+          </py:when>
+          <py:otherwise>&ndash;</py:otherwise>
         </td>
       </tr>
     </py:with>
diff --git a/trac/versioncontrol/templates/repository_index.html b/trac/versioncontrol/templates/repository_index.html
--- a/trac/versioncontrol/templates/repository_index.html
+++ b/trac/versioncontrol/templates/repository_index.html
@@ -5,7 +5,9 @@
   <table class="listing dirlist" id="${repoindex or None}">
     <xi:include href="dirlist_thead.html" />
     <tbody>
-      <py:for each="idx, (reponame, repoinfo, change, err) in enumerate(repo.repositories)">
+      <py:for each="idx, (reponame, repoinfo, change, err) in enumerate(repo.repositories)"
+              py:with="chgset_context = change and context('changeset', change.rev, parent=Resource('repository', reponame));
+                       chgset_view = change and 'CHANGESET_VIEW' in perm(chgset_context.resource)">
         <tr class="${idx % 2 and 'even' or 'odd'}">
           <td class="name">
             <em py:strip="not err">
@@ -22,21 +24,22 @@
               <a title="View Changeset" class="chgset" href="${href.changeset(change.rev, reponame)}">&nbsp;</a>
             </py:if>
           </td>
-          <td class="age" style="${change and repo.timerange and 'border-color: rgb(%s,%s,%s)' %
-            repo.colorize_age(repo.timerange.relative(change.date)) or None}">
-            ${change and dateinfo(change.date) or '-'}
+          <td class="age" style="${chgset_view and change and repo.timerange and 'border-color: rgb(%s,%s,%s)' %
+                                   repo.colorize_age(repo.timerange.relative(change.date)) or None}">
+            ${('&ndash;', dateinfo(change.date))[chgset_view]}
           </td>
-          <td class="change">
-            <span class="author" py:if="change">${authorinfo(change.author)}:</span>
-            <span class="change" py:choose=""
-                  py:with="chgset_context = context('changeset', change.rev, parent=Resource('repository', reponame))">
-              <em py:when="err" py:content="err" />
-              <py:when test="not change or 'CHANGESET_VIEW' not in perm(chgset_context.resource)">-</py:when>
-              <py:when test="wiki_format_messages">
-                ${change and wiki_to_oneliner(chgset_context, change.message, shorten=True)}
-              </py:when>
-              <py:otherwise>${change and shorten_line(change.message)}</py:otherwise>
-            </span>
+          <td class="change" py:choose="">
+            <span py:when="err" class="change"><em py:content="err"></em></span>
+            <py:when test="chgset_view">
+              <span class="author">${authorinfo(change.author)}:</span>
+              <span class="change" py:choose="">
+                <py:when test="wiki_format_messages">
+                  ${wiki_to_oneliner(chgset_context, change.message, shorten=True)}
+                </py:when>
+                <py:otherwise>${shorten_line(change.message)}</py:otherwise>
+              </span>
+            </py:when>
+            <py:otherwise>&ndash;</py:otherwise>
           </td>
         </tr>
         <tr class="${idx % 2 and 'even' or 'odd'}" py:if="repoinfo.description">
diff --git a/trac/versioncontrol/templates/revisionlog.html b/trac/versioncontrol/templates/revisionlog.html
--- a/trac/versioncontrol/templates/revisionlog.html
+++ b/trac/versioncontrol/templates/revisionlog.html
@@ -111,7 +111,6 @@
               <py:with vars="change = changes[item.rev];
                              is_separator = item.change is None;
                              chgset_context = context('changeset', change.rev, parent=repos_resource);
-                             chgset_view = 'CHANGESET_VIEW' in perm(chgset_context.resource);
                              odd_even = idx % 2 and 'odd' or 'even'">
                 <!--! highlight copy or rename operations -->
                 <tr py:if="not is_separator and item.get('copyfrom_path')" class="$odd_even">
@@ -148,7 +147,7 @@
                     <td class="age" py:content="dateinfo(change.date)" />
                     <td class="author" py:content="authorinfo(change.author)" />
                     <td class="summary" py:choose="">
-                      <py:when test="verbose or not chgset_view"></py:when>
+                      <py:when test="verbose"></py:when>
                       <py:when test="wiki_format_messages">
                         ${wiki_to_oneliner(chgset_context, change.message, shorten=True)}
                       </py:when>
diff --git a/trac/versioncontrol/templates/revisionlog.txt b/trac/versioncontrol/templates/revisionlog.txt
--- a/trac/versioncontrol/templates/revisionlog.txt
+++ b/trac/versioncontrol/templates/revisionlog.txt
@@ -8,8 +8,10 @@
 {%   with change = changes[item.rev]; extra = extra_changes[item.rev] %}\
 ${http_date(change.date)} ${format_author(change.author)} [$item.rev]
 {%     for idx, file in enumerate(extra.files) %}\
+{%       if 'FILE_VIEW' in perm(context('source', file, version=change.rev, parent=repos_resource).resource) %}\
 	* $file (${dict(edit='modified', add='added', delete='deleted',
                         copy='copied', move='moved')[extra.actions[idx]]})
+{%       end %}\
 {%     end %}\
 
 ${verbose and extra.message or shorten_line(extra.message)}
diff --git a/trac/versioncontrol/tests/api.py b/trac/versioncontrol/tests/api.py
--- a/trac/versioncontrol/tests/api.py
+++ b/trac/versioncontrol/tests/api.py
@@ -22,7 +22,7 @@
 
     def setUp(self):
         self.repo_base = Repository('testrepo', {'name': 'testrepo', 'id': 1},
-                                    None, None)
+                                    None)
 
     def test_raise_NotImplementedError_close(self):
         self.failUnlessRaises(NotImplementedError, self.repo_base.close)
diff --git a/trac/versioncontrol/tests/cache.py b/trac/versioncontrol/tests/cache.py
--- a/trac/versioncontrol/tests/cache.py
+++ b/trac/versioncontrol/tests/cache.py
@@ -44,13 +44,13 @@
             raise NoSuchChangeset(rev)
             
         repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1},
-                     None, self.log,
+                     self.log,
                      get_changeset=no_changeset,
                      get_oldest_rev=lambda: 1,
                      get_youngest_rev=lambda: 0,
                      normalize_rev=no_changeset,
                      next_rev=lambda x: None)
-        cache = CachedRepository(self.env, repos, None, self.log)
+        cache = CachedRepository(self.env, repos, self.log)
         cache.sync()
 
         cursor = self.db.cursor()
@@ -69,13 +69,13 @@
                       Mock(Changeset, 1, 'Import', 'joe', t2,
                            get_changes=lambda: iter(changes))]
         repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1},
-                     None, self.log,
+                     self.log,
                      get_changeset=lambda x: changesets[int(x)],
                      get_oldest_rev=lambda: 0,
                      get_youngest_rev=lambda: 1,
                      normalize_rev=lambda x: x,
                      next_rev=lambda x: int(x) == 0 and 1 or None)
-        cache = CachedRepository(self.env, repos, None, self.log)
+        cache = CachedRepository(self.env, repos, self.log)
         cache.sync()
 
         cursor = self.db.cursor()
@@ -114,13 +114,13 @@
         changeset = Mock(Changeset, 2, 'Update', 'joe', t3,
                          get_changes=lambda: iter(changes))
         repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1},
-                     None, self.log,
+                     self.log,
                      get_changeset=lambda x: changeset,
                      get_youngest_rev=lambda: 2,
                      get_oldest_rev=lambda: 0,
                      normalize_rev=lambda x: x,                    
                      next_rev=lambda x: x and int(x) == 1 and 2 or None)
-        cache = CachedRepository(self.env, repos, None, self.log)
+        cache = CachedRepository(self.env, repos, self.log)
         cache.sync()
 
         cursor = self.db.cursor()
@@ -152,13 +152,13 @@
                        "WHERE id=1 AND name='youngest_rev'")
 
         repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1},
-                     None, self.log,
+                     self.log,
                      get_changeset=lambda x: None,
                      get_youngest_rev=lambda: 1,
                      get_oldest_rev=lambda: 0,
                      next_rev=lambda x: None,
                      normalize_rev=lambda rev: rev)
-        cache = CachedRepository(self.env, repos, None, self.log)
+        cache = CachedRepository(self.env, repos, self.log)
         self.assertEqual('1', cache.youngest_rev)
         changeset = cache.get_changeset(1)
         self.assertEqual('joe', changeset.author)
diff --git a/trac/versioncontrol/tests/svn_authz.py b/trac/versioncontrol/tests/svn_authz.py
--- a/trac/versioncontrol/tests/svn_authz.py
+++ b/trac/versioncontrol/tests/svn_authz.py
@@ -1,221 +1,268 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+
+import os.path
+import tempfile
 import unittest
-import sys
 
-def tests():
-  """
-  Subversion Authz File Permissions
-  =================================
-  
-  Setup code
-  ----------
-  We'll use the ``make_auth`` method to create Authorizer objects
-  for testing the use of authz files.  ``make_auth`` takes a module name
-  and a string for the authz configuration contents.
-  
-  >>> from trac.versioncontrol.svn_authz import RealSubversionAuthorizer
-  >>> from StringIO import StringIO
-  >>> make_auth = lambda mod, cfg: RealSubversionAuthorizer(None,
-  ...                   'user', mod, None, StringIO(cfg))
-  
-  
-  Simple operation
-  ----------------
-  Returns 1 if no path is given:
-      >>> int(make_auth('', '').has_permission(None))
-      1
-  
-  By default read permission is not enabled:
-      >>> int(make_auth('', '').has_permission('/'))
-      0
-  
-  Read and Write Permissions
-  ----------------------
-  Trac is only concerned about read permissions.
-      >>> a = make_auth('', '''
-      ... [/readonly]
-      ... user = r
-      ... [/writeonly]
-      ... user = w
-      ... [/readwrite]
-      ... user = rw
-      ... [/empty]
-      ... user = 
-      ... ''')
-  
-  Permissions of 'r' or 'rw' will allow access:
-      >>> int(a.has_permission('/readonly'))
-      1
-      >>> int(a.has_permission('/readwrite'))
-      1
-  
-  If only 'w' permission is given, Trac does not allow access:
-      >>> int(a.has_permission('/writeonly'))
-      0
-  
-  And an empty permission does not give access:
-      >>> int(a.has_permission('/empty'))
-      0
-  
-  Trailing Slashes
-  ----------------
-  Checks all combinations of trailing slashes in the configuration
-  or in the path parameter:
-      >>> a = make_auth('', '''
-      ... [/a]
-      ... user = r
-      ... [/b/]
-      ... user = r
-      ... ''')
-      >>> int(a.has_permission('/a'))
-      1
-      >>> int(a.has_permission('/a/'))
-      1
-      >>> int(a.has_permission('/b'))
-      1
-      >>> int(a.has_permission('/b/'))
-      1
-  
-  
-  Module Usage
-  ------------
-  If a module name is specified, the rules used are specific to the module.
-      >>> a = make_auth('module', '''
-      ... [module:/a]
-      ... user = r
-      ... [other:/b]
-      ... user = r
-      ... ''')
-      >>> int(a.has_permission('/a'))
-      1
-      >>> int(a.has_permission('/b'))
-      0
-  
-  If a module is specified, but the configuration contains a non-module
-  path, the non-module path can still apply:
-      >>> int(make_auth('module', '''
-      ... [/a]
-      ... user = r
-      ... ''').has_permission('/a'))
-      1
-  
-  However, the module-specific rule will take precedence if both exist:
-      >>> int(make_auth('module', '''
-      ... [module:/a]
-      ... user = 
-      ... [/a]
-      ... user = r
-      ... ''').has_permission('/a'))
-      0
-  
-  
-  Groups and Wildcards
-  --------------------
-  Authz provides a * wildcard for matching any user:
-      >>> int(make_auth('', '''
-      ... [/a]
-      ... * = r
-      ... ''').has_permission('/a'))
-      1
-  
-  Groups are specified in a separate section and used with an @ prefix:
-      >>> int(make_auth('', '''
-      ... [groups]
-      ... grp = user
-      ... [/a]
-      ... @grp = r
-      ... ''').has_permission('/a'))
-      1
+from trac.resource import Resource
+from trac.test import EnvironmentStub
+from trac.util import create_file
+from trac.versioncontrol.svn_authz import AuthzPermissionPolicy, ParseError, \
+                                          parse
 
-  Groups can also be members of other groups:
-      >>> int(make_auth('', '''
-      ... [groups]
-      ... grp1 = user
-      ... grp2 = @grp1
-      ... [/a]
-      ... @grp2 = r
-      ... ''').has_permission('/a'))
-      1
 
-  Groups should not be defined cyclically, but they are handled appropriately
-  to avoid infinite loops:
-      >>> int(make_auth('', '''
-      ... [groups]
-      ... grp1 = @grp2
-      ... grp2 = @grp3
-      ... grp3 = @grp1, user
-      ... [/a]
-      ... @grp1 = r
-      ... ''').has_permission('/a'))
-      1
-  
-  If more than one group matches at the specific path, access is granted
-  if any of the group rules allow access.
-      >>> a = make_auth('', '''
-      ... [groups]
-      ... grp1 = user
-      ... grp2 = user
-      ... [/a]
-      ... @grp1 = r
-      ... @grp2 = 
-      ... [/b]
-      ... @grp1 = 
-      ... @grp2 = r
-      ... ''')
-      >>> int(a.has_permission('/a'))
-      1
-      >>> int(a.has_permission('/b'))
-      1
-  
-  
-  Precedence
-  ----------
-  Precedence is user, group, then *:
-      >>> a = make_auth('', '''
-      ... [groups]
-      ... grp = user
-      ... [/a]
-      ... @grp = r
-      ... user = 
-      ... [/b]
-      ... * = r
-      ... @grp = 
-      ... ''')
-  
-  User specific permission overrides the group permission:
-      >>> int(a.has_permission('/a'))
-      0
-  
-  And group permission overrides the * permission:
-      >>> int(a.has_permission('/b'))
-      0
-  
-  The most specific matching path takes precedence:
-      >>> a = make_auth('', '''
-      ... [/]
-      ... * = r
-      ... [/b]
-      ... user = 
-      ... ''')
-      >>> int(a.has_permission('/'))
-      1
-      >>> int(a.has_permission('/a'))
-      1
-      >>> int(a.has_permission('/b'))
-      0
-  
-  Changeset Permissions
-  ---------------------
-  A test should go here for the changeset permissions.
-  """
+class AuthzParserTestCase(unittest.TestCase):
+
+    def test_parse_file(self):
+        authz = parse("""\
+[groups]
+developers = foo, bar
+users = @developers, &baz
+
+[aliases]
+baz = CN=Hàröld Hacker,OU=Enginéers,DC=red-bean,DC=com
+
+# Applies to all repositories
+[/]
+* = r
+
+[/trunk]
+@developers = rw
+&baz = 
+@users = r
+
+[/branches]
+bar = rw
+
+# Applies only to module
+[module:/trunk]
+foo = rw
+&baz = r
+""")
+        self.assertEqual({
+            '': {
+                '/': {
+                    '*': True,
+                },
+                '/trunk': {
+                    'foo': True,
+                    'bar': True,
+                    u'CN=Hàröld Hacker,OU=Enginéers,DC=red-bean,DC=com': False,
+                },
+                '/branches': {
+                    'bar': True,
+                },
+            },
+            'module': {
+                '/trunk': {
+                    'foo': True,
+                    u'CN=Hàröld Hacker,OU=Enginéers,DC=red-bean,DC=com': True,
+                },
+            },
+        }, authz)
+
+    def test_parse_errors(self):
+        self.assertRaises(ParseError, parse, """\
+user = r
+
+[module:/trunk]
+user = r
+""")
+        self.assertRaises(ParseError, parse, """\
+[module:/trunk]
+user
+""")
+
+
+class AuthzPolicyTestCase(unittest.TestCase):
+
+    def setUp(self):
+        tmpdir = os.path.realpath(tempfile.gettempdir())
+        self.authz = os.path.join(tmpdir, 'trac-authz')
+        create_file(self.authz, """\
+[groups]
+group1 = user
+group2 = @group1
+
+cycle1 = @cycle2
+cycle2 = @cycle3
+cycle3 = @cycle1, user
+
+alias1 = &jekyll
+alias2 = @alias1
+
+[aliases]
+jekyll = Mr Hyde
+
+# Read / write permissions
+[/readonly]
+user = r
+[/writeonly]
+user = w
+[/readwrite]
+user = rw
+[/empty]
+user =
+
+# Trailing slashes
+[/trailing_a]
+user = r
+[/trailing_b/]
+user = r
+
+# Sub-paths
+[/sub/path]
+user = r
+
+# Module usage
+[module:/module_a]
+user = r
+[other:/module_b]
+user = r
+[/module_c]
+user = r
+[module:/module_d]
+user =
+[/module_d]
+user = r
+
+# Wildcards
+[/wildcard]
+* = r
+
+# Groups
+[/groups_a]
+@group1 = r
+[/groups_b]
+@group2 = r
+[/cyclic]
+@cycle1 = r
+
+# Precedence
+[module:/precedence_a]
+user =
+[/precedence_a]
+user = r
+[/precedence_b]
+user = r
+[/precedence_b/sub]
+user =
+[/precedence_b/sub/test]
+user = r
+[/precedence_c]
+user =
+@group1 = r
+[/precedence_d]
+@group1 = r
+user =
+
+# Aliases
+[/aliases_a]
+&jekyll = r
+[/aliases_b]
+@alias2 = r
+""")
+        self.env = EnvironmentStub(enable=[AuthzPermissionPolicy])
+        self.env.config.set('trac', 'authz_file', self.authz)
+        self.policy = AuthzPermissionPolicy(self.env)
+
+    def tearDown(self):
+        self.env.reset_db()
+        os.remove(self.authz)
+
+    def assertPermission(self, result, user, reponame, path):
+        """Assert that `user` is granted access `result` to `path` within
+        the repository `reponame`.
+        """
+        resource = Resource('source', path,
+                            parent=Resource('repository', reponame))
+        check = self.policy.check_permission('FILE_VIEW', user, resource, None)
+        self.assertEqual(result, check)
+        
+    def test_default_permission(self):
+        # By default, no permission is granted
+        self.assertPermission(False, 'joe', '', '/not_defined')
+        self.assertPermission(False, 'jane', 'repo', '/not/defined/either')
+
+    def test_read_write(self):
+        # Allow 'r' and 'rw' entries, deny 'w' and empty entries
+        self.assertPermission(True, 'user', '', '/readonly')
+        self.assertPermission(True, 'user', '', '/readwrite')
+        self.assertPermission(False, 'user', '', '/writeonly')
+        self.assertPermission(False, 'user', '', '/empty')
+
+    def test_trailing_slashes(self):
+        # Combinations of trailing slashes in the file and in the path
+        self.assertPermission(True, 'user', '', '/trailing_a')
+        self.assertPermission(True, 'user', '', '/trailing_a/')
+        self.assertPermission(True, 'user', '', '/trailing_b')
+        self.assertPermission(True, 'user', '', '/trailing_b/')
+
+    def test_sub_path(self):
+        # Permissions are inherited from containing directories
+        self.assertPermission(True, 'user', '', '/sub/path')
+        self.assertPermission(True, 'user', '', '/sub/path/test')
+        self.assertPermission(True, 'user', '', '/sub/path/other/sub')
+        
+    def test_module_usage(self):
+        # If a module name is specified, the rules are specific to the module
+        self.assertPermission(True, 'user', 'module', '/module_a')
+        self.assertPermission(False, 'user', 'module', '/module_b')
+        # If a module is specified, but the configuration contains a non-module
+        # path, the non-module path can still apply
+        self.assertPermission(True, 'user', 'module', '/module_c')
+        # The module-specific rule takes precedence
+        self.assertPermission(False, 'user', 'module', '/module_d')
+
+    def test_wildcard(self):
+        # The * wildcard matches all users
+        self.assertPermission(True, 'joe', '', '/wildcard')
+        self.assertPermission(True, 'jane', '', '/wildcard')
+
+    def test_groups(self):
+        # Groups are specified in a separate section and used with an @ prefix
+        self.assertPermission(True, 'user', '', '/groups_a')
+        # Groups can also be members of other groups
+        self.assertPermission(True, 'user', '', '/groups_b')
+        # Groups should not be defined cyclically, but they are still handled
+        # correctly to avoid infinite loops
+        self.assertPermission(True, 'user', '', '/cyclic')
+
+    def test_precedence(self):
+        # Module-specific sections take precedence over non-module sections
+        self.assertPermission(False, 'user', 'module', '/precedence_a')
+        # The most specific section applies
+        self.assertPermission(True, 'user', '', '/precedence_b/sub/test')
+        self.assertPermission(False, 'user', '', '/precedence_b/sub')
+        self.assertPermission(True, 'user', '', '/precedence_b')
+        # Within a section, the first matching rule applies
+        self.assertPermission(False, 'user', '', '/precedence_c')
+        self.assertPermission(True, 'user', '', '/precedence_d')
+
+    def test_aliases(self):
+        # Aliases are specified in a separate section and used with an & prefix
+        self.assertPermission(True, 'Mr Hyde', '', '/aliases_a')
+        # Aliases can also be used in groups
+        self.assertPermission(True, 'Mr Hyde', '', '/aliases_b')
+
 
 def suite():
-    try:
-        from doctest import DocTestSuite
-        return DocTestSuite(sys.modules[__name__])
-    except ImportError:
-        print >> sys.stderr, "WARNING: DocTestSuite required to run these " \
-                             "tests"
-    return unittest.TestSuite()
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(AuthzParserTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(AuthzPolicyTestCase, 'test'))
+    return suite
+
 
 if __name__ == '__main__':
     runner = unittest.TextTestRunner()
diff --git a/trac/versioncontrol/tests/svn_fs.py b/trac/versioncontrol/tests/svn_fs.py
--- a/trac/versioncontrol/tests/svn_fs.py
+++ b/trac/versioncontrol/tests/svn_fs.py
@@ -87,7 +87,7 @@
     def setUp(self):
         self.repos = SubversionRepository(REPOS_PATH,
                                           {'name': 'repo', 'id': 1},
-                                          None, logger_factory('test'))
+                                          logger_factory('test'))
 
     def tearDown(self):
         self.repos = None
@@ -510,7 +510,7 @@
     def setUp(self):
         self.repos = SubversionRepository(REPOS_PATH + u'/tête',
                                           {'name': 'repo', 'id': 1},
-                                          None, logger_factory('test'))
+                                          logger_factory('test'))
 
     def tearDown(self):
         self.repos = None
@@ -760,7 +760,7 @@
     def setUp(self):
         self.repos = SubversionRepository(REPOS_PATH + u'/tête/dir1',
                                           {'name': 'repo', 'id': 1},
-                                          None, logger_factory('test'))
+                                          logger_factory('test'))
 
     def tearDown(self):
         self.repos = None
@@ -781,7 +781,7 @@
     def setUp(self):
         self.repos = SubversionRepository(REPOS_PATH + '/tags/v1',
                                           {'name': 'repo', 'id': 1},
-                                          None, logger_factory('test'))
+                                          logger_factory('test'))
 
     def tearDown(self):
         self.repos = None
@@ -800,7 +800,7 @@
     def setUp(self):
         self.repos = SubversionRepository(REPOS_PATH + '/branches',
                                           {'name': 'repo', 'id': 1},
-                                          None, logger_factory('test'))
+                                          logger_factory('test'))
 
     def tearDown(self):
         self.repos = None
diff --git a/trac/versioncontrol/web_ui/browser.py b/trac/versioncontrol/web_ui/browser.py
--- a/trac/versioncontrol/web_ui/browser.py
+++ b/trac/versioncontrol/web_ui/browser.py
@@ -332,7 +332,7 @@
         xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest'
         
         rm = RepositoryManager(self.env)
-        reponame, repos, path = rm.get_repository_by_path(path, req.authname)
+        reponame, repos, path = rm.get_repository_by_path(path)
 
         # Repository index
         all_repositories = None
@@ -480,7 +480,7 @@
             if not reponame or repoinfo.get('hidden') in _TRUE_VALUES:
                 continue
             try:
-                repos = rm.get_repository(reponame, context.perm.username)
+                repos = rm.get_repository(reponame)
                 if repos:
                     youngest = repos.get_changeset(repos.youngest_rev)
                     if self.color_scale and youngest:
@@ -518,7 +518,11 @@
                 for f in entry.__slots__:
                     setattr(self, f, getattr(node, f))
                 
-        entries = [entry(n) for n in node.get_entries()]
+        repos_resource = Resource('repository', reponame)
+        child = repos_resource.child('source')
+        entries = [entry(n) for n in node.get_entries()
+                   if 'FILE_VIEW' in req.perm(child(id=n.created_path,
+                                                    version=n.created_rev))]
         changes = get_changes(repos, [i.rev for i in entries])
 
         if rev:
diff --git a/trac/versioncontrol/web_ui/changeset.py b/trac/versioncontrol/web_ui/changeset.py
--- a/trac/versioncontrol/web_ui/changeset.py
+++ b/trac/versioncontrol/web_ui/changeset.py
@@ -229,14 +229,13 @@
 
         rm = RepositoryManager(self.env)
         if reponame:
-            repos = rm.get_repository(reponame, req.authname)
+            repos = rm.get_repository(reponame)
         else:
-            reponame, repos, new_path = rm.get_repository_by_path(
-                    new_path, req.authname)
+            reponame, repos, new_path = rm.get_repository_by_path(new_path)
 
             if old_path:
-                old_reponame, old_repos, old_path = rm.get_repository_by_path(
-                        old_path, req.authname)
+                old_reponame, old_repos, old_path = \
+                    rm.get_repository_by_path(old_path)
                 if old_repos != repos:
                     raise TracError(_("Can't compare across different "
                                       "repositories: %(old)s vs. %(new)s",
@@ -254,9 +253,6 @@
         try:
             new_path = repos.normalize_path(new_path)
             new = repos.normalize_rev(new)
-            
-            repos.authz.assert_permission_for_changeset(new)
-            
             old_path = repos.normalize_path(old_path or new_path)
             old = repos.normalize_rev(old or new)
         except NoSuchChangeset, e:
@@ -500,18 +496,9 @@
         options = data['diff']['options']
         repos_resource = Resource('repository', reponame)
 
-        def _prop_changes(old_node, new_node):
-            old_source = Resource('source', old_node.created_path,
-                                  version=old_node.created_rev,
-                                  parent=repos_resource)
-            new_source = Resource('source', new_node.created_path,
-                                  version=new_node.created_rev,
-                                  parent=repos_resource)
-            old_props = new_props = []
-            if 'FILE_VIEW' in req.perm(old_source):
-                old_props = old_node.get_properties()
-            if 'FILE_VIEW' in req.perm(new_source):
-                new_props = new_node.get_properties()
+        def _prop_changes(old_node, old_source, new_node, new_source):
+            old_props = old_node.get_properties()
+            new_props = new_node.get_properties()
             old_ctx = Context.from_request(req, old_source)
             new_ctx = Context.from_request(req, new_source)
             changed_properties = []
@@ -613,12 +600,21 @@
         for old_node, new_node, kind, change in get_changes():
             props = []
             diffs = []
+            old_source = old_node and Resource('source', old_node.created_path,
+                                               version=old_node.created_rev,
+                                               parent=repos_resource)
+            new_source = new_node and Resource('source', new_node.created_path,
+                                               version=new_node.created_rev,
+                                               parent=repos_resource)
+            show_old = old_node and 'FILE_VIEW' in req.perm(old_source)
+            show_new = new_node and 'FILE_VIEW' in req.perm(new_source)
             show_entry = change != Changeset.EDIT
             show_diff = show_diffs or (new_node and new_node.path == annotated)
 
-            if change in Changeset.DIFF_CHANGES and 'FILE_VIEW' in req.perm:
+            if change in Changeset.DIFF_CHANGES and show_old and show_new:
                 assert old_node and new_node
-                props = _prop_changes(old_node, new_node)
+                props = _prop_changes(old_node, old_source, new_node,
+                                      new_source)
                 if props:
                     show_entry = True
                 if kind == Node.FILE and show_diff:
@@ -628,7 +624,7 @@
                             has_diffs = True
                         # elif None (means: manually compare to (previous))
                         show_entry = True
-            if show_entry or not show_diff:
+            if (show_old or show_new) and (show_entry or not show_diff):
                 info = {'change': change,
                         'old': old_node and node_info(old_node, annotated),
                         'new': new_node and node_info(new_node, annotated),
@@ -933,7 +929,7 @@
             for reponame in rm.get_all_repositories():
                 if all_repos or ('repo-' + reponame) in repo_filters:
                     try:
-                        repos = rm.get_repository(reponame, req.authname)
+                        repos = rm.get_repository(reponame)
                         for event in generate_changesets(reponame, repos):
                             yield event
                     except TracError, e:
@@ -1055,9 +1051,9 @@
             rev, path = chgset, '/'
         reponame = rm.get_default_repository(formatter.context)
         if reponame is not None:
-            repos = rm.get_repository(reponame, authname)
+            repos = rm.get_repository(reponame)
         else:
-            reponame, repos, path = rm.get_repository_by_path(path, authname)
+            reponame, repos, path = rm.get_repository_by_path(path)
         if path == '/':
             path = None
 
@@ -1157,8 +1153,7 @@
         if req.get_header('X-Requested-With') == 'XMLHttpRequest':
             dirname, prefix = posixpath.split(req.args.get('q'))
             prefix = prefix.lower()
-            reponame, repos, path = rm.get_repository_by_path(dirname,
-                                                              req.authname)
+            reponame, repos, path = rm.get_repository_by_path(dirname)
             # an entry is a (isdir, name, path) tuple
             def kind_order(entry):
                 return (not entry[0], embedded_numbers(entry[1]))
@@ -1190,9 +1185,9 @@
 
         # -- normalize
         new_reponame, new_repos, new_path = \
-            rm.get_repository_by_path(new_path, req.authname)
+            rm.get_repository_by_path(new_path)
         old_reponame, old_repos, old_path = \
-            rm.get_repository_by_path(old_path, req.authname)
+            rm.get_repository_by_path(old_path)
         new_rev = new_repos.normalize_rev(new_rev)
         old_rev = old_repos.normalize_rev(old_rev)
 
diff --git a/trac/versioncontrol/web_ui/log.py b/trac/versioncontrol/web_ui/log.py
--- a/trac/versioncontrol/web_ui/log.py
+++ b/trac/versioncontrol/web_ui/log.py
@@ -83,7 +83,7 @@
         limit = int(req.args.get('limit') or self.default_log_limit)
 
         reponame, repos, path = RepositoryManager(self.env).\
-                get_repository_by_path(path, req.authname)
+                get_repository_by_path(path)
         repos_resource = Resource('repository', reponame)
 
         normpath = repos.normalize_path(path)
@@ -105,12 +105,14 @@
         #    unless explicit ranges have been specified
         #  * for ''show only add, delete'' we're using
         #   `Repository.get_path_history()` 
+        cset_resource = Resource('changeset', parent=repos_resource)
         if mode == 'path_history':
-            def history(limit):
-                for h in repos.get_path_history(path, rev, limit):
-                    yield h
+            def history():
+                for h in repos.get_path_history(path, rev):
+                    if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])):
+                        yield h
         elif revranges:
-            def history(limit):
+            def history():
                 prevpath = path
                 expected_next_item = None
                 ranges = list(revranges.pairs)
@@ -123,14 +125,15 @@
                         p, rev, chg = node_history[0]
                         if rev < a:
                             break # simply skip, no separator
-                        if expected_next_item:
-                            # check whether we're continuing previous range
-                            np, nrev, nchg = expected_next_item
-                            if rev != nrev: # no, we need a separator
-                                yield (np, nrev, None)
-                        yield node_history[0]
+                        if 'CHANGESET_VIEW' in req.perm(cset_resource(id=rev)):
+                            if expected_next_item:
+                                # check whether we're continuing previous range
+                                np, nrev, nchg = expected_next_item
+                                if rev != nrev: # no, we need a separator
+                                    yield (np, nrev, None)
+                            yield node_history[0]
                         prevpath = node_history[-1][0] # follow copy
-                        b = rev-1
+                        b = rev - 1
                         if len(node_history) > 1:
                             expected_next_item = node_history[-1]
                         else:
@@ -138,14 +141,18 @@
                 if expected_next_item:
                     yield (expected_next_item[0], expected_next_item[1], None)
         else:
-            history = get_existing_node(req, repos, path, rev).get_history
+            def history():
+                node = get_existing_node(req, repos, path, rev)
+                for h in node.get_history():
+                    if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])):
+                        yield h
 
         # -- retrieve history, asking for limit+1 results
         info = []
         depth = 1
         previous_path = normpath
         count = 0
-        for old_path, old_rev, old_chg in history(limit+1):
+        for old_path, old_rev, old_chg in history():
             if stop_rev and repos.rev_older_than(old_rev, stop_rev):
                 break
             old_path = repos.normalize_path(old_path)
@@ -353,9 +360,9 @@
         authname = formatter.perm.username
         reponame = rm.get_default_repository(formatter.context)
         if reponame is not None:
-            repos = rm.get_repository(reponame, authname)
+            repos = rm.get_repository(reponame)
         else:
-            reponame, repos, path = rm.get_repository_by_path(path, authname)
+            reponame, repos, path = rm.get_repository_by_path(path)
 
         revranges = None
         if any(c for c in ':-,' if c in revs):

