##// END OF EJS Templates
let action always return pull command for better security on pull restricted repos
marcink -
r1128:62a1d415 beta
parent child Browse files
Show More
@@ -1,272 +1,272
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.middleware.simplehg
3 rhodecode.lib.middleware.simplehg
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 SimpleHG middleware for handling mercurial protocol request
6 SimpleHG middleware for handling mercurial protocol request
7 (push/clone etc.). It's implemented with basic auth function
7 (push/clone etc.). It's implemented with basic auth function
8
8
9 :created_on: Apr 28, 2010
9 :created_on: Apr 28, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software; you can redistribute it and/or
14 # This program is free software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; version 2
16 # as published by the Free Software Foundation; version 2
17 # of the License or (at your opinion) any later version of the license.
17 # of the License or (at your opinion) any later version of the license.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
27 # MA 02110-1301, USA.
27 # MA 02110-1301, USA.
28
28
29 import os
29 import os
30 import logging
30 import logging
31 import traceback
31 import traceback
32
32
33 from mercurial.error import RepoError
33 from mercurial.error import RepoError
34 from mercurial.hgweb import hgweb
34 from mercurial.hgweb import hgweb
35 from mercurial.hgweb.request import wsgiapplication
35 from mercurial.hgweb.request import wsgiapplication
36
36
37 from paste.auth.basic import AuthBasicAuthenticator
37 from paste.auth.basic import AuthBasicAuthenticator
38 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
38 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
39
39
40 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
40 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
41 from rhodecode.lib.utils import make_ui, invalidate_cache, \
41 from rhodecode.lib.utils import make_ui, invalidate_cache, \
42 check_repo_fast, ui_sections
42 check_repo_fast, ui_sections
43 from rhodecode.model.user import UserModel
43 from rhodecode.model.user import UserModel
44
44
45 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
45 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49 def is_mercurial(environ):
49 def is_mercurial(environ):
50 """Returns True if request's target is mercurial server - header
50 """Returns True if request's target is mercurial server - header
51 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
51 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
52 """
52 """
53 http_accept = environ.get('HTTP_ACCEPT')
53 http_accept = environ.get('HTTP_ACCEPT')
54 if http_accept and http_accept.startswith('application/mercurial'):
54 if http_accept and http_accept.startswith('application/mercurial'):
55 return True
55 return True
56 return False
56 return False
57
57
58 class SimpleHg(object):
58 class SimpleHg(object):
59
59
60 def __init__(self, application, config):
60 def __init__(self, application, config):
61 self.application = application
61 self.application = application
62 self.config = config
62 self.config = config
63 #authenticate this mercurial request using authfunc
63 #authenticate this mercurial request using authfunc
64 self.authenticate = AuthBasicAuthenticator('', authfunc)
64 self.authenticate = AuthBasicAuthenticator('', authfunc)
65 self.ipaddr = '0.0.0.0'
65 self.ipaddr = '0.0.0.0'
66 self.repo_name = None
66 self.repo_name = None
67 self.username = None
67 self.username = None
68 self.action = None
68 self.action = None
69
69
70 def __call__(self, environ, start_response):
70 def __call__(self, environ, start_response):
71 if not is_mercurial(environ):
71 if not is_mercurial(environ):
72 return self.application(environ, start_response)
72 return self.application(environ, start_response)
73
73
74 proxy_key = 'HTTP_X_REAL_IP'
74 proxy_key = 'HTTP_X_REAL_IP'
75 def_key = 'REMOTE_ADDR'
75 def_key = 'REMOTE_ADDR'
76 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
76 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
77 # skip passing error to error controller
77 # skip passing error to error controller
78 environ['pylons.status_code_redirect'] = True
78 environ['pylons.status_code_redirect'] = True
79
79
80 #======================================================================
80 #======================================================================
81 # GET ACTION PULL or PUSH
81 # GET ACTION PULL or PUSH
82 #======================================================================
82 #======================================================================
83 self.action = self.__get_action(environ)
83 self.action = self.__get_action(environ)
84 try:
84 try:
85 #==================================================================
85 #==================================================================
86 # GET REPOSITORY NAME
86 # GET REPOSITORY NAME
87 #==================================================================
87 #==================================================================
88 self.repo_name = self.__get_repository(environ)
88 self.repo_name = self.__get_repository(environ)
89 except:
89 except:
90 return HTTPInternalServerError()(environ, start_response)
90 return HTTPInternalServerError()(environ, start_response)
91
91
92 #======================================================================
92 #======================================================================
93 # CHECK ANONYMOUS PERMISSION
93 # CHECK ANONYMOUS PERMISSION
94 #======================================================================
94 #======================================================================
95 if self.action in ['pull', 'push']:
95 if self.action in ['pull', 'push']:
96 anonymous_user = self.__get_user('default')
96 anonymous_user = self.__get_user('default')
97 self.username = anonymous_user.username
97 self.username = anonymous_user.username
98 anonymous_perm = self.__check_permission(self.action, anonymous_user ,
98 anonymous_perm = self.__check_permission(self.action, anonymous_user ,
99 self.repo_name)
99 self.repo_name)
100
100
101 if anonymous_perm is not True or anonymous_user.active is False:
101 if anonymous_perm is not True or anonymous_user.active is False:
102 if anonymous_perm is not True:
102 if anonymous_perm is not True:
103 log.debug('Not enough credentials to access this repository'
103 log.debug('Not enough credentials to access this repository'
104 'as anonymous user')
104 'as anonymous user')
105 if anonymous_user.active is False:
105 if anonymous_user.active is False:
106 log.debug('Anonymous access is disabled, running '
106 log.debug('Anonymous access is disabled, running '
107 'authentication')
107 'authentication')
108 #==============================================================
108 #==============================================================
109 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
109 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
110 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
110 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
111 #==============================================================
111 #==============================================================
112
112
113 if not REMOTE_USER(environ):
113 if not REMOTE_USER(environ):
114 self.authenticate.realm = str(self.config['rhodecode_realm'])
114 self.authenticate.realm = str(self.config['rhodecode_realm'])
115 result = self.authenticate(environ)
115 result = self.authenticate(environ)
116 if isinstance(result, str):
116 if isinstance(result, str):
117 AUTH_TYPE.update(environ, 'basic')
117 AUTH_TYPE.update(environ, 'basic')
118 REMOTE_USER.update(environ, result)
118 REMOTE_USER.update(environ, result)
119 else:
119 else:
120 return result.wsgi_application(environ, start_response)
120 return result.wsgi_application(environ, start_response)
121
121
122
122
123 #==============================================================
123 #==============================================================
124 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
124 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
125 # BASIC AUTH
125 # BASIC AUTH
126 #==============================================================
126 #==============================================================
127
127
128 if self.action in ['pull', 'push']:
128 if self.action in ['pull', 'push']:
129 username = REMOTE_USER(environ)
129 username = REMOTE_USER(environ)
130 try:
130 try:
131 user = self.__get_user(username)
131 user = self.__get_user(username)
132 self.username = user.username
132 self.username = user.username
133 except:
133 except:
134 log.error(traceback.format_exc())
134 log.error(traceback.format_exc())
135 return HTTPInternalServerError()(environ, start_response)
135 return HTTPInternalServerError()(environ, start_response)
136
136
137 #check permissions for this repository
137 #check permissions for this repository
138 perm = self.__check_permission(self.action, user, self.repo_name)
138 perm = self.__check_permission(self.action, user, self.repo_name)
139 if perm is not True:
139 if perm is not True:
140 return HTTPForbidden()(environ, start_response)
140 return HTTPForbidden()(environ, start_response)
141
141
142 self.extras = {'ip':self.ipaddr,
142 self.extras = {'ip':self.ipaddr,
143 'username':self.username,
143 'username':self.username,
144 'action':self.action,
144 'action':self.action,
145 'repository':self.repo_name}
145 'repository':self.repo_name}
146
146
147 #===================================================================
147 #===================================================================
148 # MERCURIAL REQUEST HANDLING
148 # MERCURIAL REQUEST HANDLING
149 #===================================================================
149 #===================================================================
150 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
150 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
151 self.baseui = make_ui('db')
151 self.baseui = make_ui('db')
152 self.basepath = self.config['base_path']
152 self.basepath = self.config['base_path']
153 self.repo_path = os.path.join(self.basepath, self.repo_name)
153 self.repo_path = os.path.join(self.basepath, self.repo_name)
154
154
155 #quick check if that dir exists...
155 #quick check if that dir exists...
156 if check_repo_fast(self.repo_name, self.basepath):
156 if check_repo_fast(self.repo_name, self.basepath):
157 return HTTPNotFound()(environ, start_response)
157 return HTTPNotFound()(environ, start_response)
158 try:
158 try:
159 app = wsgiapplication(self.__make_app)
159 app = wsgiapplication(self.__make_app)
160 except RepoError, e:
160 except RepoError, e:
161 if str(e).find('not found') != -1:
161 if str(e).find('not found') != -1:
162 return HTTPNotFound()(environ, start_response)
162 return HTTPNotFound()(environ, start_response)
163 except Exception:
163 except Exception:
164 log.error(traceback.format_exc())
164 log.error(traceback.format_exc())
165 return HTTPInternalServerError()(environ, start_response)
165 return HTTPInternalServerError()(environ, start_response)
166
166
167 #invalidate cache on push
167 #invalidate cache on push
168 if self.action == 'push':
168 if self.action == 'push':
169 self.__invalidate_cache(self.repo_name)
169 self.__invalidate_cache(self.repo_name)
170
170
171 return app(environ, start_response)
171 return app(environ, start_response)
172
172
173
173
174 def __make_app(self):
174 def __make_app(self):
175 """Make an wsgi application using hgweb, and my generated baseui
175 """Make an wsgi application using hgweb, and my generated baseui
176 instance
176 instance
177 """
177 """
178
178
179 hgserve = hgweb(str(self.repo_path), baseui=self.baseui)
179 hgserve = hgweb(str(self.repo_path), baseui=self.baseui)
180 return self.__load_web_settings(hgserve, self.extras)
180 return self.__load_web_settings(hgserve, self.extras)
181
181
182
182
183 def __check_permission(self, action, user, repo_name):
183 def __check_permission(self, action, user, repo_name):
184 """Checks permissions using action (push/pull) user and repository
184 """Checks permissions using action (push/pull) user and repository
185 name
185 name
186
186
187 :param action: push or pull action
187 :param action: push or pull action
188 :param user: user instance
188 :param user: user instance
189 :param repo_name: repository name
189 :param repo_name: repository name
190 """
190 """
191 if action == 'push':
191 if action == 'push':
192 if not HasPermissionAnyMiddleware('repository.write',
192 if not HasPermissionAnyMiddleware('repository.write',
193 'repository.admin')\
193 'repository.admin')\
194 (user, repo_name):
194 (user, repo_name):
195 return False
195 return False
196
196
197 else:
197 else:
198 #any other action need at least read permission
198 #any other action need at least read permission
199 if not HasPermissionAnyMiddleware('repository.read',
199 if not HasPermissionAnyMiddleware('repository.read',
200 'repository.write',
200 'repository.write',
201 'repository.admin')\
201 'repository.admin')\
202 (user, repo_name):
202 (user, repo_name):
203 return False
203 return False
204
204
205 return True
205 return True
206
206
207
207
208 def __get_repository(self, environ):
208 def __get_repository(self, environ):
209 """Get's repository name out of PATH_INFO header
209 """Get's repository name out of PATH_INFO header
210
210
211 :param environ: environ where PATH_INFO is stored
211 :param environ: environ where PATH_INFO is stored
212 """
212 """
213 try:
213 try:
214 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
214 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
215 if repo_name.endswith('/'):
215 if repo_name.endswith('/'):
216 repo_name = repo_name.rstrip('/')
216 repo_name = repo_name.rstrip('/')
217 except:
217 except:
218 log.error(traceback.format_exc())
218 log.error(traceback.format_exc())
219 raise
219 raise
220
220
221 return repo_name
221 return repo_name
222
222
223 def __get_user(self, username):
223 def __get_user(self, username):
224 return UserModel().get_by_username(username, cache=True)
224 return UserModel().get_by_username(username, cache=True)
225
225
226 def __get_action(self, environ):
226 def __get_action(self, environ):
227 """Maps mercurial request commands into a clone,pull or push command.
227 """Maps mercurial request commands into a clone,pull or push command.
228 This should always return a valid command string
228 This should always return a valid command string
229
229
230 :param environ:
230 :param environ:
231 """
231 """
232 mapping = {'changegroup': 'pull',
232 mapping = {'changegroup': 'pull',
233 'changegroupsubset': 'pull',
233 'changegroupsubset': 'pull',
234 'stream_out': 'pull',
234 'stream_out': 'pull',
235 'listkeys': 'pull',
235 'listkeys': 'pull',
236 'unbundle': 'push',
236 'unbundle': 'push',
237 'pushkey': 'push', }
237 'pushkey': 'push', }
238 for qry in environ['QUERY_STRING'].split('&'):
238 for qry in environ['QUERY_STRING'].split('&'):
239 if qry.startswith('cmd'):
239 if qry.startswith('cmd'):
240 cmd = qry.split('=')[-1]
240 cmd = qry.split('=')[-1]
241 if mapping.has_key(cmd):
241 if mapping.has_key(cmd):
242 return mapping[cmd]
242 return mapping[cmd]
243 else:
243 else:
244 return cmd
244 return 'pull'
245
245
246 def __invalidate_cache(self, repo_name):
246 def __invalidate_cache(self, repo_name):
247 """we know that some change was made to repositories and we should
247 """we know that some change was made to repositories and we should
248 invalidate the cache to see the changes right away but only for
248 invalidate the cache to see the changes right away but only for
249 push requests"""
249 push requests"""
250 invalidate_cache('get_repo_cached_%s' % repo_name)
250 invalidate_cache('get_repo_cached_%s' % repo_name)
251
251
252
252
253 def __load_web_settings(self, hgserve, extras={}):
253 def __load_web_settings(self, hgserve, extras={}):
254 #set the global ui for hgserve instance passed
254 #set the global ui for hgserve instance passed
255 hgserve.repo.ui = self.baseui
255 hgserve.repo.ui = self.baseui
256
256
257 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
257 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
258
258
259 #inject some additional parameters that will be available in ui
259 #inject some additional parameters that will be available in ui
260 #for hooks
260 #for hooks
261 for k, v in extras.items():
261 for k, v in extras.items():
262 hgserve.repo.ui.setconfig('rhodecode_extras', k, v)
262 hgserve.repo.ui.setconfig('rhodecode_extras', k, v)
263
263
264 repoui = make_ui('file', hgrc, False)
264 repoui = make_ui('file', hgrc, False)
265
265
266 if repoui:
266 if repoui:
267 #overwrite our ui instance with the section from hgrc file
267 #overwrite our ui instance with the section from hgrc file
268 for section in ui_sections:
268 for section in ui_sections:
269 for k, v in repoui.configitems(section):
269 for k, v in repoui.configitems(section):
270 hgserve.repo.ui.setconfig(section, k, v)
270 hgserve.repo.ui.setconfig(section, k, v)
271
271
272 return hgserve
272 return hgserve
General Comments 0
You need to be logged in to leave comments. Login now