hgext/acl.py
changeset 2344 ae12e5a2c4a3
child 3059 5e39ad2c8b52
equal deleted inserted replaced
2343:af81d8770620 2344:ae12e5a2c4a3
       
     1 # acl.py - changeset access control for mercurial
       
     2 #
       
     3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms
       
     6 # of the GNU General Public License, incorporated herein by reference.
       
     7 #
       
     8 # this hook allows to allow or deny access to parts of a repo when
       
     9 # taking incoming changesets.
       
    10 #
       
    11 # authorization is against local user name on system where hook is
       
    12 # run, not committer of original changeset (since that is easy to
       
    13 # spoof).
       
    14 #
       
    15 # acl hook is best to use if you use hgsh to set up restricted shells
       
    16 # for authenticated users to only push to / pull from.  not safe if
       
    17 # user has interactive shell access, because they can disable hook.
       
    18 # also not safe if remote users share one local account, because then
       
    19 # no way to tell remote users apart.
       
    20 #
       
    21 # to use, configure acl extension in hgrc like this:
       
    22 #
       
    23 #   [extensions]
       
    24 #   hgext.acl =
       
    25 #
       
    26 #   [hooks]
       
    27 #   pretxnchangegroup.acl = python:hgext.acl.hook
       
    28 #
       
    29 #   [acl]
       
    30 #   sources = serve        # check if source of incoming changes in this list
       
    31 #                          # ("serve" == ssh or http, "push", "pull", "bundle")
       
    32 #
       
    33 # allow and deny lists have subtree pattern (default syntax is glob)
       
    34 # on left, user names on right. deny list checked before allow list.
       
    35 #
       
    36 #   [acl.allow]
       
    37 #   # if acl.allow not present, all users allowed by default
       
    38 #   # empty acl.allow = no users allowed
       
    39 #   docs/** = doc_writer
       
    40 #   .hgtags = release_engineer
       
    41 #
       
    42 #   [acl.deny]
       
    43 #   # if acl.deny not present, no users denied by default
       
    44 #   # empty acl.deny = all users allowed
       
    45 #   glob pattern = user4, user5
       
    46 #   ** = user6
       
    47 
       
    48 from mercurial.demandload import *
       
    49 from mercurial.i18n import gettext as _
       
    50 from mercurial.node import *
       
    51 demandload(globals(), 'getpass mercurial:util')
       
    52 
       
    53 class checker(object):
       
    54     '''acl checker.'''
       
    55 
       
    56     def buildmatch(self, key):
       
    57         '''return tuple of (match function, list enabled).'''
       
    58         if not self.ui.has_config(key):
       
    59             self.ui.debug(_('acl: %s not enabled\n') % key)
       
    60             return None, False
       
    61 
       
    62         thisuser = self.getuser()
       
    63         pats = [pat for pat, user in self.ui.configitems(key)
       
    64                 if user == thisuser]
       
    65         self.ui.debug(_('acl: %s enabled, %d entries for user %s\n') %
       
    66                       (key, len(pats), thisuser))
       
    67         if pats:
       
    68             match = util.matcher(self.repo.root, names=pats)[1]
       
    69         else:
       
    70             match = util.never
       
    71         return match, True
       
    72 
       
    73     def getuser(self):
       
    74         '''return name of authenticated user.'''
       
    75         return self.user
       
    76 
       
    77     def __init__(self, ui, repo):
       
    78         self.ui = ui
       
    79         self.repo = repo
       
    80         self.user = getpass.getuser()
       
    81         cfg = self.ui.config('acl', 'config')
       
    82         if cfg:
       
    83             self.ui.readconfig(cfg)
       
    84         self.allow, self.allowable = self.buildmatch('acl.allow')
       
    85         self.deny, self.deniable = self.buildmatch('acl.deny')
       
    86 
       
    87     def skipsource(self, source):
       
    88         '''true if incoming changes from this source should be skipped.'''
       
    89         ok_sources = self.ui.config('acl', 'sources', 'serve').split()
       
    90         return source not in ok_sources
       
    91 
       
    92     def check(self, node):
       
    93         '''return if access allowed, raise exception if not.'''
       
    94         files = self.repo.changelog.read(node)[3]
       
    95         if self.deniable:
       
    96             for f in files:
       
    97                 if self.deny(f):
       
    98                     self.ui.debug(_('acl: user %s denied on %s\n') %
       
    99                                   (self.getuser(), f))
       
   100                     raise util.Abort(_('acl: access denied for changeset %s') %
       
   101                                      short(node))
       
   102         if self.allowable:
       
   103             for f in files:
       
   104                 if not self.allow(f):
       
   105                     self.ui.debug(_('acl: user %s not allowed on %s\n') %
       
   106                                   (self.getuser(), f))
       
   107                     raise util.Abort(_('acl: access denied for changeset %s') %
       
   108                                      short(node))
       
   109         self.ui.debug(_('acl: allowing changeset %s\n') % short(node))
       
   110 
       
   111 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
       
   112     if hooktype != 'pretxnchangegroup':
       
   113         raise util.Abort(_('config error - hook type "%s" cannot stop '
       
   114                            'incoming changesets') % hooktype)
       
   115 
       
   116     c = checker(ui, repo)
       
   117     if c.skipsource(source):
       
   118         ui.debug(_('acl: changes have source "%s" - skipping\n') % source)
       
   119         return
       
   120 
       
   121     start = repo.changelog.rev(bin(node))
       
   122     end = repo.changelog.count()
       
   123     for rev in xrange(start, end):
       
   124         c.check(repo.changelog.node(rev))