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