Ticket #7116: 7716-authz-policy-2-r9054.patch
| File 7716-authz-policy-2-r9054.patch, 96.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 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 True if view permission is granted on the repository.""" 884 return 'BROWSER_VIEW' in perm(self.resource.child('source', '/')) 885 886 886 887 887 class Node(object): 888 888 """Represents a directory or file in the repository at a given revision.""" … … 890 890 DIRECTORY = "dir" 891 891 FILE = "file" 892 892 893 resource = property(lambda self: Resource('source', self.created_path, 894 version=self.created_rev, 895 parent=self.repos.resource)) 896 893 897 # created_path and created_rev properties refer to the Node "creation" 894 898 # in the Subversion meaning of a Node in a versioned tree (see #3340). 895 899 # … … 898 902 created_rev = None 899 903 created_path = None 900 904 901 def __init__(self, path, rev, kind):905 def __init__(self, repos, path, rev, kind): 902 906 assert kind in (Node.DIRECTORY, Node.FILE), \ 903 907 "Unknown node kind %s" % kind 908 self.repos = repos 904 909 self.path = to_unicode(path) 905 910 self.rev = rev 906 911 self.kind = kind … … 990 995 isdir = property(lambda x: x.kind == Node.DIRECTORY) 991 996 isfile = property(lambda x: x.kind == Node.FILE) 992 997 998 def can_view(self, perm): 999 """Return True if view permission is granted on the node.""" 1000 return (self.isdir and 'BROWSER_VIEW' or 'FILE_VIEW') \ 1001 in perm(self.resource) 1002 993 1003 994 1004 class Changeset(object): 995 1005 """Represents a set of changes committed at once in a repository.""" … … 1005 1015 OTHER_CHANGES = (ADD, DELETE) 1006 1016 ALL_CHANGES = DIFF_CHANGES + OTHER_CHANGES 1007 1017 1008 def __init__(self, rev, message, author, date): 1018 resource = property(lambda self: Resource('changeset', self.rev, 1019 parent=self.repos.resource)) 1020 1021 def __init__(self, repos, rev, message, author, date): 1022 self.repos = repos 1009 1023 self.rev = rev 1010 1024 self.message = message or '' 1011 1025 self.author = author or '' … … 1036 1050 """ 1037 1051 raise NotImplementedError 1038 1052 1053 def can_view(self, perm): 1054 """Return True if view permission is granted on the changeset.""" 1055 return 'CHANGESET_VIEW' in perm(self.resource) 1039 1056 1040 1057 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 1058 # Note: Since Trac 0.12, Exception PermissionDenied class is gone, 1059 # and class Authorizer is gone as well. 1060 # 1061 # 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/changeset.html
diff --git a/trac/versioncontrol/templates/changeset.html b/trac/versioncontrol/templates/changeset.html
a b 122 122 123 123 </py:when> 124 124 <py:when test="wiki_format_messages"> 125 ${wiki_to_html(context('changeset', changeset.rev, parent=repos_resource), 126 changeset.message, escape_newlines=True)} 125 ${wiki_to_html(context, changeset.message, escape_newlines=True)} 127 126 </py:when> 128 127 <py:otherwise><pre>${changeset.message}</pre></py:otherwise> 129 128 </dd> -
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 change.can_view(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 change.can_view(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 110 110 <py:for each="idx, item in enumerate(items)"> 111 111 <py:with vars="change = changes[item.rev]; 112 112 is_separator = item.change is None; 113 chgset_context = context('changeset', change.rev, parent=repos_resource); 114 chgset_view = 'CHANGESET_VIEW' in perm(chgset_context.resource); 113 chgset_context = context('changeset', change.rev, parent=context.resource.parent); 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.rss
diff --git a/trac/versioncontrol/templates/revisionlog.rss b/trac/versioncontrol/templates/revisionlog.rss
a b 17 17 18 18 <item py:for="item in items" 19 19 py:with="change = changes[item.rev]; 20 item_context = context('changeset', change.rev, parent= repos_resource)">20 item_context = context('changeset', change.rev, parent=context.resource.parent)"> 21 21 ${author_or_creator(change.author, email_map)} 22 22 <pubDate>${http_date(change.date)}</pubDate> 23 23 <title>Revision $item.rev: ${shorten_line(change.message)}</title> -
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(repos.resource.child('source', file, version=change.rev)) %}\ 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 rm = RepositoryManager(self.env) 294 all_repos = rm.get_all_repositories() 295 if any(info.get('hidden') not in _TRUE_VALUES 296 and rm.get_repository(reponame).can_view(req.perm) 297 for reponame, info in all_repos.iteritems()): 298 yield ('mainnav', 'browser', 299 tag.a(_('Browse Source'), href=req.href.browser())) 299 300 300 301 # IPermissionRequestor methods 301 302 … … 332 333 xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest' 333 334 334 335 rm = RepositoryManager(self.env) 335 reponame, repos, path = rm.get_repository_by_path(path , req.authname)336 reponame, repos, path = rm.get_repository_by_path(path) 336 337 337 338 # Repository index 338 339 all_repositories = None 339 340 if not reponame and path == '/': 340 341 all_repositories = rm.get_all_repositories() 341 if all_repositories and repos \342 and all_repositories[''].get('hidden') in _TRUE_VALUES:342 if repos and (all_repositories[''].get('hidden') in _TRUE_VALUES 343 or not repos.can_view(req.perm)): 343 344 repos = None 344 345 345 346 if not repos and reponame: … … 467 468 # Internal methods 468 469 469 470 def _render_repository_index(self, context, all_repositories, order, desc): 470 context.perm.require('BROWSER_VIEW')471 472 471 # Color scale for the age column 473 472 timerange = custom_colorizer = None 474 473 if self.color_scale: … … 480 479 if not reponame or repoinfo.get('hidden') in _TRUE_VALUES: 481 480 continue 482 481 try: 483 repos = rm.get_repository(reponame , context.perm.username)482 repos = rm.get_repository(reponame) 484 483 if repos: 484 if not repos.can_view(context.perm): 485 continue 485 486 youngest = repos.get_changeset(repos.youngest_rev) 486 487 if self.color_scale and youngest: 487 488 if not timerange: … … 509 510 'timerange': timerange, 'colorize_age': custom_colorizer} 510 511 511 512 def _render_dir(self, req, reponame, repos, node, rev, order, desc): 512 req.perm .require('BROWSER_VIEW')513 req.perm(node.resource).require('BROWSER_VIEW') 513 514 514 515 # Entries metadata 515 516 class entry(object): … … 518 519 for f in entry.__slots__: 519 520 setattr(self, f, getattr(node, f)) 520 521 521 entries = [entry(n) for n in node.get_entries()] 522 entries = [entry(n) for n in node.get_entries() 523 if n.can_view(req.perm)] 522 524 changes = get_changes(repos, [i.rev for i in entries]) 523 525 524 526 if rev: … … 577 579 } 578 580 579 581 def _render_file(self, req, context, reponame, repos, node, rev=None): 580 req.perm( context.resource).require('FILE_VIEW')582 req.perm(node.resource).require('FILE_VIEW') 581 583 582 584 mimeview = Mimeview(self.env) 583 585 … … 777 779 order = kwargs.get('order') 778 780 desc = kwargs.get('desc', 0) 779 781 780 all_repositories = [rdata for rdata in RepositoryManager(self.env). 781 get_all_repositories().items() 782 if fnmatchcase(rdata[0], glob)] 782 rm = RepositoryManager(self.env) 783 all_repos = dict(rdata for rdata in rm.get_all_repositories().items() 784 if fnmatchcase(rdata[0], glob)) 785 783 786 if format == 'table': 784 787 data = self._render_repository_index( 785 formatter.context, all_repos itories, order, desc)788 formatter.context, all_repos, order, desc) 786 789 787 790 add_stylesheet(formatter.req, 'common/css/browser.css') 788 791 from trac.web.chrome import Chrome … … 791 794 {'repo': data}, None, fragment=True) 792 795 793 796 def repolink(reponame): 794 return Markup(tag.a(reponame, 795 title=_('View repository %(repo)s', repo=reponame), 797 label = reponame or _('(default)') 798 return Markup(tag.a(label, 799 title=_('View repository %(repo)s', repo=label), 796 800 href=formatter.href.browser(reponame or None))) 797 801 802 all_repos = sorted( 803 (reponame, info) for reponame, info in all_repos.iteritems() 804 if info.get('hidden') not in _TRUE_VALUE 805 and rm.get_repository(reponame).can_view(formatter.perm)) 806 798 807 if format == 'list': 799 808 return tag.dl([ 800 809 tag(tag.dt(repolink(reponame)), 801 810 tag.dd(repoinfo.get('description'))) 802 for reponame, repoinfo in all_repos itories])811 for reponame, repoinfo in all_repos]) 803 812 else: # compact 804 return Markup(', ').join([ 805 repolink(reponame) for reponame, repoinfo in all_repositories 806 if reponame]) 813 return Markup(', ').join([repolink(reponame) 814 for reponame, repoinfo in all_repos]) 807 815 808 816 809 817 -
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 * … … 229 228 230 229 rm = RepositoryManager(self.env) 231 230 if reponame: 232 repos = rm.get_repository(reponame , req.authname)231 repos = rm.get_repository(reponame) 233 232 else: 234 reponame, repos, new_path = rm.get_repository_by_path( 235 new_path, req.authname) 233 reponame, repos, new_path = rm.get_repository_by_path(new_path) 236 234 237 235 if old_path: 238 old_reponame, old_repos, old_path = rm.get_repository_by_path(239 old_path, req.authname)236 old_reponame, old_repos, old_path = \ 237 rm.get_repository_by_path(old_path) 240 238 if old_repos != repos: 241 239 raise TracError(_("Can't compare across different " 242 240 "repositories: %(old)s vs. %(new)s", … … 254 252 try: 255 253 new_path = repos.normalize_path(new_path) 256 254 new = repos.normalize_rev(new) 257 258 repos.authz.assert_permission_for_changeset(new)259 260 255 old_path = repos.normalize_path(old_path or new_path) 261 256 old = repos.normalize_rev(old or new) 262 257 except NoSuchChangeset, e: … … 310 305 data['wiki_format_messages'] = self.wiki_format_messages 311 306 312 307 if chgset: 313 resource = Resource('repository', reponame).child('changeset', new)314 req.perm(resource).require('CHANGESET_VIEW')315 308 chgset = repos.get_changeset(new) 309 req.perm(chgset.resource).require('CHANGESET_VIEW') 316 310 317 311 # TODO: find a cheaper way to reimplement r2636 318 312 req.check_modified(chgset.date, [ … … 324 318 format = req.args.get('format') 325 319 326 320 if format in ['diff', 'zip']: 327 req.perm.require('FILE_VIEW')328 321 # choosing an appropriate filename 329 322 rpath = new_path.replace('/','_') 330 323 if chgset: … … 413 406 414 407 # Support for revision properties (#2545) 415 408 repos_resource = Resource('repository', reponame) 416 data['repos_resource'] = repos_resource417 409 context = Context.from_request(req, 'changeset', chgset.rev, 418 410 parent=repos_resource) 411 data['context'] = context 419 412 revprops = chgset.get_properties() 420 413 data['properties'] = browser.render_properties('revprop', context, 421 414 revprops) … … 498 491 # with _that_ node specific history... 499 492 500 493 options = data['diff']['options'] 501 repos_resource = Resource('repository', reponame)502 494 503 495 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() 515 old_ctx = Context.from_request(req, old_source) 516 new_ctx = Context.from_request(req, new_source) 496 old_props = old_node.get_properties() 497 new_props = new_node.get_properties() 498 old_ctx = Context.from_request(req, old_node.resource) 499 new_ctx = Context.from_request(req, new_node.resource) 517 500 changed_properties = [] 518 501 if old_props != new_props: 519 502 for k, v in sorted(old_props.items()): … … 584 567 else: 585 568 return [] 586 569 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 570 diff_bytes = diff_files = 0 571 if self.max_diff_bytes or self.max_diff_files: 572 for old_node, new_node, kind, change in get_changes(): 573 if change in Changeset.DIFF_CHANGES and kind == Node.FILE \ 574 and old_node.can_view(req.perm) \ 575 and new_node.can_view(req.perm): 576 diff_files += 1 577 diff_bytes += _estimate_changes(old_node, new_node) 578 show_diffs = (not self.max_diff_files or \ 579 0 < diff_files <= self.max_diff_files) and \ 580 (not self.max_diff_bytes or \ 581 diff_bytes <= self.max_diff_bytes or \ 582 diff_files == 1) 601 583 602 584 # XHR is used for blame support: display the changeset view without 603 585 # the navigation and with the changes concerning the annotated file … … 613 595 for old_node, new_node, kind, change in get_changes(): 614 596 props = [] 615 597 diffs = [] 598 show_old = old_node and old_node.can_view(req.perm) 599 show_new = new_node and new_node.can_view(req.perm) 616 600 show_entry = change != Changeset.EDIT 617 601 show_diff = show_diffs or (new_node and new_node.path == annotated) 618 602 619 if change in Changeset.DIFF_CHANGES and 'FILE_VIEW' in req.perm:603 if change in Changeset.DIFF_CHANGES and show_old and show_new: 620 604 assert old_node and new_node 621 605 props = _prop_changes(old_node, new_node) 622 606 if props: … … 628 612 has_diffs = True 629 613 # elif None (means: manually compare to (previous)) 630 614 show_entry = True 631 if show_entry or not show_diff:615 if (show_old or show_new) and (show_entry or not show_diff): 632 616 info = {'change': change, 633 617 'old': old_node and node_info(old_node, annotated), 634 618 'new': new_node and node_info(new_node, annotated), … … 692 676 mimeview = Mimeview(self.env) 693 677 694 678 for old_node, new_node, kind, change in repos.get_changes( 695 new_path=data['new_path'], new_rev=data['new_rev'],696 old_path=data['old_path'], old_rev=data['old_rev']):679 new_path=data['new_path'], new_rev=data['new_rev'], 680 old_path=data['old_path'], old_rev=data['old_rev']): 697 681 # TODO: Property changes 698 682 699 683 # Content changes … … 702 686 703 687 new_content = old_content = '' 704 688 new_node_info = old_node_info = ('','') 705 mimeview = Mimeview(self.env)706 689 707 690 if old_node: 691 if not old_node.can_view(req.perm): 692 continue 708 693 if mimeview.is_binary(old_node.content_type, old_node.path): 709 694 continue 710 695 old_content = old_node.get_content().read() … … 714 699 old_content = mimeview.to_unicode(old_content, 715 700 old_node.content_type) 716 701 if new_node: 702 if not new_node.can_view(req.perm): 703 continue 717 704 if mimeview.is_binary(new_node.content_type, new_node.path): 718 705 continue 719 706 new_content = new_node.get_content().read() … … 770 757 for old_node, new_node, kind, change in repos.get_changes( 771 758 new_path=data['new_path'], new_rev=data['new_rev'], 772 759 old_path=data['old_path'], old_rev=data['old_rev']): 773 if kind == Node.FILE and change != Changeset.DELETE :774 assert new_node760 if kind == Node.FILE and change != Changeset.DELETE \ 761 and new_node.can_view(req.perm): 775 762 zipinfo = ZipInfo() 776 763 zipinfo.filename = new_node.path.strip('/').encode('utf-8') 777 764 # Note: unicode filenames are not supported by zipfile. … … 860 847 rm = RepositoryManager(self.env) 861 848 repositories = rm.get_all_repositories() 862 849 if len(repositories) > 1: 863 visible_repos = set(name for name, info in repositories.items() 864 if info.get('hidden') not in _TRUE_VALUES) 850 visible_repos = set( 851 name for name, info in repositories.items() 852 if info.get('hidden') not in _TRUE_VALUES 853 and rm.get_repository(name).can_view(req.perm)) 865 854 default_is_aliased = any(info.get('alias') == '' and 866 855 name in visible_repos 867 856 for name, info in repositories.items()) … … 912 901 for cset in changesets: 913 902 cset_resource = Resource('changeset', cset.rev, 914 903 parent=repos_resource) 915 if 'CHANGESET_VIEW' in req.perm(cset_resource):904 if cset.can_view(req.perm): 916 905 repos_for_uid = [reponame] 917 906 uid = repos.get_changeset_uid(cset.rev) 918 907 if uid: … … 933 922 for reponame in rm.get_all_repositories(): 934 923 if all_repos or ('repo-' + reponame) in repo_filters: 935 924 try: 936 repos = rm.get_repository(reponame , req.authname)925 repos = rm.get_repository(reponame) 937 926 for event in generate_changesets(reponame, repos): 938 927 yield event 939 928 except TracError, e: … … 971 960 filestats = self._prepare_filestats() 972 961 for c, r, repos_for_c in changesets: 973 962 for chg in c.get_changes(): 963 resource = c.resource.parent.child('source', 964 chg[0] or '/', r.id) 965 if not 'FILE_VIEW' in context.perm(resource): 966 continue 974 967 filestats[chg[2]] += 1 975 968 files.append(chg[0]) 976 969 stats = [(tag.div(class_=kind), … … 987 980 elif show_files: 988 981 for c, r, repos_for_c in changesets: 989 982 for chg in c.get_changes(): 983 resource = c.resource.parent.child('source', 984 chg[0] or '/', r.id) 985 if not 'FILE_VIEW' in context.perm(resource): 986 continue 990 987 if show_files > 0 and len(files) > show_files: 991 988 break 992 989 files.append(tag.li(tag.div(class_=chg[2]), … … 1046 1043 1047 1044 # identifying repository 1048 1045 rm = RepositoryManager(self.env) 1049 authname = formatter.perm.username1050 1046 chgset, params, fragment = formatter.split_link(chgset) 1051 1047 sep = chgset.find('/') 1052 1048 if sep > 0: … … 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 1064 1060 # rendering changeset link 1065 resource = Resource('repository', reponame).child('changeset', rev) 1066 if repos and 'CHANGESET_VIEW' in formatter.perm(resource): 1061 if repos: 1067 1062 try: 1068 1063 changeset = repos.get_changeset(rev) 1069 href = formatter.href.changeset(rev, reponame or None, path) 1070 return tag.a(label, class_="changeset", 1071 title=shorten_line(changeset.message), 1072 href=href + params + fragment) 1064 if changeset.can_view(formatter.perm): 1065 href = formatter.href.changeset(rev, reponame or None, 1066 path) 1067 return tag.a(label, class_="changeset", 1068 title=shorten_line(changeset.message), 1069 href=href + params + fragment) 1070 errmsg = _("No permission to view changset %(rev)s " 1071 "on %(repos)s", rev=rev, 1072 repos=reponame or _('(default)')) 1073 1073 except TracError, e: 1074 1074 errmsg = to_unicode(e) 1075 1075 elif reponame: … … 1128 1128 repos = self.env.get_repository(reponame) 1129 1129 if not repos: 1130 1130 continue # revisions for a no longer active repository 1131 if not repos.authz.has_permission_for_changeset(rev): 1132 continue 1133 # FIXME get rid of .authz and use only the normal Permission system 1134 #cset = Resource('repository', reponame).child('changeset' , rev) 1135 #cset = repos.resource.child('changeset' , rev) 1136 #cset = repos.changeset_resource(rev) 1137 cset = Resource('repository', reponame).child('changeset', rev) 1131 cset = Resource('repository', repos.reponame).child('changeset', 1132 rev) 1138 1133 if 'CHANGESET_VIEW' in req.perm(cset): 1139 1134 yield (req.href.changeset(rev, reponame or None), 1140 1135 '[%s]: %s' % (rev, shorten_line(log)), … … 1157 1152 if req.get_header('X-Requested-With') == 'XMLHttpRequest': 1158 1153 dirname, prefix = posixpath.split(req.args.get('q')) 1159 1154 prefix = prefix.lower() 1160 reponame, repos, path = rm.get_repository_by_path(dirname, 1161 req.authname) 1155 reponame, repos, path = rm.get_repository_by_path(dirname) 1162 1156 # an entry is a (isdir, name, path) tuple 1163 1157 def kind_order(entry): 1164 1158 return (not entry[0], embedded_numbers(entry[1])) 1165 1159 1166 1160 if repos: 1167 node = repos.get_node(path)1168 1161 entries = [(e.isdir, e.name, 1169 1162 '/' + 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()] 1163 for e in repos.get_node(path).get_entries() 1164 if e.can_view(req.perm)] 1165 if not reponame: 1166 entries.extend((True, name, '/' + name) 1167 for name in rm.get_all_repositories() 1168 if rm.get_repository(name).can_view(req.perm)) 1174 1169 1175 1170 elem = tag.ul( 1176 1171 [tag.li(isdir and tag.b(path) or path) … … 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 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 1194 # -- prepare rendering 1204 1195 data = {'new_path': posixpath.join(new_reponame, new_path), 1205 1196 '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 71 71 return True 72 72 73 73 def process_request(self, req): 74 req.perm. assert_permission('LOG_VIEW')74 req.perm.require('LOG_VIEW') 75 75 76 76 mode = req.args.get('mode', 'stop_on_copy') 77 77 path = req.args.get('path', '/') … … 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) 87 repos_resource = Resource('repository', reponame) 86 get_repository_by_path(path) 88 87 89 88 normpath = repos.normalize_path(path) 90 89 # if `revs` parameter is given, then we're restricted to the … … 105 104 # unless explicit ranges have been specified 106 105 # * for ''show only add, delete'' we're using 107 106 # `Repository.get_path_history()` 107 cset_resource = repos.resource.child('changeset') 108 108 if mode == 'path_history': 109 def history(limit): 110 for h in repos.get_path_history(path, rev, limit): 111 yield h 109 def history(): 110 for h in repos.get_path_history(path, rev): 111 if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])): 112 yield h 112 113 elif revranges: 113 def history( limit):114 def history(): 114 115 prevpath = path 115 116 expected_next_item = None 116 117 ranges = list(revranges.pairs) … … 123 124 p, rev, chg = node_history[0] 124 125 if rev < a: 125 126 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] 127 if 'CHANGESET_VIEW' in req.perm(cset_resource(id=rev)): 128 if expected_next_item: 129 # check whether we're continuing previous range 130 np, nrev, nchg = expected_next_item 131 if rev != nrev: # no, we need a separator 132 yield (np, nrev, None) 133 yield node_history[0] 132 134 prevpath = node_history[-1][0] # follow copy 133 b = rev -1135 b = rev - 1 134 136 if len(node_history) > 1: 135 137 expected_next_item = node_history[-1] 136 138 else: … … 138 140 if expected_next_item: 139 141 yield (expected_next_item[0], expected_next_item[1], None) 140 142 else: 141 history = get_existing_node(req, repos, path, rev).get_history 143 def history(): 144 node = get_existing_node(req, repos, path, rev) 145 for h in node.get_history(): 146 if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])): 147 yield h 142 148 143 149 # -- retrieve history, asking for limit+1 results 144 150 info = [] 145 151 depth = 1 146 152 previous_path = normpath 147 153 count = 0 148 for old_path, old_rev, old_chg in history( limit+1):154 for old_path, old_rev, old_chg in history(): 149 155 if stop_rev and repos.rev_older_than(old_rev, stop_rev): 150 156 break 151 157 old_path = repos.normalize_path(old_path) … … 241 247 cs['actions'] = actions 242 248 extra_changes[rev] = cs 243 249 250 repos_resource = Resource('repository', reponame) 244 251 data = { 245 252 'context': Context.from_request(req, 'source', path, 246 253 parent=repos_resource), 247 'reponame': reponame or None, 'repos_resource': repos_resource, 248 'path': path, 'rev': rev, 'stop_rev': stop_rev, 254 'reponame': reponame or None, 'repos': repos, 249 255 'path': path, 'rev': rev, 'stop_rev': stop_rev, 250 256 'revranges': revranges, 251 257 'mode': mode, 'verbose': verbose, 'limit' : limit, … … 350 356 path, revs = match[:idx], match[idx+1:] 351 357 352 358 rm = RepositoryManager(self.env) 353 authname = formatter.perm.username354 359 reponame = rm.get_default_repository(formatter.context) 355 360 if reponame is not None: 356 repos = rm.get_repository(reponame , authname)361 repos = rm.get_repository(reponame) 357 362 else: 358 reponame, repos, path = rm.get_repository_by_path(path , authname)363 reponame, repos, path = rm.get_repository_by_path(path) 359 364 360 365 revranges = None 361 366 if any(c for c in ':-,' if c in revs): -
trac/versioncontrol/web_ui/tests/wikisyntax.py
diff --git a/trac/versioncontrol/web_ui/tests/wikisyntax.py b/trac/versioncontrol/web_ui/tests/wikisyntax.py
a b 11 11 12 12 def _get_changeset(rev): 13 13 if rev == '1': 14 return Mock(message="start" )14 return Mock(message="start", can_view=lambda perm: True) 15 15 else: 16 16 raise NoSuchChangeset(rev) 17 17
