##// END OF EJS Templates
feed-token, user, performance: lazy load the feed_token. We only need it for...
marcink -
r2424:76f7c7cb default
parent child Browse files
Show More
@@ -1,2187 +1,2190 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 authentication and permission libraries
23 23 """
24 24
25 25 import os
26 26 import inspect
27 27 import collections
28 28 import fnmatch
29 29 import hashlib
30 30 import itertools
31 31 import logging
32 32 import random
33 33 import traceback
34 34 from functools import wraps
35 35
36 36 import ipaddress
37 37
38 38 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
39 39 from sqlalchemy.orm.exc import ObjectDeletedError
40 40 from sqlalchemy.orm import joinedload
41 41 from zope.cachedescriptors.property import Lazy as LazyProperty
42 42
43 43 import rhodecode
44 44 from rhodecode.model import meta
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.user import UserModel
47 47 from rhodecode.model.db import (
48 48 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
49 49 UserIpMap, UserApiKeys, RepoGroup, UserGroup)
50 50 from rhodecode.lib import caches
51 51 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, get_repo_group_slug, get_user_group_slug)
54 54 from rhodecode.lib.caching_query import FromCache
55 55
56 56
57 57 if rhodecode.is_unix:
58 58 import bcrypt
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62 csrf_token_key = "csrf_token"
63 63
64 64
65 65 class PasswordGenerator(object):
66 66 """
67 67 This is a simple class for generating password from different sets of
68 68 characters
69 69 usage::
70 70
71 71 passwd_gen = PasswordGenerator()
72 72 #print 8-letter password containing only big and small letters
73 73 of alphabet
74 74 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
75 75 """
76 76 ALPHABETS_NUM = r'''1234567890'''
77 77 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
78 78 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
79 79 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
80 80 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
81 81 + ALPHABETS_NUM + ALPHABETS_SPECIAL
82 82 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
83 83 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
84 84 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
85 85 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
86 86
87 87 def __init__(self, passwd=''):
88 88 self.passwd = passwd
89 89
90 90 def gen_password(self, length, type_=None):
91 91 if type_ is None:
92 92 type_ = self.ALPHABETS_FULL
93 93 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
94 94 return self.passwd
95 95
96 96
97 97 class _RhodeCodeCryptoBase(object):
98 98 ENC_PREF = None
99 99
100 100 def hash_create(self, str_):
101 101 """
102 102 hash the string using
103 103
104 104 :param str_: password to hash
105 105 """
106 106 raise NotImplementedError
107 107
108 108 def hash_check_with_upgrade(self, password, hashed):
109 109 """
110 110 Returns tuple in which first element is boolean that states that
111 111 given password matches it's hashed version, and the second is new hash
112 112 of the password, in case this password should be migrated to new
113 113 cipher.
114 114 """
115 115 checked_hash = self.hash_check(password, hashed)
116 116 return checked_hash, None
117 117
118 118 def hash_check(self, password, hashed):
119 119 """
120 120 Checks matching password with it's hashed value.
121 121
122 122 :param password: password
123 123 :param hashed: password in hashed form
124 124 """
125 125 raise NotImplementedError
126 126
127 127 def _assert_bytes(self, value):
128 128 """
129 129 Passing in an `unicode` object can lead to hard to detect issues
130 130 if passwords contain non-ascii characters. Doing a type check
131 131 during runtime, so that such mistakes are detected early on.
132 132 """
133 133 if not isinstance(value, str):
134 134 raise TypeError(
135 135 "Bytestring required as input, got %r." % (value, ))
136 136
137 137
138 138 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
139 139 ENC_PREF = ('$2a$10', '$2b$10')
140 140
141 141 def hash_create(self, str_):
142 142 self._assert_bytes(str_)
143 143 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
144 144
145 145 def hash_check_with_upgrade(self, password, hashed):
146 146 """
147 147 Returns tuple in which first element is boolean that states that
148 148 given password matches it's hashed version, and the second is new hash
149 149 of the password, in case this password should be migrated to new
150 150 cipher.
151 151
152 152 This implements special upgrade logic which works like that:
153 153 - check if the given password == bcrypted hash, if yes then we
154 154 properly used password and it was already in bcrypt. Proceed
155 155 without any changes
156 156 - if bcrypt hash check is not working try with sha256. If hash compare
157 157 is ok, it means we using correct but old hashed password. indicate
158 158 hash change and proceed
159 159 """
160 160
161 161 new_hash = None
162 162
163 163 # regular pw check
164 164 password_match_bcrypt = self.hash_check(password, hashed)
165 165
166 166 # now we want to know if the password was maybe from sha256
167 167 # basically calling _RhodeCodeCryptoSha256().hash_check()
168 168 if not password_match_bcrypt:
169 169 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
170 170 new_hash = self.hash_create(password) # make new bcrypt hash
171 171 password_match_bcrypt = True
172 172
173 173 return password_match_bcrypt, new_hash
174 174
175 175 def hash_check(self, password, hashed):
176 176 """
177 177 Checks matching password with it's hashed value.
178 178
179 179 :param password: password
180 180 :param hashed: password in hashed form
181 181 """
182 182 self._assert_bytes(password)
183 183 try:
184 184 return bcrypt.hashpw(password, hashed) == hashed
185 185 except ValueError as e:
186 186 # we're having a invalid salt here probably, we should not crash
187 187 # just return with False as it would be a wrong password.
188 188 log.debug('Failed to check password hash using bcrypt %s',
189 189 safe_str(e))
190 190
191 191 return False
192 192
193 193
194 194 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
195 195 ENC_PREF = '_'
196 196
197 197 def hash_create(self, str_):
198 198 self._assert_bytes(str_)
199 199 return hashlib.sha256(str_).hexdigest()
200 200
201 201 def hash_check(self, password, hashed):
202 202 """
203 203 Checks matching password with it's hashed value.
204 204
205 205 :param password: password
206 206 :param hashed: password in hashed form
207 207 """
208 208 self._assert_bytes(password)
209 209 return hashlib.sha256(password).hexdigest() == hashed
210 210
211 211
212 212 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
213 213 ENC_PREF = '_'
214 214
215 215 def hash_create(self, str_):
216 216 self._assert_bytes(str_)
217 217 return hashlib.md5(str_).hexdigest()
218 218
219 219 def hash_check(self, password, hashed):
220 220 """
221 221 Checks matching password with it's hashed value.
222 222
223 223 :param password: password
224 224 :param hashed: password in hashed form
225 225 """
226 226 self._assert_bytes(password)
227 227 return hashlib.md5(password).hexdigest() == hashed
228 228
229 229
230 230 def crypto_backend():
231 231 """
232 232 Return the matching crypto backend.
233 233
234 234 Selection is based on if we run tests or not, we pick md5 backend to run
235 235 tests faster since BCRYPT is expensive to calculate
236 236 """
237 237 if rhodecode.is_test:
238 238 RhodeCodeCrypto = _RhodeCodeCryptoMd5()
239 239 else:
240 240 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
241 241
242 242 return RhodeCodeCrypto
243 243
244 244
245 245 def get_crypt_password(password):
246 246 """
247 247 Create the hash of `password` with the active crypto backend.
248 248
249 249 :param password: The cleartext password.
250 250 :type password: unicode
251 251 """
252 252 password = safe_str(password)
253 253 return crypto_backend().hash_create(password)
254 254
255 255
256 256 def check_password(password, hashed):
257 257 """
258 258 Check if the value in `password` matches the hash in `hashed`.
259 259
260 260 :param password: The cleartext password.
261 261 :type password: unicode
262 262
263 263 :param hashed: The expected hashed version of the password.
264 264 :type hashed: The hash has to be passed in in text representation.
265 265 """
266 266 password = safe_str(password)
267 267 return crypto_backend().hash_check(password, hashed)
268 268
269 269
270 270 def generate_auth_token(data, salt=None):
271 271 """
272 272 Generates API KEY from given string
273 273 """
274 274
275 275 if salt is None:
276 276 salt = os.urandom(16)
277 277 return hashlib.sha1(safe_str(data) + salt).hexdigest()
278 278
279 279
280 280 def get_came_from(request):
281 281 """
282 282 get query_string+path from request sanitized after removing auth_token
283 283 """
284 284 _req = request
285 285
286 286 path = _req.path
287 287 if 'auth_token' in _req.GET:
288 288 # sanitize the request and remove auth_token for redirection
289 289 _req.GET.pop('auth_token')
290 290 qs = _req.query_string
291 291 if qs:
292 292 path += '?' + qs
293 293
294 294 return path
295 295
296 296
297 297 class CookieStoreWrapper(object):
298 298
299 299 def __init__(self, cookie_store):
300 300 self.cookie_store = cookie_store
301 301
302 302 def __repr__(self):
303 303 return 'CookieStore<%s>' % (self.cookie_store)
304 304
305 305 def get(self, key, other=None):
306 306 if isinstance(self.cookie_store, dict):
307 307 return self.cookie_store.get(key, other)
308 308 elif isinstance(self.cookie_store, AuthUser):
309 309 return self.cookie_store.__dict__.get(key, other)
310 310
311 311
312 312 def _cached_perms_data(user_id, scope, user_is_admin,
313 313 user_inherit_default_permissions, explicit, algo,
314 314 calculate_super_admin):
315 315
316 316 permissions = PermissionCalculator(
317 317 user_id, scope, user_is_admin, user_inherit_default_permissions,
318 318 explicit, algo, calculate_super_admin)
319 319 return permissions.calculate()
320 320
321 321
322 322 class PermOrigin(object):
323 323 SUPER_ADMIN = 'superadmin'
324 324
325 325 REPO_USER = 'user:%s'
326 326 REPO_USERGROUP = 'usergroup:%s'
327 327 REPO_OWNER = 'repo.owner'
328 328 REPO_DEFAULT = 'repo.default'
329 329 REPO_DEFAULT_NO_INHERIT = 'repo.default.no.inherit'
330 330 REPO_PRIVATE = 'repo.private'
331 331
332 332 REPOGROUP_USER = 'user:%s'
333 333 REPOGROUP_USERGROUP = 'usergroup:%s'
334 334 REPOGROUP_OWNER = 'group.owner'
335 335 REPOGROUP_DEFAULT = 'group.default'
336 336 REPOGROUP_DEFAULT_NO_INHERIT = 'group.default.no.inherit'
337 337
338 338 USERGROUP_USER = 'user:%s'
339 339 USERGROUP_USERGROUP = 'usergroup:%s'
340 340 USERGROUP_OWNER = 'usergroup.owner'
341 341 USERGROUP_DEFAULT = 'usergroup.default'
342 342 USERGROUP_DEFAULT_NO_INHERIT = 'usergroup.default.no.inherit'
343 343
344 344
345 345 class PermOriginDict(dict):
346 346 """
347 347 A special dict used for tracking permissions along with their origins.
348 348
349 349 `__setitem__` has been overridden to expect a tuple(perm, origin)
350 350 `__getitem__` will return only the perm
351 351 `.perm_origin_stack` will return the stack of (perm, origin) set per key
352 352
353 353 >>> perms = PermOriginDict()
354 354 >>> perms['resource'] = 'read', 'default'
355 355 >>> perms['resource']
356 356 'read'
357 357 >>> perms['resource'] = 'write', 'admin'
358 358 >>> perms['resource']
359 359 'write'
360 360 >>> perms.perm_origin_stack
361 361 {'resource': [('read', 'default'), ('write', 'admin')]}
362 362 """
363 363
364 364 def __init__(self, *args, **kw):
365 365 dict.__init__(self, *args, **kw)
366 366 self.perm_origin_stack = collections.OrderedDict()
367 367
368 368 def __setitem__(self, key, (perm, origin)):
369 369 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
370 370 dict.__setitem__(self, key, perm)
371 371
372 372
373 373 class PermissionCalculator(object):
374 374
375 375 def __init__(
376 376 self, user_id, scope, user_is_admin,
377 377 user_inherit_default_permissions, explicit, algo,
378 378 calculate_super_admin=False):
379 379
380 380 self.user_id = user_id
381 381 self.user_is_admin = user_is_admin
382 382 self.inherit_default_permissions = user_inherit_default_permissions
383 383 self.explicit = explicit
384 384 self.algo = algo
385 385 self.calculate_super_admin = calculate_super_admin
386 386
387 387 scope = scope or {}
388 388 self.scope_repo_id = scope.get('repo_id')
389 389 self.scope_repo_group_id = scope.get('repo_group_id')
390 390 self.scope_user_group_id = scope.get('user_group_id')
391 391
392 392 self.default_user_id = User.get_default_user(cache=True).user_id
393 393
394 394 self.permissions_repositories = PermOriginDict()
395 395 self.permissions_repository_groups = PermOriginDict()
396 396 self.permissions_user_groups = PermOriginDict()
397 397 self.permissions_global = set()
398 398
399 399 self.default_repo_perms = Permission.get_default_repo_perms(
400 400 self.default_user_id, self.scope_repo_id)
401 401 self.default_repo_groups_perms = Permission.get_default_group_perms(
402 402 self.default_user_id, self.scope_repo_group_id)
403 403 self.default_user_group_perms = \
404 404 Permission.get_default_user_group_perms(
405 405 self.default_user_id, self.scope_user_group_id)
406 406
407 407 def calculate(self):
408 408 if self.user_is_admin and not self.calculate_super_admin:
409 409 return self._admin_permissions()
410 410
411 411 self._calculate_global_default_permissions()
412 412 self._calculate_global_permissions()
413 413 self._calculate_default_permissions()
414 414 self._calculate_repository_permissions()
415 415 self._calculate_repository_group_permissions()
416 416 self._calculate_user_group_permissions()
417 417 return self._permission_structure()
418 418
419 419 def _admin_permissions(self):
420 420 """
421 421 admin user have all default rights for repositories
422 422 and groups set to admin
423 423 """
424 424 self.permissions_global.add('hg.admin')
425 425 self.permissions_global.add('hg.create.write_on_repogroup.true')
426 426
427 427 # repositories
428 428 for perm in self.default_repo_perms:
429 429 r_k = perm.UserRepoToPerm.repository.repo_name
430 430 p = 'repository.admin'
431 431 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
432 432
433 433 # repository groups
434 434 for perm in self.default_repo_groups_perms:
435 435 rg_k = perm.UserRepoGroupToPerm.group.group_name
436 436 p = 'group.admin'
437 437 self.permissions_repository_groups[rg_k] = p, PermOrigin.SUPER_ADMIN
438 438
439 439 # user groups
440 440 for perm in self.default_user_group_perms:
441 441 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
442 442 p = 'usergroup.admin'
443 443 self.permissions_user_groups[u_k] = p, PermOrigin.SUPER_ADMIN
444 444
445 445 return self._permission_structure()
446 446
447 447 def _calculate_global_default_permissions(self):
448 448 """
449 449 global permissions taken from the default user
450 450 """
451 451 default_global_perms = UserToPerm.query()\
452 452 .filter(UserToPerm.user_id == self.default_user_id)\
453 453 .options(joinedload(UserToPerm.permission))
454 454
455 455 for perm in default_global_perms:
456 456 self.permissions_global.add(perm.permission.permission_name)
457 457
458 458 if self.user_is_admin:
459 459 self.permissions_global.add('hg.admin')
460 460 self.permissions_global.add('hg.create.write_on_repogroup.true')
461 461
462 462 def _calculate_global_permissions(self):
463 463 """
464 464 Set global system permissions with user permissions or permissions
465 465 taken from the user groups of the current user.
466 466
467 467 The permissions include repo creating, repo group creating, forking
468 468 etc.
469 469 """
470 470
471 471 # now we read the defined permissions and overwrite what we have set
472 472 # before those can be configured from groups or users explicitly.
473 473
474 474 # TODO: johbo: This seems to be out of sync, find out the reason
475 475 # for the comment below and update it.
476 476
477 477 # In case we want to extend this list we should be always in sync with
478 478 # User.DEFAULT_USER_PERMISSIONS definitions
479 479 _configurable = frozenset([
480 480 'hg.fork.none', 'hg.fork.repository',
481 481 'hg.create.none', 'hg.create.repository',
482 482 'hg.usergroup.create.false', 'hg.usergroup.create.true',
483 483 'hg.repogroup.create.false', 'hg.repogroup.create.true',
484 484 'hg.create.write_on_repogroup.false',
485 485 'hg.create.write_on_repogroup.true',
486 486 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
487 487 ])
488 488
489 489 # USER GROUPS comes first user group global permissions
490 490 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
491 491 .options(joinedload(UserGroupToPerm.permission))\
492 492 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
493 493 UserGroupMember.users_group_id))\
494 494 .filter(UserGroupMember.user_id == self.user_id)\
495 495 .order_by(UserGroupToPerm.users_group_id)\
496 496 .all()
497 497
498 498 # need to group here by groups since user can be in more than
499 499 # one group, so we get all groups
500 500 _explicit_grouped_perms = [
501 501 [x, list(y)] for x, y in
502 502 itertools.groupby(user_perms_from_users_groups,
503 503 lambda _x: _x.users_group)]
504 504
505 505 for gr, perms in _explicit_grouped_perms:
506 506 # since user can be in multiple groups iterate over them and
507 507 # select the lowest permissions first (more explicit)
508 508 # TODO: marcink: do this^^
509 509
510 510 # group doesn't inherit default permissions so we actually set them
511 511 if not gr.inherit_default_permissions:
512 512 # NEED TO IGNORE all previously set configurable permissions
513 513 # and replace them with explicitly set from this user
514 514 # group permissions
515 515 self.permissions_global = self.permissions_global.difference(
516 516 _configurable)
517 517 for perm in perms:
518 518 self.permissions_global.add(perm.permission.permission_name)
519 519
520 520 # user explicit global permissions
521 521 user_perms = Session().query(UserToPerm)\
522 522 .options(joinedload(UserToPerm.permission))\
523 523 .filter(UserToPerm.user_id == self.user_id).all()
524 524
525 525 if not self.inherit_default_permissions:
526 526 # NEED TO IGNORE all configurable permissions and
527 527 # replace them with explicitly set from this user permissions
528 528 self.permissions_global = self.permissions_global.difference(
529 529 _configurable)
530 530 for perm in user_perms:
531 531 self.permissions_global.add(perm.permission.permission_name)
532 532
533 533 def _calculate_default_permissions(self):
534 534 """
535 535 Set default user permissions for repositories, repository groups
536 536 taken from the default user.
537 537
538 538 Calculate inheritance of object permissions based on what we have now
539 539 in GLOBAL permissions. We check if .false is in GLOBAL since this is
540 540 explicitly set. Inherit is the opposite of .false being there.
541 541
542 542 .. note::
543 543
544 544 the syntax is little bit odd but what we need to check here is
545 545 the opposite of .false permission being in the list so even for
546 546 inconsistent state when both .true/.false is there
547 547 .false is more important
548 548
549 549 """
550 550 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
551 551 in self.permissions_global)
552 552
553 553 # defaults for repositories, taken from `default` user permissions
554 554 # on given repo
555 555 for perm in self.default_repo_perms:
556 556 r_k = perm.UserRepoToPerm.repository.repo_name
557 557 p = perm.Permission.permission_name
558 558 o = PermOrigin.REPO_DEFAULT
559 559 self.permissions_repositories[r_k] = p, o
560 560
561 561 # if we decide this user isn't inheriting permissions from
562 562 # default user we set him to .none so only explicit
563 563 # permissions work
564 564 if not user_inherit_object_permissions:
565 565 p = 'repository.none'
566 566 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
567 567 self.permissions_repositories[r_k] = p, o
568 568
569 569 if perm.Repository.private and not (
570 570 perm.Repository.user_id == self.user_id):
571 571 # disable defaults for private repos,
572 572 p = 'repository.none'
573 573 o = PermOrigin.REPO_PRIVATE
574 574 self.permissions_repositories[r_k] = p, o
575 575
576 576 elif perm.Repository.user_id == self.user_id:
577 577 # set admin if owner
578 578 p = 'repository.admin'
579 579 o = PermOrigin.REPO_OWNER
580 580 self.permissions_repositories[r_k] = p, o
581 581
582 582 if self.user_is_admin:
583 583 p = 'repository.admin'
584 584 o = PermOrigin.SUPER_ADMIN
585 585 self.permissions_repositories[r_k] = p, o
586 586
587 587 # defaults for repository groups taken from `default` user permission
588 588 # on given group
589 589 for perm in self.default_repo_groups_perms:
590 590 rg_k = perm.UserRepoGroupToPerm.group.group_name
591 591 p = perm.Permission.permission_name
592 592 o = PermOrigin.REPOGROUP_DEFAULT
593 593 self.permissions_repository_groups[rg_k] = p, o
594 594
595 595 # if we decide this user isn't inheriting permissions from default
596 596 # user we set him to .none so only explicit permissions work
597 597 if not user_inherit_object_permissions:
598 598 p = 'group.none'
599 599 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
600 600 self.permissions_repository_groups[rg_k] = p, o
601 601
602 602 if perm.RepoGroup.user_id == self.user_id:
603 603 # set admin if owner
604 604 p = 'group.admin'
605 605 o = PermOrigin.REPOGROUP_OWNER
606 606 self.permissions_repository_groups[rg_k] = p, o
607 607
608 608 if self.user_is_admin:
609 609 p = 'group.admin'
610 610 o = PermOrigin.SUPER_ADMIN
611 611 self.permissions_repository_groups[rg_k] = p, o
612 612
613 613 # defaults for user groups taken from `default` user permission
614 614 # on given user group
615 615 for perm in self.default_user_group_perms:
616 616 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
617 617 p = perm.Permission.permission_name
618 618 o = PermOrigin.USERGROUP_DEFAULT
619 619 self.permissions_user_groups[u_k] = p, o
620 620
621 621 # if we decide this user isn't inheriting permissions from default
622 622 # user we set him to .none so only explicit permissions work
623 623 if not user_inherit_object_permissions:
624 624 p = 'usergroup.none'
625 625 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
626 626 self.permissions_user_groups[u_k] = p, o
627 627
628 628 if perm.UserGroup.user_id == self.user_id:
629 629 # set admin if owner
630 630 p = 'usergroup.admin'
631 631 o = PermOrigin.USERGROUP_OWNER
632 632 self.permissions_user_groups[u_k] = p, o
633 633
634 634 if self.user_is_admin:
635 635 p = 'usergroup.admin'
636 636 o = PermOrigin.SUPER_ADMIN
637 637 self.permissions_user_groups[u_k] = p, o
638 638
639 639 def _calculate_repository_permissions(self):
640 640 """
641 641 Repository permissions for the current user.
642 642
643 643 Check if the user is part of user groups for this repository and
644 644 fill in the permission from it. `_choose_permission` decides of which
645 645 permission should be selected based on selected method.
646 646 """
647 647
648 648 # user group for repositories permissions
649 649 user_repo_perms_from_user_group = Permission\
650 650 .get_default_repo_perms_from_user_group(
651 651 self.user_id, self.scope_repo_id)
652 652
653 653 multiple_counter = collections.defaultdict(int)
654 654 for perm in user_repo_perms_from_user_group:
655 655 r_k = perm.UserGroupRepoToPerm.repository.repo_name
656 656 multiple_counter[r_k] += 1
657 657 p = perm.Permission.permission_name
658 658 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
659 659 .users_group.users_group_name
660 660
661 661 if multiple_counter[r_k] > 1:
662 662 cur_perm = self.permissions_repositories[r_k]
663 663 p = self._choose_permission(p, cur_perm)
664 664
665 665 self.permissions_repositories[r_k] = p, o
666 666
667 667 if perm.Repository.user_id == self.user_id:
668 668 # set admin if owner
669 669 p = 'repository.admin'
670 670 o = PermOrigin.REPO_OWNER
671 671 self.permissions_repositories[r_k] = p, o
672 672
673 673 if self.user_is_admin:
674 674 p = 'repository.admin'
675 675 o = PermOrigin.SUPER_ADMIN
676 676 self.permissions_repositories[r_k] = p, o
677 677
678 678 # user explicit permissions for repositories, overrides any specified
679 679 # by the group permission
680 680 user_repo_perms = Permission.get_default_repo_perms(
681 681 self.user_id, self.scope_repo_id)
682 682 for perm in user_repo_perms:
683 683 r_k = perm.UserRepoToPerm.repository.repo_name
684 684 p = perm.Permission.permission_name
685 685 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
686 686
687 687 if not self.explicit:
688 688 cur_perm = self.permissions_repositories.get(
689 689 r_k, 'repository.none')
690 690 p = self._choose_permission(p, cur_perm)
691 691
692 692 self.permissions_repositories[r_k] = p, o
693 693
694 694 if perm.Repository.user_id == self.user_id:
695 695 # set admin if owner
696 696 p = 'repository.admin'
697 697 o = PermOrigin.REPO_OWNER
698 698 self.permissions_repositories[r_k] = p, o
699 699
700 700 if self.user_is_admin:
701 701 p = 'repository.admin'
702 702 o = PermOrigin.SUPER_ADMIN
703 703 self.permissions_repositories[r_k] = p, o
704 704
705 705 def _calculate_repository_group_permissions(self):
706 706 """
707 707 Repository group permissions for the current user.
708 708
709 709 Check if the user is part of user groups for repository groups and
710 710 fill in the permissions from it. `_choose_permission` decides of which
711 711 permission should be selected based on selected method.
712 712 """
713 713 # user group for repo groups permissions
714 714 user_repo_group_perms_from_user_group = Permission\
715 715 .get_default_group_perms_from_user_group(
716 716 self.user_id, self.scope_repo_group_id)
717 717
718 718 multiple_counter = collections.defaultdict(int)
719 719 for perm in user_repo_group_perms_from_user_group:
720 720 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
721 721 multiple_counter[rg_k] += 1
722 722 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
723 723 .users_group.users_group_name
724 724 p = perm.Permission.permission_name
725 725
726 726 if multiple_counter[rg_k] > 1:
727 727 cur_perm = self.permissions_repository_groups[rg_k]
728 728 p = self._choose_permission(p, cur_perm)
729 729 self.permissions_repository_groups[rg_k] = p, o
730 730
731 731 if perm.RepoGroup.user_id == self.user_id:
732 732 # set admin if owner, even for member of other user group
733 733 p = 'group.admin'
734 734 o = PermOrigin.REPOGROUP_OWNER
735 735 self.permissions_repository_groups[rg_k] = p, o
736 736
737 737 if self.user_is_admin:
738 738 p = 'group.admin'
739 739 o = PermOrigin.SUPER_ADMIN
740 740 self.permissions_repository_groups[rg_k] = p, o
741 741
742 742 # user explicit permissions for repository groups
743 743 user_repo_groups_perms = Permission.get_default_group_perms(
744 744 self.user_id, self.scope_repo_group_id)
745 745 for perm in user_repo_groups_perms:
746 746 rg_k = perm.UserRepoGroupToPerm.group.group_name
747 747 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
748 748 .user.username
749 749 p = perm.Permission.permission_name
750 750
751 751 if not self.explicit:
752 752 cur_perm = self.permissions_repository_groups.get(
753 753 rg_k, 'group.none')
754 754 p = self._choose_permission(p, cur_perm)
755 755
756 756 self.permissions_repository_groups[rg_k] = p, o
757 757
758 758 if perm.RepoGroup.user_id == self.user_id:
759 759 # set admin if owner
760 760 p = 'group.admin'
761 761 o = PermOrigin.REPOGROUP_OWNER
762 762 self.permissions_repository_groups[rg_k] = p, o
763 763
764 764 if self.user_is_admin:
765 765 p = 'group.admin'
766 766 o = PermOrigin.SUPER_ADMIN
767 767 self.permissions_repository_groups[rg_k] = p, o
768 768
769 769 def _calculate_user_group_permissions(self):
770 770 """
771 771 User group permissions for the current user.
772 772 """
773 773 # user group for user group permissions
774 774 user_group_from_user_group = Permission\
775 775 .get_default_user_group_perms_from_user_group(
776 776 self.user_id, self.scope_user_group_id)
777 777
778 778 multiple_counter = collections.defaultdict(int)
779 779 for perm in user_group_from_user_group:
780 780 ug_k = perm.UserGroupUserGroupToPerm\
781 781 .target_user_group.users_group_name
782 782 multiple_counter[ug_k] += 1
783 783 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
784 784 .user_group.users_group_name
785 785 p = perm.Permission.permission_name
786 786
787 787 if multiple_counter[ug_k] > 1:
788 788 cur_perm = self.permissions_user_groups[ug_k]
789 789 p = self._choose_permission(p, cur_perm)
790 790
791 791 self.permissions_user_groups[ug_k] = p, o
792 792
793 793 if perm.UserGroup.user_id == self.user_id:
794 794 # set admin if owner, even for member of other user group
795 795 p = 'usergroup.admin'
796 796 o = PermOrigin.USERGROUP_OWNER
797 797 self.permissions_user_groups[ug_k] = p, o
798 798
799 799 if self.user_is_admin:
800 800 p = 'usergroup.admin'
801 801 o = PermOrigin.SUPER_ADMIN
802 802 self.permissions_user_groups[ug_k] = p, o
803 803
804 804 # user explicit permission for user groups
805 805 user_user_groups_perms = Permission.get_default_user_group_perms(
806 806 self.user_id, self.scope_user_group_id)
807 807 for perm in user_user_groups_perms:
808 808 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
809 809 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
810 810 .user.username
811 811 p = perm.Permission.permission_name
812 812
813 813 if not self.explicit:
814 814 cur_perm = self.permissions_user_groups.get(
815 815 ug_k, 'usergroup.none')
816 816 p = self._choose_permission(p, cur_perm)
817 817
818 818 self.permissions_user_groups[ug_k] = p, o
819 819
820 820 if perm.UserGroup.user_id == self.user_id:
821 821 # set admin if owner
822 822 p = 'usergroup.admin'
823 823 o = PermOrigin.USERGROUP_OWNER
824 824 self.permissions_user_groups[ug_k] = p, o
825 825
826 826 if self.user_is_admin:
827 827 p = 'usergroup.admin'
828 828 o = PermOrigin.SUPER_ADMIN
829 829 self.permissions_user_groups[ug_k] = p, o
830 830
831 831 def _choose_permission(self, new_perm, cur_perm):
832 832 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
833 833 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
834 834 if self.algo == 'higherwin':
835 835 if new_perm_val > cur_perm_val:
836 836 return new_perm
837 837 return cur_perm
838 838 elif self.algo == 'lowerwin':
839 839 if new_perm_val < cur_perm_val:
840 840 return new_perm
841 841 return cur_perm
842 842
843 843 def _permission_structure(self):
844 844 return {
845 845 'global': self.permissions_global,
846 846 'repositories': self.permissions_repositories,
847 847 'repositories_groups': self.permissions_repository_groups,
848 848 'user_groups': self.permissions_user_groups,
849 849 }
850 850
851 851
852 852 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
853 853 """
854 854 Check if given controller_name is in whitelist of auth token access
855 855 """
856 856 if not whitelist:
857 857 from rhodecode import CONFIG
858 858 whitelist = aslist(
859 859 CONFIG.get('api_access_controllers_whitelist'), sep=',')
860 860 # backward compat translation
861 861 compat = {
862 862 # old controller, new VIEW
863 863 'ChangesetController:*': 'RepoCommitsView:*',
864 864 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
865 865 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
866 866 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
867 867 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
868 868 'GistsController:*': 'GistView:*',
869 869 }
870 870
871 871 log.debug(
872 872 'Allowed views for AUTH TOKEN access: %s' % (whitelist,))
873 873 auth_token_access_valid = False
874 874
875 875 for entry in whitelist:
876 876 token_match = True
877 877 if entry in compat:
878 878 # translate from old Controllers to Pyramid Views
879 879 entry = compat[entry]
880 880
881 881 if '@' in entry:
882 882 # specific AuthToken
883 883 entry, allowed_token = entry.split('@', 1)
884 884 token_match = auth_token == allowed_token
885 885
886 886 if fnmatch.fnmatch(view_name, entry) and token_match:
887 887 auth_token_access_valid = True
888 888 break
889 889
890 890 if auth_token_access_valid:
891 891 log.debug('view: `%s` matches entry in whitelist: %s'
892 892 % (view_name, whitelist))
893 893 else:
894 894 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
895 895 % (view_name, whitelist))
896 896 if auth_token:
897 897 # if we use auth token key and don't have access it's a warning
898 898 log.warning(msg)
899 899 else:
900 900 log.debug(msg)
901 901
902 902 return auth_token_access_valid
903 903
904 904
905 905 class AuthUser(object):
906 906 """
907 907 A simple object that handles all attributes of user in RhodeCode
908 908
909 909 It does lookup based on API key,given user, or user present in session
910 910 Then it fills all required information for such user. It also checks if
911 911 anonymous access is enabled and if so, it returns default user as logged in
912 912 """
913 913 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
914 914
915 915 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
916 916
917 917 self.user_id = user_id
918 918 self._api_key = api_key
919 919
920 920 self.api_key = None
921 self.feed_token = ''
922 921 self.username = username
923 922 self.ip_addr = ip_addr
924 923 self.name = ''
925 924 self.lastname = ''
926 925 self.first_name = ''
927 926 self.last_name = ''
928 927 self.email = ''
929 928 self.is_authenticated = False
930 929 self.admin = False
931 930 self.inherit_default_permissions = False
932 931 self.password = ''
933 932
934 933 self.anonymous_user = None # propagated on propagate_data
935 934 self.propagate_data()
936 935 self._instance = None
937 936 self._permissions_scoped_cache = {} # used to bind scoped calculation
938 937
939 938 @LazyProperty
940 939 def permissions(self):
941 940 return self.get_perms(user=self, cache=False)
942 941
943 942 @LazyProperty
944 943 def permissions_safe(self):
945 944 """
946 945 Filtered permissions excluding not allowed repositories
947 946 """
948 947 perms = self.get_perms(user=self, cache=False)
949 948
950 949 perms['repositories'] = {
951 950 k: v for k, v in perms['repositories'].iteritems()
952 951 if v != 'repository.none'}
953 952 perms['repositories_groups'] = {
954 953 k: v for k, v in perms['repositories_groups'].iteritems()
955 954 if v != 'group.none'}
956 955 perms['user_groups'] = {
957 956 k: v for k, v in perms['user_groups'].iteritems()
958 957 if v != 'usergroup.none'}
959 958 return perms
960 959
961 960 @LazyProperty
962 961 def permissions_full_details(self):
963 962 return self.get_perms(
964 963 user=self, cache=False, calculate_super_admin=True)
965 964
966 965 def permissions_with_scope(self, scope):
967 966 """
968 967 Call the get_perms function with scoped data. The scope in that function
969 968 narrows the SQL calls to the given ID of objects resulting in fetching
970 969 Just particular permission we want to obtain. If scope is an empty dict
971 970 then it basically narrows the scope to GLOBAL permissions only.
972 971
973 972 :param scope: dict
974 973 """
975 974 if 'repo_name' in scope:
976 975 obj = Repository.get_by_repo_name(scope['repo_name'])
977 976 if obj:
978 977 scope['repo_id'] = obj.repo_id
979 978 _scope = {
980 979 'repo_id': -1,
981 980 'user_group_id': -1,
982 981 'repo_group_id': -1,
983 982 }
984 983 _scope.update(scope)
985 984 cache_key = "_".join(map(safe_str, reduce(lambda a, b: a+b,
986 985 _scope.items())))
987 986 if cache_key not in self._permissions_scoped_cache:
988 987 # store in cache to mimic how the @LazyProperty works,
989 988 # the difference here is that we use the unique key calculated
990 989 # from params and values
991 990 res = self.get_perms(user=self, cache=False, scope=_scope)
992 991 self._permissions_scoped_cache[cache_key] = res
993 992 return self._permissions_scoped_cache[cache_key]
994 993
995 994 def get_instance(self):
996 995 return User.get(self.user_id)
997 996
998 997 def update_lastactivity(self):
999 998 if self.user_id:
1000 999 User.get(self.user_id).update_lastactivity()
1001 1000
1002 1001 def propagate_data(self):
1003 1002 """
1004 1003 Fills in user data and propagates values to this instance. Maps fetched
1005 1004 user attributes to this class instance attributes
1006 1005 """
1007 1006 log.debug('AuthUser: starting data propagation for new potential user')
1008 1007 user_model = UserModel()
1009 1008 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1010 1009 is_user_loaded = False
1011 1010
1012 1011 # lookup by userid
1013 1012 if self.user_id is not None and self.user_id != anon_user.user_id:
1014 1013 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1015 1014 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1016 1015
1017 1016 # try go get user by api key
1018 1017 elif self._api_key and self._api_key != anon_user.api_key:
1019 1018 log.debug('Trying Auth User lookup by API KEY: `%s`', self._api_key)
1020 1019 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1021 1020
1022 1021 # lookup by username
1023 1022 elif self.username:
1024 1023 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1025 1024 is_user_loaded = user_model.fill_data(self, username=self.username)
1026 1025 else:
1027 1026 log.debug('No data in %s that could been used to log in', self)
1028 1027
1029 1028 if not is_user_loaded:
1030 1029 log.debug(
1031 1030 'Failed to load user. Fallback to default user %s', anon_user)
1032 1031 # if we cannot authenticate user try anonymous
1033 1032 if anon_user.active:
1034 1033 log.debug('default user is active, using it as a session user')
1035 1034 user_model.fill_data(self, user_id=anon_user.user_id)
1036 1035 # then we set this user is logged in
1037 1036 self.is_authenticated = True
1038 1037 else:
1039 1038 log.debug('default user is NOT active')
1040 1039 # in case of disabled anonymous user we reset some of the
1041 1040 # parameters so such user is "corrupted", skipping the fill_data
1042 1041 for attr in ['user_id', 'username', 'admin', 'active']:
1043 1042 setattr(self, attr, None)
1044 1043 self.is_authenticated = False
1045 1044
1046 1045 if not self.username:
1047 1046 self.username = 'None'
1048 1047
1049 1048 log.debug('AuthUser: propagated user is now %s', self)
1050 1049
1051 1050 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1052 1051 calculate_super_admin=False, cache=False):
1053 1052 """
1054 1053 Fills user permission attribute with permissions taken from database
1055 1054 works for permissions given for repositories, and for permissions that
1056 1055 are granted to groups
1057 1056
1058 1057 :param user: instance of User object from database
1059 1058 :param explicit: In case there are permissions both for user and a group
1060 1059 that user is part of, explicit flag will defiine if user will
1061 1060 explicitly override permissions from group, if it's False it will
1062 1061 make decision based on the algo
1063 1062 :param algo: algorithm to decide what permission should be choose if
1064 1063 it's multiple defined, eg user in two different groups. It also
1065 1064 decides if explicit flag is turned off how to specify the permission
1066 1065 for case when user is in a group + have defined separate permission
1067 1066 """
1068 1067 user_id = user.user_id
1069 1068 user_is_admin = user.is_admin
1070 1069
1071 1070 # inheritance of global permissions like create repo/fork repo etc
1072 1071 user_inherit_default_permissions = user.inherit_default_permissions
1073 1072
1074 1073 log.debug('Computing PERMISSION tree for scope %s' % (scope, ))
1075 1074 compute = caches.conditional_cache(
1076 1075 'short_term', 'cache_desc',
1077 1076 condition=cache, func=_cached_perms_data)
1078 1077 result = compute(user_id, scope, user_is_admin,
1079 1078 user_inherit_default_permissions, explicit, algo,
1080 1079 calculate_super_admin)
1081 1080
1082 1081 result_repr = []
1083 1082 for k in result:
1084 1083 result_repr.append((k, len(result[k])))
1085 1084
1086 1085 log.debug('PERMISSION tree computed %s' % (result_repr,))
1087 1086 return result
1088 1087
1089 1088 @property
1090 1089 def is_default(self):
1091 1090 return self.username == User.DEFAULT_USER
1092 1091
1093 1092 @property
1094 1093 def is_admin(self):
1095 1094 return self.admin
1096 1095
1097 1096 @property
1098 1097 def is_user_object(self):
1099 1098 return self.user_id is not None
1100 1099
1101 1100 @property
1102 1101 def repositories_admin(self):
1103 1102 """
1104 1103 Returns list of repositories you're an admin of
1105 1104 """
1106 1105 return [
1107 1106 x[0] for x in self.permissions['repositories'].iteritems()
1108 1107 if x[1] == 'repository.admin']
1109 1108
1110 1109 @property
1111 1110 def repository_groups_admin(self):
1112 1111 """
1113 1112 Returns list of repository groups you're an admin of
1114 1113 """
1115 1114 return [
1116 1115 x[0] for x in self.permissions['repositories_groups'].iteritems()
1117 1116 if x[1] == 'group.admin']
1118 1117
1119 1118 @property
1120 1119 def user_groups_admin(self):
1121 1120 """
1122 1121 Returns list of user groups you're an admin of
1123 1122 """
1124 1123 return [
1125 1124 x[0] for x in self.permissions['user_groups'].iteritems()
1126 1125 if x[1] == 'usergroup.admin']
1127 1126
1128 1127 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1129 1128 """
1130 1129 Returns list of repository ids that user have access to based on given
1131 1130 perms. The cache flag should be only used in cases that are used for
1132 1131 display purposes, NOT IN ANY CASE for permission checks.
1133 1132 """
1134 1133 from rhodecode.model.scm import RepoList
1135 1134 if not perms:
1136 1135 perms = [
1137 1136 'repository.read', 'repository.write', 'repository.admin']
1138 1137
1139 1138 def _cached_repo_acl(user_id, perm_def, name_filter):
1140 1139 qry = Repository.query()
1141 1140 if name_filter:
1142 1141 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1143 1142 qry = qry.filter(
1144 1143 Repository.repo_name.ilike(ilike_expression))
1145 1144
1146 1145 return [x.repo_id for x in
1147 1146 RepoList(qry, perm_set=perm_def)]
1148 1147
1149 1148 compute = caches.conditional_cache(
1150 1149 'long_term', 'repo_acl_ids',
1151 1150 condition=cache, func=_cached_repo_acl)
1152 1151 return compute(self.user_id, perms, name_filter)
1153 1152
1154 1153 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1155 1154 """
1156 1155 Returns list of repository group ids that user have access to based on given
1157 1156 perms. The cache flag should be only used in cases that are used for
1158 1157 display purposes, NOT IN ANY CASE for permission checks.
1159 1158 """
1160 1159 from rhodecode.model.scm import RepoGroupList
1161 1160 if not perms:
1162 1161 perms = [
1163 1162 'group.read', 'group.write', 'group.admin']
1164 1163
1165 1164 def _cached_repo_group_acl(user_id, perm_def, name_filter):
1166 1165 qry = RepoGroup.query()
1167 1166 if name_filter:
1168 1167 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1169 1168 qry = qry.filter(
1170 1169 RepoGroup.group_name.ilike(ilike_expression))
1171 1170
1172 1171 return [x.group_id for x in
1173 1172 RepoGroupList(qry, perm_set=perm_def)]
1174 1173
1175 1174 compute = caches.conditional_cache(
1176 1175 'long_term', 'repo_group_acl_ids',
1177 1176 condition=cache, func=_cached_repo_group_acl)
1178 1177 return compute(self.user_id, perms, name_filter)
1179 1178
1180 1179 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1181 1180 """
1182 1181 Returns list of user group ids that user have access to based on given
1183 1182 perms. The cache flag should be only used in cases that are used for
1184 1183 display purposes, NOT IN ANY CASE for permission checks.
1185 1184 """
1186 1185 from rhodecode.model.scm import UserGroupList
1187 1186 if not perms:
1188 1187 perms = [
1189 1188 'usergroup.read', 'usergroup.write', 'usergroup.admin']
1190 1189
1191 1190 def _cached_user_group_acl(user_id, perm_def, name_filter):
1192 1191 qry = UserGroup.query()
1193 1192 if name_filter:
1194 1193 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1195 1194 qry = qry.filter(
1196 1195 UserGroup.users_group_name.ilike(ilike_expression))
1197 1196
1198 1197 return [x.users_group_id for x in
1199 1198 UserGroupList(qry, perm_set=perm_def)]
1200 1199
1201 1200 compute = caches.conditional_cache(
1202 1201 'long_term', 'user_group_acl_ids',
1203 1202 condition=cache, func=_cached_user_group_acl)
1204 1203 return compute(self.user_id, perms, name_filter)
1205 1204
1206 1205 @property
1207 1206 def ip_allowed(self):
1208 1207 """
1209 1208 Checks if ip_addr used in constructor is allowed from defined list of
1210 1209 allowed ip_addresses for user
1211 1210
1212 1211 :returns: boolean, True if ip is in allowed ip range
1213 1212 """
1214 1213 # check IP
1215 1214 inherit = self.inherit_default_permissions
1216 1215 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1217 1216 inherit_from_default=inherit)
1218 1217 @property
1219 1218 def personal_repo_group(self):
1220 1219 return RepoGroup.get_user_personal_repo_group(self.user_id)
1221 1220
1221 @LazyProperty
1222 def feed_token(self):
1223 return self.get_instance().feed_token
1224
1222 1225 @classmethod
1223 1226 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1224 1227 allowed_ips = AuthUser.get_allowed_ips(
1225 1228 user_id, cache=True, inherit_from_default=inherit_from_default)
1226 1229 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1227 1230 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
1228 1231 return True
1229 1232 else:
1230 1233 log.info('Access for IP:%s forbidden, '
1231 1234 'not in %s' % (ip_addr, allowed_ips))
1232 1235 return False
1233 1236
1234 1237 def __repr__(self):
1235 1238 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1236 1239 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1237 1240
1238 1241 def set_authenticated(self, authenticated=True):
1239 1242 if self.user_id != self.anonymous_user.user_id:
1240 1243 self.is_authenticated = authenticated
1241 1244
1242 1245 def get_cookie_store(self):
1243 1246 return {
1244 1247 'username': self.username,
1245 1248 'password': md5(self.password or ''),
1246 1249 'user_id': self.user_id,
1247 1250 'is_authenticated': self.is_authenticated
1248 1251 }
1249 1252
1250 1253 @classmethod
1251 1254 def from_cookie_store(cls, cookie_store):
1252 1255 """
1253 1256 Creates AuthUser from a cookie store
1254 1257
1255 1258 :param cls:
1256 1259 :param cookie_store:
1257 1260 """
1258 1261 user_id = cookie_store.get('user_id')
1259 1262 username = cookie_store.get('username')
1260 1263 api_key = cookie_store.get('api_key')
1261 1264 return AuthUser(user_id, api_key, username)
1262 1265
1263 1266 @classmethod
1264 1267 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1265 1268 _set = set()
1266 1269
1267 1270 if inherit_from_default:
1268 1271 default_ips = UserIpMap.query().filter(
1269 1272 UserIpMap.user == User.get_default_user(cache=True))
1270 1273 if cache:
1271 1274 default_ips = default_ips.options(
1272 1275 FromCache("sql_cache_short", "get_user_ips_default"))
1273 1276
1274 1277 # populate from default user
1275 1278 for ip in default_ips:
1276 1279 try:
1277 1280 _set.add(ip.ip_addr)
1278 1281 except ObjectDeletedError:
1279 1282 # since we use heavy caching sometimes it happens that
1280 1283 # we get deleted objects here, we just skip them
1281 1284 pass
1282 1285
1283 1286 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1284 1287 if cache:
1285 1288 user_ips = user_ips.options(
1286 1289 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1287 1290
1288 1291 for ip in user_ips:
1289 1292 try:
1290 1293 _set.add(ip.ip_addr)
1291 1294 except ObjectDeletedError:
1292 1295 # since we use heavy caching sometimes it happens that we get
1293 1296 # deleted objects here, we just skip them
1294 1297 pass
1295 1298 return _set or set(['0.0.0.0/0', '::/0'])
1296 1299
1297 1300
1298 1301 def set_available_permissions(settings):
1299 1302 """
1300 1303 This function will propagate pyramid settings with all available defined
1301 1304 permission given in db. We don't want to check each time from db for new
1302 1305 permissions since adding a new permission also requires application restart
1303 1306 ie. to decorate new views with the newly created permission
1304 1307
1305 1308 :param settings: current pyramid registry.settings
1306 1309
1307 1310 """
1308 1311 log.debug('auth: getting information about all available permissions')
1309 1312 try:
1310 1313 sa = meta.Session
1311 1314 all_perms = sa.query(Permission).all()
1312 1315 settings.setdefault('available_permissions',
1313 1316 [x.permission_name for x in all_perms])
1314 1317 log.debug('auth: set available permissions')
1315 1318 except Exception:
1316 1319 log.exception('Failed to fetch permissions from the database.')
1317 1320 raise
1318 1321
1319 1322
1320 1323 def get_csrf_token(session, force_new=False, save_if_missing=True):
1321 1324 """
1322 1325 Return the current authentication token, creating one if one doesn't
1323 1326 already exist and the save_if_missing flag is present.
1324 1327
1325 1328 :param session: pass in the pyramid session, else we use the global ones
1326 1329 :param force_new: force to re-generate the token and store it in session
1327 1330 :param save_if_missing: save the newly generated token if it's missing in
1328 1331 session
1329 1332 """
1330 1333 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1331 1334 # from pyramid.csrf import get_csrf_token
1332 1335
1333 1336 if (csrf_token_key not in session and save_if_missing) or force_new:
1334 1337 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1335 1338 session[csrf_token_key] = token
1336 1339 if hasattr(session, 'save'):
1337 1340 session.save()
1338 1341 return session.get(csrf_token_key)
1339 1342
1340 1343
1341 1344 def get_request(perm_class_instance):
1342 1345 from pyramid.threadlocal import get_current_request
1343 1346 pyramid_request = get_current_request()
1344 1347 return pyramid_request
1345 1348
1346 1349
1347 1350 # CHECK DECORATORS
1348 1351 class CSRFRequired(object):
1349 1352 """
1350 1353 Decorator for authenticating a form
1351 1354
1352 1355 This decorator uses an authorization token stored in the client's
1353 1356 session for prevention of certain Cross-site request forgery (CSRF)
1354 1357 attacks (See
1355 1358 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1356 1359 information).
1357 1360
1358 1361 For use with the ``webhelpers.secure_form`` helper functions.
1359 1362
1360 1363 """
1361 1364 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1362 1365 except_methods=None):
1363 1366 self.token = token
1364 1367 self.header = header
1365 1368 self.except_methods = except_methods or []
1366 1369
1367 1370 def __call__(self, func):
1368 1371 return get_cython_compat_decorator(self.__wrapper, func)
1369 1372
1370 1373 def _get_csrf(self, _request):
1371 1374 return _request.POST.get(self.token, _request.headers.get(self.header))
1372 1375
1373 1376 def check_csrf(self, _request, cur_token):
1374 1377 supplied_token = self._get_csrf(_request)
1375 1378 return supplied_token and supplied_token == cur_token
1376 1379
1377 1380 def _get_request(self):
1378 1381 return get_request(self)
1379 1382
1380 1383 def __wrapper(self, func, *fargs, **fkwargs):
1381 1384 request = self._get_request()
1382 1385
1383 1386 if request.method in self.except_methods:
1384 1387 return func(*fargs, **fkwargs)
1385 1388
1386 1389 cur_token = get_csrf_token(request.session, save_if_missing=False)
1387 1390 if self.check_csrf(request, cur_token):
1388 1391 if request.POST.get(self.token):
1389 1392 del request.POST[self.token]
1390 1393 return func(*fargs, **fkwargs)
1391 1394 else:
1392 1395 reason = 'token-missing'
1393 1396 supplied_token = self._get_csrf(request)
1394 1397 if supplied_token and cur_token != supplied_token:
1395 1398 reason = 'token-mismatch [%s:%s]' % (
1396 1399 cur_token or ''[:6], supplied_token or ''[:6])
1397 1400
1398 1401 csrf_message = \
1399 1402 ("Cross-site request forgery detected, request denied. See "
1400 1403 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1401 1404 "more information.")
1402 1405 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1403 1406 'REMOTE_ADDR:%s, HEADERS:%s' % (
1404 1407 request, reason, request.remote_addr, request.headers))
1405 1408
1406 1409 raise HTTPForbidden(explanation=csrf_message)
1407 1410
1408 1411
1409 1412 class LoginRequired(object):
1410 1413 """
1411 1414 Must be logged in to execute this function else
1412 1415 redirect to login page
1413 1416
1414 1417 :param api_access: if enabled this checks only for valid auth token
1415 1418 and grants access based on valid token
1416 1419 """
1417 1420 def __init__(self, auth_token_access=None):
1418 1421 self.auth_token_access = auth_token_access
1419 1422
1420 1423 def __call__(self, func):
1421 1424 return get_cython_compat_decorator(self.__wrapper, func)
1422 1425
1423 1426 def _get_request(self):
1424 1427 return get_request(self)
1425 1428
1426 1429 def __wrapper(self, func, *fargs, **fkwargs):
1427 1430 from rhodecode.lib import helpers as h
1428 1431 cls = fargs[0]
1429 1432 user = cls._rhodecode_user
1430 1433 request = self._get_request()
1431 1434 _ = request.translate
1432 1435
1433 1436 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1434 1437 log.debug('Starting login restriction checks for user: %s' % (user,))
1435 1438 # check if our IP is allowed
1436 1439 ip_access_valid = True
1437 1440 if not user.ip_allowed:
1438 1441 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1439 1442 category='warning')
1440 1443 ip_access_valid = False
1441 1444
1442 1445 # check if we used an APIKEY and it's a valid one
1443 1446 # defined white-list of controllers which API access will be enabled
1444 1447 _auth_token = request.GET.get(
1445 1448 'auth_token', '') or request.GET.get('api_key', '')
1446 1449 auth_token_access_valid = allowed_auth_token_access(
1447 1450 loc, auth_token=_auth_token)
1448 1451
1449 1452 # explicit controller is enabled or API is in our whitelist
1450 1453 if self.auth_token_access or auth_token_access_valid:
1451 1454 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1452 1455 db_user = user.get_instance()
1453 1456
1454 1457 if db_user:
1455 1458 if self.auth_token_access:
1456 1459 roles = self.auth_token_access
1457 1460 else:
1458 1461 roles = [UserApiKeys.ROLE_HTTP]
1459 1462 token_match = db_user.authenticate_by_token(
1460 1463 _auth_token, roles=roles)
1461 1464 else:
1462 1465 log.debug('Unable to fetch db instance for auth user: %s', user)
1463 1466 token_match = False
1464 1467
1465 1468 if _auth_token and token_match:
1466 1469 auth_token_access_valid = True
1467 1470 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1468 1471 else:
1469 1472 auth_token_access_valid = False
1470 1473 if not _auth_token:
1471 1474 log.debug("AUTH TOKEN *NOT* present in request")
1472 1475 else:
1473 1476 log.warning(
1474 1477 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1475 1478
1476 1479 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1477 1480 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1478 1481 else 'AUTH_TOKEN_AUTH'
1479 1482
1480 1483 if ip_access_valid and (
1481 1484 user.is_authenticated or auth_token_access_valid):
1482 1485 log.info(
1483 1486 'user %s authenticating with:%s IS authenticated on func %s'
1484 1487 % (user, reason, loc))
1485 1488
1486 1489 # update user data to check last activity
1487 1490 user.update_lastactivity()
1488 1491 Session().commit()
1489 1492 return func(*fargs, **fkwargs)
1490 1493 else:
1491 1494 log.warning(
1492 1495 'user %s authenticating with:%s NOT authenticated on '
1493 1496 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1494 1497 % (user, reason, loc, ip_access_valid,
1495 1498 auth_token_access_valid))
1496 1499 # we preserve the get PARAM
1497 1500 came_from = get_came_from(request)
1498 1501
1499 1502 log.debug('redirecting to login page with %s' % (came_from,))
1500 1503 raise HTTPFound(
1501 1504 h.route_path('login', _query={'came_from': came_from}))
1502 1505
1503 1506
1504 1507 class NotAnonymous(object):
1505 1508 """
1506 1509 Must be logged in to execute this function else
1507 1510 redirect to login page
1508 1511 """
1509 1512
1510 1513 def __call__(self, func):
1511 1514 return get_cython_compat_decorator(self.__wrapper, func)
1512 1515
1513 1516 def _get_request(self):
1514 1517 return get_request(self)
1515 1518
1516 1519 def __wrapper(self, func, *fargs, **fkwargs):
1517 1520 import rhodecode.lib.helpers as h
1518 1521 cls = fargs[0]
1519 1522 self.user = cls._rhodecode_user
1520 1523 request = self._get_request()
1521 1524 _ = request.translate
1522 1525 log.debug('Checking if user is not anonymous @%s' % cls)
1523 1526
1524 1527 anonymous = self.user.username == User.DEFAULT_USER
1525 1528
1526 1529 if anonymous:
1527 1530 came_from = get_came_from(request)
1528 1531 h.flash(_('You need to be a registered user to '
1529 1532 'perform this action'),
1530 1533 category='warning')
1531 1534 raise HTTPFound(
1532 1535 h.route_path('login', _query={'came_from': came_from}))
1533 1536 else:
1534 1537 return func(*fargs, **fkwargs)
1535 1538
1536 1539
1537 1540 class PermsDecorator(object):
1538 1541 """
1539 1542 Base class for controller decorators, we extract the current user from
1540 1543 the class itself, which has it stored in base controllers
1541 1544 """
1542 1545
1543 1546 def __init__(self, *required_perms):
1544 1547 self.required_perms = set(required_perms)
1545 1548
1546 1549 def __call__(self, func):
1547 1550 return get_cython_compat_decorator(self.__wrapper, func)
1548 1551
1549 1552 def _get_request(self):
1550 1553 return get_request(self)
1551 1554
1552 1555 def __wrapper(self, func, *fargs, **fkwargs):
1553 1556 import rhodecode.lib.helpers as h
1554 1557 cls = fargs[0]
1555 1558 _user = cls._rhodecode_user
1556 1559 request = self._get_request()
1557 1560 _ = request.translate
1558 1561
1559 1562 log.debug('checking %s permissions %s for %s %s',
1560 1563 self.__class__.__name__, self.required_perms, cls, _user)
1561 1564
1562 1565 if self.check_permissions(_user):
1563 1566 log.debug('Permission granted for %s %s', cls, _user)
1564 1567 return func(*fargs, **fkwargs)
1565 1568
1566 1569 else:
1567 1570 log.debug('Permission denied for %s %s', cls, _user)
1568 1571 anonymous = _user.username == User.DEFAULT_USER
1569 1572
1570 1573 if anonymous:
1571 1574 came_from = get_came_from(self._get_request())
1572 1575 h.flash(_('You need to be signed in to view this page'),
1573 1576 category='warning')
1574 1577 raise HTTPFound(
1575 1578 h.route_path('login', _query={'came_from': came_from}))
1576 1579
1577 1580 else:
1578 1581 # redirect with 404 to prevent resource discovery
1579 1582 raise HTTPNotFound()
1580 1583
1581 1584 def check_permissions(self, user):
1582 1585 """Dummy function for overriding"""
1583 1586 raise NotImplementedError(
1584 1587 'You have to write this function in child class')
1585 1588
1586 1589
1587 1590 class HasPermissionAllDecorator(PermsDecorator):
1588 1591 """
1589 1592 Checks for access permission for all given predicates. All of them
1590 1593 have to be meet in order to fulfill the request
1591 1594 """
1592 1595
1593 1596 def check_permissions(self, user):
1594 1597 perms = user.permissions_with_scope({})
1595 1598 if self.required_perms.issubset(perms['global']):
1596 1599 return True
1597 1600 return False
1598 1601
1599 1602
1600 1603 class HasPermissionAnyDecorator(PermsDecorator):
1601 1604 """
1602 1605 Checks for access permission for any of given predicates. In order to
1603 1606 fulfill the request any of predicates must be meet
1604 1607 """
1605 1608
1606 1609 def check_permissions(self, user):
1607 1610 perms = user.permissions_with_scope({})
1608 1611 if self.required_perms.intersection(perms['global']):
1609 1612 return True
1610 1613 return False
1611 1614
1612 1615
1613 1616 class HasRepoPermissionAllDecorator(PermsDecorator):
1614 1617 """
1615 1618 Checks for access permission for all given predicates for specific
1616 1619 repository. All of them have to be meet in order to fulfill the request
1617 1620 """
1618 1621 def _get_repo_name(self):
1619 1622 _request = self._get_request()
1620 1623 return get_repo_slug(_request)
1621 1624
1622 1625 def check_permissions(self, user):
1623 1626 perms = user.permissions
1624 1627 repo_name = self._get_repo_name()
1625 1628
1626 1629 try:
1627 1630 user_perms = set([perms['repositories'][repo_name]])
1628 1631 except KeyError:
1629 1632 log.debug('cannot locate repo with name: `%s` in permissions defs',
1630 1633 repo_name)
1631 1634 return False
1632 1635
1633 1636 log.debug('checking `%s` permissions for repo `%s`',
1634 1637 user_perms, repo_name)
1635 1638 if self.required_perms.issubset(user_perms):
1636 1639 return True
1637 1640 return False
1638 1641
1639 1642
1640 1643 class HasRepoPermissionAnyDecorator(PermsDecorator):
1641 1644 """
1642 1645 Checks for access permission for any of given predicates for specific
1643 1646 repository. In order to fulfill the request any of predicates must be meet
1644 1647 """
1645 1648 def _get_repo_name(self):
1646 1649 _request = self._get_request()
1647 1650 return get_repo_slug(_request)
1648 1651
1649 1652 def check_permissions(self, user):
1650 1653 perms = user.permissions
1651 1654 repo_name = self._get_repo_name()
1652 1655
1653 1656 try:
1654 1657 user_perms = set([perms['repositories'][repo_name]])
1655 1658 except KeyError:
1656 1659 log.debug(
1657 1660 'cannot locate repo with name: `%s` in permissions defs',
1658 1661 repo_name)
1659 1662 return False
1660 1663
1661 1664 log.debug('checking `%s` permissions for repo `%s`',
1662 1665 user_perms, repo_name)
1663 1666 if self.required_perms.intersection(user_perms):
1664 1667 return True
1665 1668 return False
1666 1669
1667 1670
1668 1671 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1669 1672 """
1670 1673 Checks for access permission for all given predicates for specific
1671 1674 repository group. All of them have to be meet in order to
1672 1675 fulfill the request
1673 1676 """
1674 1677 def _get_repo_group_name(self):
1675 1678 _request = self._get_request()
1676 1679 return get_repo_group_slug(_request)
1677 1680
1678 1681 def check_permissions(self, user):
1679 1682 perms = user.permissions
1680 1683 group_name = self._get_repo_group_name()
1681 1684 try:
1682 1685 user_perms = set([perms['repositories_groups'][group_name]])
1683 1686 except KeyError:
1684 1687 log.debug(
1685 1688 'cannot locate repo group with name: `%s` in permissions defs',
1686 1689 group_name)
1687 1690 return False
1688 1691
1689 1692 log.debug('checking `%s` permissions for repo group `%s`',
1690 1693 user_perms, group_name)
1691 1694 if self.required_perms.issubset(user_perms):
1692 1695 return True
1693 1696 return False
1694 1697
1695 1698
1696 1699 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1697 1700 """
1698 1701 Checks for access permission for any of given predicates for specific
1699 1702 repository group. In order to fulfill the request any
1700 1703 of predicates must be met
1701 1704 """
1702 1705 def _get_repo_group_name(self):
1703 1706 _request = self._get_request()
1704 1707 return get_repo_group_slug(_request)
1705 1708
1706 1709 def check_permissions(self, user):
1707 1710 perms = user.permissions
1708 1711 group_name = self._get_repo_group_name()
1709 1712
1710 1713 try:
1711 1714 user_perms = set([perms['repositories_groups'][group_name]])
1712 1715 except KeyError:
1713 1716 log.debug(
1714 1717 'cannot locate repo group with name: `%s` in permissions defs',
1715 1718 group_name)
1716 1719 return False
1717 1720
1718 1721 log.debug('checking `%s` permissions for repo group `%s`',
1719 1722 user_perms, group_name)
1720 1723 if self.required_perms.intersection(user_perms):
1721 1724 return True
1722 1725 return False
1723 1726
1724 1727
1725 1728 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1726 1729 """
1727 1730 Checks for access permission for all given predicates for specific
1728 1731 user group. All of them have to be meet in order to fulfill the request
1729 1732 """
1730 1733 def _get_user_group_name(self):
1731 1734 _request = self._get_request()
1732 1735 return get_user_group_slug(_request)
1733 1736
1734 1737 def check_permissions(self, user):
1735 1738 perms = user.permissions
1736 1739 group_name = self._get_user_group_name()
1737 1740 try:
1738 1741 user_perms = set([perms['user_groups'][group_name]])
1739 1742 except KeyError:
1740 1743 return False
1741 1744
1742 1745 if self.required_perms.issubset(user_perms):
1743 1746 return True
1744 1747 return False
1745 1748
1746 1749
1747 1750 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1748 1751 """
1749 1752 Checks for access permission for any of given predicates for specific
1750 1753 user group. In order to fulfill the request any of predicates must be meet
1751 1754 """
1752 1755 def _get_user_group_name(self):
1753 1756 _request = self._get_request()
1754 1757 return get_user_group_slug(_request)
1755 1758
1756 1759 def check_permissions(self, user):
1757 1760 perms = user.permissions
1758 1761 group_name = self._get_user_group_name()
1759 1762 try:
1760 1763 user_perms = set([perms['user_groups'][group_name]])
1761 1764 except KeyError:
1762 1765 return False
1763 1766
1764 1767 if self.required_perms.intersection(user_perms):
1765 1768 return True
1766 1769 return False
1767 1770
1768 1771
1769 1772 # CHECK FUNCTIONS
1770 1773 class PermsFunction(object):
1771 1774 """Base function for other check functions"""
1772 1775
1773 1776 def __init__(self, *perms):
1774 1777 self.required_perms = set(perms)
1775 1778 self.repo_name = None
1776 1779 self.repo_group_name = None
1777 1780 self.user_group_name = None
1778 1781
1779 1782 def __bool__(self):
1780 1783 frame = inspect.currentframe()
1781 1784 stack_trace = traceback.format_stack(frame)
1782 1785 log.error('Checking bool value on a class instance of perm '
1783 1786 'function is not allowed: %s' % ''.join(stack_trace))
1784 1787 # rather than throwing errors, here we always return False so if by
1785 1788 # accident someone checks truth for just an instance it will always end
1786 1789 # up in returning False
1787 1790 return False
1788 1791 __nonzero__ = __bool__
1789 1792
1790 1793 def __call__(self, check_location='', user=None):
1791 1794 if not user:
1792 1795 log.debug('Using user attribute from global request')
1793 1796 # TODO: remove this someday,put as user as attribute here
1794 1797 request = self._get_request()
1795 1798 user = request.user
1796 1799
1797 1800 # init auth user if not already given
1798 1801 if not isinstance(user, AuthUser):
1799 1802 log.debug('Wrapping user %s into AuthUser', user)
1800 1803 user = AuthUser(user.user_id)
1801 1804
1802 1805 cls_name = self.__class__.__name__
1803 1806 check_scope = self._get_check_scope(cls_name)
1804 1807 check_location = check_location or 'unspecified location'
1805 1808
1806 1809 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1807 1810 self.required_perms, user, check_scope, check_location)
1808 1811 if not user:
1809 1812 log.warning('Empty user given for permission check')
1810 1813 return False
1811 1814
1812 1815 if self.check_permissions(user):
1813 1816 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1814 1817 check_scope, user, check_location)
1815 1818 return True
1816 1819
1817 1820 else:
1818 1821 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1819 1822 check_scope, user, check_location)
1820 1823 return False
1821 1824
1822 1825 def _get_request(self):
1823 1826 return get_request(self)
1824 1827
1825 1828 def _get_check_scope(self, cls_name):
1826 1829 return {
1827 1830 'HasPermissionAll': 'GLOBAL',
1828 1831 'HasPermissionAny': 'GLOBAL',
1829 1832 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1830 1833 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1831 1834 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1832 1835 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1833 1836 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1834 1837 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1835 1838 }.get(cls_name, '?:%s' % cls_name)
1836 1839
1837 1840 def check_permissions(self, user):
1838 1841 """Dummy function for overriding"""
1839 1842 raise Exception('You have to write this function in child class')
1840 1843
1841 1844
1842 1845 class HasPermissionAll(PermsFunction):
1843 1846 def check_permissions(self, user):
1844 1847 perms = user.permissions_with_scope({})
1845 1848 if self.required_perms.issubset(perms.get('global')):
1846 1849 return True
1847 1850 return False
1848 1851
1849 1852
1850 1853 class HasPermissionAny(PermsFunction):
1851 1854 def check_permissions(self, user):
1852 1855 perms = user.permissions_with_scope({})
1853 1856 if self.required_perms.intersection(perms.get('global')):
1854 1857 return True
1855 1858 return False
1856 1859
1857 1860
1858 1861 class HasRepoPermissionAll(PermsFunction):
1859 1862 def __call__(self, repo_name=None, check_location='', user=None):
1860 1863 self.repo_name = repo_name
1861 1864 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1862 1865
1863 1866 def _get_repo_name(self):
1864 1867 if not self.repo_name:
1865 1868 _request = self._get_request()
1866 1869 self.repo_name = get_repo_slug(_request)
1867 1870 return self.repo_name
1868 1871
1869 1872 def check_permissions(self, user):
1870 1873 self.repo_name = self._get_repo_name()
1871 1874 perms = user.permissions
1872 1875 try:
1873 1876 user_perms = set([perms['repositories'][self.repo_name]])
1874 1877 except KeyError:
1875 1878 return False
1876 1879 if self.required_perms.issubset(user_perms):
1877 1880 return True
1878 1881 return False
1879 1882
1880 1883
1881 1884 class HasRepoPermissionAny(PermsFunction):
1882 1885 def __call__(self, repo_name=None, check_location='', user=None):
1883 1886 self.repo_name = repo_name
1884 1887 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1885 1888
1886 1889 def _get_repo_name(self):
1887 1890 if not self.repo_name:
1888 1891 _request = self._get_request()
1889 1892 self.repo_name = get_repo_slug(_request)
1890 1893 return self.repo_name
1891 1894
1892 1895 def check_permissions(self, user):
1893 1896 self.repo_name = self._get_repo_name()
1894 1897 perms = user.permissions
1895 1898 try:
1896 1899 user_perms = set([perms['repositories'][self.repo_name]])
1897 1900 except KeyError:
1898 1901 return False
1899 1902 if self.required_perms.intersection(user_perms):
1900 1903 return True
1901 1904 return False
1902 1905
1903 1906
1904 1907 class HasRepoGroupPermissionAny(PermsFunction):
1905 1908 def __call__(self, group_name=None, check_location='', user=None):
1906 1909 self.repo_group_name = group_name
1907 1910 return super(HasRepoGroupPermissionAny, self).__call__(
1908 1911 check_location, user)
1909 1912
1910 1913 def check_permissions(self, user):
1911 1914 perms = user.permissions
1912 1915 try:
1913 1916 user_perms = set(
1914 1917 [perms['repositories_groups'][self.repo_group_name]])
1915 1918 except KeyError:
1916 1919 return False
1917 1920 if self.required_perms.intersection(user_perms):
1918 1921 return True
1919 1922 return False
1920 1923
1921 1924
1922 1925 class HasRepoGroupPermissionAll(PermsFunction):
1923 1926 def __call__(self, group_name=None, check_location='', user=None):
1924 1927 self.repo_group_name = group_name
1925 1928 return super(HasRepoGroupPermissionAll, self).__call__(
1926 1929 check_location, user)
1927 1930
1928 1931 def check_permissions(self, user):
1929 1932 perms = user.permissions
1930 1933 try:
1931 1934 user_perms = set(
1932 1935 [perms['repositories_groups'][self.repo_group_name]])
1933 1936 except KeyError:
1934 1937 return False
1935 1938 if self.required_perms.issubset(user_perms):
1936 1939 return True
1937 1940 return False
1938 1941
1939 1942
1940 1943 class HasUserGroupPermissionAny(PermsFunction):
1941 1944 def __call__(self, user_group_name=None, check_location='', user=None):
1942 1945 self.user_group_name = user_group_name
1943 1946 return super(HasUserGroupPermissionAny, self).__call__(
1944 1947 check_location, user)
1945 1948
1946 1949 def check_permissions(self, user):
1947 1950 perms = user.permissions
1948 1951 try:
1949 1952 user_perms = set([perms['user_groups'][self.user_group_name]])
1950 1953 except KeyError:
1951 1954 return False
1952 1955 if self.required_perms.intersection(user_perms):
1953 1956 return True
1954 1957 return False
1955 1958
1956 1959
1957 1960 class HasUserGroupPermissionAll(PermsFunction):
1958 1961 def __call__(self, user_group_name=None, check_location='', user=None):
1959 1962 self.user_group_name = user_group_name
1960 1963 return super(HasUserGroupPermissionAll, self).__call__(
1961 1964 check_location, user)
1962 1965
1963 1966 def check_permissions(self, user):
1964 1967 perms = user.permissions
1965 1968 try:
1966 1969 user_perms = set([perms['user_groups'][self.user_group_name]])
1967 1970 except KeyError:
1968 1971 return False
1969 1972 if self.required_perms.issubset(user_perms):
1970 1973 return True
1971 1974 return False
1972 1975
1973 1976
1974 1977 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1975 1978 class HasPermissionAnyMiddleware(object):
1976 1979 def __init__(self, *perms):
1977 1980 self.required_perms = set(perms)
1978 1981
1979 1982 def __call__(self, user, repo_name):
1980 1983 # repo_name MUST be unicode, since we handle keys in permission
1981 1984 # dict by unicode
1982 1985 repo_name = safe_unicode(repo_name)
1983 1986 user = AuthUser(user.user_id)
1984 1987 log.debug(
1985 1988 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
1986 1989 self.required_perms, user, repo_name)
1987 1990
1988 1991 if self.check_permissions(user, repo_name):
1989 1992 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
1990 1993 repo_name, user, 'PermissionMiddleware')
1991 1994 return True
1992 1995
1993 1996 else:
1994 1997 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
1995 1998 repo_name, user, 'PermissionMiddleware')
1996 1999 return False
1997 2000
1998 2001 def check_permissions(self, user, repo_name):
1999 2002 perms = user.permissions_with_scope({'repo_name': repo_name})
2000 2003
2001 2004 try:
2002 2005 user_perms = set([perms['repositories'][repo_name]])
2003 2006 except Exception:
2004 2007 log.exception('Error while accessing user permissions')
2005 2008 return False
2006 2009
2007 2010 if self.required_perms.intersection(user_perms):
2008 2011 return True
2009 2012 return False
2010 2013
2011 2014
2012 2015 # SPECIAL VERSION TO HANDLE API AUTH
2013 2016 class _BaseApiPerm(object):
2014 2017 def __init__(self, *perms):
2015 2018 self.required_perms = set(perms)
2016 2019
2017 2020 def __call__(self, check_location=None, user=None, repo_name=None,
2018 2021 group_name=None, user_group_name=None):
2019 2022 cls_name = self.__class__.__name__
2020 2023 check_scope = 'global:%s' % (self.required_perms,)
2021 2024 if repo_name:
2022 2025 check_scope += ', repo_name:%s' % (repo_name,)
2023 2026
2024 2027 if group_name:
2025 2028 check_scope += ', repo_group_name:%s' % (group_name,)
2026 2029
2027 2030 if user_group_name:
2028 2031 check_scope += ', user_group_name:%s' % (user_group_name,)
2029 2032
2030 2033 log.debug(
2031 2034 'checking cls:%s %s %s @ %s'
2032 2035 % (cls_name, self.required_perms, check_scope, check_location))
2033 2036 if not user:
2034 2037 log.debug('Empty User passed into arguments')
2035 2038 return False
2036 2039
2037 2040 # process user
2038 2041 if not isinstance(user, AuthUser):
2039 2042 user = AuthUser(user.user_id)
2040 2043 if not check_location:
2041 2044 check_location = 'unspecified'
2042 2045 if self.check_permissions(user.permissions, repo_name, group_name,
2043 2046 user_group_name):
2044 2047 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2045 2048 check_scope, user, check_location)
2046 2049 return True
2047 2050
2048 2051 else:
2049 2052 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2050 2053 check_scope, user, check_location)
2051 2054 return False
2052 2055
2053 2056 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2054 2057 user_group_name=None):
2055 2058 """
2056 2059 implement in child class should return True if permissions are ok,
2057 2060 False otherwise
2058 2061
2059 2062 :param perm_defs: dict with permission definitions
2060 2063 :param repo_name: repo name
2061 2064 """
2062 2065 raise NotImplementedError()
2063 2066
2064 2067
2065 2068 class HasPermissionAllApi(_BaseApiPerm):
2066 2069 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2067 2070 user_group_name=None):
2068 2071 if self.required_perms.issubset(perm_defs.get('global')):
2069 2072 return True
2070 2073 return False
2071 2074
2072 2075
2073 2076 class HasPermissionAnyApi(_BaseApiPerm):
2074 2077 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2075 2078 user_group_name=None):
2076 2079 if self.required_perms.intersection(perm_defs.get('global')):
2077 2080 return True
2078 2081 return False
2079 2082
2080 2083
2081 2084 class HasRepoPermissionAllApi(_BaseApiPerm):
2082 2085 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2083 2086 user_group_name=None):
2084 2087 try:
2085 2088 _user_perms = set([perm_defs['repositories'][repo_name]])
2086 2089 except KeyError:
2087 2090 log.warning(traceback.format_exc())
2088 2091 return False
2089 2092 if self.required_perms.issubset(_user_perms):
2090 2093 return True
2091 2094 return False
2092 2095
2093 2096
2094 2097 class HasRepoPermissionAnyApi(_BaseApiPerm):
2095 2098 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2096 2099 user_group_name=None):
2097 2100 try:
2098 2101 _user_perms = set([perm_defs['repositories'][repo_name]])
2099 2102 except KeyError:
2100 2103 log.warning(traceback.format_exc())
2101 2104 return False
2102 2105 if self.required_perms.intersection(_user_perms):
2103 2106 return True
2104 2107 return False
2105 2108
2106 2109
2107 2110 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2108 2111 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2109 2112 user_group_name=None):
2110 2113 try:
2111 2114 _user_perms = set([perm_defs['repositories_groups'][group_name]])
2112 2115 except KeyError:
2113 2116 log.warning(traceback.format_exc())
2114 2117 return False
2115 2118 if self.required_perms.intersection(_user_perms):
2116 2119 return True
2117 2120 return False
2118 2121
2119 2122
2120 2123 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2121 2124 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2122 2125 user_group_name=None):
2123 2126 try:
2124 2127 _user_perms = set([perm_defs['repositories_groups'][group_name]])
2125 2128 except KeyError:
2126 2129 log.warning(traceback.format_exc())
2127 2130 return False
2128 2131 if self.required_perms.issubset(_user_perms):
2129 2132 return True
2130 2133 return False
2131 2134
2132 2135
2133 2136 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2134 2137 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2135 2138 user_group_name=None):
2136 2139 try:
2137 2140 _user_perms = set([perm_defs['user_groups'][user_group_name]])
2138 2141 except KeyError:
2139 2142 log.warning(traceback.format_exc())
2140 2143 return False
2141 2144 if self.required_perms.intersection(_user_perms):
2142 2145 return True
2143 2146 return False
2144 2147
2145 2148
2146 2149 def check_ip_access(source_ip, allowed_ips=None):
2147 2150 """
2148 2151 Checks if source_ip is a subnet of any of allowed_ips.
2149 2152
2150 2153 :param source_ip:
2151 2154 :param allowed_ips: list of allowed ips together with mask
2152 2155 """
2153 2156 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
2154 2157 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2155 2158 if isinstance(allowed_ips, (tuple, list, set)):
2156 2159 for ip in allowed_ips:
2157 2160 ip = safe_unicode(ip)
2158 2161 try:
2159 2162 network_address = ipaddress.ip_network(ip, strict=False)
2160 2163 if source_ip_address in network_address:
2161 2164 log.debug('IP %s is network %s' %
2162 2165 (source_ip_address, network_address))
2163 2166 return True
2164 2167 # for any case we cannot determine the IP, don't crash just
2165 2168 # skip it and log as error, we want to say forbidden still when
2166 2169 # sending bad IP
2167 2170 except Exception:
2168 2171 log.error(traceback.format_exc())
2169 2172 continue
2170 2173 return False
2171 2174
2172 2175
2173 2176 def get_cython_compat_decorator(wrapper, func):
2174 2177 """
2175 2178 Creates a cython compatible decorator. The previously used
2176 2179 decorator.decorator() function seems to be incompatible with cython.
2177 2180
2178 2181 :param wrapper: __wrapper method of the decorator class
2179 2182 :param func: decorated function
2180 2183 """
2181 2184 @wraps(func)
2182 2185 def local_wrapper(*args, **kwds):
2183 2186 return wrapper(func, *args, **kwds)
2184 2187 local_wrapper.__wrapped__ = func
2185 2188 return local_wrapper
2186 2189
2187 2190
@@ -1,4366 +1,4370 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37 from sqlalchemy import (
38 38 or_, and_, not_, func, TypeDecorator, event,
39 39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 41 Text, Float, PickleType)
42 42 from sqlalchemy.sql.expression import true, false
43 43 from sqlalchemy.sql.functions import coalesce, count # noqa
44 44 from sqlalchemy.orm import (
45 45 relationship, joinedload, class_mapper, validates, aliased)
46 46 from sqlalchemy.ext.declarative import declared_attr
47 47 from sqlalchemy.ext.hybrid import hybrid_property
48 48 from sqlalchemy.exc import IntegrityError # noqa
49 49 from sqlalchemy.dialects.mysql import LONGTEXT
50 50 from beaker.cache import cache_region
51 51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 52
53 53 from pyramid.threadlocal import get_current_request
54 54
55 55 from rhodecode.translation import _
56 56 from rhodecode.lib.vcs import get_vcs_instance
57 57 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
58 58 from rhodecode.lib.utils2 import (
59 59 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
60 60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 61 glob2re, StrictAttributeDict, cleaned_uri)
62 62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
63 63 JsonRaw
64 64 from rhodecode.lib.ext_json import json
65 65 from rhodecode.lib.caching_query import FromCache
66 66 from rhodecode.lib.encrypt import AESCipher
67 67
68 68 from rhodecode.model.meta import Base, Session
69 69
70 70 URL_SEP = '/'
71 71 log = logging.getLogger(__name__)
72 72
73 73 # =============================================================================
74 74 # BASE CLASSES
75 75 # =============================================================================
76 76
77 77 # this is propagated from .ini file rhodecode.encrypted_values.secret or
78 78 # beaker.session.secret if first is not set.
79 79 # and initialized at environment.py
80 80 ENCRYPTION_KEY = None
81 81
82 82 # used to sort permissions by types, '#' used here is not allowed to be in
83 83 # usernames, and it's very early in sorted string.printable table.
84 84 PERMISSION_TYPE_SORT = {
85 85 'admin': '####',
86 86 'write': '###',
87 87 'read': '##',
88 88 'none': '#',
89 89 }
90 90
91 91
92 92 def display_user_sort(obj):
93 93 """
94 94 Sort function used to sort permissions in .permissions() function of
95 95 Repository, RepoGroup, UserGroup. Also it put the default user in front
96 96 of all other resources
97 97 """
98 98
99 99 if obj.username == User.DEFAULT_USER:
100 100 return '#####'
101 101 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
102 102 return prefix + obj.username
103 103
104 104
105 105 def display_user_group_sort(obj):
106 106 """
107 107 Sort function used to sort permissions in .permissions() function of
108 108 Repository, RepoGroup, UserGroup. Also it put the default user in front
109 109 of all other resources
110 110 """
111 111
112 112 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
113 113 return prefix + obj.users_group_name
114 114
115 115
116 116 def _hash_key(k):
117 117 return md5_safe(k)
118 118
119 119
120 120 def in_filter_generator(qry, items, limit=500):
121 121 """
122 122 Splits IN() into multiple with OR
123 123 e.g.::
124 124 cnt = Repository.query().filter(
125 125 or_(
126 126 *in_filter_generator(Repository.repo_id, range(100000))
127 127 )).count()
128 128 """
129 129 if not items:
130 130 # empty list will cause empty query which might cause security issues
131 131 # this can lead to hidden unpleasant results
132 132 items = [-1]
133 133
134 134 parts = []
135 135 for chunk in xrange(0, len(items), limit):
136 136 parts.append(
137 137 qry.in_(items[chunk: chunk + limit])
138 138 )
139 139
140 140 return parts
141 141
142 142
143 143 class EncryptedTextValue(TypeDecorator):
144 144 """
145 145 Special column for encrypted long text data, use like::
146 146
147 147 value = Column("encrypted_value", EncryptedValue(), nullable=False)
148 148
149 149 This column is intelligent so if value is in unencrypted form it return
150 150 unencrypted form, but on save it always encrypts
151 151 """
152 152 impl = Text
153 153
154 154 def process_bind_param(self, value, dialect):
155 155 if not value:
156 156 return value
157 157 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
158 158 # protect against double encrypting if someone manually starts
159 159 # doing
160 160 raise ValueError('value needs to be in unencrypted format, ie. '
161 161 'not starting with enc$aes')
162 162 return 'enc$aes_hmac$%s' % AESCipher(
163 163 ENCRYPTION_KEY, hmac=True).encrypt(value)
164 164
165 165 def process_result_value(self, value, dialect):
166 166 import rhodecode
167 167
168 168 if not value:
169 169 return value
170 170
171 171 parts = value.split('$', 3)
172 172 if not len(parts) == 3:
173 173 # probably not encrypted values
174 174 return value
175 175 else:
176 176 if parts[0] != 'enc':
177 177 # parts ok but without our header ?
178 178 return value
179 179 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
180 180 'rhodecode.encrypted_values.strict') or True)
181 181 # at that stage we know it's our encryption
182 182 if parts[1] == 'aes':
183 183 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
184 184 elif parts[1] == 'aes_hmac':
185 185 decrypted_data = AESCipher(
186 186 ENCRYPTION_KEY, hmac=True,
187 187 strict_verification=enc_strict_mode).decrypt(parts[2])
188 188 else:
189 189 raise ValueError(
190 190 'Encryption type part is wrong, must be `aes` '
191 191 'or `aes_hmac`, got `%s` instead' % (parts[1]))
192 192 return decrypted_data
193 193
194 194
195 195 class BaseModel(object):
196 196 """
197 197 Base Model for all classes
198 198 """
199 199
200 200 @classmethod
201 201 def _get_keys(cls):
202 202 """return column names for this model """
203 203 return class_mapper(cls).c.keys()
204 204
205 205 def get_dict(self):
206 206 """
207 207 return dict with keys and values corresponding
208 208 to this model data """
209 209
210 210 d = {}
211 211 for k in self._get_keys():
212 212 d[k] = getattr(self, k)
213 213
214 214 # also use __json__() if present to get additional fields
215 215 _json_attr = getattr(self, '__json__', None)
216 216 if _json_attr:
217 217 # update with attributes from __json__
218 218 if callable(_json_attr):
219 219 _json_attr = _json_attr()
220 220 for k, val in _json_attr.iteritems():
221 221 d[k] = val
222 222 return d
223 223
224 224 def get_appstruct(self):
225 225 """return list with keys and values tuples corresponding
226 226 to this model data """
227 227
228 228 lst = []
229 229 for k in self._get_keys():
230 230 lst.append((k, getattr(self, k),))
231 231 return lst
232 232
233 233 def populate_obj(self, populate_dict):
234 234 """populate model with data from given populate_dict"""
235 235
236 236 for k in self._get_keys():
237 237 if k in populate_dict:
238 238 setattr(self, k, populate_dict[k])
239 239
240 240 @classmethod
241 241 def query(cls):
242 242 return Session().query(cls)
243 243
244 244 @classmethod
245 245 def get(cls, id_):
246 246 if id_:
247 247 return cls.query().get(id_)
248 248
249 249 @classmethod
250 250 def get_or_404(cls, id_):
251 251 from pyramid.httpexceptions import HTTPNotFound
252 252
253 253 try:
254 254 id_ = int(id_)
255 255 except (TypeError, ValueError):
256 256 raise HTTPNotFound()
257 257
258 258 res = cls.query().get(id_)
259 259 if not res:
260 260 raise HTTPNotFound()
261 261 return res
262 262
263 263 @classmethod
264 264 def getAll(cls):
265 265 # deprecated and left for backward compatibility
266 266 return cls.get_all()
267 267
268 268 @classmethod
269 269 def get_all(cls):
270 270 return cls.query().all()
271 271
272 272 @classmethod
273 273 def delete(cls, id_):
274 274 obj = cls.query().get(id_)
275 275 Session().delete(obj)
276 276
277 277 @classmethod
278 278 def identity_cache(cls, session, attr_name, value):
279 279 exist_in_session = []
280 280 for (item_cls, pkey), instance in session.identity_map.items():
281 281 if cls == item_cls and getattr(instance, attr_name) == value:
282 282 exist_in_session.append(instance)
283 283 if exist_in_session:
284 284 if len(exist_in_session) == 1:
285 285 return exist_in_session[0]
286 286 log.exception(
287 287 'multiple objects with attr %s and '
288 288 'value %s found with same name: %r',
289 289 attr_name, value, exist_in_session)
290 290
291 291 def __repr__(self):
292 292 if hasattr(self, '__unicode__'):
293 293 # python repr needs to return str
294 294 try:
295 295 return safe_str(self.__unicode__())
296 296 except UnicodeDecodeError:
297 297 pass
298 298 return '<DB:%s>' % (self.__class__.__name__)
299 299
300 300
301 301 class RhodeCodeSetting(Base, BaseModel):
302 302 __tablename__ = 'rhodecode_settings'
303 303 __table_args__ = (
304 304 UniqueConstraint('app_settings_name'),
305 305 {'extend_existing': True, 'mysql_engine': 'InnoDB',
306 306 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
307 307 )
308 308
309 309 SETTINGS_TYPES = {
310 310 'str': safe_str,
311 311 'int': safe_int,
312 312 'unicode': safe_unicode,
313 313 'bool': str2bool,
314 314 'list': functools.partial(aslist, sep=',')
315 315 }
316 316 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
317 317 GLOBAL_CONF_KEY = 'app_settings'
318 318
319 319 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
320 320 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
321 321 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
322 322 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
323 323
324 324 def __init__(self, key='', val='', type='unicode'):
325 325 self.app_settings_name = key
326 326 self.app_settings_type = type
327 327 self.app_settings_value = val
328 328
329 329 @validates('_app_settings_value')
330 330 def validate_settings_value(self, key, val):
331 331 assert type(val) == unicode
332 332 return val
333 333
334 334 @hybrid_property
335 335 def app_settings_value(self):
336 336 v = self._app_settings_value
337 337 _type = self.app_settings_type
338 338 if _type:
339 339 _type = self.app_settings_type.split('.')[0]
340 340 # decode the encrypted value
341 341 if 'encrypted' in self.app_settings_type:
342 342 cipher = EncryptedTextValue()
343 343 v = safe_unicode(cipher.process_result_value(v, None))
344 344
345 345 converter = self.SETTINGS_TYPES.get(_type) or \
346 346 self.SETTINGS_TYPES['unicode']
347 347 return converter(v)
348 348
349 349 @app_settings_value.setter
350 350 def app_settings_value(self, val):
351 351 """
352 352 Setter that will always make sure we use unicode in app_settings_value
353 353
354 354 :param val:
355 355 """
356 356 val = safe_unicode(val)
357 357 # encode the encrypted value
358 358 if 'encrypted' in self.app_settings_type:
359 359 cipher = EncryptedTextValue()
360 360 val = safe_unicode(cipher.process_bind_param(val, None))
361 361 self._app_settings_value = val
362 362
363 363 @hybrid_property
364 364 def app_settings_type(self):
365 365 return self._app_settings_type
366 366
367 367 @app_settings_type.setter
368 368 def app_settings_type(self, val):
369 369 if val.split('.')[0] not in self.SETTINGS_TYPES:
370 370 raise Exception('type must be one of %s got %s'
371 371 % (self.SETTINGS_TYPES.keys(), val))
372 372 self._app_settings_type = val
373 373
374 374 def __unicode__(self):
375 375 return u"<%s('%s:%s[%s]')>" % (
376 376 self.__class__.__name__,
377 377 self.app_settings_name, self.app_settings_value,
378 378 self.app_settings_type
379 379 )
380 380
381 381
382 382 class RhodeCodeUi(Base, BaseModel):
383 383 __tablename__ = 'rhodecode_ui'
384 384 __table_args__ = (
385 385 UniqueConstraint('ui_key'),
386 386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
387 387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
388 388 )
389 389
390 390 HOOK_REPO_SIZE = 'changegroup.repo_size'
391 391 # HG
392 392 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
393 393 HOOK_PULL = 'outgoing.pull_logger'
394 394 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
395 395 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
396 396 HOOK_PUSH = 'changegroup.push_logger'
397 397 HOOK_PUSH_KEY = 'pushkey.key_push'
398 398
399 399 # TODO: johbo: Unify way how hooks are configured for git and hg,
400 400 # git part is currently hardcoded.
401 401
402 402 # SVN PATTERNS
403 403 SVN_BRANCH_ID = 'vcs_svn_branch'
404 404 SVN_TAG_ID = 'vcs_svn_tag'
405 405
406 406 ui_id = Column(
407 407 "ui_id", Integer(), nullable=False, unique=True, default=None,
408 408 primary_key=True)
409 409 ui_section = Column(
410 410 "ui_section", String(255), nullable=True, unique=None, default=None)
411 411 ui_key = Column(
412 412 "ui_key", String(255), nullable=True, unique=None, default=None)
413 413 ui_value = Column(
414 414 "ui_value", String(255), nullable=True, unique=None, default=None)
415 415 ui_active = Column(
416 416 "ui_active", Boolean(), nullable=True, unique=None, default=True)
417 417
418 418 def __repr__(self):
419 419 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
420 420 self.ui_key, self.ui_value)
421 421
422 422
423 423 class RepoRhodeCodeSetting(Base, BaseModel):
424 424 __tablename__ = 'repo_rhodecode_settings'
425 425 __table_args__ = (
426 426 UniqueConstraint(
427 427 'app_settings_name', 'repository_id',
428 428 name='uq_repo_rhodecode_setting_name_repo_id'),
429 429 {'extend_existing': True, 'mysql_engine': 'InnoDB',
430 430 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
431 431 )
432 432
433 433 repository_id = Column(
434 434 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
435 435 nullable=False)
436 436 app_settings_id = Column(
437 437 "app_settings_id", Integer(), nullable=False, unique=True,
438 438 default=None, primary_key=True)
439 439 app_settings_name = Column(
440 440 "app_settings_name", String(255), nullable=True, unique=None,
441 441 default=None)
442 442 _app_settings_value = Column(
443 443 "app_settings_value", String(4096), nullable=True, unique=None,
444 444 default=None)
445 445 _app_settings_type = Column(
446 446 "app_settings_type", String(255), nullable=True, unique=None,
447 447 default=None)
448 448
449 449 repository = relationship('Repository')
450 450
451 451 def __init__(self, repository_id, key='', val='', type='unicode'):
452 452 self.repository_id = repository_id
453 453 self.app_settings_name = key
454 454 self.app_settings_type = type
455 455 self.app_settings_value = val
456 456
457 457 @validates('_app_settings_value')
458 458 def validate_settings_value(self, key, val):
459 459 assert type(val) == unicode
460 460 return val
461 461
462 462 @hybrid_property
463 463 def app_settings_value(self):
464 464 v = self._app_settings_value
465 465 type_ = self.app_settings_type
466 466 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
467 467 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
468 468 return converter(v)
469 469
470 470 @app_settings_value.setter
471 471 def app_settings_value(self, val):
472 472 """
473 473 Setter that will always make sure we use unicode in app_settings_value
474 474
475 475 :param val:
476 476 """
477 477 self._app_settings_value = safe_unicode(val)
478 478
479 479 @hybrid_property
480 480 def app_settings_type(self):
481 481 return self._app_settings_type
482 482
483 483 @app_settings_type.setter
484 484 def app_settings_type(self, val):
485 485 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
486 486 if val not in SETTINGS_TYPES:
487 487 raise Exception('type must be one of %s got %s'
488 488 % (SETTINGS_TYPES.keys(), val))
489 489 self._app_settings_type = val
490 490
491 491 def __unicode__(self):
492 492 return u"<%s('%s:%s:%s[%s]')>" % (
493 493 self.__class__.__name__, self.repository.repo_name,
494 494 self.app_settings_name, self.app_settings_value,
495 495 self.app_settings_type
496 496 )
497 497
498 498
499 499 class RepoRhodeCodeUi(Base, BaseModel):
500 500 __tablename__ = 'repo_rhodecode_ui'
501 501 __table_args__ = (
502 502 UniqueConstraint(
503 503 'repository_id', 'ui_section', 'ui_key',
504 504 name='uq_repo_rhodecode_ui_repository_id_section_key'),
505 505 {'extend_existing': True, 'mysql_engine': 'InnoDB',
506 506 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
507 507 )
508 508
509 509 repository_id = Column(
510 510 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
511 511 nullable=False)
512 512 ui_id = Column(
513 513 "ui_id", Integer(), nullable=False, unique=True, default=None,
514 514 primary_key=True)
515 515 ui_section = Column(
516 516 "ui_section", String(255), nullable=True, unique=None, default=None)
517 517 ui_key = Column(
518 518 "ui_key", String(255), nullable=True, unique=None, default=None)
519 519 ui_value = Column(
520 520 "ui_value", String(255), nullable=True, unique=None, default=None)
521 521 ui_active = Column(
522 522 "ui_active", Boolean(), nullable=True, unique=None, default=True)
523 523
524 524 repository = relationship('Repository')
525 525
526 526 def __repr__(self):
527 527 return '<%s[%s:%s]%s=>%s]>' % (
528 528 self.__class__.__name__, self.repository.repo_name,
529 529 self.ui_section, self.ui_key, self.ui_value)
530 530
531 531
532 532 class User(Base, BaseModel):
533 533 __tablename__ = 'users'
534 534 __table_args__ = (
535 535 UniqueConstraint('username'), UniqueConstraint('email'),
536 536 Index('u_username_idx', 'username'),
537 537 Index('u_email_idx', 'email'),
538 538 {'extend_existing': True, 'mysql_engine': 'InnoDB',
539 539 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
540 540 )
541 541 DEFAULT_USER = 'default'
542 542 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
543 543 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
544 544
545 545 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
546 546 username = Column("username", String(255), nullable=True, unique=None, default=None)
547 547 password = Column("password", String(255), nullable=True, unique=None, default=None)
548 548 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
549 549 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
550 550 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
551 551 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
552 552 _email = Column("email", String(255), nullable=True, unique=None, default=None)
553 553 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
554 554 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
555 555
556 556 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
557 557 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
558 558 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
559 559 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
560 560 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
561 561 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
562 562
563 563 user_log = relationship('UserLog')
564 564 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
565 565
566 566 repositories = relationship('Repository')
567 567 repository_groups = relationship('RepoGroup')
568 568 user_groups = relationship('UserGroup')
569 569
570 570 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
571 571 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
572 572
573 573 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
574 574 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
575 575 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
576 576
577 577 group_member = relationship('UserGroupMember', cascade='all')
578 578
579 579 notifications = relationship('UserNotification', cascade='all')
580 580 # notifications assigned to this user
581 581 user_created_notifications = relationship('Notification', cascade='all')
582 582 # comments created by this user
583 583 user_comments = relationship('ChangesetComment', cascade='all')
584 584 # user profile extra info
585 585 user_emails = relationship('UserEmailMap', cascade='all')
586 586 user_ip_map = relationship('UserIpMap', cascade='all')
587 587 user_auth_tokens = relationship('UserApiKeys', cascade='all')
588 588 user_ssh_keys = relationship('UserSshKeys', cascade='all')
589 589
590 590 # gists
591 591 user_gists = relationship('Gist', cascade='all')
592 592 # user pull requests
593 593 user_pull_requests = relationship('PullRequest', cascade='all')
594 594 # external identities
595 595 extenal_identities = relationship(
596 596 'ExternalIdentity',
597 597 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
598 598 cascade='all')
599 599 # review rules
600 600 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
601 601
602 602 def __unicode__(self):
603 603 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
604 604 self.user_id, self.username)
605 605
606 606 @hybrid_property
607 607 def email(self):
608 608 return self._email
609 609
610 610 @email.setter
611 611 def email(self, val):
612 612 self._email = val.lower() if val else None
613 613
614 614 @hybrid_property
615 615 def first_name(self):
616 616 from rhodecode.lib import helpers as h
617 617 if self.name:
618 618 return h.escape(self.name)
619 619 return self.name
620 620
621 621 @hybrid_property
622 622 def last_name(self):
623 623 from rhodecode.lib import helpers as h
624 624 if self.lastname:
625 625 return h.escape(self.lastname)
626 626 return self.lastname
627 627
628 628 @hybrid_property
629 629 def api_key(self):
630 630 """
631 631 Fetch if exist an auth-token with role ALL connected to this user
632 632 """
633 633 user_auth_token = UserApiKeys.query()\
634 634 .filter(UserApiKeys.user_id == self.user_id)\
635 635 .filter(or_(UserApiKeys.expires == -1,
636 636 UserApiKeys.expires >= time.time()))\
637 637 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
638 638 if user_auth_token:
639 639 user_auth_token = user_auth_token.api_key
640 640
641 641 return user_auth_token
642 642
643 643 @api_key.setter
644 644 def api_key(self, val):
645 645 # don't allow to set API key this is deprecated for now
646 646 self._api_key = None
647 647
648 648 @property
649 649 def reviewer_pull_requests(self):
650 650 return PullRequestReviewers.query() \
651 651 .options(joinedload(PullRequestReviewers.pull_request)) \
652 652 .filter(PullRequestReviewers.user_id == self.user_id) \
653 653 .all()
654 654
655 655 @property
656 656 def firstname(self):
657 657 # alias for future
658 658 return self.name
659 659
660 660 @property
661 661 def emails(self):
662 662 other = UserEmailMap.query()\
663 663 .filter(UserEmailMap.user == self) \
664 664 .order_by(UserEmailMap.email_id.asc()) \
665 665 .all()
666 666 return [self.email] + [x.email for x in other]
667 667
668 668 @property
669 669 def auth_tokens(self):
670 670 auth_tokens = self.get_auth_tokens()
671 671 return [x.api_key for x in auth_tokens]
672 672
673 673 def get_auth_tokens(self):
674 674 return UserApiKeys.query()\
675 675 .filter(UserApiKeys.user == self)\
676 676 .order_by(UserApiKeys.user_api_key_id.asc())\
677 677 .all()
678 678
679 @property
679 @LazyProperty
680 680 def feed_token(self):
681 681 return self.get_feed_token()
682 682
683 def get_feed_token(self):
683 def get_feed_token(self, cache=True):
684 684 feed_tokens = UserApiKeys.query()\
685 685 .filter(UserApiKeys.user == self)\
686 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
687 .all()
686 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
687 if cache:
688 feed_tokens = feed_tokens.options(
689 FromCache("long_term", "get_user_feed_token_%s" % self.user_id))
690
691 feed_tokens = feed_tokens.all()
688 692 if feed_tokens:
689 693 return feed_tokens[0].api_key
690 694 return 'NO_FEED_TOKEN_AVAILABLE'
691 695
692 696 @classmethod
693 697 def get(cls, user_id, cache=False):
694 698 if not user_id:
695 699 return
696 700
697 701 user = cls.query()
698 702 if cache:
699 703 user = user.options(
700 704 FromCache("sql_cache_short", "get_users_%s" % user_id))
701 705 return user.get(user_id)
702 706
703 707 @classmethod
704 708 def extra_valid_auth_tokens(cls, user, role=None):
705 709 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
706 710 .filter(or_(UserApiKeys.expires == -1,
707 711 UserApiKeys.expires >= time.time()))
708 712 if role:
709 713 tokens = tokens.filter(or_(UserApiKeys.role == role,
710 714 UserApiKeys.role == UserApiKeys.ROLE_ALL))
711 715 return tokens.all()
712 716
713 717 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
714 718 from rhodecode.lib import auth
715 719
716 720 log.debug('Trying to authenticate user: %s via auth-token, '
717 721 'and roles: %s', self, roles)
718 722
719 723 if not auth_token:
720 724 return False
721 725
722 726 crypto_backend = auth.crypto_backend()
723 727
724 728 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
725 729 tokens_q = UserApiKeys.query()\
726 730 .filter(UserApiKeys.user_id == self.user_id)\
727 731 .filter(or_(UserApiKeys.expires == -1,
728 732 UserApiKeys.expires >= time.time()))
729 733
730 734 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
731 735
732 736 plain_tokens = []
733 737 hash_tokens = []
734 738
735 739 for token in tokens_q.all():
736 740 # verify scope first
737 741 if token.repo_id:
738 742 # token has a scope, we need to verify it
739 743 if scope_repo_id != token.repo_id:
740 744 log.debug(
741 745 'Scope mismatch: token has a set repo scope: %s, '
742 746 'and calling scope is:%s, skipping further checks',
743 747 token.repo, scope_repo_id)
744 748 # token has a scope, and it doesn't match, skip token
745 749 continue
746 750
747 751 if token.api_key.startswith(crypto_backend.ENC_PREF):
748 752 hash_tokens.append(token.api_key)
749 753 else:
750 754 plain_tokens.append(token.api_key)
751 755
752 756 is_plain_match = auth_token in plain_tokens
753 757 if is_plain_match:
754 758 return True
755 759
756 760 for hashed in hash_tokens:
757 761 # TODO(marcink): this is expensive to calculate, but most secure
758 762 match = crypto_backend.hash_check(auth_token, hashed)
759 763 if match:
760 764 return True
761 765
762 766 return False
763 767
764 768 @property
765 769 def ip_addresses(self):
766 770 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
767 771 return [x.ip_addr for x in ret]
768 772
769 773 @property
770 774 def username_and_name(self):
771 775 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
772 776
773 777 @property
774 778 def username_or_name_or_email(self):
775 779 full_name = self.full_name if self.full_name is not ' ' else None
776 780 return self.username or full_name or self.email
777 781
778 782 @property
779 783 def full_name(self):
780 784 return '%s %s' % (self.first_name, self.last_name)
781 785
782 786 @property
783 787 def full_name_or_username(self):
784 788 return ('%s %s' % (self.first_name, self.last_name)
785 789 if (self.first_name and self.last_name) else self.username)
786 790
787 791 @property
788 792 def full_contact(self):
789 793 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
790 794
791 795 @property
792 796 def short_contact(self):
793 797 return '%s %s' % (self.first_name, self.last_name)
794 798
795 799 @property
796 800 def is_admin(self):
797 801 return self.admin
798 802
799 803 def AuthUser(self, **kwargs):
800 804 """
801 805 Returns instance of AuthUser for this user
802 806 """
803 807 from rhodecode.lib.auth import AuthUser
804 808 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
805 809
806 810 @hybrid_property
807 811 def user_data(self):
808 812 if not self._user_data:
809 813 return {}
810 814
811 815 try:
812 816 return json.loads(self._user_data)
813 817 except TypeError:
814 818 return {}
815 819
816 820 @user_data.setter
817 821 def user_data(self, val):
818 822 if not isinstance(val, dict):
819 823 raise Exception('user_data must be dict, got %s' % type(val))
820 824 try:
821 825 self._user_data = json.dumps(val)
822 826 except Exception:
823 827 log.error(traceback.format_exc())
824 828
825 829 @classmethod
826 830 def get_by_username(cls, username, case_insensitive=False,
827 831 cache=False, identity_cache=False):
828 832 session = Session()
829 833
830 834 if case_insensitive:
831 835 q = cls.query().filter(
832 836 func.lower(cls.username) == func.lower(username))
833 837 else:
834 838 q = cls.query().filter(cls.username == username)
835 839
836 840 if cache:
837 841 if identity_cache:
838 842 val = cls.identity_cache(session, 'username', username)
839 843 if val:
840 844 return val
841 845 else:
842 846 cache_key = "get_user_by_name_%s" % _hash_key(username)
843 847 q = q.options(
844 848 FromCache("sql_cache_short", cache_key))
845 849
846 850 return q.scalar()
847 851
848 852 @classmethod
849 853 def get_by_auth_token(cls, auth_token, cache=False):
850 854 q = UserApiKeys.query()\
851 855 .filter(UserApiKeys.api_key == auth_token)\
852 856 .filter(or_(UserApiKeys.expires == -1,
853 857 UserApiKeys.expires >= time.time()))
854 858 if cache:
855 859 q = q.options(
856 860 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
857 861
858 862 match = q.first()
859 863 if match:
860 864 return match.user
861 865
862 866 @classmethod
863 867 def get_by_email(cls, email, case_insensitive=False, cache=False):
864 868
865 869 if case_insensitive:
866 870 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
867 871
868 872 else:
869 873 q = cls.query().filter(cls.email == email)
870 874
871 875 email_key = _hash_key(email)
872 876 if cache:
873 877 q = q.options(
874 878 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
875 879
876 880 ret = q.scalar()
877 881 if ret is None:
878 882 q = UserEmailMap.query()
879 883 # try fetching in alternate email map
880 884 if case_insensitive:
881 885 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
882 886 else:
883 887 q = q.filter(UserEmailMap.email == email)
884 888 q = q.options(joinedload(UserEmailMap.user))
885 889 if cache:
886 890 q = q.options(
887 891 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
888 892 ret = getattr(q.scalar(), 'user', None)
889 893
890 894 return ret
891 895
892 896 @classmethod
893 897 def get_from_cs_author(cls, author):
894 898 """
895 899 Tries to get User objects out of commit author string
896 900
897 901 :param author:
898 902 """
899 903 from rhodecode.lib.helpers import email, author_name
900 904 # Valid email in the attribute passed, see if they're in the system
901 905 _email = email(author)
902 906 if _email:
903 907 user = cls.get_by_email(_email, case_insensitive=True)
904 908 if user:
905 909 return user
906 910 # Maybe we can match by username?
907 911 _author = author_name(author)
908 912 user = cls.get_by_username(_author, case_insensitive=True)
909 913 if user:
910 914 return user
911 915
912 916 def update_userdata(self, **kwargs):
913 917 usr = self
914 918 old = usr.user_data
915 919 old.update(**kwargs)
916 920 usr.user_data = old
917 921 Session().add(usr)
918 922 log.debug('updated userdata with ', kwargs)
919 923
920 924 def update_lastlogin(self):
921 925 """Update user lastlogin"""
922 926 self.last_login = datetime.datetime.now()
923 927 Session().add(self)
924 928 log.debug('updated user %s lastlogin', self.username)
925 929
926 930 def update_lastactivity(self):
927 931 """Update user lastactivity"""
928 932 self.last_activity = datetime.datetime.now()
929 933 Session().add(self)
930 934 log.debug('updated user `%s` last activity', self.username)
931 935
932 936 def update_password(self, new_password):
933 937 from rhodecode.lib.auth import get_crypt_password
934 938
935 939 self.password = get_crypt_password(new_password)
936 940 Session().add(self)
937 941
938 942 @classmethod
939 943 def get_first_super_admin(cls):
940 944 user = User.query().filter(User.admin == true()).first()
941 945 if user is None:
942 946 raise Exception('FATAL: Missing administrative account!')
943 947 return user
944 948
945 949 @classmethod
946 950 def get_all_super_admins(cls):
947 951 """
948 952 Returns all admin accounts sorted by username
949 953 """
950 954 return User.query().filter(User.admin == true())\
951 955 .order_by(User.username.asc()).all()
952 956
953 957 @classmethod
954 958 def get_default_user(cls, cache=False, refresh=False):
955 959 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
956 960 if user is None:
957 961 raise Exception('FATAL: Missing default account!')
958 962 if refresh:
959 963 # The default user might be based on outdated state which
960 964 # has been loaded from the cache.
961 965 # A call to refresh() ensures that the
962 966 # latest state from the database is used.
963 967 Session().refresh(user)
964 968 return user
965 969
966 970 def _get_default_perms(self, user, suffix=''):
967 971 from rhodecode.model.permission import PermissionModel
968 972 return PermissionModel().get_default_perms(user.user_perms, suffix)
969 973
970 974 def get_default_perms(self, suffix=''):
971 975 return self._get_default_perms(self, suffix)
972 976
973 977 def get_api_data(self, include_secrets=False, details='full'):
974 978 """
975 979 Common function for generating user related data for API
976 980
977 981 :param include_secrets: By default secrets in the API data will be replaced
978 982 by a placeholder value to prevent exposing this data by accident. In case
979 983 this data shall be exposed, set this flag to ``True``.
980 984
981 985 :param details: details can be 'basic|full' basic gives only a subset of
982 986 the available user information that includes user_id, name and emails.
983 987 """
984 988 user = self
985 989 user_data = self.user_data
986 990 data = {
987 991 'user_id': user.user_id,
988 992 'username': user.username,
989 993 'firstname': user.name,
990 994 'lastname': user.lastname,
991 995 'email': user.email,
992 996 'emails': user.emails,
993 997 }
994 998 if details == 'basic':
995 999 return data
996 1000
997 1001 auth_token_length = 40
998 1002 auth_token_replacement = '*' * auth_token_length
999 1003
1000 1004 extras = {
1001 1005 'auth_tokens': [auth_token_replacement],
1002 1006 'active': user.active,
1003 1007 'admin': user.admin,
1004 1008 'extern_type': user.extern_type,
1005 1009 'extern_name': user.extern_name,
1006 1010 'last_login': user.last_login,
1007 1011 'last_activity': user.last_activity,
1008 1012 'ip_addresses': user.ip_addresses,
1009 1013 'language': user_data.get('language')
1010 1014 }
1011 1015 data.update(extras)
1012 1016
1013 1017 if include_secrets:
1014 1018 data['auth_tokens'] = user.auth_tokens
1015 1019 return data
1016 1020
1017 1021 def __json__(self):
1018 1022 data = {
1019 1023 'full_name': self.full_name,
1020 1024 'full_name_or_username': self.full_name_or_username,
1021 1025 'short_contact': self.short_contact,
1022 1026 'full_contact': self.full_contact,
1023 1027 }
1024 1028 data.update(self.get_api_data())
1025 1029 return data
1026 1030
1027 1031
1028 1032 class UserApiKeys(Base, BaseModel):
1029 1033 __tablename__ = 'user_api_keys'
1030 1034 __table_args__ = (
1031 1035 Index('uak_api_key_idx', 'api_key', unique=True),
1032 1036 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1033 1037 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1034 1038 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1035 1039 )
1036 1040 __mapper_args__ = {}
1037 1041
1038 1042 # ApiKey role
1039 1043 ROLE_ALL = 'token_role_all'
1040 1044 ROLE_HTTP = 'token_role_http'
1041 1045 ROLE_VCS = 'token_role_vcs'
1042 1046 ROLE_API = 'token_role_api'
1043 1047 ROLE_FEED = 'token_role_feed'
1044 1048 ROLE_PASSWORD_RESET = 'token_password_reset'
1045 1049
1046 1050 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1047 1051
1048 1052 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 1053 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 1054 api_key = Column("api_key", String(255), nullable=False, unique=True)
1051 1055 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1052 1056 expires = Column('expires', Float(53), nullable=False)
1053 1057 role = Column('role', String(255), nullable=True)
1054 1058 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1055 1059
1056 1060 # scope columns
1057 1061 repo_id = Column(
1058 1062 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1059 1063 nullable=True, unique=None, default=None)
1060 1064 repo = relationship('Repository', lazy='joined')
1061 1065
1062 1066 repo_group_id = Column(
1063 1067 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1064 1068 nullable=True, unique=None, default=None)
1065 1069 repo_group = relationship('RepoGroup', lazy='joined')
1066 1070
1067 1071 user = relationship('User', lazy='joined')
1068 1072
1069 1073 def __unicode__(self):
1070 1074 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1071 1075
1072 1076 def __json__(self):
1073 1077 data = {
1074 1078 'auth_token': self.api_key,
1075 1079 'role': self.role,
1076 1080 'scope': self.scope_humanized,
1077 1081 'expired': self.expired
1078 1082 }
1079 1083 return data
1080 1084
1081 1085 def get_api_data(self, include_secrets=False):
1082 1086 data = self.__json__()
1083 1087 if include_secrets:
1084 1088 return data
1085 1089 else:
1086 1090 data['auth_token'] = self.token_obfuscated
1087 1091 return data
1088 1092
1089 1093 @hybrid_property
1090 1094 def description_safe(self):
1091 1095 from rhodecode.lib import helpers as h
1092 1096 return h.escape(self.description)
1093 1097
1094 1098 @property
1095 1099 def expired(self):
1096 1100 if self.expires == -1:
1097 1101 return False
1098 1102 return time.time() > self.expires
1099 1103
1100 1104 @classmethod
1101 1105 def _get_role_name(cls, role):
1102 1106 return {
1103 1107 cls.ROLE_ALL: _('all'),
1104 1108 cls.ROLE_HTTP: _('http/web interface'),
1105 1109 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1106 1110 cls.ROLE_API: _('api calls'),
1107 1111 cls.ROLE_FEED: _('feed access'),
1108 1112 }.get(role, role)
1109 1113
1110 1114 @property
1111 1115 def role_humanized(self):
1112 1116 return self._get_role_name(self.role)
1113 1117
1114 1118 def _get_scope(self):
1115 1119 if self.repo:
1116 1120 return repr(self.repo)
1117 1121 if self.repo_group:
1118 1122 return repr(self.repo_group) + ' (recursive)'
1119 1123 return 'global'
1120 1124
1121 1125 @property
1122 1126 def scope_humanized(self):
1123 1127 return self._get_scope()
1124 1128
1125 1129 @property
1126 1130 def token_obfuscated(self):
1127 1131 if self.api_key:
1128 1132 return self.api_key[:4] + "****"
1129 1133
1130 1134
1131 1135 class UserEmailMap(Base, BaseModel):
1132 1136 __tablename__ = 'user_email_map'
1133 1137 __table_args__ = (
1134 1138 Index('uem_email_idx', 'email'),
1135 1139 UniqueConstraint('email'),
1136 1140 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1137 1141 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1138 1142 )
1139 1143 __mapper_args__ = {}
1140 1144
1141 1145 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1142 1146 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1143 1147 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1144 1148 user = relationship('User', lazy='joined')
1145 1149
1146 1150 @validates('_email')
1147 1151 def validate_email(self, key, email):
1148 1152 # check if this email is not main one
1149 1153 main_email = Session().query(User).filter(User.email == email).scalar()
1150 1154 if main_email is not None:
1151 1155 raise AttributeError('email %s is present is user table' % email)
1152 1156 return email
1153 1157
1154 1158 @hybrid_property
1155 1159 def email(self):
1156 1160 return self._email
1157 1161
1158 1162 @email.setter
1159 1163 def email(self, val):
1160 1164 self._email = val.lower() if val else None
1161 1165
1162 1166
1163 1167 class UserIpMap(Base, BaseModel):
1164 1168 __tablename__ = 'user_ip_map'
1165 1169 __table_args__ = (
1166 1170 UniqueConstraint('user_id', 'ip_addr'),
1167 1171 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1168 1172 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1169 1173 )
1170 1174 __mapper_args__ = {}
1171 1175
1172 1176 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1173 1177 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1174 1178 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1175 1179 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1176 1180 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1177 1181 user = relationship('User', lazy='joined')
1178 1182
1179 1183 @hybrid_property
1180 1184 def description_safe(self):
1181 1185 from rhodecode.lib import helpers as h
1182 1186 return h.escape(self.description)
1183 1187
1184 1188 @classmethod
1185 1189 def _get_ip_range(cls, ip_addr):
1186 1190 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1187 1191 return [str(net.network_address), str(net.broadcast_address)]
1188 1192
1189 1193 def __json__(self):
1190 1194 return {
1191 1195 'ip_addr': self.ip_addr,
1192 1196 'ip_range': self._get_ip_range(self.ip_addr),
1193 1197 }
1194 1198
1195 1199 def __unicode__(self):
1196 1200 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1197 1201 self.user_id, self.ip_addr)
1198 1202
1199 1203
1200 1204 class UserSshKeys(Base, BaseModel):
1201 1205 __tablename__ = 'user_ssh_keys'
1202 1206 __table_args__ = (
1203 1207 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1204 1208
1205 1209 UniqueConstraint('ssh_key_fingerprint'),
1206 1210
1207 1211 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1208 1212 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1209 1213 )
1210 1214 __mapper_args__ = {}
1211 1215
1212 1216 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1213 1217 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1214 1218 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1215 1219
1216 1220 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1217 1221
1218 1222 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1219 1223 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1220 1224 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1221 1225
1222 1226 user = relationship('User', lazy='joined')
1223 1227
1224 1228 def __json__(self):
1225 1229 data = {
1226 1230 'ssh_fingerprint': self.ssh_key_fingerprint,
1227 1231 'description': self.description,
1228 1232 'created_on': self.created_on
1229 1233 }
1230 1234 return data
1231 1235
1232 1236 def get_api_data(self):
1233 1237 data = self.__json__()
1234 1238 return data
1235 1239
1236 1240
1237 1241 class UserLog(Base, BaseModel):
1238 1242 __tablename__ = 'user_logs'
1239 1243 __table_args__ = (
1240 1244 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1241 1245 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1242 1246 )
1243 1247 VERSION_1 = 'v1'
1244 1248 VERSION_2 = 'v2'
1245 1249 VERSIONS = [VERSION_1, VERSION_2]
1246 1250
1247 1251 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1248 1252 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1249 1253 username = Column("username", String(255), nullable=True, unique=None, default=None)
1250 1254 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1251 1255 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1252 1256 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1253 1257 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1254 1258 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1255 1259
1256 1260 version = Column("version", String(255), nullable=True, default=VERSION_1)
1257 1261 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1258 1262 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1259 1263
1260 1264 def __unicode__(self):
1261 1265 return u"<%s('id:%s:%s')>" % (
1262 1266 self.__class__.__name__, self.repository_name, self.action)
1263 1267
1264 1268 def __json__(self):
1265 1269 return {
1266 1270 'user_id': self.user_id,
1267 1271 'username': self.username,
1268 1272 'repository_id': self.repository_id,
1269 1273 'repository_name': self.repository_name,
1270 1274 'user_ip': self.user_ip,
1271 1275 'action_date': self.action_date,
1272 1276 'action': self.action,
1273 1277 }
1274 1278
1275 1279 @hybrid_property
1276 1280 def entry_id(self):
1277 1281 return self.user_log_id
1278 1282
1279 1283 @property
1280 1284 def action_as_day(self):
1281 1285 return datetime.date(*self.action_date.timetuple()[:3])
1282 1286
1283 1287 user = relationship('User')
1284 1288 repository = relationship('Repository', cascade='')
1285 1289
1286 1290
1287 1291 class UserGroup(Base, BaseModel):
1288 1292 __tablename__ = 'users_groups'
1289 1293 __table_args__ = (
1290 1294 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1291 1295 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1292 1296 )
1293 1297
1294 1298 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1295 1299 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1296 1300 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1297 1301 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1298 1302 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1299 1303 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1300 1304 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1301 1305 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1302 1306
1303 1307 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1304 1308 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1305 1309 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1306 1310 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1307 1311 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1308 1312 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1309 1313
1310 1314 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1311 1315 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1312 1316
1313 1317 @classmethod
1314 1318 def _load_group_data(cls, column):
1315 1319 if not column:
1316 1320 return {}
1317 1321
1318 1322 try:
1319 1323 return json.loads(column) or {}
1320 1324 except TypeError:
1321 1325 return {}
1322 1326
1323 1327 @hybrid_property
1324 1328 def description_safe(self):
1325 1329 from rhodecode.lib import helpers as h
1326 1330 return h.escape(self.description)
1327 1331
1328 1332 @hybrid_property
1329 1333 def group_data(self):
1330 1334 return self._load_group_data(self._group_data)
1331 1335
1332 1336 @group_data.expression
1333 1337 def group_data(self, **kwargs):
1334 1338 return self._group_data
1335 1339
1336 1340 @group_data.setter
1337 1341 def group_data(self, val):
1338 1342 try:
1339 1343 self._group_data = json.dumps(val)
1340 1344 except Exception:
1341 1345 log.error(traceback.format_exc())
1342 1346
1343 1347 def __unicode__(self):
1344 1348 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1345 1349 self.users_group_id,
1346 1350 self.users_group_name)
1347 1351
1348 1352 @classmethod
1349 1353 def get_by_group_name(cls, group_name, cache=False,
1350 1354 case_insensitive=False):
1351 1355 if case_insensitive:
1352 1356 q = cls.query().filter(func.lower(cls.users_group_name) ==
1353 1357 func.lower(group_name))
1354 1358
1355 1359 else:
1356 1360 q = cls.query().filter(cls.users_group_name == group_name)
1357 1361 if cache:
1358 1362 q = q.options(
1359 1363 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1360 1364 return q.scalar()
1361 1365
1362 1366 @classmethod
1363 1367 def get(cls, user_group_id, cache=False):
1364 1368 if not user_group_id:
1365 1369 return
1366 1370
1367 1371 user_group = cls.query()
1368 1372 if cache:
1369 1373 user_group = user_group.options(
1370 1374 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1371 1375 return user_group.get(user_group_id)
1372 1376
1373 1377 def permissions(self, with_admins=True, with_owner=True):
1374 1378 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1375 1379 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1376 1380 joinedload(UserUserGroupToPerm.user),
1377 1381 joinedload(UserUserGroupToPerm.permission),)
1378 1382
1379 1383 # get owners and admins and permissions. We do a trick of re-writing
1380 1384 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1381 1385 # has a global reference and changing one object propagates to all
1382 1386 # others. This means if admin is also an owner admin_row that change
1383 1387 # would propagate to both objects
1384 1388 perm_rows = []
1385 1389 for _usr in q.all():
1386 1390 usr = AttributeDict(_usr.user.get_dict())
1387 1391 usr.permission = _usr.permission.permission_name
1388 1392 perm_rows.append(usr)
1389 1393
1390 1394 # filter the perm rows by 'default' first and then sort them by
1391 1395 # admin,write,read,none permissions sorted again alphabetically in
1392 1396 # each group
1393 1397 perm_rows = sorted(perm_rows, key=display_user_sort)
1394 1398
1395 1399 _admin_perm = 'usergroup.admin'
1396 1400 owner_row = []
1397 1401 if with_owner:
1398 1402 usr = AttributeDict(self.user.get_dict())
1399 1403 usr.owner_row = True
1400 1404 usr.permission = _admin_perm
1401 1405 owner_row.append(usr)
1402 1406
1403 1407 super_admin_rows = []
1404 1408 if with_admins:
1405 1409 for usr in User.get_all_super_admins():
1406 1410 # if this admin is also owner, don't double the record
1407 1411 if usr.user_id == owner_row[0].user_id:
1408 1412 owner_row[0].admin_row = True
1409 1413 else:
1410 1414 usr = AttributeDict(usr.get_dict())
1411 1415 usr.admin_row = True
1412 1416 usr.permission = _admin_perm
1413 1417 super_admin_rows.append(usr)
1414 1418
1415 1419 return super_admin_rows + owner_row + perm_rows
1416 1420
1417 1421 def permission_user_groups(self):
1418 1422 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1419 1423 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1420 1424 joinedload(UserGroupUserGroupToPerm.target_user_group),
1421 1425 joinedload(UserGroupUserGroupToPerm.permission),)
1422 1426
1423 1427 perm_rows = []
1424 1428 for _user_group in q.all():
1425 1429 usr = AttributeDict(_user_group.user_group.get_dict())
1426 1430 usr.permission = _user_group.permission.permission_name
1427 1431 perm_rows.append(usr)
1428 1432
1429 1433 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1430 1434 return perm_rows
1431 1435
1432 1436 def _get_default_perms(self, user_group, suffix=''):
1433 1437 from rhodecode.model.permission import PermissionModel
1434 1438 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1435 1439
1436 1440 def get_default_perms(self, suffix=''):
1437 1441 return self._get_default_perms(self, suffix)
1438 1442
1439 1443 def get_api_data(self, with_group_members=True, include_secrets=False):
1440 1444 """
1441 1445 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1442 1446 basically forwarded.
1443 1447
1444 1448 """
1445 1449 user_group = self
1446 1450 data = {
1447 1451 'users_group_id': user_group.users_group_id,
1448 1452 'group_name': user_group.users_group_name,
1449 1453 'group_description': user_group.user_group_description,
1450 1454 'active': user_group.users_group_active,
1451 1455 'owner': user_group.user.username,
1452 1456 'owner_email': user_group.user.email,
1453 1457 }
1454 1458
1455 1459 if with_group_members:
1456 1460 users = []
1457 1461 for user in user_group.members:
1458 1462 user = user.user
1459 1463 users.append(user.get_api_data(include_secrets=include_secrets))
1460 1464 data['users'] = users
1461 1465
1462 1466 return data
1463 1467
1464 1468
1465 1469 class UserGroupMember(Base, BaseModel):
1466 1470 __tablename__ = 'users_groups_members'
1467 1471 __table_args__ = (
1468 1472 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1469 1473 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1470 1474 )
1471 1475
1472 1476 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1473 1477 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1474 1478 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1475 1479
1476 1480 user = relationship('User', lazy='joined')
1477 1481 users_group = relationship('UserGroup')
1478 1482
1479 1483 def __init__(self, gr_id='', u_id=''):
1480 1484 self.users_group_id = gr_id
1481 1485 self.user_id = u_id
1482 1486
1483 1487
1484 1488 class RepositoryField(Base, BaseModel):
1485 1489 __tablename__ = 'repositories_fields'
1486 1490 __table_args__ = (
1487 1491 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1488 1492 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1489 1493 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1490 1494 )
1491 1495 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1492 1496
1493 1497 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1494 1498 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1495 1499 field_key = Column("field_key", String(250))
1496 1500 field_label = Column("field_label", String(1024), nullable=False)
1497 1501 field_value = Column("field_value", String(10000), nullable=False)
1498 1502 field_desc = Column("field_desc", String(1024), nullable=False)
1499 1503 field_type = Column("field_type", String(255), nullable=False, unique=None)
1500 1504 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1501 1505
1502 1506 repository = relationship('Repository')
1503 1507
1504 1508 @property
1505 1509 def field_key_prefixed(self):
1506 1510 return 'ex_%s' % self.field_key
1507 1511
1508 1512 @classmethod
1509 1513 def un_prefix_key(cls, key):
1510 1514 if key.startswith(cls.PREFIX):
1511 1515 return key[len(cls.PREFIX):]
1512 1516 return key
1513 1517
1514 1518 @classmethod
1515 1519 def get_by_key_name(cls, key, repo):
1516 1520 row = cls.query()\
1517 1521 .filter(cls.repository == repo)\
1518 1522 .filter(cls.field_key == key).scalar()
1519 1523 return row
1520 1524
1521 1525
1522 1526 class Repository(Base, BaseModel):
1523 1527 __tablename__ = 'repositories'
1524 1528 __table_args__ = (
1525 1529 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1526 1530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1527 1531 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1528 1532 )
1529 1533 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1530 1534 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1531 1535
1532 1536 STATE_CREATED = 'repo_state_created'
1533 1537 STATE_PENDING = 'repo_state_pending'
1534 1538 STATE_ERROR = 'repo_state_error'
1535 1539
1536 1540 LOCK_AUTOMATIC = 'lock_auto'
1537 1541 LOCK_API = 'lock_api'
1538 1542 LOCK_WEB = 'lock_web'
1539 1543 LOCK_PULL = 'lock_pull'
1540 1544
1541 1545 NAME_SEP = URL_SEP
1542 1546
1543 1547 repo_id = Column(
1544 1548 "repo_id", Integer(), nullable=False, unique=True, default=None,
1545 1549 primary_key=True)
1546 1550 _repo_name = Column(
1547 1551 "repo_name", Text(), nullable=False, default=None)
1548 1552 _repo_name_hash = Column(
1549 1553 "repo_name_hash", String(255), nullable=False, unique=True)
1550 1554 repo_state = Column("repo_state", String(255), nullable=True)
1551 1555
1552 1556 clone_uri = Column(
1553 1557 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1554 1558 default=None)
1555 1559 repo_type = Column(
1556 1560 "repo_type", String(255), nullable=False, unique=False, default=None)
1557 1561 user_id = Column(
1558 1562 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1559 1563 unique=False, default=None)
1560 1564 private = Column(
1561 1565 "private", Boolean(), nullable=True, unique=None, default=None)
1562 1566 enable_statistics = Column(
1563 1567 "statistics", Boolean(), nullable=True, unique=None, default=True)
1564 1568 enable_downloads = Column(
1565 1569 "downloads", Boolean(), nullable=True, unique=None, default=True)
1566 1570 description = Column(
1567 1571 "description", String(10000), nullable=True, unique=None, default=None)
1568 1572 created_on = Column(
1569 1573 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1570 1574 default=datetime.datetime.now)
1571 1575 updated_on = Column(
1572 1576 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1573 1577 default=datetime.datetime.now)
1574 1578 _landing_revision = Column(
1575 1579 "landing_revision", String(255), nullable=False, unique=False,
1576 1580 default=None)
1577 1581 enable_locking = Column(
1578 1582 "enable_locking", Boolean(), nullable=False, unique=None,
1579 1583 default=False)
1580 1584 _locked = Column(
1581 1585 "locked", String(255), nullable=True, unique=False, default=None)
1582 1586 _changeset_cache = Column(
1583 1587 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1584 1588
1585 1589 fork_id = Column(
1586 1590 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1587 1591 nullable=True, unique=False, default=None)
1588 1592 group_id = Column(
1589 1593 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1590 1594 unique=False, default=None)
1591 1595
1592 1596 user = relationship('User', lazy='joined')
1593 1597 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1594 1598 group = relationship('RepoGroup', lazy='joined')
1595 1599 repo_to_perm = relationship(
1596 1600 'UserRepoToPerm', cascade='all',
1597 1601 order_by='UserRepoToPerm.repo_to_perm_id')
1598 1602 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1599 1603 stats = relationship('Statistics', cascade='all', uselist=False)
1600 1604
1601 1605 followers = relationship(
1602 1606 'UserFollowing',
1603 1607 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1604 1608 cascade='all')
1605 1609 extra_fields = relationship(
1606 1610 'RepositoryField', cascade="all, delete, delete-orphan")
1607 1611 logs = relationship('UserLog')
1608 1612 comments = relationship(
1609 1613 'ChangesetComment', cascade="all, delete, delete-orphan")
1610 1614 pull_requests_source = relationship(
1611 1615 'PullRequest',
1612 1616 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1613 1617 cascade="all, delete, delete-orphan")
1614 1618 pull_requests_target = relationship(
1615 1619 'PullRequest',
1616 1620 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1617 1621 cascade="all, delete, delete-orphan")
1618 1622 ui = relationship('RepoRhodeCodeUi', cascade="all")
1619 1623 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1620 1624 integrations = relationship('Integration',
1621 1625 cascade="all, delete, delete-orphan")
1622 1626
1623 1627 def __unicode__(self):
1624 1628 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1625 1629 safe_unicode(self.repo_name))
1626 1630
1627 1631 @hybrid_property
1628 1632 def description_safe(self):
1629 1633 from rhodecode.lib import helpers as h
1630 1634 return h.escape(self.description)
1631 1635
1632 1636 @hybrid_property
1633 1637 def landing_rev(self):
1634 1638 # always should return [rev_type, rev]
1635 1639 if self._landing_revision:
1636 1640 _rev_info = self._landing_revision.split(':')
1637 1641 if len(_rev_info) < 2:
1638 1642 _rev_info.insert(0, 'rev')
1639 1643 return [_rev_info[0], _rev_info[1]]
1640 1644 return [None, None]
1641 1645
1642 1646 @landing_rev.setter
1643 1647 def landing_rev(self, val):
1644 1648 if ':' not in val:
1645 1649 raise ValueError('value must be delimited with `:` and consist '
1646 1650 'of <rev_type>:<rev>, got %s instead' % val)
1647 1651 self._landing_revision = val
1648 1652
1649 1653 @hybrid_property
1650 1654 def locked(self):
1651 1655 if self._locked:
1652 1656 user_id, timelocked, reason = self._locked.split(':')
1653 1657 lock_values = int(user_id), timelocked, reason
1654 1658 else:
1655 1659 lock_values = [None, None, None]
1656 1660 return lock_values
1657 1661
1658 1662 @locked.setter
1659 1663 def locked(self, val):
1660 1664 if val and isinstance(val, (list, tuple)):
1661 1665 self._locked = ':'.join(map(str, val))
1662 1666 else:
1663 1667 self._locked = None
1664 1668
1665 1669 @hybrid_property
1666 1670 def changeset_cache(self):
1667 1671 from rhodecode.lib.vcs.backends.base import EmptyCommit
1668 1672 dummy = EmptyCommit().__json__()
1669 1673 if not self._changeset_cache:
1670 1674 return dummy
1671 1675 try:
1672 1676 return json.loads(self._changeset_cache)
1673 1677 except TypeError:
1674 1678 return dummy
1675 1679 except Exception:
1676 1680 log.error(traceback.format_exc())
1677 1681 return dummy
1678 1682
1679 1683 @changeset_cache.setter
1680 1684 def changeset_cache(self, val):
1681 1685 try:
1682 1686 self._changeset_cache = json.dumps(val)
1683 1687 except Exception:
1684 1688 log.error(traceback.format_exc())
1685 1689
1686 1690 @hybrid_property
1687 1691 def repo_name(self):
1688 1692 return self._repo_name
1689 1693
1690 1694 @repo_name.setter
1691 1695 def repo_name(self, value):
1692 1696 self._repo_name = value
1693 1697 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1694 1698
1695 1699 @classmethod
1696 1700 def normalize_repo_name(cls, repo_name):
1697 1701 """
1698 1702 Normalizes os specific repo_name to the format internally stored inside
1699 1703 database using URL_SEP
1700 1704
1701 1705 :param cls:
1702 1706 :param repo_name:
1703 1707 """
1704 1708 return cls.NAME_SEP.join(repo_name.split(os.sep))
1705 1709
1706 1710 @classmethod
1707 1711 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1708 1712 session = Session()
1709 1713 q = session.query(cls).filter(cls.repo_name == repo_name)
1710 1714
1711 1715 if cache:
1712 1716 if identity_cache:
1713 1717 val = cls.identity_cache(session, 'repo_name', repo_name)
1714 1718 if val:
1715 1719 return val
1716 1720 else:
1717 1721 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1718 1722 q = q.options(
1719 1723 FromCache("sql_cache_short", cache_key))
1720 1724
1721 1725 return q.scalar()
1722 1726
1723 1727 @classmethod
1724 1728 def get_by_full_path(cls, repo_full_path):
1725 1729 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1726 1730 repo_name = cls.normalize_repo_name(repo_name)
1727 1731 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1728 1732
1729 1733 @classmethod
1730 1734 def get_repo_forks(cls, repo_id):
1731 1735 return cls.query().filter(Repository.fork_id == repo_id)
1732 1736
1733 1737 @classmethod
1734 1738 def base_path(cls):
1735 1739 """
1736 1740 Returns base path when all repos are stored
1737 1741
1738 1742 :param cls:
1739 1743 """
1740 1744 q = Session().query(RhodeCodeUi)\
1741 1745 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1742 1746 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1743 1747 return q.one().ui_value
1744 1748
1745 1749 @classmethod
1746 1750 def is_valid(cls, repo_name):
1747 1751 """
1748 1752 returns True if given repo name is a valid filesystem repository
1749 1753
1750 1754 :param cls:
1751 1755 :param repo_name:
1752 1756 """
1753 1757 from rhodecode.lib.utils import is_valid_repo
1754 1758
1755 1759 return is_valid_repo(repo_name, cls.base_path())
1756 1760
1757 1761 @classmethod
1758 1762 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1759 1763 case_insensitive=True):
1760 1764 q = Repository.query()
1761 1765
1762 1766 if not isinstance(user_id, Optional):
1763 1767 q = q.filter(Repository.user_id == user_id)
1764 1768
1765 1769 if not isinstance(group_id, Optional):
1766 1770 q = q.filter(Repository.group_id == group_id)
1767 1771
1768 1772 if case_insensitive:
1769 1773 q = q.order_by(func.lower(Repository.repo_name))
1770 1774 else:
1771 1775 q = q.order_by(Repository.repo_name)
1772 1776 return q.all()
1773 1777
1774 1778 @property
1775 1779 def forks(self):
1776 1780 """
1777 1781 Return forks of this repo
1778 1782 """
1779 1783 return Repository.get_repo_forks(self.repo_id)
1780 1784
1781 1785 @property
1782 1786 def parent(self):
1783 1787 """
1784 1788 Returns fork parent
1785 1789 """
1786 1790 return self.fork
1787 1791
1788 1792 @property
1789 1793 def just_name(self):
1790 1794 return self.repo_name.split(self.NAME_SEP)[-1]
1791 1795
1792 1796 @property
1793 1797 def groups_with_parents(self):
1794 1798 groups = []
1795 1799 if self.group is None:
1796 1800 return groups
1797 1801
1798 1802 cur_gr = self.group
1799 1803 groups.insert(0, cur_gr)
1800 1804 while 1:
1801 1805 gr = getattr(cur_gr, 'parent_group', None)
1802 1806 cur_gr = cur_gr.parent_group
1803 1807 if gr is None:
1804 1808 break
1805 1809 groups.insert(0, gr)
1806 1810
1807 1811 return groups
1808 1812
1809 1813 @property
1810 1814 def groups_and_repo(self):
1811 1815 return self.groups_with_parents, self
1812 1816
1813 1817 @LazyProperty
1814 1818 def repo_path(self):
1815 1819 """
1816 1820 Returns base full path for that repository means where it actually
1817 1821 exists on a filesystem
1818 1822 """
1819 1823 q = Session().query(RhodeCodeUi).filter(
1820 1824 RhodeCodeUi.ui_key == self.NAME_SEP)
1821 1825 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1822 1826 return q.one().ui_value
1823 1827
1824 1828 @property
1825 1829 def repo_full_path(self):
1826 1830 p = [self.repo_path]
1827 1831 # we need to split the name by / since this is how we store the
1828 1832 # names in the database, but that eventually needs to be converted
1829 1833 # into a valid system path
1830 1834 p += self.repo_name.split(self.NAME_SEP)
1831 1835 return os.path.join(*map(safe_unicode, p))
1832 1836
1833 1837 @property
1834 1838 def cache_keys(self):
1835 1839 """
1836 1840 Returns associated cache keys for that repo
1837 1841 """
1838 1842 return CacheKey.query()\
1839 1843 .filter(CacheKey.cache_args == self.repo_name)\
1840 1844 .order_by(CacheKey.cache_key)\
1841 1845 .all()
1842 1846
1843 1847 def get_new_name(self, repo_name):
1844 1848 """
1845 1849 returns new full repository name based on assigned group and new new
1846 1850
1847 1851 :param group_name:
1848 1852 """
1849 1853 path_prefix = self.group.full_path_splitted if self.group else []
1850 1854 return self.NAME_SEP.join(path_prefix + [repo_name])
1851 1855
1852 1856 @property
1853 1857 def _config(self):
1854 1858 """
1855 1859 Returns db based config object.
1856 1860 """
1857 1861 from rhodecode.lib.utils import make_db_config
1858 1862 return make_db_config(clear_session=False, repo=self)
1859 1863
1860 1864 def permissions(self, with_admins=True, with_owner=True):
1861 1865 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1862 1866 q = q.options(joinedload(UserRepoToPerm.repository),
1863 1867 joinedload(UserRepoToPerm.user),
1864 1868 joinedload(UserRepoToPerm.permission),)
1865 1869
1866 1870 # get owners and admins and permissions. We do a trick of re-writing
1867 1871 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1868 1872 # has a global reference and changing one object propagates to all
1869 1873 # others. This means if admin is also an owner admin_row that change
1870 1874 # would propagate to both objects
1871 1875 perm_rows = []
1872 1876 for _usr in q.all():
1873 1877 usr = AttributeDict(_usr.user.get_dict())
1874 1878 usr.permission = _usr.permission.permission_name
1875 1879 perm_rows.append(usr)
1876 1880
1877 1881 # filter the perm rows by 'default' first and then sort them by
1878 1882 # admin,write,read,none permissions sorted again alphabetically in
1879 1883 # each group
1880 1884 perm_rows = sorted(perm_rows, key=display_user_sort)
1881 1885
1882 1886 _admin_perm = 'repository.admin'
1883 1887 owner_row = []
1884 1888 if with_owner:
1885 1889 usr = AttributeDict(self.user.get_dict())
1886 1890 usr.owner_row = True
1887 1891 usr.permission = _admin_perm
1888 1892 owner_row.append(usr)
1889 1893
1890 1894 super_admin_rows = []
1891 1895 if with_admins:
1892 1896 for usr in User.get_all_super_admins():
1893 1897 # if this admin is also owner, don't double the record
1894 1898 if usr.user_id == owner_row[0].user_id:
1895 1899 owner_row[0].admin_row = True
1896 1900 else:
1897 1901 usr = AttributeDict(usr.get_dict())
1898 1902 usr.admin_row = True
1899 1903 usr.permission = _admin_perm
1900 1904 super_admin_rows.append(usr)
1901 1905
1902 1906 return super_admin_rows + owner_row + perm_rows
1903 1907
1904 1908 def permission_user_groups(self):
1905 1909 q = UserGroupRepoToPerm.query().filter(
1906 1910 UserGroupRepoToPerm.repository == self)
1907 1911 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1908 1912 joinedload(UserGroupRepoToPerm.users_group),
1909 1913 joinedload(UserGroupRepoToPerm.permission),)
1910 1914
1911 1915 perm_rows = []
1912 1916 for _user_group in q.all():
1913 1917 usr = AttributeDict(_user_group.users_group.get_dict())
1914 1918 usr.permission = _user_group.permission.permission_name
1915 1919 perm_rows.append(usr)
1916 1920
1917 1921 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1918 1922 return perm_rows
1919 1923
1920 1924 def get_api_data(self, include_secrets=False):
1921 1925 """
1922 1926 Common function for generating repo api data
1923 1927
1924 1928 :param include_secrets: See :meth:`User.get_api_data`.
1925 1929
1926 1930 """
1927 1931 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1928 1932 # move this methods on models level.
1929 1933 from rhodecode.model.settings import SettingsModel
1930 1934 from rhodecode.model.repo import RepoModel
1931 1935
1932 1936 repo = self
1933 1937 _user_id, _time, _reason = self.locked
1934 1938
1935 1939 data = {
1936 1940 'repo_id': repo.repo_id,
1937 1941 'repo_name': repo.repo_name,
1938 1942 'repo_type': repo.repo_type,
1939 1943 'clone_uri': repo.clone_uri or '',
1940 1944 'url': RepoModel().get_url(self),
1941 1945 'private': repo.private,
1942 1946 'created_on': repo.created_on,
1943 1947 'description': repo.description_safe,
1944 1948 'landing_rev': repo.landing_rev,
1945 1949 'owner': repo.user.username,
1946 1950 'fork_of': repo.fork.repo_name if repo.fork else None,
1947 1951 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1948 1952 'enable_statistics': repo.enable_statistics,
1949 1953 'enable_locking': repo.enable_locking,
1950 1954 'enable_downloads': repo.enable_downloads,
1951 1955 'last_changeset': repo.changeset_cache,
1952 1956 'locked_by': User.get(_user_id).get_api_data(
1953 1957 include_secrets=include_secrets) if _user_id else None,
1954 1958 'locked_date': time_to_datetime(_time) if _time else None,
1955 1959 'lock_reason': _reason if _reason else None,
1956 1960 }
1957 1961
1958 1962 # TODO: mikhail: should be per-repo settings here
1959 1963 rc_config = SettingsModel().get_all_settings()
1960 1964 repository_fields = str2bool(
1961 1965 rc_config.get('rhodecode_repository_fields'))
1962 1966 if repository_fields:
1963 1967 for f in self.extra_fields:
1964 1968 data[f.field_key_prefixed] = f.field_value
1965 1969
1966 1970 return data
1967 1971
1968 1972 @classmethod
1969 1973 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1970 1974 if not lock_time:
1971 1975 lock_time = time.time()
1972 1976 if not lock_reason:
1973 1977 lock_reason = cls.LOCK_AUTOMATIC
1974 1978 repo.locked = [user_id, lock_time, lock_reason]
1975 1979 Session().add(repo)
1976 1980 Session().commit()
1977 1981
1978 1982 @classmethod
1979 1983 def unlock(cls, repo):
1980 1984 repo.locked = None
1981 1985 Session().add(repo)
1982 1986 Session().commit()
1983 1987
1984 1988 @classmethod
1985 1989 def getlock(cls, repo):
1986 1990 return repo.locked
1987 1991
1988 1992 def is_user_lock(self, user_id):
1989 1993 if self.lock[0]:
1990 1994 lock_user_id = safe_int(self.lock[0])
1991 1995 user_id = safe_int(user_id)
1992 1996 # both are ints, and they are equal
1993 1997 return all([lock_user_id, user_id]) and lock_user_id == user_id
1994 1998
1995 1999 return False
1996 2000
1997 2001 def get_locking_state(self, action, user_id, only_when_enabled=True):
1998 2002 """
1999 2003 Checks locking on this repository, if locking is enabled and lock is
2000 2004 present returns a tuple of make_lock, locked, locked_by.
2001 2005 make_lock can have 3 states None (do nothing) True, make lock
2002 2006 False release lock, This value is later propagated to hooks, which
2003 2007 do the locking. Think about this as signals passed to hooks what to do.
2004 2008
2005 2009 """
2006 2010 # TODO: johbo: This is part of the business logic and should be moved
2007 2011 # into the RepositoryModel.
2008 2012
2009 2013 if action not in ('push', 'pull'):
2010 2014 raise ValueError("Invalid action value: %s" % repr(action))
2011 2015
2012 2016 # defines if locked error should be thrown to user
2013 2017 currently_locked = False
2014 2018 # defines if new lock should be made, tri-state
2015 2019 make_lock = None
2016 2020 repo = self
2017 2021 user = User.get(user_id)
2018 2022
2019 2023 lock_info = repo.locked
2020 2024
2021 2025 if repo and (repo.enable_locking or not only_when_enabled):
2022 2026 if action == 'push':
2023 2027 # check if it's already locked !, if it is compare users
2024 2028 locked_by_user_id = lock_info[0]
2025 2029 if user.user_id == locked_by_user_id:
2026 2030 log.debug(
2027 2031 'Got `push` action from user %s, now unlocking', user)
2028 2032 # unlock if we have push from user who locked
2029 2033 make_lock = False
2030 2034 else:
2031 2035 # we're not the same user who locked, ban with
2032 2036 # code defined in settings (default is 423 HTTP Locked) !
2033 2037 log.debug('Repo %s is currently locked by %s', repo, user)
2034 2038 currently_locked = True
2035 2039 elif action == 'pull':
2036 2040 # [0] user [1] date
2037 2041 if lock_info[0] and lock_info[1]:
2038 2042 log.debug('Repo %s is currently locked by %s', repo, user)
2039 2043 currently_locked = True
2040 2044 else:
2041 2045 log.debug('Setting lock on repo %s by %s', repo, user)
2042 2046 make_lock = True
2043 2047
2044 2048 else:
2045 2049 log.debug('Repository %s do not have locking enabled', repo)
2046 2050
2047 2051 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2048 2052 make_lock, currently_locked, lock_info)
2049 2053
2050 2054 from rhodecode.lib.auth import HasRepoPermissionAny
2051 2055 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2052 2056 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2053 2057 # if we don't have at least write permission we cannot make a lock
2054 2058 log.debug('lock state reset back to FALSE due to lack '
2055 2059 'of at least read permission')
2056 2060 make_lock = False
2057 2061
2058 2062 return make_lock, currently_locked, lock_info
2059 2063
2060 2064 @property
2061 2065 def last_db_change(self):
2062 2066 return self.updated_on
2063 2067
2064 2068 @property
2065 2069 def clone_uri_hidden(self):
2066 2070 clone_uri = self.clone_uri
2067 2071 if clone_uri:
2068 2072 import urlobject
2069 2073 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2070 2074 if url_obj.password:
2071 2075 clone_uri = url_obj.with_password('*****')
2072 2076 return clone_uri
2073 2077
2074 2078 def clone_url(self, **override):
2075 2079 from rhodecode.model.settings import SettingsModel
2076 2080
2077 2081 uri_tmpl = None
2078 2082 if 'with_id' in override:
2079 2083 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2080 2084 del override['with_id']
2081 2085
2082 2086 if 'uri_tmpl' in override:
2083 2087 uri_tmpl = override['uri_tmpl']
2084 2088 del override['uri_tmpl']
2085 2089
2086 2090 # we didn't override our tmpl from **overrides
2087 2091 if not uri_tmpl:
2088 2092 rc_config = SettingsModel().get_all_settings(cache=True)
2089 2093 uri_tmpl = rc_config.get(
2090 2094 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2091 2095
2092 2096 request = get_current_request()
2093 2097 return get_clone_url(request=request,
2094 2098 uri_tmpl=uri_tmpl,
2095 2099 repo_name=self.repo_name,
2096 2100 repo_id=self.repo_id, **override)
2097 2101
2098 2102 def set_state(self, state):
2099 2103 self.repo_state = state
2100 2104 Session().add(self)
2101 2105 #==========================================================================
2102 2106 # SCM PROPERTIES
2103 2107 #==========================================================================
2104 2108
2105 2109 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2106 2110 return get_commit_safe(
2107 2111 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2108 2112
2109 2113 def get_changeset(self, rev=None, pre_load=None):
2110 2114 warnings.warn("Use get_commit", DeprecationWarning)
2111 2115 commit_id = None
2112 2116 commit_idx = None
2113 2117 if isinstance(rev, basestring):
2114 2118 commit_id = rev
2115 2119 else:
2116 2120 commit_idx = rev
2117 2121 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2118 2122 pre_load=pre_load)
2119 2123
2120 2124 def get_landing_commit(self):
2121 2125 """
2122 2126 Returns landing commit, or if that doesn't exist returns the tip
2123 2127 """
2124 2128 _rev_type, _rev = self.landing_rev
2125 2129 commit = self.get_commit(_rev)
2126 2130 if isinstance(commit, EmptyCommit):
2127 2131 return self.get_commit()
2128 2132 return commit
2129 2133
2130 2134 def update_commit_cache(self, cs_cache=None, config=None):
2131 2135 """
2132 2136 Update cache of last changeset for repository, keys should be::
2133 2137
2134 2138 short_id
2135 2139 raw_id
2136 2140 revision
2137 2141 parents
2138 2142 message
2139 2143 date
2140 2144 author
2141 2145
2142 2146 :param cs_cache:
2143 2147 """
2144 2148 from rhodecode.lib.vcs.backends.base import BaseChangeset
2145 2149 if cs_cache is None:
2146 2150 # use no-cache version here
2147 2151 scm_repo = self.scm_instance(cache=False, config=config)
2148 2152 if scm_repo:
2149 2153 cs_cache = scm_repo.get_commit(
2150 2154 pre_load=["author", "date", "message", "parents"])
2151 2155 else:
2152 2156 cs_cache = EmptyCommit()
2153 2157
2154 2158 if isinstance(cs_cache, BaseChangeset):
2155 2159 cs_cache = cs_cache.__json__()
2156 2160
2157 2161 def is_outdated(new_cs_cache):
2158 2162 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2159 2163 new_cs_cache['revision'] != self.changeset_cache['revision']):
2160 2164 return True
2161 2165 return False
2162 2166
2163 2167 # check if we have maybe already latest cached revision
2164 2168 if is_outdated(cs_cache) or not self.changeset_cache:
2165 2169 _default = datetime.datetime.fromtimestamp(0)
2166 2170 last_change = cs_cache.get('date') or _default
2167 2171 log.debug('updated repo %s with new cs cache %s',
2168 2172 self.repo_name, cs_cache)
2169 2173 self.updated_on = last_change
2170 2174 self.changeset_cache = cs_cache
2171 2175 Session().add(self)
2172 2176 Session().commit()
2173 2177 else:
2174 2178 log.debug('Skipping update_commit_cache for repo:`%s` '
2175 2179 'commit already with latest changes', self.repo_name)
2176 2180
2177 2181 @property
2178 2182 def tip(self):
2179 2183 return self.get_commit('tip')
2180 2184
2181 2185 @property
2182 2186 def author(self):
2183 2187 return self.tip.author
2184 2188
2185 2189 @property
2186 2190 def last_change(self):
2187 2191 return self.scm_instance().last_change
2188 2192
2189 2193 def get_comments(self, revisions=None):
2190 2194 """
2191 2195 Returns comments for this repository grouped by revisions
2192 2196
2193 2197 :param revisions: filter query by revisions only
2194 2198 """
2195 2199 cmts = ChangesetComment.query()\
2196 2200 .filter(ChangesetComment.repo == self)
2197 2201 if revisions:
2198 2202 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2199 2203 grouped = collections.defaultdict(list)
2200 2204 for cmt in cmts.all():
2201 2205 grouped[cmt.revision].append(cmt)
2202 2206 return grouped
2203 2207
2204 2208 def statuses(self, revisions=None):
2205 2209 """
2206 2210 Returns statuses for this repository
2207 2211
2208 2212 :param revisions: list of revisions to get statuses for
2209 2213 """
2210 2214 statuses = ChangesetStatus.query()\
2211 2215 .filter(ChangesetStatus.repo == self)\
2212 2216 .filter(ChangesetStatus.version == 0)
2213 2217
2214 2218 if revisions:
2215 2219 # Try doing the filtering in chunks to avoid hitting limits
2216 2220 size = 500
2217 2221 status_results = []
2218 2222 for chunk in xrange(0, len(revisions), size):
2219 2223 status_results += statuses.filter(
2220 2224 ChangesetStatus.revision.in_(
2221 2225 revisions[chunk: chunk+size])
2222 2226 ).all()
2223 2227 else:
2224 2228 status_results = statuses.all()
2225 2229
2226 2230 grouped = {}
2227 2231
2228 2232 # maybe we have open new pullrequest without a status?
2229 2233 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2230 2234 status_lbl = ChangesetStatus.get_status_lbl(stat)
2231 2235 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2232 2236 for rev in pr.revisions:
2233 2237 pr_id = pr.pull_request_id
2234 2238 pr_repo = pr.target_repo.repo_name
2235 2239 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2236 2240
2237 2241 for stat in status_results:
2238 2242 pr_id = pr_repo = None
2239 2243 if stat.pull_request:
2240 2244 pr_id = stat.pull_request.pull_request_id
2241 2245 pr_repo = stat.pull_request.target_repo.repo_name
2242 2246 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2243 2247 pr_id, pr_repo]
2244 2248 return grouped
2245 2249
2246 2250 # ==========================================================================
2247 2251 # SCM CACHE INSTANCE
2248 2252 # ==========================================================================
2249 2253
2250 2254 def scm_instance(self, **kwargs):
2251 2255 import rhodecode
2252 2256
2253 2257 # Passing a config will not hit the cache currently only used
2254 2258 # for repo2dbmapper
2255 2259 config = kwargs.pop('config', None)
2256 2260 cache = kwargs.pop('cache', None)
2257 2261 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2258 2262 # if cache is NOT defined use default global, else we have a full
2259 2263 # control over cache behaviour
2260 2264 if cache is None and full_cache and not config:
2261 2265 return self._get_instance_cached()
2262 2266 return self._get_instance(cache=bool(cache), config=config)
2263 2267
2264 2268 def _get_instance_cached(self):
2265 2269 @cache_region('long_term')
2266 2270 def _get_repo(cache_key):
2267 2271 return self._get_instance()
2268 2272
2269 2273 invalidator_context = CacheKey.repo_context_cache(
2270 2274 _get_repo, self.repo_name, None, thread_scoped=True)
2271 2275
2272 2276 with invalidator_context as context:
2273 2277 context.invalidate()
2274 2278 repo = context.compute()
2275 2279
2276 2280 return repo
2277 2281
2278 2282 def _get_instance(self, cache=True, config=None):
2279 2283 config = config or self._config
2280 2284 custom_wire = {
2281 2285 'cache': cache # controls the vcs.remote cache
2282 2286 }
2283 2287 repo = get_vcs_instance(
2284 2288 repo_path=safe_str(self.repo_full_path),
2285 2289 config=config,
2286 2290 with_wire=custom_wire,
2287 2291 create=False,
2288 2292 _vcs_alias=self.repo_type)
2289 2293
2290 2294 return repo
2291 2295
2292 2296 def __json__(self):
2293 2297 return {'landing_rev': self.landing_rev}
2294 2298
2295 2299 def get_dict(self):
2296 2300
2297 2301 # Since we transformed `repo_name` to a hybrid property, we need to
2298 2302 # keep compatibility with the code which uses `repo_name` field.
2299 2303
2300 2304 result = super(Repository, self).get_dict()
2301 2305 result['repo_name'] = result.pop('_repo_name', None)
2302 2306 return result
2303 2307
2304 2308
2305 2309 class RepoGroup(Base, BaseModel):
2306 2310 __tablename__ = 'groups'
2307 2311 __table_args__ = (
2308 2312 UniqueConstraint('group_name', 'group_parent_id'),
2309 2313 CheckConstraint('group_id != group_parent_id'),
2310 2314 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2311 2315 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2312 2316 )
2313 2317 __mapper_args__ = {'order_by': 'group_name'}
2314 2318
2315 2319 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2316 2320
2317 2321 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2318 2322 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2319 2323 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2320 2324 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2321 2325 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2322 2326 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2323 2327 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2324 2328 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2325 2329 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2326 2330
2327 2331 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2328 2332 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2329 2333 parent_group = relationship('RepoGroup', remote_side=group_id)
2330 2334 user = relationship('User')
2331 2335 integrations = relationship('Integration',
2332 2336 cascade="all, delete, delete-orphan")
2333 2337
2334 2338 def __init__(self, group_name='', parent_group=None):
2335 2339 self.group_name = group_name
2336 2340 self.parent_group = parent_group
2337 2341
2338 2342 def __unicode__(self):
2339 2343 return u"<%s('id:%s:%s')>" % (
2340 2344 self.__class__.__name__, self.group_id, self.group_name)
2341 2345
2342 2346 @hybrid_property
2343 2347 def description_safe(self):
2344 2348 from rhodecode.lib import helpers as h
2345 2349 return h.escape(self.group_description)
2346 2350
2347 2351 @classmethod
2348 2352 def _generate_choice(cls, repo_group):
2349 2353 from webhelpers.html import literal as _literal
2350 2354 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2351 2355 return repo_group.group_id, _name(repo_group.full_path_splitted)
2352 2356
2353 2357 @classmethod
2354 2358 def groups_choices(cls, groups=None, show_empty_group=True):
2355 2359 if not groups:
2356 2360 groups = cls.query().all()
2357 2361
2358 2362 repo_groups = []
2359 2363 if show_empty_group:
2360 2364 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2361 2365
2362 2366 repo_groups.extend([cls._generate_choice(x) for x in groups])
2363 2367
2364 2368 repo_groups = sorted(
2365 2369 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2366 2370 return repo_groups
2367 2371
2368 2372 @classmethod
2369 2373 def url_sep(cls):
2370 2374 return URL_SEP
2371 2375
2372 2376 @classmethod
2373 2377 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2374 2378 if case_insensitive:
2375 2379 gr = cls.query().filter(func.lower(cls.group_name)
2376 2380 == func.lower(group_name))
2377 2381 else:
2378 2382 gr = cls.query().filter(cls.group_name == group_name)
2379 2383 if cache:
2380 2384 name_key = _hash_key(group_name)
2381 2385 gr = gr.options(
2382 2386 FromCache("sql_cache_short", "get_group_%s" % name_key))
2383 2387 return gr.scalar()
2384 2388
2385 2389 @classmethod
2386 2390 def get_user_personal_repo_group(cls, user_id):
2387 2391 user = User.get(user_id)
2388 2392 if user.username == User.DEFAULT_USER:
2389 2393 return None
2390 2394
2391 2395 return cls.query()\
2392 2396 .filter(cls.personal == true()) \
2393 2397 .filter(cls.user == user).scalar()
2394 2398
2395 2399 @classmethod
2396 2400 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2397 2401 case_insensitive=True):
2398 2402 q = RepoGroup.query()
2399 2403
2400 2404 if not isinstance(user_id, Optional):
2401 2405 q = q.filter(RepoGroup.user_id == user_id)
2402 2406
2403 2407 if not isinstance(group_id, Optional):
2404 2408 q = q.filter(RepoGroup.group_parent_id == group_id)
2405 2409
2406 2410 if case_insensitive:
2407 2411 q = q.order_by(func.lower(RepoGroup.group_name))
2408 2412 else:
2409 2413 q = q.order_by(RepoGroup.group_name)
2410 2414 return q.all()
2411 2415
2412 2416 @property
2413 2417 def parents(self):
2414 2418 parents_recursion_limit = 10
2415 2419 groups = []
2416 2420 if self.parent_group is None:
2417 2421 return groups
2418 2422 cur_gr = self.parent_group
2419 2423 groups.insert(0, cur_gr)
2420 2424 cnt = 0
2421 2425 while 1:
2422 2426 cnt += 1
2423 2427 gr = getattr(cur_gr, 'parent_group', None)
2424 2428 cur_gr = cur_gr.parent_group
2425 2429 if gr is None:
2426 2430 break
2427 2431 if cnt == parents_recursion_limit:
2428 2432 # this will prevent accidental infinit loops
2429 2433 log.error(('more than %s parents found for group %s, stopping '
2430 2434 'recursive parent fetching' % (parents_recursion_limit, self)))
2431 2435 break
2432 2436
2433 2437 groups.insert(0, gr)
2434 2438 return groups
2435 2439
2436 2440 @property
2437 2441 def last_db_change(self):
2438 2442 return self.updated_on
2439 2443
2440 2444 @property
2441 2445 def children(self):
2442 2446 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2443 2447
2444 2448 @property
2445 2449 def name(self):
2446 2450 return self.group_name.split(RepoGroup.url_sep())[-1]
2447 2451
2448 2452 @property
2449 2453 def full_path(self):
2450 2454 return self.group_name
2451 2455
2452 2456 @property
2453 2457 def full_path_splitted(self):
2454 2458 return self.group_name.split(RepoGroup.url_sep())
2455 2459
2456 2460 @property
2457 2461 def repositories(self):
2458 2462 return Repository.query()\
2459 2463 .filter(Repository.group == self)\
2460 2464 .order_by(Repository.repo_name)
2461 2465
2462 2466 @property
2463 2467 def repositories_recursive_count(self):
2464 2468 cnt = self.repositories.count()
2465 2469
2466 2470 def children_count(group):
2467 2471 cnt = 0
2468 2472 for child in group.children:
2469 2473 cnt += child.repositories.count()
2470 2474 cnt += children_count(child)
2471 2475 return cnt
2472 2476
2473 2477 return cnt + children_count(self)
2474 2478
2475 2479 def _recursive_objects(self, include_repos=True):
2476 2480 all_ = []
2477 2481
2478 2482 def _get_members(root_gr):
2479 2483 if include_repos:
2480 2484 for r in root_gr.repositories:
2481 2485 all_.append(r)
2482 2486 childs = root_gr.children.all()
2483 2487 if childs:
2484 2488 for gr in childs:
2485 2489 all_.append(gr)
2486 2490 _get_members(gr)
2487 2491
2488 2492 _get_members(self)
2489 2493 return [self] + all_
2490 2494
2491 2495 def recursive_groups_and_repos(self):
2492 2496 """
2493 2497 Recursive return all groups, with repositories in those groups
2494 2498 """
2495 2499 return self._recursive_objects()
2496 2500
2497 2501 def recursive_groups(self):
2498 2502 """
2499 2503 Returns all children groups for this group including children of children
2500 2504 """
2501 2505 return self._recursive_objects(include_repos=False)
2502 2506
2503 2507 def get_new_name(self, group_name):
2504 2508 """
2505 2509 returns new full group name based on parent and new name
2506 2510
2507 2511 :param group_name:
2508 2512 """
2509 2513 path_prefix = (self.parent_group.full_path_splitted if
2510 2514 self.parent_group else [])
2511 2515 return RepoGroup.url_sep().join(path_prefix + [group_name])
2512 2516
2513 2517 def permissions(self, with_admins=True, with_owner=True):
2514 2518 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2515 2519 q = q.options(joinedload(UserRepoGroupToPerm.group),
2516 2520 joinedload(UserRepoGroupToPerm.user),
2517 2521 joinedload(UserRepoGroupToPerm.permission),)
2518 2522
2519 2523 # get owners and admins and permissions. We do a trick of re-writing
2520 2524 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2521 2525 # has a global reference and changing one object propagates to all
2522 2526 # others. This means if admin is also an owner admin_row that change
2523 2527 # would propagate to both objects
2524 2528 perm_rows = []
2525 2529 for _usr in q.all():
2526 2530 usr = AttributeDict(_usr.user.get_dict())
2527 2531 usr.permission = _usr.permission.permission_name
2528 2532 perm_rows.append(usr)
2529 2533
2530 2534 # filter the perm rows by 'default' first and then sort them by
2531 2535 # admin,write,read,none permissions sorted again alphabetically in
2532 2536 # each group
2533 2537 perm_rows = sorted(perm_rows, key=display_user_sort)
2534 2538
2535 2539 _admin_perm = 'group.admin'
2536 2540 owner_row = []
2537 2541 if with_owner:
2538 2542 usr = AttributeDict(self.user.get_dict())
2539 2543 usr.owner_row = True
2540 2544 usr.permission = _admin_perm
2541 2545 owner_row.append(usr)
2542 2546
2543 2547 super_admin_rows = []
2544 2548 if with_admins:
2545 2549 for usr in User.get_all_super_admins():
2546 2550 # if this admin is also owner, don't double the record
2547 2551 if usr.user_id == owner_row[0].user_id:
2548 2552 owner_row[0].admin_row = True
2549 2553 else:
2550 2554 usr = AttributeDict(usr.get_dict())
2551 2555 usr.admin_row = True
2552 2556 usr.permission = _admin_perm
2553 2557 super_admin_rows.append(usr)
2554 2558
2555 2559 return super_admin_rows + owner_row + perm_rows
2556 2560
2557 2561 def permission_user_groups(self):
2558 2562 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2559 2563 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2560 2564 joinedload(UserGroupRepoGroupToPerm.users_group),
2561 2565 joinedload(UserGroupRepoGroupToPerm.permission),)
2562 2566
2563 2567 perm_rows = []
2564 2568 for _user_group in q.all():
2565 2569 usr = AttributeDict(_user_group.users_group.get_dict())
2566 2570 usr.permission = _user_group.permission.permission_name
2567 2571 perm_rows.append(usr)
2568 2572
2569 2573 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2570 2574 return perm_rows
2571 2575
2572 2576 def get_api_data(self):
2573 2577 """
2574 2578 Common function for generating api data
2575 2579
2576 2580 """
2577 2581 group = self
2578 2582 data = {
2579 2583 'group_id': group.group_id,
2580 2584 'group_name': group.group_name,
2581 2585 'group_description': group.description_safe,
2582 2586 'parent_group': group.parent_group.group_name if group.parent_group else None,
2583 2587 'repositories': [x.repo_name for x in group.repositories],
2584 2588 'owner': group.user.username,
2585 2589 }
2586 2590 return data
2587 2591
2588 2592
2589 2593 class Permission(Base, BaseModel):
2590 2594 __tablename__ = 'permissions'
2591 2595 __table_args__ = (
2592 2596 Index('p_perm_name_idx', 'permission_name'),
2593 2597 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 2598 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2595 2599 )
2596 2600 PERMS = [
2597 2601 ('hg.admin', _('RhodeCode Super Administrator')),
2598 2602
2599 2603 ('repository.none', _('Repository no access')),
2600 2604 ('repository.read', _('Repository read access')),
2601 2605 ('repository.write', _('Repository write access')),
2602 2606 ('repository.admin', _('Repository admin access')),
2603 2607
2604 2608 ('group.none', _('Repository group no access')),
2605 2609 ('group.read', _('Repository group read access')),
2606 2610 ('group.write', _('Repository group write access')),
2607 2611 ('group.admin', _('Repository group admin access')),
2608 2612
2609 2613 ('usergroup.none', _('User group no access')),
2610 2614 ('usergroup.read', _('User group read access')),
2611 2615 ('usergroup.write', _('User group write access')),
2612 2616 ('usergroup.admin', _('User group admin access')),
2613 2617
2614 2618 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2615 2619 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2616 2620
2617 2621 ('hg.usergroup.create.false', _('User Group creation disabled')),
2618 2622 ('hg.usergroup.create.true', _('User Group creation enabled')),
2619 2623
2620 2624 ('hg.create.none', _('Repository creation disabled')),
2621 2625 ('hg.create.repository', _('Repository creation enabled')),
2622 2626 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2623 2627 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2624 2628
2625 2629 ('hg.fork.none', _('Repository forking disabled')),
2626 2630 ('hg.fork.repository', _('Repository forking enabled')),
2627 2631
2628 2632 ('hg.register.none', _('Registration disabled')),
2629 2633 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2630 2634 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2631 2635
2632 2636 ('hg.password_reset.enabled', _('Password reset enabled')),
2633 2637 ('hg.password_reset.hidden', _('Password reset hidden')),
2634 2638 ('hg.password_reset.disabled', _('Password reset disabled')),
2635 2639
2636 2640 ('hg.extern_activate.manual', _('Manual activation of external account')),
2637 2641 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2638 2642
2639 2643 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2640 2644 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2641 2645 ]
2642 2646
2643 2647 # definition of system default permissions for DEFAULT user
2644 2648 DEFAULT_USER_PERMISSIONS = [
2645 2649 'repository.read',
2646 2650 'group.read',
2647 2651 'usergroup.read',
2648 2652 'hg.create.repository',
2649 2653 'hg.repogroup.create.false',
2650 2654 'hg.usergroup.create.false',
2651 2655 'hg.create.write_on_repogroup.true',
2652 2656 'hg.fork.repository',
2653 2657 'hg.register.manual_activate',
2654 2658 'hg.password_reset.enabled',
2655 2659 'hg.extern_activate.auto',
2656 2660 'hg.inherit_default_perms.true',
2657 2661 ]
2658 2662
2659 2663 # defines which permissions are more important higher the more important
2660 2664 # Weight defines which permissions are more important.
2661 2665 # The higher number the more important.
2662 2666 PERM_WEIGHTS = {
2663 2667 'repository.none': 0,
2664 2668 'repository.read': 1,
2665 2669 'repository.write': 3,
2666 2670 'repository.admin': 4,
2667 2671
2668 2672 'group.none': 0,
2669 2673 'group.read': 1,
2670 2674 'group.write': 3,
2671 2675 'group.admin': 4,
2672 2676
2673 2677 'usergroup.none': 0,
2674 2678 'usergroup.read': 1,
2675 2679 'usergroup.write': 3,
2676 2680 'usergroup.admin': 4,
2677 2681
2678 2682 'hg.repogroup.create.false': 0,
2679 2683 'hg.repogroup.create.true': 1,
2680 2684
2681 2685 'hg.usergroup.create.false': 0,
2682 2686 'hg.usergroup.create.true': 1,
2683 2687
2684 2688 'hg.fork.none': 0,
2685 2689 'hg.fork.repository': 1,
2686 2690 'hg.create.none': 0,
2687 2691 'hg.create.repository': 1
2688 2692 }
2689 2693
2690 2694 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2691 2695 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2692 2696 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2693 2697
2694 2698 def __unicode__(self):
2695 2699 return u"<%s('%s:%s')>" % (
2696 2700 self.__class__.__name__, self.permission_id, self.permission_name
2697 2701 )
2698 2702
2699 2703 @classmethod
2700 2704 def get_by_key(cls, key):
2701 2705 return cls.query().filter(cls.permission_name == key).scalar()
2702 2706
2703 2707 @classmethod
2704 2708 def get_default_repo_perms(cls, user_id, repo_id=None):
2705 2709 q = Session().query(UserRepoToPerm, Repository, Permission)\
2706 2710 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2707 2711 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2708 2712 .filter(UserRepoToPerm.user_id == user_id)
2709 2713 if repo_id:
2710 2714 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2711 2715 return q.all()
2712 2716
2713 2717 @classmethod
2714 2718 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2715 2719 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2716 2720 .join(
2717 2721 Permission,
2718 2722 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2719 2723 .join(
2720 2724 Repository,
2721 2725 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2722 2726 .join(
2723 2727 UserGroup,
2724 2728 UserGroupRepoToPerm.users_group_id ==
2725 2729 UserGroup.users_group_id)\
2726 2730 .join(
2727 2731 UserGroupMember,
2728 2732 UserGroupRepoToPerm.users_group_id ==
2729 2733 UserGroupMember.users_group_id)\
2730 2734 .filter(
2731 2735 UserGroupMember.user_id == user_id,
2732 2736 UserGroup.users_group_active == true())
2733 2737 if repo_id:
2734 2738 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2735 2739 return q.all()
2736 2740
2737 2741 @classmethod
2738 2742 def get_default_group_perms(cls, user_id, repo_group_id=None):
2739 2743 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2740 2744 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2741 2745 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2742 2746 .filter(UserRepoGroupToPerm.user_id == user_id)
2743 2747 if repo_group_id:
2744 2748 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2745 2749 return q.all()
2746 2750
2747 2751 @classmethod
2748 2752 def get_default_group_perms_from_user_group(
2749 2753 cls, user_id, repo_group_id=None):
2750 2754 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2751 2755 .join(
2752 2756 Permission,
2753 2757 UserGroupRepoGroupToPerm.permission_id ==
2754 2758 Permission.permission_id)\
2755 2759 .join(
2756 2760 RepoGroup,
2757 2761 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2758 2762 .join(
2759 2763 UserGroup,
2760 2764 UserGroupRepoGroupToPerm.users_group_id ==
2761 2765 UserGroup.users_group_id)\
2762 2766 .join(
2763 2767 UserGroupMember,
2764 2768 UserGroupRepoGroupToPerm.users_group_id ==
2765 2769 UserGroupMember.users_group_id)\
2766 2770 .filter(
2767 2771 UserGroupMember.user_id == user_id,
2768 2772 UserGroup.users_group_active == true())
2769 2773 if repo_group_id:
2770 2774 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2771 2775 return q.all()
2772 2776
2773 2777 @classmethod
2774 2778 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2775 2779 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2776 2780 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2777 2781 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2778 2782 .filter(UserUserGroupToPerm.user_id == user_id)
2779 2783 if user_group_id:
2780 2784 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2781 2785 return q.all()
2782 2786
2783 2787 @classmethod
2784 2788 def get_default_user_group_perms_from_user_group(
2785 2789 cls, user_id, user_group_id=None):
2786 2790 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2787 2791 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2788 2792 .join(
2789 2793 Permission,
2790 2794 UserGroupUserGroupToPerm.permission_id ==
2791 2795 Permission.permission_id)\
2792 2796 .join(
2793 2797 TargetUserGroup,
2794 2798 UserGroupUserGroupToPerm.target_user_group_id ==
2795 2799 TargetUserGroup.users_group_id)\
2796 2800 .join(
2797 2801 UserGroup,
2798 2802 UserGroupUserGroupToPerm.user_group_id ==
2799 2803 UserGroup.users_group_id)\
2800 2804 .join(
2801 2805 UserGroupMember,
2802 2806 UserGroupUserGroupToPerm.user_group_id ==
2803 2807 UserGroupMember.users_group_id)\
2804 2808 .filter(
2805 2809 UserGroupMember.user_id == user_id,
2806 2810 UserGroup.users_group_active == true())
2807 2811 if user_group_id:
2808 2812 q = q.filter(
2809 2813 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2810 2814
2811 2815 return q.all()
2812 2816
2813 2817
2814 2818 class UserRepoToPerm(Base, BaseModel):
2815 2819 __tablename__ = 'repo_to_perm'
2816 2820 __table_args__ = (
2817 2821 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2818 2822 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2819 2823 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2820 2824 )
2821 2825 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2822 2826 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2823 2827 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2824 2828 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2825 2829
2826 2830 user = relationship('User')
2827 2831 repository = relationship('Repository')
2828 2832 permission = relationship('Permission')
2829 2833
2830 2834 @classmethod
2831 2835 def create(cls, user, repository, permission):
2832 2836 n = cls()
2833 2837 n.user = user
2834 2838 n.repository = repository
2835 2839 n.permission = permission
2836 2840 Session().add(n)
2837 2841 return n
2838 2842
2839 2843 def __unicode__(self):
2840 2844 return u'<%s => %s >' % (self.user, self.repository)
2841 2845
2842 2846
2843 2847 class UserUserGroupToPerm(Base, BaseModel):
2844 2848 __tablename__ = 'user_user_group_to_perm'
2845 2849 __table_args__ = (
2846 2850 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2847 2851 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2848 2852 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2849 2853 )
2850 2854 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2851 2855 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2852 2856 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2853 2857 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2854 2858
2855 2859 user = relationship('User')
2856 2860 user_group = relationship('UserGroup')
2857 2861 permission = relationship('Permission')
2858 2862
2859 2863 @classmethod
2860 2864 def create(cls, user, user_group, permission):
2861 2865 n = cls()
2862 2866 n.user = user
2863 2867 n.user_group = user_group
2864 2868 n.permission = permission
2865 2869 Session().add(n)
2866 2870 return n
2867 2871
2868 2872 def __unicode__(self):
2869 2873 return u'<%s => %s >' % (self.user, self.user_group)
2870 2874
2871 2875
2872 2876 class UserToPerm(Base, BaseModel):
2873 2877 __tablename__ = 'user_to_perm'
2874 2878 __table_args__ = (
2875 2879 UniqueConstraint('user_id', 'permission_id'),
2876 2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2877 2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2878 2882 )
2879 2883 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2880 2884 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2881 2885 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2882 2886
2883 2887 user = relationship('User')
2884 2888 permission = relationship('Permission', lazy='joined')
2885 2889
2886 2890 def __unicode__(self):
2887 2891 return u'<%s => %s >' % (self.user, self.permission)
2888 2892
2889 2893
2890 2894 class UserGroupRepoToPerm(Base, BaseModel):
2891 2895 __tablename__ = 'users_group_repo_to_perm'
2892 2896 __table_args__ = (
2893 2897 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2894 2898 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 2899 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2896 2900 )
2897 2901 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2898 2902 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2899 2903 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2900 2904 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2901 2905
2902 2906 users_group = relationship('UserGroup')
2903 2907 permission = relationship('Permission')
2904 2908 repository = relationship('Repository')
2905 2909
2906 2910 @classmethod
2907 2911 def create(cls, users_group, repository, permission):
2908 2912 n = cls()
2909 2913 n.users_group = users_group
2910 2914 n.repository = repository
2911 2915 n.permission = permission
2912 2916 Session().add(n)
2913 2917 return n
2914 2918
2915 2919 def __unicode__(self):
2916 2920 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2917 2921
2918 2922
2919 2923 class UserGroupUserGroupToPerm(Base, BaseModel):
2920 2924 __tablename__ = 'user_group_user_group_to_perm'
2921 2925 __table_args__ = (
2922 2926 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2923 2927 CheckConstraint('target_user_group_id != user_group_id'),
2924 2928 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2925 2929 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2926 2930 )
2927 2931 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2928 2932 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2929 2933 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2930 2934 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2931 2935
2932 2936 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2933 2937 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2934 2938 permission = relationship('Permission')
2935 2939
2936 2940 @classmethod
2937 2941 def create(cls, target_user_group, user_group, permission):
2938 2942 n = cls()
2939 2943 n.target_user_group = target_user_group
2940 2944 n.user_group = user_group
2941 2945 n.permission = permission
2942 2946 Session().add(n)
2943 2947 return n
2944 2948
2945 2949 def __unicode__(self):
2946 2950 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2947 2951
2948 2952
2949 2953 class UserGroupToPerm(Base, BaseModel):
2950 2954 __tablename__ = 'users_group_to_perm'
2951 2955 __table_args__ = (
2952 2956 UniqueConstraint('users_group_id', 'permission_id',),
2953 2957 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2954 2958 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2955 2959 )
2956 2960 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2957 2961 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2958 2962 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2959 2963
2960 2964 users_group = relationship('UserGroup')
2961 2965 permission = relationship('Permission')
2962 2966
2963 2967
2964 2968 class UserRepoGroupToPerm(Base, BaseModel):
2965 2969 __tablename__ = 'user_repo_group_to_perm'
2966 2970 __table_args__ = (
2967 2971 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2968 2972 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2969 2973 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2970 2974 )
2971 2975
2972 2976 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2973 2977 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2974 2978 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2975 2979 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2976 2980
2977 2981 user = relationship('User')
2978 2982 group = relationship('RepoGroup')
2979 2983 permission = relationship('Permission')
2980 2984
2981 2985 @classmethod
2982 2986 def create(cls, user, repository_group, permission):
2983 2987 n = cls()
2984 2988 n.user = user
2985 2989 n.group = repository_group
2986 2990 n.permission = permission
2987 2991 Session().add(n)
2988 2992 return n
2989 2993
2990 2994
2991 2995 class UserGroupRepoGroupToPerm(Base, BaseModel):
2992 2996 __tablename__ = 'users_group_repo_group_to_perm'
2993 2997 __table_args__ = (
2994 2998 UniqueConstraint('users_group_id', 'group_id'),
2995 2999 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2996 3000 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2997 3001 )
2998 3002
2999 3003 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)
3000 3004 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3001 3005 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3002 3006 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3003 3007
3004 3008 users_group = relationship('UserGroup')
3005 3009 permission = relationship('Permission')
3006 3010 group = relationship('RepoGroup')
3007 3011
3008 3012 @classmethod
3009 3013 def create(cls, user_group, repository_group, permission):
3010 3014 n = cls()
3011 3015 n.users_group = user_group
3012 3016 n.group = repository_group
3013 3017 n.permission = permission
3014 3018 Session().add(n)
3015 3019 return n
3016 3020
3017 3021 def __unicode__(self):
3018 3022 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3019 3023
3020 3024
3021 3025 class Statistics(Base, BaseModel):
3022 3026 __tablename__ = 'statistics'
3023 3027 __table_args__ = (
3024 3028 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3025 3029 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3026 3030 )
3027 3031 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3028 3032 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3029 3033 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3030 3034 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3031 3035 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3032 3036 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3033 3037
3034 3038 repository = relationship('Repository', single_parent=True)
3035 3039
3036 3040
3037 3041 class UserFollowing(Base, BaseModel):
3038 3042 __tablename__ = 'user_followings'
3039 3043 __table_args__ = (
3040 3044 UniqueConstraint('user_id', 'follows_repository_id'),
3041 3045 UniqueConstraint('user_id', 'follows_user_id'),
3042 3046 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3043 3047 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3044 3048 )
3045 3049
3046 3050 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3047 3051 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3048 3052 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3049 3053 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3050 3054 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3051 3055
3052 3056 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3053 3057
3054 3058 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3055 3059 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3056 3060
3057 3061 @classmethod
3058 3062 def get_repo_followers(cls, repo_id):
3059 3063 return cls.query().filter(cls.follows_repo_id == repo_id)
3060 3064
3061 3065
3062 3066 class CacheKey(Base, BaseModel):
3063 3067 __tablename__ = 'cache_invalidation'
3064 3068 __table_args__ = (
3065 3069 UniqueConstraint('cache_key'),
3066 3070 Index('key_idx', 'cache_key'),
3067 3071 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3068 3072 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3069 3073 )
3070 3074 CACHE_TYPE_ATOM = 'ATOM'
3071 3075 CACHE_TYPE_RSS = 'RSS'
3072 3076 CACHE_TYPE_README = 'README'
3073 3077
3074 3078 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3075 3079 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3076 3080 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3077 3081 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3078 3082
3079 3083 def __init__(self, cache_key, cache_args=''):
3080 3084 self.cache_key = cache_key
3081 3085 self.cache_args = cache_args
3082 3086 self.cache_active = False
3083 3087
3084 3088 def __unicode__(self):
3085 3089 return u"<%s('%s:%s[%s]')>" % (
3086 3090 self.__class__.__name__,
3087 3091 self.cache_id, self.cache_key, self.cache_active)
3088 3092
3089 3093 def _cache_key_partition(self):
3090 3094 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3091 3095 return prefix, repo_name, suffix
3092 3096
3093 3097 def get_prefix(self):
3094 3098 """
3095 3099 Try to extract prefix from existing cache key. The key could consist
3096 3100 of prefix, repo_name, suffix
3097 3101 """
3098 3102 # this returns prefix, repo_name, suffix
3099 3103 return self._cache_key_partition()[0]
3100 3104
3101 3105 def get_suffix(self):
3102 3106 """
3103 3107 get suffix that might have been used in _get_cache_key to
3104 3108 generate self.cache_key. Only used for informational purposes
3105 3109 in repo_edit.mako.
3106 3110 """
3107 3111 # prefix, repo_name, suffix
3108 3112 return self._cache_key_partition()[2]
3109 3113
3110 3114 @classmethod
3111 3115 def delete_all_cache(cls):
3112 3116 """
3113 3117 Delete all cache keys from database.
3114 3118 Should only be run when all instances are down and all entries
3115 3119 thus stale.
3116 3120 """
3117 3121 cls.query().delete()
3118 3122 Session().commit()
3119 3123
3120 3124 @classmethod
3121 3125 def get_cache_key(cls, repo_name, cache_type):
3122 3126 """
3123 3127
3124 3128 Generate a cache key for this process of RhodeCode instance.
3125 3129 Prefix most likely will be process id or maybe explicitly set
3126 3130 instance_id from .ini file.
3127 3131 """
3128 3132 import rhodecode
3129 3133 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3130 3134
3131 3135 repo_as_unicode = safe_unicode(repo_name)
3132 3136 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3133 3137 if cache_type else repo_as_unicode
3134 3138
3135 3139 return u'{}{}'.format(prefix, key)
3136 3140
3137 3141 @classmethod
3138 3142 def set_invalidate(cls, repo_name, delete=False):
3139 3143 """
3140 3144 Mark all caches of a repo as invalid in the database.
3141 3145 """
3142 3146
3143 3147 try:
3144 3148 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3145 3149 if delete:
3146 3150 log.debug('cache objects deleted for repo %s',
3147 3151 safe_str(repo_name))
3148 3152 qry.delete()
3149 3153 else:
3150 3154 log.debug('cache objects marked as invalid for repo %s',
3151 3155 safe_str(repo_name))
3152 3156 qry.update({"cache_active": False})
3153 3157
3154 3158 Session().commit()
3155 3159 except Exception:
3156 3160 log.exception(
3157 3161 'Cache key invalidation failed for repository %s',
3158 3162 safe_str(repo_name))
3159 3163 Session().rollback()
3160 3164
3161 3165 @classmethod
3162 3166 def get_active_cache(cls, cache_key):
3163 3167 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3164 3168 if inv_obj:
3165 3169 return inv_obj
3166 3170 return None
3167 3171
3168 3172 @classmethod
3169 3173 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3170 3174 thread_scoped=False):
3171 3175 """
3172 3176 @cache_region('long_term')
3173 3177 def _heavy_calculation(cache_key):
3174 3178 return 'result'
3175 3179
3176 3180 cache_context = CacheKey.repo_context_cache(
3177 3181 _heavy_calculation, repo_name, cache_type)
3178 3182
3179 3183 with cache_context as context:
3180 3184 context.invalidate()
3181 3185 computed = context.compute()
3182 3186
3183 3187 assert computed == 'result'
3184 3188 """
3185 3189 from rhodecode.lib import caches
3186 3190 return caches.InvalidationContext(
3187 3191 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3188 3192
3189 3193
3190 3194 class ChangesetComment(Base, BaseModel):
3191 3195 __tablename__ = 'changeset_comments'
3192 3196 __table_args__ = (
3193 3197 Index('cc_revision_idx', 'revision'),
3194 3198 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3195 3199 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3196 3200 )
3197 3201
3198 3202 COMMENT_OUTDATED = u'comment_outdated'
3199 3203 COMMENT_TYPE_NOTE = u'note'
3200 3204 COMMENT_TYPE_TODO = u'todo'
3201 3205 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3202 3206
3203 3207 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3204 3208 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3205 3209 revision = Column('revision', String(40), nullable=True)
3206 3210 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3207 3211 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3208 3212 line_no = Column('line_no', Unicode(10), nullable=True)
3209 3213 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3210 3214 f_path = Column('f_path', Unicode(1000), nullable=True)
3211 3215 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3212 3216 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3213 3217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3214 3218 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3215 3219 renderer = Column('renderer', Unicode(64), nullable=True)
3216 3220 display_state = Column('display_state', Unicode(128), nullable=True)
3217 3221
3218 3222 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3219 3223 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3220 3224 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3221 3225 author = relationship('User', lazy='joined')
3222 3226 repo = relationship('Repository')
3223 3227 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3224 3228 pull_request = relationship('PullRequest', lazy='joined')
3225 3229 pull_request_version = relationship('PullRequestVersion')
3226 3230
3227 3231 @classmethod
3228 3232 def get_users(cls, revision=None, pull_request_id=None):
3229 3233 """
3230 3234 Returns user associated with this ChangesetComment. ie those
3231 3235 who actually commented
3232 3236
3233 3237 :param cls:
3234 3238 :param revision:
3235 3239 """
3236 3240 q = Session().query(User)\
3237 3241 .join(ChangesetComment.author)
3238 3242 if revision:
3239 3243 q = q.filter(cls.revision == revision)
3240 3244 elif pull_request_id:
3241 3245 q = q.filter(cls.pull_request_id == pull_request_id)
3242 3246 return q.all()
3243 3247
3244 3248 @classmethod
3245 3249 def get_index_from_version(cls, pr_version, versions):
3246 3250 num_versions = [x.pull_request_version_id for x in versions]
3247 3251 try:
3248 3252 return num_versions.index(pr_version) +1
3249 3253 except (IndexError, ValueError):
3250 3254 return
3251 3255
3252 3256 @property
3253 3257 def outdated(self):
3254 3258 return self.display_state == self.COMMENT_OUTDATED
3255 3259
3256 3260 def outdated_at_version(self, version):
3257 3261 """
3258 3262 Checks if comment is outdated for given pull request version
3259 3263 """
3260 3264 return self.outdated and self.pull_request_version_id != version
3261 3265
3262 3266 def older_than_version(self, version):
3263 3267 """
3264 3268 Checks if comment is made from previous version than given
3265 3269 """
3266 3270 if version is None:
3267 3271 return self.pull_request_version_id is not None
3268 3272
3269 3273 return self.pull_request_version_id < version
3270 3274
3271 3275 @property
3272 3276 def resolved(self):
3273 3277 return self.resolved_by[0] if self.resolved_by else None
3274 3278
3275 3279 @property
3276 3280 def is_todo(self):
3277 3281 return self.comment_type == self.COMMENT_TYPE_TODO
3278 3282
3279 3283 @property
3280 3284 def is_inline(self):
3281 3285 return self.line_no and self.f_path
3282 3286
3283 3287 def get_index_version(self, versions):
3284 3288 return self.get_index_from_version(
3285 3289 self.pull_request_version_id, versions)
3286 3290
3287 3291 def __repr__(self):
3288 3292 if self.comment_id:
3289 3293 return '<DB:Comment #%s>' % self.comment_id
3290 3294 else:
3291 3295 return '<DB:Comment at %#x>' % id(self)
3292 3296
3293 3297 def get_api_data(self):
3294 3298 comment = self
3295 3299 data = {
3296 3300 'comment_id': comment.comment_id,
3297 3301 'comment_type': comment.comment_type,
3298 3302 'comment_text': comment.text,
3299 3303 'comment_status': comment.status_change,
3300 3304 'comment_f_path': comment.f_path,
3301 3305 'comment_lineno': comment.line_no,
3302 3306 'comment_author': comment.author,
3303 3307 'comment_created_on': comment.created_on
3304 3308 }
3305 3309 return data
3306 3310
3307 3311 def __json__(self):
3308 3312 data = dict()
3309 3313 data.update(self.get_api_data())
3310 3314 return data
3311 3315
3312 3316
3313 3317 class ChangesetStatus(Base, BaseModel):
3314 3318 __tablename__ = 'changeset_statuses'
3315 3319 __table_args__ = (
3316 3320 Index('cs_revision_idx', 'revision'),
3317 3321 Index('cs_version_idx', 'version'),
3318 3322 UniqueConstraint('repo_id', 'revision', 'version'),
3319 3323 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3320 3324 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3321 3325 )
3322 3326 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3323 3327 STATUS_APPROVED = 'approved'
3324 3328 STATUS_REJECTED = 'rejected'
3325 3329 STATUS_UNDER_REVIEW = 'under_review'
3326 3330
3327 3331 STATUSES = [
3328 3332 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3329 3333 (STATUS_APPROVED, _("Approved")),
3330 3334 (STATUS_REJECTED, _("Rejected")),
3331 3335 (STATUS_UNDER_REVIEW, _("Under Review")),
3332 3336 ]
3333 3337
3334 3338 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3335 3339 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3336 3340 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3337 3341 revision = Column('revision', String(40), nullable=False)
3338 3342 status = Column('status', String(128), nullable=False, default=DEFAULT)
3339 3343 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3340 3344 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3341 3345 version = Column('version', Integer(), nullable=False, default=0)
3342 3346 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3343 3347
3344 3348 author = relationship('User', lazy='joined')
3345 3349 repo = relationship('Repository')
3346 3350 comment = relationship('ChangesetComment', lazy='joined')
3347 3351 pull_request = relationship('PullRequest', lazy='joined')
3348 3352
3349 3353 def __unicode__(self):
3350 3354 return u"<%s('%s[v%s]:%s')>" % (
3351 3355 self.__class__.__name__,
3352 3356 self.status, self.version, self.author
3353 3357 )
3354 3358
3355 3359 @classmethod
3356 3360 def get_status_lbl(cls, value):
3357 3361 return dict(cls.STATUSES).get(value)
3358 3362
3359 3363 @property
3360 3364 def status_lbl(self):
3361 3365 return ChangesetStatus.get_status_lbl(self.status)
3362 3366
3363 3367 def get_api_data(self):
3364 3368 status = self
3365 3369 data = {
3366 3370 'status_id': status.changeset_status_id,
3367 3371 'status': status.status,
3368 3372 }
3369 3373 return data
3370 3374
3371 3375 def __json__(self):
3372 3376 data = dict()
3373 3377 data.update(self.get_api_data())
3374 3378 return data
3375 3379
3376 3380
3377 3381 class _PullRequestBase(BaseModel):
3378 3382 """
3379 3383 Common attributes of pull request and version entries.
3380 3384 """
3381 3385
3382 3386 # .status values
3383 3387 STATUS_NEW = u'new'
3384 3388 STATUS_OPEN = u'open'
3385 3389 STATUS_CLOSED = u'closed'
3386 3390
3387 3391 title = Column('title', Unicode(255), nullable=True)
3388 3392 description = Column(
3389 3393 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3390 3394 nullable=True)
3391 3395 # new/open/closed status of pull request (not approve/reject/etc)
3392 3396 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3393 3397 created_on = Column(
3394 3398 'created_on', DateTime(timezone=False), nullable=False,
3395 3399 default=datetime.datetime.now)
3396 3400 updated_on = Column(
3397 3401 'updated_on', DateTime(timezone=False), nullable=False,
3398 3402 default=datetime.datetime.now)
3399 3403
3400 3404 @declared_attr
3401 3405 def user_id(cls):
3402 3406 return Column(
3403 3407 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3404 3408 unique=None)
3405 3409
3406 3410 # 500 revisions max
3407 3411 _revisions = Column(
3408 3412 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3409 3413
3410 3414 @declared_attr
3411 3415 def source_repo_id(cls):
3412 3416 # TODO: dan: rename column to source_repo_id
3413 3417 return Column(
3414 3418 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3415 3419 nullable=False)
3416 3420
3417 3421 source_ref = Column('org_ref', Unicode(255), nullable=False)
3418 3422
3419 3423 @declared_attr
3420 3424 def target_repo_id(cls):
3421 3425 # TODO: dan: rename column to target_repo_id
3422 3426 return Column(
3423 3427 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3424 3428 nullable=False)
3425 3429
3426 3430 target_ref = Column('other_ref', Unicode(255), nullable=False)
3427 3431 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3428 3432
3429 3433 # TODO: dan: rename column to last_merge_source_rev
3430 3434 _last_merge_source_rev = Column(
3431 3435 'last_merge_org_rev', String(40), nullable=True)
3432 3436 # TODO: dan: rename column to last_merge_target_rev
3433 3437 _last_merge_target_rev = Column(
3434 3438 'last_merge_other_rev', String(40), nullable=True)
3435 3439 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3436 3440 merge_rev = Column('merge_rev', String(40), nullable=True)
3437 3441
3438 3442 reviewer_data = Column(
3439 3443 'reviewer_data_json', MutationObj.as_mutable(
3440 3444 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3441 3445
3442 3446 @property
3443 3447 def reviewer_data_json(self):
3444 3448 return json.dumps(self.reviewer_data)
3445 3449
3446 3450 @hybrid_property
3447 3451 def description_safe(self):
3448 3452 from rhodecode.lib import helpers as h
3449 3453 return h.escape(self.description)
3450 3454
3451 3455 @hybrid_property
3452 3456 def revisions(self):
3453 3457 return self._revisions.split(':') if self._revisions else []
3454 3458
3455 3459 @revisions.setter
3456 3460 def revisions(self, val):
3457 3461 self._revisions = ':'.join(val)
3458 3462
3459 3463 @hybrid_property
3460 3464 def last_merge_status(self):
3461 3465 return safe_int(self._last_merge_status)
3462 3466
3463 3467 @last_merge_status.setter
3464 3468 def last_merge_status(self, val):
3465 3469 self._last_merge_status = val
3466 3470
3467 3471 @declared_attr
3468 3472 def author(cls):
3469 3473 return relationship('User', lazy='joined')
3470 3474
3471 3475 @declared_attr
3472 3476 def source_repo(cls):
3473 3477 return relationship(
3474 3478 'Repository',
3475 3479 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3476 3480
3477 3481 @property
3478 3482 def source_ref_parts(self):
3479 3483 return self.unicode_to_reference(self.source_ref)
3480 3484
3481 3485 @declared_attr
3482 3486 def target_repo(cls):
3483 3487 return relationship(
3484 3488 'Repository',
3485 3489 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3486 3490
3487 3491 @property
3488 3492 def target_ref_parts(self):
3489 3493 return self.unicode_to_reference(self.target_ref)
3490 3494
3491 3495 @property
3492 3496 def shadow_merge_ref(self):
3493 3497 return self.unicode_to_reference(self._shadow_merge_ref)
3494 3498
3495 3499 @shadow_merge_ref.setter
3496 3500 def shadow_merge_ref(self, ref):
3497 3501 self._shadow_merge_ref = self.reference_to_unicode(ref)
3498 3502
3499 3503 def unicode_to_reference(self, raw):
3500 3504 """
3501 3505 Convert a unicode (or string) to a reference object.
3502 3506 If unicode evaluates to False it returns None.
3503 3507 """
3504 3508 if raw:
3505 3509 refs = raw.split(':')
3506 3510 return Reference(*refs)
3507 3511 else:
3508 3512 return None
3509 3513
3510 3514 def reference_to_unicode(self, ref):
3511 3515 """
3512 3516 Convert a reference object to unicode.
3513 3517 If reference is None it returns None.
3514 3518 """
3515 3519 if ref:
3516 3520 return u':'.join(ref)
3517 3521 else:
3518 3522 return None
3519 3523
3520 3524 def get_api_data(self, with_merge_state=True):
3521 3525 from rhodecode.model.pull_request import PullRequestModel
3522 3526
3523 3527 pull_request = self
3524 3528 if with_merge_state:
3525 3529 merge_status = PullRequestModel().merge_status(pull_request)
3526 3530 merge_state = {
3527 3531 'status': merge_status[0],
3528 3532 'message': safe_unicode(merge_status[1]),
3529 3533 }
3530 3534 else:
3531 3535 merge_state = {'status': 'not_available',
3532 3536 'message': 'not_available'}
3533 3537
3534 3538 merge_data = {
3535 3539 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3536 3540 'reference': (
3537 3541 pull_request.shadow_merge_ref._asdict()
3538 3542 if pull_request.shadow_merge_ref else None),
3539 3543 }
3540 3544
3541 3545 data = {
3542 3546 'pull_request_id': pull_request.pull_request_id,
3543 3547 'url': PullRequestModel().get_url(pull_request),
3544 3548 'title': pull_request.title,
3545 3549 'description': pull_request.description,
3546 3550 'status': pull_request.status,
3547 3551 'created_on': pull_request.created_on,
3548 3552 'updated_on': pull_request.updated_on,
3549 3553 'commit_ids': pull_request.revisions,
3550 3554 'review_status': pull_request.calculated_review_status(),
3551 3555 'mergeable': merge_state,
3552 3556 'source': {
3553 3557 'clone_url': pull_request.source_repo.clone_url(),
3554 3558 'repository': pull_request.source_repo.repo_name,
3555 3559 'reference': {
3556 3560 'name': pull_request.source_ref_parts.name,
3557 3561 'type': pull_request.source_ref_parts.type,
3558 3562 'commit_id': pull_request.source_ref_parts.commit_id,
3559 3563 },
3560 3564 },
3561 3565 'target': {
3562 3566 'clone_url': pull_request.target_repo.clone_url(),
3563 3567 'repository': pull_request.target_repo.repo_name,
3564 3568 'reference': {
3565 3569 'name': pull_request.target_ref_parts.name,
3566 3570 'type': pull_request.target_ref_parts.type,
3567 3571 'commit_id': pull_request.target_ref_parts.commit_id,
3568 3572 },
3569 3573 },
3570 3574 'merge': merge_data,
3571 3575 'author': pull_request.author.get_api_data(include_secrets=False,
3572 3576 details='basic'),
3573 3577 'reviewers': [
3574 3578 {
3575 3579 'user': reviewer.get_api_data(include_secrets=False,
3576 3580 details='basic'),
3577 3581 'reasons': reasons,
3578 3582 'review_status': st[0][1].status if st else 'not_reviewed',
3579 3583 }
3580 3584 for reviewer, reasons, mandatory, st in
3581 3585 pull_request.reviewers_statuses()
3582 3586 ]
3583 3587 }
3584 3588
3585 3589 return data
3586 3590
3587 3591
3588 3592 class PullRequest(Base, _PullRequestBase):
3589 3593 __tablename__ = 'pull_requests'
3590 3594 __table_args__ = (
3591 3595 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3592 3596 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3593 3597 )
3594 3598
3595 3599 pull_request_id = Column(
3596 3600 'pull_request_id', Integer(), nullable=False, primary_key=True)
3597 3601
3598 3602 def __repr__(self):
3599 3603 if self.pull_request_id:
3600 3604 return '<DB:PullRequest #%s>' % self.pull_request_id
3601 3605 else:
3602 3606 return '<DB:PullRequest at %#x>' % id(self)
3603 3607
3604 3608 reviewers = relationship('PullRequestReviewers',
3605 3609 cascade="all, delete, delete-orphan")
3606 3610 statuses = relationship('ChangesetStatus',
3607 3611 cascade="all, delete, delete-orphan")
3608 3612 comments = relationship('ChangesetComment',
3609 3613 cascade="all, delete, delete-orphan")
3610 3614 versions = relationship('PullRequestVersion',
3611 3615 cascade="all, delete, delete-orphan",
3612 3616 lazy='dynamic')
3613 3617
3614 3618 @classmethod
3615 3619 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3616 3620 internal_methods=None):
3617 3621
3618 3622 class PullRequestDisplay(object):
3619 3623 """
3620 3624 Special object wrapper for showing PullRequest data via Versions
3621 3625 It mimics PR object as close as possible. This is read only object
3622 3626 just for display
3623 3627 """
3624 3628
3625 3629 def __init__(self, attrs, internal=None):
3626 3630 self.attrs = attrs
3627 3631 # internal have priority over the given ones via attrs
3628 3632 self.internal = internal or ['versions']
3629 3633
3630 3634 def __getattr__(self, item):
3631 3635 if item in self.internal:
3632 3636 return getattr(self, item)
3633 3637 try:
3634 3638 return self.attrs[item]
3635 3639 except KeyError:
3636 3640 raise AttributeError(
3637 3641 '%s object has no attribute %s' % (self, item))
3638 3642
3639 3643 def __repr__(self):
3640 3644 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3641 3645
3642 3646 def versions(self):
3643 3647 return pull_request_obj.versions.order_by(
3644 3648 PullRequestVersion.pull_request_version_id).all()
3645 3649
3646 3650 def is_closed(self):
3647 3651 return pull_request_obj.is_closed()
3648 3652
3649 3653 @property
3650 3654 def pull_request_version_id(self):
3651 3655 return getattr(pull_request_obj, 'pull_request_version_id', None)
3652 3656
3653 3657 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3654 3658
3655 3659 attrs.author = StrictAttributeDict(
3656 3660 pull_request_obj.author.get_api_data())
3657 3661 if pull_request_obj.target_repo:
3658 3662 attrs.target_repo = StrictAttributeDict(
3659 3663 pull_request_obj.target_repo.get_api_data())
3660 3664 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3661 3665
3662 3666 if pull_request_obj.source_repo:
3663 3667 attrs.source_repo = StrictAttributeDict(
3664 3668 pull_request_obj.source_repo.get_api_data())
3665 3669 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3666 3670
3667 3671 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3668 3672 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3669 3673 attrs.revisions = pull_request_obj.revisions
3670 3674
3671 3675 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3672 3676 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3673 3677 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3674 3678
3675 3679 return PullRequestDisplay(attrs, internal=internal_methods)
3676 3680
3677 3681 def is_closed(self):
3678 3682 return self.status == self.STATUS_CLOSED
3679 3683
3680 3684 def __json__(self):
3681 3685 return {
3682 3686 'revisions': self.revisions,
3683 3687 }
3684 3688
3685 3689 def calculated_review_status(self):
3686 3690 from rhodecode.model.changeset_status import ChangesetStatusModel
3687 3691 return ChangesetStatusModel().calculated_review_status(self)
3688 3692
3689 3693 def reviewers_statuses(self):
3690 3694 from rhodecode.model.changeset_status import ChangesetStatusModel
3691 3695 return ChangesetStatusModel().reviewers_statuses(self)
3692 3696
3693 3697 @property
3694 3698 def workspace_id(self):
3695 3699 from rhodecode.model.pull_request import PullRequestModel
3696 3700 return PullRequestModel()._workspace_id(self)
3697 3701
3698 3702 def get_shadow_repo(self):
3699 3703 workspace_id = self.workspace_id
3700 3704 vcs_obj = self.target_repo.scm_instance()
3701 3705 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3702 3706 workspace_id)
3703 3707 return vcs_obj._get_shadow_instance(shadow_repository_path)
3704 3708
3705 3709
3706 3710 class PullRequestVersion(Base, _PullRequestBase):
3707 3711 __tablename__ = 'pull_request_versions'
3708 3712 __table_args__ = (
3709 3713 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3710 3714 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3711 3715 )
3712 3716
3713 3717 pull_request_version_id = Column(
3714 3718 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3715 3719 pull_request_id = Column(
3716 3720 'pull_request_id', Integer(),
3717 3721 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3718 3722 pull_request = relationship('PullRequest')
3719 3723
3720 3724 def __repr__(self):
3721 3725 if self.pull_request_version_id:
3722 3726 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3723 3727 else:
3724 3728 return '<DB:PullRequestVersion at %#x>' % id(self)
3725 3729
3726 3730 @property
3727 3731 def reviewers(self):
3728 3732 return self.pull_request.reviewers
3729 3733
3730 3734 @property
3731 3735 def versions(self):
3732 3736 return self.pull_request.versions
3733 3737
3734 3738 def is_closed(self):
3735 3739 # calculate from original
3736 3740 return self.pull_request.status == self.STATUS_CLOSED
3737 3741
3738 3742 def calculated_review_status(self):
3739 3743 return self.pull_request.calculated_review_status()
3740 3744
3741 3745 def reviewers_statuses(self):
3742 3746 return self.pull_request.reviewers_statuses()
3743 3747
3744 3748
3745 3749 class PullRequestReviewers(Base, BaseModel):
3746 3750 __tablename__ = 'pull_request_reviewers'
3747 3751 __table_args__ = (
3748 3752 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3749 3753 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3750 3754 )
3751 3755
3752 3756 @hybrid_property
3753 3757 def reasons(self):
3754 3758 if not self._reasons:
3755 3759 return []
3756 3760 return self._reasons
3757 3761
3758 3762 @reasons.setter
3759 3763 def reasons(self, val):
3760 3764 val = val or []
3761 3765 if any(not isinstance(x, basestring) for x in val):
3762 3766 raise Exception('invalid reasons type, must be list of strings')
3763 3767 self._reasons = val
3764 3768
3765 3769 pull_requests_reviewers_id = Column(
3766 3770 'pull_requests_reviewers_id', Integer(), nullable=False,
3767 3771 primary_key=True)
3768 3772 pull_request_id = Column(
3769 3773 "pull_request_id", Integer(),
3770 3774 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3771 3775 user_id = Column(
3772 3776 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3773 3777 _reasons = Column(
3774 3778 'reason', MutationList.as_mutable(
3775 3779 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3776 3780 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3777 3781 user = relationship('User')
3778 3782 pull_request = relationship('PullRequest')
3779 3783
3780 3784
3781 3785 class Notification(Base, BaseModel):
3782 3786 __tablename__ = 'notifications'
3783 3787 __table_args__ = (
3784 3788 Index('notification_type_idx', 'type'),
3785 3789 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3786 3790 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3787 3791 )
3788 3792
3789 3793 TYPE_CHANGESET_COMMENT = u'cs_comment'
3790 3794 TYPE_MESSAGE = u'message'
3791 3795 TYPE_MENTION = u'mention'
3792 3796 TYPE_REGISTRATION = u'registration'
3793 3797 TYPE_PULL_REQUEST = u'pull_request'
3794 3798 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3795 3799
3796 3800 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3797 3801 subject = Column('subject', Unicode(512), nullable=True)
3798 3802 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3799 3803 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3800 3804 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3801 3805 type_ = Column('type', Unicode(255))
3802 3806
3803 3807 created_by_user = relationship('User')
3804 3808 notifications_to_users = relationship('UserNotification', lazy='joined',
3805 3809 cascade="all, delete, delete-orphan")
3806 3810
3807 3811 @property
3808 3812 def recipients(self):
3809 3813 return [x.user for x in UserNotification.query()\
3810 3814 .filter(UserNotification.notification == self)\
3811 3815 .order_by(UserNotification.user_id.asc()).all()]
3812 3816
3813 3817 @classmethod
3814 3818 def create(cls, created_by, subject, body, recipients, type_=None):
3815 3819 if type_ is None:
3816 3820 type_ = Notification.TYPE_MESSAGE
3817 3821
3818 3822 notification = cls()
3819 3823 notification.created_by_user = created_by
3820 3824 notification.subject = subject
3821 3825 notification.body = body
3822 3826 notification.type_ = type_
3823 3827 notification.created_on = datetime.datetime.now()
3824 3828
3825 3829 for u in recipients:
3826 3830 assoc = UserNotification()
3827 3831 assoc.notification = notification
3828 3832
3829 3833 # if created_by is inside recipients mark his notification
3830 3834 # as read
3831 3835 if u.user_id == created_by.user_id:
3832 3836 assoc.read = True
3833 3837
3834 3838 u.notifications.append(assoc)
3835 3839 Session().add(notification)
3836 3840
3837 3841 return notification
3838 3842
3839 3843
3840 3844 class UserNotification(Base, BaseModel):
3841 3845 __tablename__ = 'user_to_notification'
3842 3846 __table_args__ = (
3843 3847 UniqueConstraint('user_id', 'notification_id'),
3844 3848 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3845 3849 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3846 3850 )
3847 3851 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3848 3852 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3849 3853 read = Column('read', Boolean, default=False)
3850 3854 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3851 3855
3852 3856 user = relationship('User', lazy="joined")
3853 3857 notification = relationship('Notification', lazy="joined",
3854 3858 order_by=lambda: Notification.created_on.desc(),)
3855 3859
3856 3860 def mark_as_read(self):
3857 3861 self.read = True
3858 3862 Session().add(self)
3859 3863
3860 3864
3861 3865 class Gist(Base, BaseModel):
3862 3866 __tablename__ = 'gists'
3863 3867 __table_args__ = (
3864 3868 Index('g_gist_access_id_idx', 'gist_access_id'),
3865 3869 Index('g_created_on_idx', 'created_on'),
3866 3870 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3867 3871 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3868 3872 )
3869 3873 GIST_PUBLIC = u'public'
3870 3874 GIST_PRIVATE = u'private'
3871 3875 DEFAULT_FILENAME = u'gistfile1.txt'
3872 3876
3873 3877 ACL_LEVEL_PUBLIC = u'acl_public'
3874 3878 ACL_LEVEL_PRIVATE = u'acl_private'
3875 3879
3876 3880 gist_id = Column('gist_id', Integer(), primary_key=True)
3877 3881 gist_access_id = Column('gist_access_id', Unicode(250))
3878 3882 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3879 3883 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3880 3884 gist_expires = Column('gist_expires', Float(53), nullable=False)
3881 3885 gist_type = Column('gist_type', Unicode(128), nullable=False)
3882 3886 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3883 3887 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3884 3888 acl_level = Column('acl_level', Unicode(128), nullable=True)
3885 3889
3886 3890 owner = relationship('User')
3887 3891
3888 3892 def __repr__(self):
3889 3893 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3890 3894
3891 3895 @hybrid_property
3892 3896 def description_safe(self):
3893 3897 from rhodecode.lib import helpers as h
3894 3898 return h.escape(self.gist_description)
3895 3899
3896 3900 @classmethod
3897 3901 def get_or_404(cls, id_):
3898 3902 from pyramid.httpexceptions import HTTPNotFound
3899 3903
3900 3904 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3901 3905 if not res:
3902 3906 raise HTTPNotFound()
3903 3907 return res
3904 3908
3905 3909 @classmethod
3906 3910 def get_by_access_id(cls, gist_access_id):
3907 3911 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3908 3912
3909 3913 def gist_url(self):
3910 3914 from rhodecode.model.gist import GistModel
3911 3915 return GistModel().get_url(self)
3912 3916
3913 3917 @classmethod
3914 3918 def base_path(cls):
3915 3919 """
3916 3920 Returns base path when all gists are stored
3917 3921
3918 3922 :param cls:
3919 3923 """
3920 3924 from rhodecode.model.gist import GIST_STORE_LOC
3921 3925 q = Session().query(RhodeCodeUi)\
3922 3926 .filter(RhodeCodeUi.ui_key == URL_SEP)
3923 3927 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3924 3928 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3925 3929
3926 3930 def get_api_data(self):
3927 3931 """
3928 3932 Common function for generating gist related data for API
3929 3933 """
3930 3934 gist = self
3931 3935 data = {
3932 3936 'gist_id': gist.gist_id,
3933 3937 'type': gist.gist_type,
3934 3938 'access_id': gist.gist_access_id,
3935 3939 'description': gist.gist_description,
3936 3940 'url': gist.gist_url(),
3937 3941 'expires': gist.gist_expires,
3938 3942 'created_on': gist.created_on,
3939 3943 'modified_at': gist.modified_at,
3940 3944 'content': None,
3941 3945 'acl_level': gist.acl_level,
3942 3946 }
3943 3947 return data
3944 3948
3945 3949 def __json__(self):
3946 3950 data = dict(
3947 3951 )
3948 3952 data.update(self.get_api_data())
3949 3953 return data
3950 3954 # SCM functions
3951 3955
3952 3956 def scm_instance(self, **kwargs):
3953 3957 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3954 3958 return get_vcs_instance(
3955 3959 repo_path=safe_str(full_repo_path), create=False)
3956 3960
3957 3961
3958 3962 class ExternalIdentity(Base, BaseModel):
3959 3963 __tablename__ = 'external_identities'
3960 3964 __table_args__ = (
3961 3965 Index('local_user_id_idx', 'local_user_id'),
3962 3966 Index('external_id_idx', 'external_id'),
3963 3967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3964 3968 'mysql_charset': 'utf8'})
3965 3969
3966 3970 external_id = Column('external_id', Unicode(255), default=u'',
3967 3971 primary_key=True)
3968 3972 external_username = Column('external_username', Unicode(1024), default=u'')
3969 3973 local_user_id = Column('local_user_id', Integer(),
3970 3974 ForeignKey('users.user_id'), primary_key=True)
3971 3975 provider_name = Column('provider_name', Unicode(255), default=u'',
3972 3976 primary_key=True)
3973 3977 access_token = Column('access_token', String(1024), default=u'')
3974 3978 alt_token = Column('alt_token', String(1024), default=u'')
3975 3979 token_secret = Column('token_secret', String(1024), default=u'')
3976 3980
3977 3981 @classmethod
3978 3982 def by_external_id_and_provider(cls, external_id, provider_name,
3979 3983 local_user_id=None):
3980 3984 """
3981 3985 Returns ExternalIdentity instance based on search params
3982 3986
3983 3987 :param external_id:
3984 3988 :param provider_name:
3985 3989 :return: ExternalIdentity
3986 3990 """
3987 3991 query = cls.query()
3988 3992 query = query.filter(cls.external_id == external_id)
3989 3993 query = query.filter(cls.provider_name == provider_name)
3990 3994 if local_user_id:
3991 3995 query = query.filter(cls.local_user_id == local_user_id)
3992 3996 return query.first()
3993 3997
3994 3998 @classmethod
3995 3999 def user_by_external_id_and_provider(cls, external_id, provider_name):
3996 4000 """
3997 4001 Returns User instance based on search params
3998 4002
3999 4003 :param external_id:
4000 4004 :param provider_name:
4001 4005 :return: User
4002 4006 """
4003 4007 query = User.query()
4004 4008 query = query.filter(cls.external_id == external_id)
4005 4009 query = query.filter(cls.provider_name == provider_name)
4006 4010 query = query.filter(User.user_id == cls.local_user_id)
4007 4011 return query.first()
4008 4012
4009 4013 @classmethod
4010 4014 def by_local_user_id(cls, local_user_id):
4011 4015 """
4012 4016 Returns all tokens for user
4013 4017
4014 4018 :param local_user_id:
4015 4019 :return: ExternalIdentity
4016 4020 """
4017 4021 query = cls.query()
4018 4022 query = query.filter(cls.local_user_id == local_user_id)
4019 4023 return query
4020 4024
4021 4025
4022 4026 class Integration(Base, BaseModel):
4023 4027 __tablename__ = 'integrations'
4024 4028 __table_args__ = (
4025 4029 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4026 4030 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
4027 4031 )
4028 4032
4029 4033 integration_id = Column('integration_id', Integer(), primary_key=True)
4030 4034 integration_type = Column('integration_type', String(255))
4031 4035 enabled = Column('enabled', Boolean(), nullable=False)
4032 4036 name = Column('name', String(255), nullable=False)
4033 4037 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4034 4038 default=False)
4035 4039
4036 4040 settings = Column(
4037 4041 'settings_json', MutationObj.as_mutable(
4038 4042 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4039 4043 repo_id = Column(
4040 4044 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4041 4045 nullable=True, unique=None, default=None)
4042 4046 repo = relationship('Repository', lazy='joined')
4043 4047
4044 4048 repo_group_id = Column(
4045 4049 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4046 4050 nullable=True, unique=None, default=None)
4047 4051 repo_group = relationship('RepoGroup', lazy='joined')
4048 4052
4049 4053 @property
4050 4054 def scope(self):
4051 4055 if self.repo:
4052 4056 return repr(self.repo)
4053 4057 if self.repo_group:
4054 4058 if self.child_repos_only:
4055 4059 return repr(self.repo_group) + ' (child repos only)'
4056 4060 else:
4057 4061 return repr(self.repo_group) + ' (recursive)'
4058 4062 if self.child_repos_only:
4059 4063 return 'root_repos'
4060 4064 return 'global'
4061 4065
4062 4066 def __repr__(self):
4063 4067 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4064 4068
4065 4069
4066 4070 class RepoReviewRuleUser(Base, BaseModel):
4067 4071 __tablename__ = 'repo_review_rules_users'
4068 4072 __table_args__ = (
4069 4073 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4070 4074 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4071 4075 )
4072 4076 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4073 4077 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4074 4078 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4075 4079 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4076 4080 user = relationship('User')
4077 4081
4078 4082 def rule_data(self):
4079 4083 return {
4080 4084 'mandatory': self.mandatory
4081 4085 }
4082 4086
4083 4087
4084 4088 class RepoReviewRuleUserGroup(Base, BaseModel):
4085 4089 __tablename__ = 'repo_review_rules_users_groups'
4086 4090 __table_args__ = (
4087 4091 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4088 4092 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4089 4093 )
4090 4094 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4091 4095 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4092 4096 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4093 4097 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4094 4098 users_group = relationship('UserGroup')
4095 4099
4096 4100 def rule_data(self):
4097 4101 return {
4098 4102 'mandatory': self.mandatory
4099 4103 }
4100 4104
4101 4105
4102 4106 class RepoReviewRule(Base, BaseModel):
4103 4107 __tablename__ = 'repo_review_rules'
4104 4108 __table_args__ = (
4105 4109 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4106 4110 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4107 4111 )
4108 4112
4109 4113 repo_review_rule_id = Column(
4110 4114 'repo_review_rule_id', Integer(), primary_key=True)
4111 4115 repo_id = Column(
4112 4116 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4113 4117 repo = relationship('Repository', backref='review_rules')
4114 4118
4115 4119 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4116 4120 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4117 4121
4118 4122 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4119 4123 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4120 4124 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4121 4125 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4122 4126
4123 4127 rule_users = relationship('RepoReviewRuleUser')
4124 4128 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4125 4129
4126 4130 @hybrid_property
4127 4131 def branch_pattern(self):
4128 4132 return self._branch_pattern or '*'
4129 4133
4130 4134 def _validate_glob(self, value):
4131 4135 re.compile('^' + glob2re(value) + '$')
4132 4136
4133 4137 @branch_pattern.setter
4134 4138 def branch_pattern(self, value):
4135 4139 self._validate_glob(value)
4136 4140 self._branch_pattern = value or '*'
4137 4141
4138 4142 @hybrid_property
4139 4143 def file_pattern(self):
4140 4144 return self._file_pattern or '*'
4141 4145
4142 4146 @file_pattern.setter
4143 4147 def file_pattern(self, value):
4144 4148 self._validate_glob(value)
4145 4149 self._file_pattern = value or '*'
4146 4150
4147 4151 def matches(self, branch, files_changed):
4148 4152 """
4149 4153 Check if this review rule matches a branch/files in a pull request
4150 4154
4151 4155 :param branch: branch name for the commit
4152 4156 :param files_changed: list of file paths changed in the pull request
4153 4157 """
4154 4158
4155 4159 branch = branch or ''
4156 4160 files_changed = files_changed or []
4157 4161
4158 4162 branch_matches = True
4159 4163 if branch:
4160 4164 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4161 4165 branch_matches = bool(branch_regex.search(branch))
4162 4166
4163 4167 files_matches = True
4164 4168 if self.file_pattern != '*':
4165 4169 files_matches = False
4166 4170 file_regex = re.compile(glob2re(self.file_pattern))
4167 4171 for filename in files_changed:
4168 4172 if file_regex.search(filename):
4169 4173 files_matches = True
4170 4174 break
4171 4175
4172 4176 return branch_matches and files_matches
4173 4177
4174 4178 @property
4175 4179 def review_users(self):
4176 4180 """ Returns the users which this rule applies to """
4177 4181
4178 4182 users = collections.OrderedDict()
4179 4183
4180 4184 for rule_user in self.rule_users:
4181 4185 if rule_user.user.active:
4182 4186 if rule_user.user not in users:
4183 4187 users[rule_user.user.username] = {
4184 4188 'user': rule_user.user,
4185 4189 'source': 'user',
4186 4190 'source_data': {},
4187 4191 'data': rule_user.rule_data()
4188 4192 }
4189 4193
4190 4194 for rule_user_group in self.rule_user_groups:
4191 4195 source_data = {
4192 4196 'name': rule_user_group.users_group.users_group_name,
4193 4197 'members': len(rule_user_group.users_group.members)
4194 4198 }
4195 4199 for member in rule_user_group.users_group.members:
4196 4200 if member.user.active:
4197 4201 users[member.user.username] = {
4198 4202 'user': member.user,
4199 4203 'source': 'user_group',
4200 4204 'source_data': source_data,
4201 4205 'data': rule_user_group.rule_data()
4202 4206 }
4203 4207
4204 4208 return users
4205 4209
4206 4210 def __repr__(self):
4207 4211 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4208 4212 self.repo_review_rule_id, self.repo)
4209 4213
4210 4214
4211 4215 class ScheduleEntry(Base, BaseModel):
4212 4216 __tablename__ = 'schedule_entries'
4213 4217 __table_args__ = (
4214 4218 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4215 4219 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4216 4220 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4217 4221 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4218 4222 )
4219 4223 schedule_types = ['crontab', 'timedelta', 'integer']
4220 4224 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4221 4225
4222 4226 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4223 4227 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4224 4228 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4225 4229
4226 4230 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4227 4231 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4228 4232
4229 4233 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4230 4234 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4231 4235
4232 4236 # task
4233 4237 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4234 4238 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4235 4239 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4236 4240 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4237 4241
4238 4242 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4239 4243 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4240 4244
4241 4245 @hybrid_property
4242 4246 def schedule_type(self):
4243 4247 return self._schedule_type
4244 4248
4245 4249 @schedule_type.setter
4246 4250 def schedule_type(self, val):
4247 4251 if val not in self.schedule_types:
4248 4252 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4249 4253 val, self.schedule_type))
4250 4254
4251 4255 self._schedule_type = val
4252 4256
4253 4257 @classmethod
4254 4258 def get_uid(cls, obj):
4255 4259 args = obj.task_args
4256 4260 kwargs = obj.task_kwargs
4257 4261 if isinstance(args, JsonRaw):
4258 4262 try:
4259 4263 args = json.loads(args)
4260 4264 except ValueError:
4261 4265 args = tuple()
4262 4266
4263 4267 if isinstance(kwargs, JsonRaw):
4264 4268 try:
4265 4269 kwargs = json.loads(kwargs)
4266 4270 except ValueError:
4267 4271 kwargs = dict()
4268 4272
4269 4273 dot_notation = obj.task_dot_notation
4270 4274 val = '.'.join(map(safe_str, [
4271 4275 sorted(dot_notation), args, sorted(kwargs.items())]))
4272 4276 return hashlib.sha1(val).hexdigest()
4273 4277
4274 4278 @classmethod
4275 4279 def get_by_schedule_name(cls, schedule_name):
4276 4280 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4277 4281
4278 4282 @classmethod
4279 4283 def get_by_schedule_id(cls, schedule_id):
4280 4284 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4281 4285
4282 4286 @property
4283 4287 def task(self):
4284 4288 return self.task_dot_notation
4285 4289
4286 4290 @property
4287 4291 def schedule(self):
4288 4292 from rhodecode.lib.celerylib.utils import raw_2_schedule
4289 4293 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4290 4294 return schedule
4291 4295
4292 4296 @property
4293 4297 def args(self):
4294 4298 try:
4295 4299 return list(self.task_args or [])
4296 4300 except ValueError:
4297 4301 return list()
4298 4302
4299 4303 @property
4300 4304 def kwargs(self):
4301 4305 try:
4302 4306 return dict(self.task_kwargs or {})
4303 4307 except ValueError:
4304 4308 return dict()
4305 4309
4306 4310 def _as_raw(self, val):
4307 4311 if hasattr(val, 'de_coerce'):
4308 4312 val = val.de_coerce()
4309 4313 if val:
4310 4314 val = json.dumps(val)
4311 4315
4312 4316 return val
4313 4317
4314 4318 @property
4315 4319 def schedule_definition_raw(self):
4316 4320 return self._as_raw(self.schedule_definition)
4317 4321
4318 4322 @property
4319 4323 def args_raw(self):
4320 4324 return self._as_raw(self.task_args)
4321 4325
4322 4326 @property
4323 4327 def kwargs_raw(self):
4324 4328 return self._as_raw(self.task_kwargs)
4325 4329
4326 4330 def __repr__(self):
4327 4331 return '<DB:ScheduleEntry({}:{})>'.format(
4328 4332 self.schedule_entry_id, self.schedule_name)
4329 4333
4330 4334
4331 4335 @event.listens_for(ScheduleEntry, 'before_update')
4332 4336 def update_task_uid(mapper, connection, target):
4333 4337 target.task_uid = ScheduleEntry.get_uid(target)
4334 4338
4335 4339
4336 4340 @event.listens_for(ScheduleEntry, 'before_insert')
4337 4341 def set_task_uid(mapper, connection, target):
4338 4342 target.task_uid = ScheduleEntry.get_uid(target)
4339 4343
4340 4344
4341 4345 class DbMigrateVersion(Base, BaseModel):
4342 4346 __tablename__ = 'db_migrate_version'
4343 4347 __table_args__ = (
4344 4348 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4345 4349 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4346 4350 )
4347 4351 repository_id = Column('repository_id', String(250), primary_key=True)
4348 4352 repository_path = Column('repository_path', Text)
4349 4353 version = Column('version', Integer)
4350 4354
4351 4355
4352 4356 class DbSession(Base, BaseModel):
4353 4357 __tablename__ = 'db_session'
4354 4358 __table_args__ = (
4355 4359 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4356 4360 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4357 4361 )
4358 4362
4359 4363 def __repr__(self):
4360 4364 return '<DB:DbSession({})>'.format(self.id)
4361 4365
4362 4366 id = Column('id', Integer())
4363 4367 namespace = Column('namespace', String(255), primary_key=True)
4364 4368 accessed = Column('accessed', DateTime, nullable=False)
4365 4369 created = Column('created', DateTime, nullable=False)
4366 4370 data = Column('data', PickleType, nullable=False)
@@ -1,911 +1,909 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28 import ipaddress
29 29
30 30 from pyramid.threadlocal import get_current_request
31 31 from sqlalchemy.exc import DatabaseError
32 32
33 33 from rhodecode import events
34 34 from rhodecode.lib.user_log_filter import user_log_filter
35 35 from rhodecode.lib.utils2 import (
36 36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 37 AttributeDict, str2bool)
38 38 from rhodecode.lib.exceptions import (
39 39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 40 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
41 41 from rhodecode.lib.caching_query import FromCache
42 42 from rhodecode.model import BaseModel
43 43 from rhodecode.model.auth_token import AuthTokenModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
46 46 UserEmailMap, UserIpMap, UserLog)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(
61 61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def _serialize_user(self, user):
68 68 import rhodecode.lib.helpers as h
69 69
70 70 return {
71 71 'id': user.user_id,
72 72 'first_name': user.first_name,
73 73 'last_name': user.last_name,
74 74 'username': user.username,
75 75 'email': user.email,
76 76 'icon_link': h.gravatar_url(user.email, 30),
77 77 'value_display': h.escape(h.person(user)),
78 78 'value': user.username,
79 79 'value_type': 'user',
80 80 'active': user.active,
81 81 }
82 82
83 83 def get_users(self, name_contains=None, limit=20, only_active=True):
84 84
85 85 query = self.sa.query(User)
86 86 if only_active:
87 87 query = query.filter(User.active == true())
88 88
89 89 if name_contains:
90 90 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
91 91 query = query.filter(
92 92 or_(
93 93 User.name.ilike(ilike_expression),
94 94 User.lastname.ilike(ilike_expression),
95 95 User.username.ilike(ilike_expression)
96 96 )
97 97 )
98 98 query = query.limit(limit)
99 99 users = query.all()
100 100
101 101 _users = [
102 102 self._serialize_user(user) for user in users
103 103 ]
104 104 return _users
105 105
106 106 def get_by_username(self, username, cache=False, case_insensitive=False):
107 107
108 108 if case_insensitive:
109 109 user = self.sa.query(User).filter(User.username.ilike(username))
110 110 else:
111 111 user = self.sa.query(User)\
112 112 .filter(User.username == username)
113 113 if cache:
114 114 name_key = _hash_key(username)
115 115 user = user.options(
116 116 FromCache("sql_cache_short", "get_user_%s" % name_key))
117 117 return user.scalar()
118 118
119 119 def get_by_email(self, email, cache=False, case_insensitive=False):
120 120 return User.get_by_email(email, case_insensitive, cache)
121 121
122 122 def get_by_auth_token(self, auth_token, cache=False):
123 123 return User.get_by_auth_token(auth_token, cache)
124 124
125 125 def get_active_user_count(self, cache=False):
126 126 return User.query().filter(
127 127 User.active == True).filter(
128 128 User.username != User.DEFAULT_USER).count()
129 129
130 130 def create(self, form_data, cur_user=None):
131 131 if not cur_user:
132 132 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
133 133
134 134 user_data = {
135 135 'username': form_data['username'],
136 136 'password': form_data['password'],
137 137 'email': form_data['email'],
138 138 'firstname': form_data['firstname'],
139 139 'lastname': form_data['lastname'],
140 140 'active': form_data['active'],
141 141 'extern_type': form_data['extern_type'],
142 142 'extern_name': form_data['extern_name'],
143 143 'admin': False,
144 144 'cur_user': cur_user
145 145 }
146 146
147 147 if 'create_repo_group' in form_data:
148 148 user_data['create_repo_group'] = str2bool(
149 149 form_data.get('create_repo_group'))
150 150
151 151 try:
152 152 if form_data.get('password_change'):
153 153 user_data['force_password_change'] = True
154 154 return UserModel().create_or_update(**user_data)
155 155 except Exception:
156 156 log.error(traceback.format_exc())
157 157 raise
158 158
159 159 def update_user(self, user, skip_attrs=None, **kwargs):
160 160 from rhodecode.lib.auth import get_crypt_password
161 161
162 162 user = self._get_user(user)
163 163 if user.username == User.DEFAULT_USER:
164 164 raise DefaultUserException(
165 165 "You can't edit this user (`%(username)s`) since it's "
166 166 "crucial for entire application" % {
167 167 'username': user.username})
168 168
169 169 # first store only defaults
170 170 user_attrs = {
171 171 'updating_user_id': user.user_id,
172 172 'username': user.username,
173 173 'password': user.password,
174 174 'email': user.email,
175 175 'firstname': user.name,
176 176 'lastname': user.lastname,
177 177 'active': user.active,
178 178 'admin': user.admin,
179 179 'extern_name': user.extern_name,
180 180 'extern_type': user.extern_type,
181 181 'language': user.user_data.get('language')
182 182 }
183 183
184 184 # in case there's new_password, that comes from form, use it to
185 185 # store password
186 186 if kwargs.get('new_password'):
187 187 kwargs['password'] = kwargs['new_password']
188 188
189 189 # cleanups, my_account password change form
190 190 kwargs.pop('current_password', None)
191 191 kwargs.pop('new_password', None)
192 192
193 193 # cleanups, user edit password change form
194 194 kwargs.pop('password_confirmation', None)
195 195 kwargs.pop('password_change', None)
196 196
197 197 # create repo group on user creation
198 198 kwargs.pop('create_repo_group', None)
199 199
200 200 # legacy forms send name, which is the firstname
201 201 firstname = kwargs.pop('name', None)
202 202 if firstname:
203 203 kwargs['firstname'] = firstname
204 204
205 205 for k, v in kwargs.items():
206 206 # skip if we don't want to update this
207 207 if skip_attrs and k in skip_attrs:
208 208 continue
209 209
210 210 user_attrs[k] = v
211 211
212 212 try:
213 213 return self.create_or_update(**user_attrs)
214 214 except Exception:
215 215 log.error(traceback.format_exc())
216 216 raise
217 217
218 218 def create_or_update(
219 219 self, username, password, email, firstname='', lastname='',
220 220 active=True, admin=False, extern_type=None, extern_name=None,
221 221 cur_user=None, plugin=None, force_password_change=False,
222 222 allow_to_create_user=True, create_repo_group=None,
223 223 updating_user_id=None, language=None, strict_creation_check=True):
224 224 """
225 225 Creates a new instance if not found, or updates current one
226 226
227 227 :param username:
228 228 :param password:
229 229 :param email:
230 230 :param firstname:
231 231 :param lastname:
232 232 :param active:
233 233 :param admin:
234 234 :param extern_type:
235 235 :param extern_name:
236 236 :param cur_user:
237 237 :param plugin: optional plugin this method was called from
238 238 :param force_password_change: toggles new or existing user flag
239 239 for password change
240 240 :param allow_to_create_user: Defines if the method can actually create
241 241 new users
242 242 :param create_repo_group: Defines if the method should also
243 243 create an repo group with user name, and owner
244 244 :param updating_user_id: if we set it up this is the user we want to
245 245 update this allows to editing username.
246 246 :param language: language of user from interface.
247 247
248 248 :returns: new User object with injected `is_new_user` attribute.
249 249 """
250 250
251 251 if not cur_user:
252 252 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
253 253
254 254 from rhodecode.lib.auth import (
255 255 get_crypt_password, check_password, generate_auth_token)
256 256 from rhodecode.lib.hooks_base import (
257 257 log_create_user, check_allowed_create_user)
258 258
259 259 def _password_change(new_user, password):
260 260 old_password = new_user.password or ''
261 261 # empty password
262 262 if not old_password:
263 263 return False
264 264
265 265 # password check is only needed for RhodeCode internal auth calls
266 266 # in case it's a plugin we don't care
267 267 if not plugin:
268 268
269 269 # first check if we gave crypted password back, and if it
270 270 # matches it's not password change
271 271 if new_user.password == password:
272 272 return False
273 273
274 274 password_match = check_password(password, old_password)
275 275 if not password_match:
276 276 return True
277 277
278 278 return False
279 279
280 280 # read settings on default personal repo group creation
281 281 if create_repo_group is None:
282 282 default_create_repo_group = RepoGroupModel()\
283 283 .get_default_create_personal_repo_group()
284 284 create_repo_group = default_create_repo_group
285 285
286 286 user_data = {
287 287 'username': username,
288 288 'password': password,
289 289 'email': email,
290 290 'firstname': firstname,
291 291 'lastname': lastname,
292 292 'active': active,
293 293 'admin': admin
294 294 }
295 295
296 296 if updating_user_id:
297 297 log.debug('Checking for existing account in RhodeCode '
298 298 'database with user_id `%s` ' % (updating_user_id,))
299 299 user = User.get(updating_user_id)
300 300 else:
301 301 log.debug('Checking for existing account in RhodeCode '
302 302 'database with username `%s` ' % (username,))
303 303 user = User.get_by_username(username, case_insensitive=True)
304 304
305 305 if user is None:
306 306 # we check internal flag if this method is actually allowed to
307 307 # create new user
308 308 if not allow_to_create_user:
309 309 msg = ('Method wants to create new user, but it is not '
310 310 'allowed to do so')
311 311 log.warning(msg)
312 312 raise NotAllowedToCreateUserError(msg)
313 313
314 314 log.debug('Creating new user %s', username)
315 315
316 316 # only if we create user that is active
317 317 new_active_user = active
318 318 if new_active_user and strict_creation_check:
319 319 # raises UserCreationError if it's not allowed for any reason to
320 320 # create new active user, this also executes pre-create hooks
321 321 check_allowed_create_user(user_data, cur_user, strict_check=True)
322 322 events.trigger(events.UserPreCreate(user_data))
323 323 new_user = User()
324 324 edit = False
325 325 else:
326 326 log.debug('updating user %s', username)
327 327 events.trigger(events.UserPreUpdate(user, user_data))
328 328 new_user = user
329 329 edit = True
330 330
331 331 # we're not allowed to edit default user
332 332 if user.username == User.DEFAULT_USER:
333 333 raise DefaultUserException(
334 334 "You can't edit this user (`%(username)s`) since it's "
335 335 "crucial for entire application"
336 336 % {'username': user.username})
337 337
338 338 # inject special attribute that will tell us if User is new or old
339 339 new_user.is_new_user = not edit
340 340 # for users that didn's specify auth type, we use RhodeCode built in
341 341 from rhodecode.authentication.plugins import auth_rhodecode
342 342 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
343 343 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
344 344
345 345 try:
346 346 new_user.username = username
347 347 new_user.admin = admin
348 348 new_user.email = email
349 349 new_user.active = active
350 350 new_user.extern_name = safe_unicode(extern_name)
351 351 new_user.extern_type = safe_unicode(extern_type)
352 352 new_user.name = firstname
353 353 new_user.lastname = lastname
354 354
355 355 # set password only if creating an user or password is changed
356 356 if not edit or _password_change(new_user, password):
357 357 reason = 'new password' if edit else 'new user'
358 358 log.debug('Updating password reason=>%s', reason)
359 359 new_user.password = get_crypt_password(password) if password else None
360 360
361 361 if force_password_change:
362 362 new_user.update_userdata(force_password_change=True)
363 363 if language:
364 364 new_user.update_userdata(language=language)
365 365 new_user.update_userdata(notification_status=True)
366 366
367 367 self.sa.add(new_user)
368 368
369 369 if not edit and create_repo_group:
370 370 RepoGroupModel().create_personal_repo_group(
371 371 new_user, commit_early=False)
372 372
373 373 if not edit:
374 374 # add the RSS token
375 375 AuthTokenModel().create(username,
376 376 description=u'Generated feed token',
377 377 role=AuthTokenModel.cls.ROLE_FEED)
378 378 kwargs = new_user.get_dict()
379 379 # backward compat, require api_keys present
380 380 kwargs['api_keys'] = kwargs['auth_tokens']
381 381 log_create_user(created_by=cur_user, **kwargs)
382 382 events.trigger(events.UserPostCreate(user_data))
383 383 return new_user
384 384 except (DatabaseError,):
385 385 log.error(traceback.format_exc())
386 386 raise
387 387
388 388 def create_registration(self, form_data):
389 389 from rhodecode.model.notification import NotificationModel
390 390 from rhodecode.model.notification import EmailNotificationModel
391 391
392 392 try:
393 393 form_data['admin'] = False
394 394 form_data['extern_name'] = 'rhodecode'
395 395 form_data['extern_type'] = 'rhodecode'
396 396 new_user = self.create(form_data)
397 397
398 398 self.sa.add(new_user)
399 399 self.sa.flush()
400 400
401 401 user_data = new_user.get_dict()
402 402 kwargs = {
403 403 # use SQLALCHEMY safe dump of user data
404 404 'user': AttributeDict(user_data),
405 405 'date': datetime.datetime.now()
406 406 }
407 407 notification_type = EmailNotificationModel.TYPE_REGISTRATION
408 408 # pre-generate the subject for notification itself
409 409 (subject,
410 410 _h, _e, # we don't care about those
411 411 body_plaintext) = EmailNotificationModel().render_email(
412 412 notification_type, **kwargs)
413 413
414 414 # create notification objects, and emails
415 415 NotificationModel().create(
416 416 created_by=new_user,
417 417 notification_subject=subject,
418 418 notification_body=body_plaintext,
419 419 notification_type=notification_type,
420 420 recipients=None, # all admins
421 421 email_kwargs=kwargs,
422 422 )
423 423
424 424 return new_user
425 425 except Exception:
426 426 log.error(traceback.format_exc())
427 427 raise
428 428
429 429 def _handle_user_repos(self, username, repositories, handle_mode=None):
430 430 _superadmin = self.cls.get_first_super_admin()
431 431 left_overs = True
432 432
433 433 from rhodecode.model.repo import RepoModel
434 434
435 435 if handle_mode == 'detach':
436 436 for obj in repositories:
437 437 obj.user = _superadmin
438 438 # set description we know why we super admin now owns
439 439 # additional repositories that were orphaned !
440 440 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
441 441 self.sa.add(obj)
442 442 left_overs = False
443 443 elif handle_mode == 'delete':
444 444 for obj in repositories:
445 445 RepoModel().delete(obj, forks='detach')
446 446 left_overs = False
447 447
448 448 # if nothing is done we have left overs left
449 449 return left_overs
450 450
451 451 def _handle_user_repo_groups(self, username, repository_groups,
452 452 handle_mode=None):
453 453 _superadmin = self.cls.get_first_super_admin()
454 454 left_overs = True
455 455
456 456 from rhodecode.model.repo_group import RepoGroupModel
457 457
458 458 if handle_mode == 'detach':
459 459 for r in repository_groups:
460 460 r.user = _superadmin
461 461 # set description we know why we super admin now owns
462 462 # additional repositories that were orphaned !
463 463 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
464 464 self.sa.add(r)
465 465 left_overs = False
466 466 elif handle_mode == 'delete':
467 467 for r in repository_groups:
468 468 RepoGroupModel().delete(r)
469 469 left_overs = False
470 470
471 471 # if nothing is done we have left overs left
472 472 return left_overs
473 473
474 474 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
475 475 _superadmin = self.cls.get_first_super_admin()
476 476 left_overs = True
477 477
478 478 from rhodecode.model.user_group import UserGroupModel
479 479
480 480 if handle_mode == 'detach':
481 481 for r in user_groups:
482 482 for user_user_group_to_perm in r.user_user_group_to_perm:
483 483 if user_user_group_to_perm.user.username == username:
484 484 user_user_group_to_perm.user = _superadmin
485 485 r.user = _superadmin
486 486 # set description we know why we super admin now owns
487 487 # additional repositories that were orphaned !
488 488 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
489 489 self.sa.add(r)
490 490 left_overs = False
491 491 elif handle_mode == 'delete':
492 492 for r in user_groups:
493 493 UserGroupModel().delete(r)
494 494 left_overs = False
495 495
496 496 # if nothing is done we have left overs left
497 497 return left_overs
498 498
499 499 def delete(self, user, cur_user=None, handle_repos=None,
500 500 handle_repo_groups=None, handle_user_groups=None):
501 501 if not cur_user:
502 502 cur_user = getattr(
503 503 get_current_rhodecode_user(), 'username', None)
504 504 user = self._get_user(user)
505 505
506 506 try:
507 507 if user.username == User.DEFAULT_USER:
508 508 raise DefaultUserException(
509 509 u"You can't remove this user since it's"
510 510 u" crucial for entire application")
511 511
512 512 left_overs = self._handle_user_repos(
513 513 user.username, user.repositories, handle_repos)
514 514 if left_overs and user.repositories:
515 515 repos = [x.repo_name for x in user.repositories]
516 516 raise UserOwnsReposException(
517 517 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
518 518 u'removed. Switch owners or remove those repositories:%(list_repos)s'
519 519 % {'username': user.username, 'len_repos': len(repos),
520 520 'list_repos': ', '.join(repos)})
521 521
522 522 left_overs = self._handle_user_repo_groups(
523 523 user.username, user.repository_groups, handle_repo_groups)
524 524 if left_overs and user.repository_groups:
525 525 repo_groups = [x.group_name for x in user.repository_groups]
526 526 raise UserOwnsRepoGroupsException(
527 527 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
528 528 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
529 529 % {'username': user.username, 'len_repo_groups': len(repo_groups),
530 530 'list_repo_groups': ', '.join(repo_groups)})
531 531
532 532 left_overs = self._handle_user_user_groups(
533 533 user.username, user.user_groups, handle_user_groups)
534 534 if left_overs and user.user_groups:
535 535 user_groups = [x.users_group_name for x in user.user_groups]
536 536 raise UserOwnsUserGroupsException(
537 537 u'user "%s" still owns %s user groups and cannot be '
538 538 u'removed. Switch owners or remove those user groups:%s'
539 539 % (user.username, len(user_groups), ', '.join(user_groups)))
540 540
541 541 # we might change the user data with detach/delete, make sure
542 542 # the object is marked as expired before actually deleting !
543 543 self.sa.expire(user)
544 544 self.sa.delete(user)
545 545 from rhodecode.lib.hooks_base import log_delete_user
546 546 log_delete_user(deleted_by=cur_user, **user.get_dict())
547 547 except Exception:
548 548 log.error(traceback.format_exc())
549 549 raise
550 550
551 551 def reset_password_link(self, data, pwd_reset_url):
552 552 from rhodecode.lib.celerylib import tasks, run_task
553 553 from rhodecode.model.notification import EmailNotificationModel
554 554 user_email = data['email']
555 555 try:
556 556 user = User.get_by_email(user_email)
557 557 if user:
558 558 log.debug('password reset user found %s', user)
559 559
560 560 email_kwargs = {
561 561 'password_reset_url': pwd_reset_url,
562 562 'user': user,
563 563 'email': user_email,
564 564 'date': datetime.datetime.now()
565 565 }
566 566
567 567 (subject, headers, email_body,
568 568 email_body_plaintext) = EmailNotificationModel().render_email(
569 569 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
570 570
571 571 recipients = [user_email]
572 572
573 573 action_logger_generic(
574 574 'sending password reset email to user: {}'.format(
575 575 user), namespace='security.password_reset')
576 576
577 577 run_task(tasks.send_email, recipients, subject,
578 578 email_body_plaintext, email_body)
579 579
580 580 else:
581 581 log.debug("password reset email %s not found", user_email)
582 582 except Exception:
583 583 log.error(traceback.format_exc())
584 584 return False
585 585
586 586 return True
587 587
588 588 def reset_password(self, data):
589 589 from rhodecode.lib.celerylib import tasks, run_task
590 590 from rhodecode.model.notification import EmailNotificationModel
591 591 from rhodecode.lib import auth
592 592 user_email = data['email']
593 593 pre_db = True
594 594 try:
595 595 user = User.get_by_email(user_email)
596 596 new_passwd = auth.PasswordGenerator().gen_password(
597 597 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
598 598 if user:
599 599 user.password = auth.get_crypt_password(new_passwd)
600 600 # also force this user to reset his password !
601 601 user.update_userdata(force_password_change=True)
602 602
603 603 Session().add(user)
604 604
605 605 # now delete the token in question
606 606 UserApiKeys = AuthTokenModel.cls
607 607 UserApiKeys().query().filter(
608 608 UserApiKeys.api_key == data['token']).delete()
609 609
610 610 Session().commit()
611 611 log.info('successfully reset password for `%s`', user_email)
612 612
613 613 if new_passwd is None:
614 614 raise Exception('unable to generate new password')
615 615
616 616 pre_db = False
617 617
618 618 email_kwargs = {
619 619 'new_password': new_passwd,
620 620 'user': user,
621 621 'email': user_email,
622 622 'date': datetime.datetime.now()
623 623 }
624 624
625 625 (subject, headers, email_body,
626 626 email_body_plaintext) = EmailNotificationModel().render_email(
627 627 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
628 628 **email_kwargs)
629 629
630 630 recipients = [user_email]
631 631
632 632 action_logger_generic(
633 633 'sent new password to user: {} with email: {}'.format(
634 634 user, user_email), namespace='security.password_reset')
635 635
636 636 run_task(tasks.send_email, recipients, subject,
637 637 email_body_plaintext, email_body)
638 638
639 639 except Exception:
640 640 log.error('Failed to update user password')
641 641 log.error(traceback.format_exc())
642 642 if pre_db:
643 643 # we rollback only if local db stuff fails. If it goes into
644 644 # run_task, we're pass rollback state this wouldn't work then
645 645 Session().rollback()
646 646
647 647 return True
648 648
649 649 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
650 650 """
651 651 Fetches auth_user by user_id,or api_key if present.
652 652 Fills auth_user attributes with those taken from database.
653 653 Additionally set's is_authenitated if lookup fails
654 654 present in database
655 655
656 656 :param auth_user: instance of user to set attributes
657 657 :param user_id: user id to fetch by
658 658 :param api_key: api key to fetch by
659 659 :param username: username to fetch by
660 660 """
661 661 if user_id is None and api_key is None and username is None:
662 662 raise Exception('You need to pass user_id, api_key or username')
663 663
664 664 log.debug(
665 665 'AuthUser: fill data execution based on: '
666 666 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
667 667 try:
668 668 dbuser = None
669 669 if user_id:
670 670 dbuser = self.get(user_id)
671 671 elif api_key:
672 672 dbuser = self.get_by_auth_token(api_key)
673 673 elif username:
674 674 dbuser = self.get_by_username(username)
675 675
676 676 if not dbuser:
677 677 log.warning(
678 678 'Unable to lookup user by id:%s api_key:%s username:%s',
679 679 user_id, api_key, username)
680 680 return False
681 681 if not dbuser.active:
682 682 log.debug('User `%s:%s` is inactive, skipping fill data',
683 683 username, user_id)
684 684 return False
685 685
686 686 log.debug('AuthUser: filling found user:%s data', dbuser)
687 687 user_data = dbuser.get_dict()
688 688
689 689 user_data.update({
690 690 # set explicit the safe escaped values
691 691 'first_name': dbuser.first_name,
692 692 'last_name': dbuser.last_name,
693 693 })
694 694
695 695 for k, v in user_data.items():
696 696 # properties of auth user we dont update
697 697 if k not in ['auth_tokens', 'permissions']:
698 698 setattr(auth_user, k, v)
699 699
700 # few extras
701 setattr(auth_user, 'feed_token', dbuser.feed_token)
702 700 except Exception:
703 701 log.error(traceback.format_exc())
704 702 auth_user.is_authenticated = False
705 703 return False
706 704
707 705 return True
708 706
709 707 def has_perm(self, user, perm):
710 708 perm = self._get_perm(perm)
711 709 user = self._get_user(user)
712 710
713 711 return UserToPerm.query().filter(UserToPerm.user == user)\
714 712 .filter(UserToPerm.permission == perm).scalar() is not None
715 713
716 714 def grant_perm(self, user, perm):
717 715 """
718 716 Grant user global permissions
719 717
720 718 :param user:
721 719 :param perm:
722 720 """
723 721 user = self._get_user(user)
724 722 perm = self._get_perm(perm)
725 723 # if this permission is already granted skip it
726 724 _perm = UserToPerm.query()\
727 725 .filter(UserToPerm.user == user)\
728 726 .filter(UserToPerm.permission == perm)\
729 727 .scalar()
730 728 if _perm:
731 729 return
732 730 new = UserToPerm()
733 731 new.user = user
734 732 new.permission = perm
735 733 self.sa.add(new)
736 734 return new
737 735
738 736 def revoke_perm(self, user, perm):
739 737 """
740 738 Revoke users global permissions
741 739
742 740 :param user:
743 741 :param perm:
744 742 """
745 743 user = self._get_user(user)
746 744 perm = self._get_perm(perm)
747 745
748 746 obj = UserToPerm.query()\
749 747 .filter(UserToPerm.user == user)\
750 748 .filter(UserToPerm.permission == perm)\
751 749 .scalar()
752 750 if obj:
753 751 self.sa.delete(obj)
754 752
755 753 def add_extra_email(self, user, email):
756 754 """
757 755 Adds email address to UserEmailMap
758 756
759 757 :param user:
760 758 :param email:
761 759 """
762 760
763 761 user = self._get_user(user)
764 762
765 763 obj = UserEmailMap()
766 764 obj.user = user
767 765 obj.email = email
768 766 self.sa.add(obj)
769 767 return obj
770 768
771 769 def delete_extra_email(self, user, email_id):
772 770 """
773 771 Removes email address from UserEmailMap
774 772
775 773 :param user:
776 774 :param email_id:
777 775 """
778 776 user = self._get_user(user)
779 777 obj = UserEmailMap.query().get(email_id)
780 778 if obj and obj.user_id == user.user_id:
781 779 self.sa.delete(obj)
782 780
783 781 def parse_ip_range(self, ip_range):
784 782 ip_list = []
785 783
786 784 def make_unique(value):
787 785 seen = []
788 786 return [c for c in value if not (c in seen or seen.append(c))]
789 787
790 788 # firsts split by commas
791 789 for ip_range in ip_range.split(','):
792 790 if not ip_range:
793 791 continue
794 792 ip_range = ip_range.strip()
795 793 if '-' in ip_range:
796 794 start_ip, end_ip = ip_range.split('-', 1)
797 795 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
798 796 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
799 797 parsed_ip_range = []
800 798
801 799 for index in xrange(int(start_ip), int(end_ip) + 1):
802 800 new_ip = ipaddress.ip_address(index)
803 801 parsed_ip_range.append(str(new_ip))
804 802 ip_list.extend(parsed_ip_range)
805 803 else:
806 804 ip_list.append(ip_range)
807 805
808 806 return make_unique(ip_list)
809 807
810 808 def add_extra_ip(self, user, ip, description=None):
811 809 """
812 810 Adds ip address to UserIpMap
813 811
814 812 :param user:
815 813 :param ip:
816 814 """
817 815
818 816 user = self._get_user(user)
819 817 obj = UserIpMap()
820 818 obj.user = user
821 819 obj.ip_addr = ip
822 820 obj.description = description
823 821 self.sa.add(obj)
824 822 return obj
825 823
826 824 def delete_extra_ip(self, user, ip_id):
827 825 """
828 826 Removes ip address from UserIpMap
829 827
830 828 :param user:
831 829 :param ip_id:
832 830 """
833 831 user = self._get_user(user)
834 832 obj = UserIpMap.query().get(ip_id)
835 833 if obj and obj.user_id == user.user_id:
836 834 self.sa.delete(obj)
837 835
838 836 def get_accounts_in_creation_order(self, current_user=None):
839 837 """
840 838 Get accounts in order of creation for deactivation for license limits
841 839
842 840 pick currently logged in user, and append to the list in position 0
843 841 pick all super-admins in order of creation date and add it to the list
844 842 pick all other accounts in order of creation and add it to the list.
845 843
846 844 Based on that list, the last accounts can be disabled as they are
847 845 created at the end and don't include any of the super admins as well
848 846 as the current user.
849 847
850 848 :param current_user: optionally current user running this operation
851 849 """
852 850
853 851 if not current_user:
854 852 current_user = get_current_rhodecode_user()
855 853 active_super_admins = [
856 854 x.user_id for x in User.query()
857 855 .filter(User.user_id != current_user.user_id)
858 856 .filter(User.active == true())
859 857 .filter(User.admin == true())
860 858 .order_by(User.created_on.asc())]
861 859
862 860 active_regular_users = [
863 861 x.user_id for x in User.query()
864 862 .filter(User.user_id != current_user.user_id)
865 863 .filter(User.active == true())
866 864 .filter(User.admin == false())
867 865 .order_by(User.created_on.asc())]
868 866
869 867 list_of_accounts = [current_user.user_id]
870 868 list_of_accounts += active_super_admins
871 869 list_of_accounts += active_regular_users
872 870
873 871 return list_of_accounts
874 872
875 873 def deactivate_last_users(self, expected_users, current_user=None):
876 874 """
877 875 Deactivate accounts that are over the license limits.
878 876 Algorithm of which accounts to disabled is based on the formula:
879 877
880 878 Get current user, then super admins in creation order, then regular
881 879 active users in creation order.
882 880
883 881 Using that list we mark all accounts from the end of it as inactive.
884 882 This way we block only latest created accounts.
885 883
886 884 :param expected_users: list of users in special order, we deactivate
887 885 the end N ammoun of users from that list
888 886 """
889 887
890 888 list_of_accounts = self.get_accounts_in_creation_order(
891 889 current_user=current_user)
892 890
893 891 for acc_id in list_of_accounts[expected_users + 1:]:
894 892 user = User.get(acc_id)
895 893 log.info('Deactivating account %s for license unlock', user)
896 894 user.active = False
897 895 Session().add(user)
898 896 Session().commit()
899 897
900 898 return
901 899
902 900 def get_user_log(self, user, filter_term):
903 901 user_log = UserLog.query()\
904 902 .filter(or_(UserLog.user_id == user.user_id,
905 903 UserLog.username == user.username))\
906 904 .options(joinedload(UserLog.user))\
907 905 .options(joinedload(UserLog.repository))\
908 906 .order_by(UserLog.action_date.desc())
909 907
910 908 user_log = user_log_filter(user_log, filter_term)
911 909 return user_log
General Comments 0
You need to be logged in to leave comments. Login now