##// END OF EJS Templates
acl: support for getting authenticated user from web server (issue298)...
Henrik Stuart -
r8846:b3077538 default
parent child Browse files
Show More
@@ -1,91 +1,99 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, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7 #
7 #
8 # this hook allows to allow or deny access to parts of a repo when
8 # this hook allows to allow or deny access to parts of a repo when
9 # taking incoming changesets.
9 # taking incoming changesets.
10 #
10 #
11 # authorization is against local user name on system where hook is
11 # authorization is against local user name on system where hook is
12 # run, not committer of original changeset (since that is easy to
12 # run, not committer of original changeset (since that is easy to
13 # spoof).
13 # spoof).
14 #
14 #
15 # acl hook is best to use if you use hgsh to set up restricted shells
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
16 # for authenticated users to only push to / pull from. not safe if
17 # user has interactive shell access, because they can disable hook.
17 # user has interactive shell access, because they can disable hook.
18 # also not safe if remote users share one local account, because then
18 # also not safe if remote users share one local account, because then
19 # no way to tell remote users apart.
19 # no way to tell remote users apart.
20 #
20 #
21 # to use, configure acl extension in hgrc like this:
21 # to use, configure acl extension in hgrc like this:
22 #
22 #
23 # [extensions]
23 # [extensions]
24 # hgext.acl =
24 # hgext.acl =
25 #
25 #
26 # [hooks]
26 # [hooks]
27 # pretxnchangegroup.acl = python:hgext.acl.hook
27 # pretxnchangegroup.acl = python:hgext.acl.hook
28 #
28 #
29 # [acl]
29 # [acl]
30 # sources = serve # check if source of incoming changes in this list
30 # sources = serve # check if source of incoming changes in this list
31 # # ("serve" == ssh or http, "push", "pull", "bundle")
31 # # ("serve" == ssh or http, "push", "pull", "bundle")
32 #
32 #
33 # allow and deny lists have subtree pattern (default syntax is glob)
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.
34 # on left, user names on right. deny list checked before allow list.
35 #
35 #
36 # [acl.allow]
36 # [acl.allow]
37 # # if acl.allow not present, all users allowed by default
37 # # if acl.allow not present, all users allowed by default
38 # # empty acl.allow = no users allowed
38 # # empty acl.allow = no users allowed
39 # docs/** = doc_writer
39 # docs/** = doc_writer
40 # .hgtags = release_engineer
40 # .hgtags = release_engineer
41 #
41 #
42 # [acl.deny]
42 # [acl.deny]
43 # # if acl.deny not present, no users denied by default
43 # # if acl.deny not present, no users denied by default
44 # # empty acl.deny = all users allowed
44 # # empty acl.deny = all users allowed
45 # glob pattern = user4, user5
45 # glob pattern = user4, user5
46 # ** = user6
46 # ** = user6
47
47
48 from mercurial.i18n import _
48 from mercurial.i18n import _
49 from mercurial import util, match
49 from mercurial import util, match
50 import getpass
50 import getpass, urllib
51
51
52 def buildmatch(ui, repo, user, key):
52 def buildmatch(ui, repo, user, key):
53 '''return tuple of (match function, list enabled).'''
53 '''return tuple of (match function, list enabled).'''
54 if not ui.has_section(key):
54 if not ui.has_section(key):
55 ui.debug(_('acl: %s not enabled\n') % key)
55 ui.debug(_('acl: %s not enabled\n') % key)
56 return None
56 return None
57
57
58 pats = [pat for pat, users in ui.configitems(key)
58 pats = [pat for pat, users in ui.configitems(key)
59 if user in users.replace(',', ' ').split()]
59 if user in users.replace(',', ' ').split()]
60 ui.debug(_('acl: %s enabled, %d entries for user %s\n') %
60 ui.debug(_('acl: %s enabled, %d entries for user %s\n') %
61 (key, len(pats), user))
61 (key, len(pats), user))
62 if pats:
62 if pats:
63 return match.match(repo.root, '', pats)
63 return match.match(repo.root, '', pats)
64 return match.exact(repo.root, '', [])
64 return match.exact(repo.root, '', [])
65
65
66
66
67 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
67 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
68 if hooktype != 'pretxnchangegroup':
68 if hooktype != 'pretxnchangegroup':
69 raise util.Abort(_('config error - hook type "%s" cannot stop '
69 raise util.Abort(_('config error - hook type "%s" cannot stop '
70 'incoming changesets') % hooktype)
70 'incoming changesets') % hooktype)
71 if source not in ui.config('acl', 'sources', 'serve').split():
71 if source not in ui.config('acl', 'sources', 'serve').split():
72 ui.debug(_('acl: changes have source "%s" - skipping\n') % source)
72 ui.debug(_('acl: changes have source "%s" - skipping\n') % source)
73 return
73 return
74
74
75 user = getpass.getuser()
75 user = None
76 if source == 'serve' and 'url' in kwargs:
77 url = kwargs['url'].split(':')
78 if url[0] == 'remote' and url[1].startswith('http'):
79 user = urllib.unquote(url[2])
80
81 if user is None:
82 user = getpass.getuser()
83
76 cfg = ui.config('acl', 'config')
84 cfg = ui.config('acl', 'config')
77 if cfg:
85 if cfg:
78 ui.readconfig(cfg, sections = ['acl.allow', 'acl.deny'])
86 ui.readconfig(cfg, sections = ['acl.allow', 'acl.deny'])
79 allow = buildmatch(ui, repo, user, 'acl.allow')
87 allow = buildmatch(ui, repo, user, 'acl.allow')
80 deny = buildmatch(ui, repo, user, 'acl.deny')
88 deny = buildmatch(ui, repo, user, 'acl.deny')
81
89
82 for rev in xrange(repo[node], len(repo)):
90 for rev in xrange(repo[node], len(repo)):
83 ctx = repo[rev]
91 ctx = repo[rev]
84 for f in ctx.files():
92 for f in ctx.files():
85 if deny and deny(f):
93 if deny and deny(f):
86 ui.debug(_('acl: user %s denied on %s\n') % (user, f))
94 ui.debug(_('acl: user %s denied on %s\n') % (user, f))
87 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
95 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
88 if allow and not allow(f):
96 if allow and not allow(f):
89 ui.debug(_('acl: user %s not allowed on %s\n') % (user, f))
97 ui.debug(_('acl: user %s not allowed on %s\n') % (user, f))
90 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
98 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
91 ui.debug(_('acl: allowing changeset %s\n') % ctx)
99 ui.debug(_('acl: allowing changeset %s\n') % ctx)
@@ -1,205 +1,206 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 import cStringIO, zlib, tempfile, errno, os, sys, urllib
8 import cStringIO, zlib, tempfile, errno, os, sys, urllib
9 from mercurial import util, streamclone
9 from mercurial import util, streamclone
10 from mercurial.node import bin, hex
10 from mercurial.node import bin, hex
11 from mercurial import changegroup as changegroupmod
11 from mercurial import changegroup as changegroupmod
12 from common import ErrorResponse, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
12 from common import ErrorResponse, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13
13
14 # __all__ is populated with the allowed commands. Be sure to add to it if
14 # __all__ is populated with the allowed commands. Be sure to add to it if
15 # you're adding a new command, or the new command won't work.
15 # you're adding a new command, or the new command won't work.
16
16
17 __all__ = [
17 __all__ = [
18 'lookup', 'heads', 'branches', 'between', 'changegroup',
18 'lookup', 'heads', 'branches', 'between', 'changegroup',
19 'changegroupsubset', 'capabilities', 'unbundle', 'stream_out',
19 'changegroupsubset', 'capabilities', 'unbundle', 'stream_out',
20 'branchmap',
20 'branchmap',
21 ]
21 ]
22
22
23 HGTYPE = 'application/mercurial-0.1'
23 HGTYPE = 'application/mercurial-0.1'
24
24
25 def lookup(repo, req):
25 def lookup(repo, req):
26 try:
26 try:
27 r = hex(repo.lookup(req.form['key'][0]))
27 r = hex(repo.lookup(req.form['key'][0]))
28 success = 1
28 success = 1
29 except Exception,inst:
29 except Exception,inst:
30 r = str(inst)
30 r = str(inst)
31 success = 0
31 success = 0
32 resp = "%s %s\n" % (success, r)
32 resp = "%s %s\n" % (success, r)
33 req.respond(HTTP_OK, HGTYPE, length=len(resp))
33 req.respond(HTTP_OK, HGTYPE, length=len(resp))
34 yield resp
34 yield resp
35
35
36 def heads(repo, req):
36 def heads(repo, req):
37 resp = " ".join(map(hex, repo.heads())) + "\n"
37 resp = " ".join(map(hex, repo.heads())) + "\n"
38 req.respond(HTTP_OK, HGTYPE, length=len(resp))
38 req.respond(HTTP_OK, HGTYPE, length=len(resp))
39 yield resp
39 yield resp
40
40
41 def branchmap(repo, req):
41 def branchmap(repo, req):
42 branches = repo.branchmap()
42 branches = repo.branchmap()
43 heads = []
43 heads = []
44 for branch, nodes in branches.iteritems():
44 for branch, nodes in branches.iteritems():
45 branchname = urllib.quote(branch)
45 branchname = urllib.quote(branch)
46 branchnodes = [hex(node) for node in nodes]
46 branchnodes = [hex(node) for node in nodes]
47 heads.append('%s %s' % (branchname, ' '.join(branchnodes)))
47 heads.append('%s %s' % (branchname, ' '.join(branchnodes)))
48 resp = '\n'.join(heads)
48 resp = '\n'.join(heads)
49 req.respond(HTTP_OK, HGTYPE, length=len(resp))
49 req.respond(HTTP_OK, HGTYPE, length=len(resp))
50 yield resp
50 yield resp
51
51
52 def branches(repo, req):
52 def branches(repo, req):
53 nodes = []
53 nodes = []
54 if 'nodes' in req.form:
54 if 'nodes' in req.form:
55 nodes = map(bin, req.form['nodes'][0].split(" "))
55 nodes = map(bin, req.form['nodes'][0].split(" "))
56 resp = cStringIO.StringIO()
56 resp = cStringIO.StringIO()
57 for b in repo.branches(nodes):
57 for b in repo.branches(nodes):
58 resp.write(" ".join(map(hex, b)) + "\n")
58 resp.write(" ".join(map(hex, b)) + "\n")
59 resp = resp.getvalue()
59 resp = resp.getvalue()
60 req.respond(HTTP_OK, HGTYPE, length=len(resp))
60 req.respond(HTTP_OK, HGTYPE, length=len(resp))
61 yield resp
61 yield resp
62
62
63 def between(repo, req):
63 def between(repo, req):
64 if 'pairs' in req.form:
64 if 'pairs' in req.form:
65 pairs = [map(bin, p.split("-"))
65 pairs = [map(bin, p.split("-"))
66 for p in req.form['pairs'][0].split(" ")]
66 for p in req.form['pairs'][0].split(" ")]
67 resp = cStringIO.StringIO()
67 resp = cStringIO.StringIO()
68 for b in repo.between(pairs):
68 for b in repo.between(pairs):
69 resp.write(" ".join(map(hex, b)) + "\n")
69 resp.write(" ".join(map(hex, b)) + "\n")
70 resp = resp.getvalue()
70 resp = resp.getvalue()
71 req.respond(HTTP_OK, HGTYPE, length=len(resp))
71 req.respond(HTTP_OK, HGTYPE, length=len(resp))
72 yield resp
72 yield resp
73
73
74 def changegroup(repo, req):
74 def changegroup(repo, req):
75 req.respond(HTTP_OK, HGTYPE)
75 req.respond(HTTP_OK, HGTYPE)
76 nodes = []
76 nodes = []
77
77
78 if 'roots' in req.form:
78 if 'roots' in req.form:
79 nodes = map(bin, req.form['roots'][0].split(" "))
79 nodes = map(bin, req.form['roots'][0].split(" "))
80
80
81 z = zlib.compressobj()
81 z = zlib.compressobj()
82 f = repo.changegroup(nodes, 'serve')
82 f = repo.changegroup(nodes, 'serve')
83 while 1:
83 while 1:
84 chunk = f.read(4096)
84 chunk = f.read(4096)
85 if not chunk:
85 if not chunk:
86 break
86 break
87 yield z.compress(chunk)
87 yield z.compress(chunk)
88
88
89 yield z.flush()
89 yield z.flush()
90
90
91 def changegroupsubset(repo, req):
91 def changegroupsubset(repo, req):
92 req.respond(HTTP_OK, HGTYPE)
92 req.respond(HTTP_OK, HGTYPE)
93 bases = []
93 bases = []
94 heads = []
94 heads = []
95
95
96 if 'bases' in req.form:
96 if 'bases' in req.form:
97 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
97 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
98 if 'heads' in req.form:
98 if 'heads' in req.form:
99 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
99 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
100
100
101 z = zlib.compressobj()
101 z = zlib.compressobj()
102 f = repo.changegroupsubset(bases, heads, 'serve')
102 f = repo.changegroupsubset(bases, heads, 'serve')
103 while 1:
103 while 1:
104 chunk = f.read(4096)
104 chunk = f.read(4096)
105 if not chunk:
105 if not chunk:
106 break
106 break
107 yield z.compress(chunk)
107 yield z.compress(chunk)
108
108
109 yield z.flush()
109 yield z.flush()
110
110
111 def capabilities(repo, req):
111 def capabilities(repo, req):
112 caps = ['lookup', 'changegroupsubset', 'branchmap']
112 caps = ['lookup', 'changegroupsubset', 'branchmap']
113 if repo.ui.configbool('server', 'uncompressed', untrusted=True):
113 if repo.ui.configbool('server', 'uncompressed', untrusted=True):
114 caps.append('stream=%d' % repo.changelog.version)
114 caps.append('stream=%d' % repo.changelog.version)
115 if changegroupmod.bundlepriority:
115 if changegroupmod.bundlepriority:
116 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
116 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
117 rsp = ' '.join(caps)
117 rsp = ' '.join(caps)
118 req.respond(HTTP_OK, HGTYPE, length=len(rsp))
118 req.respond(HTTP_OK, HGTYPE, length=len(rsp))
119 yield rsp
119 yield rsp
120
120
121 def unbundle(repo, req):
121 def unbundle(repo, req):
122
122
123 proto = req.env.get('wsgi.url_scheme') or 'http'
123 proto = req.env.get('wsgi.url_scheme') or 'http'
124 their_heads = req.form['heads'][0].split(' ')
124 their_heads = req.form['heads'][0].split(' ')
125
125
126 def check_heads():
126 def check_heads():
127 heads = map(hex, repo.heads())
127 heads = map(hex, repo.heads())
128 return their_heads == [hex('force')] or their_heads == heads
128 return their_heads == [hex('force')] or their_heads == heads
129
129
130 # fail early if possible
130 # fail early if possible
131 if not check_heads():
131 if not check_heads():
132 req.drain()
132 req.drain()
133 raise ErrorResponse(HTTP_OK, 'unsynced changes')
133 raise ErrorResponse(HTTP_OK, 'unsynced changes')
134
134
135 # do not lock repo until all changegroup data is
135 # do not lock repo until all changegroup data is
136 # streamed. save to temporary file.
136 # streamed. save to temporary file.
137
137
138 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
138 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
139 fp = os.fdopen(fd, 'wb+')
139 fp = os.fdopen(fd, 'wb+')
140 try:
140 try:
141 length = int(req.env['CONTENT_LENGTH'])
141 length = int(req.env['CONTENT_LENGTH'])
142 for s in util.filechunkiter(req, limit=length):
142 for s in util.filechunkiter(req, limit=length):
143 fp.write(s)
143 fp.write(s)
144
144
145 try:
145 try:
146 lock = repo.lock()
146 lock = repo.lock()
147 try:
147 try:
148 if not check_heads():
148 if not check_heads():
149 raise ErrorResponse(HTTP_OK, 'unsynced changes')
149 raise ErrorResponse(HTTP_OK, 'unsynced changes')
150
150
151 fp.seek(0)
151 fp.seek(0)
152 header = fp.read(6)
152 header = fp.read(6)
153 if header.startswith('HG') and not header.startswith('HG10'):
153 if header.startswith('HG') and not header.startswith('HG10'):
154 raise ValueError('unknown bundle version')
154 raise ValueError('unknown bundle version')
155 elif header not in changegroupmod.bundletypes:
155 elif header not in changegroupmod.bundletypes:
156 raise ValueError('unknown bundle compression type')
156 raise ValueError('unknown bundle compression type')
157 gen = changegroupmod.unbundle(header, fp)
157 gen = changegroupmod.unbundle(header, fp)
158
158
159 # send addchangegroup output to client
159 # send addchangegroup output to client
160
160
161 oldio = sys.stdout, sys.stderr
161 oldio = sys.stdout, sys.stderr
162 sys.stderr = sys.stdout = cStringIO.StringIO()
162 sys.stderr = sys.stdout = cStringIO.StringIO()
163
163
164 try:
164 try:
165 url = 'remote:%s:%s' % (proto,
165 url = 'remote:%s:%s:%s' % (
166 urllib.quote(
166 proto,
167 req.env.get('REMOTE_HOST', '')))
167 urllib.quote(req.env.get('REMOTE_HOST', '')),
168 urllib.quote(req.env.get('REMOTE_USER', '')))
168 try:
169 try:
169 ret = repo.addchangegroup(gen, 'serve', url)
170 ret = repo.addchangegroup(gen, 'serve', url)
170 except util.Abort, inst:
171 except util.Abort, inst:
171 sys.stdout.write("abort: %s\n" % inst)
172 sys.stdout.write("abort: %s\n" % inst)
172 ret = 0
173 ret = 0
173 finally:
174 finally:
174 val = sys.stdout.getvalue()
175 val = sys.stdout.getvalue()
175 sys.stdout, sys.stderr = oldio
176 sys.stdout, sys.stderr = oldio
176 req.respond(HTTP_OK, HGTYPE)
177 req.respond(HTTP_OK, HGTYPE)
177 return '%d\n%s' % (ret, val),
178 return '%d\n%s' % (ret, val),
178 finally:
179 finally:
179 lock.release()
180 lock.release()
180 except ValueError, inst:
181 except ValueError, inst:
181 raise ErrorResponse(HTTP_OK, inst)
182 raise ErrorResponse(HTTP_OK, inst)
182 except (OSError, IOError), inst:
183 except (OSError, IOError), inst:
183 filename = getattr(inst, 'filename', '')
184 filename = getattr(inst, 'filename', '')
184 # Don't send our filesystem layout to the client
185 # Don't send our filesystem layout to the client
185 if filename.startswith(repo.root):
186 if filename.startswith(repo.root):
186 filename = filename[len(repo.root)+1:]
187 filename = filename[len(repo.root)+1:]
187 else:
188 else:
188 filename = ''
189 filename = ''
189 error = getattr(inst, 'strerror', 'Unknown error')
190 error = getattr(inst, 'strerror', 'Unknown error')
190 if inst.errno == errno.ENOENT:
191 if inst.errno == errno.ENOENT:
191 code = HTTP_NOT_FOUND
192 code = HTTP_NOT_FOUND
192 else:
193 else:
193 code = HTTP_SERVER_ERROR
194 code = HTTP_SERVER_ERROR
194 raise ErrorResponse(code, '%s: %s' % (error, filename))
195 raise ErrorResponse(code, '%s: %s' % (error, filename))
195 finally:
196 finally:
196 fp.close()
197 fp.close()
197 os.unlink(tempname)
198 os.unlink(tempname)
198
199
199 def stream_out(repo, req):
200 def stream_out(repo, req):
200 req.respond(HTTP_OK, HGTYPE)
201 req.respond(HTTP_OK, HGTYPE)
201 try:
202 try:
202 for chunk in streamclone.stream_out(repo, untrusted=True):
203 for chunk in streamclone.stream_out(repo, untrusted=True):
203 yield chunk
204 yield chunk
204 except streamclone.StreamException, inst:
205 except streamclone.StreamException, inst:
205 yield str(inst)
206 yield str(inst)
General Comments 0
You need to be logged in to leave comments. Login now