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