##// END OF EJS Templates
authn: Rename 'container' -> 'headers' in authentication.
johbo -
r106:5d16fb96 default
parent child Browse files
Show More
@@ -1,602 +1,598 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 logging
26 26 import time
27 27 import traceback
28 28
29 29 from pyramid.threadlocal import get_current_registry
30 30 from sqlalchemy.ext.hybrid import hybrid_property
31 31
32 32 from rhodecode.authentication.interface import IAuthnPluginRegistry
33 33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 34 from rhodecode.lib import caches
35 35 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
36 36 from rhodecode.lib.utils2 import md5_safe, safe_int
37 37 from rhodecode.lib.utils2 import safe_str
38 38 from rhodecode.model.db import User
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.settings import SettingsModel
41 41 from rhodecode.model.user import UserModel
42 42 from rhodecode.model.user_group import UserGroupModel
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 # auth types that authenticate() function can receive
48 48 VCS_TYPE = 'vcs'
49 49 HTTP_TYPE = 'http'
50 50
51 51
52 52 class LazyFormencode(object):
53 53 def __init__(self, formencode_obj, *args, **kwargs):
54 54 self.formencode_obj = formencode_obj
55 55 self.args = args
56 56 self.kwargs = kwargs
57 57
58 58 def __call__(self, *args, **kwargs):
59 59 from inspect import isfunction
60 60 formencode_obj = self.formencode_obj
61 61 if isfunction(formencode_obj):
62 62 # case we wrap validators into functions
63 63 formencode_obj = self.formencode_obj(*args, **kwargs)
64 64 return formencode_obj(*self.args, **self.kwargs)
65 65
66 66
67 67 class RhodeCodeAuthPluginBase(object):
68 68 # cache the authentication request for N amount of seconds. Some kind
69 69 # of authentication methods are very heavy and it's very efficient to cache
70 70 # the result of a call. If it's set to None (default) cache is off
71 71 AUTH_CACHE_TTL = None
72 72 AUTH_CACHE = {}
73 73
74 74 auth_func_attrs = {
75 75 "username": "unique username",
76 76 "firstname": "first name",
77 77 "lastname": "last name",
78 78 "email": "email address",
79 79 "groups": '["list", "of", "groups"]',
80 80 "extern_name": "name in external source of record",
81 81 "extern_type": "type of external source of record",
82 82 "admin": 'True|False defines if user should be RhodeCode super admin',
83 83 "active":
84 84 'True|False defines active state of user internally for RhodeCode',
85 85 "active_from_extern":
86 86 "True|False\None, active state from the external auth, "
87 87 "None means use definition from RhodeCode extern_type active value"
88 88 }
89 89 # set on authenticate() method and via set_auth_type func.
90 90 auth_type = None
91 91
92 92 # List of setting names to store encrypted. Plugins may override this list
93 93 # to store settings encrypted.
94 94 _settings_encrypted = []
95 95
96 96 # Mapping of python to DB settings model types. Plugins may override or
97 97 # extend this mapping.
98 98 _settings_type_map = {
99 99 str: 'str',
100 100 int: 'int',
101 101 unicode: 'unicode',
102 102 bool: 'bool',
103 103 list: 'list',
104 104 }
105 105
106 106 def __init__(self, plugin_id):
107 107 self._plugin_id = plugin_id
108 108
109 109 def _get_setting_full_name(self, name):
110 110 """
111 111 Return the full setting name used for storing values in the database.
112 112 """
113 113 # TODO: johbo: Using the name here is problematic. It would be good to
114 114 # introduce either new models in the database to hold Plugin and
115 115 # PluginSetting or to use the plugin id here.
116 116 return 'auth_{}_{}'.format(self.name, name)
117 117
118 118 def _get_setting_type(self, name, value):
119 119 """
120 120 Get the type as used by the SettingsModel accordingly to type of passed
121 121 value. Optionally the suffix `.encrypted` is appended to instruct
122 122 SettingsModel to store it encrypted.
123 123 """
124 124 type_ = self._settings_type_map.get(type(value), 'unicode')
125 125 if name in self._settings_encrypted:
126 126 type_ = '{}.encrypted'.format(type_)
127 127 return type_
128 128
129 129 def is_enabled(self):
130 130 """
131 131 Returns true if this plugin is enabled. An enabled plugin can be
132 132 configured in the admin interface but it is not consulted during
133 133 authentication.
134 134 """
135 135 auth_plugins = SettingsModel().get_auth_plugins()
136 136 return self.get_id() in auth_plugins
137 137
138 138 def is_active(self):
139 139 """
140 140 Returns true if the plugin is activated. An activated plugin is
141 141 consulted during authentication, assumed it is also enabled.
142 142 """
143 143 return self.get_setting_by_name('enabled')
144 144
145 145 def get_id(self):
146 146 """
147 147 Returns the plugin id.
148 148 """
149 149 return self._plugin_id
150 150
151 151 def get_display_name(self):
152 152 """
153 153 Returns a translation string for displaying purposes.
154 154 """
155 155 raise NotImplementedError('Not implemented in base class')
156 156
157 157 def get_settings_schema(self):
158 158 """
159 159 Returns a colander schema, representing the plugin settings.
160 160 """
161 161 return AuthnPluginSettingsSchemaBase()
162 162
163 163 def get_setting_by_name(self, name):
164 164 """
165 165 Returns a plugin setting by name.
166 166 """
167 167 full_name = self._get_setting_full_name(name)
168 168 db_setting = SettingsModel().get_setting_by_name(full_name)
169 169 return db_setting.app_settings_value if db_setting else None
170 170
171 171 def create_or_update_setting(self, name, value):
172 172 """
173 173 Create or update a setting for this plugin in the persistent storage.
174 174 """
175 175 full_name = self._get_setting_full_name(name)
176 176 type_ = self._get_setting_type(name, value)
177 177 db_setting = SettingsModel().create_or_update_setting(
178 178 full_name, value, type_)
179 179 return db_setting.app_settings_value
180 180
181 181 def get_settings(self):
182 182 """
183 183 Returns the plugin settings as dictionary.
184 184 """
185 185 settings = {}
186 186 for node in self.get_settings_schema():
187 187 settings[node.name] = self.get_setting_by_name(node.name)
188 188 return settings
189 189
190 190 @property
191 191 def validators(self):
192 192 """
193 193 Exposes RhodeCode validators modules
194 194 """
195 195 # this is a hack to overcome issues with pylons threadlocals and
196 196 # translator object _() not beein registered properly.
197 197 class LazyCaller(object):
198 198 def __init__(self, name):
199 199 self.validator_name = name
200 200
201 201 def __call__(self, *args, **kwargs):
202 202 from rhodecode.model import validators as v
203 203 obj = getattr(v, self.validator_name)
204 204 # log.debug('Initializing lazy formencode object: %s', obj)
205 205 return LazyFormencode(obj, *args, **kwargs)
206 206
207 207 class ProxyGet(object):
208 208 def __getattribute__(self, name):
209 209 return LazyCaller(name)
210 210
211 211 return ProxyGet()
212 212
213 213 @hybrid_property
214 214 def name(self):
215 215 """
216 216 Returns the name of this authentication plugin.
217 217
218 218 :returns: string
219 219 """
220 220 raise NotImplementedError("Not implemented in base class")
221 221
222 222 @hybrid_property
223 223 def is_container_auth(self):
224 224 """
225 Returns bool if this module uses container auth.
226
227 This property will trigger an automatic call to authenticate on
228 a visit to the website or during a push/pull.
229
230 :returns: bool
225 Deprecated method that indicates if this authentication plugin uses
226 HTTP headers as authentication method.
231 227 """
232 228 return False
233 229
234 230 @hybrid_property
235 231 def allows_creating_users(self):
236 232 """
237 233 Defines if Plugin allows users to be created on-the-fly when
238 234 authentication is called. Controls how external plugins should behave
239 235 in terms if they are allowed to create new users, or not. Base plugins
240 236 should not be allowed to, but External ones should be !
241 237
242 238 :return: bool
243 239 """
244 240 return False
245 241
246 242 def set_auth_type(self, auth_type):
247 243 self.auth_type = auth_type
248 244
249 245 def allows_authentication_from(
250 246 self, user, allows_non_existing_user=True,
251 247 allowed_auth_plugins=None, allowed_auth_sources=None):
252 248 """
253 249 Checks if this authentication module should accept a request for
254 250 the current user.
255 251
256 252 :param user: user object fetched using plugin's get_user() method.
257 253 :param allows_non_existing_user: if True, don't allow the
258 254 user to be empty, meaning not existing in our database
259 255 :param allowed_auth_plugins: if provided, users extern_type will be
260 256 checked against a list of provided extern types, which are plugin
261 257 auth_names in the end
262 258 :param allowed_auth_sources: authentication type allowed,
263 259 `http` or `vcs` default is both.
264 260 defines if plugin will accept only http authentication vcs
265 261 authentication(git/hg) or both
266 262 :returns: boolean
267 263 """
268 264 if not user and not allows_non_existing_user:
269 265 log.debug('User is empty but plugin does not allow empty users,'
270 266 'not allowed to authenticate')
271 267 return False
272 268
273 269 expected_auth_plugins = allowed_auth_plugins or [self.name]
274 270 if user and (user.extern_type and
275 271 user.extern_type not in expected_auth_plugins):
276 272 log.debug(
277 273 'User `%s` is bound to `%s` auth type. Plugin allows only '
278 274 '%s, skipping', user, user.extern_type, expected_auth_plugins)
279 275
280 276 return False
281 277
282 278 # by default accept both
283 279 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
284 280 if self.auth_type not in expected_auth_from:
285 281 log.debug('Current auth source is %s but plugin only allows %s',
286 282 self.auth_type, expected_auth_from)
287 283 return False
288 284
289 285 return True
290 286
291 287 def get_user(self, username=None, **kwargs):
292 288 """
293 289 Helper method for user fetching in plugins, by default it's using
294 290 simple fetch by username, but this method can be custimized in plugins
295 eg. container auth plugin to fetch user by environ params
291 eg. headers auth plugin to fetch user by environ params
296 292
297 293 :param username: username if given to fetch from database
298 294 :param kwargs: extra arguments needed for user fetching.
299 295 """
300 296 user = None
301 297 log.debug(
302 298 'Trying to fetch user `%s` from RhodeCode database', username)
303 299 if username:
304 300 user = User.get_by_username(username)
305 301 if not user:
306 302 log.debug('User not found, fallback to fetch user in '
307 303 'case insensitive mode')
308 304 user = User.get_by_username(username, case_insensitive=True)
309 305 else:
310 306 log.debug('provided username:`%s` is empty skipping...', username)
311 307 if not user:
312 308 log.debug('User `%s` not found in database', username)
313 309 return user
314 310
315 311 def user_activation_state(self):
316 312 """
317 313 Defines user activation state when creating new users
318 314
319 315 :returns: boolean
320 316 """
321 317 raise NotImplementedError("Not implemented in base class")
322 318
323 319 def auth(self, userobj, username, passwd, settings, **kwargs):
324 320 """
325 321 Given a user object (which may be null), username, a plaintext
326 322 password, and a settings object (containing all the keys needed as
327 323 listed in settings()), authenticate this user's login attempt.
328 324
329 325 Return None on failure. On success, return a dictionary of the form:
330 326
331 327 see: RhodeCodeAuthPluginBase.auth_func_attrs
332 328 This is later validated for correctness
333 329 """
334 330 raise NotImplementedError("not implemented in base class")
335 331
336 332 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
337 333 """
338 334 Wrapper to call self.auth() that validates call on it
339 335
340 336 :param userobj: userobj
341 337 :param username: username
342 338 :param passwd: plaintext password
343 339 :param settings: plugin settings
344 340 """
345 341 auth = self.auth(userobj, username, passwd, settings, **kwargs)
346 342 if auth:
347 343 # check if hash should be migrated ?
348 344 new_hash = auth.get('_hash_migrate')
349 345 if new_hash:
350 346 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
351 347 return self._validate_auth_return(auth)
352 348 return auth
353 349
354 350 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
355 351 new_hash_cypher = _RhodeCodeCryptoBCrypt()
356 352 # extra checks, so make sure new hash is correct.
357 353 password_encoded = safe_str(password)
358 354 if new_hash and new_hash_cypher.hash_check(
359 355 password_encoded, new_hash):
360 356 cur_user = User.get_by_username(username)
361 357 cur_user.password = new_hash
362 358 Session().add(cur_user)
363 359 Session().flush()
364 360 log.info('Migrated user %s hash to bcrypt', cur_user)
365 361
366 362 def _validate_auth_return(self, ret):
367 363 if not isinstance(ret, dict):
368 364 raise Exception('returned value from auth must be a dict')
369 365 for k in self.auth_func_attrs:
370 366 if k not in ret:
371 367 raise Exception('Missing %s attribute from returned data' % k)
372 368 return ret
373 369
374 370
375 371 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
376 372
377 373 @hybrid_property
378 374 def allows_creating_users(self):
379 375 return True
380 376
381 377 def use_fake_password(self):
382 378 """
383 379 Return a boolean that indicates whether or not we should set the user's
384 380 password to a random value when it is authenticated by this plugin.
385 381 If your plugin provides authentication, then you will generally
386 382 want this.
387 383
388 384 :returns: boolean
389 385 """
390 386 raise NotImplementedError("Not implemented in base class")
391 387
392 388 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
393 389 # at this point _authenticate calls plugin's `auth()` function
394 390 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
395 391 userobj, username, passwd, settings, **kwargs)
396 392 if auth:
397 393 # maybe plugin will clean the username ?
398 394 # we should use the return value
399 395 username = auth['username']
400 396
401 397 # if external source tells us that user is not active, we should
402 398 # skip rest of the process. This can prevent from creating users in
403 399 # RhodeCode when using external authentication, but if it's
404 400 # inactive user we shouldn't create that user anyway
405 401 if auth['active_from_extern'] is False:
406 402 log.warning(
407 403 "User %s authenticated against %s, but is inactive",
408 404 username, self.__module__)
409 405 return None
410 406
411 407 cur_user = User.get_by_username(username, case_insensitive=True)
412 408 is_user_existing = cur_user is not None
413 409
414 410 if is_user_existing:
415 411 log.debug('Syncing user `%s` from '
416 412 '`%s` plugin', username, self.name)
417 413 else:
418 414 log.debug('Creating non existing user `%s` from '
419 415 '`%s` plugin', username, self.name)
420 416
421 417 if self.allows_creating_users:
422 418 log.debug('Plugin `%s` allows to '
423 419 'create new users', self.name)
424 420 else:
425 421 log.debug('Plugin `%s` does not allow to '
426 422 'create new users', self.name)
427 423
428 424 user_parameters = {
429 425 'username': username,
430 426 'email': auth["email"],
431 427 'firstname': auth["firstname"],
432 428 'lastname': auth["lastname"],
433 429 'active': auth["active"],
434 430 'admin': auth["admin"],
435 431 'extern_name': auth["extern_name"],
436 432 'extern_type': self.name,
437 433 'plugin': self,
438 434 'allow_to_create_user': self.allows_creating_users,
439 435 }
440 436
441 437 if not is_user_existing:
442 438 if self.use_fake_password():
443 439 # Randomize the PW because we don't need it, but don't want
444 440 # them blank either
445 441 passwd = PasswordGenerator().gen_password(length=16)
446 442 user_parameters['password'] = passwd
447 443 else:
448 444 # Since the password is required by create_or_update method of
449 445 # UserModel, we need to set it explicitly.
450 446 # The create_or_update method is smart and recognises the
451 447 # password hashes as well.
452 448 user_parameters['password'] = cur_user.password
453 449
454 450 # we either create or update users, we also pass the flag
455 451 # that controls if this method can actually do that.
456 452 # raises NotAllowedToCreateUserError if it cannot, and we try to.
457 453 user = UserModel().create_or_update(**user_parameters)
458 454 Session().flush()
459 455 # enforce user is just in given groups, all of them has to be ones
460 456 # created from plugins. We store this info in _group_data JSON
461 457 # field
462 458 try:
463 459 groups = auth['groups'] or []
464 460 UserGroupModel().enforce_groups(user, groups, self.name)
465 461 except Exception:
466 462 # for any reason group syncing fails, we should
467 463 # proceed with login
468 464 log.error(traceback.format_exc())
469 465 Session().commit()
470 466 return auth
471 467
472 468
473 469 def loadplugin(plugin_id):
474 470 """
475 471 Loads and returns an instantiated authentication plugin.
476 472 Returns the RhodeCodeAuthPluginBase subclass on success,
477 473 or None on failure.
478 474 """
479 475 # TODO: Disusing pyramids thread locals to retrieve the registry.
480 476 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
481 477 plugin = authn_registry.get_plugin(plugin_id)
482 478 if plugin is None:
483 479 log.error('Authentication plugin not found: "%s"', plugin_id)
484 480 return plugin
485 481
486 482
487 483 def get_auth_cache_manager(custom_ttl=None):
488 484 return caches.get_cache_manager(
489 485 'auth_plugins', 'rhodecode.authentication', custom_ttl)
490 486
491 487
492 488 def authenticate(username, password, environ=None, auth_type=None,
493 489 skip_missing=False):
494 490 """
495 491 Authentication function used for access control,
496 492 It tries to authenticate based on enabled authentication modules.
497 493
498 :param username: username can be empty for container auth
499 :param password: password can be empty for container auth
500 :param environ: environ headers passed for container auth
494 :param username: username can be empty for headers auth
495 :param password: password can be empty for headers auth
496 :param environ: environ headers passed for headers auth
501 497 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
502 498 :param skip_missing: ignores plugins that are in db but not in environment
503 499 :returns: None if auth failed, plugin_user dict if auth is correct
504 500 """
505 501 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
506 502 raise ValueError('auth type must be on of http, vcs got "%s" instead'
507 503 % auth_type)
508 container_only = environ and not (username and password)
504 headers_only = environ and not (username and password)
509 505
510 506 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
511 507 for plugin in authn_registry.get_plugins_for_authentication():
512 508 plugin.set_auth_type(auth_type)
513 509 user = plugin.get_user(username)
514 510 display_user = user.username if user else username
515 511
516 if container_only and not plugin.is_container_auth:
517 log.debug('Auth type is for container only and plugin `%s` is not '
518 'container plugin, skipping...', plugin.get_id())
512 if headers_only and not plugin.is_headers_auth:
513 log.debug('Auth type is for headers only and plugin `%s` is not '
514 'headers plugin, skipping...', plugin.get_id())
519 515 continue
520 516
521 517 # load plugin settings from RhodeCode database
522 518 plugin_settings = plugin.get_settings()
523 519 log.debug('Plugin settings:%s', plugin_settings)
524 520
525 521 log.debug('Trying authentication using ** %s **', plugin.get_id())
526 522 # use plugin's method of user extraction.
527 523 user = plugin.get_user(username, environ=environ,
528 524 settings=plugin_settings)
529 525 display_user = user.username if user else username
530 526 log.debug(
531 527 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
532 528
533 529 if not plugin.allows_authentication_from(user):
534 530 log.debug('Plugin %s does not accept user `%s` for authentication',
535 531 plugin.get_id(), display_user)
536 532 continue
537 533 else:
538 534 log.debug('Plugin %s accepted user `%s` for authentication',
539 535 plugin.get_id(), display_user)
540 536
541 537 log.info('Authenticating user `%s` using %s plugin',
542 538 display_user, plugin.get_id())
543 539
544 540 _cache_ttl = 0
545 541
546 542 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
547 543 # plugin cache set inside is more important than the settings value
548 544 _cache_ttl = plugin.AUTH_CACHE_TTL
549 545 elif plugin_settings.get('auth_cache_ttl'):
550 546 _cache_ttl = safe_int(plugin_settings.get('auth_cache_ttl'), 0)
551 547
552 548 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
553 549
554 550 # get instance of cache manager configured for a namespace
555 551 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
556 552
557 553 log.debug('Cache for plugin `%s` active: %s', plugin.get_id(),
558 554 plugin_cache_active)
559 555
560 556 # for environ based password can be empty, but then the validation is
561 557 # on the server that fills in the env data needed for authentication
562 558 _password_hash = md5_safe(plugin.name + username + (password or ''))
563 559
564 560 # _authenticate is a wrapper for .auth() method of plugin.
565 561 # it checks if .auth() sends proper data.
566 562 # For RhodeCodeExternalAuthPlugin it also maps users to
567 563 # Database and maps the attributes returned from .auth()
568 564 # to RhodeCode database. If this function returns data
569 565 # then auth is correct.
570 566 start = time.time()
571 567 log.debug('Running plugin `%s` _authenticate method',
572 568 plugin.get_id())
573 569
574 570 def auth_func():
575 571 """
576 572 This function is used internally in Cache of Beaker to calculate
577 573 Results
578 574 """
579 575 return plugin._authenticate(
580 576 user, username, password, plugin_settings,
581 577 environ=environ or {})
582 578
583 579 if plugin_cache_active:
584 580 plugin_user = cache_manager.get(
585 581 _password_hash, createfunc=auth_func)
586 582 else:
587 583 plugin_user = auth_func()
588 584
589 585 auth_time = time.time() - start
590 586 log.debug('Authentication for plugin `%s` completed in %.3fs, '
591 587 'expiration time of fetched cache %.1fs.',
592 588 plugin.get_id(), auth_time, _cache_ttl)
593 589
594 590 log.debug('PLUGIN USER DATA: %s', plugin_user)
595 591
596 592 if plugin_user:
597 593 log.debug('Plugin returned proper authentication data')
598 594 return plugin_user
599 595 # we failed to Auth because .auth() method didn't return proper user
600 596 log.debug("User `%s` failed to authenticate against %s",
601 597 display_user, plugin.get_id())
602 598 return None
@@ -1,337 +1,337 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2016 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 datetime
22 22 import formencode
23 23 import logging
24 24 import urlparse
25 25
26 26 from pylons import url
27 27 from pyramid.httpexceptions import HTTPFound
28 28 from pyramid.view import view_config
29 29 from recaptcha.client.captcha import submit
30 30
31 31 from rhodecode.authentication.base import authenticate, HTTP_TYPE
32 32 from rhodecode.events import UserRegistered
33 33 from rhodecode.lib.auth import (
34 34 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
35 35 from rhodecode.lib.base import get_ip_addr
36 36 from rhodecode.lib.exceptions import UserCreationError
37 37 from rhodecode.lib.utils2 import safe_str
38 38 from rhodecode.model.db import User
39 39 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
40 40 from rhodecode.model.login_session import LoginSession
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.translation import _
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 def _store_user_in_session(session, username, remember=False):
51 51 user = User.get_by_username(username, case_insensitive=True)
52 52 auth_user = AuthUser(user.user_id)
53 53 auth_user.set_authenticated()
54 54 cs = auth_user.get_cookie_store()
55 55 session['rhodecode_user'] = cs
56 56 user.update_lastlogin()
57 57 Session().commit()
58 58
59 59 # If they want to be remembered, update the cookie
60 60 if remember:
61 61 _year = (datetime.datetime.now() +
62 62 datetime.timedelta(seconds=60 * 60 * 24 * 365))
63 63 session._set_cookie_expires(_year)
64 64
65 65 session.save()
66 66
67 67 log.info('user %s is now authenticated and stored in '
68 68 'session, session attrs %s', username, cs)
69 69
70 70 # dumps session attrs back to cookie
71 71 session._update_cookie_out()
72 72 # we set new cookie
73 73 headers = None
74 74 if session.request['set_cookie']:
75 75 # send set-cookie headers back to response to update cookie
76 76 headers = [('Set-Cookie', session.request['cookie_out'])]
77 77 return headers
78 78
79 79
80 80 def get_came_from(request):
81 81 came_from = safe_str(request.GET.get('came_from', ''))
82 82 parsed = urlparse.urlparse(came_from)
83 83 allowed_schemes = ['http', 'https']
84 84 if parsed.scheme and parsed.scheme not in allowed_schemes:
85 85 log.error('Suspicious URL scheme detected %s for url %s' %
86 86 (parsed.scheme, parsed))
87 87 came_from = url('home')
88 88 elif parsed.netloc and request.host != parsed.netloc:
89 89 log.error('Suspicious NETLOC detected %s for url %s server url '
90 90 'is: %s' % (parsed.netloc, parsed, request.host))
91 91 came_from = url('home')
92 92 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
93 93 log.error('Header injection detected `%s` for url %s server url ' %
94 94 (parsed.path, parsed))
95 95 came_from = url('home')
96 96
97 97 return came_from or url('home')
98 98
99 99
100 100 class LoginView(object):
101 101
102 102 def __init__(self, context, request):
103 103 self.request = request
104 104 self.context = context
105 105 self.session = request.session
106 106 self._rhodecode_user = request.user
107 107
108 108 def _get_template_context(self):
109 109 return {
110 110 'came_from': get_came_from(self.request),
111 111 'defaults': {},
112 112 'errors': {},
113 113 }
114 114
115 115 @view_config(
116 116 route_name='login', request_method='GET',
117 117 renderer='rhodecode:templates/login.html')
118 118 def login(self):
119 119 came_from = get_came_from(self.request)
120 120 user = self.request.user
121 121
122 122 # redirect if already logged in
123 123 if user.is_authenticated and not user.is_default and user.ip_allowed:
124 124 raise HTTPFound(came_from)
125 125
126 # check if we use container plugin, and try to login using it.
126 # check if we use headers plugin, and try to login using it.
127 127 try:
128 log.debug('Running PRE-AUTH for container based authentication')
128 log.debug('Running PRE-AUTH for headers based authentication')
129 129 auth_info = authenticate(
130 130 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
131 131 if auth_info:
132 132 headers = _store_user_in_session(
133 133 self.session, auth_info.get('username'))
134 134 raise HTTPFound(came_from, headers=headers)
135 135 except UserCreationError as e:
136 136 log.error(e)
137 137 self.session.flash(e, queue='error')
138 138
139 139 return self._get_template_context()
140 140
141 141 @view_config(
142 142 route_name='login', request_method='POST',
143 143 renderer='rhodecode:templates/login.html')
144 144 def login_post(self):
145 145 came_from = get_came_from(self.request)
146 146 session = self.request.session
147 147 login_form = LoginForm()()
148 148
149 149 try:
150 150 session.invalidate()
151 151 form_result = login_form.to_python(self.request.params)
152 152 # form checks for username/password, now we're authenticated
153 153 headers = _store_user_in_session(
154 154 self.session,
155 155 username=form_result['username'],
156 156 remember=form_result['remember'])
157 157 raise HTTPFound(came_from, headers=headers)
158 158 except formencode.Invalid as errors:
159 159 defaults = errors.value
160 160 # remove password from filling in form again
161 161 del defaults['password']
162 162 render_ctx = self._get_template_context()
163 163 render_ctx.update({
164 164 'errors': errors.error_dict,
165 165 'defaults': defaults,
166 166 })
167 167 return render_ctx
168 168
169 169 except UserCreationError as e:
170 # container auth or other auth functions that create users on
170 # headers auth or other auth functions that create users on
171 171 # the fly can throw this exception signaling that there's issue
172 172 # with user creation, explanation should be provided in
173 173 # Exception itself
174 174 session.flash(e, queue='error')
175 175 return self._get_template_context()
176 176
177 177 @CSRFRequired()
178 178 @view_config(route_name='logout', request_method='POST')
179 179 def logout(self):
180 180 LoginSession().destroy_user_session()
181 181 return HTTPFound(url('home'))
182 182
183 183 @HasPermissionAnyDecorator(
184 184 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
185 185 @view_config(
186 186 route_name='register', request_method='GET',
187 187 renderer='rhodecode:templates/register.html',)
188 188 def register(self, defaults=None, errors=None):
189 189 defaults = defaults or {}
190 190 errors = errors or {}
191 191
192 192 settings = SettingsModel().get_all_settings()
193 193 captcha_public_key = settings.get('rhodecode_captcha_public_key')
194 194 captcha_private_key = settings.get('rhodecode_captcha_private_key')
195 195 captcha_active = bool(captcha_private_key)
196 196 register_message = settings.get('rhodecode_register_message') or ''
197 197 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
198 198 .AuthUser.permissions['global']
199 199
200 200 render_ctx = self._get_template_context()
201 201 render_ctx.update({
202 202 'defaults': defaults,
203 203 'errors': errors,
204 204 'auto_active': auto_active,
205 205 'captcha_active': captcha_active,
206 206 'captcha_public_key': captcha_public_key,
207 207 'register_message': register_message,
208 208 })
209 209 return render_ctx
210 210
211 211 @view_config(
212 212 route_name='register', request_method='POST',
213 213 renderer='rhodecode:templates/register.html')
214 214 def register_post(self):
215 215 captcha_private_key = SettingsModel().get_setting_by_name(
216 216 'rhodecode_captcha_private_key')
217 217 captcha_active = bool(captcha_private_key)
218 218 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
219 219 .AuthUser.permissions['global']
220 220
221 221 register_form = RegisterForm()()
222 222 try:
223 223 form_result = register_form.to_python(self.request.params)
224 224 form_result['active'] = auto_active
225 225
226 226 if captcha_active:
227 227 response = submit(
228 228 self.request.params.get('recaptcha_challenge_field'),
229 229 self.request.params.get('recaptcha_response_field'),
230 230 private_key=captcha_private_key,
231 231 remoteip=get_ip_addr(self.request.environ))
232 232 if captcha_active and not response.is_valid:
233 233 _value = form_result
234 234 _msg = _('bad captcha')
235 235 error_dict = {'recaptcha_field': _msg}
236 236 raise formencode.Invalid(_msg, _value, None,
237 237 error_dict=error_dict)
238 238
239 239 new_user = UserModel().create_registration(form_result)
240 240 event = UserRegistered(user=new_user, session=self.session)
241 241 self.request.registry.notify(event)
242 242 self.session.flash(
243 243 _('You have successfully registered with RhodeCode'),
244 244 queue='success')
245 245 Session().commit()
246 246
247 247 redirect_ro = self.request.route_path('login')
248 248 raise HTTPFound(redirect_ro)
249 249
250 250 except formencode.Invalid as errors:
251 251 del errors.value['password']
252 252 del errors.value['password_confirmation']
253 253 return self.register(
254 254 defaults=errors.value, errors=errors.error_dict)
255 255
256 256 except UserCreationError as e:
257 257 # container auth or other auth functions that create users on
258 258 # the fly can throw this exception signaling that there's issue
259 259 # with user creation, explanation should be provided in
260 260 # Exception itself
261 261 self.session.flash(e, queue='error')
262 262 return self.register()
263 263
264 264 @view_config(
265 265 route_name='reset_password', request_method=('GET', 'POST'),
266 266 renderer='rhodecode:templates/password_reset.html')
267 267 def password_reset(self):
268 268 settings = SettingsModel().get_all_settings()
269 269 captcha_private_key = settings.get('rhodecode_captcha_private_key')
270 270 captcha_active = bool(captcha_private_key)
271 271 captcha_public_key = settings.get('rhodecode_captcha_public_key')
272 272
273 273 render_ctx = {
274 274 'captcha_active': captcha_active,
275 275 'captcha_public_key': captcha_public_key,
276 276 'defaults': {},
277 277 'errors': {},
278 278 }
279 279
280 280 if self.request.POST:
281 281 password_reset_form = PasswordResetForm()()
282 282 try:
283 283 form_result = password_reset_form.to_python(
284 284 self.request.params)
285 285 if captcha_active:
286 286 response = submit(
287 287 self.request.params.get('recaptcha_challenge_field'),
288 288 self.request.params.get('recaptcha_response_field'),
289 289 private_key=captcha_private_key,
290 290 remoteip=get_ip_addr(self.request.environ))
291 291 if captcha_active and not response.is_valid:
292 292 _value = form_result
293 293 _msg = _('bad captcha')
294 294 error_dict = {'recaptcha_field': _msg}
295 295 raise formencode.Invalid(_msg, _value, None,
296 296 error_dict=error_dict)
297 297
298 298 # Generate reset URL and send mail.
299 299 user_email = form_result['email']
300 300 user = User.get_by_email(user_email)
301 301 password_reset_url = self.request.route_url(
302 302 'reset_password_confirmation',
303 303 _query={'key': user.api_key})
304 304 UserModel().reset_password_link(
305 305 form_result, password_reset_url)
306 306
307 307 # Display success message and redirect.
308 308 self.session.flash(
309 309 _('Your password reset link was sent'),
310 310 queue='success')
311 311 return HTTPFound(self.request.route_path('login'))
312 312
313 313 except formencode.Invalid as errors:
314 314 render_ctx.update({
315 315 'defaults': errors.value,
316 316 'errors': errors.error_dict,
317 317 })
318 318
319 319 return render_ctx
320 320
321 321 @view_config(route_name='reset_password_confirmation',
322 322 request_method='GET')
323 323 def password_reset_confirmation(self):
324 324 if self.request.GET and self.request.GET.get('key'):
325 325 try:
326 326 user = User.get_by_auth_token(self.request.GET.get('key'))
327 327 data = {'email': user.email}
328 328 UserModel().reset_password(data)
329 329 self.session.flash(
330 330 _('Your password reset was successful, '
331 331 'a new password has been sent to your email'),
332 332 queue='success')
333 333 except Exception as e:
334 334 log.error(e)
335 335 return HTTPFound(self.request.route_path('reset_password'))
336 336
337 337 return HTTPFound(self.request.route_path('login'))
General Comments 0
You need to be logged in to leave comments. Login now