##// END OF EJS Templates
more work on improving info logging
marcink -
r2025:7e979933 beta
parent child Browse files
Show More
@@ -1,92 +1,92 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.__init__
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode, a web based repository management based on pylons
7 7 versioning implementation: http://semver.org/
8 8
9 9 :created_on: Apr 9, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import sys
27 27 import platform
28 28
29 29 VERSION = (1, 3, 0, 'beta')
30 30 __version__ = '.'.join((str(each) for each in VERSION[:4]))
31 31 __dbversion__ = 5 # defines current db version for migrations
32 32 __platform__ = platform.system()
33 33 __license__ = 'GPLv3'
34 34 __py_version__ = sys.version_info
35 35
36 36 PLATFORM_WIN = ('Windows')
37 37 PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS')
38 38
39 39 requirements = [
40 40 "Pylons==1.0.0",
41 41 "Beaker==1.6.2",
42 42 "WebHelpers>=1.2",
43 43 "formencode==1.2.4",
44 44 "SQLAlchemy==0.7.4",
45 45 "Mako==0.5.0",
46 46 "pygments>=1.4",
47 47 "whoosh>=2.3.0,<2.4",
48 48 "celery>=2.2.5,<2.3",
49 49 "babel",
50 50 "python-dateutil>=1.5.0,<2.0.0",
51 51 "dulwich>=0.8.0,<0.9.0",
52 52 "webob==1.0.8",
53 53 "markdown==2.1.1",
54 54 "docutils==0.8.1",
55 55 ]
56 56
57 57 if __py_version__ < (2, 6):
58 58 requirements.append("simplejson")
59 59 requirements.append("pysqlite")
60 60
61 61 if __platform__ in PLATFORM_WIN:
62 62 requirements.append("mercurial>=2.1,<2.2")
63 63 else:
64 64 requirements.append("py-bcrypt")
65 65 requirements.append("mercurial>=2.1,<2.2")
66 66
67 67
68 68 try:
69 69 from rhodecode.lib import get_current_revision
70 70 _rev = get_current_revision()
71 71 except ImportError:
72 72 # this is needed when doing some setup.py operations
73 73 _rev = False
74 74
75 75 if len(VERSION) > 3 and _rev:
76 76 __version__ += ' [rev:%s]' % _rev[0]
77 77
78 78
79 79 def get_version():
80 80 """Returns shorter version (digit parts only) as string."""
81 81
82 82 return '.'.join((str(each) for each in VERSION[:3]))
83 83
84 84 BACKENDS = {
85 85 'hg': 'Mercurial repository',
86 86 'git': 'Git repository',
87 87 }
88 88
89 89 CELERY_ON = False
90 90
91 91 # link to config for pylons
92 CONFIG = None
92 CONFIG = {}
@@ -1,781 +1,785 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.auth
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import random
27 27 import logging
28 28 import traceback
29 29 import hashlib
30 30
31 31 from tempfile import _RandomNameSequence
32 32 from decorator import decorator
33 33
34 34 from pylons import config, url, request
35 35 from pylons.controllers.util import abort, redirect
36 36 from pylons.i18n.translation import _
37 37
38 38 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
39 39 from rhodecode.model.meta import Session
40 40
41 41 if __platform__ in PLATFORM_WIN:
42 42 from hashlib import sha256
43 43 if __platform__ in PLATFORM_OTHERS:
44 44 import bcrypt
45 45
46 46 from rhodecode.lib import str2bool, safe_unicode
47 47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
48 48 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
49 49 from rhodecode.lib.auth_ldap import AuthLdap
50 50
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.user import UserModel
53 53 from rhodecode.model.db import Permission, RhodeCodeSetting, User
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class PasswordGenerator(object):
59 59 """
60 60 This is a simple class for generating password from different sets of
61 61 characters
62 62 usage::
63 63
64 64 passwd_gen = PasswordGenerator()
65 65 #print 8-letter password containing only big and small letters
66 66 of alphabet
67 67 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
68 68 """
69 69 ALPHABETS_NUM = r'''1234567890'''
70 70 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
71 71 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
72 72 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
73 73 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
74 74 + ALPHABETS_NUM + ALPHABETS_SPECIAL
75 75 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
76 76 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
77 77 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
78 78 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
79 79
80 80 def __init__(self, passwd=''):
81 81 self.passwd = passwd
82 82
83 83 def gen_password(self, length, type_=None):
84 84 if type_ is None:
85 85 type_ = self.ALPHABETS_FULL
86 86 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
87 87 return self.passwd
88 88
89 89
90 90 class RhodeCodeCrypto(object):
91 91
92 92 @classmethod
93 93 def hash_string(cls, str_):
94 94 """
95 95 Cryptographic function used for password hashing based on pybcrypt
96 96 or pycrypto in windows
97 97
98 98 :param password: password to hash
99 99 """
100 100 if __platform__ in PLATFORM_WIN:
101 101 return sha256(str_).hexdigest()
102 102 elif __platform__ in PLATFORM_OTHERS:
103 103 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
104 104 else:
105 105 raise Exception('Unknown or unsupported platform %s' \
106 106 % __platform__)
107 107
108 108 @classmethod
109 109 def hash_check(cls, password, hashed):
110 110 """
111 111 Checks matching password with it's hashed value, runs different
112 112 implementation based on platform it runs on
113 113
114 114 :param password: password
115 115 :param hashed: password in hashed form
116 116 """
117 117
118 118 if __platform__ in PLATFORM_WIN:
119 119 return sha256(password).hexdigest() == hashed
120 120 elif __platform__ in PLATFORM_OTHERS:
121 121 return bcrypt.hashpw(password, hashed) == hashed
122 122 else:
123 123 raise Exception('Unknown or unsupported platform %s' \
124 124 % __platform__)
125 125
126 126
127 127 def get_crypt_password(password):
128 128 return RhodeCodeCrypto.hash_string(password)
129 129
130 130
131 131 def check_password(password, hashed):
132 132 return RhodeCodeCrypto.hash_check(password, hashed)
133 133
134 134
135 135 def generate_api_key(str_, salt=None):
136 136 """
137 137 Generates API KEY from given string
138 138
139 139 :param str_:
140 140 :param salt:
141 141 """
142 142
143 143 if salt is None:
144 144 salt = _RandomNameSequence().next()
145 145
146 146 return hashlib.sha1(str_ + salt).hexdigest()
147 147
148 148
149 149 def authfunc(environ, username, password):
150 150 """
151 151 Dummy authentication wrapper function used in Mercurial and Git for
152 152 access control.
153 153
154 154 :param environ: needed only for using in Basic auth
155 155 """
156 156 return authenticate(username, password)
157 157
158 158
159 159 def authenticate(username, password):
160 160 """
161 161 Authentication function used for access control,
162 162 firstly checks for db authentication then if ldap is enabled for ldap
163 163 authentication, also creates ldap user if not in database
164 164
165 165 :param username: username
166 166 :param password: password
167 167 """
168 168
169 169 user_model = UserModel()
170 170 user = User.get_by_username(username)
171 171
172 172 log.debug('Authenticating user using RhodeCode account')
173 173 if user is not None and not user.ldap_dn:
174 174 if user.active:
175 175 if user.username == 'default' and user.active:
176 log.info('user %s authenticated correctly as anonymous user',
176 log.info('user %s authenticated correctly as anonymous user' %
177 177 username)
178 178 return True
179 179
180 180 elif user.username == username and check_password(password,
181 181 user.password):
182 182 log.info('user %s authenticated correctly' % username)
183 183 return True
184 184 else:
185 log.warning('user %s is disabled' % username)
185 log.warning('user %s tried auth but is disabled' % username)
186 186
187 187 else:
188 188 log.debug('Regular authentication failed')
189 189 user_obj = User.get_by_username(username, case_insensitive=True)
190 190
191 191 if user_obj is not None and not user_obj.ldap_dn:
192 192 log.debug('this user already exists as non ldap')
193 193 return False
194 194
195 195 ldap_settings = RhodeCodeSetting.get_ldap_settings()
196 196 #======================================================================
197 197 # FALLBACK TO LDAP AUTH IF ENABLE
198 198 #======================================================================
199 199 if str2bool(ldap_settings.get('ldap_active')):
200 200 log.debug("Authenticating user using ldap")
201 201 kwargs = {
202 202 'server': ldap_settings.get('ldap_host', ''),
203 203 'base_dn': ldap_settings.get('ldap_base_dn', ''),
204 204 'port': ldap_settings.get('ldap_port'),
205 205 'bind_dn': ldap_settings.get('ldap_dn_user'),
206 206 'bind_pass': ldap_settings.get('ldap_dn_pass'),
207 207 'tls_kind': ldap_settings.get('ldap_tls_kind'),
208 208 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
209 209 'ldap_filter': ldap_settings.get('ldap_filter'),
210 210 'search_scope': ldap_settings.get('ldap_search_scope'),
211 211 'attr_login': ldap_settings.get('ldap_attr_login'),
212 212 'ldap_version': 3,
213 213 }
214 214 log.debug('Checking for ldap authentication')
215 215 try:
216 216 aldap = AuthLdap(**kwargs)
217 217 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
218 218 password)
219 219 log.debug('Got ldap DN response %s' % user_dn)
220 220
221 221 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
222 222 .get(k), [''])[0]
223 223
224 224 user_attrs = {
225 225 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
226 226 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
227 227 'email': get_ldap_attr('ldap_attr_email'),
228 228 }
229 229
230 230 # don't store LDAP password since we don't need it. Override
231 231 # with some random generated password
232 232 _password = PasswordGenerator().gen_password(length=8)
233 233 # create this user on the fly if it doesn't exist in rhodecode
234 234 # database
235 235 if user_model.create_ldap(username, _password, user_dn,
236 236 user_attrs):
237 237 log.info('created new ldap user %s' % username)
238 238
239 239 Session.commit()
240 240 return True
241 241 except (LdapUsernameError, LdapPasswordError,):
242 242 pass
243 243 except (Exception,):
244 244 log.error(traceback.format_exc())
245 245 pass
246 246 return False
247 247
248 248
249 249 def login_container_auth(username):
250 250 user = User.get_by_username(username)
251 251 if user is None:
252 252 user_attrs = {
253 253 'name': username,
254 254 'lastname': None,
255 255 'email': None,
256 256 }
257 257 user = UserModel().create_for_container_auth(username, user_attrs)
258 258 if not user:
259 259 return None
260 260 log.info('User %s was created by container authentication' % username)
261 261
262 262 if not user.active:
263 263 return None
264 264
265 265 user.update_lastlogin()
266 266 Session.commit()
267 267
268 268 log.debug('User %s is now logged in by container authentication',
269 269 user.username)
270 270 return user
271 271
272 272
273 273 def get_container_username(environ, config):
274 274 username = None
275 275
276 276 if str2bool(config.get('container_auth_enabled', False)):
277 277 from paste.httpheaders import REMOTE_USER
278 278 username = REMOTE_USER(environ)
279 279
280 280 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
281 281 username = environ.get('HTTP_X_FORWARDED_USER')
282 282
283 283 if username:
284 284 # Removing realm and domain from username
285 285 username = username.partition('@')[0]
286 286 username = username.rpartition('\\')[2]
287 287 log.debug('Received username %s from container' % username)
288 288
289 289 return username
290 290
291 291
292 292 class AuthUser(object):
293 293 """
294 294 A simple object that handles all attributes of user in RhodeCode
295 295
296 296 It does lookup based on API key,given user, or user present in session
297 297 Then it fills all required information for such user. It also checks if
298 298 anonymous access is enabled and if so, it returns default user as logged
299 299 in
300 300 """
301 301
302 302 def __init__(self, user_id=None, api_key=None, username=None):
303 303
304 304 self.user_id = user_id
305 305 self.api_key = None
306 306 self.username = username
307 307
308 308 self.name = ''
309 309 self.lastname = ''
310 310 self.email = ''
311 311 self.is_authenticated = False
312 312 self.admin = False
313 313 self.permissions = {}
314 314 self._api_key = api_key
315 315 self.propagate_data()
316 316 self._instance = None
317 317
318 318 def propagate_data(self):
319 319 user_model = UserModel()
320 320 self.anonymous_user = User.get_by_username('default', cache=True)
321 321 is_user_loaded = False
322 322
323 323 # try go get user by api key
324 324 if self._api_key and self._api_key != self.anonymous_user.api_key:
325 325 log.debug('Auth User lookup by API KEY %s' % self._api_key)
326 326 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
327 327 # lookup by userid
328 328 elif (self.user_id is not None and
329 329 self.user_id != self.anonymous_user.user_id):
330 330 log.debug('Auth User lookup by USER ID %s' % self.user_id)
331 331 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
332 332 # lookup by username
333 333 elif self.username and \
334 334 str2bool(config.get('container_auth_enabled', False)):
335 335
336 336 log.debug('Auth User lookup by USER NAME %s' % self.username)
337 337 dbuser = login_container_auth(self.username)
338 338 if dbuser is not None:
339 339 for k, v in dbuser.get_dict().items():
340 340 setattr(self, k, v)
341 341 self.set_authenticated()
342 342 is_user_loaded = True
343 343
344 344 if not is_user_loaded:
345 345 # if we cannot authenticate user try anonymous
346 346 if self.anonymous_user.active is True:
347 347 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
348 348 # then we set this user is logged in
349 349 self.is_authenticated = True
350 350 else:
351 351 self.user_id = None
352 352 self.username = None
353 353 self.is_authenticated = False
354 354
355 355 if not self.username:
356 356 self.username = 'None'
357 357
358 358 log.debug('Auth User is now %s' % self)
359 359 user_model.fill_perms(self)
360 360
361 361 @property
362 362 def is_admin(self):
363 363 return self.admin
364 364
365 365 def __repr__(self):
366 366 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
367 367 self.is_authenticated)
368 368
369 369 def set_authenticated(self, authenticated=True):
370 370 if self.user_id != self.anonymous_user.user_id:
371 371 self.is_authenticated = authenticated
372 372
373 373 def get_cookie_store(self):
374 374 return {'username': self.username,
375 375 'user_id': self.user_id,
376 376 'is_authenticated': self.is_authenticated}
377 377
378 378 @classmethod
379 379 def from_cookie_store(cls, cookie_store):
380 380 user_id = cookie_store.get('user_id')
381 381 username = cookie_store.get('username')
382 382 api_key = cookie_store.get('api_key')
383 383 return AuthUser(user_id, api_key, username)
384 384
385 385
386 386 def set_available_permissions(config):
387 387 """
388 388 This function will propagate pylons globals with all available defined
389 389 permission given in db. We don't want to check each time from db for new
390 390 permissions since adding a new permission also requires application restart
391 391 ie. to decorate new views with the newly created permission
392 392
393 393 :param config: current pylons config instance
394 394
395 395 """
396 396 log.info('getting information about all available permissions')
397 397 try:
398 398 sa = meta.Session
399 399 all_perms = sa.query(Permission).all()
400 400 except Exception:
401 401 pass
402 402 finally:
403 403 meta.Session.remove()
404 404
405 405 config['available_permissions'] = [x.permission_name for x in all_perms]
406 406
407 407
408 408 #==============================================================================
409 409 # CHECK DECORATORS
410 410 #==============================================================================
411 411 class LoginRequired(object):
412 412 """
413 413 Must be logged in to execute this function else
414 414 redirect to login page
415 415
416 416 :param api_access: if enabled this checks only for valid auth token
417 417 and grants access based on valid token
418 418 """
419 419
420 420 def __init__(self, api_access=False):
421 421 self.api_access = api_access
422 422
423 423 def __call__(self, func):
424 424 return decorator(self.__wrapper, func)
425 425
426 426 def __wrapper(self, func, *fargs, **fkwargs):
427 427 cls = fargs[0]
428 428 user = cls.rhodecode_user
429 429
430 430 api_access_ok = False
431 431 if self.api_access:
432 432 log.debug('Checking API KEY access for %s' % cls)
433 433 if user.api_key == request.GET.get('api_key'):
434 434 api_access_ok = True
435 435 else:
436 436 log.debug("API KEY token not valid")
437
438 log.debug('Checking if %s is authenticated @ %s' % (user.username, cls))
437 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
438 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
439 439 if user.is_authenticated or api_access_ok:
440 log.debug('user %s is authenticated' % user.username)
440 log.info('user %s is authenticated and granted access to %s' % (
441 user.username, loc)
442 )
441 443 return func(*fargs, **fkwargs)
442 444 else:
443 log.warn('user %s NOT authenticated' % user.username)
445 log.warn('user %s NOT authenticated on func: %s' % (
446 user, loc)
447 )
444 448 p = url.current()
445 449
446 450 log.debug('redirecting to login page with %s' % p)
447 451 return redirect(url('login_home', came_from=p))
448 452
449 453
450 454 class NotAnonymous(object):
451 455 """
452 456 Must be logged in to execute this function else
453 457 redirect to login page"""
454 458
455 459 def __call__(self, func):
456 460 return decorator(self.__wrapper, func)
457 461
458 462 def __wrapper(self, func, *fargs, **fkwargs):
459 463 cls = fargs[0]
460 464 self.user = cls.rhodecode_user
461 465
462 466 log.debug('Checking if user is not anonymous @%s' % cls)
463 467
464 468 anonymous = self.user.username == 'default'
465 469
466 470 if anonymous:
467 471 p = url.current()
468 472
469 473 import rhodecode.lib.helpers as h
470 474 h.flash(_('You need to be a registered user to '
471 475 'perform this action'),
472 476 category='warning')
473 477 return redirect(url('login_home', came_from=p))
474 478 else:
475 479 return func(*fargs, **fkwargs)
476 480
477 481
478 482 class PermsDecorator(object):
479 483 """Base class for controller decorators"""
480 484
481 485 def __init__(self, *required_perms):
482 486 available_perms = config['available_permissions']
483 487 for perm in required_perms:
484 488 if perm not in available_perms:
485 489 raise Exception("'%s' permission is not defined" % perm)
486 490 self.required_perms = set(required_perms)
487 491 self.user_perms = None
488 492
489 493 def __call__(self, func):
490 494 return decorator(self.__wrapper, func)
491 495
492 496 def __wrapper(self, func, *fargs, **fkwargs):
493 497 cls = fargs[0]
494 498 self.user = cls.rhodecode_user
495 499 self.user_perms = self.user.permissions
496 500 log.debug('checking %s permissions %s for %s %s',
497 501 self.__class__.__name__, self.required_perms, cls,
498 502 self.user)
499 503
500 504 if self.check_permissions():
501 505 log.debug('Permission granted for %s %s' % (cls, self.user))
502 506 return func(*fargs, **fkwargs)
503 507
504 508 else:
505 log.warning('Permission denied for %s %s' % (cls, self.user))
509 log.debug('Permission denied for %s %s' % (cls, self.user))
506 510 anonymous = self.user.username == 'default'
507 511
508 512 if anonymous:
509 513 p = url.current()
510 514
511 515 import rhodecode.lib.helpers as h
512 516 h.flash(_('You need to be a signed in to '
513 517 'view this page'),
514 518 category='warning')
515 519 return redirect(url('login_home', came_from=p))
516 520
517 521 else:
518 522 # redirect with forbidden ret code
519 523 return abort(403)
520 524
521 525 def check_permissions(self):
522 526 """Dummy function for overriding"""
523 527 raise Exception('You have to write this function in child class')
524 528
525 529
526 530 class HasPermissionAllDecorator(PermsDecorator):
527 531 """
528 532 Checks for access permission for all given predicates. All of them
529 533 have to be meet in order to fulfill the request
530 534 """
531 535
532 536 def check_permissions(self):
533 537 if self.required_perms.issubset(self.user_perms.get('global')):
534 538 return True
535 539 return False
536 540
537 541
538 542 class HasPermissionAnyDecorator(PermsDecorator):
539 543 """
540 544 Checks for access permission for any of given predicates. In order to
541 545 fulfill the request any of predicates must be meet
542 546 """
543 547
544 548 def check_permissions(self):
545 549 if self.required_perms.intersection(self.user_perms.get('global')):
546 550 return True
547 551 return False
548 552
549 553
550 554 class HasRepoPermissionAllDecorator(PermsDecorator):
551 555 """
552 556 Checks for access permission for all given predicates for specific
553 557 repository. All of them have to be meet in order to fulfill the request
554 558 """
555 559
556 560 def check_permissions(self):
557 561 repo_name = get_repo_slug(request)
558 562 try:
559 563 user_perms = set([self.user_perms['repositories'][repo_name]])
560 564 except KeyError:
561 565 return False
562 566 if self.required_perms.issubset(user_perms):
563 567 return True
564 568 return False
565 569
566 570
567 571 class HasRepoPermissionAnyDecorator(PermsDecorator):
568 572 """
569 573 Checks for access permission for any of given predicates for specific
570 574 repository. In order to fulfill the request any of predicates must be meet
571 575 """
572 576
573 577 def check_permissions(self):
574 578 repo_name = get_repo_slug(request)
575 579
576 580 try:
577 581 user_perms = set([self.user_perms['repositories'][repo_name]])
578 582 except KeyError:
579 583 return False
580 584 if self.required_perms.intersection(user_perms):
581 585 return True
582 586 return False
583 587
584 588
585 589 class HasReposGroupPermissionAllDecorator(PermsDecorator):
586 590 """
587 591 Checks for access permission for all given predicates for specific
588 592 repository. All of them have to be meet in order to fulfill the request
589 593 """
590 594
591 595 def check_permissions(self):
592 596 group_name = get_repos_group_slug(request)
593 597 try:
594 598 user_perms = set([self.user_perms['repositories_groups'][group_name]])
595 599 except KeyError:
596 600 return False
597 601 if self.required_perms.issubset(user_perms):
598 602 return True
599 603 return False
600 604
601 605
602 606 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
603 607 """
604 608 Checks for access permission for any of given predicates for specific
605 609 repository. In order to fulfill the request any of predicates must be meet
606 610 """
607 611
608 612 def check_permissions(self):
609 613 group_name = get_repos_group_slug(request)
610 614
611 615 try:
612 616 user_perms = set([self.user_perms['repositories_groups'][group_name]])
613 617 except KeyError:
614 618 return False
615 619 if self.required_perms.intersection(user_perms):
616 620 return True
617 621 return False
618 622
619 623
620 624 #==============================================================================
621 625 # CHECK FUNCTIONS
622 626 #==============================================================================
623 627 class PermsFunction(object):
624 628 """Base function for other check functions"""
625 629
626 630 def __init__(self, *perms):
627 631 available_perms = config['available_permissions']
628 632
629 633 for perm in perms:
630 634 if perm not in available_perms:
631 635 raise Exception("'%s' permission in not defined" % perm)
632 636 self.required_perms = set(perms)
633 637 self.user_perms = None
634 638 self.granted_for = ''
635 639 self.repo_name = None
636 640
637 641 def __call__(self, check_Location=''):
638 642 user = request.user
639 643 if not user:
640 644 return False
641 645 self.user_perms = user.permissions
642 646 self.granted_for = user
643 647 log.debug('checking %s %s %s', self.__class__.__name__,
644 648 self.required_perms, user)
645 649
646 650 if self.check_permissions():
647 651 log.debug('Permission granted %s @ %s', self.granted_for,
648 652 check_Location or 'unspecified location')
649 653 return True
650 654
651 655 else:
652 log.warning('Permission denied for %s @ %s', self.granted_for,
656 log.debug('Permission denied for %s @ %s', self.granted_for,
653 657 check_Location or 'unspecified location')
654 658 return False
655 659
656 660 def check_permissions(self):
657 661 """Dummy function for overriding"""
658 662 raise Exception('You have to write this function in child class')
659 663
660 664
661 665 class HasPermissionAll(PermsFunction):
662 666 def check_permissions(self):
663 667 if self.required_perms.issubset(self.user_perms.get('global')):
664 668 return True
665 669 return False
666 670
667 671
668 672 class HasPermissionAny(PermsFunction):
669 673 def check_permissions(self):
670 674 if self.required_perms.intersection(self.user_perms.get('global')):
671 675 return True
672 676 return False
673 677
674 678
675 679 class HasRepoPermissionAll(PermsFunction):
676 680
677 681 def __call__(self, repo_name=None, check_Location=''):
678 682 self.repo_name = repo_name
679 683 return super(HasRepoPermissionAll, self).__call__(check_Location)
680 684
681 685 def check_permissions(self):
682 686 if not self.repo_name:
683 687 self.repo_name = get_repo_slug(request)
684 688
685 689 try:
686 690 self.user_perms = set(
687 691 [self.user_perms['repositories'][self.repo_name]]
688 692 )
689 693 except KeyError:
690 694 return False
691 695 self.granted_for = self.repo_name
692 696 if self.required_perms.issubset(self.user_perms):
693 697 return True
694 698 return False
695 699
696 700
697 701 class HasRepoPermissionAny(PermsFunction):
698 702
699 703 def __call__(self, repo_name=None, check_Location=''):
700 704 self.repo_name = repo_name
701 705 return super(HasRepoPermissionAny, self).__call__(check_Location)
702 706
703 707 def check_permissions(self):
704 708 if not self.repo_name:
705 709 self.repo_name = get_repo_slug(request)
706 710
707 711 try:
708 712 self.user_perms = set(
709 713 [self.user_perms['repositories'][self.repo_name]]
710 714 )
711 715 except KeyError:
712 716 return False
713 717 self.granted_for = self.repo_name
714 718 if self.required_perms.intersection(self.user_perms):
715 719 return True
716 720 return False
717 721
718 722
719 723 class HasReposGroupPermissionAny(PermsFunction):
720 724 def __call__(self, group_name=None, check_Location=''):
721 725 self.group_name = group_name
722 726 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
723 727
724 728 def check_permissions(self):
725 729 try:
726 730 self.user_perms = set(
727 731 [self.user_perms['repositories_groups'][self.group_name]]
728 732 )
729 733 except KeyError:
730 734 return False
731 735 self.granted_for = self.repo_name
732 736 if self.required_perms.intersection(self.user_perms):
733 737 return True
734 738 return False
735 739
736 740
737 741 class HasReposGroupPermissionAll(PermsFunction):
738 742 def __call__(self, group_name=None, check_Location=''):
739 743 self.group_name = group_name
740 744 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
741 745
742 746 def check_permissions(self):
743 747 try:
744 748 self.user_perms = set(
745 749 [self.user_perms['repositories_groups'][self.group_name]]
746 750 )
747 751 except KeyError:
748 752 return False
749 753 self.granted_for = self.repo_name
750 754 if self.required_perms.issubset(self.user_perms):
751 755 return True
752 756 return False
753 757
754 758
755 759 #==============================================================================
756 760 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
757 761 #==============================================================================
758 762 class HasPermissionAnyMiddleware(object):
759 763 def __init__(self, *perms):
760 764 self.required_perms = set(perms)
761 765
762 766 def __call__(self, user, repo_name):
763 767 usr = AuthUser(user.user_id)
764 768 try:
765 769 self.user_perms = set([usr.permissions['repositories'][repo_name]])
766 770 except:
767 771 self.user_perms = set()
768 772 self.granted_for = ''
769 773 self.username = user.username
770 774 self.repo_name = repo_name
771 775 return self.check_permissions()
772 776
773 777 def check_permissions(self):
774 778 log.debug('checking mercurial protocol '
775 779 'permissions %s for user:%s repository:%s', self.user_perms,
776 780 self.username, self.repo_name)
777 781 if self.required_perms.intersection(self.user_perms):
778 782 log.debug('permission granted')
779 783 return True
780 784 log.debug('permission denied')
781 785 return False
@@ -1,181 +1,184 b''
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 6 import time
7 7 import traceback
8 8
9 9 from paste.auth.basic import AuthBasicAuthenticator
10 10
11 11 from pylons import config, tmpl_context as c, request, session, url
12 12 from pylons.controllers import WSGIController
13 13 from pylons.controllers.util import redirect
14 14 from pylons.templating import render_mako as render
15 15
16 16 from rhodecode import __version__, BACKENDS
17 17
18 18 from rhodecode.lib import str2bool
19 19 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
20 20 HasPermissionAnyMiddleware
21 21 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
22 22 from rhodecode.model import meta
23 23
24 24 from rhodecode.model.db import Repository
25 25 from rhodecode.model.notification import NotificationModel
26 26 from rhodecode.model.scm import ScmModel
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 class BaseVCSController(object):
32 32
33 33 def __init__(self, application, config):
34 34 self.application = application
35 35 self.config = config
36 36 # base path of repo locations
37 37 self.basepath = self.config['base_path']
38 38 #authenticate this mercurial request using authfunc
39 39 self.authenticate = AuthBasicAuthenticator('', authfunc)
40 40 self.ipaddr = '0.0.0.0'
41 41
42 42 def _handle_request(self, environ, start_response):
43 43 raise NotImplementedError()
44 44
45 45 def _get_by_id(self, repo_name):
46 46 """
47 47 Get's a special pattern _<ID> from clone url and tries to replace it
48 48 with a repository_name for support of _<ID> non changable urls
49 49
50 50 :param repo_name:
51 51 """
52 52 try:
53 53 data = repo_name.split('/')
54 54 if len(data) >= 2:
55 55 by_id = data[1].split('_')
56 56 if len(by_id) == 2 and by_id[1].isdigit():
57 57 _repo_name = Repository.get(by_id[1]).repo_name
58 58 data[1] = _repo_name
59 59 except:
60 60 log.debug('Failed to extract repo_name from id %s' % (
61 61 traceback.format_exc()
62 62 )
63 63 )
64 64
65 65 return '/'.join(data)
66 66
67 67 def _invalidate_cache(self, repo_name):
68 68 """
69 69 Set's cache for this repository for invalidation on next access
70 70
71 71 :param repo_name: full repo name, also a cache key
72 72 """
73 73 invalidate_cache('get_repo_cached_%s' % repo_name)
74 74
75 75 def _check_permission(self, action, user, repo_name):
76 76 """
77 77 Checks permissions using action (push/pull) user and repository
78 78 name
79 79
80 80 :param action: push or pull action
81 81 :param user: user instance
82 82 :param repo_name: repository name
83 83 """
84 84 if action == 'push':
85 85 if not HasPermissionAnyMiddleware('repository.write',
86 86 'repository.admin')(user,
87 87 repo_name):
88 88 return False
89 89
90 90 else:
91 91 #any other action need at least read permission
92 92 if not HasPermissionAnyMiddleware('repository.read',
93 93 'repository.write',
94 94 'repository.admin')(user,
95 95 repo_name):
96 96 return False
97 97
98 98 return True
99 99
100 100 def __call__(self, environ, start_response):
101 101 start = time.time()
102 102 try:
103 103 return self._handle_request(environ, start_response)
104 104 finally:
105 105 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
106 106 log.debug('Request time: %.3fs' % (time.time() - start))
107 107 meta.Session.remove()
108 108
109 109
110 110 class BaseController(WSGIController):
111 111
112 112 def __before__(self):
113 113 c.rhodecode_version = __version__
114 114 c.rhodecode_instanceid = config.get('instance_id')
115 115 c.rhodecode_name = config.get('rhodecode_title')
116 116 c.use_gravatar = str2bool(config.get('use_gravatar'))
117 117 c.ga_code = config.get('rhodecode_ga_code')
118 118 c.repo_name = get_repo_slug(request)
119 119 c.backends = BACKENDS.keys()
120 120 c.unread_notifications = NotificationModel()\
121 121 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
122 122 self.cut_off_limit = int(config.get('cut_off_limit'))
123 123
124 124 self.sa = meta.Session
125 125 self.scm_model = ScmModel(self.sa)
126 126
127 127 def __call__(self, environ, start_response):
128 128 """Invoke the Controller"""
129 129 # WSGIController.__call__ dispatches to the Controller method
130 130 # the request is routed to. This routing information is
131 131 # available in environ['pylons.routes_dict']
132 132 start = time.time()
133 133 try:
134 134 # make sure that we update permissions each time we call controller
135 135 api_key = request.GET.get('api_key')
136 136 cookie_store = session.get('rhodecode_user') or {}
137 137 user_id = cookie_store.get('user_id', None)
138 138 username = get_container_username(environ, config)
139 139
140 140 auth_user = AuthUser(user_id, api_key, username)
141 141 request.user = auth_user
142 142 self.rhodecode_user = c.rhodecode_user = auth_user
143 143 if not self.rhodecode_user.is_authenticated and \
144 144 self.rhodecode_user.user_id is not None:
145 145 self.rhodecode_user\
146 146 .set_authenticated(cookie_store.get('is_authenticated'))
147 147
148 148 session['rhodecode_user'] = self.rhodecode_user.get_cookie_store()
149 149 session.save()
150 log.info('User: %s accessed %s' % (auth_user,
151 environ.get('PATH_INFO')))
150 152 return WSGIController.__call__(self, environ, start_response)
151 153 finally:
152 log.debug('Request time: %.3fs' % (time.time() - start))
154 log.info('Request to %s time: %.3fs' % (environ.get('PATH_INFO'),
155 time.time() - start))
153 156 meta.Session.remove()
154 157
155 158
156 159 class BaseRepoController(BaseController):
157 160 """
158 161 Base class for controllers responsible for loading all needed data for
159 162 repository loaded items are
160 163
161 164 c.rhodecode_repo: instance of scm repository
162 165 c.rhodecode_db_repo: instance of db
163 166 c.repository_followers: number of followers
164 167 c.repository_forks: number of forks
165 168 """
166 169
167 170 def __before__(self):
168 171 super(BaseRepoController, self).__before__()
169 172 if c.repo_name:
170 173
171 174 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
172 175 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
173 176
174 177 if c.rhodecode_repo is None:
175 178 log.error('%s this repository is present in database but it '
176 179 'cannot be created as an scm instance', c.repo_name)
177 180
178 181 redirect(url('home'))
179 182
180 183 c.repository_followers = self.scm_model.get_followers(c.repo_name)
181 184 c.repository_forks = self.scm_model.get_forks(c.repo_name)
@@ -1,139 +1,139 b''
1 1 """
2 2 This module provides some useful tools for ``vcs`` like annotate/diff html
3 3 output. It also includes some internal helpers.
4 4 """
5 5 import sys
6 6 import time
7 7 import datetime
8 8
9 9
10 10 def makedate():
11 11 lt = time.localtime()
12 12 if lt[8] == 1 and time.daylight:
13 13 tz = time.altzone
14 14 else:
15 15 tz = time.timezone
16 16 return time.mktime(lt), tz
17 17
18 18
19 19 def date_fromtimestamp(unixts, tzoffset=0):
20 20 """
21 21 Makes a local datetime object out of unix timestamp
22 22
23 23 :param unixts:
24 24 :param tzoffset:
25 25 """
26 26
27 27 return datetime.datetime.fromtimestamp(float(unixts))
28 28
29 29
30 30 def safe_unicode(str_, from_encoding=None):
31 31 """
32 32 safe unicode function. Does few trick to turn str_ into unicode
33 33
34 34 In case of UnicodeDecode error we try to return it with encoding detected
35 35 by chardet library if it fails fallback to unicode with errors replaced
36 36
37 37 :param str_: string to decode
38 38 :rtype: unicode
39 39 :returns: unicode object
40 40 """
41 41 if isinstance(str_, unicode):
42 42 return str_
43 43 if not from_encoding:
44 44 import rhodecode
45 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
45 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
46 46 from_encoding = DEFAULT_ENCODING
47 47 try:
48 48 return unicode(str_)
49 49 except UnicodeDecodeError:
50 50 pass
51 51
52 52 try:
53 53 return unicode(str_, from_encoding)
54 54 except UnicodeDecodeError:
55 55 pass
56 56
57 57 try:
58 58 import chardet
59 59 encoding = chardet.detect(str_)['encoding']
60 60 if encoding is None:
61 61 raise Exception()
62 62 return str_.decode(encoding)
63 63 except (ImportError, UnicodeDecodeError, Exception):
64 64 return unicode(str_, from_encoding, 'replace')
65 65
66 66
67 67 def safe_str(unicode_, to_encoding=None):
68 68 """
69 69 safe str function. Does few trick to turn unicode_ into string
70 70
71 71 In case of UnicodeEncodeError we try to return it with encoding detected
72 72 by chardet library if it fails fallback to string with errors replaced
73 73
74 74 :param unicode_: unicode to encode
75 75 :rtype: str
76 76 :returns: str object
77 77 """
78 78
79 79 if isinstance(unicode_, str):
80 80 return unicode_
81 81 if not to_encoding:
82 82 import rhodecode
83 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
83 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
84 84 to_encoding = DEFAULT_ENCODING
85 85 try:
86 86 return unicode_.encode(to_encoding)
87 87 except UnicodeEncodeError:
88 88 pass
89 89
90 90 try:
91 91 import chardet
92 92 encoding = chardet.detect(unicode_)['encoding']
93 93 print encoding
94 94 if encoding is None:
95 95 raise UnicodeEncodeError()
96 96
97 97 return unicode_.encode(encoding)
98 98 except (ImportError, UnicodeEncodeError):
99 99 return unicode_.encode(to_encoding, 'replace')
100 100
101 101 return safe_str
102 102
103 103
104 104 def author_email(author):
105 105 """
106 106 returns email address of given author.
107 107 If any of <,> sign are found, it fallbacks to regex findall()
108 108 and returns first found result or empty string
109 109
110 110 Regex taken from http://www.regular-expressions.info/email.html
111 111 """
112 112 import re
113 113 r = author.find('>')
114 114 l = author.find('<')
115 115
116 116 if l == -1 or r == -1:
117 117 # fallback to regex match of email out of a string
118 118 email_re = re.compile(r"""[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!"""
119 119 r"""#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z"""
120 120 r"""0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]"""
121 121 r"""*[a-z0-9])?""", re.IGNORECASE)
122 122 m = re.findall(email_re, author)
123 123 return m[0] if m else ''
124 124
125 125 return author[l + 1:r].strip()
126 126
127 127
128 128 def author_name(author):
129 129 """
130 130 get name of author, or else username.
131 131 It'll try to find an email in the author string and just cut it off
132 132 to get the username
133 133 """
134 134
135 135 if not '@' in author:
136 136 return author
137 137 else:
138 138 return author.replace(author_email(author), '').replace('<', '')\
139 139 .replace('>', '').strip()
@@ -1,758 +1,758 b''
1 1 """ this is forms validation classes
2 2 http://formencode.org/module-formencode.validators.html
3 3 for list off all availible validators
4 4
5 5 we can create our own validators
6 6
7 7 The table below outlines the options which can be used in a schema in addition to the validators themselves
8 8 pre_validators [] These validators will be applied before the schema
9 9 chained_validators [] These validators will be applied after the schema
10 10 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
11 11 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
12 12 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
13 13 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
14 14
15 15
16 16 <name> = formencode.validators.<name of validator>
17 17 <name> must equal form name
18 18 list=[1,2,3,4,5]
19 19 for SELECT use formencode.All(OneOf(list), Int())
20 20
21 21 """
22 22 import os
23 23 import re
24 24 import logging
25 25 import traceback
26 26
27 27 import formencode
28 28 from formencode import All
29 29 from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
30 30 Email, Bool, StringBoolean, Set
31 31
32 32 from pylons.i18n.translation import _
33 33 from webhelpers.pylonslib.secure_form import authentication_token
34 34
35 35 from rhodecode.config.routing import ADMIN_PREFIX
36 36 from rhodecode.lib.utils import repo_name_slug
37 37 from rhodecode.lib.auth import authenticate, get_crypt_password
38 38 from rhodecode.lib.exceptions import LdapImportError
39 39 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
40 40 from rhodecode import BACKENDS
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 #this is needed to translate the messages using _() in validators
46 46 class State_obj(object):
47 47 _ = staticmethod(_)
48 48
49 49
50 50 #==============================================================================
51 51 # VALIDATORS
52 52 #==============================================================================
53 53 class ValidAuthToken(formencode.validators.FancyValidator):
54 54 messages = {'invalid_token': _('Token mismatch')}
55 55
56 56 def validate_python(self, value, state):
57 57
58 58 if value != authentication_token():
59 59 raise formencode.Invalid(
60 60 self.message('invalid_token',
61 61 state, search_number=value),
62 62 value,
63 63 state
64 64 )
65 65
66 66
67 67 def ValidUsername(edit, old_data):
68 68 class _ValidUsername(formencode.validators.FancyValidator):
69 69
70 70 def validate_python(self, value, state):
71 71 if value in ['default', 'new_user']:
72 72 raise formencode.Invalid(_('Invalid username'), value, state)
73 73 #check if user is unique
74 74 old_un = None
75 75 if edit:
76 76 old_un = User.get(old_data.get('user_id')).username
77 77
78 78 if old_un != value or not edit:
79 79 if User.get_by_username(value, case_insensitive=True):
80 80 raise formencode.Invalid(_('This username already '
81 81 'exists') , value, state)
82 82
83 83 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
84 84 raise formencode.Invalid(
85 85 _('Username may only contain alphanumeric characters '
86 86 'underscores, periods or dashes and must begin with '
87 87 'alphanumeric character'),
88 88 value,
89 89 state
90 90 )
91 91
92 92 return _ValidUsername
93 93
94 94
95 95 def ValidUsersGroup(edit, old_data):
96 96
97 97 class _ValidUsersGroup(formencode.validators.FancyValidator):
98 98
99 99 def validate_python(self, value, state):
100 100 if value in ['default']:
101 101 raise formencode.Invalid(_('Invalid group name'), value, state)
102 102 #check if group is unique
103 103 old_ugname = None
104 104 if edit:
105 105 old_ugname = UsersGroup.get(
106 106 old_data.get('users_group_id')).users_group_name
107 107
108 108 if old_ugname != value or not edit:
109 109 if UsersGroup.get_by_group_name(value, cache=False,
110 110 case_insensitive=True):
111 111 raise formencode.Invalid(_('This users group '
112 112 'already exists'), value,
113 113 state)
114 114
115 115 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
116 116 raise formencode.Invalid(
117 117 _('RepoGroup name may only contain alphanumeric characters '
118 118 'underscores, periods or dashes and must begin with '
119 119 'alphanumeric character'),
120 120 value,
121 121 state
122 122 )
123 123
124 124 return _ValidUsersGroup
125 125
126 126
127 127 def ValidReposGroup(edit, old_data):
128 128 class _ValidReposGroup(formencode.validators.FancyValidator):
129 129
130 130 def validate_python(self, value, state):
131 131 # TODO WRITE VALIDATIONS
132 132 group_name = value.get('group_name')
133 133 group_parent_id = value.get('group_parent_id')
134 134
135 135 # slugify repo group just in case :)
136 136 slug = repo_name_slug(group_name)
137 137
138 138 # check for parent of self
139 139 parent_of_self = lambda: (
140 140 old_data['group_id'] == int(group_parent_id)
141 141 if group_parent_id else False
142 142 )
143 143 if edit and parent_of_self():
144 144 e_dict = {
145 145 'group_parent_id': _('Cannot assign this group as parent')
146 146 }
147 147 raise formencode.Invalid('', value, state,
148 148 error_dict=e_dict)
149 149
150 150 old_gname = None
151 151 if edit:
152 152 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
153 153
154 154 if old_gname != group_name or not edit:
155 155
156 156 # check group
157 157 gr = RepoGroup.query()\
158 158 .filter(RepoGroup.group_name == slug)\
159 159 .filter(RepoGroup.group_parent_id == group_parent_id)\
160 160 .scalar()
161 161
162 162 if gr:
163 163 e_dict = {
164 164 'group_name': _('This group already exists')
165 165 }
166 166 raise formencode.Invalid('', value, state,
167 167 error_dict=e_dict)
168 168
169 169 # check for same repo
170 170 repo = Repository.query()\
171 171 .filter(Repository.repo_name == slug)\
172 172 .scalar()
173 173
174 174 if repo:
175 175 e_dict = {
176 176 'group_name': _('Repository with this name already exists')
177 177 }
178 178 raise formencode.Invalid('', value, state,
179 179 error_dict=e_dict)
180 180
181 181 return _ValidReposGroup
182 182
183 183
184 184 class ValidPassword(formencode.validators.FancyValidator):
185 185
186 186 def to_python(self, value, state):
187 187
188 188 if not value:
189 189 return
190 190
191 191 if value.get('password'):
192 192 try:
193 193 value['password'] = get_crypt_password(value['password'])
194 194 except UnicodeEncodeError:
195 195 e_dict = {'password': _('Invalid characters in password')}
196 196 raise formencode.Invalid('', value, state, error_dict=e_dict)
197 197
198 198 if value.get('password_confirmation'):
199 199 try:
200 200 value['password_confirmation'] = \
201 201 get_crypt_password(value['password_confirmation'])
202 202 except UnicodeEncodeError:
203 203 e_dict = {
204 204 'password_confirmation': _('Invalid characters in password')
205 205 }
206 206 raise formencode.Invalid('', value, state, error_dict=e_dict)
207 207
208 208 if value.get('new_password'):
209 209 try:
210 210 value['new_password'] = \
211 211 get_crypt_password(value['new_password'])
212 212 except UnicodeEncodeError:
213 213 e_dict = {'new_password': _('Invalid characters in password')}
214 214 raise formencode.Invalid('', value, state, error_dict=e_dict)
215 215
216 216 return value
217 217
218 218
219 219 class ValidPasswordsMatch(formencode.validators.FancyValidator):
220 220
221 221 def validate_python(self, value, state):
222 222
223 223 pass_val = value.get('password') or value.get('new_password')
224 224 if pass_val != value['password_confirmation']:
225 225 e_dict = {'password_confirmation':
226 226 _('Passwords do not match')}
227 227 raise formencode.Invalid('', value, state, error_dict=e_dict)
228 228
229 229
230 230 class ValidAuth(formencode.validators.FancyValidator):
231 231 messages = {
232 232 'invalid_password':_('invalid password'),
233 233 'invalid_login':_('invalid user name'),
234 234 'disabled_account':_('Your account is disabled')
235 235 }
236 236
237 237 # error mapping
238 238 e_dict = {'username': messages['invalid_login'],
239 239 'password': messages['invalid_password']}
240 240 e_dict_disable = {'username': messages['disabled_account']}
241 241
242 242 def validate_python(self, value, state):
243 243 password = value['password']
244 244 username = value['username']
245 245 user = User.get_by_username(username)
246 246
247 247 if authenticate(username, password):
248 248 return value
249 249 else:
250 250 if user and user.active is False:
251 251 log.warning('user %s is disabled' % username)
252 252 raise formencode.Invalid(
253 253 self.message('disabled_account',
254 254 state=State_obj),
255 255 value, state,
256 256 error_dict=self.e_dict_disable
257 257 )
258 258 else:
259 log.warning('user %s not authenticated' % username)
259 log.warning('user %s failed to authenticate' % username)
260 260 raise formencode.Invalid(
261 261 self.message('invalid_password',
262 262 state=State_obj), value, state,
263 263 error_dict=self.e_dict
264 264 )
265 265
266 266
267 267 class ValidRepoUser(formencode.validators.FancyValidator):
268 268
269 269 def to_python(self, value, state):
270 270 try:
271 271 User.query().filter(User.active == True)\
272 272 .filter(User.username == value).one()
273 273 except Exception:
274 274 raise formencode.Invalid(_('This username is not valid'),
275 275 value, state)
276 276 return value
277 277
278 278
279 279 def ValidRepoName(edit, old_data):
280 280 class _ValidRepoName(formencode.validators.FancyValidator):
281 281 def to_python(self, value, state):
282 282
283 283 repo_name = value.get('repo_name')
284 284
285 285 slug = repo_name_slug(repo_name)
286 286 if slug in [ADMIN_PREFIX, '']:
287 287 e_dict = {'repo_name': _('This repository name is disallowed')}
288 288 raise formencode.Invalid('', value, state, error_dict=e_dict)
289 289
290 290 if value.get('repo_group'):
291 291 gr = RepoGroup.get(value.get('repo_group'))
292 292 group_path = gr.full_path
293 293 # value needs to be aware of group name in order to check
294 294 # db key This is an actual just the name to store in the
295 295 # database
296 296 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
297 297
298 298 else:
299 299 group_path = ''
300 300 repo_name_full = repo_name
301 301
302 302 value['repo_name_full'] = repo_name_full
303 303 rename = old_data.get('repo_name') != repo_name_full
304 304 create = not edit
305 305 if rename or create:
306 306
307 307 if group_path != '':
308 308 if Repository.get_by_repo_name(repo_name_full):
309 309 e_dict = {
310 310 'repo_name': _('This repository already exists in '
311 311 'a group "%s"') % gr.group_name
312 312 }
313 313 raise formencode.Invalid('', value, state,
314 314 error_dict=e_dict)
315 315 elif RepoGroup.get_by_group_name(repo_name_full):
316 316 e_dict = {
317 317 'repo_name': _('There is a group with this name '
318 318 'already "%s"') % repo_name_full
319 319 }
320 320 raise formencode.Invalid('', value, state,
321 321 error_dict=e_dict)
322 322
323 323 elif Repository.get_by_repo_name(repo_name_full):
324 324 e_dict = {'repo_name': _('This repository '
325 325 'already exists')}
326 326 raise formencode.Invalid('', value, state,
327 327 error_dict=e_dict)
328 328
329 329 return value
330 330
331 331 return _ValidRepoName
332 332
333 333
334 334 def ValidForkName(*args, **kwargs):
335 335 return ValidRepoName(*args, **kwargs)
336 336
337 337
338 338 def SlugifyName():
339 339 class _SlugifyName(formencode.validators.FancyValidator):
340 340
341 341 def to_python(self, value, state):
342 342 return repo_name_slug(value)
343 343
344 344 return _SlugifyName
345 345
346 346
347 347 def ValidCloneUri():
348 348 from mercurial.httprepo import httprepository, httpsrepository
349 349 from rhodecode.lib.utils import make_ui
350 350
351 351 class _ValidCloneUri(formencode.validators.FancyValidator):
352 352
353 353 def to_python(self, value, state):
354 354 if not value:
355 355 pass
356 356 elif value.startswith('https'):
357 357 try:
358 358 httpsrepository(make_ui('db'), value).capabilities
359 359 except Exception:
360 360 log.error(traceback.format_exc())
361 361 raise formencode.Invalid(_('invalid clone url'), value,
362 362 state)
363 363 elif value.startswith('http'):
364 364 try:
365 365 httprepository(make_ui('db'), value).capabilities
366 366 except Exception:
367 367 log.error(traceback.format_exc())
368 368 raise formencode.Invalid(_('invalid clone url'), value,
369 369 state)
370 370 else:
371 371 raise formencode.Invalid(_('Invalid clone url, provide a '
372 372 'valid clone http\s url'), value,
373 373 state)
374 374 return value
375 375
376 376 return _ValidCloneUri
377 377
378 378
379 379 def ValidForkType(old_data):
380 380 class _ValidForkType(formencode.validators.FancyValidator):
381 381
382 382 def to_python(self, value, state):
383 383 if old_data['repo_type'] != value:
384 384 raise formencode.Invalid(_('Fork have to be the same '
385 385 'type as original'), value, state)
386 386
387 387 return value
388 388 return _ValidForkType
389 389
390 390
391 391 def ValidPerms(type_='repo'):
392 392 if type_ == 'group':
393 393 EMPTY_PERM = 'group.none'
394 394 elif type_ == 'repo':
395 395 EMPTY_PERM = 'repository.none'
396 396
397 397 class _ValidPerms(formencode.validators.FancyValidator):
398 398 messages = {
399 399 'perm_new_member_name':
400 400 _('This username or users group name is not valid')
401 401 }
402 402
403 403 def to_python(self, value, state):
404 404 perms_update = []
405 405 perms_new = []
406 406 # build a list of permission to update and new permission to create
407 407 for k, v in value.items():
408 408 # means new added member to permissions
409 409 if k.startswith('perm_new_member'):
410 410 new_perm = value.get('perm_new_member', False)
411 411 new_member = value.get('perm_new_member_name', False)
412 412 new_type = value.get('perm_new_member_type')
413 413
414 414 if new_member and new_perm:
415 415 if (new_member, new_perm, new_type) not in perms_new:
416 416 perms_new.append((new_member, new_perm, new_type))
417 417 elif k.startswith('u_perm_') or k.startswith('g_perm_'):
418 418 member = k[7:]
419 419 t = {'u': 'user',
420 420 'g': 'users_group'
421 421 }[k[0]]
422 422 if member == 'default':
423 423 if value.get('private'):
424 424 # set none for default when updating to private repo
425 425 v = EMPTY_PERM
426 426 perms_update.append((member, v, t))
427 427
428 428 value['perms_updates'] = perms_update
429 429 value['perms_new'] = perms_new
430 430
431 431 # update permissions
432 432 for k, v, t in perms_new:
433 433 try:
434 434 if t is 'user':
435 435 self.user_db = User.query()\
436 436 .filter(User.active == True)\
437 437 .filter(User.username == k).one()
438 438 if t is 'users_group':
439 439 self.user_db = UsersGroup.query()\
440 440 .filter(UsersGroup.users_group_active == True)\
441 441 .filter(UsersGroup.users_group_name == k).one()
442 442
443 443 except Exception:
444 444 msg = self.message('perm_new_member_name',
445 445 state=State_obj)
446 446 raise formencode.Invalid(
447 447 msg, value, state, error_dict={'perm_new_member_name': msg}
448 448 )
449 449 return value
450 450 return _ValidPerms
451 451
452 452
453 453 class ValidSettings(formencode.validators.FancyValidator):
454 454
455 455 def to_python(self, value, state):
456 456 # settings form can't edit user
457 457 if 'user' in value:
458 458 del['value']['user']
459 459 return value
460 460
461 461
462 462 class ValidPath(formencode.validators.FancyValidator):
463 463 def to_python(self, value, state):
464 464
465 465 if not os.path.isdir(value):
466 466 msg = _('This is not a valid path')
467 467 raise formencode.Invalid(msg, value, state,
468 468 error_dict={'paths_root_path': msg})
469 469 return value
470 470
471 471
472 472 def UniqSystemEmail(old_data):
473 473 class _UniqSystemEmail(formencode.validators.FancyValidator):
474 474 def to_python(self, value, state):
475 475 value = value.lower()
476 476 if old_data.get('email', '').lower() != value:
477 477 user = User.get_by_email(value, case_insensitive=True)
478 478 if user:
479 479 raise formencode.Invalid(
480 480 _("This e-mail address is already taken"), value, state
481 481 )
482 482 return value
483 483
484 484 return _UniqSystemEmail
485 485
486 486
487 487 class ValidSystemEmail(formencode.validators.FancyValidator):
488 488 def to_python(self, value, state):
489 489 value = value.lower()
490 490 user = User.get_by_email(value, case_insensitive=True)
491 491 if user is None:
492 492 raise formencode.Invalid(
493 493 _("This e-mail address doesn't exist."), value, state
494 494 )
495 495
496 496 return value
497 497
498 498
499 499 class LdapLibValidator(formencode.validators.FancyValidator):
500 500
501 501 def to_python(self, value, state):
502 502
503 503 try:
504 504 import ldap
505 505 except ImportError:
506 506 raise LdapImportError
507 507 return value
508 508
509 509
510 510 class AttrLoginValidator(formencode.validators.FancyValidator):
511 511
512 512 def to_python(self, value, state):
513 513
514 514 if not value or not isinstance(value, (str, unicode)):
515 515 raise formencode.Invalid(
516 516 _("The LDAP Login attribute of the CN must be specified - "
517 517 "this is the name of the attribute that is equivalent "
518 518 "to 'username'"), value, state
519 519 )
520 520
521 521 return value
522 522
523 523
524 524 #==============================================================================
525 525 # FORMS
526 526 #==============================================================================
527 527 class LoginForm(formencode.Schema):
528 528 allow_extra_fields = True
529 529 filter_extra_fields = True
530 530 username = UnicodeString(
531 531 strip=True,
532 532 min=1,
533 533 not_empty=True,
534 534 messages={
535 535 'empty': _('Please enter a login'),
536 536 'tooShort': _('Enter a value %(min)i characters long or more')}
537 537 )
538 538
539 539 password = UnicodeString(
540 540 strip=True,
541 541 min=3,
542 542 not_empty=True,
543 543 messages={
544 544 'empty': _('Please enter a password'),
545 545 'tooShort': _('Enter %(min)i characters or more')}
546 546 )
547 547
548 548 remember = StringBoolean(if_missing=False)
549 549
550 550 chained_validators = [ValidAuth]
551 551
552 552
553 553 def UserForm(edit=False, old_data={}):
554 554 class _UserForm(formencode.Schema):
555 555 allow_extra_fields = True
556 556 filter_extra_fields = True
557 557 username = All(UnicodeString(strip=True, min=1, not_empty=True),
558 558 ValidUsername(edit, old_data))
559 559 if edit:
560 560 new_password = All(UnicodeString(strip=True, min=6, not_empty=False))
561 561 password_confirmation = All(UnicodeString(strip=True, min=6,
562 562 not_empty=False))
563 563 admin = StringBoolean(if_missing=False)
564 564 else:
565 565 password = All(UnicodeString(strip=True, min=6, not_empty=True))
566 566 password_confirmation = All(UnicodeString(strip=True, min=6,
567 567 not_empty=False))
568 568
569 569 active = StringBoolean(if_missing=False)
570 570 name = UnicodeString(strip=True, min=1, not_empty=False)
571 571 lastname = UnicodeString(strip=True, min=1, not_empty=False)
572 572 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
573 573
574 574 chained_validators = [ValidPasswordsMatch, ValidPassword]
575 575
576 576 return _UserForm
577 577
578 578
579 579 def UsersGroupForm(edit=False, old_data={}, available_members=[]):
580 580 class _UsersGroupForm(formencode.Schema):
581 581 allow_extra_fields = True
582 582 filter_extra_fields = True
583 583
584 584 users_group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
585 585 ValidUsersGroup(edit, old_data))
586 586
587 587 users_group_active = StringBoolean(if_missing=False)
588 588
589 589 if edit:
590 590 users_group_members = OneOf(available_members, hideList=False,
591 591 testValueList=True,
592 592 if_missing=None, not_empty=False)
593 593
594 594 return _UsersGroupForm
595 595
596 596
597 597 def ReposGroupForm(edit=False, old_data={}, available_groups=[]):
598 598 class _ReposGroupForm(formencode.Schema):
599 599 allow_extra_fields = True
600 600 filter_extra_fields = False
601 601
602 602 group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
603 603 SlugifyName())
604 604 group_description = UnicodeString(strip=True, min=1,
605 605 not_empty=True)
606 606 group_parent_id = OneOf(available_groups, hideList=False,
607 607 testValueList=True,
608 608 if_missing=None, not_empty=False)
609 609
610 610 chained_validators = [ValidReposGroup(edit, old_data), ValidPerms('group')]
611 611
612 612 return _ReposGroupForm
613 613
614 614
615 615 def RegisterForm(edit=False, old_data={}):
616 616 class _RegisterForm(formencode.Schema):
617 617 allow_extra_fields = True
618 618 filter_extra_fields = True
619 619 username = All(ValidUsername(edit, old_data),
620 620 UnicodeString(strip=True, min=1, not_empty=True))
621 621 password = All(UnicodeString(strip=True, min=6, not_empty=True))
622 622 password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=True))
623 623 active = StringBoolean(if_missing=False)
624 624 name = UnicodeString(strip=True, min=1, not_empty=False)
625 625 lastname = UnicodeString(strip=True, min=1, not_empty=False)
626 626 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
627 627
628 628 chained_validators = [ValidPasswordsMatch, ValidPassword]
629 629
630 630 return _RegisterForm
631 631
632 632
633 633 def PasswordResetForm():
634 634 class _PasswordResetForm(formencode.Schema):
635 635 allow_extra_fields = True
636 636 filter_extra_fields = True
637 637 email = All(ValidSystemEmail(), Email(not_empty=True))
638 638 return _PasswordResetForm
639 639
640 640
641 641 def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
642 642 repo_groups=[]):
643 643 class _RepoForm(formencode.Schema):
644 644 allow_extra_fields = True
645 645 filter_extra_fields = False
646 646 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
647 647 SlugifyName())
648 648 clone_uri = All(UnicodeString(strip=True, min=1, not_empty=False),
649 649 ValidCloneUri()())
650 650 repo_group = OneOf(repo_groups, hideList=True)
651 651 repo_type = OneOf(supported_backends)
652 652 description = UnicodeString(strip=True, min=1, not_empty=True)
653 653 private = StringBoolean(if_missing=False)
654 654 enable_statistics = StringBoolean(if_missing=False)
655 655 enable_downloads = StringBoolean(if_missing=False)
656 656
657 657 if edit:
658 658 #this is repo owner
659 659 user = All(UnicodeString(not_empty=True), ValidRepoUser)
660 660
661 661 chained_validators = [ValidRepoName(edit, old_data), ValidPerms()]
662 662 return _RepoForm
663 663
664 664
665 665 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
666 666 repo_groups=[]):
667 667 class _RepoForkForm(formencode.Schema):
668 668 allow_extra_fields = True
669 669 filter_extra_fields = False
670 670 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
671 671 SlugifyName())
672 672 repo_group = OneOf(repo_groups, hideList=True)
673 673 repo_type = All(ValidForkType(old_data), OneOf(supported_backends))
674 674 description = UnicodeString(strip=True, min=1, not_empty=True)
675 675 private = StringBoolean(if_missing=False)
676 676 copy_permissions = StringBoolean(if_missing=False)
677 677 update_after_clone = StringBoolean(if_missing=False)
678 678 fork_parent_id = UnicodeString()
679 679 chained_validators = [ValidForkName(edit, old_data)]
680 680
681 681 return _RepoForkForm
682 682
683 683
684 684 def RepoSettingsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
685 685 repo_groups=[]):
686 686 class _RepoForm(formencode.Schema):
687 687 allow_extra_fields = True
688 688 filter_extra_fields = False
689 689 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
690 690 SlugifyName())
691 691 description = UnicodeString(strip=True, min=1, not_empty=True)
692 692 repo_group = OneOf(repo_groups, hideList=True)
693 693 private = StringBoolean(if_missing=False)
694 694
695 695 chained_validators = [ValidRepoName(edit, old_data), ValidPerms(),
696 696 ValidSettings]
697 697 return _RepoForm
698 698
699 699
700 700 def ApplicationSettingsForm():
701 701 class _ApplicationSettingsForm(formencode.Schema):
702 702 allow_extra_fields = True
703 703 filter_extra_fields = False
704 704 rhodecode_title = UnicodeString(strip=True, min=1, not_empty=True)
705 705 rhodecode_realm = UnicodeString(strip=True, min=1, not_empty=True)
706 706 rhodecode_ga_code = UnicodeString(strip=True, min=1, not_empty=False)
707 707
708 708 return _ApplicationSettingsForm
709 709
710 710
711 711 def ApplicationUiSettingsForm():
712 712 class _ApplicationUiSettingsForm(formencode.Schema):
713 713 allow_extra_fields = True
714 714 filter_extra_fields = False
715 715 web_push_ssl = OneOf(['true', 'false'], if_missing='false')
716 716 paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
717 717 hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
718 718 hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
719 719 hooks_pretxnchangegroup_push_logger = OneOf(['True', 'False'], if_missing=False)
720 720 hooks_preoutgoing_pull_logger = OneOf(['True', 'False'], if_missing=False)
721 721
722 722 return _ApplicationUiSettingsForm
723 723
724 724
725 725 def DefaultPermissionsForm(perms_choices, register_choices, create_choices):
726 726 class _DefaultPermissionsForm(formencode.Schema):
727 727 allow_extra_fields = True
728 728 filter_extra_fields = True
729 729 overwrite_default = StringBoolean(if_missing=False)
730 730 anonymous = OneOf(['True', 'False'], if_missing=False)
731 731 default_perm = OneOf(perms_choices)
732 732 default_register = OneOf(register_choices)
733 733 default_create = OneOf(create_choices)
734 734
735 735 return _DefaultPermissionsForm
736 736
737 737
738 738 def LdapSettingsForm(tls_reqcert_choices, search_scope_choices, tls_kind_choices):
739 739 class _LdapSettingsForm(formencode.Schema):
740 740 allow_extra_fields = True
741 741 filter_extra_fields = True
742 742 pre_validators = [LdapLibValidator]
743 743 ldap_active = StringBoolean(if_missing=False)
744 744 ldap_host = UnicodeString(strip=True,)
745 745 ldap_port = Number(strip=True,)
746 746 ldap_tls_kind = OneOf(tls_kind_choices)
747 747 ldap_tls_reqcert = OneOf(tls_reqcert_choices)
748 748 ldap_dn_user = UnicodeString(strip=True,)
749 749 ldap_dn_pass = UnicodeString(strip=True,)
750 750 ldap_base_dn = UnicodeString(strip=True,)
751 751 ldap_filter = UnicodeString(strip=True,)
752 752 ldap_search_scope = OneOf(search_scope_choices)
753 753 ldap_attr_login = All(AttrLoginValidator, UnicodeString(strip=True,))
754 754 ldap_attr_firstname = UnicodeString(strip=True,)
755 755 ldap_attr_lastname = UnicodeString(strip=True,)
756 756 ldap_attr_email = UnicodeString(strip=True,)
757 757
758 758 return _LdapSettingsForm
General Comments 0
You need to be logged in to leave comments. Login now