summaryrefslogtreecommitdiff
path: root/hgext/acl.py
diff options
context:
space:
mode:
Diffstat (limited to 'hgext/acl.py')
-rw-r--r--hgext/acl.py316
1 files changed, 316 insertions, 0 deletions
diff --git a/hgext/acl.py b/hgext/acl.py
new file mode 100644
index 0000000..2bf41aa
--- /dev/null
+++ b/hgext/acl.py
@@ -0,0 +1,316 @@
+# acl.py - changeset access control for mercurial
+#
+# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+'''hooks for controlling repository access
+
+This hook makes it possible to allow or deny write access to given
+branches and paths of a repository when receiving incoming changesets
+via pretxnchangegroup and pretxncommit.
+
+The authorization is matched based on the local user name on the
+system where the hook runs, and not the committer of the original
+changeset (since the latter is merely informative).
+
+The acl hook is best used along with a restricted shell like hgsh,
+preventing authenticating users from doing anything other than pushing
+or pulling. The hook is not safe to use if users have interactive
+shell access, as they can then disable the hook. Nor is it safe if
+remote users share an account, because then there is no way to
+distinguish them.
+
+The order in which access checks are performed is:
+
+1) Deny list for branches (section ``acl.deny.branches``)
+2) Allow list for branches (section ``acl.allow.branches``)
+3) Deny list for paths (section ``acl.deny``)
+4) Allow list for paths (section ``acl.allow``)
+
+The allow and deny sections take key-value pairs.
+
+Branch-based Access Control
+---------------------------
+
+Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
+have branch-based access control. Keys in these sections can be
+either:
+
+- a branch name, or
+- an asterisk, to match any branch;
+
+The corresponding values can be either:
+
+- a comma-separated list containing users and groups, or
+- an asterisk, to match anyone;
+
+You can add the "!" prefix to a user or group name to invert the sense
+of the match.
+
+Path-based Access Control
+-------------------------
+
+Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
+access control. Keys in these sections accept a subtree pattern (with
+a glob syntax by default). The corresponding values follow the same
+syntax as the other sections above.
+
+Groups
+------
+
+Group names must be prefixed with an ``@`` symbol. Specifying a group
+name has the same effect as specifying all the users in that group.
+
+You can define group members in the ``acl.groups`` section.
+If a group name is not defined there, and Mercurial is running under
+a Unix-like system, the list of users will be taken from the OS.
+Otherwise, an exception will be raised.
+
+Example Configuration
+---------------------
+
+::
+
+ [hooks]
+
+ # Use this if you want to check access restrictions at commit time
+ pretxncommit.acl = python:hgext.acl.hook
+
+ # Use this if you want to check access restrictions for pull, push,
+ # bundle and serve.
+ pretxnchangegroup.acl = python:hgext.acl.hook
+
+ [acl]
+ # Allow or deny access for incoming changes only if their source is
+ # listed here, let them pass otherwise. Source is "serve" for all
+ # remote access (http or ssh), "push", "pull" or "bundle" when the
+ # related commands are run locally.
+ # Default: serve
+ sources = serve
+
+ [acl.deny.branches]
+
+ # Everyone is denied to the frozen branch:
+ frozen-branch = *
+
+ # A bad user is denied on all branches:
+ * = bad-user
+
+ [acl.allow.branches]
+
+ # A few users are allowed on branch-a:
+ branch-a = user-1, user-2, user-3
+
+ # Only one user is allowed on branch-b:
+ branch-b = user-1
+
+ # The super user is allowed on any branch:
+ * = super-user
+
+ # Everyone is allowed on branch-for-tests:
+ branch-for-tests = *
+
+ [acl.deny]
+ # This list is checked first. If a match is found, acl.allow is not
+ # checked. All users are granted access if acl.deny is not present.
+ # Format for both lists: glob pattern = user, ..., @group, ...
+
+ # To match everyone, use an asterisk for the user:
+ # my/glob/pattern = *
+
+ # user6 will not have write access to any file:
+ ** = user6
+
+ # Group "hg-denied" will not have write access to any file:
+ ** = @hg-denied
+
+ # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
+ # everyone being able to change all other files. See below.
+ src/main/resources/DONT-TOUCH-THIS.txt = *
+
+ [acl.allow]
+ # if acl.allow is not present, all users are allowed by default
+ # empty acl.allow = no users allowed
+
+ # User "doc_writer" has write access to any file under the "docs"
+ # folder:
+ docs/** = doc_writer
+
+ # User "jack" and group "designers" have write access to any file
+ # under the "images" folder:
+ images/** = jack, @designers
+
+ # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
+ # will have write access to any file under the "resources" folder
+ # (except for 1 file. See acl.deny):
+ src/main/resources/** = *
+
+ .hgtags = release_engineer
+
+Examples using the "!" prefix
+.............................
+
+Suppose there's a branch that only a given user (or group) should be able to
+push to, and you don't want to restrict access to any other branch that may
+be created.
+
+The "!" prefix allows you to prevent anyone except a given user or group to
+push changesets in a given branch or path.
+
+In the examples below, we will:
+1) Deny access to branch "ring" to anyone but user "gollum"
+2) Deny access to branch "lake" to anyone but members of the group "hobbit"
+3) Deny access to a file to anyone but user "gollum"
+
+::
+
+ [acl.allow.branches]
+ # Empty
+
+ [acl.deny.branches]
+
+ # 1) only 'gollum' can commit to branch 'ring';
+ # 'gollum' and anyone else can still commit to any other branch.
+ ring = !gollum
+
+ # 2) only members of the group 'hobbit' can commit to branch 'lake';
+ # 'hobbit' members and anyone else can still commit to any other branch.
+ lake = !@hobbit
+
+ # You can also deny access based on file paths:
+
+ [acl.allow]
+ # Empty
+
+ [acl.deny]
+ # 3) only 'gollum' can change the file below;
+ # 'gollum' and anyone else can still change any other file.
+ /misty/mountains/cave/ring = !gollum
+
+'''
+
+from mercurial.i18n import _
+from mercurial import util, match
+import getpass, urllib
+
+testedwith = 'internal'
+
+def _getusers(ui, group):
+
+ # First, try to use group definition from section [acl.groups]
+ hgrcusers = ui.configlist('acl.groups', group)
+ if hgrcusers:
+ return hgrcusers
+
+ ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
+ # If no users found in group definition, get users from OS-level group
+ try:
+ return util.groupmembers(group)
+ except KeyError:
+ raise util.Abort(_("group '%s' is undefined") % group)
+
+def _usermatch(ui, user, usersorgroups):
+
+ if usersorgroups == '*':
+ return True
+
+ for ug in usersorgroups.replace(',', ' ').split():
+
+ if ug.startswith('!'):
+ # Test for excluded user or group. Format:
+ # if ug is a user name: !username
+ # if ug is a group name: !@groupname
+ ug = ug[1:]
+ if not ug.startswith('@') and user != ug \
+ or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
+ return True
+
+ # Test for user or group. Format:
+ # if ug is a user name: username
+ # if ug is a group name: @groupname
+ elif user == ug \
+ or ug.startswith('@') and user in _getusers(ui, ug[1:]):
+ return True
+
+ return False
+
+def buildmatch(ui, repo, user, key):
+ '''return tuple of (match function, list enabled).'''
+ if not ui.has_section(key):
+ ui.debug('acl: %s not enabled\n' % key)
+ return None
+
+ pats = [pat for pat, users in ui.configitems(key)
+ if _usermatch(ui, user, users)]
+ ui.debug('acl: %s enabled, %d entries for user %s\n' %
+ (key, len(pats), user))
+
+ # Branch-based ACL
+ if not repo:
+ if pats:
+ # If there's an asterisk (meaning "any branch"), always return True;
+ # Otherwise, test if b is in pats
+ if '*' in pats:
+ return util.always
+ return lambda b: b in pats
+ return util.never
+
+ # Path-based ACL
+ if pats:
+ return match.match(repo.root, '', pats)
+ return util.never
+
+def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
+ if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
+ raise util.Abort(_('config error - hook type "%s" cannot stop '
+ 'incoming changesets nor commits') % hooktype)
+ if (hooktype == 'pretxnchangegroup' and
+ source not in ui.config('acl', 'sources', 'serve').split()):
+ ui.debug('acl: changes have source "%s" - skipping\n' % source)
+ return
+
+ user = None
+ if source == 'serve' and 'url' in kwargs:
+ url = kwargs['url'].split(':')
+ if url[0] == 'remote' and url[1].startswith('http'):
+ user = urllib.unquote(url[3])
+
+ if user is None:
+ user = getpass.getuser()
+
+ ui.debug('acl: checking access for user "%s"\n' % user)
+
+ cfg = ui.config('acl', 'config')
+ if cfg:
+ ui.readconfig(cfg, sections = ['acl.groups', 'acl.allow.branches',
+ 'acl.deny.branches', 'acl.allow', 'acl.deny'])
+
+ allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
+ denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
+ allow = buildmatch(ui, repo, user, 'acl.allow')
+ deny = buildmatch(ui, repo, user, 'acl.deny')
+
+ for rev in xrange(repo[node], len(repo)):
+ ctx = repo[rev]
+ branch = ctx.branch()
+ if denybranches and denybranches(branch):
+ raise util.Abort(_('acl: user "%s" denied on branch "%s"'
+ ' (changeset "%s")')
+ % (user, branch, ctx))
+ if allowbranches and not allowbranches(branch):
+ raise util.Abort(_('acl: user "%s" not allowed on branch "%s"'
+ ' (changeset "%s")')
+ % (user, branch, ctx))
+ ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
+ % (ctx, branch))
+
+ for f in ctx.files():
+ if deny and deny(f):
+ raise util.Abort(_('acl: user "%s" denied on "%s"'
+ ' (changeset "%s")') % (user, f, ctx))
+ if allow and not allow(f):
+ raise util.Abort(_('acl: user "%s" not allowed on "%s"'
+ ' (changeset "%s")') % (user, f, ctx))
+ ui.debug('acl: path access granted: "%s"\n' % ctx)