Ticket #7116: 7116-authz-policy-r9049.patch
| File 7116-authz-policy-r9049.patch, 69.6 KB (added by rblank, 2 years ago) |
|---|
-
setup.py
diff --git a/setup.py b/setup.py
a b 108 108 trac.ticket.web_ui = trac.ticket.web_ui 109 109 trac.timeline = trac.timeline.web_ui 110 110 trac.versioncontrol.admin = trac.versioncontrol.admin 111 trac.versioncontrol.svn_authz = trac.versioncontrol.svn_authz 111 112 trac.versioncontrol.svn_fs = trac.versioncontrol.svn_fs 112 113 trac.versioncontrol.svn_prop = trac.versioncontrol.svn_prop 113 114 trac.versioncontrol.web_ui = trac.versioncontrol.web_ui -
trac/env.py
diff --git a/trac/env.py b/trac/env.py
a b 307 307 308 308 @param authname: user name for authorization 309 309 """ 310 return RepositoryManager(self).get_repository(reponame , authname)310 return RepositoryManager(self).get_repository(reponame) 311 311 312 312 def create(self, options=[]): 313 313 """Create the basic directory structure of the environment, initialize -
trac/versioncontrol/api.py
diff --git a/trac/versioncontrol/api.py b/trac/versioncontrol/api.py
a b 318 318 if is_default(reponame): 319 319 reponame = '' 320 320 try: 321 repo = self.get_repository(reponame , req.authname)321 repo = self.get_repository(reponame) 322 322 if repo: 323 323 repo.sync() 324 324 except TracError, e: … … 422 422 if prio >= 0) 423 423 return list(types) 424 424 425 def get_repositories_by_dir(self, directory , authname):425 def get_repositories_by_dir(self, directory): 426 426 """Retrieve the repositories based on the given directory. 427 427 428 428 :param directory: the key for identifying the repositories. … … 435 435 if dir: 436 436 dir = os.path.join(os.path.normcase(dir), '') 437 437 if dir.startswith(directory): 438 repos = self.get_repository(reponame , authname)438 repos = self.get_repository(reponame) 439 439 if repos: 440 440 repositories.append(repos) 441 441 return repositories … … 459 459 db.commit() 460 460 return id 461 461 462 def get_repository(self, reponame , authname):462 def get_repository(self, reponame): 463 463 """Retrieve the appropriate Repository for the given name. 464 464 465 465 :param reponame: the key for specifying the repository. 466 466 If no name is given, take the default 467 467 repository. 468 :param authname: deprecated (use fine grained permissions)469 468 :return: if no corresponding repository was defined, 470 469 simply return `None`. 471 470 """ … … 499 498 finally: 500 499 self._lock.release() 501 500 502 def get_repository_by_path(self, path , authname):501 def get_repository_by_path(self, path): 503 502 """Retrieve a matching Repository for the given path. 504 503 505 504 :param path: the eventually scoped repository-scoped path … … 519 518 path = path[length:] 520 519 else: 521 520 reponame = '' 522 return (reponame, self.get_repository(reponame , authname),521 return (reponame, self.get_repository(reponame), 523 522 path.rstrip('/') or '/') 524 523 525 524 def get_default_repository(self, context): … … 549 548 self._all_repositories[reponame] = info 550 549 return self._all_repositories 551 550 552 def get_real_repositories(self , authname):551 def get_real_repositories(self): 553 552 """Return a set of all real repositories (i.e. excluding aliases).""" 554 553 repositories = set() 555 554 for reponame in self.get_all_repositories(): 556 555 try: 557 repos = self.get_repository(reponame , authname)556 repos = self.get_repository(reponame) 558 557 if repos is not None: 559 558 repositories.add(repos) 560 559 except TracError: … … 572 571 self._lock.release() 573 572 self.config.touch() # Force environment reload 574 573 575 def notify(self, event, reponame, revs , authname):574 def notify(self, event, reponame, revs): 576 575 """Notify repositories and change listeners about repository events. 577 576 578 577 The supported events are the names of the methods defined in the … … 594 593 else: 595 594 base = reponame 596 595 if base: 597 repositories = [r for r in self.get_real_repositories( authname)596 repositories = [r for r in self.get_real_repositories() 598 597 if r.get_base() == base] 599 598 if not repositories: 600 599 self.log.warn("Found no repositories matching '%s' base.", … … 676 675 class Repository(object): 677 676 """Base class for a repository provided by a version control system.""" 678 677 679 def __init__(self, name, params, authz,log):678 def __init__(self, name, params, log): 680 679 """Initialize a repository. 681 680 682 681 :param name: a unique name identifying the repository, usually a … … 686 685 the name of the repository under the key "name" and 687 686 the surrogate key that identifies the repository in 688 687 the database under the key "id". 689 :param authz: a repository authorizer (deprecated).690 688 :param log: a logger instance. 691 689 """ 692 690 self.name = name 693 691 self.params = params 694 692 self.reponame = params['name'] 695 693 self.id = params['id'] 696 self.authz = authz or Authorizer()697 694 self.log = log 698 695 699 696 def close(self): … … 771 768 """ 772 769 rev = self.youngest_rev 773 770 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 780 776 rev = self.previous_rev(rev) 781 777 782 778 def has_node(self, path, rev=None): … … 1038 1034 1039 1035 1040 1036 1041 class PermissionDenied(PermissionError): 1042 """Exception raised by an authorizer. 1043 1044 This exception is raise if the user has insufficient permissions 1045 to view a specific part of the repository. 1046 """ 1047 def __str__(self): 1048 return self.action 1049 1050 1051 class Authorizer(object): 1052 """Controls the view access to parts of the repository. 1053 1054 Base class for authorizers that are responsible to granting or denying 1055 access to view certain parts of a repository. 1056 """ 1057 1058 def assert_permission(self, path): 1059 if not self.has_permission(path): 1060 raise PermissionDenied(_('Insufficient permissions to access ' 1061 '%(path)s', path=path)) 1062 1063 def assert_permission_for_changeset(self, rev): 1064 if not self.has_permission_for_changeset(rev): 1065 raise PermissionDenied(_('Insufficient permissions to access ' 1066 'changeset %(id)s', id=rev)) 1067 1068 def has_permission(self, path): 1069 return True 1070 1071 def has_permission_for_changeset(self, rev): 1072 return True 1037 # Note: Since Trac 0.12, Exception PermissionDenied class is gone, 1038 # and class Authorizer is gone as well. 1039 # 1040 # Fine-grained permissions are now handled via normal permission policies. -
trac/versioncontrol/cache.py
diff --git a/trac/versioncontrol/cache.py b/trac/versioncontrol/cache.py
a b 22 22 from trac.core import TracError 23 23 from trac.util.datefmt import utc, to_timestamp 24 24 from trac.util.translation import _ 25 from trac.versioncontrol import Changeset, Node, Repository, Authorizer, \ 26 NoSuchChangeset 25 from trac.versioncontrol import Changeset, Node, Repository, NoSuchChangeset 27 26 28 27 29 28 _kindmap = {'D': Node.DIRECTORY, 'F': Node.FILE} … … 43 42 44 43 scope = property(lambda self: self.repos.scope) 45 44 46 def __init__(self, env, repos, authz,log):45 def __init__(self, env, repos, log): 47 46 self.env = env 48 47 self.repos = repos 49 48 self.metadata = CacheProxy(self.__class__.__module__ + '.' 50 49 + self.__class__.__name__ + '.metadata:' 51 50 + str(self.repos.id), self._metadata, 52 51 self.env) 53 Repository.__init__(self, repos.name, repos.params, authz,log)52 Repository.__init__(self, repos.name, repos.params, log) 54 53 55 54 def close(self): 56 55 self.repos.close() … … 65 64 return self.repos.get_path_url(path, rev) 66 65 67 66 def get_changeset(self, rev): 68 return CachedChangeset(self.repos, self.normalize_rev(rev), 69 self.env, self.authz) 67 return CachedChangeset(self.repos, self.normalize_rev(rev), self.env) 70 68 71 69 def get_changeset_uid(self, rev): 72 70 return self.repos.get_changeset_uid(rev) … … 81 79 to_timestamp(stop))) 82 80 for rev, in cursor: 83 81 try: 84 if self.authz.has_permission_for_changeset(rev): 85 yield self.get_changeset(rev) 82 yield self.get_changeset(rev) 86 83 except NoSuchChangeset: 87 84 pass # skip changesets currently being resync'ed 88 85 … … 220 217 # 1. prepare for resyncing 221 218 # (there still might be a race condition at this point) 222 219 223 authz = self.repos.authz224 self.repos.authz = Authorizer() # remove permission checking225 226 220 kindmap = dict(zip(_kindmap.values(), _kindmap.keys())) 227 221 actionmap = dict(zip(_actionmap.values(), _actionmap.keys())) 228 222 229 try: 230 while next_youngest is not None: 231 232 # 1.1 Attempt to resync the 'revision' table 233 self.log.info("Trying to sync revision [%s]" % 234 next_youngest) 235 cset = self.repos.get_changeset(next_youngest) 236 try: 237 cursor.execute("INSERT INTO revision " 238 " (repos,rev,time,author,message) " 239 "VALUES (%s,%s,%s,%s,%s)", 240 (self.id, str(next_youngest), 241 to_timestamp(cset.date), 242 cset.author, cset.message)) 243 except Exception, e: # *another* 1.1. resync attempt won 244 self.log.warning('Revision %s already cached: %s' % 245 (next_youngest, e)) 246 # also potentially in progress, so keep ''previous'' 247 # notion of 'youngest' 248 self.repos.clear(youngest_rev=youngest) 249 db.rollback() 250 return 223 while next_youngest is not None: 224 225 # 1.1 Attempt to resync the 'revision' table 226 self.log.info("Trying to sync revision [%s]" % 227 next_youngest) 228 cset = self.repos.get_changeset(next_youngest) 229 try: 230 cursor.execute("INSERT INTO revision " 231 " (repos,rev,time,author,message) " 232 "VALUES (%s,%s,%s,%s,%s)", 233 (self.id, str(next_youngest), 234 to_timestamp(cset.date), 235 cset.author, cset.message)) 236 except Exception, e: # *another* 1.1. resync attempt won 237 self.log.warning('Revision %s already cached: %s' % 238 (next_youngest, e)) 239 # also potentially in progress, so keep ''previous'' 240 # notion of 'youngest' 241 self.repos.clear(youngest_rev=youngest) 242 db.rollback() 243 return 251 244 252 # 1.2. now *only* one process was able to get there253 # (i.e. there *shouldn't* be any race condition here)245 # 1.2. now *only* one process was able to get there 246 # (i.e. there *shouldn't* be any race condition here) 254 247 255 for path, kind, action, bpath, brev in cset.get_changes():256 self.log.debug("Caching node change in [%s]: %s"257 % (next_youngest,258 (path,kind,action,bpath,brev)))259 kind = kindmap[kind]260 action = actionmap[action]261 cursor.execute("INSERT INTO node_change "262 " (repos,rev,path,node_type,"263 " change_type,base_path,base_rev) "264 "VALUES (%s,%s,%s,%s,%s,%s,%s)",265 (self.id, str(next_youngest),266 path, kind, action, bpath, brev))248 for path, kind, action, bpath, brev in cset.get_changes(): 249 self.log.debug("Caching node change in [%s]: %s" 250 % (next_youngest, 251 (path,kind,action,bpath,brev))) 252 kind = kindmap[kind] 253 action = actionmap[action] 254 cursor.execute("INSERT INTO node_change " 255 " (repos,rev,path,node_type," 256 " change_type,base_path,base_rev) " 257 "VALUES (%s,%s,%s,%s,%s,%s,%s)", 258 (self.id, str(next_youngest), 259 path, kind, action, bpath, brev)) 267 260 268 # 1.3. iterate (1.1 should always succeed now)269 youngest = next_youngest270 next_youngest = self.repos.next_rev(next_youngest)261 # 1.3. iterate (1.1 should always succeed now) 262 youngest = next_youngest 263 next_youngest = self.repos.next_rev(next_youngest) 271 264 272 # 1.4. update 'youngest_rev' metadata273 # (minimize possibility of failures at point 0.)274 cursor.execute("UPDATE repository SET value=%s "275 "WHERE id=%s AND name=%s",276 (str(youngest), self.id,277 CACHE_YOUNGEST_REV))278 self.metadata.invalidate(db)279 db.commit()265 # 1.4. update 'youngest_rev' metadata 266 # (minimize possibility of failures at point 0.) 267 cursor.execute("UPDATE repository SET value=%s " 268 "WHERE id=%s AND name=%s", 269 (str(youngest), self.id, 270 CACHE_YOUNGEST_REV)) 271 self.metadata.invalidate(db) 272 db.commit() 280 273 281 # 1.5. provide some feedback 282 if feedback: 283 feedback(youngest) 284 finally: 285 # 3. restore permission checking (after 1.) 286 self.repos.authz = authz 274 # 1.5. provide some feedback 275 if feedback: 276 feedback(youngest) 287 277 288 278 def get_node(self, path, rev=None): 289 279 return self.repos.get_node(path, self.normalize_rev(rev)) … … 397 387 398 388 class CachedChangeset(Changeset): 399 389 400 def __init__(self, repos, rev, env , authz):390 def __init__(self, repos, rev, env): 401 391 self.repos = repos 402 392 self.env = env 403 self.authz = authz404 393 db = self.env.get_db_cnx() 405 394 cursor = db.cursor() 406 395 cursor.execute("SELECT time,author,message FROM revision " … … 422 411 "FROM node_change WHERE repos=%s AND rev=%s " 423 412 "ORDER BY path", (self.repos.id, str(self.rev))) 424 413 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 continue429 414 kind = _kindmap[kind] 430 415 change = _actionmap[change] 431 416 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 18 18 19 19 import os.path 20 20 21 from trac.config import Option 21 from trac.config import Option, PathOption 22 22 from trac.core import * 23 from trac.versioncontrol import Authorizer 23 from trac.perm import IPermissionPolicy 24 from trac.resource import Resource 25 from trac.util import read_file 26 from trac.util.compat import all 27 from trac.util.text import to_unicode 28 from trac.util.translation import _ 29 from trac.versioncontrol.api import RepositoryManager 24 30 25 31 26 class SvnAuthzOptions(Component):27 28 authz_file = Option('trac', 'authz_file', '',29 """Path to Subversion30 [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 49 32 def parent_iter(path): 50 33 path = path.strip('/') 51 34 if path: … … 56 39 while 1: 57 40 yield path 58 41 if path == '/': 59 r aise StopIteration()42 return 60 43 path = path[:-1] 61 44 yield path 62 45 idx = path.rfind('/') 63 46 path = path[:idx + 1] 64 47 65 48 66 class RealSubversionAuthorizer(Authorizer):67 """ FIXME: this should become a IPermissionPolicy, of course.49 class ParseError(Exception): 50 """Exception thrown for parse errors in authz files""" 68 51 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 53 def parse(authz): 54 """Parse a Subversion authorization file. 55 56 Return a dict of modules, each containing a dict of paths, each containing 57 a dict mapping users to permissions. 58 """ 59 groups = {} 60 aliases = {} 61 sections = {} 62 section = None 63 lineno = 0 64 for line in authz.splitlines(): 65 lineno += 1 66 line = to_unicode(line.strip()) 67 if not line or line.startswith('#'): 68 continue 69 if line.startswith('[') and line.endswith(']'): 70 section = line[1:-1] 71 continue 72 if section is None: 73 raise ParseError(_('Line %(lineno)d: Entry before first ' 74 'section header', lineno=lineno)) 75 parts = line.split('=', 1) 76 if len(parts) != 2: 77 raise ParseError(_('Line %(lineno)d: Invalid entry', 78 lineno=lineno)) 79 name, value = parts 80 name = name.strip() 81 if section == 'groups': 82 group = groups.setdefault(name, set()) 83 group.update(each.strip() for each in value.split(',')) 84 elif section == 'aliases': 85 aliases[name] = value.strip() 86 else: 87 sections.setdefault(section, []).append((name.strip(), value)) 88 89 def resolve(subject, done): 90 if subject.startswith('@'): 91 done.add(subject) 92 for members in groups[subject[1:]] - done: 93 for each in resolve(members, done): 94 yield each 95 elif subject.startswith('&'): 96 yield aliases[subject[1:]] 97 else: 98 yield subject 99 100 authz = {} 101 for name, items in sections.iteritems(): 102 parts = name.split(':', 1) 103 module = authz.setdefault(len(parts) > 1 and parts[0] or '', {}) 104 section = module.setdefault(parts[-1], {}) 105 for subject, perms in items: 106 for user in resolve(subject, set()): 107 section.setdefault(user, 'r' in perms) # The first match wins 108 109 return authz 110 111 112 113 class AuthzPermissionPolicy(Component): 114 """Permission policy for `source:` resources using a Subversion authz 115 file. 72 116 """ 73 117 74 auth_name = '' 75 module_name = '' 76 conf_authz = None 118 implements(IPermissionPolicy) 119 120 authz_file = PathOption('trac', 'authz_file', '', 121 """Path to the Subversion 122 [http://svnbook.red-bean.com/en/1.1/ch06s04.html#svn-ch-6-sect-4.4.2 authorization (authz) file] 123 """) 77 124 78 def __init__(self, repos, auth_name, module_name, cfg_file, cfg_fp=None): 79 self.repos = repos 80 self.auth_name = auth_name 81 self.module_name = module_name 82 83 from ConfigParser import ConfigParser 84 self.conf_authz = ConfigParser() 85 if cfg_fp: 86 self.conf_authz.readfp(cfg_fp, cfg_file) 87 elif cfg_file: 88 self.conf_authz.read(cfg_file) 125 authz_module_name = Option('trac', 'authz_module_name', '', 126 """The module prefix used in the `authz_file` for the default 127 repository. 128 """) 89 129 90 self.groups = self._groups() 130 _mtime = 0 131 132 # IPermissionPolicy methods 91 133 92 def has_permission(self, path): 93 if path is None: 94 return 1 95 96 for p in parent_iter(path): 97 if self.module_name: 98 for perm in self._get_section(self.module_name + ':' + p): 134 def check_permission(self, action, username, resource, perm): 135 # print "*** perm=%r resource=%r" % (action, resource) 136 if resource is None: 137 return True 138 if action == 'FILE_VIEW' and resource.realm == 'source': 139 authz = self._authz 140 modules = [resource.parent.id or self.authz_module_name] 141 if modules[0]: 142 modules.append('') 143 for p in parent_iter(resource.id): 144 for module in modules: 145 section = authz.get(module, {}).get(p, {}) 146 perm = section.get(username) 99 147 if perm is not None: 100 148 return perm 101 for perm in self._get_section(p): 102 if perm is not None: 103 return perm 149 perm = section.get('*') 150 if perm is not None: 151 return perm 152 return False 104 153 105 return 0 154 @property 155 def _authz(self): 156 # TODO: Inefficient. Check only e.g. once per request 157 mtime = os.path.getmtime(self.authz_file) 158 if mtime > self._mtime or not hasattr(self, '_authz_cache'): 159 self.log.debug('Parsing authz file %s') 160 self._mtime = mtime 161 # TODO: Handle parse errors 162 self._authz_cache = parse(read_file(self.authz_file)) 163 return self._authz_cache 106 164 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 check111 # if any changes can be accessed112 return 1113 return 0114 165 115 # Internal API 166 class ChangesetPermissionPolicy(Component): 167 """Default permission policy for changesets. 168 169 This permission policy denies access to changesets containing at least 170 one file, and where access to all files is denied. 171 """ 116 172 117 def _groups(self): 118 if not self.conf_authz.has_section('groups'): 119 return [] 120 121 grp_parents = {} 122 usr_grps = [] 123 124 for group in self.conf_authz.options('groups'): 125 for member in self.conf_authz.get('groups', group).split(','): 126 member = member.strip() 127 if member == self.auth_name: 128 usr_grps.append(group) 129 elif member.startswith('@'): 130 grp_parents.setdefault(member[1:], []).append(group) 131 132 expanded = {} 133 134 def expand_group(group): 135 if group in expanded: 136 return 137 expanded[group] = True 138 for parent in grp_parents.get(group, []): 139 expand_group(parent) 140 141 for g in usr_grps: 142 expand_group(g) 143 144 # expand groups 145 return expanded.keys() 146 147 def _get_section(self, section): 148 if not self.conf_authz.has_section(section): 173 implements(IPermissionPolicy) 174 175 def check_permission(self, action, username, resource, perm): 176 if resource is None: 149 177 return 150 151 yield self._get_permission(section, self.auth_name) 152 153 group_perm = None 154 for g in self.groups: 155 p = self._get_permission(section, '@' + g) 156 if p is not None: 157 group_perm = p 158 159 if group_perm: 160 yield 1 161 162 yield group_perm 163 164 yield self._get_permission(section, '*') 165 166 def _get_permission(self, section, subject): 167 if self.conf_authz.has_option(section, subject): 168 return 'r' in self.conf_authz.get(section, subject) 169 return None 178 if action == 'CHANGESET_VIEW' and resource.realm == 'changeset': 179 rm = RepositoryManager(self.env) 180 repos = rm.get_repository(resource.parent.id) 181 changes = list(repos.get_changeset(resource.id).get_changes()) 182 if changes: 183 source = Resource('source', version=resource.id, 184 parent=resource.parent) 185 if all('FILE_VIEW' not in perm(source(id=change[0])) 186 for change in changes): 187 return False 188 189 No newline at end of file -
trac/versioncontrol/svn_fs.py
diff --git a/trac/versioncontrol/svn_fs.py b/trac/versioncontrol/svn_fs.py
a b 54 54 IRepositoryConnector, \ 55 55 NoSuchChangeset, NoSuchNode 56 56 from trac.versioncontrol.cache import CachedRepository 57 from trac.versioncontrol.svn_authz import SubversionAuthorizer58 57 from trac.util import embedded_numbers 59 58 from trac.util.text import exception_to_unicode, to_unicode 60 59 from trac.util.translation import _ … … 280 279 self._version = self._get_version() 281 280 self.env.systeminfo.append(('Subversion', self._version)) 282 281 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) 284 283 if type == 'direct-svnfs': 285 284 repos = fs_repos 286 285 else: 287 repos = CachedRepository(self.env, fs_repos, None,self.log)286 repos = CachedRepository(self.env, fs_repos, self.log) 288 287 repos.has_linear_changesets = True 289 # FIXME: convert SubversionAuthorizer to a PermissionPolicy290 if 'authname' in params:291 authz = SubversionAuthorizer(self.env, weakref.proxy(repos),292 params['authname'])293 repos.authz = fs_repos.authz = authz294 288 return repos 295 289 296 290 def _get_version(self): … … 305 299 class SubversionRepository(Repository): 306 300 """Repository implementation based on the svn.fs API.""" 307 301 308 def __init__(self, path, params, authz,log):302 def __init__(self, path, params, log): 309 303 self.log = log 310 304 self.pool = Pool() 311 305 … … 335 329 self.base = 'svn:%s:%s' % (self.uuid, _from_svn(root_path_utf8)) 336 330 name = 'svn:%s:%s' % (self.uuid, self.path) 337 331 338 Repository.__init__(self, name, params, authz,log)332 Repository.__init__(self, name, params, log) 339 333 340 334 # if root_path_utf8 is shorter than the path_utf8, the difference is 341 335 # this scope (which always starts with a '/') … … 430 424 431 425 def get_changeset(self, rev): 432 426 rev = self.normalize_rev(rev) 433 return SubversionChangeset(rev, self.authz, self.scope, 434 self.fs_ptr, self.pool) 427 return SubversionChangeset(rev, self.scope, self.fs_ptr, self.pool) 435 428 436 429 def get_changeset_uid(self, rev): 437 430 return (self.uuid, rev) 438 431 439 432 def get_node(self, path, rev=None): 440 433 path = path or '' 441 self.authz.assert_permission(posixpath.join(self.scope,442 path.strip('/')))443 434 if path and path[-1] == '/': 444 435 path = path[:-1] 445 436 … … 490 481 if rev < end: 491 482 break 492 483 path = _from_svn(path_utf8) 493 if not self.authz.has_permission(path):494 break495 484 yield path, rev 496 485 del tmp1 497 486 del tmp2 … … 669 658 def __init__(self, path, rev, repos, pool=None, parent_root=None): 670 659 self.repos = repos 671 660 self.fs_ptr = repos.fs_ptr 672 self.authz = repos.authz673 661 self.scope = repos.scope 674 662 self._scoped_path_utf8 = _to_svn(self.scope, path) 675 663 self.pool = Pool(pool) … … 719 707 entries = fs.dir_entries(self.root, self._scoped_path_utf8, pool()) 720 708 for item in entries.keys(): 721 709 path = posixpath.join(self.path, _from_svn(item)) 722 if not self.authz.has_permission(posixpath.join(self.scope,723 path.strip('/'))):724 continue725 710 yield SubversionNode(path, self._requested_rev, self.repos, 726 711 self.pool, self.root) 727 712 … … 846 831 847 832 class SubversionChangeset(Changeset): 848 833 849 def __init__(self, rev, authz,scope, fs_ptr, pool=None):834 def __init__(self, rev, scope, fs_ptr, pool=None): 850 835 self.rev = rev 851 self.authz = authz852 836 self.scope = scope 853 837 self.fs_ptr = fs_ptr 854 838 self.pool = Pool(pool) … … 896 880 path = _from_svn(path_utf8) 897 881 898 882 # Filtering on `path` 899 if not (_is_path_within_scope(self.scope, path) and 900 self.authz.has_permission(path)): 883 if not _is_path_within_scope(self.scope, path): 901 884 continue 902 885 903 886 path_utf8 = change.path … … 907 890 base_rev = change.base_rev 908 891 909 892 # Ensure `base_path` is within the scope 910 if not (_is_path_within_scope(self.scope, base_path) and 911 self.authz.has_permission(base_path)): 893 if not _is_path_within_scope(self.scope, base_path): 912 894 base_path, base_rev = None, -1 913 895 914 896 # Determine the action -
trac/versioncontrol/templates/dir_entries.html
diff --git a/trac/versioncontrol/templates/dir_entries.html b/trac/versioncontrol/templates/dir_entries.html
a b 6 6 <xi:include href="macros.html" /> 7 7 </py:if> 8 8 <py:for each="idx, entry in enumerate(dir.entries)"> 9 <py:with vars="change = dir.changes[entry.rev]"> 9 <py:with vars="change = dir.changes[entry.rev]; 10 chgset_context = change and context('changeset', change.rev, parent=repos_resource); 11 chgset_view = change and 'CHANGESET_VIEW' in perm(chgset_context.resource)"> 10 12 <tr class="${idx % 2 and 'even' or 'odd'}"> 11 13 <td class="name"> 12 14 <a class="$entry.kind" title="View ${entry.kind.capitalize()}" … … 18 20 <a title="View Revision Log" href="${href.log(reponame, entry.path, rev=rev)}">$entry.rev</a> 19 21 <a title="View Changeset" class="chgset" href="${href.changeset(change.rev, reponame)}"> </a> 20 22 </td> 21 <td class="age" style="${ch angeand 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)' % 22 24 dir.colorize_age(dir.timerange.relative(change.date)) or None}"> 23 ${ change and dateinfo(change.date) or '-'}25 ${('–', dateinfo(change.date))[chgset_view]} 24 26 </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>–</py:otherwise> 35 38 </td> 36 39 </tr> 37 40 </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 5 5 <table class="listing dirlist" id="${repoindex or None}"> 6 6 <xi:include href="dirlist_thead.html" /> 7 7 <tbody> 8 <py:for each="idx, (reponame, repoinfo, change, err) in enumerate(repo.repositories)"> 8 <py:for each="idx, (reponame, repoinfo, change, err) in enumerate(repo.repositories)" 9 py:with="chgset_context = change and context('changeset', change.rev, parent=Resource('repository', reponame)); 10 chgset_view = change and 'CHANGESET_VIEW' in perm(chgset_context.resource)"> 9 11 <tr class="${idx % 2 and 'even' or 'odd'}"> 10 12 <td class="name"> 11 13 <em py:strip="not err"> … … 22 24 <a title="View Changeset" class="chgset" href="${href.changeset(change.rev, reponame)}"> </a> 23 25 </py:if> 24 26 </td> 25 <td class="age" style="${ch ange 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 ${('–', dateinfo(change.date))[chgset_view]} 28 30 </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>–</py:otherwise> 40 43 </td> 41 44 </tr> 42 45 <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 111 111 <py:with vars="change = changes[item.rev]; 112 112 is_separator = item.change is None; 113 113 chgset_context = context('changeset', change.rev, parent=repos_resource); 114 chgset_view = 'CHANGESET_VIEW' in perm(chgset_context.resource);115 114 odd_even = idx % 2 and 'odd' or 'even'"> 116 115 <!--! highlight copy or rename operations --> 117 116 <tr py:if="not is_separator and item.get('copyfrom_path')" class="$odd_even"> … … 148 147 <td class="age" py:content="dateinfo(change.date)" /> 149 148 <td class="author" py:content="authorinfo(change.author)" /> 150 149 <td class="summary" py:choose=""> 151 <py:when test="verbose or not chgset_view"></py:when>150 <py:when test="verbose"></py:when> 152 151 <py:when test="wiki_format_messages"> 153 152 ${wiki_to_oneliner(chgset_context, change.message, shorten=True)} 154 153 </py:when> -
trac/versioncontrol/templates/revisionlog.txt
diff --git a/trac/versioncontrol/templates/revisionlog.txt b/trac/versioncontrol/templates/revisionlog.txt
a b 8 8 {% with change = changes[item.rev]; extra = extra_changes[item.rev] %}\ 9 9 ${http_date(change.date)} ${format_author(change.author)} [$item.rev] 10 10 {% for idx, file in enumerate(extra.files) %}\ 11 {% if 'FILE_VIEW' in perm(context('source', file, version=change.rev, parent=repos_resource).resource) %}\ 11 12 * $file (${dict(edit='modified', add='added', delete='deleted', 12 13 copy='copied', move='moved')[extra.actions[idx]]}) 14 {% end %}\ 13 15 {% end %}\ 14 16 15 17 ${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 22 22 23 23 def setUp(self): 24 24 self.repo_base = Repository('testrepo', {'name': 'testrepo', 'id': 1}, 25 None , None)25 None) 26 26 27 27 def test_raise_NotImplementedError_close(self): 28 28 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 44 44 raise NoSuchChangeset(rev) 45 45 46 46 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 47 None,self.log,47 self.log, 48 48 get_changeset=no_changeset, 49 49 get_oldest_rev=lambda: 1, 50 50 get_youngest_rev=lambda: 0, 51 51 normalize_rev=no_changeset, 52 52 next_rev=lambda x: None) 53 cache = CachedRepository(self.env, repos, None,self.log)53 cache = CachedRepository(self.env, repos, self.log) 54 54 cache.sync() 55 55 56 56 cursor = self.db.cursor() … … 69 69 Mock(Changeset, 1, 'Import', 'joe', t2, 70 70 get_changes=lambda: iter(changes))] 71 71 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 72 None,self.log,72 self.log, 73 73 get_changeset=lambda x: changesets[int(x)], 74 74 get_oldest_rev=lambda: 0, 75 75 get_youngest_rev=lambda: 1, 76 76 normalize_rev=lambda x: x, 77 77 next_rev=lambda x: int(x) == 0 and 1 or None) 78 cache = CachedRepository(self.env, repos, None,self.log)78 cache = CachedRepository(self.env, repos, self.log) 79 79 cache.sync() 80 80 81 81 cursor = self.db.cursor() … … 114 114 changeset = Mock(Changeset, 2, 'Update', 'joe', t3, 115 115 get_changes=lambda: iter(changes)) 116 116 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 117 None,self.log,117 self.log, 118 118 get_changeset=lambda x: changeset, 119 119 get_youngest_rev=lambda: 2, 120 120 get_oldest_rev=lambda: 0, 121 121 normalize_rev=lambda x: x, 122 122 next_rev=lambda x: x and int(x) == 1 and 2 or None) 123 cache = CachedRepository(self.env, repos, None,self.log)123 cache = CachedRepository(self.env, repos, self.log) 124 124 cache.sync() 125 125 126 126 cursor = self.db.cursor() … … 152 152 "WHERE id=1 AND name='youngest_rev'") 153 153 154 154 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 155 None,self.log,155 self.log, 156 156 get_changeset=lambda x: None, 157 157 get_youngest_rev=lambda: 1, 158 158 get_oldest_rev=lambda: 0, 159 159 next_rev=lambda x: None, 160 160 normalize_rev=lambda rev: rev) 161 cache = CachedRepository(self.env, repos, None,self.log)161 cache = CachedRepository(self.env, repos, self.log) 162 162 self.assertEqual('1', cache.youngest_rev) 163 163 changeset = cache.get_changeset(1) 164 164 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 14 import os.path 15 import tempfile 1 16 import unittest 2 import sys3 17 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 18 from trac.resource import Resource 19 from trac.test import EnvironmentStub 20 from trac.util import create_file 21 from trac.versioncontrol.svn_authz import AuthzPermissionPolicy, ParseError, \ 22 parse 128 23 129 Groups can also be members of other groups:130 >>> int(make_auth('', '''131 ... [groups]132 ... grp1 = user133 ... grp2 = @grp1134 ... [/a]135 ... @grp2 = r136 ... ''').has_permission('/a'))137 1138 24 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 """ 25 class AuthzParserTestCase(unittest.TestCase): 26 27 def test_parse_file(self): 28 authz = parse("""\ 29 [groups] 30 developers = foo, bar 31 users = @developers, &baz 32 33 [aliases] 34 baz = 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] 46 bar = rw 47 48 # Applies only to module 49 [module:/trunk] 50 foo = 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, """\ 77 user = r 78 79 [module:/trunk] 80 user = r 81 """) 82 self.assertRaises(ParseError, parse, """\ 83 [module:/trunk] 84 user 85 """) 86 87 88 class AuthzPolicyTestCase(unittest.TestCase): 89 90 def setUp(self): 91 tmpdir = os.path.realpath(tempfile.gettempdir()) 92 self.authz = os.path.join(tmpdir, 'trac-authz') 93 create_file(self.authz, """\ 94 [groups] 95 group1 = user 96 group2 = @group1 97 98 cycle1 = @cycle2 99 cycle2 = @cycle3 100 cycle3 = @cycle1, user 101 102 alias1 = &jekyll 103 alias2 = @alias1 104 105 [aliases] 106 jekyll = Mr Hyde 107 108 # Read / write permissions 109 [/readonly] 110 user = r 111 [/writeonly] 112 user = w 113 [/readwrite] 114 user = rw 115 [/empty] 116 user = 117 118 # Trailing slashes 119 [/trailing_a] 120 user = r 121 [/trailing_b/] 122 user = r 123 124 # Sub-paths 125 [/sub/path] 126 user = r 127 128 # Module usage 129 [module:/module_a] 130 user = r 131 [other:/module_b] 132 user = r 133 [/module_c] 134 user = r 135 [module:/module_d] 136 user = 137 [/module_d] 138 user = 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] 154 user = 155 [/precedence_a] 156 user = r 157 [/precedence_b] 158 user = r 159 [/precedence_b/sub] 160 user = 161 [/precedence_b/sub/test] 162 user = r 163 [/precedence_c] 164 user = 165 @group1 = r 166 [/precedence_d] 167 @group1 = r 168 user = 169 170 # Aliases 171 [/aliases_a] 172 &jekyll = r 173 [/aliases_b] 174 @alias2 = r 175 """) 176 self.env = EnvironmentStub(enable=[AuthzPermissionPolicy]) 177 self.env.config.set('trac', 'authz_file', self.authz) 178 self.policy = AuthzPermissionPolicy(self.env) 179 180 def tearDown(self): 181 self.env.reset_db() 182 os.remove(self.authz) 183 184 def assertPermission(self, result, user, reponame, path): 185 """Assert that `user` is granted access `result` to `path` within 186 the repository `reponame`. 187 """ 188 resource = Resource('source', path, 189 parent=Resource('repository', reponame)) 190 check = self.policy.check_permission('FILE_VIEW', user, resource, None) 191 self.assertEqual(result, check) 192 193 def test_default_permission(self): 194 # By default, no permission is granted 195 self.assertPermission(False, 'joe', '', '/not_defined') 196 self.assertPermission(False, 'jane', 'repo', '/not/defined/either') 197 198 def test_read_write(self): 199 # Allow 'r' and 'rw' entries, deny 'w' and empty entries 200 self.assertPermission(True, 'user', '', '/readonly') 201 self.assertPermission(True, 'user', '', '/readwrite') 202 self.assertPermission(False, 'user', '', '/writeonly') 203 self.assertPermission(False, 'user', '', '/empty') 204 205 def test_trailing_slashes(self): 206 # Combinations of trailing slashes in the file and in the path 207 self.assertPermission(True, 'user', '', '/trailing_a') 208 self.assertPermission(True, 'user', '', '/trailing_a/') 209 self.assertPermission(True, 'user', '', '/trailing_b') 210 self.assertPermission(True, 'user', '', '/trailing_b/') 211 212 def test_sub_path(self): 213 # Permissions are inherited from containing directories 214 self.assertPermission(True, 'user', '', '/sub/path') 215 self.assertPermission(True, 'user', '', '/sub/path/test') 216 self.assertPermission(True, 'user', '', '/sub/path/other/sub') 217 218 def test_module_usage(self): 219 # If a module name is specified, the rules are specific to the module 220 self.assertPermission(True, 'user', 'module', '/module_a') 221 self.assertPermission(False, 'user', 'module', '/module_b') 222 # If a module is specified, but the configuration contains a non-module 223 # path, the non-module path can still apply 224 self.assertPermission(True, 'user', 'module', '/module_c') 225 # The module-specific rule takes precedence 226 self.assertPermission(False, 'user', 'module', '/module_d') 227 228 def test_wildcard(self): 229 # The * wildcard matches all users 230 self.assertPermission(True, 'joe', '', '/wildcard') 231 self.assertPermission(True, 'jane', '', '/wildcard') 232 233 def test_groups(self): 234 # Groups are specified in a separate section and used with an @ prefix 235 self.assertPermission(True, 'user', '', '/groups_a') 236 # Groups can also be members of other groups 237 self.assertPermission(True, 'user', '', '/groups_b') 238 # Groups should not be defined cyclically, but they are still handled 239 # correctly to avoid infinite loops 240 self.assertPermission(True, 'user', '', '/cyclic') 241 242 def test_precedence(self): 243 # Module-specific sections take precedence over non-module sections 244 self.assertPermission(False, 'user', 'module', '/precedence_a') 245 # The most specific section applies 246 self.assertPermission(True, 'user', '', '/precedence_b/sub/test') 247 self.assertPermission(False, 'user', '', '/precedence_b/sub') 248 self.assertPermission(True, 'user', '', '/precedence_b') 249 # Within a section, the first matching rule applies 250 self.assertPermission(False, 'user', '', '/precedence_c') 251 self.assertPermission(True, 'user', '', '/precedence_d') 252 253 def test_aliases(self): 254 # Aliases are specified in a separate section and used with an & prefix 255 self.assertPermission(True, 'Mr Hyde', '', '/aliases_a') 256 # Aliases can also be used in groups 257 self.assertPermission(True, 'Mr Hyde', '', '/aliases_b') 258 210 259 211 260 def suite(): 212 try: 213 from doctest import DocTestSuite 214 return DocTestSuite(sys.modules[__name__]) 215 except ImportError: 216 print >> sys.stderr, "WARNING: DocTestSuite required to run these " \ 217 "tests" 218 return unittest.TestSuite() 261 suite = unittest.TestSuite() 262 suite.addTest(unittest.makeSuite(AuthzParserTestCase, 'test')) 263 suite.addTest(unittest.makeSuite(AuthzPolicyTestCase, 'test')) 264 return suite 265 219 266 220 267 if __name__ == '__main__': 221 268 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 87 87 def setUp(self): 88 88 self.repos = SubversionRepository(REPOS_PATH, 89 89 {'name': 'repo', 'id': 1}, 90 None,logger_factory('test'))90 logger_factory('test')) 91 91 92 92 def tearDown(self): 93 93 self.repos = None … … 510 510 def setUp(self): 511 511 self.repos = SubversionRepository(REPOS_PATH + u'/tête', 512 512 {'name': 'repo', 'id': 1}, 513 None,logger_factory('test'))513 logger_factory('test')) 514 514 515 515 def tearDown(self): 516 516 self.repos = None … … 760 760 def setUp(self): 761 761 self.repos = SubversionRepository(REPOS_PATH + u'/tête/dir1', 762 762 {'name': 'repo', 'id': 1}, 763 None,logger_factory('test'))763 logger_factory('test')) 764 764 765 765 def tearDown(self): 766 766 self.repos = None … … 781 781 def setUp(self): 782 782 self.repos = SubversionRepository(REPOS_PATH + '/tags/v1', 783 783 {'name': 'repo', 'id': 1}, 784 None,logger_factory('test'))784 logger_factory('test')) 785 785 786 786 def tearDown(self): 787 787 self.repos = None … … 800 800 def setUp(self): 801 801 self.repos = SubversionRepository(REPOS_PATH + '/branches', 802 802 {'name': 'repo', 'id': 1}, 803 None,logger_factory('test'))803 logger_factory('test')) 804 804 805 805 def tearDown(self): 806 806 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 332 332 xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest' 333 333 334 334 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) 336 336 337 337 # Repository index 338 338 all_repositories = None … … 480 480 if not reponame or repoinfo.get('hidden') in _TRUE_VALUES: 481 481 continue 482 482 try: 483 repos = rm.get_repository(reponame , context.perm.username)483 repos = rm.get_repository(reponame) 484 484 if repos: 485 485 youngest = repos.get_changeset(repos.youngest_rev) 486 486 if self.color_scale and youngest: … … 518 518 for f in entry.__slots__: 519 519 setattr(self, f, getattr(node, f)) 520 520 521 entries = [entry(n) for n in node.get_entries()] 521 repos_resource = Resource('repository', reponame) 522 child = repos_resource.child('source') 523 entries = [entry(n) for n in node.get_entries() 524 if 'FILE_VIEW' in req.perm(child(id=n.created_path, 525 version=n.created_rev))] 522 526 changes = get_changes(repos, [i.rev for i in entries]) 523 527 524 528 if rev: -
trac/versioncontrol/web_ui/changeset.py
diff --git a/trac/versioncontrol/web_ui/changeset.py b/trac/versioncontrol/web_ui/changeset.py
a b 229 229 230 230 rm = RepositoryManager(self.env) 231 231 if reponame: 232 repos = rm.get_repository(reponame , req.authname)232 repos = rm.get_repository(reponame) 233 233 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) 236 235 237 236 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) 240 239 if old_repos != repos: 241 240 raise TracError(_("Can't compare across different " 242 241 "repositories: %(old)s vs. %(new)s", … … 254 253 try: 255 254 new_path = repos.normalize_path(new_path) 256 255 new = repos.normalize_rev(new) 257 258 repos.authz.assert_permission_for_changeset(new)259 260 256 old_path = repos.normalize_path(old_path or new_path) 261 257 old = repos.normalize_rev(old or new) 262 258 except NoSuchChangeset, e: … … 500 496 options = data['diff']['options'] 501 497 repos_resource = Resource('repository', reponame) 502 498 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() 515 502 old_ctx = Context.from_request(req, old_source) 516 503 new_ctx = Context.from_request(req, new_source) 517 504 changed_properties = [] … … 613 600 for old_node, new_node, kind, change in get_changes(): 614 601 props = [] 615 602 diffs = [] 603 old_source = old_node and Resource('source', old_node.created_path, 604 version=old_node.created_rev, 605 parent=repos_resource) 606 new_source = new_node and Resource('source', new_node.created_path, 607 version=new_node.created_rev, 608 parent=repos_resource) 609 show_old = old_node and 'FILE_VIEW' in req.perm(old_source) 610 show_new = new_node and 'FILE_VIEW' in req.perm(new_source) 616 611 show_entry = change != Changeset.EDIT 617 612 show_diff = show_diffs or (new_node and new_node.path == annotated) 618 613 619 if change in Changeset.DIFF_CHANGES and 'FILE_VIEW' in req.perm:614 if change in Changeset.DIFF_CHANGES and show_old and show_new: 620 615 assert old_node and new_node 621 props = _prop_changes(old_node, new_node) 616 props = _prop_changes(old_node, old_source, new_node, 617 new_source) 622 618 if props: 623 619 show_entry = True 624 620 if kind == Node.FILE and show_diff: … … 628 624 has_diffs = True 629 625 # elif None (means: manually compare to (previous)) 630 626 show_entry = True 631 if show_entry or not show_diff:627 if (show_old or show_new) and (show_entry or not show_diff): 632 628 info = {'change': change, 633 629 'old': old_node and node_info(old_node, annotated), 634 630 'new': new_node and node_info(new_node, annotated), … … 933 929 for reponame in rm.get_all_repositories(): 934 930 if all_repos or ('repo-' + reponame) in repo_filters: 935 931 try: 936 repos = rm.get_repository(reponame , req.authname)932 repos = rm.get_repository(reponame) 937 933 for event in generate_changesets(reponame, repos): 938 934 yield event 939 935 except TracError, e: … … 1055 1051 rev, path = chgset, '/' 1056 1052 reponame = rm.get_default_repository(formatter.context) 1057 1053 if reponame is not None: 1058 repos = rm.get_repository(reponame , authname)1054 repos = rm.get_repository(reponame) 1059 1055 else: 1060 reponame, repos, path = rm.get_repository_by_path(path , authname)1056 reponame, repos, path = rm.get_repository_by_path(path) 1061 1057 if path == '/': 1062 1058 path = None 1063 1059 … … 1157 1153 if req.get_header('X-Requested-With') == 'XMLHttpRequest': 1158 1154 dirname, prefix = posixpath.split(req.args.get('q')) 1159 1155 prefix = prefix.lower() 1160 reponame, repos, path = rm.get_repository_by_path(dirname, 1161 req.authname) 1156 reponame, repos, path = rm.get_repository_by_path(dirname) 1162 1157 # an entry is a (isdir, name, path) tuple 1163 1158 def kind_order(entry): 1164 1159 return (not entry[0], embedded_numbers(entry[1])) … … 1190 1185 1191 1186 # -- normalize 1192 1187 new_reponame, new_repos, new_path = \ 1193 rm.get_repository_by_path(new_path , req.authname)1188 rm.get_repository_by_path(new_path) 1194 1189 old_reponame, old_repos, old_path = \ 1195 rm.get_repository_by_path(old_path , req.authname)1190 rm.get_repository_by_path(old_path) 1196 1191 new_rev = new_repos.normalize_rev(new_rev) 1197 1192 old_rev = old_repos.normalize_rev(old_rev) 1198 1193 -
trac/versioncontrol/web_ui/log.py
diff --git a/trac/versioncontrol/web_ui/log.py b/trac/versioncontrol/web_ui/log.py
a b 83 83 limit = int(req.args.get('limit') or self.default_log_limit) 84 84 85 85 reponame, repos, path = RepositoryManager(self.env).\ 86 get_repository_by_path(path , req.authname)86 get_repository_by_path(path) 87 87 repos_resource = Resource('repository', reponame) 88 88 89 89 normpath = repos.normalize_path(path) … … 105 105 # unless explicit ranges have been specified 106 106 # * for ''show only add, delete'' we're using 107 107 # `Repository.get_path_history()` 108 cset_resource = Resource('changeset', parent=repos_resource) 108 109 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 112 114 elif revranges: 113 def history( limit):115 def history(): 114 116 prevpath = path 115 117 expected_next_item = None 116 118 ranges = list(revranges.pairs) … … 123 125 p, rev, chg = node_history[0] 124 126 if rev < a: 125 127 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] 132 135 prevpath = node_history[-1][0] # follow copy 133 b = rev -1136 b = rev - 1 134 137 if len(node_history) > 1: 135 138 expected_next_item = node_history[-1] 136 139 else: … … 138 141 if expected_next_item: 139 142 yield (expected_next_item[0], expected_next_item[1], None) 140 143 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 142 149 143 150 # -- retrieve history, asking for limit+1 results 144 151 info = [] 145 152 depth = 1 146 153 previous_path = normpath 147 154 count = 0 148 for old_path, old_rev, old_chg in history( limit+1):155 for old_path, old_rev, old_chg in history(): 149 156 if stop_rev and repos.rev_older_than(old_rev, stop_rev): 150 157 break 151 158 old_path = repos.normalize_path(old_path) … … 353 360 authname = formatter.perm.username 354 361 reponame = rm.get_default_repository(formatter.context) 355 362 if reponame is not None: 356 repos = rm.get_repository(reponame , authname)363 repos = rm.get_repository(reponame) 357 364 else: 358 reponame, repos, path = rm.get_repository_by_path(path , authname)365 reponame, repos, path = rm.get_repository_by_path(path) 359 366 360 367 revranges = None 361 368 if any(c for c in ':-,' if c in revs):
