##// END OF EJS Templates
auth: use cache_ttl from a plugin to also cache permissions....
marcink -
r2154:574d07a8 default
parent child Browse files
Show More
@@ -1,711 +1,730 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 modules
23 23 """
24 24
25 25 import colander
26 26 import copy
27 27 import logging
28 28 import time
29 29 import traceback
30 30 import warnings
31 31 import functools
32 32
33 33 from pyramid.threadlocal import get_current_registry
34 34 from zope.cachedescriptors.property import Lazy as LazyProperty
35 35
36 36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 38 from rhodecode.lib import caches
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.utils2 import md5_safe, safe_int
40 from rhodecode.lib.utils2 import safe_int
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.model.db import User
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.model.settings import SettingsModel
45 45 from rhodecode.model.user import UserModel
46 46 from rhodecode.model.user_group import UserGroupModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51 # auth types that authenticate() function can receive
52 52 VCS_TYPE = 'vcs'
53 53 HTTP_TYPE = 'http'
54 54
55 55
56 56 class hybrid_property(object):
57 57 """
58 58 a property decorator that works both for instance and class
59 59 """
60 60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 61 self.fget = fget
62 62 self.fset = fset
63 63 self.fdel = fdel
64 64 self.expr = expr or fget
65 65 functools.update_wrapper(self, fget)
66 66
67 67 def __get__(self, instance, owner):
68 68 if instance is None:
69 69 return self.expr(owner)
70 70 else:
71 71 return self.fget(instance)
72 72
73 73 def __set__(self, instance, value):
74 74 self.fset(instance, value)
75 75
76 76 def __delete__(self, instance):
77 77 self.fdel(instance)
78 78
79 79
80 80
81 81 class LazyFormencode(object):
82 82 def __init__(self, formencode_obj, *args, **kwargs):
83 83 self.formencode_obj = formencode_obj
84 84 self.args = args
85 85 self.kwargs = kwargs
86 86
87 87 def __call__(self, *args, **kwargs):
88 88 from inspect import isfunction
89 89 formencode_obj = self.formencode_obj
90 90 if isfunction(formencode_obj):
91 91 # case we wrap validators into functions
92 92 formencode_obj = self.formencode_obj(*args, **kwargs)
93 93 return formencode_obj(*self.args, **self.kwargs)
94 94
95 95
96 96 class RhodeCodeAuthPluginBase(object):
97 97 # cache the authentication request for N amount of seconds. Some kind
98 98 # of authentication methods are very heavy and it's very efficient to cache
99 99 # the result of a call. If it's set to None (default) cache is off
100 100 AUTH_CACHE_TTL = None
101 101 AUTH_CACHE = {}
102 102
103 103 auth_func_attrs = {
104 104 "username": "unique username",
105 105 "firstname": "first name",
106 106 "lastname": "last name",
107 107 "email": "email address",
108 108 "groups": '["list", "of", "groups"]',
109 109 "extern_name": "name in external source of record",
110 110 "extern_type": "type of external source of record",
111 111 "admin": 'True|False defines if user should be RhodeCode super admin',
112 112 "active":
113 113 'True|False defines active state of user internally for RhodeCode',
114 114 "active_from_extern":
115 115 "True|False\None, active state from the external auth, "
116 116 "None means use definition from RhodeCode extern_type active value"
117 117 }
118 118 # set on authenticate() method and via set_auth_type func.
119 119 auth_type = None
120 120
121 121 # set on authenticate() method and via set_calling_scope_repo, this is a
122 122 # calling scope repository when doing authentication most likely on VCS
123 123 # operations
124 124 acl_repo_name = None
125 125
126 126 # List of setting names to store encrypted. Plugins may override this list
127 127 # to store settings encrypted.
128 128 _settings_encrypted = []
129 129
130 130 # Mapping of python to DB settings model types. Plugins may override or
131 131 # extend this mapping.
132 132 _settings_type_map = {
133 133 colander.String: 'unicode',
134 134 colander.Integer: 'int',
135 135 colander.Boolean: 'bool',
136 136 colander.List: 'list',
137 137 }
138 138
139 139 # list of keys in settings that are unsafe to be logged, should be passwords
140 140 # or other crucial credentials
141 141 _settings_unsafe_keys = []
142 142
143 143 def __init__(self, plugin_id):
144 144 self._plugin_id = plugin_id
145 145
146 146 def __str__(self):
147 147 return self.get_id()
148 148
149 149 def _get_setting_full_name(self, name):
150 150 """
151 151 Return the full setting name used for storing values in the database.
152 152 """
153 153 # TODO: johbo: Using the name here is problematic. It would be good to
154 154 # introduce either new models in the database to hold Plugin and
155 155 # PluginSetting or to use the plugin id here.
156 156 return 'auth_{}_{}'.format(self.name, name)
157 157
158 158 def _get_setting_type(self, name):
159 159 """
160 160 Return the type of a setting. This type is defined by the SettingsModel
161 161 and determines how the setting is stored in DB. Optionally the suffix
162 162 `.encrypted` is appended to instruct SettingsModel to store it
163 163 encrypted.
164 164 """
165 165 schema_node = self.get_settings_schema().get(name)
166 166 db_type = self._settings_type_map.get(
167 167 type(schema_node.typ), 'unicode')
168 168 if name in self._settings_encrypted:
169 169 db_type = '{}.encrypted'.format(db_type)
170 170 return db_type
171 171
172 172 @LazyProperty
173 173 def plugin_settings(self):
174 174 settings = SettingsModel().get_all_settings()
175 175 return settings
176 176
177 177 def is_enabled(self):
178 178 """
179 179 Returns true if this plugin is enabled. An enabled plugin can be
180 180 configured in the admin interface but it is not consulted during
181 181 authentication.
182 182 """
183 183 auth_plugins = SettingsModel().get_auth_plugins()
184 184 return self.get_id() in auth_plugins
185 185
186 186 def is_active(self):
187 187 """
188 188 Returns true if the plugin is activated. An activated plugin is
189 189 consulted during authentication, assumed it is also enabled.
190 190 """
191 191 return self.get_setting_by_name('enabled')
192 192
193 193 def get_id(self):
194 194 """
195 195 Returns the plugin id.
196 196 """
197 197 return self._plugin_id
198 198
199 199 def get_display_name(self):
200 200 """
201 201 Returns a translation string for displaying purposes.
202 202 """
203 203 raise NotImplementedError('Not implemented in base class')
204 204
205 205 def get_settings_schema(self):
206 206 """
207 207 Returns a colander schema, representing the plugin settings.
208 208 """
209 209 return AuthnPluginSettingsSchemaBase()
210 210
211 211 def get_setting_by_name(self, name, default=None):
212 212 """
213 213 Returns a plugin setting by name.
214 214 """
215 215 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
216 216 plugin_settings = self.plugin_settings
217 217
218 218 return plugin_settings.get(full_name) or default
219 219
220 220 def create_or_update_setting(self, name, value):
221 221 """
222 222 Create or update a setting for this plugin in the persistent storage.
223 223 """
224 224 full_name = self._get_setting_full_name(name)
225 225 type_ = self._get_setting_type(name)
226 226 db_setting = SettingsModel().create_or_update_setting(
227 227 full_name, value, type_)
228 228 return db_setting.app_settings_value
229 229
230 230 def get_settings(self):
231 231 """
232 232 Returns the plugin settings as dictionary.
233 233 """
234 234 settings = {}
235 235 for node in self.get_settings_schema():
236 236 settings[node.name] = self.get_setting_by_name(node.name)
237 237 return settings
238 238
239 239 def log_safe_settings(self, settings):
240 240 """
241 241 returns a log safe representation of settings, without any secrets
242 242 """
243 243 settings_copy = copy.deepcopy(settings)
244 244 for k in self._settings_unsafe_keys:
245 245 if k in settings_copy:
246 246 del settings_copy[k]
247 247 return settings_copy
248 248
249 249 @property
250 250 def validators(self):
251 251 """
252 252 Exposes RhodeCode validators modules
253 253 """
254 254 # this is a hack to overcome issues with pylons threadlocals and
255 255 # translator object _() not being registered properly.
256 256 class LazyCaller(object):
257 257 def __init__(self, name):
258 258 self.validator_name = name
259 259
260 260 def __call__(self, *args, **kwargs):
261 261 from rhodecode.model import validators as v
262 262 obj = getattr(v, self.validator_name)
263 263 # log.debug('Initializing lazy formencode object: %s', obj)
264 264 return LazyFormencode(obj, *args, **kwargs)
265 265
266 266 class ProxyGet(object):
267 267 def __getattribute__(self, name):
268 268 return LazyCaller(name)
269 269
270 270 return ProxyGet()
271 271
272 272 @hybrid_property
273 273 def name(self):
274 274 """
275 275 Returns the name of this authentication plugin.
276 276
277 277 :returns: string
278 278 """
279 279 raise NotImplementedError("Not implemented in base class")
280 280
281 281 def get_url_slug(self):
282 282 """
283 283 Returns a slug which should be used when constructing URLs which refer
284 284 to this plugin. By default it returns the plugin name. If the name is
285 285 not suitable for using it in an URL the plugin should override this
286 286 method.
287 287 """
288 288 return self.name
289 289
290 290 @property
291 291 def is_headers_auth(self):
292 292 """
293 293 Returns True if this authentication plugin uses HTTP headers as
294 294 authentication method.
295 295 """
296 296 return False
297 297
298 298 @hybrid_property
299 299 def is_container_auth(self):
300 300 """
301 301 Deprecated method that indicates if this authentication plugin uses
302 302 HTTP headers as authentication method.
303 303 """
304 304 warnings.warn(
305 305 'Use is_headers_auth instead.', category=DeprecationWarning)
306 306 return self.is_headers_auth
307 307
308 308 @hybrid_property
309 309 def allows_creating_users(self):
310 310 """
311 311 Defines if Plugin allows users to be created on-the-fly when
312 312 authentication is called. Controls how external plugins should behave
313 313 in terms if they are allowed to create new users, or not. Base plugins
314 314 should not be allowed to, but External ones should be !
315 315
316 316 :return: bool
317 317 """
318 318 return False
319 319
320 320 def set_auth_type(self, auth_type):
321 321 self.auth_type = auth_type
322 322
323 323 def set_calling_scope_repo(self, acl_repo_name):
324 324 self.acl_repo_name = acl_repo_name
325 325
326 326 def allows_authentication_from(
327 327 self, user, allows_non_existing_user=True,
328 328 allowed_auth_plugins=None, allowed_auth_sources=None):
329 329 """
330 330 Checks if this authentication module should accept a request for
331 331 the current user.
332 332
333 333 :param user: user object fetched using plugin's get_user() method.
334 334 :param allows_non_existing_user: if True, don't allow the
335 335 user to be empty, meaning not existing in our database
336 336 :param allowed_auth_plugins: if provided, users extern_type will be
337 337 checked against a list of provided extern types, which are plugin
338 338 auth_names in the end
339 339 :param allowed_auth_sources: authentication type allowed,
340 340 `http` or `vcs` default is both.
341 341 defines if plugin will accept only http authentication vcs
342 342 authentication(git/hg) or both
343 343 :returns: boolean
344 344 """
345 345 if not user and not allows_non_existing_user:
346 346 log.debug('User is empty but plugin does not allow empty users,'
347 347 'not allowed to authenticate')
348 348 return False
349 349
350 350 expected_auth_plugins = allowed_auth_plugins or [self.name]
351 351 if user and (user.extern_type and
352 352 user.extern_type not in expected_auth_plugins):
353 353 log.debug(
354 354 'User `%s` is bound to `%s` auth type. Plugin allows only '
355 355 '%s, skipping', user, user.extern_type, expected_auth_plugins)
356 356
357 357 return False
358 358
359 359 # by default accept both
360 360 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
361 361 if self.auth_type not in expected_auth_from:
362 362 log.debug('Current auth source is %s but plugin only allows %s',
363 363 self.auth_type, expected_auth_from)
364 364 return False
365 365
366 366 return True
367 367
368 368 def get_user(self, username=None, **kwargs):
369 369 """
370 370 Helper method for user fetching in plugins, by default it's using
371 371 simple fetch by username, but this method can be custimized in plugins
372 372 eg. headers auth plugin to fetch user by environ params
373 373
374 374 :param username: username if given to fetch from database
375 375 :param kwargs: extra arguments needed for user fetching.
376 376 """
377 377 user = None
378 378 log.debug(
379 379 'Trying to fetch user `%s` from RhodeCode database', username)
380 380 if username:
381 381 user = User.get_by_username(username)
382 382 if not user:
383 383 log.debug('User not found, fallback to fetch user in '
384 384 'case insensitive mode')
385 385 user = User.get_by_username(username, case_insensitive=True)
386 386 else:
387 387 log.debug('provided username:`%s` is empty skipping...', username)
388 388 if not user:
389 389 log.debug('User `%s` not found in database', username)
390 390 else:
391 391 log.debug('Got DB user:%s', user)
392 392 return user
393 393
394 394 def user_activation_state(self):
395 395 """
396 396 Defines user activation state when creating new users
397 397
398 398 :returns: boolean
399 399 """
400 400 raise NotImplementedError("Not implemented in base class")
401 401
402 402 def auth(self, userobj, username, passwd, settings, **kwargs):
403 403 """
404 404 Given a user object (which may be null), username, a plaintext
405 405 password, and a settings object (containing all the keys needed as
406 406 listed in settings()), authenticate this user's login attempt.
407 407
408 408 Return None on failure. On success, return a dictionary of the form:
409 409
410 410 see: RhodeCodeAuthPluginBase.auth_func_attrs
411 411 This is later validated for correctness
412 412 """
413 413 raise NotImplementedError("not implemented in base class")
414 414
415 415 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
416 416 """
417 417 Wrapper to call self.auth() that validates call on it
418 418
419 419 :param userobj: userobj
420 420 :param username: username
421 421 :param passwd: plaintext password
422 422 :param settings: plugin settings
423 423 """
424 424 auth = self.auth(userobj, username, passwd, settings, **kwargs)
425 425 if auth:
426 auth['_plugin'] = self.name
427 auth['_ttl_cache'] = self.get_ttl_cache(settings)
426 428 # check if hash should be migrated ?
427 429 new_hash = auth.get('_hash_migrate')
428 430 if new_hash:
429 431 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
430 432 return self._validate_auth_return(auth)
433
431 434 return auth
432 435
433 436 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
434 437 new_hash_cypher = _RhodeCodeCryptoBCrypt()
435 438 # extra checks, so make sure new hash is correct.
436 439 password_encoded = safe_str(password)
437 440 if new_hash and new_hash_cypher.hash_check(
438 441 password_encoded, new_hash):
439 442 cur_user = User.get_by_username(username)
440 443 cur_user.password = new_hash
441 444 Session().add(cur_user)
442 445 Session().flush()
443 446 log.info('Migrated user %s hash to bcrypt', cur_user)
444 447
445 448 def _validate_auth_return(self, ret):
446 449 if not isinstance(ret, dict):
447 450 raise Exception('returned value from auth must be a dict')
448 451 for k in self.auth_func_attrs:
449 452 if k not in ret:
450 453 raise Exception('Missing %s attribute from returned data' % k)
451 454 return ret
452 455
456 def get_ttl_cache(self, settings=None):
457 plugin_settings = settings or self.get_settings()
458 cache_ttl = 0
459
460 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
461 # plugin cache set inside is more important than the settings value
462 cache_ttl = self.AUTH_CACHE_TTL
463 elif plugin_settings.get('cache_ttl'):
464 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
465
466 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
467 return plugin_cache_active, cache_ttl
468
453 469
454 470 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
455 471
456 472 @hybrid_property
457 473 def allows_creating_users(self):
458 474 return True
459 475
460 476 def use_fake_password(self):
461 477 """
462 478 Return a boolean that indicates whether or not we should set the user's
463 479 password to a random value when it is authenticated by this plugin.
464 480 If your plugin provides authentication, then you will generally
465 481 want this.
466 482
467 483 :returns: boolean
468 484 """
469 485 raise NotImplementedError("Not implemented in base class")
470 486
471 487 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
472 488 # at this point _authenticate calls plugin's `auth()` function
473 489 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
474 490 userobj, username, passwd, settings, **kwargs)
475 491
476 492 if auth:
477 493 # maybe plugin will clean the username ?
478 494 # we should use the return value
479 495 username = auth['username']
480 496
481 497 # if external source tells us that user is not active, we should
482 498 # skip rest of the process. This can prevent from creating users in
483 499 # RhodeCode when using external authentication, but if it's
484 500 # inactive user we shouldn't create that user anyway
485 501 if auth['active_from_extern'] is False:
486 502 log.warning(
487 503 "User %s authenticated against %s, but is inactive",
488 504 username, self.__module__)
489 505 return None
490 506
491 507 cur_user = User.get_by_username(username, case_insensitive=True)
492 508 is_user_existing = cur_user is not None
493 509
494 510 if is_user_existing:
495 511 log.debug('Syncing user `%s` from '
496 512 '`%s` plugin', username, self.name)
497 513 else:
498 514 log.debug('Creating non existing user `%s` from '
499 515 '`%s` plugin', username, self.name)
500 516
501 517 if self.allows_creating_users:
502 518 log.debug('Plugin `%s` allows to '
503 519 'create new users', self.name)
504 520 else:
505 521 log.debug('Plugin `%s` does not allow to '
506 522 'create new users', self.name)
507 523
508 524 user_parameters = {
509 525 'username': username,
510 526 'email': auth["email"],
511 527 'firstname': auth["firstname"],
512 528 'lastname': auth["lastname"],
513 529 'active': auth["active"],
514 530 'admin': auth["admin"],
515 531 'extern_name': auth["extern_name"],
516 532 'extern_type': self.name,
517 533 'plugin': self,
518 534 'allow_to_create_user': self.allows_creating_users,
519 535 }
520 536
521 537 if not is_user_existing:
522 538 if self.use_fake_password():
523 539 # Randomize the PW because we don't need it, but don't want
524 540 # them blank either
525 541 passwd = PasswordGenerator().gen_password(length=16)
526 542 user_parameters['password'] = passwd
527 543 else:
528 544 # Since the password is required by create_or_update method of
529 545 # UserModel, we need to set it explicitly.
530 546 # The create_or_update method is smart and recognises the
531 547 # password hashes as well.
532 548 user_parameters['password'] = cur_user.password
533 549
534 550 # we either create or update users, we also pass the flag
535 551 # that controls if this method can actually do that.
536 552 # raises NotAllowedToCreateUserError if it cannot, and we try to.
537 553 user = UserModel().create_or_update(**user_parameters)
538 554 Session().flush()
539 555 # enforce user is just in given groups, all of them has to be ones
540 556 # created from plugins. We store this info in _group_data JSON
541 557 # field
542 558 try:
543 559 groups = auth['groups'] or []
544 560 log.debug(
545 561 'Performing user_group sync based on set `%s` '
546 562 'returned by this plugin', groups)
547 563 UserGroupModel().enforce_groups(user, groups, self.name)
548 564 except Exception:
549 565 # for any reason group syncing fails, we should
550 566 # proceed with login
551 567 log.error(traceback.format_exc())
552 568 Session().commit()
553 569 return auth
554 570
555 571
556 572 def loadplugin(plugin_id):
557 573 """
558 574 Loads and returns an instantiated authentication plugin.
559 575 Returns the RhodeCodeAuthPluginBase subclass on success,
560 576 or None on failure.
561 577 """
562 578 # TODO: Disusing pyramids thread locals to retrieve the registry.
563 579 authn_registry = get_authn_registry()
564 580 plugin = authn_registry.get_plugin(plugin_id)
565 581 if plugin is None:
566 582 log.error('Authentication plugin not found: "%s"', plugin_id)
567 583 return plugin
568 584
569 585
570 586 def get_authn_registry(registry=None):
571 587 registry = registry or get_current_registry()
572 588 authn_registry = registry.getUtility(IAuthnPluginRegistry)
573 589 return authn_registry
574 590
575 591
576 592 def get_auth_cache_manager(custom_ttl=None):
577 593 return caches.get_cache_manager(
578 594 'auth_plugins', 'rhodecode.authentication', custom_ttl)
579 595
580 596
597 def get_perms_cache_manager(custom_ttl=None):
598 return caches.get_cache_manager(
599 'auth_plugins', 'rhodecode.permissions', custom_ttl)
600
601
581 602 def authenticate(username, password, environ=None, auth_type=None,
582 603 skip_missing=False, registry=None, acl_repo_name=None):
583 604 """
584 605 Authentication function used for access control,
585 606 It tries to authenticate based on enabled authentication modules.
586 607
587 608 :param username: username can be empty for headers auth
588 609 :param password: password can be empty for headers auth
589 610 :param environ: environ headers passed for headers auth
590 611 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
591 612 :param skip_missing: ignores plugins that are in db but not in environment
592 613 :returns: None if auth failed, plugin_user dict if auth is correct
593 614 """
594 615 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
595 616 raise ValueError('auth type must be on of http, vcs got "%s" instead'
596 617 % auth_type)
597 618 headers_only = environ and not (username and password)
598 619
599 620 authn_registry = get_authn_registry(registry)
600 621 plugins_to_check = authn_registry.get_plugins_for_authentication()
601 622 log.debug('Starting ordered authentication chain using %s plugins',
602 623 plugins_to_check)
603 624 for plugin in plugins_to_check:
604 625 plugin.set_auth_type(auth_type)
605 626 plugin.set_calling_scope_repo(acl_repo_name)
606 627
607 628 if headers_only and not plugin.is_headers_auth:
608 629 log.debug('Auth type is for headers only and plugin `%s` is not '
609 630 'headers plugin, skipping...', plugin.get_id())
610 631 continue
611 632
612 633 # load plugin settings from RhodeCode database
613 634 plugin_settings = plugin.get_settings()
614 635 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
615 636 log.debug('Plugin settings:%s', plugin_sanitized_settings)
616 637
617 638 log.debug('Trying authentication using ** %s **', plugin.get_id())
618 639 # use plugin's method of user extraction.
619 640 user = plugin.get_user(username, environ=environ,
620 641 settings=plugin_settings)
621 642 display_user = user.username if user else username
622 643 log.debug(
623 644 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
624 645
625 646 if not plugin.allows_authentication_from(user):
626 647 log.debug('Plugin %s does not accept user `%s` for authentication',
627 648 plugin.get_id(), display_user)
628 649 continue
629 650 else:
630 651 log.debug('Plugin %s accepted user `%s` for authentication',
631 652 plugin.get_id(), display_user)
632 653
633 654 log.info('Authenticating user `%s` using %s plugin',
634 655 display_user, plugin.get_id())
635 656
636 _cache_ttl = 0
637
638 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
639 # plugin cache set inside is more important than the settings value
640 _cache_ttl = plugin.AUTH_CACHE_TTL
641 elif plugin_settings.get('cache_ttl'):
642 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
643
644 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
657 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
645 658
646 659 # get instance of cache manager configured for a namespace
647 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
660 cache_manager = get_auth_cache_manager(custom_ttl=cache_ttl)
648 661
649 662 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
650 plugin.get_id(), plugin_cache_active, _cache_ttl)
663 plugin.get_id(), plugin_cache_active, cache_ttl)
651 664
652 665 # for environ based password can be empty, but then the validation is
653 666 # on the server that fills in the env data needed for authentication
654 _password_hash = md5_safe(plugin.name + username + (password or ''))
667
668 _password_hash = caches.compute_key_from_params(
669 plugin.name, username, (password or ''))
655 670
656 671 # _authenticate is a wrapper for .auth() method of plugin.
657 672 # it checks if .auth() sends proper data.
658 673 # For RhodeCodeExternalAuthPlugin it also maps users to
659 674 # Database and maps the attributes returned from .auth()
660 675 # to RhodeCode database. If this function returns data
661 676 # then auth is correct.
662 677 start = time.time()
663 678 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
664 679
665 680 def auth_func():
666 681 """
667 682 This function is used internally in Cache of Beaker to calculate
668 683 Results
669 684 """
685 log.debug('auth: calculating password access now...')
670 686 return plugin._authenticate(
671 687 user, username, password, plugin_settings,
672 688 environ=environ or {})
673 689
674 690 if plugin_cache_active:
691 log.debug('Trying to fetch cached auth by %s', _password_hash[:6])
675 692 plugin_user = cache_manager.get(
676 693 _password_hash, createfunc=auth_func)
677 694 else:
678 695 plugin_user = auth_func()
679 696
680 697 auth_time = time.time() - start
681 698 log.debug('Authentication for plugin `%s` completed in %.3fs, '
682 699 'expiration time of fetched cache %.1fs.',
683 plugin.get_id(), auth_time, _cache_ttl)
700 plugin.get_id(), auth_time, cache_ttl)
684 701
685 702 log.debug('PLUGIN USER DATA: %s', plugin_user)
686 703
687 704 if plugin_user:
688 705 log.debug('Plugin returned proper authentication data')
689 706 return plugin_user
690 707 # we failed to Auth because .auth() method didn't return proper user
691 708 log.debug("User `%s` failed to authenticate against %s",
692 709 display_user, plugin.get_id())
710
711 # case when we failed to authenticate against all defined plugins
693 712 return None
694 713
695 714
696 715 def chop_at(s, sub, inclusive=False):
697 716 """Truncate string ``s`` at the first occurrence of ``sub``.
698 717
699 718 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
700 719
701 720 >>> chop_at("plutocratic brats", "rat")
702 721 'plutoc'
703 722 >>> chop_at("plutocratic brats", "rat", True)
704 723 'plutocrat'
705 724 """
706 725 pos = s.find(sub)
707 726 if pos == -1:
708 727 return s
709 728 if inclusive:
710 729 return s[:pos+len(sub)]
711 730 return s[:pos]
@@ -1,52 +1,51 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-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 import colander
22 22
23 23 from rhodecode.translation import _
24 24
25 25
26 26 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
27 27 """
28 28 This base schema is intended for use in authentication plugins.
29 29 It adds a few default settings (e.g., "enabled"), so that plugin
30 30 authors don't have to maintain a bunch of boilerplate.
31 31 """
32 32 enabled = colander.SchemaNode(
33 33 colander.Bool(),
34 34 default=False,
35 35 description=_('Enable or disable this authentication plugin.'),
36 36 missing=False,
37 37 title=_('Enabled'),
38 38 widget='bool',
39 39 )
40 40 cache_ttl = colander.SchemaNode(
41 41 colander.Int(),
42 42 default=0,
43 description=_('Amount of seconds to cache the authentication response'
44 'call for this plugin. \n'
45 'Useful for long calls like LDAP to improve the '
46 'performance of the authentication system '
47 '(0 means disabled).'),
43 description=_('Amount of seconds to cache the authentication and '
44 'permissions check response call for this plugin. \n'
45 'Useful for expensive calls like LDAP to improve the '
46 'performance of the system (0 means disabled).'),
48 47 missing=0,
49 48 title=_('Auth Cache TTL'),
50 49 validator=colander.Range(min=0, max=None),
51 50 widget='int',
52 51 )
@@ -1,630 +1,631 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 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 32 import pyramid.threadlocal
33 33
34 34 from paste.auth.basic import AuthBasicAuthenticator
35 35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37 37
38 38 import rhodecode
39 39 from rhodecode.authentication.base import VCS_TYPE
40 40 from rhodecode.lib import auth, utils2
41 41 from rhodecode.lib import helpers as h
42 42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 43 from rhodecode.lib.exceptions import UserCreationError
44 44 from rhodecode.lib.utils import (
45 45 get_repo_slug, set_rhodecode_config, password_changed,
46 46 get_enabled_hook_classes)
47 47 from rhodecode.lib.utils2 import (
48 48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
49 49 from rhodecode.model import meta
50 50 from rhodecode.model.db import Repository, User, ChangesetComment
51 51 from rhodecode.model.notification import NotificationModel
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
54 54
55 55 # NOTE(marcink): remove after base controller is no longer required
56 56 from pylons.controllers import WSGIController
57 57 from pylons.i18n import translation
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 # hack to make the migration to pyramid easier
63 63 def render(template_name, extra_vars=None, cache_key=None,
64 64 cache_type=None, cache_expire=None):
65 65 """Render a template with Mako
66 66
67 67 Accepts the cache options ``cache_key``, ``cache_type``, and
68 68 ``cache_expire``.
69 69
70 70 """
71 71 from pylons.templating import literal
72 72 from pylons.templating import cached_template, pylons_globals
73 73
74 74 # Create a render callable for the cache function
75 75 def render_template():
76 76 # Pull in extra vars if needed
77 77 globs = extra_vars or {}
78 78
79 79 # Second, get the globals
80 80 globs.update(pylons_globals())
81 81
82 82 globs['_ungettext'] = globs['ungettext']
83 83 # Grab a template reference
84 84 template = globs['app_globals'].mako_lookup.get_template(template_name)
85 85
86 86 return literal(template.render_unicode(**globs))
87 87
88 88 return cached_template(template_name, render_template, cache_key=cache_key,
89 89 cache_type=cache_type, cache_expire=cache_expire)
90 90
91 91 def _filter_proxy(ip):
92 92 """
93 93 Passed in IP addresses in HEADERS can be in a special format of multiple
94 94 ips. Those comma separated IPs are passed from various proxies in the
95 95 chain of request processing. The left-most being the original client.
96 96 We only care about the first IP which came from the org. client.
97 97
98 98 :param ip: ip string from headers
99 99 """
100 100 if ',' in ip:
101 101 _ips = ip.split(',')
102 102 _first_ip = _ips[0].strip()
103 103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
104 104 return _first_ip
105 105 return ip
106 106
107 107
108 108 def _filter_port(ip):
109 109 """
110 110 Removes a port from ip, there are 4 main cases to handle here.
111 111 - ipv4 eg. 127.0.0.1
112 112 - ipv6 eg. ::1
113 113 - ipv4+port eg. 127.0.0.1:8080
114 114 - ipv6+port eg. [::1]:8080
115 115
116 116 :param ip:
117 117 """
118 118 def is_ipv6(ip_addr):
119 119 if hasattr(socket, 'inet_pton'):
120 120 try:
121 121 socket.inet_pton(socket.AF_INET6, ip_addr)
122 122 except socket.error:
123 123 return False
124 124 else:
125 125 # fallback to ipaddress
126 126 try:
127 127 ipaddress.IPv6Address(safe_unicode(ip_addr))
128 128 except Exception:
129 129 return False
130 130 return True
131 131
132 132 if ':' not in ip: # must be ipv4 pure ip
133 133 return ip
134 134
135 135 if '[' in ip and ']' in ip: # ipv6 with port
136 136 return ip.split(']')[0][1:].lower()
137 137
138 138 # must be ipv6 or ipv4 with port
139 139 if is_ipv6(ip):
140 140 return ip
141 141 else:
142 142 ip, _port = ip.split(':')[:2] # means ipv4+port
143 143 return ip
144 144
145 145
146 146 def get_ip_addr(environ):
147 147 proxy_key = 'HTTP_X_REAL_IP'
148 148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
149 149 def_key = 'REMOTE_ADDR'
150 150 _filters = lambda x: _filter_port(_filter_proxy(x))
151 151
152 152 ip = environ.get(proxy_key)
153 153 if ip:
154 154 return _filters(ip)
155 155
156 156 ip = environ.get(proxy_key2)
157 157 if ip:
158 158 return _filters(ip)
159 159
160 160 ip = environ.get(def_key, '0.0.0.0')
161 161 return _filters(ip)
162 162
163 163
164 164 def get_server_ip_addr(environ, log_errors=True):
165 165 hostname = environ.get('SERVER_NAME')
166 166 try:
167 167 return socket.gethostbyname(hostname)
168 168 except Exception as e:
169 169 if log_errors:
170 170 # in some cases this lookup is not possible, and we don't want to
171 171 # make it an exception in logs
172 172 log.exception('Could not retrieve server ip address: %s', e)
173 173 return hostname
174 174
175 175
176 176 def get_server_port(environ):
177 177 return environ.get('SERVER_PORT')
178 178
179 179
180 180 def get_access_path(environ):
181 181 path = environ.get('PATH_INFO')
182 182 org_req = environ.get('pylons.original_request')
183 183 if org_req:
184 184 path = org_req.environ.get('PATH_INFO')
185 185 return path
186 186
187 187
188 188 def get_user_agent(environ):
189 189 return environ.get('HTTP_USER_AGENT')
190 190
191 191
192 192 def vcs_operation_context(
193 193 environ, repo_name, username, action, scm, check_locking=True,
194 194 is_shadow_repo=False):
195 195 """
196 196 Generate the context for a vcs operation, e.g. push or pull.
197 197
198 198 This context is passed over the layers so that hooks triggered by the
199 199 vcs operation know details like the user, the user's IP address etc.
200 200
201 201 :param check_locking: Allows to switch of the computation of the locking
202 202 data. This serves mainly the need of the simplevcs middleware to be
203 203 able to disable this for certain operations.
204 204
205 205 """
206 206 # Tri-state value: False: unlock, None: nothing, True: lock
207 207 make_lock = None
208 208 locked_by = [None, None, None]
209 209 is_anonymous = username == User.DEFAULT_USER
210 210 if not is_anonymous and check_locking:
211 211 log.debug('Checking locking on repository "%s"', repo_name)
212 212 user = User.get_by_username(username)
213 213 repo = Repository.get_by_repo_name(repo_name)
214 214 make_lock, __, locked_by = repo.get_locking_state(
215 215 action, user.user_id)
216 216
217 217 settings_model = VcsSettingsModel(repo=repo_name)
218 218 ui_settings = settings_model.get_ui_settings()
219 219
220 220 extras = {
221 221 'ip': get_ip_addr(environ),
222 222 'username': username,
223 223 'action': action,
224 224 'repository': repo_name,
225 225 'scm': scm,
226 226 'config': rhodecode.CONFIG['__file__'],
227 227 'make_lock': make_lock,
228 228 'locked_by': locked_by,
229 229 'server_url': utils2.get_server_url(environ),
230 230 'user_agent': get_user_agent(environ),
231 231 'hooks': get_enabled_hook_classes(ui_settings),
232 232 'is_shadow_repo': is_shadow_repo,
233 233 }
234 234 return extras
235 235
236 236
237 237 class BasicAuth(AuthBasicAuthenticator):
238 238
239 239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
240 240 initial_call_detection=False, acl_repo_name=None):
241 241 self.realm = realm
242 242 self.initial_call = initial_call_detection
243 243 self.authfunc = authfunc
244 244 self.registry = registry
245 245 self.acl_repo_name = acl_repo_name
246 246 self._rc_auth_http_code = auth_http_code
247 247
248 248 def _get_response_from_code(self, http_code):
249 249 try:
250 250 return get_exception(safe_int(http_code))
251 251 except Exception:
252 252 log.exception('Failed to fetch response for code %s' % http_code)
253 253 return HTTPForbidden
254 254
255 255 def get_rc_realm(self):
256 256 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
257 257
258 258 def build_authentication(self):
259 259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
260 260 if self._rc_auth_http_code and not self.initial_call:
261 261 # return alternative HTTP code if alternative http return code
262 262 # is specified in RhodeCode config, but ONLY if it's not the
263 263 # FIRST call
264 264 custom_response_klass = self._get_response_from_code(
265 265 self._rc_auth_http_code)
266 266 return custom_response_klass(headers=head)
267 267 return HTTPUnauthorized(headers=head)
268 268
269 269 def authenticate(self, environ):
270 270 authorization = AUTHORIZATION(environ)
271 271 if not authorization:
272 272 return self.build_authentication()
273 273 (authmeth, auth) = authorization.split(' ', 1)
274 274 if 'basic' != authmeth.lower():
275 275 return self.build_authentication()
276 276 auth = auth.strip().decode('base64')
277 277 _parts = auth.split(':', 1)
278 278 if len(_parts) == 2:
279 279 username, password = _parts
280 if self.authfunc(
280 auth_data = self.authfunc(
281 281 username, password, environ, VCS_TYPE,
282 registry=self.registry, acl_repo_name=self.acl_repo_name):
283 return username
282 registry=self.registry, acl_repo_name=self.acl_repo_name)
283 if auth_data:
284 return {'username': username, 'auth_data': auth_data}
284 285 if username and password:
285 286 # we mark that we actually executed authentication once, at
286 287 # that point we can use the alternative auth code
287 288 self.initial_call = False
288 289
289 290 return self.build_authentication()
290 291
291 292 __call__ = authenticate
292 293
293 294
294 295 def calculate_version_hash(config):
295 296 return md5(
296 297 config.get('beaker.session.secret', '') +
297 298 rhodecode.__version__)[:8]
298 299
299 300
300 301 def get_current_lang(request):
301 302 # NOTE(marcink): remove after pyramid move
302 303 try:
303 304 return translation.get_lang()[0]
304 305 except:
305 306 pass
306 307
307 308 return getattr(request, '_LOCALE_', request.locale_name)
308 309
309 310
310 311 def attach_context_attributes(context, request, user_id):
311 312 """
312 313 Attach variables into template context called `c`, please note that
313 314 request could be pylons or pyramid request in here.
314 315 """
315 316 # NOTE(marcink): remove check after pyramid migration
316 317 if hasattr(request, 'registry'):
317 318 config = request.registry.settings
318 319 else:
319 320 from pylons import config
320 321
321 322 rc_config = SettingsModel().get_all_settings(cache=True)
322 323
323 324 context.rhodecode_version = rhodecode.__version__
324 325 context.rhodecode_edition = config.get('rhodecode.edition')
325 326 # unique secret + version does not leak the version but keep consistency
326 327 context.rhodecode_version_hash = calculate_version_hash(config)
327 328
328 329 # Default language set for the incoming request
329 330 context.language = get_current_lang(request)
330 331
331 332 # Visual options
332 333 context.visual = AttributeDict({})
333 334
334 335 # DB stored Visual Items
335 336 context.visual.show_public_icon = str2bool(
336 337 rc_config.get('rhodecode_show_public_icon'))
337 338 context.visual.show_private_icon = str2bool(
338 339 rc_config.get('rhodecode_show_private_icon'))
339 340 context.visual.stylify_metatags = str2bool(
340 341 rc_config.get('rhodecode_stylify_metatags'))
341 342 context.visual.dashboard_items = safe_int(
342 343 rc_config.get('rhodecode_dashboard_items', 100))
343 344 context.visual.admin_grid_items = safe_int(
344 345 rc_config.get('rhodecode_admin_grid_items', 100))
345 346 context.visual.repository_fields = str2bool(
346 347 rc_config.get('rhodecode_repository_fields'))
347 348 context.visual.show_version = str2bool(
348 349 rc_config.get('rhodecode_show_version'))
349 350 context.visual.use_gravatar = str2bool(
350 351 rc_config.get('rhodecode_use_gravatar'))
351 352 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
352 353 context.visual.default_renderer = rc_config.get(
353 354 'rhodecode_markup_renderer', 'rst')
354 355 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
355 356 context.visual.rhodecode_support_url = \
356 357 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
357 358
358 359 context.visual.affected_files_cut_off = 60
359 360
360 361 context.pre_code = rc_config.get('rhodecode_pre_code')
361 362 context.post_code = rc_config.get('rhodecode_post_code')
362 363 context.rhodecode_name = rc_config.get('rhodecode_title')
363 364 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
364 365 # if we have specified default_encoding in the request, it has more
365 366 # priority
366 367 if request.GET.get('default_encoding'):
367 368 context.default_encodings.insert(0, request.GET.get('default_encoding'))
368 369 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
369 370
370 371 # INI stored
371 372 context.labs_active = str2bool(
372 373 config.get('labs_settings_active', 'false'))
373 374 context.visual.allow_repo_location_change = str2bool(
374 375 config.get('allow_repo_location_change', True))
375 376 context.visual.allow_custom_hooks_settings = str2bool(
376 377 config.get('allow_custom_hooks_settings', True))
377 378 context.debug_style = str2bool(config.get('debug_style', False))
378 379
379 380 context.rhodecode_instanceid = config.get('instance_id')
380 381
381 382 context.visual.cut_off_limit_diff = safe_int(
382 383 config.get('cut_off_limit_diff'))
383 384 context.visual.cut_off_limit_file = safe_int(
384 385 config.get('cut_off_limit_file'))
385 386
386 387 # AppEnlight
387 388 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
388 389 context.appenlight_api_public_key = config.get(
389 390 'appenlight.api_public_key', '')
390 391 context.appenlight_server_url = config.get('appenlight.server_url', '')
391 392
392 393 # JS template context
393 394 context.template_context = {
394 395 'repo_name': None,
395 396 'repo_type': None,
396 397 'repo_landing_commit': None,
397 398 'rhodecode_user': {
398 399 'username': None,
399 400 'email': None,
400 401 'notification_status': False
401 402 },
402 403 'visual': {
403 404 'default_renderer': None
404 405 },
405 406 'commit_data': {
406 407 'commit_id': None
407 408 },
408 409 'pull_request_data': {'pull_request_id': None},
409 410 'timeago': {
410 411 'refresh_time': 120 * 1000,
411 412 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
412 413 },
413 414 'pyramid_dispatch': {
414 415
415 416 },
416 417 'extra': {'plugins': {}}
417 418 }
418 419 # END CONFIG VARS
419 420
420 421 # TODO: This dosn't work when called from pylons compatibility tween.
421 422 # Fix this and remove it from base controller.
422 423 # context.repo_name = get_repo_slug(request) # can be empty
423 424
424 425 diffmode = 'sideside'
425 426 if request.GET.get('diffmode'):
426 427 if request.GET['diffmode'] == 'unified':
427 428 diffmode = 'unified'
428 429 elif request.session.get('diffmode'):
429 430 diffmode = request.session['diffmode']
430 431
431 432 context.diffmode = diffmode
432 433
433 434 if request.session.get('diffmode') != diffmode:
434 435 request.session['diffmode'] = diffmode
435 436
436 437 context.csrf_token = auth.get_csrf_token(session=request.session)
437 438 context.backends = rhodecode.BACKENDS.keys()
438 439 context.backends.sort()
439 440 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
440 441
441 442 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
442 443 # given request will ALWAYS be pyramid one
443 444 pyramid_request = pyramid.threadlocal.get_current_request()
444 445 context.pyramid_request = pyramid_request
445 446
446 447 # web case
447 448 if hasattr(pyramid_request, 'user'):
448 449 context.auth_user = pyramid_request.user
449 450 context.rhodecode_user = pyramid_request.user
450 451
451 452 # api case
452 453 if hasattr(pyramid_request, 'rpc_user'):
453 454 context.auth_user = pyramid_request.rpc_user
454 455 context.rhodecode_user = pyramid_request.rpc_user
455 456
456 457 # attach the whole call context to the request
457 458 request.call_context = context
458 459
459 460
460 461 def get_auth_user(request):
461 462 environ = request.environ
462 463 session = request.session
463 464
464 465 ip_addr = get_ip_addr(environ)
465 466 # make sure that we update permissions each time we call controller
466 467 _auth_token = (request.GET.get('auth_token', '') or
467 468 request.GET.get('api_key', ''))
468 469
469 470 if _auth_token:
470 471 # when using API_KEY we assume user exists, and
471 472 # doesn't need auth based on cookies.
472 473 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
473 474 authenticated = False
474 475 else:
475 476 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
476 477 try:
477 478 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
478 479 ip_addr=ip_addr)
479 480 except UserCreationError as e:
480 481 h.flash(e, 'error')
481 482 # container auth or other auth functions that create users
482 483 # on the fly can throw this exception signaling that there's
483 484 # issue with user creation, explanation should be provided
484 485 # in Exception itself. We then create a simple blank
485 486 # AuthUser
486 487 auth_user = AuthUser(ip_addr=ip_addr)
487 488
488 489 if password_changed(auth_user, session):
489 490 session.invalidate()
490 491 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
491 492 auth_user = AuthUser(ip_addr=ip_addr)
492 493
493 494 authenticated = cookie_store.get('is_authenticated')
494 495
495 496 if not auth_user.is_authenticated and auth_user.is_user_object:
496 497 # user is not authenticated and not empty
497 498 auth_user.set_authenticated(authenticated)
498 499
499 500 return auth_user
500 501
501 502
502 503 class BaseController(WSGIController):
503 504
504 505 def __before__(self):
505 506 """
506 507 __before__ is called before controller methods and after __call__
507 508 """
508 509 # on each call propagate settings calls into global settings.
509 510 from pylons import config
510 511 from pylons import tmpl_context as c, request, url
511 512 set_rhodecode_config(config)
512 513 attach_context_attributes(c, request, self._rhodecode_user.user_id)
513 514
514 515 # TODO: Remove this when fixed in attach_context_attributes()
515 516 c.repo_name = get_repo_slug(request) # can be empty
516 517
517 518 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
518 519 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
519 520 self.sa = meta.Session
520 521 self.scm_model = ScmModel(self.sa)
521 522
522 523 # set user language
523 524 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
524 525 if user_lang:
525 526 translation.set_lang(user_lang)
526 527 log.debug('set language to %s for user %s',
527 528 user_lang, self._rhodecode_user)
528 529
529 530 def _dispatch_redirect(self, with_url, environ, start_response):
530 531 from webob.exc import HTTPFound
531 532 resp = HTTPFound(with_url)
532 533 environ['SCRIPT_NAME'] = '' # handle prefix middleware
533 534 environ['PATH_INFO'] = with_url
534 535 return resp(environ, start_response)
535 536
536 537 def __call__(self, environ, start_response):
537 538 """Invoke the Controller"""
538 539 # WSGIController.__call__ dispatches to the Controller method
539 540 # the request is routed to. This routing information is
540 541 # available in environ['pylons.routes_dict']
541 542 from rhodecode.lib import helpers as h
542 543 from pylons import tmpl_context as c, request, url
543 544
544 545 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
545 546 if environ.get('debugtoolbar.wants_pylons_context', False):
546 547 environ['debugtoolbar.pylons_context'] = c._current_obj()
547 548
548 549 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
549 550 environ['pylons.routes_dict']['action']])
550 551
551 552 self.rc_config = SettingsModel().get_all_settings(cache=True)
552 553 self.ip_addr = get_ip_addr(environ)
553 554
554 555 # The rhodecode auth user is looked up and passed through the
555 556 # environ by the pylons compatibility tween in pyramid.
556 557 # So we can just grab it from there.
557 558 auth_user = environ['rc_auth_user']
558 559
559 560 # set globals for auth user
560 561 request.user = auth_user
561 562 self._rhodecode_user = auth_user
562 563
563 564 log.info('IP: %s User: %s accessed %s [%s]' % (
564 565 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
565 566 _route_name)
566 567 )
567 568
568 569 user_obj = auth_user.get_instance()
569 570 if user_obj and user_obj.user_data.get('force_password_change'):
570 571 h.flash('You are required to change your password', 'warning',
571 572 ignore_duplicate=True)
572 573 return self._dispatch_redirect(
573 574 url('my_account_password'), environ, start_response)
574 575
575 576 return WSGIController.__call__(self, environ, start_response)
576 577
577 578
578 579 def h_filter(s):
579 580 """
580 581 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
581 582 we wrap this with additional functionality that converts None to empty
582 583 strings
583 584 """
584 585 if s is None:
585 586 return markupsafe.Markup()
586 587 return markupsafe.escape(s)
587 588
588 589
589 590 def add_events_routes(config):
590 591 """
591 592 Adds routing that can be used in events. Because some events are triggered
592 593 outside of pyramid context, we need to bootstrap request with some
593 594 routing registered
594 595 """
595 596 config.add_route(name='home', pattern='/')
596 597
597 598 config.add_route(name='repo_summary', pattern='/{repo_name}')
598 599 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
599 600 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
600 601
601 602 config.add_route(name='pullrequest_show',
602 603 pattern='/{repo_name}/pull-request/{pull_request_id}')
603 604 config.add_route(name='pull_requests_global',
604 605 pattern='/pull-request/{pull_request_id}')
605 606
606 607 config.add_route(name='repo_commit',
607 608 pattern='/{repo_name}/changeset/{commit_id}')
608 609 config.add_route(name='repo_files',
609 610 pattern='/{repo_name}/files/{commit_id}/{f_path}')
610 611
611 612
612 613 def bootstrap_request(**kwargs):
613 614 import pyramid.testing
614 615
615 616 class TestRequest(pyramid.testing.DummyRequest):
616 617 application_url = kwargs.pop('application_url', 'http://example.com')
617 618 host = kwargs.pop('host', 'example.com:80')
618 619 domain = kwargs.pop('domain', 'example.com')
619 620
620 621 class TestDummySession(pyramid.testing.DummySession):
621 622 def save(*arg, **kw):
622 623 pass
623 624
624 625 request = TestRequest(**kwargs)
625 626 request.session = TestDummySession()
626 627
627 628 config = pyramid.testing.setUp(request=request)
628 629 add_events_routes(config)
629 630 return request
630 631
@@ -1,540 +1,599 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-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 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 import re
27 28 import logging
28 29 import importlib
29 import re
30 30 from functools import wraps
31 31
32 import time
32 33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 34 from webob.exc import (
34 35 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35 36
36 37 import rhodecode
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 from rhodecode.authentication.base import (
39 authenticate, get_perms_cache_manager, VCS_TYPE)
40 from rhodecode.lib import caches
38 41 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 42 from rhodecode.lib.base import (
40 43 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
41 44 from rhodecode.lib.exceptions import (
42 45 HTTPLockedRC, HTTPRequirementError, UserCreationError,
43 46 NotAllowedToCreateUserError)
44 47 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
45 48 from rhodecode.lib.middleware import appenlight
46 49 from rhodecode.lib.middleware.utils import scm_app_http
47 from rhodecode.lib.utils import (
48 is_valid_repo, get_rhodecode_base_path, SLUG_RE)
50 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
49 51 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
50 52 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 53 from rhodecode.lib.vcs.backends import base
52 54 from rhodecode.model import meta
53 55 from rhodecode.model.db import User, Repository, PullRequest
54 56 from rhodecode.model.scm import ScmModel
55 57 from rhodecode.model.pull_request import PullRequestModel
56 58 from rhodecode.model.settings import SettingsModel
57 59
58 60 log = logging.getLogger(__name__)
59 61
60 62
61 63 def initialize_generator(factory):
62 64 """
63 65 Initializes the returned generator by draining its first element.
64 66
65 67 This can be used to give a generator an initializer, which is the code
66 68 up to the first yield statement. This decorator enforces that the first
67 69 produced element has the value ``"__init__"`` to make its special
68 70 purpose very explicit in the using code.
69 71 """
70 72
71 73 @wraps(factory)
72 74 def wrapper(*args, **kwargs):
73 75 gen = factory(*args, **kwargs)
74 76 try:
75 77 init = gen.next()
76 78 except StopIteration:
77 79 raise ValueError('Generator must yield at least one element.')
78 80 if init != "__init__":
79 81 raise ValueError('First yielded element must be "__init__".')
80 82 return gen
81 83 return wrapper
82 84
83 85
84 86 class SimpleVCS(object):
85 87 """Common functionality for SCM HTTP handlers."""
86 88
87 89 SCM = 'unknown'
88 90
89 91 acl_repo_name = None
90 92 url_repo_name = None
91 93 vcs_repo_name = None
92 94
93 95 # We have to handle requests to shadow repositories different than requests
94 96 # to normal repositories. Therefore we have to distinguish them. To do this
95 97 # we use this regex which will match only on URLs pointing to shadow
96 98 # repositories.
97 99 shadow_repo_re = re.compile(
98 100 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
99 101 '(?P<target>{slug_pat})/' # target repo
100 102 'pull-request/(?P<pr_id>\d+)/' # pull request
101 103 'repository$' # shadow repo
102 104 .format(slug_pat=SLUG_RE.pattern))
103 105
104 106 def __init__(self, application, config, registry):
105 107 self.registry = registry
106 108 self.application = application
107 109 self.config = config
108 110 # re-populated by specialized middleware
109 111 self.repo_vcs_config = base.Config()
110 112 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
111 113 self.basepath = rhodecode.CONFIG['base_path']
112 114 registry.rhodecode_settings = self.rhodecode_settings
113 115 # authenticate this VCS request using authfunc
114 116 auth_ret_code_detection = \
115 117 str2bool(self.config.get('auth_ret_code_detection', False))
116 118 self.authenticate = BasicAuth(
117 119 '', authenticate, registry, config.get('auth_ret_code'),
118 120 auth_ret_code_detection)
119 121 self.ip_addr = '0.0.0.0'
120 122
121 123 def set_repo_names(self, environ):
122 124 """
123 125 This will populate the attributes acl_repo_name, url_repo_name,
124 126 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
125 127 shadow) repositories all names are equal. In case of requests to a
126 128 shadow repository the acl-name points to the target repo of the pull
127 129 request and the vcs-name points to the shadow repo file system path.
128 130 The url-name is always the URL used by the vcs client program.
129 131
130 132 Example in case of a shadow repo:
131 133 acl_repo_name = RepoGroup/MyRepo
132 134 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
133 135 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
134 136 """
135 137 # First we set the repo name from URL for all attributes. This is the
136 138 # default if handling normal (non shadow) repo requests.
137 139 self.url_repo_name = self._get_repository_name(environ)
138 140 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
139 141 self.is_shadow_repo = False
140 142
141 143 # Check if this is a request to a shadow repository.
142 144 match = self.shadow_repo_re.match(self.url_repo_name)
143 145 if match:
144 146 match_dict = match.groupdict()
145 147
146 148 # Build acl repo name from regex match.
147 149 acl_repo_name = safe_unicode('{groups}{target}'.format(
148 150 groups=match_dict['groups'] or '',
149 151 target=match_dict['target']))
150 152
151 153 # Retrieve pull request instance by ID from regex match.
152 154 pull_request = PullRequest.get(match_dict['pr_id'])
153 155
154 156 # Only proceed if we got a pull request and if acl repo name from
155 157 # URL equals the target repo name of the pull request.
156 158 if pull_request and (acl_repo_name ==
157 159 pull_request.target_repo.repo_name):
158 160 # Get file system path to shadow repository.
159 161 workspace_id = PullRequestModel()._workspace_id(pull_request)
160 162 target_vcs = pull_request.target_repo.scm_instance()
161 163 vcs_repo_name = target_vcs._get_shadow_repository_path(
162 164 workspace_id)
163 165
164 166 # Store names for later usage.
165 167 self.vcs_repo_name = vcs_repo_name
166 168 self.acl_repo_name = acl_repo_name
167 169 self.is_shadow_repo = True
168 170
169 171 log.debug('Setting all VCS repository names: %s', {
170 172 'acl_repo_name': self.acl_repo_name,
171 173 'url_repo_name': self.url_repo_name,
172 174 'vcs_repo_name': self.vcs_repo_name,
173 175 })
174 176
175 177 @property
176 178 def scm_app(self):
177 179 custom_implementation = self.config['vcs.scm_app_implementation']
178 180 if custom_implementation == 'http':
179 181 log.info('Using HTTP implementation of scm app.')
180 182 scm_app_impl = scm_app_http
181 183 else:
182 184 log.info('Using custom implementation of scm_app: "{}"'.format(
183 185 custom_implementation))
184 186 scm_app_impl = importlib.import_module(custom_implementation)
185 187 return scm_app_impl
186 188
187 189 def _get_by_id(self, repo_name):
188 190 """
189 191 Gets a special pattern _<ID> from clone url and tries to replace it
190 192 with a repository_name for support of _<ID> non changeable urls
191 193 """
192 194
193 195 data = repo_name.split('/')
194 196 if len(data) >= 2:
195 197 from rhodecode.model.repo import RepoModel
196 198 by_id_match = RepoModel().get_repo_by_id(repo_name)
197 199 if by_id_match:
198 200 data[1] = by_id_match.repo_name
199 201
200 202 return safe_str('/'.join(data))
201 203
202 204 def _invalidate_cache(self, repo_name):
203 205 """
204 206 Set's cache for this repository for invalidation on next access
205 207
206 208 :param repo_name: full repo name, also a cache key
207 209 """
208 210 ScmModel().mark_for_invalidation(repo_name)
209 211
210 212 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
211 213 db_repo = Repository.get_by_repo_name(repo_name)
212 214 if not db_repo:
213 215 log.debug('Repository `%s` not found inside the database.',
214 216 repo_name)
215 217 return False
216 218
217 219 if db_repo.repo_type != scm_type:
218 220 log.warning(
219 221 'Repository `%s` have incorrect scm_type, expected %s got %s',
220 222 repo_name, db_repo.repo_type, scm_type)
221 223 return False
222 224
223 225 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
224 226
225 227 def valid_and_active_user(self, user):
226 228 """
227 229 Checks if that user is not empty, and if it's actually object it checks
228 230 if he's active.
229 231
230 232 :param user: user object or None
231 233 :return: boolean
232 234 """
233 235 if user is None:
234 236 return False
235 237
236 238 elif user.active:
237 239 return True
238 240
239 241 return False
240 242
241 243 @property
242 244 def is_shadow_repo_dir(self):
243 245 return os.path.isdir(self.vcs_repo_name)
244 246
245 def _check_permission(self, action, user, repo_name, ip_addr=None):
247 def _check_permission(self, action, user, repo_name, ip_addr=None,
248 plugin_id='', plugin_cache_active=False, cache_ttl=0):
246 249 """
247 250 Checks permissions using action (push/pull) user and repository
248 name
251 name. If plugin_cache and ttl is set it will use the plugin which
252 authenticated the user to store the cached permissions result for N
253 amount of seconds as in cache_ttl
249 254
250 255 :param action: push or pull action
251 256 :param user: user instance
252 257 :param repo_name: repository name
253 258 """
254 # check IP
255 inherit = user.inherit_default_permissions
256 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
257 inherit_from_default=inherit)
258 if ip_allowed:
259 log.info('Access for IP:%s allowed', ip_addr)
260 else:
261 return False
259
260 # get instance of cache manager configured for a namespace
261 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
262 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
263 plugin_id, plugin_cache_active, cache_ttl)
264
265 # for environ based password can be empty, but then the validation is
266 # on the server that fills in the env data needed for authentication
267 _perm_calc_hash = caches.compute_key_from_params(
268 plugin_id, action, user.user_id, repo_name, ip_addr)
262 269
263 if action == 'push':
264 if not HasPermissionAnyMiddleware('repository.write',
265 'repository.admin')(user,
266 repo_name):
270 # _authenticate is a wrapper for .auth() method of plugin.
271 # it checks if .auth() sends proper data.
272 # For RhodeCodeExternalAuthPlugin it also maps users to
273 # Database and maps the attributes returned from .auth()
274 # to RhodeCode database. If this function returns data
275 # then auth is correct.
276 start = time.time()
277 log.debug('Running plugin `%s` permissions check', plugin_id)
278
279 def perm_func():
280 """
281 This function is used internally in Cache of Beaker to calculate
282 Results
283 """
284 log.debug('auth: calculating permission access now...')
285 # check IP
286 inherit = user.inherit_default_permissions
287 ip_allowed = AuthUser.check_ip_allowed(
288 user.user_id, ip_addr, inherit_from_default=inherit)
289 if ip_allowed:
290 log.info('Access for IP:%s allowed', ip_addr)
291 else:
267 292 return False
268 293
294 if action == 'push':
295 perms = ('repository.write', 'repository.admin')
296 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
297 return False
298
299 else:
300 # any other action need at least read permission
301 perms = (
302 'repository.read', 'repository.write', 'repository.admin')
303 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
304 return False
305
306 return True
307
308 if plugin_cache_active:
309 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
310 perm_result = cache_manager.get(
311 _perm_calc_hash, createfunc=perm_func)
269 312 else:
270 # any other action need at least read permission
271 if not HasPermissionAnyMiddleware('repository.read',
272 'repository.write',
273 'repository.admin')(user,
274 repo_name):
275 return False
313 perm_result = perm_func()
276 314
277 return True
315 auth_time = time.time() - start
316 log.debug('Permissions for plugin `%s` completed in %.3fs, '
317 'expiration time of fetched cache %.1fs.',
318 plugin_id, auth_time, cache_ttl)
319
320 return perm_result
278 321
279 322 def _check_ssl(self, environ, start_response):
280 323 """
281 324 Checks the SSL check flag and returns False if SSL is not present
282 325 and required True otherwise
283 326 """
284 327 org_proto = environ['wsgi._org_proto']
285 328 # check if we have SSL required ! if not it's a bad request !
286 329 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
287 330 if require_ssl and org_proto == 'http':
288 331 log.debug('proto is %s and SSL is required BAD REQUEST !',
289 332 org_proto)
290 333 return False
291 334 return True
292 335
293 336 def __call__(self, environ, start_response):
294 337 try:
295 338 return self._handle_request(environ, start_response)
296 339 except Exception:
297 340 log.exception("Exception while handling request")
298 341 appenlight.track_exception(environ)
299 342 return HTTPInternalServerError()(environ, start_response)
300 343 finally:
301 344 meta.Session.remove()
302 345
303 346 def _handle_request(self, environ, start_response):
304 347
305 348 if not self._check_ssl(environ, start_response):
306 349 reason = ('SSL required, while RhodeCode was unable '
307 350 'to detect this as SSL request')
308 351 log.debug('User not allowed to proceed, %s', reason)
309 352 return HTTPNotAcceptable(reason)(environ, start_response)
310 353
311 354 if not self.url_repo_name:
312 355 log.warning('Repository name is empty: %s', self.url_repo_name)
313 356 # failed to get repo name, we fail now
314 357 return HTTPNotFound()(environ, start_response)
315 358 log.debug('Extracted repo name is %s', self.url_repo_name)
316 359
317 360 ip_addr = get_ip_addr(environ)
318 361 user_agent = get_user_agent(environ)
319 362 username = None
320 363
321 364 # skip passing error to error controller
322 365 environ['pylons.status_code_redirect'] = True
323 366
324 367 # ======================================================================
325 368 # GET ACTION PULL or PUSH
326 369 # ======================================================================
327 370 action = self._get_action(environ)
328 371
329 372 # ======================================================================
330 373 # Check if this is a request to a shadow repository of a pull request.
331 374 # In this case only pull action is allowed.
332 375 # ======================================================================
333 376 if self.is_shadow_repo and action != 'pull':
334 377 reason = 'Only pull action is allowed for shadow repositories.'
335 378 log.debug('User not allowed to proceed, %s', reason)
336 379 return HTTPNotAcceptable(reason)(environ, start_response)
337 380
338 381 # Check if the shadow repo actually exists, in case someone refers
339 382 # to it, and it has been deleted because of successful merge.
340 383 if self.is_shadow_repo and not self.is_shadow_repo_dir:
341 384 return HTTPNotFound()(environ, start_response)
342 385
343 386 # ======================================================================
344 387 # CHECK ANONYMOUS PERMISSION
345 388 # ======================================================================
346 389 if action in ['pull', 'push']:
347 390 anonymous_user = User.get_default_user()
348 391 username = anonymous_user.username
349 392 if anonymous_user.active:
350 393 # ONLY check permissions if the user is activated
351 394 anonymous_perm = self._check_permission(
352 395 action, anonymous_user, self.acl_repo_name, ip_addr)
353 396 else:
354 397 anonymous_perm = False
355 398
356 399 if not anonymous_user.active or not anonymous_perm:
357 400 if not anonymous_user.active:
358 401 log.debug('Anonymous access is disabled, running '
359 402 'authentication')
360 403
361 404 if not anonymous_perm:
362 405 log.debug('Not enough credentials to access this '
363 406 'repository as anonymous user')
364 407
365 408 username = None
366 409 # ==============================================================
367 410 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
368 411 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
369 412 # ==============================================================
370 413
371 414 # try to auth based on environ, container auth methods
372 415 log.debug('Running PRE-AUTH for container based authentication')
373 416 pre_auth = authenticate(
374 417 '', '', environ, VCS_TYPE, registry=self.registry,
375 418 acl_repo_name=self.acl_repo_name)
376 419 if pre_auth and pre_auth.get('username'):
377 420 username = pre_auth['username']
378 421 log.debug('PRE-AUTH got %s as username', username)
422 if pre_auth:
423 log.debug('PRE-AUTH successful from %s',
424 pre_auth.get('auth_data', {}).get('_plugin'))
379 425
380 426 # If not authenticated by the container, running basic auth
381 427 # before inject the calling repo_name for special scope checks
382 428 self.authenticate.acl_repo_name = self.acl_repo_name
429
430 plugin_cache_active, cache_ttl = False, 0
431 plugin = None
383 432 if not username:
384 433 self.authenticate.realm = self.authenticate.get_rc_realm()
385 434
386 435 try:
387 result = self.authenticate(environ)
436 auth_result = self.authenticate(environ)
388 437 except (UserCreationError, NotAllowedToCreateUserError) as e:
389 438 log.error(e)
390 439 reason = safe_str(e)
391 440 return HTTPNotAcceptable(reason)(environ, start_response)
392 441
393 if isinstance(result, str):
442 if isinstance(auth_result, dict):
394 443 AUTH_TYPE.update(environ, 'basic')
395 REMOTE_USER.update(environ, result)
396 username = result
444 REMOTE_USER.update(environ, auth_result['username'])
445 username = auth_result['username']
446 plugin = auth_result.get('auth_data', {}).get('_plugin')
447 log.info(
448 'MAIN-AUTH successful for user `%s` from %s plugin',
449 username, plugin)
450
451 plugin_cache_active, cache_ttl = auth_result.get(
452 'auth_data', {}).get('_ttl_cache') or (False, 0)
397 453 else:
398 return result.wsgi_application(environ, start_response)
454 return auth_result.wsgi_application(
455 environ, start_response)
456
399 457
400 458 # ==============================================================
401 459 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
402 460 # ==============================================================
403 461 user = User.get_by_username(username)
404 462 if not self.valid_and_active_user(user):
405 463 return HTTPForbidden()(environ, start_response)
406 464 username = user.username
407 465 user.update_lastactivity()
408 466 meta.Session().commit()
409 467
410 468 # check user attributes for password change flag
411 469 user_obj = user
412 470 if user_obj and user_obj.username != User.DEFAULT_USER and \
413 471 user_obj.user_data.get('force_password_change'):
414 472 reason = 'password change required'
415 473 log.debug('User not allowed to authenticate, %s', reason)
416 474 return HTTPNotAcceptable(reason)(environ, start_response)
417 475
418 476 # check permissions for this repository
419 477 perm = self._check_permission(
420 action, user, self.acl_repo_name, ip_addr)
478 action, user, self.acl_repo_name, ip_addr,
479 plugin, plugin_cache_active, cache_ttl)
421 480 if not perm:
422 481 return HTTPForbidden()(environ, start_response)
423 482
424 483 # extras are injected into UI object and later available
425 # in hooks executed by rhodecode
484 # in hooks executed by RhodeCode
426 485 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
427 486 extras = vcs_operation_context(
428 487 environ, repo_name=self.acl_repo_name, username=username,
429 488 action=action, scm=self.SCM, check_locking=check_locking,
430 489 is_shadow_repo=self.is_shadow_repo
431 490 )
432 491
433 492 # ======================================================================
434 493 # REQUEST HANDLING
435 494 # ======================================================================
436 495 repo_path = os.path.join(
437 496 safe_str(self.basepath), safe_str(self.vcs_repo_name))
438 497 log.debug('Repository path is %s', repo_path)
439 498
440 499 fix_PATH()
441 500
442 501 log.info(
443 502 '%s action on %s repo "%s" by "%s" from %s %s',
444 503 action, self.SCM, safe_str(self.url_repo_name),
445 504 safe_str(username), ip_addr, user_agent)
446 505
447 506 return self._generate_vcs_response(
448 507 environ, start_response, repo_path, extras, action)
449 508
450 509 @initialize_generator
451 510 def _generate_vcs_response(
452 511 self, environ, start_response, repo_path, extras, action):
453 512 """
454 513 Returns a generator for the response content.
455 514
456 515 This method is implemented as a generator, so that it can trigger
457 516 the cache validation after all content sent back to the client. It
458 517 also handles the locking exceptions which will be triggered when
459 518 the first chunk is produced by the underlying WSGI application.
460 519 """
461 520 callback_daemon, extras = self._prepare_callback_daemon(extras)
462 521 config = self._create_config(extras, self.acl_repo_name)
463 522 log.debug('HOOKS extras is %s', extras)
464 523 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
465 524
466 525 try:
467 526 with callback_daemon:
468 527 try:
469 528 response = app(environ, start_response)
470 529 finally:
471 530 # This statement works together with the decorator
472 531 # "initialize_generator" above. The decorator ensures that
473 532 # we hit the first yield statement before the generator is
474 533 # returned back to the WSGI server. This is needed to
475 534 # ensure that the call to "app" above triggers the
476 535 # needed callback to "start_response" before the
477 536 # generator is actually used.
478 537 yield "__init__"
479 538
480 539 for chunk in response:
481 540 yield chunk
482 541 except Exception as exc:
483 542 # TODO: martinb: Exceptions are only raised in case of the Pyro4
484 543 # backend. Refactor this except block after dropping Pyro4 support.
485 544 # TODO: johbo: Improve "translating" back the exception.
486 545 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
487 546 exc = HTTPLockedRC(*exc.args)
488 547 _code = rhodecode.CONFIG.get('lock_ret_code')
489 548 log.debug('Repository LOCKED ret code %s!', (_code,))
490 549 elif getattr(exc, '_vcs_kind', None) == 'requirement':
491 550 log.debug(
492 551 'Repository requires features unknown to this Mercurial')
493 552 exc = HTTPRequirementError(*exc.args)
494 553 else:
495 554 raise
496 555
497 556 for chunk in exc(environ, start_response):
498 557 yield chunk
499 558 finally:
500 559 # invalidate cache on push
501 560 try:
502 561 if action == 'push':
503 562 self._invalidate_cache(self.url_repo_name)
504 563 finally:
505 564 meta.Session.remove()
506 565
507 566 def _get_repository_name(self, environ):
508 567 """Get repository name out of the environmnent
509 568
510 569 :param environ: WSGI environment
511 570 """
512 571 raise NotImplementedError()
513 572
514 573 def _get_action(self, environ):
515 574 """Map request commands into a pull or push command.
516 575
517 576 :param environ: WSGI environment
518 577 """
519 578 raise NotImplementedError()
520 579
521 580 def _create_wsgi_app(self, repo_path, repo_name, config):
522 581 """Return the WSGI app that will finally handle the request."""
523 582 raise NotImplementedError()
524 583
525 584 def _create_config(self, extras, repo_name):
526 585 """Create a safe config representation."""
527 586 raise NotImplementedError()
528 587
529 588 def _prepare_callback_daemon(self, extras):
530 589 return prepare_callback_daemon(
531 590 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
532 591 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
533 592
534 593
535 594 def _should_check_locking(query_string):
536 595 # this is kind of hacky, but due to how mercurial handles client-server
537 596 # server see all operation on commit; bookmarks, phases and
538 597 # obsolescence marker in different transaction, we don't want to check
539 598 # locking on those
540 599 return query_string not in ['cmd=listkeys']
@@ -1,4227 +1,4227 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
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from sqlalchemy.sql.functions import coalesce, count # noqa
45 45 from sqlalchemy.exc import IntegrityError # noqa
46 46 from sqlalchemy.dialects.mysql import LONGTEXT
47 47 from beaker.cache import cache_region
48 48 from zope.cachedescriptors.property import Lazy as LazyProperty
49 49
50 50 from pyramid.threadlocal import get_current_request
51 51
52 52 from rhodecode.translation import _
53 53 from rhodecode.lib.vcs import get_vcs_instance
54 54 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
57 57 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
58 58 glob2re, StrictAttributeDict, cleaned_uri)
59 59 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
60 60 from rhodecode.lib.ext_json import json
61 61 from rhodecode.lib.caching_query import FromCache
62 62 from rhodecode.lib.encrypt import AESCipher
63 63
64 64 from rhodecode.model.meta import Base, Session
65 65
66 66 URL_SEP = '/'
67 67 log = logging.getLogger(__name__)
68 68
69 69 # =============================================================================
70 70 # BASE CLASSES
71 71 # =============================================================================
72 72
73 73 # this is propagated from .ini file rhodecode.encrypted_values.secret or
74 74 # beaker.session.secret if first is not set.
75 75 # and initialized at environment.py
76 76 ENCRYPTION_KEY = None
77 77
78 78 # used to sort permissions by types, '#' used here is not allowed to be in
79 79 # usernames, and it's very early in sorted string.printable table.
80 80 PERMISSION_TYPE_SORT = {
81 81 'admin': '####',
82 82 'write': '###',
83 83 'read': '##',
84 84 'none': '#',
85 85 }
86 86
87 87
88 88 def display_user_sort(obj):
89 89 """
90 90 Sort function used to sort permissions in .permissions() function of
91 91 Repository, RepoGroup, UserGroup. Also it put the default user in front
92 92 of all other resources
93 93 """
94 94
95 95 if obj.username == User.DEFAULT_USER:
96 96 return '#####'
97 97 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
98 98 return prefix + obj.username
99 99
100 100
101 101 def display_user_group_sort(obj):
102 102 """
103 103 Sort function used to sort permissions in .permissions() function of
104 104 Repository, RepoGroup, UserGroup. Also it put the default user in front
105 105 of all other resources
106 106 """
107 107
108 108 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
109 109 return prefix + obj.users_group_name
110 110
111 111
112 112 def _hash_key(k):
113 113 return md5_safe(k)
114 114
115 115
116 116 def in_filter_generator(qry, items, limit=500):
117 117 """
118 118 Splits IN() into multiple with OR
119 119 e.g.::
120 120 cnt = Repository.query().filter(
121 121 or_(
122 122 *in_filter_generator(Repository.repo_id, range(100000))
123 123 )).count()
124 124 """
125 125 parts = []
126 126 for chunk in xrange(0, len(items), limit):
127 127 parts.append(
128 128 qry.in_(items[chunk: chunk + limit])
129 129 )
130 130
131 131 return parts
132 132
133 133
134 134 class EncryptedTextValue(TypeDecorator):
135 135 """
136 136 Special column for encrypted long text data, use like::
137 137
138 138 value = Column("encrypted_value", EncryptedValue(), nullable=False)
139 139
140 140 This column is intelligent so if value is in unencrypted form it return
141 141 unencrypted form, but on save it always encrypts
142 142 """
143 143 impl = Text
144 144
145 145 def process_bind_param(self, value, dialect):
146 146 if not value:
147 147 return value
148 148 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
149 149 # protect against double encrypting if someone manually starts
150 150 # doing
151 151 raise ValueError('value needs to be in unencrypted format, ie. '
152 152 'not starting with enc$aes')
153 153 return 'enc$aes_hmac$%s' % AESCipher(
154 154 ENCRYPTION_KEY, hmac=True).encrypt(value)
155 155
156 156 def process_result_value(self, value, dialect):
157 157 import rhodecode
158 158
159 159 if not value:
160 160 return value
161 161
162 162 parts = value.split('$', 3)
163 163 if not len(parts) == 3:
164 164 # probably not encrypted values
165 165 return value
166 166 else:
167 167 if parts[0] != 'enc':
168 168 # parts ok but without our header ?
169 169 return value
170 170 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
171 171 'rhodecode.encrypted_values.strict') or True)
172 172 # at that stage we know it's our encryption
173 173 if parts[1] == 'aes':
174 174 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
175 175 elif parts[1] == 'aes_hmac':
176 176 decrypted_data = AESCipher(
177 177 ENCRYPTION_KEY, hmac=True,
178 178 strict_verification=enc_strict_mode).decrypt(parts[2])
179 179 else:
180 180 raise ValueError(
181 181 'Encryption type part is wrong, must be `aes` '
182 182 'or `aes_hmac`, got `%s` instead' % (parts[1]))
183 183 return decrypted_data
184 184
185 185
186 186 class BaseModel(object):
187 187 """
188 188 Base Model for all classes
189 189 """
190 190
191 191 @classmethod
192 192 def _get_keys(cls):
193 193 """return column names for this model """
194 194 return class_mapper(cls).c.keys()
195 195
196 196 def get_dict(self):
197 197 """
198 198 return dict with keys and values corresponding
199 199 to this model data """
200 200
201 201 d = {}
202 202 for k in self._get_keys():
203 203 d[k] = getattr(self, k)
204 204
205 205 # also use __json__() if present to get additional fields
206 206 _json_attr = getattr(self, '__json__', None)
207 207 if _json_attr:
208 208 # update with attributes from __json__
209 209 if callable(_json_attr):
210 210 _json_attr = _json_attr()
211 211 for k, val in _json_attr.iteritems():
212 212 d[k] = val
213 213 return d
214 214
215 215 def get_appstruct(self):
216 216 """return list with keys and values tuples corresponding
217 217 to this model data """
218 218
219 219 l = []
220 220 for k in self._get_keys():
221 221 l.append((k, getattr(self, k),))
222 222 return l
223 223
224 224 def populate_obj(self, populate_dict):
225 225 """populate model with data from given populate_dict"""
226 226
227 227 for k in self._get_keys():
228 228 if k in populate_dict:
229 229 setattr(self, k, populate_dict[k])
230 230
231 231 @classmethod
232 232 def query(cls):
233 233 return Session().query(cls)
234 234
235 235 @classmethod
236 236 def get(cls, id_):
237 237 if id_:
238 238 return cls.query().get(id_)
239 239
240 240 @classmethod
241 241 def get_or_404(cls, id_):
242 242 from pyramid.httpexceptions import HTTPNotFound
243 243
244 244 try:
245 245 id_ = int(id_)
246 246 except (TypeError, ValueError):
247 247 raise HTTPNotFound()
248 248
249 249 res = cls.query().get(id_)
250 250 if not res:
251 251 raise HTTPNotFound()
252 252 return res
253 253
254 254 @classmethod
255 255 def getAll(cls):
256 256 # deprecated and left for backward compatibility
257 257 return cls.get_all()
258 258
259 259 @classmethod
260 260 def get_all(cls):
261 261 return cls.query().all()
262 262
263 263 @classmethod
264 264 def delete(cls, id_):
265 265 obj = cls.query().get(id_)
266 266 Session().delete(obj)
267 267
268 268 @classmethod
269 269 def identity_cache(cls, session, attr_name, value):
270 270 exist_in_session = []
271 271 for (item_cls, pkey), instance in session.identity_map.items():
272 272 if cls == item_cls and getattr(instance, attr_name) == value:
273 273 exist_in_session.append(instance)
274 274 if exist_in_session:
275 275 if len(exist_in_session) == 1:
276 276 return exist_in_session[0]
277 277 log.exception(
278 278 'multiple objects with attr %s and '
279 279 'value %s found with same name: %r',
280 280 attr_name, value, exist_in_session)
281 281
282 282 def __repr__(self):
283 283 if hasattr(self, '__unicode__'):
284 284 # python repr needs to return str
285 285 try:
286 286 return safe_str(self.__unicode__())
287 287 except UnicodeDecodeError:
288 288 pass
289 289 return '<DB:%s>' % (self.__class__.__name__)
290 290
291 291
292 292 class RhodeCodeSetting(Base, BaseModel):
293 293 __tablename__ = 'rhodecode_settings'
294 294 __table_args__ = (
295 295 UniqueConstraint('app_settings_name'),
296 296 {'extend_existing': True, 'mysql_engine': 'InnoDB',
297 297 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
298 298 )
299 299
300 300 SETTINGS_TYPES = {
301 301 'str': safe_str,
302 302 'int': safe_int,
303 303 'unicode': safe_unicode,
304 304 'bool': str2bool,
305 305 'list': functools.partial(aslist, sep=',')
306 306 }
307 307 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
308 308 GLOBAL_CONF_KEY = 'app_settings'
309 309
310 310 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
311 311 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
312 312 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
313 313 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
314 314
315 315 def __init__(self, key='', val='', type='unicode'):
316 316 self.app_settings_name = key
317 317 self.app_settings_type = type
318 318 self.app_settings_value = val
319 319
320 320 @validates('_app_settings_value')
321 321 def validate_settings_value(self, key, val):
322 322 assert type(val) == unicode
323 323 return val
324 324
325 325 @hybrid_property
326 326 def app_settings_value(self):
327 327 v = self._app_settings_value
328 328 _type = self.app_settings_type
329 329 if _type:
330 330 _type = self.app_settings_type.split('.')[0]
331 331 # decode the encrypted value
332 332 if 'encrypted' in self.app_settings_type:
333 333 cipher = EncryptedTextValue()
334 334 v = safe_unicode(cipher.process_result_value(v, None))
335 335
336 336 converter = self.SETTINGS_TYPES.get(_type) or \
337 337 self.SETTINGS_TYPES['unicode']
338 338 return converter(v)
339 339
340 340 @app_settings_value.setter
341 341 def app_settings_value(self, val):
342 342 """
343 343 Setter that will always make sure we use unicode in app_settings_value
344 344
345 345 :param val:
346 346 """
347 347 val = safe_unicode(val)
348 348 # encode the encrypted value
349 349 if 'encrypted' in self.app_settings_type:
350 350 cipher = EncryptedTextValue()
351 351 val = safe_unicode(cipher.process_bind_param(val, None))
352 352 self._app_settings_value = val
353 353
354 354 @hybrid_property
355 355 def app_settings_type(self):
356 356 return self._app_settings_type
357 357
358 358 @app_settings_type.setter
359 359 def app_settings_type(self, val):
360 360 if val.split('.')[0] not in self.SETTINGS_TYPES:
361 361 raise Exception('type must be one of %s got %s'
362 362 % (self.SETTINGS_TYPES.keys(), val))
363 363 self._app_settings_type = val
364 364
365 365 def __unicode__(self):
366 366 return u"<%s('%s:%s[%s]')>" % (
367 367 self.__class__.__name__,
368 368 self.app_settings_name, self.app_settings_value,
369 369 self.app_settings_type
370 370 )
371 371
372 372
373 373 class RhodeCodeUi(Base, BaseModel):
374 374 __tablename__ = 'rhodecode_ui'
375 375 __table_args__ = (
376 376 UniqueConstraint('ui_key'),
377 377 {'extend_existing': True, 'mysql_engine': 'InnoDB',
378 378 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
379 379 )
380 380
381 381 HOOK_REPO_SIZE = 'changegroup.repo_size'
382 382 # HG
383 383 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
384 384 HOOK_PULL = 'outgoing.pull_logger'
385 385 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
386 386 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
387 387 HOOK_PUSH = 'changegroup.push_logger'
388 388 HOOK_PUSH_KEY = 'pushkey.key_push'
389 389
390 390 # TODO: johbo: Unify way how hooks are configured for git and hg,
391 391 # git part is currently hardcoded.
392 392
393 393 # SVN PATTERNS
394 394 SVN_BRANCH_ID = 'vcs_svn_branch'
395 395 SVN_TAG_ID = 'vcs_svn_tag'
396 396
397 397 ui_id = Column(
398 398 "ui_id", Integer(), nullable=False, unique=True, default=None,
399 399 primary_key=True)
400 400 ui_section = Column(
401 401 "ui_section", String(255), nullable=True, unique=None, default=None)
402 402 ui_key = Column(
403 403 "ui_key", String(255), nullable=True, unique=None, default=None)
404 404 ui_value = Column(
405 405 "ui_value", String(255), nullable=True, unique=None, default=None)
406 406 ui_active = Column(
407 407 "ui_active", Boolean(), nullable=True, unique=None, default=True)
408 408
409 409 def __repr__(self):
410 410 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
411 411 self.ui_key, self.ui_value)
412 412
413 413
414 414 class RepoRhodeCodeSetting(Base, BaseModel):
415 415 __tablename__ = 'repo_rhodecode_settings'
416 416 __table_args__ = (
417 417 UniqueConstraint(
418 418 'app_settings_name', 'repository_id',
419 419 name='uq_repo_rhodecode_setting_name_repo_id'),
420 420 {'extend_existing': True, 'mysql_engine': 'InnoDB',
421 421 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
422 422 )
423 423
424 424 repository_id = Column(
425 425 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
426 426 nullable=False)
427 427 app_settings_id = Column(
428 428 "app_settings_id", Integer(), nullable=False, unique=True,
429 429 default=None, primary_key=True)
430 430 app_settings_name = Column(
431 431 "app_settings_name", String(255), nullable=True, unique=None,
432 432 default=None)
433 433 _app_settings_value = Column(
434 434 "app_settings_value", String(4096), nullable=True, unique=None,
435 435 default=None)
436 436 _app_settings_type = Column(
437 437 "app_settings_type", String(255), nullable=True, unique=None,
438 438 default=None)
439 439
440 440 repository = relationship('Repository')
441 441
442 442 def __init__(self, repository_id, key='', val='', type='unicode'):
443 443 self.repository_id = repository_id
444 444 self.app_settings_name = key
445 445 self.app_settings_type = type
446 446 self.app_settings_value = val
447 447
448 448 @validates('_app_settings_value')
449 449 def validate_settings_value(self, key, val):
450 450 assert type(val) == unicode
451 451 return val
452 452
453 453 @hybrid_property
454 454 def app_settings_value(self):
455 455 v = self._app_settings_value
456 456 type_ = self.app_settings_type
457 457 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
458 458 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
459 459 return converter(v)
460 460
461 461 @app_settings_value.setter
462 462 def app_settings_value(self, val):
463 463 """
464 464 Setter that will always make sure we use unicode in app_settings_value
465 465
466 466 :param val:
467 467 """
468 468 self._app_settings_value = safe_unicode(val)
469 469
470 470 @hybrid_property
471 471 def app_settings_type(self):
472 472 return self._app_settings_type
473 473
474 474 @app_settings_type.setter
475 475 def app_settings_type(self, val):
476 476 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
477 477 if val not in SETTINGS_TYPES:
478 478 raise Exception('type must be one of %s got %s'
479 479 % (SETTINGS_TYPES.keys(), val))
480 480 self._app_settings_type = val
481 481
482 482 def __unicode__(self):
483 483 return u"<%s('%s:%s:%s[%s]')>" % (
484 484 self.__class__.__name__, self.repository.repo_name,
485 485 self.app_settings_name, self.app_settings_value,
486 486 self.app_settings_type
487 487 )
488 488
489 489
490 490 class RepoRhodeCodeUi(Base, BaseModel):
491 491 __tablename__ = 'repo_rhodecode_ui'
492 492 __table_args__ = (
493 493 UniqueConstraint(
494 494 'repository_id', 'ui_section', 'ui_key',
495 495 name='uq_repo_rhodecode_ui_repository_id_section_key'),
496 496 {'extend_existing': True, 'mysql_engine': 'InnoDB',
497 497 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
498 498 )
499 499
500 500 repository_id = Column(
501 501 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
502 502 nullable=False)
503 503 ui_id = Column(
504 504 "ui_id", Integer(), nullable=False, unique=True, default=None,
505 505 primary_key=True)
506 506 ui_section = Column(
507 507 "ui_section", String(255), nullable=True, unique=None, default=None)
508 508 ui_key = Column(
509 509 "ui_key", String(255), nullable=True, unique=None, default=None)
510 510 ui_value = Column(
511 511 "ui_value", String(255), nullable=True, unique=None, default=None)
512 512 ui_active = Column(
513 513 "ui_active", Boolean(), nullable=True, unique=None, default=True)
514 514
515 515 repository = relationship('Repository')
516 516
517 517 def __repr__(self):
518 518 return '<%s[%s:%s]%s=>%s]>' % (
519 519 self.__class__.__name__, self.repository.repo_name,
520 520 self.ui_section, self.ui_key, self.ui_value)
521 521
522 522
523 523 class User(Base, BaseModel):
524 524 __tablename__ = 'users'
525 525 __table_args__ = (
526 526 UniqueConstraint('username'), UniqueConstraint('email'),
527 527 Index('u_username_idx', 'username'),
528 528 Index('u_email_idx', 'email'),
529 529 {'extend_existing': True, 'mysql_engine': 'InnoDB',
530 530 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
531 531 )
532 532 DEFAULT_USER = 'default'
533 533 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
534 534 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
535 535
536 536 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
537 537 username = Column("username", String(255), nullable=True, unique=None, default=None)
538 538 password = Column("password", String(255), nullable=True, unique=None, default=None)
539 539 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
540 540 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
541 541 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
542 542 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
543 543 _email = Column("email", String(255), nullable=True, unique=None, default=None)
544 544 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
545 545 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
546 546
547 547 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
548 548 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
549 549 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
550 550 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
551 551 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
552 552 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
553 553
554 554 user_log = relationship('UserLog')
555 555 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
556 556
557 557 repositories = relationship('Repository')
558 558 repository_groups = relationship('RepoGroup')
559 559 user_groups = relationship('UserGroup')
560 560
561 561 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
562 562 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
563 563
564 564 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
565 565 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
566 566 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
567 567
568 568 group_member = relationship('UserGroupMember', cascade='all')
569 569
570 570 notifications = relationship('UserNotification', cascade='all')
571 571 # notifications assigned to this user
572 572 user_created_notifications = relationship('Notification', cascade='all')
573 573 # comments created by this user
574 574 user_comments = relationship('ChangesetComment', cascade='all')
575 575 # user profile extra info
576 576 user_emails = relationship('UserEmailMap', cascade='all')
577 577 user_ip_map = relationship('UserIpMap', cascade='all')
578 578 user_auth_tokens = relationship('UserApiKeys', cascade='all')
579 579 user_ssh_keys = relationship('UserSshKeys', cascade='all')
580 580
581 581 # gists
582 582 user_gists = relationship('Gist', cascade='all')
583 583 # user pull requests
584 584 user_pull_requests = relationship('PullRequest', cascade='all')
585 585 # external identities
586 586 extenal_identities = relationship(
587 587 'ExternalIdentity',
588 588 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
589 589 cascade='all')
590 590 # review rules
591 591 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
592 592
593 593 def __unicode__(self):
594 594 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
595 595 self.user_id, self.username)
596 596
597 597 @hybrid_property
598 598 def email(self):
599 599 return self._email
600 600
601 601 @email.setter
602 602 def email(self, val):
603 603 self._email = val.lower() if val else None
604 604
605 605 @hybrid_property
606 606 def first_name(self):
607 607 from rhodecode.lib import helpers as h
608 608 if self.name:
609 609 return h.escape(self.name)
610 610 return self.name
611 611
612 612 @hybrid_property
613 613 def last_name(self):
614 614 from rhodecode.lib import helpers as h
615 615 if self.lastname:
616 616 return h.escape(self.lastname)
617 617 return self.lastname
618 618
619 619 @hybrid_property
620 620 def api_key(self):
621 621 """
622 622 Fetch if exist an auth-token with role ALL connected to this user
623 623 """
624 624 user_auth_token = UserApiKeys.query()\
625 625 .filter(UserApiKeys.user_id == self.user_id)\
626 626 .filter(or_(UserApiKeys.expires == -1,
627 627 UserApiKeys.expires >= time.time()))\
628 628 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
629 629 if user_auth_token:
630 630 user_auth_token = user_auth_token.api_key
631 631
632 632 return user_auth_token
633 633
634 634 @api_key.setter
635 635 def api_key(self, val):
636 636 # don't allow to set API key this is deprecated for now
637 637 self._api_key = None
638 638
639 639 @property
640 640 def reviewer_pull_requests(self):
641 641 return PullRequestReviewers.query() \
642 642 .options(joinedload(PullRequestReviewers.pull_request)) \
643 643 .filter(PullRequestReviewers.user_id == self.user_id) \
644 644 .all()
645 645
646 646 @property
647 647 def firstname(self):
648 648 # alias for future
649 649 return self.name
650 650
651 651 @property
652 652 def emails(self):
653 653 other = UserEmailMap.query()\
654 654 .filter(UserEmailMap.user == self) \
655 655 .order_by(UserEmailMap.email_id.asc()) \
656 656 .all()
657 657 return [self.email] + [x.email for x in other]
658 658
659 659 @property
660 660 def auth_tokens(self):
661 661 auth_tokens = self.get_auth_tokens()
662 662 return [x.api_key for x in auth_tokens]
663 663
664 664 def get_auth_tokens(self):
665 665 return UserApiKeys.query()\
666 666 .filter(UserApiKeys.user == self)\
667 667 .order_by(UserApiKeys.user_api_key_id.asc())\
668 668 .all()
669 669
670 670 @property
671 671 def feed_token(self):
672 672 return self.get_feed_token()
673 673
674 674 def get_feed_token(self):
675 675 feed_tokens = UserApiKeys.query()\
676 676 .filter(UserApiKeys.user == self)\
677 677 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
678 678 .all()
679 679 if feed_tokens:
680 680 return feed_tokens[0].api_key
681 681 return 'NO_FEED_TOKEN_AVAILABLE'
682 682
683 683 @classmethod
684 684 def get(cls, user_id, cache=False):
685 685 if not user_id:
686 686 return
687 687
688 688 user = cls.query()
689 689 if cache:
690 690 user = user.options(
691 691 FromCache("sql_cache_short", "get_users_%s" % user_id))
692 692 return user.get(user_id)
693 693
694 694 @classmethod
695 695 def extra_valid_auth_tokens(cls, user, role=None):
696 696 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
697 697 .filter(or_(UserApiKeys.expires == -1,
698 698 UserApiKeys.expires >= time.time()))
699 699 if role:
700 700 tokens = tokens.filter(or_(UserApiKeys.role == role,
701 701 UserApiKeys.role == UserApiKeys.ROLE_ALL))
702 702 return tokens.all()
703 703
704 704 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
705 705 from rhodecode.lib import auth
706 706
707 707 log.debug('Trying to authenticate user: %s via auth-token, '
708 708 'and roles: %s', self, roles)
709 709
710 710 if not auth_token:
711 711 return False
712 712
713 713 crypto_backend = auth.crypto_backend()
714 714
715 715 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
716 716 tokens_q = UserApiKeys.query()\
717 717 .filter(UserApiKeys.user_id == self.user_id)\
718 718 .filter(or_(UserApiKeys.expires == -1,
719 719 UserApiKeys.expires >= time.time()))
720 720
721 721 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
722 722
723 723 plain_tokens = []
724 724 hash_tokens = []
725 725
726 726 for token in tokens_q.all():
727 727 # verify scope first
728 728 if token.repo_id:
729 729 # token has a scope, we need to verify it
730 730 if scope_repo_id != token.repo_id:
731 731 log.debug(
732 732 'Scope mismatch: token has a set repo scope: %s, '
733 733 'and calling scope is:%s, skipping further checks',
734 734 token.repo, scope_repo_id)
735 735 # token has a scope, and it doesn't match, skip token
736 736 continue
737 737
738 738 if token.api_key.startswith(crypto_backend.ENC_PREF):
739 739 hash_tokens.append(token.api_key)
740 740 else:
741 741 plain_tokens.append(token.api_key)
742 742
743 743 is_plain_match = auth_token in plain_tokens
744 744 if is_plain_match:
745 745 return True
746 746
747 747 for hashed in hash_tokens:
748 748 # TODO(marcink): this is expensive to calculate, but most secure
749 749 match = crypto_backend.hash_check(auth_token, hashed)
750 750 if match:
751 751 return True
752 752
753 753 return False
754 754
755 755 @property
756 756 def ip_addresses(self):
757 757 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
758 758 return [x.ip_addr for x in ret]
759 759
760 760 @property
761 761 def username_and_name(self):
762 762 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
763 763
764 764 @property
765 765 def username_or_name_or_email(self):
766 766 full_name = self.full_name if self.full_name is not ' ' else None
767 767 return self.username or full_name or self.email
768 768
769 769 @property
770 770 def full_name(self):
771 771 return '%s %s' % (self.first_name, self.last_name)
772 772
773 773 @property
774 774 def full_name_or_username(self):
775 775 return ('%s %s' % (self.first_name, self.last_name)
776 776 if (self.first_name and self.last_name) else self.username)
777 777
778 778 @property
779 779 def full_contact(self):
780 780 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
781 781
782 782 @property
783 783 def short_contact(self):
784 784 return '%s %s' % (self.first_name, self.last_name)
785 785
786 786 @property
787 787 def is_admin(self):
788 788 return self.admin
789 789
790 790 def AuthUser(self, **kwargs):
791 791 """
792 792 Returns instance of AuthUser for this user
793 793 """
794 794 from rhodecode.lib.auth import AuthUser
795 795 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
796 796
797 797 @hybrid_property
798 798 def user_data(self):
799 799 if not self._user_data:
800 800 return {}
801 801
802 802 try:
803 803 return json.loads(self._user_data)
804 804 except TypeError:
805 805 return {}
806 806
807 807 @user_data.setter
808 808 def user_data(self, val):
809 809 if not isinstance(val, dict):
810 810 raise Exception('user_data must be dict, got %s' % type(val))
811 811 try:
812 812 self._user_data = json.dumps(val)
813 813 except Exception:
814 814 log.error(traceback.format_exc())
815 815
816 816 @classmethod
817 817 def get_by_username(cls, username, case_insensitive=False,
818 818 cache=False, identity_cache=False):
819 819 session = Session()
820 820
821 821 if case_insensitive:
822 822 q = cls.query().filter(
823 823 func.lower(cls.username) == func.lower(username))
824 824 else:
825 825 q = cls.query().filter(cls.username == username)
826 826
827 827 if cache:
828 828 if identity_cache:
829 829 val = cls.identity_cache(session, 'username', username)
830 830 if val:
831 831 return val
832 832 else:
833 833 cache_key = "get_user_by_name_%s" % _hash_key(username)
834 834 q = q.options(
835 835 FromCache("sql_cache_short", cache_key))
836 836
837 837 return q.scalar()
838 838
839 839 @classmethod
840 840 def get_by_auth_token(cls, auth_token, cache=False):
841 841 q = UserApiKeys.query()\
842 842 .filter(UserApiKeys.api_key == auth_token)\
843 843 .filter(or_(UserApiKeys.expires == -1,
844 844 UserApiKeys.expires >= time.time()))
845 845 if cache:
846 846 q = q.options(
847 847 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
848 848
849 849 match = q.first()
850 850 if match:
851 851 return match.user
852 852
853 853 @classmethod
854 854 def get_by_email(cls, email, case_insensitive=False, cache=False):
855 855
856 856 if case_insensitive:
857 857 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
858 858
859 859 else:
860 860 q = cls.query().filter(cls.email == email)
861 861
862 862 email_key = _hash_key(email)
863 863 if cache:
864 864 q = q.options(
865 865 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
866 866
867 867 ret = q.scalar()
868 868 if ret is None:
869 869 q = UserEmailMap.query()
870 870 # try fetching in alternate email map
871 871 if case_insensitive:
872 872 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
873 873 else:
874 874 q = q.filter(UserEmailMap.email == email)
875 875 q = q.options(joinedload(UserEmailMap.user))
876 876 if cache:
877 877 q = q.options(
878 878 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
879 879 ret = getattr(q.scalar(), 'user', None)
880 880
881 881 return ret
882 882
883 883 @classmethod
884 884 def get_from_cs_author(cls, author):
885 885 """
886 886 Tries to get User objects out of commit author string
887 887
888 888 :param author:
889 889 """
890 890 from rhodecode.lib.helpers import email, author_name
891 891 # Valid email in the attribute passed, see if they're in the system
892 892 _email = email(author)
893 893 if _email:
894 894 user = cls.get_by_email(_email, case_insensitive=True)
895 895 if user:
896 896 return user
897 897 # Maybe we can match by username?
898 898 _author = author_name(author)
899 899 user = cls.get_by_username(_author, case_insensitive=True)
900 900 if user:
901 901 return user
902 902
903 903 def update_userdata(self, **kwargs):
904 904 usr = self
905 905 old = usr.user_data
906 906 old.update(**kwargs)
907 907 usr.user_data = old
908 908 Session().add(usr)
909 909 log.debug('updated userdata with ', kwargs)
910 910
911 911 def update_lastlogin(self):
912 912 """Update user lastlogin"""
913 913 self.last_login = datetime.datetime.now()
914 914 Session().add(self)
915 915 log.debug('updated user %s lastlogin', self.username)
916 916
917 917 def update_lastactivity(self):
918 918 """Update user lastactivity"""
919 919 self.last_activity = datetime.datetime.now()
920 920 Session().add(self)
921 log.debug('updated user %s lastactivity', self.username)
921 log.debug('updated user `%s` last activity', self.username)
922 922
923 923 def update_password(self, new_password):
924 924 from rhodecode.lib.auth import get_crypt_password
925 925
926 926 self.password = get_crypt_password(new_password)
927 927 Session().add(self)
928 928
929 929 @classmethod
930 930 def get_first_super_admin(cls):
931 931 user = User.query().filter(User.admin == true()).first()
932 932 if user is None:
933 933 raise Exception('FATAL: Missing administrative account!')
934 934 return user
935 935
936 936 @classmethod
937 937 def get_all_super_admins(cls):
938 938 """
939 939 Returns all admin accounts sorted by username
940 940 """
941 941 return User.query().filter(User.admin == true())\
942 942 .order_by(User.username.asc()).all()
943 943
944 944 @classmethod
945 945 def get_default_user(cls, cache=False, refresh=False):
946 946 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
947 947 if user is None:
948 948 raise Exception('FATAL: Missing default account!')
949 949 if refresh:
950 950 # The default user might be based on outdated state which
951 951 # has been loaded from the cache.
952 952 # A call to refresh() ensures that the
953 953 # latest state from the database is used.
954 954 Session().refresh(user)
955 955 return user
956 956
957 957 def _get_default_perms(self, user, suffix=''):
958 958 from rhodecode.model.permission import PermissionModel
959 959 return PermissionModel().get_default_perms(user.user_perms, suffix)
960 960
961 961 def get_default_perms(self, suffix=''):
962 962 return self._get_default_perms(self, suffix)
963 963
964 964 def get_api_data(self, include_secrets=False, details='full'):
965 965 """
966 966 Common function for generating user related data for API
967 967
968 968 :param include_secrets: By default secrets in the API data will be replaced
969 969 by a placeholder value to prevent exposing this data by accident. In case
970 970 this data shall be exposed, set this flag to ``True``.
971 971
972 972 :param details: details can be 'basic|full' basic gives only a subset of
973 973 the available user information that includes user_id, name and emails.
974 974 """
975 975 user = self
976 976 user_data = self.user_data
977 977 data = {
978 978 'user_id': user.user_id,
979 979 'username': user.username,
980 980 'firstname': user.name,
981 981 'lastname': user.lastname,
982 982 'email': user.email,
983 983 'emails': user.emails,
984 984 }
985 985 if details == 'basic':
986 986 return data
987 987
988 988 auth_token_length = 40
989 989 auth_token_replacement = '*' * auth_token_length
990 990
991 991 extras = {
992 992 'auth_tokens': [auth_token_replacement],
993 993 'active': user.active,
994 994 'admin': user.admin,
995 995 'extern_type': user.extern_type,
996 996 'extern_name': user.extern_name,
997 997 'last_login': user.last_login,
998 998 'last_activity': user.last_activity,
999 999 'ip_addresses': user.ip_addresses,
1000 1000 'language': user_data.get('language')
1001 1001 }
1002 1002 data.update(extras)
1003 1003
1004 1004 if include_secrets:
1005 1005 data['auth_tokens'] = user.auth_tokens
1006 1006 return data
1007 1007
1008 1008 def __json__(self):
1009 1009 data = {
1010 1010 'full_name': self.full_name,
1011 1011 'full_name_or_username': self.full_name_or_username,
1012 1012 'short_contact': self.short_contact,
1013 1013 'full_contact': self.full_contact,
1014 1014 }
1015 1015 data.update(self.get_api_data())
1016 1016 return data
1017 1017
1018 1018
1019 1019 class UserApiKeys(Base, BaseModel):
1020 1020 __tablename__ = 'user_api_keys'
1021 1021 __table_args__ = (
1022 1022 Index('uak_api_key_idx', 'api_key', unique=True),
1023 1023 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1024 1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1026 1026 )
1027 1027 __mapper_args__ = {}
1028 1028
1029 1029 # ApiKey role
1030 1030 ROLE_ALL = 'token_role_all'
1031 1031 ROLE_HTTP = 'token_role_http'
1032 1032 ROLE_VCS = 'token_role_vcs'
1033 1033 ROLE_API = 'token_role_api'
1034 1034 ROLE_FEED = 'token_role_feed'
1035 1035 ROLE_PASSWORD_RESET = 'token_password_reset'
1036 1036
1037 1037 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1038 1038
1039 1039 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1040 1040 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1041 1041 api_key = Column("api_key", String(255), nullable=False, unique=True)
1042 1042 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1043 1043 expires = Column('expires', Float(53), nullable=False)
1044 1044 role = Column('role', String(255), nullable=True)
1045 1045 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1046 1046
1047 1047 # scope columns
1048 1048 repo_id = Column(
1049 1049 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1050 1050 nullable=True, unique=None, default=None)
1051 1051 repo = relationship('Repository', lazy='joined')
1052 1052
1053 1053 repo_group_id = Column(
1054 1054 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1055 1055 nullable=True, unique=None, default=None)
1056 1056 repo_group = relationship('RepoGroup', lazy='joined')
1057 1057
1058 1058 user = relationship('User', lazy='joined')
1059 1059
1060 1060 def __unicode__(self):
1061 1061 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1062 1062
1063 1063 def __json__(self):
1064 1064 data = {
1065 1065 'auth_token': self.api_key,
1066 1066 'role': self.role,
1067 1067 'scope': self.scope_humanized,
1068 1068 'expired': self.expired
1069 1069 }
1070 1070 return data
1071 1071
1072 1072 def get_api_data(self, include_secrets=False):
1073 1073 data = self.__json__()
1074 1074 if include_secrets:
1075 1075 return data
1076 1076 else:
1077 1077 data['auth_token'] = self.token_obfuscated
1078 1078 return data
1079 1079
1080 1080 @hybrid_property
1081 1081 def description_safe(self):
1082 1082 from rhodecode.lib import helpers as h
1083 1083 return h.escape(self.description)
1084 1084
1085 1085 @property
1086 1086 def expired(self):
1087 1087 if self.expires == -1:
1088 1088 return False
1089 1089 return time.time() > self.expires
1090 1090
1091 1091 @classmethod
1092 1092 def _get_role_name(cls, role):
1093 1093 return {
1094 1094 cls.ROLE_ALL: _('all'),
1095 1095 cls.ROLE_HTTP: _('http/web interface'),
1096 1096 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1097 1097 cls.ROLE_API: _('api calls'),
1098 1098 cls.ROLE_FEED: _('feed access'),
1099 1099 }.get(role, role)
1100 1100
1101 1101 @property
1102 1102 def role_humanized(self):
1103 1103 return self._get_role_name(self.role)
1104 1104
1105 1105 def _get_scope(self):
1106 1106 if self.repo:
1107 1107 return repr(self.repo)
1108 1108 if self.repo_group:
1109 1109 return repr(self.repo_group) + ' (recursive)'
1110 1110 return 'global'
1111 1111
1112 1112 @property
1113 1113 def scope_humanized(self):
1114 1114 return self._get_scope()
1115 1115
1116 1116 @property
1117 1117 def token_obfuscated(self):
1118 1118 if self.api_key:
1119 1119 return self.api_key[:4] + "****"
1120 1120
1121 1121
1122 1122 class UserEmailMap(Base, BaseModel):
1123 1123 __tablename__ = 'user_email_map'
1124 1124 __table_args__ = (
1125 1125 Index('uem_email_idx', 'email'),
1126 1126 UniqueConstraint('email'),
1127 1127 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1128 1128 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1129 1129 )
1130 1130 __mapper_args__ = {}
1131 1131
1132 1132 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1133 1133 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1134 1134 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1135 1135 user = relationship('User', lazy='joined')
1136 1136
1137 1137 @validates('_email')
1138 1138 def validate_email(self, key, email):
1139 1139 # check if this email is not main one
1140 1140 main_email = Session().query(User).filter(User.email == email).scalar()
1141 1141 if main_email is not None:
1142 1142 raise AttributeError('email %s is present is user table' % email)
1143 1143 return email
1144 1144
1145 1145 @hybrid_property
1146 1146 def email(self):
1147 1147 return self._email
1148 1148
1149 1149 @email.setter
1150 1150 def email(self, val):
1151 1151 self._email = val.lower() if val else None
1152 1152
1153 1153
1154 1154 class UserIpMap(Base, BaseModel):
1155 1155 __tablename__ = 'user_ip_map'
1156 1156 __table_args__ = (
1157 1157 UniqueConstraint('user_id', 'ip_addr'),
1158 1158 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1159 1159 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1160 1160 )
1161 1161 __mapper_args__ = {}
1162 1162
1163 1163 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1164 1164 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1165 1165 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1166 1166 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1167 1167 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1168 1168 user = relationship('User', lazy='joined')
1169 1169
1170 1170 @hybrid_property
1171 1171 def description_safe(self):
1172 1172 from rhodecode.lib import helpers as h
1173 1173 return h.escape(self.description)
1174 1174
1175 1175 @classmethod
1176 1176 def _get_ip_range(cls, ip_addr):
1177 1177 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1178 1178 return [str(net.network_address), str(net.broadcast_address)]
1179 1179
1180 1180 def __json__(self):
1181 1181 return {
1182 1182 'ip_addr': self.ip_addr,
1183 1183 'ip_range': self._get_ip_range(self.ip_addr),
1184 1184 }
1185 1185
1186 1186 def __unicode__(self):
1187 1187 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1188 1188 self.user_id, self.ip_addr)
1189 1189
1190 1190
1191 1191 class UserSshKeys(Base, BaseModel):
1192 1192 __tablename__ = 'user_ssh_keys'
1193 1193 __table_args__ = (
1194 1194 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1195 1195
1196 1196 UniqueConstraint('ssh_key_fingerprint'),
1197 1197
1198 1198 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1199 1199 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1200 1200 )
1201 1201 __mapper_args__ = {}
1202 1202
1203 1203 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1204 1204 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1205 1205 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(1024), nullable=False, unique=None, default=None)
1206 1206
1207 1207 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1208 1208
1209 1209 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1210 1210 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1211 1211 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1212 1212
1213 1213 user = relationship('User', lazy='joined')
1214 1214
1215 1215 def __json__(self):
1216 1216 data = {
1217 1217 'ssh_fingerprint': self.ssh_key_fingerprint,
1218 1218 'description': self.description,
1219 1219 'created_on': self.created_on
1220 1220 }
1221 1221 return data
1222 1222
1223 1223 def get_api_data(self):
1224 1224 data = self.__json__()
1225 1225 return data
1226 1226
1227 1227
1228 1228 class UserLog(Base, BaseModel):
1229 1229 __tablename__ = 'user_logs'
1230 1230 __table_args__ = (
1231 1231 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1232 1232 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1233 1233 )
1234 1234 VERSION_1 = 'v1'
1235 1235 VERSION_2 = 'v2'
1236 1236 VERSIONS = [VERSION_1, VERSION_2]
1237 1237
1238 1238 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1239 1239 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1240 1240 username = Column("username", String(255), nullable=True, unique=None, default=None)
1241 1241 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1242 1242 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1243 1243 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1244 1244 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1245 1245 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1246 1246
1247 1247 version = Column("version", String(255), nullable=True, default=VERSION_1)
1248 1248 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1249 1249 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1250 1250
1251 1251 def __unicode__(self):
1252 1252 return u"<%s('id:%s:%s')>" % (
1253 1253 self.__class__.__name__, self.repository_name, self.action)
1254 1254
1255 1255 def __json__(self):
1256 1256 return {
1257 1257 'user_id': self.user_id,
1258 1258 'username': self.username,
1259 1259 'repository_id': self.repository_id,
1260 1260 'repository_name': self.repository_name,
1261 1261 'user_ip': self.user_ip,
1262 1262 'action_date': self.action_date,
1263 1263 'action': self.action,
1264 1264 }
1265 1265
1266 1266 @hybrid_property
1267 1267 def entry_id(self):
1268 1268 return self.user_log_id
1269 1269
1270 1270 @property
1271 1271 def action_as_day(self):
1272 1272 return datetime.date(*self.action_date.timetuple()[:3])
1273 1273
1274 1274 user = relationship('User')
1275 1275 repository = relationship('Repository', cascade='')
1276 1276
1277 1277
1278 1278 class UserGroup(Base, BaseModel):
1279 1279 __tablename__ = 'users_groups'
1280 1280 __table_args__ = (
1281 1281 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1282 1282 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1283 1283 )
1284 1284
1285 1285 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1286 1286 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1287 1287 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1288 1288 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1289 1289 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1290 1290 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1291 1291 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1292 1292 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1293 1293
1294 1294 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1295 1295 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1296 1296 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1297 1297 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1298 1298 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1299 1299 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1300 1300
1301 1301 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1302 1302 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1303 1303
1304 1304 @classmethod
1305 1305 def _load_group_data(cls, column):
1306 1306 if not column:
1307 1307 return {}
1308 1308
1309 1309 try:
1310 1310 return json.loads(column) or {}
1311 1311 except TypeError:
1312 1312 return {}
1313 1313
1314 1314 @hybrid_property
1315 1315 def description_safe(self):
1316 1316 from rhodecode.lib import helpers as h
1317 1317 return h.escape(self.description)
1318 1318
1319 1319 @hybrid_property
1320 1320 def group_data(self):
1321 1321 return self._load_group_data(self._group_data)
1322 1322
1323 1323 @group_data.expression
1324 1324 def group_data(self, **kwargs):
1325 1325 return self._group_data
1326 1326
1327 1327 @group_data.setter
1328 1328 def group_data(self, val):
1329 1329 try:
1330 1330 self._group_data = json.dumps(val)
1331 1331 except Exception:
1332 1332 log.error(traceback.format_exc())
1333 1333
1334 1334 def __unicode__(self):
1335 1335 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1336 1336 self.users_group_id,
1337 1337 self.users_group_name)
1338 1338
1339 1339 @classmethod
1340 1340 def get_by_group_name(cls, group_name, cache=False,
1341 1341 case_insensitive=False):
1342 1342 if case_insensitive:
1343 1343 q = cls.query().filter(func.lower(cls.users_group_name) ==
1344 1344 func.lower(group_name))
1345 1345
1346 1346 else:
1347 1347 q = cls.query().filter(cls.users_group_name == group_name)
1348 1348 if cache:
1349 1349 q = q.options(
1350 1350 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1351 1351 return q.scalar()
1352 1352
1353 1353 @classmethod
1354 1354 def get(cls, user_group_id, cache=False):
1355 1355 if not user_group_id:
1356 1356 return
1357 1357
1358 1358 user_group = cls.query()
1359 1359 if cache:
1360 1360 user_group = user_group.options(
1361 1361 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1362 1362 return user_group.get(user_group_id)
1363 1363
1364 1364 def permissions(self, with_admins=True, with_owner=True):
1365 1365 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1366 1366 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1367 1367 joinedload(UserUserGroupToPerm.user),
1368 1368 joinedload(UserUserGroupToPerm.permission),)
1369 1369
1370 1370 # get owners and admins and permissions. We do a trick of re-writing
1371 1371 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1372 1372 # has a global reference and changing one object propagates to all
1373 1373 # others. This means if admin is also an owner admin_row that change
1374 1374 # would propagate to both objects
1375 1375 perm_rows = []
1376 1376 for _usr in q.all():
1377 1377 usr = AttributeDict(_usr.user.get_dict())
1378 1378 usr.permission = _usr.permission.permission_name
1379 1379 perm_rows.append(usr)
1380 1380
1381 1381 # filter the perm rows by 'default' first and then sort them by
1382 1382 # admin,write,read,none permissions sorted again alphabetically in
1383 1383 # each group
1384 1384 perm_rows = sorted(perm_rows, key=display_user_sort)
1385 1385
1386 1386 _admin_perm = 'usergroup.admin'
1387 1387 owner_row = []
1388 1388 if with_owner:
1389 1389 usr = AttributeDict(self.user.get_dict())
1390 1390 usr.owner_row = True
1391 1391 usr.permission = _admin_perm
1392 1392 owner_row.append(usr)
1393 1393
1394 1394 super_admin_rows = []
1395 1395 if with_admins:
1396 1396 for usr in User.get_all_super_admins():
1397 1397 # if this admin is also owner, don't double the record
1398 1398 if usr.user_id == owner_row[0].user_id:
1399 1399 owner_row[0].admin_row = True
1400 1400 else:
1401 1401 usr = AttributeDict(usr.get_dict())
1402 1402 usr.admin_row = True
1403 1403 usr.permission = _admin_perm
1404 1404 super_admin_rows.append(usr)
1405 1405
1406 1406 return super_admin_rows + owner_row + perm_rows
1407 1407
1408 1408 def permission_user_groups(self):
1409 1409 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1410 1410 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1411 1411 joinedload(UserGroupUserGroupToPerm.target_user_group),
1412 1412 joinedload(UserGroupUserGroupToPerm.permission),)
1413 1413
1414 1414 perm_rows = []
1415 1415 for _user_group in q.all():
1416 1416 usr = AttributeDict(_user_group.user_group.get_dict())
1417 1417 usr.permission = _user_group.permission.permission_name
1418 1418 perm_rows.append(usr)
1419 1419
1420 1420 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1421 1421 return perm_rows
1422 1422
1423 1423 def _get_default_perms(self, user_group, suffix=''):
1424 1424 from rhodecode.model.permission import PermissionModel
1425 1425 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1426 1426
1427 1427 def get_default_perms(self, suffix=''):
1428 1428 return self._get_default_perms(self, suffix)
1429 1429
1430 1430 def get_api_data(self, with_group_members=True, include_secrets=False):
1431 1431 """
1432 1432 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1433 1433 basically forwarded.
1434 1434
1435 1435 """
1436 1436 user_group = self
1437 1437 data = {
1438 1438 'users_group_id': user_group.users_group_id,
1439 1439 'group_name': user_group.users_group_name,
1440 1440 'group_description': user_group.user_group_description,
1441 1441 'active': user_group.users_group_active,
1442 1442 'owner': user_group.user.username,
1443 1443 'owner_email': user_group.user.email,
1444 1444 }
1445 1445
1446 1446 if with_group_members:
1447 1447 users = []
1448 1448 for user in user_group.members:
1449 1449 user = user.user
1450 1450 users.append(user.get_api_data(include_secrets=include_secrets))
1451 1451 data['users'] = users
1452 1452
1453 1453 return data
1454 1454
1455 1455
1456 1456 class UserGroupMember(Base, BaseModel):
1457 1457 __tablename__ = 'users_groups_members'
1458 1458 __table_args__ = (
1459 1459 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1460 1460 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1461 1461 )
1462 1462
1463 1463 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1464 1464 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1465 1465 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1466 1466
1467 1467 user = relationship('User', lazy='joined')
1468 1468 users_group = relationship('UserGroup')
1469 1469
1470 1470 def __init__(self, gr_id='', u_id=''):
1471 1471 self.users_group_id = gr_id
1472 1472 self.user_id = u_id
1473 1473
1474 1474
1475 1475 class RepositoryField(Base, BaseModel):
1476 1476 __tablename__ = 'repositories_fields'
1477 1477 __table_args__ = (
1478 1478 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1479 1479 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1480 1480 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1481 1481 )
1482 1482 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1483 1483
1484 1484 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1485 1485 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1486 1486 field_key = Column("field_key", String(250))
1487 1487 field_label = Column("field_label", String(1024), nullable=False)
1488 1488 field_value = Column("field_value", String(10000), nullable=False)
1489 1489 field_desc = Column("field_desc", String(1024), nullable=False)
1490 1490 field_type = Column("field_type", String(255), nullable=False, unique=None)
1491 1491 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1492 1492
1493 1493 repository = relationship('Repository')
1494 1494
1495 1495 @property
1496 1496 def field_key_prefixed(self):
1497 1497 return 'ex_%s' % self.field_key
1498 1498
1499 1499 @classmethod
1500 1500 def un_prefix_key(cls, key):
1501 1501 if key.startswith(cls.PREFIX):
1502 1502 return key[len(cls.PREFIX):]
1503 1503 return key
1504 1504
1505 1505 @classmethod
1506 1506 def get_by_key_name(cls, key, repo):
1507 1507 row = cls.query()\
1508 1508 .filter(cls.repository == repo)\
1509 1509 .filter(cls.field_key == key).scalar()
1510 1510 return row
1511 1511
1512 1512
1513 1513 class Repository(Base, BaseModel):
1514 1514 __tablename__ = 'repositories'
1515 1515 __table_args__ = (
1516 1516 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1517 1517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1518 1518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1519 1519 )
1520 1520 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1521 1521 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1522 1522
1523 1523 STATE_CREATED = 'repo_state_created'
1524 1524 STATE_PENDING = 'repo_state_pending'
1525 1525 STATE_ERROR = 'repo_state_error'
1526 1526
1527 1527 LOCK_AUTOMATIC = 'lock_auto'
1528 1528 LOCK_API = 'lock_api'
1529 1529 LOCK_WEB = 'lock_web'
1530 1530 LOCK_PULL = 'lock_pull'
1531 1531
1532 1532 NAME_SEP = URL_SEP
1533 1533
1534 1534 repo_id = Column(
1535 1535 "repo_id", Integer(), nullable=False, unique=True, default=None,
1536 1536 primary_key=True)
1537 1537 _repo_name = Column(
1538 1538 "repo_name", Text(), nullable=False, default=None)
1539 1539 _repo_name_hash = Column(
1540 1540 "repo_name_hash", String(255), nullable=False, unique=True)
1541 1541 repo_state = Column("repo_state", String(255), nullable=True)
1542 1542
1543 1543 clone_uri = Column(
1544 1544 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1545 1545 default=None)
1546 1546 repo_type = Column(
1547 1547 "repo_type", String(255), nullable=False, unique=False, default=None)
1548 1548 user_id = Column(
1549 1549 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1550 1550 unique=False, default=None)
1551 1551 private = Column(
1552 1552 "private", Boolean(), nullable=True, unique=None, default=None)
1553 1553 enable_statistics = Column(
1554 1554 "statistics", Boolean(), nullable=True, unique=None, default=True)
1555 1555 enable_downloads = Column(
1556 1556 "downloads", Boolean(), nullable=True, unique=None, default=True)
1557 1557 description = Column(
1558 1558 "description", String(10000), nullable=True, unique=None, default=None)
1559 1559 created_on = Column(
1560 1560 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1561 1561 default=datetime.datetime.now)
1562 1562 updated_on = Column(
1563 1563 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1564 1564 default=datetime.datetime.now)
1565 1565 _landing_revision = Column(
1566 1566 "landing_revision", String(255), nullable=False, unique=False,
1567 1567 default=None)
1568 1568 enable_locking = Column(
1569 1569 "enable_locking", Boolean(), nullable=False, unique=None,
1570 1570 default=False)
1571 1571 _locked = Column(
1572 1572 "locked", String(255), nullable=True, unique=False, default=None)
1573 1573 _changeset_cache = Column(
1574 1574 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1575 1575
1576 1576 fork_id = Column(
1577 1577 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1578 1578 nullable=True, unique=False, default=None)
1579 1579 group_id = Column(
1580 1580 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1581 1581 unique=False, default=None)
1582 1582
1583 1583 user = relationship('User', lazy='joined')
1584 1584 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1585 1585 group = relationship('RepoGroup', lazy='joined')
1586 1586 repo_to_perm = relationship(
1587 1587 'UserRepoToPerm', cascade='all',
1588 1588 order_by='UserRepoToPerm.repo_to_perm_id')
1589 1589 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1590 1590 stats = relationship('Statistics', cascade='all', uselist=False)
1591 1591
1592 1592 followers = relationship(
1593 1593 'UserFollowing',
1594 1594 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1595 1595 cascade='all')
1596 1596 extra_fields = relationship(
1597 1597 'RepositoryField', cascade="all, delete, delete-orphan")
1598 1598 logs = relationship('UserLog')
1599 1599 comments = relationship(
1600 1600 'ChangesetComment', cascade="all, delete, delete-orphan")
1601 1601 pull_requests_source = relationship(
1602 1602 'PullRequest',
1603 1603 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1604 1604 cascade="all, delete, delete-orphan")
1605 1605 pull_requests_target = relationship(
1606 1606 'PullRequest',
1607 1607 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1608 1608 cascade="all, delete, delete-orphan")
1609 1609 ui = relationship('RepoRhodeCodeUi', cascade="all")
1610 1610 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1611 1611 integrations = relationship('Integration',
1612 1612 cascade="all, delete, delete-orphan")
1613 1613
1614 1614 def __unicode__(self):
1615 1615 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1616 1616 safe_unicode(self.repo_name))
1617 1617
1618 1618 @hybrid_property
1619 1619 def description_safe(self):
1620 1620 from rhodecode.lib import helpers as h
1621 1621 return h.escape(self.description)
1622 1622
1623 1623 @hybrid_property
1624 1624 def landing_rev(self):
1625 1625 # always should return [rev_type, rev]
1626 1626 if self._landing_revision:
1627 1627 _rev_info = self._landing_revision.split(':')
1628 1628 if len(_rev_info) < 2:
1629 1629 _rev_info.insert(0, 'rev')
1630 1630 return [_rev_info[0], _rev_info[1]]
1631 1631 return [None, None]
1632 1632
1633 1633 @landing_rev.setter
1634 1634 def landing_rev(self, val):
1635 1635 if ':' not in val:
1636 1636 raise ValueError('value must be delimited with `:` and consist '
1637 1637 'of <rev_type>:<rev>, got %s instead' % val)
1638 1638 self._landing_revision = val
1639 1639
1640 1640 @hybrid_property
1641 1641 def locked(self):
1642 1642 if self._locked:
1643 1643 user_id, timelocked, reason = self._locked.split(':')
1644 1644 lock_values = int(user_id), timelocked, reason
1645 1645 else:
1646 1646 lock_values = [None, None, None]
1647 1647 return lock_values
1648 1648
1649 1649 @locked.setter
1650 1650 def locked(self, val):
1651 1651 if val and isinstance(val, (list, tuple)):
1652 1652 self._locked = ':'.join(map(str, val))
1653 1653 else:
1654 1654 self._locked = None
1655 1655
1656 1656 @hybrid_property
1657 1657 def changeset_cache(self):
1658 1658 from rhodecode.lib.vcs.backends.base import EmptyCommit
1659 1659 dummy = EmptyCommit().__json__()
1660 1660 if not self._changeset_cache:
1661 1661 return dummy
1662 1662 try:
1663 1663 return json.loads(self._changeset_cache)
1664 1664 except TypeError:
1665 1665 return dummy
1666 1666 except Exception:
1667 1667 log.error(traceback.format_exc())
1668 1668 return dummy
1669 1669
1670 1670 @changeset_cache.setter
1671 1671 def changeset_cache(self, val):
1672 1672 try:
1673 1673 self._changeset_cache = json.dumps(val)
1674 1674 except Exception:
1675 1675 log.error(traceback.format_exc())
1676 1676
1677 1677 @hybrid_property
1678 1678 def repo_name(self):
1679 1679 return self._repo_name
1680 1680
1681 1681 @repo_name.setter
1682 1682 def repo_name(self, value):
1683 1683 self._repo_name = value
1684 1684 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1685 1685
1686 1686 @classmethod
1687 1687 def normalize_repo_name(cls, repo_name):
1688 1688 """
1689 1689 Normalizes os specific repo_name to the format internally stored inside
1690 1690 database using URL_SEP
1691 1691
1692 1692 :param cls:
1693 1693 :param repo_name:
1694 1694 """
1695 1695 return cls.NAME_SEP.join(repo_name.split(os.sep))
1696 1696
1697 1697 @classmethod
1698 1698 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1699 1699 session = Session()
1700 1700 q = session.query(cls).filter(cls.repo_name == repo_name)
1701 1701
1702 1702 if cache:
1703 1703 if identity_cache:
1704 1704 val = cls.identity_cache(session, 'repo_name', repo_name)
1705 1705 if val:
1706 1706 return val
1707 1707 else:
1708 1708 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1709 1709 q = q.options(
1710 1710 FromCache("sql_cache_short", cache_key))
1711 1711
1712 1712 return q.scalar()
1713 1713
1714 1714 @classmethod
1715 1715 def get_by_full_path(cls, repo_full_path):
1716 1716 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1717 1717 repo_name = cls.normalize_repo_name(repo_name)
1718 1718 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1719 1719
1720 1720 @classmethod
1721 1721 def get_repo_forks(cls, repo_id):
1722 1722 return cls.query().filter(Repository.fork_id == repo_id)
1723 1723
1724 1724 @classmethod
1725 1725 def base_path(cls):
1726 1726 """
1727 1727 Returns base path when all repos are stored
1728 1728
1729 1729 :param cls:
1730 1730 """
1731 1731 q = Session().query(RhodeCodeUi)\
1732 1732 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1733 1733 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1734 1734 return q.one().ui_value
1735 1735
1736 1736 @classmethod
1737 1737 def is_valid(cls, repo_name):
1738 1738 """
1739 1739 returns True if given repo name is a valid filesystem repository
1740 1740
1741 1741 :param cls:
1742 1742 :param repo_name:
1743 1743 """
1744 1744 from rhodecode.lib.utils import is_valid_repo
1745 1745
1746 1746 return is_valid_repo(repo_name, cls.base_path())
1747 1747
1748 1748 @classmethod
1749 1749 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1750 1750 case_insensitive=True):
1751 1751 q = Repository.query()
1752 1752
1753 1753 if not isinstance(user_id, Optional):
1754 1754 q = q.filter(Repository.user_id == user_id)
1755 1755
1756 1756 if not isinstance(group_id, Optional):
1757 1757 q = q.filter(Repository.group_id == group_id)
1758 1758
1759 1759 if case_insensitive:
1760 1760 q = q.order_by(func.lower(Repository.repo_name))
1761 1761 else:
1762 1762 q = q.order_by(Repository.repo_name)
1763 1763 return q.all()
1764 1764
1765 1765 @property
1766 1766 def forks(self):
1767 1767 """
1768 1768 Return forks of this repo
1769 1769 """
1770 1770 return Repository.get_repo_forks(self.repo_id)
1771 1771
1772 1772 @property
1773 1773 def parent(self):
1774 1774 """
1775 1775 Returns fork parent
1776 1776 """
1777 1777 return self.fork
1778 1778
1779 1779 @property
1780 1780 def just_name(self):
1781 1781 return self.repo_name.split(self.NAME_SEP)[-1]
1782 1782
1783 1783 @property
1784 1784 def groups_with_parents(self):
1785 1785 groups = []
1786 1786 if self.group is None:
1787 1787 return groups
1788 1788
1789 1789 cur_gr = self.group
1790 1790 groups.insert(0, cur_gr)
1791 1791 while 1:
1792 1792 gr = getattr(cur_gr, 'parent_group', None)
1793 1793 cur_gr = cur_gr.parent_group
1794 1794 if gr is None:
1795 1795 break
1796 1796 groups.insert(0, gr)
1797 1797
1798 1798 return groups
1799 1799
1800 1800 @property
1801 1801 def groups_and_repo(self):
1802 1802 return self.groups_with_parents, self
1803 1803
1804 1804 @LazyProperty
1805 1805 def repo_path(self):
1806 1806 """
1807 1807 Returns base full path for that repository means where it actually
1808 1808 exists on a filesystem
1809 1809 """
1810 1810 q = Session().query(RhodeCodeUi).filter(
1811 1811 RhodeCodeUi.ui_key == self.NAME_SEP)
1812 1812 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1813 1813 return q.one().ui_value
1814 1814
1815 1815 @property
1816 1816 def repo_full_path(self):
1817 1817 p = [self.repo_path]
1818 1818 # we need to split the name by / since this is how we store the
1819 1819 # names in the database, but that eventually needs to be converted
1820 1820 # into a valid system path
1821 1821 p += self.repo_name.split(self.NAME_SEP)
1822 1822 return os.path.join(*map(safe_unicode, p))
1823 1823
1824 1824 @property
1825 1825 def cache_keys(self):
1826 1826 """
1827 1827 Returns associated cache keys for that repo
1828 1828 """
1829 1829 return CacheKey.query()\
1830 1830 .filter(CacheKey.cache_args == self.repo_name)\
1831 1831 .order_by(CacheKey.cache_key)\
1832 1832 .all()
1833 1833
1834 1834 def get_new_name(self, repo_name):
1835 1835 """
1836 1836 returns new full repository name based on assigned group and new new
1837 1837
1838 1838 :param group_name:
1839 1839 """
1840 1840 path_prefix = self.group.full_path_splitted if self.group else []
1841 1841 return self.NAME_SEP.join(path_prefix + [repo_name])
1842 1842
1843 1843 @property
1844 1844 def _config(self):
1845 1845 """
1846 1846 Returns db based config object.
1847 1847 """
1848 1848 from rhodecode.lib.utils import make_db_config
1849 1849 return make_db_config(clear_session=False, repo=self)
1850 1850
1851 1851 def permissions(self, with_admins=True, with_owner=True):
1852 1852 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1853 1853 q = q.options(joinedload(UserRepoToPerm.repository),
1854 1854 joinedload(UserRepoToPerm.user),
1855 1855 joinedload(UserRepoToPerm.permission),)
1856 1856
1857 1857 # get owners and admins and permissions. We do a trick of re-writing
1858 1858 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1859 1859 # has a global reference and changing one object propagates to all
1860 1860 # others. This means if admin is also an owner admin_row that change
1861 1861 # would propagate to both objects
1862 1862 perm_rows = []
1863 1863 for _usr in q.all():
1864 1864 usr = AttributeDict(_usr.user.get_dict())
1865 1865 usr.permission = _usr.permission.permission_name
1866 1866 perm_rows.append(usr)
1867 1867
1868 1868 # filter the perm rows by 'default' first and then sort them by
1869 1869 # admin,write,read,none permissions sorted again alphabetically in
1870 1870 # each group
1871 1871 perm_rows = sorted(perm_rows, key=display_user_sort)
1872 1872
1873 1873 _admin_perm = 'repository.admin'
1874 1874 owner_row = []
1875 1875 if with_owner:
1876 1876 usr = AttributeDict(self.user.get_dict())
1877 1877 usr.owner_row = True
1878 1878 usr.permission = _admin_perm
1879 1879 owner_row.append(usr)
1880 1880
1881 1881 super_admin_rows = []
1882 1882 if with_admins:
1883 1883 for usr in User.get_all_super_admins():
1884 1884 # if this admin is also owner, don't double the record
1885 1885 if usr.user_id == owner_row[0].user_id:
1886 1886 owner_row[0].admin_row = True
1887 1887 else:
1888 1888 usr = AttributeDict(usr.get_dict())
1889 1889 usr.admin_row = True
1890 1890 usr.permission = _admin_perm
1891 1891 super_admin_rows.append(usr)
1892 1892
1893 1893 return super_admin_rows + owner_row + perm_rows
1894 1894
1895 1895 def permission_user_groups(self):
1896 1896 q = UserGroupRepoToPerm.query().filter(
1897 1897 UserGroupRepoToPerm.repository == self)
1898 1898 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1899 1899 joinedload(UserGroupRepoToPerm.users_group),
1900 1900 joinedload(UserGroupRepoToPerm.permission),)
1901 1901
1902 1902 perm_rows = []
1903 1903 for _user_group in q.all():
1904 1904 usr = AttributeDict(_user_group.users_group.get_dict())
1905 1905 usr.permission = _user_group.permission.permission_name
1906 1906 perm_rows.append(usr)
1907 1907
1908 1908 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1909 1909 return perm_rows
1910 1910
1911 1911 def get_api_data(self, include_secrets=False):
1912 1912 """
1913 1913 Common function for generating repo api data
1914 1914
1915 1915 :param include_secrets: See :meth:`User.get_api_data`.
1916 1916
1917 1917 """
1918 1918 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1919 1919 # move this methods on models level.
1920 1920 from rhodecode.model.settings import SettingsModel
1921 1921 from rhodecode.model.repo import RepoModel
1922 1922
1923 1923 repo = self
1924 1924 _user_id, _time, _reason = self.locked
1925 1925
1926 1926 data = {
1927 1927 'repo_id': repo.repo_id,
1928 1928 'repo_name': repo.repo_name,
1929 1929 'repo_type': repo.repo_type,
1930 1930 'clone_uri': repo.clone_uri or '',
1931 1931 'url': RepoModel().get_url(self),
1932 1932 'private': repo.private,
1933 1933 'created_on': repo.created_on,
1934 1934 'description': repo.description_safe,
1935 1935 'landing_rev': repo.landing_rev,
1936 1936 'owner': repo.user.username,
1937 1937 'fork_of': repo.fork.repo_name if repo.fork else None,
1938 1938 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1939 1939 'enable_statistics': repo.enable_statistics,
1940 1940 'enable_locking': repo.enable_locking,
1941 1941 'enable_downloads': repo.enable_downloads,
1942 1942 'last_changeset': repo.changeset_cache,
1943 1943 'locked_by': User.get(_user_id).get_api_data(
1944 1944 include_secrets=include_secrets) if _user_id else None,
1945 1945 'locked_date': time_to_datetime(_time) if _time else None,
1946 1946 'lock_reason': _reason if _reason else None,
1947 1947 }
1948 1948
1949 1949 # TODO: mikhail: should be per-repo settings here
1950 1950 rc_config = SettingsModel().get_all_settings()
1951 1951 repository_fields = str2bool(
1952 1952 rc_config.get('rhodecode_repository_fields'))
1953 1953 if repository_fields:
1954 1954 for f in self.extra_fields:
1955 1955 data[f.field_key_prefixed] = f.field_value
1956 1956
1957 1957 return data
1958 1958
1959 1959 @classmethod
1960 1960 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1961 1961 if not lock_time:
1962 1962 lock_time = time.time()
1963 1963 if not lock_reason:
1964 1964 lock_reason = cls.LOCK_AUTOMATIC
1965 1965 repo.locked = [user_id, lock_time, lock_reason]
1966 1966 Session().add(repo)
1967 1967 Session().commit()
1968 1968
1969 1969 @classmethod
1970 1970 def unlock(cls, repo):
1971 1971 repo.locked = None
1972 1972 Session().add(repo)
1973 1973 Session().commit()
1974 1974
1975 1975 @classmethod
1976 1976 def getlock(cls, repo):
1977 1977 return repo.locked
1978 1978
1979 1979 def is_user_lock(self, user_id):
1980 1980 if self.lock[0]:
1981 1981 lock_user_id = safe_int(self.lock[0])
1982 1982 user_id = safe_int(user_id)
1983 1983 # both are ints, and they are equal
1984 1984 return all([lock_user_id, user_id]) and lock_user_id == user_id
1985 1985
1986 1986 return False
1987 1987
1988 1988 def get_locking_state(self, action, user_id, only_when_enabled=True):
1989 1989 """
1990 1990 Checks locking on this repository, if locking is enabled and lock is
1991 1991 present returns a tuple of make_lock, locked, locked_by.
1992 1992 make_lock can have 3 states None (do nothing) True, make lock
1993 1993 False release lock, This value is later propagated to hooks, which
1994 1994 do the locking. Think about this as signals passed to hooks what to do.
1995 1995
1996 1996 """
1997 1997 # TODO: johbo: This is part of the business logic and should be moved
1998 1998 # into the RepositoryModel.
1999 1999
2000 2000 if action not in ('push', 'pull'):
2001 2001 raise ValueError("Invalid action value: %s" % repr(action))
2002 2002
2003 2003 # defines if locked error should be thrown to user
2004 2004 currently_locked = False
2005 2005 # defines if new lock should be made, tri-state
2006 2006 make_lock = None
2007 2007 repo = self
2008 2008 user = User.get(user_id)
2009 2009
2010 2010 lock_info = repo.locked
2011 2011
2012 2012 if repo and (repo.enable_locking or not only_when_enabled):
2013 2013 if action == 'push':
2014 2014 # check if it's already locked !, if it is compare users
2015 2015 locked_by_user_id = lock_info[0]
2016 2016 if user.user_id == locked_by_user_id:
2017 2017 log.debug(
2018 2018 'Got `push` action from user %s, now unlocking', user)
2019 2019 # unlock if we have push from user who locked
2020 2020 make_lock = False
2021 2021 else:
2022 2022 # we're not the same user who locked, ban with
2023 2023 # code defined in settings (default is 423 HTTP Locked) !
2024 2024 log.debug('Repo %s is currently locked by %s', repo, user)
2025 2025 currently_locked = True
2026 2026 elif action == 'pull':
2027 2027 # [0] user [1] date
2028 2028 if lock_info[0] and lock_info[1]:
2029 2029 log.debug('Repo %s is currently locked by %s', repo, user)
2030 2030 currently_locked = True
2031 2031 else:
2032 2032 log.debug('Setting lock on repo %s by %s', repo, user)
2033 2033 make_lock = True
2034 2034
2035 2035 else:
2036 2036 log.debug('Repository %s do not have locking enabled', repo)
2037 2037
2038 2038 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2039 2039 make_lock, currently_locked, lock_info)
2040 2040
2041 2041 from rhodecode.lib.auth import HasRepoPermissionAny
2042 2042 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2043 2043 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2044 2044 # if we don't have at least write permission we cannot make a lock
2045 2045 log.debug('lock state reset back to FALSE due to lack '
2046 2046 'of at least read permission')
2047 2047 make_lock = False
2048 2048
2049 2049 return make_lock, currently_locked, lock_info
2050 2050
2051 2051 @property
2052 2052 def last_db_change(self):
2053 2053 return self.updated_on
2054 2054
2055 2055 @property
2056 2056 def clone_uri_hidden(self):
2057 2057 clone_uri = self.clone_uri
2058 2058 if clone_uri:
2059 2059 import urlobject
2060 2060 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2061 2061 if url_obj.password:
2062 2062 clone_uri = url_obj.with_password('*****')
2063 2063 return clone_uri
2064 2064
2065 2065 def clone_url(self, **override):
2066 2066 from rhodecode.model.settings import SettingsModel
2067 2067
2068 2068 uri_tmpl = None
2069 2069 if 'with_id' in override:
2070 2070 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2071 2071 del override['with_id']
2072 2072
2073 2073 if 'uri_tmpl' in override:
2074 2074 uri_tmpl = override['uri_tmpl']
2075 2075 del override['uri_tmpl']
2076 2076
2077 2077 # we didn't override our tmpl from **overrides
2078 2078 if not uri_tmpl:
2079 2079 rc_config = SettingsModel().get_all_settings(cache=True)
2080 2080 uri_tmpl = rc_config.get(
2081 2081 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2082 2082
2083 2083 request = get_current_request()
2084 2084 return get_clone_url(request=request,
2085 2085 uri_tmpl=uri_tmpl,
2086 2086 repo_name=self.repo_name,
2087 2087 repo_id=self.repo_id, **override)
2088 2088
2089 2089 def set_state(self, state):
2090 2090 self.repo_state = state
2091 2091 Session().add(self)
2092 2092 #==========================================================================
2093 2093 # SCM PROPERTIES
2094 2094 #==========================================================================
2095 2095
2096 2096 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2097 2097 return get_commit_safe(
2098 2098 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2099 2099
2100 2100 def get_changeset(self, rev=None, pre_load=None):
2101 2101 warnings.warn("Use get_commit", DeprecationWarning)
2102 2102 commit_id = None
2103 2103 commit_idx = None
2104 2104 if isinstance(rev, basestring):
2105 2105 commit_id = rev
2106 2106 else:
2107 2107 commit_idx = rev
2108 2108 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2109 2109 pre_load=pre_load)
2110 2110
2111 2111 def get_landing_commit(self):
2112 2112 """
2113 2113 Returns landing commit, or if that doesn't exist returns the tip
2114 2114 """
2115 2115 _rev_type, _rev = self.landing_rev
2116 2116 commit = self.get_commit(_rev)
2117 2117 if isinstance(commit, EmptyCommit):
2118 2118 return self.get_commit()
2119 2119 return commit
2120 2120
2121 2121 def update_commit_cache(self, cs_cache=None, config=None):
2122 2122 """
2123 2123 Update cache of last changeset for repository, keys should be::
2124 2124
2125 2125 short_id
2126 2126 raw_id
2127 2127 revision
2128 2128 parents
2129 2129 message
2130 2130 date
2131 2131 author
2132 2132
2133 2133 :param cs_cache:
2134 2134 """
2135 2135 from rhodecode.lib.vcs.backends.base import BaseChangeset
2136 2136 if cs_cache is None:
2137 2137 # use no-cache version here
2138 2138 scm_repo = self.scm_instance(cache=False, config=config)
2139 2139 if scm_repo:
2140 2140 cs_cache = scm_repo.get_commit(
2141 2141 pre_load=["author", "date", "message", "parents"])
2142 2142 else:
2143 2143 cs_cache = EmptyCommit()
2144 2144
2145 2145 if isinstance(cs_cache, BaseChangeset):
2146 2146 cs_cache = cs_cache.__json__()
2147 2147
2148 2148 def is_outdated(new_cs_cache):
2149 2149 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2150 2150 new_cs_cache['revision'] != self.changeset_cache['revision']):
2151 2151 return True
2152 2152 return False
2153 2153
2154 2154 # check if we have maybe already latest cached revision
2155 2155 if is_outdated(cs_cache) or not self.changeset_cache:
2156 2156 _default = datetime.datetime.fromtimestamp(0)
2157 2157 last_change = cs_cache.get('date') or _default
2158 2158 log.debug('updated repo %s with new cs cache %s',
2159 2159 self.repo_name, cs_cache)
2160 2160 self.updated_on = last_change
2161 2161 self.changeset_cache = cs_cache
2162 2162 Session().add(self)
2163 2163 Session().commit()
2164 2164 else:
2165 2165 log.debug('Skipping update_commit_cache for repo:`%s` '
2166 2166 'commit already with latest changes', self.repo_name)
2167 2167
2168 2168 @property
2169 2169 def tip(self):
2170 2170 return self.get_commit('tip')
2171 2171
2172 2172 @property
2173 2173 def author(self):
2174 2174 return self.tip.author
2175 2175
2176 2176 @property
2177 2177 def last_change(self):
2178 2178 return self.scm_instance().last_change
2179 2179
2180 2180 def get_comments(self, revisions=None):
2181 2181 """
2182 2182 Returns comments for this repository grouped by revisions
2183 2183
2184 2184 :param revisions: filter query by revisions only
2185 2185 """
2186 2186 cmts = ChangesetComment.query()\
2187 2187 .filter(ChangesetComment.repo == self)
2188 2188 if revisions:
2189 2189 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2190 2190 grouped = collections.defaultdict(list)
2191 2191 for cmt in cmts.all():
2192 2192 grouped[cmt.revision].append(cmt)
2193 2193 return grouped
2194 2194
2195 2195 def statuses(self, revisions=None):
2196 2196 """
2197 2197 Returns statuses for this repository
2198 2198
2199 2199 :param revisions: list of revisions to get statuses for
2200 2200 """
2201 2201 statuses = ChangesetStatus.query()\
2202 2202 .filter(ChangesetStatus.repo == self)\
2203 2203 .filter(ChangesetStatus.version == 0)
2204 2204
2205 2205 if revisions:
2206 2206 # Try doing the filtering in chunks to avoid hitting limits
2207 2207 size = 500
2208 2208 status_results = []
2209 2209 for chunk in xrange(0, len(revisions), size):
2210 2210 status_results += statuses.filter(
2211 2211 ChangesetStatus.revision.in_(
2212 2212 revisions[chunk: chunk+size])
2213 2213 ).all()
2214 2214 else:
2215 2215 status_results = statuses.all()
2216 2216
2217 2217 grouped = {}
2218 2218
2219 2219 # maybe we have open new pullrequest without a status?
2220 2220 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2221 2221 status_lbl = ChangesetStatus.get_status_lbl(stat)
2222 2222 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2223 2223 for rev in pr.revisions:
2224 2224 pr_id = pr.pull_request_id
2225 2225 pr_repo = pr.target_repo.repo_name
2226 2226 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2227 2227
2228 2228 for stat in status_results:
2229 2229 pr_id = pr_repo = None
2230 2230 if stat.pull_request:
2231 2231 pr_id = stat.pull_request.pull_request_id
2232 2232 pr_repo = stat.pull_request.target_repo.repo_name
2233 2233 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2234 2234 pr_id, pr_repo]
2235 2235 return grouped
2236 2236
2237 2237 # ==========================================================================
2238 2238 # SCM CACHE INSTANCE
2239 2239 # ==========================================================================
2240 2240
2241 2241 def scm_instance(self, **kwargs):
2242 2242 import rhodecode
2243 2243
2244 2244 # Passing a config will not hit the cache currently only used
2245 2245 # for repo2dbmapper
2246 2246 config = kwargs.pop('config', None)
2247 2247 cache = kwargs.pop('cache', None)
2248 2248 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2249 2249 # if cache is NOT defined use default global, else we have a full
2250 2250 # control over cache behaviour
2251 2251 if cache is None and full_cache and not config:
2252 2252 return self._get_instance_cached()
2253 2253 return self._get_instance(cache=bool(cache), config=config)
2254 2254
2255 2255 def _get_instance_cached(self):
2256 2256 @cache_region('long_term')
2257 2257 def _get_repo(cache_key):
2258 2258 return self._get_instance()
2259 2259
2260 2260 invalidator_context = CacheKey.repo_context_cache(
2261 2261 _get_repo, self.repo_name, None, thread_scoped=True)
2262 2262
2263 2263 with invalidator_context as context:
2264 2264 context.invalidate()
2265 2265 repo = context.compute()
2266 2266
2267 2267 return repo
2268 2268
2269 2269 def _get_instance(self, cache=True, config=None):
2270 2270 config = config or self._config
2271 2271 custom_wire = {
2272 2272 'cache': cache # controls the vcs.remote cache
2273 2273 }
2274 2274 repo = get_vcs_instance(
2275 2275 repo_path=safe_str(self.repo_full_path),
2276 2276 config=config,
2277 2277 with_wire=custom_wire,
2278 2278 create=False,
2279 2279 _vcs_alias=self.repo_type)
2280 2280
2281 2281 return repo
2282 2282
2283 2283 def __json__(self):
2284 2284 return {'landing_rev': self.landing_rev}
2285 2285
2286 2286 def get_dict(self):
2287 2287
2288 2288 # Since we transformed `repo_name` to a hybrid property, we need to
2289 2289 # keep compatibility with the code which uses `repo_name` field.
2290 2290
2291 2291 result = super(Repository, self).get_dict()
2292 2292 result['repo_name'] = result.pop('_repo_name', None)
2293 2293 return result
2294 2294
2295 2295
2296 2296 class RepoGroup(Base, BaseModel):
2297 2297 __tablename__ = 'groups'
2298 2298 __table_args__ = (
2299 2299 UniqueConstraint('group_name', 'group_parent_id'),
2300 2300 CheckConstraint('group_id != group_parent_id'),
2301 2301 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2302 2302 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2303 2303 )
2304 2304 __mapper_args__ = {'order_by': 'group_name'}
2305 2305
2306 2306 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2307 2307
2308 2308 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2309 2309 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2310 2310 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2311 2311 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2312 2312 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2313 2313 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2314 2314 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2315 2315 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2316 2316 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2317 2317
2318 2318 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2319 2319 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2320 2320 parent_group = relationship('RepoGroup', remote_side=group_id)
2321 2321 user = relationship('User')
2322 2322 integrations = relationship('Integration',
2323 2323 cascade="all, delete, delete-orphan")
2324 2324
2325 2325 def __init__(self, group_name='', parent_group=None):
2326 2326 self.group_name = group_name
2327 2327 self.parent_group = parent_group
2328 2328
2329 2329 def __unicode__(self):
2330 2330 return u"<%s('id:%s:%s')>" % (
2331 2331 self.__class__.__name__, self.group_id, self.group_name)
2332 2332
2333 2333 @hybrid_property
2334 2334 def description_safe(self):
2335 2335 from rhodecode.lib import helpers as h
2336 2336 return h.escape(self.group_description)
2337 2337
2338 2338 @classmethod
2339 2339 def _generate_choice(cls, repo_group):
2340 2340 from webhelpers.html import literal as _literal
2341 2341 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2342 2342 return repo_group.group_id, _name(repo_group.full_path_splitted)
2343 2343
2344 2344 @classmethod
2345 2345 def groups_choices(cls, groups=None, show_empty_group=True):
2346 2346 if not groups:
2347 2347 groups = cls.query().all()
2348 2348
2349 2349 repo_groups = []
2350 2350 if show_empty_group:
2351 2351 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2352 2352
2353 2353 repo_groups.extend([cls._generate_choice(x) for x in groups])
2354 2354
2355 2355 repo_groups = sorted(
2356 2356 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2357 2357 return repo_groups
2358 2358
2359 2359 @classmethod
2360 2360 def url_sep(cls):
2361 2361 return URL_SEP
2362 2362
2363 2363 @classmethod
2364 2364 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2365 2365 if case_insensitive:
2366 2366 gr = cls.query().filter(func.lower(cls.group_name)
2367 2367 == func.lower(group_name))
2368 2368 else:
2369 2369 gr = cls.query().filter(cls.group_name == group_name)
2370 2370 if cache:
2371 2371 name_key = _hash_key(group_name)
2372 2372 gr = gr.options(
2373 2373 FromCache("sql_cache_short", "get_group_%s" % name_key))
2374 2374 return gr.scalar()
2375 2375
2376 2376 @classmethod
2377 2377 def get_user_personal_repo_group(cls, user_id):
2378 2378 user = User.get(user_id)
2379 2379 if user.username == User.DEFAULT_USER:
2380 2380 return None
2381 2381
2382 2382 return cls.query()\
2383 2383 .filter(cls.personal == true()) \
2384 2384 .filter(cls.user == user).scalar()
2385 2385
2386 2386 @classmethod
2387 2387 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2388 2388 case_insensitive=True):
2389 2389 q = RepoGroup.query()
2390 2390
2391 2391 if not isinstance(user_id, Optional):
2392 2392 q = q.filter(RepoGroup.user_id == user_id)
2393 2393
2394 2394 if not isinstance(group_id, Optional):
2395 2395 q = q.filter(RepoGroup.group_parent_id == group_id)
2396 2396
2397 2397 if case_insensitive:
2398 2398 q = q.order_by(func.lower(RepoGroup.group_name))
2399 2399 else:
2400 2400 q = q.order_by(RepoGroup.group_name)
2401 2401 return q.all()
2402 2402
2403 2403 @property
2404 2404 def parents(self):
2405 2405 parents_recursion_limit = 10
2406 2406 groups = []
2407 2407 if self.parent_group is None:
2408 2408 return groups
2409 2409 cur_gr = self.parent_group
2410 2410 groups.insert(0, cur_gr)
2411 2411 cnt = 0
2412 2412 while 1:
2413 2413 cnt += 1
2414 2414 gr = getattr(cur_gr, 'parent_group', None)
2415 2415 cur_gr = cur_gr.parent_group
2416 2416 if gr is None:
2417 2417 break
2418 2418 if cnt == parents_recursion_limit:
2419 2419 # this will prevent accidental infinit loops
2420 2420 log.error(('more than %s parents found for group %s, stopping '
2421 2421 'recursive parent fetching' % (parents_recursion_limit, self)))
2422 2422 break
2423 2423
2424 2424 groups.insert(0, gr)
2425 2425 return groups
2426 2426
2427 2427 @property
2428 2428 def last_db_change(self):
2429 2429 return self.updated_on
2430 2430
2431 2431 @property
2432 2432 def children(self):
2433 2433 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2434 2434
2435 2435 @property
2436 2436 def name(self):
2437 2437 return self.group_name.split(RepoGroup.url_sep())[-1]
2438 2438
2439 2439 @property
2440 2440 def full_path(self):
2441 2441 return self.group_name
2442 2442
2443 2443 @property
2444 2444 def full_path_splitted(self):
2445 2445 return self.group_name.split(RepoGroup.url_sep())
2446 2446
2447 2447 @property
2448 2448 def repositories(self):
2449 2449 return Repository.query()\
2450 2450 .filter(Repository.group == self)\
2451 2451 .order_by(Repository.repo_name)
2452 2452
2453 2453 @property
2454 2454 def repositories_recursive_count(self):
2455 2455 cnt = self.repositories.count()
2456 2456
2457 2457 def children_count(group):
2458 2458 cnt = 0
2459 2459 for child in group.children:
2460 2460 cnt += child.repositories.count()
2461 2461 cnt += children_count(child)
2462 2462 return cnt
2463 2463
2464 2464 return cnt + children_count(self)
2465 2465
2466 2466 def _recursive_objects(self, include_repos=True):
2467 2467 all_ = []
2468 2468
2469 2469 def _get_members(root_gr):
2470 2470 if include_repos:
2471 2471 for r in root_gr.repositories:
2472 2472 all_.append(r)
2473 2473 childs = root_gr.children.all()
2474 2474 if childs:
2475 2475 for gr in childs:
2476 2476 all_.append(gr)
2477 2477 _get_members(gr)
2478 2478
2479 2479 _get_members(self)
2480 2480 return [self] + all_
2481 2481
2482 2482 def recursive_groups_and_repos(self):
2483 2483 """
2484 2484 Recursive return all groups, with repositories in those groups
2485 2485 """
2486 2486 return self._recursive_objects()
2487 2487
2488 2488 def recursive_groups(self):
2489 2489 """
2490 2490 Returns all children groups for this group including children of children
2491 2491 """
2492 2492 return self._recursive_objects(include_repos=False)
2493 2493
2494 2494 def get_new_name(self, group_name):
2495 2495 """
2496 2496 returns new full group name based on parent and new name
2497 2497
2498 2498 :param group_name:
2499 2499 """
2500 2500 path_prefix = (self.parent_group.full_path_splitted if
2501 2501 self.parent_group else [])
2502 2502 return RepoGroup.url_sep().join(path_prefix + [group_name])
2503 2503
2504 2504 def permissions(self, with_admins=True, with_owner=True):
2505 2505 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2506 2506 q = q.options(joinedload(UserRepoGroupToPerm.group),
2507 2507 joinedload(UserRepoGroupToPerm.user),
2508 2508 joinedload(UserRepoGroupToPerm.permission),)
2509 2509
2510 2510 # get owners and admins and permissions. We do a trick of re-writing
2511 2511 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2512 2512 # has a global reference and changing one object propagates to all
2513 2513 # others. This means if admin is also an owner admin_row that change
2514 2514 # would propagate to both objects
2515 2515 perm_rows = []
2516 2516 for _usr in q.all():
2517 2517 usr = AttributeDict(_usr.user.get_dict())
2518 2518 usr.permission = _usr.permission.permission_name
2519 2519 perm_rows.append(usr)
2520 2520
2521 2521 # filter the perm rows by 'default' first and then sort them by
2522 2522 # admin,write,read,none permissions sorted again alphabetically in
2523 2523 # each group
2524 2524 perm_rows = sorted(perm_rows, key=display_user_sort)
2525 2525
2526 2526 _admin_perm = 'group.admin'
2527 2527 owner_row = []
2528 2528 if with_owner:
2529 2529 usr = AttributeDict(self.user.get_dict())
2530 2530 usr.owner_row = True
2531 2531 usr.permission = _admin_perm
2532 2532 owner_row.append(usr)
2533 2533
2534 2534 super_admin_rows = []
2535 2535 if with_admins:
2536 2536 for usr in User.get_all_super_admins():
2537 2537 # if this admin is also owner, don't double the record
2538 2538 if usr.user_id == owner_row[0].user_id:
2539 2539 owner_row[0].admin_row = True
2540 2540 else:
2541 2541 usr = AttributeDict(usr.get_dict())
2542 2542 usr.admin_row = True
2543 2543 usr.permission = _admin_perm
2544 2544 super_admin_rows.append(usr)
2545 2545
2546 2546 return super_admin_rows + owner_row + perm_rows
2547 2547
2548 2548 def permission_user_groups(self):
2549 2549 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2550 2550 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2551 2551 joinedload(UserGroupRepoGroupToPerm.users_group),
2552 2552 joinedload(UserGroupRepoGroupToPerm.permission),)
2553 2553
2554 2554 perm_rows = []
2555 2555 for _user_group in q.all():
2556 2556 usr = AttributeDict(_user_group.users_group.get_dict())
2557 2557 usr.permission = _user_group.permission.permission_name
2558 2558 perm_rows.append(usr)
2559 2559
2560 2560 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2561 2561 return perm_rows
2562 2562
2563 2563 def get_api_data(self):
2564 2564 """
2565 2565 Common function for generating api data
2566 2566
2567 2567 """
2568 2568 group = self
2569 2569 data = {
2570 2570 'group_id': group.group_id,
2571 2571 'group_name': group.group_name,
2572 2572 'group_description': group.description_safe,
2573 2573 'parent_group': group.parent_group.group_name if group.parent_group else None,
2574 2574 'repositories': [x.repo_name for x in group.repositories],
2575 2575 'owner': group.user.username,
2576 2576 }
2577 2577 return data
2578 2578
2579 2579
2580 2580 class Permission(Base, BaseModel):
2581 2581 __tablename__ = 'permissions'
2582 2582 __table_args__ = (
2583 2583 Index('p_perm_name_idx', 'permission_name'),
2584 2584 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2585 2585 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2586 2586 )
2587 2587 PERMS = [
2588 2588 ('hg.admin', _('RhodeCode Super Administrator')),
2589 2589
2590 2590 ('repository.none', _('Repository no access')),
2591 2591 ('repository.read', _('Repository read access')),
2592 2592 ('repository.write', _('Repository write access')),
2593 2593 ('repository.admin', _('Repository admin access')),
2594 2594
2595 2595 ('group.none', _('Repository group no access')),
2596 2596 ('group.read', _('Repository group read access')),
2597 2597 ('group.write', _('Repository group write access')),
2598 2598 ('group.admin', _('Repository group admin access')),
2599 2599
2600 2600 ('usergroup.none', _('User group no access')),
2601 2601 ('usergroup.read', _('User group read access')),
2602 2602 ('usergroup.write', _('User group write access')),
2603 2603 ('usergroup.admin', _('User group admin access')),
2604 2604
2605 2605 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2606 2606 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2607 2607
2608 2608 ('hg.usergroup.create.false', _('User Group creation disabled')),
2609 2609 ('hg.usergroup.create.true', _('User Group creation enabled')),
2610 2610
2611 2611 ('hg.create.none', _('Repository creation disabled')),
2612 2612 ('hg.create.repository', _('Repository creation enabled')),
2613 2613 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2614 2614 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2615 2615
2616 2616 ('hg.fork.none', _('Repository forking disabled')),
2617 2617 ('hg.fork.repository', _('Repository forking enabled')),
2618 2618
2619 2619 ('hg.register.none', _('Registration disabled')),
2620 2620 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2621 2621 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2622 2622
2623 2623 ('hg.password_reset.enabled', _('Password reset enabled')),
2624 2624 ('hg.password_reset.hidden', _('Password reset hidden')),
2625 2625 ('hg.password_reset.disabled', _('Password reset disabled')),
2626 2626
2627 2627 ('hg.extern_activate.manual', _('Manual activation of external account')),
2628 2628 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2629 2629
2630 2630 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2631 2631 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2632 2632 ]
2633 2633
2634 2634 # definition of system default permissions for DEFAULT user
2635 2635 DEFAULT_USER_PERMISSIONS = [
2636 2636 'repository.read',
2637 2637 'group.read',
2638 2638 'usergroup.read',
2639 2639 'hg.create.repository',
2640 2640 'hg.repogroup.create.false',
2641 2641 'hg.usergroup.create.false',
2642 2642 'hg.create.write_on_repogroup.true',
2643 2643 'hg.fork.repository',
2644 2644 'hg.register.manual_activate',
2645 2645 'hg.password_reset.enabled',
2646 2646 'hg.extern_activate.auto',
2647 2647 'hg.inherit_default_perms.true',
2648 2648 ]
2649 2649
2650 2650 # defines which permissions are more important higher the more important
2651 2651 # Weight defines which permissions are more important.
2652 2652 # The higher number the more important.
2653 2653 PERM_WEIGHTS = {
2654 2654 'repository.none': 0,
2655 2655 'repository.read': 1,
2656 2656 'repository.write': 3,
2657 2657 'repository.admin': 4,
2658 2658
2659 2659 'group.none': 0,
2660 2660 'group.read': 1,
2661 2661 'group.write': 3,
2662 2662 'group.admin': 4,
2663 2663
2664 2664 'usergroup.none': 0,
2665 2665 'usergroup.read': 1,
2666 2666 'usergroup.write': 3,
2667 2667 'usergroup.admin': 4,
2668 2668
2669 2669 'hg.repogroup.create.false': 0,
2670 2670 'hg.repogroup.create.true': 1,
2671 2671
2672 2672 'hg.usergroup.create.false': 0,
2673 2673 'hg.usergroup.create.true': 1,
2674 2674
2675 2675 'hg.fork.none': 0,
2676 2676 'hg.fork.repository': 1,
2677 2677 'hg.create.none': 0,
2678 2678 'hg.create.repository': 1
2679 2679 }
2680 2680
2681 2681 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2682 2682 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2683 2683 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2684 2684
2685 2685 def __unicode__(self):
2686 2686 return u"<%s('%s:%s')>" % (
2687 2687 self.__class__.__name__, self.permission_id, self.permission_name
2688 2688 )
2689 2689
2690 2690 @classmethod
2691 2691 def get_by_key(cls, key):
2692 2692 return cls.query().filter(cls.permission_name == key).scalar()
2693 2693
2694 2694 @classmethod
2695 2695 def get_default_repo_perms(cls, user_id, repo_id=None):
2696 2696 q = Session().query(UserRepoToPerm, Repository, Permission)\
2697 2697 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2698 2698 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2699 2699 .filter(UserRepoToPerm.user_id == user_id)
2700 2700 if repo_id:
2701 2701 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2702 2702 return q.all()
2703 2703
2704 2704 @classmethod
2705 2705 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2706 2706 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2707 2707 .join(
2708 2708 Permission,
2709 2709 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2710 2710 .join(
2711 2711 Repository,
2712 2712 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2713 2713 .join(
2714 2714 UserGroup,
2715 2715 UserGroupRepoToPerm.users_group_id ==
2716 2716 UserGroup.users_group_id)\
2717 2717 .join(
2718 2718 UserGroupMember,
2719 2719 UserGroupRepoToPerm.users_group_id ==
2720 2720 UserGroupMember.users_group_id)\
2721 2721 .filter(
2722 2722 UserGroupMember.user_id == user_id,
2723 2723 UserGroup.users_group_active == true())
2724 2724 if repo_id:
2725 2725 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2726 2726 return q.all()
2727 2727
2728 2728 @classmethod
2729 2729 def get_default_group_perms(cls, user_id, repo_group_id=None):
2730 2730 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2731 2731 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2732 2732 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2733 2733 .filter(UserRepoGroupToPerm.user_id == user_id)
2734 2734 if repo_group_id:
2735 2735 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2736 2736 return q.all()
2737 2737
2738 2738 @classmethod
2739 2739 def get_default_group_perms_from_user_group(
2740 2740 cls, user_id, repo_group_id=None):
2741 2741 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2742 2742 .join(
2743 2743 Permission,
2744 2744 UserGroupRepoGroupToPerm.permission_id ==
2745 2745 Permission.permission_id)\
2746 2746 .join(
2747 2747 RepoGroup,
2748 2748 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2749 2749 .join(
2750 2750 UserGroup,
2751 2751 UserGroupRepoGroupToPerm.users_group_id ==
2752 2752 UserGroup.users_group_id)\
2753 2753 .join(
2754 2754 UserGroupMember,
2755 2755 UserGroupRepoGroupToPerm.users_group_id ==
2756 2756 UserGroupMember.users_group_id)\
2757 2757 .filter(
2758 2758 UserGroupMember.user_id == user_id,
2759 2759 UserGroup.users_group_active == true())
2760 2760 if repo_group_id:
2761 2761 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2762 2762 return q.all()
2763 2763
2764 2764 @classmethod
2765 2765 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2766 2766 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2767 2767 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2768 2768 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2769 2769 .filter(UserUserGroupToPerm.user_id == user_id)
2770 2770 if user_group_id:
2771 2771 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2772 2772 return q.all()
2773 2773
2774 2774 @classmethod
2775 2775 def get_default_user_group_perms_from_user_group(
2776 2776 cls, user_id, user_group_id=None):
2777 2777 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2778 2778 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2779 2779 .join(
2780 2780 Permission,
2781 2781 UserGroupUserGroupToPerm.permission_id ==
2782 2782 Permission.permission_id)\
2783 2783 .join(
2784 2784 TargetUserGroup,
2785 2785 UserGroupUserGroupToPerm.target_user_group_id ==
2786 2786 TargetUserGroup.users_group_id)\
2787 2787 .join(
2788 2788 UserGroup,
2789 2789 UserGroupUserGroupToPerm.user_group_id ==
2790 2790 UserGroup.users_group_id)\
2791 2791 .join(
2792 2792 UserGroupMember,
2793 2793 UserGroupUserGroupToPerm.user_group_id ==
2794 2794 UserGroupMember.users_group_id)\
2795 2795 .filter(
2796 2796 UserGroupMember.user_id == user_id,
2797 2797 UserGroup.users_group_active == true())
2798 2798 if user_group_id:
2799 2799 q = q.filter(
2800 2800 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2801 2801
2802 2802 return q.all()
2803 2803
2804 2804
2805 2805 class UserRepoToPerm(Base, BaseModel):
2806 2806 __tablename__ = 'repo_to_perm'
2807 2807 __table_args__ = (
2808 2808 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2809 2809 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2810 2810 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2811 2811 )
2812 2812 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2813 2813 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2814 2814 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2815 2815 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2816 2816
2817 2817 user = relationship('User')
2818 2818 repository = relationship('Repository')
2819 2819 permission = relationship('Permission')
2820 2820
2821 2821 @classmethod
2822 2822 def create(cls, user, repository, permission):
2823 2823 n = cls()
2824 2824 n.user = user
2825 2825 n.repository = repository
2826 2826 n.permission = permission
2827 2827 Session().add(n)
2828 2828 return n
2829 2829
2830 2830 def __unicode__(self):
2831 2831 return u'<%s => %s >' % (self.user, self.repository)
2832 2832
2833 2833
2834 2834 class UserUserGroupToPerm(Base, BaseModel):
2835 2835 __tablename__ = 'user_user_group_to_perm'
2836 2836 __table_args__ = (
2837 2837 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2838 2838 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2839 2839 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2840 2840 )
2841 2841 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2842 2842 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2843 2843 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2844 2844 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2845 2845
2846 2846 user = relationship('User')
2847 2847 user_group = relationship('UserGroup')
2848 2848 permission = relationship('Permission')
2849 2849
2850 2850 @classmethod
2851 2851 def create(cls, user, user_group, permission):
2852 2852 n = cls()
2853 2853 n.user = user
2854 2854 n.user_group = user_group
2855 2855 n.permission = permission
2856 2856 Session().add(n)
2857 2857 return n
2858 2858
2859 2859 def __unicode__(self):
2860 2860 return u'<%s => %s >' % (self.user, self.user_group)
2861 2861
2862 2862
2863 2863 class UserToPerm(Base, BaseModel):
2864 2864 __tablename__ = 'user_to_perm'
2865 2865 __table_args__ = (
2866 2866 UniqueConstraint('user_id', 'permission_id'),
2867 2867 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2868 2868 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2869 2869 )
2870 2870 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2871 2871 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2872 2872 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2873 2873
2874 2874 user = relationship('User')
2875 2875 permission = relationship('Permission', lazy='joined')
2876 2876
2877 2877 def __unicode__(self):
2878 2878 return u'<%s => %s >' % (self.user, self.permission)
2879 2879
2880 2880
2881 2881 class UserGroupRepoToPerm(Base, BaseModel):
2882 2882 __tablename__ = 'users_group_repo_to_perm'
2883 2883 __table_args__ = (
2884 2884 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2885 2885 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2886 2886 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2887 2887 )
2888 2888 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2889 2889 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2890 2890 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2891 2891 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2892 2892
2893 2893 users_group = relationship('UserGroup')
2894 2894 permission = relationship('Permission')
2895 2895 repository = relationship('Repository')
2896 2896
2897 2897 @classmethod
2898 2898 def create(cls, users_group, repository, permission):
2899 2899 n = cls()
2900 2900 n.users_group = users_group
2901 2901 n.repository = repository
2902 2902 n.permission = permission
2903 2903 Session().add(n)
2904 2904 return n
2905 2905
2906 2906 def __unicode__(self):
2907 2907 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2908 2908
2909 2909
2910 2910 class UserGroupUserGroupToPerm(Base, BaseModel):
2911 2911 __tablename__ = 'user_group_user_group_to_perm'
2912 2912 __table_args__ = (
2913 2913 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2914 2914 CheckConstraint('target_user_group_id != user_group_id'),
2915 2915 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2916 2916 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2917 2917 )
2918 2918 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)
2919 2919 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2920 2920 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2921 2921 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2922 2922
2923 2923 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2924 2924 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2925 2925 permission = relationship('Permission')
2926 2926
2927 2927 @classmethod
2928 2928 def create(cls, target_user_group, user_group, permission):
2929 2929 n = cls()
2930 2930 n.target_user_group = target_user_group
2931 2931 n.user_group = user_group
2932 2932 n.permission = permission
2933 2933 Session().add(n)
2934 2934 return n
2935 2935
2936 2936 def __unicode__(self):
2937 2937 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2938 2938
2939 2939
2940 2940 class UserGroupToPerm(Base, BaseModel):
2941 2941 __tablename__ = 'users_group_to_perm'
2942 2942 __table_args__ = (
2943 2943 UniqueConstraint('users_group_id', 'permission_id',),
2944 2944 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2945 2945 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2946 2946 )
2947 2947 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2948 2948 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2949 2949 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2950 2950
2951 2951 users_group = relationship('UserGroup')
2952 2952 permission = relationship('Permission')
2953 2953
2954 2954
2955 2955 class UserRepoGroupToPerm(Base, BaseModel):
2956 2956 __tablename__ = 'user_repo_group_to_perm'
2957 2957 __table_args__ = (
2958 2958 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2959 2959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2960 2960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2961 2961 )
2962 2962
2963 2963 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2964 2964 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2965 2965 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2966 2966 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2967 2967
2968 2968 user = relationship('User')
2969 2969 group = relationship('RepoGroup')
2970 2970 permission = relationship('Permission')
2971 2971
2972 2972 @classmethod
2973 2973 def create(cls, user, repository_group, permission):
2974 2974 n = cls()
2975 2975 n.user = user
2976 2976 n.group = repository_group
2977 2977 n.permission = permission
2978 2978 Session().add(n)
2979 2979 return n
2980 2980
2981 2981
2982 2982 class UserGroupRepoGroupToPerm(Base, BaseModel):
2983 2983 __tablename__ = 'users_group_repo_group_to_perm'
2984 2984 __table_args__ = (
2985 2985 UniqueConstraint('users_group_id', 'group_id'),
2986 2986 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2987 2987 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2988 2988 )
2989 2989
2990 2990 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)
2991 2991 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2992 2992 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2993 2993 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2994 2994
2995 2995 users_group = relationship('UserGroup')
2996 2996 permission = relationship('Permission')
2997 2997 group = relationship('RepoGroup')
2998 2998
2999 2999 @classmethod
3000 3000 def create(cls, user_group, repository_group, permission):
3001 3001 n = cls()
3002 3002 n.users_group = user_group
3003 3003 n.group = repository_group
3004 3004 n.permission = permission
3005 3005 Session().add(n)
3006 3006 return n
3007 3007
3008 3008 def __unicode__(self):
3009 3009 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3010 3010
3011 3011
3012 3012 class Statistics(Base, BaseModel):
3013 3013 __tablename__ = 'statistics'
3014 3014 __table_args__ = (
3015 3015 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3016 3016 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3017 3017 )
3018 3018 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3019 3019 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3020 3020 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3021 3021 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3022 3022 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3023 3023 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3024 3024
3025 3025 repository = relationship('Repository', single_parent=True)
3026 3026
3027 3027
3028 3028 class UserFollowing(Base, BaseModel):
3029 3029 __tablename__ = 'user_followings'
3030 3030 __table_args__ = (
3031 3031 UniqueConstraint('user_id', 'follows_repository_id'),
3032 3032 UniqueConstraint('user_id', 'follows_user_id'),
3033 3033 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3034 3034 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3035 3035 )
3036 3036
3037 3037 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3038 3038 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3039 3039 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3040 3040 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3041 3041 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3042 3042
3043 3043 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3044 3044
3045 3045 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3046 3046 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3047 3047
3048 3048 @classmethod
3049 3049 def get_repo_followers(cls, repo_id):
3050 3050 return cls.query().filter(cls.follows_repo_id == repo_id)
3051 3051
3052 3052
3053 3053 class CacheKey(Base, BaseModel):
3054 3054 __tablename__ = 'cache_invalidation'
3055 3055 __table_args__ = (
3056 3056 UniqueConstraint('cache_key'),
3057 3057 Index('key_idx', 'cache_key'),
3058 3058 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3059 3059 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3060 3060 )
3061 3061 CACHE_TYPE_ATOM = 'ATOM'
3062 3062 CACHE_TYPE_RSS = 'RSS'
3063 3063 CACHE_TYPE_README = 'README'
3064 3064
3065 3065 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3066 3066 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3067 3067 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3068 3068 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3069 3069
3070 3070 def __init__(self, cache_key, cache_args=''):
3071 3071 self.cache_key = cache_key
3072 3072 self.cache_args = cache_args
3073 3073 self.cache_active = False
3074 3074
3075 3075 def __unicode__(self):
3076 3076 return u"<%s('%s:%s[%s]')>" % (
3077 3077 self.__class__.__name__,
3078 3078 self.cache_id, self.cache_key, self.cache_active)
3079 3079
3080 3080 def _cache_key_partition(self):
3081 3081 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3082 3082 return prefix, repo_name, suffix
3083 3083
3084 3084 def get_prefix(self):
3085 3085 """
3086 3086 Try to extract prefix from existing cache key. The key could consist
3087 3087 of prefix, repo_name, suffix
3088 3088 """
3089 3089 # this returns prefix, repo_name, suffix
3090 3090 return self._cache_key_partition()[0]
3091 3091
3092 3092 def get_suffix(self):
3093 3093 """
3094 3094 get suffix that might have been used in _get_cache_key to
3095 3095 generate self.cache_key. Only used for informational purposes
3096 3096 in repo_edit.mako.
3097 3097 """
3098 3098 # prefix, repo_name, suffix
3099 3099 return self._cache_key_partition()[2]
3100 3100
3101 3101 @classmethod
3102 3102 def delete_all_cache(cls):
3103 3103 """
3104 3104 Delete all cache keys from database.
3105 3105 Should only be run when all instances are down and all entries
3106 3106 thus stale.
3107 3107 """
3108 3108 cls.query().delete()
3109 3109 Session().commit()
3110 3110
3111 3111 @classmethod
3112 3112 def get_cache_key(cls, repo_name, cache_type):
3113 3113 """
3114 3114
3115 3115 Generate a cache key for this process of RhodeCode instance.
3116 3116 Prefix most likely will be process id or maybe explicitly set
3117 3117 instance_id from .ini file.
3118 3118 """
3119 3119 import rhodecode
3120 3120 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3121 3121
3122 3122 repo_as_unicode = safe_unicode(repo_name)
3123 3123 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3124 3124 if cache_type else repo_as_unicode
3125 3125
3126 3126 return u'{}{}'.format(prefix, key)
3127 3127
3128 3128 @classmethod
3129 3129 def set_invalidate(cls, repo_name, delete=False):
3130 3130 """
3131 3131 Mark all caches of a repo as invalid in the database.
3132 3132 """
3133 3133
3134 3134 try:
3135 3135 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3136 3136 if delete:
3137 3137 log.debug('cache objects deleted for repo %s',
3138 3138 safe_str(repo_name))
3139 3139 qry.delete()
3140 3140 else:
3141 3141 log.debug('cache objects marked as invalid for repo %s',
3142 3142 safe_str(repo_name))
3143 3143 qry.update({"cache_active": False})
3144 3144
3145 3145 Session().commit()
3146 3146 except Exception:
3147 3147 log.exception(
3148 3148 'Cache key invalidation failed for repository %s',
3149 3149 safe_str(repo_name))
3150 3150 Session().rollback()
3151 3151
3152 3152 @classmethod
3153 3153 def get_active_cache(cls, cache_key):
3154 3154 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3155 3155 if inv_obj:
3156 3156 return inv_obj
3157 3157 return None
3158 3158
3159 3159 @classmethod
3160 3160 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3161 3161 thread_scoped=False):
3162 3162 """
3163 3163 @cache_region('long_term')
3164 3164 def _heavy_calculation(cache_key):
3165 3165 return 'result'
3166 3166
3167 3167 cache_context = CacheKey.repo_context_cache(
3168 3168 _heavy_calculation, repo_name, cache_type)
3169 3169
3170 3170 with cache_context as context:
3171 3171 context.invalidate()
3172 3172 computed = context.compute()
3173 3173
3174 3174 assert computed == 'result'
3175 3175 """
3176 3176 from rhodecode.lib import caches
3177 3177 return caches.InvalidationContext(
3178 3178 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3179 3179
3180 3180
3181 3181 class ChangesetComment(Base, BaseModel):
3182 3182 __tablename__ = 'changeset_comments'
3183 3183 __table_args__ = (
3184 3184 Index('cc_revision_idx', 'revision'),
3185 3185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3186 3186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3187 3187 )
3188 3188
3189 3189 COMMENT_OUTDATED = u'comment_outdated'
3190 3190 COMMENT_TYPE_NOTE = u'note'
3191 3191 COMMENT_TYPE_TODO = u'todo'
3192 3192 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3193 3193
3194 3194 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3195 3195 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3196 3196 revision = Column('revision', String(40), nullable=True)
3197 3197 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3198 3198 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3199 3199 line_no = Column('line_no', Unicode(10), nullable=True)
3200 3200 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3201 3201 f_path = Column('f_path', Unicode(1000), nullable=True)
3202 3202 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3203 3203 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3204 3204 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3205 3205 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3206 3206 renderer = Column('renderer', Unicode(64), nullable=True)
3207 3207 display_state = Column('display_state', Unicode(128), nullable=True)
3208 3208
3209 3209 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3210 3210 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3211 3211 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3212 3212 author = relationship('User', lazy='joined')
3213 3213 repo = relationship('Repository')
3214 3214 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3215 3215 pull_request = relationship('PullRequest', lazy='joined')
3216 3216 pull_request_version = relationship('PullRequestVersion')
3217 3217
3218 3218 @classmethod
3219 3219 def get_users(cls, revision=None, pull_request_id=None):
3220 3220 """
3221 3221 Returns user associated with this ChangesetComment. ie those
3222 3222 who actually commented
3223 3223
3224 3224 :param cls:
3225 3225 :param revision:
3226 3226 """
3227 3227 q = Session().query(User)\
3228 3228 .join(ChangesetComment.author)
3229 3229 if revision:
3230 3230 q = q.filter(cls.revision == revision)
3231 3231 elif pull_request_id:
3232 3232 q = q.filter(cls.pull_request_id == pull_request_id)
3233 3233 return q.all()
3234 3234
3235 3235 @classmethod
3236 3236 def get_index_from_version(cls, pr_version, versions):
3237 3237 num_versions = [x.pull_request_version_id for x in versions]
3238 3238 try:
3239 3239 return num_versions.index(pr_version) +1
3240 3240 except (IndexError, ValueError):
3241 3241 return
3242 3242
3243 3243 @property
3244 3244 def outdated(self):
3245 3245 return self.display_state == self.COMMENT_OUTDATED
3246 3246
3247 3247 def outdated_at_version(self, version):
3248 3248 """
3249 3249 Checks if comment is outdated for given pull request version
3250 3250 """
3251 3251 return self.outdated and self.pull_request_version_id != version
3252 3252
3253 3253 def older_than_version(self, version):
3254 3254 """
3255 3255 Checks if comment is made from previous version than given
3256 3256 """
3257 3257 if version is None:
3258 3258 return self.pull_request_version_id is not None
3259 3259
3260 3260 return self.pull_request_version_id < version
3261 3261
3262 3262 @property
3263 3263 def resolved(self):
3264 3264 return self.resolved_by[0] if self.resolved_by else None
3265 3265
3266 3266 @property
3267 3267 def is_todo(self):
3268 3268 return self.comment_type == self.COMMENT_TYPE_TODO
3269 3269
3270 3270 @property
3271 3271 def is_inline(self):
3272 3272 return self.line_no and self.f_path
3273 3273
3274 3274 def get_index_version(self, versions):
3275 3275 return self.get_index_from_version(
3276 3276 self.pull_request_version_id, versions)
3277 3277
3278 3278 def __repr__(self):
3279 3279 if self.comment_id:
3280 3280 return '<DB:Comment #%s>' % self.comment_id
3281 3281 else:
3282 3282 return '<DB:Comment at %#x>' % id(self)
3283 3283
3284 3284 def get_api_data(self):
3285 3285 comment = self
3286 3286 data = {
3287 3287 'comment_id': comment.comment_id,
3288 3288 'comment_type': comment.comment_type,
3289 3289 'comment_text': comment.text,
3290 3290 'comment_status': comment.status_change,
3291 3291 'comment_f_path': comment.f_path,
3292 3292 'comment_lineno': comment.line_no,
3293 3293 'comment_author': comment.author,
3294 3294 'comment_created_on': comment.created_on
3295 3295 }
3296 3296 return data
3297 3297
3298 3298 def __json__(self):
3299 3299 data = dict()
3300 3300 data.update(self.get_api_data())
3301 3301 return data
3302 3302
3303 3303
3304 3304 class ChangesetStatus(Base, BaseModel):
3305 3305 __tablename__ = 'changeset_statuses'
3306 3306 __table_args__ = (
3307 3307 Index('cs_revision_idx', 'revision'),
3308 3308 Index('cs_version_idx', 'version'),
3309 3309 UniqueConstraint('repo_id', 'revision', 'version'),
3310 3310 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3311 3311 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3312 3312 )
3313 3313 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3314 3314 STATUS_APPROVED = 'approved'
3315 3315 STATUS_REJECTED = 'rejected'
3316 3316 STATUS_UNDER_REVIEW = 'under_review'
3317 3317
3318 3318 STATUSES = [
3319 3319 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3320 3320 (STATUS_APPROVED, _("Approved")),
3321 3321 (STATUS_REJECTED, _("Rejected")),
3322 3322 (STATUS_UNDER_REVIEW, _("Under Review")),
3323 3323 ]
3324 3324
3325 3325 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3326 3326 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3327 3327 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3328 3328 revision = Column('revision', String(40), nullable=False)
3329 3329 status = Column('status', String(128), nullable=False, default=DEFAULT)
3330 3330 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3331 3331 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3332 3332 version = Column('version', Integer(), nullable=False, default=0)
3333 3333 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3334 3334
3335 3335 author = relationship('User', lazy='joined')
3336 3336 repo = relationship('Repository')
3337 3337 comment = relationship('ChangesetComment', lazy='joined')
3338 3338 pull_request = relationship('PullRequest', lazy='joined')
3339 3339
3340 3340 def __unicode__(self):
3341 3341 return u"<%s('%s[v%s]:%s')>" % (
3342 3342 self.__class__.__name__,
3343 3343 self.status, self.version, self.author
3344 3344 )
3345 3345
3346 3346 @classmethod
3347 3347 def get_status_lbl(cls, value):
3348 3348 return dict(cls.STATUSES).get(value)
3349 3349
3350 3350 @property
3351 3351 def status_lbl(self):
3352 3352 return ChangesetStatus.get_status_lbl(self.status)
3353 3353
3354 3354 def get_api_data(self):
3355 3355 status = self
3356 3356 data = {
3357 3357 'status_id': status.changeset_status_id,
3358 3358 'status': status.status,
3359 3359 }
3360 3360 return data
3361 3361
3362 3362 def __json__(self):
3363 3363 data = dict()
3364 3364 data.update(self.get_api_data())
3365 3365 return data
3366 3366
3367 3367
3368 3368 class _PullRequestBase(BaseModel):
3369 3369 """
3370 3370 Common attributes of pull request and version entries.
3371 3371 """
3372 3372
3373 3373 # .status values
3374 3374 STATUS_NEW = u'new'
3375 3375 STATUS_OPEN = u'open'
3376 3376 STATUS_CLOSED = u'closed'
3377 3377
3378 3378 title = Column('title', Unicode(255), nullable=True)
3379 3379 description = Column(
3380 3380 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3381 3381 nullable=True)
3382 3382 # new/open/closed status of pull request (not approve/reject/etc)
3383 3383 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3384 3384 created_on = Column(
3385 3385 'created_on', DateTime(timezone=False), nullable=False,
3386 3386 default=datetime.datetime.now)
3387 3387 updated_on = Column(
3388 3388 'updated_on', DateTime(timezone=False), nullable=False,
3389 3389 default=datetime.datetime.now)
3390 3390
3391 3391 @declared_attr
3392 3392 def user_id(cls):
3393 3393 return Column(
3394 3394 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3395 3395 unique=None)
3396 3396
3397 3397 # 500 revisions max
3398 3398 _revisions = Column(
3399 3399 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3400 3400
3401 3401 @declared_attr
3402 3402 def source_repo_id(cls):
3403 3403 # TODO: dan: rename column to source_repo_id
3404 3404 return Column(
3405 3405 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3406 3406 nullable=False)
3407 3407
3408 3408 source_ref = Column('org_ref', Unicode(255), nullable=False)
3409 3409
3410 3410 @declared_attr
3411 3411 def target_repo_id(cls):
3412 3412 # TODO: dan: rename column to target_repo_id
3413 3413 return Column(
3414 3414 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3415 3415 nullable=False)
3416 3416
3417 3417 target_ref = Column('other_ref', Unicode(255), nullable=False)
3418 3418 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3419 3419
3420 3420 # TODO: dan: rename column to last_merge_source_rev
3421 3421 _last_merge_source_rev = Column(
3422 3422 'last_merge_org_rev', String(40), nullable=True)
3423 3423 # TODO: dan: rename column to last_merge_target_rev
3424 3424 _last_merge_target_rev = Column(
3425 3425 'last_merge_other_rev', String(40), nullable=True)
3426 3426 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3427 3427 merge_rev = Column('merge_rev', String(40), nullable=True)
3428 3428
3429 3429 reviewer_data = Column(
3430 3430 'reviewer_data_json', MutationObj.as_mutable(
3431 3431 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3432 3432
3433 3433 @property
3434 3434 def reviewer_data_json(self):
3435 3435 return json.dumps(self.reviewer_data)
3436 3436
3437 3437 @hybrid_property
3438 3438 def description_safe(self):
3439 3439 from rhodecode.lib import helpers as h
3440 3440 return h.escape(self.description)
3441 3441
3442 3442 @hybrid_property
3443 3443 def revisions(self):
3444 3444 return self._revisions.split(':') if self._revisions else []
3445 3445
3446 3446 @revisions.setter
3447 3447 def revisions(self, val):
3448 3448 self._revisions = ':'.join(val)
3449 3449
3450 3450 @hybrid_property
3451 3451 def last_merge_status(self):
3452 3452 return safe_int(self._last_merge_status)
3453 3453
3454 3454 @last_merge_status.setter
3455 3455 def last_merge_status(self, val):
3456 3456 self._last_merge_status = val
3457 3457
3458 3458 @declared_attr
3459 3459 def author(cls):
3460 3460 return relationship('User', lazy='joined')
3461 3461
3462 3462 @declared_attr
3463 3463 def source_repo(cls):
3464 3464 return relationship(
3465 3465 'Repository',
3466 3466 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3467 3467
3468 3468 @property
3469 3469 def source_ref_parts(self):
3470 3470 return self.unicode_to_reference(self.source_ref)
3471 3471
3472 3472 @declared_attr
3473 3473 def target_repo(cls):
3474 3474 return relationship(
3475 3475 'Repository',
3476 3476 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3477 3477
3478 3478 @property
3479 3479 def target_ref_parts(self):
3480 3480 return self.unicode_to_reference(self.target_ref)
3481 3481
3482 3482 @property
3483 3483 def shadow_merge_ref(self):
3484 3484 return self.unicode_to_reference(self._shadow_merge_ref)
3485 3485
3486 3486 @shadow_merge_ref.setter
3487 3487 def shadow_merge_ref(self, ref):
3488 3488 self._shadow_merge_ref = self.reference_to_unicode(ref)
3489 3489
3490 3490 def unicode_to_reference(self, raw):
3491 3491 """
3492 3492 Convert a unicode (or string) to a reference object.
3493 3493 If unicode evaluates to False it returns None.
3494 3494 """
3495 3495 if raw:
3496 3496 refs = raw.split(':')
3497 3497 return Reference(*refs)
3498 3498 else:
3499 3499 return None
3500 3500
3501 3501 def reference_to_unicode(self, ref):
3502 3502 """
3503 3503 Convert a reference object to unicode.
3504 3504 If reference is None it returns None.
3505 3505 """
3506 3506 if ref:
3507 3507 return u':'.join(ref)
3508 3508 else:
3509 3509 return None
3510 3510
3511 3511 def get_api_data(self, with_merge_state=True):
3512 3512 from rhodecode.model.pull_request import PullRequestModel
3513 3513
3514 3514 pull_request = self
3515 3515 if with_merge_state:
3516 3516 merge_status = PullRequestModel().merge_status(pull_request)
3517 3517 merge_state = {
3518 3518 'status': merge_status[0],
3519 3519 'message': safe_unicode(merge_status[1]),
3520 3520 }
3521 3521 else:
3522 3522 merge_state = {'status': 'not_available',
3523 3523 'message': 'not_available'}
3524 3524
3525 3525 merge_data = {
3526 3526 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3527 3527 'reference': (
3528 3528 pull_request.shadow_merge_ref._asdict()
3529 3529 if pull_request.shadow_merge_ref else None),
3530 3530 }
3531 3531
3532 3532 data = {
3533 3533 'pull_request_id': pull_request.pull_request_id,
3534 3534 'url': PullRequestModel().get_url(pull_request),
3535 3535 'title': pull_request.title,
3536 3536 'description': pull_request.description,
3537 3537 'status': pull_request.status,
3538 3538 'created_on': pull_request.created_on,
3539 3539 'updated_on': pull_request.updated_on,
3540 3540 'commit_ids': pull_request.revisions,
3541 3541 'review_status': pull_request.calculated_review_status(),
3542 3542 'mergeable': merge_state,
3543 3543 'source': {
3544 3544 'clone_url': pull_request.source_repo.clone_url(),
3545 3545 'repository': pull_request.source_repo.repo_name,
3546 3546 'reference': {
3547 3547 'name': pull_request.source_ref_parts.name,
3548 3548 'type': pull_request.source_ref_parts.type,
3549 3549 'commit_id': pull_request.source_ref_parts.commit_id,
3550 3550 },
3551 3551 },
3552 3552 'target': {
3553 3553 'clone_url': pull_request.target_repo.clone_url(),
3554 3554 'repository': pull_request.target_repo.repo_name,
3555 3555 'reference': {
3556 3556 'name': pull_request.target_ref_parts.name,
3557 3557 'type': pull_request.target_ref_parts.type,
3558 3558 'commit_id': pull_request.target_ref_parts.commit_id,
3559 3559 },
3560 3560 },
3561 3561 'merge': merge_data,
3562 3562 'author': pull_request.author.get_api_data(include_secrets=False,
3563 3563 details='basic'),
3564 3564 'reviewers': [
3565 3565 {
3566 3566 'user': reviewer.get_api_data(include_secrets=False,
3567 3567 details='basic'),
3568 3568 'reasons': reasons,
3569 3569 'review_status': st[0][1].status if st else 'not_reviewed',
3570 3570 }
3571 3571 for reviewer, reasons, mandatory, st in
3572 3572 pull_request.reviewers_statuses()
3573 3573 ]
3574 3574 }
3575 3575
3576 3576 return data
3577 3577
3578 3578
3579 3579 class PullRequest(Base, _PullRequestBase):
3580 3580 __tablename__ = 'pull_requests'
3581 3581 __table_args__ = (
3582 3582 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3583 3583 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3584 3584 )
3585 3585
3586 3586 pull_request_id = Column(
3587 3587 'pull_request_id', Integer(), nullable=False, primary_key=True)
3588 3588
3589 3589 def __repr__(self):
3590 3590 if self.pull_request_id:
3591 3591 return '<DB:PullRequest #%s>' % self.pull_request_id
3592 3592 else:
3593 3593 return '<DB:PullRequest at %#x>' % id(self)
3594 3594
3595 3595 reviewers = relationship('PullRequestReviewers',
3596 3596 cascade="all, delete, delete-orphan")
3597 3597 statuses = relationship('ChangesetStatus',
3598 3598 cascade="all, delete, delete-orphan")
3599 3599 comments = relationship('ChangesetComment',
3600 3600 cascade="all, delete, delete-orphan")
3601 3601 versions = relationship('PullRequestVersion',
3602 3602 cascade="all, delete, delete-orphan",
3603 3603 lazy='dynamic')
3604 3604
3605 3605 @classmethod
3606 3606 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3607 3607 internal_methods=None):
3608 3608
3609 3609 class PullRequestDisplay(object):
3610 3610 """
3611 3611 Special object wrapper for showing PullRequest data via Versions
3612 3612 It mimics PR object as close as possible. This is read only object
3613 3613 just for display
3614 3614 """
3615 3615
3616 3616 def __init__(self, attrs, internal=None):
3617 3617 self.attrs = attrs
3618 3618 # internal have priority over the given ones via attrs
3619 3619 self.internal = internal or ['versions']
3620 3620
3621 3621 def __getattr__(self, item):
3622 3622 if item in self.internal:
3623 3623 return getattr(self, item)
3624 3624 try:
3625 3625 return self.attrs[item]
3626 3626 except KeyError:
3627 3627 raise AttributeError(
3628 3628 '%s object has no attribute %s' % (self, item))
3629 3629
3630 3630 def __repr__(self):
3631 3631 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3632 3632
3633 3633 def versions(self):
3634 3634 return pull_request_obj.versions.order_by(
3635 3635 PullRequestVersion.pull_request_version_id).all()
3636 3636
3637 3637 def is_closed(self):
3638 3638 return pull_request_obj.is_closed()
3639 3639
3640 3640 @property
3641 3641 def pull_request_version_id(self):
3642 3642 return getattr(pull_request_obj, 'pull_request_version_id', None)
3643 3643
3644 3644 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3645 3645
3646 3646 attrs.author = StrictAttributeDict(
3647 3647 pull_request_obj.author.get_api_data())
3648 3648 if pull_request_obj.target_repo:
3649 3649 attrs.target_repo = StrictAttributeDict(
3650 3650 pull_request_obj.target_repo.get_api_data())
3651 3651 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3652 3652
3653 3653 if pull_request_obj.source_repo:
3654 3654 attrs.source_repo = StrictAttributeDict(
3655 3655 pull_request_obj.source_repo.get_api_data())
3656 3656 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3657 3657
3658 3658 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3659 3659 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3660 3660 attrs.revisions = pull_request_obj.revisions
3661 3661
3662 3662 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3663 3663 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3664 3664 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3665 3665
3666 3666 return PullRequestDisplay(attrs, internal=internal_methods)
3667 3667
3668 3668 def is_closed(self):
3669 3669 return self.status == self.STATUS_CLOSED
3670 3670
3671 3671 def __json__(self):
3672 3672 return {
3673 3673 'revisions': self.revisions,
3674 3674 }
3675 3675
3676 3676 def calculated_review_status(self):
3677 3677 from rhodecode.model.changeset_status import ChangesetStatusModel
3678 3678 return ChangesetStatusModel().calculated_review_status(self)
3679 3679
3680 3680 def reviewers_statuses(self):
3681 3681 from rhodecode.model.changeset_status import ChangesetStatusModel
3682 3682 return ChangesetStatusModel().reviewers_statuses(self)
3683 3683
3684 3684 @property
3685 3685 def workspace_id(self):
3686 3686 from rhodecode.model.pull_request import PullRequestModel
3687 3687 return PullRequestModel()._workspace_id(self)
3688 3688
3689 3689 def get_shadow_repo(self):
3690 3690 workspace_id = self.workspace_id
3691 3691 vcs_obj = self.target_repo.scm_instance()
3692 3692 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3693 3693 workspace_id)
3694 3694 return vcs_obj._get_shadow_instance(shadow_repository_path)
3695 3695
3696 3696
3697 3697 class PullRequestVersion(Base, _PullRequestBase):
3698 3698 __tablename__ = 'pull_request_versions'
3699 3699 __table_args__ = (
3700 3700 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3701 3701 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3702 3702 )
3703 3703
3704 3704 pull_request_version_id = Column(
3705 3705 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3706 3706 pull_request_id = Column(
3707 3707 'pull_request_id', Integer(),
3708 3708 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3709 3709 pull_request = relationship('PullRequest')
3710 3710
3711 3711 def __repr__(self):
3712 3712 if self.pull_request_version_id:
3713 3713 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3714 3714 else:
3715 3715 return '<DB:PullRequestVersion at %#x>' % id(self)
3716 3716
3717 3717 @property
3718 3718 def reviewers(self):
3719 3719 return self.pull_request.reviewers
3720 3720
3721 3721 @property
3722 3722 def versions(self):
3723 3723 return self.pull_request.versions
3724 3724
3725 3725 def is_closed(self):
3726 3726 # calculate from original
3727 3727 return self.pull_request.status == self.STATUS_CLOSED
3728 3728
3729 3729 def calculated_review_status(self):
3730 3730 return self.pull_request.calculated_review_status()
3731 3731
3732 3732 def reviewers_statuses(self):
3733 3733 return self.pull_request.reviewers_statuses()
3734 3734
3735 3735
3736 3736 class PullRequestReviewers(Base, BaseModel):
3737 3737 __tablename__ = 'pull_request_reviewers'
3738 3738 __table_args__ = (
3739 3739 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3740 3740 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3741 3741 )
3742 3742
3743 3743 @hybrid_property
3744 3744 def reasons(self):
3745 3745 if not self._reasons:
3746 3746 return []
3747 3747 return self._reasons
3748 3748
3749 3749 @reasons.setter
3750 3750 def reasons(self, val):
3751 3751 val = val or []
3752 3752 if any(not isinstance(x, basestring) for x in val):
3753 3753 raise Exception('invalid reasons type, must be list of strings')
3754 3754 self._reasons = val
3755 3755
3756 3756 pull_requests_reviewers_id = Column(
3757 3757 'pull_requests_reviewers_id', Integer(), nullable=False,
3758 3758 primary_key=True)
3759 3759 pull_request_id = Column(
3760 3760 "pull_request_id", Integer(),
3761 3761 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3762 3762 user_id = Column(
3763 3763 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3764 3764 _reasons = Column(
3765 3765 'reason', MutationList.as_mutable(
3766 3766 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3767 3767 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3768 3768 user = relationship('User')
3769 3769 pull_request = relationship('PullRequest')
3770 3770
3771 3771
3772 3772 class Notification(Base, BaseModel):
3773 3773 __tablename__ = 'notifications'
3774 3774 __table_args__ = (
3775 3775 Index('notification_type_idx', 'type'),
3776 3776 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3777 3777 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3778 3778 )
3779 3779
3780 3780 TYPE_CHANGESET_COMMENT = u'cs_comment'
3781 3781 TYPE_MESSAGE = u'message'
3782 3782 TYPE_MENTION = u'mention'
3783 3783 TYPE_REGISTRATION = u'registration'
3784 3784 TYPE_PULL_REQUEST = u'pull_request'
3785 3785 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3786 3786
3787 3787 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3788 3788 subject = Column('subject', Unicode(512), nullable=True)
3789 3789 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3790 3790 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3791 3791 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3792 3792 type_ = Column('type', Unicode(255))
3793 3793
3794 3794 created_by_user = relationship('User')
3795 3795 notifications_to_users = relationship('UserNotification', lazy='joined',
3796 3796 cascade="all, delete, delete-orphan")
3797 3797
3798 3798 @property
3799 3799 def recipients(self):
3800 3800 return [x.user for x in UserNotification.query()\
3801 3801 .filter(UserNotification.notification == self)\
3802 3802 .order_by(UserNotification.user_id.asc()).all()]
3803 3803
3804 3804 @classmethod
3805 3805 def create(cls, created_by, subject, body, recipients, type_=None):
3806 3806 if type_ is None:
3807 3807 type_ = Notification.TYPE_MESSAGE
3808 3808
3809 3809 notification = cls()
3810 3810 notification.created_by_user = created_by
3811 3811 notification.subject = subject
3812 3812 notification.body = body
3813 3813 notification.type_ = type_
3814 3814 notification.created_on = datetime.datetime.now()
3815 3815
3816 3816 for u in recipients:
3817 3817 assoc = UserNotification()
3818 3818 assoc.notification = notification
3819 3819
3820 3820 # if created_by is inside recipients mark his notification
3821 3821 # as read
3822 3822 if u.user_id == created_by.user_id:
3823 3823 assoc.read = True
3824 3824
3825 3825 u.notifications.append(assoc)
3826 3826 Session().add(notification)
3827 3827
3828 3828 return notification
3829 3829
3830 3830
3831 3831 class UserNotification(Base, BaseModel):
3832 3832 __tablename__ = 'user_to_notification'
3833 3833 __table_args__ = (
3834 3834 UniqueConstraint('user_id', 'notification_id'),
3835 3835 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3836 3836 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3837 3837 )
3838 3838 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3839 3839 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3840 3840 read = Column('read', Boolean, default=False)
3841 3841 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3842 3842
3843 3843 user = relationship('User', lazy="joined")
3844 3844 notification = relationship('Notification', lazy="joined",
3845 3845 order_by=lambda: Notification.created_on.desc(),)
3846 3846
3847 3847 def mark_as_read(self):
3848 3848 self.read = True
3849 3849 Session().add(self)
3850 3850
3851 3851
3852 3852 class Gist(Base, BaseModel):
3853 3853 __tablename__ = 'gists'
3854 3854 __table_args__ = (
3855 3855 Index('g_gist_access_id_idx', 'gist_access_id'),
3856 3856 Index('g_created_on_idx', 'created_on'),
3857 3857 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3858 3858 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3859 3859 )
3860 3860 GIST_PUBLIC = u'public'
3861 3861 GIST_PRIVATE = u'private'
3862 3862 DEFAULT_FILENAME = u'gistfile1.txt'
3863 3863
3864 3864 ACL_LEVEL_PUBLIC = u'acl_public'
3865 3865 ACL_LEVEL_PRIVATE = u'acl_private'
3866 3866
3867 3867 gist_id = Column('gist_id', Integer(), primary_key=True)
3868 3868 gist_access_id = Column('gist_access_id', Unicode(250))
3869 3869 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3870 3870 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3871 3871 gist_expires = Column('gist_expires', Float(53), nullable=False)
3872 3872 gist_type = Column('gist_type', Unicode(128), nullable=False)
3873 3873 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3874 3874 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3875 3875 acl_level = Column('acl_level', Unicode(128), nullable=True)
3876 3876
3877 3877 owner = relationship('User')
3878 3878
3879 3879 def __repr__(self):
3880 3880 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3881 3881
3882 3882 @hybrid_property
3883 3883 def description_safe(self):
3884 3884 from rhodecode.lib import helpers as h
3885 3885 return h.escape(self.gist_description)
3886 3886
3887 3887 @classmethod
3888 3888 def get_or_404(cls, id_):
3889 3889 from pyramid.httpexceptions import HTTPNotFound
3890 3890
3891 3891 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3892 3892 if not res:
3893 3893 raise HTTPNotFound()
3894 3894 return res
3895 3895
3896 3896 @classmethod
3897 3897 def get_by_access_id(cls, gist_access_id):
3898 3898 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3899 3899
3900 3900 def gist_url(self):
3901 3901 from rhodecode.model.gist import GistModel
3902 3902 return GistModel().get_url(self)
3903 3903
3904 3904 @classmethod
3905 3905 def base_path(cls):
3906 3906 """
3907 3907 Returns base path when all gists are stored
3908 3908
3909 3909 :param cls:
3910 3910 """
3911 3911 from rhodecode.model.gist import GIST_STORE_LOC
3912 3912 q = Session().query(RhodeCodeUi)\
3913 3913 .filter(RhodeCodeUi.ui_key == URL_SEP)
3914 3914 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3915 3915 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3916 3916
3917 3917 def get_api_data(self):
3918 3918 """
3919 3919 Common function for generating gist related data for API
3920 3920 """
3921 3921 gist = self
3922 3922 data = {
3923 3923 'gist_id': gist.gist_id,
3924 3924 'type': gist.gist_type,
3925 3925 'access_id': gist.gist_access_id,
3926 3926 'description': gist.gist_description,
3927 3927 'url': gist.gist_url(),
3928 3928 'expires': gist.gist_expires,
3929 3929 'created_on': gist.created_on,
3930 3930 'modified_at': gist.modified_at,
3931 3931 'content': None,
3932 3932 'acl_level': gist.acl_level,
3933 3933 }
3934 3934 return data
3935 3935
3936 3936 def __json__(self):
3937 3937 data = dict(
3938 3938 )
3939 3939 data.update(self.get_api_data())
3940 3940 return data
3941 3941 # SCM functions
3942 3942
3943 3943 def scm_instance(self, **kwargs):
3944 3944 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3945 3945 return get_vcs_instance(
3946 3946 repo_path=safe_str(full_repo_path), create=False)
3947 3947
3948 3948
3949 3949 class ExternalIdentity(Base, BaseModel):
3950 3950 __tablename__ = 'external_identities'
3951 3951 __table_args__ = (
3952 3952 Index('local_user_id_idx', 'local_user_id'),
3953 3953 Index('external_id_idx', 'external_id'),
3954 3954 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3955 3955 'mysql_charset': 'utf8'})
3956 3956
3957 3957 external_id = Column('external_id', Unicode(255), default=u'',
3958 3958 primary_key=True)
3959 3959 external_username = Column('external_username', Unicode(1024), default=u'')
3960 3960 local_user_id = Column('local_user_id', Integer(),
3961 3961 ForeignKey('users.user_id'), primary_key=True)
3962 3962 provider_name = Column('provider_name', Unicode(255), default=u'',
3963 3963 primary_key=True)
3964 3964 access_token = Column('access_token', String(1024), default=u'')
3965 3965 alt_token = Column('alt_token', String(1024), default=u'')
3966 3966 token_secret = Column('token_secret', String(1024), default=u'')
3967 3967
3968 3968 @classmethod
3969 3969 def by_external_id_and_provider(cls, external_id, provider_name,
3970 3970 local_user_id=None):
3971 3971 """
3972 3972 Returns ExternalIdentity instance based on search params
3973 3973
3974 3974 :param external_id:
3975 3975 :param provider_name:
3976 3976 :return: ExternalIdentity
3977 3977 """
3978 3978 query = cls.query()
3979 3979 query = query.filter(cls.external_id == external_id)
3980 3980 query = query.filter(cls.provider_name == provider_name)
3981 3981 if local_user_id:
3982 3982 query = query.filter(cls.local_user_id == local_user_id)
3983 3983 return query.first()
3984 3984
3985 3985 @classmethod
3986 3986 def user_by_external_id_and_provider(cls, external_id, provider_name):
3987 3987 """
3988 3988 Returns User instance based on search params
3989 3989
3990 3990 :param external_id:
3991 3991 :param provider_name:
3992 3992 :return: User
3993 3993 """
3994 3994 query = User.query()
3995 3995 query = query.filter(cls.external_id == external_id)
3996 3996 query = query.filter(cls.provider_name == provider_name)
3997 3997 query = query.filter(User.user_id == cls.local_user_id)
3998 3998 return query.first()
3999 3999
4000 4000 @classmethod
4001 4001 def by_local_user_id(cls, local_user_id):
4002 4002 """
4003 4003 Returns all tokens for user
4004 4004
4005 4005 :param local_user_id:
4006 4006 :return: ExternalIdentity
4007 4007 """
4008 4008 query = cls.query()
4009 4009 query = query.filter(cls.local_user_id == local_user_id)
4010 4010 return query
4011 4011
4012 4012
4013 4013 class Integration(Base, BaseModel):
4014 4014 __tablename__ = 'integrations'
4015 4015 __table_args__ = (
4016 4016 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4017 4017 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
4018 4018 )
4019 4019
4020 4020 integration_id = Column('integration_id', Integer(), primary_key=True)
4021 4021 integration_type = Column('integration_type', String(255))
4022 4022 enabled = Column('enabled', Boolean(), nullable=False)
4023 4023 name = Column('name', String(255), nullable=False)
4024 4024 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4025 4025 default=False)
4026 4026
4027 4027 settings = Column(
4028 4028 'settings_json', MutationObj.as_mutable(
4029 4029 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4030 4030 repo_id = Column(
4031 4031 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4032 4032 nullable=True, unique=None, default=None)
4033 4033 repo = relationship('Repository', lazy='joined')
4034 4034
4035 4035 repo_group_id = Column(
4036 4036 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4037 4037 nullable=True, unique=None, default=None)
4038 4038 repo_group = relationship('RepoGroup', lazy='joined')
4039 4039
4040 4040 @property
4041 4041 def scope(self):
4042 4042 if self.repo:
4043 4043 return repr(self.repo)
4044 4044 if self.repo_group:
4045 4045 if self.child_repos_only:
4046 4046 return repr(self.repo_group) + ' (child repos only)'
4047 4047 else:
4048 4048 return repr(self.repo_group) + ' (recursive)'
4049 4049 if self.child_repos_only:
4050 4050 return 'root_repos'
4051 4051 return 'global'
4052 4052
4053 4053 def __repr__(self):
4054 4054 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4055 4055
4056 4056
4057 4057 class RepoReviewRuleUser(Base, BaseModel):
4058 4058 __tablename__ = 'repo_review_rules_users'
4059 4059 __table_args__ = (
4060 4060 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4061 4061 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4062 4062 )
4063 4063 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4064 4064 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4065 4065 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4066 4066 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4067 4067 user = relationship('User')
4068 4068
4069 4069 def rule_data(self):
4070 4070 return {
4071 4071 'mandatory': self.mandatory
4072 4072 }
4073 4073
4074 4074
4075 4075 class RepoReviewRuleUserGroup(Base, BaseModel):
4076 4076 __tablename__ = 'repo_review_rules_users_groups'
4077 4077 __table_args__ = (
4078 4078 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4079 4079 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4080 4080 )
4081 4081 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4082 4082 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4083 4083 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4084 4084 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4085 4085 users_group = relationship('UserGroup')
4086 4086
4087 4087 def rule_data(self):
4088 4088 return {
4089 4089 'mandatory': self.mandatory
4090 4090 }
4091 4091
4092 4092
4093 4093 class RepoReviewRule(Base, BaseModel):
4094 4094 __tablename__ = 'repo_review_rules'
4095 4095 __table_args__ = (
4096 4096 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4097 4097 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4098 4098 )
4099 4099
4100 4100 repo_review_rule_id = Column(
4101 4101 'repo_review_rule_id', Integer(), primary_key=True)
4102 4102 repo_id = Column(
4103 4103 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4104 4104 repo = relationship('Repository', backref='review_rules')
4105 4105
4106 4106 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4107 4107 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4108 4108
4109 4109 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4110 4110 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4111 4111 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4112 4112 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4113 4113
4114 4114 rule_users = relationship('RepoReviewRuleUser')
4115 4115 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4116 4116
4117 4117 @hybrid_property
4118 4118 def branch_pattern(self):
4119 4119 return self._branch_pattern or '*'
4120 4120
4121 4121 def _validate_glob(self, value):
4122 4122 re.compile('^' + glob2re(value) + '$')
4123 4123
4124 4124 @branch_pattern.setter
4125 4125 def branch_pattern(self, value):
4126 4126 self._validate_glob(value)
4127 4127 self._branch_pattern = value or '*'
4128 4128
4129 4129 @hybrid_property
4130 4130 def file_pattern(self):
4131 4131 return self._file_pattern or '*'
4132 4132
4133 4133 @file_pattern.setter
4134 4134 def file_pattern(self, value):
4135 4135 self._validate_glob(value)
4136 4136 self._file_pattern = value or '*'
4137 4137
4138 4138 def matches(self, branch, files_changed):
4139 4139 """
4140 4140 Check if this review rule matches a branch/files in a pull request
4141 4141
4142 4142 :param branch: branch name for the commit
4143 4143 :param files_changed: list of file paths changed in the pull request
4144 4144 """
4145 4145
4146 4146 branch = branch or ''
4147 4147 files_changed = files_changed or []
4148 4148
4149 4149 branch_matches = True
4150 4150 if branch:
4151 4151 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4152 4152 branch_matches = bool(branch_regex.search(branch))
4153 4153
4154 4154 files_matches = True
4155 4155 if self.file_pattern != '*':
4156 4156 files_matches = False
4157 4157 file_regex = re.compile(glob2re(self.file_pattern))
4158 4158 for filename in files_changed:
4159 4159 if file_regex.search(filename):
4160 4160 files_matches = True
4161 4161 break
4162 4162
4163 4163 return branch_matches and files_matches
4164 4164
4165 4165 @property
4166 4166 def review_users(self):
4167 4167 """ Returns the users which this rule applies to """
4168 4168
4169 4169 users = collections.OrderedDict()
4170 4170
4171 4171 for rule_user in self.rule_users:
4172 4172 if rule_user.user.active:
4173 4173 if rule_user.user not in users:
4174 4174 users[rule_user.user.username] = {
4175 4175 'user': rule_user.user,
4176 4176 'source': 'user',
4177 4177 'source_data': {},
4178 4178 'data': rule_user.rule_data()
4179 4179 }
4180 4180
4181 4181 for rule_user_group in self.rule_user_groups:
4182 4182 source_data = {
4183 4183 'name': rule_user_group.users_group.users_group_name,
4184 4184 'members': len(rule_user_group.users_group.members)
4185 4185 }
4186 4186 for member in rule_user_group.users_group.members:
4187 4187 if member.user.active:
4188 4188 users[member.user.username] = {
4189 4189 'user': member.user,
4190 4190 'source': 'user_group',
4191 4191 'source_data': source_data,
4192 4192 'data': rule_user_group.rule_data()
4193 4193 }
4194 4194
4195 4195 return users
4196 4196
4197 4197 def __repr__(self):
4198 4198 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4199 4199 self.repo_review_rule_id, self.repo)
4200 4200
4201 4201
4202 4202 class DbMigrateVersion(Base, BaseModel):
4203 4203 __tablename__ = 'db_migrate_version'
4204 4204 __table_args__ = (
4205 4205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4206 4206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4207 4207 )
4208 4208 repository_id = Column('repository_id', String(250), primary_key=True)
4209 4209 repository_path = Column('repository_path', Text)
4210 4210 version = Column('version', Integer)
4211 4211
4212 4212
4213 4213 class DbSession(Base, BaseModel):
4214 4214 __tablename__ = 'db_session'
4215 4215 __table_args__ = (
4216 4216 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4217 4217 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4218 4218 )
4219 4219
4220 4220 def __repr__(self):
4221 4221 return '<DB:DbSession({})>'.format(self.id)
4222 4222
4223 4223 id = Column('id', Integer())
4224 4224 namespace = Column('namespace', String(255), primary_key=True)
4225 4225 accessed = Column('accessed', DateTime, nullable=False)
4226 4226 created = Column('created', DateTime, nullable=False)
4227 4227 data = Column('data', PickleType, nullable=False)
@@ -1,183 +1,188 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 import mock
22 22 import pytest
23 23
24 24 from rhodecode.lib.auth import _RhodeCodeCryptoBCrypt
25 25 from rhodecode.authentication.base import RhodeCodeAuthPluginBase
26 26 from rhodecode.authentication.plugins.auth_ldap import RhodeCodeAuthPlugin
27 27 from rhodecode.model import db
28 28
29 29
30 class TestAuthPlugin(RhodeCodeAuthPluginBase):
31
32 def name(self):
33 return 'stub_auth'
34
30 35 def test_authenticate_returns_from_auth(stub_auth_data):
31 plugin = RhodeCodeAuthPluginBase('stub_id')
36 plugin = TestAuthPlugin('stub_id')
32 37 with mock.patch.object(plugin, 'auth') as auth_mock:
33 38 auth_mock.return_value = stub_auth_data
34 39 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
35 40 assert stub_auth_data == result
36 41
37 42
38 43 def test_authenticate_returns_empty_auth_data():
39 44 auth_data = {}
40 plugin = RhodeCodeAuthPluginBase('stub_id')
45 plugin = TestAuthPlugin('stub_id')
41 46 with mock.patch.object(plugin, 'auth') as auth_mock:
42 47 auth_mock.return_value = auth_data
43 48 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
44 49 assert auth_data == result
45 50
46 51
47 52 def test_authenticate_skips_hash_migration_if_mismatch(stub_auth_data):
48 53 stub_auth_data['_hash_migrate'] = 'new-hash'
49 plugin = RhodeCodeAuthPluginBase('stub_id')
54 plugin = TestAuthPlugin('stub_id')
50 55 with mock.patch.object(plugin, 'auth') as auth_mock:
51 56 auth_mock.return_value = stub_auth_data
52 57 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
53 58
54 59 user = db.User.get_by_username(stub_auth_data['username'])
55 60 assert user.password != 'new-hash'
56 61 assert result == stub_auth_data
57 62
58 63
59 64 def test_authenticate_migrates_to_new_hash(stub_auth_data):
60 65 new_password = b'new-password'
61 66 new_hash = _RhodeCodeCryptoBCrypt().hash_create(new_password)
62 67 stub_auth_data['_hash_migrate'] = new_hash
63 plugin = RhodeCodeAuthPluginBase('stub_id')
68 plugin = TestAuthPlugin('stub_id')
64 69 with mock.patch.object(plugin, 'auth') as auth_mock:
65 70 auth_mock.return_value = stub_auth_data
66 71 result = plugin._authenticate(
67 72 mock.Mock(), stub_auth_data['username'], new_password, {})
68 73
69 74 user = db.User.get_by_username(stub_auth_data['username'])
70 75 assert user.password == new_hash
71 76 assert result == stub_auth_data
72 77
73 78
74 79 @pytest.fixture
75 80 def stub_auth_data(user_util):
76 81 user = user_util.create_user()
77 82 data = {
78 83 'username': user.username,
79 84 'password': 'password',
80 85 'email': 'test@example.org',
81 86 'firstname': 'John',
82 87 'lastname': 'Smith',
83 88 'groups': [],
84 89 'active': True,
85 90 'admin': False,
86 91 'extern_name': 'test',
87 92 'extern_type': 'ldap',
88 93 'active_from_extern': True
89 94 }
90 95 return data
91 96
92 97
93 98 class TestRhodeCodeAuthPlugin(object):
94 99 def setup_method(self, method):
95 100 self.finalizers = []
96 101 self.user = mock.Mock()
97 102 self.user.username = 'test'
98 103 self.user.password = 'old-password'
99 104 self.fake_auth = {
100 105 'username': 'test',
101 106 'password': 'test',
102 107 'email': 'test@example.org',
103 108 'firstname': 'John',
104 109 'lastname': 'Smith',
105 110 'groups': [],
106 111 'active': True,
107 112 'admin': False,
108 113 'extern_name': 'test',
109 114 'extern_type': 'ldap',
110 115 'active_from_extern': True
111 116 }
112 117
113 118 def teardown_method(self, method):
114 119 if self.finalizers:
115 120 for finalizer in self.finalizers:
116 121 finalizer()
117 122 self.finalizers = []
118 123
119 124 def test_fake_password_is_created_for_the_new_user(self):
120 125 self._patch()
121 126 auth_plugin = RhodeCodeAuthPlugin('stub_id')
122 127 auth_plugin._authenticate(self.user, 'test', 'test', [])
123 128 self.password_generator_mock.assert_called_once_with(length=16)
124 129 create_user_kwargs = self.create_user_mock.call_args[1]
125 130 assert create_user_kwargs['password'] == 'new-password'
126 131
127 132 def test_fake_password_is_not_created_for_the_existing_user(self):
128 133 self._patch()
129 134 self.get_user_mock.return_value = self.user
130 135 auth_plugin = RhodeCodeAuthPlugin('stub_id')
131 136 auth_plugin._authenticate(self.user, 'test', 'test', [])
132 137 assert self.password_generator_mock.called is False
133 138 create_user_kwargs = self.create_user_mock.call_args[1]
134 139 assert create_user_kwargs['password'] == self.user.password
135 140
136 141 def _patch(self):
137 142 get_user_patch = mock.patch('rhodecode.model.db.User.get_by_username')
138 143 self.get_user_mock = get_user_patch.start()
139 144 self.get_user_mock.return_value = None
140 145 self.finalizers.append(get_user_patch.stop)
141 146
142 147 create_user_patch = mock.patch(
143 148 'rhodecode.model.user.UserModel.create_or_update')
144 149 self.create_user_mock = create_user_patch.start()
145 150 self.create_user_mock.return_value = None
146 151 self.finalizers.append(create_user_patch.stop)
147 152
148 153 auth_patch = mock.patch.object(RhodeCodeAuthPlugin, 'auth')
149 154 self.auth_mock = auth_patch.start()
150 155 self.auth_mock.return_value = self.fake_auth
151 156 self.finalizers.append(auth_patch.stop)
152 157
153 158 password_generator_patch = mock.patch(
154 159 'rhodecode.lib.auth.PasswordGenerator.gen_password')
155 160 self.password_generator_mock = password_generator_patch.start()
156 161 self.password_generator_mock.return_value = 'new-password'
157 162 self.finalizers.append(password_generator_patch.stop)
158 163
159 164
160 165 def test_missing_ldap():
161 166 from rhodecode.model.validators import Missing
162 167
163 168 try:
164 169 import ldap_not_existing
165 170 except ImportError:
166 171 # means that python-ldap is not installed
167 172 ldap_not_existing = Missing
168 173
169 174 # missing is singleton
170 175 assert ldap_not_existing == Missing
171 176
172 177
173 178 def test_import_ldap():
174 179 from rhodecode.model.validators import Missing
175 180
176 181 try:
177 182 import ldap
178 183 except ImportError:
179 184 # means that python-ldap is not installed
180 185 ldap = Missing
181 186
182 187 # missing is singleton
183 188 assert False is (ldap == Missing)
General Comments 0
You need to be logged in to leave comments. Login now