##// END OF EJS Templates
authn: Add an INI option to set an authentication plugin fallback. #3953...
johbo -
r52:a007b8c5 default
parent child Browse files
Show More
@@ -1,85 +1,85 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 pkg_resources import iter_entry_points
23 from pkg_resources import iter_entry_points
24 from pyramid.authentication import SessionAuthenticationPolicy
24 from pyramid.authentication import SessionAuthenticationPolicy
25
25
26 from rhodecode.authentication.registry import AuthenticationPluginRegistry
26 from rhodecode.authentication.registry import AuthenticationPluginRegistry
27 from rhodecode.authentication.routes import root_factory
27 from rhodecode.authentication.routes import root_factory
28 from rhodecode.authentication.routes import AuthnRootResource
28 from rhodecode.authentication.routes import AuthnRootResource
29 from rhodecode.config.routing import ADMIN_PREFIX
29 from rhodecode.config.routing import ADMIN_PREFIX
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 # TODO: Currently this is only used to discover the authentication plugins.
34 # TODO: Currently this is only used to discover the authentication plugins.
35 # Later on this may be used in a generic way to look up and include all kinds
35 # Later on this may be used in a generic way to look up and include all kinds
36 # of supported enterprise plugins. Therefore this has to be moved and
36 # of supported enterprise plugins. Therefore this has to be moved and
37 # refactored to a real 'plugin look up' machinery.
37 # refactored to a real 'plugin look up' machinery.
38 # TODO: When refactoring this think about splitting it up into distinct
38 # TODO: When refactoring this think about splitting it up into distinct
39 # discover, load and include phases.
39 # discover, load and include phases.
40 def _discover_plugins(config, entry_point='enterprise.plugins1'):
40 def _discover_plugins(config, entry_point='enterprise.plugins1'):
41 _discovered_plugins = {}
41 _discovered_plugins = {}
42
42
43 for ep in iter_entry_points(entry_point):
43 for ep in iter_entry_points(entry_point):
44 plugin_id = 'egg:{}#{}'.format(ep.dist.project_name, ep.name)
44 plugin_id = 'egg:{}#{}'.format(ep.dist.project_name, ep.name)
45 log.debug('Plugin discovered: "%s"', plugin_id)
45 log.debug('Plugin discovered: "%s"', plugin_id)
46 module = ep.load()
46 module = ep.load()
47 plugin = module(plugin_id=plugin_id)
47 plugin = module(plugin_id=plugin_id)
48 config.include(plugin.includeme)
48 config.include(plugin.includeme)
49
49
50 return _discovered_plugins
50 return _discovered_plugins
51
51
52
52
53 def includeme(config):
53 def includeme(config):
54 # Set authentication policy.
54 # Set authentication policy.
55 authn_policy = SessionAuthenticationPolicy()
55 authn_policy = SessionAuthenticationPolicy()
56 config.set_authentication_policy(authn_policy)
56 config.set_authentication_policy(authn_policy)
57
57
58 # Create authentication plugin registry and add it to the pyramid registry.
58 # Create authentication plugin registry and add it to the pyramid registry.
59 authn_registry = AuthenticationPluginRegistry()
59 authn_registry = AuthenticationPluginRegistry(config.get_settings())
60 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
60 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
61 config.registry.registerUtility(authn_registry)
61 config.registry.registerUtility(authn_registry)
62
62
63 # Create authentication traversal root resource.
63 # Create authentication traversal root resource.
64 authn_root_resource = root_factory()
64 authn_root_resource = root_factory()
65 config.add_directive('add_authn_resource',
65 config.add_directive('add_authn_resource',
66 authn_root_resource.add_authn_resource)
66 authn_root_resource.add_authn_resource)
67
67
68 # Add the authentication traversal route.
68 # Add the authentication traversal route.
69 config.add_route('auth_home',
69 config.add_route('auth_home',
70 ADMIN_PREFIX + '/auth*traverse',
70 ADMIN_PREFIX + '/auth*traverse',
71 factory=root_factory)
71 factory=root_factory)
72 # Add the authentication settings root views.
72 # Add the authentication settings root views.
73 config.add_view('rhodecode.authentication.views.AuthSettingsView',
73 config.add_view('rhodecode.authentication.views.AuthSettingsView',
74 attr='index',
74 attr='index',
75 request_method='GET',
75 request_method='GET',
76 route_name='auth_home',
76 route_name='auth_home',
77 context=AuthnRootResource)
77 context=AuthnRootResource)
78 config.add_view('rhodecode.authentication.views.AuthSettingsView',
78 config.add_view('rhodecode.authentication.views.AuthSettingsView',
79 attr='auth_settings',
79 attr='auth_settings',
80 request_method='POST',
80 request_method='POST',
81 route_name='auth_home',
81 route_name='auth_home',
82 context=AuthnRootResource)
82 context=AuthnRootResource)
83
83
84 # Auto discover authentication plugins and include their configuration.
84 # Auto discover authentication plugins and include their configuration.
85 _discover_plugins(config)
85 _discover_plugins(config)
@@ -1,624 +1,607 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 logging
25 import logging
26 import time
26 import time
27 import traceback
27 import traceback
28
28
29 from authomatic import Authomatic
30 from authomatic.adapters import WebObAdapter
31 from authomatic.providers import oauth2, oauth1
32 from pylons import url
33 from pylons.controllers.util import Response
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
29 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.ext.hybrid import hybrid_property
30 from sqlalchemy.ext.hybrid import hybrid_property
37
31
38 import rhodecode.lib.helpers as h
39 from rhodecode.authentication.interface import IAuthnPluginRegistry
32 from rhodecode.authentication.interface import IAuthnPluginRegistry
40 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
41 from rhodecode.lib import caches
34 from rhodecode.lib import caches
42 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
35 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
43 from rhodecode.lib.utils2 import md5_safe, safe_int
36 from rhodecode.lib.utils2 import md5_safe, safe_int
44 from rhodecode.lib.utils2 import safe_str
37 from rhodecode.lib.utils2 import safe_str
45 from rhodecode.model.db import User, ExternalIdentity
38 from rhodecode.model.db import User
46 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
47 from rhodecode.model.settings import SettingsModel
40 from rhodecode.model.settings import SettingsModel
48 from rhodecode.model.user import UserModel
41 from rhodecode.model.user import UserModel
49 from rhodecode.model.user_group import UserGroupModel
42 from rhodecode.model.user_group import UserGroupModel
50
43
51
44
52 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
53
46
54 # auth types that authenticate() function can receive
47 # auth types that authenticate() function can receive
55 VCS_TYPE = 'vcs'
48 VCS_TYPE = 'vcs'
56 HTTP_TYPE = 'http'
49 HTTP_TYPE = 'http'
57
50
58
51
59 class LazyFormencode(object):
52 class LazyFormencode(object):
60 def __init__(self, formencode_obj, *args, **kwargs):
53 def __init__(self, formencode_obj, *args, **kwargs):
61 self.formencode_obj = formencode_obj
54 self.formencode_obj = formencode_obj
62 self.args = args
55 self.args = args
63 self.kwargs = kwargs
56 self.kwargs = kwargs
64
57
65 def __call__(self, *args, **kwargs):
58 def __call__(self, *args, **kwargs):
66 from inspect import isfunction
59 from inspect import isfunction
67 formencode_obj = self.formencode_obj
60 formencode_obj = self.formencode_obj
68 if isfunction(formencode_obj):
61 if isfunction(formencode_obj):
69 # case we wrap validators into functions
62 # case we wrap validators into functions
70 formencode_obj = self.formencode_obj(*args, **kwargs)
63 formencode_obj = self.formencode_obj(*args, **kwargs)
71 return formencode_obj(*self.args, **self.kwargs)
64 return formencode_obj(*self.args, **self.kwargs)
72
65
73
66
74 class RhodeCodeAuthPluginBase(object):
67 class RhodeCodeAuthPluginBase(object):
75 # cache the authentication request for N amount of seconds. Some kind
68 # cache the authentication request for N amount of seconds. Some kind
76 # of authentication methods are very heavy and it's very efficient to cache
69 # of authentication methods are very heavy and it's very efficient to cache
77 # the result of a call. If it's set to None (default) cache is off
70 # the result of a call. If it's set to None (default) cache is off
78 AUTH_CACHE_TTL = None
71 AUTH_CACHE_TTL = None
79 AUTH_CACHE = {}
72 AUTH_CACHE = {}
80
73
81 auth_func_attrs = {
74 auth_func_attrs = {
82 "username": "unique username",
75 "username": "unique username",
83 "firstname": "first name",
76 "firstname": "first name",
84 "lastname": "last name",
77 "lastname": "last name",
85 "email": "email address",
78 "email": "email address",
86 "groups": '["list", "of", "groups"]',
79 "groups": '["list", "of", "groups"]',
87 "extern_name": "name in external source of record",
80 "extern_name": "name in external source of record",
88 "extern_type": "type of external source of record",
81 "extern_type": "type of external source of record",
89 "admin": 'True|False defines if user should be RhodeCode super admin',
82 "admin": 'True|False defines if user should be RhodeCode super admin',
90 "active":
83 "active":
91 'True|False defines active state of user internally for RhodeCode',
84 'True|False defines active state of user internally for RhodeCode',
92 "active_from_extern":
85 "active_from_extern":
93 "True|False\None, active state from the external auth, "
86 "True|False\None, active state from the external auth, "
94 "None means use definition from RhodeCode extern_type active value"
87 "None means use definition from RhodeCode extern_type active value"
95 }
88 }
96 # set on authenticate() method and via set_auth_type func.
89 # set on authenticate() method and via set_auth_type func.
97 auth_type = None
90 auth_type = None
98
91
99 # List of setting names to store encrypted. Plugins may override this list
92 # List of setting names to store encrypted. Plugins may override this list
100 # to store settings encrypted.
93 # to store settings encrypted.
101 _settings_encrypted = []
94 _settings_encrypted = []
102
95
103 # Mapping of python to DB settings model types. Plugins may override or
96 # Mapping of python to DB settings model types. Plugins may override or
104 # extend this mapping.
97 # extend this mapping.
105 _settings_type_map = {
98 _settings_type_map = {
106 str: 'str',
99 str: 'str',
107 int: 'int',
100 int: 'int',
108 unicode: 'unicode',
101 unicode: 'unicode',
109 bool: 'bool',
102 bool: 'bool',
110 list: 'list',
103 list: 'list',
111 }
104 }
112
105
113 def __init__(self, plugin_id):
106 def __init__(self, plugin_id):
114 self._plugin_id = plugin_id
107 self._plugin_id = plugin_id
115
108
116 def _get_setting_full_name(self, name):
109 def _get_setting_full_name(self, name):
117 """
110 """
118 Return the full setting name used for storing values in the database.
111 Return the full setting name used for storing values in the database.
119 """
112 """
120 # TODO: johbo: Using the name here is problematic. It would be good to
113 # TODO: johbo: Using the name here is problematic. It would be good to
121 # introduce either new models in the database to hold Plugin and
114 # introduce either new models in the database to hold Plugin and
122 # PluginSetting or to use the plugin id here.
115 # PluginSetting or to use the plugin id here.
123 return 'auth_{}_{}'.format(self.name, name)
116 return 'auth_{}_{}'.format(self.name, name)
124
117
125 def _get_setting_type(self, name, value):
118 def _get_setting_type(self, name, value):
126 """
119 """
127 Get the type as used by the SettingsModel accordingly to type of passed
120 Get the type as used by the SettingsModel accordingly to type of passed
128 value. Optionally the suffix `.encrypted` is appended to instruct
121 value. Optionally the suffix `.encrypted` is appended to instruct
129 SettingsModel to store it encrypted.
122 SettingsModel to store it encrypted.
130 """
123 """
131 type_ = self._settings_type_map.get(type(value), 'unicode')
124 type_ = self._settings_type_map.get(type(value), 'unicode')
132 if name in self._settings_encrypted:
125 if name in self._settings_encrypted:
133 type_ = '{}.encrypted'.format(type_)
126 type_ = '{}.encrypted'.format(type_)
134 return type_
127 return type_
135
128
136 def is_enabled(self):
129 def is_enabled(self):
137 """
130 """
138 Returns true if this plugin is enabled. An enabled plugin can be
131 Returns true if this plugin is enabled. An enabled plugin can be
139 configured in the admin interface but it is not consulted during
132 configured in the admin interface but it is not consulted during
140 authentication.
133 authentication.
141 """
134 """
142 auth_plugins = SettingsModel().get_auth_plugins()
135 auth_plugins = SettingsModel().get_auth_plugins()
143 return self.get_id() in auth_plugins
136 return self.get_id() in auth_plugins
144
137
145 def is_active(self):
138 def is_active(self):
146 """
139 """
147 Returns true if the plugin is activated. An activated plugin is
140 Returns true if the plugin is activated. An activated plugin is
148 consulted during authentication, assumed it is also enabled.
141 consulted during authentication, assumed it is also enabled.
149 """
142 """
150 return self.get_setting_by_name('enabled')
143 return self.get_setting_by_name('enabled')
151
144
152 def get_id(self):
145 def get_id(self):
153 """
146 """
154 Returns the plugin id.
147 Returns the plugin id.
155 """
148 """
156 return self._plugin_id
149 return self._plugin_id
157
150
158 def get_display_name(self):
151 def get_display_name(self):
159 """
152 """
160 Returns a translation string for displaying purposes.
153 Returns a translation string for displaying purposes.
161 """
154 """
162 raise NotImplementedError('Not implemented in base class')
155 raise NotImplementedError('Not implemented in base class')
163
156
164 def get_settings_schema(self):
157 def get_settings_schema(self):
165 """
158 """
166 Returns a colander schema, representing the plugin settings.
159 Returns a colander schema, representing the plugin settings.
167 """
160 """
168 return AuthnPluginSettingsSchemaBase()
161 return AuthnPluginSettingsSchemaBase()
169
162
170 def get_setting_by_name(self, name):
163 def get_setting_by_name(self, name):
171 """
164 """
172 Returns a plugin setting by name.
165 Returns a plugin setting by name.
173 """
166 """
174 full_name = self._get_setting_full_name(name)
167 full_name = self._get_setting_full_name(name)
175 db_setting = SettingsModel().get_setting_by_name(full_name)
168 db_setting = SettingsModel().get_setting_by_name(full_name)
176 return db_setting.app_settings_value if db_setting else None
169 return db_setting.app_settings_value if db_setting else None
177
170
178 def create_or_update_setting(self, name, value):
171 def create_or_update_setting(self, name, value):
179 """
172 """
180 Create or update a setting for this plugin in the persistent storage.
173 Create or update a setting for this plugin in the persistent storage.
181 """
174 """
182 full_name = self._get_setting_full_name(name)
175 full_name = self._get_setting_full_name(name)
183 type_ = self._get_setting_type(name, value)
176 type_ = self._get_setting_type(name, value)
184 db_setting = SettingsModel().create_or_update_setting(
177 db_setting = SettingsModel().create_or_update_setting(
185 full_name, value, type_)
178 full_name, value, type_)
186 return db_setting.app_settings_value
179 return db_setting.app_settings_value
187
180
188 def get_settings(self):
181 def get_settings(self):
189 """
182 """
190 Returns the plugin settings as dictionary.
183 Returns the plugin settings as dictionary.
191 """
184 """
192 settings = {}
185 settings = {}
193 for node in self.get_settings_schema():
186 for node in self.get_settings_schema():
194 settings[node.name] = self.get_setting_by_name(node.name)
187 settings[node.name] = self.get_setting_by_name(node.name)
195 return settings
188 return settings
196
189
197 @property
190 @property
198 def validators(self):
191 def validators(self):
199 """
192 """
200 Exposes RhodeCode validators modules
193 Exposes RhodeCode validators modules
201 """
194 """
202 # this is a hack to overcome issues with pylons threadlocals and
195 # this is a hack to overcome issues with pylons threadlocals and
203 # translator object _() not beein registered properly.
196 # translator object _() not beein registered properly.
204 class LazyCaller(object):
197 class LazyCaller(object):
205 def __init__(self, name):
198 def __init__(self, name):
206 self.validator_name = name
199 self.validator_name = name
207
200
208 def __call__(self, *args, **kwargs):
201 def __call__(self, *args, **kwargs):
209 from rhodecode.model import validators as v
202 from rhodecode.model import validators as v
210 obj = getattr(v, self.validator_name)
203 obj = getattr(v, self.validator_name)
211 # log.debug('Initializing lazy formencode object: %s', obj)
204 # log.debug('Initializing lazy formencode object: %s', obj)
212 return LazyFormencode(obj, *args, **kwargs)
205 return LazyFormencode(obj, *args, **kwargs)
213
206
214 class ProxyGet(object):
207 class ProxyGet(object):
215 def __getattribute__(self, name):
208 def __getattribute__(self, name):
216 return LazyCaller(name)
209 return LazyCaller(name)
217
210
218 return ProxyGet()
211 return ProxyGet()
219
212
220 @hybrid_property
213 @hybrid_property
221 def name(self):
214 def name(self):
222 """
215 """
223 Returns the name of this authentication plugin.
216 Returns the name of this authentication plugin.
224
217
225 :returns: string
218 :returns: string
226 """
219 """
227 raise NotImplementedError("Not implemented in base class")
220 raise NotImplementedError("Not implemented in base class")
228
221
229 @hybrid_property
222 @hybrid_property
230 def is_container_auth(self):
223 def is_container_auth(self):
231 """
224 """
232 Returns bool if this module uses container auth.
225 Returns bool if this module uses container auth.
233
226
234 This property will trigger an automatic call to authenticate on
227 This property will trigger an automatic call to authenticate on
235 a visit to the website or during a push/pull.
228 a visit to the website or during a push/pull.
236
229
237 :returns: bool
230 :returns: bool
238 """
231 """
239 return False
232 return False
240
233
241 @hybrid_property
234 @hybrid_property
242 def allows_creating_users(self):
235 def allows_creating_users(self):
243 """
236 """
244 Defines if Plugin allows users to be created on-the-fly when
237 Defines if Plugin allows users to be created on-the-fly when
245 authentication is called. Controls how external plugins should behave
238 authentication is called. Controls how external plugins should behave
246 in terms if they are allowed to create new users, or not. Base plugins
239 in terms if they are allowed to create new users, or not. Base plugins
247 should not be allowed to, but External ones should be !
240 should not be allowed to, but External ones should be !
248
241
249 :return: bool
242 :return: bool
250 """
243 """
251 return False
244 return False
252
245
253 def set_auth_type(self, auth_type):
246 def set_auth_type(self, auth_type):
254 self.auth_type = auth_type
247 self.auth_type = auth_type
255
248
256 def allows_authentication_from(
249 def allows_authentication_from(
257 self, user, allows_non_existing_user=True,
250 self, user, allows_non_existing_user=True,
258 allowed_auth_plugins=None, allowed_auth_sources=None):
251 allowed_auth_plugins=None, allowed_auth_sources=None):
259 """
252 """
260 Checks if this authentication module should accept a request for
253 Checks if this authentication module should accept a request for
261 the current user.
254 the current user.
262
255
263 :param user: user object fetched using plugin's get_user() method.
256 :param user: user object fetched using plugin's get_user() method.
264 :param allows_non_existing_user: if True, don't allow the
257 :param allows_non_existing_user: if True, don't allow the
265 user to be empty, meaning not existing in our database
258 user to be empty, meaning not existing in our database
266 :param allowed_auth_plugins: if provided, users extern_type will be
259 :param allowed_auth_plugins: if provided, users extern_type will be
267 checked against a list of provided extern types, which are plugin
260 checked against a list of provided extern types, which are plugin
268 auth_names in the end
261 auth_names in the end
269 :param allowed_auth_sources: authentication type allowed,
262 :param allowed_auth_sources: authentication type allowed,
270 `http` or `vcs` default is both.
263 `http` or `vcs` default is both.
271 defines if plugin will accept only http authentication vcs
264 defines if plugin will accept only http authentication vcs
272 authentication(git/hg) or both
265 authentication(git/hg) or both
273 :returns: boolean
266 :returns: boolean
274 """
267 """
275 if not user and not allows_non_existing_user:
268 if not user and not allows_non_existing_user:
276 log.debug('User is empty but plugin does not allow empty users,'
269 log.debug('User is empty but plugin does not allow empty users,'
277 'not allowed to authenticate')
270 'not allowed to authenticate')
278 return False
271 return False
279
272
280 expected_auth_plugins = allowed_auth_plugins or [self.name]
273 expected_auth_plugins = allowed_auth_plugins or [self.name]
281 if user and (user.extern_type and
274 if user and (user.extern_type and
282 user.extern_type not in expected_auth_plugins):
275 user.extern_type not in expected_auth_plugins):
283 log.debug(
276 log.debug(
284 'User `%s` is bound to `%s` auth type. Plugin allows only '
277 'User `%s` is bound to `%s` auth type. Plugin allows only '
285 '%s, skipping', user, user.extern_type, expected_auth_plugins)
278 '%s, skipping', user, user.extern_type, expected_auth_plugins)
286
279
287 return False
280 return False
288
281
289 # by default accept both
282 # by default accept both
290 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
283 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
291 if self.auth_type not in expected_auth_from:
284 if self.auth_type not in expected_auth_from:
292 log.debug('Current auth source is %s but plugin only allows %s',
285 log.debug('Current auth source is %s but plugin only allows %s',
293 self.auth_type, expected_auth_from)
286 self.auth_type, expected_auth_from)
294 return False
287 return False
295
288
296 return True
289 return True
297
290
298 def get_user(self, username=None, **kwargs):
291 def get_user(self, username=None, **kwargs):
299 """
292 """
300 Helper method for user fetching in plugins, by default it's using
293 Helper method for user fetching in plugins, by default it's using
301 simple fetch by username, but this method can be custimized in plugins
294 simple fetch by username, but this method can be custimized in plugins
302 eg. container auth plugin to fetch user by environ params
295 eg. container auth plugin to fetch user by environ params
303
296
304 :param username: username if given to fetch from database
297 :param username: username if given to fetch from database
305 :param kwargs: extra arguments needed for user fetching.
298 :param kwargs: extra arguments needed for user fetching.
306 """
299 """
307 user = None
300 user = None
308 log.debug(
301 log.debug(
309 'Trying to fetch user `%s` from RhodeCode database', username)
302 'Trying to fetch user `%s` from RhodeCode database', username)
310 if username:
303 if username:
311 user = User.get_by_username(username)
304 user = User.get_by_username(username)
312 if not user:
305 if not user:
313 log.debug('User not found, fallback to fetch user in '
306 log.debug('User not found, fallback to fetch user in '
314 'case insensitive mode')
307 'case insensitive mode')
315 user = User.get_by_username(username, case_insensitive=True)
308 user = User.get_by_username(username, case_insensitive=True)
316 else:
309 else:
317 log.debug('provided username:`%s` is empty skipping...', username)
310 log.debug('provided username:`%s` is empty skipping...', username)
318 if not user:
311 if not user:
319 log.debug('User `%s` not found in database', username)
312 log.debug('User `%s` not found in database', username)
320 return user
313 return user
321
314
322 def user_activation_state(self):
315 def user_activation_state(self):
323 """
316 """
324 Defines user activation state when creating new users
317 Defines user activation state when creating new users
325
318
326 :returns: boolean
319 :returns: boolean
327 """
320 """
328 raise NotImplementedError("Not implemented in base class")
321 raise NotImplementedError("Not implemented in base class")
329
322
330 def auth(self, userobj, username, passwd, settings, **kwargs):
323 def auth(self, userobj, username, passwd, settings, **kwargs):
331 """
324 """
332 Given a user object (which may be null), username, a plaintext
325 Given a user object (which may be null), username, a plaintext
333 password, and a settings object (containing all the keys needed as
326 password, and a settings object (containing all the keys needed as
334 listed in settings()), authenticate this user's login attempt.
327 listed in settings()), authenticate this user's login attempt.
335
328
336 Return None on failure. On success, return a dictionary of the form:
329 Return None on failure. On success, return a dictionary of the form:
337
330
338 see: RhodeCodeAuthPluginBase.auth_func_attrs
331 see: RhodeCodeAuthPluginBase.auth_func_attrs
339 This is later validated for correctness
332 This is later validated for correctness
340 """
333 """
341 raise NotImplementedError("not implemented in base class")
334 raise NotImplementedError("not implemented in base class")
342
335
343 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
336 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
344 """
337 """
345 Wrapper to call self.auth() that validates call on it
338 Wrapper to call self.auth() that validates call on it
346
339
347 :param userobj: userobj
340 :param userobj: userobj
348 :param username: username
341 :param username: username
349 :param passwd: plaintext password
342 :param passwd: plaintext password
350 :param settings: plugin settings
343 :param settings: plugin settings
351 """
344 """
352 auth = self.auth(userobj, username, passwd, settings, **kwargs)
345 auth = self.auth(userobj, username, passwd, settings, **kwargs)
353 if auth:
346 if auth:
354 # check if hash should be migrated ?
347 # check if hash should be migrated ?
355 new_hash = auth.get('_hash_migrate')
348 new_hash = auth.get('_hash_migrate')
356 if new_hash:
349 if new_hash:
357 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
350 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
358 return self._validate_auth_return(auth)
351 return self._validate_auth_return(auth)
359 return auth
352 return auth
360
353
361 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
354 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
362 new_hash_cypher = _RhodeCodeCryptoBCrypt()
355 new_hash_cypher = _RhodeCodeCryptoBCrypt()
363 # extra checks, so make sure new hash is correct.
356 # extra checks, so make sure new hash is correct.
364 password_encoded = safe_str(password)
357 password_encoded = safe_str(password)
365 if new_hash and new_hash_cypher.hash_check(
358 if new_hash and new_hash_cypher.hash_check(
366 password_encoded, new_hash):
359 password_encoded, new_hash):
367 cur_user = User.get_by_username(username)
360 cur_user = User.get_by_username(username)
368 cur_user.password = new_hash
361 cur_user.password = new_hash
369 Session().add(cur_user)
362 Session().add(cur_user)
370 Session().flush()
363 Session().flush()
371 log.info('Migrated user %s hash to bcrypt', cur_user)
364 log.info('Migrated user %s hash to bcrypt', cur_user)
372
365
373 def _validate_auth_return(self, ret):
366 def _validate_auth_return(self, ret):
374 if not isinstance(ret, dict):
367 if not isinstance(ret, dict):
375 raise Exception('returned value from auth must be a dict')
368 raise Exception('returned value from auth must be a dict')
376 for k in self.auth_func_attrs:
369 for k in self.auth_func_attrs:
377 if k not in ret:
370 if k not in ret:
378 raise Exception('Missing %s attribute from returned data' % k)
371 raise Exception('Missing %s attribute from returned data' % k)
379 return ret
372 return ret
380
373
381
374
382 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
375 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
383
376
384 @hybrid_property
377 @hybrid_property
385 def allows_creating_users(self):
378 def allows_creating_users(self):
386 return True
379 return True
387
380
388 def use_fake_password(self):
381 def use_fake_password(self):
389 """
382 """
390 Return a boolean that indicates whether or not we should set the user's
383 Return a boolean that indicates whether or not we should set the user's
391 password to a random value when it is authenticated by this plugin.
384 password to a random value when it is authenticated by this plugin.
392 If your plugin provides authentication, then you will generally
385 If your plugin provides authentication, then you will generally
393 want this.
386 want this.
394
387
395 :returns: boolean
388 :returns: boolean
396 """
389 """
397 raise NotImplementedError("Not implemented in base class")
390 raise NotImplementedError("Not implemented in base class")
398
391
399 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
392 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
400 # at this point _authenticate calls plugin's `auth()` function
393 # at this point _authenticate calls plugin's `auth()` function
401 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
394 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
402 userobj, username, passwd, settings, **kwargs)
395 userobj, username, passwd, settings, **kwargs)
403 if auth:
396 if auth:
404 # maybe plugin will clean the username ?
397 # maybe plugin will clean the username ?
405 # we should use the return value
398 # we should use the return value
406 username = auth['username']
399 username = auth['username']
407
400
408 # if external source tells us that user is not active, we should
401 # if external source tells us that user is not active, we should
409 # skip rest of the process. This can prevent from creating users in
402 # skip rest of the process. This can prevent from creating users in
410 # RhodeCode when using external authentication, but if it's
403 # RhodeCode when using external authentication, but if it's
411 # inactive user we shouldn't create that user anyway
404 # inactive user we shouldn't create that user anyway
412 if auth['active_from_extern'] is False:
405 if auth['active_from_extern'] is False:
413 log.warning(
406 log.warning(
414 "User %s authenticated against %s, but is inactive",
407 "User %s authenticated against %s, but is inactive",
415 username, self.__module__)
408 username, self.__module__)
416 return None
409 return None
417
410
418 cur_user = User.get_by_username(username, case_insensitive=True)
411 cur_user = User.get_by_username(username, case_insensitive=True)
419 is_user_existing = cur_user is not None
412 is_user_existing = cur_user is not None
420
413
421 if is_user_existing:
414 if is_user_existing:
422 log.debug('Syncing user `%s` from '
415 log.debug('Syncing user `%s` from '
423 '`%s` plugin', username, self.name)
416 '`%s` plugin', username, self.name)
424 else:
417 else:
425 log.debug('Creating non existing user `%s` from '
418 log.debug('Creating non existing user `%s` from '
426 '`%s` plugin', username, self.name)
419 '`%s` plugin', username, self.name)
427
420
428 if self.allows_creating_users:
421 if self.allows_creating_users:
429 log.debug('Plugin `%s` allows to '
422 log.debug('Plugin `%s` allows to '
430 'create new users', self.name)
423 'create new users', self.name)
431 else:
424 else:
432 log.debug('Plugin `%s` does not allow to '
425 log.debug('Plugin `%s` does not allow to '
433 'create new users', self.name)
426 'create new users', self.name)
434
427
435 user_parameters = {
428 user_parameters = {
436 'username': username,
429 'username': username,
437 'email': auth["email"],
430 'email': auth["email"],
438 'firstname': auth["firstname"],
431 'firstname': auth["firstname"],
439 'lastname': auth["lastname"],
432 'lastname': auth["lastname"],
440 'active': auth["active"],
433 'active': auth["active"],
441 'admin': auth["admin"],
434 'admin': auth["admin"],
442 'extern_name': auth["extern_name"],
435 'extern_name': auth["extern_name"],
443 'extern_type': self.name,
436 'extern_type': self.name,
444 'plugin': self,
437 'plugin': self,
445 'allow_to_create_user': self.allows_creating_users,
438 'allow_to_create_user': self.allows_creating_users,
446 }
439 }
447
440
448 if not is_user_existing:
441 if not is_user_existing:
449 if self.use_fake_password():
442 if self.use_fake_password():
450 # Randomize the PW because we don't need it, but don't want
443 # Randomize the PW because we don't need it, but don't want
451 # them blank either
444 # them blank either
452 passwd = PasswordGenerator().gen_password(length=16)
445 passwd = PasswordGenerator().gen_password(length=16)
453 user_parameters['password'] = passwd
446 user_parameters['password'] = passwd
454 else:
447 else:
455 # Since the password is required by create_or_update method of
448 # Since the password is required by create_or_update method of
456 # UserModel, we need to set it explicitly.
449 # UserModel, we need to set it explicitly.
457 # The create_or_update method is smart and recognises the
450 # The create_or_update method is smart and recognises the
458 # password hashes as well.
451 # password hashes as well.
459 user_parameters['password'] = cur_user.password
452 user_parameters['password'] = cur_user.password
460
453
461 # we either create or update users, we also pass the flag
454 # we either create or update users, we also pass the flag
462 # that controls if this method can actually do that.
455 # that controls if this method can actually do that.
463 # raises NotAllowedToCreateUserError if it cannot, and we try to.
456 # raises NotAllowedToCreateUserError if it cannot, and we try to.
464 user = UserModel().create_or_update(**user_parameters)
457 user = UserModel().create_or_update(**user_parameters)
465 Session().flush()
458 Session().flush()
466 # enforce user is just in given groups, all of them has to be ones
459 # enforce user is just in given groups, all of them has to be ones
467 # created from plugins. We store this info in _group_data JSON
460 # created from plugins. We store this info in _group_data JSON
468 # field
461 # field
469 try:
462 try:
470 groups = auth['groups'] or []
463 groups = auth['groups'] or []
471 UserGroupModel().enforce_groups(user, groups, self.name)
464 UserGroupModel().enforce_groups(user, groups, self.name)
472 except Exception:
465 except Exception:
473 # for any reason group syncing fails, we should
466 # for any reason group syncing fails, we should
474 # proceed with login
467 # proceed with login
475 log.error(traceback.format_exc())
468 log.error(traceback.format_exc())
476 Session().commit()
469 Session().commit()
477 return auth
470 return auth
478
471
479
472
480 def loadplugin(plugin_id):
473 def loadplugin(plugin_id):
481 """
474 """
482 Loads and returns an instantiated authentication plugin.
475 Loads and returns an instantiated authentication plugin.
483 Returns the RhodeCodeAuthPluginBase subclass on success,
476 Returns the RhodeCodeAuthPluginBase subclass on success,
484 raises exceptions on failure.
477 raises exceptions on failure.
485
478
486 raises:
479 raises:
487 KeyError -- if no plugin available with given name
480 KeyError -- if no plugin available with given name
488 TypeError -- if the RhodeCodeAuthPlugin is not a subclass of
481 TypeError -- if the RhodeCodeAuthPlugin is not a subclass of
489 ours RhodeCodeAuthPluginBase
482 ours RhodeCodeAuthPluginBase
490 """
483 """
491 # TODO: Disusing pyramids thread locals to retrieve the registry.
484 # TODO: Disusing pyramids thread locals to retrieve the registry.
492 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
485 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
493 plugin = authn_registry.get_plugin(plugin_id)
486 plugin = authn_registry.get_plugin(plugin_id)
494 if plugin is None:
487 if plugin is None:
495 log.error('Authentication plugin not found: "%s"', plugin_id)
488 log.error('Authentication plugin not found: "%s"', plugin_id)
496 return plugin
489 return plugin
497
490
498
491
499 def get_auth_cache_manager(custom_ttl=None):
492 def get_auth_cache_manager(custom_ttl=None):
500 return caches.get_cache_manager(
493 return caches.get_cache_manager(
501 'auth_plugins', 'rhodecode.authentication', custom_ttl)
494 'auth_plugins', 'rhodecode.authentication', custom_ttl)
502
495
503
496
504 def authenticate(username, password, environ=None, auth_type=None,
497 def authenticate(username, password, environ=None, auth_type=None,
505 skip_missing=False):
498 skip_missing=False):
506 """
499 """
507 Authentication function used for access control,
500 Authentication function used for access control,
508 It tries to authenticate based on enabled authentication modules.
501 It tries to authenticate based on enabled authentication modules.
509
502
510 :param username: username can be empty for container auth
503 :param username: username can be empty for container auth
511 :param password: password can be empty for container auth
504 :param password: password can be empty for container auth
512 :param environ: environ headers passed for container auth
505 :param environ: environ headers passed for container auth
513 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
506 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
514 :param skip_missing: ignores plugins that are in db but not in environment
507 :param skip_missing: ignores plugins that are in db but not in environment
515 :returns: None if auth failed, plugin_user dict if auth is correct
508 :returns: None if auth failed, plugin_user dict if auth is correct
516 """
509 """
517 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
510 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
518 raise ValueError('auth type must be on of http, vcs got "%s" instead'
511 raise ValueError('auth type must be on of http, vcs got "%s" instead'
519 % auth_type)
512 % auth_type)
520 container_only = environ and not (username and password)
513 container_only = environ and not (username and password)
521 auth_plugins = SettingsModel().get_auth_plugins()
522 for plugin_id in auth_plugins:
523 plugin = loadplugin(plugin_id)
524
514
525 if plugin is None:
515 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
526 log.warning('Authentication plugin missing: "{}"'.format(
516 for plugin in authn_registry.get_plugins_for_authentication():
527 plugin_id))
528 continue
529
530 if not plugin.is_active():
531 log.info('Authentication plugin is inactive: "{}"'.format(
532 plugin_id))
533 continue
534
535 plugin.set_auth_type(auth_type)
517 plugin.set_auth_type(auth_type)
536 user = plugin.get_user(username)
518 user = plugin.get_user(username)
537 display_user = user.username if user else username
519 display_user = user.username if user else username
538
520
539 if container_only and not plugin.is_container_auth:
521 if container_only and not plugin.is_container_auth:
540 log.debug('Auth type is for container only and plugin `%s` is not '
522 log.debug('Auth type is for container only and plugin `%s` is not '
541 'container plugin, skipping...', plugin_id)
523 'container plugin, skipping...', plugin.get_id())
542 continue
524 continue
543
525
544 # load plugin settings from RhodeCode database
526 # load plugin settings from RhodeCode database
545 plugin_settings = plugin.get_settings()
527 plugin_settings = plugin.get_settings()
546 log.debug('Plugin settings:%s', plugin_settings)
528 log.debug('Plugin settings:%s', plugin_settings)
547
529
548 log.debug('Trying authentication using ** %s **', plugin_id)
530 log.debug('Trying authentication using ** %s **', plugin.get_id())
549 # use plugin's method of user extraction.
531 # use plugin's method of user extraction.
550 user = plugin.get_user(username, environ=environ,
532 user = plugin.get_user(username, environ=environ,
551 settings=plugin_settings)
533 settings=plugin_settings)
552 display_user = user.username if user else username
534 display_user = user.username if user else username
553 log.debug('Plugin %s extracted user is `%s`', plugin_id, display_user)
535 log.debug(
536 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
554
537
555 if not plugin.allows_authentication_from(user):
538 if not plugin.allows_authentication_from(user):
556 log.debug('Plugin %s does not accept user `%s` for authentication',
539 log.debug('Plugin %s does not accept user `%s` for authentication',
557 plugin_id, display_user)
540 plugin.get_id(), display_user)
558 continue
541 continue
559 else:
542 else:
560 log.debug('Plugin %s accepted user `%s` for authentication',
543 log.debug('Plugin %s accepted user `%s` for authentication',
561 plugin_id, display_user)
544 plugin.get_id(), display_user)
562
545
563 log.info('Authenticating user `%s` using %s plugin',
546 log.info('Authenticating user `%s` using %s plugin',
564 display_user, plugin_id)
547 display_user, plugin.get_id())
565
548
566 _cache_ttl = 0
549 _cache_ttl = 0
567
550
568 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
551 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
569 # plugin cache set inside is more important than the settings value
552 # plugin cache set inside is more important than the settings value
570 _cache_ttl = plugin.AUTH_CACHE_TTL
553 _cache_ttl = plugin.AUTH_CACHE_TTL
571 elif plugin_settings.get('auth_cache_ttl'):
554 elif plugin_settings.get('auth_cache_ttl'):
572 _cache_ttl = safe_int(plugin_settings.get('auth_cache_ttl'), 0)
555 _cache_ttl = safe_int(plugin_settings.get('auth_cache_ttl'), 0)
573
556
574 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
557 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
575
558
576 # get instance of cache manager configured for a namespace
559 # get instance of cache manager configured for a namespace
577 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
560 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
578
561
579 log.debug('Cache for plugin `%s` active: %s', plugin_id,
562 log.debug('Cache for plugin `%s` active: %s', plugin.get_id(),
580 plugin_cache_active)
563 plugin_cache_active)
581
564
582 # for environ based password can be empty, but then the validation is
565 # for environ based password can be empty, but then the validation is
583 # on the server that fills in the env data needed for authentication
566 # on the server that fills in the env data needed for authentication
584 _password_hash = md5_safe(plugin.name + username + (password or ''))
567 _password_hash = md5_safe(plugin.name + username + (password or ''))
585
568
586 # _authenticate is a wrapper for .auth() method of plugin.
569 # _authenticate is a wrapper for .auth() method of plugin.
587 # it checks if .auth() sends proper data.
570 # it checks if .auth() sends proper data.
588 # For RhodeCodeExternalAuthPlugin it also maps users to
571 # For RhodeCodeExternalAuthPlugin it also maps users to
589 # Database and maps the attributes returned from .auth()
572 # Database and maps the attributes returned from .auth()
590 # to RhodeCode database. If this function returns data
573 # to RhodeCode database. If this function returns data
591 # then auth is correct.
574 # then auth is correct.
592 start = time.time()
575 start = time.time()
593 log.debug('Running plugin `%s` _authenticate method',
576 log.debug('Running plugin `%s` _authenticate method',
594 plugin_id)
577 plugin.get_id())
595
578
596 def auth_func():
579 def auth_func():
597 """
580 """
598 This function is used internally in Cache of Beaker to calculate
581 This function is used internally in Cache of Beaker to calculate
599 Results
582 Results
600 """
583 """
601 return plugin._authenticate(
584 return plugin._authenticate(
602 user, username, password, plugin_settings,
585 user, username, password, plugin_settings,
603 environ=environ or {})
586 environ=environ or {})
604
587
605 if plugin_cache_active:
588 if plugin_cache_active:
606 plugin_user = cache_manager.get(
589 plugin_user = cache_manager.get(
607 _password_hash, createfunc=auth_func)
590 _password_hash, createfunc=auth_func)
608 else:
591 else:
609 plugin_user = auth_func()
592 plugin_user = auth_func()
610
593
611 auth_time = time.time() - start
594 auth_time = time.time() - start
612 log.debug('Authentication for plugin `%s` completed in %.3fs, '
595 log.debug('Authentication for plugin `%s` completed in %.3fs, '
613 'expiration time of fetched cache %.1fs.',
596 'expiration time of fetched cache %.1fs.',
614 plugin_id, auth_time, _cache_ttl)
597 plugin.get_id(), auth_time, _cache_ttl)
615
598
616 log.debug('PLUGIN USER DATA: %s', plugin_user)
599 log.debug('PLUGIN USER DATA: %s', plugin_user)
617
600
618 if plugin_user:
601 if plugin_user:
619 log.debug('Plugin returned proper authentication data')
602 log.debug('Plugin returned proper authentication data')
620 return plugin_user
603 return plugin_user
621 # we failed to Auth because .auth() method didn't return proper user
604 # we failed to Auth because .auth() method didn't return proper user
622 log.debug("User `%s` failed to authenticate against %s",
605 log.debug("User `%s` failed to authenticate against %s",
623 display_user, plugin_id)
606 display_user, plugin.get_id())
624 return None
607 return None
@@ -1,53 +1,78 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 @implementer(IAuthnPluginRegistry)
32 @implementer(IAuthnPluginRegistry)
33 class AuthenticationPluginRegistry(object):
33 class AuthenticationPluginRegistry(object):
34 def __init__(self):
34
35 # INI settings key to set a fallback authentication plugin.
36 fallback_plugin_key = 'rhodecode.auth_plugin_fallback'
37
38 def __init__(self, settings):
35 self._plugins = {}
39 self._plugins = {}
40 self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
36
41
37 def add_authn_plugin(self, config, plugin):
42 def add_authn_plugin(self, config, plugin):
38 plugin_id = plugin.get_id()
43 plugin_id = plugin.get_id()
39 if plugin_id in self._plugins.keys():
44 if plugin_id in self._plugins.keys():
40 raise ConfigurationError(
45 raise ConfigurationError(
41 'Cannot register authentication plugin twice: "%s"', plugin_id)
46 'Cannot register authentication plugin twice: "%s"', plugin_id)
42 else:
47 else:
43 log.debug('Register authentication plugin: "%s"', plugin_id)
48 log.debug('Register authentication plugin: "%s"', plugin_id)
44 self._plugins[plugin_id] = plugin
49 self._plugins[plugin_id] = plugin
45
50
46 def get_plugins(self):
51 def get_plugins(self):
47 def sort_key(plugin):
52 def sort_key(plugin):
48 return str.lower(safe_str(plugin.get_display_name()))
53 return str.lower(safe_str(plugin.get_display_name()))
49
54
50 return sorted(self._plugins.values(), key=sort_key)
55 return sorted(self._plugins.values(), key=sort_key)
51
56
52 def get_plugin(self, plugin_id):
57 def get_plugin(self, plugin_id):
53 return self._plugins.get(plugin_id, None)
58 return self._plugins.get(plugin_id, None)
59
60 def get_plugins_for_authentication(self):
61 """
62 Returns a list of plugins which should be consulted when authenticating
63 a user. It only returns plugins which are enabled and active.
64 Additionally it includes the fallback plugin from the INI file, if
65 `rhodecode.auth_plugin_fallback` is set to a plugin ID.
66 """
67 plugins = []
68 for plugin in self.get_plugins():
69 if (self._fallback_plugin and
70 plugin.get_id() == self._fallback_plugin):
71 log.warn(
72 'Using fallback authentication plugin from INI file: "%s"',
73 plugin.get_id())
74 plugins.append(plugin)
75 elif plugin.is_enabled() and plugin.is_active():
76 plugins.append(plugin)
77
78 return plugins
@@ -1,218 +1,217 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 get_auth_cache_manager
29 from rhodecode.authentication.base import get_auth_cache_manager
30 from rhodecode.authentication.interface import IAuthnPluginRegistry
30 from rhodecode.authentication.interface import IAuthnPluginRegistry
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
47
48 # TODO: Think about replacing the htmlfill stuff.
48 # TODO: Think about replacing the htmlfill stuff.
49 def _render_and_fill(self, template, template_context, request,
49 def _render_and_fill(self, template, template_context, request,
50 form_defaults, validation_errors):
50 form_defaults, validation_errors):
51 """
51 """
52 Helper to render a template and fill the HTML form fields with
52 Helper to render a template and fill the HTML form fields with
53 defaults. Also displays the form errors.
53 defaults. Also displays the form errors.
54 """
54 """
55 # Render template to string.
55 # Render template to string.
56 html = render(template, template_context, request=request)
56 html = render(template, template_context, request=request)
57
57
58 # Fill the HTML form fields with default values and add error messages.
58 # Fill the HTML form fields with default values and add error messages.
59 html = formencode.htmlfill.render(
59 html = formencode.htmlfill.render(
60 html,
60 html,
61 defaults=form_defaults,
61 defaults=form_defaults,
62 errors=validation_errors,
62 errors=validation_errors,
63 prefix_error=False,
63 prefix_error=False,
64 encoding="UTF-8",
64 encoding="UTF-8",
65 force_defaults=False)
65 force_defaults=False)
66
66
67 return html
67 return html
68
68
69 def settings_get(self):
69 def settings_get(self):
70 """
70 """
71 View that displays the plugin settings as a form.
71 View that displays the plugin settings as a form.
72 """
72 """
73 form_defaults = {}
73 form_defaults = {}
74 validation_errors = None
74 validation_errors = None
75 schema = self.plugin.get_settings_schema()
75 schema = self.plugin.get_settings_schema()
76
76
77 # Get default values for the form.
77 # Get default values for the form.
78 for node in schema.children:
78 for node in schema.children:
79 value = self.plugin.get_setting_by_name(node.name) or node.default
79 value = self.plugin.get_setting_by_name(node.name) or node.default
80 form_defaults[node.name] = value
80 form_defaults[node.name] = value
81
81
82 template_context = {
82 template_context = {
83 'resource': self.context,
83 'resource': self.context,
84 'plugin': self.context.plugin
84 'plugin': self.context.plugin
85 }
85 }
86
86
87 return Response(self._render_and_fill(
87 return Response(self._render_and_fill(
88 'rhodecode:templates/admin/auth/plugin_settings.html',
88 'rhodecode:templates/admin/auth/plugin_settings.html',
89 template_context,
89 template_context,
90 self.request,
90 self.request,
91 form_defaults,
91 form_defaults,
92 validation_errors))
92 validation_errors))
93
93
94 def settings_post(self):
94 def settings_post(self):
95 """
95 """
96 View that validates and stores the plugin settings.
96 View that validates and stores the plugin settings.
97 """
97 """
98 schema = self.plugin.get_settings_schema()
98 schema = self.plugin.get_settings_schema()
99 try:
99 try:
100 valid_data = schema.deserialize(self.request.params)
100 valid_data = schema.deserialize(self.request.params)
101 except colander.Invalid, e:
101 except colander.Invalid, e:
102 # Display error message and display form again.
102 # Display error message and display form again.
103 form_defaults = self.request.params
103 form_defaults = self.request.params
104 validation_errors = e.asdict()
104 validation_errors = e.asdict()
105 self.request.session.flash(
105 self.request.session.flash(
106 _('Errors exist when saving plugin settings. '
106 _('Errors exist when saving plugin settings. '
107 'Please check the form inputs.'),
107 'Please check the form inputs.'),
108 queue='error')
108 queue='error')
109
109
110 template_context = {
110 template_context = {
111 'resource': self.context,
111 'resource': self.context,
112 'plugin': self.context.plugin
112 'plugin': self.context.plugin
113 }
113 }
114
114
115 return Response(self._render_and_fill(
115 return Response(self._render_and_fill(
116 'rhodecode:templates/admin/auth/plugin_settings.html',
116 'rhodecode:templates/admin/auth/plugin_settings.html',
117 template_context,
117 template_context,
118 self.request,
118 self.request,
119 form_defaults,
119 form_defaults,
120 validation_errors))
120 validation_errors))
121
121
122 # Store validated data.
122 # Store validated data.
123 for name, value in valid_data.items():
123 for name, value in valid_data.items():
124 self.plugin.create_or_update_setting(name, value)
124 self.plugin.create_or_update_setting(name, value)
125 Session.commit()
125 Session.commit()
126
126
127 # Display success message and redirect.
127 # Display success message and redirect.
128 self.request.session.flash(
128 self.request.session.flash(
129 _('Auth settings updated successfully.'),
129 _('Auth settings updated successfully.'),
130 queue='success')
130 queue='success')
131 redirect_to = self.request.resource_path(
131 redirect_to = self.request.resource_path(
132 self.context, route_name='auth_home')
132 self.context, route_name='auth_home')
133 return HTTPFound(redirect_to)
133 return HTTPFound(redirect_to)
134
134
135
135
136 # TODO: Ongoing migration in these views.
136 # TODO: Ongoing migration in these views.
137 # - Maybe we should also use a colander schema for these views.
137 # - Maybe we should also use a colander schema for these views.
138 class AuthSettingsView(object):
138 class AuthSettingsView(object):
139 def __init__(self, context, request):
139 def __init__(self, context, request):
140 self.context = context
140 self.context = context
141 self.request = request
141 self.request = request
142
142
143 # TODO: Move this into a utility function. It is needed in all view
143 # TODO: Move this into a utility function. It is needed in all view
144 # classes during migration. Maybe a mixin?
144 # classes during migration. Maybe a mixin?
145
145
146 # Some of the decorators rely on this attribute to be present on the
146 # Some of the decorators rely on this attribute to be present on the
147 # class of the decorated method.
147 # class of the decorated method.
148 self._rhodecode_user = request.user
148 self._rhodecode_user = request.user
149
149
150 @LoginRequired()
150 @LoginRequired()
151 @HasPermissionAllDecorator('hg.admin')
151 @HasPermissionAllDecorator('hg.admin')
152 def index(self, defaults={}, errors=None, prefix_error=False):
152 def index(self, defaults={}, errors=None, prefix_error=False):
153 authn_registry = self.request.registry.getUtility(IAuthnPluginRegistry)
153 authn_registry = self.request.registry.getUtility(IAuthnPluginRegistry)
154 default_plugins = ['egg:rhodecode-enterprise-ce#rhodecode']
154 enabled_plugins = SettingsModel().get_auth_plugins()
155 enabled_plugins = SettingsModel().get_auth_plugins() or default_plugins
156
155
157 # Create template context and render it.
156 # Create template context and render it.
158 template_context = {
157 template_context = {
159 'resource': self.context,
158 'resource': self.context,
160 'available_plugins': authn_registry.get_plugins(),
159 'available_plugins': authn_registry.get_plugins(),
161 'enabled_plugins': enabled_plugins,
160 'enabled_plugins': enabled_plugins,
162 }
161 }
163 html = render('rhodecode:templates/admin/auth/auth_settings.html',
162 html = render('rhodecode:templates/admin/auth/auth_settings.html',
164 template_context,
163 template_context,
165 request=self.request)
164 request=self.request)
166
165
167 # Create form default values and fill the form.
166 # Create form default values and fill the form.
168 form_defaults = {
167 form_defaults = {
169 'auth_plugins': ','.join(enabled_plugins)
168 'auth_plugins': ','.join(enabled_plugins)
170 }
169 }
171 form_defaults.update(defaults)
170 form_defaults.update(defaults)
172 html = formencode.htmlfill.render(
171 html = formencode.htmlfill.render(
173 html,
172 html,
174 defaults=form_defaults,
173 defaults=form_defaults,
175 errors=errors,
174 errors=errors,
176 prefix_error=prefix_error,
175 prefix_error=prefix_error,
177 encoding="UTF-8",
176 encoding="UTF-8",
178 force_defaults=False)
177 force_defaults=False)
179
178
180 return Response(html)
179 return Response(html)
181
180
182 @LoginRequired()
181 @LoginRequired()
183 @HasPermissionAllDecorator('hg.admin')
182 @HasPermissionAllDecorator('hg.admin')
184 @auth.CSRFRequired()
183 @auth.CSRFRequired()
185 def auth_settings(self):
184 def auth_settings(self):
186 try:
185 try:
187 form = AuthSettingsForm()()
186 form = AuthSettingsForm()()
188 form_result = form.to_python(self.request.params)
187 form_result = form.to_python(self.request.params)
189 plugins = ','.join(form_result['auth_plugins'])
188 plugins = ','.join(form_result['auth_plugins'])
190 setting = SettingsModel().create_or_update_setting(
189 setting = SettingsModel().create_or_update_setting(
191 'auth_plugins', plugins)
190 'auth_plugins', plugins)
192 Session().add(setting)
191 Session().add(setting)
193 Session().commit()
192 Session().commit()
194
193
195 cache_manager = get_auth_cache_manager()
194 cache_manager = get_auth_cache_manager()
196 cache_manager.clear()
195 cache_manager.clear()
197 self.request.session.flash(
196 self.request.session.flash(
198 _('Auth settings updated successfully.'),
197 _('Auth settings updated successfully.'),
199 queue='success')
198 queue='success')
200 except formencode.Invalid as errors:
199 except formencode.Invalid as errors:
201 e = errors.error_dict or {}
200 e = errors.error_dict or {}
202 self.request.session.flash(
201 self.request.session.flash(
203 _('Errors exist when saving plugin setting. '
202 _('Errors exist when saving plugin setting. '
204 'Please check the form inputs.'),
203 'Please check the form inputs.'),
205 queue='error')
204 queue='error')
206 return self.index(
205 return self.index(
207 defaults=errors.value,
206 defaults=errors.value,
208 errors=e,
207 errors=e,
209 prefix_error=False)
208 prefix_error=False)
210 except Exception:
209 except Exception:
211 log.exception('Exception in auth_settings')
210 log.exception('Exception in auth_settings')
212 self.request.session.flash(
211 self.request.session.flash(
213 _('Error occurred during update of auth settings.'),
212 _('Error occurred during update of auth settings.'),
214 queue='error')
213 queue='error')
215
214
216 redirect_to = self.request.resource_path(
215 redirect_to = self.request.resource_path(
217 self.context, route_name='auth_home')
216 self.context, route_name='auth_home')
218 return HTTPFound(redirect_to)
217 return HTTPFound(redirect_to)
General Comments 0
You need to be logged in to leave comments. Login now