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