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