##// END OF EJS Templates
IP restrictions now also enabled for IPv6
marcink -
r3212:6c28533d beta
parent child Browse files
Show More
@@ -1,995 +1,1008 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 from sqlalchemy.orm.exc import ObjectDeletedError
37 38
38 39 from rhodecode import __platform__, is_windows, is_unix
39 40 from rhodecode.model.meta import Session
40 41
41 42 from rhodecode.lib.utils2 import str2bool, safe_unicode
42 43 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
43 44 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
44 45 from rhodecode.lib.auth_ldap import AuthLdap
45 46
46 47 from rhodecode.model import meta
47 48 from rhodecode.model.user import UserModel
48 49 from rhodecode.model.db import Permission, RhodeCodeSetting, User, UserIpMap
49 50 from rhodecode.lib.caching_query import FromCache
50 51
51 52 log = logging.getLogger(__name__)
52 53
53 54
54 55 class PasswordGenerator(object):
55 56 """
56 57 This is a simple class for generating password from different sets of
57 58 characters
58 59 usage::
59 60
60 61 passwd_gen = PasswordGenerator()
61 62 #print 8-letter password containing only big and small letters
62 63 of alphabet
63 64 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
64 65 """
65 66 ALPHABETS_NUM = r'''1234567890'''
66 67 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
67 68 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
68 69 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
69 70 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
70 71 + ALPHABETS_NUM + ALPHABETS_SPECIAL
71 72 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
72 73 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
73 74 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
74 75 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
75 76
76 77 def __init__(self, passwd=''):
77 78 self.passwd = passwd
78 79
79 80 def gen_password(self, length, type_=None):
80 81 if type_ is None:
81 82 type_ = self.ALPHABETS_FULL
82 83 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
83 84 return self.passwd
84 85
85 86
86 87 class RhodeCodeCrypto(object):
87 88
88 89 @classmethod
89 90 def hash_string(cls, str_):
90 91 """
91 92 Cryptographic function used for password hashing based on pybcrypt
92 93 or pycrypto in windows
93 94
94 95 :param password: password to hash
95 96 """
96 97 if is_windows:
97 98 from hashlib import sha256
98 99 return sha256(str_).hexdigest()
99 100 elif is_unix:
100 101 import bcrypt
101 102 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
102 103 else:
103 104 raise Exception('Unknown or unsupported platform %s' \
104 105 % __platform__)
105 106
106 107 @classmethod
107 108 def hash_check(cls, password, hashed):
108 109 """
109 110 Checks matching password with it's hashed value, runs different
110 111 implementation based on platform it runs on
111 112
112 113 :param password: password
113 114 :param hashed: password in hashed form
114 115 """
115 116
116 117 if is_windows:
117 118 from hashlib import sha256
118 119 return sha256(password).hexdigest() == hashed
119 120 elif is_unix:
120 121 import bcrypt
121 122 return bcrypt.hashpw(password, hashed) == hashed
122 123 else:
123 124 raise Exception('Unknown or unsupported platform %s' \
124 125 % __platform__)
125 126
126 127
127 128 def get_crypt_password(password):
128 129 return RhodeCodeCrypto.hash_string(password)
129 130
130 131
131 132 def check_password(password, hashed):
132 133 return RhodeCodeCrypto.hash_check(password, hashed)
133 134
134 135
135 136 def generate_api_key(str_, salt=None):
136 137 """
137 138 Generates API KEY from given string
138 139
139 140 :param str_:
140 141 :param salt:
141 142 """
142 143
143 144 if salt is None:
144 145 salt = _RandomNameSequence().next()
145 146
146 147 return hashlib.sha1(str_ + salt).hexdigest()
147 148
148 149
149 150 def authfunc(environ, username, password):
150 151 """
151 152 Dummy authentication wrapper function used in Mercurial and Git for
152 153 access control.
153 154
154 155 :param environ: needed only for using in Basic auth
155 156 """
156 157 return authenticate(username, password)
157 158
158 159
159 160 def authenticate(username, password):
160 161 """
161 162 Authentication function used for access control,
162 163 firstly checks for db authentication then if ldap is enabled for ldap
163 164 authentication, also creates ldap user if not in database
164 165
165 166 :param username: username
166 167 :param password: password
167 168 """
168 169
169 170 user_model = UserModel()
170 171 user = User.get_by_username(username)
171 172
172 173 log.debug('Authenticating user using RhodeCode account')
173 174 if user is not None and not user.ldap_dn:
174 175 if user.active:
175 176 if user.username == 'default' and user.active:
176 177 log.info('user %s authenticated correctly as anonymous user' %
177 178 username)
178 179 return True
179 180
180 181 elif user.username == username and check_password(password,
181 182 user.password):
182 183 log.info('user %s authenticated correctly' % username)
183 184 return True
184 185 else:
185 186 log.warning('user %s tried auth but is disabled' % username)
186 187
187 188 else:
188 189 log.debug('Regular authentication failed')
189 190 user_obj = User.get_by_username(username, case_insensitive=True)
190 191
191 192 if user_obj is not None and not user_obj.ldap_dn:
192 193 log.debug('this user already exists as non ldap')
193 194 return False
194 195
195 196 ldap_settings = RhodeCodeSetting.get_ldap_settings()
196 197 #======================================================================
197 198 # FALLBACK TO LDAP AUTH IF ENABLE
198 199 #======================================================================
199 200 if str2bool(ldap_settings.get('ldap_active')):
200 201 log.debug("Authenticating user using ldap")
201 202 kwargs = {
202 203 'server': ldap_settings.get('ldap_host', ''),
203 204 'base_dn': ldap_settings.get('ldap_base_dn', ''),
204 205 'port': ldap_settings.get('ldap_port'),
205 206 'bind_dn': ldap_settings.get('ldap_dn_user'),
206 207 'bind_pass': ldap_settings.get('ldap_dn_pass'),
207 208 'tls_kind': ldap_settings.get('ldap_tls_kind'),
208 209 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
209 210 'ldap_filter': ldap_settings.get('ldap_filter'),
210 211 'search_scope': ldap_settings.get('ldap_search_scope'),
211 212 'attr_login': ldap_settings.get('ldap_attr_login'),
212 213 'ldap_version': 3,
213 214 }
214 215 log.debug('Checking for ldap authentication')
215 216 try:
216 217 aldap = AuthLdap(**kwargs)
217 218 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
218 219 password)
219 220 log.debug('Got ldap DN response %s' % user_dn)
220 221
221 222 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
222 223 .get(k), [''])[0]
223 224
224 225 user_attrs = {
225 226 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
226 227 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
227 228 'email': get_ldap_attr('ldap_attr_email'),
228 229 }
229 230
230 231 # don't store LDAP password since we don't need it. Override
231 232 # with some random generated password
232 233 _password = PasswordGenerator().gen_password(length=8)
233 234 # create this user on the fly if it doesn't exist in rhodecode
234 235 # database
235 236 if user_model.create_ldap(username, _password, user_dn,
236 237 user_attrs):
237 238 log.info('created new ldap user %s' % username)
238 239
239 240 Session().commit()
240 241 return True
241 242 except (LdapUsernameError, LdapPasswordError,):
242 243 pass
243 244 except (Exception,):
244 245 log.error(traceback.format_exc())
245 246 pass
246 247 return False
247 248
248 249
249 250 def login_container_auth(username):
250 251 user = User.get_by_username(username)
251 252 if user is None:
252 253 user_attrs = {
253 254 'name': username,
254 255 'lastname': None,
255 256 'email': None,
256 257 }
257 258 user = UserModel().create_for_container_auth(username, user_attrs)
258 259 if not user:
259 260 return None
260 261 log.info('User %s was created by container authentication' % username)
261 262
262 263 if not user.active:
263 264 return None
264 265
265 266 user.update_lastlogin()
266 267 Session().commit()
267 268
268 269 log.debug('User %s is now logged in by container authentication',
269 270 user.username)
270 271 return user
271 272
272 273
273 274 def get_container_username(environ, config, clean_username=False):
274 275 """
275 276 Get's the container_auth username (or email). It tries to get username
276 277 from REMOTE_USER if container_auth_enabled is enabled, if that fails
277 278 it tries to get username from HTTP_X_FORWARDED_USER if proxypass_auth_enabled
278 279 is enabled. clean_username extracts the username from this data if it's
279 280 having @ in it.
280 281
281 282 :param environ:
282 283 :param config:
283 284 :param clean_username:
284 285 """
285 286 username = None
286 287
287 288 if str2bool(config.get('container_auth_enabled', False)):
288 289 from paste.httpheaders import REMOTE_USER
289 290 username = REMOTE_USER(environ)
290 291 log.debug('extracted REMOTE_USER:%s' % (username))
291 292
292 293 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
293 294 username = environ.get('HTTP_X_FORWARDED_USER')
294 295 log.debug('extracted HTTP_X_FORWARDED_USER:%s' % (username))
295 296
296 297 if username and clean_username:
297 298 # Removing realm and domain from username
298 299 username = username.partition('@')[0]
299 300 username = username.rpartition('\\')[2]
300 301 log.debug('Received username %s from container' % username)
301 302
302 303 return username
303 304
304 305
305 306 class CookieStoreWrapper(object):
306 307
307 308 def __init__(self, cookie_store):
308 309 self.cookie_store = cookie_store
309 310
310 311 def __repr__(self):
311 312 return 'CookieStore<%s>' % (self.cookie_store)
312 313
313 314 def get(self, key, other=None):
314 315 if isinstance(self.cookie_store, dict):
315 316 return self.cookie_store.get(key, other)
316 317 elif isinstance(self.cookie_store, AuthUser):
317 318 return self.cookie_store.__dict__.get(key, other)
318 319
319 320
320 321 class AuthUser(object):
321 322 """
322 323 A simple object that handles all attributes of user in RhodeCode
323 324
324 325 It does lookup based on API key,given user, or user present in session
325 326 Then it fills all required information for such user. It also checks if
326 327 anonymous access is enabled and if so, it returns default user as logged
327 328 in
328 329 """
329 330
330 331 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
331 332
332 333 self.user_id = user_id
333 334 self.api_key = None
334 335 self.username = username
335 336 self.ip_addr = ip_addr
336 337
337 338 self.name = ''
338 339 self.lastname = ''
339 340 self.email = ''
340 341 self.is_authenticated = False
341 342 self.admin = False
342 343 self.inherit_default_permissions = False
343 344 self.permissions = {}
344 345 self._api_key = api_key
345 346 self.propagate_data()
346 347 self._instance = None
347 348
348 349 def propagate_data(self):
349 350 user_model = UserModel()
350 351 self.anonymous_user = User.get_by_username('default', cache=True)
351 352 is_user_loaded = False
352 353
353 354 # try go get user by api key
354 355 if self._api_key and self._api_key != self.anonymous_user.api_key:
355 356 log.debug('Auth User lookup by API KEY %s' % self._api_key)
356 357 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
357 358 # lookup by userid
358 359 elif (self.user_id is not None and
359 360 self.user_id != self.anonymous_user.user_id):
360 361 log.debug('Auth User lookup by USER ID %s' % self.user_id)
361 362 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
362 363 # lookup by username
363 364 elif self.username and \
364 365 str2bool(config.get('container_auth_enabled', False)):
365 366
366 367 log.debug('Auth User lookup by USER NAME %s' % self.username)
367 368 dbuser = login_container_auth(self.username)
368 369 if dbuser is not None:
369 370 log.debug('filling all attributes to object')
370 371 for k, v in dbuser.get_dict().items():
371 372 setattr(self, k, v)
372 373 self.set_authenticated()
373 374 is_user_loaded = True
374 375 else:
375 376 log.debug('No data in %s that could been used to log in' % self)
376 377
377 378 if not is_user_loaded:
378 379 # if we cannot authenticate user try anonymous
379 380 if self.anonymous_user.active is True:
380 381 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
381 382 # then we set this user is logged in
382 383 self.is_authenticated = True
383 384 else:
384 385 self.user_id = None
385 386 self.username = None
386 387 self.is_authenticated = False
387 388
388 389 if not self.username:
389 390 self.username = 'None'
390 391
391 392 log.debug('Auth User is now %s' % self)
392 393 user_model.fill_perms(self)
393 394
394 395 @property
395 396 def is_admin(self):
396 397 return self.admin
397 398
398 399 @property
399 400 def ip_allowed(self):
400 401 """
401 402 Checks if ip_addr used in constructor is allowed from defined list of
402 403 allowed ip_addresses for user
403 404
404 405 :returns: boolean, True if ip is in allowed ip range
405 406 """
406 407 #check IP
407 408 allowed_ips = AuthUser.get_allowed_ips(self.user_id, cache=True)
408 409 if check_ip_access(source_ip=self.ip_addr, allowed_ips=allowed_ips):
409 410 log.debug('IP:%s is in range of %s' % (self.ip_addr, allowed_ips))
410 411 return True
411 412 else:
412 413 log.info('Access for IP:%s forbidden, '
413 414 'not in %s' % (self.ip_addr, allowed_ips))
414 415 return False
415 416
416 417 def __repr__(self):
417 418 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
418 419 self.is_authenticated)
419 420
420 421 def set_authenticated(self, authenticated=True):
421 422 if self.user_id != self.anonymous_user.user_id:
422 423 self.is_authenticated = authenticated
423 424
424 425 def get_cookie_store(self):
425 426 return {'username': self.username,
426 427 'user_id': self.user_id,
427 428 'is_authenticated': self.is_authenticated}
428 429
429 430 @classmethod
430 431 def from_cookie_store(cls, cookie_store):
431 432 """
432 433 Creates AuthUser from a cookie store
433 434
434 435 :param cls:
435 436 :param cookie_store:
436 437 """
437 438 user_id = cookie_store.get('user_id')
438 439 username = cookie_store.get('username')
439 440 api_key = cookie_store.get('api_key')
440 441 return AuthUser(user_id, api_key, username)
441 442
442 443 @classmethod
443 444 def get_allowed_ips(cls, user_id, cache=False):
444 445 _set = set()
445 446 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
446 447 if cache:
447 448 user_ips = user_ips.options(FromCache("sql_cache_short",
448 449 "get_user_ips_%s" % user_id))
449 450 for ip in user_ips:
451 try:
450 452 _set.add(ip.ip_addr)
451 return _set or set(['0.0.0.0/0'])
453 except ObjectDeletedError:
454 # since we use heavy caching sometimes it happens that we get
455 # deleted objects here, we just skip them
456 pass
457 return _set or set(['0.0.0.0/0', '::/0'])
452 458
453 459
454 460 def set_available_permissions(config):
455 461 """
456 462 This function will propagate pylons globals with all available defined
457 463 permission given in db. We don't want to check each time from db for new
458 464 permissions since adding a new permission also requires application restart
459 465 ie. to decorate new views with the newly created permission
460 466
461 467 :param config: current pylons config instance
462 468
463 469 """
464 470 log.info('getting information about all available permissions')
465 471 try:
466 472 sa = meta.Session
467 473 all_perms = sa.query(Permission).all()
468 474 except Exception:
469 475 pass
470 476 finally:
471 477 meta.Session.remove()
472 478
473 479 config['available_permissions'] = [x.permission_name for x in all_perms]
474 480
475 481
476 482 #==============================================================================
477 483 # CHECK DECORATORS
478 484 #==============================================================================
479 485 class LoginRequired(object):
480 486 """
481 487 Must be logged in to execute this function else
482 488 redirect to login page
483 489
484 490 :param api_access: if enabled this checks only for valid auth token
485 491 and grants access based on valid token
486 492 """
487 493
488 494 def __init__(self, api_access=False):
489 495 self.api_access = api_access
490 496
491 497 def __call__(self, func):
492 498 return decorator(self.__wrapper, func)
493 499
494 500 def __wrapper(self, func, *fargs, **fkwargs):
495 501 cls = fargs[0]
496 502 user = cls.rhodecode_user
497 503 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
498 504
499 505 #check IP
500 506 ip_access_ok = True
501 507 if not user.ip_allowed:
502 508 from rhodecode.lib import helpers as h
503 509 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr))),
504 510 category='warning')
505 511 ip_access_ok = False
506 512
507 513 api_access_ok = False
508 514 if self.api_access:
509 515 log.debug('Checking API KEY access for %s' % cls)
510 516 if user.api_key == request.GET.get('api_key'):
511 517 api_access_ok = True
512 518 else:
513 519 log.debug("API KEY token not valid")
514 520
515 521 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
516 522 if (user.is_authenticated or api_access_ok) and ip_access_ok:
517 523 reason = 'RegularAuth' if user.is_authenticated else 'APIAuth'
518 524 log.info('user %s is authenticated and granted access to %s '
519 525 'using %s' % (user.username, loc, reason)
520 526 )
521 527 return func(*fargs, **fkwargs)
522 528 else:
523 529 log.warn('user %s NOT authenticated on func: %s' % (
524 530 user, loc)
525 531 )
526 532 p = url.current()
527 533
528 534 log.debug('redirecting to login page with %s' % p)
529 535 return redirect(url('login_home', came_from=p))
530 536
531 537
532 538 class NotAnonymous(object):
533 539 """
534 540 Must be logged in to execute this function else
535 541 redirect to login page"""
536 542
537 543 def __call__(self, func):
538 544 return decorator(self.__wrapper, func)
539 545
540 546 def __wrapper(self, func, *fargs, **fkwargs):
541 547 cls = fargs[0]
542 548 self.user = cls.rhodecode_user
543 549
544 550 log.debug('Checking if user is not anonymous @%s' % cls)
545 551
546 552 anonymous = self.user.username == 'default'
547 553
548 554 if anonymous:
549 555 p = url.current()
550 556
551 557 import rhodecode.lib.helpers as h
552 558 h.flash(_('You need to be a registered user to '
553 559 'perform this action'),
554 560 category='warning')
555 561 return redirect(url('login_home', came_from=p))
556 562 else:
557 563 return func(*fargs, **fkwargs)
558 564
559 565
560 566 class PermsDecorator(object):
561 567 """Base class for controller decorators"""
562 568
563 569 def __init__(self, *required_perms):
564 570 available_perms = config['available_permissions']
565 571 for perm in required_perms:
566 572 if perm not in available_perms:
567 573 raise Exception("'%s' permission is not defined" % perm)
568 574 self.required_perms = set(required_perms)
569 575 self.user_perms = None
570 576
571 577 def __call__(self, func):
572 578 return decorator(self.__wrapper, func)
573 579
574 580 def __wrapper(self, func, *fargs, **fkwargs):
575 581 cls = fargs[0]
576 582 self.user = cls.rhodecode_user
577 583 self.user_perms = self.user.permissions
578 584 log.debug('checking %s permissions %s for %s %s',
579 585 self.__class__.__name__, self.required_perms, cls, self.user)
580 586
581 587 if self.check_permissions():
582 588 log.debug('Permission granted for %s %s' % (cls, self.user))
583 589 return func(*fargs, **fkwargs)
584 590
585 591 else:
586 592 log.debug('Permission denied for %s %s' % (cls, self.user))
587 593 anonymous = self.user.username == 'default'
588 594
589 595 if anonymous:
590 596 p = url.current()
591 597
592 598 import rhodecode.lib.helpers as h
593 599 h.flash(_('You need to be a signed in to '
594 600 'view this page'),
595 601 category='warning')
596 602 return redirect(url('login_home', came_from=p))
597 603
598 604 else:
599 605 # redirect with forbidden ret code
600 606 return abort(403)
601 607
602 608 def check_permissions(self):
603 609 """Dummy function for overriding"""
604 610 raise Exception('You have to write this function in child class')
605 611
606 612
607 613 class HasPermissionAllDecorator(PermsDecorator):
608 614 """
609 615 Checks for access permission for all given predicates. All of them
610 616 have to be meet in order to fulfill the request
611 617 """
612 618
613 619 def check_permissions(self):
614 620 if self.required_perms.issubset(self.user_perms.get('global')):
615 621 return True
616 622 return False
617 623
618 624
619 625 class HasPermissionAnyDecorator(PermsDecorator):
620 626 """
621 627 Checks for access permission for any of given predicates. In order to
622 628 fulfill the request any of predicates must be meet
623 629 """
624 630
625 631 def check_permissions(self):
626 632 if self.required_perms.intersection(self.user_perms.get('global')):
627 633 return True
628 634 return False
629 635
630 636
631 637 class HasRepoPermissionAllDecorator(PermsDecorator):
632 638 """
633 639 Checks for access permission for all given predicates for specific
634 640 repository. All of them have to be meet in order to fulfill the request
635 641 """
636 642
637 643 def check_permissions(self):
638 644 repo_name = get_repo_slug(request)
639 645 try:
640 646 user_perms = set([self.user_perms['repositories'][repo_name]])
641 647 except KeyError:
642 648 return False
643 649 if self.required_perms.issubset(user_perms):
644 650 return True
645 651 return False
646 652
647 653
648 654 class HasRepoPermissionAnyDecorator(PermsDecorator):
649 655 """
650 656 Checks for access permission for any of given predicates for specific
651 657 repository. In order to fulfill the request any of predicates must be meet
652 658 """
653 659
654 660 def check_permissions(self):
655 661 repo_name = get_repo_slug(request)
656 662
657 663 try:
658 664 user_perms = set([self.user_perms['repositories'][repo_name]])
659 665 except KeyError:
660 666 return False
661 667
662 668 if self.required_perms.intersection(user_perms):
663 669 return True
664 670 return False
665 671
666 672
667 673 class HasReposGroupPermissionAllDecorator(PermsDecorator):
668 674 """
669 675 Checks for access permission for all given predicates for specific
670 676 repository. All of them have to be meet in order to fulfill the request
671 677 """
672 678
673 679 def check_permissions(self):
674 680 group_name = get_repos_group_slug(request)
675 681 try:
676 682 user_perms = set([self.user_perms['repositories_groups'][group_name]])
677 683 except KeyError:
678 684 return False
679 685 if self.required_perms.issubset(user_perms):
680 686 return True
681 687 return False
682 688
683 689
684 690 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
685 691 """
686 692 Checks for access permission for any of given predicates for specific
687 693 repository. In order to fulfill the request any of predicates must be meet
688 694 """
689 695
690 696 def check_permissions(self):
691 697 group_name = get_repos_group_slug(request)
692 698
693 699 try:
694 700 user_perms = set([self.user_perms['repositories_groups'][group_name]])
695 701 except KeyError:
696 702 return False
697 703 if self.required_perms.intersection(user_perms):
698 704 return True
699 705 return False
700 706
701 707
702 708 #==============================================================================
703 709 # CHECK FUNCTIONS
704 710 #==============================================================================
705 711 class PermsFunction(object):
706 712 """Base function for other check functions"""
707 713
708 714 def __init__(self, *perms):
709 715 available_perms = config['available_permissions']
710 716
711 717 for perm in perms:
712 718 if perm not in available_perms:
713 719 raise Exception("'%s' permission is not defined" % perm)
714 720 self.required_perms = set(perms)
715 721 self.user_perms = None
716 722 self.repo_name = None
717 723 self.group_name = None
718 724
719 725 def __call__(self, check_Location=''):
720 726 user = request.user
721 727 cls_name = self.__class__.__name__
722 728 check_scope = {
723 729 'HasPermissionAll': '',
724 730 'HasPermissionAny': '',
725 731 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
726 732 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
727 733 'HasReposGroupPermissionAll': 'group:%s' % self.group_name,
728 734 'HasReposGroupPermissionAny': 'group:%s' % self.group_name,
729 735 }.get(cls_name, '?')
730 736 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
731 737 self.required_perms, user, check_scope,
732 738 check_Location or 'unspecified location')
733 739 if not user:
734 740 log.debug('Empty request user')
735 741 return False
736 742 self.user_perms = user.permissions
737 743 if self.check_permissions():
738 744 log.debug('Permission to %s granted for user: %s @ %s', self.repo_name, user,
739 745 check_Location or 'unspecified location')
740 746 return True
741 747
742 748 else:
743 749 log.debug('Permission to %s denied for user: %s @ %s', self.repo_name, user,
744 750 check_Location or 'unspecified location')
745 751 return False
746 752
747 753 def check_permissions(self):
748 754 """Dummy function for overriding"""
749 755 raise Exception('You have to write this function in child class')
750 756
751 757
752 758 class HasPermissionAll(PermsFunction):
753 759 def check_permissions(self):
754 760 if self.required_perms.issubset(self.user_perms.get('global')):
755 761 return True
756 762 return False
757 763
758 764
759 765 class HasPermissionAny(PermsFunction):
760 766 def check_permissions(self):
761 767 if self.required_perms.intersection(self.user_perms.get('global')):
762 768 return True
763 769 return False
764 770
765 771
766 772 class HasRepoPermissionAll(PermsFunction):
767 773 def __call__(self, repo_name=None, check_Location=''):
768 774 self.repo_name = repo_name
769 775 return super(HasRepoPermissionAll, self).__call__(check_Location)
770 776
771 777 def check_permissions(self):
772 778 if not self.repo_name:
773 779 self.repo_name = get_repo_slug(request)
774 780
775 781 try:
776 782 self._user_perms = set(
777 783 [self.user_perms['repositories'][self.repo_name]]
778 784 )
779 785 except KeyError:
780 786 return False
781 787 if self.required_perms.issubset(self._user_perms):
782 788 return True
783 789 return False
784 790
785 791
786 792 class HasRepoPermissionAny(PermsFunction):
787 793 def __call__(self, repo_name=None, check_Location=''):
788 794 self.repo_name = repo_name
789 795 return super(HasRepoPermissionAny, self).__call__(check_Location)
790 796
791 797 def check_permissions(self):
792 798 if not self.repo_name:
793 799 self.repo_name = get_repo_slug(request)
794 800
795 801 try:
796 802 self._user_perms = set(
797 803 [self.user_perms['repositories'][self.repo_name]]
798 804 )
799 805 except KeyError:
800 806 return False
801 807 if self.required_perms.intersection(self._user_perms):
802 808 return True
803 809 return False
804 810
805 811
806 812 class HasReposGroupPermissionAny(PermsFunction):
807 813 def __call__(self, group_name=None, check_Location=''):
808 814 self.group_name = group_name
809 815 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
810 816
811 817 def check_permissions(self):
812 818 try:
813 819 self._user_perms = set(
814 820 [self.user_perms['repositories_groups'][self.group_name]]
815 821 )
816 822 except KeyError:
817 823 return False
818 824 if self.required_perms.intersection(self._user_perms):
819 825 return True
820 826 return False
821 827
822 828
823 829 class HasReposGroupPermissionAll(PermsFunction):
824 830 def __call__(self, group_name=None, check_Location=''):
825 831 self.group_name = group_name
826 832 return super(HasReposGroupPermissionAll, self).__call__(check_Location)
827 833
828 834 def check_permissions(self):
829 835 try:
830 836 self._user_perms = set(
831 837 [self.user_perms['repositories_groups'][self.group_name]]
832 838 )
833 839 except KeyError:
834 840 return False
835 841 if self.required_perms.issubset(self._user_perms):
836 842 return True
837 843 return False
838 844
839 845
840 846 #==============================================================================
841 847 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
842 848 #==============================================================================
843 849 class HasPermissionAnyMiddleware(object):
844 850 def __init__(self, *perms):
845 851 self.required_perms = set(perms)
846 852
847 853 def __call__(self, user, repo_name):
848 854 # repo_name MUST be unicode, since we handle keys in permission
849 855 # dict by unicode
850 856 repo_name = safe_unicode(repo_name)
851 857 usr = AuthUser(user.user_id)
852 858 try:
853 859 self.user_perms = set([usr.permissions['repositories'][repo_name]])
854 860 except Exception:
855 861 log.error('Exception while accessing permissions %s' %
856 862 traceback.format_exc())
857 863 self.user_perms = set()
858 864 self.username = user.username
859 865 self.repo_name = repo_name
860 866 return self.check_permissions()
861 867
862 868 def check_permissions(self):
863 869 log.debug('checking VCS protocol '
864 870 'permissions %s for user:%s repository:%s', self.user_perms,
865 871 self.username, self.repo_name)
866 872 if self.required_perms.intersection(self.user_perms):
867 873 log.debug('permission granted for user:%s on repo:%s' % (
868 874 self.username, self.repo_name
869 875 )
870 876 )
871 877 return True
872 878 log.debug('permission denied for user:%s on repo:%s' % (
873 879 self.username, self.repo_name
874 880 )
875 881 )
876 882 return False
877 883
878 884
879 885 #==============================================================================
880 886 # SPECIAL VERSION TO HANDLE API AUTH
881 887 #==============================================================================
882 888 class _BaseApiPerm(object):
883 889 def __init__(self, *perms):
884 890 self.required_perms = set(perms)
885 891
886 892 def __call__(self, check_location='unspecified', user=None, repo_name=None):
887 893 cls_name = self.__class__.__name__
888 894 check_scope = 'user:%s, repo:%s' % (user, repo_name)
889 895 log.debug('checking cls:%s %s %s @ %s', cls_name,
890 896 self.required_perms, check_scope, check_location)
891 897 if not user:
892 898 log.debug('Empty User passed into arguments')
893 899 return False
894 900
895 901 ## process user
896 902 if not isinstance(user, AuthUser):
897 903 user = AuthUser(user.user_id)
898 904
899 905 if self.check_permissions(user.permissions, repo_name):
900 906 log.debug('Permission to %s granted for user: %s @ %s', repo_name,
901 907 user, check_location)
902 908 return True
903 909
904 910 else:
905 911 log.debug('Permission to %s denied for user: %s @ %s', repo_name,
906 912 user, check_location)
907 913 return False
908 914
909 915 def check_permissions(self, perm_defs, repo_name):
910 916 """
911 917 implement in child class should return True if permissions are ok,
912 918 False otherwise
913 919
914 920 :param perm_defs: dict with permission definitions
915 921 :param repo_name: repo name
916 922 """
917 923 raise NotImplementedError()
918 924
919 925
920 926 class HasPermissionAllApi(_BaseApiPerm):
921 927 def __call__(self, user, check_location=''):
922 928 return super(HasPermissionAllApi, self)\
923 929 .__call__(check_location=check_location, user=user)
924 930
925 931 def check_permissions(self, perm_defs, repo):
926 932 if self.required_perms.issubset(perm_defs.get('global')):
927 933 return True
928 934 return False
929 935
930 936
931 937 class HasPermissionAnyApi(_BaseApiPerm):
932 938 def __call__(self, user, check_location=''):
933 939 return super(HasPermissionAnyApi, self)\
934 940 .__call__(check_location=check_location, user=user)
935 941
936 942 def check_permissions(self, perm_defs, repo):
937 943 if self.required_perms.intersection(perm_defs.get('global')):
938 944 return True
939 945 return False
940 946
941 947
942 948 class HasRepoPermissionAllApi(_BaseApiPerm):
943 949 def __call__(self, user, repo_name, check_location=''):
944 950 return super(HasRepoPermissionAllApi, self)\
945 951 .__call__(check_location=check_location, user=user,
946 952 repo_name=repo_name)
947 953
948 954 def check_permissions(self, perm_defs, repo_name):
949 955
950 956 try:
951 957 self._user_perms = set(
952 958 [perm_defs['repositories'][repo_name]]
953 959 )
954 960 except KeyError:
955 961 log.warning(traceback.format_exc())
956 962 return False
957 963 if self.required_perms.issubset(self._user_perms):
958 964 return True
959 965 return False
960 966
961 967
962 968 class HasRepoPermissionAnyApi(_BaseApiPerm):
963 969 def __call__(self, user, repo_name, check_location=''):
964 970 return super(HasRepoPermissionAnyApi, self)\
965 971 .__call__(check_location=check_location, user=user,
966 972 repo_name=repo_name)
967 973
968 974 def check_permissions(self, perm_defs, repo_name):
969 975
970 976 try:
971 977 _user_perms = set(
972 978 [perm_defs['repositories'][repo_name]]
973 979 )
974 980 except KeyError:
975 981 log.warning(traceback.format_exc())
976 982 return False
977 983 if self.required_perms.intersection(_user_perms):
978 984 return True
979 985 return False
980 986
981 987
982 988 def check_ip_access(source_ip, allowed_ips=None):
983 989 """
984 990 Checks if source_ip is a subnet of any of allowed_ips.
985 991
986 992 :param source_ip:
987 993 :param allowed_ips: list of allowed ips together with mask
988 994 """
989 995 from rhodecode.lib import ipaddr
990 996 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
991 997 if isinstance(allowed_ips, (tuple, list, set)):
992 998 for ip in allowed_ips:
999 try:
993 1000 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
994 1001 return True
1002 # for any case we cannot determine the IP, don't crash just
1003 # skip it and log as error, we want to say forbidden still when
1004 # sending bad IP
1005 except Exception:
1006 log.error(traceback.format_exc())
1007 continue
995 1008 return False
@@ -1,1968 +1,1968 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 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 os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 import time
32 32 from collections import defaultdict
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47
48 48 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
49 49 safe_unicode, remove_suffix, remove_prefix
50 50 from rhodecode.lib.compat import json
51 51 from rhodecode.lib.caching_query import FromCache
52 52
53 53 from rhodecode.model.meta import Base, Session
54 54
55 55 URL_SEP = '/'
56 56 log = logging.getLogger(__name__)
57 57
58 58 #==============================================================================
59 59 # BASE CLASSES
60 60 #==============================================================================
61 61
62 62 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
63 63
64 64
65 65 class BaseModel(object):
66 66 """
67 67 Base Model for all classess
68 68 """
69 69
70 70 @classmethod
71 71 def _get_keys(cls):
72 72 """return column names for this model """
73 73 return class_mapper(cls).c.keys()
74 74
75 75 def get_dict(self):
76 76 """
77 77 return dict with keys and values corresponding
78 78 to this model data """
79 79
80 80 d = {}
81 81 for k in self._get_keys():
82 82 d[k] = getattr(self, k)
83 83
84 84 # also use __json__() if present to get additional fields
85 85 _json_attr = getattr(self, '__json__', None)
86 86 if _json_attr:
87 87 # update with attributes from __json__
88 88 if callable(_json_attr):
89 89 _json_attr = _json_attr()
90 90 for k, val in _json_attr.iteritems():
91 91 d[k] = val
92 92 return d
93 93
94 94 def get_appstruct(self):
95 95 """return list with keys and values tupples corresponding
96 96 to this model data """
97 97
98 98 l = []
99 99 for k in self._get_keys():
100 100 l.append((k, getattr(self, k),))
101 101 return l
102 102
103 103 def populate_obj(self, populate_dict):
104 104 """populate model with data from given populate_dict"""
105 105
106 106 for k in self._get_keys():
107 107 if k in populate_dict:
108 108 setattr(self, k, populate_dict[k])
109 109
110 110 @classmethod
111 111 def query(cls):
112 112 return Session().query(cls)
113 113
114 114 @classmethod
115 115 def get(cls, id_):
116 116 if id_:
117 117 return cls.query().get(id_)
118 118
119 119 @classmethod
120 120 def get_or_404(cls, id_):
121 121 try:
122 122 id_ = int(id_)
123 123 except (TypeError, ValueError):
124 124 raise HTTPNotFound
125 125
126 126 res = cls.query().get(id_)
127 127 if not res:
128 128 raise HTTPNotFound
129 129 return res
130 130
131 131 @classmethod
132 132 def getAll(cls):
133 133 return cls.query().all()
134 134
135 135 @classmethod
136 136 def delete(cls, id_):
137 137 obj = cls.query().get(id_)
138 138 Session().delete(obj)
139 139
140 140 def __repr__(self):
141 141 if hasattr(self, '__unicode__'):
142 142 # python repr needs to return str
143 143 return safe_str(self.__unicode__())
144 144 return '<DB:%s>' % (self.__class__.__name__)
145 145
146 146
147 147 class RhodeCodeSetting(Base, BaseModel):
148 148 __tablename__ = 'rhodecode_settings'
149 149 __table_args__ = (
150 150 UniqueConstraint('app_settings_name'),
151 151 {'extend_existing': True, 'mysql_engine': 'InnoDB',
152 152 'mysql_charset': 'utf8'}
153 153 )
154 154 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
155 155 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
156 156 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 157
158 158 def __init__(self, k='', v=''):
159 159 self.app_settings_name = k
160 160 self.app_settings_value = v
161 161
162 162 @validates('_app_settings_value')
163 163 def validate_settings_value(self, key, val):
164 164 assert type(val) == unicode
165 165 return val
166 166
167 167 @hybrid_property
168 168 def app_settings_value(self):
169 169 v = self._app_settings_value
170 170 if self.app_settings_name in ["ldap_active",
171 171 "default_repo_enable_statistics",
172 172 "default_repo_enable_locking",
173 173 "default_repo_private",
174 174 "default_repo_enable_downloads"]:
175 175 v = str2bool(v)
176 176 return v
177 177
178 178 @app_settings_value.setter
179 179 def app_settings_value(self, val):
180 180 """
181 181 Setter that will always make sure we use unicode in app_settings_value
182 182
183 183 :param val:
184 184 """
185 185 self._app_settings_value = safe_unicode(val)
186 186
187 187 def __unicode__(self):
188 188 return u"<%s('%s:%s')>" % (
189 189 self.__class__.__name__,
190 190 self.app_settings_name, self.app_settings_value
191 191 )
192 192
193 193 @classmethod
194 194 def get_by_name(cls, key):
195 195 return cls.query()\
196 196 .filter(cls.app_settings_name == key).scalar()
197 197
198 198 @classmethod
199 199 def get_by_name_or_create(cls, key):
200 200 res = cls.get_by_name(key)
201 201 if not res:
202 202 res = cls(key)
203 203 return res
204 204
205 205 @classmethod
206 206 def get_app_settings(cls, cache=False):
207 207
208 208 ret = cls.query()
209 209
210 210 if cache:
211 211 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
212 212
213 213 if not ret:
214 214 raise Exception('Could not get application settings !')
215 215 settings = {}
216 216 for each in ret:
217 217 settings['rhodecode_' + each.app_settings_name] = \
218 218 each.app_settings_value
219 219
220 220 return settings
221 221
222 222 @classmethod
223 223 def get_ldap_settings(cls, cache=False):
224 224 ret = cls.query()\
225 225 .filter(cls.app_settings_name.startswith('ldap_')).all()
226 226 fd = {}
227 227 for row in ret:
228 228 fd.update({row.app_settings_name: row.app_settings_value})
229 229
230 230 return fd
231 231
232 232 @classmethod
233 233 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
234 234 ret = cls.query()\
235 235 .filter(cls.app_settings_name.startswith('default_')).all()
236 236 fd = {}
237 237 for row in ret:
238 238 key = row.app_settings_name
239 239 if strip_prefix:
240 240 key = remove_prefix(key, prefix='default_')
241 241 fd.update({key: row.app_settings_value})
242 242
243 243 return fd
244 244
245 245
246 246 class RhodeCodeUi(Base, BaseModel):
247 247 __tablename__ = 'rhodecode_ui'
248 248 __table_args__ = (
249 249 UniqueConstraint('ui_key'),
250 250 {'extend_existing': True, 'mysql_engine': 'InnoDB',
251 251 'mysql_charset': 'utf8'}
252 252 )
253 253
254 254 HOOK_UPDATE = 'changegroup.update'
255 255 HOOK_REPO_SIZE = 'changegroup.repo_size'
256 256 HOOK_PUSH = 'changegroup.push_logger'
257 257 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
258 258 HOOK_PULL = 'outgoing.pull_logger'
259 259 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
260 260
261 261 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
262 262 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
263 263 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 264 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 265 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
266 266
267 267 @classmethod
268 268 def get_by_key(cls, key):
269 269 return cls.query().filter(cls.ui_key == key).scalar()
270 270
271 271 @classmethod
272 272 def get_builtin_hooks(cls):
273 273 q = cls.query()
274 274 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
275 275 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
276 276 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
277 277 return q.all()
278 278
279 279 @classmethod
280 280 def get_custom_hooks(cls):
281 281 q = cls.query()
282 282 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
283 283 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
284 284 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
285 285 q = q.filter(cls.ui_section == 'hooks')
286 286 return q.all()
287 287
288 288 @classmethod
289 289 def get_repos_location(cls):
290 290 return cls.get_by_key('/').ui_value
291 291
292 292 @classmethod
293 293 def create_or_update_hook(cls, key, val):
294 294 new_ui = cls.get_by_key(key) or cls()
295 295 new_ui.ui_section = 'hooks'
296 296 new_ui.ui_active = True
297 297 new_ui.ui_key = key
298 298 new_ui.ui_value = val
299 299
300 300 Session().add(new_ui)
301 301
302 302 def __repr__(self):
303 303 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
304 304 self.ui_value)
305 305
306 306
307 307 class User(Base, BaseModel):
308 308 __tablename__ = 'users'
309 309 __table_args__ = (
310 310 UniqueConstraint('username'), UniqueConstraint('email'),
311 311 Index('u_username_idx', 'username'),
312 312 Index('u_email_idx', 'email'),
313 313 {'extend_existing': True, 'mysql_engine': 'InnoDB',
314 314 'mysql_charset': 'utf8'}
315 315 )
316 316 DEFAULT_USER = 'default'
317 317 DEFAULT_PERMISSIONS = [
318 318 'hg.register.manual_activate', 'hg.create.repository',
319 319 'hg.fork.repository', 'repository.read', 'group.read'
320 320 ]
321 321 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
322 322 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
323 323 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
324 324 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
325 325 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
326 326 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
327 327 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
328 328 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 329 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
330 330 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
331 331 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 332 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
333 333
334 334 user_log = relationship('UserLog')
335 335 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
336 336
337 337 repositories = relationship('Repository')
338 338 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
339 339 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
340 340
341 341 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
342 342 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
343 343
344 344 group_member = relationship('UsersGroupMember', cascade='all')
345 345
346 346 notifications = relationship('UserNotification', cascade='all')
347 347 # notifications assigned to this user
348 348 user_created_notifications = relationship('Notification', cascade='all')
349 349 # comments created by this user
350 350 user_comments = relationship('ChangesetComment', cascade='all')
351 351 #extra emails for this user
352 352 user_emails = relationship('UserEmailMap', cascade='all')
353 353
354 354 @hybrid_property
355 355 def email(self):
356 356 return self._email
357 357
358 358 @email.setter
359 359 def email(self, val):
360 360 self._email = val.lower() if val else None
361 361
362 362 @property
363 363 def firstname(self):
364 364 # alias for future
365 365 return self.name
366 366
367 367 @property
368 368 def emails(self):
369 369 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
370 370 return [self.email] + [x.email for x in other]
371 371
372 372 @property
373 373 def ip_addresses(self):
374 374 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
375 375 return [x.ip_addr for x in ret]
376 376
377 377 @property
378 378 def username_and_name(self):
379 379 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
380 380
381 381 @property
382 382 def full_name(self):
383 383 return '%s %s' % (self.firstname, self.lastname)
384 384
385 385 @property
386 386 def full_name_or_username(self):
387 387 return ('%s %s' % (self.firstname, self.lastname)
388 388 if (self.firstname and self.lastname) else self.username)
389 389
390 390 @property
391 391 def full_contact(self):
392 392 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
393 393
394 394 @property
395 395 def short_contact(self):
396 396 return '%s %s' % (self.firstname, self.lastname)
397 397
398 398 @property
399 399 def is_admin(self):
400 400 return self.admin
401 401
402 402 def __unicode__(self):
403 403 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
404 404 self.user_id, self.username)
405 405
406 406 @classmethod
407 407 def get_by_username(cls, username, case_insensitive=False, cache=False):
408 408 if case_insensitive:
409 409 q = cls.query().filter(cls.username.ilike(username))
410 410 else:
411 411 q = cls.query().filter(cls.username == username)
412 412
413 413 if cache:
414 414 q = q.options(FromCache(
415 415 "sql_cache_short",
416 416 "get_user_%s" % _hash_key(username)
417 417 )
418 418 )
419 419 return q.scalar()
420 420
421 421 @classmethod
422 422 def get_by_api_key(cls, api_key, cache=False):
423 423 q = cls.query().filter(cls.api_key == api_key)
424 424
425 425 if cache:
426 426 q = q.options(FromCache("sql_cache_short",
427 427 "get_api_key_%s" % api_key))
428 428 return q.scalar()
429 429
430 430 @classmethod
431 431 def get_by_email(cls, email, case_insensitive=False, cache=False):
432 432 if case_insensitive:
433 433 q = cls.query().filter(cls.email.ilike(email))
434 434 else:
435 435 q = cls.query().filter(cls.email == email)
436 436
437 437 if cache:
438 438 q = q.options(FromCache("sql_cache_short",
439 439 "get_email_key_%s" % email))
440 440
441 441 ret = q.scalar()
442 442 if ret is None:
443 443 q = UserEmailMap.query()
444 444 # try fetching in alternate email map
445 445 if case_insensitive:
446 446 q = q.filter(UserEmailMap.email.ilike(email))
447 447 else:
448 448 q = q.filter(UserEmailMap.email == email)
449 449 q = q.options(joinedload(UserEmailMap.user))
450 450 if cache:
451 451 q = q.options(FromCache("sql_cache_short",
452 452 "get_email_map_key_%s" % email))
453 453 ret = getattr(q.scalar(), 'user', None)
454 454
455 455 return ret
456 456
457 457 @classmethod
458 458 def get_from_cs_author(cls, author):
459 459 """
460 460 Tries to get User objects out of commit author string
461 461
462 462 :param author:
463 463 """
464 464 from rhodecode.lib.helpers import email, author_name
465 465 # Valid email in the attribute passed, see if they're in the system
466 466 _email = email(author)
467 467 if _email:
468 468 user = cls.get_by_email(_email, case_insensitive=True)
469 469 if user:
470 470 return user
471 471 # Maybe we can match by username?
472 472 _author = author_name(author)
473 473 user = cls.get_by_username(_author, case_insensitive=True)
474 474 if user:
475 475 return user
476 476
477 477 def update_lastlogin(self):
478 478 """Update user lastlogin"""
479 479 self.last_login = datetime.datetime.now()
480 480 Session().add(self)
481 481 log.debug('updated user %s lastlogin' % self.username)
482 482
483 483 def get_api_data(self):
484 484 """
485 485 Common function for generating user related data for API
486 486 """
487 487 user = self
488 488 data = dict(
489 489 user_id=user.user_id,
490 490 username=user.username,
491 491 firstname=user.name,
492 492 lastname=user.lastname,
493 493 email=user.email,
494 494 emails=user.emails,
495 495 api_key=user.api_key,
496 496 active=user.active,
497 497 admin=user.admin,
498 498 ldap_dn=user.ldap_dn,
499 499 last_login=user.last_login,
500 500 ip_addresses=user.ip_addresses
501 501 )
502 502 return data
503 503
504 504 def __json__(self):
505 505 data = dict(
506 506 full_name=self.full_name,
507 507 full_name_or_username=self.full_name_or_username,
508 508 short_contact=self.short_contact,
509 509 full_contact=self.full_contact
510 510 )
511 511 data.update(self.get_api_data())
512 512 return data
513 513
514 514
515 515 class UserEmailMap(Base, BaseModel):
516 516 __tablename__ = 'user_email_map'
517 517 __table_args__ = (
518 518 Index('uem_email_idx', 'email'),
519 519 UniqueConstraint('email'),
520 520 {'extend_existing': True, 'mysql_engine': 'InnoDB',
521 521 'mysql_charset': 'utf8'}
522 522 )
523 523 __mapper_args__ = {}
524 524
525 525 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
526 526 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
527 527 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
528 528 user = relationship('User', lazy='joined')
529 529
530 530 @validates('_email')
531 531 def validate_email(self, key, email):
532 532 # check if this email is not main one
533 533 main_email = Session().query(User).filter(User.email == email).scalar()
534 534 if main_email is not None:
535 535 raise AttributeError('email %s is present is user table' % email)
536 536 return email
537 537
538 538 @hybrid_property
539 539 def email(self):
540 540 return self._email
541 541
542 542 @email.setter
543 543 def email(self, val):
544 544 self._email = val.lower() if val else None
545 545
546 546
547 547 class UserIpMap(Base, BaseModel):
548 548 __tablename__ = 'user_ip_map'
549 549 __table_args__ = (
550 550 UniqueConstraint('user_id', 'ip_addr'),
551 551 {'extend_existing': True, 'mysql_engine': 'InnoDB',
552 552 'mysql_charset': 'utf8'}
553 553 )
554 554 __mapper_args__ = {}
555 555
556 556 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
557 557 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
558 558 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
559 559 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
560 560 user = relationship('User', lazy='joined')
561 561
562 562 @classmethod
563 563 def _get_ip_range(cls, ip_addr):
564 564 from rhodecode.lib import ipaddr
565 net = ipaddr.IPv4Network(ip_addr)
565 net = ipaddr.IPNetwork(address=ip_addr)
566 566 return [str(net.network), str(net.broadcast)]
567 567
568 568 def __json__(self):
569 569 return dict(
570 570 ip_addr=self.ip_addr,
571 571 ip_range=self._get_ip_range(self.ip_addr)
572 572 )
573 573
574 574
575 575 class UserLog(Base, BaseModel):
576 576 __tablename__ = 'user_logs'
577 577 __table_args__ = (
578 578 {'extend_existing': True, 'mysql_engine': 'InnoDB',
579 579 'mysql_charset': 'utf8'},
580 580 )
581 581 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
582 582 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
583 583 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
584 584 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
585 585 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
586 586 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
587 587 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
588 588 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
589 589
590 590 @property
591 591 def action_as_day(self):
592 592 return datetime.date(*self.action_date.timetuple()[:3])
593 593
594 594 user = relationship('User')
595 595 repository = relationship('Repository', cascade='')
596 596
597 597
598 598 class UsersGroup(Base, BaseModel):
599 599 __tablename__ = 'users_groups'
600 600 __table_args__ = (
601 601 {'extend_existing': True, 'mysql_engine': 'InnoDB',
602 602 'mysql_charset': 'utf8'},
603 603 )
604 604
605 605 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
606 606 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
607 607 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
608 608 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
609 609
610 610 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
611 611 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
612 612 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
613 613
614 614 def __unicode__(self):
615 615 return u'<userGroup(%s)>' % (self.users_group_name)
616 616
617 617 @classmethod
618 618 def get_by_group_name(cls, group_name, cache=False,
619 619 case_insensitive=False):
620 620 if case_insensitive:
621 621 q = cls.query().filter(cls.users_group_name.ilike(group_name))
622 622 else:
623 623 q = cls.query().filter(cls.users_group_name == group_name)
624 624 if cache:
625 625 q = q.options(FromCache(
626 626 "sql_cache_short",
627 627 "get_user_%s" % _hash_key(group_name)
628 628 )
629 629 )
630 630 return q.scalar()
631 631
632 632 @classmethod
633 633 def get(cls, users_group_id, cache=False):
634 634 users_group = cls.query()
635 635 if cache:
636 636 users_group = users_group.options(FromCache("sql_cache_short",
637 637 "get_users_group_%s" % users_group_id))
638 638 return users_group.get(users_group_id)
639 639
640 640 def get_api_data(self):
641 641 users_group = self
642 642
643 643 data = dict(
644 644 users_group_id=users_group.users_group_id,
645 645 group_name=users_group.users_group_name,
646 646 active=users_group.users_group_active,
647 647 )
648 648
649 649 return data
650 650
651 651
652 652 class UsersGroupMember(Base, BaseModel):
653 653 __tablename__ = 'users_groups_members'
654 654 __table_args__ = (
655 655 {'extend_existing': True, 'mysql_engine': 'InnoDB',
656 656 'mysql_charset': 'utf8'},
657 657 )
658 658
659 659 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
660 660 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
661 661 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
662 662
663 663 user = relationship('User', lazy='joined')
664 664 users_group = relationship('UsersGroup')
665 665
666 666 def __init__(self, gr_id='', u_id=''):
667 667 self.users_group_id = gr_id
668 668 self.user_id = u_id
669 669
670 670
671 671 class Repository(Base, BaseModel):
672 672 __tablename__ = 'repositories'
673 673 __table_args__ = (
674 674 UniqueConstraint('repo_name'),
675 675 Index('r_repo_name_idx', 'repo_name'),
676 676 {'extend_existing': True, 'mysql_engine': 'InnoDB',
677 677 'mysql_charset': 'utf8'},
678 678 )
679 679
680 680 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
681 681 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
682 682 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
683 683 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
684 684 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
685 685 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
686 686 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
687 687 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
688 688 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
689 689 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
690 690 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
691 691 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
692 692 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
693 693 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
694 694 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
695 695
696 696 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
697 697 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
698 698
699 699 user = relationship('User')
700 700 fork = relationship('Repository', remote_side=repo_id)
701 701 group = relationship('RepoGroup')
702 702 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
703 703 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
704 704 stats = relationship('Statistics', cascade='all', uselist=False)
705 705
706 706 followers = relationship('UserFollowing',
707 707 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
708 708 cascade='all')
709 709
710 710 logs = relationship('UserLog')
711 711 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
712 712
713 713 pull_requests_org = relationship('PullRequest',
714 714 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
715 715 cascade="all, delete, delete-orphan")
716 716
717 717 pull_requests_other = relationship('PullRequest',
718 718 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
719 719 cascade="all, delete, delete-orphan")
720 720
721 721 def __unicode__(self):
722 722 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
723 723 self.repo_name)
724 724
725 725 @hybrid_property
726 726 def locked(self):
727 727 # always should return [user_id, timelocked]
728 728 if self._locked:
729 729 _lock_info = self._locked.split(':')
730 730 return int(_lock_info[0]), _lock_info[1]
731 731 return [None, None]
732 732
733 733 @locked.setter
734 734 def locked(self, val):
735 735 if val and isinstance(val, (list, tuple)):
736 736 self._locked = ':'.join(map(str, val))
737 737 else:
738 738 self._locked = None
739 739
740 740 @hybrid_property
741 741 def changeset_cache(self):
742 742 from rhodecode.lib.vcs.backends.base import EmptyChangeset
743 743 dummy = EmptyChangeset().__json__()
744 744 if not self._changeset_cache:
745 745 return dummy
746 746 try:
747 747 return json.loads(self._changeset_cache)
748 748 except TypeError:
749 749 return dummy
750 750
751 751 @changeset_cache.setter
752 752 def changeset_cache(self, val):
753 753 try:
754 754 self._changeset_cache = json.dumps(val)
755 755 except:
756 756 log.error(traceback.format_exc())
757 757
758 758 @classmethod
759 759 def url_sep(cls):
760 760 return URL_SEP
761 761
762 762 @classmethod
763 763 def normalize_repo_name(cls, repo_name):
764 764 """
765 765 Normalizes os specific repo_name to the format internally stored inside
766 766 dabatabase using URL_SEP
767 767
768 768 :param cls:
769 769 :param repo_name:
770 770 """
771 771 return cls.url_sep().join(repo_name.split(os.sep))
772 772
773 773 @classmethod
774 774 def get_by_repo_name(cls, repo_name):
775 775 q = Session().query(cls).filter(cls.repo_name == repo_name)
776 776 q = q.options(joinedload(Repository.fork))\
777 777 .options(joinedload(Repository.user))\
778 778 .options(joinedload(Repository.group))
779 779 return q.scalar()
780 780
781 781 @classmethod
782 782 def get_by_full_path(cls, repo_full_path):
783 783 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
784 784 repo_name = cls.normalize_repo_name(repo_name)
785 785 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
786 786
787 787 @classmethod
788 788 def get_repo_forks(cls, repo_id):
789 789 return cls.query().filter(Repository.fork_id == repo_id)
790 790
791 791 @classmethod
792 792 def base_path(cls):
793 793 """
794 794 Returns base path when all repos are stored
795 795
796 796 :param cls:
797 797 """
798 798 q = Session().query(RhodeCodeUi)\
799 799 .filter(RhodeCodeUi.ui_key == cls.url_sep())
800 800 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
801 801 return q.one().ui_value
802 802
803 803 @property
804 804 def forks(self):
805 805 """
806 806 Return forks of this repo
807 807 """
808 808 return Repository.get_repo_forks(self.repo_id)
809 809
810 810 @property
811 811 def parent(self):
812 812 """
813 813 Returns fork parent
814 814 """
815 815 return self.fork
816 816
817 817 @property
818 818 def just_name(self):
819 819 return self.repo_name.split(Repository.url_sep())[-1]
820 820
821 821 @property
822 822 def groups_with_parents(self):
823 823 groups = []
824 824 if self.group is None:
825 825 return groups
826 826
827 827 cur_gr = self.group
828 828 groups.insert(0, cur_gr)
829 829 while 1:
830 830 gr = getattr(cur_gr, 'parent_group', None)
831 831 cur_gr = cur_gr.parent_group
832 832 if gr is None:
833 833 break
834 834 groups.insert(0, gr)
835 835
836 836 return groups
837 837
838 838 @property
839 839 def groups_and_repo(self):
840 840 return self.groups_with_parents, self.just_name
841 841
842 842 @LazyProperty
843 843 def repo_path(self):
844 844 """
845 845 Returns base full path for that repository means where it actually
846 846 exists on a filesystem
847 847 """
848 848 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
849 849 Repository.url_sep())
850 850 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
851 851 return q.one().ui_value
852 852
853 853 @property
854 854 def repo_full_path(self):
855 855 p = [self.repo_path]
856 856 # we need to split the name by / since this is how we store the
857 857 # names in the database, but that eventually needs to be converted
858 858 # into a valid system path
859 859 p += self.repo_name.split(Repository.url_sep())
860 860 return os.path.join(*p)
861 861
862 862 @property
863 863 def cache_keys(self):
864 864 """
865 865 Returns associated cache keys for that repo
866 866 """
867 867 return CacheInvalidation.query()\
868 868 .filter(CacheInvalidation.cache_args == self.repo_name)\
869 869 .order_by(CacheInvalidation.cache_key)\
870 870 .all()
871 871
872 872 def get_new_name(self, repo_name):
873 873 """
874 874 returns new full repository name based on assigned group and new new
875 875
876 876 :param group_name:
877 877 """
878 878 path_prefix = self.group.full_path_splitted if self.group else []
879 879 return Repository.url_sep().join(path_prefix + [repo_name])
880 880
881 881 @property
882 882 def _ui(self):
883 883 """
884 884 Creates an db based ui object for this repository
885 885 """
886 886 from rhodecode.lib.utils import make_ui
887 887 return make_ui('db', clear_session=False)
888 888
889 889 @classmethod
890 890 def inject_ui(cls, repo, extras={}):
891 891 from rhodecode.lib.vcs.backends.hg import MercurialRepository
892 892 from rhodecode.lib.vcs.backends.git import GitRepository
893 893 required = (MercurialRepository, GitRepository)
894 894 if not isinstance(repo, required):
895 895 raise Exception('repo must be instance of %s' % required)
896 896
897 897 # inject ui extra param to log this action via push logger
898 898 for k, v in extras.items():
899 899 repo._repo.ui.setconfig('rhodecode_extras', k, v)
900 900
901 901 @classmethod
902 902 def is_valid(cls, repo_name):
903 903 """
904 904 returns True if given repo name is a valid filesystem repository
905 905
906 906 :param cls:
907 907 :param repo_name:
908 908 """
909 909 from rhodecode.lib.utils import is_valid_repo
910 910
911 911 return is_valid_repo(repo_name, cls.base_path())
912 912
913 913 def get_api_data(self):
914 914 """
915 915 Common function for generating repo api data
916 916
917 917 """
918 918 repo = self
919 919 data = dict(
920 920 repo_id=repo.repo_id,
921 921 repo_name=repo.repo_name,
922 922 repo_type=repo.repo_type,
923 923 clone_uri=repo.clone_uri,
924 924 private=repo.private,
925 925 created_on=repo.created_on,
926 926 description=repo.description,
927 927 landing_rev=repo.landing_rev,
928 928 owner=repo.user.username,
929 929 fork_of=repo.fork.repo_name if repo.fork else None,
930 930 enable_statistics=repo.enable_statistics,
931 931 enable_locking=repo.enable_locking,
932 932 enable_downloads=repo.enable_downloads,
933 933 last_changeset=repo.changeset_cache
934 934 )
935 935
936 936 return data
937 937
938 938 @classmethod
939 939 def lock(cls, repo, user_id):
940 940 repo.locked = [user_id, time.time()]
941 941 Session().add(repo)
942 942 Session().commit()
943 943
944 944 @classmethod
945 945 def unlock(cls, repo):
946 946 repo.locked = None
947 947 Session().add(repo)
948 948 Session().commit()
949 949
950 950 @property
951 951 def last_db_change(self):
952 952 return self.updated_on
953 953
954 954 def clone_url(self, **override):
955 955 from pylons import url
956 956 from urlparse import urlparse
957 957 import urllib
958 958 parsed_url = urlparse(url('home', qualified=True))
959 959 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
960 960 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
961 961 args = {
962 962 'user': '',
963 963 'pass': '',
964 964 'scheme': parsed_url.scheme,
965 965 'netloc': parsed_url.netloc,
966 966 'prefix': decoded_path,
967 967 'path': self.repo_name
968 968 }
969 969
970 970 args.update(override)
971 971 return default_clone_uri % args
972 972
973 973 #==========================================================================
974 974 # SCM PROPERTIES
975 975 #==========================================================================
976 976
977 977 def get_changeset(self, rev=None):
978 978 return get_changeset_safe(self.scm_instance, rev)
979 979
980 980 def get_landing_changeset(self):
981 981 """
982 982 Returns landing changeset, or if that doesn't exist returns the tip
983 983 """
984 984 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
985 985 return cs
986 986
987 987 def update_changeset_cache(self, cs_cache=None):
988 988 """
989 989 Update cache of last changeset for repository, keys should be::
990 990
991 991 short_id
992 992 raw_id
993 993 revision
994 994 message
995 995 date
996 996 author
997 997
998 998 :param cs_cache:
999 999 """
1000 1000 from rhodecode.lib.vcs.backends.base import BaseChangeset
1001 1001 if cs_cache is None:
1002 1002 cs_cache = self.get_changeset()
1003 1003 if isinstance(cs_cache, BaseChangeset):
1004 1004 cs_cache = cs_cache.__json__()
1005 1005
1006 1006 if cs_cache != self.changeset_cache:
1007 1007 last_change = cs_cache.get('date') or self.last_change
1008 1008 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
1009 1009 self.updated_on = last_change
1010 1010 self.changeset_cache = cs_cache
1011 1011 Session().add(self)
1012 1012 Session().commit()
1013 1013
1014 1014 @property
1015 1015 def tip(self):
1016 1016 return self.get_changeset('tip')
1017 1017
1018 1018 @property
1019 1019 def author(self):
1020 1020 return self.tip.author
1021 1021
1022 1022 @property
1023 1023 def last_change(self):
1024 1024 return self.scm_instance.last_change
1025 1025
1026 1026 def get_comments(self, revisions=None):
1027 1027 """
1028 1028 Returns comments for this repository grouped by revisions
1029 1029
1030 1030 :param revisions: filter query by revisions only
1031 1031 """
1032 1032 cmts = ChangesetComment.query()\
1033 1033 .filter(ChangesetComment.repo == self)
1034 1034 if revisions:
1035 1035 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1036 1036 grouped = defaultdict(list)
1037 1037 for cmt in cmts.all():
1038 1038 grouped[cmt.revision].append(cmt)
1039 1039 return grouped
1040 1040
1041 1041 def statuses(self, revisions=None):
1042 1042 """
1043 1043 Returns statuses for this repository
1044 1044
1045 1045 :param revisions: list of revisions to get statuses for
1046 1046 :type revisions: list
1047 1047 """
1048 1048
1049 1049 statuses = ChangesetStatus.query()\
1050 1050 .filter(ChangesetStatus.repo == self)\
1051 1051 .filter(ChangesetStatus.version == 0)
1052 1052 if revisions:
1053 1053 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1054 1054 grouped = {}
1055 1055
1056 1056 #maybe we have open new pullrequest without a status ?
1057 1057 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1058 1058 status_lbl = ChangesetStatus.get_status_lbl(stat)
1059 1059 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1060 1060 for rev in pr.revisions:
1061 1061 pr_id = pr.pull_request_id
1062 1062 pr_repo = pr.other_repo.repo_name
1063 1063 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1064 1064
1065 1065 for stat in statuses.all():
1066 1066 pr_id = pr_repo = None
1067 1067 if stat.pull_request:
1068 1068 pr_id = stat.pull_request.pull_request_id
1069 1069 pr_repo = stat.pull_request.other_repo.repo_name
1070 1070 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1071 1071 pr_id, pr_repo]
1072 1072 return grouped
1073 1073
1074 1074 #==========================================================================
1075 1075 # SCM CACHE INSTANCE
1076 1076 #==========================================================================
1077 1077
1078 1078 @property
1079 1079 def invalidate(self):
1080 1080 return CacheInvalidation.invalidate(self.repo_name)
1081 1081
1082 1082 def set_invalidate(self):
1083 1083 """
1084 1084 set a cache for invalidation for this instance
1085 1085 """
1086 1086 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
1087 1087
1088 1088 @LazyProperty
1089 1089 def scm_instance(self):
1090 1090 import rhodecode
1091 1091 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1092 1092 if full_cache:
1093 1093 return self.scm_instance_cached()
1094 1094 return self.__get_instance()
1095 1095
1096 1096 def scm_instance_cached(self, cache_map=None):
1097 1097 @cache_region('long_term')
1098 1098 def _c(repo_name):
1099 1099 return self.__get_instance()
1100 1100 rn = self.repo_name
1101 1101 log.debug('Getting cached instance of repo')
1102 1102
1103 1103 if cache_map:
1104 1104 # get using prefilled cache_map
1105 1105 invalidate_repo = cache_map[self.repo_name]
1106 1106 if invalidate_repo:
1107 1107 invalidate_repo = (None if invalidate_repo.cache_active
1108 1108 else invalidate_repo)
1109 1109 else:
1110 1110 # get from invalidate
1111 1111 invalidate_repo = self.invalidate
1112 1112
1113 1113 if invalidate_repo is not None:
1114 1114 region_invalidate(_c, None, rn)
1115 1115 # update our cache
1116 1116 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1117 1117 return _c(rn)
1118 1118
1119 1119 def __get_instance(self):
1120 1120 repo_full_path = self.repo_full_path
1121 1121 try:
1122 1122 alias = get_scm(repo_full_path)[0]
1123 1123 log.debug('Creating instance of %s repository' % alias)
1124 1124 backend = get_backend(alias)
1125 1125 except VCSError:
1126 1126 log.error(traceback.format_exc())
1127 1127 log.error('Perhaps this repository is in db and not in '
1128 1128 'filesystem run rescan repositories with '
1129 1129 '"destroy old data " option from admin panel')
1130 1130 return
1131 1131
1132 1132 if alias == 'hg':
1133 1133
1134 1134 repo = backend(safe_str(repo_full_path), create=False,
1135 1135 baseui=self._ui)
1136 1136 # skip hidden web repository
1137 1137 if repo._get_hidden():
1138 1138 return
1139 1139 else:
1140 1140 repo = backend(repo_full_path, create=False)
1141 1141
1142 1142 return repo
1143 1143
1144 1144
1145 1145 class RepoGroup(Base, BaseModel):
1146 1146 __tablename__ = 'groups'
1147 1147 __table_args__ = (
1148 1148 UniqueConstraint('group_name', 'group_parent_id'),
1149 1149 CheckConstraint('group_id != group_parent_id'),
1150 1150 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1151 1151 'mysql_charset': 'utf8'},
1152 1152 )
1153 1153 __mapper_args__ = {'order_by': 'group_name'}
1154 1154
1155 1155 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1156 1156 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1157 1157 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1158 1158 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1159 1159 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1160 1160
1161 1161 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1162 1162 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
1163 1163
1164 1164 parent_group = relationship('RepoGroup', remote_side=group_id)
1165 1165
1166 1166 def __init__(self, group_name='', parent_group=None):
1167 1167 self.group_name = group_name
1168 1168 self.parent_group = parent_group
1169 1169
1170 1170 def __unicode__(self):
1171 1171 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1172 1172 self.group_name)
1173 1173
1174 1174 @classmethod
1175 1175 def groups_choices(cls, check_perms=False):
1176 1176 from webhelpers.html import literal as _literal
1177 1177 from rhodecode.model.scm import ScmModel
1178 1178 groups = cls.query().all()
1179 1179 if check_perms:
1180 1180 #filter group user have access to, it's done
1181 1181 #magically inside ScmModel based on current user
1182 1182 groups = ScmModel().get_repos_groups(groups)
1183 1183 repo_groups = [('', '')]
1184 1184 sep = ' &raquo; '
1185 1185 _name = lambda k: _literal(sep.join(k))
1186 1186
1187 1187 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1188 1188 for x in groups])
1189 1189
1190 1190 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1191 1191 return repo_groups
1192 1192
1193 1193 @classmethod
1194 1194 def url_sep(cls):
1195 1195 return URL_SEP
1196 1196
1197 1197 @classmethod
1198 1198 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1199 1199 if case_insensitive:
1200 1200 gr = cls.query()\
1201 1201 .filter(cls.group_name.ilike(group_name))
1202 1202 else:
1203 1203 gr = cls.query()\
1204 1204 .filter(cls.group_name == group_name)
1205 1205 if cache:
1206 1206 gr = gr.options(FromCache(
1207 1207 "sql_cache_short",
1208 1208 "get_group_%s" % _hash_key(group_name)
1209 1209 )
1210 1210 )
1211 1211 return gr.scalar()
1212 1212
1213 1213 @property
1214 1214 def parents(self):
1215 1215 parents_recursion_limit = 5
1216 1216 groups = []
1217 1217 if self.parent_group is None:
1218 1218 return groups
1219 1219 cur_gr = self.parent_group
1220 1220 groups.insert(0, cur_gr)
1221 1221 cnt = 0
1222 1222 while 1:
1223 1223 cnt += 1
1224 1224 gr = getattr(cur_gr, 'parent_group', None)
1225 1225 cur_gr = cur_gr.parent_group
1226 1226 if gr is None:
1227 1227 break
1228 1228 if cnt == parents_recursion_limit:
1229 1229 # this will prevent accidental infinit loops
1230 1230 log.error('group nested more than %s' %
1231 1231 parents_recursion_limit)
1232 1232 break
1233 1233
1234 1234 groups.insert(0, gr)
1235 1235 return groups
1236 1236
1237 1237 @property
1238 1238 def children(self):
1239 1239 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1240 1240
1241 1241 @property
1242 1242 def name(self):
1243 1243 return self.group_name.split(RepoGroup.url_sep())[-1]
1244 1244
1245 1245 @property
1246 1246 def full_path(self):
1247 1247 return self.group_name
1248 1248
1249 1249 @property
1250 1250 def full_path_splitted(self):
1251 1251 return self.group_name.split(RepoGroup.url_sep())
1252 1252
1253 1253 @property
1254 1254 def repositories(self):
1255 1255 return Repository.query()\
1256 1256 .filter(Repository.group == self)\
1257 1257 .order_by(Repository.repo_name)
1258 1258
1259 1259 @property
1260 1260 def repositories_recursive_count(self):
1261 1261 cnt = self.repositories.count()
1262 1262
1263 1263 def children_count(group):
1264 1264 cnt = 0
1265 1265 for child in group.children:
1266 1266 cnt += child.repositories.count()
1267 1267 cnt += children_count(child)
1268 1268 return cnt
1269 1269
1270 1270 return cnt + children_count(self)
1271 1271
1272 1272 def recursive_groups_and_repos(self):
1273 1273 """
1274 1274 Recursive return all groups, with repositories in those groups
1275 1275 """
1276 1276 all_ = []
1277 1277
1278 1278 def _get_members(root_gr):
1279 1279 for r in root_gr.repositories:
1280 1280 all_.append(r)
1281 1281 childs = root_gr.children.all()
1282 1282 if childs:
1283 1283 for gr in childs:
1284 1284 all_.append(gr)
1285 1285 _get_members(gr)
1286 1286
1287 1287 _get_members(self)
1288 1288 return [self] + all_
1289 1289
1290 1290 def get_new_name(self, group_name):
1291 1291 """
1292 1292 returns new full group name based on parent and new name
1293 1293
1294 1294 :param group_name:
1295 1295 """
1296 1296 path_prefix = (self.parent_group.full_path_splitted if
1297 1297 self.parent_group else [])
1298 1298 return RepoGroup.url_sep().join(path_prefix + [group_name])
1299 1299
1300 1300
1301 1301 class Permission(Base, BaseModel):
1302 1302 __tablename__ = 'permissions'
1303 1303 __table_args__ = (
1304 1304 Index('p_perm_name_idx', 'permission_name'),
1305 1305 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1306 1306 'mysql_charset': 'utf8'},
1307 1307 )
1308 1308 PERMS = [
1309 1309 ('repository.none', _('Repository no access')),
1310 1310 ('repository.read', _('Repository read access')),
1311 1311 ('repository.write', _('Repository write access')),
1312 1312 ('repository.admin', _('Repository admin access')),
1313 1313
1314 1314 ('group.none', _('Repositories Group no access')),
1315 1315 ('group.read', _('Repositories Group read access')),
1316 1316 ('group.write', _('Repositories Group write access')),
1317 1317 ('group.admin', _('Repositories Group admin access')),
1318 1318
1319 1319 ('hg.admin', _('RhodeCode Administrator')),
1320 1320 ('hg.create.none', _('Repository creation disabled')),
1321 1321 ('hg.create.repository', _('Repository creation enabled')),
1322 1322 ('hg.fork.none', _('Repository forking disabled')),
1323 1323 ('hg.fork.repository', _('Repository forking enabled')),
1324 1324 ('hg.register.none', _('Register disabled')),
1325 1325 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1326 1326 'with manual activation')),
1327 1327
1328 1328 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1329 1329 'with auto activation')),
1330 1330 ]
1331 1331
1332 1332 # defines which permissions are more important higher the more important
1333 1333 PERM_WEIGHTS = {
1334 1334 'repository.none': 0,
1335 1335 'repository.read': 1,
1336 1336 'repository.write': 3,
1337 1337 'repository.admin': 4,
1338 1338
1339 1339 'group.none': 0,
1340 1340 'group.read': 1,
1341 1341 'group.write': 3,
1342 1342 'group.admin': 4,
1343 1343
1344 1344 'hg.fork.none': 0,
1345 1345 'hg.fork.repository': 1,
1346 1346 'hg.create.none': 0,
1347 1347 'hg.create.repository':1
1348 1348 }
1349 1349
1350 1350 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1351 1351 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1352 1352 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1353 1353
1354 1354 def __unicode__(self):
1355 1355 return u"<%s('%s:%s')>" % (
1356 1356 self.__class__.__name__, self.permission_id, self.permission_name
1357 1357 )
1358 1358
1359 1359 @classmethod
1360 1360 def get_by_key(cls, key):
1361 1361 return cls.query().filter(cls.permission_name == key).scalar()
1362 1362
1363 1363 @classmethod
1364 1364 def get_default_perms(cls, default_user_id):
1365 1365 q = Session().query(UserRepoToPerm, Repository, cls)\
1366 1366 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1367 1367 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1368 1368 .filter(UserRepoToPerm.user_id == default_user_id)
1369 1369
1370 1370 return q.all()
1371 1371
1372 1372 @classmethod
1373 1373 def get_default_group_perms(cls, default_user_id):
1374 1374 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1375 1375 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1376 1376 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1377 1377 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1378 1378
1379 1379 return q.all()
1380 1380
1381 1381
1382 1382 class UserRepoToPerm(Base, BaseModel):
1383 1383 __tablename__ = 'repo_to_perm'
1384 1384 __table_args__ = (
1385 1385 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1386 1386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1387 1387 'mysql_charset': 'utf8'}
1388 1388 )
1389 1389 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1390 1390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1391 1391 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1392 1392 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1393 1393
1394 1394 user = relationship('User')
1395 1395 repository = relationship('Repository')
1396 1396 permission = relationship('Permission')
1397 1397
1398 1398 @classmethod
1399 1399 def create(cls, user, repository, permission):
1400 1400 n = cls()
1401 1401 n.user = user
1402 1402 n.repository = repository
1403 1403 n.permission = permission
1404 1404 Session().add(n)
1405 1405 return n
1406 1406
1407 1407 def __unicode__(self):
1408 1408 return u'<user:%s => %s >' % (self.user, self.repository)
1409 1409
1410 1410
1411 1411 class UserToPerm(Base, BaseModel):
1412 1412 __tablename__ = 'user_to_perm'
1413 1413 __table_args__ = (
1414 1414 UniqueConstraint('user_id', 'permission_id'),
1415 1415 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1416 1416 'mysql_charset': 'utf8'}
1417 1417 )
1418 1418 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1419 1419 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1420 1420 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1421 1421
1422 1422 user = relationship('User')
1423 1423 permission = relationship('Permission', lazy='joined')
1424 1424
1425 1425
1426 1426 class UsersGroupRepoToPerm(Base, BaseModel):
1427 1427 __tablename__ = 'users_group_repo_to_perm'
1428 1428 __table_args__ = (
1429 1429 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1430 1430 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1431 1431 'mysql_charset': 'utf8'}
1432 1432 )
1433 1433 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1434 1434 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1435 1435 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1436 1436 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1437 1437
1438 1438 users_group = relationship('UsersGroup')
1439 1439 permission = relationship('Permission')
1440 1440 repository = relationship('Repository')
1441 1441
1442 1442 @classmethod
1443 1443 def create(cls, users_group, repository, permission):
1444 1444 n = cls()
1445 1445 n.users_group = users_group
1446 1446 n.repository = repository
1447 1447 n.permission = permission
1448 1448 Session().add(n)
1449 1449 return n
1450 1450
1451 1451 def __unicode__(self):
1452 1452 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1453 1453
1454 1454
1455 1455 class UsersGroupToPerm(Base, BaseModel):
1456 1456 __tablename__ = 'users_group_to_perm'
1457 1457 __table_args__ = (
1458 1458 UniqueConstraint('users_group_id', 'permission_id',),
1459 1459 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1460 1460 'mysql_charset': 'utf8'}
1461 1461 )
1462 1462 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1463 1463 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1464 1464 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1465 1465
1466 1466 users_group = relationship('UsersGroup')
1467 1467 permission = relationship('Permission')
1468 1468
1469 1469
1470 1470 class UserRepoGroupToPerm(Base, BaseModel):
1471 1471 __tablename__ = 'user_repo_group_to_perm'
1472 1472 __table_args__ = (
1473 1473 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1474 1474 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1475 1475 'mysql_charset': 'utf8'}
1476 1476 )
1477 1477
1478 1478 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1479 1479 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1480 1480 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1481 1481 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1482 1482
1483 1483 user = relationship('User')
1484 1484 group = relationship('RepoGroup')
1485 1485 permission = relationship('Permission')
1486 1486
1487 1487
1488 1488 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1489 1489 __tablename__ = 'users_group_repo_group_to_perm'
1490 1490 __table_args__ = (
1491 1491 UniqueConstraint('users_group_id', 'group_id'),
1492 1492 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1493 1493 'mysql_charset': 'utf8'}
1494 1494 )
1495 1495
1496 1496 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1497 1497 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1498 1498 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1499 1499 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1500 1500
1501 1501 users_group = relationship('UsersGroup')
1502 1502 permission = relationship('Permission')
1503 1503 group = relationship('RepoGroup')
1504 1504
1505 1505
1506 1506 class Statistics(Base, BaseModel):
1507 1507 __tablename__ = 'statistics'
1508 1508 __table_args__ = (
1509 1509 UniqueConstraint('repository_id'),
1510 1510 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1511 1511 'mysql_charset': 'utf8'}
1512 1512 )
1513 1513 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1514 1514 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1515 1515 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1516 1516 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1517 1517 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1518 1518 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1519 1519
1520 1520 repository = relationship('Repository', single_parent=True)
1521 1521
1522 1522
1523 1523 class UserFollowing(Base, BaseModel):
1524 1524 __tablename__ = 'user_followings'
1525 1525 __table_args__ = (
1526 1526 UniqueConstraint('user_id', 'follows_repository_id'),
1527 1527 UniqueConstraint('user_id', 'follows_user_id'),
1528 1528 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1529 1529 'mysql_charset': 'utf8'}
1530 1530 )
1531 1531
1532 1532 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1533 1533 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1534 1534 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1535 1535 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1536 1536 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1537 1537
1538 1538 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1539 1539
1540 1540 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1541 1541 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1542 1542
1543 1543 @classmethod
1544 1544 def get_repo_followers(cls, repo_id):
1545 1545 return cls.query().filter(cls.follows_repo_id == repo_id)
1546 1546
1547 1547
1548 1548 class CacheInvalidation(Base, BaseModel):
1549 1549 __tablename__ = 'cache_invalidation'
1550 1550 __table_args__ = (
1551 1551 UniqueConstraint('cache_key'),
1552 1552 Index('key_idx', 'cache_key'),
1553 1553 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1554 1554 'mysql_charset': 'utf8'},
1555 1555 )
1556 1556 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1557 1557 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1558 1558 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1559 1559 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1560 1560
1561 1561 def __init__(self, cache_key, cache_args=''):
1562 1562 self.cache_key = cache_key
1563 1563 self.cache_args = cache_args
1564 1564 self.cache_active = False
1565 1565
1566 1566 def __unicode__(self):
1567 1567 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1568 1568 self.cache_id, self.cache_key)
1569 1569
1570 1570 @property
1571 1571 def prefix(self):
1572 1572 _split = self.cache_key.split(self.cache_args, 1)
1573 1573 if _split and len(_split) == 2:
1574 1574 return _split[0]
1575 1575 return ''
1576 1576
1577 1577 @classmethod
1578 1578 def clear_cache(cls):
1579 1579 cls.query().delete()
1580 1580
1581 1581 @classmethod
1582 1582 def _get_key(cls, key):
1583 1583 """
1584 1584 Wrapper for generating a key, together with a prefix
1585 1585
1586 1586 :param key:
1587 1587 """
1588 1588 import rhodecode
1589 1589 prefix = ''
1590 1590 org_key = key
1591 1591 iid = rhodecode.CONFIG.get('instance_id')
1592 1592 if iid:
1593 1593 prefix = iid
1594 1594
1595 1595 return "%s%s" % (prefix, key), prefix, org_key
1596 1596
1597 1597 @classmethod
1598 1598 def get_by_key(cls, key):
1599 1599 return cls.query().filter(cls.cache_key == key).scalar()
1600 1600
1601 1601 @classmethod
1602 1602 def get_by_repo_name(cls, repo_name):
1603 1603 return cls.query().filter(cls.cache_args == repo_name).all()
1604 1604
1605 1605 @classmethod
1606 1606 def _get_or_create_key(cls, key, repo_name, commit=True):
1607 1607 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1608 1608 if not inv_obj:
1609 1609 try:
1610 1610 inv_obj = CacheInvalidation(key, repo_name)
1611 1611 Session().add(inv_obj)
1612 1612 if commit:
1613 1613 Session().commit()
1614 1614 except Exception:
1615 1615 log.error(traceback.format_exc())
1616 1616 Session().rollback()
1617 1617 return inv_obj
1618 1618
1619 1619 @classmethod
1620 1620 def invalidate(cls, key):
1621 1621 """
1622 1622 Returns Invalidation object if this given key should be invalidated
1623 1623 None otherwise. `cache_active = False` means that this cache
1624 1624 state is not valid and needs to be invalidated
1625 1625
1626 1626 :param key:
1627 1627 """
1628 1628 repo_name = key
1629 1629 repo_name = remove_suffix(repo_name, '_README')
1630 1630 repo_name = remove_suffix(repo_name, '_RSS')
1631 1631 repo_name = remove_suffix(repo_name, '_ATOM')
1632 1632
1633 1633 # adds instance prefix
1634 1634 key, _prefix, _org_key = cls._get_key(key)
1635 1635 inv = cls._get_or_create_key(key, repo_name)
1636 1636
1637 1637 if inv and inv.cache_active is False:
1638 1638 return inv
1639 1639
1640 1640 @classmethod
1641 1641 def set_invalidate(cls, key=None, repo_name=None):
1642 1642 """
1643 1643 Mark this Cache key for invalidation, either by key or whole
1644 1644 cache sets based on repo_name
1645 1645
1646 1646 :param key:
1647 1647 """
1648 1648 if key:
1649 1649 key, _prefix, _org_key = cls._get_key(key)
1650 1650 inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
1651 1651 elif repo_name:
1652 1652 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1653 1653
1654 1654 log.debug('marking %s key[s] for invalidation based on key=%s,repo_name=%s'
1655 1655 % (len(inv_objs), key, repo_name))
1656 1656 try:
1657 1657 for inv_obj in inv_objs:
1658 1658 inv_obj.cache_active = False
1659 1659 Session().add(inv_obj)
1660 1660 Session().commit()
1661 1661 except Exception:
1662 1662 log.error(traceback.format_exc())
1663 1663 Session().rollback()
1664 1664
1665 1665 @classmethod
1666 1666 def set_valid(cls, key):
1667 1667 """
1668 1668 Mark this cache key as active and currently cached
1669 1669
1670 1670 :param key:
1671 1671 """
1672 1672 inv_obj = cls.get_by_key(key)
1673 1673 inv_obj.cache_active = True
1674 1674 Session().add(inv_obj)
1675 1675 Session().commit()
1676 1676
1677 1677 @classmethod
1678 1678 def get_cache_map(cls):
1679 1679
1680 1680 class cachemapdict(dict):
1681 1681
1682 1682 def __init__(self, *args, **kwargs):
1683 1683 fixkey = kwargs.get('fixkey')
1684 1684 if fixkey:
1685 1685 del kwargs['fixkey']
1686 1686 self.fixkey = fixkey
1687 1687 super(cachemapdict, self).__init__(*args, **kwargs)
1688 1688
1689 1689 def __getattr__(self, name):
1690 1690 key = name
1691 1691 if self.fixkey:
1692 1692 key, _prefix, _org_key = cls._get_key(key)
1693 1693 if key in self.__dict__:
1694 1694 return self.__dict__[key]
1695 1695 else:
1696 1696 return self[key]
1697 1697
1698 1698 def __getitem__(self, key):
1699 1699 if self.fixkey:
1700 1700 key, _prefix, _org_key = cls._get_key(key)
1701 1701 try:
1702 1702 return super(cachemapdict, self).__getitem__(key)
1703 1703 except KeyError:
1704 1704 return
1705 1705
1706 1706 cache_map = cachemapdict(fixkey=True)
1707 1707 for obj in cls.query().all():
1708 1708 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1709 1709 return cache_map
1710 1710
1711 1711
1712 1712 class ChangesetComment(Base, BaseModel):
1713 1713 __tablename__ = 'changeset_comments'
1714 1714 __table_args__ = (
1715 1715 Index('cc_revision_idx', 'revision'),
1716 1716 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1717 1717 'mysql_charset': 'utf8'},
1718 1718 )
1719 1719 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1720 1720 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1721 1721 revision = Column('revision', String(40), nullable=True)
1722 1722 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1723 1723 line_no = Column('line_no', Unicode(10), nullable=True)
1724 1724 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1725 1725 f_path = Column('f_path', Unicode(1000), nullable=True)
1726 1726 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1727 1727 text = Column('text', UnicodeText(25000), nullable=False)
1728 1728 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1729 1729 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1730 1730
1731 1731 author = relationship('User', lazy='joined')
1732 1732 repo = relationship('Repository')
1733 1733 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1734 1734 pull_request = relationship('PullRequest', lazy='joined')
1735 1735
1736 1736 @classmethod
1737 1737 def get_users(cls, revision=None, pull_request_id=None):
1738 1738 """
1739 1739 Returns user associated with this ChangesetComment. ie those
1740 1740 who actually commented
1741 1741
1742 1742 :param cls:
1743 1743 :param revision:
1744 1744 """
1745 1745 q = Session().query(User)\
1746 1746 .join(ChangesetComment.author)
1747 1747 if revision:
1748 1748 q = q.filter(cls.revision == revision)
1749 1749 elif pull_request_id:
1750 1750 q = q.filter(cls.pull_request_id == pull_request_id)
1751 1751 return q.all()
1752 1752
1753 1753
1754 1754 class ChangesetStatus(Base, BaseModel):
1755 1755 __tablename__ = 'changeset_statuses'
1756 1756 __table_args__ = (
1757 1757 Index('cs_revision_idx', 'revision'),
1758 1758 Index('cs_version_idx', 'version'),
1759 1759 UniqueConstraint('repo_id', 'revision', 'version'),
1760 1760 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1761 1761 'mysql_charset': 'utf8'}
1762 1762 )
1763 1763 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1764 1764 STATUS_APPROVED = 'approved'
1765 1765 STATUS_REJECTED = 'rejected'
1766 1766 STATUS_UNDER_REVIEW = 'under_review'
1767 1767
1768 1768 STATUSES = [
1769 1769 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1770 1770 (STATUS_APPROVED, _("Approved")),
1771 1771 (STATUS_REJECTED, _("Rejected")),
1772 1772 (STATUS_UNDER_REVIEW, _("Under Review")),
1773 1773 ]
1774 1774
1775 1775 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1776 1776 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1777 1777 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1778 1778 revision = Column('revision', String(40), nullable=False)
1779 1779 status = Column('status', String(128), nullable=False, default=DEFAULT)
1780 1780 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1781 1781 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1782 1782 version = Column('version', Integer(), nullable=False, default=0)
1783 1783 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1784 1784
1785 1785 author = relationship('User', lazy='joined')
1786 1786 repo = relationship('Repository')
1787 1787 comment = relationship('ChangesetComment', lazy='joined')
1788 1788 pull_request = relationship('PullRequest', lazy='joined')
1789 1789
1790 1790 def __unicode__(self):
1791 1791 return u"<%s('%s:%s')>" % (
1792 1792 self.__class__.__name__,
1793 1793 self.status, self.author
1794 1794 )
1795 1795
1796 1796 @classmethod
1797 1797 def get_status_lbl(cls, value):
1798 1798 return dict(cls.STATUSES).get(value)
1799 1799
1800 1800 @property
1801 1801 def status_lbl(self):
1802 1802 return ChangesetStatus.get_status_lbl(self.status)
1803 1803
1804 1804
1805 1805 class PullRequest(Base, BaseModel):
1806 1806 __tablename__ = 'pull_requests'
1807 1807 __table_args__ = (
1808 1808 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1809 1809 'mysql_charset': 'utf8'},
1810 1810 )
1811 1811
1812 1812 STATUS_NEW = u'new'
1813 1813 STATUS_OPEN = u'open'
1814 1814 STATUS_CLOSED = u'closed'
1815 1815
1816 1816 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1817 1817 title = Column('title', Unicode(256), nullable=True)
1818 1818 description = Column('description', UnicodeText(10240), nullable=True)
1819 1819 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1820 1820 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1821 1821 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1822 1822 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1823 1823 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1824 1824 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1825 1825 org_ref = Column('org_ref', Unicode(256), nullable=False)
1826 1826 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1827 1827 other_ref = Column('other_ref', Unicode(256), nullable=False)
1828 1828
1829 1829 @hybrid_property
1830 1830 def revisions(self):
1831 1831 return self._revisions.split(':')
1832 1832
1833 1833 @revisions.setter
1834 1834 def revisions(self, val):
1835 1835 self._revisions = ':'.join(val)
1836 1836
1837 1837 @property
1838 1838 def org_ref_parts(self):
1839 1839 return self.org_ref.split(':')
1840 1840
1841 1841 @property
1842 1842 def other_ref_parts(self):
1843 1843 return self.other_ref.split(':')
1844 1844
1845 1845 author = relationship('User', lazy='joined')
1846 1846 reviewers = relationship('PullRequestReviewers',
1847 1847 cascade="all, delete, delete-orphan")
1848 1848 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1849 1849 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1850 1850 statuses = relationship('ChangesetStatus')
1851 1851 comments = relationship('ChangesetComment',
1852 1852 cascade="all, delete, delete-orphan")
1853 1853
1854 1854 def is_closed(self):
1855 1855 return self.status == self.STATUS_CLOSED
1856 1856
1857 1857 def __json__(self):
1858 1858 return dict(
1859 1859 revisions=self.revisions
1860 1860 )
1861 1861
1862 1862
1863 1863 class PullRequestReviewers(Base, BaseModel):
1864 1864 __tablename__ = 'pull_request_reviewers'
1865 1865 __table_args__ = (
1866 1866 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1867 1867 'mysql_charset': 'utf8'},
1868 1868 )
1869 1869
1870 1870 def __init__(self, user=None, pull_request=None):
1871 1871 self.user = user
1872 1872 self.pull_request = pull_request
1873 1873
1874 1874 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1875 1875 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1876 1876 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1877 1877
1878 1878 user = relationship('User')
1879 1879 pull_request = relationship('PullRequest')
1880 1880
1881 1881
1882 1882 class Notification(Base, BaseModel):
1883 1883 __tablename__ = 'notifications'
1884 1884 __table_args__ = (
1885 1885 Index('notification_type_idx', 'type'),
1886 1886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1887 1887 'mysql_charset': 'utf8'},
1888 1888 )
1889 1889
1890 1890 TYPE_CHANGESET_COMMENT = u'cs_comment'
1891 1891 TYPE_MESSAGE = u'message'
1892 1892 TYPE_MENTION = u'mention'
1893 1893 TYPE_REGISTRATION = u'registration'
1894 1894 TYPE_PULL_REQUEST = u'pull_request'
1895 1895 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1896 1896
1897 1897 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1898 1898 subject = Column('subject', Unicode(512), nullable=True)
1899 1899 body = Column('body', UnicodeText(50000), nullable=True)
1900 1900 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1901 1901 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1902 1902 type_ = Column('type', Unicode(256))
1903 1903
1904 1904 created_by_user = relationship('User')
1905 1905 notifications_to_users = relationship('UserNotification', lazy='joined',
1906 1906 cascade="all, delete, delete-orphan")
1907 1907
1908 1908 @property
1909 1909 def recipients(self):
1910 1910 return [x.user for x in UserNotification.query()\
1911 1911 .filter(UserNotification.notification == self)\
1912 1912 .order_by(UserNotification.user_id.asc()).all()]
1913 1913
1914 1914 @classmethod
1915 1915 def create(cls, created_by, subject, body, recipients, type_=None):
1916 1916 if type_ is None:
1917 1917 type_ = Notification.TYPE_MESSAGE
1918 1918
1919 1919 notification = cls()
1920 1920 notification.created_by_user = created_by
1921 1921 notification.subject = subject
1922 1922 notification.body = body
1923 1923 notification.type_ = type_
1924 1924 notification.created_on = datetime.datetime.now()
1925 1925
1926 1926 for u in recipients:
1927 1927 assoc = UserNotification()
1928 1928 assoc.notification = notification
1929 1929 u.notifications.append(assoc)
1930 1930 Session().add(notification)
1931 1931 return notification
1932 1932
1933 1933 @property
1934 1934 def description(self):
1935 1935 from rhodecode.model.notification import NotificationModel
1936 1936 return NotificationModel().make_description(self)
1937 1937
1938 1938
1939 1939 class UserNotification(Base, BaseModel):
1940 1940 __tablename__ = 'user_to_notification'
1941 1941 __table_args__ = (
1942 1942 UniqueConstraint('user_id', 'notification_id'),
1943 1943 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1944 1944 'mysql_charset': 'utf8'}
1945 1945 )
1946 1946 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1947 1947 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1948 1948 read = Column('read', Boolean, default=False)
1949 1949 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1950 1950
1951 1951 user = relationship('User', lazy="joined")
1952 1952 notification = relationship('Notification', lazy="joined",
1953 1953 order_by=lambda: Notification.created_on.desc(),)
1954 1954
1955 1955 def mark_as_read(self):
1956 1956 self.read = True
1957 1957 Session().add(self)
1958 1958
1959 1959
1960 1960 class DbMigrateVersion(Base, BaseModel):
1961 1961 __tablename__ = 'db_migrate_version'
1962 1962 __table_args__ = (
1963 1963 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1964 1964 'mysql_charset': 'utf8'},
1965 1965 )
1966 1966 repository_id = Column('repository_id', String(250), primary_key=True)
1967 1967 repository_path = Column('repository_path', Text)
1968 1968 version = Column('version', Integer)
@@ -1,745 +1,742 b''
1 1 """
2 2 Set of generic validators
3 3 """
4 4 import os
5 5 import re
6 6 import formencode
7 7 import logging
8 8 from collections import defaultdict
9 9 from pylons.i18n.translation import _
10 10 from webhelpers.pylonslib.secure_form import authentication_token
11 11
12 12 from formencode.validators import (
13 13 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
14 14 NotEmpty, IPAddress, CIDR
15 15 )
16 16 from rhodecode.lib.compat import OrderedSet
17 from rhodecode.lib import ipaddr
17 18 from rhodecode.lib.utils import repo_name_slug
18 19 from rhodecode.model.db import RepoGroup, Repository, UsersGroup, User,\
19 20 ChangesetStatus
20 21 from rhodecode.lib.exceptions import LdapImportError
21 22 from rhodecode.config.routing import ADMIN_PREFIX
22 23 from rhodecode.lib.auth import HasReposGroupPermissionAny
23 24
24 25 # silence warnings and pylint
25 26 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
26 27 NotEmpty, IPAddress, CIDR
27 28
28 29 log = logging.getLogger(__name__)
29 30
30 31
31 32 class UniqueList(formencode.FancyValidator):
32 33 """
33 34 Unique List !
34 35 """
35 36 messages = dict(
36 37 empty=_('Value cannot be an empty list'),
37 38 missing_value=_('Value cannot be an empty list'),
38 39 )
39 40
40 41 def _to_python(self, value, state):
41 42 if isinstance(value, list):
42 43 return value
43 44 elif isinstance(value, set):
44 45 return list(value)
45 46 elif isinstance(value, tuple):
46 47 return list(value)
47 48 elif value is None:
48 49 return []
49 50 else:
50 51 return [value]
51 52
52 53 def empty_value(self, value):
53 54 return []
54 55
55 56
56 57 class StateObj(object):
57 58 """
58 59 this is needed to translate the messages using _() in validators
59 60 """
60 61 _ = staticmethod(_)
61 62
62 63
63 64 def M(self, key, state=None, **kwargs):
64 65 """
65 66 returns string from self.message based on given key,
66 67 passed kw params are used to substitute %(named)s params inside
67 68 translated strings
68 69
69 70 :param msg:
70 71 :param state:
71 72 """
72 73 if state is None:
73 74 state = StateObj()
74 75 else:
75 76 state._ = staticmethod(_)
76 77 #inject validator into state object
77 78 return self.message(key, state, **kwargs)
78 79
79 80
80 81 def ValidUsername(edit=False, old_data={}):
81 82 class _validator(formencode.validators.FancyValidator):
82 83 messages = {
83 84 'username_exists': _(u'Username "%(username)s" already exists'),
84 85 'system_invalid_username':
85 86 _(u'Username "%(username)s" is forbidden'),
86 87 'invalid_username':
87 88 _(u'Username may only contain alphanumeric characters '
88 89 'underscores, periods or dashes and must begin with '
89 90 'alphanumeric character')
90 91 }
91 92
92 93 def validate_python(self, value, state):
93 94 if value in ['default', 'new_user']:
94 95 msg = M(self, 'system_invalid_username', state, username=value)
95 96 raise formencode.Invalid(msg, value, state)
96 97 #check if user is unique
97 98 old_un = None
98 99 if edit:
99 100 old_un = User.get(old_data.get('user_id')).username
100 101
101 102 if old_un != value or not edit:
102 103 if User.get_by_username(value, case_insensitive=True):
103 104 msg = M(self, 'username_exists', state, username=value)
104 105 raise formencode.Invalid(msg, value, state)
105 106
106 107 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
107 108 msg = M(self, 'invalid_username', state)
108 109 raise formencode.Invalid(msg, value, state)
109 110 return _validator
110 111
111 112
112 113 def ValidRepoUser():
113 114 class _validator(formencode.validators.FancyValidator):
114 115 messages = {
115 116 'invalid_username': _(u'Username %(username)s is not valid')
116 117 }
117 118
118 119 def validate_python(self, value, state):
119 120 try:
120 121 User.query().filter(User.active == True)\
121 122 .filter(User.username == value).one()
122 123 except Exception:
123 124 msg = M(self, 'invalid_username', state, username=value)
124 125 raise formencode.Invalid(msg, value, state,
125 126 error_dict=dict(username=msg)
126 127 )
127 128
128 129 return _validator
129 130
130 131
131 132 def ValidUsersGroup(edit=False, old_data={}):
132 133 class _validator(formencode.validators.FancyValidator):
133 134 messages = {
134 135 'invalid_group': _(u'Invalid users group name'),
135 136 'group_exist': _(u'Users group "%(usersgroup)s" already exists'),
136 137 'invalid_usersgroup_name':
137 138 _(u'users group name may only contain alphanumeric '
138 139 'characters underscores, periods or dashes and must begin '
139 140 'with alphanumeric character')
140 141 }
141 142
142 143 def validate_python(self, value, state):
143 144 if value in ['default']:
144 145 msg = M(self, 'invalid_group', state)
145 146 raise formencode.Invalid(msg, value, state,
146 147 error_dict=dict(users_group_name=msg)
147 148 )
148 149 #check if group is unique
149 150 old_ugname = None
150 151 if edit:
151 152 old_id = old_data.get('users_group_id')
152 153 old_ugname = UsersGroup.get(old_id).users_group_name
153 154
154 155 if old_ugname != value or not edit:
155 156 is_existing_group = UsersGroup.get_by_group_name(value,
156 157 case_insensitive=True)
157 158 if is_existing_group:
158 159 msg = M(self, 'group_exist', state, usersgroup=value)
159 160 raise formencode.Invalid(msg, value, state,
160 161 error_dict=dict(users_group_name=msg)
161 162 )
162 163
163 164 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
164 165 msg = M(self, 'invalid_usersgroup_name', state)
165 166 raise formencode.Invalid(msg, value, state,
166 167 error_dict=dict(users_group_name=msg)
167 168 )
168 169
169 170 return _validator
170 171
171 172
172 173 def ValidReposGroup(edit=False, old_data={}):
173 174 class _validator(formencode.validators.FancyValidator):
174 175 messages = {
175 176 'group_parent_id': _(u'Cannot assign this group as parent'),
176 177 'group_exists': _(u'Group "%(group_name)s" already exists'),
177 178 'repo_exists':
178 179 _(u'Repository with name "%(group_name)s" already exists')
179 180 }
180 181
181 182 def validate_python(self, value, state):
182 183 # TODO WRITE VALIDATIONS
183 184 group_name = value.get('group_name')
184 185 group_parent_id = value.get('group_parent_id')
185 186
186 187 # slugify repo group just in case :)
187 188 slug = repo_name_slug(group_name)
188 189
189 190 # check for parent of self
190 191 parent_of_self = lambda: (
191 192 old_data['group_id'] == int(group_parent_id)
192 193 if group_parent_id else False
193 194 )
194 195 if edit and parent_of_self():
195 196 msg = M(self, 'group_parent_id', state)
196 197 raise formencode.Invalid(msg, value, state,
197 198 error_dict=dict(group_parent_id=msg)
198 199 )
199 200
200 201 old_gname = None
201 202 if edit:
202 203 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
203 204
204 205 if old_gname != group_name or not edit:
205 206
206 207 # check group
207 208 gr = RepoGroup.query()\
208 209 .filter(RepoGroup.group_name == slug)\
209 210 .filter(RepoGroup.group_parent_id == group_parent_id)\
210 211 .scalar()
211 212
212 213 if gr:
213 214 msg = M(self, 'group_exists', state, group_name=slug)
214 215 raise formencode.Invalid(msg, value, state,
215 216 error_dict=dict(group_name=msg)
216 217 )
217 218
218 219 # check for same repo
219 220 repo = Repository.query()\
220 221 .filter(Repository.repo_name == slug)\
221 222 .scalar()
222 223
223 224 if repo:
224 225 msg = M(self, 'repo_exists', state, group_name=slug)
225 226 raise formencode.Invalid(msg, value, state,
226 227 error_dict=dict(group_name=msg)
227 228 )
228 229
229 230 return _validator
230 231
231 232
232 233 def ValidPassword():
233 234 class _validator(formencode.validators.FancyValidator):
234 235 messages = {
235 236 'invalid_password':
236 237 _(u'Invalid characters (non-ascii) in password')
237 238 }
238 239
239 240 def validate_python(self, value, state):
240 241 try:
241 242 (value or '').decode('ascii')
242 243 except UnicodeError:
243 244 msg = M(self, 'invalid_password', state)
244 245 raise formencode.Invalid(msg, value, state,)
245 246 return _validator
246 247
247 248
248 249 def ValidPasswordsMatch():
249 250 class _validator(formencode.validators.FancyValidator):
250 251 messages = {
251 252 'password_mismatch': _(u'Passwords do not match'),
252 253 }
253 254
254 255 def validate_python(self, value, state):
255 256
256 257 pass_val = value.get('password') or value.get('new_password')
257 258 if pass_val != value['password_confirmation']:
258 259 msg = M(self, 'password_mismatch', state)
259 260 raise formencode.Invalid(msg, value, state,
260 261 error_dict=dict(password_confirmation=msg)
261 262 )
262 263 return _validator
263 264
264 265
265 266 def ValidAuth():
266 267 class _validator(formencode.validators.FancyValidator):
267 268 messages = {
268 269 'invalid_password': _(u'invalid password'),
269 270 'invalid_username': _(u'invalid user name'),
270 271 'disabled_account': _(u'Your account is disabled')
271 272 }
272 273
273 274 def validate_python(self, value, state):
274 275 from rhodecode.lib.auth import authenticate
275 276
276 277 password = value['password']
277 278 username = value['username']
278 279
279 280 if not authenticate(username, password):
280 281 user = User.get_by_username(username)
281 282 if user and user.active is False:
282 283 log.warning('user %s is disabled' % username)
283 284 msg = M(self, 'disabled_account', state)
284 285 raise formencode.Invalid(msg, value, state,
285 286 error_dict=dict(username=msg)
286 287 )
287 288 else:
288 289 log.warning('user %s failed to authenticate' % username)
289 290 msg = M(self, 'invalid_username', state)
290 291 msg2 = M(self, 'invalid_password', state)
291 292 raise formencode.Invalid(msg, value, state,
292 293 error_dict=dict(username=msg, password=msg2)
293 294 )
294 295 return _validator
295 296
296 297
297 298 def ValidAuthToken():
298 299 class _validator(formencode.validators.FancyValidator):
299 300 messages = {
300 301 'invalid_token': _(u'Token mismatch')
301 302 }
302 303
303 304 def validate_python(self, value, state):
304 305 if value != authentication_token():
305 306 msg = M(self, 'invalid_token', state)
306 307 raise formencode.Invalid(msg, value, state)
307 308 return _validator
308 309
309 310
310 311 def ValidRepoName(edit=False, old_data={}):
311 312 class _validator(formencode.validators.FancyValidator):
312 313 messages = {
313 314 'invalid_repo_name':
314 315 _(u'Repository name %(repo)s is disallowed'),
315 316 'repository_exists':
316 317 _(u'Repository named %(repo)s already exists'),
317 318 'repository_in_group_exists': _(u'Repository "%(repo)s" already '
318 319 'exists in group "%(group)s"'),
319 320 'same_group_exists': _(u'Repositories group with name "%(repo)s" '
320 321 'already exists')
321 322 }
322 323
323 324 def _to_python(self, value, state):
324 325 repo_name = repo_name_slug(value.get('repo_name', ''))
325 326 repo_group = value.get('repo_group')
326 327 if repo_group:
327 328 gr = RepoGroup.get(repo_group)
328 329 group_path = gr.full_path
329 330 group_name = gr.group_name
330 331 # value needs to be aware of group name in order to check
331 332 # db key This is an actual just the name to store in the
332 333 # database
333 334 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
334 335 else:
335 336 group_name = group_path = ''
336 337 repo_name_full = repo_name
337 338
338 339 value['repo_name'] = repo_name
339 340 value['repo_name_full'] = repo_name_full
340 341 value['group_path'] = group_path
341 342 value['group_name'] = group_name
342 343 return value
343 344
344 345 def validate_python(self, value, state):
345 346
346 347 repo_name = value.get('repo_name')
347 348 repo_name_full = value.get('repo_name_full')
348 349 group_path = value.get('group_path')
349 350 group_name = value.get('group_name')
350 351
351 352 if repo_name in [ADMIN_PREFIX, '']:
352 353 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
353 354 raise formencode.Invalid(msg, value, state,
354 355 error_dict=dict(repo_name=msg)
355 356 )
356 357
357 358 rename = old_data.get('repo_name') != repo_name_full
358 359 create = not edit
359 360 if rename or create:
360 361
361 362 if group_path != '':
362 363 if Repository.get_by_repo_name(repo_name_full):
363 364 msg = M(self, 'repository_in_group_exists', state,
364 365 repo=repo_name, group=group_name)
365 366 raise formencode.Invalid(msg, value, state,
366 367 error_dict=dict(repo_name=msg)
367 368 )
368 369 elif RepoGroup.get_by_group_name(repo_name_full):
369 370 msg = M(self, 'same_group_exists', state,
370 371 repo=repo_name)
371 372 raise formencode.Invalid(msg, value, state,
372 373 error_dict=dict(repo_name=msg)
373 374 )
374 375
375 376 elif Repository.get_by_repo_name(repo_name_full):
376 377 msg = M(self, 'repository_exists', state,
377 378 repo=repo_name)
378 379 raise formencode.Invalid(msg, value, state,
379 380 error_dict=dict(repo_name=msg)
380 381 )
381 382 return value
382 383 return _validator
383 384
384 385
385 386 def ValidForkName(*args, **kwargs):
386 387 return ValidRepoName(*args, **kwargs)
387 388
388 389
389 390 def SlugifyName():
390 391 class _validator(formencode.validators.FancyValidator):
391 392
392 393 def _to_python(self, value, state):
393 394 return repo_name_slug(value)
394 395
395 396 def validate_python(self, value, state):
396 397 pass
397 398
398 399 return _validator
399 400
400 401
401 402 def ValidCloneUri():
402 403 from rhodecode.lib.utils import make_ui
403 404
404 405 def url_handler(repo_type, url, ui=None):
405 406 if repo_type == 'hg':
406 407 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
407 408 from mercurial.httppeer import httppeer
408 409 if url.startswith('http'):
409 410 ## initially check if it's at least the proper URL
410 411 ## or does it pass basic auth
411 412 MercurialRepository._check_url(url)
412 413 httppeer(ui, url)._capabilities()
413 414 elif url.startswith('svn+http'):
414 415 from hgsubversion.svnrepo import svnremoterepo
415 416 svnremoterepo(ui, url).capabilities
416 417 elif url.startswith('git+http'):
417 418 raise NotImplementedError()
418 419
419 420 elif repo_type == 'git':
420 421 from rhodecode.lib.vcs.backends.git.repository import GitRepository
421 422 if url.startswith('http'):
422 423 ## initially check if it's at least the proper URL
423 424 ## or does it pass basic auth
424 425 GitRepository._check_url(url)
425 426 elif url.startswith('svn+http'):
426 427 raise NotImplementedError()
427 428 elif url.startswith('hg+http'):
428 429 raise NotImplementedError()
429 430
430 431 class _validator(formencode.validators.FancyValidator):
431 432 messages = {
432 433 'clone_uri': _(u'invalid clone url'),
433 434 'invalid_clone_uri': _(u'Invalid clone url, provide a '
434 435 'valid clone http(s)/svn+http(s) url')
435 436 }
436 437
437 438 def validate_python(self, value, state):
438 439 repo_type = value.get('repo_type')
439 440 url = value.get('clone_uri')
440 441
441 442 if not url:
442 443 pass
443 444 else:
444 445 try:
445 446 url_handler(repo_type, url, make_ui('db', clear_session=False))
446 447 except Exception:
447 448 log.exception('Url validation failed')
448 449 msg = M(self, 'clone_uri')
449 450 raise formencode.Invalid(msg, value, state,
450 451 error_dict=dict(clone_uri=msg)
451 452 )
452 453 return _validator
453 454
454 455
455 456 def ValidForkType(old_data={}):
456 457 class _validator(formencode.validators.FancyValidator):
457 458 messages = {
458 459 'invalid_fork_type': _(u'Fork have to be the same type as parent')
459 460 }
460 461
461 462 def validate_python(self, value, state):
462 463 if old_data['repo_type'] != value:
463 464 msg = M(self, 'invalid_fork_type', state)
464 465 raise formencode.Invalid(msg, value, state,
465 466 error_dict=dict(repo_type=msg)
466 467 )
467 468 return _validator
468 469
469 470
470 471 def CanWriteGroup():
471 472 class _validator(formencode.validators.FancyValidator):
472 473 messages = {
473 474 'permission_denied': _(u"You don't have permissions "
474 475 "to create repository in this group")
475 476 }
476 477
477 478 def validate_python(self, value, state):
478 479 gr = RepoGroup.get(value)
479 480 if not HasReposGroupPermissionAny(
480 481 'group.write', 'group.admin'
481 482 )(gr.group_name, 'get group of repo form'):
482 483 msg = M(self, 'permission_denied', state)
483 484 raise formencode.Invalid(msg, value, state,
484 485 error_dict=dict(repo_type=msg)
485 486 )
486 487 return _validator
487 488
488 489
489 490 def ValidPerms(type_='repo'):
490 491 if type_ == 'group':
491 492 EMPTY_PERM = 'group.none'
492 493 elif type_ == 'repo':
493 494 EMPTY_PERM = 'repository.none'
494 495
495 496 class _validator(formencode.validators.FancyValidator):
496 497 messages = {
497 498 'perm_new_member_name':
498 499 _(u'This username or users group name is not valid')
499 500 }
500 501
501 502 def to_python(self, value, state):
502 503 perms_update = OrderedSet()
503 504 perms_new = OrderedSet()
504 505 # build a list of permission to update and new permission to create
505 506
506 507 #CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
507 508 new_perms_group = defaultdict(dict)
508 509 for k, v in value.copy().iteritems():
509 510 if k.startswith('perm_new_member'):
510 511 del value[k]
511 512 _type, part = k.split('perm_new_member_')
512 513 args = part.split('_')
513 514 if len(args) == 1:
514 515 new_perms_group[args[0]]['perm'] = v
515 516 elif len(args) == 2:
516 517 _key, pos = args
517 518 new_perms_group[pos][_key] = v
518 519
519 520 # fill new permissions in order of how they were added
520 521 for k in sorted(map(int, new_perms_group.keys())):
521 522 perm_dict = new_perms_group[str(k)]
522 523 new_member = perm_dict.get('name')
523 524 new_perm = perm_dict.get('perm')
524 525 new_type = perm_dict.get('type')
525 526 if new_member and new_perm and new_type:
526 527 perms_new.add((new_member, new_perm, new_type))
527 528
528 529 for k, v in value.iteritems():
529 530 if k.startswith('u_perm_') or k.startswith('g_perm_'):
530 531 member = k[7:]
531 532 t = {'u': 'user',
532 533 'g': 'users_group'
533 534 }[k[0]]
534 535 if member == 'default':
535 536 if value.get('private'):
536 537 # set none for default when updating to
537 538 # private repo
538 539 v = EMPTY_PERM
539 540 perms_update.add((member, v, t))
540 541
541 542 value['perms_updates'] = list(perms_update)
542 543 value['perms_new'] = list(perms_new)
543 544
544 545 # update permissions
545 546 for k, v, t in perms_new:
546 547 try:
547 548 if t is 'user':
548 549 self.user_db = User.query()\
549 550 .filter(User.active == True)\
550 551 .filter(User.username == k).one()
551 552 if t is 'users_group':
552 553 self.user_db = UsersGroup.query()\
553 554 .filter(UsersGroup.users_group_active == True)\
554 555 .filter(UsersGroup.users_group_name == k).one()
555 556
556 557 except Exception:
557 558 log.exception('Updated permission failed')
558 559 msg = M(self, 'perm_new_member_type', state)
559 560 raise formencode.Invalid(msg, value, state,
560 561 error_dict=dict(perm_new_member_name=msg)
561 562 )
562 563 return value
563 564 return _validator
564 565
565 566
566 567 def ValidSettings():
567 568 class _validator(formencode.validators.FancyValidator):
568 569 def _to_python(self, value, state):
569 570 # settings form for users that are not admin
570 571 # can't edit certain parameters, it's extra backup if they mangle
571 572 # with forms
572 573
573 574 forbidden_params = [
574 575 'user', 'repo_type', 'repo_enable_locking',
575 576 'repo_enable_downloads', 'repo_enable_statistics'
576 577 ]
577 578
578 579 for param in forbidden_params:
579 580 if param in value:
580 581 del value[param]
581 582 return value
582 583
583 584 def validate_python(self, value, state):
584 585 pass
585 586 return _validator
586 587
587 588
588 589 def ValidPath():
589 590 class _validator(formencode.validators.FancyValidator):
590 591 messages = {
591 592 'invalid_path': _(u'This is not a valid path')
592 593 }
593 594
594 595 def validate_python(self, value, state):
595 596 if not os.path.isdir(value):
596 597 msg = M(self, 'invalid_path', state)
597 598 raise formencode.Invalid(msg, value, state,
598 599 error_dict=dict(paths_root_path=msg)
599 600 )
600 601 return _validator
601 602
602 603
603 604 def UniqSystemEmail(old_data={}):
604 605 class _validator(formencode.validators.FancyValidator):
605 606 messages = {
606 607 'email_taken': _(u'This e-mail address is already taken')
607 608 }
608 609
609 610 def _to_python(self, value, state):
610 611 return value.lower()
611 612
612 613 def validate_python(self, value, state):
613 614 if (old_data.get('email') or '').lower() != value:
614 615 user = User.get_by_email(value, case_insensitive=True)
615 616 if user:
616 617 msg = M(self, 'email_taken', state)
617 618 raise formencode.Invalid(msg, value, state,
618 619 error_dict=dict(email=msg)
619 620 )
620 621 return _validator
621 622
622 623
623 624 def ValidSystemEmail():
624 625 class _validator(formencode.validators.FancyValidator):
625 626 messages = {
626 627 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
627 628 }
628 629
629 630 def _to_python(self, value, state):
630 631 return value.lower()
631 632
632 633 def validate_python(self, value, state):
633 634 user = User.get_by_email(value, case_insensitive=True)
634 635 if user is None:
635 636 msg = M(self, 'non_existing_email', state, email=value)
636 637 raise formencode.Invalid(msg, value, state,
637 638 error_dict=dict(email=msg)
638 639 )
639 640
640 641 return _validator
641 642
642 643
643 644 def LdapLibValidator():
644 645 class _validator(formencode.validators.FancyValidator):
645 646 messages = {
646 647
647 648 }
648 649
649 650 def validate_python(self, value, state):
650 651 try:
651 652 import ldap
652 653 ldap # pyflakes silence !
653 654 except ImportError:
654 655 raise LdapImportError()
655 656
656 657 return _validator
657 658
658 659
659 660 def AttrLoginValidator():
660 661 class _validator(formencode.validators.FancyValidator):
661 662 messages = {
662 663 'invalid_cn':
663 664 _(u'The LDAP Login attribute of the CN must be specified - '
664 665 'this is the name of the attribute that is equivalent '
665 666 'to "username"')
666 667 }
667 668
668 669 def validate_python(self, value, state):
669 670 if not value or not isinstance(value, (str, unicode)):
670 671 msg = M(self, 'invalid_cn', state)
671 672 raise formencode.Invalid(msg, value, state,
672 673 error_dict=dict(ldap_attr_login=msg)
673 674 )
674 675
675 676 return _validator
676 677
677 678
678 679 def NotReviewedRevisions(repo_id):
679 680 class _validator(formencode.validators.FancyValidator):
680 681 messages = {
681 682 'rev_already_reviewed':
682 683 _(u'Revisions %(revs)s are already part of pull request '
683 684 'or have set status')
684 685 }
685 686
686 687 def validate_python(self, value, state):
687 688 # check revisions if they are not reviewed, or a part of another
688 689 # pull request
689 690 statuses = ChangesetStatus.query()\
690 691 .filter(ChangesetStatus.revision.in_(value))\
691 692 .filter(ChangesetStatus.repo_id == repo_id)\
692 693 .all()
693 694
694 695 errors = []
695 696 for cs in statuses:
696 697 if cs.pull_request_id:
697 698 errors.append(['pull_req', cs.revision[:12]])
698 699 elif cs.status:
699 700 errors.append(['status', cs.revision[:12]])
700 701
701 702 if errors:
702 703 revs = ','.join([x[1] for x in errors])
703 704 msg = M(self, 'rev_already_reviewed', state, revs=revs)
704 705 raise formencode.Invalid(msg, value, state,
705 706 error_dict=dict(revisions=revs)
706 707 )
707 708
708 709 return _validator
709 710
710 711
711 712 def ValidIp():
712 713 class _validator(CIDR):
713 714 messages = dict(
714 badFormat=_('Please enter a valid IP address (a.b.c.d)'),
715 illegalOctets=_('The octets must be within the range of 0-255'
716 ' (not %(octet)r)'),
715 badFormat=_('Please enter a valid IPv4 or IpV6 address'),
717 716 illegalBits=_('The network size (bits) must be within the range'
718 717 ' of 0-32 (not %(bits)r)'))
719 718
719 def to_python(self, value, state):
720 v = super(_validator, self).to_python(value, state)
721 v = v.strip()
722 net = ipaddr.IPNetwork(address=v)
723 if isinstance(net, ipaddr.IPv4Network):
724 #if IPv4 doesn't end with a mask, add /32
725 if '/' not in value:
726 v += '/32'
727 if isinstance(net, ipaddr.IPv6Network):
728 #if IPv6 doesn't end with a mask, add /128
729 if '/' not in value:
730 v += '/128'
731 return v
732
720 733 def validate_python(self, value, state):
721 734 try:
722 # Split into octets and bits
723 if '/' in value: # a.b.c.d/e
724 addr, bits = value.split('/')
725 else: # a.b.c.d
726 addr, bits = value, 32
727 # Use IPAddress validator to validate the IP part
728 IPAddress.validate_python(self, addr, state)
729 # Bits (netmask) correct?
730 if not 0 <= int(bits) <= 32:
731 raise formencode.Invalid(
732 self.message('illegalBits', state, bits=bits),
733 value, state)
734 # Splitting faild: wrong syntax
735 addr = value.strip()
736 #this raises an ValueError if address is not IpV4 or IpV6
737 ipaddr.IPNetwork(address=addr)
735 738 except ValueError:
736 739 raise formencode.Invalid(self.message('badFormat', state),
737 740 value, state)
738 741
739 def to_python(self, value, state):
740 v = super(_validator, self).to_python(value, state)
741 #if IP doesn't end with a mask, add /32
742 if '/' not in value:
743 v += '/32'
744 return v
745 742 return _validator
General Comments 0
You need to be logged in to leave comments. Login now