1 # acl.py - changeset access control for mercurial
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
9 '''hooks for controlling repository access
11 This hook makes it possible to allow or deny write access to portions
12 of a repository when receiving incoming changesets.
14 The authorization is matched based on the local user name on the
15 system where the hook runs, and not the committer of the original
16 changeset (since the latter is merely informative).
18 The acl hook is best used along with a restricted shell like hgsh,
19 preventing authenticating users from doing anything other than
20 pushing or pulling. The hook is not safe to use if users have
21 interactive shell access, as they can then disable the hook.
22 Nor is it safe if remote users share an account, because then there
23 is no way to distinguish them.
25 To use this hook, configure the acl extension in your hgrc like this::
31 pretxnchangegroup.acl = python:hgext.acl.hook
34 # Check whether the source of incoming changes is in this list
35 # ("serve" == ssh or http, "push", "pull", "bundle")
38 The allow and deny sections take a subtree pattern as key (with a glob
39 syntax by default), and a comma separated list of users as the
40 corresponding value. The deny list is checked before the allow list
44 # If acl.allow is not present, all users are allowed by default.
45 # An empty acl.allow section means no users allowed.
47 .hgtags = release_engineer
50 # If acl.deny is not present, no users are refused by default.
51 # An empty acl.deny section means all users allowed.
52 glob pattern = user4, user5
56 from mercurial.i18n import _
57 from mercurial import util, match
58 import getpass, urllib
60 def buildmatch(ui, repo, user, key):
61 '''return tuple of (match function, list enabled).'''
62 if not ui.has_section(key):
63 ui.debug(_('acl: %s not enabled\n') % key)
66 pats = [pat for pat, users in ui.configitems(key)
67 if user in users.replace(',', ' ').split()]
68 ui.debug(_('acl: %s enabled, %d entries for user %s\n') %
69 (key, len(pats), user))
71 return match.match(repo.root, '', pats)
72 return match.exact(repo.root, '', [])
75 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
76 if hooktype != 'pretxnchangegroup':
77 raise util.Abort(_('config error - hook type "%s" cannot stop '
78 'incoming changesets') % hooktype)
79 if source not in ui.config('acl', 'sources', 'serve').split():
80 ui.debug(_('acl: changes have source "%s" - skipping\n') % source)
84 if source == 'serve' and 'url' in kwargs:
85 url = kwargs['url'].split(':')
86 if url[0] == 'remote' and url[1].startswith('http'):
87 user = urllib.unquote(url[3])
90 user = getpass.getuser()
92 cfg = ui.config('acl', 'config')
94 ui.readconfig(cfg, sections = ['acl.allow', 'acl.deny'])
95 allow = buildmatch(ui, repo, user, 'acl.allow')
96 deny = buildmatch(ui, repo, user, 'acl.deny')
98 for rev in xrange(repo[node], len(repo)):
100 for f in ctx.files():
102 ui.debug(_('acl: user %s denied on %s\n') % (user, f))
103 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
104 if allow and not allow(f):
105 ui.debug(_('acl: user %s not allowed on %s\n') % (user, f))
106 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
107 ui.debug(_('acl: allowing changeset %s\n') % ctx)