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