##// END OF EJS Templates
auth: use our own simpler hybrid property....
marcink -
r1961:4409087f default
parent child Browse files
Show More
@@ -1,672 +1,697 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Authentication modules
23 23 """
24 24
25 25 import colander
26 26 import copy
27 27 import logging
28 28 import time
29 29 import traceback
30 30 import warnings
31 import functools
31 32
32 33 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.ext.hybrid import hybrid_property
34 34
35 35 from rhodecode.authentication.interface import IAuthnPluginRegistry
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.lib import caches
38 38 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 39 from rhodecode.lib.utils2 import md5_safe, safe_int
40 40 from rhodecode.lib.utils2 import safe_str
41 41 from rhodecode.model.db import User
42 42 from rhodecode.model.meta import Session
43 43 from rhodecode.model.settings import SettingsModel
44 44 from rhodecode.model.user import UserModel
45 45 from rhodecode.model.user_group import UserGroupModel
46 46
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50 # auth types that authenticate() function can receive
51 51 VCS_TYPE = 'vcs'
52 52 HTTP_TYPE = 'http'
53 53
54 54
55 class hybrid_property(object):
56 """
57 a property decorator that works both for instance and class
58 """
59 def __init__(self, fget, fset=None, fdel=None, expr=None):
60 self.fget = fget
61 self.fset = fset
62 self.fdel = fdel
63 self.expr = expr or fget
64 functools.update_wrapper(self, fget)
65
66 def __get__(self, instance, owner):
67 if instance is None:
68 return self.expr(owner)
69 else:
70 return self.fget(instance)
71
72 def __set__(self, instance, value):
73 self.fset(instance, value)
74
75 def __delete__(self, instance):
76 self.fdel(instance)
77
78
79
55 80 class LazyFormencode(object):
56 81 def __init__(self, formencode_obj, *args, **kwargs):
57 82 self.formencode_obj = formencode_obj
58 83 self.args = args
59 84 self.kwargs = kwargs
60 85
61 86 def __call__(self, *args, **kwargs):
62 87 from inspect import isfunction
63 88 formencode_obj = self.formencode_obj
64 89 if isfunction(formencode_obj):
65 90 # case we wrap validators into functions
66 91 formencode_obj = self.formencode_obj(*args, **kwargs)
67 92 return formencode_obj(*self.args, **self.kwargs)
68 93
69 94
70 95 class RhodeCodeAuthPluginBase(object):
71 96 # cache the authentication request for N amount of seconds. Some kind
72 97 # of authentication methods are very heavy and it's very efficient to cache
73 98 # the result of a call. If it's set to None (default) cache is off
74 99 AUTH_CACHE_TTL = None
75 100 AUTH_CACHE = {}
76 101
77 102 auth_func_attrs = {
78 103 "username": "unique username",
79 104 "firstname": "first name",
80 105 "lastname": "last name",
81 106 "email": "email address",
82 107 "groups": '["list", "of", "groups"]',
83 108 "extern_name": "name in external source of record",
84 109 "extern_type": "type of external source of record",
85 110 "admin": 'True|False defines if user should be RhodeCode super admin',
86 111 "active":
87 112 'True|False defines active state of user internally for RhodeCode',
88 113 "active_from_extern":
89 114 "True|False\None, active state from the external auth, "
90 115 "None means use definition from RhodeCode extern_type active value"
91 116 }
92 117 # set on authenticate() method and via set_auth_type func.
93 118 auth_type = None
94 119
95 120 # set on authenticate() method and via set_calling_scope_repo, this is a
96 121 # calling scope repository when doing authentication most likely on VCS
97 122 # operations
98 123 acl_repo_name = None
99 124
100 125 # List of setting names to store encrypted. Plugins may override this list
101 126 # to store settings encrypted.
102 127 _settings_encrypted = []
103 128
104 129 # Mapping of python to DB settings model types. Plugins may override or
105 130 # extend this mapping.
106 131 _settings_type_map = {
107 132 colander.String: 'unicode',
108 133 colander.Integer: 'int',
109 134 colander.Boolean: 'bool',
110 135 colander.List: 'list',
111 136 }
112 137
113 138 # list of keys in settings that are unsafe to be logged, should be passwords
114 139 # or other crucial credentials
115 140 _settings_unsafe_keys = []
116 141
117 142 def __init__(self, plugin_id):
118 143 self._plugin_id = plugin_id
119 144
120 145 def __str__(self):
121 146 return self.get_id()
122 147
123 148 def _get_setting_full_name(self, name):
124 149 """
125 150 Return the full setting name used for storing values in the database.
126 151 """
127 152 # TODO: johbo: Using the name here is problematic. It would be good to
128 153 # introduce either new models in the database to hold Plugin and
129 154 # PluginSetting or to use the plugin id here.
130 155 return 'auth_{}_{}'.format(self.name, name)
131 156
132 157 def _get_setting_type(self, name):
133 158 """
134 159 Return the type of a setting. This type is defined by the SettingsModel
135 160 and determines how the setting is stored in DB. Optionally the suffix
136 161 `.encrypted` is appended to instruct SettingsModel to store it
137 162 encrypted.
138 163 """
139 164 schema_node = self.get_settings_schema().get(name)
140 165 db_type = self._settings_type_map.get(
141 166 type(schema_node.typ), 'unicode')
142 167 if name in self._settings_encrypted:
143 168 db_type = '{}.encrypted'.format(db_type)
144 169 return db_type
145 170
146 171 def is_enabled(self):
147 172 """
148 173 Returns true if this plugin is enabled. An enabled plugin can be
149 174 configured in the admin interface but it is not consulted during
150 175 authentication.
151 176 """
152 177 auth_plugins = SettingsModel().get_auth_plugins()
153 178 return self.get_id() in auth_plugins
154 179
155 180 def is_active(self):
156 181 """
157 182 Returns true if the plugin is activated. An activated plugin is
158 183 consulted during authentication, assumed it is also enabled.
159 184 """
160 185 return self.get_setting_by_name('enabled')
161 186
162 187 def get_id(self):
163 188 """
164 189 Returns the plugin id.
165 190 """
166 191 return self._plugin_id
167 192
168 193 def get_display_name(self):
169 194 """
170 195 Returns a translation string for displaying purposes.
171 196 """
172 197 raise NotImplementedError('Not implemented in base class')
173 198
174 199 def get_settings_schema(self):
175 200 """
176 201 Returns a colander schema, representing the plugin settings.
177 202 """
178 203 return AuthnPluginSettingsSchemaBase()
179 204
180 205 def get_setting_by_name(self, name, default=None):
181 206 """
182 207 Returns a plugin setting by name.
183 208 """
184 209 full_name = self._get_setting_full_name(name)
185 210 db_setting = SettingsModel().get_setting_by_name(full_name)
186 211 return db_setting.app_settings_value if db_setting else default
187 212
188 213 def create_or_update_setting(self, name, value):
189 214 """
190 215 Create or update a setting for this plugin in the persistent storage.
191 216 """
192 217 full_name = self._get_setting_full_name(name)
193 218 type_ = self._get_setting_type(name)
194 219 db_setting = SettingsModel().create_or_update_setting(
195 220 full_name, value, type_)
196 221 return db_setting.app_settings_value
197 222
198 223 def get_settings(self):
199 224 """
200 225 Returns the plugin settings as dictionary.
201 226 """
202 227 settings = {}
203 228 for node in self.get_settings_schema():
204 229 settings[node.name] = self.get_setting_by_name(node.name)
205 230 return settings
206 231
207 232 def log_safe_settings(self, settings):
208 233 """
209 234 returns a log safe representation of settings, without any secrets
210 235 """
211 236 settings_copy = copy.deepcopy(settings)
212 237 for k in self._settings_unsafe_keys:
213 238 if k in settings_copy:
214 239 del settings_copy[k]
215 240 return settings_copy
216 241
217 242 @property
218 243 def validators(self):
219 244 """
220 245 Exposes RhodeCode validators modules
221 246 """
222 247 # this is a hack to overcome issues with pylons threadlocals and
223 248 # translator object _() not being registered properly.
224 249 class LazyCaller(object):
225 250 def __init__(self, name):
226 251 self.validator_name = name
227 252
228 253 def __call__(self, *args, **kwargs):
229 254 from rhodecode.model import validators as v
230 255 obj = getattr(v, self.validator_name)
231 256 # log.debug('Initializing lazy formencode object: %s', obj)
232 257 return LazyFormencode(obj, *args, **kwargs)
233 258
234 259 class ProxyGet(object):
235 260 def __getattribute__(self, name):
236 261 return LazyCaller(name)
237 262
238 263 return ProxyGet()
239 264
240 265 @hybrid_property
241 266 def name(self):
242 267 """
243 268 Returns the name of this authentication plugin.
244 269
245 270 :returns: string
246 271 """
247 272 raise NotImplementedError("Not implemented in base class")
248 273
249 274 def get_url_slug(self):
250 275 """
251 276 Returns a slug which should be used when constructing URLs which refer
252 277 to this plugin. By default it returns the plugin name. If the name is
253 278 not suitable for using it in an URL the plugin should override this
254 279 method.
255 280 """
256 281 return self.name
257 282
258 283 @property
259 284 def is_headers_auth(self):
260 285 """
261 286 Returns True if this authentication plugin uses HTTP headers as
262 287 authentication method.
263 288 """
264 289 return False
265 290
266 291 @hybrid_property
267 292 def is_container_auth(self):
268 293 """
269 294 Deprecated method that indicates if this authentication plugin uses
270 295 HTTP headers as authentication method.
271 296 """
272 297 warnings.warn(
273 298 'Use is_headers_auth instead.', category=DeprecationWarning)
274 299 return self.is_headers_auth
275 300
276 301 @hybrid_property
277 302 def allows_creating_users(self):
278 303 """
279 304 Defines if Plugin allows users to be created on-the-fly when
280 305 authentication is called. Controls how external plugins should behave
281 306 in terms if they are allowed to create new users, or not. Base plugins
282 307 should not be allowed to, but External ones should be !
283 308
284 309 :return: bool
285 310 """
286 311 return False
287 312
288 313 def set_auth_type(self, auth_type):
289 314 self.auth_type = auth_type
290 315
291 316 def set_calling_scope_repo(self, acl_repo_name):
292 317 self.acl_repo_name = acl_repo_name
293 318
294 319 def allows_authentication_from(
295 320 self, user, allows_non_existing_user=True,
296 321 allowed_auth_plugins=None, allowed_auth_sources=None):
297 322 """
298 323 Checks if this authentication module should accept a request for
299 324 the current user.
300 325
301 326 :param user: user object fetched using plugin's get_user() method.
302 327 :param allows_non_existing_user: if True, don't allow the
303 328 user to be empty, meaning not existing in our database
304 329 :param allowed_auth_plugins: if provided, users extern_type will be
305 330 checked against a list of provided extern types, which are plugin
306 331 auth_names in the end
307 332 :param allowed_auth_sources: authentication type allowed,
308 333 `http` or `vcs` default is both.
309 334 defines if plugin will accept only http authentication vcs
310 335 authentication(git/hg) or both
311 336 :returns: boolean
312 337 """
313 338 if not user and not allows_non_existing_user:
314 339 log.debug('User is empty but plugin does not allow empty users,'
315 340 'not allowed to authenticate')
316 341 return False
317 342
318 343 expected_auth_plugins = allowed_auth_plugins or [self.name]
319 344 if user and (user.extern_type and
320 345 user.extern_type not in expected_auth_plugins):
321 346 log.debug(
322 347 'User `%s` is bound to `%s` auth type. Plugin allows only '
323 348 '%s, skipping', user, user.extern_type, expected_auth_plugins)
324 349
325 350 return False
326 351
327 352 # by default accept both
328 353 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
329 354 if self.auth_type not in expected_auth_from:
330 355 log.debug('Current auth source is %s but plugin only allows %s',
331 356 self.auth_type, expected_auth_from)
332 357 return False
333 358
334 359 return True
335 360
336 361 def get_user(self, username=None, **kwargs):
337 362 """
338 363 Helper method for user fetching in plugins, by default it's using
339 364 simple fetch by username, but this method can be custimized in plugins
340 365 eg. headers auth plugin to fetch user by environ params
341 366
342 367 :param username: username if given to fetch from database
343 368 :param kwargs: extra arguments needed for user fetching.
344 369 """
345 370 user = None
346 371 log.debug(
347 372 'Trying to fetch user `%s` from RhodeCode database', username)
348 373 if username:
349 374 user = User.get_by_username(username)
350 375 if not user:
351 376 log.debug('User not found, fallback to fetch user in '
352 377 'case insensitive mode')
353 378 user = User.get_by_username(username, case_insensitive=True)
354 379 else:
355 380 log.debug('provided username:`%s` is empty skipping...', username)
356 381 if not user:
357 382 log.debug('User `%s` not found in database', username)
358 383 else:
359 384 log.debug('Got DB user:%s', user)
360 385 return user
361 386
362 387 def user_activation_state(self):
363 388 """
364 389 Defines user activation state when creating new users
365 390
366 391 :returns: boolean
367 392 """
368 393 raise NotImplementedError("Not implemented in base class")
369 394
370 395 def auth(self, userobj, username, passwd, settings, **kwargs):
371 396 """
372 397 Given a user object (which may be null), username, a plaintext
373 398 password, and a settings object (containing all the keys needed as
374 399 listed in settings()), authenticate this user's login attempt.
375 400
376 401 Return None on failure. On success, return a dictionary of the form:
377 402
378 403 see: RhodeCodeAuthPluginBase.auth_func_attrs
379 404 This is later validated for correctness
380 405 """
381 406 raise NotImplementedError("not implemented in base class")
382 407
383 408 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
384 409 """
385 410 Wrapper to call self.auth() that validates call on it
386 411
387 412 :param userobj: userobj
388 413 :param username: username
389 414 :param passwd: plaintext password
390 415 :param settings: plugin settings
391 416 """
392 417 auth = self.auth(userobj, username, passwd, settings, **kwargs)
393 418 if auth:
394 419 # check if hash should be migrated ?
395 420 new_hash = auth.get('_hash_migrate')
396 421 if new_hash:
397 422 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
398 423 return self._validate_auth_return(auth)
399 424 return auth
400 425
401 426 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
402 427 new_hash_cypher = _RhodeCodeCryptoBCrypt()
403 428 # extra checks, so make sure new hash is correct.
404 429 password_encoded = safe_str(password)
405 430 if new_hash and new_hash_cypher.hash_check(
406 431 password_encoded, new_hash):
407 432 cur_user = User.get_by_username(username)
408 433 cur_user.password = new_hash
409 434 Session().add(cur_user)
410 435 Session().flush()
411 436 log.info('Migrated user %s hash to bcrypt', cur_user)
412 437
413 438 def _validate_auth_return(self, ret):
414 439 if not isinstance(ret, dict):
415 440 raise Exception('returned value from auth must be a dict')
416 441 for k in self.auth_func_attrs:
417 442 if k not in ret:
418 443 raise Exception('Missing %s attribute from returned data' % k)
419 444 return ret
420 445
421 446
422 447 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
423 448
424 449 @hybrid_property
425 450 def allows_creating_users(self):
426 451 return True
427 452
428 453 def use_fake_password(self):
429 454 """
430 455 Return a boolean that indicates whether or not we should set the user's
431 456 password to a random value when it is authenticated by this plugin.
432 457 If your plugin provides authentication, then you will generally
433 458 want this.
434 459
435 460 :returns: boolean
436 461 """
437 462 raise NotImplementedError("Not implemented in base class")
438 463
439 464 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
440 465 # at this point _authenticate calls plugin's `auth()` function
441 466 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
442 467 userobj, username, passwd, settings, **kwargs)
443 468 if auth:
444 469 # maybe plugin will clean the username ?
445 470 # we should use the return value
446 471 username = auth['username']
447 472
448 473 # if external source tells us that user is not active, we should
449 474 # skip rest of the process. This can prevent from creating users in
450 475 # RhodeCode when using external authentication, but if it's
451 476 # inactive user we shouldn't create that user anyway
452 477 if auth['active_from_extern'] is False:
453 478 log.warning(
454 479 "User %s authenticated against %s, but is inactive",
455 480 username, self.__module__)
456 481 return None
457 482
458 483 cur_user = User.get_by_username(username, case_insensitive=True)
459 484 is_user_existing = cur_user is not None
460 485
461 486 if is_user_existing:
462 487 log.debug('Syncing user `%s` from '
463 488 '`%s` plugin', username, self.name)
464 489 else:
465 490 log.debug('Creating non existing user `%s` from '
466 491 '`%s` plugin', username, self.name)
467 492
468 493 if self.allows_creating_users:
469 494 log.debug('Plugin `%s` allows to '
470 495 'create new users', self.name)
471 496 else:
472 497 log.debug('Plugin `%s` does not allow to '
473 498 'create new users', self.name)
474 499
475 500 user_parameters = {
476 501 'username': username,
477 502 'email': auth["email"],
478 503 'firstname': auth["firstname"],
479 504 'lastname': auth["lastname"],
480 505 'active': auth["active"],
481 506 'admin': auth["admin"],
482 507 'extern_name': auth["extern_name"],
483 508 'extern_type': self.name,
484 509 'plugin': self,
485 510 'allow_to_create_user': self.allows_creating_users,
486 511 }
487 512
488 513 if not is_user_existing:
489 514 if self.use_fake_password():
490 515 # Randomize the PW because we don't need it, but don't want
491 516 # them blank either
492 517 passwd = PasswordGenerator().gen_password(length=16)
493 518 user_parameters['password'] = passwd
494 519 else:
495 520 # Since the password is required by create_or_update method of
496 521 # UserModel, we need to set it explicitly.
497 522 # The create_or_update method is smart and recognises the
498 523 # password hashes as well.
499 524 user_parameters['password'] = cur_user.password
500 525
501 526 # we either create or update users, we also pass the flag
502 527 # that controls if this method can actually do that.
503 528 # raises NotAllowedToCreateUserError if it cannot, and we try to.
504 529 user = UserModel().create_or_update(**user_parameters)
505 530 Session().flush()
506 531 # enforce user is just in given groups, all of them has to be ones
507 532 # created from plugins. We store this info in _group_data JSON
508 533 # field
509 534 try:
510 535 groups = auth['groups'] or []
511 536 UserGroupModel().enforce_groups(user, groups, self.name)
512 537 except Exception:
513 538 # for any reason group syncing fails, we should
514 539 # proceed with login
515 540 log.error(traceback.format_exc())
516 541 Session().commit()
517 542 return auth
518 543
519 544
520 545 def loadplugin(plugin_id):
521 546 """
522 547 Loads and returns an instantiated authentication plugin.
523 548 Returns the RhodeCodeAuthPluginBase subclass on success,
524 549 or None on failure.
525 550 """
526 551 # TODO: Disusing pyramids thread locals to retrieve the registry.
527 552 authn_registry = get_authn_registry()
528 553 plugin = authn_registry.get_plugin(plugin_id)
529 554 if plugin is None:
530 555 log.error('Authentication plugin not found: "%s"', plugin_id)
531 556 return plugin
532 557
533 558
534 559 def get_authn_registry(registry=None):
535 560 registry = registry or get_current_registry()
536 561 authn_registry = registry.getUtility(IAuthnPluginRegistry)
537 562 return authn_registry
538 563
539 564
540 565 def get_auth_cache_manager(custom_ttl=None):
541 566 return caches.get_cache_manager(
542 567 'auth_plugins', 'rhodecode.authentication', custom_ttl)
543 568
544 569
545 570 def authenticate(username, password, environ=None, auth_type=None,
546 571 skip_missing=False, registry=None, acl_repo_name=None):
547 572 """
548 573 Authentication function used for access control,
549 574 It tries to authenticate based on enabled authentication modules.
550 575
551 576 :param username: username can be empty for headers auth
552 577 :param password: password can be empty for headers auth
553 578 :param environ: environ headers passed for headers auth
554 579 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
555 580 :param skip_missing: ignores plugins that are in db but not in environment
556 581 :returns: None if auth failed, plugin_user dict if auth is correct
557 582 """
558 583 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
559 584 raise ValueError('auth type must be on of http, vcs got "%s" instead'
560 585 % auth_type)
561 586 headers_only = environ and not (username and password)
562 587
563 588 authn_registry = get_authn_registry(registry)
564 589 for plugin in authn_registry.get_plugins_for_authentication():
565 590 plugin.set_auth_type(auth_type)
566 591 plugin.set_calling_scope_repo(acl_repo_name)
567 592
568 593 if headers_only and not plugin.is_headers_auth:
569 594 log.debug('Auth type is for headers only and plugin `%s` is not '
570 595 'headers plugin, skipping...', plugin.get_id())
571 596 continue
572 597
573 598 # load plugin settings from RhodeCode database
574 599 plugin_settings = plugin.get_settings()
575 600 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
576 601 log.debug('Plugin settings:%s', plugin_sanitized_settings)
577 602
578 603 log.debug('Trying authentication using ** %s **', plugin.get_id())
579 604 # use plugin's method of user extraction.
580 605 user = plugin.get_user(username, environ=environ,
581 606 settings=plugin_settings)
582 607 display_user = user.username if user else username
583 608 log.debug(
584 609 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
585 610
586 611 if not plugin.allows_authentication_from(user):
587 612 log.debug('Plugin %s does not accept user `%s` for authentication',
588 613 plugin.get_id(), display_user)
589 614 continue
590 615 else:
591 616 log.debug('Plugin %s accepted user `%s` for authentication',
592 617 plugin.get_id(), display_user)
593 618
594 619 log.info('Authenticating user `%s` using %s plugin',
595 620 display_user, plugin.get_id())
596 621
597 622 _cache_ttl = 0
598 623
599 624 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
600 625 # plugin cache set inside is more important than the settings value
601 626 _cache_ttl = plugin.AUTH_CACHE_TTL
602 627 elif plugin_settings.get('cache_ttl'):
603 628 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
604 629
605 630 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
606 631
607 632 # get instance of cache manager configured for a namespace
608 633 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
609 634
610 635 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
611 636 plugin.get_id(), plugin_cache_active, _cache_ttl)
612 637
613 638 # for environ based password can be empty, but then the validation is
614 639 # on the server that fills in the env data needed for authentication
615 640 _password_hash = md5_safe(plugin.name + username + (password or ''))
616 641
617 642 # _authenticate is a wrapper for .auth() method of plugin.
618 643 # it checks if .auth() sends proper data.
619 644 # For RhodeCodeExternalAuthPlugin it also maps users to
620 645 # Database and maps the attributes returned from .auth()
621 646 # to RhodeCode database. If this function returns data
622 647 # then auth is correct.
623 648 start = time.time()
624 649 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
625 650
626 651 def auth_func():
627 652 """
628 653 This function is used internally in Cache of Beaker to calculate
629 654 Results
630 655 """
631 656 return plugin._authenticate(
632 657 user, username, password, plugin_settings,
633 658 environ=environ or {})
634 659
635 660 if plugin_cache_active:
636 661 plugin_user = cache_manager.get(
637 662 _password_hash, createfunc=auth_func)
638 663 else:
639 664 plugin_user = auth_func()
640 665
641 666 auth_time = time.time() - start
642 667 log.debug('Authentication for plugin `%s` completed in %.3fs, '
643 668 'expiration time of fetched cache %.1fs.',
644 669 plugin.get_id(), auth_time, _cache_ttl)
645 670
646 671 log.debug('PLUGIN USER DATA: %s', plugin_user)
647 672
648 673 if plugin_user:
649 674 log.debug('Plugin returned proper authentication data')
650 675 return plugin_user
651 676 # we failed to Auth because .auth() method didn't return proper user
652 677 log.debug("User `%s` failed to authenticate against %s",
653 678 display_user, plugin.get_id())
654 679 return None
655 680
656 681
657 682 def chop_at(s, sub, inclusive=False):
658 683 """Truncate string ``s`` at the first occurrence of ``sub``.
659 684
660 685 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
661 686
662 687 >>> chop_at("plutocratic brats", "rat")
663 688 'plutoc'
664 689 >>> chop_at("plutocratic brats", "rat", True)
665 690 'plutocrat'
666 691 """
667 692 pos = s.find(sub)
668 693 if pos == -1:
669 694 return s
670 695 if inclusive:
671 696 return s[:pos+len(sub)]
672 697 return s[:pos]
@@ -1,109 +1,109 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 External module for testing plugins
23 23
24 24 rhodecode.tests.auth_external_test
25 25
26 26 """
27 27 import logging
28 28 import traceback
29 29
30 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
31 from sqlalchemy.ext.hybrid import hybrid_property
30 from rhodecode.authentication.base import (
31 RhodeCodeExternalAuthPlugin, hybrid_property)
32 32 from rhodecode.model.db import User
33 33 from rhodecode.lib.ext_json import formatted_json
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
39 39 def __init__(self):
40 40 self._logger = logging.getLogger(__name__)
41 41
42 42 @hybrid_property
43 43 def allows_creating_users(self):
44 44 return True
45 45
46 46 @hybrid_property
47 47 def name(self):
48 48 return "external_test"
49 49
50 50 def settings(self):
51 51 settings = [
52 52 ]
53 53 return settings
54 54
55 55 def use_fake_password(self):
56 56 return True
57 57
58 58 def user_activation_state(self):
59 59 def_user_perms = User.get_default_user().AuthUser.permissions['global']
60 60 return 'hg.extern_activate.auto' in def_user_perms
61 61
62 62 def auth(self, userobj, username, password, settings, **kwargs):
63 63 """
64 64 Given a user object (which may be null), username, a plaintext password,
65 65 and a settings object (containing all the keys needed as listed in settings()),
66 66 authenticate this user's login attempt.
67 67
68 68 Return None on failure. On success, return a dictionary of the form:
69 69
70 70 see: RhodeCodeAuthPluginBase.auth_func_attrs
71 71 This is later validated for correctness
72 72 """
73 73
74 74 if not username or not password:
75 75 log.debug('Empty username or password skipping...')
76 76 return None
77 77
78 78 try:
79 79 user_dn = username
80 80
81 81 # # old attrs fetched from RhodeCode database
82 82 admin = getattr(userobj, 'admin', False)
83 83 active = getattr(userobj, 'active', True)
84 84 email = getattr(userobj, 'email', '')
85 85 firstname = getattr(userobj, 'firstname', '')
86 86 lastname = getattr(userobj, 'lastname', '')
87 87 extern_type = getattr(userobj, 'extern_type', '')
88 88 #
89 89 user_attrs = {
90 90 'username': username,
91 91 'firstname': firstname,
92 92 'lastname': lastname,
93 93 'groups': [],
94 94 'email': '%s@rhodecode.com' % username,
95 95 'admin': admin,
96 96 'active': active,
97 97 "active_from_extern": None,
98 98 'extern_name': user_dn,
99 99 'extern_type': extern_type,
100 100 }
101 101
102 102 log.debug('EXTERNAL user: \n%s' % formatted_json(user_attrs))
103 103 log.info('user %s authenticated correctly' % user_attrs['username'])
104 104
105 105 return user_attrs
106 106
107 107 except (Exception,):
108 108 log.error(traceback.format_exc())
109 109 return None
General Comments 0
You need to be logged in to leave comments. Login now