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