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