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