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