##// END OF EJS Templates
authentications: remove utf8 markers
super-admin -
r5056:d6440b8b default
parent child Browse files
Show More
@@ -1,101 +1,101 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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 import importlib
23 23
24 24 from pyramid.authentication import SessionAuthenticationPolicy
25 25
26 26 from rhodecode.authentication.registry import AuthenticationPluginRegistry
27 27 from rhodecode.authentication.routes import root_factory
28 28 from rhodecode.authentication.routes import AuthnRootResource
29 29 from rhodecode.apps._base import ADMIN_PREFIX
30 30 from rhodecode.model.settings import SettingsModel
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34 legacy_plugin_prefix = 'py:'
35 35 plugin_default_auth_ttl = 30
36 36
37 37
38 38 def _import_legacy_plugin(plugin_id):
39 39 module_name = plugin_id.split(legacy_plugin_prefix, 1)[-1]
40 40 module = importlib.import_module(module_name)
41 41 return module.plugin_factory(plugin_id=plugin_id)
42 42
43 43
44 44 def discover_legacy_plugins(config, prefix=legacy_plugin_prefix):
45 45 """
46 46 Function that imports the legacy plugins stored in the 'auth_plugins'
47 47 setting in database which are using the specified prefix. Normally 'py:' is
48 48 used for the legacy plugins.
49 49 """
50 50
51 51 log.debug('authentication: running legacy plugin discovery for prefix %s',
52 52 legacy_plugin_prefix)
53 53 try:
54 54 auth_plugins = SettingsModel().get_setting_by_name('auth_plugins')
55 55 enabled_plugins = auth_plugins.app_settings_value
56 56 legacy_plugins = [id_ for id_ in enabled_plugins if id_.startswith(prefix)]
57 57 except Exception:
58 58 legacy_plugins = []
59 59
60 60 for plugin_id in legacy_plugins:
61 61 log.debug('Legacy plugin discovered: "%s"', plugin_id)
62 62 try:
63 63 plugin = _import_legacy_plugin(plugin_id)
64 64 config.include(plugin.includeme)
65 65 except Exception as e:
66 66 log.exception(
67 67 'Exception while loading legacy authentication plugin '
68 68 '"%s": %s', plugin_id, e)
69 69
70 70
71 71 def includeme(config):
72 72
73 73 # Set authentication policy.
74 74 authn_policy = SessionAuthenticationPolicy()
75 75 config.set_authentication_policy(authn_policy)
76 76
77 77 # Create authentication plugin registry and add it to the pyramid registry.
78 78 authn_registry = AuthenticationPluginRegistry(config.get_settings())
79 79 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
80 80 config.registry.registerUtility(authn_registry)
81 81
82 82 # Create authentication traversal root resource.
83 83 authn_root_resource = root_factory()
84 84 config.add_directive('add_authn_resource',
85 85 authn_root_resource.add_authn_resource)
86 86
87 87 # Add the authentication traversal route.
88 88 config.add_route('auth_home',
89 89 ADMIN_PREFIX + '/auth*traverse',
90 90 factory=root_factory)
91 91 # Add the authentication settings root views.
92 92 config.add_view('rhodecode.authentication.views.AuthSettingsView',
93 93 attr='index',
94 94 request_method='GET',
95 95 route_name='auth_home',
96 96 context=AuthnRootResource)
97 97 config.add_view('rhodecode.authentication.views.AuthSettingsView',
98 98 attr='auth_settings',
99 99 request_method='POST',
100 100 route_name='auth_home',
101 101 context=AuthnRootResource)
@@ -1,818 +1,817 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 """
22 21 Authentication modules
23 22 """
24 23 import socket
25 24 import string
26 25 import colander
27 26 import copy
28 27 import logging
29 28 import time
30 29 import traceback
31 30 import warnings
32 31 import functools
33 32
34 33 from pyramid.threadlocal import get_current_registry
35 34
36 35 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 37 from rhodecode.lib import rc_cache
39 38 from rhodecode.lib.statsd_client import StatsdClient
40 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
41 40 from rhodecode.lib.utils2 import safe_int, safe_str
42 41 from rhodecode.lib.exceptions import (LdapConnectionError, LdapUsernameError, LdapPasswordError)
43 42 from rhodecode.model.db import User
44 43 from rhodecode.model.meta import Session
45 44 from rhodecode.model.settings import SettingsModel
46 45 from rhodecode.model.user import UserModel
47 46 from rhodecode.model.user_group import UserGroupModel
48 47
49 48
50 49 log = logging.getLogger(__name__)
51 50
52 51 # auth types that authenticate() function can receive
53 52 VCS_TYPE = 'vcs'
54 53 HTTP_TYPE = 'http'
55 54
56 55 external_auth_session_key = 'rhodecode.external_auth'
57 56
58 57
59 58 class hybrid_property(object):
60 59 """
61 60 a property decorator that works both for instance and class
62 61 """
63 62 def __init__(self, fget, fset=None, fdel=None, expr=None):
64 63 self.fget = fget
65 64 self.fset = fset
66 65 self.fdel = fdel
67 66 self.expr = expr or fget
68 67 functools.update_wrapper(self, fget)
69 68
70 69 def __get__(self, instance, owner):
71 70 if instance is None:
72 71 return self.expr(owner)
73 72 else:
74 73 return self.fget(instance)
75 74
76 75 def __set__(self, instance, value):
77 76 self.fset(instance, value)
78 77
79 78 def __delete__(self, instance):
80 79 self.fdel(instance)
81 80
82 81
83 82 class LazyFormencode(object):
84 83 def __init__(self, formencode_obj, *args, **kwargs):
85 84 self.formencode_obj = formencode_obj
86 85 self.args = args
87 86 self.kwargs = kwargs
88 87
89 88 def __call__(self, *args, **kwargs):
90 89 from inspect import isfunction
91 90 formencode_obj = self.formencode_obj
92 91 if isfunction(formencode_obj):
93 92 # case we wrap validators into functions
94 93 formencode_obj = self.formencode_obj(*args, **kwargs)
95 94 return formencode_obj(*self.args, **self.kwargs)
96 95
97 96
98 97 class RhodeCodeAuthPluginBase(object):
99 98 # UID is used to register plugin to the registry
100 99 uid = None
101 100
102 101 # cache the authentication request for N amount of seconds. Some kind
103 102 # of authentication methods are very heavy and it's very efficient to cache
104 103 # the result of a call. If it's set to None (default) cache is off
105 104 AUTH_CACHE_TTL = None
106 105 AUTH_CACHE = {}
107 106
108 107 auth_func_attrs = {
109 108 "username": "unique username",
110 109 "firstname": "first name",
111 110 "lastname": "last name",
112 111 "email": "email address",
113 112 "groups": '["list", "of", "groups"]',
114 113 "user_group_sync":
115 114 'True|False defines if returned user groups should be synced',
116 115 "extern_name": "name in external source of record",
117 116 "extern_type": "type of external source of record",
118 117 "admin": 'True|False defines if user should be RhodeCode super admin',
119 118 "active":
120 119 'True|False defines active state of user internally for RhodeCode',
121 120 "active_from_extern":
122 121 "True|False|None, active state from the external auth, "
123 122 "None means use definition from RhodeCode extern_type active value"
124 123
125 124 }
126 125 # set on authenticate() method and via set_auth_type func.
127 126 auth_type = None
128 127
129 128 # set on authenticate() method and via set_calling_scope_repo, this is a
130 129 # calling scope repository when doing authentication most likely on VCS
131 130 # operations
132 131 acl_repo_name = None
133 132
134 133 # List of setting names to store encrypted. Plugins may override this list
135 134 # to store settings encrypted.
136 135 _settings_encrypted = []
137 136
138 137 # Mapping of python to DB settings model types. Plugins may override or
139 138 # extend this mapping.
140 139 _settings_type_map = {
141 140 colander.String: 'unicode',
142 141 colander.Integer: 'int',
143 142 colander.Boolean: 'bool',
144 143 colander.List: 'list',
145 144 }
146 145
147 146 # list of keys in settings that are unsafe to be logged, should be passwords
148 147 # or other crucial credentials
149 148 _settings_unsafe_keys = []
150 149
151 150 def __init__(self, plugin_id):
152 151 self._plugin_id = plugin_id
153 152
154 153 def __str__(self):
155 154 return self.get_id()
156 155
157 156 def _get_setting_full_name(self, name):
158 157 """
159 158 Return the full setting name used for storing values in the database.
160 159 """
161 160 # TODO: johbo: Using the name here is problematic. It would be good to
162 161 # introduce either new models in the database to hold Plugin and
163 162 # PluginSetting or to use the plugin id here.
164 163 return 'auth_{}_{}'.format(self.name, name)
165 164
166 165 def _get_setting_type(self, name):
167 166 """
168 167 Return the type of a setting. This type is defined by the SettingsModel
169 168 and determines how the setting is stored in DB. Optionally the suffix
170 169 `.encrypted` is appended to instruct SettingsModel to store it
171 170 encrypted.
172 171 """
173 172 schema_node = self.get_settings_schema().get(name)
174 173 db_type = self._settings_type_map.get(
175 174 type(schema_node.typ), 'unicode')
176 175 if name in self._settings_encrypted:
177 176 db_type = '{}.encrypted'.format(db_type)
178 177 return db_type
179 178
180 179 @classmethod
181 180 def docs(cls):
182 181 """
183 182 Defines documentation url which helps with plugin setup
184 183 """
185 184 return ''
186 185
187 186 @classmethod
188 187 def icon(cls):
189 188 """
190 189 Defines ICON in SVG format for authentication method
191 190 """
192 191 return ''
193 192
194 193 def is_enabled(self):
195 194 """
196 195 Returns true if this plugin is enabled. An enabled plugin can be
197 196 configured in the admin interface but it is not consulted during
198 197 authentication.
199 198 """
200 199 auth_plugins = SettingsModel().get_auth_plugins()
201 200 return self.get_id() in auth_plugins
202 201
203 202 def is_active(self, plugin_cached_settings=None):
204 203 """
205 204 Returns true if the plugin is activated. An activated plugin is
206 205 consulted during authentication, assumed it is also enabled.
207 206 """
208 207 return self.get_setting_by_name(
209 208 'enabled', plugin_cached_settings=plugin_cached_settings)
210 209
211 210 def get_id(self):
212 211 """
213 212 Returns the plugin id.
214 213 """
215 214 return self._plugin_id
216 215
217 216 def get_display_name(self, load_from_settings=False):
218 217 """
219 218 Returns a translation string for displaying purposes.
220 219 if load_from_settings is set, plugin settings can override the display name
221 220 """
222 221 raise NotImplementedError('Not implemented in base class')
223 222
224 223 def get_settings_schema(self):
225 224 """
226 225 Returns a colander schema, representing the plugin settings.
227 226 """
228 227 return AuthnPluginSettingsSchemaBase()
229 228
230 229 def _propagate_settings(self, raw_settings):
231 230 settings = {}
232 231 for node in self.get_settings_schema():
233 232 settings[node.name] = self.get_setting_by_name(
234 233 node.name, plugin_cached_settings=raw_settings)
235 234 return settings
236 235
237 236 def get_settings(self, use_cache=True):
238 237 """
239 238 Returns the plugin settings as dictionary.
240 239 """
241 240
242 241 raw_settings = SettingsModel().get_all_settings(cache=use_cache)
243 242 settings = self._propagate_settings(raw_settings)
244 243
245 244 return settings
246 245
247 246 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
248 247 """
249 248 Returns a plugin setting by name.
250 249 """
251 250 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
252 251 if plugin_cached_settings:
253 252 plugin_settings = plugin_cached_settings
254 253 else:
255 254 plugin_settings = SettingsModel().get_all_settings()
256 255
257 256 if full_name in plugin_settings:
258 257 return plugin_settings[full_name]
259 258 else:
260 259 return default
261 260
262 261 def create_or_update_setting(self, name, value):
263 262 """
264 263 Create or update a setting for this plugin in the persistent storage.
265 264 """
266 265 full_name = self._get_setting_full_name(name)
267 266 type_ = self._get_setting_type(name)
268 267 db_setting = SettingsModel().create_or_update_setting(
269 268 full_name, value, type_)
270 269 return db_setting.app_settings_value
271 270
272 271 def log_safe_settings(self, settings):
273 272 """
274 273 returns a log safe representation of settings, without any secrets
275 274 """
276 275 settings_copy = copy.deepcopy(settings)
277 276 for k in self._settings_unsafe_keys:
278 277 if k in settings_copy:
279 278 del settings_copy[k]
280 279 return settings_copy
281 280
282 281 @hybrid_property
283 282 def name(self):
284 283 """
285 284 Returns the name of this authentication plugin.
286 285
287 286 :returns: string
288 287 """
289 288 raise NotImplementedError("Not implemented in base class")
290 289
291 290 def get_url_slug(self):
292 291 """
293 292 Returns a slug which should be used when constructing URLs which refer
294 293 to this plugin. By default it returns the plugin name. If the name is
295 294 not suitable for using it in an URL the plugin should override this
296 295 method.
297 296 """
298 297 return self.name
299 298
300 299 @property
301 300 def is_headers_auth(self):
302 301 """
303 302 Returns True if this authentication plugin uses HTTP headers as
304 303 authentication method.
305 304 """
306 305 return False
307 306
308 307 @hybrid_property
309 308 def is_container_auth(self):
310 309 """
311 310 Deprecated method that indicates if this authentication plugin uses
312 311 HTTP headers as authentication method.
313 312 """
314 313 warnings.warn(
315 314 'Use is_headers_auth instead.', category=DeprecationWarning)
316 315 return self.is_headers_auth
317 316
318 317 @hybrid_property
319 318 def allows_creating_users(self):
320 319 """
321 320 Defines if Plugin allows users to be created on-the-fly when
322 321 authentication is called. Controls how external plugins should behave
323 322 in terms if they are allowed to create new users, or not. Base plugins
324 323 should not be allowed to, but External ones should be !
325 324
326 325 :return: bool
327 326 """
328 327 return False
329 328
330 329 def set_auth_type(self, auth_type):
331 330 self.auth_type = auth_type
332 331
333 332 def set_calling_scope_repo(self, acl_repo_name):
334 333 self.acl_repo_name = acl_repo_name
335 334
336 335 def allows_authentication_from(
337 336 self, user, allows_non_existing_user=True,
338 337 allowed_auth_plugins=None, allowed_auth_sources=None):
339 338 """
340 339 Checks if this authentication module should accept a request for
341 340 the current user.
342 341
343 342 :param user: user object fetched using plugin's get_user() method.
344 343 :param allows_non_existing_user: if True, don't allow the
345 344 user to be empty, meaning not existing in our database
346 345 :param allowed_auth_plugins: if provided, users extern_type will be
347 346 checked against a list of provided extern types, which are plugin
348 347 auth_names in the end
349 348 :param allowed_auth_sources: authentication type allowed,
350 349 `http` or `vcs` default is both.
351 350 defines if plugin will accept only http authentication vcs
352 351 authentication(git/hg) or both
353 352 :returns: boolean
354 353 """
355 354 if not user and not allows_non_existing_user:
356 355 log.debug('User is empty but plugin does not allow empty users,'
357 356 'not allowed to authenticate')
358 357 return False
359 358
360 359 expected_auth_plugins = allowed_auth_plugins or [self.name]
361 360 if user and (user.extern_type and
362 361 user.extern_type not in expected_auth_plugins):
363 362 log.debug(
364 363 'User `%s` is bound to `%s` auth type. Plugin allows only '
365 364 '%s, skipping', user, user.extern_type, expected_auth_plugins)
366 365
367 366 return False
368 367
369 368 # by default accept both
370 369 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
371 370 if self.auth_type not in expected_auth_from:
372 371 log.debug('Current auth source is %s but plugin only allows %s',
373 372 self.auth_type, expected_auth_from)
374 373 return False
375 374
376 375 return True
377 376
378 377 def get_user(self, username=None, **kwargs):
379 378 """
380 379 Helper method for user fetching in plugins, by default it's using
381 380 simple fetch by username, but this method can be custimized in plugins
382 381 eg. headers auth plugin to fetch user by environ params
383 382
384 383 :param username: username if given to fetch from database
385 384 :param kwargs: extra arguments needed for user fetching.
386 385 """
387 386 user = None
388 387 log.debug(
389 388 'Trying to fetch user `%s` from RhodeCode database', username)
390 389 if username:
391 390 user = User.get_by_username(username)
392 391 if not user:
393 392 log.debug('User not found, fallback to fetch user in '
394 393 'case insensitive mode')
395 394 user = User.get_by_username(username, case_insensitive=True)
396 395 else:
397 396 log.debug('provided username:`%s` is empty skipping...', username)
398 397 if not user:
399 398 log.debug('User `%s` not found in database', username)
400 399 else:
401 400 log.debug('Got DB user:%s', user)
402 401 return user
403 402
404 403 def user_activation_state(self):
405 404 """
406 405 Defines user activation state when creating new users
407 406
408 407 :returns: boolean
409 408 """
410 409 raise NotImplementedError("Not implemented in base class")
411 410
412 411 def auth(self, userobj, username, passwd, settings, **kwargs):
413 412 """
414 413 Given a user object (which may be null), username, a plaintext
415 414 password, and a settings object (containing all the keys needed as
416 415 listed in settings()), authenticate this user's login attempt.
417 416
418 417 Return None on failure. On success, return a dictionary of the form:
419 418
420 419 see: RhodeCodeAuthPluginBase.auth_func_attrs
421 420 This is later validated for correctness
422 421 """
423 422 raise NotImplementedError("not implemented in base class")
424 423
425 424 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
426 425 """
427 426 Wrapper to call self.auth() that validates call on it
428 427
429 428 :param userobj: userobj
430 429 :param username: username
431 430 :param passwd: plaintext password
432 431 :param settings: plugin settings
433 432 """
434 433 auth = self.auth(userobj, username, passwd, settings, **kwargs)
435 434 if auth:
436 435 auth['_plugin'] = self.name
437 436 auth['_ttl_cache'] = self.get_ttl_cache(settings)
438 437 # check if hash should be migrated ?
439 438 new_hash = auth.get('_hash_migrate')
440 439 if new_hash:
441 440 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
442 441 if 'user_group_sync' not in auth:
443 442 auth['user_group_sync'] = False
444 443 return self._validate_auth_return(auth)
445 444 return auth
446 445
447 446 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
448 447 new_hash_cypher = _RhodeCodeCryptoBCrypt()
449 448 # extra checks, so make sure new hash is correct.
450 449 password_encoded = safe_str(password)
451 450 if new_hash and new_hash_cypher.hash_check(
452 451 password_encoded, new_hash):
453 452 cur_user = User.get_by_username(username)
454 453 cur_user.password = new_hash
455 454 Session().add(cur_user)
456 455 Session().flush()
457 456 log.info('Migrated user %s hash to bcrypt', cur_user)
458 457
459 458 def _validate_auth_return(self, ret):
460 459 if not isinstance(ret, dict):
461 460 raise Exception('returned value from auth must be a dict')
462 461 for k in self.auth_func_attrs:
463 462 if k not in ret:
464 463 raise Exception('Missing %s attribute from returned data' % k)
465 464 return ret
466 465
467 466 def get_ttl_cache(self, settings=None):
468 467 plugin_settings = settings or self.get_settings()
469 468 # we set default to 30, we make a compromise here,
470 469 # performance > security, mostly due to LDAP/SVN, majority
471 470 # of users pick cache_ttl to be enabled
472 471 from rhodecode.authentication import plugin_default_auth_ttl
473 472 cache_ttl = plugin_default_auth_ttl
474 473
475 474 if isinstance(self.AUTH_CACHE_TTL, int):
476 475 # plugin cache set inside is more important than the settings value
477 476 cache_ttl = self.AUTH_CACHE_TTL
478 477 elif plugin_settings.get('cache_ttl'):
479 478 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
480 479
481 480 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
482 481 return plugin_cache_active, cache_ttl
483 482
484 483
485 484 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
486 485
487 486 @hybrid_property
488 487 def allows_creating_users(self):
489 488 return True
490 489
491 490 def use_fake_password(self):
492 491 """
493 492 Return a boolean that indicates whether or not we should set the user's
494 493 password to a random value when it is authenticated by this plugin.
495 494 If your plugin provides authentication, then you will generally
496 495 want this.
497 496
498 497 :returns: boolean
499 498 """
500 499 raise NotImplementedError("Not implemented in base class")
501 500
502 501 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
503 502 # at this point _authenticate calls plugin's `auth()` function
504 503 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
505 504 userobj, username, passwd, settings, **kwargs)
506 505
507 506 if auth:
508 507 # maybe plugin will clean the username ?
509 508 # we should use the return value
510 509 username = auth['username']
511 510
512 511 # if external source tells us that user is not active, we should
513 512 # skip rest of the process. This can prevent from creating users in
514 513 # RhodeCode when using external authentication, but if it's
515 514 # inactive user we shouldn't create that user anyway
516 515 if auth['active_from_extern'] is False:
517 516 log.warning(
518 517 "User %s authenticated against %s, but is inactive",
519 518 username, self.__module__)
520 519 return None
521 520
522 521 cur_user = User.get_by_username(username, case_insensitive=True)
523 522 is_user_existing = cur_user is not None
524 523
525 524 if is_user_existing:
526 525 log.debug('Syncing user `%s` from '
527 526 '`%s` plugin', username, self.name)
528 527 else:
529 528 log.debug('Creating non existing user `%s` from '
530 529 '`%s` plugin', username, self.name)
531 530
532 531 if self.allows_creating_users:
533 532 log.debug('Plugin `%s` allows to '
534 533 'create new users', self.name)
535 534 else:
536 535 log.debug('Plugin `%s` does not allow to '
537 536 'create new users', self.name)
538 537
539 538 user_parameters = {
540 539 'username': username,
541 540 'email': auth["email"],
542 541 'firstname': auth["firstname"],
543 542 'lastname': auth["lastname"],
544 543 'active': auth["active"],
545 544 'admin': auth["admin"],
546 545 'extern_name': auth["extern_name"],
547 546 'extern_type': self.name,
548 547 'plugin': self,
549 548 'allow_to_create_user': self.allows_creating_users,
550 549 }
551 550
552 551 if not is_user_existing:
553 552 if self.use_fake_password():
554 553 # Randomize the PW because we don't need it, but don't want
555 554 # them blank either
556 555 passwd = PasswordGenerator().gen_password(length=16)
557 556 user_parameters['password'] = passwd
558 557 else:
559 558 # Since the password is required by create_or_update method of
560 559 # UserModel, we need to set it explicitly.
561 560 # The create_or_update method is smart and recognises the
562 561 # password hashes as well.
563 562 user_parameters['password'] = cur_user.password
564 563
565 564 # we either create or update users, we also pass the flag
566 565 # that controls if this method can actually do that.
567 566 # raises NotAllowedToCreateUserError if it cannot, and we try to.
568 567 user = UserModel().create_or_update(**user_parameters)
569 568 Session().flush()
570 569 # enforce user is just in given groups, all of them has to be ones
571 570 # created from plugins. We store this info in _group_data JSON
572 571 # field
573 572
574 573 if auth['user_group_sync']:
575 574 try:
576 575 groups = auth['groups'] or []
577 576 log.debug(
578 577 'Performing user_group sync based on set `%s` '
579 578 'returned by `%s` plugin', groups, self.name)
580 579 UserGroupModel().enforce_groups(user, groups, self.name)
581 580 except Exception:
582 581 # for any reason group syncing fails, we should
583 582 # proceed with login
584 583 log.error(traceback.format_exc())
585 584
586 585 Session().commit()
587 586 return auth
588 587
589 588
590 589 class AuthLdapBase(object):
591 590
592 591 @classmethod
593 592 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
594 593
595 594 def host_resolver(host, port, full_resolve=True):
596 595 """
597 596 Main work for this function is to prevent ldap connection issues,
598 597 and detect them early using a "greenified" sockets
599 598 """
600 599 host = host.strip()
601 600 if not full_resolve:
602 601 return '{}:{}'.format(host, port)
603 602
604 603 log.debug('LDAP: Resolving IP for LDAP host `%s`', host)
605 604 try:
606 605 ip = socket.gethostbyname(host)
607 606 log.debug('LDAP: Got LDAP host `%s` ip %s', host, ip)
608 607 except Exception:
609 608 raise LdapConnectionError('Failed to resolve host: `{}`'.format(host))
610 609
611 610 log.debug('LDAP: Checking if IP %s is accessible', ip)
612 611 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
613 612 try:
614 613 s.connect((ip, int(port)))
615 614 s.shutdown(socket.SHUT_RD)
616 615 log.debug('LDAP: connection to %s successful', ip)
617 616 except Exception:
618 617 raise LdapConnectionError(
619 618 'Failed to connect to host: `{}:{}`'.format(host, port))
620 619
621 620 return '{}:{}'.format(host, port)
622 621
623 622 if len(ldap_server) == 1:
624 623 # in case of single server use resolver to detect potential
625 624 # connection issues
626 625 full_resolve = True
627 626 else:
628 627 full_resolve = False
629 628
630 629 return ', '.join(
631 630 ["{}://{}".format(
632 631 ldap_server_type,
633 632 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
634 633 for host in ldap_server])
635 634
636 635 @classmethod
637 636 def _get_server_list(cls, servers):
638 637 return map(string.strip, servers.split(','))
639 638
640 639 @classmethod
641 640 def get_uid(cls, username, server_addresses):
642 641 uid = username
643 642 for server_addr in server_addresses:
644 643 uid = chop_at(username, "@%s" % server_addr)
645 644 return uid
646 645
647 646 @classmethod
648 647 def validate_username(cls, username):
649 648 if "," in username:
650 649 raise LdapUsernameError(
651 650 "invalid character `,` in username: `{}`".format(username))
652 651
653 652 @classmethod
654 653 def validate_password(cls, username, password):
655 654 if not password:
656 655 msg = "Authenticating user %s with blank password not allowed"
657 656 log.warning(msg, username)
658 657 raise LdapPasswordError(msg)
659 658
660 659
661 660 def loadplugin(plugin_id):
662 661 """
663 662 Loads and returns an instantiated authentication plugin.
664 663 Returns the RhodeCodeAuthPluginBase subclass on success,
665 664 or None on failure.
666 665 """
667 666 # TODO: Disusing pyramids thread locals to retrieve the registry.
668 667 authn_registry = get_authn_registry()
669 668 plugin = authn_registry.get_plugin(plugin_id)
670 669 if plugin is None:
671 670 log.error('Authentication plugin not found: "%s"', plugin_id)
672 671 return plugin
673 672
674 673
675 674 def get_authn_registry(registry=None):
676 675 registry = registry or get_current_registry()
677 676 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
678 677 return authn_registry
679 678
680 679
681 680 def authenticate(username, password, environ=None, auth_type=None,
682 681 skip_missing=False, registry=None, acl_repo_name=None):
683 682 """
684 683 Authentication function used for access control,
685 684 It tries to authenticate based on enabled authentication modules.
686 685
687 686 :param username: username can be empty for headers auth
688 687 :param password: password can be empty for headers auth
689 688 :param environ: environ headers passed for headers auth
690 689 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
691 690 :param skip_missing: ignores plugins that are in db but not in environment
692 691 :returns: None if auth failed, plugin_user dict if auth is correct
693 692 """
694 693 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
695 694 raise ValueError('auth type must be on of http, vcs got "%s" instead'
696 695 % auth_type)
697 696 headers_only = environ and not (username and password)
698 697
699 698 authn_registry = get_authn_registry(registry)
700 699
701 700 plugins_to_check = authn_registry.get_plugins_for_authentication()
702 701 log.debug('Starting ordered authentication chain using %s plugins',
703 702 [x.name for x in plugins_to_check])
704 703 for plugin in plugins_to_check:
705 704 plugin.set_auth_type(auth_type)
706 705 plugin.set_calling_scope_repo(acl_repo_name)
707 706
708 707 if headers_only and not plugin.is_headers_auth:
709 708 log.debug('Auth type is for headers only and plugin `%s` is not '
710 709 'headers plugin, skipping...', plugin.get_id())
711 710 continue
712 711
713 712 log.debug('Trying authentication using ** %s **', plugin.get_id())
714 713
715 714 # load plugin settings from RhodeCode database
716 715 plugin_settings = plugin.get_settings()
717 716 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
718 717 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
719 718
720 719 # use plugin's method of user extraction.
721 720 user = plugin.get_user(username, environ=environ,
722 721 settings=plugin_settings)
723 722 display_user = user.username if user else username
724 723 log.debug(
725 724 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
726 725
727 726 if not plugin.allows_authentication_from(user):
728 727 log.debug('Plugin %s does not accept user `%s` for authentication',
729 728 plugin.get_id(), display_user)
730 729 continue
731 730 else:
732 731 log.debug('Plugin %s accepted user `%s` for authentication',
733 732 plugin.get_id(), display_user)
734 733
735 734 log.info('Authenticating user `%s` using %s plugin',
736 735 display_user, plugin.get_id())
737 736
738 737 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
739 738
740 739 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
741 740 plugin.get_id(), plugin_cache_active, cache_ttl)
742 741
743 742 user_id = user.user_id if user else 'no-user'
744 743 # don't cache for empty users
745 744 plugin_cache_active = plugin_cache_active and user_id
746 745 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
747 746 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
748 747
749 748 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
750 749 expiration_time=cache_ttl,
751 750 condition=plugin_cache_active)
752 751 def compute_auth(
753 752 cache_name, plugin_name, username, password):
754 753
755 754 # _authenticate is a wrapper for .auth() method of plugin.
756 755 # it checks if .auth() sends proper data.
757 756 # For RhodeCodeExternalAuthPlugin it also maps users to
758 757 # Database and maps the attributes returned from .auth()
759 758 # to RhodeCode database. If this function returns data
760 759 # then auth is correct.
761 760 log.debug('Running plugin `%s` _authenticate method '
762 761 'using username and password', plugin.get_id())
763 762 return plugin._authenticate(
764 763 user, username, password, plugin_settings,
765 764 environ=environ or {})
766 765
767 766 start = time.time()
768 767 # for environ based auth, password can be empty, but then the validation is
769 768 # on the server that fills in the env data needed for authentication
770 769 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
771 770
772 771 auth_time = time.time() - start
773 772 log.debug('Authentication for plugin `%s` completed in %.4fs, '
774 773 'expiration time of fetched cache %.1fs.',
775 774 plugin.get_id(), auth_time, cache_ttl,
776 775 extra={"plugin": plugin.get_id(), "time": auth_time})
777 776
778 777 log.debug('PLUGIN USER DATA: %s', plugin_user)
779 778
780 779 statsd = StatsdClient.statsd
781 780
782 781 if plugin_user:
783 782 log.debug('Plugin returned proper authentication data')
784 783 if statsd:
785 784 elapsed_time_ms = round(1000.0 * auth_time) # use ms only
786 785 statsd.incr('rhodecode_login_success_total')
787 786 statsd.timing("rhodecode_login_timing.histogram", elapsed_time_ms,
788 787 tags=["plugin:{}".format(plugin.get_id())],
789 788 use_decimals=False
790 789 )
791 790 return plugin_user
792 791
793 792 # we failed to Auth because .auth() method didn't return proper user
794 793 log.debug("User `%s` failed to authenticate against %s",
795 794 display_user, plugin.get_id())
796 795 if statsd:
797 796 statsd.incr('rhodecode_login_fail_total')
798 797
799 798 # case when we failed to authenticate against all defined plugins
800 799 return None
801 800
802 801
803 802 def chop_at(s, sub, inclusive=False):
804 803 """Truncate string ``s`` at the first occurrence of ``sub``.
805 804
806 805 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
807 806
808 807 >>> chop_at("plutocratic brats", "rat")
809 808 'plutoc'
810 809 >>> chop_at("plutocratic brats", "rat", True)
811 810 'plutocrat'
812 811 """
813 812 pos = s.find(sub)
814 813 if pos == -1:
815 814 return s
816 815 if inclusive:
817 816 return s[:pos+len(sub)]
818 817 return s[:pos]
@@ -1,29 +1,29 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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 from zope.interface import Interface
22 22
23 23
24 24 class IAuthnPluginRegistry(Interface):
25 25 """
26 26 Interface for the authentication plugin registry. Currently this is only
27 27 used to register and retrieve it via pyramids registry.
28 28 """
29 29 pass
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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/
@@ -1,295 +1,295 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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 Atlassian CROWD
23 23 """
24 24
25 25
26 26 import colander
27 27 import base64
28 28 import logging
29 29 import urllib.request, urllib.error, urllib.parse
30 30
31 31 from rhodecode.translation import _
32 32 from rhodecode.authentication.base import (
33 33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 36 from rhodecode.lib.colander_utils import strip_whitespace
37 37 from rhodecode.lib.ext_json import json, formatted_json
38 38 from rhodecode.model.db import User
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 def plugin_factory(plugin_id, *args, **kwargs):
44 44 """
45 45 Factory function that is called during plugin discovery.
46 46 It returns the plugin instance.
47 47 """
48 48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 49 return plugin
50 50
51 51
52 52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 53 pass
54 54
55 55
56 56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 57 host = colander.SchemaNode(
58 58 colander.String(),
59 59 default='127.0.0.1',
60 60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 61 preparer=strip_whitespace,
62 62 title=_('Host'),
63 63 widget='string')
64 64 port = colander.SchemaNode(
65 65 colander.Int(),
66 66 default=8095,
67 67 description=_('The Port in use by the Atlassian CROWD Server'),
68 68 preparer=strip_whitespace,
69 69 title=_('Port'),
70 70 validator=colander.Range(min=0, max=65536),
71 71 widget='int')
72 72 app_name = colander.SchemaNode(
73 73 colander.String(),
74 74 default='',
75 75 description=_('The Application Name to authenticate to CROWD'),
76 76 preparer=strip_whitespace,
77 77 title=_('Application Name'),
78 78 widget='string')
79 79 app_password = colander.SchemaNode(
80 80 colander.String(),
81 81 default='',
82 82 description=_('The password to authenticate to CROWD'),
83 83 preparer=strip_whitespace,
84 84 title=_('Application Password'),
85 85 widget='password')
86 86 admin_groups = colander.SchemaNode(
87 87 colander.String(),
88 88 default='',
89 89 description=_('A comma separated list of group names that identify '
90 90 'users as RhodeCode Administrators'),
91 91 missing='',
92 92 preparer=strip_whitespace,
93 93 title=_('Admin Groups'),
94 94 widget='string')
95 95
96 96
97 97 class CrowdServer(object):
98 98 def __init__(self, *args, **kwargs):
99 99 """
100 100 Create a new CrowdServer object that points to IP/Address 'host',
101 101 on the given port, and using the given method (https/http). user and
102 102 passwd can be set here or with set_credentials. If unspecified,
103 103 "version" defaults to "latest".
104 104
105 105 example::
106 106
107 107 cserver = CrowdServer(host="127.0.0.1",
108 108 port="8095",
109 109 user="some_app",
110 110 passwd="some_passwd",
111 111 version="1")
112 112 """
113 113 if not "port" in kwargs:
114 114 kwargs["port"] = "8095"
115 115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 117 kwargs.get("host", "127.0.0.1"),
118 118 kwargs.get("port", "8095"))
119 119 self.set_credentials(kwargs.get("user", ""),
120 120 kwargs.get("passwd", ""))
121 121 self._version = kwargs.get("version", "latest")
122 122 self._url_list = None
123 123 self._appname = "crowd"
124 124
125 125 def set_credentials(self, user, passwd):
126 126 self.user = user
127 127 self.passwd = passwd
128 128 self._make_opener()
129 129
130 130 def _make_opener(self):
131 131 mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
132 132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 133 handler = urllib.request.HTTPBasicAuthHandler(mgr)
134 134 self.opener = urllib.request.build_opener(handler)
135 135
136 136 def _request(self, url, body=None, headers=None,
137 137 method=None, noformat=False,
138 138 empty_response_ok=False):
139 139 _headers = {"Content-type": "application/json",
140 140 "Accept": "application/json"}
141 141 if self.user and self.passwd:
142 142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 143 _headers["Authorization"] = "Basic %s" % authstring
144 144 if headers:
145 145 _headers.update(headers)
146 146 log.debug("Sent crowd: \n%s"
147 147 % (formatted_json({"url": url, "body": body,
148 148 "headers": _headers})))
149 149 request = urllib.request.Request(url, body, _headers)
150 150 if method:
151 151 request.get_method = lambda: method
152 152
153 153 global msg
154 154 msg = ""
155 155 try:
156 156 ret_doc = self.opener.open(request)
157 157 msg = ret_doc.read()
158 158 if not msg and empty_response_ok:
159 159 ret_val = {}
160 160 ret_val["status"] = True
161 161 ret_val["error"] = "Response body was empty"
162 162 elif not noformat:
163 163 ret_val = json.loads(msg)
164 164 ret_val["status"] = True
165 165 else:
166 166 ret_val = msg
167 167 except Exception as e:
168 168 if not noformat:
169 169 ret_val = {"status": False,
170 170 "body": body,
171 171 "error": "{}\n{}".format(e, msg)}
172 172 else:
173 173 ret_val = None
174 174 return ret_val
175 175
176 176 def user_auth(self, username, password):
177 177 """Authenticate a user against crowd. Returns brief information about
178 178 the user."""
179 179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 180 % (self._uri, self._version, username))
181 181 body = json.dumps({"value": password})
182 182 return self._request(url, body)
183 183
184 184 def user_groups(self, username):
185 185 """Retrieve a list of groups to which this user belongs."""
186 186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 187 % (self._uri, self._version, username))
188 188 return self._request(url)
189 189
190 190
191 191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 192 uid = 'crowd'
193 193 _settings_unsafe_keys = ['app_password']
194 194
195 195 def includeme(self, config):
196 196 config.add_authn_plugin(self)
197 197 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
198 198 config.add_view(
199 199 'rhodecode.authentication.views.AuthnPluginViewBase',
200 200 attr='settings_get',
201 201 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
202 202 request_method='GET',
203 203 route_name='auth_home',
204 204 context=CrowdAuthnResource)
205 205 config.add_view(
206 206 'rhodecode.authentication.views.AuthnPluginViewBase',
207 207 attr='settings_post',
208 208 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
209 209 request_method='POST',
210 210 route_name='auth_home',
211 211 context=CrowdAuthnResource)
212 212
213 213 def get_settings_schema(self):
214 214 return CrowdSettingsSchema()
215 215
216 216 def get_display_name(self, load_from_settings=False):
217 217 return _('CROWD')
218 218
219 219 @classmethod
220 220 def docs(cls):
221 221 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
222 222
223 223 @hybrid_property
224 224 def name(self):
225 225 return u"crowd"
226 226
227 227 def use_fake_password(self):
228 228 return True
229 229
230 230 def user_activation_state(self):
231 231 def_user_perms = User.get_default_user().AuthUser().permissions['global']
232 232 return 'hg.extern_activate.auto' in def_user_perms
233 233
234 234 def auth(self, userobj, username, password, settings, **kwargs):
235 235 """
236 236 Given a user object (which may be null), username, a plaintext password,
237 237 and a settings object (containing all the keys needed as listed in settings()),
238 238 authenticate this user's login attempt.
239 239
240 240 Return None on failure. On success, return a dictionary of the form:
241 241
242 242 see: RhodeCodeAuthPluginBase.auth_func_attrs
243 243 This is later validated for correctness
244 244 """
245 245 if not username or not password:
246 246 log.debug('Empty username or password skipping...')
247 247 return None
248 248
249 249 log.debug("Crowd settings: \n%s", formatted_json(settings))
250 250 server = CrowdServer(**settings)
251 251 server.set_credentials(settings["app_name"], settings["app_password"])
252 252 crowd_user = server.user_auth(username, password)
253 253 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
254 254 if not crowd_user["status"]:
255 255 return None
256 256
257 257 res = server.user_groups(crowd_user["name"])
258 258 log.debug("Crowd groups: \n%s", formatted_json(res))
259 259 crowd_user["groups"] = [x["name"] for x in res["groups"]]
260 260
261 261 # old attrs fetched from RhodeCode database
262 262 admin = getattr(userobj, 'admin', False)
263 263 active = getattr(userobj, 'active', True)
264 264 email = getattr(userobj, 'email', '')
265 265 username = getattr(userobj, 'username', username)
266 266 firstname = getattr(userobj, 'firstname', '')
267 267 lastname = getattr(userobj, 'lastname', '')
268 268 extern_type = getattr(userobj, 'extern_type', '')
269 269
270 270 user_attrs = {
271 271 'username': username,
272 272 'firstname': crowd_user["first-name"] or firstname,
273 273 'lastname': crowd_user["last-name"] or lastname,
274 274 'groups': crowd_user["groups"],
275 275 'user_group_sync': True,
276 276 'email': crowd_user["email"] or email,
277 277 'admin': admin,
278 278 'active': active,
279 279 'active_from_extern': crowd_user.get('active'),
280 280 'extern_name': crowd_user["name"],
281 281 'extern_type': extern_type,
282 282 }
283 283
284 284 # set an admin if we're in admin_groups of crowd
285 285 for group in settings["admin_groups"]:
286 286 if group in user_attrs["groups"]:
287 287 user_attrs["admin"] = True
288 288 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
289 289 log.info('user `%s` authenticated correctly', user_attrs['username'])
290 290 return user_attrs
291 291
292 292
293 293 def includeme(config):
294 294 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
295 295 plugin_factory(plugin_id).includeme(config)
@@ -1,232 +1,232 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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 rhodecode.translation import _
25 25 from rhodecode.authentication.base import (
26 26 RhodeCodeExternalAuthPlugin, hybrid_property)
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
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwargs):
38 38 """
39 39 Factory function that is called during plugin discovery.
40 40 It returns the plugin instance.
41 41 """
42 42 plugin = RhodeCodeAuthPlugin(plugin_id)
43 43 return plugin
44 44
45 45
46 46 class HeadersAuthnResource(AuthnPluginResourceBase):
47 47 pass
48 48
49 49
50 50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 51 header = colander.SchemaNode(
52 52 colander.String(),
53 53 default='REMOTE_USER',
54 54 description=_('Header to extract the user from'),
55 55 preparer=strip_whitespace,
56 56 title=_('Header'),
57 57 widget='string')
58 58 fallback_header = colander.SchemaNode(
59 59 colander.String(),
60 60 default='HTTP_X_FORWARDED_USER',
61 61 description=_('Header to extract the user from when main one fails'),
62 62 preparer=strip_whitespace,
63 63 title=_('Fallback header'),
64 64 widget='string')
65 65 clean_username = colander.SchemaNode(
66 66 colander.Boolean(),
67 67 default=True,
68 68 description=_('Perform cleaning of user, if passed user has @ in '
69 69 'username then first part before @ is taken. '
70 70 'If there\'s \\ in the username only the part after '
71 71 ' \\ is taken'),
72 72 missing=False,
73 73 title=_('Clean username'),
74 74 widget='bool')
75 75
76 76
77 77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78 78 uid = 'headers'
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.mako',
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.mako',
94 94 request_method='POST',
95 95 route_name='auth_home',
96 96 context=HeadersAuthnResource)
97 97
98 98 def get_display_name(self, load_from_settings=False):
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 u"headers"
107 107
108 108 @property
109 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 'user_group_sync': False,
217 217 'email': email or '',
218 218 'admin': admin or False,
219 219 'active': active,
220 220 'active_from_extern': True,
221 221 'extern_name': username,
222 222 'extern_type': extern_type,
223 223 }
224 224
225 225 log.info('user `%s` authenticated correctly', user_attrs['username'],
226 226 extra={"action": "user_auth_ok", "auth_module": "auth_headers", "username": user_attrs["username"]})
227 227 return user_attrs
228 228
229 229
230 230 def includeme(config):
231 231 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
232 232 plugin_factory(plugin_id).includeme(config)
@@ -1,173 +1,173 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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.request, urllib.parse, urllib.error
31 31 import urllib.request, urllib.error, urllib.parse
32 32
33 33 from rhodecode.translation import _
34 34 from rhodecode.authentication.base import (
35 35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 38 from rhodecode.lib.colander_utils import strip_whitespace
39 39 from rhodecode.lib.utils2 import safe_unicode
40 40 from rhodecode.model.db import User
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 def plugin_factory(plugin_id, *args, **kwargs):
46 46 """
47 47 Factory function that is called during plugin discovery.
48 48 It returns the plugin instance.
49 49 """
50 50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 51 return plugin
52 52
53 53
54 54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 55 pass
56 56
57 57
58 58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 59 service_url = colander.SchemaNode(
60 60 colander.String(),
61 61 default='https://domain.com/cas/v1/tickets',
62 62 description=_('The url of the Jasig CAS REST service'),
63 63 preparer=strip_whitespace,
64 64 title=_('URL'),
65 65 widget='string')
66 66
67 67
68 68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69 69 uid = 'jasig_cas'
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.mako',
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.mako',
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, load_from_settings=False):
93 93 return _('Jasig-CAS')
94 94
95 95 @hybrid_property
96 96 def name(self):
97 97 return u"jasig-cas"
98 98
99 99 @property
100 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.parse.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 = urllib.request.Request(url, params, headers)
135 135 try:
136 136 response = urllib.request.urlopen(request)
137 137 except urllib.error.HTTPError as e:
138 138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)", e.code)
139 139 return None
140 140 except urllib.error.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 'user_group_sync': False,
159 159 'email': email or '',
160 160 'admin': admin or False,
161 161 'active': active,
162 162 'active_from_extern': True,
163 163 'extern_name': username,
164 164 'extern_type': extern_type,
165 165 }
166 166
167 167 log.info('user `%s` authenticated correctly', user_attrs['username'])
168 168 return user_attrs
169 169
170 170
171 171 def includeme(config):
172 172 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
173 173 plugin_factory(plugin_id).includeme(config)
@@ -1,552 +1,551 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 """
22 21 RhodeCode authentication plugin for LDAP
23 22 """
24 23
25 24 import logging
26 25 import traceback
27 26
28 27 import colander
29 28 from rhodecode.translation import _
30 29 from rhodecode.authentication.base import (
31 30 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
32 31 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 32 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 33 from rhodecode.lib.colander_utils import strip_whitespace
35 34 from rhodecode.lib.exceptions import (
36 35 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 36 )
38 37 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 38 from rhodecode.model.db import User
40 39 from rhodecode.model.validators import Missing
41 40
42 41 log = logging.getLogger(__name__)
43 42
44 43 try:
45 44 import ldap
46 45 except ImportError:
47 46 # means that python-ldap is not installed, we use Missing object to mark
48 47 # ldap lib is Missing
49 48 ldap = Missing
50 49
51 50
52 51 class LdapError(Exception):
53 52 pass
54 53
55 54
56 55 def plugin_factory(plugin_id, *args, **kwargs):
57 56 """
58 57 Factory function that is called during plugin discovery.
59 58 It returns the plugin instance.
60 59 """
61 60 plugin = RhodeCodeAuthPlugin(plugin_id)
62 61 return plugin
63 62
64 63
65 64 class LdapAuthnResource(AuthnPluginResourceBase):
66 65 pass
67 66
68 67
69 68 class AuthLdap(AuthLdapBase):
70 69 default_tls_cert_dir = '/etc/openldap/cacerts'
71 70
72 71 scope_labels = {
73 72 ldap.SCOPE_BASE: 'SCOPE_BASE',
74 73 ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
75 74 ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
76 75 }
77 76
78 77 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
79 78 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
80 79 tls_cert_dir=None, ldap_version=3,
81 80 search_scope='SUBTREE', attr_login='uid',
82 81 ldap_filter='', timeout=None):
83 82 if ldap == Missing:
84 83 raise LdapImportError("Missing or incompatible ldap library")
85 84
86 85 self.debug = False
87 86 self.timeout = timeout or 60 * 5
88 87 self.ldap_version = ldap_version
89 88 self.ldap_server_type = 'ldap'
90 89
91 90 self.TLS_KIND = tls_kind
92 91
93 92 if self.TLS_KIND == 'LDAPS':
94 93 port = port or 636
95 94 self.ldap_server_type += 's'
96 95
97 96 OPT_X_TLS_DEMAND = 2
98 97 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
99 98 self.TLS_CERT_FILE = tls_cert_file or ''
100 99 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
101 100
102 101 # split server into list
103 102 self.SERVER_ADDRESSES = self._get_server_list(server)
104 103 self.LDAP_SERVER_PORT = port
105 104
106 105 # USE FOR READ ONLY BIND TO LDAP SERVER
107 106 self.attr_login = attr_login
108 107
109 108 self.LDAP_BIND_DN = safe_str(bind_dn)
110 109 self.LDAP_BIND_PASS = safe_str(bind_pass)
111 110
112 111 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
113 112 self.BASE_DN = safe_str(base_dn)
114 113 self.LDAP_FILTER = safe_str(ldap_filter)
115 114
116 115 def _get_ldap_conn(self):
117 116
118 117 if self.debug:
119 118 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
120 119
121 120 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
122 121 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
123 122
124 123 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
125 124 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
126 125
127 126 if self.TLS_KIND != 'PLAIN':
128 127 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
129 128
130 129 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
131 130 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
132 131
133 132 # init connection now
134 133 ldap_servers = self._build_servers(
135 134 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
136 135 log.debug('initializing LDAP connection to:%s', ldap_servers)
137 136 ldap_conn = ldap.initialize(ldap_servers)
138 137 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
139 138 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
140 139 ldap_conn.timeout = self.timeout
141 140
142 141 if self.ldap_version == 2:
143 142 ldap_conn.protocol = ldap.VERSION2
144 143 else:
145 144 ldap_conn.protocol = ldap.VERSION3
146 145
147 146 if self.TLS_KIND == 'START_TLS':
148 147 ldap_conn.start_tls_s()
149 148
150 149 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
151 150 log.debug('Trying simple_bind with password and given login DN: %r',
152 151 self.LDAP_BIND_DN)
153 152 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
154 153 log.debug('simple_bind successful')
155 154 return ldap_conn
156 155
157 156 def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
158 157 scope = ldap.SCOPE_BASE
159 158 scope_label = self.scope_labels.get(scope)
160 159 ldap_filter = '(objectClass=*)'
161 160
162 161 try:
163 162 log.debug('Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)',
164 163 dn, scope_label, ldap_filter)
165 164 ldap_conn.simple_bind_s(dn, safe_str(password))
166 165 response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=['*', '+'])
167 166
168 167 if not response:
169 168 log.error('search bind returned empty results: %r', response)
170 169 return {}
171 170 else:
172 171 _dn, attrs = response[0]
173 172 return attrs
174 173
175 174 except ldap.INVALID_CREDENTIALS:
176 175 log.debug("LDAP rejected password for user '%s': %s, org_exc:",
177 176 username, dn, exc_info=True)
178 177
179 178 def authenticate_ldap(self, username, password):
180 179 """
181 180 Authenticate a user via LDAP and return his/her LDAP properties.
182 181
183 182 Raises AuthenticationError if the credentials are rejected, or
184 183 EnvironmentError if the LDAP server can't be reached.
185 184
186 185 :param username: username
187 186 :param password: password
188 187 """
189 188
190 189 uid = self.get_uid(username, self.SERVER_ADDRESSES)
191 190 user_attrs = {}
192 191 dn = ''
193 192
194 193 self.validate_password(username, password)
195 194 self.validate_username(username)
196 195 scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
197 196
198 197 ldap_conn = None
199 198 try:
200 199 ldap_conn = self._get_ldap_conn()
201 200 filter_ = '(&%s(%s=%s))' % (
202 201 self.LDAP_FILTER, self.attr_login, username)
203 202 log.debug("Authenticating %r filter %s and scope: %s",
204 203 self.BASE_DN, filter_, scope_label)
205 204
206 205 ldap_objects = ldap_conn.search_ext_s(
207 206 self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=['*', '+'])
208 207
209 208 if not ldap_objects:
210 209 log.debug("No matching LDAP objects for authentication "
211 210 "of UID:'%s' username:(%s)", uid, username)
212 211 raise ldap.NO_SUCH_OBJECT()
213 212
214 213 log.debug('Found %s matching ldap object[s], trying to authenticate on each one now...', len(ldap_objects))
215 214 for (dn, _attrs) in ldap_objects:
216 215 if dn is None:
217 216 continue
218 217
219 218 user_attrs = self.fetch_attrs_from_simple_bind(
220 219 ldap_conn, dn, username, password)
221 220
222 221 if user_attrs:
223 222 log.debug('Got authenticated user attributes from DN:%s', dn)
224 223 break
225 224 else:
226 225 raise LdapPasswordError(
227 226 'Failed to authenticate user `{}` with given password'.format(username))
228 227
229 228 except ldap.NO_SUCH_OBJECT:
230 229 log.debug("LDAP says no such user '%s' (%s), org_exc:",
231 230 uid, username, exc_info=True)
232 231 raise LdapUsernameError('Unable to find user')
233 232 except ldap.SERVER_DOWN:
234 233 org_exc = traceback.format_exc()
235 234 raise LdapConnectionError(
236 235 "LDAP can't access authentication server, org_exc:%s" % org_exc)
237 236 finally:
238 237 if ldap_conn:
239 238 log.debug('ldap: connection release')
240 239 try:
241 240 ldap_conn.unbind_s()
242 241 except Exception:
243 242 # for any reason this can raise exception we must catch it
244 243 # to not crush the server
245 244 pass
246 245
247 246 return dn, user_attrs
248 247
249 248
250 249 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
251 250 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
252 251 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
253 252 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
254 253
255 254 host = colander.SchemaNode(
256 255 colander.String(),
257 256 default='',
258 257 description=_('Host[s] of the LDAP Server \n'
259 258 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
260 259 'Multiple servers can be specified using commas'),
261 260 preparer=strip_whitespace,
262 261 title=_('LDAP Host'),
263 262 widget='string')
264 263 port = colander.SchemaNode(
265 264 colander.Int(),
266 265 default=389,
267 266 description=_('Custom port that the LDAP server is listening on. '
268 267 'Default value is: 389, use 636 for LDAPS (SSL)'),
269 268 preparer=strip_whitespace,
270 269 title=_('Port'),
271 270 validator=colander.Range(min=0, max=65536),
272 271 widget='int')
273 272
274 273 timeout = colander.SchemaNode(
275 274 colander.Int(),
276 275 default=60 * 5,
277 276 description=_('Timeout for LDAP connection'),
278 277 preparer=strip_whitespace,
279 278 title=_('Connection timeout'),
280 279 validator=colander.Range(min=1),
281 280 widget='int')
282 281
283 282 dn_user = colander.SchemaNode(
284 283 colander.String(),
285 284 default='',
286 285 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
287 286 'e.g., cn=admin,dc=mydomain,dc=com, or '
288 287 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
289 288 missing='',
290 289 preparer=strip_whitespace,
291 290 title=_('Bind account'),
292 291 widget='string')
293 292 dn_pass = colander.SchemaNode(
294 293 colander.String(),
295 294 default='',
296 295 description=_('Password to authenticate for given user DN.'),
297 296 missing='',
298 297 preparer=strip_whitespace,
299 298 title=_('Bind account password'),
300 299 widget='password')
301 300 tls_kind = colander.SchemaNode(
302 301 colander.String(),
303 302 default=tls_kind_choices[0],
304 303 description=_('TLS Type'),
305 304 title=_('Connection Security'),
306 305 validator=colander.OneOf(tls_kind_choices),
307 306 widget='select')
308 307 tls_reqcert = colander.SchemaNode(
309 308 colander.String(),
310 309 default=tls_reqcert_choices[0],
311 310 description=_('Require Cert over TLS?. Self-signed and custom '
312 311 'certificates can be used when\n `RhodeCode Certificate` '
313 312 'found in admin > settings > system info page is extended.'),
314 313 title=_('Certificate Checks'),
315 314 validator=colander.OneOf(tls_reqcert_choices),
316 315 widget='select')
317 316 tls_cert_file = colander.SchemaNode(
318 317 colander.String(),
319 318 default='',
320 319 description=_('This specifies the PEM-format file path containing '
321 320 'certificates for use in TLS connection.\n'
322 321 'If not specified `TLS Cert dir` will be used'),
323 322 title=_('TLS Cert file'),
324 323 missing='',
325 324 widget='string')
326 325 tls_cert_dir = colander.SchemaNode(
327 326 colander.String(),
328 327 default=AuthLdap.default_tls_cert_dir,
329 328 description=_('This specifies the path of a directory that contains individual '
330 329 'CA certificates in separate files.'),
331 330 title=_('TLS Cert dir'),
332 331 widget='string')
333 332 base_dn = colander.SchemaNode(
334 333 colander.String(),
335 334 default='',
336 335 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
337 336 'in it to be replaced with current user username \n'
338 337 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
339 338 missing='',
340 339 preparer=strip_whitespace,
341 340 title=_('Base DN'),
342 341 widget='string')
343 342 filter = colander.SchemaNode(
344 343 colander.String(),
345 344 default='',
346 345 description=_('Filter to narrow results \n'
347 346 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
348 347 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
349 348 missing='',
350 349 preparer=strip_whitespace,
351 350 title=_('LDAP Search Filter'),
352 351 widget='string')
353 352
354 353 search_scope = colander.SchemaNode(
355 354 colander.String(),
356 355 default=search_scope_choices[2],
357 356 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
358 357 title=_('LDAP Search Scope'),
359 358 validator=colander.OneOf(search_scope_choices),
360 359 widget='select')
361 360 attr_login = colander.SchemaNode(
362 361 colander.String(),
363 362 default='uid',
364 363 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
365 364 preparer=strip_whitespace,
366 365 title=_('Login Attribute'),
367 366 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
368 367 widget='string')
369 368 attr_email = colander.SchemaNode(
370 369 colander.String(),
371 370 default='',
372 371 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
373 372 'Emails are a crucial part of RhodeCode. \n'
374 373 'If possible add a valid email attribute to ldap users.'),
375 374 missing='',
376 375 preparer=strip_whitespace,
377 376 title=_('Email Attribute'),
378 377 widget='string')
379 378 attr_firstname = colander.SchemaNode(
380 379 colander.String(),
381 380 default='',
382 381 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
383 382 missing='',
384 383 preparer=strip_whitespace,
385 384 title=_('First Name Attribute'),
386 385 widget='string')
387 386 attr_lastname = colander.SchemaNode(
388 387 colander.String(),
389 388 default='',
390 389 description=_('LDAP Attribute to map to last name (e.g., sn)'),
391 390 missing='',
392 391 preparer=strip_whitespace,
393 392 title=_('Last Name Attribute'),
394 393 widget='string')
395 394
396 395
397 396 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
398 397 uid = 'ldap'
399 398 # used to define dynamic binding in the
400 399 DYNAMIC_BIND_VAR = '$login'
401 400 _settings_unsafe_keys = ['dn_pass']
402 401
403 402 def includeme(self, config):
404 403 config.add_authn_plugin(self)
405 404 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
406 405 config.add_view(
407 406 'rhodecode.authentication.views.AuthnPluginViewBase',
408 407 attr='settings_get',
409 408 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
410 409 request_method='GET',
411 410 route_name='auth_home',
412 411 context=LdapAuthnResource)
413 412 config.add_view(
414 413 'rhodecode.authentication.views.AuthnPluginViewBase',
415 414 attr='settings_post',
416 415 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
417 416 request_method='POST',
418 417 route_name='auth_home',
419 418 context=LdapAuthnResource)
420 419
421 420 def get_settings_schema(self):
422 421 return LdapSettingsSchema()
423 422
424 423 def get_display_name(self, load_from_settings=False):
425 424 return _('LDAP')
426 425
427 426 @classmethod
428 427 def docs(cls):
429 428 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
430 429
431 430 @hybrid_property
432 431 def name(self):
433 432 return u"ldap"
434 433
435 434 def use_fake_password(self):
436 435 return True
437 436
438 437 def user_activation_state(self):
439 438 def_user_perms = User.get_default_user().AuthUser().permissions['global']
440 439 return 'hg.extern_activate.auto' in def_user_perms
441 440
442 441 def try_dynamic_binding(self, username, password, current_args):
443 442 """
444 443 Detects marker inside our original bind, and uses dynamic auth if
445 444 present
446 445 """
447 446
448 447 org_bind = current_args['bind_dn']
449 448 passwd = current_args['bind_pass']
450 449
451 450 def has_bind_marker(username):
452 451 if self.DYNAMIC_BIND_VAR in username:
453 452 return True
454 453
455 454 # we only passed in user with "special" variable
456 455 if org_bind and has_bind_marker(org_bind) and not passwd:
457 456 log.debug('Using dynamic user/password binding for ldap '
458 457 'authentication. Replacing `%s` with username',
459 458 self.DYNAMIC_BIND_VAR)
460 459 current_args['bind_dn'] = org_bind.replace(
461 460 self.DYNAMIC_BIND_VAR, username)
462 461 current_args['bind_pass'] = password
463 462
464 463 return current_args
465 464
466 465 def auth(self, userobj, username, password, settings, **kwargs):
467 466 """
468 467 Given a user object (which may be null), username, a plaintext password,
469 468 and a settings object (containing all the keys needed as listed in
470 469 settings()), authenticate this user's login attempt.
471 470
472 471 Return None on failure. On success, return a dictionary of the form:
473 472
474 473 see: RhodeCodeAuthPluginBase.auth_func_attrs
475 474 This is later validated for correctness
476 475 """
477 476
478 477 if not username or not password:
479 478 log.debug('Empty username or password skipping...')
480 479 return None
481 480
482 481 ldap_args = {
483 482 'server': settings.get('host', ''),
484 483 'base_dn': settings.get('base_dn', ''),
485 484 'port': settings.get('port'),
486 485 'bind_dn': settings.get('dn_user'),
487 486 'bind_pass': settings.get('dn_pass'),
488 487 'tls_kind': settings.get('tls_kind'),
489 488 'tls_reqcert': settings.get('tls_reqcert'),
490 489 'tls_cert_file': settings.get('tls_cert_file'),
491 490 'tls_cert_dir': settings.get('tls_cert_dir'),
492 491 'search_scope': settings.get('search_scope'),
493 492 'attr_login': settings.get('attr_login'),
494 493 'ldap_version': 3,
495 494 'ldap_filter': settings.get('filter'),
496 495 'timeout': settings.get('timeout')
497 496 }
498 497
499 498 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
500 499
501 500 log.debug('Checking for ldap authentication.')
502 501
503 502 try:
504 503 aldap = AuthLdap(**ldap_args)
505 504 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
506 505 log.debug('Got ldap DN response %s', user_dn)
507 506
508 507 def get_ldap_attr(k):
509 508 return ldap_attrs.get(settings.get(k), [''])[0]
510 509
511 510 # old attrs fetched from RhodeCode database
512 511 admin = getattr(userobj, 'admin', False)
513 512 active = getattr(userobj, 'active', True)
514 513 email = getattr(userobj, 'email', '')
515 514 username = getattr(userobj, 'username', username)
516 515 firstname = getattr(userobj, 'firstname', '')
517 516 lastname = getattr(userobj, 'lastname', '')
518 517 extern_type = getattr(userobj, 'extern_type', '')
519 518
520 519 groups = []
521 520
522 521 user_attrs = {
523 522 'username': username,
524 523 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
525 524 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
526 525 'groups': groups,
527 526 'user_group_sync': False,
528 527 'email': get_ldap_attr('attr_email') or email,
529 528 'admin': admin,
530 529 'active': active,
531 530 'active_from_extern': None,
532 531 'extern_name': user_dn,
533 532 'extern_type': extern_type,
534 533 }
535 534
536 535 log.debug('ldap user: %s', user_attrs)
537 536 log.info('user `%s` authenticated correctly', user_attrs['username'],
538 537 extra={"action": "user_auth_ok", "auth_module": "auth_ldap", "username": user_attrs["username"]})
539 538
540 539 return user_attrs
541 540
542 541 except (LdapUsernameError, LdapPasswordError, LdapImportError):
543 542 log.exception("LDAP related exception")
544 543 return None
545 544 except (Exception,):
546 545 log.exception("Other exception")
547 546 return None
548 547
549 548
550 549 def includeme(config):
551 550 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
552 551 plugin_factory(plugin_id).includeme(config)
@@ -1,172 +1,172 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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 library for PAM
23 23 """
24 24
25 25 import colander
26 26 import grp
27 27 import logging
28 28 import pam
29 29 import pwd
30 30 import re
31 31 import socket
32 32
33 33 from rhodecode.translation import _
34 34 from rhodecode.authentication.base import (
35 35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 38 from rhodecode.lib.colander_utils import strip_whitespace
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 def plugin_factory(plugin_id, *args, **kwargs):
44 44 """
45 45 Factory function that is called during plugin discovery.
46 46 It returns the plugin instance.
47 47 """
48 48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 49 return plugin
50 50
51 51
52 52 class PamAuthnResource(AuthnPluginResourceBase):
53 53 pass
54 54
55 55
56 56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
57 57 service = colander.SchemaNode(
58 58 colander.String(),
59 59 default='login',
60 60 description=_('PAM service name to use for authentication.'),
61 61 preparer=strip_whitespace,
62 62 title=_('PAM service name'),
63 63 widget='string')
64 64 gecos = colander.SchemaNode(
65 65 colander.String(),
66 66 default=r'(?P<last_name>.+),\s*(?P<first_name>\w+)',
67 67 description=_('Regular expression for extracting user name/email etc. '
68 68 'from Unix userinfo.'),
69 69 preparer=strip_whitespace,
70 70 title=_('Gecos Regex'),
71 71 widget='string')
72 72
73 73
74 74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
75 75 uid = 'pam'
76 76 # PAM authentication can be slow. Repository operations involve a lot of
77 77 # auth calls. Little caching helps speedup push/pull operations significantly
78 78 AUTH_CACHE_TTL = 4
79 79
80 80 def includeme(self, config):
81 81 config.add_authn_plugin(self)
82 82 config.add_authn_resource(self.get_id(), PamAuthnResource(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.mako',
87 87 request_method='GET',
88 88 route_name='auth_home',
89 89 context=PamAuthnResource)
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.mako',
94 94 request_method='POST',
95 95 route_name='auth_home',
96 96 context=PamAuthnResource)
97 97
98 98 def get_display_name(self, load_from_settings=False):
99 99 return _('PAM')
100 100
101 101 @classmethod
102 102 def docs(cls):
103 103 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
104 104
105 105 @hybrid_property
106 106 def name(self):
107 107 return u"pam"
108 108
109 109 def get_settings_schema(self):
110 110 return PamSettingsSchema()
111 111
112 112 def use_fake_password(self):
113 113 return True
114 114
115 115 def auth(self, userobj, username, password, settings, **kwargs):
116 116 if not username or not password:
117 117 log.debug('Empty username or password skipping...')
118 118 return None
119 119 _pam = pam.pam()
120 120 auth_result = _pam.authenticate(username, password, settings["service"])
121 121
122 122 if not auth_result:
123 123 log.error("PAM was unable to authenticate user: %s", username)
124 124 return None
125 125
126 126 log.debug('Got PAM response %s', auth_result)
127 127
128 128 # old attrs fetched from RhodeCode database
129 129 default_email = "%s@%s" % (username, socket.gethostname())
130 130 admin = getattr(userobj, 'admin', False)
131 131 active = getattr(userobj, 'active', True)
132 132 email = getattr(userobj, 'email', '') or default_email
133 133 username = getattr(userobj, 'username', username)
134 134 firstname = getattr(userobj, 'firstname', '')
135 135 lastname = getattr(userobj, 'lastname', '')
136 136 extern_type = getattr(userobj, 'extern_type', '')
137 137
138 138 user_attrs = {
139 139 'username': username,
140 140 'firstname': firstname,
141 141 'lastname': lastname,
142 142 'groups': [g.gr_name for g in grp.getgrall()
143 143 if username in g.gr_mem],
144 144 'user_group_sync': True,
145 145 'email': email,
146 146 'admin': admin,
147 147 'active': active,
148 148 'active_from_extern': None,
149 149 'extern_name': username,
150 150 'extern_type': extern_type,
151 151 }
152 152
153 153 try:
154 154 user_data = pwd.getpwnam(username)
155 155 regex = settings["gecos"]
156 156 match = re.search(regex, user_data.pw_gecos)
157 157 if match:
158 158 user_attrs["firstname"] = match.group('first_name')
159 159 user_attrs["lastname"] = match.group('last_name')
160 160 except Exception:
161 161 log.warning("Cannot extract additional info for PAM user")
162 162 pass
163 163
164 164 log.debug("pamuser: %s", user_attrs)
165 165 log.info('user `%s` authenticated correctly', user_attrs['username'],
166 166 extra={"action": "user_auth_ok", "auth_module": "auth_pam", "username": user_attrs["username"]})
167 167 return user_attrs
168 168
169 169
170 170 def includeme(config):
171 171 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
172 172 plugin_factory(plugin_id).includeme(config)
@@ -1,222 +1,222 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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 built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 27 import colander
28 28
29 29 from rhodecode.translation import _
30 30 from rhodecode.lib.utils2 import safe_str
31 31 from rhodecode.model.db import User
32 32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 33 from rhodecode.authentication.base import (
34 34 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
35 35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 def plugin_factory(plugin_id, *args, **kwargs):
41 41 plugin = RhodeCodeAuthPlugin(plugin_id)
42 42 return plugin
43 43
44 44
45 45 class RhodecodeAuthnResource(AuthnPluginResourceBase):
46 46 pass
47 47
48 48
49 49 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
50 50 uid = 'rhodecode'
51 51 AUTH_RESTRICTION_NONE = 'user_all'
52 52 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
53 53 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
54 54 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
55 55 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
56 56
57 57 def includeme(self, config):
58 58 config.add_authn_plugin(self)
59 59 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
60 60 config.add_view(
61 61 'rhodecode.authentication.views.AuthnPluginViewBase',
62 62 attr='settings_get',
63 63 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 64 request_method='GET',
65 65 route_name='auth_home',
66 66 context=RhodecodeAuthnResource)
67 67 config.add_view(
68 68 'rhodecode.authentication.views.AuthnPluginViewBase',
69 69 attr='settings_post',
70 70 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
71 71 request_method='POST',
72 72 route_name='auth_home',
73 73 context=RhodecodeAuthnResource)
74 74
75 75 def get_settings_schema(self):
76 76 return RhodeCodeSettingsSchema()
77 77
78 78 def get_display_name(self, load_from_settings=False):
79 79 return _('RhodeCode Internal')
80 80
81 81 @classmethod
82 82 def docs(cls):
83 83 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
84 84
85 85 @hybrid_property
86 86 def name(self):
87 87 return u"rhodecode"
88 88
89 89 def user_activation_state(self):
90 90 def_user_perms = User.get_default_user().AuthUser().permissions['global']
91 91 return 'hg.register.auto_activate' in def_user_perms
92 92
93 93 def allows_authentication_from(
94 94 self, user, allows_non_existing_user=True,
95 95 allowed_auth_plugins=None, allowed_auth_sources=None):
96 96 """
97 97 Custom method for this auth that doesn't accept non existing users.
98 98 We know that user exists in our database.
99 99 """
100 100 allows_non_existing_user = False
101 101 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
102 102 user, allows_non_existing_user=allows_non_existing_user)
103 103
104 104 def auth(self, userobj, username, password, settings, **kwargs):
105 105 if not userobj:
106 106 log.debug('userobj was:%s skipping', userobj)
107 107 return None
108 108
109 109 if userobj.extern_type != self.name:
110 110 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
111 111 userobj, userobj.extern_type, self.name)
112 112 return None
113 113
114 114 # check scope of auth
115 115 scope_restriction = settings.get('scope_restriction', '')
116 116
117 117 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
118 118 and self.auth_type != HTTP_TYPE:
119 119 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
120 120 userobj, self.auth_type, scope_restriction)
121 121 return None
122 122
123 123 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
124 124 and self.auth_type != VCS_TYPE:
125 125 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
126 126 userobj, self.auth_type, scope_restriction)
127 127 return None
128 128
129 129 # check super-admin restriction
130 130 auth_restriction = settings.get('auth_restriction', '')
131 131
132 132 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
133 133 and userobj.admin is False:
134 134 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
135 135 userobj, auth_restriction)
136 136 return None
137 137
138 138 user_attrs = {
139 139 "username": userobj.username,
140 140 "firstname": userobj.firstname,
141 141 "lastname": userobj.lastname,
142 142 "groups": [],
143 143 'user_group_sync': False,
144 144 "email": userobj.email,
145 145 "admin": userobj.admin,
146 146 "active": userobj.active,
147 147 "active_from_extern": userobj.active,
148 148 "extern_name": userobj.user_id,
149 149 "extern_type": userobj.extern_type,
150 150 }
151 151
152 152 log.debug("User attributes:%s", user_attrs)
153 153 if userobj.active:
154 154 from rhodecode.lib import auth
155 155 crypto_backend = auth.crypto_backend()
156 156 password_encoded = safe_str(password)
157 157 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
158 158 password_encoded, userobj.password or '')
159 159
160 160 if password_match and new_hash:
161 161 log.debug('user %s properly authenticated, but '
162 162 'requires hash change to bcrypt', userobj)
163 163 # if password match, and we use OLD deprecated hash,
164 164 # we should migrate this user hash password to the new hash
165 165 # we store the new returned by hash_check_with_upgrade function
166 166 user_attrs['_hash_migrate'] = new_hash
167 167
168 168 if userobj.username == User.DEFAULT_USER and userobj.active:
169 169 log.info('user `%s` authenticated correctly as anonymous user',
170 170 userobj.username,
171 171 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
172 172 return user_attrs
173 173
174 174 elif userobj.username == username and password_match:
175 175 log.info('user `%s` authenticated correctly', userobj.username,
176 176 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
177 177 return user_attrs
178 178 log.warning("user `%s` used a wrong password when "
179 179 "authenticating on this plugin", userobj.username)
180 180 return None
181 181 else:
182 182 log.warning('user `%s` failed to authenticate via %s, reason: account not '
183 183 'active.', username, self.name)
184 184 return None
185 185
186 186
187 187 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
188 188
189 189 auth_restriction_choices = [
190 190 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
191 191 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
192 192 ]
193 193
194 194 auth_scope_choices = [
195 195 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
196 196 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
197 197 ]
198 198
199 199 auth_restriction = colander.SchemaNode(
200 200 colander.String(),
201 201 default=auth_restriction_choices[0],
202 202 description=_('Allowed user types for authentication using this plugin.'),
203 203 title=_('User restriction'),
204 204 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
205 205 widget='select_with_labels',
206 206 choices=auth_restriction_choices
207 207 )
208 208 scope_restriction = colander.SchemaNode(
209 209 colander.String(),
210 210 default=auth_scope_choices[0],
211 211 description=_('Allowed protocols for authentication using this plugin. '
212 212 'VCS means GIT/HG/SVN. HTTP is web based login.'),
213 213 title=_('Scope restriction'),
214 214 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
215 215 widget='select_with_labels',
216 216 choices=auth_scope_choices
217 217 )
218 218
219 219
220 220 def includeme(config):
221 221 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
222 222 plugin_factory(plugin_id).includeme(config)
@@ -1,177 +1,177 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 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 token plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26 import colander
27 27
28 28 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
29 29 from rhodecode.translation import _
30 30 from rhodecode.authentication.base import (
31 31 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
32 32 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 33 from rhodecode.model.db import User, UserApiKeys, Repository
34 34
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 def plugin_factory(plugin_id, *args, **kwargs):
40 40 plugin = RhodeCodeAuthPlugin(plugin_id)
41 41 return plugin
42 42
43 43
44 44 class RhodecodeAuthnResource(AuthnPluginResourceBase):
45 45 pass
46 46
47 47
48 48 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
49 49 """
50 50 Enables usage of authentication tokens for vcs operations.
51 51 """
52 52 uid = 'token'
53 53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
54 54
55 55 def includeme(self, config):
56 56 config.add_authn_plugin(self)
57 57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
58 58 config.add_view(
59 59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 60 attr='settings_get',
61 61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 62 request_method='GET',
63 63 route_name='auth_home',
64 64 context=RhodecodeAuthnResource)
65 65 config.add_view(
66 66 'rhodecode.authentication.views.AuthnPluginViewBase',
67 67 attr='settings_post',
68 68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
69 69 request_method='POST',
70 70 route_name='auth_home',
71 71 context=RhodecodeAuthnResource)
72 72
73 73 def get_settings_schema(self):
74 74 return RhodeCodeSettingsSchema()
75 75
76 76 def get_display_name(self, load_from_settings=False):
77 77 return _('Rhodecode Token')
78 78
79 79 @classmethod
80 80 def docs(cls):
81 81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-token.html"
82 82
83 83 @hybrid_property
84 84 def name(self):
85 85 return u"authtoken"
86 86
87 87 def user_activation_state(self):
88 88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
89 89 return 'hg.register.auto_activate' in def_user_perms
90 90
91 91 def allows_authentication_from(
92 92 self, user, allows_non_existing_user=True,
93 93 allowed_auth_plugins=None, allowed_auth_sources=None):
94 94 """
95 95 Custom method for this auth that doesn't accept empty users. And also
96 96 allows users from all other active plugins to use it and also
97 97 authenticate against it. But only via vcs mode
98 98 """
99 99 from rhodecode.authentication.base import get_authn_registry
100 100 authn_registry = get_authn_registry()
101 101
102 102 active_plugins = set(
103 103 [x.name for x in authn_registry.get_plugins_for_authentication()])
104 104 active_plugins.discard(self.name)
105 105
106 106 allowed_auth_plugins = [self.name] + list(active_plugins)
107 107 # only for vcs operations
108 108 allowed_auth_sources = [VCS_TYPE]
109 109
110 110 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
111 111 user, allows_non_existing_user=False,
112 112 allowed_auth_plugins=allowed_auth_plugins,
113 113 allowed_auth_sources=allowed_auth_sources)
114 114
115 115 def auth(self, userobj, username, password, settings, **kwargs):
116 116 if not userobj:
117 117 log.debug('userobj was:%s skipping', userobj)
118 118 return None
119 119
120 120 user_attrs = {
121 121 "username": userobj.username,
122 122 "firstname": userobj.firstname,
123 123 "lastname": userobj.lastname,
124 124 "groups": [],
125 125 'user_group_sync': False,
126 126 "email": userobj.email,
127 127 "admin": userobj.admin,
128 128 "active": userobj.active,
129 129 "active_from_extern": userobj.active,
130 130 "extern_name": userobj.user_id,
131 131 "extern_type": userobj.extern_type,
132 132 }
133 133
134 134 log.debug('Authenticating user with args %s', user_attrs)
135 135 if userobj.active:
136 136 # calling context repo for token scopes
137 137 scope_repo_id = None
138 138 if self.acl_repo_name:
139 139 repo = Repository.get_by_repo_name(self.acl_repo_name)
140 140 scope_repo_id = repo.repo_id if repo else None
141 141
142 142 token_match = userobj.authenticate_by_token(
143 143 password, roles=[UserApiKeys.ROLE_VCS],
144 144 scope_repo_id=scope_repo_id)
145 145
146 146 if userobj.username == username and token_match:
147 147 log.info(
148 148 'user `%s` successfully authenticated via %s',
149 149 user_attrs['username'], self.name)
150 150 return user_attrs
151 151 log.warning('user `%s` failed to authenticate via %s, reason: bad or '
152 152 'inactive token.', username, self.name)
153 153 else:
154 154 log.warning('user `%s` failed to authenticate via %s, reason: account not '
155 155 'active.', username, self.name)
156 156 return None
157 157
158 158
159 159 def includeme(config):
160 160 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
161 161 plugin_factory(plugin_id).includeme(config)
162 162
163 163
164 164 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
165 165 auth_scope_choices = [
166 166 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS, 'VCS only'),
167 167 ]
168 168
169 169 scope_restriction = colander.SchemaNode(
170 170 colander.String(),
171 171 default=auth_scope_choices[0],
172 172 description=_('Choose operation scope restriction when authenticating.'),
173 173 title=_('Scope restriction'),
174 174 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
175 175 widget='select_with_labels',
176 176 choices=auth_scope_choices
177 177 )
@@ -1,121 +1,121 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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 time
22 22 import logging
23 23
24 24 from pyramid.exceptions import ConfigurationError
25 25 from zope.interface import implementer
26 26
27 27 from rhodecode.authentication.interface import IAuthnPluginRegistry
28 28 from rhodecode.model.settings import SettingsModel
29 29 from rhodecode.lib.utils2 import safe_str
30 30 from rhodecode.lib.statsd_client import StatsdClient
31 31 from rhodecode.lib import rc_cache
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 @implementer(IAuthnPluginRegistry)
37 37 class AuthenticationPluginRegistry(object):
38 38
39 39 # INI settings key to set a fallback authentication plugin.
40 40 fallback_plugin_key = 'rhodecode.auth_plugin_fallback'
41 41
42 42 def __init__(self, settings):
43 43 self._plugins = {}
44 44 self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
45 45
46 46 def add_authn_plugin(self, config, plugin):
47 47 plugin_id = plugin.get_id()
48 48 if plugin_id in self._plugins.keys():
49 49 raise ConfigurationError(
50 50 'Cannot register authentication plugin twice: "%s"', plugin_id)
51 51 else:
52 52 log.debug('Register authentication plugin: "%s"', plugin_id)
53 53 self._plugins[plugin_id] = plugin
54 54
55 55 def get_plugins(self):
56 56 def sort_key(plugin):
57 57 return str.lower(safe_str(plugin.get_display_name()))
58 58
59 59 return sorted(self._plugins.values(), key=sort_key)
60 60
61 61 def get_plugin(self, plugin_id):
62 62 return self._plugins.get(plugin_id, None)
63 63
64 64 def get_plugin_by_uid(self, plugin_uid):
65 65 for plugin in self._plugins.values():
66 66 if plugin.uid == plugin_uid:
67 67 return plugin
68 68
69 69 def get_plugins_for_authentication(self, cache=True):
70 70 """
71 71 Returns a list of plugins which should be consulted when authenticating
72 72 a user. It only returns plugins which are enabled and active.
73 73 Additionally it includes the fallback plugin from the INI file, if
74 74 `rhodecode.auth_plugin_fallback` is set to a plugin ID.
75 75 """
76 76
77 77 cache_namespace_uid = 'cache_auth_plugins'
78 78 region = rc_cache.get_or_create_region('cache_general', cache_namespace_uid)
79 79
80 80 @region.conditional_cache_on_arguments(condition=cache)
81 81 def _get_auth_plugins(name, key, fallback_plugin):
82 82 plugins = []
83 83
84 84 # Add all enabled and active plugins to the list. We iterate over the
85 85 # auth_plugins setting from DB because it also represents the ordering.
86 86 enabled_plugins = SettingsModel().get_auth_plugins()
87 87 raw_settings = SettingsModel().get_all_settings(cache=False)
88 88 for plugin_id in enabled_plugins:
89 89 plugin = self.get_plugin(plugin_id)
90 90 if plugin is not None and plugin.is_active(
91 91 plugin_cached_settings=raw_settings):
92 92
93 93 # inject settings into plugin, we can re-use the DB fetched settings here
94 94 plugin._settings = plugin._propagate_settings(raw_settings)
95 95 plugins.append(plugin)
96 96
97 97 # Add the fallback plugin from ini file.
98 98 if fallback_plugin:
99 99 log.warning(
100 100 'Using fallback authentication plugin from INI file: "%s"',
101 101 fallback_plugin)
102 102 plugin = self.get_plugin(fallback_plugin)
103 103 if plugin is not None and plugin not in plugins:
104 104 plugin._settings = plugin._propagate_settings(raw_settings)
105 105 plugins.append(plugin)
106 106 return plugins
107 107
108 108 start = time.time()
109 109 plugins = _get_auth_plugins('rhodecode_auth_plugins', 'v1', self._fallback_plugin)
110 110
111 111 compute_time = time.time() - start
112 112 log.debug('cached method:%s took %.4fs', _get_auth_plugins.__name__, compute_time)
113 113
114 114 statsd = StatsdClient.statsd
115 115 if statsd:
116 116 elapsed_time_ms = round(1000.0 * compute_time) # use ms only
117 117 statsd.timing("rhodecode_auth_plugins_timing.histogram", elapsed_time_ms,
118 118 use_decimals=False)
119 119
120 120 return plugins
121 121
@@ -1,155 +1,155 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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 import collections
23 23
24 24 from pyramid.exceptions import ConfigurationError
25 25
26 26 from rhodecode.lib.utils2 import safe_str
27 27 from rhodecode.model.settings import SettingsModel
28 28 from rhodecode.translation import _
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class AuthnResourceBase(object):
35 35 __name__ = None
36 36 __parent__ = None
37 37
38 38 def get_root(self):
39 39 current = self
40 40 while current.__parent__ is not None:
41 41 current = current.__parent__
42 42 return current
43 43
44 44
45 45 class AuthnPluginResourceBase(AuthnResourceBase):
46 46
47 47 def __init__(self, plugin):
48 48 self.plugin = plugin
49 49 self.__name__ = plugin.get_url_slug()
50 50 self.display_name = plugin.get_display_name()
51 51
52 52
53 53 class AuthnRootResource(AuthnResourceBase):
54 54 """
55 55 This is the root traversal resource object for the authentication settings.
56 56 """
57 57
58 58 def __init__(self):
59 59 self._store = collections.OrderedDict()
60 60 self._resource_name_map = {}
61 61 self.display_name = _('Authentication Plugins')
62 62
63 63 def __getitem__(self, key):
64 64 """
65 65 Customized get item function to return only items (plugins) that are
66 66 activated.
67 67 """
68 68 if self._is_item_active(key):
69 69 return self._store[key]
70 70 else:
71 71 raise KeyError('Authentication plugin "{}" is not active.'.format(
72 72 key))
73 73
74 74 def __iter__(self):
75 75 for key in self._store.keys():
76 76 if self._is_item_active(key):
77 77 yield self._store[key]
78 78
79 79 def _is_item_active(self, key):
80 80 activated_plugins = SettingsModel().get_auth_plugins()
81 81 plugin_id = self.get_plugin_id(key)
82 82 return plugin_id in activated_plugins
83 83
84 84 def get_plugin_id(self, resource_name):
85 85 """
86 86 Return the plugin id for the given traversal resource name.
87 87 """
88 88 # TODO: Store this info in the resource element.
89 89 return self._resource_name_map[resource_name]
90 90
91 91 def get_sorted_list(self, sort_key=None):
92 92 """
93 93 Returns a sorted list of sub resources for displaying purposes.
94 94 """
95 95 def default_sort_key(resource):
96 96 return str.lower(safe_str(resource.display_name))
97 97
98 98 active = [item for item in self]
99 99 return sorted(active, key=sort_key or default_sort_key)
100 100
101 101 def get_nav_list(self, sort=True):
102 102 """
103 103 Returns a sorted list of resources for displaying the navigation.
104 104 """
105 105 if sort:
106 106 nav_list = self.get_sorted_list()
107 107 else:
108 108 nav_list = [item for item in self]
109 109
110 110 nav_list.insert(0, self)
111 111 return nav_list
112 112
113 113 def add_authn_resource(self, config, plugin_id, resource):
114 114 """
115 115 Register a traversal resource as a sub element to the authentication
116 116 settings. This method is registered as a directive on the pyramid
117 117 configurator object and called by plugins.
118 118 """
119 119
120 120 def _ensure_unique_name(name, limit=100):
121 121 counter = 1
122 122 current = name
123 123 while current in self._store.keys():
124 124 current = '{}{}'.format(name, counter)
125 125 counter += 1
126 126 if counter > limit:
127 127 raise ConfigurationError(
128 128 'Cannot build unique name for traversal resource "%s" '
129 129 'registered by plugin "%s"', name, plugin_id)
130 130 return current
131 131
132 132 # Allow plugin resources with identical names by rename duplicates.
133 133 unique_name = _ensure_unique_name(resource.__name__)
134 134 if unique_name != resource.__name__:
135 135 log.warning('Name collision for traversal resource "%s" registered '
136 136 'by authentication plugin "%s"', resource.__name__,
137 137 plugin_id)
138 138 resource.__name__ = unique_name
139 139
140 140 log.debug('Register traversal resource "%s" for plugin "%s"',
141 141 unique_name, plugin_id)
142 142 self._resource_name_map[unique_name] = plugin_id
143 143 resource.__parent__ = self
144 144 self._store[unique_name] = resource
145 145
146 146
147 147 root = AuthnRootResource()
148 148
149 149
150 150 def root_factory(request=None):
151 151 """
152 152 Returns the root traversal resource instance used for the authentication
153 153 settings route.
154 154 """
155 155 return root
@@ -1,52 +1,52 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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
23 23 from rhodecode.authentication import plugin_default_auth_ttl
24 24 from rhodecode.translation import _
25 25
26 26
27 27 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
28 28 """
29 29 This base schema is intended for use in authentication plugins.
30 30 It adds a few default settings (e.g., "enabled"), so that plugin
31 31 authors don't have to maintain a bunch of boilerplate.
32 32 """
33 33 enabled = colander.SchemaNode(
34 34 colander.Bool(),
35 35 default=False,
36 36 description=_('Enable or disable this authentication plugin.'),
37 37 missing=False,
38 38 title=_('Enabled'),
39 39 widget='bool',
40 40 )
41 41 cache_ttl = colander.SchemaNode(
42 42 colander.Int(),
43 43 default=plugin_default_auth_ttl,
44 44 description=_('Amount of seconds to cache the authentication and '
45 45 'permissions check response call for this plugin. \n'
46 46 'Useful for expensive calls like LDAP to improve the '
47 47 'performance of the system (0 means disabled).'),
48 48 missing=0,
49 49 title=_('Auth Cache TTL'),
50 50 validator=colander.Range(min=0, max=None),
51 51 widget='int',
52 52 )
@@ -1,89 +1,89 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 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 import pytest
23 23
24 24
25 25 class EnabledAuthPlugin(object):
26 26 """
27 27 Context manager that updates the 'auth_plugins' setting in DB to enable
28 28 a plugin. Previous setting is restored on exit. The rhodecode auth plugin
29 29 is also enabled because it is needed to login the test users.
30 30 """
31 31
32 32 def __init__(self, plugin):
33 33 self.new_value = {'egg:rhodecode-enterprise-ce#rhodecode', plugin.get_id()}
34 34
35 35 def __enter__(self):
36 36 from rhodecode.model.settings import SettingsModel
37 37 self._old_value = SettingsModel().get_auth_plugins()
38 38 SettingsModel().create_or_update_setting(
39 39 'auth_plugins', ','.join(self.new_value))
40 40
41 41 def __exit__(self, type, value, traceback):
42 42 from rhodecode.model.settings import SettingsModel
43 43 SettingsModel().create_or_update_setting(
44 44 'auth_plugins', ','.join(self._old_value))
45 45
46 46
47 47 class DisabledAuthPlugin(object):
48 48 """
49 49 Context manager that updates the 'auth_plugins' setting in DB to disable
50 50 a plugin. Previous setting is restored on exit.
51 51 """
52 52
53 53 def __init__(self, plugin):
54 54 self.plugin_id = plugin.get_id()
55 55
56 56 def __enter__(self):
57 57 from rhodecode.model.settings import SettingsModel
58 58 self._old_value = SettingsModel().get_auth_plugins()
59 59 new_value = [id_ for id_ in self._old_value if id_ != self.plugin_id]
60 60 SettingsModel().create_or_update_setting(
61 61 'auth_plugins', ','.join(new_value))
62 62
63 63 def __exit__(self, type, value, traceback):
64 64 from rhodecode.model.settings import SettingsModel
65 65 SettingsModel().create_or_update_setting(
66 66 'auth_plugins', ','.join(self._old_value))
67 67
68 68
69 69 @pytest.fixture(params=[
70 70 ('rhodecode.authentication.plugins.auth_crowd', 'egg:rhodecode-enterprise-ce#crowd'),
71 71 ('rhodecode.authentication.plugins.auth_headers', 'egg:rhodecode-enterprise-ce#headers'),
72 72 ('rhodecode.authentication.plugins.auth_jasig_cas', 'egg:rhodecode-enterprise-ce#jasig_cas'),
73 73 ('rhodecode.authentication.plugins.auth_ldap', 'egg:rhodecode-enterprise-ce#ldap'),
74 74 ('rhodecode.authentication.plugins.auth_pam', 'egg:rhodecode-enterprise-ce#pam'),
75 75 ('rhodecode.authentication.plugins.auth_rhodecode', 'egg:rhodecode-enterprise-ce#rhodecode'),
76 76 ('rhodecode.authentication.plugins.auth_token', 'egg:rhodecode-enterprise-ce#token'),
77 77 ])
78 78 def auth_plugin(request):
79 79 """
80 80 Fixture that provides instance for each authentication plugin. These
81 81 instances are NOT the instances which are registered to the authentication
82 82 registry.
83 83 """
84 84 from importlib import import_module
85 85
86 86 # Create plugin instance.
87 87 module, plugin_id = request.param
88 88 plugin_module = import_module(module)
89 89 return plugin_module.plugin_factory(plugin_id)
@@ -1,77 +1,77 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 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 import pytest
23 23
24 24 from rhodecode.authentication.tests.conftest import (
25 25 EnabledAuthPlugin, DisabledAuthPlugin)
26 26 from rhodecode.apps._base import ADMIN_PREFIX
27 27
28 28
29 29 @pytest.mark.usefixtures('autologin_user', 'app')
30 30 class TestAuthenticationSettings:
31 31
32 32 def test_auth_settings_global_view_get(self, app):
33 33 url = '{prefix}/auth/'.format(prefix=ADMIN_PREFIX)
34 34 response = app.get(url)
35 35 assert response.status_code == 200
36 36
37 37 def test_plugin_settings_view_get(self, app, auth_plugin):
38 38 url = '{prefix}/auth/{name}'.format(
39 39 prefix=ADMIN_PREFIX,
40 40 name=auth_plugin.name)
41 41 with EnabledAuthPlugin(auth_plugin):
42 42 response = app.get(url)
43 43 assert response.status_code == 200
44 44
45 45 def test_plugin_settings_view_post(self, app, auth_plugin, csrf_token):
46 46 url = '{prefix}/auth/{name}'.format(
47 47 prefix=ADMIN_PREFIX,
48 48 name=auth_plugin.name)
49 49 params = {
50 50 'enabled': True,
51 51 'cache_ttl': 0,
52 52 'csrf_token': csrf_token,
53 53 }
54 54 with EnabledAuthPlugin(auth_plugin):
55 55 response = app.post(url, params=params)
56 56 assert response.status_code in [200, 302]
57 57
58 58 def test_plugin_settings_view_get_404(self, app, auth_plugin):
59 59 url = '{prefix}/auth/{name}'.format(
60 60 prefix=ADMIN_PREFIX,
61 61 name=auth_plugin.name)
62 62 with DisabledAuthPlugin(auth_plugin):
63 63 response = app.get(url, status=404)
64 64 assert response.status_code == 404
65 65
66 66 def test_plugin_settings_view_post_404(self, app, auth_plugin, csrf_token):
67 67 url = '{prefix}/auth/{name}'.format(
68 68 prefix=ADMIN_PREFIX,
69 69 name=auth_plugin.name)
70 70 params = {
71 71 'enabled': True,
72 72 'cache_ttl': 0,
73 73 'csrf_token': csrf_token,
74 74 }
75 75 with DisabledAuthPlugin(auth_plugin):
76 76 response = app.post(url, params=params, status=404)
77 77 assert response.status_code == 404
@@ -1,180 +1,180 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 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 formencode.htmlfill
23 23 import logging
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import BaseAppView
30 30 from rhodecode.authentication.base import get_authn_registry
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 34 from rhodecode.model.forms import AuthSettingsForm
35 35 from rhodecode.model.meta import Session
36 36 from rhodecode.model.settings import SettingsModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class AuthnPluginViewBase(BaseAppView):
42 42
43 43 def load_default_context(self):
44 44 c = self._get_local_tmpl_context()
45 45 self.plugin = self.context.plugin
46 46 return c
47 47
48 48 @LoginRequired()
49 49 @HasPermissionAllDecorator('hg.admin')
50 50 def settings_get(self, defaults=None, errors=None):
51 51 """
52 52 View that displays the plugin settings as a form.
53 53 """
54 54 c = self.load_default_context()
55 55 defaults = defaults or {}
56 56 errors = errors or {}
57 57 schema = self.plugin.get_settings_schema()
58 58
59 59 # Compute default values for the form. Priority is:
60 60 # 1. Passed to this method 2. DB value 3. Schema default
61 61 for node in schema:
62 62 if node.name not in defaults:
63 63 defaults[node.name] = self.plugin.get_setting_by_name(
64 64 node.name, node.default)
65 65
66 66 template_context = {
67 67 'defaults': defaults,
68 68 'errors': errors,
69 69 'plugin': self.context.plugin,
70 70 'resource': self.context,
71 71 }
72 72
73 73 return self._get_template_context(c, **template_context)
74 74
75 75 @LoginRequired()
76 76 @HasPermissionAllDecorator('hg.admin')
77 77 @CSRFRequired()
78 78 def settings_post(self):
79 79 """
80 80 View that validates and stores the plugin settings.
81 81 """
82 82 _ = self.request.translate
83 83 self.load_default_context()
84 84 schema = self.plugin.get_settings_schema()
85 85 data = self.request.params
86 86
87 87 try:
88 88 valid_data = schema.deserialize(data)
89 89 except colander.Invalid as e:
90 90 # Display error message and display form again.
91 91 h.flash(
92 92 _('Errors exist when saving plugin settings. '
93 93 'Please check the form inputs.'),
94 94 category='error')
95 95 defaults = {key: data[key] for key in data if key in schema}
96 96 return self.settings_get(errors=e.asdict(), defaults=defaults)
97 97
98 98 # Store validated data.
99 99 for name, value in valid_data.items():
100 100 self.plugin.create_or_update_setting(name, value)
101 101 Session().commit()
102 102 SettingsModel().invalidate_settings_cache()
103 103
104 104 # Display success message and redirect.
105 105 h.flash(_('Auth settings updated successfully.'), category='success')
106 106 redirect_to = self.request.resource_path(self.context, route_name='auth_home')
107 107
108 108 return HTTPFound(redirect_to)
109 109
110 110
111 111 class AuthSettingsView(BaseAppView):
112 112 def load_default_context(self):
113 113 c = self._get_local_tmpl_context()
114 114 return c
115 115
116 116 @LoginRequired()
117 117 @HasPermissionAllDecorator('hg.admin')
118 118 def index(self, defaults=None, errors=None, prefix_error=False):
119 119 c = self.load_default_context()
120 120
121 121 defaults = defaults or {}
122 122 authn_registry = get_authn_registry(self.request.registry)
123 123 enabled_plugins = SettingsModel().get_auth_plugins()
124 124
125 125 # Create template context and render it.
126 126 template_context = {
127 127 'resource': self.context,
128 128 'available_plugins': authn_registry.get_plugins(),
129 129 'enabled_plugins': enabled_plugins,
130 130 }
131 131 html = render('rhodecode:templates/admin/auth/auth_settings.mako',
132 132 self._get_template_context(c, **template_context),
133 133 self.request)
134 134
135 135 # Create form default values and fill the form.
136 136 form_defaults = {
137 137 'auth_plugins': ',\n'.join(enabled_plugins)
138 138 }
139 139 form_defaults.update(defaults)
140 140 html = formencode.htmlfill.render(
141 141 html,
142 142 defaults=form_defaults,
143 143 errors=errors,
144 144 prefix_error=prefix_error,
145 145 encoding="UTF-8",
146 146 force_defaults=False)
147 147
148 148 return Response(html)
149 149
150 150 @LoginRequired()
151 151 @HasPermissionAllDecorator('hg.admin')
152 152 @CSRFRequired()
153 153 def auth_settings(self):
154 154 _ = self.request.translate
155 155 try:
156 156 form = AuthSettingsForm(self.request.translate)()
157 157 form_result = form.to_python(self.request.POST)
158 158 plugins = ','.join(form_result['auth_plugins'])
159 159 setting = SettingsModel().create_or_update_setting(
160 160 'auth_plugins', plugins)
161 161 Session().add(setting)
162 162 Session().commit()
163 163 SettingsModel().invalidate_settings_cache()
164 164 h.flash(_('Auth settings updated successfully.'), category='success')
165 165 except formencode.Invalid as errors:
166 166 e = errors.error_dict or {}
167 167 h.flash(_('Errors exist when saving plugin setting. '
168 168 'Please check the form inputs.'), category='error')
169 169 return self.index(
170 170 defaults=errors.value,
171 171 errors=e,
172 172 prefix_error=False)
173 173 except Exception:
174 174 log.exception('Exception in auth_settings')
175 175 h.flash(_('Error occurred during update of auth settings.'),
176 176 category='error')
177 177
178 178 redirect_to = self.request.resource_path(self.context, route_name='auth_home')
179 179
180 180 return HTTPFound(redirect_to)
General Comments 0
You need to be logged in to leave comments. Login now