##// END OF EJS Templates
caches: use individual namespaces per user to prevent beaker caching problems....
marcink -
r2572:5b07455a default
parent child Browse files
Show More
@@ -1,719 +1,726 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Authentication modules
23 23 """
24 24
25 25 import colander
26 26 import copy
27 27 import logging
28 28 import time
29 29 import traceback
30 30 import warnings
31 31 import functools
32 32
33 33 from pyramid.threadlocal import get_current_registry
34 34 from zope.cachedescriptors.property import Lazy as LazyProperty
35 35
36 36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 38 from rhodecode.lib import caches
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 40 from rhodecode.lib.utils2 import safe_int
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.model.db import User
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.model.settings import SettingsModel
45 45 from rhodecode.model.user import UserModel
46 46 from rhodecode.model.user_group import UserGroupModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51 # auth types that authenticate() function can receive
52 52 VCS_TYPE = 'vcs'
53 53 HTTP_TYPE = 'http'
54 54
55 55
56 56 class hybrid_property(object):
57 57 """
58 58 a property decorator that works both for instance and class
59 59 """
60 60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 61 self.fget = fget
62 62 self.fset = fset
63 63 self.fdel = fdel
64 64 self.expr = expr or fget
65 65 functools.update_wrapper(self, fget)
66 66
67 67 def __get__(self, instance, owner):
68 68 if instance is None:
69 69 return self.expr(owner)
70 70 else:
71 71 return self.fget(instance)
72 72
73 73 def __set__(self, instance, value):
74 74 self.fset(instance, value)
75 75
76 76 def __delete__(self, instance):
77 77 self.fdel(instance)
78 78
79 79
80 80 class LazyFormencode(object):
81 81 def __init__(self, formencode_obj, *args, **kwargs):
82 82 self.formencode_obj = formencode_obj
83 83 self.args = args
84 84 self.kwargs = kwargs
85 85
86 86 def __call__(self, *args, **kwargs):
87 87 from inspect import isfunction
88 88 formencode_obj = self.formencode_obj
89 89 if isfunction(formencode_obj):
90 90 # case we wrap validators into functions
91 91 formencode_obj = self.formencode_obj(*args, **kwargs)
92 92 return formencode_obj(*self.args, **self.kwargs)
93 93
94 94
95 95 class RhodeCodeAuthPluginBase(object):
96 96 # cache the authentication request for N amount of seconds. Some kind
97 97 # of authentication methods are very heavy and it's very efficient to cache
98 98 # the result of a call. If it's set to None (default) cache is off
99 99 AUTH_CACHE_TTL = None
100 100 AUTH_CACHE = {}
101 101
102 102 auth_func_attrs = {
103 103 "username": "unique username",
104 104 "firstname": "first name",
105 105 "lastname": "last name",
106 106 "email": "email address",
107 107 "groups": '["list", "of", "groups"]',
108 108 "user_group_sync":
109 109 'True|False defines if returned user groups should be synced',
110 110 "extern_name": "name in external source of record",
111 111 "extern_type": "type of external source of record",
112 112 "admin": 'True|False defines if user should be RhodeCode super admin',
113 113 "active":
114 114 'True|False defines active state of user internally for RhodeCode',
115 115 "active_from_extern":
116 116 "True|False\None, active state from the external auth, "
117 117 "None means use definition from RhodeCode extern_type active value"
118 118
119 119 }
120 120 # set on authenticate() method and via set_auth_type func.
121 121 auth_type = None
122 122
123 123 # set on authenticate() method and via set_calling_scope_repo, this is a
124 124 # calling scope repository when doing authentication most likely on VCS
125 125 # operations
126 126 acl_repo_name = None
127 127
128 128 # List of setting names to store encrypted. Plugins may override this list
129 129 # to store settings encrypted.
130 130 _settings_encrypted = []
131 131
132 132 # Mapping of python to DB settings model types. Plugins may override or
133 133 # extend this mapping.
134 134 _settings_type_map = {
135 135 colander.String: 'unicode',
136 136 colander.Integer: 'int',
137 137 colander.Boolean: 'bool',
138 138 colander.List: 'list',
139 139 }
140 140
141 141 # list of keys in settings that are unsafe to be logged, should be passwords
142 142 # or other crucial credentials
143 143 _settings_unsafe_keys = []
144 144
145 145 def __init__(self, plugin_id):
146 146 self._plugin_id = plugin_id
147 147
148 148 def __str__(self):
149 149 return self.get_id()
150 150
151 151 def _get_setting_full_name(self, name):
152 152 """
153 153 Return the full setting name used for storing values in the database.
154 154 """
155 155 # TODO: johbo: Using the name here is problematic. It would be good to
156 156 # introduce either new models in the database to hold Plugin and
157 157 # PluginSetting or to use the plugin id here.
158 158 return 'auth_{}_{}'.format(self.name, name)
159 159
160 160 def _get_setting_type(self, name):
161 161 """
162 162 Return the type of a setting. This type is defined by the SettingsModel
163 163 and determines how the setting is stored in DB. Optionally the suffix
164 164 `.encrypted` is appended to instruct SettingsModel to store it
165 165 encrypted.
166 166 """
167 167 schema_node = self.get_settings_schema().get(name)
168 168 db_type = self._settings_type_map.get(
169 169 type(schema_node.typ), 'unicode')
170 170 if name in self._settings_encrypted:
171 171 db_type = '{}.encrypted'.format(db_type)
172 172 return db_type
173 173
174 174 @LazyProperty
175 175 def plugin_settings(self):
176 176 settings = SettingsModel().get_all_settings()
177 177 return settings
178 178
179 179 def is_enabled(self):
180 180 """
181 181 Returns true if this plugin is enabled. An enabled plugin can be
182 182 configured in the admin interface but it is not consulted during
183 183 authentication.
184 184 """
185 185 auth_plugins = SettingsModel().get_auth_plugins()
186 186 return self.get_id() in auth_plugins
187 187
188 188 def is_active(self):
189 189 """
190 190 Returns true if the plugin is activated. An activated plugin is
191 191 consulted during authentication, assumed it is also enabled.
192 192 """
193 193 return self.get_setting_by_name('enabled')
194 194
195 195 def get_id(self):
196 196 """
197 197 Returns the plugin id.
198 198 """
199 199 return self._plugin_id
200 200
201 201 def get_display_name(self):
202 202 """
203 203 Returns a translation string for displaying purposes.
204 204 """
205 205 raise NotImplementedError('Not implemented in base class')
206 206
207 207 def get_settings_schema(self):
208 208 """
209 209 Returns a colander schema, representing the plugin settings.
210 210 """
211 211 return AuthnPluginSettingsSchemaBase()
212 212
213 213 def get_setting_by_name(self, name, default=None, cache=True):
214 214 """
215 215 Returns a plugin setting by name.
216 216 """
217 217 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
218 218 if cache:
219 219 plugin_settings = self.plugin_settings
220 220 else:
221 221 plugin_settings = SettingsModel().get_all_settings()
222 222
223 223 if full_name in plugin_settings:
224 224 return plugin_settings[full_name]
225 225 else:
226 226 return default
227 227
228 228 def create_or_update_setting(self, name, value):
229 229 """
230 230 Create or update a setting for this plugin in the persistent storage.
231 231 """
232 232 full_name = self._get_setting_full_name(name)
233 233 type_ = self._get_setting_type(name)
234 234 db_setting = SettingsModel().create_or_update_setting(
235 235 full_name, value, type_)
236 236 return db_setting.app_settings_value
237 237
238 238 def get_settings(self):
239 239 """
240 240 Returns the plugin settings as dictionary.
241 241 """
242 242 settings = {}
243 243 for node in self.get_settings_schema():
244 244 settings[node.name] = self.get_setting_by_name(node.name)
245 245 return settings
246 246
247 247 def log_safe_settings(self, settings):
248 248 """
249 249 returns a log safe representation of settings, without any secrets
250 250 """
251 251 settings_copy = copy.deepcopy(settings)
252 252 for k in self._settings_unsafe_keys:
253 253 if k in settings_copy:
254 254 del settings_copy[k]
255 255 return settings_copy
256 256
257 257 @hybrid_property
258 258 def name(self):
259 259 """
260 260 Returns the name of this authentication plugin.
261 261
262 262 :returns: string
263 263 """
264 264 raise NotImplementedError("Not implemented in base class")
265 265
266 266 def get_url_slug(self):
267 267 """
268 268 Returns a slug which should be used when constructing URLs which refer
269 269 to this plugin. By default it returns the plugin name. If the name is
270 270 not suitable for using it in an URL the plugin should override this
271 271 method.
272 272 """
273 273 return self.name
274 274
275 275 @property
276 276 def is_headers_auth(self):
277 277 """
278 278 Returns True if this authentication plugin uses HTTP headers as
279 279 authentication method.
280 280 """
281 281 return False
282 282
283 283 @hybrid_property
284 284 def is_container_auth(self):
285 285 """
286 286 Deprecated method that indicates if this authentication plugin uses
287 287 HTTP headers as authentication method.
288 288 """
289 289 warnings.warn(
290 290 'Use is_headers_auth instead.', category=DeprecationWarning)
291 291 return self.is_headers_auth
292 292
293 293 @hybrid_property
294 294 def allows_creating_users(self):
295 295 """
296 296 Defines if Plugin allows users to be created on-the-fly when
297 297 authentication is called. Controls how external plugins should behave
298 298 in terms if they are allowed to create new users, or not. Base plugins
299 299 should not be allowed to, but External ones should be !
300 300
301 301 :return: bool
302 302 """
303 303 return False
304 304
305 305 def set_auth_type(self, auth_type):
306 306 self.auth_type = auth_type
307 307
308 308 def set_calling_scope_repo(self, acl_repo_name):
309 309 self.acl_repo_name = acl_repo_name
310 310
311 311 def allows_authentication_from(
312 312 self, user, allows_non_existing_user=True,
313 313 allowed_auth_plugins=None, allowed_auth_sources=None):
314 314 """
315 315 Checks if this authentication module should accept a request for
316 316 the current user.
317 317
318 318 :param user: user object fetched using plugin's get_user() method.
319 319 :param allows_non_existing_user: if True, don't allow the
320 320 user to be empty, meaning not existing in our database
321 321 :param allowed_auth_plugins: if provided, users extern_type will be
322 322 checked against a list of provided extern types, which are plugin
323 323 auth_names in the end
324 324 :param allowed_auth_sources: authentication type allowed,
325 325 `http` or `vcs` default is both.
326 326 defines if plugin will accept only http authentication vcs
327 327 authentication(git/hg) or both
328 328 :returns: boolean
329 329 """
330 330 if not user and not allows_non_existing_user:
331 331 log.debug('User is empty but plugin does not allow empty users,'
332 332 'not allowed to authenticate')
333 333 return False
334 334
335 335 expected_auth_plugins = allowed_auth_plugins or [self.name]
336 336 if user and (user.extern_type and
337 337 user.extern_type not in expected_auth_plugins):
338 338 log.debug(
339 339 'User `%s` is bound to `%s` auth type. Plugin allows only '
340 340 '%s, skipping', user, user.extern_type, expected_auth_plugins)
341 341
342 342 return False
343 343
344 344 # by default accept both
345 345 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
346 346 if self.auth_type not in expected_auth_from:
347 347 log.debug('Current auth source is %s but plugin only allows %s',
348 348 self.auth_type, expected_auth_from)
349 349 return False
350 350
351 351 return True
352 352
353 353 def get_user(self, username=None, **kwargs):
354 354 """
355 355 Helper method for user fetching in plugins, by default it's using
356 356 simple fetch by username, but this method can be custimized in plugins
357 357 eg. headers auth plugin to fetch user by environ params
358 358
359 359 :param username: username if given to fetch from database
360 360 :param kwargs: extra arguments needed for user fetching.
361 361 """
362 362 user = None
363 363 log.debug(
364 364 'Trying to fetch user `%s` from RhodeCode database', username)
365 365 if username:
366 366 user = User.get_by_username(username)
367 367 if not user:
368 368 log.debug('User not found, fallback to fetch user in '
369 369 'case insensitive mode')
370 370 user = User.get_by_username(username, case_insensitive=True)
371 371 else:
372 372 log.debug('provided username:`%s` is empty skipping...', username)
373 373 if not user:
374 374 log.debug('User `%s` not found in database', username)
375 375 else:
376 376 log.debug('Got DB user:%s', user)
377 377 return user
378 378
379 379 def user_activation_state(self):
380 380 """
381 381 Defines user activation state when creating new users
382 382
383 383 :returns: boolean
384 384 """
385 385 raise NotImplementedError("Not implemented in base class")
386 386
387 387 def auth(self, userobj, username, passwd, settings, **kwargs):
388 388 """
389 389 Given a user object (which may be null), username, a plaintext
390 390 password, and a settings object (containing all the keys needed as
391 391 listed in settings()), authenticate this user's login attempt.
392 392
393 393 Return None on failure. On success, return a dictionary of the form:
394 394
395 395 see: RhodeCodeAuthPluginBase.auth_func_attrs
396 396 This is later validated for correctness
397 397 """
398 398 raise NotImplementedError("not implemented in base class")
399 399
400 400 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
401 401 """
402 402 Wrapper to call self.auth() that validates call on it
403 403
404 404 :param userobj: userobj
405 405 :param username: username
406 406 :param passwd: plaintext password
407 407 :param settings: plugin settings
408 408 """
409 409 auth = self.auth(userobj, username, passwd, settings, **kwargs)
410 410 if auth:
411 411 auth['_plugin'] = self.name
412 412 auth['_ttl_cache'] = self.get_ttl_cache(settings)
413 413 # check if hash should be migrated ?
414 414 new_hash = auth.get('_hash_migrate')
415 415 if new_hash:
416 416 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
417 417 if 'user_group_sync' not in auth:
418 418 auth['user_group_sync'] = False
419 419 return self._validate_auth_return(auth)
420 420 return auth
421 421
422 422 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
423 423 new_hash_cypher = _RhodeCodeCryptoBCrypt()
424 424 # extra checks, so make sure new hash is correct.
425 425 password_encoded = safe_str(password)
426 426 if new_hash and new_hash_cypher.hash_check(
427 427 password_encoded, new_hash):
428 428 cur_user = User.get_by_username(username)
429 429 cur_user.password = new_hash
430 430 Session().add(cur_user)
431 431 Session().flush()
432 432 log.info('Migrated user %s hash to bcrypt', cur_user)
433 433
434 434 def _validate_auth_return(self, ret):
435 435 if not isinstance(ret, dict):
436 436 raise Exception('returned value from auth must be a dict')
437 437 for k in self.auth_func_attrs:
438 438 if k not in ret:
439 439 raise Exception('Missing %s attribute from returned data' % k)
440 440 return ret
441 441
442 442 def get_ttl_cache(self, settings=None):
443 443 plugin_settings = settings or self.get_settings()
444 444 cache_ttl = 0
445 445
446 446 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
447 447 # plugin cache set inside is more important than the settings value
448 448 cache_ttl = self.AUTH_CACHE_TTL
449 449 elif plugin_settings.get('cache_ttl'):
450 450 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
451 451
452 452 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
453 453 return plugin_cache_active, cache_ttl
454 454
455 455
456 456 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
457 457
458 458 @hybrid_property
459 459 def allows_creating_users(self):
460 460 return True
461 461
462 462 def use_fake_password(self):
463 463 """
464 464 Return a boolean that indicates whether or not we should set the user's
465 465 password to a random value when it is authenticated by this plugin.
466 466 If your plugin provides authentication, then you will generally
467 467 want this.
468 468
469 469 :returns: boolean
470 470 """
471 471 raise NotImplementedError("Not implemented in base class")
472 472
473 473 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
474 474 # at this point _authenticate calls plugin's `auth()` function
475 475 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
476 476 userobj, username, passwd, settings, **kwargs)
477 477
478 478 if auth:
479 479 # maybe plugin will clean the username ?
480 480 # we should use the return value
481 481 username = auth['username']
482 482
483 483 # if external source tells us that user is not active, we should
484 484 # skip rest of the process. This can prevent from creating users in
485 485 # RhodeCode when using external authentication, but if it's
486 486 # inactive user we shouldn't create that user anyway
487 487 if auth['active_from_extern'] is False:
488 488 log.warning(
489 489 "User %s authenticated against %s, but is inactive",
490 490 username, self.__module__)
491 491 return None
492 492
493 493 cur_user = User.get_by_username(username, case_insensitive=True)
494 494 is_user_existing = cur_user is not None
495 495
496 496 if is_user_existing:
497 497 log.debug('Syncing user `%s` from '
498 498 '`%s` plugin', username, self.name)
499 499 else:
500 500 log.debug('Creating non existing user `%s` from '
501 501 '`%s` plugin', username, self.name)
502 502
503 503 if self.allows_creating_users:
504 504 log.debug('Plugin `%s` allows to '
505 505 'create new users', self.name)
506 506 else:
507 507 log.debug('Plugin `%s` does not allow to '
508 508 'create new users', self.name)
509 509
510 510 user_parameters = {
511 511 'username': username,
512 512 'email': auth["email"],
513 513 'firstname': auth["firstname"],
514 514 'lastname': auth["lastname"],
515 515 'active': auth["active"],
516 516 'admin': auth["admin"],
517 517 'extern_name': auth["extern_name"],
518 518 'extern_type': self.name,
519 519 'plugin': self,
520 520 'allow_to_create_user': self.allows_creating_users,
521 521 }
522 522
523 523 if not is_user_existing:
524 524 if self.use_fake_password():
525 525 # Randomize the PW because we don't need it, but don't want
526 526 # them blank either
527 527 passwd = PasswordGenerator().gen_password(length=16)
528 528 user_parameters['password'] = passwd
529 529 else:
530 530 # Since the password is required by create_or_update method of
531 531 # UserModel, we need to set it explicitly.
532 532 # The create_or_update method is smart and recognises the
533 533 # password hashes as well.
534 534 user_parameters['password'] = cur_user.password
535 535
536 536 # we either create or update users, we also pass the flag
537 537 # that controls if this method can actually do that.
538 538 # raises NotAllowedToCreateUserError if it cannot, and we try to.
539 539 user = UserModel().create_or_update(**user_parameters)
540 540 Session().flush()
541 541 # enforce user is just in given groups, all of them has to be ones
542 542 # created from plugins. We store this info in _group_data JSON
543 543 # field
544 544
545 545 if auth['user_group_sync']:
546 546 try:
547 547 groups = auth['groups'] or []
548 548 log.debug(
549 549 'Performing user_group sync based on set `%s` '
550 550 'returned by `%s` plugin', groups, self.name)
551 551 UserGroupModel().enforce_groups(user, groups, self.name)
552 552 except Exception:
553 553 # for any reason group syncing fails, we should
554 554 # proceed with login
555 555 log.error(traceback.format_exc())
556 556
557 557 Session().commit()
558 558 return auth
559 559
560 560
561 561 def loadplugin(plugin_id):
562 562 """
563 563 Loads and returns an instantiated authentication plugin.
564 564 Returns the RhodeCodeAuthPluginBase subclass on success,
565 565 or None on failure.
566 566 """
567 567 # TODO: Disusing pyramids thread locals to retrieve the registry.
568 568 authn_registry = get_authn_registry()
569 569 plugin = authn_registry.get_plugin(plugin_id)
570 570 if plugin is None:
571 571 log.error('Authentication plugin not found: "%s"', plugin_id)
572 572 return plugin
573 573
574 574
575 575 def get_authn_registry(registry=None):
576 576 registry = registry or get_current_registry()
577 577 authn_registry = registry.getUtility(IAuthnPluginRegistry)
578 578 return authn_registry
579 579
580 580
581 def get_auth_cache_manager(custom_ttl=None):
581 def get_auth_cache_manager(custom_ttl=None, suffix=None):
582 cache_name = 'rhodecode.authentication'
583 if suffix:
584 cache_name = 'rhodecode.authentication.{}'.format(suffix)
582 585 return caches.get_cache_manager(
583 'auth_plugins', 'rhodecode.authentication', custom_ttl)
586 'auth_plugins', cache_name, custom_ttl)
584 587
585 588
586 def get_perms_cache_manager(custom_ttl=None):
589 def get_perms_cache_manager(custom_ttl=None, suffix=None):
590 cache_name = 'rhodecode.permissions'
591 if suffix:
592 cache_name = 'rhodecode.permissions.{}'.format(suffix)
587 593 return caches.get_cache_manager(
588 'auth_plugins', 'rhodecode.permissions', custom_ttl)
594 'auth_plugins', cache_name, custom_ttl)
589 595
590 596
591 597 def authenticate(username, password, environ=None, auth_type=None,
592 598 skip_missing=False, registry=None, acl_repo_name=None):
593 599 """
594 600 Authentication function used for access control,
595 601 It tries to authenticate based on enabled authentication modules.
596 602
597 603 :param username: username can be empty for headers auth
598 604 :param password: password can be empty for headers auth
599 605 :param environ: environ headers passed for headers auth
600 606 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
601 607 :param skip_missing: ignores plugins that are in db but not in environment
602 608 :returns: None if auth failed, plugin_user dict if auth is correct
603 609 """
604 610 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
605 611 raise ValueError('auth type must be on of http, vcs got "%s" instead'
606 612 % auth_type)
607 613 headers_only = environ and not (username and password)
608 614
609 615 authn_registry = get_authn_registry(registry)
610 616 plugins_to_check = authn_registry.get_plugins_for_authentication()
611 617 log.debug('Starting ordered authentication chain using %s plugins',
612 618 plugins_to_check)
613 619 for plugin in plugins_to_check:
614 620 plugin.set_auth_type(auth_type)
615 621 plugin.set_calling_scope_repo(acl_repo_name)
616 622
617 623 if headers_only and not plugin.is_headers_auth:
618 624 log.debug('Auth type is for headers only and plugin `%s` is not '
619 625 'headers plugin, skipping...', plugin.get_id())
620 626 continue
621 627
622 628 # load plugin settings from RhodeCode database
623 629 plugin_settings = plugin.get_settings()
624 630 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
625 631 log.debug('Plugin settings:%s', plugin_sanitized_settings)
626 632
627 633 log.debug('Trying authentication using ** %s **', plugin.get_id())
628 634 # use plugin's method of user extraction.
629 635 user = plugin.get_user(username, environ=environ,
630 636 settings=plugin_settings)
631 637 display_user = user.username if user else username
632 638 log.debug(
633 639 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
634 640
635 641 if not plugin.allows_authentication_from(user):
636 642 log.debug('Plugin %s does not accept user `%s` for authentication',
637 643 plugin.get_id(), display_user)
638 644 continue
639 645 else:
640 646 log.debug('Plugin %s accepted user `%s` for authentication',
641 647 plugin.get_id(), display_user)
642 648
643 649 log.info('Authenticating user `%s` using %s plugin',
644 650 display_user, plugin.get_id())
645 651
646 652 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
647 653
648 654 # get instance of cache manager configured for a namespace
649 cache_manager = get_auth_cache_manager(custom_ttl=cache_ttl)
655 cache_manager = get_auth_cache_manager(
656 custom_ttl=cache_ttl, suffix=user.user_id)
650 657
651 658 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
652 659 plugin.get_id(), plugin_cache_active, cache_ttl)
653 660
654 661 # for environ based password can be empty, but then the validation is
655 662 # on the server that fills in the env data needed for authentication
656 663
657 664 _password_hash = caches.compute_key_from_params(
658 665 plugin.name, username, (password or ''))
659 666
660 667 # _authenticate is a wrapper for .auth() method of plugin.
661 668 # it checks if .auth() sends proper data.
662 669 # For RhodeCodeExternalAuthPlugin it also maps users to
663 670 # Database and maps the attributes returned from .auth()
664 671 # to RhodeCode database. If this function returns data
665 672 # then auth is correct.
666 673 start = time.time()
667 674 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
668 675
669 676 def auth_func():
670 677 """
671 678 This function is used internally in Cache of Beaker to calculate
672 679 Results
673 680 """
674 681 log.debug('auth: calculating password access now...')
675 682 return plugin._authenticate(
676 683 user, username, password, plugin_settings,
677 684 environ=environ or {})
678 685
679 686 if plugin_cache_active:
680 687 log.debug('Trying to fetch cached auth by `...%s`', _password_hash[:6])
681 688 plugin_user = cache_manager.get(
682 689 _password_hash, createfunc=auth_func)
683 690 else:
684 691 plugin_user = auth_func()
685 692
686 693 auth_time = time.time() - start
687 694 log.debug('Authentication for plugin `%s` completed in %.3fs, '
688 695 'expiration time of fetched cache %.1fs.',
689 696 plugin.get_id(), auth_time, cache_ttl)
690 697
691 698 log.debug('PLUGIN USER DATA: %s', plugin_user)
692 699
693 700 if plugin_user:
694 701 log.debug('Plugin returned proper authentication data')
695 702 return plugin_user
696 703 # we failed to Auth because .auth() method didn't return proper user
697 704 log.debug("User `%s` failed to authenticate against %s",
698 705 display_user, plugin.get_id())
699 706
700 707 # case when we failed to authenticate against all defined plugins
701 708 return None
702 709
703 710
704 711 def chop_at(s, sub, inclusive=False):
705 712 """Truncate string ``s`` at the first occurrence of ``sub``.
706 713
707 714 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
708 715
709 716 >>> chop_at("plutocratic brats", "rat")
710 717 'plutoc'
711 718 >>> chop_at("plutocratic brats", "rat", True)
712 719 'plutocrat'
713 720 """
714 721 pos = s.find(sub)
715 722 if pos == -1:
716 723 return s
717 724 if inclusive:
718 725 return s[:pos+len(sub)]
719 726 return s[:pos]
@@ -1,186 +1,191 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import formencode.htmlfill
23 23 import logging
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import BaseAppView
30 30 from rhodecode.authentication.base import (
31 31 get_auth_cache_manager, get_perms_cache_manager, get_authn_registry)
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 from rhodecode.lib.caches import clear_cache_manager
35 36 from rhodecode.model.forms import AuthSettingsForm
36 37 from rhodecode.model.meta import Session
37 38 from rhodecode.model.settings import SettingsModel
38 39
39 40 log = logging.getLogger(__name__)
40 41
41 42
42 43 class AuthnPluginViewBase(BaseAppView):
43 44
44 45 def load_default_context(self):
45 46 c = self._get_local_tmpl_context()
46 47 self.plugin = self.context.plugin
47 48 return c
48 49
49 50 @LoginRequired()
50 51 @HasPermissionAllDecorator('hg.admin')
51 52 def settings_get(self, defaults=None, errors=None):
52 53 """
53 54 View that displays the plugin settings as a form.
54 55 """
55 56 c = self.load_default_context()
56 57 defaults = defaults or {}
57 58 errors = errors or {}
58 59 schema = self.plugin.get_settings_schema()
59 60
60 61 # Compute default values for the form. Priority is:
61 62 # 1. Passed to this method 2. DB value 3. Schema default
62 63 for node in schema:
63 64 if node.name not in defaults:
64 65 defaults[node.name] = self.plugin.get_setting_by_name(
65 66 node.name, node.default, cache=False)
66 67
67 68 template_context = {
68 69 'defaults': defaults,
69 70 'errors': errors,
70 71 'plugin': self.context.plugin,
71 72 'resource': self.context,
72 73 }
73 74
74 75 return self._get_template_context(c, **template_context)
75 76
76 77 @LoginRequired()
77 78 @HasPermissionAllDecorator('hg.admin')
78 79 @CSRFRequired()
79 80 def settings_post(self):
80 81 """
81 82 View that validates and stores the plugin settings.
82 83 """
83 84 _ = self.request.translate
84 85 self.load_default_context()
85 86 schema = self.plugin.get_settings_schema()
86 87 data = self.request.params
87 88
88 89 try:
89 90 valid_data = schema.deserialize(data)
90 91 except colander.Invalid as e:
91 92 # Display error message and display form again.
92 93 h.flash(
93 94 _('Errors exist when saving plugin settings. '
94 95 'Please check the form inputs.'),
95 96 category='error')
96 97 defaults = {key: data[key] for key in data if key in schema}
97 98 return self.settings_get(errors=e.asdict(), defaults=defaults)
98 99
99 100 # Store validated data.
100 101 for name, value in valid_data.items():
101 102 self.plugin.create_or_update_setting(name, value)
102 103 Session().commit()
103 104
105 # cleanup cache managers in case of change for plugin
106 # TODO(marcink): because we can register multiple namespaces
107 # we should at some point figure out how to retrieve ALL namespace
108 # cache managers and clear them...
109 cache_manager = get_auth_cache_manager()
110 clear_cache_manager(cache_manager)
111
112 cache_manager = get_perms_cache_manager()
113 clear_cache_manager(cache_manager)
114
104 115 # Display success message and redirect.
105 116 h.flash(_('Auth settings updated successfully.'), category='success')
106 117 redirect_to = self.request.resource_path(
107 118 self.context, route_name='auth_home')
108 119 return HTTPFound(redirect_to)
109 120
110 121
111 122 class AuthSettingsView(BaseAppView):
112 123 def load_default_context(self):
113 124 c = self._get_local_tmpl_context()
114 125 return c
115 126
116 127 @LoginRequired()
117 128 @HasPermissionAllDecorator('hg.admin')
118 129 def index(self, defaults=None, errors=None, prefix_error=False):
119 130 c = self.load_default_context()
120 131
121 132 defaults = defaults or {}
122 133 authn_registry = get_authn_registry(self.request.registry)
123 134 enabled_plugins = SettingsModel().get_auth_plugins()
124 135
125 136 # Create template context and render it.
126 137 template_context = {
127 138 'resource': self.context,
128 139 'available_plugins': authn_registry.get_plugins(),
129 140 'enabled_plugins': enabled_plugins,
130 141 }
131 142 html = render('rhodecode:templates/admin/auth/auth_settings.mako',
132 143 self._get_template_context(c, **template_context),
133 144 self.request)
134 145
135 146 # Create form default values and fill the form.
136 147 form_defaults = {
137 148 'auth_plugins': ','.join(enabled_plugins)
138 149 }
139 150 form_defaults.update(defaults)
140 151 html = formencode.htmlfill.render(
141 152 html,
142 153 defaults=form_defaults,
143 154 errors=errors,
144 155 prefix_error=prefix_error,
145 156 encoding="UTF-8",
146 157 force_defaults=False)
147 158
148 159 return Response(html)
149 160
150 161 @LoginRequired()
151 162 @HasPermissionAllDecorator('hg.admin')
152 163 @CSRFRequired()
153 164 def auth_settings(self):
154 165 _ = self.request.translate
155 166 try:
156 167 form = AuthSettingsForm(self.request.translate)()
157 168 form_result = form.to_python(self.request.POST)
158 169 plugins = ','.join(form_result['auth_plugins'])
159 170 setting = SettingsModel().create_or_update_setting(
160 171 'auth_plugins', plugins)
161 172 Session().add(setting)
162 173 Session().commit()
163 174
164 cache_manager = get_auth_cache_manager()
165 cache_manager.clear()
166
167 cache_manager = get_perms_cache_manager()
168 cache_manager.clear()
169
170 175 h.flash(_('Auth settings updated successfully.'), category='success')
171 176 except formencode.Invalid as errors:
172 177 e = errors.error_dict or {}
173 178 h.flash(_('Errors exist when saving plugin setting. '
174 179 'Please check the form inputs.'), category='error')
175 180 return self.index(
176 181 defaults=errors.value,
177 182 errors=e,
178 183 prefix_error=False)
179 184 except Exception:
180 185 log.exception('Exception in auth_settings')
181 186 h.flash(_('Error occurred during update of auth settings.'),
182 187 category='error')
183 188
184 189 redirect_to = self.request.resource_path(
185 190 self.context, route_name='auth_home')
186 191 return HTTPFound(redirect_to)
@@ -1,644 +1,645 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2018 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 re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31
32 32 import time
33 33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 34 # TODO(marcink): check if we should use webob.exc here ?
35 35 from pyramid.httpexceptions import (
36 36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 37 from zope.cachedescriptors.property import Lazy as LazyProperty
38 38
39 39 import rhodecode
40 40 from rhodecode.authentication.base import (
41 41 authenticate, get_perms_cache_manager, VCS_TYPE, loadplugin)
42 42 from rhodecode.lib import caches
43 43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 44 from rhodecode.lib.base import (
45 45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 46 from rhodecode.lib.exceptions import (
47 47 HTTPLockedRC, HTTPRequirementError, UserCreationError,
48 48 NotAllowedToCreateUserError)
49 49 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
50 50 from rhodecode.lib.middleware import appenlight
51 51 from rhodecode.lib.middleware.utils import scm_app_http
52 52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
53 53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
54 54 from rhodecode.lib.vcs.conf import settings as vcs_settings
55 55 from rhodecode.lib.vcs.backends import base
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def initialize_generator(factory):
66 66 """
67 67 Initializes the returned generator by draining its first element.
68 68
69 69 This can be used to give a generator an initializer, which is the code
70 70 up to the first yield statement. This decorator enforces that the first
71 71 produced element has the value ``"__init__"`` to make its special
72 72 purpose very explicit in the using code.
73 73 """
74 74
75 75 @wraps(factory)
76 76 def wrapper(*args, **kwargs):
77 77 gen = factory(*args, **kwargs)
78 78 try:
79 79 init = gen.next()
80 80 except StopIteration:
81 81 raise ValueError('Generator must yield at least one element.')
82 82 if init != "__init__":
83 83 raise ValueError('First yielded element must be "__init__".')
84 84 return gen
85 85 return wrapper
86 86
87 87
88 88 class SimpleVCS(object):
89 89 """Common functionality for SCM HTTP handlers."""
90 90
91 91 SCM = 'unknown'
92 92
93 93 acl_repo_name = None
94 94 url_repo_name = None
95 95 vcs_repo_name = None
96 96 rc_extras = {}
97 97
98 98 # We have to handle requests to shadow repositories different than requests
99 99 # to normal repositories. Therefore we have to distinguish them. To do this
100 100 # we use this regex which will match only on URLs pointing to shadow
101 101 # repositories.
102 102 shadow_repo_re = re.compile(
103 103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
104 104 '(?P<target>{slug_pat})/' # target repo
105 105 'pull-request/(?P<pr_id>\d+)/' # pull request
106 106 'repository$' # shadow repo
107 107 .format(slug_pat=SLUG_RE.pattern))
108 108
109 109 def __init__(self, config, registry):
110 110 self.registry = registry
111 111 self.config = config
112 112 # re-populated by specialized middleware
113 113 self.repo_vcs_config = base.Config()
114 114 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
115 115
116 116 registry.rhodecode_settings = self.rhodecode_settings
117 117 # authenticate this VCS request using authfunc
118 118 auth_ret_code_detection = \
119 119 str2bool(self.config.get('auth_ret_code_detection', False))
120 120 self.authenticate = BasicAuth(
121 121 '', authenticate, registry, config.get('auth_ret_code'),
122 122 auth_ret_code_detection)
123 123 self.ip_addr = '0.0.0.0'
124 124
125 125 @LazyProperty
126 126 def global_vcs_config(self):
127 127 try:
128 128 return VcsSettingsModel().get_ui_settings_as_config_obj()
129 129 except Exception:
130 130 return base.Config()
131 131
132 132 @property
133 133 def base_path(self):
134 134 settings_path = self.repo_vcs_config.get(
135 135 *VcsSettingsModel.PATH_SETTING)
136 136
137 137 if not settings_path:
138 138 settings_path = self.global_vcs_config.get(
139 139 *VcsSettingsModel.PATH_SETTING)
140 140
141 141 if not settings_path:
142 142 # try, maybe we passed in explicitly as config option
143 143 settings_path = self.config.get('base_path')
144 144
145 145 if not settings_path:
146 146 raise ValueError('FATAL: base_path is empty')
147 147 return settings_path
148 148
149 149 def set_repo_names(self, environ):
150 150 """
151 151 This will populate the attributes acl_repo_name, url_repo_name,
152 152 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
153 153 shadow) repositories all names are equal. In case of requests to a
154 154 shadow repository the acl-name points to the target repo of the pull
155 155 request and the vcs-name points to the shadow repo file system path.
156 156 The url-name is always the URL used by the vcs client program.
157 157
158 158 Example in case of a shadow repo:
159 159 acl_repo_name = RepoGroup/MyRepo
160 160 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
161 161 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
162 162 """
163 163 # First we set the repo name from URL for all attributes. This is the
164 164 # default if handling normal (non shadow) repo requests.
165 165 self.url_repo_name = self._get_repository_name(environ)
166 166 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
167 167 self.is_shadow_repo = False
168 168
169 169 # Check if this is a request to a shadow repository.
170 170 match = self.shadow_repo_re.match(self.url_repo_name)
171 171 if match:
172 172 match_dict = match.groupdict()
173 173
174 174 # Build acl repo name from regex match.
175 175 acl_repo_name = safe_unicode('{groups}{target}'.format(
176 176 groups=match_dict['groups'] or '',
177 177 target=match_dict['target']))
178 178
179 179 # Retrieve pull request instance by ID from regex match.
180 180 pull_request = PullRequest.get(match_dict['pr_id'])
181 181
182 182 # Only proceed if we got a pull request and if acl repo name from
183 183 # URL equals the target repo name of the pull request.
184 184 if pull_request and (acl_repo_name ==
185 185 pull_request.target_repo.repo_name):
186 186 # Get file system path to shadow repository.
187 187 workspace_id = PullRequestModel()._workspace_id(pull_request)
188 188 target_vcs = pull_request.target_repo.scm_instance()
189 189 vcs_repo_name = target_vcs._get_shadow_repository_path(
190 190 workspace_id)
191 191
192 192 # Store names for later usage.
193 193 self.vcs_repo_name = vcs_repo_name
194 194 self.acl_repo_name = acl_repo_name
195 195 self.is_shadow_repo = True
196 196
197 197 log.debug('Setting all VCS repository names: %s', {
198 198 'acl_repo_name': self.acl_repo_name,
199 199 'url_repo_name': self.url_repo_name,
200 200 'vcs_repo_name': self.vcs_repo_name,
201 201 })
202 202
203 203 @property
204 204 def scm_app(self):
205 205 custom_implementation = self.config['vcs.scm_app_implementation']
206 206 if custom_implementation == 'http':
207 207 log.info('Using HTTP implementation of scm app.')
208 208 scm_app_impl = scm_app_http
209 209 else:
210 210 log.info('Using custom implementation of scm_app: "{}"'.format(
211 211 custom_implementation))
212 212 scm_app_impl = importlib.import_module(custom_implementation)
213 213 return scm_app_impl
214 214
215 215 def _get_by_id(self, repo_name):
216 216 """
217 217 Gets a special pattern _<ID> from clone url and tries to replace it
218 218 with a repository_name for support of _<ID> non changeable urls
219 219 """
220 220
221 221 data = repo_name.split('/')
222 222 if len(data) >= 2:
223 223 from rhodecode.model.repo import RepoModel
224 224 by_id_match = RepoModel().get_repo_by_id(repo_name)
225 225 if by_id_match:
226 226 data[1] = by_id_match.repo_name
227 227
228 228 return safe_str('/'.join(data))
229 229
230 230 def _invalidate_cache(self, repo_name):
231 231 """
232 232 Set's cache for this repository for invalidation on next access
233 233
234 234 :param repo_name: full repo name, also a cache key
235 235 """
236 236 ScmModel().mark_for_invalidation(repo_name)
237 237
238 238 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
239 239 db_repo = Repository.get_by_repo_name(repo_name)
240 240 if not db_repo:
241 241 log.debug('Repository `%s` not found inside the database.',
242 242 repo_name)
243 243 return False
244 244
245 245 if db_repo.repo_type != scm_type:
246 246 log.warning(
247 247 'Repository `%s` have incorrect scm_type, expected %s got %s',
248 248 repo_name, db_repo.repo_type, scm_type)
249 249 return False
250 250
251 251 config = db_repo._config
252 252 config.set('extensions', 'largefiles', '')
253 253 return is_valid_repo(
254 254 repo_name, base_path,
255 255 explicit_scm=scm_type, expect_scm=scm_type, config=config)
256 256
257 257 def valid_and_active_user(self, user):
258 258 """
259 259 Checks if that user is not empty, and if it's actually object it checks
260 260 if he's active.
261 261
262 262 :param user: user object or None
263 263 :return: boolean
264 264 """
265 265 if user is None:
266 266 return False
267 267
268 268 elif user.active:
269 269 return True
270 270
271 271 return False
272 272
273 273 @property
274 274 def is_shadow_repo_dir(self):
275 275 return os.path.isdir(self.vcs_repo_name)
276 276
277 277 def _check_permission(self, action, user, repo_name, ip_addr=None,
278 278 plugin_id='', plugin_cache_active=False, cache_ttl=0):
279 279 """
280 280 Checks permissions using action (push/pull) user and repository
281 281 name. If plugin_cache and ttl is set it will use the plugin which
282 282 authenticated the user to store the cached permissions result for N
283 283 amount of seconds as in cache_ttl
284 284
285 285 :param action: push or pull action
286 286 :param user: user instance
287 287 :param repo_name: repository name
288 288 """
289 289
290 290 # get instance of cache manager configured for a namespace
291 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
291 cache_manager = get_perms_cache_manager(
292 custom_ttl=cache_ttl, suffix=user.user_id)
292 293 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
293 294 plugin_id, plugin_cache_active, cache_ttl)
294 295
295 296 # for environ based password can be empty, but then the validation is
296 297 # on the server that fills in the env data needed for authentication
297 298 _perm_calc_hash = caches.compute_key_from_params(
298 299 plugin_id, action, user.user_id, repo_name, ip_addr)
299 300
300 301 # _authenticate is a wrapper for .auth() method of plugin.
301 302 # it checks if .auth() sends proper data.
302 303 # For RhodeCodeExternalAuthPlugin it also maps users to
303 304 # Database and maps the attributes returned from .auth()
304 305 # to RhodeCode database. If this function returns data
305 306 # then auth is correct.
306 307 start = time.time()
307 308 log.debug('Running plugin `%s` permissions check', plugin_id)
308 309
309 310 def perm_func():
310 311 """
311 312 This function is used internally in Cache of Beaker to calculate
312 313 Results
313 314 """
314 315 log.debug('auth: calculating permission access now...')
315 316 # check IP
316 317 inherit = user.inherit_default_permissions
317 318 ip_allowed = AuthUser.check_ip_allowed(
318 319 user.user_id, ip_addr, inherit_from_default=inherit)
319 320 if ip_allowed:
320 321 log.info('Access for IP:%s allowed', ip_addr)
321 322 else:
322 323 return False
323 324
324 325 if action == 'push':
325 326 perms = ('repository.write', 'repository.admin')
326 327 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
327 328 return False
328 329
329 330 else:
330 331 # any other action need at least read permission
331 332 perms = (
332 333 'repository.read', 'repository.write', 'repository.admin')
333 334 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
334 335 return False
335 336
336 337 return True
337 338
338 339 if plugin_cache_active:
339 340 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
340 341 perm_result = cache_manager.get(
341 342 _perm_calc_hash, createfunc=perm_func)
342 343 else:
343 344 perm_result = perm_func()
344 345
345 346 auth_time = time.time() - start
346 347 log.debug('Permissions for plugin `%s` completed in %.3fs, '
347 348 'expiration time of fetched cache %.1fs.',
348 349 plugin_id, auth_time, cache_ttl)
349 350
350 351 return perm_result
351 352
352 353 def _check_ssl(self, environ, start_response):
353 354 """
354 355 Checks the SSL check flag and returns False if SSL is not present
355 356 and required True otherwise
356 357 """
357 358 org_proto = environ['wsgi._org_proto']
358 359 # check if we have SSL required ! if not it's a bad request !
359 360 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
360 361 if require_ssl and org_proto == 'http':
361 362 log.debug('proto is %s and SSL is required BAD REQUEST !',
362 363 org_proto)
363 364 return False
364 365 return True
365 366
366 367 def _get_default_cache_ttl(self):
367 368 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
368 369 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
369 370 plugin_settings = plugin.get_settings()
370 371 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
371 372 plugin_settings) or (False, 0)
372 373 return plugin_cache_active, cache_ttl
373 374
374 375 def __call__(self, environ, start_response):
375 376 try:
376 377 return self._handle_request(environ, start_response)
377 378 except Exception:
378 379 log.exception("Exception while handling request")
379 380 appenlight.track_exception(environ)
380 381 return HTTPInternalServerError()(environ, start_response)
381 382 finally:
382 383 meta.Session.remove()
383 384
384 385 def _handle_request(self, environ, start_response):
385 386
386 387 if not self._check_ssl(environ, start_response):
387 388 reason = ('SSL required, while RhodeCode was unable '
388 389 'to detect this as SSL request')
389 390 log.debug('User not allowed to proceed, %s', reason)
390 391 return HTTPNotAcceptable(reason)(environ, start_response)
391 392
392 393 if not self.url_repo_name:
393 394 log.warning('Repository name is empty: %s', self.url_repo_name)
394 395 # failed to get repo name, we fail now
395 396 return HTTPNotFound()(environ, start_response)
396 397 log.debug('Extracted repo name is %s', self.url_repo_name)
397 398
398 399 ip_addr = get_ip_addr(environ)
399 400 user_agent = get_user_agent(environ)
400 401 username = None
401 402
402 403 # skip passing error to error controller
403 404 environ['pylons.status_code_redirect'] = True
404 405
405 406 # ======================================================================
406 407 # GET ACTION PULL or PUSH
407 408 # ======================================================================
408 409 action = self._get_action(environ)
409 410
410 411 # ======================================================================
411 412 # Check if this is a request to a shadow repository of a pull request.
412 413 # In this case only pull action is allowed.
413 414 # ======================================================================
414 415 if self.is_shadow_repo and action != 'pull':
415 416 reason = 'Only pull action is allowed for shadow repositories.'
416 417 log.debug('User not allowed to proceed, %s', reason)
417 418 return HTTPNotAcceptable(reason)(environ, start_response)
418 419
419 420 # Check if the shadow repo actually exists, in case someone refers
420 421 # to it, and it has been deleted because of successful merge.
421 422 if self.is_shadow_repo and not self.is_shadow_repo_dir:
422 423 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
423 424 self.is_shadow_repo_dir)
424 425 return HTTPNotFound()(environ, start_response)
425 426
426 427 # ======================================================================
427 428 # CHECK ANONYMOUS PERMISSION
428 429 # ======================================================================
429 430 if action in ['pull', 'push']:
430 431 anonymous_user = User.get_default_user()
431 432 username = anonymous_user.username
432 433 if anonymous_user.active:
433 434 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
434 435 # ONLY check permissions if the user is activated
435 436 anonymous_perm = self._check_permission(
436 437 action, anonymous_user, self.acl_repo_name, ip_addr,
437 438 plugin_id='anonymous_access',
438 439 plugin_cache_active=plugin_cache_active, cache_ttl=cache_ttl,
439 440 )
440 441 else:
441 442 anonymous_perm = False
442 443
443 444 if not anonymous_user.active or not anonymous_perm:
444 445 if not anonymous_user.active:
445 446 log.debug('Anonymous access is disabled, running '
446 447 'authentication')
447 448
448 449 if not anonymous_perm:
449 450 log.debug('Not enough credentials to access this '
450 451 'repository as anonymous user')
451 452
452 453 username = None
453 454 # ==============================================================
454 455 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
455 456 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
456 457 # ==============================================================
457 458
458 459 # try to auth based on environ, container auth methods
459 460 log.debug('Running PRE-AUTH for container based authentication')
460 461 pre_auth = authenticate(
461 462 '', '', environ, VCS_TYPE, registry=self.registry,
462 463 acl_repo_name=self.acl_repo_name)
463 464 if pre_auth and pre_auth.get('username'):
464 465 username = pre_auth['username']
465 466 log.debug('PRE-AUTH got %s as username', username)
466 467 if pre_auth:
467 468 log.debug('PRE-AUTH successful from %s',
468 469 pre_auth.get('auth_data', {}).get('_plugin'))
469 470
470 471 # If not authenticated by the container, running basic auth
471 472 # before inject the calling repo_name for special scope checks
472 473 self.authenticate.acl_repo_name = self.acl_repo_name
473 474
474 475 plugin_cache_active, cache_ttl = False, 0
475 476 plugin = None
476 477 if not username:
477 478 self.authenticate.realm = self.authenticate.get_rc_realm()
478 479
479 480 try:
480 481 auth_result = self.authenticate(environ)
481 482 except (UserCreationError, NotAllowedToCreateUserError) as e:
482 483 log.error(e)
483 484 reason = safe_str(e)
484 485 return HTTPNotAcceptable(reason)(environ, start_response)
485 486
486 487 if isinstance(auth_result, dict):
487 488 AUTH_TYPE.update(environ, 'basic')
488 489 REMOTE_USER.update(environ, auth_result['username'])
489 490 username = auth_result['username']
490 491 plugin = auth_result.get('auth_data', {}).get('_plugin')
491 492 log.info(
492 493 'MAIN-AUTH successful for user `%s` from %s plugin',
493 494 username, plugin)
494 495
495 496 plugin_cache_active, cache_ttl = auth_result.get(
496 497 'auth_data', {}).get('_ttl_cache') or (False, 0)
497 498 else:
498 499 return auth_result.wsgi_application(
499 500 environ, start_response)
500 501
501 502
502 503 # ==============================================================
503 504 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
504 505 # ==============================================================
505 506 user = User.get_by_username(username)
506 507 if not self.valid_and_active_user(user):
507 508 return HTTPForbidden()(environ, start_response)
508 509 username = user.username
509 510 user.update_lastactivity()
510 511 meta.Session().commit()
511 512
512 513 # check user attributes for password change flag
513 514 user_obj = user
514 515 if user_obj and user_obj.username != User.DEFAULT_USER and \
515 516 user_obj.user_data.get('force_password_change'):
516 517 reason = 'password change required'
517 518 log.debug('User not allowed to authenticate, %s', reason)
518 519 return HTTPNotAcceptable(reason)(environ, start_response)
519 520
520 521 # check permissions for this repository
521 522 perm = self._check_permission(
522 523 action, user, self.acl_repo_name, ip_addr,
523 524 plugin, plugin_cache_active, cache_ttl)
524 525 if not perm:
525 526 return HTTPForbidden()(environ, start_response)
526 527
527 528 # extras are injected into UI object and later available
528 529 # in hooks executed by RhodeCode
529 530 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
530 531 extras = vcs_operation_context(
531 532 environ, repo_name=self.acl_repo_name, username=username,
532 533 action=action, scm=self.SCM, check_locking=check_locking,
533 534 is_shadow_repo=self.is_shadow_repo
534 535 )
535 536
536 537 # ======================================================================
537 538 # REQUEST HANDLING
538 539 # ======================================================================
539 540 repo_path = os.path.join(
540 541 safe_str(self.base_path), safe_str(self.vcs_repo_name))
541 542 log.debug('Repository path is %s', repo_path)
542 543
543 544 fix_PATH()
544 545
545 546 log.info(
546 547 '%s action on %s repo "%s" by "%s" from %s %s',
547 548 action, self.SCM, safe_str(self.url_repo_name),
548 549 safe_str(username), ip_addr, user_agent)
549 550
550 551 return self._generate_vcs_response(
551 552 environ, start_response, repo_path, extras, action)
552 553
553 554 @initialize_generator
554 555 def _generate_vcs_response(
555 556 self, environ, start_response, repo_path, extras, action):
556 557 """
557 558 Returns a generator for the response content.
558 559
559 560 This method is implemented as a generator, so that it can trigger
560 561 the cache validation after all content sent back to the client. It
561 562 also handles the locking exceptions which will be triggered when
562 563 the first chunk is produced by the underlying WSGI application.
563 564 """
564 565 callback_daemon, extras = self._prepare_callback_daemon(extras)
565 566 config = self._create_config(extras, self.acl_repo_name)
566 567 log.debug('HOOKS extras is %s', extras)
567 568 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
568 569 app.rc_extras = extras
569 570
570 571 try:
571 572 with callback_daemon:
572 573 try:
573 574 response = app(environ, start_response)
574 575 finally:
575 576 # This statement works together with the decorator
576 577 # "initialize_generator" above. The decorator ensures that
577 578 # we hit the first yield statement before the generator is
578 579 # returned back to the WSGI server. This is needed to
579 580 # ensure that the call to "app" above triggers the
580 581 # needed callback to "start_response" before the
581 582 # generator is actually used.
582 583 yield "__init__"
583 584
584 585 for chunk in response:
585 586 yield chunk
586 587 except Exception as exc:
587 588 # TODO: martinb: Exceptions are only raised in case of the Pyro4
588 589 # backend. Refactor this except block after dropping Pyro4 support.
589 590 # TODO: johbo: Improve "translating" back the exception.
590 591 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
591 592 exc = HTTPLockedRC(*exc.args)
592 593 _code = rhodecode.CONFIG.get('lock_ret_code')
593 594 log.debug('Repository LOCKED ret code %s!', (_code,))
594 595 elif getattr(exc, '_vcs_kind', None) == 'requirement':
595 596 log.debug(
596 597 'Repository requires features unknown to this Mercurial')
597 598 exc = HTTPRequirementError(*exc.args)
598 599 else:
599 600 raise
600 601
601 602 for chunk in exc(environ, start_response):
602 603 yield chunk
603 604 finally:
604 605 # invalidate cache on push
605 606 try:
606 607 if action == 'push':
607 608 self._invalidate_cache(self.url_repo_name)
608 609 finally:
609 610 meta.Session.remove()
610 611
611 612 def _get_repository_name(self, environ):
612 613 """Get repository name out of the environmnent
613 614
614 615 :param environ: WSGI environment
615 616 """
616 617 raise NotImplementedError()
617 618
618 619 def _get_action(self, environ):
619 620 """Map request commands into a pull or push command.
620 621
621 622 :param environ: WSGI environment
622 623 """
623 624 raise NotImplementedError()
624 625
625 626 def _create_wsgi_app(self, repo_path, repo_name, config):
626 627 """Return the WSGI app that will finally handle the request."""
627 628 raise NotImplementedError()
628 629
629 630 def _create_config(self, extras, repo_name):
630 631 """Create a safe config representation."""
631 632 raise NotImplementedError()
632 633
633 634 def _prepare_callback_daemon(self, extras):
634 635 return prepare_callback_daemon(
635 636 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
636 637 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
637 638
638 639
639 640 def _should_check_locking(query_string):
640 641 # this is kind of hacky, but due to how mercurial handles client-server
641 642 # server see all operation on commit; bookmarks, phases and
642 643 # obsolescence marker in different transaction, we don't want to check
643 644 # locking on those
644 645 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now