##// END OF EJS Templates
authentication: enabled authentication with auth_token and repository scope....
marcink -
r1510:77606b4c default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -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 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -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