Ticket #7116: 7116-authz-policy-r9054.patch
| File 7116-authz-policy-r9054.patch, 94.4 KB (added by rblank, 8 months 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 319 319 320 320 @param authname: user name for authorization 321 321 """ 322 return RepositoryManager(self).get_repository(reponame , authname)322 return RepositoryManager(self).get_repository(reponame) 323 323 324 324 def create(self, options=[]): 325 325 """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 83 83 84 84 def _do_changeset_added(self, reponame, *revs): 85 85 rm = RepositoryManager(self.env) 86 rm.notify('changeset_added', reponame, revs , None)86 rm.notify('changeset_added', reponame, revs) 87 87 88 88 def _do_changeset_modified(self, reponame, *revs): 89 89 rm = RepositoryManager(self.env) 90 rm.notify('changeset_modified', reponame, revs , None)90 rm.notify('changeset_modified', reponame, revs) 91 91 92 92 def _do_list(self): 93 93 rm = RepositoryManager(self.env) … … 106 106 if rev is not None: 107 107 raise TracError(_('Cannot synchronize a single revision ' 108 108 'on multiple repositories')) 109 repositories = rm.get_real_repositories( None)109 repositories = rm.get_real_repositories() 110 110 else: 111 111 if reponame == '(default)': 112 112 reponame = '' 113 repos = rm.get_repository(reponame , None)113 repos = rm.get_repository(reponame) 114 114 if repos is None: 115 115 raise TracError(_("Unknown repository '%(reponame)s'", 116 116 reponame=reponame or '(default)')) … … 284 284 info['editable'] = editable 285 285 if not info.get('alias'): 286 286 try: 287 repos = RepositoryManager(self.env).get_repository(reponame , None)287 repos = RepositoryManager(self.env).get_repository(reponame) 288 288 info['rev'] = repos.get_youngest_rev() 289 289 except Exception: 290 290 pass -
trac/versioncontrol/api.py
diff --git a/trac/versioncontrol/api.py b/trac/versioncontrol/api.py
a b 26 26 from trac.admin import AdminCommandError, IAdminCommandProvider 27 27 from trac.config import ListOption, Option 28 28 from trac.core import * 29 from trac.perm import PermissionError 30 from trac.resource import IResourceManager, ResourceNotFound 29 from trac.resource import IResourceManager, Resource, ResourceNotFound 31 30 from trac.util.text import printout, to_unicode 32 31 from trac.util.translation import _ 33 32 from trac.web.api import IRequestFilter … … 318 317 if is_default(reponame): 319 318 reponame = '' 320 319 try: 321 repo = self.get_repository(reponame , req.authname)320 repo = self.get_repository(reponame) 322 321 if repo: 323 322 repo.sync() 324 323 except TracError, e: … … 422 421 if prio >= 0) 423 422 return list(types) 424 423 425 def get_repositories_by_dir(self, directory , authname):424 def get_repositories_by_dir(self, directory): 426 425 """Retrieve the repositories based on the given directory. 427 426 428 427 :param directory: the key for identifying the repositories. … … 435 434 if dir: 436 435 dir = os.path.join(os.path.normcase(dir), '') 437 436 if dir.startswith(directory): 438 repos = self.get_repository(reponame , authname)437 repos = self.get_repository(reponame) 439 438 if repos: 440 439 repositories.append(repos) 441 440 return repositories … … 459 458 db.commit() 460 459 return id 461 460 462 def get_repository(self, reponame , authname):461 def get_repository(self, reponame): 463 462 """Retrieve the appropriate Repository for the given name. 464 463 465 464 :param reponame: the key for specifying the repository. 466 465 If no name is given, take the default 467 466 repository. 468 :param authname: deprecated (use fine grained permissions)469 467 :return: if no corresponding repository was defined, 470 468 simply return `None`. 471 469 """ … … 499 497 finally: 500 498 self._lock.release() 501 499 502 def get_repository_by_path(self, path , authname):500 def get_repository_by_path(self, path): 503 501 """Retrieve a matching Repository for the given path. 504 502 505 503 :param path: the eventually scoped repository-scoped path … … 519 517 path = path[length:] 520 518 else: 521 519 reponame = '' 522 return (reponame, self.get_repository(reponame , authname),520 return (reponame, self.get_repository(reponame), 523 521 path.rstrip('/') or '/') 524 522 525 523 def get_default_repository(self, context): … … 549 547 self._all_repositories[reponame] = info 550 548 return self._all_repositories 551 549 552 def get_real_repositories(self , authname):550 def get_real_repositories(self): 553 551 """Return a set of all real repositories (i.e. excluding aliases).""" 554 552 repositories = set() 555 553 for reponame in self.get_all_repositories(): 556 554 try: 557 repos = self.get_repository(reponame , authname)555 repos = self.get_repository(reponame) 558 556 if repos is not None: 559 557 repositories.add(repos) 560 558 except TracError: … … 572 570 self._lock.release() 573 571 self.config.touch() # Force environment reload 574 572 575 def notify(self, event, reponame, revs , authname):573 def notify(self, event, reponame, revs): 576 574 """Notify repositories and change listeners about repository events. 577 575 578 576 The supported events are the names of the methods defined in the … … 583 581 584 582 # Notify a repository by name, and all repositories with the same 585 583 # base, or all repositories by base or by repository dir 586 repos = self.get_repository(reponame , None)584 repos = self.get_repository(reponame) 587 585 repositories = [] 588 586 if repos: 589 587 base = repos.get_base() 590 588 else: 591 repositories = self.get_repositories_by_dir(reponame , None)589 repositories = self.get_repositories_by_dir(reponame) 592 590 if repositories: 593 591 base = None 594 592 else: 595 593 base = reponame 596 594 if base: 597 repositories = [r for r in self.get_real_repositories( authname)595 repositories = [r for r in self.get_real_repositories() 598 596 if r.get_base() == base] 599 597 if not repositories: 600 598 self.log.warn("Found no repositories matching '%s' base.", … … 676 674 class Repository(object): 677 675 """Base class for a repository provided by a version control system.""" 678 676 679 def __init__(self, name, params, authz,log):677 def __init__(self, name, params, log): 680 678 """Initialize a repository. 681 679 682 680 :param name: a unique name identifying the repository, usually a … … 686 684 the name of the repository under the key "name" and 687 685 the surrogate key that identifies the repository in 688 686 the database under the key "id". 689 :param authz: a repository authorizer (deprecated).690 687 :param log: a logger instance. 691 688 """ 692 689 self.name = name 693 690 self.params = params 694 691 self.reponame = params['name'] 695 692 self.id = params['id'] 696 self.authz = authz or Authorizer()697 693 self.log = log 694 self.resource = Resource('repository', self.reponame) 698 695 699 696 def close(self): 700 697 """Close the connection to the repository.""" … … 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): … … 883 879 """ 884 880 raise NotImplementedError 885 881 882 def can_view(self, perm): 883 return 'BROWSER_VIEW' in perm(self.resource.child('source', '/')) 884 886 885 887 886 class Node(object): 888 887 """Represents a directory or file in the repository at a given revision.""" … … 890 889 DIRECTORY = "dir" 891 890 FILE = "file" 892 891 892 resource = property(lambda self: Resource('source', self.created_path, 893 version=self.created_rev, 894 parent=self.repos.resource)) 895 893 896 # created_path and created_rev properties refer to the Node "creation" 894 897 # in the Subversion meaning of a Node in a versioned tree (see #3340). 895 898 # … … 898 901 created_rev = None 899 902 created_path = None 900 903 901 def __init__(self, path, rev, kind):904 def __init__(self, repos, path, rev, kind): 902 905 assert kind in (Node.DIRECTORY, Node.FILE), \ 903 906 "Unknown node kind %s" % kind 907 self.repos = repos 904 908 self.path = to_unicode(path) 905 909 self.rev = rev 906 910 self.kind = kind … … 990 994 isdir = property(lambda x: x.kind == Node.DIRECTORY) 991 995 isfile = property(lambda x: x.kind == Node.FILE) 992 996 997 def can_view(self, perm): 998 return (self.isdir and 'BROWSER_VIEW' or 'FILE_VIEW') \ 999 in perm(self.resource) 1000 993 1001 994 1002 class Changeset(object): 995 1003 """Represents a set of changes committed at once in a repository.""" … … 1005 1013 OTHER_CHANGES = (ADD, DELETE) 1006 1014 ALL_CHANGES = DIFF_CHANGES + OTHER_CHANGES 1007 1015 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 1009 1021 self.rev = rev 1010 1022 self.message = message or '' 1011 1023 self.author = author or '' … … 1038 1050 1039 1051 1040 1052 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 16 16 17 17 from datetime import datetime 18 18 import os 19 import posixpath20 19 21 20 from trac.cache import CacheProxy 22 21 from trac.core import TracError 23 22 from trac.util.datefmt import utc, to_timestamp 24 23 from trac.util.translation import _ 25 from trac.versioncontrol import Changeset, Node, Repository, Authorizer, \ 26 NoSuchChangeset 24 from trac.versioncontrol import Changeset, Node, Repository, NoSuchChangeset 27 25 28 26 29 27 _kindmap = {'D': Node.DIRECTORY, 'F': Node.FILE} … … 43 41 44 42 scope = property(lambda self: self.repos.scope) 45 43 46 def __init__(self, env, repos, authz,log):44 def __init__(self, env, repos, log): 47 45 self.env = env 48 46 self.repos = repos 49 47 self.metadata = CacheProxy(self.__class__.__module__ + '.' 50 48 + self.__class__.__name__ + '.metadata:' 51 49 + str(self.repos.id), self._metadata, 52 50 self.env) 53 Repository.__init__(self, repos.name, repos.params, authz,log)51 Repository.__init__(self, repos.name, repos.params, log) 54 52 55 53 def close(self): 56 54 self.repos.close() … … 65 63 return self.repos.get_path_url(path, rev) 66 64 67 65 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) 70 67 71 68 def get_changeset_uid(self, rev): 72 69 return self.repos.get_changeset_uid(rev) … … 81 78 to_timestamp(stop))) 82 79 for rev, in cursor: 83 80 try: 84 if self.authz.has_permission_for_changeset(rev): 85 yield self.get_changeset(rev) 81 yield self.get_changeset(rev) 86 82 except NoSuchChangeset: 87 83 pass # skip changesets currently being resync'ed 88 84 … … 220 216 # 1. prepare for resyncing 221 217 # (there still might be a race condition at this point) 222 218 223 authz = self.repos.authz224 self.repos.authz = Authorizer() # remove permission checking225 226 219 kindmap = dict(zip(_kindmap.values(), _kindmap.keys())) 227 220 actionmap = dict(zip(_actionmap.values(), _actionmap.keys())) 228 221 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 251 243 252 # 1.2. now *only* one process was able to get there253 # (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) 254 246 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)) 267 259 268 # 1.3. iterate (1.1 should always succeed now)269 youngest = next_youngest270 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) 271 263 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()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() 280 272 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) 287 276 288 277 def get_node(self, path, rev=None): 289 278 return self.repos.get_node(path, self.normalize_rev(rev)) … … 397 386 398 387 class CachedChangeset(Changeset): 399 388 400 def __init__(self, repos, rev, env, authz): 401 self.repos = repos 389 def __init__(self, repos, rev, env): 402 390 self.env = env 403 self.authz = authz404 391 db = self.env.get_db_cnx() 405 392 cursor = db.cursor() 406 393 cursor.execute("SELECT time,author,message FROM revision " 407 394 "WHERE repos=%s AND rev=%s", 408 ( self.repos.id, str(rev)))395 (repos.id, str(rev))) 409 396 row = cursor.fetchone() 410 397 if row: 411 398 _date, author, message = row 412 399 date = datetime.fromtimestamp(_date, utc) 413 Changeset.__init__(self, re v, message, author, date)400 Changeset.__init__(self, repos, rev, message, author, date) 414 401 else: 415 402 raise NoSuchChangeset(rev) 416 403 self.scope = getattr(repos, 'scope', '') … … 422 409 "FROM node_change WHERE repos=%s AND rev=%s " 423 410 "ORDER BY path", (self.repos.id, str(self.rev))) 424 411 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 412 kind = _kindmap[kind] 430 413 change = _actionmap[change] 431 414 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 any 27 from trac.util.text import exception_to_unicode, 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('#') 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 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. 72 122 """ 73 123 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 """) 77 130 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 """) 89 135 90 self.groups = self._groups() 136 _mtime = 0 137 _authz = {} 138 _users = set() 139 140 # IPermissionPolicy methods 91 141 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) 95 179 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 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(self, rev, self.scope, 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 … … 667 656 class SubversionNode(Node): 668 657 669 658 def __init__(self, path, rev, repos, pool=None, parent_root=None): 670 self.repos = repos671 659 self.fs_ptr = repos.fs_ptr 672 self.authz = repos.authz673 660 self.scope = repos.scope 674 661 self._scoped_path_utf8 = _to_svn(self.scope, path) 675 662 self.pool = Pool(pool) … … 700 687 self.created_rev, self.created_path = rev, path 701 688 self.rev = self.created_rev 702 689 # 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]) 704 691 705 692 def get_content(self): 706 693 if self.isdir: … … 719 706 entries = fs.dir_entries(self.root, self._scoped_path_utf8, pool()) 720 707 for item in entries.keys(): 721 708 path = posixpath.join(self.path, _from_svn(item)) 722 if not self.authz.has_permission(posixpath.join(self.scope,723 path.strip('/'))):724 continue725 709 yield SubversionNode(path, self._requested_rev, self.repos, 726 710 self.pool, self.root) 727 711 … … 846 830 847 831 class SubversionChangeset(Changeset): 848 832 849 def __init__(self, re v, authz, scope, fs_ptr, pool=None):833 def __init__(self, repos, rev, scope, pool=None): 850 834 self.rev = rev 851 self.authz = authz852 835 self.scope = scope 853 self.fs_ptr = fs_ptr836 self.fs_ptr = repos.fs_ptr 854 837 self.pool = Pool(pool) 855 838 try: 856 839 message = self._get_prop(core.SVN_PROP_REVISION_LOG) … … 866 849 date = datetime.fromtimestamp(ts, utc) 867 850 else: 868 851 date = None 869 Changeset.__init__(self, re v, message, author, date)852 Changeset.__init__(self, repos, rev, message, author, date) 870 853 871 854 def get_properties(self): 872 855 props = fs.revision_proplist(self.fs_ptr, self.rev, self.pool()) … … 896 879 path = _from_svn(path_utf8) 897 880 898 881 # 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): 901 883 continue 902 884 903 885 path_utf8 = change.path … … 907 889 base_rev = change.base_rev 908 890 909 891 # 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): 912 893 base_path, base_rev = None, -1 913 894 914 895 # Determine the action -
trac/versioncontrol/templates/browser.html
diff --git a/trac/versioncontrol/templates/browser.html b/trac/versioncontrol/templates/browser.html
a b 49 49 50 50 <py:if test="dir or file"> 51 51 <py:choose> 52 <h1 py:when="repo ">Default Repository</h1>52 <h1 py:when="repo and repo.repositories">Default Repository</h1> 53 53 <h1 py:otherwise=""><xi:include href="path_links.html" /></h1> 54 54 </py:choose> 55 55 -
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 chgset_context.perm"> 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 chgset_context.perm"> 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 context('source', file, version=change.rev, parent=repos_resource).perm %}\ 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() … … 64 64 t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc) 65 65 changes = [('trunk', Node.DIRECTORY, Changeset.ADD, None, None), 66 66 ('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))]71 67 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 72 None,self.log,68 self.log, 73 69 get_changeset=lambda x: changesets[int(x)], 74 70 get_oldest_rev=lambda: 0, 75 71 get_youngest_rev=lambda: 1, 76 72 normalize_rev=lambda x: x, 77 73 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) 79 79 cache.sync() 80 80 81 81 cursor = self.db.cursor() … … 111 111 "WHERE id=1 AND name='youngest_rev'") 112 112 113 113 changes = [('trunk/README', Node.FILE, Changeset.EDIT, 'trunk/README', 1)] 114 changeset = Mock(Changeset, 2, 'Update', 'joe', t3,115 get_changes=lambda: iter(changes))116 114 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 117 None,self.log,115 self.log, 118 116 get_changeset=lambda x: changeset, 119 117 get_youngest_rev=lambda: 2, 120 118 get_oldest_rev=lambda: 0, 121 119 normalize_rev=lambda x: x, 122 120 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) 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 AuthzSourcePolicy, 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 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] 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=[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 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(AuthzSourcePolicyTestCase, '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 28 28 from trac.perm import IPermissionRequestor 29 29 from trac.resource import Resource, ResourceNotFound 30 30 from trac.util import embedded_numbers 31 from trac.util.compat import a ll31 from trac.util.compat import any 32 32 from trac.util.datefmt import http_date, utc 33 33 from trac.util.html import escape, Markup 34 34 from trac.util.text import exception_to_unicode, shorten_line … … 290 290 return 'browser' 291 291 292 292 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())) 299 299 300 300 # IPermissionRequestor methods 301 301 … … 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 … … 341 341 if all_repositories and repos \ 342 342 and all_repositories[''].get('hidden') in _TRUE_VALUES: 343 343 repos = None 344 root_resource = Resource('repository', '').child('source', '/') 345 if 'BROWSER_VIEW' not in req.perm(root_resource): 346 repos = None 344 347 345 348 if not repos and reponame: 346 349 raise ResourceNotFound(_("No repository '%(repo)s' found", … … 375 378 if node: 376 379 if node.isdir: 377 380 dir_data = self._render_dir( 378 req, reponame, repos, node, rev, order, desc)381 req, context, reponame, repos, node, rev, order, desc) 379 382 elif node.isfile: 380 383 file_data = self._render_file( 381 384 req, context, reponame, repos, node, rev) … … 467 470 # Internal methods 468 471 469 472 def _render_repository_index(self, context, all_repositories, order, desc): 470 context.perm.require('BROWSER_VIEW')471 472 473 # Color scale for the age column 473 474 timerange = custom_colorizer = None 474 475 if self.color_scale: … … 477 478 rm = RepositoryManager(self.env) 478 479 repositories = [] 479 480 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 480 484 if not reponame or repoinfo.get('hidden') in _TRUE_VALUES: 481 485 continue 482 486 try: 483 repos = rm.get_repository(reponame , context.perm.username)487 repos = rm.get_repository(reponame) 484 488 if repos: 485 489 youngest = repos.get_changeset(repos.youngest_rev) 486 490 if self.color_scale and youngest: … … 508 512 return {'repositories' : repositories, 509 513 'timerange': timerange, 'colorize_age': custom_colorizer} 510 514 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') 513 518 514 519 # Entries metadata 515 520 class entry(object): … … 518 523 for f in entry.__slots__: 519 524 setattr(self, f, getattr(node, f)) 520 525 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)] 522 529 changes = get_changes(repos, [i.rev for i in entries]) 523 530 524 531 if rev: … … 577 584 } 578 585 579 586 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') 581 588 582 589 mimeview = Mimeview(self.env) 583 590 … … 777 784 order = kwargs.get('order') 778 785 desc = kwargs.get('desc', 0) 779 786 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 783 791 if format == 'table': 784 792 data = self._render_repository_index( 785 formatter.context, all_repos itories, order, desc)793 formatter.context, all_repos, order, desc) 786 794 787 795 add_stylesheet(formatter.req, 'common/css/browser.css') 788 796 from trac.web.chrome import Chrome … … 791 799 {'repo': data}, None, fragment=True) 792 800 793 801 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), 796 805 href=formatter.href.browser(reponame or None))) 797 806 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 798 812 if format == 'list': 799 813 return tag.dl([ 800 814 tag(tag.dt(repolink(reponame)), 801 815 tag.dd(repoinfo.get('description'))) 802 for reponame, repoinfo in all_repos itories])816 for reponame, repoinfo in all_repos]) 803 817 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]) 807 820 808 821 809 822 -
trac/versioncontrol/web_ui/changeset.py
diff --git a/trac/versioncontrol/web_ui/changeset.py b/trac/versioncontrol/web_ui/changeset.py
a b 26 26 from StringIO import StringIO 27 27 28 28 from genshi.builder import tag 29 from genshi.core import Markup30 29 31 30 from trac.config import Option, BoolOption, IntOption, _TRUE_VALUES 32 31 from trac.core import * … … 46 45 from trac.versioncontrol.diff import get_diff_options, diff_blocks, \ 47 46 unified_diff 48 47 from trac.versioncontrol.web_ui.browser import BrowserModule 48 from trac.versioncontrol.web_ui.util import can_view_node 49 49 from trac.web import IRequestHandler, RequestDone 50 50 from trac.web.chrome import add_ctxtnav, add_link, add_script, add_stylesheet, \ 51 51 prevnext_nav, INavigationContributor, Chrome … … 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: … … 309 305 data['diff'] = diff_data 310 306 data['wiki_format_messages'] = self.wiki_format_messages 311 307 308 repos_resource = Resource('repository', reponame) 312 309 if chgset: 313 resource = Resource('repository', reponame).child('changeset', new)310 resource = repos_resource.child('changeset', new) 314 311 req.perm(resource).require('CHANGESET_VIEW') 315 312 chgset = repos.get_changeset(new) 316 313 … … 324 321 format = req.args.get('format') 325 322 326 323 if format in ['diff', 'zip']: 327 req.perm.require('FILE_VIEW')328 324 # choosing an appropriate filename 329 325 rpath = new_path.replace('/','_') 330 326 if chgset: … … 342 338 filename = 'diff-from-%s-r%s-to-%s-r%s' \ 343 339 % (old_path.replace('/','_'), old, rpath, new) 344 340 if format == 'diff': 345 self._render_diff(req, filename, repos , data)341 self._render_diff(req, filename, repos_resource, repos, data) 346 342 elif format == 'zip': 347 self._render_zip(req, filename, repos , data)343 self._render_zip(req, filename, repos_resource, repos, data) 348 344 349 345 # -- HTML format 350 346 self._render_html(req, reponame, repos, chgset, restricted, xhr, data) … … 478 474 479 475 data['title'] = title 480 476 481 if 'BROWSER_VIEW' not in req.perm: 477 if 'BROWSER_VIEW' not in req.perm: # FIXME: What should we do here? 482 478 return 483 479 484 480 def node_info(node, annotated): … … 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 = [] … … 584 571 else: 585 572 return [] 586 573 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) 601 587 602 588 # XHR is used for blame support: display the changeset view without 603 589 # the navigation and with the changes concerning the annotated file … … 613 599 for old_node, new_node, kind, change in get_changes(): 614 600 props = [] 615 601 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) 616 610 show_entry = change != Changeset.EDIT 617 611 show_diff = show_diffs or (new_node and new_node.path == annotated) 618 612 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: 620 614 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) 622 617 if props: 623 618 show_entry = True 624 619 if kind == Node.FILE and show_diff: … … 628 623 has_diffs = True 629 624 # elif None (means: manually compare to (previous)) 630 625 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): 632 627 info = {'change': change, 633 628 'old': old_node and node_info(old_node, annotated), 634 629 'new': new_node and node_info(new_node, annotated), … … 682 677 683 678 return data 684 679 685 def _render_diff(self, req, filename, repos , data):680 def _render_diff(self, req, filename, repos_resource, repos, data): 686 681 """Raw Unified Diff version""" 687 682 req.send_response(200) 688 683 req.send_header('Content-Type', 'text/x-patch;charset=utf-8') … … 696 691 old_path=data['old_path'], old_rev=data['old_rev']): 697 692 # TODO: Property changes 698 693 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): 701 698 continue 702 699 703 700 new_content = old_content = '' … … 756 753 req.write(diff_str) 757 754 raise RequestDone 758 755 759 def _render_zip(self, req, filename, repos , data):756 def _render_zip(self, req, filename, repos_resource, repos, data): 760 757 """ZIP archive with all the added and/or modified files.""" 761 758 req.send_response(200) 762 759 req.send_header('Content-Type', 'application/zip') … … 770 767 for old_node, new_node, kind, change in repos.get_changes( 771 768 new_path=data['new_path'], new_rev=data['new_rev'], 772 769 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): 774 772 assert new_node 775 773 zipinfo = ZipInfo() 776 774 zipinfo.filename = new_node.path.strip('/').encode('utf-8') … … 860 858 rm = RepositoryManager(self.env) 861 859 repositories = rm.get_all_repositories() 862 860 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', '/'))) 865 866 default_is_aliased = any(info.get('alias') == '' and 866 867 name in visible_repos 867 868 for name, info in repositories.items()) … … 933 934 for reponame in rm.get_all_repositories(): 934 935 if all_repos or ('repo-' + reponame) in repo_filters: 935 936 try: 936 repos = rm.get_repository(reponame , req.authname)937 repos = rm.get_repository(reponame) 937 938 for event in generate_changesets(reponame, repos): 938 939 yield event 939 940 except TracError, e: … … 987 988 elif show_files: 988 989 for c, r, repos_for_c in changesets: 989 990 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 990 995 if show_files > 0 and len(files) > show_files: 991 996 break 992 997 files.append(tag.li(tag.div(class_=chg[2]), … … 1046 1051 1047 1052 # identifying repository 1048 1053 rm = RepositoryManager(self.env) 1049 authname = formatter.perm.username1050 1054 chgset, params, fragment = formatter.split_link(chgset) 1051 1055 sep = chgset.find('/') 1052 1056 if sep > 0: … … 1055 1059 rev, path = chgset, '/' 1056 1060 reponame = rm.get_default_repository(formatter.context) 1057 1061 if reponame is not None: 1058 repos = rm.get_repository(reponame , authname)1062 repos = rm.get_repository(reponame) 1059 1063 else: 1060 reponame, repos, path = rm.get_repository_by_path(path , authname)1064 reponame, repos, path = rm.get_repository_by_path(path) 1061 1065 if path == '/': 1062 1066 path = None 1063 1067 … … 1128 1132 repos = self.env.get_repository(reponame) 1129 1133 if not repos: 1130 1134 continue # revisions for a no longer active repository 1131 if not repos.authz.has_permission_for_changeset(rev):1132 continue1133 # FIXME get rid of .authz and use only the normal Permission system1134 #cset = Resource('repository', reponame).child('changeset' , rev)1135 #cset = repos.resource.child('changeset' , rev)1136 #cset = repos.changeset_resource(rev)1137 1135 cset = Resource('repository', reponame).child('changeset', rev) 1138 1136 if 'CHANGESET_VIEW' in req.perm(cset): 1139 1137 yield (req.href.changeset(rev, reponame or None), … … 1157 1155 if req.get_header('X-Requested-With') == 'XMLHttpRequest': 1158 1156 dirname, prefix = posixpath.split(req.args.get('q')) 1159 1157 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) 1162 1159 # an entry is a (isdir, name, path) tuple 1163 1160 def kind_order(entry): 1164 1161 return (not entry[0], embedded_numbers(entry[1])) 1165 1162 1166 1163 if repos: 1167 node = repos.get_node(path)1164 repos_resource = Resource('repository', reponame) 1168 1165 entries = [(e.isdir, e.name, 1169 1166 '/' + 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', '/'))) 1174 1175 1175 1176 elem = tag.ul( 1176 1177 [tag.li(isdir and tag.b(path) or path) … … 1190 1191 1191 1192 # -- normalize 1192 1193 new_reponame, new_repos, new_path = \ 1193 rm.get_repository_by_path(new_path , req.authname)1194 rm.get_repository_by_path(new_path) 1194 1195 old_reponame, old_repos, old_path = \ 1195 rm.get_repository_by_path(old_path , req.authname)1196 rm.get_repository_by_path(old_path) 1196 1197 new_rev = new_repos.normalize_rev(new_rev) 1197 1198 old_rev = old_repos.normalize_rev(old_rev) 1198 1199 1199 # FIXME: replace by fine grained permission checks1200 new_repos.authz.assert_permission_for_changeset(new_rev)1201 old_repos.authz.assert_permission_for_changeset(old_rev)1202 1203 1200 # -- prepare rendering 1204 1201 data = {'new_path': posixpath.join(new_reponame, new_path), 1205 1202 '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 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) … … 350 357 path, revs = match[:idx], match[idx+1:] 351 358 352 359 rm = RepositoryManager(self.env) 353 authname = formatter.perm.username354 360 reponame = rm.get_default_repository(formatter.context) 355 361 if reponame is not None: 356 repos = rm.get_repository(reponame , authname)362 repos = rm.get_repository(reponame) 357 363 else: 358 reponame, repos, path = rm.get_repository_by_path(path , authname)364 reponame, repos, path = rm.get_repository_by_path(path) 359 365 360 366 revranges = None 361 367 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 18 18 19 19 from genshi.builder import tag 20 20 21 from trac.resource import Resource NotFound21 from trac.resource import Resource, ResourceNotFound 22 22 from trac.util.datefmt import pretty_timedelta 23 23 from trac.util.text import shorten_line 24 24 from trac.util.translation import tag_, _ 25 25 from trac.versioncontrol.api import NoSuchNode, NoSuchChangeset 26 26 27 __all__ = ['get_changes', 'get_path_links', 'get_existing_node'] 27 __all__ = ['can_view_node', 'get_changes', 'get_path_links', 28 'get_existing_node'] 28 29 29 30 def get_changes(repos, revs): 30 31 changes = {} … … 68 69 tag.p(tag_("You can %(search)s in the repository history to see " 69 70 "if that path existed but was later removed", 70 71 search=search_a)))) 72 73 def 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))
