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