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