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