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