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