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