##// END OF EJS Templates
implemented cache for repeated queries in simplehg mercurial requests
marcink -
r343:64849630 default
parent child Browse files
Show More
@@ -1,133 +1,135 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # pylons_app - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 ############################################
11 11 ## Uncomment and replace with the address ##
12 12 ## which should receive any error reports ##
13 13 ############################################
14 14 #email_to = admin@localhost
15 15 #smtp_server = mail.server.com
16 16 #error_email_from = paste_error@localhost
17 17 #smtp_username =
18 18 #smtp_password =
19 19 #error_message = 'mercurial crash !'
20 20
21 21 [server:main]
22 22 ##nr of threads to spawn
23 23 threadpool_workers = 5
24 24
25 25 ##max request before
26 26 threadpool_max_requests = 2
27 27
28 28 ##option to use threads of process
29 29 use_threadpool = true
30 30
31 31 use = egg:Paste#http
32 32 host = 127.0.0.1
33 33 port = 5000
34 34
35 35 [app:main]
36 36 use = egg:pylons_app
37 37 full_stack = true
38 38 static_files = true
39 39 lang=en
40 40 cache_dir = %(here)s/data
41 41
42 42 ####################################
43 43 ### BEAKER CACHE ####
44 44 ####################################
45 45 beaker.cache.data_dir=/%(here)s/data/cache/data
46 46 beaker.cache.lock_dir=/%(here)s/data/cache/lock
47 beaker.cache.regions=short_term,long_term
47 beaker.cache.regions=super_short_term,short_term,long_term
48 48 beaker.cache.long_term.type=memory
49 49 beaker.cache.long_term.expire=36000
50 50 beaker.cache.short_term.type=memory
51 51 beaker.cache.short_term.expire=60
52 beaker.cache.super_short_term.type=memory
53 beaker.cache.super_short_term.expire=10
52 54
53 55 ################################################################################
54 56 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
55 57 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
56 58 ## execute malicious code after an exception is raised. ##
57 59 ################################################################################
58 60 #set debug = false
59 61
60 62 ##################################
61 63 ### LOGVIEW CONFIG ###
62 64 ##################################
63 65 logview.sqlalchemy = #faa
64 66 logview.pylons.templating = #bfb
65 67 logview.pylons.util = #eee
66 68
67 69 #########################################################
68 70 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
69 71 #########################################################
70 72 sqlalchemy.db1.url = sqlite:///%(here)s/hg_app.db
71 73 #sqlalchemy.db1.echo = False
72 74 #sqlalchemy.db1.pool_recycle = 3600
73 75 sqlalchemy.convert_unicode = true
74 76
75 77 ################################
76 78 ### LOGGING CONFIGURATION ####
77 79 ################################
78 80 [loggers]
79 81 keys = root, routes, pylons_app, sqlalchemy
80 82
81 83 [handlers]
82 84 keys = console
83 85
84 86 [formatters]
85 87 keys = generic,color_formatter
86 88
87 89 #############
88 90 ## LOGGERS ##
89 91 #############
90 92 [logger_root]
91 93 level = NOTSET
92 94 handlers = console
93 95
94 96 [logger_routes]
95 97 level = DEBUG
96 98 handlers = console
97 99 qualname = routes.middleware
98 100 # "level = DEBUG" logs the route matched and routing variables.
99 101
100 102 [logger_pylons_app]
101 103 level = DEBUG
102 104 handlers = console
103 105 qualname = pylons_app
104 106 propagate = 0
105 107
106 108 [logger_sqlalchemy]
107 109 level = ERROR
108 110 handlers = console
109 111 qualname = sqlalchemy.engine
110 112 propagate = 0
111 113
112 114 ##############
113 115 ## HANDLERS ##
114 116 ##############
115 117
116 118 [handler_console]
117 119 class = StreamHandler
118 120 args = (sys.stderr,)
119 121 level = NOTSET
120 122 formatter = color_formatter
121 123
122 124 ################
123 125 ## FORMATTERS ##
124 126 ################
125 127
126 128 [formatter_generic]
127 129 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
128 130 datefmt = %Y-%m-%d %H:%M:%S
129 131
130 132 [formatter_color_formatter]
131 133 class=pylons_app.lib.colored_formatter.ColorFormatter
132 134 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
133 135 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
@@ -1,134 +1,135 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # pylons_app - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 ############################################
11 11 ## Uncomment and replace with the address ##
12 12 ## which should receive any error reports ##
13 13 ############################################
14 14 #email_to = admin@localhost
15 15 #smtp_server = mail.server.com
16 16 #error_email_from = paste_error@localhost
17 17 #smtp_username =
18 18 #smtp_password =
19 19 #error_message = 'mercurial crash !'
20 20
21 21 [server:main]
22 22 ##nr of threads to spawn
23 23 threadpool_workers = 5
24 24
25 25 ##max request before
26 26 threadpool_max_requests = 2
27 27
28 28 ##option to use threads of process
29 29 use_threadpool = true
30 30
31 31 use = egg:Paste#http
32 32 host = 127.0.0.1
33 33 port = 8001
34 34
35 35 [app:main]
36 36 use = egg:pylons_app
37 37 full_stack = true
38 38 static_files = false
39 39 lang=en
40 40 cache_dir = %(here)s/data
41 41
42
43 42 ####################################
44 43 ### BEAKER CACHE ####
45 44 ####################################
46 45 beaker.cache.data_dir=/%(here)s/data/cache/data
47 46 beaker.cache.lock_dir=/%(here)s/data/cache/lock
48 beaker.cache.regions=short_term,long_term
47 beaker.cache.regions=super_short_term,short_term,long_term
49 48 beaker.cache.long_term.type=memory
50 49 beaker.cache.long_term.expire=36000
51 50 beaker.cache.short_term.type=memory
52 51 beaker.cache.short_term.expire=60
52 beaker.cache.super_short_term.type=memory
53 beaker.cache.super_short_term.expire=10
53 54
54 55 ################################################################################
55 56 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
56 57 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
57 58 ## execute malicious code after an exception is raised. ##
58 59 ################################################################################
59 60 set debug = false
60 61
61 62 ##################################
62 63 ### LOGVIEW CONFIG ###
63 64 ##################################
64 65 logview.sqlalchemy = #faa
65 66 logview.pylons.templating = #bfb
66 67 logview.pylons.util = #eee
67 68
68 69 #########################################################
69 70 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
70 71 #########################################################
71 72 sqlalchemy.db1.url = sqlite:///%(here)s/hg_app.db
72 73 #sqlalchemy.db1.echo = False
73 74 #sqlalchemy.db1.pool_recycle = 3600
74 75 sqlalchemy.convert_unicode = true
75 76
76 77 ################################
77 78 ### LOGGING CONFIGURATION ####
78 79 ################################
79 80 [loggers]
80 81 keys = root, routes, pylons_app, sqlalchemy
81 82
82 83 [handlers]
83 84 keys = console
84 85
85 86 [formatters]
86 87 keys = generic,color_formatter
87 88
88 89 #############
89 90 ## LOGGERS ##
90 91 #############
91 92 [logger_root]
92 93 level = INFO
93 94 handlers = console
94 95
95 96 [logger_routes]
96 97 level = INFO
97 98 handlers = console
98 99 qualname = routes.middleware
99 100 # "level = DEBUG" logs the route matched and routing variables.
100 101
101 102 [logger_pylons_app]
102 103 level = DEBUG
103 104 handlers = console
104 105 qualname = pylons_app
105 106 propagate = 0
106 107
107 108 [logger_sqlalchemy]
108 109 level = ERROR
109 110 handlers = console
110 111 qualname = sqlalchemy.engine
111 112 propagate = 0
112 113
113 114 ##############
114 115 ## HANDLERS ##
115 116 ##############
116 117
117 118 [handler_console]
118 119 class = StreamHandler
119 120 args = (sys.stderr,)
120 121 level = NOTSET
121 122 formatter = color_formatter
122 123
123 124 ################
124 125 ## FORMATTERS ##
125 126 ################
126 127
127 128 [formatter_generic]
128 129 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
129 130 datefmt = %Y-%m-%d %H:%M:%S
130 131
131 132 [formatter_color_formatter]
132 133 class=pylons_app.lib.colored_formatter.ColorFormatter
133 134 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
134 135 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
@@ -1,399 +1,405 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 # authentication and permission libraries
4 4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 5
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; version 2
9 9 # of the License or (at your opinion) any later version of the license.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 19 # MA 02110-1301, USA.
20 20 """
21 21 Created on April 4, 2010
22 22
23 23 @author: marcink
24 24 """
25
25 from beaker.cache import cache_region
26 26 from functools import wraps
27 from pylons import session, url, request
27 from pylons import config, session, url, request
28 28 from pylons.controllers.util import abort, redirect
29 from pylons_app.lib.utils import get_repo_slug
29 30 from pylons_app.model import meta
30 31 from pylons_app.model.db import User, Repo2Perm, Repository, Permission
31 from pylons_app.lib.utils import get_repo_slug
32 32 from sqlalchemy.exc import OperationalError
33 33 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
34 34 import crypt
35 35 import logging
36 from pylons import config
36
37 37 log = logging.getLogger(__name__)
38 38
39 39 def get_crypt_password(password):
40 40 """
41 41 Cryptographic function used for password hashing
42 42 @param password: password to hash
43 43 """
44 44 return crypt.crypt(password, '6a')
45 45
46
47 @cache_region('super_short_term', 'cached_user')
48 def get_user_cached(username):
49 sa = meta.Session
50 user = sa.query(User).filter(User.username == username).one()
51 return user
52
46 53 def authfunc(environ, username, password):
47 sa = meta.Session
48 54 password_crypt = get_crypt_password(password)
49 55 try:
50 user = sa.query(User).filter(User.username == username).one()
56 user = get_user_cached(username)
51 57 except (NoResultFound, MultipleResultsFound, OperationalError) as e:
52 58 log.error(e)
53 59 user = None
54 60
55 61 if user:
56 62 if user.active:
57 63 if user.username == username and user.password == password_crypt:
58 64 log.info('user %s authenticated correctly', username)
59 65 return True
60 66 else:
61 67 log.error('user %s is disabled', username)
62 68
63 69 return False
64 70
65 71 class AuthUser(object):
66 72 """
67 73 A simple object that handles a mercurial username for authentication
68 74 """
69 75 def __init__(self):
70 76 self.username = 'None'
71 77 self.user_id = None
72 78 self.is_authenticated = False
73 79 self.is_admin = False
74 80 self.permissions = {}
75 81
76 82
77 83 def set_available_permissions(config):
78 84 """
79 85 This function will propagate pylons globals with all available defined
80 86 permission given in db. We don't wannt to check each time from db for new
81 87 permissions since adding a new permission also requires application restart
82 88 ie. to decorate new views with the newly created permission
83 89 @param config:
84 90 """
85 91 log.info('getting information about all available permissions')
86 92 sa = meta.Session
87 93 all_perms = sa.query(Permission).all()
88 94 config['available_permissions'] = [x.permission_name for x in all_perms]
89 95
90 96 def set_base_path(config):
91 97 config['base_path'] = config['pylons.app_globals'].base_path
92 98
93 99 def fill_perms(user):
94 100 sa = meta.Session
95 101 user.permissions['repositories'] = {}
96 102
97 103 #first fetch default permissions
98 104 default_perms = sa.query(Repo2Perm, Repository, Permission)\
99 105 .join((Repository, Repo2Perm.repository == Repository.repo_name))\
100 106 .join((Permission, Repo2Perm.permission_id == Permission.permission_id))\
101 107 .filter(Repo2Perm.user_id == sa.query(User).filter(User.username ==
102 108 'default').one().user_id).all()
103 109
104 110 if user.is_admin:
105 111 user.permissions['global'] = set(['hg.admin'])
106 112 #admin have all rights full
107 113 for perm in default_perms:
108 114 p = 'repository.admin'
109 115 user.permissions['repositories'][perm.Repo2Perm.repository] = p
110 116
111 117 else:
112 118 user.permissions['global'] = set()
113 119 for perm in default_perms:
114 120 if perm.Repository.private:
115 121 #disable defaults for private repos,
116 122 p = 'repository.none'
117 123 elif perm.Repository.user_id == user.user_id:
118 124 #set admin if owner
119 125 p = 'repository.admin'
120 126 else:
121 127 p = perm.Permission.permission_name
122 128
123 129 user.permissions['repositories'][perm.Repo2Perm.repository] = p
124 130
125 131
126 132 user_perms = sa.query(Repo2Perm, Permission, Repository)\
127 133 .join((Repository, Repo2Perm.repository == Repository.repo_name))\
128 134 .join((Permission, Repo2Perm.permission_id == Permission.permission_id))\
129 135 .filter(Repo2Perm.user_id == user.user_id).all()
130 136 #overwrite userpermissions with defaults
131 137 for perm in user_perms:
132 138 #set write if owner
133 139 if perm.Repository.user_id == user.user_id:
134 140 p = 'repository.write'
135 141 else:
136 142 p = perm.Permission.permission_name
137 143 user.permissions['repositories'][perm.Repo2Perm.repository] = p
138 144 return user
139 145
140 146 def get_user(session):
141 147 """
142 148 Gets user from session, and wraps permissions into user
143 149 @param session:
144 150 """
145 151 user = session.get('hg_app_user', AuthUser())
146 152
147 153 if user.is_authenticated:
148 154 user = fill_perms(user)
149 155
150 156 session['hg_app_user'] = user
151 157 session.save()
152 158 return user
153 159
154 160 #===============================================================================
155 161 # CHECK DECORATORS
156 162 #===============================================================================
157 163 class LoginRequired(object):
158 164 """
159 165 Must be logged in to execute this function else redirect to login page
160 166 """
161 167
162 168 def __call__(self, func):
163 169 @wraps(func)
164 170 def _wrapper(*fargs, **fkwargs):
165 171 user = session.get('hg_app_user', AuthUser())
166 172 log.debug('Checking login required for user:%s', user.username)
167 173 if user.is_authenticated:
168 174 log.debug('user %s is authenticated', user.username)
169 175 func(*fargs)
170 176 else:
171 177 log.warn('user %s not authenticated', user.username)
172 178 log.debug('redirecting to login page')
173 179 return redirect(url('login_home'))
174 180
175 181 return _wrapper
176 182
177 183 class PermsDecorator(object):
178 184 """
179 185 Base class for decorators
180 186 """
181 187
182 188 def __init__(self, *required_perms):
183 189 available_perms = config['available_permissions']
184 190 for perm in required_perms:
185 191 if perm not in available_perms:
186 192 raise Exception("'%s' permission is not defined" % perm)
187 193 self.required_perms = set(required_perms)
188 194 self.user_perms = None
189 195
190 196 def __call__(self, func):
191 197 @wraps(func)
192 198 def _wrapper(*fargs, **fkwargs):
193 199 self.user_perms = session.get('hg_app_user', AuthUser()).permissions
194 200 log.debug('checking %s permissions %s for %s',
195 201 self.__class__.__name__, self.required_perms, func.__name__)
196 202
197 203 if self.check_permissions():
198 204 log.debug('Permission granted for %s', func.__name__)
199 205 return func(*fargs)
200 206
201 207 else:
202 208 log.warning('Permission denied for %s', func.__name__)
203 209 #redirect with forbidden ret code
204 210 return abort(403)
205 211 return _wrapper
206 212
207 213
208 214 def check_permissions(self):
209 215 """
210 216 Dummy function for overriding
211 217 """
212 218 raise Exception('You have to write this function in child class')
213 219
214 220 class HasPermissionAllDecorator(PermsDecorator):
215 221 """
216 222 Checks for access permission for all given predicates. All of them have to
217 223 be meet in order to fulfill the request
218 224 """
219 225
220 226 def check_permissions(self):
221 227 if self.required_perms.issubset(self.user_perms.get('global')):
222 228 return True
223 229 return False
224 230
225 231
226 232 class HasPermissionAnyDecorator(PermsDecorator):
227 233 """
228 234 Checks for access permission for any of given predicates. In order to
229 235 fulfill the request any of predicates must be meet
230 236 """
231 237
232 238 def check_permissions(self):
233 239 if self.required_perms.intersection(self.user_perms.get('global')):
234 240 return True
235 241 return False
236 242
237 243 class HasRepoPermissionAllDecorator(PermsDecorator):
238 244 """
239 245 Checks for access permission for all given predicates for specific
240 246 repository. All of them have to be meet in order to fulfill the request
241 247 """
242 248
243 249 def check_permissions(self):
244 250 repo_name = get_repo_slug(request)
245 251 try:
246 252 user_perms = set([self.user_perms['repositories'][repo_name]])
247 253 except KeyError:
248 254 return False
249 255 if self.required_perms.issubset(user_perms):
250 256 return True
251 257 return False
252 258
253 259
254 260 class HasRepoPermissionAnyDecorator(PermsDecorator):
255 261 """
256 262 Checks for access permission for any of given predicates for specific
257 263 repository. In order to fulfill the request any of predicates must be meet
258 264 """
259 265
260 266 def check_permissions(self):
261 267 repo_name = get_repo_slug(request)
262 268
263 269 try:
264 270 user_perms = set([self.user_perms['repositories'][repo_name]])
265 271 except KeyError:
266 272 return False
267 273 if self.required_perms.intersection(user_perms):
268 274 return True
269 275 return False
270 276 #===============================================================================
271 277 # CHECK FUNCTIONS
272 278 #===============================================================================
273 279
274 280 class PermsFunction(object):
275 281 """
276 282 Base function for other check functions
277 283 """
278 284
279 285 def __init__(self, *perms):
280 286 available_perms = config['available_permissions']
281 287
282 288 for perm in perms:
283 289 if perm not in available_perms:
284 290 raise Exception("'%s' permission in not defined" % perm)
285 291 self.required_perms = set(perms)
286 292 self.user_perms = None
287 293 self.granted_for = ''
288 294 self.repo_name = None
289 295
290 296 def __call__(self, check_Location=''):
291 297 user = session.get('hg_app_user', False)
292 298 if not user:
293 299 return False
294 300 self.user_perms = user.permissions
295 301 self.granted_for = user.username
296 302 log.debug('checking %s %s', self.__class__.__name__, self.required_perms)
297 303
298 304 if self.check_permissions():
299 305 log.debug('Permission granted for %s @%s', self.granted_for,
300 306 check_Location)
301 307 return True
302 308
303 309 else:
304 310 log.warning('Permission denied for %s @%s', self.granted_for,
305 311 check_Location)
306 312 return False
307 313
308 314 def check_permissions(self):
309 315 """
310 316 Dummy function for overriding
311 317 """
312 318 raise Exception('You have to write this function in child class')
313 319
314 320 class HasPermissionAll(PermsFunction):
315 321 def check_permissions(self):
316 322 if self.required_perms.issubset(self.user_perms.get('global')):
317 323 return True
318 324 return False
319 325
320 326 class HasPermissionAny(PermsFunction):
321 327 def check_permissions(self):
322 328 if self.required_perms.intersection(self.user_perms.get('global')):
323 329 return True
324 330 return False
325 331
326 332 class HasRepoPermissionAll(PermsFunction):
327 333
328 334 def __call__(self, repo_name=None, check_Location=''):
329 335 self.repo_name = repo_name
330 336 return super(HasRepoPermissionAll, self).__call__(check_Location)
331 337
332 338 def check_permissions(self):
333 339 if not self.repo_name:
334 340 self.repo_name = get_repo_slug(request)
335 341
336 342 try:
337 343 self.user_perms = set([self.user_perms['repositories']\
338 344 [self.repo_name]])
339 345 except KeyError:
340 346 return False
341 347 self.granted_for = self.repo_name
342 348 if self.required_perms.issubset(self.user_perms):
343 349 return True
344 350 return False
345 351
346 352 class HasRepoPermissionAny(PermsFunction):
347 353
348 354
349 355 def __call__(self, repo_name=None, check_Location=''):
350 356 self.repo_name = repo_name
351 357 return super(HasRepoPermissionAny, self).__call__(check_Location)
352 358
353 359 def check_permissions(self):
354 360 if not self.repo_name:
355 361 self.repo_name = get_repo_slug(request)
356 362
357 363 try:
358 364 self.user_perms = set([self.user_perms['repositories']\
359 365 [self.repo_name]])
360 366 except KeyError:
361 367 return False
362 368 self.granted_for = self.repo_name
363 369 if self.required_perms.intersection(self.user_perms):
364 370 return True
365 371 return False
366 372
367 373 #===============================================================================
368 374 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
369 375 #===============================================================================
370 376
371 377 class HasPermissionAnyMiddleware(object):
372 378 def __init__(self, *perms):
373 379 self.required_perms = set(perms)
374 380
375 381 def __call__(self, user, repo_name):
376 382 usr = AuthUser()
377 383 usr.user_id = user.user_id
378 384 usr.username = user.username
379 385 usr.is_admin = user.admin
380 386
381 387 try:
382 388 self.user_perms = set([fill_perms(usr)\
383 389 .permissions['repositories'][repo_name]])
384 390 except:
385 391 self.user_perms = set()
386 392 self.granted_for = ''
387 393 self.username = user.username
388 394 self.repo_name = repo_name
389 395 return self.check_permissions()
390 396
391 397 def check_permissions(self):
392 398 log.debug('checking mercurial protocol '
393 399 'permissions for user:%s repository:%s',
394 400 self.username, self.repo_name)
395 401 if self.required_perms.intersection(self.user_perms):
396 402 log.debug('permission granted')
397 403 return True
398 404 log.debug('permission denied')
399 405 return False
@@ -1,226 +1,231 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 # middleware to handle mercurial api calls
4 4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5
5
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; version 2
9 9 # of the License or (at your opinion) any later version of the license.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 19 # MA 02110-1301, USA.
20 20
21 21 """
22 22 Created on 2010-04-28
23 23
24 24 @author: marcink
25 25 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
26 26 It's implemented with basic auth function
27 27 """
28 28 from datetime import datetime
29 29 from itertools import chain
30 from mercurial.error import RepoError
30 31 from mercurial.hgweb import hgweb
31 32 from mercurial.hgweb.request import wsgiapplication
32 from mercurial.error import RepoError
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 from pylons_app.lib.auth import authfunc, HasPermissionAnyMiddleware
35 from pylons_app.lib.auth import authfunc, HasPermissionAnyMiddleware, \
36 get_user_cached
36 37 from pylons_app.lib.utils import is_mercurial, make_ui, invalidate_cache, \
37 38 check_repo_fast
38 39 from pylons_app.model import meta
39 40 from pylons_app.model.db import UserLog, User
40 import pylons_app.lib.helpers as h
41 41 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
42 42 import logging
43 43 import os
44 import pylons_app.lib.helpers as h
44 45 import traceback
46
45 47 log = logging.getLogger(__name__)
46 48
47 49 class SimpleHg(object):
48 50
49 51 def __init__(self, application, config):
50 52 self.application = application
51 53 self.config = config
52 54 #authenticate this mercurial request using
53 55 realm = self.config['hg_app_auth_realm']
54 56 self.authenticate = AuthBasicAuthenticator(realm, authfunc)
55 57
56 58 def __call__(self, environ, start_response):
57 59 if not is_mercurial(environ):
58 60 return self.application(environ, start_response)
59 else:
60 #===================================================================
61 # AUTHENTICATE THIS MERCURIAL REQUEST
62 #===================================================================
63 username = REMOTE_USER(environ)
64 if not username:
65 result = self.authenticate(environ)
66 if isinstance(result, str):
67 AUTH_TYPE.update(environ, 'basic')
68 REMOTE_USER.update(environ, result)
69 else:
70 return result.wsgi_application(environ, start_response)
71
61
62 #===================================================================
63 # AUTHENTICATE THIS MERCURIAL REQUEST
64 #===================================================================
65 username = REMOTE_USER(environ)
66 if not username:
67 result = self.authenticate(environ)
68 if isinstance(result, str):
69 AUTH_TYPE.update(environ, 'basic')
70 REMOTE_USER.update(environ, result)
71 else:
72 return result.wsgi_application(environ, start_response)
73
74 try:
75 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
76 except:
77 log.error(traceback.format_exc())
78 return HTTPInternalServerError()(environ, start_response)
79
80 #===================================================================
81 # CHECK PERMISSIONS FOR THIS REQUEST
82 #===================================================================
83 action = self.__get_action(environ)
84 if action:
85 username = self.__get_environ_user(environ)
72 86 try:
73 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
87 user = self.__get_user(username)
74 88 except:
75 89 log.error(traceback.format_exc())
76 90 return HTTPInternalServerError()(environ, start_response)
91 #check permissions for this repository
92 if action == 'pull':
93 if not HasPermissionAnyMiddleware('repository.read',
94 'repository.write',
95 'repository.admin')\
96 (user, repo_name):
97 return HTTPForbidden()(environ, start_response)
98 if action == 'push':
99 if not HasPermissionAnyMiddleware('repository.write',
100 'repository.admin')\
101 (user, repo_name):
102 return HTTPForbidden()(environ, start_response)
77 103
78 #===================================================================
79 # CHECK PERMISSIONS FOR THIS REQUEST
80 #===================================================================
81 action = self.__get_action(environ)
82 if action:
83 username = self.__get_environ_user(environ)
84 try:
85 sa = meta.Session
86 user = sa.query(User)\
87 .filter(User.username == username).one()
88 except:
89 log.error(traceback.format_exc())
90 return HTTPInternalServerError()(environ, start_response)
91 #check permissions for this repository
92 if action == 'pull':
93 if not HasPermissionAnyMiddleware('repository.read',
94 'repository.write',
95 'repository.admin')\
96 (user, repo_name):
97 return HTTPForbidden()(environ, start_response)
98 if action == 'push':
99 if not HasPermissionAnyMiddleware('repository.write',
100 'repository.admin')\
101 (user, repo_name):
102 return HTTPForbidden()(environ, start_response)
103
104 #log action
105 proxy_key = 'HTTP_X_REAL_IP'
106 def_key = 'REMOTE_ADDR'
107 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
108 self.__log_user_action(user, action, repo_name, ipaddr)
109
110 #===================================================================
111 # MERCURIAL REQUEST HANDLING
112 #===================================================================
113 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
114 self.baseui = make_ui('db')
115 self.basepath = self.config['base_path']
116 self.repo_path = os.path.join(self.basepath, repo_name)
104 #log action
105 proxy_key = 'HTTP_X_REAL_IP'
106 def_key = 'REMOTE_ADDR'
107 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
108 self.__log_user_action(user, action, repo_name, ipaddr)
109
110 #===================================================================
111 # MERCURIAL REQUEST HANDLING
112 #===================================================================
113 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
114 self.baseui = make_ui('db')
115 self.basepath = self.config['base_path']
116 self.repo_path = os.path.join(self.basepath, repo_name)
117 117
118 #quick check if that dir exists...
119 if check_repo_fast(repo_name, self.basepath):
118 #quick check if that dir exists...
119 if check_repo_fast(repo_name, self.basepath):
120 return HTTPNotFound()(environ, start_response)
121 try:
122 app = wsgiapplication(self.__make_app)
123 except RepoError as e:
124 if str(e).find('not found') != -1:
120 125 return HTTPNotFound()(environ, start_response)
121 try:
122 app = wsgiapplication(self.__make_app)
123 except RepoError as e:
124 if str(e).find('not found') != -1:
125 return HTTPNotFound()(environ, start_response)
126 except Exception:
127 log.error(traceback.format_exc())
128 return HTTPInternalServerError()(environ, start_response)
129
130 #invalidate cache on push
131 if action == 'push':
132 self.__invalidate_cache(repo_name)
133 messages = []
134 messages.append('thank you for using hg-app')
135
136 return self.msg_wrapper(app, environ, start_response, messages)
137 else:
138 return app(environ, start_response)
126 except Exception:
127 log.error(traceback.format_exc())
128 return HTTPInternalServerError()(environ, start_response)
129
130 #invalidate cache on push
131 if action == 'push':
132 self.__invalidate_cache(repo_name)
133 messages = []
134 messages.append('thank you for using hg-app')
135
136 return self.msg_wrapper(app, environ, start_response, messages)
137 else:
138 return app(environ, start_response)
139 139
140 140
141 141 def msg_wrapper(self, app, environ, start_response, messages=[]):
142 142 """
143 143 Wrapper for custom messages that come out of mercurial respond messages
144 144 is a list of messages that the user will see at the end of response
145 145 from merurial protocol actions that involves remote answers
146 146 @param app:
147 147 @param environ:
148 148 @param start_response:
149 149 """
150 150 def custom_messages(msg_list):
151 151 for msg in msg_list:
152 152 yield msg + '\n'
153 153 org_response = app(environ, start_response)
154 154 return chain(org_response, custom_messages(messages))
155 155
156 156 def __make_app(self):
157 157 hgserve = hgweb(str(self.repo_path), baseui=self.baseui)
158 158 return self.__load_web_settings(hgserve)
159 159
160 160 def __get_environ_user(self, environ):
161 161 return environ.get('REMOTE_USER')
162 162
163 def __get_user(self, username):
164 return get_user_cached(username)
165
166
167
163 168 def __get_size(self, repo_path, content_size):
164 169 size = int(content_size)
165 170 for path, dirs, files in os.walk(repo_path):
166 171 if path.find('.hg') == -1:
167 172 for f in files:
168 173 size += os.path.getsize(os.path.join(path, f))
169 174 return size
170 175 return h.format_byte_size(size)
171 176
172 177 def __get_action(self, environ):
173 178 """
174 179 Maps mercurial request commands into a pull or push command.
175 180 @param environ:
176 181 """
177 182 mapping = {'changegroup': 'pull',
178 183 'changegroupsubset': 'pull',
179 184 'stream_out': 'pull',
180 185 'listkeys': 'pull',
181 186 'unbundle': 'push',
182 187 'pushkey': 'push', }
183 188
184 189 for qry in environ['QUERY_STRING'].split('&'):
185 190 if qry.startswith('cmd'):
186 191 cmd = qry.split('=')[-1]
187 192 if mapping.has_key(cmd):
188 193 return mapping[cmd]
189 194
190 195 def __log_user_action(self, user, action, repo, ipaddr):
191 196 sa = meta.Session
192 197 try:
193 198 user_log = UserLog()
194 199 user_log.user_id = user.user_id
195 200 user_log.action = action
196 201 user_log.repository = repo.replace('/', '')
197 202 user_log.action_date = datetime.now()
198 203 user_log.user_ip = ipaddr
199 204 sa.add(user_log)
200 205 sa.commit()
201 206 log.info('Adding user %s, action %s on %s',
202 207 user.username, action, repo)
203 208 except Exception as e:
204 209 sa.rollback()
205 210 log.error('could not log user action:%s', str(e))
206 211
207 212 def __invalidate_cache(self, repo_name):
208 213 """we know that some change was made to repositories and we should
209 214 invalidate the cache to see the changes right away but only for
210 215 push requests"""
211 216 invalidate_cache('cached_repo_list')
212 217 invalidate_cache('full_changelog', repo_name)
213 218
214 219
215 220 def __load_web_settings(self, hgserve):
216 221 #set the global ui for hgserve
217 222 hgserve.repo.ui = self.baseui
218 223
219 224 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
220 225 repoui = make_ui('file', hgrc, False)
221 226
222 227 if repoui:
223 228 #set the repository based config
224 229 hgserve.repo.ui = repoui
225 230
226 231 return hgserve
@@ -1,189 +1,194 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 # Utilities for hg app
4 4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 5 # This program is free software; you can redistribute it and/or
6 6 # modify it under the terms of the GNU General Public License
7 7 # as published by the Free Software Foundation; version 2
8 8 # of the License or (at your opinion) any later version of the license.
9 9 #
10 10 # This program is distributed in the hope that it will be useful,
11 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 13 # GNU General Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License
16 16 # along with this program; if not, write to the Free Software
17 17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 18 # MA 02110-1301, USA.
19 from beaker.cache import cache_region
19 20
20 21 """
21 22 Created on April 18, 2010
22 23 Utilities for hg app
23 24 @author: marcink
24 25 """
25 26
26 27 import os
27 28 import logging
28 29 from mercurial import ui, config, hg
29 30 from mercurial.error import RepoError
30 31 from pylons_app.model.db import Repository, User, HgAppUi
31 32 log = logging.getLogger(__name__)
32 33
33 34
34 35 def get_repo_slug(request):
35 36 return request.environ['pylons.routes_dict'].get('repo_name')
36 37
37 38 def is_mercurial(environ):
38 39 """
39 40 Returns True if request's target is mercurial server - header
40 41 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
41 42 """
42 43 http_accept = environ.get('HTTP_ACCEPT')
43 44 if http_accept and http_accept.startswith('application/mercurial'):
44 45 return True
45 46 return False
46 47
47 48 def check_repo_dir(paths):
48 49 repos_path = paths[0][1].split('/')
49 50 if repos_path[-1] in ['*', '**']:
50 51 repos_path = repos_path[:-1]
51 52 if repos_path[0] != '/':
52 53 repos_path[0] = '/'
53 54 if not os.path.isdir(os.path.join(*repos_path)):
54 55 raise Exception('Not a valid repository in %s' % paths[0][1])
55 56
56 57 def check_repo_fast(repo_name, base_path):
57 58 if os.path.isdir(os.path.join(base_path, repo_name)):return False
58 59 return True
59 60
60 61 def check_repo(repo_name, base_path, verify=True):
61 62
62 63 repo_path = os.path.join(base_path, repo_name)
63 64
64 65 try:
65 66 if not check_repo_fast(repo_name, base_path):
66 67 return False
67 68 r = hg.repository(ui.ui(), repo_path)
68 69 if verify:
69 70 hg.verify(r)
70 71 #here we hnow that repo exists it was verified
71 72 log.info('%s repo is already created', repo_name)
72 73 return False
73 74 except RepoError:
74 75 #it means that there is no valid repo there...
75 76 log.info('%s repo is free for creation', repo_name)
76 77 return True
77 78
79
80 @cache_region('super_short_term', 'cached_hg_ui')
81 def get_hg_ui_cached():
82 from pylons_app.model.meta import Session
83 sa = Session()
84 return sa.query(HgAppUi).all()
85
78 86 def make_ui(read_from='file', path=None, checkpaths=True):
79 87 """
80 88 A function that will read python rc files or database
81 89 and make an mercurial ui object from read options
82 90
83 91 @param path: path to mercurial config file
84 92 @param checkpaths: check the path
85 93 @param read_from: read from 'file' or 'db'
86 94 """
87 95 #propagated from mercurial documentation
88 96 sections = ['alias', 'auth',
89 97 'decode/encode', 'defaults',
90 98 'diff', 'email',
91 99 'extensions', 'format',
92 100 'merge-patterns', 'merge-tools',
93 101 'hooks', 'http_proxy',
94 102 'smtp', 'patch',
95 103 'paths', 'profiling',
96 104 'server', 'trusted',
97 105 'ui', 'web', ]
98 106 baseui = ui.ui()
99 107
100 108
101 109 if read_from == 'file':
102 110 if not os.path.isfile(path):
103 111 log.warning('Unable to read config file %s' % path)
104 112 return False
105 113
106 114 cfg = config.config()
107 115 cfg.read(path)
108 116 for section in sections:
109 117 for k, v in cfg.items(section):
110 118 baseui.setconfig(section, k, v)
111 119 if checkpaths:check_repo_dir(cfg.items('paths'))
112 120
113 121
114 122 elif read_from == 'db':
115 from pylons_app.model.meta import Session
116 sa = Session()
117
118 hg_ui = sa.query(HgAppUi).all()
123 hg_ui = get_hg_ui_cached()
119 124 for ui_ in hg_ui:
120 125 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
121 126
122 127
123 128 return baseui
124 129
125 130
126 131 def set_hg_app_config(config):
127 132 config['hg_app_auth_realm'] = 'realm'
128 133 config['hg_app_name'] = 'app name'
129 134
130 135 def invalidate_cache(name, *args):
131 136 """Invalidates given name cache"""
132 137
133 138 from beaker.cache import region_invalidate
134 139 log.info('INVALIDATING CACHE FOR %s', name)
135 140
136 141 """propagate our arguments to make sure invalidation works. First
137 142 argument has to be the name of cached func name give to cache decorator
138 143 without that the invalidation would not work"""
139 144 tmp = [name]
140 145 tmp.extend(args)
141 146 args = tuple(tmp)
142 147
143 148 if name == 'cached_repo_list':
144 149 from pylons_app.model.hg_model import _get_repos_cached
145 150 region_invalidate(_get_repos_cached, None, *args)
146 151
147 152 if name == 'full_changelog':
148 153 from pylons_app.model.hg_model import _full_changelog_cached
149 154 region_invalidate(_full_changelog_cached, None, *args)
150 155
151 156 from vcs.backends.base import BaseChangeset
152 157 from vcs.utils.lazy import LazyProperty
153 158 class EmptyChangeset(BaseChangeset):
154 159
155 160 revision = -1
156 161 message = ''
157 162
158 163 @LazyProperty
159 164 def raw_id(self):
160 165 """
161 166 Returns raw string identifing this changeset, useful for web
162 167 representation.
163 168 """
164 169 return '0' * 12
165 170
166 171
167 172 def repo2db_mapper(initial_repo_list):
168 173 """
169 174 maps all found repositories into db
170 175 """
171 176 from pylons_app.model.meta import Session
172 177 from pylons_app.model.repo_model import RepoModel
173 178
174 179 sa = Session()
175 180 user = sa.query(User).filter(User.admin == True).first()
176 181
177 182 rm = RepoModel()
178 183
179 184 for name, repo in initial_repo_list.items():
180 185 if not sa.query(Repository).get(name):
181 186 log.info('repository %s not found creating default', name)
182 187
183 188 form_data = {
184 189 'repo_name':name,
185 190 'description':repo.description if repo.description != 'unknown' else \
186 191 'auto description for %s' % name,
187 192 'private':False
188 193 }
189 194 rm.create(form_data, user, just_db=True)
General Comments 0
You need to be logged in to leave comments. Login now