##// END OF EJS Templates
authentications: remove utf8 markers
super-admin -
r5056:d6440b8b default
parent child Browse files
Show More
@@ -1,101 +1,101 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 import importlib
22 import importlib
23
23
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.apps._base import ADMIN_PREFIX
29 from rhodecode.apps._base import ADMIN_PREFIX
30 from rhodecode.model.settings import SettingsModel
30 from rhodecode.model.settings import SettingsModel
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34 legacy_plugin_prefix = 'py:'
34 legacy_plugin_prefix = 'py:'
35 plugin_default_auth_ttl = 30
35 plugin_default_auth_ttl = 30
36
36
37
37
38 def _import_legacy_plugin(plugin_id):
38 def _import_legacy_plugin(plugin_id):
39 module_name = plugin_id.split(legacy_plugin_prefix, 1)[-1]
39 module_name = plugin_id.split(legacy_plugin_prefix, 1)[-1]
40 module = importlib.import_module(module_name)
40 module = importlib.import_module(module_name)
41 return module.plugin_factory(plugin_id=plugin_id)
41 return module.plugin_factory(plugin_id=plugin_id)
42
42
43
43
44 def discover_legacy_plugins(config, prefix=legacy_plugin_prefix):
44 def discover_legacy_plugins(config, prefix=legacy_plugin_prefix):
45 """
45 """
46 Function that imports the legacy plugins stored in the 'auth_plugins'
46 Function that imports the legacy plugins stored in the 'auth_plugins'
47 setting in database which are using the specified prefix. Normally 'py:' is
47 setting in database which are using the specified prefix. Normally 'py:' is
48 used for the legacy plugins.
48 used for the legacy plugins.
49 """
49 """
50
50
51 log.debug('authentication: running legacy plugin discovery for prefix %s',
51 log.debug('authentication: running legacy plugin discovery for prefix %s',
52 legacy_plugin_prefix)
52 legacy_plugin_prefix)
53 try:
53 try:
54 auth_plugins = SettingsModel().get_setting_by_name('auth_plugins')
54 auth_plugins = SettingsModel().get_setting_by_name('auth_plugins')
55 enabled_plugins = auth_plugins.app_settings_value
55 enabled_plugins = auth_plugins.app_settings_value
56 legacy_plugins = [id_ for id_ in enabled_plugins if id_.startswith(prefix)]
56 legacy_plugins = [id_ for id_ in enabled_plugins if id_.startswith(prefix)]
57 except Exception:
57 except Exception:
58 legacy_plugins = []
58 legacy_plugins = []
59
59
60 for plugin_id in legacy_plugins:
60 for plugin_id in legacy_plugins:
61 log.debug('Legacy plugin discovered: "%s"', plugin_id)
61 log.debug('Legacy plugin discovered: "%s"', plugin_id)
62 try:
62 try:
63 plugin = _import_legacy_plugin(plugin_id)
63 plugin = _import_legacy_plugin(plugin_id)
64 config.include(plugin.includeme)
64 config.include(plugin.includeme)
65 except Exception as e:
65 except Exception as e:
66 log.exception(
66 log.exception(
67 'Exception while loading legacy authentication plugin '
67 'Exception while loading legacy authentication plugin '
68 '"%s": %s', plugin_id, e)
68 '"%s": %s', plugin_id, e)
69
69
70
70
71 def includeme(config):
71 def includeme(config):
72
72
73 # Set authentication policy.
73 # Set authentication policy.
74 authn_policy = SessionAuthenticationPolicy()
74 authn_policy = SessionAuthenticationPolicy()
75 config.set_authentication_policy(authn_policy)
75 config.set_authentication_policy(authn_policy)
76
76
77 # Create authentication plugin registry and add it to the pyramid registry.
77 # Create authentication plugin registry and add it to the pyramid registry.
78 authn_registry = AuthenticationPluginRegistry(config.get_settings())
78 authn_registry = AuthenticationPluginRegistry(config.get_settings())
79 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
79 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
80 config.registry.registerUtility(authn_registry)
80 config.registry.registerUtility(authn_registry)
81
81
82 # Create authentication traversal root resource.
82 # Create authentication traversal root resource.
83 authn_root_resource = root_factory()
83 authn_root_resource = root_factory()
84 config.add_directive('add_authn_resource',
84 config.add_directive('add_authn_resource',
85 authn_root_resource.add_authn_resource)
85 authn_root_resource.add_authn_resource)
86
86
87 # Add the authentication traversal route.
87 # Add the authentication traversal route.
88 config.add_route('auth_home',
88 config.add_route('auth_home',
89 ADMIN_PREFIX + '/auth*traverse',
89 ADMIN_PREFIX + '/auth*traverse',
90 factory=root_factory)
90 factory=root_factory)
91 # Add the authentication settings root views.
91 # Add the authentication settings root views.
92 config.add_view('rhodecode.authentication.views.AuthSettingsView',
92 config.add_view('rhodecode.authentication.views.AuthSettingsView',
93 attr='index',
93 attr='index',
94 request_method='GET',
94 request_method='GET',
95 route_name='auth_home',
95 route_name='auth_home',
96 context=AuthnRootResource)
96 context=AuthnRootResource)
97 config.add_view('rhodecode.authentication.views.AuthSettingsView',
97 config.add_view('rhodecode.authentication.views.AuthSettingsView',
98 attr='auth_settings',
98 attr='auth_settings',
99 request_method='POST',
99 request_method='POST',
100 route_name='auth_home',
100 route_name='auth_home',
101 context=AuthnRootResource)
101 context=AuthnRootResource)
@@ -1,818 +1,817 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 """
20 """
22 Authentication modules
21 Authentication modules
23 """
22 """
24 import socket
23 import socket
25 import string
24 import string
26 import colander
25 import colander
27 import copy
26 import copy
28 import logging
27 import logging
29 import time
28 import time
30 import traceback
29 import traceback
31 import warnings
30 import warnings
32 import functools
31 import functools
33
32
34 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
35
34
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
35 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.lib import rc_cache
37 from rhodecode.lib import rc_cache
39 from rhodecode.lib.statsd_client import StatsdClient
38 from rhodecode.lib.statsd_client import StatsdClient
40 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
41 from rhodecode.lib.utils2 import safe_int, safe_str
40 from rhodecode.lib.utils2 import safe_int, safe_str
42 from rhodecode.lib.exceptions import (LdapConnectionError, LdapUsernameError, LdapPasswordError)
41 from rhodecode.lib.exceptions import (LdapConnectionError, LdapUsernameError, LdapPasswordError)
43 from rhodecode.model.db import User
42 from rhodecode.model.db import User
44 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
46 from rhodecode.model.user import UserModel
45 from rhodecode.model.user import UserModel
47 from rhodecode.model.user_group import UserGroupModel
46 from rhodecode.model.user_group import UserGroupModel
48
47
49
48
50 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
51
50
52 # auth types that authenticate() function can receive
51 # auth types that authenticate() function can receive
53 VCS_TYPE = 'vcs'
52 VCS_TYPE = 'vcs'
54 HTTP_TYPE = 'http'
53 HTTP_TYPE = 'http'
55
54
56 external_auth_session_key = 'rhodecode.external_auth'
55 external_auth_session_key = 'rhodecode.external_auth'
57
56
58
57
59 class hybrid_property(object):
58 class hybrid_property(object):
60 """
59 """
61 a property decorator that works both for instance and class
60 a property decorator that works both for instance and class
62 """
61 """
63 def __init__(self, fget, fset=None, fdel=None, expr=None):
62 def __init__(self, fget, fset=None, fdel=None, expr=None):
64 self.fget = fget
63 self.fget = fget
65 self.fset = fset
64 self.fset = fset
66 self.fdel = fdel
65 self.fdel = fdel
67 self.expr = expr or fget
66 self.expr = expr or fget
68 functools.update_wrapper(self, fget)
67 functools.update_wrapper(self, fget)
69
68
70 def __get__(self, instance, owner):
69 def __get__(self, instance, owner):
71 if instance is None:
70 if instance is None:
72 return self.expr(owner)
71 return self.expr(owner)
73 else:
72 else:
74 return self.fget(instance)
73 return self.fget(instance)
75
74
76 def __set__(self, instance, value):
75 def __set__(self, instance, value):
77 self.fset(instance, value)
76 self.fset(instance, value)
78
77
79 def __delete__(self, instance):
78 def __delete__(self, instance):
80 self.fdel(instance)
79 self.fdel(instance)
81
80
82
81
83 class LazyFormencode(object):
82 class LazyFormencode(object):
84 def __init__(self, formencode_obj, *args, **kwargs):
83 def __init__(self, formencode_obj, *args, **kwargs):
85 self.formencode_obj = formencode_obj
84 self.formencode_obj = formencode_obj
86 self.args = args
85 self.args = args
87 self.kwargs = kwargs
86 self.kwargs = kwargs
88
87
89 def __call__(self, *args, **kwargs):
88 def __call__(self, *args, **kwargs):
90 from inspect import isfunction
89 from inspect import isfunction
91 formencode_obj = self.formencode_obj
90 formencode_obj = self.formencode_obj
92 if isfunction(formencode_obj):
91 if isfunction(formencode_obj):
93 # case we wrap validators into functions
92 # case we wrap validators into functions
94 formencode_obj = self.formencode_obj(*args, **kwargs)
93 formencode_obj = self.formencode_obj(*args, **kwargs)
95 return formencode_obj(*self.args, **self.kwargs)
94 return formencode_obj(*self.args, **self.kwargs)
96
95
97
96
98 class RhodeCodeAuthPluginBase(object):
97 class RhodeCodeAuthPluginBase(object):
99 # UID is used to register plugin to the registry
98 # UID is used to register plugin to the registry
100 uid = None
99 uid = None
101
100
102 # cache the authentication request for N amount of seconds. Some kind
101 # cache the authentication request for N amount of seconds. Some kind
103 # of authentication methods are very heavy and it's very efficient to cache
102 # of authentication methods are very heavy and it's very efficient to cache
104 # the result of a call. If it's set to None (default) cache is off
103 # the result of a call. If it's set to None (default) cache is off
105 AUTH_CACHE_TTL = None
104 AUTH_CACHE_TTL = None
106 AUTH_CACHE = {}
105 AUTH_CACHE = {}
107
106
108 auth_func_attrs = {
107 auth_func_attrs = {
109 "username": "unique username",
108 "username": "unique username",
110 "firstname": "first name",
109 "firstname": "first name",
111 "lastname": "last name",
110 "lastname": "last name",
112 "email": "email address",
111 "email": "email address",
113 "groups": '["list", "of", "groups"]',
112 "groups": '["list", "of", "groups"]',
114 "user_group_sync":
113 "user_group_sync":
115 'True|False defines if returned user groups should be synced',
114 'True|False defines if returned user groups should be synced',
116 "extern_name": "name in external source of record",
115 "extern_name": "name in external source of record",
117 "extern_type": "type of external source of record",
116 "extern_type": "type of external source of record",
118 "admin": 'True|False defines if user should be RhodeCode super admin',
117 "admin": 'True|False defines if user should be RhodeCode super admin',
119 "active":
118 "active":
120 'True|False defines active state of user internally for RhodeCode',
119 'True|False defines active state of user internally for RhodeCode',
121 "active_from_extern":
120 "active_from_extern":
122 "True|False|None, active state from the external auth, "
121 "True|False|None, active state from the external auth, "
123 "None means use definition from RhodeCode extern_type active value"
122 "None means use definition from RhodeCode extern_type active value"
124
123
125 }
124 }
126 # set on authenticate() method and via set_auth_type func.
125 # set on authenticate() method and via set_auth_type func.
127 auth_type = None
126 auth_type = None
128
127
129 # set on authenticate() method and via set_calling_scope_repo, this is a
128 # set on authenticate() method and via set_calling_scope_repo, this is a
130 # calling scope repository when doing authentication most likely on VCS
129 # calling scope repository when doing authentication most likely on VCS
131 # operations
130 # operations
132 acl_repo_name = None
131 acl_repo_name = None
133
132
134 # List of setting names to store encrypted. Plugins may override this list
133 # List of setting names to store encrypted. Plugins may override this list
135 # to store settings encrypted.
134 # to store settings encrypted.
136 _settings_encrypted = []
135 _settings_encrypted = []
137
136
138 # Mapping of python to DB settings model types. Plugins may override or
137 # Mapping of python to DB settings model types. Plugins may override or
139 # extend this mapping.
138 # extend this mapping.
140 _settings_type_map = {
139 _settings_type_map = {
141 colander.String: 'unicode',
140 colander.String: 'unicode',
142 colander.Integer: 'int',
141 colander.Integer: 'int',
143 colander.Boolean: 'bool',
142 colander.Boolean: 'bool',
144 colander.List: 'list',
143 colander.List: 'list',
145 }
144 }
146
145
147 # list of keys in settings that are unsafe to be logged, should be passwords
146 # list of keys in settings that are unsafe to be logged, should be passwords
148 # or other crucial credentials
147 # or other crucial credentials
149 _settings_unsafe_keys = []
148 _settings_unsafe_keys = []
150
149
151 def __init__(self, plugin_id):
150 def __init__(self, plugin_id):
152 self._plugin_id = plugin_id
151 self._plugin_id = plugin_id
153
152
154 def __str__(self):
153 def __str__(self):
155 return self.get_id()
154 return self.get_id()
156
155
157 def _get_setting_full_name(self, name):
156 def _get_setting_full_name(self, name):
158 """
157 """
159 Return the full setting name used for storing values in the database.
158 Return the full setting name used for storing values in the database.
160 """
159 """
161 # TODO: johbo: Using the name here is problematic. It would be good to
160 # TODO: johbo: Using the name here is problematic. It would be good to
162 # introduce either new models in the database to hold Plugin and
161 # introduce either new models in the database to hold Plugin and
163 # PluginSetting or to use the plugin id here.
162 # PluginSetting or to use the plugin id here.
164 return 'auth_{}_{}'.format(self.name, name)
163 return 'auth_{}_{}'.format(self.name, name)
165
164
166 def _get_setting_type(self, name):
165 def _get_setting_type(self, name):
167 """
166 """
168 Return the type of a setting. This type is defined by the SettingsModel
167 Return the type of a setting. This type is defined by the SettingsModel
169 and determines how the setting is stored in DB. Optionally the suffix
168 and determines how the setting is stored in DB. Optionally the suffix
170 `.encrypted` is appended to instruct SettingsModel to store it
169 `.encrypted` is appended to instruct SettingsModel to store it
171 encrypted.
170 encrypted.
172 """
171 """
173 schema_node = self.get_settings_schema().get(name)
172 schema_node = self.get_settings_schema().get(name)
174 db_type = self._settings_type_map.get(
173 db_type = self._settings_type_map.get(
175 type(schema_node.typ), 'unicode')
174 type(schema_node.typ), 'unicode')
176 if name in self._settings_encrypted:
175 if name in self._settings_encrypted:
177 db_type = '{}.encrypted'.format(db_type)
176 db_type = '{}.encrypted'.format(db_type)
178 return db_type
177 return db_type
179
178
180 @classmethod
179 @classmethod
181 def docs(cls):
180 def docs(cls):
182 """
181 """
183 Defines documentation url which helps with plugin setup
182 Defines documentation url which helps with plugin setup
184 """
183 """
185 return ''
184 return ''
186
185
187 @classmethod
186 @classmethod
188 def icon(cls):
187 def icon(cls):
189 """
188 """
190 Defines ICON in SVG format for authentication method
189 Defines ICON in SVG format for authentication method
191 """
190 """
192 return ''
191 return ''
193
192
194 def is_enabled(self):
193 def is_enabled(self):
195 """
194 """
196 Returns true if this plugin is enabled. An enabled plugin can be
195 Returns true if this plugin is enabled. An enabled plugin can be
197 configured in the admin interface but it is not consulted during
196 configured in the admin interface but it is not consulted during
198 authentication.
197 authentication.
199 """
198 """
200 auth_plugins = SettingsModel().get_auth_plugins()
199 auth_plugins = SettingsModel().get_auth_plugins()
201 return self.get_id() in auth_plugins
200 return self.get_id() in auth_plugins
202
201
203 def is_active(self, plugin_cached_settings=None):
202 def is_active(self, plugin_cached_settings=None):
204 """
203 """
205 Returns true if the plugin is activated. An activated plugin is
204 Returns true if the plugin is activated. An activated plugin is
206 consulted during authentication, assumed it is also enabled.
205 consulted during authentication, assumed it is also enabled.
207 """
206 """
208 return self.get_setting_by_name(
207 return self.get_setting_by_name(
209 'enabled', plugin_cached_settings=plugin_cached_settings)
208 'enabled', plugin_cached_settings=plugin_cached_settings)
210
209
211 def get_id(self):
210 def get_id(self):
212 """
211 """
213 Returns the plugin id.
212 Returns the plugin id.
214 """
213 """
215 return self._plugin_id
214 return self._plugin_id
216
215
217 def get_display_name(self, load_from_settings=False):
216 def get_display_name(self, load_from_settings=False):
218 """
217 """
219 Returns a translation string for displaying purposes.
218 Returns a translation string for displaying purposes.
220 if load_from_settings is set, plugin settings can override the display name
219 if load_from_settings is set, plugin settings can override the display name
221 """
220 """
222 raise NotImplementedError('Not implemented in base class')
221 raise NotImplementedError('Not implemented in base class')
223
222
224 def get_settings_schema(self):
223 def get_settings_schema(self):
225 """
224 """
226 Returns a colander schema, representing the plugin settings.
225 Returns a colander schema, representing the plugin settings.
227 """
226 """
228 return AuthnPluginSettingsSchemaBase()
227 return AuthnPluginSettingsSchemaBase()
229
228
230 def _propagate_settings(self, raw_settings):
229 def _propagate_settings(self, raw_settings):
231 settings = {}
230 settings = {}
232 for node in self.get_settings_schema():
231 for node in self.get_settings_schema():
233 settings[node.name] = self.get_setting_by_name(
232 settings[node.name] = self.get_setting_by_name(
234 node.name, plugin_cached_settings=raw_settings)
233 node.name, plugin_cached_settings=raw_settings)
235 return settings
234 return settings
236
235
237 def get_settings(self, use_cache=True):
236 def get_settings(self, use_cache=True):
238 """
237 """
239 Returns the plugin settings as dictionary.
238 Returns the plugin settings as dictionary.
240 """
239 """
241
240
242 raw_settings = SettingsModel().get_all_settings(cache=use_cache)
241 raw_settings = SettingsModel().get_all_settings(cache=use_cache)
243 settings = self._propagate_settings(raw_settings)
242 settings = self._propagate_settings(raw_settings)
244
243
245 return settings
244 return settings
246
245
247 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
246 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
248 """
247 """
249 Returns a plugin setting by name.
248 Returns a plugin setting by name.
250 """
249 """
251 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
250 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
252 if plugin_cached_settings:
251 if plugin_cached_settings:
253 plugin_settings = plugin_cached_settings
252 plugin_settings = plugin_cached_settings
254 else:
253 else:
255 plugin_settings = SettingsModel().get_all_settings()
254 plugin_settings = SettingsModel().get_all_settings()
256
255
257 if full_name in plugin_settings:
256 if full_name in plugin_settings:
258 return plugin_settings[full_name]
257 return plugin_settings[full_name]
259 else:
258 else:
260 return default
259 return default
261
260
262 def create_or_update_setting(self, name, value):
261 def create_or_update_setting(self, name, value):
263 """
262 """
264 Create or update a setting for this plugin in the persistent storage.
263 Create or update a setting for this plugin in the persistent storage.
265 """
264 """
266 full_name = self._get_setting_full_name(name)
265 full_name = self._get_setting_full_name(name)
267 type_ = self._get_setting_type(name)
266 type_ = self._get_setting_type(name)
268 db_setting = SettingsModel().create_or_update_setting(
267 db_setting = SettingsModel().create_or_update_setting(
269 full_name, value, type_)
268 full_name, value, type_)
270 return db_setting.app_settings_value
269 return db_setting.app_settings_value
271
270
272 def log_safe_settings(self, settings):
271 def log_safe_settings(self, settings):
273 """
272 """
274 returns a log safe representation of settings, without any secrets
273 returns a log safe representation of settings, without any secrets
275 """
274 """
276 settings_copy = copy.deepcopy(settings)
275 settings_copy = copy.deepcopy(settings)
277 for k in self._settings_unsafe_keys:
276 for k in self._settings_unsafe_keys:
278 if k in settings_copy:
277 if k in settings_copy:
279 del settings_copy[k]
278 del settings_copy[k]
280 return settings_copy
279 return settings_copy
281
280
282 @hybrid_property
281 @hybrid_property
283 def name(self):
282 def name(self):
284 """
283 """
285 Returns the name of this authentication plugin.
284 Returns the name of this authentication plugin.
286
285
287 :returns: string
286 :returns: string
288 """
287 """
289 raise NotImplementedError("Not implemented in base class")
288 raise NotImplementedError("Not implemented in base class")
290
289
291 def get_url_slug(self):
290 def get_url_slug(self):
292 """
291 """
293 Returns a slug which should be used when constructing URLs which refer
292 Returns a slug which should be used when constructing URLs which refer
294 to this plugin. By default it returns the plugin name. If the name is
293 to this plugin. By default it returns the plugin name. If the name is
295 not suitable for using it in an URL the plugin should override this
294 not suitable for using it in an URL the plugin should override this
296 method.
295 method.
297 """
296 """
298 return self.name
297 return self.name
299
298
300 @property
299 @property
301 def is_headers_auth(self):
300 def is_headers_auth(self):
302 """
301 """
303 Returns True if this authentication plugin uses HTTP headers as
302 Returns True if this authentication plugin uses HTTP headers as
304 authentication method.
303 authentication method.
305 """
304 """
306 return False
305 return False
307
306
308 @hybrid_property
307 @hybrid_property
309 def is_container_auth(self):
308 def is_container_auth(self):
310 """
309 """
311 Deprecated method that indicates if this authentication plugin uses
310 Deprecated method that indicates if this authentication plugin uses
312 HTTP headers as authentication method.
311 HTTP headers as authentication method.
313 """
312 """
314 warnings.warn(
313 warnings.warn(
315 'Use is_headers_auth instead.', category=DeprecationWarning)
314 'Use is_headers_auth instead.', category=DeprecationWarning)
316 return self.is_headers_auth
315 return self.is_headers_auth
317
316
318 @hybrid_property
317 @hybrid_property
319 def allows_creating_users(self):
318 def allows_creating_users(self):
320 """
319 """
321 Defines if Plugin allows users to be created on-the-fly when
320 Defines if Plugin allows users to be created on-the-fly when
322 authentication is called. Controls how external plugins should behave
321 authentication is called. Controls how external plugins should behave
323 in terms if they are allowed to create new users, or not. Base plugins
322 in terms if they are allowed to create new users, or not. Base plugins
324 should not be allowed to, but External ones should be !
323 should not be allowed to, but External ones should be !
325
324
326 :return: bool
325 :return: bool
327 """
326 """
328 return False
327 return False
329
328
330 def set_auth_type(self, auth_type):
329 def set_auth_type(self, auth_type):
331 self.auth_type = auth_type
330 self.auth_type = auth_type
332
331
333 def set_calling_scope_repo(self, acl_repo_name):
332 def set_calling_scope_repo(self, acl_repo_name):
334 self.acl_repo_name = acl_repo_name
333 self.acl_repo_name = acl_repo_name
335
334
336 def allows_authentication_from(
335 def allows_authentication_from(
337 self, user, allows_non_existing_user=True,
336 self, user, allows_non_existing_user=True,
338 allowed_auth_plugins=None, allowed_auth_sources=None):
337 allowed_auth_plugins=None, allowed_auth_sources=None):
339 """
338 """
340 Checks if this authentication module should accept a request for
339 Checks if this authentication module should accept a request for
341 the current user.
340 the current user.
342
341
343 :param user: user object fetched using plugin's get_user() method.
342 :param user: user object fetched using plugin's get_user() method.
344 :param allows_non_existing_user: if True, don't allow the
343 :param allows_non_existing_user: if True, don't allow the
345 user to be empty, meaning not existing in our database
344 user to be empty, meaning not existing in our database
346 :param allowed_auth_plugins: if provided, users extern_type will be
345 :param allowed_auth_plugins: if provided, users extern_type will be
347 checked against a list of provided extern types, which are plugin
346 checked against a list of provided extern types, which are plugin
348 auth_names in the end
347 auth_names in the end
349 :param allowed_auth_sources: authentication type allowed,
348 :param allowed_auth_sources: authentication type allowed,
350 `http` or `vcs` default is both.
349 `http` or `vcs` default is both.
351 defines if plugin will accept only http authentication vcs
350 defines if plugin will accept only http authentication vcs
352 authentication(git/hg) or both
351 authentication(git/hg) or both
353 :returns: boolean
352 :returns: boolean
354 """
353 """
355 if not user and not allows_non_existing_user:
354 if not user and not allows_non_existing_user:
356 log.debug('User is empty but plugin does not allow empty users,'
355 log.debug('User is empty but plugin does not allow empty users,'
357 'not allowed to authenticate')
356 'not allowed to authenticate')
358 return False
357 return False
359
358
360 expected_auth_plugins = allowed_auth_plugins or [self.name]
359 expected_auth_plugins = allowed_auth_plugins or [self.name]
361 if user and (user.extern_type and
360 if user and (user.extern_type and
362 user.extern_type not in expected_auth_plugins):
361 user.extern_type not in expected_auth_plugins):
363 log.debug(
362 log.debug(
364 'User `%s` is bound to `%s` auth type. Plugin allows only '
363 'User `%s` is bound to `%s` auth type. Plugin allows only '
365 '%s, skipping', user, user.extern_type, expected_auth_plugins)
364 '%s, skipping', user, user.extern_type, expected_auth_plugins)
366
365
367 return False
366 return False
368
367
369 # by default accept both
368 # by default accept both
370 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
369 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
371 if self.auth_type not in expected_auth_from:
370 if self.auth_type not in expected_auth_from:
372 log.debug('Current auth source is %s but plugin only allows %s',
371 log.debug('Current auth source is %s but plugin only allows %s',
373 self.auth_type, expected_auth_from)
372 self.auth_type, expected_auth_from)
374 return False
373 return False
375
374
376 return True
375 return True
377
376
378 def get_user(self, username=None, **kwargs):
377 def get_user(self, username=None, **kwargs):
379 """
378 """
380 Helper method for user fetching in plugins, by default it's using
379 Helper method for user fetching in plugins, by default it's using
381 simple fetch by username, but this method can be custimized in plugins
380 simple fetch by username, but this method can be custimized in plugins
382 eg. headers auth plugin to fetch user by environ params
381 eg. headers auth plugin to fetch user by environ params
383
382
384 :param username: username if given to fetch from database
383 :param username: username if given to fetch from database
385 :param kwargs: extra arguments needed for user fetching.
384 :param kwargs: extra arguments needed for user fetching.
386 """
385 """
387 user = None
386 user = None
388 log.debug(
387 log.debug(
389 'Trying to fetch user `%s` from RhodeCode database', username)
388 'Trying to fetch user `%s` from RhodeCode database', username)
390 if username:
389 if username:
391 user = User.get_by_username(username)
390 user = User.get_by_username(username)
392 if not user:
391 if not user:
393 log.debug('User not found, fallback to fetch user in '
392 log.debug('User not found, fallback to fetch user in '
394 'case insensitive mode')
393 'case insensitive mode')
395 user = User.get_by_username(username, case_insensitive=True)
394 user = User.get_by_username(username, case_insensitive=True)
396 else:
395 else:
397 log.debug('provided username:`%s` is empty skipping...', username)
396 log.debug('provided username:`%s` is empty skipping...', username)
398 if not user:
397 if not user:
399 log.debug('User `%s` not found in database', username)
398 log.debug('User `%s` not found in database', username)
400 else:
399 else:
401 log.debug('Got DB user:%s', user)
400 log.debug('Got DB user:%s', user)
402 return user
401 return user
403
402
404 def user_activation_state(self):
403 def user_activation_state(self):
405 """
404 """
406 Defines user activation state when creating new users
405 Defines user activation state when creating new users
407
406
408 :returns: boolean
407 :returns: boolean
409 """
408 """
410 raise NotImplementedError("Not implemented in base class")
409 raise NotImplementedError("Not implemented in base class")
411
410
412 def auth(self, userobj, username, passwd, settings, **kwargs):
411 def auth(self, userobj, username, passwd, settings, **kwargs):
413 """
412 """
414 Given a user object (which may be null), username, a plaintext
413 Given a user object (which may be null), username, a plaintext
415 password, and a settings object (containing all the keys needed as
414 password, and a settings object (containing all the keys needed as
416 listed in settings()), authenticate this user's login attempt.
415 listed in settings()), authenticate this user's login attempt.
417
416
418 Return None on failure. On success, return a dictionary of the form:
417 Return None on failure. On success, return a dictionary of the form:
419
418
420 see: RhodeCodeAuthPluginBase.auth_func_attrs
419 see: RhodeCodeAuthPluginBase.auth_func_attrs
421 This is later validated for correctness
420 This is later validated for correctness
422 """
421 """
423 raise NotImplementedError("not implemented in base class")
422 raise NotImplementedError("not implemented in base class")
424
423
425 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
424 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
426 """
425 """
427 Wrapper to call self.auth() that validates call on it
426 Wrapper to call self.auth() that validates call on it
428
427
429 :param userobj: userobj
428 :param userobj: userobj
430 :param username: username
429 :param username: username
431 :param passwd: plaintext password
430 :param passwd: plaintext password
432 :param settings: plugin settings
431 :param settings: plugin settings
433 """
432 """
434 auth = self.auth(userobj, username, passwd, settings, **kwargs)
433 auth = self.auth(userobj, username, passwd, settings, **kwargs)
435 if auth:
434 if auth:
436 auth['_plugin'] = self.name
435 auth['_plugin'] = self.name
437 auth['_ttl_cache'] = self.get_ttl_cache(settings)
436 auth['_ttl_cache'] = self.get_ttl_cache(settings)
438 # check if hash should be migrated ?
437 # check if hash should be migrated ?
439 new_hash = auth.get('_hash_migrate')
438 new_hash = auth.get('_hash_migrate')
440 if new_hash:
439 if new_hash:
441 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
440 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
442 if 'user_group_sync' not in auth:
441 if 'user_group_sync' not in auth:
443 auth['user_group_sync'] = False
442 auth['user_group_sync'] = False
444 return self._validate_auth_return(auth)
443 return self._validate_auth_return(auth)
445 return auth
444 return auth
446
445
447 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
446 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
448 new_hash_cypher = _RhodeCodeCryptoBCrypt()
447 new_hash_cypher = _RhodeCodeCryptoBCrypt()
449 # extra checks, so make sure new hash is correct.
448 # extra checks, so make sure new hash is correct.
450 password_encoded = safe_str(password)
449 password_encoded = safe_str(password)
451 if new_hash and new_hash_cypher.hash_check(
450 if new_hash and new_hash_cypher.hash_check(
452 password_encoded, new_hash):
451 password_encoded, new_hash):
453 cur_user = User.get_by_username(username)
452 cur_user = User.get_by_username(username)
454 cur_user.password = new_hash
453 cur_user.password = new_hash
455 Session().add(cur_user)
454 Session().add(cur_user)
456 Session().flush()
455 Session().flush()
457 log.info('Migrated user %s hash to bcrypt', cur_user)
456 log.info('Migrated user %s hash to bcrypt', cur_user)
458
457
459 def _validate_auth_return(self, ret):
458 def _validate_auth_return(self, ret):
460 if not isinstance(ret, dict):
459 if not isinstance(ret, dict):
461 raise Exception('returned value from auth must be a dict')
460 raise Exception('returned value from auth must be a dict')
462 for k in self.auth_func_attrs:
461 for k in self.auth_func_attrs:
463 if k not in ret:
462 if k not in ret:
464 raise Exception('Missing %s attribute from returned data' % k)
463 raise Exception('Missing %s attribute from returned data' % k)
465 return ret
464 return ret
466
465
467 def get_ttl_cache(self, settings=None):
466 def get_ttl_cache(self, settings=None):
468 plugin_settings = settings or self.get_settings()
467 plugin_settings = settings or self.get_settings()
469 # we set default to 30, we make a compromise here,
468 # we set default to 30, we make a compromise here,
470 # performance > security, mostly due to LDAP/SVN, majority
469 # performance > security, mostly due to LDAP/SVN, majority
471 # of users pick cache_ttl to be enabled
470 # of users pick cache_ttl to be enabled
472 from rhodecode.authentication import plugin_default_auth_ttl
471 from rhodecode.authentication import plugin_default_auth_ttl
473 cache_ttl = plugin_default_auth_ttl
472 cache_ttl = plugin_default_auth_ttl
474
473
475 if isinstance(self.AUTH_CACHE_TTL, int):
474 if isinstance(self.AUTH_CACHE_TTL, int):
476 # plugin cache set inside is more important than the settings value
475 # plugin cache set inside is more important than the settings value
477 cache_ttl = self.AUTH_CACHE_TTL
476 cache_ttl = self.AUTH_CACHE_TTL
478 elif plugin_settings.get('cache_ttl'):
477 elif plugin_settings.get('cache_ttl'):
479 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
478 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
480
479
481 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
480 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
482 return plugin_cache_active, cache_ttl
481 return plugin_cache_active, cache_ttl
483
482
484
483
485 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
484 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
486
485
487 @hybrid_property
486 @hybrid_property
488 def allows_creating_users(self):
487 def allows_creating_users(self):
489 return True
488 return True
490
489
491 def use_fake_password(self):
490 def use_fake_password(self):
492 """
491 """
493 Return a boolean that indicates whether or not we should set the user's
492 Return a boolean that indicates whether or not we should set the user's
494 password to a random value when it is authenticated by this plugin.
493 password to a random value when it is authenticated by this plugin.
495 If your plugin provides authentication, then you will generally
494 If your plugin provides authentication, then you will generally
496 want this.
495 want this.
497
496
498 :returns: boolean
497 :returns: boolean
499 """
498 """
500 raise NotImplementedError("Not implemented in base class")
499 raise NotImplementedError("Not implemented in base class")
501
500
502 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
501 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
503 # at this point _authenticate calls plugin's `auth()` function
502 # at this point _authenticate calls plugin's `auth()` function
504 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
503 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
505 userobj, username, passwd, settings, **kwargs)
504 userobj, username, passwd, settings, **kwargs)
506
505
507 if auth:
506 if auth:
508 # maybe plugin will clean the username ?
507 # maybe plugin will clean the username ?
509 # we should use the return value
508 # we should use the return value
510 username = auth['username']
509 username = auth['username']
511
510
512 # if external source tells us that user is not active, we should
511 # if external source tells us that user is not active, we should
513 # skip rest of the process. This can prevent from creating users in
512 # skip rest of the process. This can prevent from creating users in
514 # RhodeCode when using external authentication, but if it's
513 # RhodeCode when using external authentication, but if it's
515 # inactive user we shouldn't create that user anyway
514 # inactive user we shouldn't create that user anyway
516 if auth['active_from_extern'] is False:
515 if auth['active_from_extern'] is False:
517 log.warning(
516 log.warning(
518 "User %s authenticated against %s, but is inactive",
517 "User %s authenticated against %s, but is inactive",
519 username, self.__module__)
518 username, self.__module__)
520 return None
519 return None
521
520
522 cur_user = User.get_by_username(username, case_insensitive=True)
521 cur_user = User.get_by_username(username, case_insensitive=True)
523 is_user_existing = cur_user is not None
522 is_user_existing = cur_user is not None
524
523
525 if is_user_existing:
524 if is_user_existing:
526 log.debug('Syncing user `%s` from '
525 log.debug('Syncing user `%s` from '
527 '`%s` plugin', username, self.name)
526 '`%s` plugin', username, self.name)
528 else:
527 else:
529 log.debug('Creating non existing user `%s` from '
528 log.debug('Creating non existing user `%s` from '
530 '`%s` plugin', username, self.name)
529 '`%s` plugin', username, self.name)
531
530
532 if self.allows_creating_users:
531 if self.allows_creating_users:
533 log.debug('Plugin `%s` allows to '
532 log.debug('Plugin `%s` allows to '
534 'create new users', self.name)
533 'create new users', self.name)
535 else:
534 else:
536 log.debug('Plugin `%s` does not allow to '
535 log.debug('Plugin `%s` does not allow to '
537 'create new users', self.name)
536 'create new users', self.name)
538
537
539 user_parameters = {
538 user_parameters = {
540 'username': username,
539 'username': username,
541 'email': auth["email"],
540 'email': auth["email"],
542 'firstname': auth["firstname"],
541 'firstname': auth["firstname"],
543 'lastname': auth["lastname"],
542 'lastname': auth["lastname"],
544 'active': auth["active"],
543 'active': auth["active"],
545 'admin': auth["admin"],
544 'admin': auth["admin"],
546 'extern_name': auth["extern_name"],
545 'extern_name': auth["extern_name"],
547 'extern_type': self.name,
546 'extern_type': self.name,
548 'plugin': self,
547 'plugin': self,
549 'allow_to_create_user': self.allows_creating_users,
548 'allow_to_create_user': self.allows_creating_users,
550 }
549 }
551
550
552 if not is_user_existing:
551 if not is_user_existing:
553 if self.use_fake_password():
552 if self.use_fake_password():
554 # Randomize the PW because we don't need it, but don't want
553 # Randomize the PW because we don't need it, but don't want
555 # them blank either
554 # them blank either
556 passwd = PasswordGenerator().gen_password(length=16)
555 passwd = PasswordGenerator().gen_password(length=16)
557 user_parameters['password'] = passwd
556 user_parameters['password'] = passwd
558 else:
557 else:
559 # Since the password is required by create_or_update method of
558 # Since the password is required by create_or_update method of
560 # UserModel, we need to set it explicitly.
559 # UserModel, we need to set it explicitly.
561 # The create_or_update method is smart and recognises the
560 # The create_or_update method is smart and recognises the
562 # password hashes as well.
561 # password hashes as well.
563 user_parameters['password'] = cur_user.password
562 user_parameters['password'] = cur_user.password
564
563
565 # we either create or update users, we also pass the flag
564 # we either create or update users, we also pass the flag
566 # that controls if this method can actually do that.
565 # that controls if this method can actually do that.
567 # raises NotAllowedToCreateUserError if it cannot, and we try to.
566 # raises NotAllowedToCreateUserError if it cannot, and we try to.
568 user = UserModel().create_or_update(**user_parameters)
567 user = UserModel().create_or_update(**user_parameters)
569 Session().flush()
568 Session().flush()
570 # enforce user is just in given groups, all of them has to be ones
569 # enforce user is just in given groups, all of them has to be ones
571 # created from plugins. We store this info in _group_data JSON
570 # created from plugins. We store this info in _group_data JSON
572 # field
571 # field
573
572
574 if auth['user_group_sync']:
573 if auth['user_group_sync']:
575 try:
574 try:
576 groups = auth['groups'] or []
575 groups = auth['groups'] or []
577 log.debug(
576 log.debug(
578 'Performing user_group sync based on set `%s` '
577 'Performing user_group sync based on set `%s` '
579 'returned by `%s` plugin', groups, self.name)
578 'returned by `%s` plugin', groups, self.name)
580 UserGroupModel().enforce_groups(user, groups, self.name)
579 UserGroupModel().enforce_groups(user, groups, self.name)
581 except Exception:
580 except Exception:
582 # for any reason group syncing fails, we should
581 # for any reason group syncing fails, we should
583 # proceed with login
582 # proceed with login
584 log.error(traceback.format_exc())
583 log.error(traceback.format_exc())
585
584
586 Session().commit()
585 Session().commit()
587 return auth
586 return auth
588
587
589
588
590 class AuthLdapBase(object):
589 class AuthLdapBase(object):
591
590
592 @classmethod
591 @classmethod
593 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
592 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
594
593
595 def host_resolver(host, port, full_resolve=True):
594 def host_resolver(host, port, full_resolve=True):
596 """
595 """
597 Main work for this function is to prevent ldap connection issues,
596 Main work for this function is to prevent ldap connection issues,
598 and detect them early using a "greenified" sockets
597 and detect them early using a "greenified" sockets
599 """
598 """
600 host = host.strip()
599 host = host.strip()
601 if not full_resolve:
600 if not full_resolve:
602 return '{}:{}'.format(host, port)
601 return '{}:{}'.format(host, port)
603
602
604 log.debug('LDAP: Resolving IP for LDAP host `%s`', host)
603 log.debug('LDAP: Resolving IP for LDAP host `%s`', host)
605 try:
604 try:
606 ip = socket.gethostbyname(host)
605 ip = socket.gethostbyname(host)
607 log.debug('LDAP: Got LDAP host `%s` ip %s', host, ip)
606 log.debug('LDAP: Got LDAP host `%s` ip %s', host, ip)
608 except Exception:
607 except Exception:
609 raise LdapConnectionError('Failed to resolve host: `{}`'.format(host))
608 raise LdapConnectionError('Failed to resolve host: `{}`'.format(host))
610
609
611 log.debug('LDAP: Checking if IP %s is accessible', ip)
610 log.debug('LDAP: Checking if IP %s is accessible', ip)
612 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
611 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
613 try:
612 try:
614 s.connect((ip, int(port)))
613 s.connect((ip, int(port)))
615 s.shutdown(socket.SHUT_RD)
614 s.shutdown(socket.SHUT_RD)
616 log.debug('LDAP: connection to %s successful', ip)
615 log.debug('LDAP: connection to %s successful', ip)
617 except Exception:
616 except Exception:
618 raise LdapConnectionError(
617 raise LdapConnectionError(
619 'Failed to connect to host: `{}:{}`'.format(host, port))
618 'Failed to connect to host: `{}:{}`'.format(host, port))
620
619
621 return '{}:{}'.format(host, port)
620 return '{}:{}'.format(host, port)
622
621
623 if len(ldap_server) == 1:
622 if len(ldap_server) == 1:
624 # in case of single server use resolver to detect potential
623 # in case of single server use resolver to detect potential
625 # connection issues
624 # connection issues
626 full_resolve = True
625 full_resolve = True
627 else:
626 else:
628 full_resolve = False
627 full_resolve = False
629
628
630 return ', '.join(
629 return ', '.join(
631 ["{}://{}".format(
630 ["{}://{}".format(
632 ldap_server_type,
631 ldap_server_type,
633 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
632 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
634 for host in ldap_server])
633 for host in ldap_server])
635
634
636 @classmethod
635 @classmethod
637 def _get_server_list(cls, servers):
636 def _get_server_list(cls, servers):
638 return map(string.strip, servers.split(','))
637 return map(string.strip, servers.split(','))
639
638
640 @classmethod
639 @classmethod
641 def get_uid(cls, username, server_addresses):
640 def get_uid(cls, username, server_addresses):
642 uid = username
641 uid = username
643 for server_addr in server_addresses:
642 for server_addr in server_addresses:
644 uid = chop_at(username, "@%s" % server_addr)
643 uid = chop_at(username, "@%s" % server_addr)
645 return uid
644 return uid
646
645
647 @classmethod
646 @classmethod
648 def validate_username(cls, username):
647 def validate_username(cls, username):
649 if "," in username:
648 if "," in username:
650 raise LdapUsernameError(
649 raise LdapUsernameError(
651 "invalid character `,` in username: `{}`".format(username))
650 "invalid character `,` in username: `{}`".format(username))
652
651
653 @classmethod
652 @classmethod
654 def validate_password(cls, username, password):
653 def validate_password(cls, username, password):
655 if not password:
654 if not password:
656 msg = "Authenticating user %s with blank password not allowed"
655 msg = "Authenticating user %s with blank password not allowed"
657 log.warning(msg, username)
656 log.warning(msg, username)
658 raise LdapPasswordError(msg)
657 raise LdapPasswordError(msg)
659
658
660
659
661 def loadplugin(plugin_id):
660 def loadplugin(plugin_id):
662 """
661 """
663 Loads and returns an instantiated authentication plugin.
662 Loads and returns an instantiated authentication plugin.
664 Returns the RhodeCodeAuthPluginBase subclass on success,
663 Returns the RhodeCodeAuthPluginBase subclass on success,
665 or None on failure.
664 or None on failure.
666 """
665 """
667 # TODO: Disusing pyramids thread locals to retrieve the registry.
666 # TODO: Disusing pyramids thread locals to retrieve the registry.
668 authn_registry = get_authn_registry()
667 authn_registry = get_authn_registry()
669 plugin = authn_registry.get_plugin(plugin_id)
668 plugin = authn_registry.get_plugin(plugin_id)
670 if plugin is None:
669 if plugin is None:
671 log.error('Authentication plugin not found: "%s"', plugin_id)
670 log.error('Authentication plugin not found: "%s"', plugin_id)
672 return plugin
671 return plugin
673
672
674
673
675 def get_authn_registry(registry=None):
674 def get_authn_registry(registry=None):
676 registry = registry or get_current_registry()
675 registry = registry or get_current_registry()
677 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
676 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
678 return authn_registry
677 return authn_registry
679
678
680
679
681 def authenticate(username, password, environ=None, auth_type=None,
680 def authenticate(username, password, environ=None, auth_type=None,
682 skip_missing=False, registry=None, acl_repo_name=None):
681 skip_missing=False, registry=None, acl_repo_name=None):
683 """
682 """
684 Authentication function used for access control,
683 Authentication function used for access control,
685 It tries to authenticate based on enabled authentication modules.
684 It tries to authenticate based on enabled authentication modules.
686
685
687 :param username: username can be empty for headers auth
686 :param username: username can be empty for headers auth
688 :param password: password can be empty for headers auth
687 :param password: password can be empty for headers auth
689 :param environ: environ headers passed for headers auth
688 :param environ: environ headers passed for headers auth
690 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
689 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
691 :param skip_missing: ignores plugins that are in db but not in environment
690 :param skip_missing: ignores plugins that are in db but not in environment
692 :returns: None if auth failed, plugin_user dict if auth is correct
691 :returns: None if auth failed, plugin_user dict if auth is correct
693 """
692 """
694 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
693 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
695 raise ValueError('auth type must be on of http, vcs got "%s" instead'
694 raise ValueError('auth type must be on of http, vcs got "%s" instead'
696 % auth_type)
695 % auth_type)
697 headers_only = environ and not (username and password)
696 headers_only = environ and not (username and password)
698
697
699 authn_registry = get_authn_registry(registry)
698 authn_registry = get_authn_registry(registry)
700
699
701 plugins_to_check = authn_registry.get_plugins_for_authentication()
700 plugins_to_check = authn_registry.get_plugins_for_authentication()
702 log.debug('Starting ordered authentication chain using %s plugins',
701 log.debug('Starting ordered authentication chain using %s plugins',
703 [x.name for x in plugins_to_check])
702 [x.name for x in plugins_to_check])
704 for plugin in plugins_to_check:
703 for plugin in plugins_to_check:
705 plugin.set_auth_type(auth_type)
704 plugin.set_auth_type(auth_type)
706 plugin.set_calling_scope_repo(acl_repo_name)
705 plugin.set_calling_scope_repo(acl_repo_name)
707
706
708 if headers_only and not plugin.is_headers_auth:
707 if headers_only and not plugin.is_headers_auth:
709 log.debug('Auth type is for headers only and plugin `%s` is not '
708 log.debug('Auth type is for headers only and plugin `%s` is not '
710 'headers plugin, skipping...', plugin.get_id())
709 'headers plugin, skipping...', plugin.get_id())
711 continue
710 continue
712
711
713 log.debug('Trying authentication using ** %s **', plugin.get_id())
712 log.debug('Trying authentication using ** %s **', plugin.get_id())
714
713
715 # load plugin settings from RhodeCode database
714 # load plugin settings from RhodeCode database
716 plugin_settings = plugin.get_settings()
715 plugin_settings = plugin.get_settings()
717 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
716 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
718 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
717 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
719
718
720 # use plugin's method of user extraction.
719 # use plugin's method of user extraction.
721 user = plugin.get_user(username, environ=environ,
720 user = plugin.get_user(username, environ=environ,
722 settings=plugin_settings)
721 settings=plugin_settings)
723 display_user = user.username if user else username
722 display_user = user.username if user else username
724 log.debug(
723 log.debug(
725 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
724 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
726
725
727 if not plugin.allows_authentication_from(user):
726 if not plugin.allows_authentication_from(user):
728 log.debug('Plugin %s does not accept user `%s` for authentication',
727 log.debug('Plugin %s does not accept user `%s` for authentication',
729 plugin.get_id(), display_user)
728 plugin.get_id(), display_user)
730 continue
729 continue
731 else:
730 else:
732 log.debug('Plugin %s accepted user `%s` for authentication',
731 log.debug('Plugin %s accepted user `%s` for authentication',
733 plugin.get_id(), display_user)
732 plugin.get_id(), display_user)
734
733
735 log.info('Authenticating user `%s` using %s plugin',
734 log.info('Authenticating user `%s` using %s plugin',
736 display_user, plugin.get_id())
735 display_user, plugin.get_id())
737
736
738 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
737 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
739
738
740 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
739 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
741 plugin.get_id(), plugin_cache_active, cache_ttl)
740 plugin.get_id(), plugin_cache_active, cache_ttl)
742
741
743 user_id = user.user_id if user else 'no-user'
742 user_id = user.user_id if user else 'no-user'
744 # don't cache for empty users
743 # don't cache for empty users
745 plugin_cache_active = plugin_cache_active and user_id
744 plugin_cache_active = plugin_cache_active and user_id
746 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
745 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
747 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
746 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
748
747
749 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
748 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
750 expiration_time=cache_ttl,
749 expiration_time=cache_ttl,
751 condition=plugin_cache_active)
750 condition=plugin_cache_active)
752 def compute_auth(
751 def compute_auth(
753 cache_name, plugin_name, username, password):
752 cache_name, plugin_name, username, password):
754
753
755 # _authenticate is a wrapper for .auth() method of plugin.
754 # _authenticate is a wrapper for .auth() method of plugin.
756 # it checks if .auth() sends proper data.
755 # it checks if .auth() sends proper data.
757 # For RhodeCodeExternalAuthPlugin it also maps users to
756 # For RhodeCodeExternalAuthPlugin it also maps users to
758 # Database and maps the attributes returned from .auth()
757 # Database and maps the attributes returned from .auth()
759 # to RhodeCode database. If this function returns data
758 # to RhodeCode database. If this function returns data
760 # then auth is correct.
759 # then auth is correct.
761 log.debug('Running plugin `%s` _authenticate method '
760 log.debug('Running plugin `%s` _authenticate method '
762 'using username and password', plugin.get_id())
761 'using username and password', plugin.get_id())
763 return plugin._authenticate(
762 return plugin._authenticate(
764 user, username, password, plugin_settings,
763 user, username, password, plugin_settings,
765 environ=environ or {})
764 environ=environ or {})
766
765
767 start = time.time()
766 start = time.time()
768 # for environ based auth, password can be empty, but then the validation is
767 # for environ based auth, password can be empty, but then the validation is
769 # on the server that fills in the env data needed for authentication
768 # on the server that fills in the env data needed for authentication
770 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
769 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
771
770
772 auth_time = time.time() - start
771 auth_time = time.time() - start
773 log.debug('Authentication for plugin `%s` completed in %.4fs, '
772 log.debug('Authentication for plugin `%s` completed in %.4fs, '
774 'expiration time of fetched cache %.1fs.',
773 'expiration time of fetched cache %.1fs.',
775 plugin.get_id(), auth_time, cache_ttl,
774 plugin.get_id(), auth_time, cache_ttl,
776 extra={"plugin": plugin.get_id(), "time": auth_time})
775 extra={"plugin": plugin.get_id(), "time": auth_time})
777
776
778 log.debug('PLUGIN USER DATA: %s', plugin_user)
777 log.debug('PLUGIN USER DATA: %s', plugin_user)
779
778
780 statsd = StatsdClient.statsd
779 statsd = StatsdClient.statsd
781
780
782 if plugin_user:
781 if plugin_user:
783 log.debug('Plugin returned proper authentication data')
782 log.debug('Plugin returned proper authentication data')
784 if statsd:
783 if statsd:
785 elapsed_time_ms = round(1000.0 * auth_time) # use ms only
784 elapsed_time_ms = round(1000.0 * auth_time) # use ms only
786 statsd.incr('rhodecode_login_success_total')
785 statsd.incr('rhodecode_login_success_total')
787 statsd.timing("rhodecode_login_timing.histogram", elapsed_time_ms,
786 statsd.timing("rhodecode_login_timing.histogram", elapsed_time_ms,
788 tags=["plugin:{}".format(plugin.get_id())],
787 tags=["plugin:{}".format(plugin.get_id())],
789 use_decimals=False
788 use_decimals=False
790 )
789 )
791 return plugin_user
790 return plugin_user
792
791
793 # we failed to Auth because .auth() method didn't return proper user
792 # we failed to Auth because .auth() method didn't return proper user
794 log.debug("User `%s` failed to authenticate against %s",
793 log.debug("User `%s` failed to authenticate against %s",
795 display_user, plugin.get_id())
794 display_user, plugin.get_id())
796 if statsd:
795 if statsd:
797 statsd.incr('rhodecode_login_fail_total')
796 statsd.incr('rhodecode_login_fail_total')
798
797
799 # case when we failed to authenticate against all defined plugins
798 # case when we failed to authenticate against all defined plugins
800 return None
799 return None
801
800
802
801
803 def chop_at(s, sub, inclusive=False):
802 def chop_at(s, sub, inclusive=False):
804 """Truncate string ``s`` at the first occurrence of ``sub``.
803 """Truncate string ``s`` at the first occurrence of ``sub``.
805
804
806 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
805 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
807
806
808 >>> chop_at("plutocratic brats", "rat")
807 >>> chop_at("plutocratic brats", "rat")
809 'plutoc'
808 'plutoc'
810 >>> chop_at("plutocratic brats", "rat", True)
809 >>> chop_at("plutocratic brats", "rat", True)
811 'plutocrat'
810 'plutocrat'
812 """
811 """
813 pos = s.find(sub)
812 pos = s.find(sub)
814 if pos == -1:
813 if pos == -1:
815 return s
814 return s
816 if inclusive:
815 if inclusive:
817 return s[:pos+len(sub)]
816 return s[:pos+len(sub)]
818 return s[:pos]
817 return s[:pos]
@@ -1,29 +1,29 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 from zope.interface import Interface
21 from zope.interface import Interface
22
22
23
23
24 class IAuthnPluginRegistry(Interface):
24 class IAuthnPluginRegistry(Interface):
25 """
25 """
26 Interface for the authentication plugin registry. Currently this is only
26 Interface for the authentication plugin registry. Currently this is only
27 used to register and retrieve it via pyramids registry.
27 used to register and retrieve it via pyramids registry.
28 """
28 """
29 pass
29 pass
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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/
@@ -1,295 +1,295 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 RhodeCode authentication plugin for Atlassian CROWD
22 RhodeCode authentication plugin for Atlassian CROWD
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import base64
27 import base64
28 import logging
28 import logging
29 import urllib.request, urllib.error, urllib.parse
29 import urllib.request, urllib.error, urllib.parse
30
30
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.ext_json import json, formatted_json
37 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwargs):
43 def plugin_factory(plugin_id, *args, **kwargs):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class CrowdAuthnResource(AuthnPluginResourceBase):
52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 host = colander.SchemaNode(
57 host = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='127.0.0.1',
59 default='127.0.0.1',
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('Host'),
62 title=_('Host'),
63 widget='string')
63 widget='string')
64 port = colander.SchemaNode(
64 port = colander.SchemaNode(
65 colander.Int(),
65 colander.Int(),
66 default=8095,
66 default=8095,
67 description=_('The Port in use by the Atlassian CROWD Server'),
67 description=_('The Port in use by the Atlassian CROWD Server'),
68 preparer=strip_whitespace,
68 preparer=strip_whitespace,
69 title=_('Port'),
69 title=_('Port'),
70 validator=colander.Range(min=0, max=65536),
70 validator=colander.Range(min=0, max=65536),
71 widget='int')
71 widget='int')
72 app_name = colander.SchemaNode(
72 app_name = colander.SchemaNode(
73 colander.String(),
73 colander.String(),
74 default='',
74 default='',
75 description=_('The Application Name to authenticate to CROWD'),
75 description=_('The Application Name to authenticate to CROWD'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('Application Name'),
77 title=_('Application Name'),
78 widget='string')
78 widget='string')
79 app_password = colander.SchemaNode(
79 app_password = colander.SchemaNode(
80 colander.String(),
80 colander.String(),
81 default='',
81 default='',
82 description=_('The password to authenticate to CROWD'),
82 description=_('The password to authenticate to CROWD'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Application Password'),
84 title=_('Application Password'),
85 widget='password')
85 widget='password')
86 admin_groups = colander.SchemaNode(
86 admin_groups = colander.SchemaNode(
87 colander.String(),
87 colander.String(),
88 default='',
88 default='',
89 description=_('A comma separated list of group names that identify '
89 description=_('A comma separated list of group names that identify '
90 'users as RhodeCode Administrators'),
90 'users as RhodeCode Administrators'),
91 missing='',
91 missing='',
92 preparer=strip_whitespace,
92 preparer=strip_whitespace,
93 title=_('Admin Groups'),
93 title=_('Admin Groups'),
94 widget='string')
94 widget='string')
95
95
96
96
97 class CrowdServer(object):
97 class CrowdServer(object):
98 def __init__(self, *args, **kwargs):
98 def __init__(self, *args, **kwargs):
99 """
99 """
100 Create a new CrowdServer object that points to IP/Address 'host',
100 Create a new CrowdServer object that points to IP/Address 'host',
101 on the given port, and using the given method (https/http). user and
101 on the given port, and using the given method (https/http). user and
102 passwd can be set here or with set_credentials. If unspecified,
102 passwd can be set here or with set_credentials. If unspecified,
103 "version" defaults to "latest".
103 "version" defaults to "latest".
104
104
105 example::
105 example::
106
106
107 cserver = CrowdServer(host="127.0.0.1",
107 cserver = CrowdServer(host="127.0.0.1",
108 port="8095",
108 port="8095",
109 user="some_app",
109 user="some_app",
110 passwd="some_passwd",
110 passwd="some_passwd",
111 version="1")
111 version="1")
112 """
112 """
113 if not "port" in kwargs:
113 if not "port" in kwargs:
114 kwargs["port"] = "8095"
114 kwargs["port"] = "8095"
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 kwargs.get("host", "127.0.0.1"),
117 kwargs.get("host", "127.0.0.1"),
118 kwargs.get("port", "8095"))
118 kwargs.get("port", "8095"))
119 self.set_credentials(kwargs.get("user", ""),
119 self.set_credentials(kwargs.get("user", ""),
120 kwargs.get("passwd", ""))
120 kwargs.get("passwd", ""))
121 self._version = kwargs.get("version", "latest")
121 self._version = kwargs.get("version", "latest")
122 self._url_list = None
122 self._url_list = None
123 self._appname = "crowd"
123 self._appname = "crowd"
124
124
125 def set_credentials(self, user, passwd):
125 def set_credentials(self, user, passwd):
126 self.user = user
126 self.user = user
127 self.passwd = passwd
127 self.passwd = passwd
128 self._make_opener()
128 self._make_opener()
129
129
130 def _make_opener(self):
130 def _make_opener(self):
131 mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
131 mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
132 mgr.add_password(None, self._uri, self.user, self.passwd)
132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 handler = urllib.request.HTTPBasicAuthHandler(mgr)
133 handler = urllib.request.HTTPBasicAuthHandler(mgr)
134 self.opener = urllib.request.build_opener(handler)
134 self.opener = urllib.request.build_opener(handler)
135
135
136 def _request(self, url, body=None, headers=None,
136 def _request(self, url, body=None, headers=None,
137 method=None, noformat=False,
137 method=None, noformat=False,
138 empty_response_ok=False):
138 empty_response_ok=False):
139 _headers = {"Content-type": "application/json",
139 _headers = {"Content-type": "application/json",
140 "Accept": "application/json"}
140 "Accept": "application/json"}
141 if self.user and self.passwd:
141 if self.user and self.passwd:
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 _headers["Authorization"] = "Basic %s" % authstring
143 _headers["Authorization"] = "Basic %s" % authstring
144 if headers:
144 if headers:
145 _headers.update(headers)
145 _headers.update(headers)
146 log.debug("Sent crowd: \n%s"
146 log.debug("Sent crowd: \n%s"
147 % (formatted_json({"url": url, "body": body,
147 % (formatted_json({"url": url, "body": body,
148 "headers": _headers})))
148 "headers": _headers})))
149 request = urllib.request.Request(url, body, _headers)
149 request = urllib.request.Request(url, body, _headers)
150 if method:
150 if method:
151 request.get_method = lambda: method
151 request.get_method = lambda: method
152
152
153 global msg
153 global msg
154 msg = ""
154 msg = ""
155 try:
155 try:
156 ret_doc = self.opener.open(request)
156 ret_doc = self.opener.open(request)
157 msg = ret_doc.read()
157 msg = ret_doc.read()
158 if not msg and empty_response_ok:
158 if not msg and empty_response_ok:
159 ret_val = {}
159 ret_val = {}
160 ret_val["status"] = True
160 ret_val["status"] = True
161 ret_val["error"] = "Response body was empty"
161 ret_val["error"] = "Response body was empty"
162 elif not noformat:
162 elif not noformat:
163 ret_val = json.loads(msg)
163 ret_val = json.loads(msg)
164 ret_val["status"] = True
164 ret_val["status"] = True
165 else:
165 else:
166 ret_val = msg
166 ret_val = msg
167 except Exception as e:
167 except Exception as e:
168 if not noformat:
168 if not noformat:
169 ret_val = {"status": False,
169 ret_val = {"status": False,
170 "body": body,
170 "body": body,
171 "error": "{}\n{}".format(e, msg)}
171 "error": "{}\n{}".format(e, msg)}
172 else:
172 else:
173 ret_val = None
173 ret_val = None
174 return ret_val
174 return ret_val
175
175
176 def user_auth(self, username, password):
176 def user_auth(self, username, password):
177 """Authenticate a user against crowd. Returns brief information about
177 """Authenticate a user against crowd. Returns brief information about
178 the user."""
178 the user."""
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 % (self._uri, self._version, username))
180 % (self._uri, self._version, username))
181 body = json.dumps({"value": password})
181 body = json.dumps({"value": password})
182 return self._request(url, body)
182 return self._request(url, body)
183
183
184 def user_groups(self, username):
184 def user_groups(self, username):
185 """Retrieve a list of groups to which this user belongs."""
185 """Retrieve a list of groups to which this user belongs."""
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 % (self._uri, self._version, username))
187 % (self._uri, self._version, username))
188 return self._request(url)
188 return self._request(url)
189
189
190
190
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 uid = 'crowd'
192 uid = 'crowd'
193 _settings_unsafe_keys = ['app_password']
193 _settings_unsafe_keys = ['app_password']
194
194
195 def includeme(self, config):
195 def includeme(self, config):
196 config.add_authn_plugin(self)
196 config.add_authn_plugin(self)
197 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
198 config.add_view(
198 config.add_view(
199 'rhodecode.authentication.views.AuthnPluginViewBase',
199 'rhodecode.authentication.views.AuthnPluginViewBase',
200 attr='settings_get',
200 attr='settings_get',
201 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
202 request_method='GET',
202 request_method='GET',
203 route_name='auth_home',
203 route_name='auth_home',
204 context=CrowdAuthnResource)
204 context=CrowdAuthnResource)
205 config.add_view(
205 config.add_view(
206 'rhodecode.authentication.views.AuthnPluginViewBase',
206 'rhodecode.authentication.views.AuthnPluginViewBase',
207 attr='settings_post',
207 attr='settings_post',
208 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
209 request_method='POST',
209 request_method='POST',
210 route_name='auth_home',
210 route_name='auth_home',
211 context=CrowdAuthnResource)
211 context=CrowdAuthnResource)
212
212
213 def get_settings_schema(self):
213 def get_settings_schema(self):
214 return CrowdSettingsSchema()
214 return CrowdSettingsSchema()
215
215
216 def get_display_name(self, load_from_settings=False):
216 def get_display_name(self, load_from_settings=False):
217 return _('CROWD')
217 return _('CROWD')
218
218
219 @classmethod
219 @classmethod
220 def docs(cls):
220 def docs(cls):
221 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
221 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
222
222
223 @hybrid_property
223 @hybrid_property
224 def name(self):
224 def name(self):
225 return u"crowd"
225 return u"crowd"
226
226
227 def use_fake_password(self):
227 def use_fake_password(self):
228 return True
228 return True
229
229
230 def user_activation_state(self):
230 def user_activation_state(self):
231 def_user_perms = User.get_default_user().AuthUser().permissions['global']
231 def_user_perms = User.get_default_user().AuthUser().permissions['global']
232 return 'hg.extern_activate.auto' in def_user_perms
232 return 'hg.extern_activate.auto' in def_user_perms
233
233
234 def auth(self, userobj, username, password, settings, **kwargs):
234 def auth(self, userobj, username, password, settings, **kwargs):
235 """
235 """
236 Given a user object (which may be null), username, a plaintext password,
236 Given a user object (which may be null), username, a plaintext password,
237 and a settings object (containing all the keys needed as listed in settings()),
237 and a settings object (containing all the keys needed as listed in settings()),
238 authenticate this user's login attempt.
238 authenticate this user's login attempt.
239
239
240 Return None on failure. On success, return a dictionary of the form:
240 Return None on failure. On success, return a dictionary of the form:
241
241
242 see: RhodeCodeAuthPluginBase.auth_func_attrs
242 see: RhodeCodeAuthPluginBase.auth_func_attrs
243 This is later validated for correctness
243 This is later validated for correctness
244 """
244 """
245 if not username or not password:
245 if not username or not password:
246 log.debug('Empty username or password skipping...')
246 log.debug('Empty username or password skipping...')
247 return None
247 return None
248
248
249 log.debug("Crowd settings: \n%s", formatted_json(settings))
249 log.debug("Crowd settings: \n%s", formatted_json(settings))
250 server = CrowdServer(**settings)
250 server = CrowdServer(**settings)
251 server.set_credentials(settings["app_name"], settings["app_password"])
251 server.set_credentials(settings["app_name"], settings["app_password"])
252 crowd_user = server.user_auth(username, password)
252 crowd_user = server.user_auth(username, password)
253 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
253 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
254 if not crowd_user["status"]:
254 if not crowd_user["status"]:
255 return None
255 return None
256
256
257 res = server.user_groups(crowd_user["name"])
257 res = server.user_groups(crowd_user["name"])
258 log.debug("Crowd groups: \n%s", formatted_json(res))
258 log.debug("Crowd groups: \n%s", formatted_json(res))
259 crowd_user["groups"] = [x["name"] for x in res["groups"]]
259 crowd_user["groups"] = [x["name"] for x in res["groups"]]
260
260
261 # old attrs fetched from RhodeCode database
261 # old attrs fetched from RhodeCode database
262 admin = getattr(userobj, 'admin', False)
262 admin = getattr(userobj, 'admin', False)
263 active = getattr(userobj, 'active', True)
263 active = getattr(userobj, 'active', True)
264 email = getattr(userobj, 'email', '')
264 email = getattr(userobj, 'email', '')
265 username = getattr(userobj, 'username', username)
265 username = getattr(userobj, 'username', username)
266 firstname = getattr(userobj, 'firstname', '')
266 firstname = getattr(userobj, 'firstname', '')
267 lastname = getattr(userobj, 'lastname', '')
267 lastname = getattr(userobj, 'lastname', '')
268 extern_type = getattr(userobj, 'extern_type', '')
268 extern_type = getattr(userobj, 'extern_type', '')
269
269
270 user_attrs = {
270 user_attrs = {
271 'username': username,
271 'username': username,
272 'firstname': crowd_user["first-name"] or firstname,
272 'firstname': crowd_user["first-name"] or firstname,
273 'lastname': crowd_user["last-name"] or lastname,
273 'lastname': crowd_user["last-name"] or lastname,
274 'groups': crowd_user["groups"],
274 'groups': crowd_user["groups"],
275 'user_group_sync': True,
275 'user_group_sync': True,
276 'email': crowd_user["email"] or email,
276 'email': crowd_user["email"] or email,
277 'admin': admin,
277 'admin': admin,
278 'active': active,
278 'active': active,
279 'active_from_extern': crowd_user.get('active'),
279 'active_from_extern': crowd_user.get('active'),
280 'extern_name': crowd_user["name"],
280 'extern_name': crowd_user["name"],
281 'extern_type': extern_type,
281 'extern_type': extern_type,
282 }
282 }
283
283
284 # set an admin if we're in admin_groups of crowd
284 # set an admin if we're in admin_groups of crowd
285 for group in settings["admin_groups"]:
285 for group in settings["admin_groups"]:
286 if group in user_attrs["groups"]:
286 if group in user_attrs["groups"]:
287 user_attrs["admin"] = True
287 user_attrs["admin"] = True
288 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
288 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
289 log.info('user `%s` authenticated correctly', user_attrs['username'])
289 log.info('user `%s` authenticated correctly', user_attrs['username'])
290 return user_attrs
290 return user_attrs
291
291
292
292
293 def includeme(config):
293 def includeme(config):
294 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
294 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
295 plugin_factory(plugin_id).includeme(config)
295 plugin_factory(plugin_id).includeme(config)
@@ -1,232 +1,232 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 logging
22 import logging
23
23
24 from rhodecode.translation import _
24 from rhodecode.translation import _
25 from rhodecode.authentication.base import (
25 from rhodecode.authentication.base import (
26 RhodeCodeExternalAuthPlugin, hybrid_property)
26 RhodeCodeExternalAuthPlugin, hybrid_property)
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 from rhodecode.lib.colander_utils import strip_whitespace
29 from rhodecode.lib.colander_utils import strip_whitespace
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwargs):
37 def plugin_factory(plugin_id, *args, **kwargs):
38 """
38 """
39 Factory function that is called during plugin discovery.
39 Factory function that is called during plugin discovery.
40 It returns the plugin instance.
40 It returns the plugin instance.
41 """
41 """
42 plugin = RhodeCodeAuthPlugin(plugin_id)
42 plugin = RhodeCodeAuthPlugin(plugin_id)
43 return plugin
43 return plugin
44
44
45
45
46 class HeadersAuthnResource(AuthnPluginResourceBase):
46 class HeadersAuthnResource(AuthnPluginResourceBase):
47 pass
47 pass
48
48
49
49
50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 header = colander.SchemaNode(
51 header = colander.SchemaNode(
52 colander.String(),
52 colander.String(),
53 default='REMOTE_USER',
53 default='REMOTE_USER',
54 description=_('Header to extract the user from'),
54 description=_('Header to extract the user from'),
55 preparer=strip_whitespace,
55 preparer=strip_whitespace,
56 title=_('Header'),
56 title=_('Header'),
57 widget='string')
57 widget='string')
58 fallback_header = colander.SchemaNode(
58 fallback_header = colander.SchemaNode(
59 colander.String(),
59 colander.String(),
60 default='HTTP_X_FORWARDED_USER',
60 default='HTTP_X_FORWARDED_USER',
61 description=_('Header to extract the user from when main one fails'),
61 description=_('Header to extract the user from when main one fails'),
62 preparer=strip_whitespace,
62 preparer=strip_whitespace,
63 title=_('Fallback header'),
63 title=_('Fallback header'),
64 widget='string')
64 widget='string')
65 clean_username = colander.SchemaNode(
65 clean_username = colander.SchemaNode(
66 colander.Boolean(),
66 colander.Boolean(),
67 default=True,
67 default=True,
68 description=_('Perform cleaning of user, if passed user has @ in '
68 description=_('Perform cleaning of user, if passed user has @ in '
69 'username then first part before @ is taken. '
69 'username then first part before @ is taken. '
70 'If there\'s \\ in the username only the part after '
70 'If there\'s \\ in the username only the part after '
71 ' \\ is taken'),
71 ' \\ is taken'),
72 missing=False,
72 missing=False,
73 title=_('Clean username'),
73 title=_('Clean username'),
74 widget='bool')
74 widget='bool')
75
75
76
76
77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78 uid = 'headers'
78 uid = 'headers'
79
79
80 def includeme(self, config):
80 def includeme(self, config):
81 config.add_authn_plugin(self)
81 config.add_authn_plugin(self)
82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
83 config.add_view(
83 config.add_view(
84 'rhodecode.authentication.views.AuthnPluginViewBase',
84 'rhodecode.authentication.views.AuthnPluginViewBase',
85 attr='settings_get',
85 attr='settings_get',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
87 request_method='GET',
87 request_method='GET',
88 route_name='auth_home',
88 route_name='auth_home',
89 context=HeadersAuthnResource)
89 context=HeadersAuthnResource)
90 config.add_view(
90 config.add_view(
91 'rhodecode.authentication.views.AuthnPluginViewBase',
91 'rhodecode.authentication.views.AuthnPluginViewBase',
92 attr='settings_post',
92 attr='settings_post',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
94 request_method='POST',
94 request_method='POST',
95 route_name='auth_home',
95 route_name='auth_home',
96 context=HeadersAuthnResource)
96 context=HeadersAuthnResource)
97
97
98 def get_display_name(self, load_from_settings=False):
98 def get_display_name(self, load_from_settings=False):
99 return _('Headers')
99 return _('Headers')
100
100
101 def get_settings_schema(self):
101 def get_settings_schema(self):
102 return HeadersSettingsSchema()
102 return HeadersSettingsSchema()
103
103
104 @hybrid_property
104 @hybrid_property
105 def name(self):
105 def name(self):
106 return u"headers"
106 return u"headers"
107
107
108 @property
108 @property
109 def is_headers_auth(self):
109 def is_headers_auth(self):
110 return True
110 return True
111
111
112 def use_fake_password(self):
112 def use_fake_password(self):
113 return True
113 return True
114
114
115 def user_activation_state(self):
115 def user_activation_state(self):
116 def_user_perms = User.get_default_user().AuthUser().permissions['global']
116 def_user_perms = User.get_default_user().AuthUser().permissions['global']
117 return 'hg.extern_activate.auto' in def_user_perms
117 return 'hg.extern_activate.auto' in def_user_perms
118
118
119 def _clean_username(self, username):
119 def _clean_username(self, username):
120 # Removing realm and domain from username
120 # Removing realm and domain from username
121 username = username.split('@')[0]
121 username = username.split('@')[0]
122 username = username.rsplit('\\')[-1]
122 username = username.rsplit('\\')[-1]
123 return username
123 return username
124
124
125 def _get_username(self, environ, settings):
125 def _get_username(self, environ, settings):
126 username = None
126 username = None
127 environ = environ or {}
127 environ = environ or {}
128 if not environ:
128 if not environ:
129 log.debug('got empty environ: %s', environ)
129 log.debug('got empty environ: %s', environ)
130
130
131 settings = settings or {}
131 settings = settings or {}
132 if settings.get('header'):
132 if settings.get('header'):
133 header = settings.get('header')
133 header = settings.get('header')
134 username = environ.get(header)
134 username = environ.get(header)
135 log.debug('extracted %s:%s', header, username)
135 log.debug('extracted %s:%s', header, username)
136
136
137 # fallback mode
137 # fallback mode
138 if not username and settings.get('fallback_header'):
138 if not username and settings.get('fallback_header'):
139 header = settings.get('fallback_header')
139 header = settings.get('fallback_header')
140 username = environ.get(header)
140 username = environ.get(header)
141 log.debug('extracted %s:%s', header, username)
141 log.debug('extracted %s:%s', header, username)
142
142
143 if username and str2bool(settings.get('clean_username')):
143 if username and str2bool(settings.get('clean_username')):
144 log.debug('Received username `%s` from headers', username)
144 log.debug('Received username `%s` from headers', username)
145 username = self._clean_username(username)
145 username = self._clean_username(username)
146 log.debug('New cleanup user is:%s', username)
146 log.debug('New cleanup user is:%s', username)
147 return username
147 return username
148
148
149 def get_user(self, username=None, **kwargs):
149 def get_user(self, username=None, **kwargs):
150 """
150 """
151 Helper method for user fetching in plugins, by default it's using
151 Helper method for user fetching in plugins, by default it's using
152 simple fetch by username, but this method can be custimized in plugins
152 simple fetch by username, but this method can be custimized in plugins
153 eg. headers auth plugin to fetch user by environ params
153 eg. headers auth plugin to fetch user by environ params
154 :param username: username if given to fetch
154 :param username: username if given to fetch
155 :param kwargs: extra arguments needed for user fetching.
155 :param kwargs: extra arguments needed for user fetching.
156 """
156 """
157 environ = kwargs.get('environ') or {}
157 environ = kwargs.get('environ') or {}
158 settings = kwargs.get('settings') or {}
158 settings = kwargs.get('settings') or {}
159 username = self._get_username(environ, settings)
159 username = self._get_username(environ, settings)
160 # we got the username, so use default method now
160 # we got the username, so use default method now
161 return super(RhodeCodeAuthPlugin, self).get_user(username)
161 return super(RhodeCodeAuthPlugin, self).get_user(username)
162
162
163 def auth(self, userobj, username, password, settings, **kwargs):
163 def auth(self, userobj, username, password, settings, **kwargs):
164 """
164 """
165 Get's the headers_auth username (or email). It tries to get username
165 Get's the headers_auth username (or email). It tries to get username
166 from REMOTE_USER if this plugin is enabled, if that fails
166 from REMOTE_USER if this plugin is enabled, if that fails
167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
168 is set. clean_username extracts the username from this data if it's
168 is set. clean_username extracts the username from this data if it's
169 having @ in it.
169 having @ in it.
170 Return None on failure. On success, return a dictionary of the form:
170 Return None on failure. On success, return a dictionary of the form:
171
171
172 see: RhodeCodeAuthPluginBase.auth_func_attrs
172 see: RhodeCodeAuthPluginBase.auth_func_attrs
173
173
174 :param userobj:
174 :param userobj:
175 :param username:
175 :param username:
176 :param password:
176 :param password:
177 :param settings:
177 :param settings:
178 :param kwargs:
178 :param kwargs:
179 """
179 """
180 environ = kwargs.get('environ')
180 environ = kwargs.get('environ')
181 if not environ:
181 if not environ:
182 log.debug('Empty environ data skipping...')
182 log.debug('Empty environ data skipping...')
183 return None
183 return None
184
184
185 if not userobj:
185 if not userobj:
186 userobj = self.get_user('', environ=environ, settings=settings)
186 userobj = self.get_user('', environ=environ, settings=settings)
187
187
188 # we don't care passed username/password for headers auth plugins.
188 # we don't care passed username/password for headers auth plugins.
189 # only way to log in is using environ
189 # only way to log in is using environ
190 username = None
190 username = None
191 if userobj:
191 if userobj:
192 username = getattr(userobj, 'username')
192 username = getattr(userobj, 'username')
193
193
194 if not username:
194 if not username:
195 # we don't have any objects in DB user doesn't exist extract
195 # we don't have any objects in DB user doesn't exist extract
196 # username from environ based on the settings
196 # username from environ based on the settings
197 username = self._get_username(environ, settings)
197 username = self._get_username(environ, settings)
198
198
199 # if cannot fetch username, it's a no-go for this plugin to proceed
199 # if cannot fetch username, it's a no-go for this plugin to proceed
200 if not username:
200 if not username:
201 return None
201 return None
202
202
203 # old attrs fetched from RhodeCode database
203 # old attrs fetched from RhodeCode database
204 admin = getattr(userobj, 'admin', False)
204 admin = getattr(userobj, 'admin', False)
205 active = getattr(userobj, 'active', True)
205 active = getattr(userobj, 'active', True)
206 email = getattr(userobj, 'email', '')
206 email = getattr(userobj, 'email', '')
207 firstname = getattr(userobj, 'firstname', '')
207 firstname = getattr(userobj, 'firstname', '')
208 lastname = getattr(userobj, 'lastname', '')
208 lastname = getattr(userobj, 'lastname', '')
209 extern_type = getattr(userobj, 'extern_type', '')
209 extern_type = getattr(userobj, 'extern_type', '')
210
210
211 user_attrs = {
211 user_attrs = {
212 'username': username,
212 'username': username,
213 'firstname': safe_unicode(firstname or username),
213 'firstname': safe_unicode(firstname or username),
214 'lastname': safe_unicode(lastname or ''),
214 'lastname': safe_unicode(lastname or ''),
215 'groups': [],
215 'groups': [],
216 'user_group_sync': False,
216 'user_group_sync': False,
217 'email': email or '',
217 'email': email or '',
218 'admin': admin or False,
218 'admin': admin or False,
219 'active': active,
219 'active': active,
220 'active_from_extern': True,
220 'active_from_extern': True,
221 'extern_name': username,
221 'extern_name': username,
222 'extern_type': extern_type,
222 'extern_type': extern_type,
223 }
223 }
224
224
225 log.info('user `%s` authenticated correctly', user_attrs['username'],
225 log.info('user `%s` authenticated correctly', user_attrs['username'],
226 extra={"action": "user_auth_ok", "auth_module": "auth_headers", "username": user_attrs["username"]})
226 extra={"action": "user_auth_ok", "auth_module": "auth_headers", "username": user_attrs["username"]})
227 return user_attrs
227 return user_attrs
228
228
229
229
230 def includeme(config):
230 def includeme(config):
231 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
231 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
232 plugin_factory(plugin_id).includeme(config)
232 plugin_factory(plugin_id).includeme(config)
@@ -1,173 +1,173 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 RhodeCode authentication plugin for Jasig CAS
22 RhodeCode authentication plugin for Jasig CAS
23 http://www.jasig.org/cas
23 http://www.jasig.org/cas
24 """
24 """
25
25
26
26
27 import colander
27 import colander
28 import logging
28 import logging
29 import rhodecode
29 import rhodecode
30 import urllib.request, urllib.parse, urllib.error
30 import urllib.request, urllib.parse, urllib.error
31 import urllib.request, urllib.error, urllib.parse
31 import urllib.request, urllib.error, urllib.parse
32
32
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def plugin_factory(plugin_id, *args, **kwargs):
45 def plugin_factory(plugin_id, *args, **kwargs):
46 """
46 """
47 Factory function that is called during plugin discovery.
47 Factory function that is called during plugin discovery.
48 It returns the plugin instance.
48 It returns the plugin instance.
49 """
49 """
50 plugin = RhodeCodeAuthPlugin(plugin_id)
50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 return plugin
51 return plugin
52
52
53
53
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 pass
55 pass
56
56
57
57
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 service_url = colander.SchemaNode(
59 service_url = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 default='https://domain.com/cas/v1/tickets',
61 default='https://domain.com/cas/v1/tickets',
62 description=_('The url of the Jasig CAS REST service'),
62 description=_('The url of the Jasig CAS REST service'),
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 title=_('URL'),
64 title=_('URL'),
65 widget='string')
65 widget='string')
66
66
67
67
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69 uid = 'jasig_cas'
69 uid = 'jasig_cas'
70
70
71 def includeme(self, config):
71 def includeme(self, config):
72 config.add_authn_plugin(self)
72 config.add_authn_plugin(self)
73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
74 config.add_view(
74 config.add_view(
75 'rhodecode.authentication.views.AuthnPluginViewBase',
75 'rhodecode.authentication.views.AuthnPluginViewBase',
76 attr='settings_get',
76 attr='settings_get',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
78 request_method='GET',
78 request_method='GET',
79 route_name='auth_home',
79 route_name='auth_home',
80 context=JasigCasAuthnResource)
80 context=JasigCasAuthnResource)
81 config.add_view(
81 config.add_view(
82 'rhodecode.authentication.views.AuthnPluginViewBase',
82 'rhodecode.authentication.views.AuthnPluginViewBase',
83 attr='settings_post',
83 attr='settings_post',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 request_method='POST',
85 request_method='POST',
86 route_name='auth_home',
86 route_name='auth_home',
87 context=JasigCasAuthnResource)
87 context=JasigCasAuthnResource)
88
88
89 def get_settings_schema(self):
89 def get_settings_schema(self):
90 return JasigCasSettingsSchema()
90 return JasigCasSettingsSchema()
91
91
92 def get_display_name(self, load_from_settings=False):
92 def get_display_name(self, load_from_settings=False):
93 return _('Jasig-CAS')
93 return _('Jasig-CAS')
94
94
95 @hybrid_property
95 @hybrid_property
96 def name(self):
96 def name(self):
97 return u"jasig-cas"
97 return u"jasig-cas"
98
98
99 @property
99 @property
100 def is_headers_auth(self):
100 def is_headers_auth(self):
101 return True
101 return True
102
102
103 def use_fake_password(self):
103 def use_fake_password(self):
104 return True
104 return True
105
105
106 def user_activation_state(self):
106 def user_activation_state(self):
107 def_user_perms = User.get_default_user().AuthUser().permissions['global']
107 def_user_perms = User.get_default_user().AuthUser().permissions['global']
108 return 'hg.extern_activate.auto' in def_user_perms
108 return 'hg.extern_activate.auto' in def_user_perms
109
109
110 def auth(self, userobj, username, password, settings, **kwargs):
110 def auth(self, userobj, username, password, settings, **kwargs):
111 """
111 """
112 Given a user object (which may be null), username, a plaintext password,
112 Given a user object (which may be null), username, a plaintext password,
113 and a settings object (containing all the keys needed as listed in settings()),
113 and a settings object (containing all the keys needed as listed in settings()),
114 authenticate this user's login attempt.
114 authenticate this user's login attempt.
115
115
116 Return None on failure. On success, return a dictionary of the form:
116 Return None on failure. On success, return a dictionary of the form:
117
117
118 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 see: RhodeCodeAuthPluginBase.auth_func_attrs
119 This is later validated for correctness
119 This is later validated for correctness
120 """
120 """
121 if not username or not password:
121 if not username or not password:
122 log.debug('Empty username or password skipping...')
122 log.debug('Empty username or password skipping...')
123 return None
123 return None
124
124
125 log.debug("Jasig CAS settings: %s", settings)
125 log.debug("Jasig CAS settings: %s", settings)
126 params = urllib.parse.urlencode({'username': username, 'password': password})
126 params = urllib.parse.urlencode({'username': username, 'password': password})
127 headers = {"Content-type": "application/x-www-form-urlencoded",
127 headers = {"Content-type": "application/x-www-form-urlencoded",
128 "Accept": "text/plain",
128 "Accept": "text/plain",
129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
130 url = settings["service_url"]
130 url = settings["service_url"]
131
131
132 log.debug("Sent Jasig CAS: \n%s",
132 log.debug("Sent Jasig CAS: \n%s",
133 {"url": url, "body": params, "headers": headers})
133 {"url": url, "body": params, "headers": headers})
134 request = urllib.request.Request(url, params, headers)
134 request = urllib.request.Request(url, params, headers)
135 try:
135 try:
136 response = urllib.request.urlopen(request)
136 response = urllib.request.urlopen(request)
137 except urllib.error.HTTPError as e:
137 except urllib.error.HTTPError as e:
138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)", e.code)
138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)", e.code)
139 return None
139 return None
140 except urllib.error.URLError as e:
140 except urllib.error.URLError as e:
141 log.debug("URLError when requesting Jasig CAS url: %s ", url)
141 log.debug("URLError when requesting Jasig CAS url: %s ", url)
142 return None
142 return None
143
143
144 # old attrs fetched from RhodeCode database
144 # old attrs fetched from RhodeCode database
145 admin = getattr(userobj, 'admin', False)
145 admin = getattr(userobj, 'admin', False)
146 active = getattr(userobj, 'active', True)
146 active = getattr(userobj, 'active', True)
147 email = getattr(userobj, 'email', '')
147 email = getattr(userobj, 'email', '')
148 username = getattr(userobj, 'username', username)
148 username = getattr(userobj, 'username', username)
149 firstname = getattr(userobj, 'firstname', '')
149 firstname = getattr(userobj, 'firstname', '')
150 lastname = getattr(userobj, 'lastname', '')
150 lastname = getattr(userobj, 'lastname', '')
151 extern_type = getattr(userobj, 'extern_type', '')
151 extern_type = getattr(userobj, 'extern_type', '')
152
152
153 user_attrs = {
153 user_attrs = {
154 'username': username,
154 'username': username,
155 'firstname': safe_unicode(firstname or username),
155 'firstname': safe_unicode(firstname or username),
156 'lastname': safe_unicode(lastname or ''),
156 'lastname': safe_unicode(lastname or ''),
157 'groups': [],
157 'groups': [],
158 'user_group_sync': False,
158 'user_group_sync': False,
159 'email': email or '',
159 'email': email or '',
160 'admin': admin or False,
160 'admin': admin or False,
161 'active': active,
161 'active': active,
162 'active_from_extern': True,
162 'active_from_extern': True,
163 'extern_name': username,
163 'extern_name': username,
164 'extern_type': extern_type,
164 'extern_type': extern_type,
165 }
165 }
166
166
167 log.info('user `%s` authenticated correctly', user_attrs['username'])
167 log.info('user `%s` authenticated correctly', user_attrs['username'])
168 return user_attrs
168 return user_attrs
169
169
170
170
171 def includeme(config):
171 def includeme(config):
172 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
172 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
173 plugin_factory(plugin_id).includeme(config)
173 plugin_factory(plugin_id).includeme(config)
@@ -1,552 +1,551 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 """
20 """
22 RhodeCode authentication plugin for LDAP
21 RhodeCode authentication plugin for LDAP
23 """
22 """
24
23
25 import logging
24 import logging
26 import traceback
25 import traceback
27
26
28 import colander
27 import colander
29 from rhodecode.translation import _
28 from rhodecode.translation import _
30 from rhodecode.authentication.base import (
29 from rhodecode.authentication.base import (
31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
30 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
31 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 from rhodecode.lib.colander_utils import strip_whitespace
33 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.exceptions import (
34 from rhodecode.lib.exceptions import (
36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
35 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 )
36 )
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
37 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 from rhodecode.model.db import User
38 from rhodecode.model.db import User
40 from rhodecode.model.validators import Missing
39 from rhodecode.model.validators import Missing
41
40
42 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
43
42
44 try:
43 try:
45 import ldap
44 import ldap
46 except ImportError:
45 except ImportError:
47 # means that python-ldap is not installed, we use Missing object to mark
46 # means that python-ldap is not installed, we use Missing object to mark
48 # ldap lib is Missing
47 # ldap lib is Missing
49 ldap = Missing
48 ldap = Missing
50
49
51
50
52 class LdapError(Exception):
51 class LdapError(Exception):
53 pass
52 pass
54
53
55
54
56 def plugin_factory(plugin_id, *args, **kwargs):
55 def plugin_factory(plugin_id, *args, **kwargs):
57 """
56 """
58 Factory function that is called during plugin discovery.
57 Factory function that is called during plugin discovery.
59 It returns the plugin instance.
58 It returns the plugin instance.
60 """
59 """
61 plugin = RhodeCodeAuthPlugin(plugin_id)
60 plugin = RhodeCodeAuthPlugin(plugin_id)
62 return plugin
61 return plugin
63
62
64
63
65 class LdapAuthnResource(AuthnPluginResourceBase):
64 class LdapAuthnResource(AuthnPluginResourceBase):
66 pass
65 pass
67
66
68
67
69 class AuthLdap(AuthLdapBase):
68 class AuthLdap(AuthLdapBase):
70 default_tls_cert_dir = '/etc/openldap/cacerts'
69 default_tls_cert_dir = '/etc/openldap/cacerts'
71
70
72 scope_labels = {
71 scope_labels = {
73 ldap.SCOPE_BASE: 'SCOPE_BASE',
72 ldap.SCOPE_BASE: 'SCOPE_BASE',
74 ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
73 ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
75 ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
74 ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
76 }
75 }
77
76
78 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
77 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
79 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
78 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
80 tls_cert_dir=None, ldap_version=3,
79 tls_cert_dir=None, ldap_version=3,
81 search_scope='SUBTREE', attr_login='uid',
80 search_scope='SUBTREE', attr_login='uid',
82 ldap_filter='', timeout=None):
81 ldap_filter='', timeout=None):
83 if ldap == Missing:
82 if ldap == Missing:
84 raise LdapImportError("Missing or incompatible ldap library")
83 raise LdapImportError("Missing or incompatible ldap library")
85
84
86 self.debug = False
85 self.debug = False
87 self.timeout = timeout or 60 * 5
86 self.timeout = timeout or 60 * 5
88 self.ldap_version = ldap_version
87 self.ldap_version = ldap_version
89 self.ldap_server_type = 'ldap'
88 self.ldap_server_type = 'ldap'
90
89
91 self.TLS_KIND = tls_kind
90 self.TLS_KIND = tls_kind
92
91
93 if self.TLS_KIND == 'LDAPS':
92 if self.TLS_KIND == 'LDAPS':
94 port = port or 636
93 port = port or 636
95 self.ldap_server_type += 's'
94 self.ldap_server_type += 's'
96
95
97 OPT_X_TLS_DEMAND = 2
96 OPT_X_TLS_DEMAND = 2
98 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
97 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
99 self.TLS_CERT_FILE = tls_cert_file or ''
98 self.TLS_CERT_FILE = tls_cert_file or ''
100 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
99 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
101
100
102 # split server into list
101 # split server into list
103 self.SERVER_ADDRESSES = self._get_server_list(server)
102 self.SERVER_ADDRESSES = self._get_server_list(server)
104 self.LDAP_SERVER_PORT = port
103 self.LDAP_SERVER_PORT = port
105
104
106 # USE FOR READ ONLY BIND TO LDAP SERVER
105 # USE FOR READ ONLY BIND TO LDAP SERVER
107 self.attr_login = attr_login
106 self.attr_login = attr_login
108
107
109 self.LDAP_BIND_DN = safe_str(bind_dn)
108 self.LDAP_BIND_DN = safe_str(bind_dn)
110 self.LDAP_BIND_PASS = safe_str(bind_pass)
109 self.LDAP_BIND_PASS = safe_str(bind_pass)
111
110
112 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
111 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
113 self.BASE_DN = safe_str(base_dn)
112 self.BASE_DN = safe_str(base_dn)
114 self.LDAP_FILTER = safe_str(ldap_filter)
113 self.LDAP_FILTER = safe_str(ldap_filter)
115
114
116 def _get_ldap_conn(self):
115 def _get_ldap_conn(self):
117
116
118 if self.debug:
117 if self.debug:
119 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
118 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
120
119
121 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
120 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
122 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
121 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
123
122
124 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
123 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
125 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
124 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
126
125
127 if self.TLS_KIND != 'PLAIN':
126 if self.TLS_KIND != 'PLAIN':
128 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
127 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
129
128
130 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
129 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
131 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
130 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
132
131
133 # init connection now
132 # init connection now
134 ldap_servers = self._build_servers(
133 ldap_servers = self._build_servers(
135 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
134 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
136 log.debug('initializing LDAP connection to:%s', ldap_servers)
135 log.debug('initializing LDAP connection to:%s', ldap_servers)
137 ldap_conn = ldap.initialize(ldap_servers)
136 ldap_conn = ldap.initialize(ldap_servers)
138 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
137 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
139 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
138 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
140 ldap_conn.timeout = self.timeout
139 ldap_conn.timeout = self.timeout
141
140
142 if self.ldap_version == 2:
141 if self.ldap_version == 2:
143 ldap_conn.protocol = ldap.VERSION2
142 ldap_conn.protocol = ldap.VERSION2
144 else:
143 else:
145 ldap_conn.protocol = ldap.VERSION3
144 ldap_conn.protocol = ldap.VERSION3
146
145
147 if self.TLS_KIND == 'START_TLS':
146 if self.TLS_KIND == 'START_TLS':
148 ldap_conn.start_tls_s()
147 ldap_conn.start_tls_s()
149
148
150 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
149 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
151 log.debug('Trying simple_bind with password and given login DN: %r',
150 log.debug('Trying simple_bind with password and given login DN: %r',
152 self.LDAP_BIND_DN)
151 self.LDAP_BIND_DN)
153 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
152 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
154 log.debug('simple_bind successful')
153 log.debug('simple_bind successful')
155 return ldap_conn
154 return ldap_conn
156
155
157 def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
156 def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
158 scope = ldap.SCOPE_BASE
157 scope = ldap.SCOPE_BASE
159 scope_label = self.scope_labels.get(scope)
158 scope_label = self.scope_labels.get(scope)
160 ldap_filter = '(objectClass=*)'
159 ldap_filter = '(objectClass=*)'
161
160
162 try:
161 try:
163 log.debug('Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)',
162 log.debug('Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)',
164 dn, scope_label, ldap_filter)
163 dn, scope_label, ldap_filter)
165 ldap_conn.simple_bind_s(dn, safe_str(password))
164 ldap_conn.simple_bind_s(dn, safe_str(password))
166 response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=['*', '+'])
165 response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=['*', '+'])
167
166
168 if not response:
167 if not response:
169 log.error('search bind returned empty results: %r', response)
168 log.error('search bind returned empty results: %r', response)
170 return {}
169 return {}
171 else:
170 else:
172 _dn, attrs = response[0]
171 _dn, attrs = response[0]
173 return attrs
172 return attrs
174
173
175 except ldap.INVALID_CREDENTIALS:
174 except ldap.INVALID_CREDENTIALS:
176 log.debug("LDAP rejected password for user '%s': %s, org_exc:",
175 log.debug("LDAP rejected password for user '%s': %s, org_exc:",
177 username, dn, exc_info=True)
176 username, dn, exc_info=True)
178
177
179 def authenticate_ldap(self, username, password):
178 def authenticate_ldap(self, username, password):
180 """
179 """
181 Authenticate a user via LDAP and return his/her LDAP properties.
180 Authenticate a user via LDAP and return his/her LDAP properties.
182
181
183 Raises AuthenticationError if the credentials are rejected, or
182 Raises AuthenticationError if the credentials are rejected, or
184 EnvironmentError if the LDAP server can't be reached.
183 EnvironmentError if the LDAP server can't be reached.
185
184
186 :param username: username
185 :param username: username
187 :param password: password
186 :param password: password
188 """
187 """
189
188
190 uid = self.get_uid(username, self.SERVER_ADDRESSES)
189 uid = self.get_uid(username, self.SERVER_ADDRESSES)
191 user_attrs = {}
190 user_attrs = {}
192 dn = ''
191 dn = ''
193
192
194 self.validate_password(username, password)
193 self.validate_password(username, password)
195 self.validate_username(username)
194 self.validate_username(username)
196 scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
195 scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
197
196
198 ldap_conn = None
197 ldap_conn = None
199 try:
198 try:
200 ldap_conn = self._get_ldap_conn()
199 ldap_conn = self._get_ldap_conn()
201 filter_ = '(&%s(%s=%s))' % (
200 filter_ = '(&%s(%s=%s))' % (
202 self.LDAP_FILTER, self.attr_login, username)
201 self.LDAP_FILTER, self.attr_login, username)
203 log.debug("Authenticating %r filter %s and scope: %s",
202 log.debug("Authenticating %r filter %s and scope: %s",
204 self.BASE_DN, filter_, scope_label)
203 self.BASE_DN, filter_, scope_label)
205
204
206 ldap_objects = ldap_conn.search_ext_s(
205 ldap_objects = ldap_conn.search_ext_s(
207 self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=['*', '+'])
206 self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=['*', '+'])
208
207
209 if not ldap_objects:
208 if not ldap_objects:
210 log.debug("No matching LDAP objects for authentication "
209 log.debug("No matching LDAP objects for authentication "
211 "of UID:'%s' username:(%s)", uid, username)
210 "of UID:'%s' username:(%s)", uid, username)
212 raise ldap.NO_SUCH_OBJECT()
211 raise ldap.NO_SUCH_OBJECT()
213
212
214 log.debug('Found %s matching ldap object[s], trying to authenticate on each one now...', len(ldap_objects))
213 log.debug('Found %s matching ldap object[s], trying to authenticate on each one now...', len(ldap_objects))
215 for (dn, _attrs) in ldap_objects:
214 for (dn, _attrs) in ldap_objects:
216 if dn is None:
215 if dn is None:
217 continue
216 continue
218
217
219 user_attrs = self.fetch_attrs_from_simple_bind(
218 user_attrs = self.fetch_attrs_from_simple_bind(
220 ldap_conn, dn, username, password)
219 ldap_conn, dn, username, password)
221
220
222 if user_attrs:
221 if user_attrs:
223 log.debug('Got authenticated user attributes from DN:%s', dn)
222 log.debug('Got authenticated user attributes from DN:%s', dn)
224 break
223 break
225 else:
224 else:
226 raise LdapPasswordError(
225 raise LdapPasswordError(
227 'Failed to authenticate user `{}` with given password'.format(username))
226 'Failed to authenticate user `{}` with given password'.format(username))
228
227
229 except ldap.NO_SUCH_OBJECT:
228 except ldap.NO_SUCH_OBJECT:
230 log.debug("LDAP says no such user '%s' (%s), org_exc:",
229 log.debug("LDAP says no such user '%s' (%s), org_exc:",
231 uid, username, exc_info=True)
230 uid, username, exc_info=True)
232 raise LdapUsernameError('Unable to find user')
231 raise LdapUsernameError('Unable to find user')
233 except ldap.SERVER_DOWN:
232 except ldap.SERVER_DOWN:
234 org_exc = traceback.format_exc()
233 org_exc = traceback.format_exc()
235 raise LdapConnectionError(
234 raise LdapConnectionError(
236 "LDAP can't access authentication server, org_exc:%s" % org_exc)
235 "LDAP can't access authentication server, org_exc:%s" % org_exc)
237 finally:
236 finally:
238 if ldap_conn:
237 if ldap_conn:
239 log.debug('ldap: connection release')
238 log.debug('ldap: connection release')
240 try:
239 try:
241 ldap_conn.unbind_s()
240 ldap_conn.unbind_s()
242 except Exception:
241 except Exception:
243 # for any reason this can raise exception we must catch it
242 # for any reason this can raise exception we must catch it
244 # to not crush the server
243 # to not crush the server
245 pass
244 pass
246
245
247 return dn, user_attrs
246 return dn, user_attrs
248
247
249
248
250 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
249 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
251 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
250 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
252 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
251 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
253 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
252 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
254
253
255 host = colander.SchemaNode(
254 host = colander.SchemaNode(
256 colander.String(),
255 colander.String(),
257 default='',
256 default='',
258 description=_('Host[s] of the LDAP Server \n'
257 description=_('Host[s] of the LDAP Server \n'
259 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
258 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
260 'Multiple servers can be specified using commas'),
259 'Multiple servers can be specified using commas'),
261 preparer=strip_whitespace,
260 preparer=strip_whitespace,
262 title=_('LDAP Host'),
261 title=_('LDAP Host'),
263 widget='string')
262 widget='string')
264 port = colander.SchemaNode(
263 port = colander.SchemaNode(
265 colander.Int(),
264 colander.Int(),
266 default=389,
265 default=389,
267 description=_('Custom port that the LDAP server is listening on. '
266 description=_('Custom port that the LDAP server is listening on. '
268 'Default value is: 389, use 636 for LDAPS (SSL)'),
267 'Default value is: 389, use 636 for LDAPS (SSL)'),
269 preparer=strip_whitespace,
268 preparer=strip_whitespace,
270 title=_('Port'),
269 title=_('Port'),
271 validator=colander.Range(min=0, max=65536),
270 validator=colander.Range(min=0, max=65536),
272 widget='int')
271 widget='int')
273
272
274 timeout = colander.SchemaNode(
273 timeout = colander.SchemaNode(
275 colander.Int(),
274 colander.Int(),
276 default=60 * 5,
275 default=60 * 5,
277 description=_('Timeout for LDAP connection'),
276 description=_('Timeout for LDAP connection'),
278 preparer=strip_whitespace,
277 preparer=strip_whitespace,
279 title=_('Connection timeout'),
278 title=_('Connection timeout'),
280 validator=colander.Range(min=1),
279 validator=colander.Range(min=1),
281 widget='int')
280 widget='int')
282
281
283 dn_user = colander.SchemaNode(
282 dn_user = colander.SchemaNode(
284 colander.String(),
283 colander.String(),
285 default='',
284 default='',
286 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
285 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
287 'e.g., cn=admin,dc=mydomain,dc=com, or '
286 'e.g., cn=admin,dc=mydomain,dc=com, or '
288 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
287 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
289 missing='',
288 missing='',
290 preparer=strip_whitespace,
289 preparer=strip_whitespace,
291 title=_('Bind account'),
290 title=_('Bind account'),
292 widget='string')
291 widget='string')
293 dn_pass = colander.SchemaNode(
292 dn_pass = colander.SchemaNode(
294 colander.String(),
293 colander.String(),
295 default='',
294 default='',
296 description=_('Password to authenticate for given user DN.'),
295 description=_('Password to authenticate for given user DN.'),
297 missing='',
296 missing='',
298 preparer=strip_whitespace,
297 preparer=strip_whitespace,
299 title=_('Bind account password'),
298 title=_('Bind account password'),
300 widget='password')
299 widget='password')
301 tls_kind = colander.SchemaNode(
300 tls_kind = colander.SchemaNode(
302 colander.String(),
301 colander.String(),
303 default=tls_kind_choices[0],
302 default=tls_kind_choices[0],
304 description=_('TLS Type'),
303 description=_('TLS Type'),
305 title=_('Connection Security'),
304 title=_('Connection Security'),
306 validator=colander.OneOf(tls_kind_choices),
305 validator=colander.OneOf(tls_kind_choices),
307 widget='select')
306 widget='select')
308 tls_reqcert = colander.SchemaNode(
307 tls_reqcert = colander.SchemaNode(
309 colander.String(),
308 colander.String(),
310 default=tls_reqcert_choices[0],
309 default=tls_reqcert_choices[0],
311 description=_('Require Cert over TLS?. Self-signed and custom '
310 description=_('Require Cert over TLS?. Self-signed and custom '
312 'certificates can be used when\n `RhodeCode Certificate` '
311 'certificates can be used when\n `RhodeCode Certificate` '
313 'found in admin > settings > system info page is extended.'),
312 'found in admin > settings > system info page is extended.'),
314 title=_('Certificate Checks'),
313 title=_('Certificate Checks'),
315 validator=colander.OneOf(tls_reqcert_choices),
314 validator=colander.OneOf(tls_reqcert_choices),
316 widget='select')
315 widget='select')
317 tls_cert_file = colander.SchemaNode(
316 tls_cert_file = colander.SchemaNode(
318 colander.String(),
317 colander.String(),
319 default='',
318 default='',
320 description=_('This specifies the PEM-format file path containing '
319 description=_('This specifies the PEM-format file path containing '
321 'certificates for use in TLS connection.\n'
320 'certificates for use in TLS connection.\n'
322 'If not specified `TLS Cert dir` will be used'),
321 'If not specified `TLS Cert dir` will be used'),
323 title=_('TLS Cert file'),
322 title=_('TLS Cert file'),
324 missing='',
323 missing='',
325 widget='string')
324 widget='string')
326 tls_cert_dir = colander.SchemaNode(
325 tls_cert_dir = colander.SchemaNode(
327 colander.String(),
326 colander.String(),
328 default=AuthLdap.default_tls_cert_dir,
327 default=AuthLdap.default_tls_cert_dir,
329 description=_('This specifies the path of a directory that contains individual '
328 description=_('This specifies the path of a directory that contains individual '
330 'CA certificates in separate files.'),
329 'CA certificates in separate files.'),
331 title=_('TLS Cert dir'),
330 title=_('TLS Cert dir'),
332 widget='string')
331 widget='string')
333 base_dn = colander.SchemaNode(
332 base_dn = colander.SchemaNode(
334 colander.String(),
333 colander.String(),
335 default='',
334 default='',
336 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
335 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
337 'in it to be replaced with current user username \n'
336 'in it to be replaced with current user username \n'
338 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
337 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
339 missing='',
338 missing='',
340 preparer=strip_whitespace,
339 preparer=strip_whitespace,
341 title=_('Base DN'),
340 title=_('Base DN'),
342 widget='string')
341 widget='string')
343 filter = colander.SchemaNode(
342 filter = colander.SchemaNode(
344 colander.String(),
343 colander.String(),
345 default='',
344 default='',
346 description=_('Filter to narrow results \n'
345 description=_('Filter to narrow results \n'
347 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
346 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
348 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
347 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
349 missing='',
348 missing='',
350 preparer=strip_whitespace,
349 preparer=strip_whitespace,
351 title=_('LDAP Search Filter'),
350 title=_('LDAP Search Filter'),
352 widget='string')
351 widget='string')
353
352
354 search_scope = colander.SchemaNode(
353 search_scope = colander.SchemaNode(
355 colander.String(),
354 colander.String(),
356 default=search_scope_choices[2],
355 default=search_scope_choices[2],
357 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
356 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
358 title=_('LDAP Search Scope'),
357 title=_('LDAP Search Scope'),
359 validator=colander.OneOf(search_scope_choices),
358 validator=colander.OneOf(search_scope_choices),
360 widget='select')
359 widget='select')
361 attr_login = colander.SchemaNode(
360 attr_login = colander.SchemaNode(
362 colander.String(),
361 colander.String(),
363 default='uid',
362 default='uid',
364 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
363 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
365 preparer=strip_whitespace,
364 preparer=strip_whitespace,
366 title=_('Login Attribute'),
365 title=_('Login Attribute'),
367 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
366 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
368 widget='string')
367 widget='string')
369 attr_email = colander.SchemaNode(
368 attr_email = colander.SchemaNode(
370 colander.String(),
369 colander.String(),
371 default='',
370 default='',
372 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
371 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
373 'Emails are a crucial part of RhodeCode. \n'
372 'Emails are a crucial part of RhodeCode. \n'
374 'If possible add a valid email attribute to ldap users.'),
373 'If possible add a valid email attribute to ldap users.'),
375 missing='',
374 missing='',
376 preparer=strip_whitespace,
375 preparer=strip_whitespace,
377 title=_('Email Attribute'),
376 title=_('Email Attribute'),
378 widget='string')
377 widget='string')
379 attr_firstname = colander.SchemaNode(
378 attr_firstname = colander.SchemaNode(
380 colander.String(),
379 colander.String(),
381 default='',
380 default='',
382 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
381 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
383 missing='',
382 missing='',
384 preparer=strip_whitespace,
383 preparer=strip_whitespace,
385 title=_('First Name Attribute'),
384 title=_('First Name Attribute'),
386 widget='string')
385 widget='string')
387 attr_lastname = colander.SchemaNode(
386 attr_lastname = colander.SchemaNode(
388 colander.String(),
387 colander.String(),
389 default='',
388 default='',
390 description=_('LDAP Attribute to map to last name (e.g., sn)'),
389 description=_('LDAP Attribute to map to last name (e.g., sn)'),
391 missing='',
390 missing='',
392 preparer=strip_whitespace,
391 preparer=strip_whitespace,
393 title=_('Last Name Attribute'),
392 title=_('Last Name Attribute'),
394 widget='string')
393 widget='string')
395
394
396
395
397 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
396 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
398 uid = 'ldap'
397 uid = 'ldap'
399 # used to define dynamic binding in the
398 # used to define dynamic binding in the
400 DYNAMIC_BIND_VAR = '$login'
399 DYNAMIC_BIND_VAR = '$login'
401 _settings_unsafe_keys = ['dn_pass']
400 _settings_unsafe_keys = ['dn_pass']
402
401
403 def includeme(self, config):
402 def includeme(self, config):
404 config.add_authn_plugin(self)
403 config.add_authn_plugin(self)
405 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
404 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
406 config.add_view(
405 config.add_view(
407 'rhodecode.authentication.views.AuthnPluginViewBase',
406 'rhodecode.authentication.views.AuthnPluginViewBase',
408 attr='settings_get',
407 attr='settings_get',
409 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
408 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
410 request_method='GET',
409 request_method='GET',
411 route_name='auth_home',
410 route_name='auth_home',
412 context=LdapAuthnResource)
411 context=LdapAuthnResource)
413 config.add_view(
412 config.add_view(
414 'rhodecode.authentication.views.AuthnPluginViewBase',
413 'rhodecode.authentication.views.AuthnPluginViewBase',
415 attr='settings_post',
414 attr='settings_post',
416 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
415 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
417 request_method='POST',
416 request_method='POST',
418 route_name='auth_home',
417 route_name='auth_home',
419 context=LdapAuthnResource)
418 context=LdapAuthnResource)
420
419
421 def get_settings_schema(self):
420 def get_settings_schema(self):
422 return LdapSettingsSchema()
421 return LdapSettingsSchema()
423
422
424 def get_display_name(self, load_from_settings=False):
423 def get_display_name(self, load_from_settings=False):
425 return _('LDAP')
424 return _('LDAP')
426
425
427 @classmethod
426 @classmethod
428 def docs(cls):
427 def docs(cls):
429 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
428 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
430
429
431 @hybrid_property
430 @hybrid_property
432 def name(self):
431 def name(self):
433 return u"ldap"
432 return u"ldap"
434
433
435 def use_fake_password(self):
434 def use_fake_password(self):
436 return True
435 return True
437
436
438 def user_activation_state(self):
437 def user_activation_state(self):
439 def_user_perms = User.get_default_user().AuthUser().permissions['global']
438 def_user_perms = User.get_default_user().AuthUser().permissions['global']
440 return 'hg.extern_activate.auto' in def_user_perms
439 return 'hg.extern_activate.auto' in def_user_perms
441
440
442 def try_dynamic_binding(self, username, password, current_args):
441 def try_dynamic_binding(self, username, password, current_args):
443 """
442 """
444 Detects marker inside our original bind, and uses dynamic auth if
443 Detects marker inside our original bind, and uses dynamic auth if
445 present
444 present
446 """
445 """
447
446
448 org_bind = current_args['bind_dn']
447 org_bind = current_args['bind_dn']
449 passwd = current_args['bind_pass']
448 passwd = current_args['bind_pass']
450
449
451 def has_bind_marker(username):
450 def has_bind_marker(username):
452 if self.DYNAMIC_BIND_VAR in username:
451 if self.DYNAMIC_BIND_VAR in username:
453 return True
452 return True
454
453
455 # we only passed in user with "special" variable
454 # we only passed in user with "special" variable
456 if org_bind and has_bind_marker(org_bind) and not passwd:
455 if org_bind and has_bind_marker(org_bind) and not passwd:
457 log.debug('Using dynamic user/password binding for ldap '
456 log.debug('Using dynamic user/password binding for ldap '
458 'authentication. Replacing `%s` with username',
457 'authentication. Replacing `%s` with username',
459 self.DYNAMIC_BIND_VAR)
458 self.DYNAMIC_BIND_VAR)
460 current_args['bind_dn'] = org_bind.replace(
459 current_args['bind_dn'] = org_bind.replace(
461 self.DYNAMIC_BIND_VAR, username)
460 self.DYNAMIC_BIND_VAR, username)
462 current_args['bind_pass'] = password
461 current_args['bind_pass'] = password
463
462
464 return current_args
463 return current_args
465
464
466 def auth(self, userobj, username, password, settings, **kwargs):
465 def auth(self, userobj, username, password, settings, **kwargs):
467 """
466 """
468 Given a user object (which may be null), username, a plaintext password,
467 Given a user object (which may be null), username, a plaintext password,
469 and a settings object (containing all the keys needed as listed in
468 and a settings object (containing all the keys needed as listed in
470 settings()), authenticate this user's login attempt.
469 settings()), authenticate this user's login attempt.
471
470
472 Return None on failure. On success, return a dictionary of the form:
471 Return None on failure. On success, return a dictionary of the form:
473
472
474 see: RhodeCodeAuthPluginBase.auth_func_attrs
473 see: RhodeCodeAuthPluginBase.auth_func_attrs
475 This is later validated for correctness
474 This is later validated for correctness
476 """
475 """
477
476
478 if not username or not password:
477 if not username or not password:
479 log.debug('Empty username or password skipping...')
478 log.debug('Empty username or password skipping...')
480 return None
479 return None
481
480
482 ldap_args = {
481 ldap_args = {
483 'server': settings.get('host', ''),
482 'server': settings.get('host', ''),
484 'base_dn': settings.get('base_dn', ''),
483 'base_dn': settings.get('base_dn', ''),
485 'port': settings.get('port'),
484 'port': settings.get('port'),
486 'bind_dn': settings.get('dn_user'),
485 'bind_dn': settings.get('dn_user'),
487 'bind_pass': settings.get('dn_pass'),
486 'bind_pass': settings.get('dn_pass'),
488 'tls_kind': settings.get('tls_kind'),
487 'tls_kind': settings.get('tls_kind'),
489 'tls_reqcert': settings.get('tls_reqcert'),
488 'tls_reqcert': settings.get('tls_reqcert'),
490 'tls_cert_file': settings.get('tls_cert_file'),
489 'tls_cert_file': settings.get('tls_cert_file'),
491 'tls_cert_dir': settings.get('tls_cert_dir'),
490 'tls_cert_dir': settings.get('tls_cert_dir'),
492 'search_scope': settings.get('search_scope'),
491 'search_scope': settings.get('search_scope'),
493 'attr_login': settings.get('attr_login'),
492 'attr_login': settings.get('attr_login'),
494 'ldap_version': 3,
493 'ldap_version': 3,
495 'ldap_filter': settings.get('filter'),
494 'ldap_filter': settings.get('filter'),
496 'timeout': settings.get('timeout')
495 'timeout': settings.get('timeout')
497 }
496 }
498
497
499 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
498 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
500
499
501 log.debug('Checking for ldap authentication.')
500 log.debug('Checking for ldap authentication.')
502
501
503 try:
502 try:
504 aldap = AuthLdap(**ldap_args)
503 aldap = AuthLdap(**ldap_args)
505 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
504 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
506 log.debug('Got ldap DN response %s', user_dn)
505 log.debug('Got ldap DN response %s', user_dn)
507
506
508 def get_ldap_attr(k):
507 def get_ldap_attr(k):
509 return ldap_attrs.get(settings.get(k), [''])[0]
508 return ldap_attrs.get(settings.get(k), [''])[0]
510
509
511 # old attrs fetched from RhodeCode database
510 # old attrs fetched from RhodeCode database
512 admin = getattr(userobj, 'admin', False)
511 admin = getattr(userobj, 'admin', False)
513 active = getattr(userobj, 'active', True)
512 active = getattr(userobj, 'active', True)
514 email = getattr(userobj, 'email', '')
513 email = getattr(userobj, 'email', '')
515 username = getattr(userobj, 'username', username)
514 username = getattr(userobj, 'username', username)
516 firstname = getattr(userobj, 'firstname', '')
515 firstname = getattr(userobj, 'firstname', '')
517 lastname = getattr(userobj, 'lastname', '')
516 lastname = getattr(userobj, 'lastname', '')
518 extern_type = getattr(userobj, 'extern_type', '')
517 extern_type = getattr(userobj, 'extern_type', '')
519
518
520 groups = []
519 groups = []
521
520
522 user_attrs = {
521 user_attrs = {
523 'username': username,
522 'username': username,
524 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
523 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
525 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
524 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
526 'groups': groups,
525 'groups': groups,
527 'user_group_sync': False,
526 'user_group_sync': False,
528 'email': get_ldap_attr('attr_email') or email,
527 'email': get_ldap_attr('attr_email') or email,
529 'admin': admin,
528 'admin': admin,
530 'active': active,
529 'active': active,
531 'active_from_extern': None,
530 'active_from_extern': None,
532 'extern_name': user_dn,
531 'extern_name': user_dn,
533 'extern_type': extern_type,
532 'extern_type': extern_type,
534 }
533 }
535
534
536 log.debug('ldap user: %s', user_attrs)
535 log.debug('ldap user: %s', user_attrs)
537 log.info('user `%s` authenticated correctly', user_attrs['username'],
536 log.info('user `%s` authenticated correctly', user_attrs['username'],
538 extra={"action": "user_auth_ok", "auth_module": "auth_ldap", "username": user_attrs["username"]})
537 extra={"action": "user_auth_ok", "auth_module": "auth_ldap", "username": user_attrs["username"]})
539
538
540 return user_attrs
539 return user_attrs
541
540
542 except (LdapUsernameError, LdapPasswordError, LdapImportError):
541 except (LdapUsernameError, LdapPasswordError, LdapImportError):
543 log.exception("LDAP related exception")
542 log.exception("LDAP related exception")
544 return None
543 return None
545 except (Exception,):
544 except (Exception,):
546 log.exception("Other exception")
545 log.exception("Other exception")
547 return None
546 return None
548
547
549
548
550 def includeme(config):
549 def includeme(config):
551 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
550 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
552 plugin_factory(plugin_id).includeme(config)
551 plugin_factory(plugin_id).includeme(config)
@@ -1,172 +1,172 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 RhodeCode authentication library for PAM
22 RhodeCode authentication library for PAM
23 """
23 """
24
24
25 import colander
25 import colander
26 import grp
26 import grp
27 import logging
27 import logging
28 import pam
28 import pam
29 import pwd
29 import pwd
30 import re
30 import re
31 import socket
31 import socket
32
32
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwargs):
43 def plugin_factory(plugin_id, *args, **kwargs):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class PamAuthnResource(AuthnPluginResourceBase):
52 class PamAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
57 service = colander.SchemaNode(
57 service = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='login',
59 default='login',
60 description=_('PAM service name to use for authentication.'),
60 description=_('PAM service name to use for authentication.'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('PAM service name'),
62 title=_('PAM service name'),
63 widget='string')
63 widget='string')
64 gecos = colander.SchemaNode(
64 gecos = colander.SchemaNode(
65 colander.String(),
65 colander.String(),
66 default=r'(?P<last_name>.+),\s*(?P<first_name>\w+)',
66 default=r'(?P<last_name>.+),\s*(?P<first_name>\w+)',
67 description=_('Regular expression for extracting user name/email etc. '
67 description=_('Regular expression for extracting user name/email etc. '
68 'from Unix userinfo.'),
68 'from Unix userinfo.'),
69 preparer=strip_whitespace,
69 preparer=strip_whitespace,
70 title=_('Gecos Regex'),
70 title=_('Gecos Regex'),
71 widget='string')
71 widget='string')
72
72
73
73
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
75 uid = 'pam'
75 uid = 'pam'
76 # PAM authentication can be slow. Repository operations involve a lot of
76 # PAM authentication can be slow. Repository operations involve a lot of
77 # auth calls. Little caching helps speedup push/pull operations significantly
77 # auth calls. Little caching helps speedup push/pull operations significantly
78 AUTH_CACHE_TTL = 4
78 AUTH_CACHE_TTL = 4
79
79
80 def includeme(self, config):
80 def includeme(self, config):
81 config.add_authn_plugin(self)
81 config.add_authn_plugin(self)
82 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
82 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
83 config.add_view(
83 config.add_view(
84 'rhodecode.authentication.views.AuthnPluginViewBase',
84 'rhodecode.authentication.views.AuthnPluginViewBase',
85 attr='settings_get',
85 attr='settings_get',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
87 request_method='GET',
87 request_method='GET',
88 route_name='auth_home',
88 route_name='auth_home',
89 context=PamAuthnResource)
89 context=PamAuthnResource)
90 config.add_view(
90 config.add_view(
91 'rhodecode.authentication.views.AuthnPluginViewBase',
91 'rhodecode.authentication.views.AuthnPluginViewBase',
92 attr='settings_post',
92 attr='settings_post',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
94 request_method='POST',
94 request_method='POST',
95 route_name='auth_home',
95 route_name='auth_home',
96 context=PamAuthnResource)
96 context=PamAuthnResource)
97
97
98 def get_display_name(self, load_from_settings=False):
98 def get_display_name(self, load_from_settings=False):
99 return _('PAM')
99 return _('PAM')
100
100
101 @classmethod
101 @classmethod
102 def docs(cls):
102 def docs(cls):
103 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
103 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
104
104
105 @hybrid_property
105 @hybrid_property
106 def name(self):
106 def name(self):
107 return u"pam"
107 return u"pam"
108
108
109 def get_settings_schema(self):
109 def get_settings_schema(self):
110 return PamSettingsSchema()
110 return PamSettingsSchema()
111
111
112 def use_fake_password(self):
112 def use_fake_password(self):
113 return True
113 return True
114
114
115 def auth(self, userobj, username, password, settings, **kwargs):
115 def auth(self, userobj, username, password, settings, **kwargs):
116 if not username or not password:
116 if not username or not password:
117 log.debug('Empty username or password skipping...')
117 log.debug('Empty username or password skipping...')
118 return None
118 return None
119 _pam = pam.pam()
119 _pam = pam.pam()
120 auth_result = _pam.authenticate(username, password, settings["service"])
120 auth_result = _pam.authenticate(username, password, settings["service"])
121
121
122 if not auth_result:
122 if not auth_result:
123 log.error("PAM was unable to authenticate user: %s", username)
123 log.error("PAM was unable to authenticate user: %s", username)
124 return None
124 return None
125
125
126 log.debug('Got PAM response %s', auth_result)
126 log.debug('Got PAM response %s', auth_result)
127
127
128 # old attrs fetched from RhodeCode database
128 # old attrs fetched from RhodeCode database
129 default_email = "%s@%s" % (username, socket.gethostname())
129 default_email = "%s@%s" % (username, socket.gethostname())
130 admin = getattr(userobj, 'admin', False)
130 admin = getattr(userobj, 'admin', False)
131 active = getattr(userobj, 'active', True)
131 active = getattr(userobj, 'active', True)
132 email = getattr(userobj, 'email', '') or default_email
132 email = getattr(userobj, 'email', '') or default_email
133 username = getattr(userobj, 'username', username)
133 username = getattr(userobj, 'username', username)
134 firstname = getattr(userobj, 'firstname', '')
134 firstname = getattr(userobj, 'firstname', '')
135 lastname = getattr(userobj, 'lastname', '')
135 lastname = getattr(userobj, 'lastname', '')
136 extern_type = getattr(userobj, 'extern_type', '')
136 extern_type = getattr(userobj, 'extern_type', '')
137
137
138 user_attrs = {
138 user_attrs = {
139 'username': username,
139 'username': username,
140 'firstname': firstname,
140 'firstname': firstname,
141 'lastname': lastname,
141 'lastname': lastname,
142 'groups': [g.gr_name for g in grp.getgrall()
142 'groups': [g.gr_name for g in grp.getgrall()
143 if username in g.gr_mem],
143 if username in g.gr_mem],
144 'user_group_sync': True,
144 'user_group_sync': True,
145 'email': email,
145 'email': email,
146 'admin': admin,
146 'admin': admin,
147 'active': active,
147 'active': active,
148 'active_from_extern': None,
148 'active_from_extern': None,
149 'extern_name': username,
149 'extern_name': username,
150 'extern_type': extern_type,
150 'extern_type': extern_type,
151 }
151 }
152
152
153 try:
153 try:
154 user_data = pwd.getpwnam(username)
154 user_data = pwd.getpwnam(username)
155 regex = settings["gecos"]
155 regex = settings["gecos"]
156 match = re.search(regex, user_data.pw_gecos)
156 match = re.search(regex, user_data.pw_gecos)
157 if match:
157 if match:
158 user_attrs["firstname"] = match.group('first_name')
158 user_attrs["firstname"] = match.group('first_name')
159 user_attrs["lastname"] = match.group('last_name')
159 user_attrs["lastname"] = match.group('last_name')
160 except Exception:
160 except Exception:
161 log.warning("Cannot extract additional info for PAM user")
161 log.warning("Cannot extract additional info for PAM user")
162 pass
162 pass
163
163
164 log.debug("pamuser: %s", user_attrs)
164 log.debug("pamuser: %s", user_attrs)
165 log.info('user `%s` authenticated correctly', user_attrs['username'],
165 log.info('user `%s` authenticated correctly', user_attrs['username'],
166 extra={"action": "user_auth_ok", "auth_module": "auth_pam", "username": user_attrs["username"]})
166 extra={"action": "user_auth_ok", "auth_module": "auth_pam", "username": user_attrs["username"]})
167 return user_attrs
167 return user_attrs
168
168
169
169
170 def includeme(config):
170 def includeme(config):
171 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
171 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
172 plugin_factory(plugin_id).includeme(config)
172 plugin_factory(plugin_id).includeme(config)
@@ -1,222 +1,222 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 RhodeCode authentication plugin for built in internal auth
22 RhodeCode authentication plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 import colander
27 import colander
28
28
29 from rhodecode.translation import _
29 from rhodecode.translation import _
30 from rhodecode.lib.utils2 import safe_str
30 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.base import (
33 from rhodecode.authentication.base import (
34 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
34 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 def plugin_factory(plugin_id, *args, **kwargs):
40 def plugin_factory(plugin_id, *args, **kwargs):
41 plugin = RhodeCodeAuthPlugin(plugin_id)
41 plugin = RhodeCodeAuthPlugin(plugin_id)
42 return plugin
42 return plugin
43
43
44
44
45 class RhodecodeAuthnResource(AuthnPluginResourceBase):
45 class RhodecodeAuthnResource(AuthnPluginResourceBase):
46 pass
46 pass
47
47
48
48
49 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
49 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
50 uid = 'rhodecode'
50 uid = 'rhodecode'
51 AUTH_RESTRICTION_NONE = 'user_all'
51 AUTH_RESTRICTION_NONE = 'user_all'
52 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
52 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
53 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
53 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
54 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
54 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
55 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
55 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
56
56
57 def includeme(self, config):
57 def includeme(self, config):
58 config.add_authn_plugin(self)
58 config.add_authn_plugin(self)
59 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
59 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
60 config.add_view(
60 config.add_view(
61 'rhodecode.authentication.views.AuthnPluginViewBase',
61 'rhodecode.authentication.views.AuthnPluginViewBase',
62 attr='settings_get',
62 attr='settings_get',
63 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
63 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 request_method='GET',
64 request_method='GET',
65 route_name='auth_home',
65 route_name='auth_home',
66 context=RhodecodeAuthnResource)
66 context=RhodecodeAuthnResource)
67 config.add_view(
67 config.add_view(
68 'rhodecode.authentication.views.AuthnPluginViewBase',
68 'rhodecode.authentication.views.AuthnPluginViewBase',
69 attr='settings_post',
69 attr='settings_post',
70 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
70 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
71 request_method='POST',
71 request_method='POST',
72 route_name='auth_home',
72 route_name='auth_home',
73 context=RhodecodeAuthnResource)
73 context=RhodecodeAuthnResource)
74
74
75 def get_settings_schema(self):
75 def get_settings_schema(self):
76 return RhodeCodeSettingsSchema()
76 return RhodeCodeSettingsSchema()
77
77
78 def get_display_name(self, load_from_settings=False):
78 def get_display_name(self, load_from_settings=False):
79 return _('RhodeCode Internal')
79 return _('RhodeCode Internal')
80
80
81 @classmethod
81 @classmethod
82 def docs(cls):
82 def docs(cls):
83 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
83 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
84
84
85 @hybrid_property
85 @hybrid_property
86 def name(self):
86 def name(self):
87 return u"rhodecode"
87 return u"rhodecode"
88
88
89 def user_activation_state(self):
89 def user_activation_state(self):
90 def_user_perms = User.get_default_user().AuthUser().permissions['global']
90 def_user_perms = User.get_default_user().AuthUser().permissions['global']
91 return 'hg.register.auto_activate' in def_user_perms
91 return 'hg.register.auto_activate' in def_user_perms
92
92
93 def allows_authentication_from(
93 def allows_authentication_from(
94 self, user, allows_non_existing_user=True,
94 self, user, allows_non_existing_user=True,
95 allowed_auth_plugins=None, allowed_auth_sources=None):
95 allowed_auth_plugins=None, allowed_auth_sources=None):
96 """
96 """
97 Custom method for this auth that doesn't accept non existing users.
97 Custom method for this auth that doesn't accept non existing users.
98 We know that user exists in our database.
98 We know that user exists in our database.
99 """
99 """
100 allows_non_existing_user = False
100 allows_non_existing_user = False
101 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
101 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
102 user, allows_non_existing_user=allows_non_existing_user)
102 user, allows_non_existing_user=allows_non_existing_user)
103
103
104 def auth(self, userobj, username, password, settings, **kwargs):
104 def auth(self, userobj, username, password, settings, **kwargs):
105 if not userobj:
105 if not userobj:
106 log.debug('userobj was:%s skipping', userobj)
106 log.debug('userobj was:%s skipping', userobj)
107 return None
107 return None
108
108
109 if userobj.extern_type != self.name:
109 if userobj.extern_type != self.name:
110 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
110 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
111 userobj, userobj.extern_type, self.name)
111 userobj, userobj.extern_type, self.name)
112 return None
112 return None
113
113
114 # check scope of auth
114 # check scope of auth
115 scope_restriction = settings.get('scope_restriction', '')
115 scope_restriction = settings.get('scope_restriction', '')
116
116
117 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
117 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
118 and self.auth_type != HTTP_TYPE:
118 and self.auth_type != HTTP_TYPE:
119 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
119 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
120 userobj, self.auth_type, scope_restriction)
120 userobj, self.auth_type, scope_restriction)
121 return None
121 return None
122
122
123 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
123 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
124 and self.auth_type != VCS_TYPE:
124 and self.auth_type != VCS_TYPE:
125 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
125 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
126 userobj, self.auth_type, scope_restriction)
126 userobj, self.auth_type, scope_restriction)
127 return None
127 return None
128
128
129 # check super-admin restriction
129 # check super-admin restriction
130 auth_restriction = settings.get('auth_restriction', '')
130 auth_restriction = settings.get('auth_restriction', '')
131
131
132 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
132 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
133 and userobj.admin is False:
133 and userobj.admin is False:
134 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
134 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
135 userobj, auth_restriction)
135 userobj, auth_restriction)
136 return None
136 return None
137
137
138 user_attrs = {
138 user_attrs = {
139 "username": userobj.username,
139 "username": userobj.username,
140 "firstname": userobj.firstname,
140 "firstname": userobj.firstname,
141 "lastname": userobj.lastname,
141 "lastname": userobj.lastname,
142 "groups": [],
142 "groups": [],
143 'user_group_sync': False,
143 'user_group_sync': False,
144 "email": userobj.email,
144 "email": userobj.email,
145 "admin": userobj.admin,
145 "admin": userobj.admin,
146 "active": userobj.active,
146 "active": userobj.active,
147 "active_from_extern": userobj.active,
147 "active_from_extern": userobj.active,
148 "extern_name": userobj.user_id,
148 "extern_name": userobj.user_id,
149 "extern_type": userobj.extern_type,
149 "extern_type": userobj.extern_type,
150 }
150 }
151
151
152 log.debug("User attributes:%s", user_attrs)
152 log.debug("User attributes:%s", user_attrs)
153 if userobj.active:
153 if userobj.active:
154 from rhodecode.lib import auth
154 from rhodecode.lib import auth
155 crypto_backend = auth.crypto_backend()
155 crypto_backend = auth.crypto_backend()
156 password_encoded = safe_str(password)
156 password_encoded = safe_str(password)
157 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
157 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
158 password_encoded, userobj.password or '')
158 password_encoded, userobj.password or '')
159
159
160 if password_match and new_hash:
160 if password_match and new_hash:
161 log.debug('user %s properly authenticated, but '
161 log.debug('user %s properly authenticated, but '
162 'requires hash change to bcrypt', userobj)
162 'requires hash change to bcrypt', userobj)
163 # if password match, and we use OLD deprecated hash,
163 # if password match, and we use OLD deprecated hash,
164 # we should migrate this user hash password to the new hash
164 # we should migrate this user hash password to the new hash
165 # we store the new returned by hash_check_with_upgrade function
165 # we store the new returned by hash_check_with_upgrade function
166 user_attrs['_hash_migrate'] = new_hash
166 user_attrs['_hash_migrate'] = new_hash
167
167
168 if userobj.username == User.DEFAULT_USER and userobj.active:
168 if userobj.username == User.DEFAULT_USER and userobj.active:
169 log.info('user `%s` authenticated correctly as anonymous user',
169 log.info('user `%s` authenticated correctly as anonymous user',
170 userobj.username,
170 userobj.username,
171 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
171 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
172 return user_attrs
172 return user_attrs
173
173
174 elif userobj.username == username and password_match:
174 elif userobj.username == username and password_match:
175 log.info('user `%s` authenticated correctly', userobj.username,
175 log.info('user `%s` authenticated correctly', userobj.username,
176 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
176 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
177 return user_attrs
177 return user_attrs
178 log.warning("user `%s` used a wrong password when "
178 log.warning("user `%s` used a wrong password when "
179 "authenticating on this plugin", userobj.username)
179 "authenticating on this plugin", userobj.username)
180 return None
180 return None
181 else:
181 else:
182 log.warning('user `%s` failed to authenticate via %s, reason: account not '
182 log.warning('user `%s` failed to authenticate via %s, reason: account not '
183 'active.', username, self.name)
183 'active.', username, self.name)
184 return None
184 return None
185
185
186
186
187 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
187 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
188
188
189 auth_restriction_choices = [
189 auth_restriction_choices = [
190 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
190 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
191 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
191 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
192 ]
192 ]
193
193
194 auth_scope_choices = [
194 auth_scope_choices = [
195 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
195 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
196 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
196 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
197 ]
197 ]
198
198
199 auth_restriction = colander.SchemaNode(
199 auth_restriction = colander.SchemaNode(
200 colander.String(),
200 colander.String(),
201 default=auth_restriction_choices[0],
201 default=auth_restriction_choices[0],
202 description=_('Allowed user types for authentication using this plugin.'),
202 description=_('Allowed user types for authentication using this plugin.'),
203 title=_('User restriction'),
203 title=_('User restriction'),
204 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
204 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
205 widget='select_with_labels',
205 widget='select_with_labels',
206 choices=auth_restriction_choices
206 choices=auth_restriction_choices
207 )
207 )
208 scope_restriction = colander.SchemaNode(
208 scope_restriction = colander.SchemaNode(
209 colander.String(),
209 colander.String(),
210 default=auth_scope_choices[0],
210 default=auth_scope_choices[0],
211 description=_('Allowed protocols for authentication using this plugin. '
211 description=_('Allowed protocols for authentication using this plugin. '
212 'VCS means GIT/HG/SVN. HTTP is web based login.'),
212 'VCS means GIT/HG/SVN. HTTP is web based login.'),
213 title=_('Scope restriction'),
213 title=_('Scope restriction'),
214 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
214 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
215 widget='select_with_labels',
215 widget='select_with_labels',
216 choices=auth_scope_choices
216 choices=auth_scope_choices
217 )
217 )
218
218
219
219
220 def includeme(config):
220 def includeme(config):
221 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
221 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
222 plugin_factory(plugin_id).includeme(config)
222 plugin_factory(plugin_id).includeme(config)
@@ -1,177 +1,177 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 RhodeCode authentication token plugin for built in internal auth
22 RhodeCode authentication token plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26 import colander
26 import colander
27
27
28 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
29 from rhodecode.translation import _
29 from rhodecode.translation import _
30 from rhodecode.authentication.base import (
30 from rhodecode.authentication.base import (
31 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
31 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
32 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 from rhodecode.model.db import User, UserApiKeys, Repository
33 from rhodecode.model.db import User, UserApiKeys, Repository
34
34
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 def plugin_factory(plugin_id, *args, **kwargs):
39 def plugin_factory(plugin_id, *args, **kwargs):
40 plugin = RhodeCodeAuthPlugin(plugin_id)
40 plugin = RhodeCodeAuthPlugin(plugin_id)
41 return plugin
41 return plugin
42
42
43
43
44 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 class RhodecodeAuthnResource(AuthnPluginResourceBase):
45 pass
45 pass
46
46
47
47
48 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
49 """
49 """
50 Enables usage of authentication tokens for vcs operations.
50 Enables usage of authentication tokens for vcs operations.
51 """
51 """
52 uid = 'token'
52 uid = 'token'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
54
54
55 def includeme(self, config):
55 def includeme(self, config):
56 config.add_authn_plugin(self)
56 config.add_authn_plugin(self)
57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
58 config.add_view(
58 config.add_view(
59 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 attr='settings_get',
60 attr='settings_get',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 request_method='GET',
62 request_method='GET',
63 route_name='auth_home',
63 route_name='auth_home',
64 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
65 config.add_view(
65 config.add_view(
66 'rhodecode.authentication.views.AuthnPluginViewBase',
66 'rhodecode.authentication.views.AuthnPluginViewBase',
67 attr='settings_post',
67 attr='settings_post',
68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
69 request_method='POST',
69 request_method='POST',
70 route_name='auth_home',
70 route_name='auth_home',
71 context=RhodecodeAuthnResource)
71 context=RhodecodeAuthnResource)
72
72
73 def get_settings_schema(self):
73 def get_settings_schema(self):
74 return RhodeCodeSettingsSchema()
74 return RhodeCodeSettingsSchema()
75
75
76 def get_display_name(self, load_from_settings=False):
76 def get_display_name(self, load_from_settings=False):
77 return _('Rhodecode Token')
77 return _('Rhodecode Token')
78
78
79 @classmethod
79 @classmethod
80 def docs(cls):
80 def docs(cls):
81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-token.html"
81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-token.html"
82
82
83 @hybrid_property
83 @hybrid_property
84 def name(self):
84 def name(self):
85 return u"authtoken"
85 return u"authtoken"
86
86
87 def user_activation_state(self):
87 def user_activation_state(self):
88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
89 return 'hg.register.auto_activate' in def_user_perms
89 return 'hg.register.auto_activate' in def_user_perms
90
90
91 def allows_authentication_from(
91 def allows_authentication_from(
92 self, user, allows_non_existing_user=True,
92 self, user, allows_non_existing_user=True,
93 allowed_auth_plugins=None, allowed_auth_sources=None):
93 allowed_auth_plugins=None, allowed_auth_sources=None):
94 """
94 """
95 Custom method for this auth that doesn't accept empty users. And also
95 Custom method for this auth that doesn't accept empty users. And also
96 allows users from all other active plugins to use it and also
96 allows users from all other active plugins to use it and also
97 authenticate against it. But only via vcs mode
97 authenticate against it. But only via vcs mode
98 """
98 """
99 from rhodecode.authentication.base import get_authn_registry
99 from rhodecode.authentication.base import get_authn_registry
100 authn_registry = get_authn_registry()
100 authn_registry = get_authn_registry()
101
101
102 active_plugins = set(
102 active_plugins = set(
103 [x.name for x in authn_registry.get_plugins_for_authentication()])
103 [x.name for x in authn_registry.get_plugins_for_authentication()])
104 active_plugins.discard(self.name)
104 active_plugins.discard(self.name)
105
105
106 allowed_auth_plugins = [self.name] + list(active_plugins)
106 allowed_auth_plugins = [self.name] + list(active_plugins)
107 # only for vcs operations
107 # only for vcs operations
108 allowed_auth_sources = [VCS_TYPE]
108 allowed_auth_sources = [VCS_TYPE]
109
109
110 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
110 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
111 user, allows_non_existing_user=False,
111 user, allows_non_existing_user=False,
112 allowed_auth_plugins=allowed_auth_plugins,
112 allowed_auth_plugins=allowed_auth_plugins,
113 allowed_auth_sources=allowed_auth_sources)
113 allowed_auth_sources=allowed_auth_sources)
114
114
115 def auth(self, userobj, username, password, settings, **kwargs):
115 def auth(self, userobj, username, password, settings, **kwargs):
116 if not userobj:
116 if not userobj:
117 log.debug('userobj was:%s skipping', userobj)
117 log.debug('userobj was:%s skipping', userobj)
118 return None
118 return None
119
119
120 user_attrs = {
120 user_attrs = {
121 "username": userobj.username,
121 "username": userobj.username,
122 "firstname": userobj.firstname,
122 "firstname": userobj.firstname,
123 "lastname": userobj.lastname,
123 "lastname": userobj.lastname,
124 "groups": [],
124 "groups": [],
125 'user_group_sync': False,
125 'user_group_sync': False,
126 "email": userobj.email,
126 "email": userobj.email,
127 "admin": userobj.admin,
127 "admin": userobj.admin,
128 "active": userobj.active,
128 "active": userobj.active,
129 "active_from_extern": userobj.active,
129 "active_from_extern": userobj.active,
130 "extern_name": userobj.user_id,
130 "extern_name": userobj.user_id,
131 "extern_type": userobj.extern_type,
131 "extern_type": userobj.extern_type,
132 }
132 }
133
133
134 log.debug('Authenticating user with args %s', user_attrs)
134 log.debug('Authenticating user with args %s', user_attrs)
135 if userobj.active:
135 if userobj.active:
136 # calling context repo for token scopes
136 # calling context repo for token scopes
137 scope_repo_id = None
137 scope_repo_id = None
138 if self.acl_repo_name:
138 if self.acl_repo_name:
139 repo = Repository.get_by_repo_name(self.acl_repo_name)
139 repo = Repository.get_by_repo_name(self.acl_repo_name)
140 scope_repo_id = repo.repo_id if repo else None
140 scope_repo_id = repo.repo_id if repo else None
141
141
142 token_match = userobj.authenticate_by_token(
142 token_match = userobj.authenticate_by_token(
143 password, roles=[UserApiKeys.ROLE_VCS],
143 password, roles=[UserApiKeys.ROLE_VCS],
144 scope_repo_id=scope_repo_id)
144 scope_repo_id=scope_repo_id)
145
145
146 if userobj.username == username and token_match:
146 if userobj.username == username and token_match:
147 log.info(
147 log.info(
148 'user `%s` successfully authenticated via %s',
148 'user `%s` successfully authenticated via %s',
149 user_attrs['username'], self.name)
149 user_attrs['username'], self.name)
150 return user_attrs
150 return user_attrs
151 log.warning('user `%s` failed to authenticate via %s, reason: bad or '
151 log.warning('user `%s` failed to authenticate via %s, reason: bad or '
152 'inactive token.', username, self.name)
152 'inactive token.', username, self.name)
153 else:
153 else:
154 log.warning('user `%s` failed to authenticate via %s, reason: account not '
154 log.warning('user `%s` failed to authenticate via %s, reason: account not '
155 'active.', username, self.name)
155 'active.', username, self.name)
156 return None
156 return None
157
157
158
158
159 def includeme(config):
159 def includeme(config):
160 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
160 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
161 plugin_factory(plugin_id).includeme(config)
161 plugin_factory(plugin_id).includeme(config)
162
162
163
163
164 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
164 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
165 auth_scope_choices = [
165 auth_scope_choices = [
166 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS, 'VCS only'),
166 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS, 'VCS only'),
167 ]
167 ]
168
168
169 scope_restriction = colander.SchemaNode(
169 scope_restriction = colander.SchemaNode(
170 colander.String(),
170 colander.String(),
171 default=auth_scope_choices[0],
171 default=auth_scope_choices[0],
172 description=_('Choose operation scope restriction when authenticating.'),
172 description=_('Choose operation scope restriction when authenticating.'),
173 title=_('Scope restriction'),
173 title=_('Scope restriction'),
174 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
174 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
175 widget='select_with_labels',
175 widget='select_with_labels',
176 choices=auth_scope_choices
176 choices=auth_scope_choices
177 )
177 )
@@ -1,121 +1,121 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 time
21 import time
22 import logging
22 import logging
23
23
24 from pyramid.exceptions import ConfigurationError
24 from pyramid.exceptions import ConfigurationError
25 from zope.interface import implementer
25 from zope.interface import implementer
26
26
27 from rhodecode.authentication.interface import IAuthnPluginRegistry
27 from rhodecode.authentication.interface import IAuthnPluginRegistry
28 from rhodecode.model.settings import SettingsModel
28 from rhodecode.model.settings import SettingsModel
29 from rhodecode.lib.utils2 import safe_str
29 from rhodecode.lib.utils2 import safe_str
30 from rhodecode.lib.statsd_client import StatsdClient
30 from rhodecode.lib.statsd_client import StatsdClient
31 from rhodecode.lib import rc_cache
31 from rhodecode.lib import rc_cache
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 @implementer(IAuthnPluginRegistry)
36 @implementer(IAuthnPluginRegistry)
37 class AuthenticationPluginRegistry(object):
37 class AuthenticationPluginRegistry(object):
38
38
39 # INI settings key to set a fallback authentication plugin.
39 # INI settings key to set a fallback authentication plugin.
40 fallback_plugin_key = 'rhodecode.auth_plugin_fallback'
40 fallback_plugin_key = 'rhodecode.auth_plugin_fallback'
41
41
42 def __init__(self, settings):
42 def __init__(self, settings):
43 self._plugins = {}
43 self._plugins = {}
44 self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
44 self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
45
45
46 def add_authn_plugin(self, config, plugin):
46 def add_authn_plugin(self, config, plugin):
47 plugin_id = plugin.get_id()
47 plugin_id = plugin.get_id()
48 if plugin_id in self._plugins.keys():
48 if plugin_id in self._plugins.keys():
49 raise ConfigurationError(
49 raise ConfigurationError(
50 'Cannot register authentication plugin twice: "%s"', plugin_id)
50 'Cannot register authentication plugin twice: "%s"', plugin_id)
51 else:
51 else:
52 log.debug('Register authentication plugin: "%s"', plugin_id)
52 log.debug('Register authentication plugin: "%s"', plugin_id)
53 self._plugins[plugin_id] = plugin
53 self._plugins[plugin_id] = plugin
54
54
55 def get_plugins(self):
55 def get_plugins(self):
56 def sort_key(plugin):
56 def sort_key(plugin):
57 return str.lower(safe_str(plugin.get_display_name()))
57 return str.lower(safe_str(plugin.get_display_name()))
58
58
59 return sorted(self._plugins.values(), key=sort_key)
59 return sorted(self._plugins.values(), key=sort_key)
60
60
61 def get_plugin(self, plugin_id):
61 def get_plugin(self, plugin_id):
62 return self._plugins.get(plugin_id, None)
62 return self._plugins.get(plugin_id, None)
63
63
64 def get_plugin_by_uid(self, plugin_uid):
64 def get_plugin_by_uid(self, plugin_uid):
65 for plugin in self._plugins.values():
65 for plugin in self._plugins.values():
66 if plugin.uid == plugin_uid:
66 if plugin.uid == plugin_uid:
67 return plugin
67 return plugin
68
68
69 def get_plugins_for_authentication(self, cache=True):
69 def get_plugins_for_authentication(self, cache=True):
70 """
70 """
71 Returns a list of plugins which should be consulted when authenticating
71 Returns a list of plugins which should be consulted when authenticating
72 a user. It only returns plugins which are enabled and active.
72 a user. It only returns plugins which are enabled and active.
73 Additionally it includes the fallback plugin from the INI file, if
73 Additionally it includes the fallback plugin from the INI file, if
74 `rhodecode.auth_plugin_fallback` is set to a plugin ID.
74 `rhodecode.auth_plugin_fallback` is set to a plugin ID.
75 """
75 """
76
76
77 cache_namespace_uid = 'cache_auth_plugins'
77 cache_namespace_uid = 'cache_auth_plugins'
78 region = rc_cache.get_or_create_region('cache_general', cache_namespace_uid)
78 region = rc_cache.get_or_create_region('cache_general', cache_namespace_uid)
79
79
80 @region.conditional_cache_on_arguments(condition=cache)
80 @region.conditional_cache_on_arguments(condition=cache)
81 def _get_auth_plugins(name, key, fallback_plugin):
81 def _get_auth_plugins(name, key, fallback_plugin):
82 plugins = []
82 plugins = []
83
83
84 # Add all enabled and active plugins to the list. We iterate over the
84 # Add all enabled and active plugins to the list. We iterate over the
85 # auth_plugins setting from DB because it also represents the ordering.
85 # auth_plugins setting from DB because it also represents the ordering.
86 enabled_plugins = SettingsModel().get_auth_plugins()
86 enabled_plugins = SettingsModel().get_auth_plugins()
87 raw_settings = SettingsModel().get_all_settings(cache=False)
87 raw_settings = SettingsModel().get_all_settings(cache=False)
88 for plugin_id in enabled_plugins:
88 for plugin_id in enabled_plugins:
89 plugin = self.get_plugin(plugin_id)
89 plugin = self.get_plugin(plugin_id)
90 if plugin is not None and plugin.is_active(
90 if plugin is not None and plugin.is_active(
91 plugin_cached_settings=raw_settings):
91 plugin_cached_settings=raw_settings):
92
92
93 # inject settings into plugin, we can re-use the DB fetched settings here
93 # inject settings into plugin, we can re-use the DB fetched settings here
94 plugin._settings = plugin._propagate_settings(raw_settings)
94 plugin._settings = plugin._propagate_settings(raw_settings)
95 plugins.append(plugin)
95 plugins.append(plugin)
96
96
97 # Add the fallback plugin from ini file.
97 # Add the fallback plugin from ini file.
98 if fallback_plugin:
98 if fallback_plugin:
99 log.warning(
99 log.warning(
100 'Using fallback authentication plugin from INI file: "%s"',
100 'Using fallback authentication plugin from INI file: "%s"',
101 fallback_plugin)
101 fallback_plugin)
102 plugin = self.get_plugin(fallback_plugin)
102 plugin = self.get_plugin(fallback_plugin)
103 if plugin is not None and plugin not in plugins:
103 if plugin is not None and plugin not in plugins:
104 plugin._settings = plugin._propagate_settings(raw_settings)
104 plugin._settings = plugin._propagate_settings(raw_settings)
105 plugins.append(plugin)
105 plugins.append(plugin)
106 return plugins
106 return plugins
107
107
108 start = time.time()
108 start = time.time()
109 plugins = _get_auth_plugins('rhodecode_auth_plugins', 'v1', self._fallback_plugin)
109 plugins = _get_auth_plugins('rhodecode_auth_plugins', 'v1', self._fallback_plugin)
110
110
111 compute_time = time.time() - start
111 compute_time = time.time() - start
112 log.debug('cached method:%s took %.4fs', _get_auth_plugins.__name__, compute_time)
112 log.debug('cached method:%s took %.4fs', _get_auth_plugins.__name__, compute_time)
113
113
114 statsd = StatsdClient.statsd
114 statsd = StatsdClient.statsd
115 if statsd:
115 if statsd:
116 elapsed_time_ms = round(1000.0 * compute_time) # use ms only
116 elapsed_time_ms = round(1000.0 * compute_time) # use ms only
117 statsd.timing("rhodecode_auth_plugins_timing.histogram", elapsed_time_ms,
117 statsd.timing("rhodecode_auth_plugins_timing.histogram", elapsed_time_ms,
118 use_decimals=False)
118 use_decimals=False)
119
119
120 return plugins
120 return plugins
121
121
@@ -1,155 +1,155 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 import collections
22 import collections
23
23
24 from pyramid.exceptions import ConfigurationError
24 from pyramid.exceptions import ConfigurationError
25
25
26 from rhodecode.lib.utils2 import safe_str
26 from rhodecode.lib.utils2 import safe_str
27 from rhodecode.model.settings import SettingsModel
27 from rhodecode.model.settings import SettingsModel
28 from rhodecode.translation import _
28 from rhodecode.translation import _
29
29
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class AuthnResourceBase(object):
34 class AuthnResourceBase(object):
35 __name__ = None
35 __name__ = None
36 __parent__ = None
36 __parent__ = None
37
37
38 def get_root(self):
38 def get_root(self):
39 current = self
39 current = self
40 while current.__parent__ is not None:
40 while current.__parent__ is not None:
41 current = current.__parent__
41 current = current.__parent__
42 return current
42 return current
43
43
44
44
45 class AuthnPluginResourceBase(AuthnResourceBase):
45 class AuthnPluginResourceBase(AuthnResourceBase):
46
46
47 def __init__(self, plugin):
47 def __init__(self, plugin):
48 self.plugin = plugin
48 self.plugin = plugin
49 self.__name__ = plugin.get_url_slug()
49 self.__name__ = plugin.get_url_slug()
50 self.display_name = plugin.get_display_name()
50 self.display_name = plugin.get_display_name()
51
51
52
52
53 class AuthnRootResource(AuthnResourceBase):
53 class AuthnRootResource(AuthnResourceBase):
54 """
54 """
55 This is the root traversal resource object for the authentication settings.
55 This is the root traversal resource object for the authentication settings.
56 """
56 """
57
57
58 def __init__(self):
58 def __init__(self):
59 self._store = collections.OrderedDict()
59 self._store = collections.OrderedDict()
60 self._resource_name_map = {}
60 self._resource_name_map = {}
61 self.display_name = _('Authentication Plugins')
61 self.display_name = _('Authentication Plugins')
62
62
63 def __getitem__(self, key):
63 def __getitem__(self, key):
64 """
64 """
65 Customized get item function to return only items (plugins) that are
65 Customized get item function to return only items (plugins) that are
66 activated.
66 activated.
67 """
67 """
68 if self._is_item_active(key):
68 if self._is_item_active(key):
69 return self._store[key]
69 return self._store[key]
70 else:
70 else:
71 raise KeyError('Authentication plugin "{}" is not active.'.format(
71 raise KeyError('Authentication plugin "{}" is not active.'.format(
72 key))
72 key))
73
73
74 def __iter__(self):
74 def __iter__(self):
75 for key in self._store.keys():
75 for key in self._store.keys():
76 if self._is_item_active(key):
76 if self._is_item_active(key):
77 yield self._store[key]
77 yield self._store[key]
78
78
79 def _is_item_active(self, key):
79 def _is_item_active(self, key):
80 activated_plugins = SettingsModel().get_auth_plugins()
80 activated_plugins = SettingsModel().get_auth_plugins()
81 plugin_id = self.get_plugin_id(key)
81 plugin_id = self.get_plugin_id(key)
82 return plugin_id in activated_plugins
82 return plugin_id in activated_plugins
83
83
84 def get_plugin_id(self, resource_name):
84 def get_plugin_id(self, resource_name):
85 """
85 """
86 Return the plugin id for the given traversal resource name.
86 Return the plugin id for the given traversal resource name.
87 """
87 """
88 # TODO: Store this info in the resource element.
88 # TODO: Store this info in the resource element.
89 return self._resource_name_map[resource_name]
89 return self._resource_name_map[resource_name]
90
90
91 def get_sorted_list(self, sort_key=None):
91 def get_sorted_list(self, sort_key=None):
92 """
92 """
93 Returns a sorted list of sub resources for displaying purposes.
93 Returns a sorted list of sub resources for displaying purposes.
94 """
94 """
95 def default_sort_key(resource):
95 def default_sort_key(resource):
96 return str.lower(safe_str(resource.display_name))
96 return str.lower(safe_str(resource.display_name))
97
97
98 active = [item for item in self]
98 active = [item for item in self]
99 return sorted(active, key=sort_key or default_sort_key)
99 return sorted(active, key=sort_key or default_sort_key)
100
100
101 def get_nav_list(self, sort=True):
101 def get_nav_list(self, sort=True):
102 """
102 """
103 Returns a sorted list of resources for displaying the navigation.
103 Returns a sorted list of resources for displaying the navigation.
104 """
104 """
105 if sort:
105 if sort:
106 nav_list = self.get_sorted_list()
106 nav_list = self.get_sorted_list()
107 else:
107 else:
108 nav_list = [item for item in self]
108 nav_list = [item for item in self]
109
109
110 nav_list.insert(0, self)
110 nav_list.insert(0, self)
111 return nav_list
111 return nav_list
112
112
113 def add_authn_resource(self, config, plugin_id, resource):
113 def add_authn_resource(self, config, plugin_id, resource):
114 """
114 """
115 Register a traversal resource as a sub element to the authentication
115 Register a traversal resource as a sub element to the authentication
116 settings. This method is registered as a directive on the pyramid
116 settings. This method is registered as a directive on the pyramid
117 configurator object and called by plugins.
117 configurator object and called by plugins.
118 """
118 """
119
119
120 def _ensure_unique_name(name, limit=100):
120 def _ensure_unique_name(name, limit=100):
121 counter = 1
121 counter = 1
122 current = name
122 current = name
123 while current in self._store.keys():
123 while current in self._store.keys():
124 current = '{}{}'.format(name, counter)
124 current = '{}{}'.format(name, counter)
125 counter += 1
125 counter += 1
126 if counter > limit:
126 if counter > limit:
127 raise ConfigurationError(
127 raise ConfigurationError(
128 'Cannot build unique name for traversal resource "%s" '
128 'Cannot build unique name for traversal resource "%s" '
129 'registered by plugin "%s"', name, plugin_id)
129 'registered by plugin "%s"', name, plugin_id)
130 return current
130 return current
131
131
132 # Allow plugin resources with identical names by rename duplicates.
132 # Allow plugin resources with identical names by rename duplicates.
133 unique_name = _ensure_unique_name(resource.__name__)
133 unique_name = _ensure_unique_name(resource.__name__)
134 if unique_name != resource.__name__:
134 if unique_name != resource.__name__:
135 log.warning('Name collision for traversal resource "%s" registered '
135 log.warning('Name collision for traversal resource "%s" registered '
136 'by authentication plugin "%s"', resource.__name__,
136 'by authentication plugin "%s"', resource.__name__,
137 plugin_id)
137 plugin_id)
138 resource.__name__ = unique_name
138 resource.__name__ = unique_name
139
139
140 log.debug('Register traversal resource "%s" for plugin "%s"',
140 log.debug('Register traversal resource "%s" for plugin "%s"',
141 unique_name, plugin_id)
141 unique_name, plugin_id)
142 self._resource_name_map[unique_name] = plugin_id
142 self._resource_name_map[unique_name] = plugin_id
143 resource.__parent__ = self
143 resource.__parent__ = self
144 self._store[unique_name] = resource
144 self._store[unique_name] = resource
145
145
146
146
147 root = AuthnRootResource()
147 root = AuthnRootResource()
148
148
149
149
150 def root_factory(request=None):
150 def root_factory(request=None):
151 """
151 """
152 Returns the root traversal resource instance used for the authentication
152 Returns the root traversal resource instance used for the authentication
153 settings route.
153 settings route.
154 """
154 """
155 return root
155 return root
@@ -1,52 +1,52 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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
22
23 from rhodecode.authentication import plugin_default_auth_ttl
23 from rhodecode.authentication import plugin_default_auth_ttl
24 from rhodecode.translation import _
24 from rhodecode.translation import _
25
25
26
26
27 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
27 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
28 """
28 """
29 This base schema is intended for use in authentication plugins.
29 This base schema is intended for use in authentication plugins.
30 It adds a few default settings (e.g., "enabled"), so that plugin
30 It adds a few default settings (e.g., "enabled"), so that plugin
31 authors don't have to maintain a bunch of boilerplate.
31 authors don't have to maintain a bunch of boilerplate.
32 """
32 """
33 enabled = colander.SchemaNode(
33 enabled = colander.SchemaNode(
34 colander.Bool(),
34 colander.Bool(),
35 default=False,
35 default=False,
36 description=_('Enable or disable this authentication plugin.'),
36 description=_('Enable or disable this authentication plugin.'),
37 missing=False,
37 missing=False,
38 title=_('Enabled'),
38 title=_('Enabled'),
39 widget='bool',
39 widget='bool',
40 )
40 )
41 cache_ttl = colander.SchemaNode(
41 cache_ttl = colander.SchemaNode(
42 colander.Int(),
42 colander.Int(),
43 default=plugin_default_auth_ttl,
43 default=plugin_default_auth_ttl,
44 description=_('Amount of seconds to cache the authentication and '
44 description=_('Amount of seconds to cache the authentication and '
45 'permissions check response call for this plugin. \n'
45 'permissions check response call for this plugin. \n'
46 'Useful for expensive calls like LDAP to improve the '
46 'Useful for expensive calls like LDAP to improve the '
47 'performance of the system (0 means disabled).'),
47 'performance of the system (0 means disabled).'),
48 missing=0,
48 missing=0,
49 title=_('Auth Cache TTL'),
49 title=_('Auth Cache TTL'),
50 validator=colander.Range(min=0, max=None),
50 validator=colander.Range(min=0, max=None),
51 widget='int',
51 widget='int',
52 )
52 )
@@ -1,89 +1,89 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 import pytest
22 import pytest
23
23
24
24
25 class EnabledAuthPlugin(object):
25 class EnabledAuthPlugin(object):
26 """
26 """
27 Context manager that updates the 'auth_plugins' setting in DB to enable
27 Context manager that updates the 'auth_plugins' setting in DB to enable
28 a plugin. Previous setting is restored on exit. The rhodecode auth plugin
28 a plugin. Previous setting is restored on exit. The rhodecode auth plugin
29 is also enabled because it is needed to login the test users.
29 is also enabled because it is needed to login the test users.
30 """
30 """
31
31
32 def __init__(self, plugin):
32 def __init__(self, plugin):
33 self.new_value = {'egg:rhodecode-enterprise-ce#rhodecode', plugin.get_id()}
33 self.new_value = {'egg:rhodecode-enterprise-ce#rhodecode', plugin.get_id()}
34
34
35 def __enter__(self):
35 def __enter__(self):
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37 self._old_value = SettingsModel().get_auth_plugins()
37 self._old_value = SettingsModel().get_auth_plugins()
38 SettingsModel().create_or_update_setting(
38 SettingsModel().create_or_update_setting(
39 'auth_plugins', ','.join(self.new_value))
39 'auth_plugins', ','.join(self.new_value))
40
40
41 def __exit__(self, type, value, traceback):
41 def __exit__(self, type, value, traceback):
42 from rhodecode.model.settings import SettingsModel
42 from rhodecode.model.settings import SettingsModel
43 SettingsModel().create_or_update_setting(
43 SettingsModel().create_or_update_setting(
44 'auth_plugins', ','.join(self._old_value))
44 'auth_plugins', ','.join(self._old_value))
45
45
46
46
47 class DisabledAuthPlugin(object):
47 class DisabledAuthPlugin(object):
48 """
48 """
49 Context manager that updates the 'auth_plugins' setting in DB to disable
49 Context manager that updates the 'auth_plugins' setting in DB to disable
50 a plugin. Previous setting is restored on exit.
50 a plugin. Previous setting is restored on exit.
51 """
51 """
52
52
53 def __init__(self, plugin):
53 def __init__(self, plugin):
54 self.plugin_id = plugin.get_id()
54 self.plugin_id = plugin.get_id()
55
55
56 def __enter__(self):
56 def __enter__(self):
57 from rhodecode.model.settings import SettingsModel
57 from rhodecode.model.settings import SettingsModel
58 self._old_value = SettingsModel().get_auth_plugins()
58 self._old_value = SettingsModel().get_auth_plugins()
59 new_value = [id_ for id_ in self._old_value if id_ != self.plugin_id]
59 new_value = [id_ for id_ in self._old_value if id_ != self.plugin_id]
60 SettingsModel().create_or_update_setting(
60 SettingsModel().create_or_update_setting(
61 'auth_plugins', ','.join(new_value))
61 'auth_plugins', ','.join(new_value))
62
62
63 def __exit__(self, type, value, traceback):
63 def __exit__(self, type, value, traceback):
64 from rhodecode.model.settings import SettingsModel
64 from rhodecode.model.settings import SettingsModel
65 SettingsModel().create_or_update_setting(
65 SettingsModel().create_or_update_setting(
66 'auth_plugins', ','.join(self._old_value))
66 'auth_plugins', ','.join(self._old_value))
67
67
68
68
69 @pytest.fixture(params=[
69 @pytest.fixture(params=[
70 ('rhodecode.authentication.plugins.auth_crowd', 'egg:rhodecode-enterprise-ce#crowd'),
70 ('rhodecode.authentication.plugins.auth_crowd', 'egg:rhodecode-enterprise-ce#crowd'),
71 ('rhodecode.authentication.plugins.auth_headers', 'egg:rhodecode-enterprise-ce#headers'),
71 ('rhodecode.authentication.plugins.auth_headers', 'egg:rhodecode-enterprise-ce#headers'),
72 ('rhodecode.authentication.plugins.auth_jasig_cas', 'egg:rhodecode-enterprise-ce#jasig_cas'),
72 ('rhodecode.authentication.plugins.auth_jasig_cas', 'egg:rhodecode-enterprise-ce#jasig_cas'),
73 ('rhodecode.authentication.plugins.auth_ldap', 'egg:rhodecode-enterprise-ce#ldap'),
73 ('rhodecode.authentication.plugins.auth_ldap', 'egg:rhodecode-enterprise-ce#ldap'),
74 ('rhodecode.authentication.plugins.auth_pam', 'egg:rhodecode-enterprise-ce#pam'),
74 ('rhodecode.authentication.plugins.auth_pam', 'egg:rhodecode-enterprise-ce#pam'),
75 ('rhodecode.authentication.plugins.auth_rhodecode', 'egg:rhodecode-enterprise-ce#rhodecode'),
75 ('rhodecode.authentication.plugins.auth_rhodecode', 'egg:rhodecode-enterprise-ce#rhodecode'),
76 ('rhodecode.authentication.plugins.auth_token', 'egg:rhodecode-enterprise-ce#token'),
76 ('rhodecode.authentication.plugins.auth_token', 'egg:rhodecode-enterprise-ce#token'),
77 ])
77 ])
78 def auth_plugin(request):
78 def auth_plugin(request):
79 """
79 """
80 Fixture that provides instance for each authentication plugin. These
80 Fixture that provides instance for each authentication plugin. These
81 instances are NOT the instances which are registered to the authentication
81 instances are NOT the instances which are registered to the authentication
82 registry.
82 registry.
83 """
83 """
84 from importlib import import_module
84 from importlib import import_module
85
85
86 # Create plugin instance.
86 # Create plugin instance.
87 module, plugin_id = request.param
87 module, plugin_id = request.param
88 plugin_module = import_module(module)
88 plugin_module = import_module(module)
89 return plugin_module.plugin_factory(plugin_id)
89 return plugin_module.plugin_factory(plugin_id)
@@ -1,77 +1,77 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 import pytest
22 import pytest
23
23
24 from rhodecode.authentication.tests.conftest import (
24 from rhodecode.authentication.tests.conftest import (
25 EnabledAuthPlugin, DisabledAuthPlugin)
25 EnabledAuthPlugin, DisabledAuthPlugin)
26 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.apps._base import ADMIN_PREFIX
27
27
28
28
29 @pytest.mark.usefixtures('autologin_user', 'app')
29 @pytest.mark.usefixtures('autologin_user', 'app')
30 class TestAuthenticationSettings:
30 class TestAuthenticationSettings:
31
31
32 def test_auth_settings_global_view_get(self, app):
32 def test_auth_settings_global_view_get(self, app):
33 url = '{prefix}/auth/'.format(prefix=ADMIN_PREFIX)
33 url = '{prefix}/auth/'.format(prefix=ADMIN_PREFIX)
34 response = app.get(url)
34 response = app.get(url)
35 assert response.status_code == 200
35 assert response.status_code == 200
36
36
37 def test_plugin_settings_view_get(self, app, auth_plugin):
37 def test_plugin_settings_view_get(self, app, auth_plugin):
38 url = '{prefix}/auth/{name}'.format(
38 url = '{prefix}/auth/{name}'.format(
39 prefix=ADMIN_PREFIX,
39 prefix=ADMIN_PREFIX,
40 name=auth_plugin.name)
40 name=auth_plugin.name)
41 with EnabledAuthPlugin(auth_plugin):
41 with EnabledAuthPlugin(auth_plugin):
42 response = app.get(url)
42 response = app.get(url)
43 assert response.status_code == 200
43 assert response.status_code == 200
44
44
45 def test_plugin_settings_view_post(self, app, auth_plugin, csrf_token):
45 def test_plugin_settings_view_post(self, app, auth_plugin, csrf_token):
46 url = '{prefix}/auth/{name}'.format(
46 url = '{prefix}/auth/{name}'.format(
47 prefix=ADMIN_PREFIX,
47 prefix=ADMIN_PREFIX,
48 name=auth_plugin.name)
48 name=auth_plugin.name)
49 params = {
49 params = {
50 'enabled': True,
50 'enabled': True,
51 'cache_ttl': 0,
51 'cache_ttl': 0,
52 'csrf_token': csrf_token,
52 'csrf_token': csrf_token,
53 }
53 }
54 with EnabledAuthPlugin(auth_plugin):
54 with EnabledAuthPlugin(auth_plugin):
55 response = app.post(url, params=params)
55 response = app.post(url, params=params)
56 assert response.status_code in [200, 302]
56 assert response.status_code in [200, 302]
57
57
58 def test_plugin_settings_view_get_404(self, app, auth_plugin):
58 def test_plugin_settings_view_get_404(self, app, auth_plugin):
59 url = '{prefix}/auth/{name}'.format(
59 url = '{prefix}/auth/{name}'.format(
60 prefix=ADMIN_PREFIX,
60 prefix=ADMIN_PREFIX,
61 name=auth_plugin.name)
61 name=auth_plugin.name)
62 with DisabledAuthPlugin(auth_plugin):
62 with DisabledAuthPlugin(auth_plugin):
63 response = app.get(url, status=404)
63 response = app.get(url, status=404)
64 assert response.status_code == 404
64 assert response.status_code == 404
65
65
66 def test_plugin_settings_view_post_404(self, app, auth_plugin, csrf_token):
66 def test_plugin_settings_view_post_404(self, app, auth_plugin, csrf_token):
67 url = '{prefix}/auth/{name}'.format(
67 url = '{prefix}/auth/{name}'.format(
68 prefix=ADMIN_PREFIX,
68 prefix=ADMIN_PREFIX,
69 name=auth_plugin.name)
69 name=auth_plugin.name)
70 params = {
70 params = {
71 'enabled': True,
71 'enabled': True,
72 'cache_ttl': 0,
72 'cache_ttl': 0,
73 'csrf_token': csrf_token,
73 'csrf_token': csrf_token,
74 }
74 }
75 with DisabledAuthPlugin(auth_plugin):
75 with DisabledAuthPlugin(auth_plugin):
76 response = app.post(url, params=params, status=404)
76 response = app.post(url, params=params, status=404)
77 assert response.status_code == 404
77 assert response.status_code == 404
@@ -1,180 +1,180 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import formencode.htmlfill
22 import formencode.htmlfill
23 import logging
23 import logging
24
24
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import BaseAppView
29 from rhodecode.apps._base import BaseAppView
30 from rhodecode.authentication.base import get_authn_registry
30 from rhodecode.authentication.base import get_authn_registry
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
33 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
33 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 from rhodecode.model.forms import AuthSettingsForm
34 from rhodecode.model.forms import AuthSettingsForm
35 from rhodecode.model.meta import Session
35 from rhodecode.model.meta import Session
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class AuthnPluginViewBase(BaseAppView):
41 class AuthnPluginViewBase(BaseAppView):
42
42
43 def load_default_context(self):
43 def load_default_context(self):
44 c = self._get_local_tmpl_context()
44 c = self._get_local_tmpl_context()
45 self.plugin = self.context.plugin
45 self.plugin = self.context.plugin
46 return c
46 return c
47
47
48 @LoginRequired()
48 @LoginRequired()
49 @HasPermissionAllDecorator('hg.admin')
49 @HasPermissionAllDecorator('hg.admin')
50 def settings_get(self, defaults=None, errors=None):
50 def settings_get(self, defaults=None, errors=None):
51 """
51 """
52 View that displays the plugin settings as a form.
52 View that displays the plugin settings as a form.
53 """
53 """
54 c = self.load_default_context()
54 c = self.load_default_context()
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)
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 self._get_template_context(c, **template_context)
73 return self._get_template_context(c, **template_context)
74
74
75 @LoginRequired()
75 @LoginRequired()
76 @HasPermissionAllDecorator('hg.admin')
76 @HasPermissionAllDecorator('hg.admin')
77 @CSRFRequired()
77 @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 _ = self.request.translate
82 _ = self.request.translate
83 self.load_default_context()
83 self.load_default_context()
84 schema = self.plugin.get_settings_schema()
84 schema = self.plugin.get_settings_schema()
85 data = self.request.params
85 data = self.request.params
86
86
87 try:
87 try:
88 valid_data = schema.deserialize(data)
88 valid_data = schema.deserialize(data)
89 except colander.Invalid as e:
89 except colander.Invalid as e:
90 # Display error message and display form again.
90 # Display error message and display form again.
91 h.flash(
91 h.flash(
92 _('Errors exist when saving plugin settings. '
92 _('Errors exist when saving plugin settings. '
93 'Please check the form inputs.'),
93 'Please check the form inputs.'),
94 category='error')
94 category='error')
95 defaults = {key: data[key] for key in data if key in schema}
95 defaults = {key: data[key] for key in data if key in schema}
96 return self.settings_get(errors=e.asdict(), defaults=defaults)
96 return self.settings_get(errors=e.asdict(), defaults=defaults)
97
97
98 # Store validated data.
98 # Store validated data.
99 for name, value in valid_data.items():
99 for name, value in valid_data.items():
100 self.plugin.create_or_update_setting(name, value)
100 self.plugin.create_or_update_setting(name, value)
101 Session().commit()
101 Session().commit()
102 SettingsModel().invalidate_settings_cache()
102 SettingsModel().invalidate_settings_cache()
103
103
104 # Display success message and redirect.
104 # Display success message and redirect.
105 h.flash(_('Auth settings updated successfully.'), category='success')
105 h.flash(_('Auth settings updated successfully.'), category='success')
106 redirect_to = self.request.resource_path(self.context, route_name='auth_home')
106 redirect_to = self.request.resource_path(self.context, route_name='auth_home')
107
107
108 return HTTPFound(redirect_to)
108 return HTTPFound(redirect_to)
109
109
110
110
111 class AuthSettingsView(BaseAppView):
111 class AuthSettingsView(BaseAppView):
112 def load_default_context(self):
112 def load_default_context(self):
113 c = self._get_local_tmpl_context()
113 c = self._get_local_tmpl_context()
114 return c
114 return c
115
115
116 @LoginRequired()
116 @LoginRequired()
117 @HasPermissionAllDecorator('hg.admin')
117 @HasPermissionAllDecorator('hg.admin')
118 def index(self, defaults=None, errors=None, prefix_error=False):
118 def index(self, defaults=None, errors=None, prefix_error=False):
119 c = self.load_default_context()
119 c = self.load_default_context()
120
120
121 defaults = defaults or {}
121 defaults = defaults or {}
122 authn_registry = get_authn_registry(self.request.registry)
122 authn_registry = get_authn_registry(self.request.registry)
123 enabled_plugins = SettingsModel().get_auth_plugins()
123 enabled_plugins = SettingsModel().get_auth_plugins()
124
124
125 # Create template context and render it.
125 # Create template context and render it.
126 template_context = {
126 template_context = {
127 'resource': self.context,
127 'resource': self.context,
128 'available_plugins': authn_registry.get_plugins(),
128 'available_plugins': authn_registry.get_plugins(),
129 'enabled_plugins': enabled_plugins,
129 'enabled_plugins': enabled_plugins,
130 }
130 }
131 html = render('rhodecode:templates/admin/auth/auth_settings.mako',
131 html = render('rhodecode:templates/admin/auth/auth_settings.mako',
132 self._get_template_context(c, **template_context),
132 self._get_template_context(c, **template_context),
133 self.request)
133 self.request)
134
134
135 # Create form default values and fill the form.
135 # Create form default values and fill the form.
136 form_defaults = {
136 form_defaults = {
137 'auth_plugins': ',\n'.join(enabled_plugins)
137 'auth_plugins': ',\n'.join(enabled_plugins)
138 }
138 }
139 form_defaults.update(defaults)
139 form_defaults.update(defaults)
140 html = formencode.htmlfill.render(
140 html = formencode.htmlfill.render(
141 html,
141 html,
142 defaults=form_defaults,
142 defaults=form_defaults,
143 errors=errors,
143 errors=errors,
144 prefix_error=prefix_error,
144 prefix_error=prefix_error,
145 encoding="UTF-8",
145 encoding="UTF-8",
146 force_defaults=False)
146 force_defaults=False)
147
147
148 return Response(html)
148 return Response(html)
149
149
150 @LoginRequired()
150 @LoginRequired()
151 @HasPermissionAllDecorator('hg.admin')
151 @HasPermissionAllDecorator('hg.admin')
152 @CSRFRequired()
152 @CSRFRequired()
153 def auth_settings(self):
153 def auth_settings(self):
154 _ = self.request.translate
154 _ = self.request.translate
155 try:
155 try:
156 form = AuthSettingsForm(self.request.translate)()
156 form = AuthSettingsForm(self.request.translate)()
157 form_result = form.to_python(self.request.POST)
157 form_result = form.to_python(self.request.POST)
158 plugins = ','.join(form_result['auth_plugins'])
158 plugins = ','.join(form_result['auth_plugins'])
159 setting = SettingsModel().create_or_update_setting(
159 setting = SettingsModel().create_or_update_setting(
160 'auth_plugins', plugins)
160 'auth_plugins', plugins)
161 Session().add(setting)
161 Session().add(setting)
162 Session().commit()
162 Session().commit()
163 SettingsModel().invalidate_settings_cache()
163 SettingsModel().invalidate_settings_cache()
164 h.flash(_('Auth settings updated successfully.'), category='success')
164 h.flash(_('Auth settings updated successfully.'), category='success')
165 except formencode.Invalid as errors:
165 except formencode.Invalid as errors:
166 e = errors.error_dict or {}
166 e = errors.error_dict or {}
167 h.flash(_('Errors exist when saving plugin setting. '
167 h.flash(_('Errors exist when saving plugin setting. '
168 'Please check the form inputs.'), category='error')
168 'Please check the form inputs.'), category='error')
169 return self.index(
169 return self.index(
170 defaults=errors.value,
170 defaults=errors.value,
171 errors=e,
171 errors=e,
172 prefix_error=False)
172 prefix_error=False)
173 except Exception:
173 except Exception:
174 log.exception('Exception in auth_settings')
174 log.exception('Exception in auth_settings')
175 h.flash(_('Error occurred during update of auth settings.'),
175 h.flash(_('Error occurred during update of auth settings.'),
176 category='error')
176 category='error')
177
177
178 redirect_to = self.request.resource_path(self.context, route_name='auth_home')
178 redirect_to = self.request.resource_path(self.context, route_name='auth_home')
179
179
180 return HTTPFound(redirect_to)
180 return HTTPFound(redirect_to)
General Comments 0
You need to be logged in to leave comments. Login now