Edgewall Software

Ticket #7116: 7116-authz-policy-r9049.patch

File 7116-authz-policy-r9049.patch, 69.6 KB (added by rblank, 2 years ago)

Current state of AuthzPermissionPolicy.

  • setup.py

    diff --git a/setup.py b/setup.py
    a b  
    108108        trac.ticket.web_ui = trac.ticket.web_ui 
    109109        trac.timeline = trac.timeline.web_ui 
    110110        trac.versioncontrol.admin = trac.versioncontrol.admin 
     111        trac.versioncontrol.svn_authz = trac.versioncontrol.svn_authz 
    111112        trac.versioncontrol.svn_fs = trac.versioncontrol.svn_fs 
    112113        trac.versioncontrol.svn_prop = trac.versioncontrol.svn_prop 
    113114        trac.versioncontrol.web_ui = trac.versioncontrol.web_ui 
  • trac/env.py

    diff --git a/trac/env.py b/trac/env.py
    a b  
    307307         
    308308        @param authname: user name for authorization 
    309309        """ 
    310         return RepositoryManager(self).get_repository(reponame, authname) 
     310        return RepositoryManager(self).get_repository(reponame) 
    311311 
    312312    def create(self, options=[]): 
    313313        """Create the basic directory structure of the environment, initialize 
  • trac/versioncontrol/api.py

    diff --git a/trac/versioncontrol/api.py b/trac/versioncontrol/api.py
    a b  
    318318                if is_default(reponame): 
    319319                    reponame = '' 
    320320                try: 
    321                     repo = self.get_repository(reponame, req.authname) 
     321                    repo = self.get_repository(reponame) 
    322322                    if repo: 
    323323                        repo.sync() 
    324324                except TracError, e: 
     
    422422                    if prio >= 0) 
    423423        return list(types) 
    424424     
    425     def get_repositories_by_dir(self, directory, authname): 
     425    def get_repositories_by_dir(self, directory): 
    426426        """Retrieve the repositories based on the given directory. 
    427427 
    428428           :param directory: the key for identifying the repositories. 
     
    435435            if dir: 
    436436                dir = os.path.join(os.path.normcase(dir), '') 
    437437                if dir.startswith(directory): 
    438                     repos = self.get_repository(reponame, authname) 
     438                    repos = self.get_repository(reponame) 
    439439                    if repos: 
    440440                        repositories.append(repos) 
    441441        return repositories 
     
    459459            db.commit() 
    460460        return id 
    461461     
    462     def get_repository(self, reponame, authname): 
     462    def get_repository(self, reponame): 
    463463        """Retrieve the appropriate Repository for the given name. 
    464464 
    465465           :param reponame: the key for specifying the repository. 
    466466                            If no name is given, take the default  
    467467                            repository. 
    468            :param authname: deprecated (use fine grained permissions) 
    469468           :return: if no corresponding repository was defined,  
    470469                    simply return `None`. 
    471470        """ 
     
    499498        finally: 
    500499            self._lock.release() 
    501500 
    502     def get_repository_by_path(self, path, authname): 
     501    def get_repository_by_path(self, path): 
    503502        """Retrieve a matching Repository for the given path. 
    504503         
    505504        :param path: the eventually scoped repository-scoped path 
     
    519518            path = path[length:] 
    520519        else: 
    521520            reponame = '' 
    522         return (reponame, self.get_repository(reponame, authname), 
     521        return (reponame, self.get_repository(reponame), 
    523522                path.rstrip('/') or '/') 
    524523 
    525524    def get_default_repository(self, context): 
     
    549548                        self._all_repositories[reponame] = info 
    550549        return self._all_repositories 
    551550     
    552     def get_real_repositories(self, authname): 
     551    def get_real_repositories(self): 
    553552        """Return a set of all real repositories (i.e. excluding aliases).""" 
    554553        repositories = set() 
    555554        for reponame in self.get_all_repositories(): 
    556555            try: 
    557                 repos = self.get_repository(reponame, authname) 
     556                repos = self.get_repository(reponame) 
    558557                if repos is not None: 
    559558                    repositories.add(repos) 
    560559            except TracError: 
     
    572571            self._lock.release() 
    573572        self.config.touch()     # Force environment reload 
    574573  
    575     def notify(self, event, reponame, revs, authname): 
     574    def notify(self, event, reponame, revs): 
    576575        """Notify repositories and change listeners about repository events. 
    577576         
    578577        The supported events are the names of the methods defined in the 
     
    594593            else: 
    595594                base = reponame 
    596595        if base: 
    597             repositories = [r for r in self.get_real_repositories(authname) 
     596            repositories = [r for r in self.get_real_repositories() 
    598597                            if r.get_base() == base] 
    599598        if not repositories: 
    600599            self.log.warn("Found no repositories matching '%s' base.", 
     
    676675class Repository(object): 
    677676    """Base class for a repository provided by a version control system.""" 
    678677 
    679     def __init__(self, name, params, authz, log): 
     678    def __init__(self, name, params, log): 
    680679        """Initialize a repository. 
    681680         
    682681           :param name: a unique name identifying the repository, usually a 
     
    686685                          the name of the repository under the key "name" and 
    687686                          the surrogate key that identifies the repository in 
    688687                          the database under the key "id". 
    689            :param authz: a repository authorizer (deprecated). 
    690688           :param log: a logger instance. 
    691689        """ 
    692690        self.name = name 
    693691        self.params = params 
    694692        self.reponame = params['name'] 
    695693        self.id = params['id'] 
    696         self.authz = authz or Authorizer() 
    697694        self.log = log 
    698695 
    699696    def close(self): 
     
    771768        """ 
    772769        rev = self.youngest_rev 
    773770        while rev: 
    774             if self.authz.has_permission_for_changeset(rev): 
    775                 chgset = self.get_changeset(rev) 
    776                 if chgset.date < start: 
    777                     return 
    778                 if chgset.date < stop: 
    779                     yield chgset 
     771            chgset = self.get_changeset(rev) 
     772            if chgset.date < start: 
     773                return 
     774            if chgset.date < stop: 
     775                yield chgset 
    780776            rev = self.previous_rev(rev) 
    781777 
    782778    def has_node(self, path, rev=None): 
     
    10381034 
    10391035 
    10401036 
    1041 class PermissionDenied(PermissionError): 
    1042     """Exception raised by an authorizer. 
    1043  
    1044     This exception is raise if the user has insufficient permissions 
    1045     to view a specific part of the repository. 
    1046     """ 
    1047     def __str__(self): 
    1048         return self.action 
    1049  
    1050  
    1051 class Authorizer(object): 
    1052     """Controls the view access to parts of the repository. 
    1053      
    1054     Base class for authorizers that are responsible to granting or denying 
    1055     access to view certain parts of a repository. 
    1056     """ 
    1057  
    1058     def assert_permission(self, path): 
    1059         if not self.has_permission(path): 
    1060             raise PermissionDenied(_('Insufficient permissions to access ' 
    1061                                      '%(path)s', path=path)) 
    1062  
    1063     def assert_permission_for_changeset(self, rev): 
    1064         if not self.has_permission_for_changeset(rev): 
    1065             raise PermissionDenied(_('Insufficient permissions to access ' 
    1066                                      'changeset %(id)s', id=rev)) 
    1067  
    1068     def has_permission(self, path): 
    1069         return True 
    1070  
    1071     def has_permission_for_changeset(self, rev): 
    1072         return True 
     1037# Note: Since Trac 0.12, Exception PermissionDenied class is gone, 
     1038# and class Authorizer is gone as well. 
     1039# 
     1040# Fine-grained permissions are now handled via normal permission policies. 
  • trac/versioncontrol/cache.py

    diff --git a/trac/versioncontrol/cache.py b/trac/versioncontrol/cache.py
    a b  
    2222from trac.core import TracError 
    2323from trac.util.datefmt import utc, to_timestamp 
    2424from trac.util.translation import _ 
    25 from trac.versioncontrol import Changeset, Node, Repository, Authorizer, \ 
    26                                 NoSuchChangeset 
     25from trac.versioncontrol import Changeset, Node, Repository, NoSuchChangeset 
    2726 
    2827 
    2928_kindmap = {'D': Node.DIRECTORY, 'F': Node.FILE} 
     
    4342 
    4443    scope = property(lambda self: self.repos.scope) 
    4544     
    46     def __init__(self, env, repos, authz, log): 
     45    def __init__(self, env, repos, log): 
    4746        self.env = env 
    4847        self.repos = repos 
    4948        self.metadata = CacheProxy(self.__class__.__module__ + '.' 
    5049                                   + self.__class__.__name__ + '.metadata:' 
    5150                                   + str(self.repos.id), self._metadata, 
    5251                                   self.env) 
    53         Repository.__init__(self, repos.name, repos.params, authz, log) 
     52        Repository.__init__(self, repos.name, repos.params, log) 
    5453 
    5554    def close(self): 
    5655        self.repos.close() 
     
    6564        return self.repos.get_path_url(path, rev) 
    6665 
    6766    def get_changeset(self, rev): 
    68         return CachedChangeset(self.repos, self.normalize_rev(rev), 
    69                                self.env, self.authz) 
     67        return CachedChangeset(self.repos, self.normalize_rev(rev), self.env) 
    7068 
    7169    def get_changeset_uid(self, rev): 
    7270        return self.repos.get_changeset_uid(rev) 
     
    8179                        to_timestamp(stop))) 
    8280        for rev, in cursor: 
    8381            try: 
    84                 if self.authz.has_permission_for_changeset(rev): 
    85                     yield self.get_changeset(rev) 
     82                yield self.get_changeset(rev) 
    8683            except NoSuchChangeset: 
    8784                pass # skip changesets currently being resync'ed 
    8885 
     
    220217            # 1. prepare for resyncing 
    221218            #    (there still might be a race condition at this point) 
    222219 
    223             authz = self.repos.authz 
    224             self.repos.authz = Authorizer() # remove permission checking 
    225  
    226220            kindmap = dict(zip(_kindmap.values(), _kindmap.keys())) 
    227221            actionmap = dict(zip(_actionmap.values(), _actionmap.keys())) 
    228222 
    229             try: 
    230                 while next_youngest is not None: 
    231                      
    232                     # 1.1 Attempt to resync the 'revision' table 
    233                     self.log.info("Trying to sync revision [%s]" % 
    234                                   next_youngest) 
    235                     cset = self.repos.get_changeset(next_youngest) 
    236                     try: 
    237                         cursor.execute("INSERT INTO revision " 
    238                                        " (repos,rev,time,author,message) " 
    239                                        "VALUES (%s,%s,%s,%s,%s)", 
    240                                        (self.id, str(next_youngest), 
    241                                         to_timestamp(cset.date), 
    242                                         cset.author, cset.message)) 
    243                     except Exception, e: # *another* 1.1. resync attempt won  
    244                         self.log.warning('Revision %s already cached: %s' % 
    245                                          (next_youngest, e)) 
    246                         # also potentially in progress, so keep ''previous'' 
    247                         # notion of 'youngest' 
    248                         self.repos.clear(youngest_rev=youngest) 
    249                         db.rollback() 
    250                         return 
     223            while next_youngest is not None: 
     224                 
     225                # 1.1 Attempt to resync the 'revision' table 
     226                self.log.info("Trying to sync revision [%s]" % 
     227                              next_youngest) 
     228                cset = self.repos.get_changeset(next_youngest) 
     229                try: 
     230                    cursor.execute("INSERT INTO revision " 
     231                                   " (repos,rev,time,author,message) " 
     232                                   "VALUES (%s,%s,%s,%s,%s)", 
     233                                   (self.id, str(next_youngest), 
     234                                    to_timestamp(cset.date), 
     235                                    cset.author, cset.message)) 
     236                except Exception, e: # *another* 1.1. resync attempt won  
     237                    self.log.warning('Revision %s already cached: %s' % 
     238                                     (next_youngest, e)) 
     239                    # also potentially in progress, so keep ''previous'' 
     240                    # notion of 'youngest' 
     241                    self.repos.clear(youngest_rev=youngest) 
     242                    db.rollback() 
     243                    return 
    251244 
    252                     # 1.2. now *only* one process was able to get there 
    253                     #      (i.e. there *shouldn't* be any race condition here) 
     245                # 1.2. now *only* one process was able to get there 
     246                #      (i.e. there *shouldn't* be any race condition here) 
    254247 
    255                     for path, kind, action, bpath, brev in cset.get_changes(): 
    256                         self.log.debug("Caching node change in [%s]: %s" 
    257                                        % (next_youngest, 
    258                                           (path,kind,action,bpath,brev))) 
    259                         kind = kindmap[kind] 
    260                         action = actionmap[action] 
    261                         cursor.execute("INSERT INTO node_change " 
    262                                        " (repos,rev,path,node_type," 
    263                                        "  change_type,base_path,base_rev) " 
    264                                        "VALUES (%s,%s,%s,%s,%s,%s,%s)", 
    265                                        (self.id, str(next_youngest), 
    266                                         path, kind, action, bpath, brev)) 
     248                for path, kind, action, bpath, brev in cset.get_changes(): 
     249                    self.log.debug("Caching node change in [%s]: %s" 
     250                                   % (next_youngest, 
     251                                      (path,kind,action,bpath,brev))) 
     252                    kind = kindmap[kind] 
     253                    action = actionmap[action] 
     254                    cursor.execute("INSERT INTO node_change " 
     255                                   " (repos,rev,path,node_type," 
     256                                   "  change_type,base_path,base_rev) " 
     257                                   "VALUES (%s,%s,%s,%s,%s,%s,%s)", 
     258                                   (self.id, str(next_youngest), 
     259                                    path, kind, action, bpath, brev)) 
    267260 
    268                     # 1.3. iterate (1.1 should always succeed now) 
    269                     youngest = next_youngest                     
    270                     next_youngest = self.repos.next_rev(next_youngest) 
     261                # 1.3. iterate (1.1 should always succeed now) 
     262                youngest = next_youngest                     
     263                next_youngest = self.repos.next_rev(next_youngest) 
    271264 
    272                     # 1.4. update 'youngest_rev' metadata  
    273                     #      (minimize possibility of failures at point 0.) 
    274                     cursor.execute("UPDATE repository SET value=%s " 
    275                                    "WHERE id=%s AND name=%s", 
    276                                    (str(youngest), self.id, 
    277                                     CACHE_YOUNGEST_REV)) 
    278                     self.metadata.invalidate(db) 
    279                     db.commit() 
     265                # 1.4. update 'youngest_rev' metadata  
     266                #      (minimize possibility of failures at point 0.) 
     267                cursor.execute("UPDATE repository SET value=%s " 
     268                               "WHERE id=%s AND name=%s", 
     269                               (str(youngest), self.id, 
     270                                CACHE_YOUNGEST_REV)) 
     271                self.metadata.invalidate(db) 
     272                db.commit() 
    280273 
    281                     # 1.5. provide some feedback 
    282                     if feedback: 
    283                         feedback(youngest) 
    284             finally: 
    285                 # 3. restore permission checking (after 1.) 
    286                 self.repos.authz = authz 
     274                # 1.5. provide some feedback 
     275                if feedback: 
     276                    feedback(youngest) 
    287277 
    288278    def get_node(self, path, rev=None): 
    289279        return self.repos.get_node(path, self.normalize_rev(rev)) 
     
    397387 
    398388class CachedChangeset(Changeset): 
    399389 
    400     def __init__(self, repos, rev, env, authz): 
     390    def __init__(self, repos, rev, env): 
    401391        self.repos = repos 
    402392        self.env = env 
    403         self.authz = authz 
    404393        db = self.env.get_db_cnx() 
    405394        cursor = db.cursor() 
    406395        cursor.execute("SELECT time,author,message FROM revision " 
     
    422411                       "FROM node_change WHERE repos=%s AND rev=%s " 
    423412                       "ORDER BY path", (self.repos.id, str(self.rev))) 
    424413        for path, kind, change, base_path, base_rev in cursor: 
    425             if not self.authz.has_permission(posixpath.join(self.scope, 
    426                                                             path.strip('/'))): 
    427                 # FIXME: what about the base_path? 
    428                 continue 
    429414            kind = _kindmap[kind] 
    430415            change = _actionmap[change] 
    431416            yield path, kind, change, base_path, base_rev 
  • trac/versioncontrol/svn_authz.py

    diff --git a/trac/versioncontrol/svn_authz.py b/trac/versioncontrol/svn_authz.py
    a b  
    1818 
    1919import os.path 
    2020 
    21 from trac.config import Option 
     21from trac.config import Option, PathOption 
    2222from trac.core import * 
    23 from trac.versioncontrol import Authorizer 
     23from trac.perm import IPermissionPolicy 
     24from trac.resource import Resource 
     25from trac.util import read_file 
     26from trac.util.compat import all 
     27from trac.util.text import to_unicode 
     28from trac.util.translation import _ 
     29from trac.versioncontrol.api import RepositoryManager 
    2430 
    2531 
    26 class SvnAuthzOptions(Component): 
    27  
    28     authz_file = Option('trac', 'authz_file', '', 
    29         """Path to Subversion 
    30         [http://svnbook.red-bean.com/en/1.1/ch06s04.html#svn-ch-6-sect-4.4.2 authorization (authz) file] 
    31         """) 
    32  
    33     authz_module_name = Option('trac', 'authz_module_name', '', 
    34         """The module prefix used in the authz_file.""") 
    35  
    36  
    37 def SubversionAuthorizer(env, repos, authname): 
    38     authz_file = env.config.get('trac', 'authz_file') 
    39     if not authz_file: 
    40         return Authorizer() 
    41     if not os.path.isabs(authz_file): 
    42         authz_file = os.path.join(env.path, authz_file) 
    43     if not os.path.exists(authz_file): 
    44         env.log.error('[trac] authz_file (%s) does not exist.' % authz_file) 
    45  
    46     module_name = env.config.get('trac', 'authz_module_name') 
    47     return RealSubversionAuthorizer(repos, authname, module_name, authz_file) 
    48  
    4932def parent_iter(path): 
    5033    path = path.strip('/') 
    5134    if path: 
     
    5639    while 1: 
    5740        yield path 
    5841        if path == '/': 
    59             raise StopIteration() 
     42            return 
    6043        path = path[:-1] 
    6144        yield path 
    6245        idx = path.rfind('/') 
    6346        path = path[:idx + 1] 
    6447 
    6548 
    66 class RealSubversionAuthorizer(Authorizer): 
    67     """FIXME: this should become a IPermissionPolicy, of course. 
     49class ParseError(Exception): 
     50    """Exception thrown for parse errors in authz files""" 
    6851 
    69     `check_permission(username, action, resource)` should be able to 
    70     replace `has_permission(path)` when resource is a `('source', path)` 
    71     and `has_permission_for_changeset` when resource is a `('changeset', rev)`. 
     52 
     53def parse(authz): 
     54    """Parse a Subversion authorization file. 
     55     
     56    Return a dict of modules, each containing a dict of paths, each containing 
     57    a dict mapping users to permissions. 
     58    """ 
     59    groups = {} 
     60    aliases = {} 
     61    sections = {} 
     62    section = None 
     63    lineno = 0 
     64    for line in authz.splitlines(): 
     65        lineno += 1 
     66        line = to_unicode(line.strip()) 
     67        if not line or line.startswith('#'): 
     68            continue 
     69        if line.startswith('[') and line.endswith(']'): 
     70            section = line[1:-1] 
     71            continue 
     72        if section is None: 
     73            raise ParseError(_('Line %(lineno)d: Entry before first ' 
     74                               'section header', lineno=lineno)) 
     75        parts = line.split('=', 1) 
     76        if len(parts) != 2: 
     77            raise ParseError(_('Line %(lineno)d: Invalid entry', 
     78                               lineno=lineno)) 
     79        name, value = parts 
     80        name = name.strip() 
     81        if section == 'groups': 
     82            group = groups.setdefault(name, set()) 
     83            group.update(each.strip() for each in value.split(',')) 
     84        elif section == 'aliases': 
     85            aliases[name] = value.strip() 
     86        else: 
     87            sections.setdefault(section, []).append((name.strip(), value)) 
     88 
     89    def resolve(subject, done): 
     90        if subject.startswith('@'): 
     91            done.add(subject) 
     92            for members in groups[subject[1:]] - done: 
     93                for each in resolve(members, done): 
     94                    yield each 
     95        elif subject.startswith('&'): 
     96            yield aliases[subject[1:]] 
     97        else: 
     98            yield subject 
     99     
     100    authz = {} 
     101    for name, items in sections.iteritems(): 
     102        parts = name.split(':', 1) 
     103        module = authz.setdefault(len(parts) > 1 and parts[0] or '', {}) 
     104        section = module.setdefault(parts[-1], {}) 
     105        for subject, perms in items: 
     106            for user in resolve(subject, set()): 
     107                section.setdefault(user, 'r' in perms)  # The first match wins 
     108     
     109    return authz 
     110         
     111 
     112         
     113class AuthzPermissionPolicy(Component): 
     114    """Permission policy for `source:` resources using a Subversion authz 
     115    file. 
    72116    """ 
    73117 
    74     auth_name = '' 
    75     module_name = '' 
    76     conf_authz = None 
     118    implements(IPermissionPolicy) 
     119     
     120    authz_file = PathOption('trac', 'authz_file', '', 
     121        """Path to the Subversion 
     122        [http://svnbook.red-bean.com/en/1.1/ch06s04.html#svn-ch-6-sect-4.4.2 authorization (authz) file] 
     123        """) 
    77124 
    78     def __init__(self, repos, auth_name, module_name, cfg_file, cfg_fp=None): 
    79         self.repos = repos 
    80         self.auth_name = auth_name 
    81         self.module_name = module_name 
    82                                  
    83         from ConfigParser import ConfigParser 
    84         self.conf_authz = ConfigParser() 
    85         if cfg_fp: 
    86             self.conf_authz.readfp(cfg_fp, cfg_file) 
    87         elif cfg_file: 
    88             self.conf_authz.read(cfg_file) 
     125    authz_module_name = Option('trac', 'authz_module_name', '', 
     126        """The module prefix used in the `authz_file` for the default 
     127        repository. 
     128        """) 
    89129 
    90         self.groups = self._groups() 
     130    _mtime = 0 
     131     
     132    # IPermissionPolicy methods 
    91133 
    92     def has_permission(self, path): 
    93         if path is None: 
    94             return 1 
    95  
    96         for p in parent_iter(path): 
    97             if self.module_name: 
    98                 for perm in self._get_section(self.module_name + ':' + p): 
     134    def check_permission(self, action, username, resource, perm): 
     135#        print "*** perm=%r resource=%r" % (action, resource) 
     136        if resource is None: 
     137            return True 
     138        if action == 'FILE_VIEW' and resource.realm == 'source': 
     139            authz = self._authz 
     140            modules = [resource.parent.id or self.authz_module_name] 
     141            if modules[0]: 
     142                modules.append('') 
     143            for p in parent_iter(resource.id): 
     144                for module in modules: 
     145                    section = authz.get(module, {}).get(p, {}) 
     146                    perm = section.get(username) 
    99147                    if perm is not None: 
    100148                        return perm 
    101             for perm in self._get_section(p): 
    102                 if perm is not None: 
    103                     return perm 
     149                    perm = section.get('*') 
     150                    if perm is not None: 
     151                        return perm 
     152            return False 
    104153 
    105         return 0 
     154    @property 
     155    def _authz(self): 
     156        # TODO: Inefficient. Check only e.g. once per request 
     157        mtime = os.path.getmtime(self.authz_file) 
     158        if mtime > self._mtime or not hasattr(self, '_authz_cache'): 
     159            self.log.debug('Parsing authz file %s') 
     160            self._mtime = mtime 
     161            # TODO: Handle parse errors 
     162            self._authz_cache = parse(read_file(self.authz_file)) 
     163        return self._authz_cache 
    106164 
    107     def has_permission_for_changeset(self, rev): 
    108         changeset = self.repos.get_changeset(rev) 
    109         for change in changeset.get_changes(): 
    110             # the repository checks permissions for each change, so just check 
    111             # if any changes can be accessed 
    112             return 1 
    113         return 0 
    114165 
    115     # Internal API 
     166class ChangesetPermissionPolicy(Component): 
     167    """Default permission policy for changesets. 
     168     
     169    This permission policy denies access to changesets containing at least 
     170    one file, and where access to all files is denied. 
     171    """ 
    116172 
    117     def _groups(self): 
    118         if not self.conf_authz.has_section('groups'): 
    119             return [] 
    120  
    121         grp_parents = {} 
    122         usr_grps = [] 
    123  
    124         for group in self.conf_authz.options('groups'): 
    125             for member in self.conf_authz.get('groups', group).split(','): 
    126                 member = member.strip() 
    127                 if member == self.auth_name: 
    128                     usr_grps.append(group) 
    129                 elif member.startswith('@'): 
    130                     grp_parents.setdefault(member[1:], []).append(group) 
    131  
    132         expanded = {} 
    133  
    134         def expand_group(group): 
    135             if group in expanded: 
    136                 return 
    137             expanded[group] = True 
    138             for parent in grp_parents.get(group, []): 
    139                 expand_group(parent) 
    140  
    141         for g in usr_grps: 
    142             expand_group(g) 
    143  
    144         # expand groups 
    145         return expanded.keys() 
    146  
    147     def _get_section(self, section): 
    148         if not self.conf_authz.has_section(section): 
     173    implements(IPermissionPolicy) 
     174     
     175    def check_permission(self, action, username, resource, perm): 
     176        if resource is None: 
    149177            return 
    150  
    151         yield self._get_permission(section, self.auth_name) 
    152  
    153         group_perm = None 
    154         for g in self.groups: 
    155             p = self._get_permission(section, '@' + g) 
    156             if p is not None: 
    157                 group_perm = p 
    158  
    159             if group_perm: 
    160                 yield 1 
    161  
    162         yield group_perm 
    163  
    164         yield self._get_permission(section, '*') 
    165  
    166     def _get_permission(self, section, subject): 
    167         if self.conf_authz.has_option(section, subject): 
    168             return 'r' in self.conf_authz.get(section, subject) 
    169         return None 
     178        if action == 'CHANGESET_VIEW' and resource.realm == 'changeset': 
     179            rm = RepositoryManager(self.env) 
     180            repos = rm.get_repository(resource.parent.id) 
     181            changes = list(repos.get_changeset(resource.id).get_changes()) 
     182            if changes: 
     183                source = Resource('source', version=resource.id, 
     184                                  parent=resource.parent) 
     185                if all('FILE_VIEW' not in perm(source(id=change[0])) 
     186                       for change in changes): 
     187                    return False 
     188     
     189 No newline at end of file 
  • trac/versioncontrol/svn_fs.py

    diff --git a/trac/versioncontrol/svn_fs.py b/trac/versioncontrol/svn_fs.py
    a b  
    5454                                IRepositoryConnector, \ 
    5555                                NoSuchChangeset, NoSuchNode 
    5656from trac.versioncontrol.cache import CachedRepository 
    57 from trac.versioncontrol.svn_authz import SubversionAuthorizer 
    5857from trac.util import embedded_numbers 
    5958from trac.util.text import exception_to_unicode, to_unicode 
    6059from trac.util.translation import _ 
     
    280279            self._version = self._get_version() 
    281280            self.env.systeminfo.append(('Subversion', self._version)) 
    282281        params.update(tags=self.tags, branches=self.branches) 
    283         fs_repos = SubversionRepository(dir, params, None, self.log) 
     282        fs_repos = SubversionRepository(dir, params, self.log) 
    284283        if type == 'direct-svnfs': 
    285284            repos = fs_repos 
    286285        else: 
    287             repos = CachedRepository(self.env, fs_repos, None, self.log) 
     286            repos = CachedRepository(self.env, fs_repos, self.log) 
    288287            repos.has_linear_changesets = True 
    289         # FIXME: convert SubversionAuthorizer to a PermissionPolicy 
    290         if 'authname' in params: 
    291             authz = SubversionAuthorizer(self.env, weakref.proxy(repos), 
    292                                          params['authname']) 
    293             repos.authz = fs_repos.authz = authz 
    294288        return repos 
    295289 
    296290    def _get_version(self): 
     
    305299class SubversionRepository(Repository): 
    306300    """Repository implementation based on the svn.fs API.""" 
    307301 
    308     def __init__(self, path, params, authz, log): 
     302    def __init__(self, path, params, log): 
    309303        self.log = log 
    310304        self.pool = Pool() 
    311305         
     
    335329        self.base = 'svn:%s:%s' % (self.uuid, _from_svn(root_path_utf8)) 
    336330        name = 'svn:%s:%s' % (self.uuid, self.path) 
    337331 
    338         Repository.__init__(self, name, params, authz, log) 
     332        Repository.__init__(self, name, params, log) 
    339333 
    340334        # if root_path_utf8 is shorter than the path_utf8, the difference is 
    341335        # this scope (which always starts with a '/') 
     
    430424     
    431425    def get_changeset(self, rev): 
    432426        rev = self.normalize_rev(rev) 
    433         return SubversionChangeset(rev, self.authz, self.scope, 
    434                                    self.fs_ptr, self.pool) 
     427        return SubversionChangeset(rev, self.scope, self.fs_ptr, self.pool) 
    435428 
    436429    def get_changeset_uid(self, rev): 
    437430        return (self.uuid, rev) 
    438431 
    439432    def get_node(self, path, rev=None): 
    440433        path = path or '' 
    441         self.authz.assert_permission(posixpath.join(self.scope, 
    442                                                     path.strip('/'))) 
    443434        if path and path[-1] == '/': 
    444435            path = path[:-1] 
    445436 
     
    490481                if rev < end: 
    491482                    break 
    492483                path = _from_svn(path_utf8) 
    493                 if not self.authz.has_permission(path): 
    494                     break 
    495484                yield path, rev 
    496485        del tmp1 
    497486        del tmp2 
     
    669658    def __init__(self, path, rev, repos, pool=None, parent_root=None): 
    670659        self.repos = repos 
    671660        self.fs_ptr = repos.fs_ptr 
    672         self.authz = repos.authz 
    673661        self.scope = repos.scope 
    674662        self._scoped_path_utf8 = _to_svn(self.scope, path) 
    675663        self.pool = Pool(pool) 
     
    719707        entries = fs.dir_entries(self.root, self._scoped_path_utf8, pool()) 
    720708        for item in entries.keys(): 
    721709            path = posixpath.join(self.path, _from_svn(item)) 
    722             if not self.authz.has_permission(posixpath.join(self.scope, 
    723                                                             path.strip('/'))): 
    724                 continue 
    725710            yield SubversionNode(path, self._requested_rev, self.repos, 
    726711                                 self.pool, self.root) 
    727712 
     
    846831 
    847832class SubversionChangeset(Changeset): 
    848833 
    849     def __init__(self, rev, authz, scope, fs_ptr, pool=None): 
     834    def __init__(self, rev, scope, fs_ptr, pool=None): 
    850835        self.rev = rev 
    851         self.authz = authz 
    852836        self.scope = scope 
    853837        self.fs_ptr = fs_ptr 
    854838        self.pool = Pool(pool) 
     
    896880            path = _from_svn(path_utf8) 
    897881 
    898882            # Filtering on `path` 
    899             if not (_is_path_within_scope(self.scope, path) and 
    900                     self.authz.has_permission(path)): 
     883            if not _is_path_within_scope(self.scope, path): 
    901884                continue 
    902885 
    903886            path_utf8 = change.path 
     
    907890            base_rev = change.base_rev 
    908891 
    909892            # Ensure `base_path` is within the scope 
    910             if not (_is_path_within_scope(self.scope, base_path) and 
    911                     self.authz.has_permission(base_path)): 
     893            if not _is_path_within_scope(self.scope, base_path): 
    912894                base_path, base_rev = None, -1 
    913895 
    914896            # Determine the action 
  • trac/versioncontrol/templates/dir_entries.html

    diff --git a/trac/versioncontrol/templates/dir_entries.html b/trac/versioncontrol/templates/dir_entries.html
    a b  
    66    <xi:include href="macros.html" /> 
    77  </py:if> 
    88  <py:for each="idx, entry in enumerate(dir.entries)"> 
    9     <py:with vars="change = dir.changes[entry.rev]"> 
     9    <py:with vars="change = dir.changes[entry.rev]; 
     10                   chgset_context = change and context('changeset', change.rev, parent=repos_resource); 
     11                   chgset_view = change and 'CHANGESET_VIEW' in perm(chgset_context.resource)"> 
    1012      <tr class="${idx % 2 and 'even' or 'odd'}"> 
    1113        <td class="name"> 
    1214          <a class="$entry.kind" title="View ${entry.kind.capitalize()}" 
     
    1820          <a title="View Revision Log" href="${href.log(reponame, entry.path, rev=rev)}">$entry.rev</a> 
    1921          <a title="View Changeset" class="chgset" href="${href.changeset(change.rev, reponame)}">&nbsp;</a> 
    2022        </td> 
    21         <td class="age" style="${change and dir.timerange and 'border-color: rgb(%s,%s,%s)' % 
     23        <td class="age" style="${chgset_view and dir.timerange and 'border-color: rgb(%s,%s,%s)' % 
    2224                                 dir.colorize_age(dir.timerange.relative(change.date)) or None}"> 
    23           ${change and dateinfo(change.date) or '-'} 
     25          ${('&ndash;', dateinfo(change.date))[chgset_view]} 
    2426        </td> 
    25         <td class="change"> 
    26           <span class="author" py:if="change">${authorinfo(change.author)}:</span> 
    27           <span class="change" py:choose="" 
    28                 py:with="chgset_context = context('changeset', change.rev, parent=repos_resource)"> 
    29             <py:when test="not change or 'CHANGESET_VIEW' not in perm(chgset_context.resource)">-</py:when> 
    30             <py:when test="wiki_format_messages"> 
    31               ${change and wiki_to_oneliner(chgset_context, change.message, shorten=True)} 
    32             </py:when> 
    33             <py:otherwise>${change and shorten_line(change.message)}</py:otherwise> 
    34           </span> 
     27        <td class="change" py:choose=""> 
     28          <py:when test="chgset_view"> 
     29            <span class="author">${authorinfo(change.author)}:</span> 
     30            <span class="change" py:choose=""> 
     31              <py:when test="wiki_format_messages"> 
     32                ${wiki_to_oneliner(chgset_context, change.message, shorten=True)} 
     33              </py:when> 
     34              <py:otherwise>${shorten_line(change.message)}</py:otherwise> 
     35            </span> 
     36          </py:when> 
     37          <py:otherwise>&ndash;</py:otherwise> 
    3538        </td> 
    3639      </tr> 
    3740    </py:with> 
  • trac/versioncontrol/templates/repository_index.html

    diff --git a/trac/versioncontrol/templates/repository_index.html b/trac/versioncontrol/templates/repository_index.html
    a b  
    55  <table class="listing dirlist" id="${repoindex or None}"> 
    66    <xi:include href="dirlist_thead.html" /> 
    77    <tbody> 
    8       <py:for each="idx, (reponame, repoinfo, change, err) in enumerate(repo.repositories)"> 
     8      <py:for each="idx, (reponame, repoinfo, change, err) in enumerate(repo.repositories)" 
     9              py:with="chgset_context = change and context('changeset', change.rev, parent=Resource('repository', reponame)); 
     10                       chgset_view = change and 'CHANGESET_VIEW' in perm(chgset_context.resource)"> 
    911        <tr class="${idx % 2 and 'even' or 'odd'}"> 
    1012          <td class="name"> 
    1113            <em py:strip="not err"> 
     
    2224              <a title="View Changeset" class="chgset" href="${href.changeset(change.rev, reponame)}">&nbsp;</a> 
    2325            </py:if> 
    2426          </td> 
    25           <td class="age" style="${change and repo.timerange and 'border-color: rgb(%s,%s,%s)' % 
    26             repo.colorize_age(repo.timerange.relative(change.date)) or None}"> 
    27             ${change and dateinfo(change.date) or '-'} 
     27          <td class="age" style="${chgset_view and change and repo.timerange and 'border-color: rgb(%s,%s,%s)' % 
     28                                   repo.colorize_age(repo.timerange.relative(change.date)) or None}"> 
     29            ${('&ndash;', dateinfo(change.date))[chgset_view]} 
    2830          </td> 
    29           <td class="change"> 
    30             <span class="author" py:if="change">${authorinfo(change.author)}:</span> 
    31             <span class="change" py:choose="" 
    32                   py:with="chgset_context = context('changeset', change.rev, parent=Resource('repository', reponame))"> 
    33               <em py:when="err" py:content="err" /> 
    34               <py:when test="not change or 'CHANGESET_VIEW' not in perm(chgset_context.resource)">-</py:when> 
    35               <py:when test="wiki_format_messages"> 
    36                 ${change and wiki_to_oneliner(chgset_context, change.message, shorten=True)} 
    37               </py:when> 
    38               <py:otherwise>${change and shorten_line(change.message)}</py:otherwise> 
    39             </span> 
     31          <td class="change" py:choose=""> 
     32            <span py:when="err" class="change"><em py:content="err"></em></span> 
     33            <py:when test="chgset_view"> 
     34              <span class="author">${authorinfo(change.author)}:</span> 
     35              <span class="change" py:choose=""> 
     36                <py:when test="wiki_format_messages"> 
     37                  ${wiki_to_oneliner(chgset_context, change.message, shorten=True)} 
     38                </py:when> 
     39                <py:otherwise>${shorten_line(change.message)}</py:otherwise> 
     40              </span> 
     41            </py:when> 
     42            <py:otherwise>&ndash;</py:otherwise> 
    4043          </td> 
    4144        </tr> 
    4245        <tr class="${idx % 2 and 'even' or 'odd'}" py:if="repoinfo.description"> 
  • trac/versioncontrol/templates/revisionlog.html

    diff --git a/trac/versioncontrol/templates/revisionlog.html b/trac/versioncontrol/templates/revisionlog.html
    a b  
    111111              <py:with vars="change = changes[item.rev]; 
    112112                             is_separator = item.change is None; 
    113113                             chgset_context = context('changeset', change.rev, parent=repos_resource); 
    114                              chgset_view = 'CHANGESET_VIEW' in perm(chgset_context.resource); 
    115114                             odd_even = idx % 2 and 'odd' or 'even'"> 
    116115                <!--! highlight copy or rename operations --> 
    117116                <tr py:if="not is_separator and item.get('copyfrom_path')" class="$odd_even"> 
     
    148147                    <td class="age" py:content="dateinfo(change.date)" /> 
    149148                    <td class="author" py:content="authorinfo(change.author)" /> 
    150149                    <td class="summary" py:choose=""> 
    151                       <py:when test="verbose or not chgset_view"></py:when> 
     150                      <py:when test="verbose"></py:when> 
    152151                      <py:when test="wiki_format_messages"> 
    153152                        ${wiki_to_oneliner(chgset_context, change.message, shorten=True)} 
    154153                      </py:when> 
  • trac/versioncontrol/templates/revisionlog.txt

    diff --git a/trac/versioncontrol/templates/revisionlog.txt b/trac/versioncontrol/templates/revisionlog.txt
    a b  
    88{%   with change = changes[item.rev]; extra = extra_changes[item.rev] %}\ 
    99${http_date(change.date)} ${format_author(change.author)} [$item.rev] 
    1010{%     for idx, file in enumerate(extra.files) %}\ 
     11{%       if 'FILE_VIEW' in perm(context('source', file, version=change.rev, parent=repos_resource).resource) %}\ 
    1112        * $file (${dict(edit='modified', add='added', delete='deleted', 
    1213                        copy='copied', move='moved')[extra.actions[idx]]}) 
     14{%       end %}\ 
    1315{%     end %}\ 
    1416 
    1517${verbose and extra.message or shorten_line(extra.message)} 
  • trac/versioncontrol/tests/api.py

    diff --git a/trac/versioncontrol/tests/api.py b/trac/versioncontrol/tests/api.py
    a b  
    2222 
    2323    def setUp(self): 
    2424        self.repo_base = Repository('testrepo', {'name': 'testrepo', 'id': 1}, 
    25                                     None, None) 
     25                                    None) 
    2626 
    2727    def test_raise_NotImplementedError_close(self): 
    2828        self.failUnlessRaises(NotImplementedError, self.repo_base.close) 
  • trac/versioncontrol/tests/cache.py

    diff --git a/trac/versioncontrol/tests/cache.py b/trac/versioncontrol/tests/cache.py
    a b  
    4444            raise NoSuchChangeset(rev) 
    4545             
    4646        repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 
    47                      None, self.log, 
     47                     self.log, 
    4848                     get_changeset=no_changeset, 
    4949                     get_oldest_rev=lambda: 1, 
    5050                     get_youngest_rev=lambda: 0, 
    5151                     normalize_rev=no_changeset, 
    5252                     next_rev=lambda x: None) 
    53         cache = CachedRepository(self.env, repos, None, self.log) 
     53        cache = CachedRepository(self.env, repos, self.log) 
    5454        cache.sync() 
    5555 
    5656        cursor = self.db.cursor() 
     
    6969                      Mock(Changeset, 1, 'Import', 'joe', t2, 
    7070                           get_changes=lambda: iter(changes))] 
    7171        repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 
    72                      None, self.log, 
     72                     self.log, 
    7373                     get_changeset=lambda x: changesets[int(x)], 
    7474                     get_oldest_rev=lambda: 0, 
    7575                     get_youngest_rev=lambda: 1, 
    7676                     normalize_rev=lambda x: x, 
    7777                     next_rev=lambda x: int(x) == 0 and 1 or None) 
    78         cache = CachedRepository(self.env, repos, None, self.log) 
     78        cache = CachedRepository(self.env, repos, self.log) 
    7979        cache.sync() 
    8080 
    8181        cursor = self.db.cursor() 
     
    114114        changeset = Mock(Changeset, 2, 'Update', 'joe', t3, 
    115115                         get_changes=lambda: iter(changes)) 
    116116        repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 
    117                      None, self.log, 
     117                     self.log, 
    118118                     get_changeset=lambda x: changeset, 
    119119                     get_youngest_rev=lambda: 2, 
    120120                     get_oldest_rev=lambda: 0, 
    121121                     normalize_rev=lambda x: x,                     
    122122                     next_rev=lambda x: x and int(x) == 1 and 2 or None) 
    123         cache = CachedRepository(self.env, repos, None, self.log) 
     123        cache = CachedRepository(self.env, repos, self.log) 
    124124        cache.sync() 
    125125 
    126126        cursor = self.db.cursor() 
     
    152152                       "WHERE id=1 AND name='youngest_rev'") 
    153153 
    154154        repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 
    155                      None, self.log, 
     155                     self.log, 
    156156                     get_changeset=lambda x: None, 
    157157                     get_youngest_rev=lambda: 1, 
    158158                     get_oldest_rev=lambda: 0, 
    159159                     next_rev=lambda x: None, 
    160160                     normalize_rev=lambda rev: rev) 
    161         cache = CachedRepository(self.env, repos, None, self.log) 
     161        cache = CachedRepository(self.env, repos, self.log) 
    162162        self.assertEqual('1', cache.youngest_rev) 
    163163        changeset = cache.get_changeset(1) 
    164164        self.assertEqual('joe', changeset.author) 
  • trac/versioncontrol/tests/svn_authz.py

    diff --git a/trac/versioncontrol/tests/svn_authz.py b/trac/versioncontrol/tests/svn_authz.py
    a b  
     1# -*- coding: utf-8 -*- 
     2# 
     3# Copyright (C) 2010 Edgewall Software 
     4# All rights reserved. 
     5# 
     6# This software is licensed as described in the file COPYING, which 
     7# you should have received as part of this distribution. The terms 
     8# are also available at http://trac.edgewall.org/wiki/TracLicense. 
     9# 
     10# This software consists of voluntary contributions made by many 
     11# individuals. For the exact contribution history, see the revision 
     12# history and logs, available at http://trac.edgewall.org/log/. 
     13 
     14import os.path 
     15import tempfile 
    116import unittest 
    2 import sys 
    317 
    4 def tests(): 
    5   """ 
    6   Subversion Authz File Permissions 
    7   ================================= 
    8    
    9   Setup code 
    10   ---------- 
    11   We'll use the ``make_auth`` method to create Authorizer objects 
    12   for testing the use of authz files.  ``make_auth`` takes a module name 
    13   and a string for the authz configuration contents. 
    14    
    15   >>> from trac.versioncontrol.svn_authz import RealSubversionAuthorizer 
    16   >>> from StringIO import StringIO 
    17   >>> make_auth = lambda mod, cfg: RealSubversionAuthorizer(None, 
    18   ...                   'user', mod, None, StringIO(cfg)) 
    19    
    20    
    21   Simple operation 
    22   ---------------- 
    23   Returns 1 if no path is given: 
    24       >>> int(make_auth('', '').has_permission(None)) 
    25       1 
    26    
    27   By default read permission is not enabled: 
    28       >>> int(make_auth('', '').has_permission('/')) 
    29       0 
    30    
    31   Read and Write Permissions 
    32   ---------------------- 
    33   Trac is only concerned about read permissions. 
    34       >>> a = make_auth('', ''' 
    35       ... [/readonly] 
    36       ... user = r 
    37       ... [/writeonly] 
    38       ... user = w 
    39       ... [/readwrite] 
    40       ... user = rw 
    41       ... [/empty] 
    42       ... user =  
    43       ... ''') 
    44    
    45   Permissions of 'r' or 'rw' will allow access: 
    46       >>> int(a.has_permission('/readonly')) 
    47       1 
    48       >>> int(a.has_permission('/readwrite')) 
    49       1 
    50    
    51   If only 'w' permission is given, Trac does not allow access: 
    52       >>> int(a.has_permission('/writeonly')) 
    53       0 
    54    
    55   And an empty permission does not give access: 
    56       >>> int(a.has_permission('/empty')) 
    57       0 
    58    
    59   Trailing Slashes 
    60   ---------------- 
    61   Checks all combinations of trailing slashes in the configuration 
    62   or in the path parameter: 
    63       >>> a = make_auth('', ''' 
    64       ... [/a] 
    65       ... user = r 
    66       ... [/b/] 
    67       ... user = r 
    68       ... ''') 
    69       >>> int(a.has_permission('/a')) 
    70       1 
    71       >>> int(a.has_permission('/a/')) 
    72       1 
    73       >>> int(a.has_permission('/b')) 
    74       1 
    75       >>> int(a.has_permission('/b/')) 
    76       1 
    77    
    78    
    79   Module Usage 
    80   ------------ 
    81   If a module name is specified, the rules used are specific to the module. 
    82       >>> a = make_auth('module', ''' 
    83       ... [module:/a] 
    84       ... user = r 
    85       ... [other:/b] 
    86       ... user = r 
    87       ... ''') 
    88       >>> int(a.has_permission('/a')) 
    89       1 
    90       >>> int(a.has_permission('/b')) 
    91       0 
    92    
    93   If a module is specified, but the configuration contains a non-module 
    94   path, the non-module path can still apply: 
    95       >>> int(make_auth('module', ''' 
    96       ... [/a] 
    97       ... user = r 
    98       ... ''').has_permission('/a')) 
    99       1 
    100    
    101   However, the module-specific rule will take precedence if both exist: 
    102       >>> int(make_auth('module', ''' 
    103       ... [module:/a] 
    104       ... user =  
    105       ... [/a] 
    106       ... user = r 
    107       ... ''').has_permission('/a')) 
    108       0 
    109    
    110    
    111   Groups and Wildcards 
    112   -------------------- 
    113   Authz provides a * wildcard for matching any user: 
    114       >>> int(make_auth('', ''' 
    115       ... [/a] 
    116       ... * = r 
    117       ... ''').has_permission('/a')) 
    118       1 
    119    
    120   Groups are specified in a separate section and used with an @ prefix: 
    121       >>> int(make_auth('', ''' 
    122       ... [groups] 
    123       ... grp = user 
    124       ... [/a] 
    125       ... @grp = r 
    126       ... ''').has_permission('/a')) 
    127       1 
     18from trac.resource import Resource 
     19from trac.test import EnvironmentStub 
     20from trac.util import create_file 
     21from trac.versioncontrol.svn_authz import AuthzPermissionPolicy, ParseError, \ 
     22                                          parse 
    12823 
    129   Groups can also be members of other groups: 
    130       >>> int(make_auth('', ''' 
    131       ... [groups] 
    132       ... grp1 = user 
    133       ... grp2 = @grp1 
    134       ... [/a] 
    135       ... @grp2 = r 
    136       ... ''').has_permission('/a')) 
    137       1 
    13824 
    139   Groups should not be defined cyclically, but they are handled appropriately 
    140   to avoid infinite loops: 
    141       >>> int(make_auth('', ''' 
    142       ... [groups] 
    143       ... grp1 = @grp2 
    144       ... grp2 = @grp3 
    145       ... grp3 = @grp1, user 
    146       ... [/a] 
    147       ... @grp1 = r 
    148       ... ''').has_permission('/a')) 
    149       1 
    150    
    151   If more than one group matches at the specific path, access is granted 
    152   if any of the group rules allow access. 
    153       >>> a = make_auth('', ''' 
    154       ... [groups] 
    155       ... grp1 = user 
    156       ... grp2 = user 
    157       ... [/a] 
    158       ... @grp1 = r 
    159       ... @grp2 =  
    160       ... [/b] 
    161       ... @grp1 =  
    162       ... @grp2 = r 
    163       ... ''') 
    164       >>> int(a.has_permission('/a')) 
    165       1 
    166       >>> int(a.has_permission('/b')) 
    167       1 
    168    
    169    
    170   Precedence 
    171   ---------- 
    172   Precedence is user, group, then *: 
    173       >>> a = make_auth('', ''' 
    174       ... [groups] 
    175       ... grp = user 
    176       ... [/a] 
    177       ... @grp = r 
    178       ... user =  
    179       ... [/b] 
    180       ... * = r 
    181       ... @grp =  
    182       ... ''') 
    183    
    184   User specific permission overrides the group permission: 
    185       >>> int(a.has_permission('/a')) 
    186       0 
    187    
    188   And group permission overrides the * permission: 
    189       >>> int(a.has_permission('/b')) 
    190       0 
    191    
    192   The most specific matching path takes precedence: 
    193       >>> a = make_auth('', ''' 
    194       ... [/] 
    195       ... * = r 
    196       ... [/b] 
    197       ... user =  
    198       ... ''') 
    199       >>> int(a.has_permission('/')) 
    200       1 
    201       >>> int(a.has_permission('/a')) 
    202       1 
    203       >>> int(a.has_permission('/b')) 
    204       0 
    205    
    206   Changeset Permissions 
    207   --------------------- 
    208   A test should go here for the changeset permissions. 
    209   """ 
     25class AuthzParserTestCase(unittest.TestCase): 
     26 
     27    def test_parse_file(self): 
     28        authz = parse("""\ 
     29[groups] 
     30developers = foo, bar 
     31users = @developers, &baz 
     32 
     33[aliases] 
     34baz = CN=Hàröld Hacker,OU=Enginéers,DC=red-bean,DC=com 
     35 
     36# Applies to all repositories 
     37[/] 
     38* = r 
     39 
     40[/trunk] 
     41@developers = rw 
     42&baz =  
     43@users = r 
     44 
     45[/branches] 
     46bar = rw 
     47 
     48# Applies only to module 
     49[module:/trunk] 
     50foo = rw 
     51&baz = r 
     52""") 
     53        self.assertEqual({ 
     54            '': { 
     55                '/': { 
     56                    '*': True, 
     57                }, 
     58                '/trunk': { 
     59                    'foo': True, 
     60                    'bar': True, 
     61                    u'CN=Hàröld Hacker,OU=Enginéers,DC=red-bean,DC=com': False, 
     62                }, 
     63                '/branches': { 
     64                    'bar': True, 
     65                }, 
     66            }, 
     67            'module': { 
     68                '/trunk': { 
     69                    'foo': True, 
     70                    u'CN=Hàröld Hacker,OU=Enginéers,DC=red-bean,DC=com': True, 
     71                }, 
     72            }, 
     73        }, authz) 
     74 
     75    def test_parse_errors(self): 
     76        self.assertRaises(ParseError, parse, """\ 
     77user = r 
     78 
     79[module:/trunk] 
     80user = r 
     81""") 
     82        self.assertRaises(ParseError, parse, """\ 
     83[module:/trunk] 
     84user 
     85""") 
     86 
     87 
     88class AuthzPolicyTestCase(unittest.TestCase): 
     89 
     90    def setUp(self): 
     91        tmpdir = os.path.realpath(tempfile.gettempdir()) 
     92        self.authz = os.path.join(tmpdir, 'trac-authz') 
     93        create_file(self.authz, """\ 
     94[groups] 
     95group1 = user 
     96group2 = @group1 
     97 
     98cycle1 = @cycle2 
     99cycle2 = @cycle3 
     100cycle3 = @cycle1, user 
     101 
     102alias1 = &jekyll 
     103alias2 = @alias1 
     104 
     105[aliases] 
     106jekyll = Mr Hyde 
     107 
     108# Read / write permissions 
     109[/readonly] 
     110user = r 
     111[/writeonly] 
     112user = w 
     113[/readwrite] 
     114user = rw 
     115[/empty] 
     116user = 
     117 
     118# Trailing slashes 
     119[/trailing_a] 
     120user = r 
     121[/trailing_b/] 
     122user = r 
     123 
     124# Sub-paths 
     125[/sub/path] 
     126user = r 
     127 
     128# Module usage 
     129[module:/module_a] 
     130user = r 
     131[other:/module_b] 
     132user = r 
     133[/module_c] 
     134user = r 
     135[module:/module_d] 
     136user = 
     137[/module_d] 
     138user = r 
     139 
     140# Wildcards 
     141[/wildcard] 
     142* = r 
     143 
     144# Groups 
     145[/groups_a] 
     146@group1 = r 
     147[/groups_b] 
     148@group2 = r 
     149[/cyclic] 
     150@cycle1 = r 
     151 
     152# Precedence 
     153[module:/precedence_a] 
     154user = 
     155[/precedence_a] 
     156user = r 
     157[/precedence_b] 
     158user = r 
     159[/precedence_b/sub] 
     160user = 
     161[/precedence_b/sub/test] 
     162user = r 
     163[/precedence_c] 
     164user = 
     165@group1 = r 
     166[/precedence_d] 
     167@group1 = r 
     168user = 
     169 
     170# Aliases 
     171[/aliases_a] 
     172&jekyll = r 
     173[/aliases_b] 
     174@alias2 = r 
     175""") 
     176        self.env = EnvironmentStub(enable=[AuthzPermissionPolicy]) 
     177        self.env.config.set('trac', 'authz_file', self.authz) 
     178        self.policy = AuthzPermissionPolicy(self.env) 
     179 
     180    def tearDown(self): 
     181        self.env.reset_db() 
     182        os.remove(self.authz) 
     183 
     184    def assertPermission(self, result, user, reponame, path): 
     185        """Assert that `user` is granted access `result` to `path` within 
     186        the repository `reponame`. 
     187        """ 
     188        resource = Resource('source', path, 
     189                            parent=Resource('repository', reponame)) 
     190        check = self.policy.check_permission('FILE_VIEW', user, resource, None) 
     191        self.assertEqual(result, check) 
     192         
     193    def test_default_permission(self): 
     194        # By default, no permission is granted 
     195        self.assertPermission(False, 'joe', '', '/not_defined') 
     196        self.assertPermission(False, 'jane', 'repo', '/not/defined/either') 
     197 
     198    def test_read_write(self): 
     199        # Allow 'r' and 'rw' entries, deny 'w' and empty entries 
     200        self.assertPermission(True, 'user', '', '/readonly') 
     201        self.assertPermission(True, 'user', '', '/readwrite') 
     202        self.assertPermission(False, 'user', '', '/writeonly') 
     203        self.assertPermission(False, 'user', '', '/empty') 
     204 
     205    def test_trailing_slashes(self): 
     206        # Combinations of trailing slashes in the file and in the path 
     207        self.assertPermission(True, 'user', '', '/trailing_a') 
     208        self.assertPermission(True, 'user', '', '/trailing_a/') 
     209        self.assertPermission(True, 'user', '', '/trailing_b') 
     210        self.assertPermission(True, 'user', '', '/trailing_b/') 
     211 
     212    def test_sub_path(self): 
     213        # Permissions are inherited from containing directories 
     214        self.assertPermission(True, 'user', '', '/sub/path') 
     215        self.assertPermission(True, 'user', '', '/sub/path/test') 
     216        self.assertPermission(True, 'user', '', '/sub/path/other/sub') 
     217         
     218    def test_module_usage(self): 
     219        # If a module name is specified, the rules are specific to the module 
     220        self.assertPermission(True, 'user', 'module', '/module_a') 
     221        self.assertPermission(False, 'user', 'module', '/module_b') 
     222        # If a module is specified, but the configuration contains a non-module 
     223        # path, the non-module path can still apply 
     224        self.assertPermission(True, 'user', 'module', '/module_c') 
     225        # The module-specific rule takes precedence 
     226        self.assertPermission(False, 'user', 'module', '/module_d') 
     227 
     228    def test_wildcard(self): 
     229        # The * wildcard matches all users 
     230        self.assertPermission(True, 'joe', '', '/wildcard') 
     231        self.assertPermission(True, 'jane', '', '/wildcard') 
     232 
     233    def test_groups(self): 
     234        # Groups are specified in a separate section and used with an @ prefix 
     235        self.assertPermission(True, 'user', '', '/groups_a') 
     236        # Groups can also be members of other groups 
     237        self.assertPermission(True, 'user', '', '/groups_b') 
     238        # Groups should not be defined cyclically, but they are still handled 
     239        # correctly to avoid infinite loops 
     240        self.assertPermission(True, 'user', '', '/cyclic') 
     241 
     242    def test_precedence(self): 
     243        # Module-specific sections take precedence over non-module sections 
     244        self.assertPermission(False, 'user', 'module', '/precedence_a') 
     245        # The most specific section applies 
     246        self.assertPermission(True, 'user', '', '/precedence_b/sub/test') 
     247        self.assertPermission(False, 'user', '', '/precedence_b/sub') 
     248        self.assertPermission(True, 'user', '', '/precedence_b') 
     249        # Within a section, the first matching rule applies 
     250        self.assertPermission(False, 'user', '', '/precedence_c') 
     251        self.assertPermission(True, 'user', '', '/precedence_d') 
     252 
     253    def test_aliases(self): 
     254        # Aliases are specified in a separate section and used with an & prefix 
     255        self.assertPermission(True, 'Mr Hyde', '', '/aliases_a') 
     256        # Aliases can also be used in groups 
     257        self.assertPermission(True, 'Mr Hyde', '', '/aliases_b') 
     258 
    210259 
    211260def suite(): 
    212     try: 
    213         from doctest import DocTestSuite 
    214         return DocTestSuite(sys.modules[__name__]) 
    215     except ImportError: 
    216         print >> sys.stderr, "WARNING: DocTestSuite required to run these " \ 
    217                              "tests" 
    218     return unittest.TestSuite() 
     261    suite = unittest.TestSuite() 
     262    suite.addTest(unittest.makeSuite(AuthzParserTestCase, 'test')) 
     263    suite.addTest(unittest.makeSuite(AuthzPolicyTestCase, 'test')) 
     264    return suite 
     265 
    219266 
    220267if __name__ == '__main__': 
    221268    runner = unittest.TextTestRunner() 
  • trac/versioncontrol/tests/svn_fs.py

    diff --git a/trac/versioncontrol/tests/svn_fs.py b/trac/versioncontrol/tests/svn_fs.py
    a b  
    8787    def setUp(self): 
    8888        self.repos = SubversionRepository(REPOS_PATH, 
    8989                                          {'name': 'repo', 'id': 1}, 
    90                                           None, logger_factory('test')) 
     90                                          logger_factory('test')) 
    9191 
    9292    def tearDown(self): 
    9393        self.repos = None 
     
    510510    def setUp(self): 
    511511        self.repos = SubversionRepository(REPOS_PATH + u'/tête', 
    512512                                          {'name': 'repo', 'id': 1}, 
    513                                           None, logger_factory('test')) 
     513                                          logger_factory('test')) 
    514514 
    515515    def tearDown(self): 
    516516        self.repos = None 
     
    760760    def setUp(self): 
    761761        self.repos = SubversionRepository(REPOS_PATH + u'/tête/dir1', 
    762762                                          {'name': 'repo', 'id': 1}, 
    763                                           None, logger_factory('test')) 
     763                                          logger_factory('test')) 
    764764 
    765765    def tearDown(self): 
    766766        self.repos = None 
     
    781781    def setUp(self): 
    782782        self.repos = SubversionRepository(REPOS_PATH + '/tags/v1', 
    783783                                          {'name': 'repo', 'id': 1}, 
    784                                           None, logger_factory('test')) 
     784                                          logger_factory('test')) 
    785785 
    786786    def tearDown(self): 
    787787        self.repos = None 
     
    800800    def setUp(self): 
    801801        self.repos = SubversionRepository(REPOS_PATH + '/branches', 
    802802                                          {'name': 'repo', 'id': 1}, 
    803                                           None, logger_factory('test')) 
     803                                          logger_factory('test')) 
    804804 
    805805    def tearDown(self): 
    806806        self.repos = None 
  • trac/versioncontrol/web_ui/browser.py

    diff --git a/trac/versioncontrol/web_ui/browser.py b/trac/versioncontrol/web_ui/browser.py
    a b  
    332332        xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest' 
    333333         
    334334        rm = RepositoryManager(self.env) 
    335         reponame, repos, path = rm.get_repository_by_path(path, req.authname) 
     335        reponame, repos, path = rm.get_repository_by_path(path) 
    336336 
    337337        # Repository index 
    338338        all_repositories = None 
     
    480480            if not reponame or repoinfo.get('hidden') in _TRUE_VALUES: 
    481481                continue 
    482482            try: 
    483                 repos = rm.get_repository(reponame, context.perm.username) 
     483                repos = rm.get_repository(reponame) 
    484484                if repos: 
    485485                    youngest = repos.get_changeset(repos.youngest_rev) 
    486486                    if self.color_scale and youngest: 
     
    518518                for f in entry.__slots__: 
    519519                    setattr(self, f, getattr(node, f)) 
    520520                 
    521         entries = [entry(n) for n in node.get_entries()] 
     521        repos_resource = Resource('repository', reponame) 
     522        child = repos_resource.child('source') 
     523        entries = [entry(n) for n in node.get_entries() 
     524                   if 'FILE_VIEW' in req.perm(child(id=n.created_path, 
     525                                                    version=n.created_rev))] 
    522526        changes = get_changes(repos, [i.rev for i in entries]) 
    523527 
    524528        if rev: 
  • trac/versioncontrol/web_ui/changeset.py

    diff --git a/trac/versioncontrol/web_ui/changeset.py b/trac/versioncontrol/web_ui/changeset.py
    a b  
    229229 
    230230        rm = RepositoryManager(self.env) 
    231231        if reponame: 
    232             repos = rm.get_repository(reponame, req.authname) 
     232            repos = rm.get_repository(reponame) 
    233233        else: 
    234             reponame, repos, new_path = rm.get_repository_by_path( 
    235                     new_path, req.authname) 
     234            reponame, repos, new_path = rm.get_repository_by_path(new_path) 
    236235 
    237236            if old_path: 
    238                 old_reponame, old_repos, old_path = rm.get_repository_by_path( 
    239                         old_path, req.authname) 
     237                old_reponame, old_repos, old_path = \ 
     238                    rm.get_repository_by_path(old_path) 
    240239                if old_repos != repos: 
    241240                    raise TracError(_("Can't compare across different " 
    242241                                      "repositories: %(old)s vs. %(new)s", 
     
    254253        try: 
    255254            new_path = repos.normalize_path(new_path) 
    256255            new = repos.normalize_rev(new) 
    257              
    258             repos.authz.assert_permission_for_changeset(new) 
    259              
    260256            old_path = repos.normalize_path(old_path or new_path) 
    261257            old = repos.normalize_rev(old or new) 
    262258        except NoSuchChangeset, e: 
     
    500496        options = data['diff']['options'] 
    501497        repos_resource = Resource('repository', reponame) 
    502498 
    503         def _prop_changes(old_node, new_node): 
    504             old_source = Resource('source', old_node.created_path, 
    505                                   version=old_node.created_rev, 
    506                                   parent=repos_resource) 
    507             new_source = Resource('source', new_node.created_path, 
    508                                   version=new_node.created_rev, 
    509                                   parent=repos_resource) 
    510             old_props = new_props = [] 
    511             if 'FILE_VIEW' in req.perm(old_source): 
    512                 old_props = old_node.get_properties() 
    513             if 'FILE_VIEW' in req.perm(new_source): 
    514                 new_props = new_node.get_properties() 
     499        def _prop_changes(old_node, old_source, new_node, new_source): 
     500            old_props = old_node.get_properties() 
     501            new_props = new_node.get_properties() 
    515502            old_ctx = Context.from_request(req, old_source) 
    516503            new_ctx = Context.from_request(req, new_source) 
    517504            changed_properties = [] 
     
    613600        for old_node, new_node, kind, change in get_changes(): 
    614601            props = [] 
    615602            diffs = [] 
     603            old_source = old_node and Resource('source', old_node.created_path, 
     604                                               version=old_node.created_rev, 
     605                                               parent=repos_resource) 
     606            new_source = new_node and Resource('source', new_node.created_path, 
     607                                               version=new_node.created_rev, 
     608                                               parent=repos_resource) 
     609            show_old = old_node and 'FILE_VIEW' in req.perm(old_source) 
     610            show_new = new_node and 'FILE_VIEW' in req.perm(new_source) 
    616611            show_entry = change != Changeset.EDIT 
    617612            show_diff = show_diffs or (new_node and new_node.path == annotated) 
    618613 
    619             if change in Changeset.DIFF_CHANGES and 'FILE_VIEW' in req.perm: 
     614            if change in Changeset.DIFF_CHANGES and show_old and show_new: 
    620615                assert old_node and new_node 
    621                 props = _prop_changes(old_node, new_node) 
     616                props = _prop_changes(old_node, old_source, new_node, 
     617                                      new_source) 
    622618                if props: 
    623619                    show_entry = True 
    624620                if kind == Node.FILE and show_diff: 
     
    628624                            has_diffs = True 
    629625                        # elif None (means: manually compare to (previous)) 
    630626                        show_entry = True 
    631             if show_entry or not show_diff: 
     627            if (show_old or show_new) and (show_entry or not show_diff): 
    632628                info = {'change': change, 
    633629                        'old': old_node and node_info(old_node, annotated), 
    634630                        'new': new_node and node_info(new_node, annotated), 
     
    933929            for reponame in rm.get_all_repositories(): 
    934930                if all_repos or ('repo-' + reponame) in repo_filters: 
    935931                    try: 
    936                         repos = rm.get_repository(reponame, req.authname) 
     932                        repos = rm.get_repository(reponame) 
    937933                        for event in generate_changesets(reponame, repos): 
    938934                            yield event 
    939935                    except TracError, e: 
     
    10551051            rev, path = chgset, '/' 
    10561052        reponame = rm.get_default_repository(formatter.context) 
    10571053        if reponame is not None: 
    1058             repos = rm.get_repository(reponame, authname) 
     1054            repos = rm.get_repository(reponame) 
    10591055        else: 
    1060             reponame, repos, path = rm.get_repository_by_path(path, authname) 
     1056            reponame, repos, path = rm.get_repository_by_path(path) 
    10611057        if path == '/': 
    10621058            path = None 
    10631059 
     
    11571153        if req.get_header('X-Requested-With') == 'XMLHttpRequest': 
    11581154            dirname, prefix = posixpath.split(req.args.get('q')) 
    11591155            prefix = prefix.lower() 
    1160             reponame, repos, path = rm.get_repository_by_path(dirname, 
    1161                                                               req.authname) 
     1156            reponame, repos, path = rm.get_repository_by_path(dirname) 
    11621157            # an entry is a (isdir, name, path) tuple 
    11631158            def kind_order(entry): 
    11641159                return (not entry[0], embedded_numbers(entry[1])) 
     
    11901185 
    11911186        # -- normalize 
    11921187        new_reponame, new_repos, new_path = \ 
    1193             rm.get_repository_by_path(new_path, req.authname) 
     1188            rm.get_repository_by_path(new_path) 
    11941189        old_reponame, old_repos, old_path = \ 
    1195             rm.get_repository_by_path(old_path, req.authname) 
     1190            rm.get_repository_by_path(old_path) 
    11961191        new_rev = new_repos.normalize_rev(new_rev) 
    11971192        old_rev = old_repos.normalize_rev(old_rev) 
    11981193 
  • trac/versioncontrol/web_ui/log.py

    diff --git a/trac/versioncontrol/web_ui/log.py b/trac/versioncontrol/web_ui/log.py
    a b  
    8383        limit = int(req.args.get('limit') or self.default_log_limit) 
    8484 
    8585        reponame, repos, path = RepositoryManager(self.env).\ 
    86                 get_repository_by_path(path, req.authname) 
     86                get_repository_by_path(path) 
    8787        repos_resource = Resource('repository', reponame) 
    8888 
    8989        normpath = repos.normalize_path(path) 
     
    105105        #    unless explicit ranges have been specified 
    106106        #  * for ''show only add, delete'' we're using 
    107107        #   `Repository.get_path_history()`  
     108        cset_resource = Resource('changeset', parent=repos_resource) 
    108109        if mode == 'path_history': 
    109             def history(limit): 
    110                 for h in repos.get_path_history(path, rev, limit): 
    111                     yield h 
     110            def history(): 
     111                for h in repos.get_path_history(path, rev): 
     112                    if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])): 
     113                        yield h 
    112114        elif revranges: 
    113             def history(limit): 
     115            def history(): 
    114116                prevpath = path 
    115117                expected_next_item = None 
    116118                ranges = list(revranges.pairs) 
     
    123125                        p, rev, chg = node_history[0] 
    124126                        if rev < a: 
    125127                            break # simply skip, no separator 
    126                         if expected_next_item: 
    127                             # check whether we're continuing previous range 
    128                             np, nrev, nchg = expected_next_item 
    129                             if rev != nrev: # no, we need a separator 
    130                                 yield (np, nrev, None) 
    131                         yield node_history[0] 
     128                        if 'CHANGESET_VIEW' in req.perm(cset_resource(id=rev)): 
     129                            if expected_next_item: 
     130                                # check whether we're continuing previous range 
     131                                np, nrev, nchg = expected_next_item 
     132                                if rev != nrev: # no, we need a separator 
     133                                    yield (np, nrev, None) 
     134                            yield node_history[0] 
    132135                        prevpath = node_history[-1][0] # follow copy 
    133                         b = rev-1 
     136                        b = rev - 1 
    134137                        if len(node_history) > 1: 
    135138                            expected_next_item = node_history[-1] 
    136139                        else: 
     
    138141                if expected_next_item: 
    139142                    yield (expected_next_item[0], expected_next_item[1], None) 
    140143        else: 
    141             history = get_existing_node(req, repos, path, rev).get_history 
     144            def history(): 
     145                node = get_existing_node(req, repos, path, rev) 
     146                for h in node.get_history(): 
     147                    if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])): 
     148                        yield h 
    142149 
    143150        # -- retrieve history, asking for limit+1 results 
    144151        info = [] 
    145152        depth = 1 
    146153        previous_path = normpath 
    147154        count = 0 
    148         for old_path, old_rev, old_chg in history(limit+1): 
     155        for old_path, old_rev, old_chg in history(): 
    149156            if stop_rev and repos.rev_older_than(old_rev, stop_rev): 
    150157                break 
    151158            old_path = repos.normalize_path(old_path) 
     
    353360        authname = formatter.perm.username 
    354361        reponame = rm.get_default_repository(formatter.context) 
    355362        if reponame is not None: 
    356             repos = rm.get_repository(reponame, authname) 
     363            repos = rm.get_repository(reponame) 
    357364        else: 
    358             reponame, repos, path = rm.get_repository_by_path(path, authname) 
     365            reponame, repos, path = rm.get_repository_by_path(path) 
    359366 
    360367        revranges = None 
    361368        if any(c for c in ':-,' if c in revs):