PageRenderTime 44ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/hgext/acl.py

https://bitbucket.org/mirror/mercurial/
Python | 316 lines | 268 code | 1 blank | 47 comment | 0 complexity | 2cdfaeab59491132860b6c4e5ee263d5 MD5 | raw file
Possible License(s): GPL-2.0
  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 of the
  6. # GNU General Public License version 2 or any later version.
  7. '''hooks for controlling repository access
  8. This hook makes it possible to allow or deny write access to given
  9. branches and paths of a repository when receiving incoming changesets
  10. via pretxnchangegroup and pretxncommit.
  11. The authorization is matched based on the local user name on the
  12. system where the hook runs, and not the committer of the original
  13. changeset (since the latter is merely informative).
  14. The acl hook is best used along with a restricted shell like hgsh,
  15. preventing authenticating users from doing anything other than pushing
  16. or pulling. The hook is not safe to use if users have interactive
  17. shell access, as they can then disable the hook. Nor is it safe if
  18. remote users share an account, because then there is no way to
  19. distinguish them.
  20. The order in which access checks are performed is:
  21. 1) Deny list for branches (section ``acl.deny.branches``)
  22. 2) Allow list for branches (section ``acl.allow.branches``)
  23. 3) Deny list for paths (section ``acl.deny``)
  24. 4) Allow list for paths (section ``acl.allow``)
  25. The allow and deny sections take key-value pairs.
  26. Branch-based Access Control
  27. ---------------------------
  28. Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
  29. have branch-based access control. Keys in these sections can be
  30. either:
  31. - a branch name, or
  32. - an asterisk, to match any branch;
  33. The corresponding values can be either:
  34. - a comma-separated list containing users and groups, or
  35. - an asterisk, to match anyone;
  36. You can add the "!" prefix to a user or group name to invert the sense
  37. of the match.
  38. Path-based Access Control
  39. -------------------------
  40. Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
  41. access control. Keys in these sections accept a subtree pattern (with
  42. a glob syntax by default). The corresponding values follow the same
  43. syntax as the other sections above.
  44. Groups
  45. ------
  46. Group names must be prefixed with an ``@`` symbol. Specifying a group
  47. name has the same effect as specifying all the users in that group.
  48. You can define group members in the ``acl.groups`` section.
  49. If a group name is not defined there, and Mercurial is running under
  50. a Unix-like system, the list of users will be taken from the OS.
  51. Otherwise, an exception will be raised.
  52. Example Configuration
  53. ---------------------
  54. ::
  55. [hooks]
  56. # Use this if you want to check access restrictions at commit time
  57. pretxncommit.acl = python:hgext.acl.hook
  58. # Use this if you want to check access restrictions for pull, push,
  59. # bundle and serve.
  60. pretxnchangegroup.acl = python:hgext.acl.hook
  61. [acl]
  62. # Allow or deny access for incoming changes only if their source is
  63. # listed here, let them pass otherwise. Source is "serve" for all
  64. # remote access (http or ssh), "push", "pull" or "bundle" when the
  65. # related commands are run locally.
  66. # Default: serve
  67. sources = serve
  68. [acl.deny.branches]
  69. # Everyone is denied to the frozen branch:
  70. frozen-branch = *
  71. # A bad user is denied on all branches:
  72. * = bad-user
  73. [acl.allow.branches]
  74. # A few users are allowed on branch-a:
  75. branch-a = user-1, user-2, user-3
  76. # Only one user is allowed on branch-b:
  77. branch-b = user-1
  78. # The super user is allowed on any branch:
  79. * = super-user
  80. # Everyone is allowed on branch-for-tests:
  81. branch-for-tests = *
  82. [acl.deny]
  83. # This list is checked first. If a match is found, acl.allow is not
  84. # checked. All users are granted access if acl.deny is not present.
  85. # Format for both lists: glob pattern = user, ..., @group, ...
  86. # To match everyone, use an asterisk for the user:
  87. # my/glob/pattern = *
  88. # user6 will not have write access to any file:
  89. ** = user6
  90. # Group "hg-denied" will not have write access to any file:
  91. ** = @hg-denied
  92. # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
  93. # everyone being able to change all other files. See below.
  94. src/main/resources/DONT-TOUCH-THIS.txt = *
  95. [acl.allow]
  96. # if acl.allow is not present, all users are allowed by default
  97. # empty acl.allow = no users allowed
  98. # User "doc_writer" has write access to any file under the "docs"
  99. # folder:
  100. docs/** = doc_writer
  101. # User "jack" and group "designers" have write access to any file
  102. # under the "images" folder:
  103. images/** = jack, @designers
  104. # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
  105. # will have write access to any file under the "resources" folder
  106. # (except for 1 file. See acl.deny):
  107. src/main/resources/** = *
  108. .hgtags = release_engineer
  109. Examples using the "!" prefix
  110. .............................
  111. Suppose there's a branch that only a given user (or group) should be able to
  112. push to, and you don't want to restrict access to any other branch that may
  113. be created.
  114. The "!" prefix allows you to prevent anyone except a given user or group to
  115. push changesets in a given branch or path.
  116. In the examples below, we will:
  117. 1) Deny access to branch "ring" to anyone but user "gollum"
  118. 2) Deny access to branch "lake" to anyone but members of the group "hobbit"
  119. 3) Deny access to a file to anyone but user "gollum"
  120. ::
  121. [acl.allow.branches]
  122. # Empty
  123. [acl.deny.branches]
  124. # 1) only 'gollum' can commit to branch 'ring';
  125. # 'gollum' and anyone else can still commit to any other branch.
  126. ring = !gollum
  127. # 2) only members of the group 'hobbit' can commit to branch 'lake';
  128. # 'hobbit' members and anyone else can still commit to any other branch.
  129. lake = !@hobbit
  130. # You can also deny access based on file paths:
  131. [acl.allow]
  132. # Empty
  133. [acl.deny]
  134. # 3) only 'gollum' can change the file below;
  135. # 'gollum' and anyone else can still change any other file.
  136. /misty/mountains/cave/ring = !gollum
  137. '''
  138. from mercurial.i18n import _
  139. from mercurial import util, match
  140. import getpass, urllib
  141. testedwith = 'internal'
  142. def _getusers(ui, group):
  143. # First, try to use group definition from section [acl.groups]
  144. hgrcusers = ui.configlist('acl.groups', group)
  145. if hgrcusers:
  146. return hgrcusers
  147. ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
  148. # If no users found in group definition, get users from OS-level group
  149. try:
  150. return util.groupmembers(group)
  151. except KeyError:
  152. raise util.Abort(_("group '%s' is undefined") % group)
  153. def _usermatch(ui, user, usersorgroups):
  154. if usersorgroups == '*':
  155. return True
  156. for ug in usersorgroups.replace(',', ' ').split():
  157. if ug.startswith('!'):
  158. # Test for excluded user or group. Format:
  159. # if ug is a user name: !username
  160. # if ug is a group name: !@groupname
  161. ug = ug[1:]
  162. if not ug.startswith('@') and user != ug \
  163. or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
  164. return True
  165. # Test for user or group. Format:
  166. # if ug is a user name: username
  167. # if ug is a group name: @groupname
  168. elif user == ug \
  169. or ug.startswith('@') and user in _getusers(ui, ug[1:]):
  170. return True
  171. return False
  172. def buildmatch(ui, repo, user, key):
  173. '''return tuple of (match function, list enabled).'''
  174. if not ui.has_section(key):
  175. ui.debug('acl: %s not enabled\n' % key)
  176. return None
  177. pats = [pat for pat, users in ui.configitems(key)
  178. if _usermatch(ui, user, users)]
  179. ui.debug('acl: %s enabled, %d entries for user %s\n' %
  180. (key, len(pats), user))
  181. # Branch-based ACL
  182. if not repo:
  183. if pats:
  184. # If there's an asterisk (meaning "any branch"), always return True;
  185. # Otherwise, test if b is in pats
  186. if '*' in pats:
  187. return util.always
  188. return lambda b: b in pats
  189. return util.never
  190. # Path-based ACL
  191. if pats:
  192. return match.match(repo.root, '', pats)
  193. return util.never
  194. def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
  195. if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
  196. raise util.Abort(_('config error - hook type "%s" cannot stop '
  197. 'incoming changesets nor commits') % hooktype)
  198. if (hooktype == 'pretxnchangegroup' and
  199. source not in ui.config('acl', 'sources', 'serve').split()):
  200. ui.debug('acl: changes have source "%s" - skipping\n' % source)
  201. return
  202. user = None
  203. if source == 'serve' and 'url' in kwargs:
  204. url = kwargs['url'].split(':')
  205. if url[0] == 'remote' and url[1].startswith('http'):
  206. user = urllib.unquote(url[3])
  207. if user is None:
  208. user = getpass.getuser()
  209. ui.debug('acl: checking access for user "%s"\n' % user)
  210. cfg = ui.config('acl', 'config')
  211. if cfg:
  212. ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
  213. 'acl.deny.branches', 'acl.allow', 'acl.deny'])
  214. allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
  215. denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
  216. allow = buildmatch(ui, repo, user, 'acl.allow')
  217. deny = buildmatch(ui, repo, user, 'acl.deny')
  218. for rev in xrange(repo[node], len(repo)):
  219. ctx = repo[rev]
  220. branch = ctx.branch()
  221. if denybranches and denybranches(branch):
  222. raise util.Abort(_('acl: user "%s" denied on branch "%s"'
  223. ' (changeset "%s")')
  224. % (user, branch, ctx))
  225. if allowbranches and not allowbranches(branch):
  226. raise util.Abort(_('acl: user "%s" not allowed on branch "%s"'
  227. ' (changeset "%s")')
  228. % (user, branch, ctx))
  229. ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
  230. % (ctx, branch))
  231. for f in ctx.files():
  232. if deny and deny(f):
  233. raise util.Abort(_('acl: user "%s" denied on "%s"'
  234. ' (changeset "%s")') % (user, f, ctx))
  235. if allow and not allow(f):
  236. raise util.Abort(_('acl: user "%s" not allowed on "%s"'
  237. ' (changeset "%s")') % (user, f, ctx))
  238. ui.debug('acl: path access granted: "%s"\n' % ctx)