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