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