Mercurial > kallithea
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 |