##// END OF EJS Templates
py3: fix keyword arguments handling in hgext/acl.py...
Pulkit Goyal -
r36395:39212037 default
parent child Browse files
Show More
@@ -1,380 +1,380 b''
1 # acl.py - changeset access control for mercurial
1 # acl.py - changeset access control for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''hooks for controlling repository access
8 '''hooks for controlling repository access
9
9
10 This hook makes it possible to allow or deny write access to given
10 This hook makes it possible to allow or deny write access to given
11 branches and paths of a repository when receiving incoming changesets
11 branches and paths of a repository when receiving incoming changesets
12 via pretxnchangegroup and pretxncommit.
12 via pretxnchangegroup and pretxncommit.
13
13
14 The authorization is matched based on the local user name on the
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
15 system where the hook runs, and not the committer of the original
16 changeset (since the latter is merely informative).
16 changeset (since the latter is merely informative).
17
17
18 The acl hook is best used along with a restricted shell like hgsh,
18 The acl hook is best used along with a restricted shell like hgsh,
19 preventing authenticating users from doing anything other than pushing
19 preventing authenticating users from doing anything other than pushing
20 or pulling. The hook is not safe to use if users have interactive
20 or pulling. The hook is not safe to use if users have interactive
21 shell access, as they can then disable the hook. Nor is it safe if
21 shell access, as they can then disable the hook. Nor is it safe if
22 remote users share an account, because then there is no way to
22 remote users share an account, because then there is no way to
23 distinguish them.
23 distinguish them.
24
24
25 The order in which access checks are performed is:
25 The order in which access checks are performed is:
26
26
27 1) Deny list for branches (section ``acl.deny.branches``)
27 1) Deny list for branches (section ``acl.deny.branches``)
28 2) Allow list for branches (section ``acl.allow.branches``)
28 2) Allow list for branches (section ``acl.allow.branches``)
29 3) Deny list for paths (section ``acl.deny``)
29 3) Deny list for paths (section ``acl.deny``)
30 4) Allow list for paths (section ``acl.allow``)
30 4) Allow list for paths (section ``acl.allow``)
31
31
32 The allow and deny sections take key-value pairs.
32 The allow and deny sections take key-value pairs.
33
33
34 Branch-based Access Control
34 Branch-based Access Control
35 ---------------------------
35 ---------------------------
36
36
37 Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
37 Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
38 have branch-based access control. Keys in these sections can be
38 have branch-based access control. Keys in these sections can be
39 either:
39 either:
40
40
41 - a branch name, or
41 - a branch name, or
42 - an asterisk, to match any branch;
42 - an asterisk, to match any branch;
43
43
44 The corresponding values can be either:
44 The corresponding values can be either:
45
45
46 - a comma-separated list containing users and groups, or
46 - a comma-separated list containing users and groups, or
47 - an asterisk, to match anyone;
47 - an asterisk, to match anyone;
48
48
49 You can add the "!" prefix to a user or group name to invert the sense
49 You can add the "!" prefix to a user or group name to invert the sense
50 of the match.
50 of the match.
51
51
52 Path-based Access Control
52 Path-based Access Control
53 -------------------------
53 -------------------------
54
54
55 Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
55 Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
56 access control. Keys in these sections accept a subtree pattern (with
56 access control. Keys in these sections accept a subtree pattern (with
57 a glob syntax by default). The corresponding values follow the same
57 a glob syntax by default). The corresponding values follow the same
58 syntax as the other sections above.
58 syntax as the other sections above.
59
59
60 Groups
60 Groups
61 ------
61 ------
62
62
63 Group names must be prefixed with an ``@`` symbol. Specifying a group
63 Group names must be prefixed with an ``@`` symbol. Specifying a group
64 name has the same effect as specifying all the users in that group.
64 name has the same effect as specifying all the users in that group.
65
65
66 You can define group members in the ``acl.groups`` section.
66 You can define group members in the ``acl.groups`` section.
67 If a group name is not defined there, and Mercurial is running under
67 If a group name is not defined there, and Mercurial is running under
68 a Unix-like system, the list of users will be taken from the OS.
68 a Unix-like system, the list of users will be taken from the OS.
69 Otherwise, an exception will be raised.
69 Otherwise, an exception will be raised.
70
70
71 Example Configuration
71 Example Configuration
72 ---------------------
72 ---------------------
73
73
74 ::
74 ::
75
75
76 [hooks]
76 [hooks]
77
77
78 # Use this if you want to check access restrictions at commit time
78 # Use this if you want to check access restrictions at commit time
79 pretxncommit.acl = python:hgext.acl.hook
79 pretxncommit.acl = python:hgext.acl.hook
80
80
81 # Use this if you want to check access restrictions for pull, push,
81 # Use this if you want to check access restrictions for pull, push,
82 # bundle and serve.
82 # bundle and serve.
83 pretxnchangegroup.acl = python:hgext.acl.hook
83 pretxnchangegroup.acl = python:hgext.acl.hook
84
84
85 [acl]
85 [acl]
86 # Allow or deny access for incoming changes only if their source is
86 # Allow or deny access for incoming changes only if their source is
87 # listed here, let them pass otherwise. Source is "serve" for all
87 # listed here, let them pass otherwise. Source is "serve" for all
88 # remote access (http or ssh), "push", "pull" or "bundle" when the
88 # remote access (http or ssh), "push", "pull" or "bundle" when the
89 # related commands are run locally.
89 # related commands are run locally.
90 # Default: serve
90 # Default: serve
91 sources = serve
91 sources = serve
92
92
93 [acl.deny.branches]
93 [acl.deny.branches]
94
94
95 # Everyone is denied to the frozen branch:
95 # Everyone is denied to the frozen branch:
96 frozen-branch = *
96 frozen-branch = *
97
97
98 # A bad user is denied on all branches:
98 # A bad user is denied on all branches:
99 * = bad-user
99 * = bad-user
100
100
101 [acl.allow.branches]
101 [acl.allow.branches]
102
102
103 # A few users are allowed on branch-a:
103 # A few users are allowed on branch-a:
104 branch-a = user-1, user-2, user-3
104 branch-a = user-1, user-2, user-3
105
105
106 # Only one user is allowed on branch-b:
106 # Only one user is allowed on branch-b:
107 branch-b = user-1
107 branch-b = user-1
108
108
109 # The super user is allowed on any branch:
109 # The super user is allowed on any branch:
110 * = super-user
110 * = super-user
111
111
112 # Everyone is allowed on branch-for-tests:
112 # Everyone is allowed on branch-for-tests:
113 branch-for-tests = *
113 branch-for-tests = *
114
114
115 [acl.deny]
115 [acl.deny]
116 # This list is checked first. If a match is found, acl.allow is not
116 # This list is checked first. If a match is found, acl.allow is not
117 # checked. All users are granted access if acl.deny is not present.
117 # checked. All users are granted access if acl.deny is not present.
118 # Format for both lists: glob pattern = user, ..., @group, ...
118 # Format for both lists: glob pattern = user, ..., @group, ...
119
119
120 # To match everyone, use an asterisk for the user:
120 # To match everyone, use an asterisk for the user:
121 # my/glob/pattern = *
121 # my/glob/pattern = *
122
122
123 # user6 will not have write access to any file:
123 # user6 will not have write access to any file:
124 ** = user6
124 ** = user6
125
125
126 # Group "hg-denied" will not have write access to any file:
126 # Group "hg-denied" will not have write access to any file:
127 ** = @hg-denied
127 ** = @hg-denied
128
128
129 # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
129 # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
130 # everyone being able to change all other files. See below.
130 # everyone being able to change all other files. See below.
131 src/main/resources/DONT-TOUCH-THIS.txt = *
131 src/main/resources/DONT-TOUCH-THIS.txt = *
132
132
133 [acl.allow]
133 [acl.allow]
134 # if acl.allow is not present, all users are allowed by default
134 # if acl.allow is not present, all users are allowed by default
135 # empty acl.allow = no users allowed
135 # empty acl.allow = no users allowed
136
136
137 # User "doc_writer" has write access to any file under the "docs"
137 # User "doc_writer" has write access to any file under the "docs"
138 # folder:
138 # folder:
139 docs/** = doc_writer
139 docs/** = doc_writer
140
140
141 # User "jack" and group "designers" have write access to any file
141 # User "jack" and group "designers" have write access to any file
142 # under the "images" folder:
142 # under the "images" folder:
143 images/** = jack, @designers
143 images/** = jack, @designers
144
144
145 # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
145 # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
146 # will have write access to any file under the "resources" folder
146 # will have write access to any file under the "resources" folder
147 # (except for 1 file. See acl.deny):
147 # (except for 1 file. See acl.deny):
148 src/main/resources/** = *
148 src/main/resources/** = *
149
149
150 .hgtags = release_engineer
150 .hgtags = release_engineer
151
151
152 Examples using the "!" prefix
152 Examples using the "!" prefix
153 .............................
153 .............................
154
154
155 Suppose there's a branch that only a given user (or group) should be able to
155 Suppose there's a branch that only a given user (or group) should be able to
156 push to, and you don't want to restrict access to any other branch that may
156 push to, and you don't want to restrict access to any other branch that may
157 be created.
157 be created.
158
158
159 The "!" prefix allows you to prevent anyone except a given user or group to
159 The "!" prefix allows you to prevent anyone except a given user or group to
160 push changesets in a given branch or path.
160 push changesets in a given branch or path.
161
161
162 In the examples below, we will:
162 In the examples below, we will:
163 1) Deny access to branch "ring" to anyone but user "gollum"
163 1) Deny access to branch "ring" to anyone but user "gollum"
164 2) Deny access to branch "lake" to anyone but members of the group "hobbit"
164 2) Deny access to branch "lake" to anyone but members of the group "hobbit"
165 3) Deny access to a file to anyone but user "gollum"
165 3) Deny access to a file to anyone but user "gollum"
166
166
167 ::
167 ::
168
168
169 [acl.allow.branches]
169 [acl.allow.branches]
170 # Empty
170 # Empty
171
171
172 [acl.deny.branches]
172 [acl.deny.branches]
173
173
174 # 1) only 'gollum' can commit to branch 'ring';
174 # 1) only 'gollum' can commit to branch 'ring';
175 # 'gollum' and anyone else can still commit to any other branch.
175 # 'gollum' and anyone else can still commit to any other branch.
176 ring = !gollum
176 ring = !gollum
177
177
178 # 2) only members of the group 'hobbit' can commit to branch 'lake';
178 # 2) only members of the group 'hobbit' can commit to branch 'lake';
179 # 'hobbit' members and anyone else can still commit to any other branch.
179 # 'hobbit' members and anyone else can still commit to any other branch.
180 lake = !@hobbit
180 lake = !@hobbit
181
181
182 # You can also deny access based on file paths:
182 # You can also deny access based on file paths:
183
183
184 [acl.allow]
184 [acl.allow]
185 # Empty
185 # Empty
186
186
187 [acl.deny]
187 [acl.deny]
188 # 3) only 'gollum' can change the file below;
188 # 3) only 'gollum' can change the file below;
189 # 'gollum' and anyone else can still change any other file.
189 # 'gollum' and anyone else can still change any other file.
190 /misty/mountains/cave/ring = !gollum
190 /misty/mountains/cave/ring = !gollum
191
191
192 '''
192 '''
193
193
194 from __future__ import absolute_import
194 from __future__ import absolute_import
195
195
196 import getpass
196 import getpass
197
197
198 from mercurial.i18n import _
198 from mercurial.i18n import _
199 from mercurial import (
199 from mercurial import (
200 error,
200 error,
201 extensions,
201 extensions,
202 match,
202 match,
203 pycompat,
203 pycompat,
204 registrar,
204 registrar,
205 util,
205 util,
206 )
206 )
207
207
208 urlreq = util.urlreq
208 urlreq = util.urlreq
209
209
210 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
210 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
211 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
211 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
212 # be specifying the version(s) of Mercurial they are tested with, or
212 # be specifying the version(s) of Mercurial they are tested with, or
213 # leave the attribute unspecified.
213 # leave the attribute unspecified.
214 testedwith = 'ships-with-hg-core'
214 testedwith = 'ships-with-hg-core'
215
215
216 configtable = {}
216 configtable = {}
217 configitem = registrar.configitem(configtable)
217 configitem = registrar.configitem(configtable)
218
218
219 # deprecated config: acl.config
219 # deprecated config: acl.config
220 configitem('acl', 'config',
220 configitem('acl', 'config',
221 default=None,
221 default=None,
222 )
222 )
223 configitem('acl.groups', '.*',
223 configitem('acl.groups', '.*',
224 default=None,
224 default=None,
225 generic=True,
225 generic=True,
226 )
226 )
227 configitem('acl.deny.branches', '.*',
227 configitem('acl.deny.branches', '.*',
228 default=None,
228 default=None,
229 generic=True,
229 generic=True,
230 )
230 )
231 configitem('acl.allow.branches', '.*',
231 configitem('acl.allow.branches', '.*',
232 default=None,
232 default=None,
233 generic=True,
233 generic=True,
234 )
234 )
235 configitem('acl.deny', '.*',
235 configitem('acl.deny', '.*',
236 default=None,
236 default=None,
237 generic=True,
237 generic=True,
238 )
238 )
239 configitem('acl.allow', '.*',
239 configitem('acl.allow', '.*',
240 default=None,
240 default=None,
241 generic=True,
241 generic=True,
242 )
242 )
243 configitem('acl', 'sources',
243 configitem('acl', 'sources',
244 default=lambda: ['serve'],
244 default=lambda: ['serve'],
245 )
245 )
246
246
247 def _getusers(ui, group):
247 def _getusers(ui, group):
248
248
249 # First, try to use group definition from section [acl.groups]
249 # First, try to use group definition from section [acl.groups]
250 hgrcusers = ui.configlist('acl.groups', group)
250 hgrcusers = ui.configlist('acl.groups', group)
251 if hgrcusers:
251 if hgrcusers:
252 return hgrcusers
252 return hgrcusers
253
253
254 ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
254 ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
255 # If no users found in group definition, get users from OS-level group
255 # If no users found in group definition, get users from OS-level group
256 try:
256 try:
257 return util.groupmembers(group)
257 return util.groupmembers(group)
258 except KeyError:
258 except KeyError:
259 raise error.Abort(_("group '%s' is undefined") % group)
259 raise error.Abort(_("group '%s' is undefined") % group)
260
260
261 def _usermatch(ui, user, usersorgroups):
261 def _usermatch(ui, user, usersorgroups):
262
262
263 if usersorgroups == '*':
263 if usersorgroups == '*':
264 return True
264 return True
265
265
266 for ug in usersorgroups.replace(',', ' ').split():
266 for ug in usersorgroups.replace(',', ' ').split():
267
267
268 if ug.startswith('!'):
268 if ug.startswith('!'):
269 # Test for excluded user or group. Format:
269 # Test for excluded user or group. Format:
270 # if ug is a user name: !username
270 # if ug is a user name: !username
271 # if ug is a group name: !@groupname
271 # if ug is a group name: !@groupname
272 ug = ug[1:]
272 ug = ug[1:]
273 if not ug.startswith('@') and user != ug \
273 if not ug.startswith('@') and user != ug \
274 or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
274 or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
275 return True
275 return True
276
276
277 # Test for user or group. Format:
277 # Test for user or group. Format:
278 # if ug is a user name: username
278 # if ug is a user name: username
279 # if ug is a group name: @groupname
279 # if ug is a group name: @groupname
280 elif user == ug \
280 elif user == ug \
281 or ug.startswith('@') and user in _getusers(ui, ug[1:]):
281 or ug.startswith('@') and user in _getusers(ui, ug[1:]):
282 return True
282 return True
283
283
284 return False
284 return False
285
285
286 def buildmatch(ui, repo, user, key):
286 def buildmatch(ui, repo, user, key):
287 '''return tuple of (match function, list enabled).'''
287 '''return tuple of (match function, list enabled).'''
288 if not ui.has_section(key):
288 if not ui.has_section(key):
289 ui.debug('acl: %s not enabled\n' % key)
289 ui.debug('acl: %s not enabled\n' % key)
290 return None
290 return None
291
291
292 pats = [pat for pat, users in ui.configitems(key)
292 pats = [pat for pat, users in ui.configitems(key)
293 if _usermatch(ui, user, users)]
293 if _usermatch(ui, user, users)]
294 ui.debug('acl: %s enabled, %d entries for user %s\n' %
294 ui.debug('acl: %s enabled, %d entries for user %s\n' %
295 (key, len(pats), user))
295 (key, len(pats), user))
296
296
297 # Branch-based ACL
297 # Branch-based ACL
298 if not repo:
298 if not repo:
299 if pats:
299 if pats:
300 # If there's an asterisk (meaning "any branch"), always return True;
300 # If there's an asterisk (meaning "any branch"), always return True;
301 # Otherwise, test if b is in pats
301 # Otherwise, test if b is in pats
302 if '*' in pats:
302 if '*' in pats:
303 return util.always
303 return util.always
304 return lambda b: b in pats
304 return lambda b: b in pats
305 return util.never
305 return util.never
306
306
307 # Path-based ACL
307 # Path-based ACL
308 if pats:
308 if pats:
309 return match.match(repo.root, '', pats)
309 return match.match(repo.root, '', pats)
310 return util.never
310 return util.never
311
311
312 def ensureenabled(ui):
312 def ensureenabled(ui):
313 """make sure the extension is enabled when used as hook
313 """make sure the extension is enabled when used as hook
314
314
315 When acl is used through hooks, the extension is never formally loaded and
315 When acl is used through hooks, the extension is never formally loaded and
316 enabled. This has some side effect, for example the config declaration is
316 enabled. This has some side effect, for example the config declaration is
317 never loaded. This function ensure the extension is enabled when running
317 never loaded. This function ensure the extension is enabled when running
318 hooks.
318 hooks.
319 """
319 """
320 if 'acl' in ui._knownconfig:
320 if 'acl' in ui._knownconfig:
321 return
321 return
322 ui.setconfig('extensions', 'acl', '', source='internal')
322 ui.setconfig('extensions', 'acl', '', source='internal')
323 extensions.loadall(ui, ['acl'])
323 extensions.loadall(ui, ['acl'])
324
324
325 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
325 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
326
326
327 ensureenabled(ui)
327 ensureenabled(ui)
328
328
329 if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
329 if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
330 raise error.Abort(_('config error - hook type "%s" cannot stop '
330 raise error.Abort(_('config error - hook type "%s" cannot stop '
331 'incoming changesets nor commits') % hooktype)
331 'incoming changesets nor commits') % hooktype)
332 if (hooktype == 'pretxnchangegroup' and
332 if (hooktype == 'pretxnchangegroup' and
333 source not in ui.configlist('acl', 'sources')):
333 source not in ui.configlist('acl', 'sources')):
334 ui.debug('acl: changes have source "%s" - skipping\n' % source)
334 ui.debug('acl: changes have source "%s" - skipping\n' % source)
335 return
335 return
336
336
337 user = None
337 user = None
338 if source == 'serve' and 'url' in kwargs:
338 if source == 'serve' and r'url' in kwargs:
339 url = kwargs['url'].split(':')
339 url = kwargs[r'url'].split(':')
340 if url[0] == 'remote' and url[1].startswith('http'):
340 if url[0] == 'remote' and url[1].startswith('http'):
341 user = urlreq.unquote(url[3])
341 user = urlreq.unquote(url[3])
342
342
343 if user is None:
343 if user is None:
344 user = pycompat.bytestr(getpass.getuser())
344 user = pycompat.bytestr(getpass.getuser())
345
345
346 ui.debug('acl: checking access for user "%s"\n' % user)
346 ui.debug('acl: checking access for user "%s"\n' % user)
347
347
348 # deprecated config: acl.config
348 # deprecated config: acl.config
349 cfg = ui.config('acl', 'config')
349 cfg = ui.config('acl', 'config')
350 if cfg:
350 if cfg:
351 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
351 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
352 'acl.deny.branches', 'acl.allow', 'acl.deny'])
352 'acl.deny.branches', 'acl.allow', 'acl.deny'])
353
353
354 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
354 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
355 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
355 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
356 allow = buildmatch(ui, repo, user, 'acl.allow')
356 allow = buildmatch(ui, repo, user, 'acl.allow')
357 deny = buildmatch(ui, repo, user, 'acl.deny')
357 deny = buildmatch(ui, repo, user, 'acl.deny')
358
358
359 for rev in xrange(repo[node], len(repo)):
359 for rev in xrange(repo[node], len(repo)):
360 ctx = repo[rev]
360 ctx = repo[rev]
361 branch = ctx.branch()
361 branch = ctx.branch()
362 if denybranches and denybranches(branch):
362 if denybranches and denybranches(branch):
363 raise error.Abort(_('acl: user "%s" denied on branch "%s"'
363 raise error.Abort(_('acl: user "%s" denied on branch "%s"'
364 ' (changeset "%s")')
364 ' (changeset "%s")')
365 % (user, branch, ctx))
365 % (user, branch, ctx))
366 if allowbranches and not allowbranches(branch):
366 if allowbranches and not allowbranches(branch):
367 raise error.Abort(_('acl: user "%s" not allowed on branch "%s"'
367 raise error.Abort(_('acl: user "%s" not allowed on branch "%s"'
368 ' (changeset "%s")')
368 ' (changeset "%s")')
369 % (user, branch, ctx))
369 % (user, branch, ctx))
370 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
370 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
371 % (ctx, branch))
371 % (ctx, branch))
372
372
373 for f in ctx.files():
373 for f in ctx.files():
374 if deny and deny(f):
374 if deny and deny(f):
375 raise error.Abort(_('acl: user "%s" denied on "%s"'
375 raise error.Abort(_('acl: user "%s" denied on "%s"'
376 ' (changeset "%s")') % (user, f, ctx))
376 ' (changeset "%s")') % (user, f, ctx))
377 if allow and not allow(f):
377 if allow and not allow(f):
378 raise error.Abort(_('acl: user "%s" not allowed on "%s"'
378 raise error.Abort(_('acl: user "%s" not allowed on "%s"'
379 ' (changeset "%s")') % (user, f, ctx))
379 ' (changeset "%s")') % (user, f, ctx))
380 ui.debug('acl: path access granted: "%s"\n' % ctx)
380 ui.debug('acl: path access granted: "%s"\n' % ctx)
General Comments 0
You need to be logged in to leave comments. Login now