comparison rhodecode/lib/middleware/simplehg.py @ 1512:bf263968da47

merge beta in stable branch
author Marcin Kuzminski <marcin@python-works.com>
date Fri, 07 Oct 2011 01:08:50 +0200
parents 9f6560667743 7d687ed11929
children 752b0a7b7679
comparison
equal deleted inserted replaced
1329:e058df3ff2b4 1512:bf263968da47
27 import os 27 import os
28 import logging 28 import logging
29 import traceback 29 import traceback
30 30
31 from mercurial.error import RepoError 31 from mercurial.error import RepoError
32 from mercurial.hgweb import hgweb 32 from mercurial.hgweb import hgweb_mod
33 from mercurial.hgweb.request import wsgiapplication 33
34 from paste.auth.basic import AuthBasicAuthenticator 34 from paste.auth.basic import AuthBasicAuthenticator
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36
37 from rhodecode.lib import safe_str
36 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware 38 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
37 from rhodecode.lib.utils import make_ui, invalidate_cache, \ 39 from rhodecode.lib.utils import make_ui, invalidate_cache, \
38 check_repo_fast, ui_sections 40 is_valid_repo, ui_sections
39 from rhodecode.model.user import UserModel 41 from rhodecode.model.db import User
42
40 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError 43 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
41 import logging
42 import os
43 import traceback
44 44
45 log = logging.getLogger(__name__) 45 log = logging.getLogger(__name__)
46 46
47
47 def is_mercurial(environ): 48 def is_mercurial(environ):
48 """ 49 """Returns True if request's target is mercurial server - header
49 Returns True if request's target is mercurial server - header
50 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``. 50 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
51 """ 51 """
52 http_accept = environ.get('HTTP_ACCEPT') 52 http_accept = environ.get('HTTP_ACCEPT')
53 if http_accept and http_accept.startswith('application/mercurial'): 53 if http_accept and http_accept.startswith('application/mercurial'):
54 return True 54 return True
55 return False 55 return False
56 56
57
57 class SimpleHg(object): 58 class SimpleHg(object):
58 59
59 def __init__(self, application, config): 60 def __init__(self, application, config):
60 self.application = application 61 self.application = application
61 self.config = config 62 self.config = config
63 # base path of repo locations
64 self.basepath = self.config['base_path']
62 #authenticate this mercurial request using authfunc 65 #authenticate this mercurial request using authfunc
63 self.authenticate = AuthBasicAuthenticator('', authfunc) 66 self.authenticate = AuthBasicAuthenticator('', authfunc)
64 self.ipaddr = '0.0.0.0' 67 self.ipaddr = '0.0.0.0'
65 self.repository = None
66 self.username = None
67 self.action = None
68 68
69 def __call__(self, environ, start_response): 69 def __call__(self, environ, start_response):
70 if not is_mercurial(environ): 70 if not is_mercurial(environ):
71 return self.application(environ, start_response) 71 return self.application(environ, start_response)
72 72
73 proxy_key = 'HTTP_X_REAL_IP' 73 proxy_key = 'HTTP_X_REAL_IP'
74 def_key = 'REMOTE_ADDR' 74 def_key = 'REMOTE_ADDR'
75 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0')) 75 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
76
76 # skip passing error to error controller 77 # skip passing error to error controller
77 environ['pylons.status_code_redirect'] = True 78 environ['pylons.status_code_redirect'] = True
78 79
79 #=================================================================== 80 #======================================================================
80 # AUTHENTICATE THIS MERCURIAL REQUEST 81 # EXTRACT REPOSITORY NAME FROM ENV
81 #=================================================================== 82 #======================================================================
82 username = REMOTE_USER(environ)
83
84 if not username:
85 self.authenticate.realm = str(self.config['rhodecode_realm'])
86 result = self.authenticate(environ)
87 if isinstance(result, str):
88 AUTH_TYPE.update(environ, 'basic')
89 REMOTE_USER.update(environ, result)
90 else:
91 return result.wsgi_application(environ, start_response)
92
93 #=======================================================================
94 # GET REPOSITORY
95 #=======================================================================
96 try: 83 try:
97 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:]) 84 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
98 if repo_name.endswith('/'): 85 log.debug('Extracted repo name is %s' % repo_name)
99 repo_name = repo_name.rstrip('/')
100 self.repository = repo_name
101 except: 86 except:
102 log.error(traceback.format_exc())
103 return HTTPInternalServerError()(environ, start_response) 87 return HTTPInternalServerError()(environ, start_response)
104 88
105 #=================================================================== 89 #======================================================================
106 # CHECK PERMISSIONS FOR THIS REQUEST 90 # GET ACTION PULL or PUSH
107 #=================================================================== 91 #======================================================================
108 self.action = self.__get_action(environ) 92 action = self.__get_action(environ)
109 if self.action: 93
110 username = self.__get_environ_user(environ) 94 #======================================================================
111 try: 95 # CHECK ANONYMOUS PERMISSION
112 user = self.__get_user(username) 96 #======================================================================
113 self.username = user.username 97 if action in ['pull', 'push']:
114 except: 98 anonymous_user = self.__get_user('default')
115 log.error(traceback.format_exc()) 99 username = anonymous_user.username
116 return HTTPInternalServerError()(environ, start_response) 100 anonymous_perm = self.__check_permission(action,
117 101 anonymous_user,
118 #check permissions for this repository 102 repo_name)
119 if self.action == 'push': 103
120 if not HasPermissionAnyMiddleware('repository.write', 104 if anonymous_perm is not True or anonymous_user.active is False:
121 'repository.admin')\ 105 if anonymous_perm is not True:
122 (user, repo_name): 106 log.debug('Not enough credentials to access this '
123 return HTTPForbidden()(environ, start_response) 107 'repository as anonymous user')
124 108 if anonymous_user.active is False:
125 else: 109 log.debug('Anonymous access is disabled, running '
126 #any other action need at least read permission 110 'authentication')
127 if not HasPermissionAnyMiddleware('repository.read', 111 #==============================================================
128 'repository.write', 112 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
129 'repository.admin')\ 113 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
130 (user, repo_name): 114 #==============================================================
131 return HTTPForbidden()(environ, start_response) 115
132 116 if not REMOTE_USER(environ):
133 self.extras = {'ip':self.ipaddr, 117 self.authenticate.realm = \
134 'username':self.username, 118 safe_str(self.config['rhodecode_realm'])
135 'action':self.action, 119 result = self.authenticate(environ)
136 'repository':self.repository} 120 if isinstance(result, str):
137 121 AUTH_TYPE.update(environ, 'basic')
138 #=================================================================== 122 REMOTE_USER.update(environ, result)
123 else:
124 return result.wsgi_application(environ, start_response)
125
126 #==============================================================
127 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
128 # BASIC AUTH
129 #==============================================================
130
131 if action in ['pull', 'push']:
132 username = REMOTE_USER(environ)
133 try:
134 user = self.__get_user(username)
135 username = user.username
136 except:
137 log.error(traceback.format_exc())
138 return HTTPInternalServerError()(environ,
139 start_response)
140
141 #check permissions for this repository
142 perm = self.__check_permission(action, user,
143 repo_name)
144 if perm is not True:
145 return HTTPForbidden()(environ, start_response)
146
147 extras = {'ip': ipaddr,
148 'username': username,
149 'action': action,
150 'repository': repo_name}
151
152 #======================================================================
139 # MERCURIAL REQUEST HANDLING 153 # MERCURIAL REQUEST HANDLING
140 #=================================================================== 154 #======================================================================
141 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path 155
142 self.baseui = make_ui('db') 156 repo_path = safe_str(os.path.join(self.basepath, repo_name))
143 self.basepath = self.config['base_path'] 157 log.debug('Repository path is %s' % repo_path)
144 self.repo_path = os.path.join(self.basepath, repo_name) 158
145 159 baseui = make_ui('db')
146 #quick check if that dir exists... 160 self.__inject_extras(repo_path, baseui, extras)
147 if check_repo_fast(repo_name, self.basepath): 161
162
163 # quick check if that dir exists...
164 if is_valid_repo(repo_name, self.basepath) is False:
148 return HTTPNotFound()(environ, start_response) 165 return HTTPNotFound()(environ, start_response)
166
149 try: 167 try:
150 app = wsgiapplication(self.__make_app) 168 #invalidate cache on push
169 if action == 'push':
170 self.__invalidate_cache(repo_name)
171
172 app = self.__make_app(repo_path, baseui, extras)
173 return app(environ, start_response)
151 except RepoError, e: 174 except RepoError, e:
152 if str(e).find('not found') != -1: 175 if str(e).find('not found') != -1:
153 return HTTPNotFound()(environ, start_response) 176 return HTTPNotFound()(environ, start_response)
154 except Exception: 177 except Exception:
155 log.error(traceback.format_exc()) 178 log.error(traceback.format_exc())
156 return HTTPInternalServerError()(environ, start_response) 179 return HTTPInternalServerError()(environ, start_response)
157 180
158 #invalidate cache on push 181 def __make_app(self, repo_name, baseui, extras):
159 if self.action == 'push': 182 """
160 self.__invalidate_cache(repo_name) 183 Make an wsgi application using hgweb, and inject generated baseui
161 184 instance, additionally inject some extras into ui object
162 return app(environ, start_response) 185 """
163 186 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
164 187
165 def __make_app(self): 188
166 hgserve = hgweb(str(self.repo_path), baseui=self.baseui) 189 def __check_permission(self, action, user, repo_name):
167 return self.__load_web_settings(hgserve, self.extras) 190 """
168 191 Checks permissions using action (push/pull) user and repository
169 def __get_environ_user(self, environ): 192 name
170 return environ.get('REMOTE_USER') 193
194 :param action: push or pull action
195 :param user: user instance
196 :param repo_name: repository name
197 """
198 if action == 'push':
199 if not HasPermissionAnyMiddleware('repository.write',
200 'repository.admin')(user,
201 repo_name):
202 return False
203
204 else:
205 #any other action need at least read permission
206 if not HasPermissionAnyMiddleware('repository.read',
207 'repository.write',
208 'repository.admin')(user,
209 repo_name):
210 return False
211
212 return True
213
214 def __get_repository(self, environ):
215 """
216 Get's repository name out of PATH_INFO header
217
218 :param environ: environ where PATH_INFO is stored
219 """
220 try:
221 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
222 if repo_name.endswith('/'):
223 repo_name = repo_name.rstrip('/')
224 except:
225 log.error(traceback.format_exc())
226 raise
227
228 return repo_name
171 229
172 def __get_user(self, username): 230 def __get_user(self, username):
173 return UserModel().get_by_username(username, cache=True) 231 return User.by_username(username)
174 232
175 def __get_action(self, environ): 233 def __get_action(self, environ):
176 """ 234 """
177 Maps mercurial request commands into a clone,pull or push command. 235 Maps mercurial request commands into a clone,pull or push command.
178 This should always return a valid command string 236 This should always return a valid command string
237
179 :param environ: 238 :param environ:
180 """ 239 """
181 mapping = {'changegroup': 'pull', 240 mapping = {'changegroup': 'pull',
182 'changegroupsubset': 'pull', 241 'changegroupsubset': 'pull',
183 'stream_out': 'pull', 242 'stream_out': 'pull',
185 'unbundle': 'push', 244 'unbundle': 'push',
186 'pushkey': 'push', } 245 'pushkey': 'push', }
187 for qry in environ['QUERY_STRING'].split('&'): 246 for qry in environ['QUERY_STRING'].split('&'):
188 if qry.startswith('cmd'): 247 if qry.startswith('cmd'):
189 cmd = qry.split('=')[-1] 248 cmd = qry.split('=')[-1]
190 if mapping.has_key(cmd): 249 if cmd in mapping:
191 return mapping[cmd] 250 return mapping[cmd]
192 else: 251 else:
193 return cmd 252 return 'pull'
194 253
195 def __invalidate_cache(self, repo_name): 254 def __invalidate_cache(self, repo_name):
196 """we know that some change was made to repositories and we should 255 """we know that some change was made to repositories and we should
197 invalidate the cache to see the changes right away but only for 256 invalidate the cache to see the changes right away but only for
198 push requests""" 257 push requests"""
199 invalidate_cache('get_repo_cached_%s' % repo_name) 258 invalidate_cache('get_repo_cached_%s' % repo_name)
200 259
201 260 def __inject_extras(self,repo_path, baseui, extras={}):
202 def __load_web_settings(self, hgserve, extras={}): 261 """
203 #set the global ui for hgserve instance passed 262 Injects some extra params into baseui instance
204 hgserve.repo.ui = self.baseui 263
205 264 also overwrites global settings with those takes from local hgrc file
206 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc') 265
266 :param baseui: baseui instance
267 :param extras: dict with extra params to put into baseui
268 """
269
270 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
271
272 # make our hgweb quiet so it doesn't print output
273 baseui.setconfig('ui', 'quiet', 'true')
207 274
208 #inject some additional parameters that will be available in ui 275 #inject some additional parameters that will be available in ui
209 #for hooks 276 #for hooks
210 for k, v in extras.items(): 277 for k, v in extras.items():
211 hgserve.repo.ui.setconfig('rhodecode_extras', k, v) 278 baseui.setconfig('rhodecode_extras', k, v)
212 279
213 repoui = make_ui('file', hgrc, False) 280 repoui = make_ui('file', hgrc, False)
214 281
215 if repoui: 282 if repoui:
216 #overwrite our ui instance with the section from hgrc file 283 #overwrite our ui instance with the section from hgrc file
217 for section in ui_sections: 284 for section in ui_sections:
218 for k, v in repoui.configitems(section): 285 for k, v in repoui.configitems(section):
219 hgserve.repo.ui.setconfig(section, k, v) 286 baseui.setconfig(section, k, v)
220 287
221 return hgserve
222
223
224
225
226
227
228
229
230
231
232
233
234
235