##// END OF EJS Templates
auth: Fix renaming of 'auth_cahe_ttl' to 'cache_ttl' #4127
Martin Bornhold -
r503:82fa7f5f default
parent child Browse files
Show More
@@ -1,620 +1,620 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 229 @property
230 230 def is_headers_auth(self):
231 231 """
232 232 Returns True if this authentication plugin uses HTTP headers as
233 233 authentication method.
234 234 """
235 235 return False
236 236
237 237 @hybrid_property
238 238 def is_container_auth(self):
239 239 """
240 240 Deprecated method that indicates if this authentication plugin uses
241 241 HTTP headers as authentication method.
242 242 """
243 243 warnings.warn(
244 244 'Use is_headers_auth instead.', category=DeprecationWarning)
245 245 return self.is_headers_auth
246 246
247 247 @hybrid_property
248 248 def allows_creating_users(self):
249 249 """
250 250 Defines if Plugin allows users to be created on-the-fly when
251 251 authentication is called. Controls how external plugins should behave
252 252 in terms if they are allowed to create new users, or not. Base plugins
253 253 should not be allowed to, but External ones should be !
254 254
255 255 :return: bool
256 256 """
257 257 return False
258 258
259 259 def set_auth_type(self, auth_type):
260 260 self.auth_type = auth_type
261 261
262 262 def allows_authentication_from(
263 263 self, user, allows_non_existing_user=True,
264 264 allowed_auth_plugins=None, allowed_auth_sources=None):
265 265 """
266 266 Checks if this authentication module should accept a request for
267 267 the current user.
268 268
269 269 :param user: user object fetched using plugin's get_user() method.
270 270 :param allows_non_existing_user: if True, don't allow the
271 271 user to be empty, meaning not existing in our database
272 272 :param allowed_auth_plugins: if provided, users extern_type will be
273 273 checked against a list of provided extern types, which are plugin
274 274 auth_names in the end
275 275 :param allowed_auth_sources: authentication type allowed,
276 276 `http` or `vcs` default is both.
277 277 defines if plugin will accept only http authentication vcs
278 278 authentication(git/hg) or both
279 279 :returns: boolean
280 280 """
281 281 if not user and not allows_non_existing_user:
282 282 log.debug('User is empty but plugin does not allow empty users,'
283 283 'not allowed to authenticate')
284 284 return False
285 285
286 286 expected_auth_plugins = allowed_auth_plugins or [self.name]
287 287 if user and (user.extern_type and
288 288 user.extern_type not in expected_auth_plugins):
289 289 log.debug(
290 290 'User `%s` is bound to `%s` auth type. Plugin allows only '
291 291 '%s, skipping', user, user.extern_type, expected_auth_plugins)
292 292
293 293 return False
294 294
295 295 # by default accept both
296 296 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
297 297 if self.auth_type not in expected_auth_from:
298 298 log.debug('Current auth source is %s but plugin only allows %s',
299 299 self.auth_type, expected_auth_from)
300 300 return False
301 301
302 302 return True
303 303
304 304 def get_user(self, username=None, **kwargs):
305 305 """
306 306 Helper method for user fetching in plugins, by default it's using
307 307 simple fetch by username, but this method can be custimized in plugins
308 308 eg. headers auth plugin to fetch user by environ params
309 309
310 310 :param username: username if given to fetch from database
311 311 :param kwargs: extra arguments needed for user fetching.
312 312 """
313 313 user = None
314 314 log.debug(
315 315 'Trying to fetch user `%s` from RhodeCode database', username)
316 316 if username:
317 317 user = User.get_by_username(username)
318 318 if not user:
319 319 log.debug('User not found, fallback to fetch user in '
320 320 'case insensitive mode')
321 321 user = User.get_by_username(username, case_insensitive=True)
322 322 else:
323 323 log.debug('provided username:`%s` is empty skipping...', username)
324 324 if not user:
325 325 log.debug('User `%s` not found in database', username)
326 326 return user
327 327
328 328 def user_activation_state(self):
329 329 """
330 330 Defines user activation state when creating new users
331 331
332 332 :returns: boolean
333 333 """
334 334 raise NotImplementedError("Not implemented in base class")
335 335
336 336 def auth(self, userobj, username, passwd, settings, **kwargs):
337 337 """
338 338 Given a user object (which may be null), username, a plaintext
339 339 password, and a settings object (containing all the keys needed as
340 340 listed in settings()), authenticate this user's login attempt.
341 341
342 342 Return None on failure. On success, return a dictionary of the form:
343 343
344 344 see: RhodeCodeAuthPluginBase.auth_func_attrs
345 345 This is later validated for correctness
346 346 """
347 347 raise NotImplementedError("not implemented in base class")
348 348
349 349 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
350 350 """
351 351 Wrapper to call self.auth() that validates call on it
352 352
353 353 :param userobj: userobj
354 354 :param username: username
355 355 :param passwd: plaintext password
356 356 :param settings: plugin settings
357 357 """
358 358 auth = self.auth(userobj, username, passwd, settings, **kwargs)
359 359 if auth:
360 360 # check if hash should be migrated ?
361 361 new_hash = auth.get('_hash_migrate')
362 362 if new_hash:
363 363 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
364 364 return self._validate_auth_return(auth)
365 365 return auth
366 366
367 367 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
368 368 new_hash_cypher = _RhodeCodeCryptoBCrypt()
369 369 # extra checks, so make sure new hash is correct.
370 370 password_encoded = safe_str(password)
371 371 if new_hash and new_hash_cypher.hash_check(
372 372 password_encoded, new_hash):
373 373 cur_user = User.get_by_username(username)
374 374 cur_user.password = new_hash
375 375 Session().add(cur_user)
376 376 Session().flush()
377 377 log.info('Migrated user %s hash to bcrypt', cur_user)
378 378
379 379 def _validate_auth_return(self, ret):
380 380 if not isinstance(ret, dict):
381 381 raise Exception('returned value from auth must be a dict')
382 382 for k in self.auth_func_attrs:
383 383 if k not in ret:
384 384 raise Exception('Missing %s attribute from returned data' % k)
385 385 return ret
386 386
387 387
388 388 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
389 389
390 390 @hybrid_property
391 391 def allows_creating_users(self):
392 392 return True
393 393
394 394 def use_fake_password(self):
395 395 """
396 396 Return a boolean that indicates whether or not we should set the user's
397 397 password to a random value when it is authenticated by this plugin.
398 398 If your plugin provides authentication, then you will generally
399 399 want this.
400 400
401 401 :returns: boolean
402 402 """
403 403 raise NotImplementedError("Not implemented in base class")
404 404
405 405 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
406 406 # at this point _authenticate calls plugin's `auth()` function
407 407 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
408 408 userobj, username, passwd, settings, **kwargs)
409 409 if auth:
410 410 # maybe plugin will clean the username ?
411 411 # we should use the return value
412 412 username = auth['username']
413 413
414 414 # if external source tells us that user is not active, we should
415 415 # skip rest of the process. This can prevent from creating users in
416 416 # RhodeCode when using external authentication, but if it's
417 417 # inactive user we shouldn't create that user anyway
418 418 if auth['active_from_extern'] is False:
419 419 log.warning(
420 420 "User %s authenticated against %s, but is inactive",
421 421 username, self.__module__)
422 422 return None
423 423
424 424 cur_user = User.get_by_username(username, case_insensitive=True)
425 425 is_user_existing = cur_user is not None
426 426
427 427 if is_user_existing:
428 428 log.debug('Syncing user `%s` from '
429 429 '`%s` plugin', username, self.name)
430 430 else:
431 431 log.debug('Creating non existing user `%s` from '
432 432 '`%s` plugin', username, self.name)
433 433
434 434 if self.allows_creating_users:
435 435 log.debug('Plugin `%s` allows to '
436 436 'create new users', self.name)
437 437 else:
438 438 log.debug('Plugin `%s` does not allow to '
439 439 'create new users', self.name)
440 440
441 441 user_parameters = {
442 442 'username': username,
443 443 'email': auth["email"],
444 444 'firstname': auth["firstname"],
445 445 'lastname': auth["lastname"],
446 446 'active': auth["active"],
447 447 'admin': auth["admin"],
448 448 'extern_name': auth["extern_name"],
449 449 'extern_type': self.name,
450 450 'plugin': self,
451 451 'allow_to_create_user': self.allows_creating_users,
452 452 }
453 453
454 454 if not is_user_existing:
455 455 if self.use_fake_password():
456 456 # Randomize the PW because we don't need it, but don't want
457 457 # them blank either
458 458 passwd = PasswordGenerator().gen_password(length=16)
459 459 user_parameters['password'] = passwd
460 460 else:
461 461 # Since the password is required by create_or_update method of
462 462 # UserModel, we need to set it explicitly.
463 463 # The create_or_update method is smart and recognises the
464 464 # password hashes as well.
465 465 user_parameters['password'] = cur_user.password
466 466
467 467 # we either create or update users, we also pass the flag
468 468 # that controls if this method can actually do that.
469 469 # raises NotAllowedToCreateUserError if it cannot, and we try to.
470 470 user = UserModel().create_or_update(**user_parameters)
471 471 Session().flush()
472 472 # enforce user is just in given groups, all of them has to be ones
473 473 # created from plugins. We store this info in _group_data JSON
474 474 # field
475 475 try:
476 476 groups = auth['groups'] or []
477 477 UserGroupModel().enforce_groups(user, groups, self.name)
478 478 except Exception:
479 479 # for any reason group syncing fails, we should
480 480 # proceed with login
481 481 log.error(traceback.format_exc())
482 482 Session().commit()
483 483 return auth
484 484
485 485
486 486 def loadplugin(plugin_id):
487 487 """
488 488 Loads and returns an instantiated authentication plugin.
489 489 Returns the RhodeCodeAuthPluginBase subclass on success,
490 490 or None on failure.
491 491 """
492 492 # TODO: Disusing pyramids thread locals to retrieve the registry.
493 493 authn_registry = get_authn_registry()
494 494 plugin = authn_registry.get_plugin(plugin_id)
495 495 if plugin is None:
496 496 log.error('Authentication plugin not found: "%s"', plugin_id)
497 497 return plugin
498 498
499 499
500 500 def get_authn_registry(registry=None):
501 501 registry = registry or get_current_registry()
502 502 authn_registry = registry.getUtility(IAuthnPluginRegistry)
503 503 return authn_registry
504 504
505 505
506 506 def get_auth_cache_manager(custom_ttl=None):
507 507 return caches.get_cache_manager(
508 508 'auth_plugins', 'rhodecode.authentication', custom_ttl)
509 509
510 510
511 511 def authenticate(username, password, environ=None, auth_type=None,
512 512 skip_missing=False):
513 513 """
514 514 Authentication function used for access control,
515 515 It tries to authenticate based on enabled authentication modules.
516 516
517 517 :param username: username can be empty for headers auth
518 518 :param password: password can be empty for headers auth
519 519 :param environ: environ headers passed for headers auth
520 520 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
521 521 :param skip_missing: ignores plugins that are in db but not in environment
522 522 :returns: None if auth failed, plugin_user dict if auth is correct
523 523 """
524 524 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
525 525 raise ValueError('auth type must be on of http, vcs got "%s" instead'
526 526 % auth_type)
527 527 headers_only = environ and not (username and password)
528 528
529 529 authn_registry = get_authn_registry()
530 530 for plugin in authn_registry.get_plugins_for_authentication():
531 531 plugin.set_auth_type(auth_type)
532 532 user = plugin.get_user(username)
533 533 display_user = user.username if user else username
534 534
535 535 if headers_only and not plugin.is_headers_auth:
536 536 log.debug('Auth type is for headers only and plugin `%s` is not '
537 537 'headers plugin, skipping...', plugin.get_id())
538 538 continue
539 539
540 540 # load plugin settings from RhodeCode database
541 541 plugin_settings = plugin.get_settings()
542 542 log.debug('Plugin settings:%s', plugin_settings)
543 543
544 544 log.debug('Trying authentication using ** %s **', plugin.get_id())
545 545 # use plugin's method of user extraction.
546 546 user = plugin.get_user(username, environ=environ,
547 547 settings=plugin_settings)
548 548 display_user = user.username if user else username
549 549 log.debug(
550 550 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
551 551
552 552 if not plugin.allows_authentication_from(user):
553 553 log.debug('Plugin %s does not accept user `%s` for authentication',
554 554 plugin.get_id(), display_user)
555 555 continue
556 556 else:
557 557 log.debug('Plugin %s accepted user `%s` for authentication',
558 558 plugin.get_id(), display_user)
559 559
560 560 log.info('Authenticating user `%s` using %s plugin',
561 561 display_user, plugin.get_id())
562 562
563 563 _cache_ttl = 0
564 564
565 565 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
566 566 # plugin cache set inside is more important than the settings value
567 567 _cache_ttl = plugin.AUTH_CACHE_TTL
568 elif plugin_settings.get('auth_cache_ttl'):
569 _cache_ttl = safe_int(plugin_settings.get('auth_cache_ttl'), 0)
568 elif plugin_settings.get('cache_ttl'):
569 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
570 570
571 571 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
572 572
573 573 # get instance of cache manager configured for a namespace
574 574 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
575 575
576 576 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
577 577 plugin.get_id(), plugin_cache_active, _cache_ttl)
578 578
579 579 # for environ based password can be empty, but then the validation is
580 580 # on the server that fills in the env data needed for authentication
581 581 _password_hash = md5_safe(plugin.name + username + (password or ''))
582 582
583 583 # _authenticate is a wrapper for .auth() method of plugin.
584 584 # it checks if .auth() sends proper data.
585 585 # For RhodeCodeExternalAuthPlugin it also maps users to
586 586 # Database and maps the attributes returned from .auth()
587 587 # to RhodeCode database. If this function returns data
588 588 # then auth is correct.
589 589 start = time.time()
590 590 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
591 591
592 592 def auth_func():
593 593 """
594 594 This function is used internally in Cache of Beaker to calculate
595 595 Results
596 596 """
597 597 return plugin._authenticate(
598 598 user, username, password, plugin_settings,
599 599 environ=environ or {})
600 600
601 601 if plugin_cache_active:
602 602 plugin_user = cache_manager.get(
603 603 _password_hash, createfunc=auth_func)
604 604 else:
605 605 plugin_user = auth_func()
606 606
607 607 auth_time = time.time() - start
608 608 log.debug('Authentication for plugin `%s` completed in %.3fs, '
609 609 'expiration time of fetched cache %.1fs.',
610 610 plugin.get_id(), auth_time, _cache_ttl)
611 611
612 612 log.debug('PLUGIN USER DATA: %s', plugin_user)
613 613
614 614 if plugin_user:
615 615 log.debug('Plugin returned proper authentication data')
616 616 return plugin_user
617 617 # we failed to Auth because .auth() method didn't return proper user
618 618 log.debug("User `%s` failed to authenticate against %s",
619 619 display_user, plugin.get_id())
620 620 return None
@@ -1,201 +1,201 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 import pytest
22 22
23 23 from rhodecode.tests import assert_session_flash
24 24 from rhodecode.tests.utils import AssertResponse
25 25 from rhodecode.model.db import Session
26 26 from rhodecode.model.settings import SettingsModel
27 27
28 28
29 29 def assert_auth_settings_updated(response):
30 30 assert response.status_int == 302, 'Expected response HTTP Found 302'
31 31 assert_session_flash(response, 'Auth settings updated successfully')
32 32
33 33
34 34 @pytest.mark.usefixtures("autologin_user", "app")
35 35 class TestAuthSettingsController(object):
36 36
37 37 def _enable_plugins(self, plugins_list, csrf_token, override=None,
38 38 verify_response=False):
39 39 test_url = '/_admin/auth'
40 40 params = {
41 41 'auth_plugins': plugins_list,
42 42 'csrf_token': csrf_token,
43 43 }
44 44 if override:
45 45 params.update(override)
46 46 _enabled_plugins = []
47 47 for plugin in plugins_list.split(','):
48 48 plugin_name = plugin.partition('#')[-1]
49 49 enabled_plugin = '%s_enabled' % plugin_name
50 cache_ttl = '%s_auth_cache_ttl' % plugin_name
50 cache_ttl = '%s_cache_ttl' % plugin_name
51 51
52 52 # default params that are needed for each plugin,
53 # `enabled` and `auth_cache_ttl`
53 # `enabled` and `cache_ttl`
54 54 params.update({
55 55 enabled_plugin: True,
56 56 cache_ttl: 0
57 57 })
58 58 _enabled_plugins.append(enabled_plugin)
59 59
60 60 # we need to clean any enabled plugin before, since they require
61 61 # form params to be present
62 62 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
63 63 db_plugin.app_settings_value = \
64 64 'egg:rhodecode-enterprise-ce#rhodecode'
65 65 Session().add(db_plugin)
66 66 Session().commit()
67 67 for _plugin in _enabled_plugins:
68 68 db_plugin = SettingsModel().get_setting_by_name(_plugin)
69 69 if db_plugin:
70 70 Session.delete(db_plugin)
71 71 Session().commit()
72 72
73 73 response = self.app.post(url=test_url, params=params)
74 74
75 75 if verify_response:
76 76 assert_auth_settings_updated(response)
77 77 return params
78 78
79 79 def _post_ldap_settings(self, params, override=None, force=False):
80 80
81 81 params.update({
82 82 'filter': 'user',
83 83 'user_member_of': '',
84 84 'user_search_base': '',
85 85 'user_search_filter': 'test_filter',
86 86
87 87 'host': 'dc.example.com',
88 88 'port': '999',
89 89 'tls_kind': 'PLAIN',
90 90 'tls_reqcert': 'NEVER',
91 91
92 92 'dn_user': 'test_user',
93 93 'dn_pass': 'test_pass',
94 94 'base_dn': 'test_base_dn',
95 95 'search_scope': 'BASE',
96 96 'attr_login': 'test_attr_login',
97 97 'attr_firstname': 'ima',
98 98 'attr_lastname': 'tester',
99 99 'attr_email': 'test@example.com',
100 'auth_cache_ttl': '0',
100 'cache_ttl': '0',
101 101 })
102 102 if force:
103 103 params = {}
104 104 params.update(override or {})
105 105
106 106 test_url = '/_admin/auth/ldap/'
107 107
108 108 response = self.app.post(url=test_url, params=params)
109 109 return response
110 110
111 111 def test_index(self):
112 112 response = self.app.get('/_admin/auth')
113 113 response.mustcontain('Authentication Plugins')
114 114
115 115 @pytest.mark.parametrize("disable_plugin, needs_import", [
116 116 ('egg:rhodecode-enterprise-ce#headers', None),
117 117 ('egg:rhodecode-enterprise-ce#crowd', None),
118 118 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
119 119 ('egg:rhodecode-enterprise-ce#ldap', None),
120 120 ('egg:rhodecode-enterprise-ce#pam', "pam"),
121 121 ])
122 122 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
123 123 # TODO: johbo: "pam" is currently not available on darwin,
124 124 # although the docs state that it should work on darwin.
125 125 if needs_import:
126 126 pytest.importorskip(needs_import)
127 127
128 128 self._enable_plugins(
129 129 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
130 130 csrf_token, verify_response=True)
131 131
132 132 self._enable_plugins(
133 133 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
134 134 verify_response=True)
135 135
136 136 def test_ldap_save_settings(self, csrf_token):
137 137 params = self._enable_plugins(
138 138 'egg:rhodecode-enterprise-ce#rhodecode,'
139 139 'egg:rhodecode-enterprise-ce#ldap',
140 140 csrf_token)
141 141 response = self._post_ldap_settings(params)
142 142 assert_auth_settings_updated(response)
143 143
144 144 new_settings = SettingsModel().get_auth_settings()
145 145 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
146 146 'fail db write compare'
147 147
148 148 def test_ldap_error_form_wrong_port_number(self, csrf_token):
149 149 params = self._enable_plugins(
150 150 'egg:rhodecode-enterprise-ce#rhodecode,'
151 151 'egg:rhodecode-enterprise-ce#ldap',
152 152 csrf_token)
153 153 invalid_port_value = 'invalid-port-number'
154 154 response = self._post_ldap_settings(params, override={
155 155 'port': invalid_port_value,
156 156 })
157 157 assertr = AssertResponse(response)
158 158 assertr.element_contains(
159 159 '.form .field #port ~ .error-message',
160 160 invalid_port_value)
161 161
162 162 def test_ldap_error_form(self, csrf_token):
163 163 params = self._enable_plugins(
164 164 'egg:rhodecode-enterprise-ce#rhodecode,'
165 165 'egg:rhodecode-enterprise-ce#ldap',
166 166 csrf_token)
167 167 response = self._post_ldap_settings(params, override={
168 168 'attr_login': '',
169 169 })
170 170 response.mustcontain("""<span class="error-message">The LDAP Login"""
171 171 """ attribute of the CN must be specified""")
172 172
173 173 def test_post_ldap_group_settings(self, csrf_token):
174 174 params = self._enable_plugins(
175 175 'egg:rhodecode-enterprise-ce#rhodecode,'
176 176 'egg:rhodecode-enterprise-ce#ldap',
177 177 csrf_token)
178 178
179 179 response = self._post_ldap_settings(params, override={
180 180 'host': 'dc-legacy.example.com',
181 181 'port': '999',
182 182 'tls_kind': 'PLAIN',
183 183 'tls_reqcert': 'NEVER',
184 184 'dn_user': 'test_user',
185 185 'dn_pass': 'test_pass',
186 186 'base_dn': 'test_base_dn',
187 187 'filter': 'test_filter',
188 188 'search_scope': 'BASE',
189 189 'attr_login': 'test_attr_login',
190 190 'attr_firstname': 'ima',
191 191 'attr_lastname': 'tester',
192 192 'attr_email': 'test@example.com',
193 'auth_cache_ttl': '60',
193 'cache_ttl': '60',
194 194 'csrf_token': csrf_token,
195 195 }
196 196 )
197 197 assert_auth_settings_updated(response)
198 198
199 199 new_settings = SettingsModel().get_auth_settings()
200 200 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
201 201 'fail db write compare'
@@ -1,257 +1,257 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 py.test config for test suite for making push/pull operations.
23 23
24 24 .. important::
25 25
26 26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 27 to redirect things to stderr instead of stdout.
28 28 """
29 29
30 30 import ConfigParser
31 31 import os
32 32 import subprocess
33 33 import tempfile
34 34 import textwrap
35 35 import pytest
36 36
37 37 import rhodecode
38 38 from rhodecode.model.db import Repository
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.settings import SettingsModel
41 41 from rhodecode.tests import (
42 42 GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,)
43 43 from rhodecode.tests.fixture import Fixture
44 44 from rhodecode.tests.utils import (
45 45 set_anonymous_access, is_url_reachable, wait_for_url)
46 46
47 47 RC_LOG = os.path.join(tempfile.gettempdir(), 'rc.log')
48 48 REPO_GROUP = 'a_repo_group'
49 49 HG_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, HG_REPO)
50 50 GIT_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, GIT_REPO)
51 51
52 52
53 53 def assert_no_running_instance(url):
54 54 if is_url_reachable(url):
55 55 print("Hint: Usually this means another instance of Enterprise "
56 56 "is running in the background.")
57 57 pytest.fail(
58 58 "Port is not free at %s, cannot start web interface" % url)
59 59
60 60
61 61 def get_host_url(pylons_config):
62 62 """Construct the host url using the port in the test configuration."""
63 63 config = ConfigParser.ConfigParser()
64 64 config.read(pylons_config)
65 65
66 66 return '127.0.0.1:%s' % config.get('server:main', 'port')
67 67
68 68
69 69 class RcWebServer(object):
70 70 """
71 71 Represents a running RCE web server used as a test fixture.
72 72 """
73 73 def __init__(self, pylons_config):
74 74 self.pylons_config = pylons_config
75 75
76 76 def repo_clone_url(self, repo_name, **kwargs):
77 77 params = {
78 78 'user': TEST_USER_ADMIN_LOGIN,
79 79 'passwd': TEST_USER_ADMIN_PASS,
80 80 'host': get_host_url(self.pylons_config),
81 81 'cloned_repo': repo_name,
82 82 }
83 83 params.update(**kwargs)
84 84 _url = 'http://%(user)s:%(passwd)s@%(host)s/%(cloned_repo)s' % params
85 85 return _url
86 86
87 87
88 88 @pytest.fixture(scope="module")
89 89 def rcextensions(request, pylonsapp, tmpdir_factory):
90 90 """
91 91 Installs a testing rcextensions pack to ensure they work as expected.
92 92 """
93 93 init_content = textwrap.dedent("""
94 94 # Forward import the example rcextensions to make it
95 95 # active for our tests.
96 96 from rhodecode.tests.other.example_rcextensions import *
97 97 """)
98 98
99 99 # Note: rcextensions are looked up based on the path of the ini file
100 100 root_path = tmpdir_factory.getbasetemp()
101 101 rcextensions_path = root_path.join('rcextensions')
102 102 init_path = rcextensions_path.join('__init__.py')
103 103
104 104 if rcextensions_path.check():
105 105 pytest.fail(
106 106 "Path for rcextensions already exists, please clean up before "
107 107 "test run this path: %s" % (rcextensions_path, ))
108 108 return
109 109
110 110 request.addfinalizer(rcextensions_path.remove)
111 111 init_path.write_binary(init_content, ensure=True)
112 112
113 113
114 114 @pytest.fixture(scope="module")
115 115 def repos(request, pylonsapp):
116 116 """Create a copy of each test repo in a repo group."""
117 117 fixture = Fixture()
118 118 repo_group = fixture.create_repo_group(REPO_GROUP)
119 119 repo_group_id = repo_group.group_id
120 120 fixture.create_fork(HG_REPO, HG_REPO,
121 121 repo_name_full=HG_REPO_WITH_GROUP,
122 122 repo_group=repo_group_id)
123 123 fixture.create_fork(GIT_REPO, GIT_REPO,
124 124 repo_name_full=GIT_REPO_WITH_GROUP,
125 125 repo_group=repo_group_id)
126 126
127 127 @request.addfinalizer
128 128 def cleanup():
129 129 fixture.destroy_repo(HG_REPO_WITH_GROUP)
130 130 fixture.destroy_repo(GIT_REPO_WITH_GROUP)
131 131 fixture.destroy_repo_group(repo_group_id)
132 132
133 133
134 134 @pytest.fixture(scope="module")
135 135 def rc_web_server_config(pylons_config):
136 136 """
137 137 Configuration file used for the fixture `rc_web_server`.
138 138 """
139 139 return pylons_config
140 140
141 141
142 142 @pytest.fixture(scope="module")
143 143 def rc_web_server(
144 144 request, pylonsapp, rc_web_server_config, repos, rcextensions):
145 145 """
146 146 Run the web server as a subprocess.
147 147
148 148 Since we have already a running vcsserver, this is not spawned again.
149 149 """
150 150 env = os.environ.copy()
151 151 env['RC_NO_TMP_PATH'] = '1'
152 152
153 153 server_out = open(RC_LOG, 'w')
154 154
155 155 # TODO: Would be great to capture the output and err of the subprocess
156 156 # and make it available in a section of the py.test report in case of an
157 157 # error.
158 158
159 159 host_url = 'http://' + get_host_url(rc_web_server_config)
160 160 assert_no_running_instance(host_url)
161 161 command = ['rcserver', rc_web_server_config]
162 162
163 163 print('Starting rcserver: {}'.format(host_url))
164 164 print('Command: {}'.format(command))
165 165 print('Logfile: {}'.format(RC_LOG))
166 166
167 167 proc = subprocess.Popen(
168 168 command, bufsize=0, env=env, stdout=server_out, stderr=server_out)
169 169
170 170 wait_for_url(host_url, timeout=30)
171 171
172 172 @request.addfinalizer
173 173 def stop_web_server():
174 174 # TODO: Find out how to integrate with the reporting of py.test to
175 175 # make this information available.
176 176 print "\nServer log file written to %s" % (RC_LOG, )
177 177 proc.kill()
178 178 server_out.close()
179 179
180 180 return RcWebServer(rc_web_server_config)
181 181
182 182
183 183 @pytest.fixture(scope='class', autouse=True)
184 184 def disable_anonymous_user_access(pylonsapp):
185 185 set_anonymous_access(False)
186 186
187 187
188 188 @pytest.fixture
189 189 def disable_locking(pylonsapp):
190 190 r = Repository.get_by_repo_name(GIT_REPO)
191 191 Repository.unlock(r)
192 192 r.enable_locking = False
193 193 Session().add(r)
194 194 Session().commit()
195 195
196 196 r = Repository.get_by_repo_name(HG_REPO)
197 197 Repository.unlock(r)
198 198 r.enable_locking = False
199 199 Session().add(r)
200 200 Session().commit()
201 201
202 202
203 203 @pytest.fixture
204 204 def enable_auth_plugins(request, pylonsapp, csrf_token):
205 205 """
206 206 Return a factory object that when called, allows to control which
207 207 authentication plugins are enabled.
208 208 """
209 209 def _enable_plugins(plugins_list, override=None):
210 210 override = override or {}
211 211 params = {
212 212 'auth_plugins': ','.join(plugins_list),
213 213 'csrf_token': csrf_token,
214 214 }
215 215
216 216 for module in plugins_list:
217 217 plugin = rhodecode.authentication.base.loadplugin(module)
218 218 plugin_name = plugin.name
219 219 enabled_plugin = 'auth_%s_enabled' % plugin_name
220 cache_ttl = 'auth_%s_auth_cache_ttl' % plugin_name
220 cache_ttl = 'auth_%s_cache_ttl' % plugin_name
221 221
222 222 # default params that are needed for each plugin,
223 # `enabled` and `auth_cache_ttl`
223 # `enabled` and `cache_ttl`
224 224 params.update({
225 225 enabled_plugin: True,
226 226 cache_ttl: 0
227 227 })
228 228 if override.get:
229 229 params.update(override.get(module, {}))
230 230
231 231 validated_params = params
232 232 for k, v in validated_params.items():
233 233 setting = SettingsModel().create_or_update_setting(k, v)
234 234 Session().add(setting)
235 235 Session().commit()
236 236
237 237 def cleanup():
238 238 _enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode'])
239 239
240 240 request.addfinalizer(cleanup)
241 241
242 242 return _enable_plugins
243 243
244 244
245 245 @pytest.fixture
246 246 def fs_repo_only(request, rhodecode_fixtures):
247 247 def fs_repo_fabric(repo_name, repo_type):
248 248 rhodecode_fixtures.create_repo(repo_name, repo_type=repo_type)
249 249 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=False)
250 250
251 251 def cleanup():
252 252 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=True)
253 253 rhodecode_fixtures.destroy_repo_on_filesystem(repo_name)
254 254
255 255 request.addfinalizer(cleanup)
256 256
257 257 return fs_repo_fabric
General Comments 0
You need to be logged in to leave comments. Login now