Edgewall Software

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

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

Added many more permission checks.

  • 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  
    319319         
    320320        @param authname: user name for authorization 
    321321        """ 
    322         return RepositoryManager(self).get_repository(reponame, authname) 
     322        return RepositoryManager(self).get_repository(reponame) 
    323323 
    324324    def create(self, options=[]): 
    325325        """Create the basic directory structure of the environment, initialize 
  • trac/versioncontrol/admin.py

    diff --git a/trac/versioncontrol/admin.py b/trac/versioncontrol/admin.py
    a b  
    8383     
    8484    def _do_changeset_added(self, reponame, *revs): 
    8585        rm = RepositoryManager(self.env) 
    86         rm.notify('changeset_added', reponame, revs, None) 
     86        rm.notify('changeset_added', reponame, revs) 
    8787     
    8888    def _do_changeset_modified(self, reponame, *revs): 
    8989        rm = RepositoryManager(self.env) 
    90         rm.notify('changeset_modified', reponame, revs, None) 
     90        rm.notify('changeset_modified', reponame, revs) 
    9191     
    9292    def _do_list(self): 
    9393        rm = RepositoryManager(self.env) 
     
    106106            if rev is not None: 
    107107                raise TracError(_('Cannot synchronize a single revision ' 
    108108                                  'on multiple repositories')) 
    109             repositories = rm.get_real_repositories(None) 
     109            repositories = rm.get_real_repositories() 
    110110        else: 
    111111            if reponame == '(default)': 
    112112                reponame = '' 
    113             repos = rm.get_repository(reponame, None) 
     113            repos = rm.get_repository(reponame) 
    114114            if repos is None: 
    115115                raise TracError(_("Unknown repository '%(reponame)s'", 
    116116                                  reponame=reponame or '(default)')) 
     
    284284        info['editable'] = editable 
    285285        if not info.get('alias'): 
    286286            try: 
    287                 repos = RepositoryManager(self.env).get_repository(reponame, None) 
     287                repos = RepositoryManager(self.env).get_repository(reponame) 
    288288                info['rev'] = repos.get_youngest_rev() 
    289289            except Exception: 
    290290                pass 
  • trac/versioncontrol/api.py

    diff --git a/trac/versioncontrol/api.py b/trac/versioncontrol/api.py
    a b  
    2626from trac.admin import AdminCommandError, IAdminCommandProvider 
    2727from trac.config import ListOption, Option 
    2828from trac.core import * 
    29 from trac.perm import PermissionError 
    30 from trac.resource import IResourceManager, ResourceNotFound 
     29from trac.resource import IResourceManager, Resource, ResourceNotFound 
    3130from trac.util.text import printout, to_unicode 
    3231from trac.util.translation import _ 
    3332from trac.web.api import IRequestFilter 
     
    318317                if is_default(reponame): 
    319318                    reponame = '' 
    320319                try: 
    321                     repo = self.get_repository(reponame, req.authname) 
     320                    repo = self.get_repository(reponame) 
    322321                    if repo: 
    323322                        repo.sync() 
    324323                except TracError, e: 
     
    422421                    if prio >= 0) 
    423422        return list(types) 
    424423     
    425     def get_repositories_by_dir(self, directory, authname): 
     424    def get_repositories_by_dir(self, directory): 
    426425        """Retrieve the repositories based on the given directory. 
    427426 
    428427           :param directory: the key for identifying the repositories. 
     
    435434            if dir: 
    436435                dir = os.path.join(os.path.normcase(dir), '') 
    437436                if dir.startswith(directory): 
    438                     repos = self.get_repository(reponame, authname) 
     437                    repos = self.get_repository(reponame) 
    439438                    if repos: 
    440439                        repositories.append(repos) 
    441440        return repositories 
     
    459458            db.commit() 
    460459        return id 
    461460     
    462     def get_repository(self, reponame, authname): 
     461    def get_repository(self, reponame): 
    463462        """Retrieve the appropriate Repository for the given name. 
    464463 
    465464           :param reponame: the key for specifying the repository. 
    466465                            If no name is given, take the default  
    467466                            repository. 
    468            :param authname: deprecated (use fine grained permissions) 
    469467           :return: if no corresponding repository was defined,  
    470468                    simply return `None`. 
    471469        """ 
     
    499497        finally: 
    500498            self._lock.release() 
    501499 
    502     def get_repository_by_path(self, path, authname): 
     500    def get_repository_by_path(self, path): 
    503501        """Retrieve a matching Repository for the given path. 
    504502         
    505503        :param path: the eventually scoped repository-scoped path 
     
    519517            path = path[length:] 
    520518        else: 
    521519            reponame = '' 
    522         return (reponame, self.get_repository(reponame, authname), 
     520        return (reponame, self.get_repository(reponame), 
    523521                path.rstrip('/') or '/') 
    524522 
    525523    def get_default_repository(self, context): 
     
    549547                        self._all_repositories[reponame] = info 
    550548        return self._all_repositories 
    551549     
    552     def get_real_repositories(self, authname): 
     550    def get_real_repositories(self): 
    553551        """Return a set of all real repositories (i.e. excluding aliases).""" 
    554552        repositories = set() 
    555553        for reponame in self.get_all_repositories(): 
    556554            try: 
    557                 repos = self.get_repository(reponame, authname) 
     555                repos = self.get_repository(reponame) 
    558556                if repos is not None: 
    559557                    repositories.add(repos) 
    560558            except TracError: 
     
    572570            self._lock.release() 
    573571        self.config.touch()     # Force environment reload 
    574572  
    575     def notify(self, event, reponame, revs, authname): 
     573    def notify(self, event, reponame, revs): 
    576574        """Notify repositories and change listeners about repository events. 
    577575         
    578576        The supported events are the names of the methods defined in the 
     
    583581         
    584582        # Notify a repository by name, and all repositories with the same 
    585583        # base, or all repositories by base or by repository dir 
    586         repos = self.get_repository(reponame, None) 
     584        repos = self.get_repository(reponame) 
    587585        repositories = [] 
    588586        if repos: 
    589587            base = repos.get_base() 
    590588        else: 
    591             repositories = self.get_repositories_by_dir(reponame, None) 
     589            repositories = self.get_repositories_by_dir(reponame) 
    592590            if repositories: 
    593591                base = None 
    594592            else: 
    595593                base = reponame 
    596594        if base: 
    597             repositories = [r for r in self.get_real_repositories(authname) 
     595            repositories = [r for r in self.get_real_repositories() 
    598596                            if r.get_base() == base] 
    599597        if not repositories: 
    600598            self.log.warn("Found no repositories matching '%s' base.", 
     
    676674class Repository(object): 
    677675    """Base class for a repository provided by a version control system.""" 
    678676 
    679     def __init__(self, name, params, authz, log): 
     677    def __init__(self, name, params, log): 
    680678        """Initialize a repository. 
    681679         
    682680           :param name: a unique name identifying the repository, usually a 
     
    686684                          the name of the repository under the key "name" and 
    687685                          the surrogate key that identifies the repository in 
    688686                          the database under the key "id". 
    689            :param authz: a repository authorizer (deprecated). 
    690687           :param log: a logger instance. 
    691688        """ 
    692689        self.name = name 
    693690        self.params = params 
    694691        self.reponame = params['name'] 
    695692        self.id = params['id'] 
    696         self.authz = authz or Authorizer() 
    697693        self.log = log 
     694        self.resource = Resource('repository', self.reponame) 
    698695 
    699696    def close(self): 
    700697        """Close the connection to the repository.""" 
     
    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): 
     
    883879        """ 
    884880        raise NotImplementedError 
    885881 
     882    def can_view(self, perm): 
     883        return 'BROWSER_VIEW' in perm(self.resource.child('source', '/')) 
     884 
    886885 
    887886class Node(object): 
    888887    """Represents a directory or file in the repository at a given revision.""" 
     
    890889    DIRECTORY = "dir" 
    891890    FILE = "file" 
    892891 
     892    resource = property(lambda self: Resource('source', self.created_path, 
     893                                              version=self.created_rev, 
     894                                              parent=self.repos.resource)) 
     895 
    893896    # created_path and created_rev properties refer to the Node "creation" 
    894897    # in the Subversion meaning of a Node in a versioned tree (see #3340). 
    895898    # 
     
    898901    created_rev = None    
    899902    created_path = None 
    900903 
    901     def __init__(self, path, rev, kind): 
     904    def __init__(self, repos, path, rev, kind): 
    902905        assert kind in (Node.DIRECTORY, Node.FILE), \ 
    903906               "Unknown node kind %s" % kind 
     907        self.repos = repos 
    904908        self.path = to_unicode(path) 
    905909        self.rev = rev 
    906910        self.kind = kind 
     
    990994    isdir = property(lambda x: x.kind == Node.DIRECTORY) 
    991995    isfile = property(lambda x: x.kind == Node.FILE) 
    992996 
     997    def can_view(self, perm): 
     998        return (self.isdir and 'BROWSER_VIEW' or 'FILE_VIEW') \ 
     999               in perm(self.resource) 
     1000         
    9931001 
    9941002class Changeset(object): 
    9951003    """Represents a set of changes committed at once in a repository.""" 
     
    10051013    OTHER_CHANGES = (ADD, DELETE) 
    10061014    ALL_CHANGES = DIFF_CHANGES + OTHER_CHANGES 
    10071015 
    1008     def __init__(self, rev, message, author, date): 
     1016    resource = property(lambda self: Resource('changeset', self.rev, 
     1017                                              parent=self.repos.resource)) 
     1018 
     1019    def __init__(self, repos, rev, message, author, date): 
     1020        self.repos = repos 
    10091021        self.rev = rev 
    10101022        self.message = message or '' 
    10111023        self.author = author or '' 
     
    10381050 
    10391051 
    10401052 
    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 
     1053# Note: Since Trac 0.12, Exception PermissionDenied class is gone, 
     1054# and class Authorizer is gone as well. 
     1055# 
     1056# 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  
    1616 
    1717from datetime import datetime 
    1818import os 
    19 import posixpath 
    2019 
    2120from trac.cache import CacheProxy 
    2221from trac.core import TracError 
    2322from trac.util.datefmt import utc, to_timestamp 
    2423from trac.util.translation import _ 
    25 from trac.versioncontrol import Changeset, Node, Repository, Authorizer, \ 
    26                                 NoSuchChangeset 
     24from trac.versioncontrol import Changeset, Node, Repository, NoSuchChangeset 
    2725 
    2826 
    2927_kindmap = {'D': Node.DIRECTORY, 'F': Node.FILE} 
     
    4341 
    4442    scope = property(lambda self: self.repos.scope) 
    4543     
    46     def __init__(self, env, repos, authz, log): 
     44    def __init__(self, env, repos, log): 
    4745        self.env = env 
    4846        self.repos = repos 
    4947        self.metadata = CacheProxy(self.__class__.__module__ + '.' 
    5048                                   + self.__class__.__name__ + '.metadata:' 
    5149                                   + str(self.repos.id), self._metadata, 
    5250                                   self.env) 
    53         Repository.__init__(self, repos.name, repos.params, authz, log) 
     51        Repository.__init__(self, repos.name, repos.params, log) 
    5452 
    5553    def close(self): 
    5654        self.repos.close() 
     
    6563        return self.repos.get_path_url(path, rev) 
    6664 
    6765    def get_changeset(self, rev): 
    68         return CachedChangeset(self.repos, self.normalize_rev(rev), 
    69                                self.env, self.authz) 
     66        return CachedChangeset(self.repos, self.normalize_rev(rev), self.env) 
    7067 
    7168    def get_changeset_uid(self, rev): 
    7269        return self.repos.get_changeset_uid(rev) 
     
    8178                        to_timestamp(stop))) 
    8279        for rev, in cursor: 
    8380            try: 
    84                 if self.authz.has_permission_for_changeset(rev): 
    85                     yield self.get_changeset(rev) 
     81                yield self.get_changeset(rev) 
    8682            except NoSuchChangeset: 
    8783                pass # skip changesets currently being resync'ed 
    8884 
     
    220216            # 1. prepare for resyncing 
    221217            #    (there still might be a race condition at this point) 
    222218 
    223             authz = self.repos.authz 
    224             self.repos.authz = Authorizer() # remove permission checking 
    225  
    226219            kindmap = dict(zip(_kindmap.values(), _kindmap.keys())) 
    227220            actionmap = dict(zip(_actionmap.values(), _actionmap.keys())) 
    228221 
    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 
     222            while next_youngest is not None: 
     223                 
     224                # 1.1 Attempt to resync the 'revision' table 
     225                self.log.info("Trying to sync revision [%s]" % 
     226                              next_youngest) 
     227                cset = self.repos.get_changeset(next_youngest) 
     228                try: 
     229                    cursor.execute("INSERT INTO revision " 
     230                                   " (repos,rev,time,author,message) " 
     231                                   "VALUES (%s,%s,%s,%s,%s)", 
     232                                   (self.id, str(next_youngest), 
     233                                    to_timestamp(cset.date), 
     234                                    cset.author, cset.message)) 
     235                except Exception, e: # *another* 1.1. resync attempt won  
     236                    self.log.warning('Revision %s already cached: %s' % 
     237                                     (next_youngest, e)) 
     238                    # also potentially in progress, so keep ''previous'' 
     239                    # notion of 'youngest' 
     240                    self.repos.clear(youngest_rev=youngest) 
     241                    db.rollback() 
     242                    return 
    251243 
    252                     # 1.2. now *only* one process was able to get there 
    253                     #      (i.e. there *shouldn't* be any race condition here) 
     244                # 1.2. now *only* one process was able to get there 
     245                #      (i.e. there *shouldn't* be any race condition here) 
    254246 
    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)) 
     247                for path, kind, action, bpath, brev in cset.get_changes(): 
     248                    self.log.debug("Caching node change in [%s]: %s" 
     249                                   % (next_youngest, 
     250                                      (path,kind,action,bpath,brev))) 
     251                    kind = kindmap[kind] 
     252                    action = actionmap[action] 
     253                    cursor.execute("INSERT INTO node_change " 
     254                                   " (repos,rev,path,node_type," 
     255                                   "  change_type,base_path,base_rev) " 
     256                                   "VALUES (%s,%s,%s,%s,%s,%s,%s)", 
     257                                   (self.id, str(next_youngest), 
     258                                    path, kind, action, bpath, brev)) 
    267259 
    268                     # 1.3. iterate (1.1 should always succeed now) 
    269                     youngest = next_youngest                     
    270                     next_youngest = self.repos.next_rev(next_youngest) 
     260                # 1.3. iterate (1.1 should always succeed now) 
     261                youngest = next_youngest                     
     262                next_youngest = self.repos.next_rev(next_youngest) 
    271263 
    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() 
     264                # 1.4. update 'youngest_rev' metadata  
     265                #      (minimize possibility of failures at point 0.) 
     266                cursor.execute("UPDATE repository SET value=%s " 
     267                               "WHERE id=%s AND name=%s", 
     268                               (str(youngest), self.id, 
     269                                CACHE_YOUNGEST_REV)) 
     270                self.metadata.invalidate(db) 
     271                db.commit() 
    280272 
    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 
     273                # 1.5. provide some feedback 
     274                if feedback: 
     275                    feedback(youngest) 
    287276 
    288277    def get_node(self, path, rev=None): 
    289278        return self.repos.get_node(path, self.normalize_rev(rev)) 
     
    397386 
    398387class CachedChangeset(Changeset): 
    399388 
    400     def __init__(self, repos, rev, env, authz): 
    401         self.repos = repos 
     389    def __init__(self, repos, rev, env): 
    402390        self.env = env 
    403         self.authz = authz 
    404391        db = self.env.get_db_cnx() 
    405392        cursor = db.cursor() 
    406393        cursor.execute("SELECT time,author,message FROM revision " 
    407394                       "WHERE repos=%s AND rev=%s", 
    408                        (self.repos.id, str(rev))) 
     395                       (repos.id, str(rev))) 
    409396        row = cursor.fetchone() 
    410397        if row: 
    411398            _date, author, message = row 
    412399            date = datetime.fromtimestamp(_date, utc) 
    413             Changeset.__init__(self, rev, message, author, date) 
     400            Changeset.__init__(self, repos, rev, message, author, date) 
    414401        else: 
    415402            raise NoSuchChangeset(rev) 
    416403        self.scope = getattr(repos, 'scope', '') 
     
    422409                       "FROM node_change WHERE repos=%s AND rev=%s " 
    423410                       "ORDER BY path", (self.repos.id, str(self.rev))) 
    424411        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 
    429412            kind = _kindmap[kind] 
    430413            change = _actionmap[change] 
    431414            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 any 
     27from trac.util.text import exception_to_unicode, 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('#') 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 AuthzSourcePolicy(Component): 
     114    """Permission policy for `source:` and `changeset:` resources using a 
     115    Subversion authz file. 
     116     
     117    `FILE_VIEW` and `BROWSER_VIEW` permissions are granted as specified in the 
     118    authz file. 
     119     
     120    `CHANGESET_VIEW` permission is granted for changesets where `FILE_VIEW` is 
     121    granted on at least one modified file, as well as empty for changesets. 
    72122    """ 
    73123 
    74     auth_name = '' 
    75     module_name = '' 
    76     conf_authz = None 
     124    implements(IPermissionPolicy) 
     125     
     126    authz_file = PathOption('trac', 'authz_file', '', 
     127        """Path to the Subversion 
     128        [http://svnbook.red-bean.com/en/1.1/ch06s04.html#svn-ch-6-sect-4.4.2 authorization (authz) file] 
     129        """) 
    77130 
    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) 
     131    authz_module_name = Option('trac', 'authz_module_name', '', 
     132        """The module prefix used in the `authz_file` for the default 
     133        repository. 
     134        """) 
    89135 
    90         self.groups = self._groups() 
     136    _mtime = 0 
     137    _authz = {} 
     138    _users = set() 
     139     
     140    # IPermissionPolicy methods 
    91141 
    92     def has_permission(self, path): 
    93         if path is None: 
    94             return 1 
     142    def check_permission(self, action, username, resource, perm): 
     143        if action == 'FILE_VIEW' or action == 'BROWSER_VIEW': 
     144            authz, users = self._get_authz_info() 
     145            if authz is None: 
     146                return False 
     147            if resource is None: 
     148                return users is True or username in users 
     149            if resource.realm == 'source': 
     150                modules = [resource.parent.id or self.authz_module_name] 
     151                if modules[0]: 
     152                    modules.append('') 
     153                for p in parent_iter(resource.id): 
     154                    for module in modules: 
     155                        section = authz.get(module, {}).get(p, {}) 
     156                        result = section.get(username) 
     157                        if result is not None: 
     158                            return result 
     159                        result = section.get('*') 
     160                        if result is not None: 
     161                            return result 
     162                return False 
     163        elif action == 'CHANGESET_VIEW': 
     164            authz, users = self._get_authz_info() 
     165            if authz is None: 
     166                return False 
     167            if resource is None: 
     168                return users is True or username in users 
     169            if resource.realm == 'changeset': 
     170                rm = RepositoryManager(self.env) 
     171                repos = rm.get_repository(resource.parent.id) 
     172                changes = list(repos.get_changeset(resource.id).get_changes()) 
     173                if not changes: 
     174                    return True 
     175                source = Resource('source', version=resource.id, 
     176                                  parent=resource.parent) 
     177                return any('FILE_VIEW' in perm(source(id=change[0])) 
     178                           for change in changes) 
    95179 
    96         for p in parent_iter(path): 
    97             if self.module_name: 
    98                 for perm in self._get_section(self.module_name + ':' + p): 
    99                     if perm is not None: 
    100                         return perm 
    101             for perm in self._get_section(p): 
    102                 if perm is not None: 
    103                     return perm 
    104  
    105         return 0 
    106  
    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 
    114  
    115     # Internal API 
    116  
    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): 
    149             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 
     180    def _get_authz_info(self): 
     181        try: 
     182            mtime = os.path.getmtime(self.authz_file) 
     183        except OSError, e: 
     184            if self._authz is not None: 
     185                self.log.error('Error accessing authz file: %s', 
     186                               exception_to_unicode(e)) 
     187            self._mtime = mtime = 0 
     188            self._authz = None 
     189            self._users = set() 
     190        if mtime > self._mtime: 
     191            self._mtime = mtime 
     192            self.log.info('Parsing authz file: %s' % self.authz_file) 
     193            try: 
     194                self._authz = parse(read_file(self.authz_file)) 
     195                users = set(user for module in self._authz.itervalues() 
     196                            for path in module.itervalues() 
     197                            for user, result in path.iteritems() if result) 
     198                self._users = '*' in users or users 
     199            except Exception, e: 
     200                self._authz = None 
     201                self._users = set() 
     202                self.log.error('Error parsing authz file: %s', 
     203                               exception_to_unicode(e)) 
     204        return self._authz, self._users 
     205     
     206 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(self, rev, self.scope, 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 
     
    667656class SubversionNode(Node): 
    668657 
    669658    def __init__(self, path, rev, repos, pool=None, parent_root=None): 
    670         self.repos = repos 
    671659        self.fs_ptr = repos.fs_ptr 
    672         self.authz = repos.authz 
    673660        self.scope = repos.scope 
    674661        self._scoped_path_utf8 = _to_svn(self.scope, path) 
    675662        self.pool = Pool(pool) 
     
    700687            self.created_rev, self.created_path = rev, path 
    701688        self.rev = self.created_rev 
    702689        # TODO: check node id 
    703         Node.__init__(self, path, self.rev, _kindmap[node_type]) 
     690        Node.__init__(self, repos, path, self.rev, _kindmap[node_type]) 
    704691 
    705692    def get_content(self): 
    706693        if self.isdir: 
     
    719706        entries = fs.dir_entries(self.root, self._scoped_path_utf8, pool()) 
    720707        for item in entries.keys(): 
    721708            path = posixpath.join(self.path, _from_svn(item)) 
    722             if not self.authz.has_permission(posixpath.join(self.scope, 
    723                                                             path.strip('/'))): 
    724                 continue 
    725709            yield SubversionNode(path, self._requested_rev, self.repos, 
    726710                                 self.pool, self.root) 
    727711 
     
    846830 
    847831class SubversionChangeset(Changeset): 
    848832 
    849     def __init__(self, rev, authz, scope, fs_ptr, pool=None): 
     833    def __init__(self, repos, rev, scope, pool=None): 
    850834        self.rev = rev 
    851         self.authz = authz 
    852835        self.scope = scope 
    853         self.fs_ptr = fs_ptr 
     836        self.fs_ptr = repos.fs_ptr 
    854837        self.pool = Pool(pool) 
    855838        try: 
    856839            message = self._get_prop(core.SVN_PROP_REVISION_LOG) 
     
    866849            date = datetime.fromtimestamp(ts, utc) 
    867850        else: 
    868851            date = None 
    869         Changeset.__init__(self, rev, message, author, date) 
     852        Changeset.__init__(self, repos, rev, message, author, date) 
    870853 
    871854    def get_properties(self): 
    872855        props = fs.revision_proplist(self.fs_ptr, self.rev, self.pool()) 
     
    896879            path = _from_svn(path_utf8) 
    897880 
    898881            # Filtering on `path` 
    899             if not (_is_path_within_scope(self.scope, path) and 
    900                     self.authz.has_permission(path)): 
     882            if not _is_path_within_scope(self.scope, path): 
    901883                continue 
    902884 
    903885            path_utf8 = change.path 
     
    907889            base_rev = change.base_rev 
    908890 
    909891            # 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)): 
     892            if not _is_path_within_scope(self.scope, base_path): 
    912893                base_path, base_rev = None, -1 
    913894 
    914895            # Determine the action 
  • trac/versioncontrol/templates/browser.html

    diff --git a/trac/versioncontrol/templates/browser.html b/trac/versioncontrol/templates/browser.html
    a b  
    4949 
    5050      <py:if test="dir or file"> 
    5151        <py:choose> 
    52           <h1 py:when="repo">Default Repository</h1> 
     52          <h1 py:when="repo and repo.repositories">Default Repository</h1> 
    5353          <h1 py:otherwise=""><xi:include href="path_links.html" /></h1> 
    5454        </py:choose> 
    5555   
  • 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 chgset_context.perm"> 
    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 chgset_context.perm"> 
    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 context('source', file, version=change.rev, parent=repos_resource).perm %}\ 
    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() 
     
    6464        t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc) 
    6565        changes = [('trunk', Node.DIRECTORY, Changeset.ADD, None, None), 
    6666                   ('trunk/README', Node.FILE, Changeset.ADD, None, None)] 
    67         changesets = [Mock(Changeset, 0, '', '', t1, 
    68                            get_changes=lambda: []), 
    69                       Mock(Changeset, 1, 'Import', 'joe', t2, 
    70                            get_changes=lambda: iter(changes))] 
    7167        repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 
    72                      None, self.log, 
     68                     self.log, 
    7369                     get_changeset=lambda x: changesets[int(x)], 
    7470                     get_oldest_rev=lambda: 0, 
    7571                     get_youngest_rev=lambda: 1, 
    7672                     normalize_rev=lambda x: x, 
    7773                     next_rev=lambda x: int(x) == 0 and 1 or None) 
    78         cache = CachedRepository(self.env, repos, None, self.log) 
     74        changesets = [Mock(Changeset, repos, 0, '', '', t1, 
     75                           get_changes=lambda: []), 
     76                      Mock(Changeset, repos, 1, 'Import', 'joe', t2, 
     77                           get_changes=lambda: iter(changes))] 
     78        cache = CachedRepository(self.env, repos, self.log) 
    7979        cache.sync() 
    8080 
    8181        cursor = self.db.cursor() 
     
    111111                       "WHERE id=1 AND name='youngest_rev'") 
    112112 
    113113        changes = [('trunk/README', Node.FILE, Changeset.EDIT, 'trunk/README', 1)] 
    114         changeset = Mock(Changeset, 2, 'Update', 'joe', t3, 
    115                          get_changes=lambda: iter(changes)) 
    116114        repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 
    117                      None, self.log, 
     115                     self.log, 
    118116                     get_changeset=lambda x: changeset, 
    119117                     get_youngest_rev=lambda: 2, 
    120118                     get_oldest_rev=lambda: 0, 
    121119                     normalize_rev=lambda x: x,                     
    122120                     next_rev=lambda x: x and int(x) == 1 and 2 or None) 
    123         cache = CachedRepository(self.env, repos, None, self.log) 
     121        changeset = Mock(Changeset, repos, 2, 'Update', 'joe', t3, 
     122                         get_changes=lambda: iter(changes)) 
     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 AuthzSourcePolicy, 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 AuthzSourcePolicyTestCase(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=[AuthzSourcePolicy]) 
     177        self.env.config.set('trac', 'authz_file', self.authz) 
     178        self.policy = AuthzSourcePolicy(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(AuthzSourcePolicyTestCase, '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  
    2828from trac.perm import IPermissionRequestor 
    2929from trac.resource import Resource, ResourceNotFound 
    3030from trac.util import embedded_numbers 
    31 from trac.util.compat import all 
     31from trac.util.compat import any 
    3232from trac.util.datefmt import http_date, utc 
    3333from trac.util.html import escape, Markup 
    3434from trac.util.text import exception_to_unicode, shorten_line 
     
    290290        return 'browser' 
    291291 
    292292    def get_navigation_items(self, req): 
    293         if 'BROWSER_VIEW' in req.perm: 
    294             all_repos = RepositoryManager(self.env).get_all_repositories() 
    295             if all_repos and not all(info.get('hidden', False) 
    296                                      for info in all_repos.itervalues()): 
    297                 yield ('mainnav', 'browser', 
    298                        tag.a(_('Browse Source'), href=req.href.browser())) 
     293        all_repos = RepositoryManager(self.env).get_all_repositories() 
     294        if any(info.get('hidden') not in _TRUE_VALUES and 'BROWSER_VIEW' 
     295               in req.perm(Resource('repository', name).child('source', '/')) 
     296               for name, info in all_repos.iteritems()): 
     297            yield ('mainnav', 'browser', 
     298                   tag.a(_('Browse Source'), href=req.href.browser())) 
    299299 
    300300    # IPermissionRequestor methods 
    301301 
     
    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 
     
    341341            if all_repositories and repos \ 
    342342                    and all_repositories[''].get('hidden') in _TRUE_VALUES: 
    343343                repos = None 
     344            root_resource = Resource('repository', '').child('source', '/') 
     345            if 'BROWSER_VIEW' not in req.perm(root_resource): 
     346                repos = None 
    344347 
    345348        if not repos and reponame: 
    346349            raise ResourceNotFound(_("No repository '%(repo)s' found", 
     
    375378        if node: 
    376379            if node.isdir: 
    377380                dir_data = self._render_dir( 
    378                         req, reponame, repos, node, rev, order, desc) 
     381                        req, context, reponame, repos, node, rev, order, desc) 
    379382            elif node.isfile: 
    380383                file_data = self._render_file( 
    381384                        req, context, reponame, repos, node, rev) 
     
    467470    # Internal methods 
    468471 
    469472    def _render_repository_index(self, context, all_repositories, order, desc): 
    470         context.perm.require('BROWSER_VIEW') 
    471  
    472473        # Color scale for the age column 
    473474        timerange = custom_colorizer = None 
    474475        if self.color_scale: 
     
    477478        rm = RepositoryManager(self.env) 
    478479        repositories = [] 
    479480        for reponame, repoinfo in all_repositories.items(): 
     481            resource = Resource('repository', reponame).child('source', '/') 
     482            if 'BROWSER_VIEW' not in context.perm(resource): 
     483                continue 
    480484            if not reponame or repoinfo.get('hidden') in _TRUE_VALUES: 
    481485                continue 
    482486            try: 
    483                 repos = rm.get_repository(reponame, context.perm.username) 
     487                repos = rm.get_repository(reponame) 
    484488                if repos: 
    485489                    youngest = repos.get_changeset(repos.youngest_rev) 
    486490                    if self.color_scale and youngest: 
     
    508512        return {'repositories' : repositories, 
    509513                'timerange': timerange, 'colorize_age': custom_colorizer} 
    510514 
    511     def _render_dir(self, req, reponame, repos, node, rev, order, desc): 
    512         req.perm.require('BROWSER_VIEW') 
     515    def _render_dir(self, req, context, reponame, repos, node, rev, order, 
     516                    desc): 
     517        context.perm.require('BROWSER_VIEW') 
    513518 
    514519        # Entries metadata 
    515520        class entry(object): 
     
    518523                for f in entry.__slots__: 
    519524                    setattr(self, f, getattr(node, f)) 
    520525                 
    521         entries = [entry(n) for n in node.get_entries()] 
     526        repos_resource = Resource('repository', reponame) 
     527        entries = [entry(n) for n in node.get_entries() 
     528                   if can_view_node(req.perm, repos_resource, n)] 
    522529        changes = get_changes(repos, [i.rev for i in entries]) 
    523530 
    524531        if rev: 
     
    577584                } 
    578585 
    579586    def _render_file(self, req, context, reponame, repos, node, rev=None): 
    580         req.perm(context.resource).require('FILE_VIEW') 
     587        context.perm.require('FILE_VIEW') 
    581588 
    582589        mimeview = Mimeview(self.env) 
    583590 
     
    777784        order = kwargs.get('order') 
    778785        desc = kwargs.get('desc', 0) 
    779786 
    780         all_repositories = [rdata for rdata in RepositoryManager(self.env).  
    781                                                get_all_repositories().items() 
    782                             if fnmatchcase(rdata[0], glob)] 
     787        rm = RepositoryManager(self.env) 
     788        all_repos = dict(rdata for rdata in rm.get_all_repositories().items() 
     789                         if fnmatchcase(rdata[0], glob)) 
     790 
    783791        if format == 'table': 
    784792            data = self._render_repository_index( 
    785                     formatter.context, all_repositories, order, desc) 
     793                    formatter.context, all_repos, order, desc) 
    786794 
    787795            add_stylesheet(formatter.req, 'common/css/browser.css') 
    788796            from trac.web.chrome import Chrome 
     
    791799                    {'repo': data}, None, fragment=True) 
    792800 
    793801        def repolink(reponame): 
    794             return Markup(tag.a(reponame,  
    795                           title=_('View repository %(repo)s', repo=reponame), 
     802            label = reponame or _('(default)') 
     803            return Markup(tag.a(label,  
     804                          title=_('View repository %(repo)s', repo=label), 
    796805                          href=formatter.href.browser(reponame or None))) 
    797806 
     807        all_repos = sorted( 
     808            r for r in all_repos.iteritems() 
     809            if 'BROWSER_VIEW' in formatter.perm(Resource('repository', r[0]) 
     810                                                  .child('source', '/'))) 
     811         
    798812        if format == 'list': 
    799813            return tag.dl([ 
    800814                tag(tag.dt(repolink(reponame)), 
    801815                    tag.dd(repoinfo.get('description'))) 
    802                 for reponame, repoinfo in all_repositories]) 
     816                for reponame, repoinfo in all_repos]) 
    803817        else: # compact 
    804             return Markup(', ').join([ 
    805                 repolink(reponame) for reponame, repoinfo in all_repositories  
    806                 if reponame]) 
     818            return Markup(', ').join([repolink(reponame) 
     819                                      for reponame, repoinfo in all_repos]) 
    807820 
    808821         
    809822 
  • trac/versioncontrol/web_ui/changeset.py

    diff --git a/trac/versioncontrol/web_ui/changeset.py b/trac/versioncontrol/web_ui/changeset.py
    a b  
    2626from StringIO import StringIO 
    2727 
    2828from genshi.builder import tag 
    29 from genshi.core import Markup 
    3029 
    3130from trac.config import Option, BoolOption, IntOption, _TRUE_VALUES 
    3231from trac.core import * 
     
    4645from trac.versioncontrol.diff import get_diff_options, diff_blocks, \ 
    4746                                     unified_diff 
    4847from trac.versioncontrol.web_ui.browser import BrowserModule 
     48from trac.versioncontrol.web_ui.util import can_view_node 
    4949from trac.web import IRequestHandler, RequestDone 
    5050from trac.web.chrome import add_ctxtnav, add_link, add_script, add_stylesheet, \ 
    5151                            prevnext_nav, INavigationContributor, Chrome 
     
    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: 
     
    309305        data['diff'] = diff_data 
    310306        data['wiki_format_messages'] = self.wiki_format_messages 
    311307 
     308        repos_resource = Resource('repository', reponame) 
    312309        if chgset: 
    313             resource = Resource('repository', reponame).child('changeset', new) 
     310            resource = repos_resource.child('changeset', new) 
    314311            req.perm(resource).require('CHANGESET_VIEW') 
    315312            chgset = repos.get_changeset(new) 
    316313 
     
    324321        format = req.args.get('format') 
    325322 
    326323        if format in ['diff', 'zip']: 
    327             req.perm.require('FILE_VIEW') 
    328324            # choosing an appropriate filename 
    329325            rpath = new_path.replace('/','_') 
    330326            if chgset: 
     
    342338                    filename = 'diff-from-%s-r%s-to-%s-r%s' \ 
    343339                               % (old_path.replace('/','_'), old, rpath, new) 
    344340            if format == 'diff': 
    345                 self._render_diff(req, filename, repos, data) 
     341                self._render_diff(req, filename, repos_resource, repos, data) 
    346342            elif format == 'zip': 
    347                 self._render_zip(req, filename, repos, data) 
     343                self._render_zip(req, filename, repos_resource, repos, data) 
    348344 
    349345        # -- HTML format 
    350346        self._render_html(req, reponame, repos, chgset, restricted, xhr, data) 
     
    478474 
    479475        data['title'] = title 
    480476 
    481         if 'BROWSER_VIEW' not in req.perm: 
     477        if 'BROWSER_VIEW' not in req.perm:  # FIXME: What should we do here? 
    482478            return 
    483479 
    484480        def node_info(node, annotated): 
     
    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 = [] 
     
    584571            else: 
    585572                return [] 
    586573 
    587         if 'FILE_VIEW' in req.perm: 
    588             diff_bytes = diff_files = 0 
    589             if self.max_diff_bytes or self.max_diff_files: 
    590                 for old_node, new_node, kind, change in get_changes(): 
    591                     if change in Changeset.DIFF_CHANGES and kind == Node.FILE: 
    592                         diff_files += 1 
    593                         diff_bytes += _estimate_changes(old_node, new_node) 
    594             show_diffs = (not self.max_diff_files or \ 
    595                           diff_files <= self.max_diff_files) and \ 
    596                          (not self.max_diff_bytes or \ 
    597                           diff_bytes <= self.max_diff_bytes or \ 
    598                           diff_files == 1) 
    599         else: 
    600             show_diffs = False 
     574        diff_bytes = diff_files = 0 
     575        if self.max_diff_bytes or self.max_diff_files: 
     576            for old_node, new_node, kind, change in get_changes(): 
     577                if change in Changeset.DIFF_CHANGES and kind == Node.FILE \ 
     578                        and can_view_node(req.perm, repos_resource, old_node) \ 
     579                        and can_view_node(req.perm, repos_resource, new_node): 
     580                    diff_files += 1 
     581                    diff_bytes += _estimate_changes(old_node, new_node) 
     582        show_diffs = (not self.max_diff_files or \ 
     583                      0 < diff_files <= self.max_diff_files) and \ 
     584                     (not self.max_diff_bytes or \ 
     585                      diff_bytes <= self.max_diff_bytes or \ 
     586                      diff_files == 1) 
    601587 
    602588        # XHR is used for blame support: display the changeset view without 
    603589        # the navigation and with the changes concerning the annotated file 
     
    613599        for old_node, new_node, kind, change in get_changes(): 
    614600            props = [] 
    615601            diffs = [] 
     602            old_source = old_node and Resource('source', old_node.created_path, 
     603                                               version=old_node.created_rev, 
     604                                               parent=repos_resource) 
     605            new_source = new_node and Resource('source', new_node.created_path, 
     606                                               version=new_node.created_rev, 
     607                                               parent=repos_resource) 
     608            show_old = old_node and 'FILE_VIEW' in req.perm(old_source) 
     609            show_new = new_node and 'FILE_VIEW' in req.perm(new_source) 
    616610            show_entry = change != Changeset.EDIT 
    617611            show_diff = show_diffs or (new_node and new_node.path == annotated) 
    618612 
    619             if change in Changeset.DIFF_CHANGES and 'FILE_VIEW' in req.perm: 
     613            if change in Changeset.DIFF_CHANGES and show_old and show_new: 
    620614                assert old_node and new_node 
    621                 props = _prop_changes(old_node, new_node) 
     615                props = _prop_changes(old_node, old_source, new_node, 
     616                                      new_source) 
    622617                if props: 
    623618                    show_entry = True 
    624619                if kind == Node.FILE and show_diff: 
     
    628623                            has_diffs = True 
    629624                        # elif None (means: manually compare to (previous)) 
    630625                        show_entry = True 
    631             if show_entry or not show_diff: 
     626            if (show_old or show_new) and (show_entry or not show_diff): 
    632627                info = {'change': change, 
    633628                        'old': old_node and node_info(old_node, annotated), 
    634629                        'new': new_node and node_info(new_node, annotated), 
     
    682677 
    683678        return data 
    684679 
    685     def _render_diff(self, req, filename, repos, data): 
     680    def _render_diff(self, req, filename, repos_resource, repos, data): 
    686681        """Raw Unified Diff version""" 
    687682        req.send_response(200) 
    688683        req.send_header('Content-Type', 'text/x-patch;charset=utf-8') 
     
    696691            old_path=data['old_path'], old_rev=data['old_rev']): 
    697692            # TODO: Property changes 
    698693 
    699             # Content changes 
    700             if kind == Node.DIRECTORY: 
     694            # Only show allowed files 
     695            if kind == Node.DIRECTORY \ 
     696                    or not can_view_node(req.perm, repos_resource, old_node) \ 
     697                    or not can_view_node(req.perm, repos_resource, new_node): 
    701698                continue 
    702699 
    703700            new_content = old_content = '' 
     
    756753        req.write(diff_str) 
    757754        raise RequestDone 
    758755 
    759     def _render_zip(self, req, filename, repos, data): 
     756    def _render_zip(self, req, filename, repos_resource, repos, data): 
    760757        """ZIP archive with all the added and/or modified files.""" 
    761758        req.send_response(200) 
    762759        req.send_header('Content-Type', 'application/zip') 
     
    770767        for old_node, new_node, kind, change in repos.get_changes( 
    771768            new_path=data['new_path'], new_rev=data['new_rev'], 
    772769            old_path=data['old_path'], old_rev=data['old_rev']): 
    773             if kind == Node.FILE and change != Changeset.DELETE: 
     770            if kind == Node.FILE and change != Changeset.DELETE \ 
     771                    and can_view_node(req.perm, repos_resource, new_node): 
    774772                assert new_node 
    775773                zipinfo = ZipInfo() 
    776774                zipinfo.filename = new_node.path.strip('/').encode('utf-8') 
     
    860858            rm = RepositoryManager(self.env) 
    861859            repositories = rm.get_all_repositories() 
    862860            if len(repositories) > 1: 
    863                 visible_repos = set(name for name, info in repositories.items() 
    864                                     if info.get('hidden') not in _TRUE_VALUES) 
     861                visible_repos = set( 
     862                    name for name, info in repositories.items() 
     863                    if info.get('hidden') not in _TRUE_VALUES 
     864                    and 'BROWSER_VIEW' in req.perm(Resource('repository', name) 
     865                                                       .child('source', '/'))) 
    865866                default_is_aliased = any(info.get('alias') == '' and 
    866867                                         name in visible_repos 
    867868                                         for name, info in repositories.items()) 
     
    933934            for reponame in rm.get_all_repositories(): 
    934935                if all_repos or ('repo-' + reponame) in repo_filters: 
    935936                    try: 
    936                         repos = rm.get_repository(reponame, req.authname) 
     937                        repos = rm.get_repository(reponame) 
    937938                        for event in generate_changesets(reponame, repos): 
    938939                            yield event 
    939940                    except TracError, e: 
     
    987988                elif show_files: 
    988989                    for c, r, repos_for_c in changesets: 
    989990                        for chg in c.get_changes(): 
     991                            resource = r.parent.child('source', chg[0] or '/', 
     992                                                      r.id) 
     993                            if not 'FILE_VIEW' in context.perm(resource): 
     994                                continue 
    990995                            if show_files > 0 and len(files) > show_files: 
    991996                                break 
    992997                            files.append(tag.li(tag.div(class_=chg[2]), 
     
    10461051 
    10471052        # identifying repository 
    10481053        rm = RepositoryManager(self.env) 
    1049         authname = formatter.perm.username 
    10501054        chgset, params, fragment = formatter.split_link(chgset) 
    10511055        sep = chgset.find('/') 
    10521056        if sep > 0: 
     
    10551059            rev, path = chgset, '/' 
    10561060        reponame = rm.get_default_repository(formatter.context) 
    10571061        if reponame is not None: 
    1058             repos = rm.get_repository(reponame, authname) 
     1062            repos = rm.get_repository(reponame) 
    10591063        else: 
    1060             reponame, repos, path = rm.get_repository_by_path(path, authname) 
     1064            reponame, repos, path = rm.get_repository_by_path(path) 
    10611065        if path == '/': 
    10621066            path = None 
    10631067 
     
    11281132            repos = self.env.get_repository(reponame) 
    11291133            if not repos: 
    11301134                continue # revisions for a no longer active repository 
    1131             if not repos.authz.has_permission_for_changeset(rev): 
    1132                 continue 
    1133             # FIXME get rid of .authz and use only the normal Permission system 
    1134             #cset = Resource('repository', reponame).child('changeset' , rev) 
    1135             #cset = repos.resource.child('changeset' , rev) 
    1136             #cset = repos.changeset_resource(rev) 
    11371135            cset = Resource('repository', reponame).child('changeset', rev) 
    11381136            if 'CHANGESET_VIEW' in req.perm(cset): 
    11391137                yield (req.href.changeset(rev, reponame or None), 
     
    11571155        if req.get_header('X-Requested-With') == 'XMLHttpRequest': 
    11581156            dirname, prefix = posixpath.split(req.args.get('q')) 
    11591157            prefix = prefix.lower() 
    1160             reponame, repos, path = rm.get_repository_by_path(dirname, 
    1161                                                               req.authname) 
     1158            reponame, repos, path = rm.get_repository_by_path(dirname) 
    11621159            # an entry is a (isdir, name, path) tuple 
    11631160            def kind_order(entry): 
    11641161                return (not entry[0], embedded_numbers(entry[1])) 
    11651162 
    11661163            if repos: 
    1167                 node = repos.get_node(path) 
     1164                repos_resource = Resource('repository', reponame) 
    11681165                entries = [(e.isdir, e.name,  
    11691166                            '/' + posixpath.join(reponame, e.path)) 
    1170                            for e in repos.get_node(path).get_entries()] 
    1171             else: 
    1172                 entries = [(True, r, '/' + r) 
    1173                            for r in rm.get_all_repositories()] 
     1167                           for e in repos.get_node(path).get_entries() 
     1168                           if can_view_node(req.perm, repos_resource, e)] 
     1169            if not reponame: 
     1170                entries.extend((True, reponame, '/' + reponame) 
     1171                               for reponame in rm.get_all_repositories() 
     1172                               if 'BROWSER_VIEW' in req.perm( 
     1173                                   Resource('repository', reponame) 
     1174                                       .child('source', '/'))) 
    11741175 
    11751176            elem = tag.ul( 
    11761177                [tag.li(isdir and tag.b(path) or path) 
     
    11901191 
    11911192        # -- normalize 
    11921193        new_reponame, new_repos, new_path = \ 
    1193             rm.get_repository_by_path(new_path, req.authname) 
     1194            rm.get_repository_by_path(new_path) 
    11941195        old_reponame, old_repos, old_path = \ 
    1195             rm.get_repository_by_path(old_path, req.authname) 
     1196            rm.get_repository_by_path(old_path) 
    11961197        new_rev = new_repos.normalize_rev(new_rev) 
    11971198        old_rev = old_repos.normalize_rev(old_rev) 
    11981199 
    1199         # FIXME: replace by fine grained permission checks 
    1200         new_repos.authz.assert_permission_for_changeset(new_rev) 
    1201         old_repos.authz.assert_permission_for_changeset(old_rev) 
    1202  
    12031200        # -- prepare rendering 
    12041201        data = {'new_path': posixpath.join(new_reponame, new_path), 
    12051202                'new_rev': new_rev, 
  • 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) 
     
    350357                path, revs = match[:idx], match[idx+1:] 
    351358         
    352359        rm = RepositoryManager(self.env) 
    353         authname = formatter.perm.username 
    354360        reponame = rm.get_default_repository(formatter.context) 
    355361        if reponame is not None: 
    356             repos = rm.get_repository(reponame, authname) 
     362            repos = rm.get_repository(reponame) 
    357363        else: 
    358             reponame, repos, path = rm.get_repository_by_path(path, authname) 
     364            reponame, repos, path = rm.get_repository_by_path(path) 
    359365 
    360366        revranges = None 
    361367        if any(c for c in ':-,' if c in revs): 
  • trac/versioncontrol/web_ui/util.py

    diff --git a/trac/versioncontrol/web_ui/util.py b/trac/versioncontrol/web_ui/util.py
    a b  
    1818 
    1919from genshi.builder import tag 
    2020 
    21 from trac.resource import ResourceNotFound  
     21from trac.resource import Resource, ResourceNotFound  
    2222from trac.util.datefmt import pretty_timedelta 
    2323from trac.util.text import shorten_line 
    2424from trac.util.translation import tag_, _ 
    2525from trac.versioncontrol.api import NoSuchNode, NoSuchChangeset 
    2626 
    27 __all__ = ['get_changes', 'get_path_links', 'get_existing_node'] 
     27__all__ = ['can_view_node', 'get_changes', 'get_path_links', 
     28           'get_existing_node'] 
    2829 
    2930def get_changes(repos, revs): 
    3031    changes = {} 
     
    6869            tag.p(tag_("You can %(search)s in the repository history to see " 
    6970                       "if that path existed but was later removed", 
    7071                       search=search_a)))) 
     72 
     73def can_view_node(perm, repos_resource, node): 
     74    """Return True if the user has permission to view the given node.""" 
     75    return node and (node.isdir and 'BROWSER_VIEW' or 'FILE_VIEW') \ 
     76                    in perm(Resource('source', node.created_path, 
     77                                     version=node.created_rev, 
     78                                     parent=repos_resource))