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