##// END OF EJS Templates
hgweb: fix allow_read permissions when a user is specified...
Benoit Boissinot -
r7831:b5ed0ab8 default
parent child Browse files
Show More
@@ -1,313 +1,313 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os, mimetypes
10 10 from mercurial.node import hex, nullid
11 11 from mercurial import ui, hg, util, hook, error
12 12 from mercurial import templater, templatefilters
13 13 from common import get_mtime, style_map, ErrorResponse
14 14 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 15 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
16 16 from request import wsgirequest
17 17 import webcommands, protocol, webutil
18 18
19 19 perms = {
20 20 'changegroup': 'pull',
21 21 'changegroupsubset': 'pull',
22 22 'unbundle': 'push',
23 23 'stream_out': 'pull',
24 24 }
25 25
26 26 class hgweb(object):
27 27 def __init__(self, repo, name=None):
28 28 if isinstance(repo, str):
29 29 parentui = ui.ui(report_untrusted=False, interactive=False)
30 30 self.repo = hg.repository(parentui, repo)
31 31 else:
32 32 self.repo = repo
33 33
34 34 hook.redirect(True)
35 35 self.mtime = -1
36 36 self.reponame = name
37 37 self.archives = 'zip', 'gz', 'bz2'
38 38 self.stripecount = 1
39 39 # a repo owner may set web.templates in .hg/hgrc to get any file
40 40 # readable by the user running the CGI script
41 41 self.templatepath = self.config("web", "templates",
42 42 templater.templatepath(),
43 43 untrusted=False)
44 44
45 45 # The CGI scripts are often run by a user different from the repo owner.
46 46 # Trust the settings from the .hg/hgrc files by default.
47 47 def config(self, section, name, default=None, untrusted=True):
48 48 return self.repo.ui.config(section, name, default,
49 49 untrusted=untrusted)
50 50
51 51 def configbool(self, section, name, default=False, untrusted=True):
52 52 return self.repo.ui.configbool(section, name, default,
53 53 untrusted=untrusted)
54 54
55 55 def configlist(self, section, name, default=None, untrusted=True):
56 56 return self.repo.ui.configlist(section, name, default,
57 57 untrusted=untrusted)
58 58
59 59 def refresh(self):
60 60 mtime = get_mtime(self.repo.root)
61 61 if mtime != self.mtime:
62 62 self.mtime = mtime
63 63 self.repo = hg.repository(self.repo.ui, self.repo.root)
64 64 self.maxchanges = int(self.config("web", "maxchanges", 10))
65 65 self.stripecount = int(self.config("web", "stripes", 1))
66 66 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
67 67 self.maxfiles = int(self.config("web", "maxfiles", 10))
68 68 self.allowpull = self.configbool("web", "allowpull", True)
69 69 self.encoding = self.config("web", "encoding", util._encoding)
70 70
71 71 def run(self):
72 72 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
73 73 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
74 74 import mercurial.hgweb.wsgicgi as wsgicgi
75 75 wsgicgi.launch(self)
76 76
77 77 def __call__(self, env, respond):
78 78 req = wsgirequest(env, respond)
79 79 return self.run_wsgi(req)
80 80
81 81 def run_wsgi(self, req):
82 82
83 83 self.refresh()
84 84
85 85 # process this if it's a protocol request
86 86 # protocol bits don't need to create any URLs
87 87 # and the clients always use the old URL structure
88 88
89 89 cmd = req.form.get('cmd', [''])[0]
90 90 if cmd and cmd in protocol.__all__:
91 91 try:
92 92 if cmd in perms:
93 93 try:
94 94 self.check_perm(req, perms[cmd])
95 95 except ErrorResponse, inst:
96 96 if cmd == 'unbundle':
97 97 req.drain()
98 98 raise
99 99 method = getattr(protocol, cmd)
100 100 return method(self.repo, req)
101 101 except ErrorResponse, inst:
102 102 req.respond(inst, protocol.HGTYPE)
103 103 if not inst.message:
104 104 return []
105 105 return '0\n%s\n' % inst.message,
106 106
107 107 # work with CGI variables to create coherent structure
108 108 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
109 109
110 110 req.url = req.env['SCRIPT_NAME']
111 111 if not req.url.endswith('/'):
112 112 req.url += '/'
113 113 if 'REPO_NAME' in req.env:
114 114 req.url += req.env['REPO_NAME'] + '/'
115 115
116 116 if 'PATH_INFO' in req.env:
117 117 parts = req.env['PATH_INFO'].strip('/').split('/')
118 118 repo_parts = req.env.get('REPO_NAME', '').split('/')
119 119 if parts[:len(repo_parts)] == repo_parts:
120 120 parts = parts[len(repo_parts):]
121 121 query = '/'.join(parts)
122 122 else:
123 123 query = req.env['QUERY_STRING'].split('&', 1)[0]
124 124 query = query.split(';', 1)[0]
125 125
126 126 # translate user-visible url structure to internal structure
127 127
128 128 args = query.split('/', 2)
129 129 if 'cmd' not in req.form and args and args[0]:
130 130
131 131 cmd = args.pop(0)
132 132 style = cmd.rfind('-')
133 133 if style != -1:
134 134 req.form['style'] = [cmd[:style]]
135 135 cmd = cmd[style+1:]
136 136
137 137 # avoid accepting e.g. style parameter as command
138 138 if hasattr(webcommands, cmd):
139 139 req.form['cmd'] = [cmd]
140 140 else:
141 141 cmd = ''
142 142
143 143 if cmd == 'static':
144 144 req.form['file'] = ['/'.join(args)]
145 145 else:
146 146 if args and args[0]:
147 147 node = args.pop(0)
148 148 req.form['node'] = [node]
149 149 if args:
150 150 req.form['file'] = args
151 151
152 152 if cmd == 'archive':
153 153 fn = req.form['node'][0]
154 154 for type_, spec in self.archive_specs.iteritems():
155 155 ext = spec[2]
156 156 if fn.endswith(ext):
157 157 req.form['node'] = [fn[:-len(ext)]]
158 158 req.form['type'] = [type_]
159 159
160 160 # process the web interface request
161 161
162 162 try:
163 163 tmpl = self.templater(req)
164 164 ctype = tmpl('mimetype', encoding=self.encoding)
165 165 ctype = templater.stringify(ctype)
166 166
167 167 # check read permissions non-static content
168 168 if cmd != 'static':
169 169 self.check_perm(req, None)
170 170
171 171 if cmd == '':
172 172 req.form['cmd'] = [tmpl.cache['default']]
173 173 cmd = req.form['cmd'][0]
174 174
175 175 if cmd not in webcommands.__all__:
176 176 msg = 'no such method: %s' % cmd
177 177 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
178 178 elif cmd == 'file' and 'raw' in req.form.get('style', []):
179 179 self.ctype = ctype
180 180 content = webcommands.rawfile(self, req, tmpl)
181 181 else:
182 182 content = getattr(webcommands, cmd)(self, req, tmpl)
183 183 req.respond(HTTP_OK, ctype)
184 184
185 185 return content
186 186
187 187 except error.LookupError, err:
188 188 req.respond(HTTP_NOT_FOUND, ctype)
189 189 msg = str(err)
190 190 if 'manifest' not in msg:
191 191 msg = 'revision not found: %s' % err.name
192 192 return tmpl('error', error=msg)
193 193 except (error.RepoError, error.RevlogError), inst:
194 194 req.respond(HTTP_SERVER_ERROR, ctype)
195 195 return tmpl('error', error=str(inst))
196 196 except ErrorResponse, inst:
197 197 req.respond(inst, ctype)
198 198 return tmpl('error', error=inst.message)
199 199
200 200 def templater(self, req):
201 201
202 202 # determine scheme, port and server name
203 203 # this is needed to create absolute urls
204 204
205 205 proto = req.env.get('wsgi.url_scheme')
206 206 if proto == 'https':
207 207 proto = 'https'
208 208 default_port = "443"
209 209 else:
210 210 proto = 'http'
211 211 default_port = "80"
212 212
213 213 port = req.env["SERVER_PORT"]
214 214 port = port != default_port and (":" + port) or ""
215 215 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
216 216 staticurl = self.config("web", "staticurl") or req.url + 'static/'
217 217 if not staticurl.endswith('/'):
218 218 staticurl += '/'
219 219
220 220 # some functions for the templater
221 221
222 222 def header(**map):
223 223 yield tmpl('header', encoding=self.encoding, **map)
224 224
225 225 def footer(**map):
226 226 yield tmpl("footer", **map)
227 227
228 228 def motd(**map):
229 229 yield self.config("web", "motd", "")
230 230
231 231 # figure out which style to use
232 232
233 233 vars = {}
234 234 style = self.config("web", "style", "paper")
235 235 if 'style' in req.form:
236 236 style = req.form['style'][0]
237 237 vars['style'] = style
238 238
239 239 start = req.url[-1] == '?' and '&' or '?'
240 240 sessionvars = webutil.sessionvars(vars, start)
241 241 mapfile = style_map(self.templatepath, style)
242 242
243 243 if not self.reponame:
244 244 self.reponame = (self.config("web", "name")
245 245 or req.env.get('REPO_NAME')
246 246 or req.url.strip('/') or self.repo.root)
247 247
248 248 # create the templater
249 249
250 250 tmpl = templater.templater(mapfile, templatefilters.filters,
251 251 defaults={"url": req.url,
252 252 "staticurl": staticurl,
253 253 "urlbase": urlbase,
254 254 "repo": self.reponame,
255 255 "header": header,
256 256 "footer": footer,
257 257 "motd": motd,
258 258 "sessionvars": sessionvars
259 259 })
260 260 return tmpl
261 261
262 262 def archivelist(self, nodeid):
263 263 allowed = self.configlist("web", "allow_archive")
264 264 for i, spec in self.archive_specs.iteritems():
265 265 if i in allowed or self.configbool("web", "allow" + i):
266 266 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
267 267
268 268 archive_specs = {
269 269 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
270 270 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
271 271 'zip': ('application/zip', 'zip', '.zip', None),
272 272 }
273 273
274 274 def check_perm(self, req, op):
275 275 '''Check permission for operation based on request data (including
276 276 authentication info). Return if op allowed, else raise an ErrorResponse
277 277 exception.'''
278 278
279 279 user = req.env.get('REMOTE_USER')
280 280
281 281 deny_read = self.configlist('web', 'deny_read')
282 282 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
283 283 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
284 284
285 285 allow_read = self.configlist('web', 'allow_read')
286 286 result = (not allow_read) or (allow_read == ['*'])
287 if not result or user in allow_read:
287 if not (result or user in allow_read):
288 288 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
289 289
290 290 if op == 'pull' and not self.allowpull:
291 291 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
292 292 elif op == 'pull' or op is None: # op is None for interface requests
293 293 return
294 294
295 295 # enforce that you can only push using POST requests
296 296 if req.env['REQUEST_METHOD'] != 'POST':
297 297 msg = 'push requires POST request'
298 298 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
299 299
300 300 # require ssl by default for pushing, auth info cannot be sniffed
301 301 # and replayed
302 302 scheme = req.env.get('wsgi.url_scheme')
303 303 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
304 304 raise ErrorResponse(HTTP_OK, 'ssl required')
305 305
306 306 deny = self.configlist('web', 'deny_push')
307 307 if deny and (not user or deny == ['*'] or user in deny):
308 308 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
309 309
310 310 allow = self.configlist('web', 'allow_push')
311 311 result = allow and (allow == ['*'] or user in allow)
312 312 if not result:
313 313 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
General Comments 0
You need to be logged in to leave comments. Login now