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