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