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