##// END OF EJS Templates
auth-plugins: expose docs and icon methods for authentication.
marcink -
r3232:683a85b0 default
parent child Browse files
Show More
@@ -1,763 +1,777 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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
41 from rhodecode.lib.exceptions import LdapConnectionError
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.user import UserModel
45 from rhodecode.model.user import UserModel
46 from rhodecode.model.user_group import UserGroupModel
46 from rhodecode.model.user_group import UserGroupModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51 # auth types that authenticate() function can receive
51 # auth types that authenticate() function can receive
52 VCS_TYPE = 'vcs'
52 VCS_TYPE = 'vcs'
53 HTTP_TYPE = 'http'
53 HTTP_TYPE = 'http'
54
54
55
55
56 class hybrid_property(object):
56 class hybrid_property(object):
57 """
57 """
58 a property decorator that works both for instance and class
58 a property decorator that works both for instance and class
59 """
59 """
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 self.fget = fget
61 self.fget = fget
62 self.fset = fset
62 self.fset = fset
63 self.fdel = fdel
63 self.fdel = fdel
64 self.expr = expr or fget
64 self.expr = expr or fget
65 functools.update_wrapper(self, fget)
65 functools.update_wrapper(self, fget)
66
66
67 def __get__(self, instance, owner):
67 def __get__(self, instance, owner):
68 if instance is None:
68 if instance is None:
69 return self.expr(owner)
69 return self.expr(owner)
70 else:
70 else:
71 return self.fget(instance)
71 return self.fget(instance)
72
72
73 def __set__(self, instance, value):
73 def __set__(self, instance, value):
74 self.fset(instance, value)
74 self.fset(instance, value)
75
75
76 def __delete__(self, instance):
76 def __delete__(self, instance):
77 self.fdel(instance)
77 self.fdel(instance)
78
78
79
79
80 class LazyFormencode(object):
80 class LazyFormencode(object):
81 def __init__(self, formencode_obj, *args, **kwargs):
81 def __init__(self, formencode_obj, *args, **kwargs):
82 self.formencode_obj = formencode_obj
82 self.formencode_obj = formencode_obj
83 self.args = args
83 self.args = args
84 self.kwargs = kwargs
84 self.kwargs = kwargs
85
85
86 def __call__(self, *args, **kwargs):
86 def __call__(self, *args, **kwargs):
87 from inspect import isfunction
87 from inspect import isfunction
88 formencode_obj = self.formencode_obj
88 formencode_obj = self.formencode_obj
89 if isfunction(formencode_obj):
89 if isfunction(formencode_obj):
90 # case we wrap validators into functions
90 # case we wrap validators into functions
91 formencode_obj = self.formencode_obj(*args, **kwargs)
91 formencode_obj = self.formencode_obj(*args, **kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
93
93
94
94
95 class RhodeCodeAuthPluginBase(object):
95 class RhodeCodeAuthPluginBase(object):
96 # cache the authentication request for N amount of seconds. Some kind
96 # cache the authentication request for N amount of seconds. Some kind
97 # of authentication methods are very heavy and it's very efficient to cache
97 # of authentication methods are very heavy and it's very efficient to cache
98 # the result of a call. If it's set to None (default) cache is off
98 # the result of a call. If it's set to None (default) cache is off
99 AUTH_CACHE_TTL = None
99 AUTH_CACHE_TTL = None
100 AUTH_CACHE = {}
100 AUTH_CACHE = {}
101
101
102 auth_func_attrs = {
102 auth_func_attrs = {
103 "username": "unique username",
103 "username": "unique username",
104 "firstname": "first name",
104 "firstname": "first name",
105 "lastname": "last name",
105 "lastname": "last name",
106 "email": "email address",
106 "email": "email address",
107 "groups": '["list", "of", "groups"]',
107 "groups": '["list", "of", "groups"]',
108 "user_group_sync":
108 "user_group_sync":
109 'True|False defines if returned user groups should be synced',
109 'True|False defines if returned user groups should be synced',
110 "extern_name": "name in external source of record",
110 "extern_name": "name in external source of record",
111 "extern_type": "type of external source of record",
111 "extern_type": "type of external source of record",
112 "admin": 'True|False defines if user should be RhodeCode super admin',
112 "admin": 'True|False defines if user should be RhodeCode super admin',
113 "active":
113 "active":
114 'True|False defines active state of user internally for RhodeCode',
114 'True|False defines active state of user internally for RhodeCode',
115 "active_from_extern":
115 "active_from_extern":
116 "True|False\None, active state from the external auth, "
116 "True|False\None, active state from the external auth, "
117 "None means use definition from RhodeCode extern_type active value"
117 "None means use definition from RhodeCode extern_type active value"
118
118
119 }
119 }
120 # set on authenticate() method and via set_auth_type func.
120 # set on authenticate() method and via set_auth_type func.
121 auth_type = None
121 auth_type = None
122
122
123 # set on authenticate() method and via set_calling_scope_repo, this is a
123 # set on authenticate() method and via set_calling_scope_repo, this is a
124 # calling scope repository when doing authentication most likely on VCS
124 # calling scope repository when doing authentication most likely on VCS
125 # operations
125 # operations
126 acl_repo_name = None
126 acl_repo_name = None
127
127
128 # List of setting names to store encrypted. Plugins may override this list
128 # List of setting names to store encrypted. Plugins may override this list
129 # to store settings encrypted.
129 # to store settings encrypted.
130 _settings_encrypted = []
130 _settings_encrypted = []
131
131
132 # Mapping of python to DB settings model types. Plugins may override or
132 # Mapping of python to DB settings model types. Plugins may override or
133 # extend this mapping.
133 # extend this mapping.
134 _settings_type_map = {
134 _settings_type_map = {
135 colander.String: 'unicode',
135 colander.String: 'unicode',
136 colander.Integer: 'int',
136 colander.Integer: 'int',
137 colander.Boolean: 'bool',
137 colander.Boolean: 'bool',
138 colander.List: 'list',
138 colander.List: 'list',
139 }
139 }
140
140
141 # list of keys in settings that are unsafe to be logged, should be passwords
141 # list of keys in settings that are unsafe to be logged, should be passwords
142 # or other crucial credentials
142 # or other crucial credentials
143 _settings_unsafe_keys = []
143 _settings_unsafe_keys = []
144
144
145 def __init__(self, plugin_id):
145 def __init__(self, plugin_id):
146 self._plugin_id = plugin_id
146 self._plugin_id = plugin_id
147
147
148 def __str__(self):
148 def __str__(self):
149 return self.get_id()
149 return self.get_id()
150
150
151 def _get_setting_full_name(self, name):
151 def _get_setting_full_name(self, name):
152 """
152 """
153 Return the full setting name used for storing values in the database.
153 Return the full setting name used for storing values in the database.
154 """
154 """
155 # TODO: johbo: Using the name here is problematic. It would be good to
155 # TODO: johbo: Using the name here is problematic. It would be good to
156 # introduce either new models in the database to hold Plugin and
156 # introduce either new models in the database to hold Plugin and
157 # PluginSetting or to use the plugin id here.
157 # PluginSetting or to use the plugin id here.
158 return 'auth_{}_{}'.format(self.name, name)
158 return 'auth_{}_{}'.format(self.name, name)
159
159
160 def _get_setting_type(self, name):
160 def _get_setting_type(self, name):
161 """
161 """
162 Return the type of a setting. This type is defined by the SettingsModel
162 Return the type of a setting. This type is defined by the SettingsModel
163 and determines how the setting is stored in DB. Optionally the suffix
163 and determines how the setting is stored in DB. Optionally the suffix
164 `.encrypted` is appended to instruct SettingsModel to store it
164 `.encrypted` is appended to instruct SettingsModel to store it
165 encrypted.
165 encrypted.
166 """
166 """
167 schema_node = self.get_settings_schema().get(name)
167 schema_node = self.get_settings_schema().get(name)
168 db_type = self._settings_type_map.get(
168 db_type = self._settings_type_map.get(
169 type(schema_node.typ), 'unicode')
169 type(schema_node.typ), 'unicode')
170 if name in self._settings_encrypted:
170 if name in self._settings_encrypted:
171 db_type = '{}.encrypted'.format(db_type)
171 db_type = '{}.encrypted'.format(db_type)
172 return db_type
172 return db_type
173
173
174 @classmethod
175 def docs(cls):
176 """
177 Defines documentation url which helps with plugin setup
178 """
179 return ''
180
181 @classmethod
182 def icon(cls):
183 """
184 Defines ICON in SVG format for authentication method
185 """
186 return ''
187
174 def is_enabled(self):
188 def is_enabled(self):
175 """
189 """
176 Returns true if this plugin is enabled. An enabled plugin can be
190 Returns true if this plugin is enabled. An enabled plugin can be
177 configured in the admin interface but it is not consulted during
191 configured in the admin interface but it is not consulted during
178 authentication.
192 authentication.
179 """
193 """
180 auth_plugins = SettingsModel().get_auth_plugins()
194 auth_plugins = SettingsModel().get_auth_plugins()
181 return self.get_id() in auth_plugins
195 return self.get_id() in auth_plugins
182
196
183 def is_active(self, plugin_cached_settings=None):
197 def is_active(self, plugin_cached_settings=None):
184 """
198 """
185 Returns true if the plugin is activated. An activated plugin is
199 Returns true if the plugin is activated. An activated plugin is
186 consulted during authentication, assumed it is also enabled.
200 consulted during authentication, assumed it is also enabled.
187 """
201 """
188 return self.get_setting_by_name(
202 return self.get_setting_by_name(
189 'enabled', plugin_cached_settings=plugin_cached_settings)
203 'enabled', plugin_cached_settings=plugin_cached_settings)
190
204
191 def get_id(self):
205 def get_id(self):
192 """
206 """
193 Returns the plugin id.
207 Returns the plugin id.
194 """
208 """
195 return self._plugin_id
209 return self._plugin_id
196
210
197 def get_display_name(self):
211 def get_display_name(self):
198 """
212 """
199 Returns a translation string for displaying purposes.
213 Returns a translation string for displaying purposes.
200 """
214 """
201 raise NotImplementedError('Not implemented in base class')
215 raise NotImplementedError('Not implemented in base class')
202
216
203 def get_settings_schema(self):
217 def get_settings_schema(self):
204 """
218 """
205 Returns a colander schema, representing the plugin settings.
219 Returns a colander schema, representing the plugin settings.
206 """
220 """
207 return AuthnPluginSettingsSchemaBase()
221 return AuthnPluginSettingsSchemaBase()
208
222
209 def get_settings(self):
223 def get_settings(self):
210 """
224 """
211 Returns the plugin settings as dictionary.
225 Returns the plugin settings as dictionary.
212 """
226 """
213 settings = {}
227 settings = {}
214 raw_settings = SettingsModel().get_all_settings()
228 raw_settings = SettingsModel().get_all_settings()
215 for node in self.get_settings_schema():
229 for node in self.get_settings_schema():
216 settings[node.name] = self.get_setting_by_name(
230 settings[node.name] = self.get_setting_by_name(
217 node.name, plugin_cached_settings=raw_settings)
231 node.name, plugin_cached_settings=raw_settings)
218 return settings
232 return settings
219
233
220 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
234 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
221 """
235 """
222 Returns a plugin setting by name.
236 Returns a plugin setting by name.
223 """
237 """
224 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
238 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
225 if plugin_cached_settings:
239 if plugin_cached_settings:
226 plugin_settings = plugin_cached_settings
240 plugin_settings = plugin_cached_settings
227 else:
241 else:
228 plugin_settings = SettingsModel().get_all_settings()
242 plugin_settings = SettingsModel().get_all_settings()
229
243
230 if full_name in plugin_settings:
244 if full_name in plugin_settings:
231 return plugin_settings[full_name]
245 return plugin_settings[full_name]
232 else:
246 else:
233 return default
247 return default
234
248
235 def create_or_update_setting(self, name, value):
249 def create_or_update_setting(self, name, value):
236 """
250 """
237 Create or update a setting for this plugin in the persistent storage.
251 Create or update a setting for this plugin in the persistent storage.
238 """
252 """
239 full_name = self._get_setting_full_name(name)
253 full_name = self._get_setting_full_name(name)
240 type_ = self._get_setting_type(name)
254 type_ = self._get_setting_type(name)
241 db_setting = SettingsModel().create_or_update_setting(
255 db_setting = SettingsModel().create_or_update_setting(
242 full_name, value, type_)
256 full_name, value, type_)
243 return db_setting.app_settings_value
257 return db_setting.app_settings_value
244
258
245 def log_safe_settings(self, settings):
259 def log_safe_settings(self, settings):
246 """
260 """
247 returns a log safe representation of settings, without any secrets
261 returns a log safe representation of settings, without any secrets
248 """
262 """
249 settings_copy = copy.deepcopy(settings)
263 settings_copy = copy.deepcopy(settings)
250 for k in self._settings_unsafe_keys:
264 for k in self._settings_unsafe_keys:
251 if k in settings_copy:
265 if k in settings_copy:
252 del settings_copy[k]
266 del settings_copy[k]
253 return settings_copy
267 return settings_copy
254
268
255 @hybrid_property
269 @hybrid_property
256 def name(self):
270 def name(self):
257 """
271 """
258 Returns the name of this authentication plugin.
272 Returns the name of this authentication plugin.
259
273
260 :returns: string
274 :returns: string
261 """
275 """
262 raise NotImplementedError("Not implemented in base class")
276 raise NotImplementedError("Not implemented in base class")
263
277
264 def get_url_slug(self):
278 def get_url_slug(self):
265 """
279 """
266 Returns a slug which should be used when constructing URLs which refer
280 Returns a slug which should be used when constructing URLs which refer
267 to this plugin. By default it returns the plugin name. If the name is
281 to this plugin. By default it returns the plugin name. If the name is
268 not suitable for using it in an URL the plugin should override this
282 not suitable for using it in an URL the plugin should override this
269 method.
283 method.
270 """
284 """
271 return self.name
285 return self.name
272
286
273 @property
287 @property
274 def is_headers_auth(self):
288 def is_headers_auth(self):
275 """
289 """
276 Returns True if this authentication plugin uses HTTP headers as
290 Returns True if this authentication plugin uses HTTP headers as
277 authentication method.
291 authentication method.
278 """
292 """
279 return False
293 return False
280
294
281 @hybrid_property
295 @hybrid_property
282 def is_container_auth(self):
296 def is_container_auth(self):
283 """
297 """
284 Deprecated method that indicates if this authentication plugin uses
298 Deprecated method that indicates if this authentication plugin uses
285 HTTP headers as authentication method.
299 HTTP headers as authentication method.
286 """
300 """
287 warnings.warn(
301 warnings.warn(
288 'Use is_headers_auth instead.', category=DeprecationWarning)
302 'Use is_headers_auth instead.', category=DeprecationWarning)
289 return self.is_headers_auth
303 return self.is_headers_auth
290
304
291 @hybrid_property
305 @hybrid_property
292 def allows_creating_users(self):
306 def allows_creating_users(self):
293 """
307 """
294 Defines if Plugin allows users to be created on-the-fly when
308 Defines if Plugin allows users to be created on-the-fly when
295 authentication is called. Controls how external plugins should behave
309 authentication is called. Controls how external plugins should behave
296 in terms if they are allowed to create new users, or not. Base plugins
310 in terms if they are allowed to create new users, or not. Base plugins
297 should not be allowed to, but External ones should be !
311 should not be allowed to, but External ones should be !
298
312
299 :return: bool
313 :return: bool
300 """
314 """
301 return False
315 return False
302
316
303 def set_auth_type(self, auth_type):
317 def set_auth_type(self, auth_type):
304 self.auth_type = auth_type
318 self.auth_type = auth_type
305
319
306 def set_calling_scope_repo(self, acl_repo_name):
320 def set_calling_scope_repo(self, acl_repo_name):
307 self.acl_repo_name = acl_repo_name
321 self.acl_repo_name = acl_repo_name
308
322
309 def allows_authentication_from(
323 def allows_authentication_from(
310 self, user, allows_non_existing_user=True,
324 self, user, allows_non_existing_user=True,
311 allowed_auth_plugins=None, allowed_auth_sources=None):
325 allowed_auth_plugins=None, allowed_auth_sources=None):
312 """
326 """
313 Checks if this authentication module should accept a request for
327 Checks if this authentication module should accept a request for
314 the current user.
328 the current user.
315
329
316 :param user: user object fetched using plugin's get_user() method.
330 :param user: user object fetched using plugin's get_user() method.
317 :param allows_non_existing_user: if True, don't allow the
331 :param allows_non_existing_user: if True, don't allow the
318 user to be empty, meaning not existing in our database
332 user to be empty, meaning not existing in our database
319 :param allowed_auth_plugins: if provided, users extern_type will be
333 :param allowed_auth_plugins: if provided, users extern_type will be
320 checked against a list of provided extern types, which are plugin
334 checked against a list of provided extern types, which are plugin
321 auth_names in the end
335 auth_names in the end
322 :param allowed_auth_sources: authentication type allowed,
336 :param allowed_auth_sources: authentication type allowed,
323 `http` or `vcs` default is both.
337 `http` or `vcs` default is both.
324 defines if plugin will accept only http authentication vcs
338 defines if plugin will accept only http authentication vcs
325 authentication(git/hg) or both
339 authentication(git/hg) or both
326 :returns: boolean
340 :returns: boolean
327 """
341 """
328 if not user and not allows_non_existing_user:
342 if not user and not allows_non_existing_user:
329 log.debug('User is empty but plugin does not allow empty users,'
343 log.debug('User is empty but plugin does not allow empty users,'
330 'not allowed to authenticate')
344 'not allowed to authenticate')
331 return False
345 return False
332
346
333 expected_auth_plugins = allowed_auth_plugins or [self.name]
347 expected_auth_plugins = allowed_auth_plugins or [self.name]
334 if user and (user.extern_type and
348 if user and (user.extern_type and
335 user.extern_type not in expected_auth_plugins):
349 user.extern_type not in expected_auth_plugins):
336 log.debug(
350 log.debug(
337 'User `%s` is bound to `%s` auth type. Plugin allows only '
351 'User `%s` is bound to `%s` auth type. Plugin allows only '
338 '%s, skipping', user, user.extern_type, expected_auth_plugins)
352 '%s, skipping', user, user.extern_type, expected_auth_plugins)
339
353
340 return False
354 return False
341
355
342 # by default accept both
356 # by default accept both
343 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
357 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
344 if self.auth_type not in expected_auth_from:
358 if self.auth_type not in expected_auth_from:
345 log.debug('Current auth source is %s but plugin only allows %s',
359 log.debug('Current auth source is %s but plugin only allows %s',
346 self.auth_type, expected_auth_from)
360 self.auth_type, expected_auth_from)
347 return False
361 return False
348
362
349 return True
363 return True
350
364
351 def get_user(self, username=None, **kwargs):
365 def get_user(self, username=None, **kwargs):
352 """
366 """
353 Helper method for user fetching in plugins, by default it's using
367 Helper method for user fetching in plugins, by default it's using
354 simple fetch by username, but this method can be custimized in plugins
368 simple fetch by username, but this method can be custimized in plugins
355 eg. headers auth plugin to fetch user by environ params
369 eg. headers auth plugin to fetch user by environ params
356
370
357 :param username: username if given to fetch from database
371 :param username: username if given to fetch from database
358 :param kwargs: extra arguments needed for user fetching.
372 :param kwargs: extra arguments needed for user fetching.
359 """
373 """
360 user = None
374 user = None
361 log.debug(
375 log.debug(
362 'Trying to fetch user `%s` from RhodeCode database', username)
376 'Trying to fetch user `%s` from RhodeCode database', username)
363 if username:
377 if username:
364 user = User.get_by_username(username)
378 user = User.get_by_username(username)
365 if not user:
379 if not user:
366 log.debug('User not found, fallback to fetch user in '
380 log.debug('User not found, fallback to fetch user in '
367 'case insensitive mode')
381 'case insensitive mode')
368 user = User.get_by_username(username, case_insensitive=True)
382 user = User.get_by_username(username, case_insensitive=True)
369 else:
383 else:
370 log.debug('provided username:`%s` is empty skipping...', username)
384 log.debug('provided username:`%s` is empty skipping...', username)
371 if not user:
385 if not user:
372 log.debug('User `%s` not found in database', username)
386 log.debug('User `%s` not found in database', username)
373 else:
387 else:
374 log.debug('Got DB user:%s', user)
388 log.debug('Got DB user:%s', user)
375 return user
389 return user
376
390
377 def user_activation_state(self):
391 def user_activation_state(self):
378 """
392 """
379 Defines user activation state when creating new users
393 Defines user activation state when creating new users
380
394
381 :returns: boolean
395 :returns: boolean
382 """
396 """
383 raise NotImplementedError("Not implemented in base class")
397 raise NotImplementedError("Not implemented in base class")
384
398
385 def auth(self, userobj, username, passwd, settings, **kwargs):
399 def auth(self, userobj, username, passwd, settings, **kwargs):
386 """
400 """
387 Given a user object (which may be null), username, a plaintext
401 Given a user object (which may be null), username, a plaintext
388 password, and a settings object (containing all the keys needed as
402 password, and a settings object (containing all the keys needed as
389 listed in settings()), authenticate this user's login attempt.
403 listed in settings()), authenticate this user's login attempt.
390
404
391 Return None on failure. On success, return a dictionary of the form:
405 Return None on failure. On success, return a dictionary of the form:
392
406
393 see: RhodeCodeAuthPluginBase.auth_func_attrs
407 see: RhodeCodeAuthPluginBase.auth_func_attrs
394 This is later validated for correctness
408 This is later validated for correctness
395 """
409 """
396 raise NotImplementedError("not implemented in base class")
410 raise NotImplementedError("not implemented in base class")
397
411
398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
412 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
399 """
413 """
400 Wrapper to call self.auth() that validates call on it
414 Wrapper to call self.auth() that validates call on it
401
415
402 :param userobj: userobj
416 :param userobj: userobj
403 :param username: username
417 :param username: username
404 :param passwd: plaintext password
418 :param passwd: plaintext password
405 :param settings: plugin settings
419 :param settings: plugin settings
406 """
420 """
407 auth = self.auth(userobj, username, passwd, settings, **kwargs)
421 auth = self.auth(userobj, username, passwd, settings, **kwargs)
408 if auth:
422 if auth:
409 auth['_plugin'] = self.name
423 auth['_plugin'] = self.name
410 auth['_ttl_cache'] = self.get_ttl_cache(settings)
424 auth['_ttl_cache'] = self.get_ttl_cache(settings)
411 # check if hash should be migrated ?
425 # check if hash should be migrated ?
412 new_hash = auth.get('_hash_migrate')
426 new_hash = auth.get('_hash_migrate')
413 if new_hash:
427 if new_hash:
414 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
428 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
415 if 'user_group_sync' not in auth:
429 if 'user_group_sync' not in auth:
416 auth['user_group_sync'] = False
430 auth['user_group_sync'] = False
417 return self._validate_auth_return(auth)
431 return self._validate_auth_return(auth)
418 return auth
432 return auth
419
433
420 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
434 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
421 new_hash_cypher = _RhodeCodeCryptoBCrypt()
435 new_hash_cypher = _RhodeCodeCryptoBCrypt()
422 # extra checks, so make sure new hash is correct.
436 # extra checks, so make sure new hash is correct.
423 password_encoded = safe_str(password)
437 password_encoded = safe_str(password)
424 if new_hash and new_hash_cypher.hash_check(
438 if new_hash and new_hash_cypher.hash_check(
425 password_encoded, new_hash):
439 password_encoded, new_hash):
426 cur_user = User.get_by_username(username)
440 cur_user = User.get_by_username(username)
427 cur_user.password = new_hash
441 cur_user.password = new_hash
428 Session().add(cur_user)
442 Session().add(cur_user)
429 Session().flush()
443 Session().flush()
430 log.info('Migrated user %s hash to bcrypt', cur_user)
444 log.info('Migrated user %s hash to bcrypt', cur_user)
431
445
432 def _validate_auth_return(self, ret):
446 def _validate_auth_return(self, ret):
433 if not isinstance(ret, dict):
447 if not isinstance(ret, dict):
434 raise Exception('returned value from auth must be a dict')
448 raise Exception('returned value from auth must be a dict')
435 for k in self.auth_func_attrs:
449 for k in self.auth_func_attrs:
436 if k not in ret:
450 if k not in ret:
437 raise Exception('Missing %s attribute from returned data' % k)
451 raise Exception('Missing %s attribute from returned data' % k)
438 return ret
452 return ret
439
453
440 def get_ttl_cache(self, settings=None):
454 def get_ttl_cache(self, settings=None):
441 plugin_settings = settings or self.get_settings()
455 plugin_settings = settings or self.get_settings()
442 # we set default to 30, we make a compromise here,
456 # we set default to 30, we make a compromise here,
443 # performance > security, mostly due to LDAP/SVN, majority
457 # performance > security, mostly due to LDAP/SVN, majority
444 # of users pick cache_ttl to be enabled
458 # of users pick cache_ttl to be enabled
445 from rhodecode.authentication import plugin_default_auth_ttl
459 from rhodecode.authentication import plugin_default_auth_ttl
446 cache_ttl = plugin_default_auth_ttl
460 cache_ttl = plugin_default_auth_ttl
447
461
448 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
462 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
449 # plugin cache set inside is more important than the settings value
463 # plugin cache set inside is more important than the settings value
450 cache_ttl = self.AUTH_CACHE_TTL
464 cache_ttl = self.AUTH_CACHE_TTL
451 elif plugin_settings.get('cache_ttl'):
465 elif plugin_settings.get('cache_ttl'):
452 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
466 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
453
467
454 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
468 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
455 return plugin_cache_active, cache_ttl
469 return plugin_cache_active, cache_ttl
456
470
457
471
458 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
472 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
459
473
460 @hybrid_property
474 @hybrid_property
461 def allows_creating_users(self):
475 def allows_creating_users(self):
462 return True
476 return True
463
477
464 def use_fake_password(self):
478 def use_fake_password(self):
465 """
479 """
466 Return a boolean that indicates whether or not we should set the user's
480 Return a boolean that indicates whether or not we should set the user's
467 password to a random value when it is authenticated by this plugin.
481 password to a random value when it is authenticated by this plugin.
468 If your plugin provides authentication, then you will generally
482 If your plugin provides authentication, then you will generally
469 want this.
483 want this.
470
484
471 :returns: boolean
485 :returns: boolean
472 """
486 """
473 raise NotImplementedError("Not implemented in base class")
487 raise NotImplementedError("Not implemented in base class")
474
488
475 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
489 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
476 # at this point _authenticate calls plugin's `auth()` function
490 # at this point _authenticate calls plugin's `auth()` function
477 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
491 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
478 userobj, username, passwd, settings, **kwargs)
492 userobj, username, passwd, settings, **kwargs)
479
493
480 if auth:
494 if auth:
481 # maybe plugin will clean the username ?
495 # maybe plugin will clean the username ?
482 # we should use the return value
496 # we should use the return value
483 username = auth['username']
497 username = auth['username']
484
498
485 # if external source tells us that user is not active, we should
499 # if external source tells us that user is not active, we should
486 # skip rest of the process. This can prevent from creating users in
500 # skip rest of the process. This can prevent from creating users in
487 # RhodeCode when using external authentication, but if it's
501 # RhodeCode when using external authentication, but if it's
488 # inactive user we shouldn't create that user anyway
502 # inactive user we shouldn't create that user anyway
489 if auth['active_from_extern'] is False:
503 if auth['active_from_extern'] is False:
490 log.warning(
504 log.warning(
491 "User %s authenticated against %s, but is inactive",
505 "User %s authenticated against %s, but is inactive",
492 username, self.__module__)
506 username, self.__module__)
493 return None
507 return None
494
508
495 cur_user = User.get_by_username(username, case_insensitive=True)
509 cur_user = User.get_by_username(username, case_insensitive=True)
496 is_user_existing = cur_user is not None
510 is_user_existing = cur_user is not None
497
511
498 if is_user_existing:
512 if is_user_existing:
499 log.debug('Syncing user `%s` from '
513 log.debug('Syncing user `%s` from '
500 '`%s` plugin', username, self.name)
514 '`%s` plugin', username, self.name)
501 else:
515 else:
502 log.debug('Creating non existing user `%s` from '
516 log.debug('Creating non existing user `%s` from '
503 '`%s` plugin', username, self.name)
517 '`%s` plugin', username, self.name)
504
518
505 if self.allows_creating_users:
519 if self.allows_creating_users:
506 log.debug('Plugin `%s` allows to '
520 log.debug('Plugin `%s` allows to '
507 'create new users', self.name)
521 'create new users', self.name)
508 else:
522 else:
509 log.debug('Plugin `%s` does not allow to '
523 log.debug('Plugin `%s` does not allow to '
510 'create new users', self.name)
524 'create new users', self.name)
511
525
512 user_parameters = {
526 user_parameters = {
513 'username': username,
527 'username': username,
514 'email': auth["email"],
528 'email': auth["email"],
515 'firstname': auth["firstname"],
529 'firstname': auth["firstname"],
516 'lastname': auth["lastname"],
530 'lastname': auth["lastname"],
517 'active': auth["active"],
531 'active': auth["active"],
518 'admin': auth["admin"],
532 'admin': auth["admin"],
519 'extern_name': auth["extern_name"],
533 'extern_name': auth["extern_name"],
520 'extern_type': self.name,
534 'extern_type': self.name,
521 'plugin': self,
535 'plugin': self,
522 'allow_to_create_user': self.allows_creating_users,
536 'allow_to_create_user': self.allows_creating_users,
523 }
537 }
524
538
525 if not is_user_existing:
539 if not is_user_existing:
526 if self.use_fake_password():
540 if self.use_fake_password():
527 # Randomize the PW because we don't need it, but don't want
541 # Randomize the PW because we don't need it, but don't want
528 # them blank either
542 # them blank either
529 passwd = PasswordGenerator().gen_password(length=16)
543 passwd = PasswordGenerator().gen_password(length=16)
530 user_parameters['password'] = passwd
544 user_parameters['password'] = passwd
531 else:
545 else:
532 # Since the password is required by create_or_update method of
546 # Since the password is required by create_or_update method of
533 # UserModel, we need to set it explicitly.
547 # UserModel, we need to set it explicitly.
534 # The create_or_update method is smart and recognises the
548 # The create_or_update method is smart and recognises the
535 # password hashes as well.
549 # password hashes as well.
536 user_parameters['password'] = cur_user.password
550 user_parameters['password'] = cur_user.password
537
551
538 # we either create or update users, we also pass the flag
552 # we either create or update users, we also pass the flag
539 # that controls if this method can actually do that.
553 # that controls if this method can actually do that.
540 # raises NotAllowedToCreateUserError if it cannot, and we try to.
554 # raises NotAllowedToCreateUserError if it cannot, and we try to.
541 user = UserModel().create_or_update(**user_parameters)
555 user = UserModel().create_or_update(**user_parameters)
542 Session().flush()
556 Session().flush()
543 # enforce user is just in given groups, all of them has to be ones
557 # enforce user is just in given groups, all of them has to be ones
544 # created from plugins. We store this info in _group_data JSON
558 # created from plugins. We store this info in _group_data JSON
545 # field
559 # field
546
560
547 if auth['user_group_sync']:
561 if auth['user_group_sync']:
548 try:
562 try:
549 groups = auth['groups'] or []
563 groups = auth['groups'] or []
550 log.debug(
564 log.debug(
551 'Performing user_group sync based on set `%s` '
565 'Performing user_group sync based on set `%s` '
552 'returned by `%s` plugin', groups, self.name)
566 'returned by `%s` plugin', groups, self.name)
553 UserGroupModel().enforce_groups(user, groups, self.name)
567 UserGroupModel().enforce_groups(user, groups, self.name)
554 except Exception:
568 except Exception:
555 # for any reason group syncing fails, we should
569 # for any reason group syncing fails, we should
556 # proceed with login
570 # proceed with login
557 log.error(traceback.format_exc())
571 log.error(traceback.format_exc())
558
572
559 Session().commit()
573 Session().commit()
560 return auth
574 return auth
561
575
562
576
563 class AuthLdapBase(object):
577 class AuthLdapBase(object):
564
578
565 @classmethod
579 @classmethod
566 def _build_servers(cls, ldap_server_type, ldap_server, port):
580 def _build_servers(cls, ldap_server_type, ldap_server, port):
567 def host_resolver(host, port, full_resolve=True):
581 def host_resolver(host, port, full_resolve=True):
568 """
582 """
569 Main work for this function is to prevent ldap connection issues,
583 Main work for this function is to prevent ldap connection issues,
570 and detect them early using a "greenified" sockets
584 and detect them early using a "greenified" sockets
571 """
585 """
572 host = host.strip()
586 host = host.strip()
573 if not full_resolve:
587 if not full_resolve:
574 return '{}:{}'.format(host, port)
588 return '{}:{}'.format(host, port)
575
589
576 log.debug('LDAP: Resolving IP for LDAP host %s', host)
590 log.debug('LDAP: Resolving IP for LDAP host %s', host)
577 try:
591 try:
578 ip = socket.gethostbyname(host)
592 ip = socket.gethostbyname(host)
579 log.debug('Got LDAP server %s ip %s', host, ip)
593 log.debug('Got LDAP server %s ip %s', host, ip)
580 except Exception:
594 except Exception:
581 raise LdapConnectionError(
595 raise LdapConnectionError(
582 'Failed to resolve host: `{}`'.format(host))
596 'Failed to resolve host: `{}`'.format(host))
583
597
584 log.debug('LDAP: Checking if IP %s is accessible', ip)
598 log.debug('LDAP: Checking if IP %s is accessible', ip)
585 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
599 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
586 try:
600 try:
587 s.connect((ip, int(port)))
601 s.connect((ip, int(port)))
588 s.shutdown(socket.SHUT_RD)
602 s.shutdown(socket.SHUT_RD)
589 except Exception:
603 except Exception:
590 raise LdapConnectionError(
604 raise LdapConnectionError(
591 'Failed to connect to host: `{}:{}`'.format(host, port))
605 'Failed to connect to host: `{}:{}`'.format(host, port))
592
606
593 return '{}:{}'.format(host, port)
607 return '{}:{}'.format(host, port)
594
608
595 if len(ldap_server) == 1:
609 if len(ldap_server) == 1:
596 # in case of single server use resolver to detect potential
610 # in case of single server use resolver to detect potential
597 # connection issues
611 # connection issues
598 full_resolve = True
612 full_resolve = True
599 else:
613 else:
600 full_resolve = False
614 full_resolve = False
601
615
602 return ', '.join(
616 return ', '.join(
603 ["{}://{}".format(
617 ["{}://{}".format(
604 ldap_server_type,
618 ldap_server_type,
605 host_resolver(host, port, full_resolve=full_resolve))
619 host_resolver(host, port, full_resolve=full_resolve))
606 for host in ldap_server])
620 for host in ldap_server])
607
621
608 @classmethod
622 @classmethod
609 def _get_server_list(cls, servers):
623 def _get_server_list(cls, servers):
610 return map(string.strip, servers.split(','))
624 return map(string.strip, servers.split(','))
611
625
612 @classmethod
626 @classmethod
613 def get_uid(cls, username, server_addresses):
627 def get_uid(cls, username, server_addresses):
614 uid = username
628 uid = username
615 for server_addr in server_addresses:
629 for server_addr in server_addresses:
616 uid = chop_at(username, "@%s" % server_addr)
630 uid = chop_at(username, "@%s" % server_addr)
617 return uid
631 return uid
618
632
619
633
620 def loadplugin(plugin_id):
634 def loadplugin(plugin_id):
621 """
635 """
622 Loads and returns an instantiated authentication plugin.
636 Loads and returns an instantiated authentication plugin.
623 Returns the RhodeCodeAuthPluginBase subclass on success,
637 Returns the RhodeCodeAuthPluginBase subclass on success,
624 or None on failure.
638 or None on failure.
625 """
639 """
626 # TODO: Disusing pyramids thread locals to retrieve the registry.
640 # TODO: Disusing pyramids thread locals to retrieve the registry.
627 authn_registry = get_authn_registry()
641 authn_registry = get_authn_registry()
628 plugin = authn_registry.get_plugin(plugin_id)
642 plugin = authn_registry.get_plugin(plugin_id)
629 if plugin is None:
643 if plugin is None:
630 log.error('Authentication plugin not found: "%s"', plugin_id)
644 log.error('Authentication plugin not found: "%s"', plugin_id)
631 return plugin
645 return plugin
632
646
633
647
634 def get_authn_registry(registry=None):
648 def get_authn_registry(registry=None):
635 registry = registry or get_current_registry()
649 registry = registry or get_current_registry()
636 authn_registry = registry.getUtility(IAuthnPluginRegistry)
650 authn_registry = registry.getUtility(IAuthnPluginRegistry)
637 return authn_registry
651 return authn_registry
638
652
639
653
640 def authenticate(username, password, environ=None, auth_type=None,
654 def authenticate(username, password, environ=None, auth_type=None,
641 skip_missing=False, registry=None, acl_repo_name=None):
655 skip_missing=False, registry=None, acl_repo_name=None):
642 """
656 """
643 Authentication function used for access control,
657 Authentication function used for access control,
644 It tries to authenticate based on enabled authentication modules.
658 It tries to authenticate based on enabled authentication modules.
645
659
646 :param username: username can be empty for headers auth
660 :param username: username can be empty for headers auth
647 :param password: password can be empty for headers auth
661 :param password: password can be empty for headers auth
648 :param environ: environ headers passed for headers auth
662 :param environ: environ headers passed for headers auth
649 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
663 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
650 :param skip_missing: ignores plugins that are in db but not in environment
664 :param skip_missing: ignores plugins that are in db but not in environment
651 :returns: None if auth failed, plugin_user dict if auth is correct
665 :returns: None if auth failed, plugin_user dict if auth is correct
652 """
666 """
653 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
667 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
654 raise ValueError('auth type must be on of http, vcs got "%s" instead'
668 raise ValueError('auth type must be on of http, vcs got "%s" instead'
655 % auth_type)
669 % auth_type)
656 headers_only = environ and not (username and password)
670 headers_only = environ and not (username and password)
657
671
658 authn_registry = get_authn_registry(registry)
672 authn_registry = get_authn_registry(registry)
659 plugins_to_check = authn_registry.get_plugins_for_authentication()
673 plugins_to_check = authn_registry.get_plugins_for_authentication()
660 log.debug('Starting ordered authentication chain using %s plugins',
674 log.debug('Starting ordered authentication chain using %s plugins',
661 [x.name for x in plugins_to_check])
675 [x.name for x in plugins_to_check])
662 for plugin in plugins_to_check:
676 for plugin in plugins_to_check:
663 plugin.set_auth_type(auth_type)
677 plugin.set_auth_type(auth_type)
664 plugin.set_calling_scope_repo(acl_repo_name)
678 plugin.set_calling_scope_repo(acl_repo_name)
665
679
666 if headers_only and not plugin.is_headers_auth:
680 if headers_only and not plugin.is_headers_auth:
667 log.debug('Auth type is for headers only and plugin `%s` is not '
681 log.debug('Auth type is for headers only and plugin `%s` is not '
668 'headers plugin, skipping...', plugin.get_id())
682 'headers plugin, skipping...', plugin.get_id())
669 continue
683 continue
670
684
671 log.debug('Trying authentication using ** %s **', plugin.get_id())
685 log.debug('Trying authentication using ** %s **', plugin.get_id())
672
686
673 # load plugin settings from RhodeCode database
687 # load plugin settings from RhodeCode database
674 plugin_settings = plugin.get_settings()
688 plugin_settings = plugin.get_settings()
675 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
689 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
676 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
690 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
677
691
678 # use plugin's method of user extraction.
692 # use plugin's method of user extraction.
679 user = plugin.get_user(username, environ=environ,
693 user = plugin.get_user(username, environ=environ,
680 settings=plugin_settings)
694 settings=plugin_settings)
681 display_user = user.username if user else username
695 display_user = user.username if user else username
682 log.debug(
696 log.debug(
683 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
697 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
684
698
685 if not plugin.allows_authentication_from(user):
699 if not plugin.allows_authentication_from(user):
686 log.debug('Plugin %s does not accept user `%s` for authentication',
700 log.debug('Plugin %s does not accept user `%s` for authentication',
687 plugin.get_id(), display_user)
701 plugin.get_id(), display_user)
688 continue
702 continue
689 else:
703 else:
690 log.debug('Plugin %s accepted user `%s` for authentication',
704 log.debug('Plugin %s accepted user `%s` for authentication',
691 plugin.get_id(), display_user)
705 plugin.get_id(), display_user)
692
706
693 log.info('Authenticating user `%s` using %s plugin',
707 log.info('Authenticating user `%s` using %s plugin',
694 display_user, plugin.get_id())
708 display_user, plugin.get_id())
695
709
696 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
710 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
697
711
698 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
712 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
699 plugin.get_id(), plugin_cache_active, cache_ttl)
713 plugin.get_id(), plugin_cache_active, cache_ttl)
700
714
701 user_id = user.user_id if user else None
715 user_id = user.user_id if user else None
702 # don't cache for empty users
716 # don't cache for empty users
703 plugin_cache_active = plugin_cache_active and user_id
717 plugin_cache_active = plugin_cache_active and user_id
704 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
718 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
705 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
719 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
706
720
707 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
721 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
708 expiration_time=cache_ttl,
722 expiration_time=cache_ttl,
709 condition=plugin_cache_active)
723 condition=plugin_cache_active)
710 def compute_auth(
724 def compute_auth(
711 cache_name, plugin_name, username, password):
725 cache_name, plugin_name, username, password):
712
726
713 # _authenticate is a wrapper for .auth() method of plugin.
727 # _authenticate is a wrapper for .auth() method of plugin.
714 # it checks if .auth() sends proper data.
728 # it checks if .auth() sends proper data.
715 # For RhodeCodeExternalAuthPlugin it also maps users to
729 # For RhodeCodeExternalAuthPlugin it also maps users to
716 # Database and maps the attributes returned from .auth()
730 # Database and maps the attributes returned from .auth()
717 # to RhodeCode database. If this function returns data
731 # to RhodeCode database. If this function returns data
718 # then auth is correct.
732 # then auth is correct.
719 log.debug('Running plugin `%s` _authenticate method '
733 log.debug('Running plugin `%s` _authenticate method '
720 'using username and password', plugin.get_id())
734 'using username and password', plugin.get_id())
721 return plugin._authenticate(
735 return plugin._authenticate(
722 user, username, password, plugin_settings,
736 user, username, password, plugin_settings,
723 environ=environ or {})
737 environ=environ or {})
724
738
725 start = time.time()
739 start = time.time()
726 # for environ based auth, password can be empty, but then the validation is
740 # for environ based auth, password can be empty, but then the validation is
727 # on the server that fills in the env data needed for authentication
741 # on the server that fills in the env data needed for authentication
728 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
742 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
729
743
730 auth_time = time.time() - start
744 auth_time = time.time() - start
731 log.debug('Authentication for plugin `%s` completed in %.3fs, '
745 log.debug('Authentication for plugin `%s` completed in %.3fs, '
732 'expiration time of fetched cache %.1fs.',
746 'expiration time of fetched cache %.1fs.',
733 plugin.get_id(), auth_time, cache_ttl)
747 plugin.get_id(), auth_time, cache_ttl)
734
748
735 log.debug('PLUGIN USER DATA: %s', plugin_user)
749 log.debug('PLUGIN USER DATA: %s', plugin_user)
736
750
737 if plugin_user:
751 if plugin_user:
738 log.debug('Plugin returned proper authentication data')
752 log.debug('Plugin returned proper authentication data')
739 return plugin_user
753 return plugin_user
740 # we failed to Auth because .auth() method didn't return proper user
754 # we failed to Auth because .auth() method didn't return proper user
741 log.debug("User `%s` failed to authenticate against %s",
755 log.debug("User `%s` failed to authenticate against %s",
742 display_user, plugin.get_id())
756 display_user, plugin.get_id())
743
757
744 # case when we failed to authenticate against all defined plugins
758 # case when we failed to authenticate against all defined plugins
745 return None
759 return None
746
760
747
761
748 def chop_at(s, sub, inclusive=False):
762 def chop_at(s, sub, inclusive=False):
749 """Truncate string ``s`` at the first occurrence of ``sub``.
763 """Truncate string ``s`` at the first occurrence of ``sub``.
750
764
751 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
765 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
752
766
753 >>> chop_at("plutocratic brats", "rat")
767 >>> chop_at("plutocratic brats", "rat")
754 'plutoc'
768 'plutoc'
755 >>> chop_at("plutocratic brats", "rat", True)
769 >>> chop_at("plutocratic brats", "rat", True)
756 'plutocrat'
770 'plutocrat'
757 """
771 """
758 pos = s.find(sub)
772 pos = s.find(sub)
759 if pos == -1:
773 if pos == -1:
760 return s
774 return s
761 if inclusive:
775 if inclusive:
762 return s[:pos+len(sub)]
776 return s[:pos+len(sub)]
763 return s[:pos]
777 return s[:pos]
@@ -1,285 +1,289 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
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 rdoc = self.opener.open(request)
156 rdoc = self.opener.open(request)
157 msg = "".join(rdoc.readlines())
157 msg = "".join(rdoc.readlines())
158 if not msg and empty_response_ok:
158 if not msg and empty_response_ok:
159 rval = {}
159 rval = {}
160 rval["status"] = True
160 rval["status"] = True
161 rval["error"] = "Response body was empty"
161 rval["error"] = "Response body was empty"
162 elif not noformat:
162 elif not noformat:
163 rval = json.loads(msg)
163 rval = json.loads(msg)
164 rval["status"] = True
164 rval["status"] = True
165 else:
165 else:
166 rval = "".join(rdoc.readlines())
166 rval = "".join(rdoc.readlines())
167 except Exception as e:
167 except Exception as e:
168 if not noformat:
168 if not noformat:
169 rval = {"status": False,
169 rval = {"status": False,
170 "body": body,
170 "body": body,
171 "error": str(e) + "\n" + msg}
171 "error": str(e) + "\n" + msg}
172 else:
172 else:
173 rval = None
173 rval = None
174 return rval
174 return rval
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 _settings_unsafe_keys = ['app_password']
192 _settings_unsafe_keys = ['app_password']
193
193
194 def includeme(self, config):
194 def includeme(self, config):
195 config.add_authn_plugin(self)
195 config.add_authn_plugin(self)
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_view(
197 config.add_view(
198 'rhodecode.authentication.views.AuthnPluginViewBase',
198 'rhodecode.authentication.views.AuthnPluginViewBase',
199 attr='settings_get',
199 attr='settings_get',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 request_method='GET',
201 request_method='GET',
202 route_name='auth_home',
202 route_name='auth_home',
203 context=CrowdAuthnResource)
203 context=CrowdAuthnResource)
204 config.add_view(
204 config.add_view(
205 'rhodecode.authentication.views.AuthnPluginViewBase',
205 'rhodecode.authentication.views.AuthnPluginViewBase',
206 attr='settings_post',
206 attr='settings_post',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 request_method='POST',
208 request_method='POST',
209 route_name='auth_home',
209 route_name='auth_home',
210 context=CrowdAuthnResource)
210 context=CrowdAuthnResource)
211
211
212 def get_settings_schema(self):
212 def get_settings_schema(self):
213 return CrowdSettingsSchema()
213 return CrowdSettingsSchema()
214
214
215 def get_display_name(self):
215 def get_display_name(self):
216 return _('CROWD')
216 return _('CROWD')
217
217
218 @classmethod
219 def docs(cls):
220 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
221
218 @hybrid_property
222 @hybrid_property
219 def name(self):
223 def name(self):
220 return "crowd"
224 return "crowd"
221
225
222 def use_fake_password(self):
226 def use_fake_password(self):
223 return True
227 return True
224
228
225 def user_activation_state(self):
229 def user_activation_state(self):
226 def_user_perms = User.get_default_user().AuthUser().permissions['global']
230 def_user_perms = User.get_default_user().AuthUser().permissions['global']
227 return 'hg.extern_activate.auto' in def_user_perms
231 return 'hg.extern_activate.auto' in def_user_perms
228
232
229 def auth(self, userobj, username, password, settings, **kwargs):
233 def auth(self, userobj, username, password, settings, **kwargs):
230 """
234 """
231 Given a user object (which may be null), username, a plaintext password,
235 Given a user object (which may be null), username, a plaintext password,
232 and a settings object (containing all the keys needed as listed in settings()),
236 and a settings object (containing all the keys needed as listed in settings()),
233 authenticate this user's login attempt.
237 authenticate this user's login attempt.
234
238
235 Return None on failure. On success, return a dictionary of the form:
239 Return None on failure. On success, return a dictionary of the form:
236
240
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
241 see: RhodeCodeAuthPluginBase.auth_func_attrs
238 This is later validated for correctness
242 This is later validated for correctness
239 """
243 """
240 if not username or not password:
244 if not username or not password:
241 log.debug('Empty username or password skipping...')
245 log.debug('Empty username or password skipping...')
242 return None
246 return None
243
247
244 log.debug("Crowd settings: \n%s", formatted_json(settings))
248 log.debug("Crowd settings: \n%s", formatted_json(settings))
245 server = CrowdServer(**settings)
249 server = CrowdServer(**settings)
246 server.set_credentials(settings["app_name"], settings["app_password"])
250 server.set_credentials(settings["app_name"], settings["app_password"])
247 crowd_user = server.user_auth(username, password)
251 crowd_user = server.user_auth(username, password)
248 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
252 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
249 if not crowd_user["status"]:
253 if not crowd_user["status"]:
250 return None
254 return None
251
255
252 res = server.user_groups(crowd_user["name"])
256 res = server.user_groups(crowd_user["name"])
253 log.debug("Crowd groups: \n%s", formatted_json(res))
257 log.debug("Crowd groups: \n%s", formatted_json(res))
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
258 crowd_user["groups"] = [x["name"] for x in res["groups"]]
255
259
256 # old attrs fetched from RhodeCode database
260 # old attrs fetched from RhodeCode database
257 admin = getattr(userobj, 'admin', False)
261 admin = getattr(userobj, 'admin', False)
258 active = getattr(userobj, 'active', True)
262 active = getattr(userobj, 'active', True)
259 email = getattr(userobj, 'email', '')
263 email = getattr(userobj, 'email', '')
260 username = getattr(userobj, 'username', username)
264 username = getattr(userobj, 'username', username)
261 firstname = getattr(userobj, 'firstname', '')
265 firstname = getattr(userobj, 'firstname', '')
262 lastname = getattr(userobj, 'lastname', '')
266 lastname = getattr(userobj, 'lastname', '')
263 extern_type = getattr(userobj, 'extern_type', '')
267 extern_type = getattr(userobj, 'extern_type', '')
264
268
265 user_attrs = {
269 user_attrs = {
266 'username': username,
270 'username': username,
267 'firstname': crowd_user["first-name"] or firstname,
271 'firstname': crowd_user["first-name"] or firstname,
268 'lastname': crowd_user["last-name"] or lastname,
272 'lastname': crowd_user["last-name"] or lastname,
269 'groups': crowd_user["groups"],
273 'groups': crowd_user["groups"],
270 'user_group_sync': True,
274 'user_group_sync': True,
271 'email': crowd_user["email"] or email,
275 'email': crowd_user["email"] or email,
272 'admin': admin,
276 'admin': admin,
273 'active': active,
277 'active': active,
274 'active_from_extern': crowd_user.get('active'),
278 'active_from_extern': crowd_user.get('active'),
275 'extern_name': crowd_user["name"],
279 'extern_name': crowd_user["name"],
276 'extern_type': extern_type,
280 'extern_type': extern_type,
277 }
281 }
278
282
279 # set an admin if we're in admin_groups of crowd
283 # set an admin if we're in admin_groups of crowd
280 for group in settings["admin_groups"]:
284 for group in settings["admin_groups"]:
281 if group in user_attrs["groups"]:
285 if group in user_attrs["groups"]:
282 user_attrs["admin"] = True
286 user_attrs["admin"] = True
283 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
287 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
284 log.info('user `%s` authenticated correctly', user_attrs['username'])
288 log.info('user `%s` authenticated correctly', user_attrs['username'])
285 return user_attrs
289 return user_attrs
@@ -1,513 +1,517 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 os
25 import os
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 import colander
29 import colander
30 from rhodecode.translation import _
30 from rhodecode.translation import _
31 from rhodecode.authentication.base import (
31 from rhodecode.authentication.base import (
32 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
32 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.exceptions import (
36 from rhodecode.lib.exceptions import (
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 )
38 )
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.model.validators import Missing
41 from rhodecode.model.validators import Missing
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45 try:
45 try:
46 import ldap
46 import ldap
47 except ImportError:
47 except ImportError:
48 # means that python-ldap is not installed, we use Missing object to mark
48 # means that python-ldap is not installed, we use Missing object to mark
49 # ldap lib is Missing
49 # ldap lib is Missing
50 ldap = Missing
50 ldap = Missing
51
51
52
52
53 class LdapError(Exception):
53 class LdapError(Exception):
54 pass
54 pass
55
55
56
56
57 def plugin_factory(plugin_id, *args, **kwds):
57 def plugin_factory(plugin_id, *args, **kwds):
58 """
58 """
59 Factory function that is called during plugin discovery.
59 Factory function that is called during plugin discovery.
60 It returns the plugin instance.
60 It returns the plugin instance.
61 """
61 """
62 plugin = RhodeCodeAuthPlugin(plugin_id)
62 plugin = RhodeCodeAuthPlugin(plugin_id)
63 return plugin
63 return plugin
64
64
65
65
66 class LdapAuthnResource(AuthnPluginResourceBase):
66 class LdapAuthnResource(AuthnPluginResourceBase):
67 pass
67 pass
68
68
69
69
70 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
70 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
71 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
71 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
72 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
72 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
73 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
73 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
74
74
75 host = colander.SchemaNode(
75 host = colander.SchemaNode(
76 colander.String(),
76 colander.String(),
77 default='',
77 default='',
78 description=_('Host[s] of the LDAP Server \n'
78 description=_('Host[s] of the LDAP Server \n'
79 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
79 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
80 'Multiple servers can be specified using commas'),
80 'Multiple servers can be specified using commas'),
81 preparer=strip_whitespace,
81 preparer=strip_whitespace,
82 title=_('LDAP Host'),
82 title=_('LDAP Host'),
83 widget='string')
83 widget='string')
84 port = colander.SchemaNode(
84 port = colander.SchemaNode(
85 colander.Int(),
85 colander.Int(),
86 default=389,
86 default=389,
87 description=_('Custom port that the LDAP server is listening on. '
87 description=_('Custom port that the LDAP server is listening on. '
88 'Default value is: 389'),
88 'Default value is: 389'),
89 preparer=strip_whitespace,
89 preparer=strip_whitespace,
90 title=_('Port'),
90 title=_('Port'),
91 validator=colander.Range(min=0, max=65536),
91 validator=colander.Range(min=0, max=65536),
92 widget='int')
92 widget='int')
93
93
94 timeout = colander.SchemaNode(
94 timeout = colander.SchemaNode(
95 colander.Int(),
95 colander.Int(),
96 default=60 * 5,
96 default=60 * 5,
97 description=_('Timeout for LDAP connection'),
97 description=_('Timeout for LDAP connection'),
98 preparer=strip_whitespace,
98 preparer=strip_whitespace,
99 title=_('Connection timeout'),
99 title=_('Connection timeout'),
100 validator=colander.Range(min=1),
100 validator=colander.Range(min=1),
101 widget='int')
101 widget='int')
102
102
103 dn_user = colander.SchemaNode(
103 dn_user = colander.SchemaNode(
104 colander.String(),
104 colander.String(),
105 default='',
105 default='',
106 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
106 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
107 'e.g., cn=admin,dc=mydomain,dc=com, or '
107 'e.g., cn=admin,dc=mydomain,dc=com, or '
108 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
108 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
109 missing='',
109 missing='',
110 preparer=strip_whitespace,
110 preparer=strip_whitespace,
111 title=_('Account'),
111 title=_('Account'),
112 widget='string')
112 widget='string')
113 dn_pass = colander.SchemaNode(
113 dn_pass = colander.SchemaNode(
114 colander.String(),
114 colander.String(),
115 default='',
115 default='',
116 description=_('Password to authenticate for given user DN.'),
116 description=_('Password to authenticate for given user DN.'),
117 missing='',
117 missing='',
118 preparer=strip_whitespace,
118 preparer=strip_whitespace,
119 title=_('Password'),
119 title=_('Password'),
120 widget='password')
120 widget='password')
121 tls_kind = colander.SchemaNode(
121 tls_kind = colander.SchemaNode(
122 colander.String(),
122 colander.String(),
123 default=tls_kind_choices[0],
123 default=tls_kind_choices[0],
124 description=_('TLS Type'),
124 description=_('TLS Type'),
125 title=_('Connection Security'),
125 title=_('Connection Security'),
126 validator=colander.OneOf(tls_kind_choices),
126 validator=colander.OneOf(tls_kind_choices),
127 widget='select')
127 widget='select')
128 tls_reqcert = colander.SchemaNode(
128 tls_reqcert = colander.SchemaNode(
129 colander.String(),
129 colander.String(),
130 default=tls_reqcert_choices[0],
130 default=tls_reqcert_choices[0],
131 description=_('Require Cert over TLS?. Self-signed and custom '
131 description=_('Require Cert over TLS?. Self-signed and custom '
132 'certificates can be used when\n `RhodeCode Certificate` '
132 'certificates can be used when\n `RhodeCode Certificate` '
133 'found in admin > settings > system info page is extended.'),
133 'found in admin > settings > system info page is extended.'),
134 title=_('Certificate Checks'),
134 title=_('Certificate Checks'),
135 validator=colander.OneOf(tls_reqcert_choices),
135 validator=colander.OneOf(tls_reqcert_choices),
136 widget='select')
136 widget='select')
137 base_dn = colander.SchemaNode(
137 base_dn = colander.SchemaNode(
138 colander.String(),
138 colander.String(),
139 default='',
139 default='',
140 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
140 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
141 'in it to be replaced with current user credentials \n'
141 'in it to be replaced with current user credentials \n'
142 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
142 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
143 missing='',
143 missing='',
144 preparer=strip_whitespace,
144 preparer=strip_whitespace,
145 title=_('Base DN'),
145 title=_('Base DN'),
146 widget='string')
146 widget='string')
147 filter = colander.SchemaNode(
147 filter = colander.SchemaNode(
148 colander.String(),
148 colander.String(),
149 default='',
149 default='',
150 description=_('Filter to narrow results \n'
150 description=_('Filter to narrow results \n'
151 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
151 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
152 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
152 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
153 missing='',
153 missing='',
154 preparer=strip_whitespace,
154 preparer=strip_whitespace,
155 title=_('LDAP Search Filter'),
155 title=_('LDAP Search Filter'),
156 widget='string')
156 widget='string')
157
157
158 search_scope = colander.SchemaNode(
158 search_scope = colander.SchemaNode(
159 colander.String(),
159 colander.String(),
160 default=search_scope_choices[2],
160 default=search_scope_choices[2],
161 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
161 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
162 title=_('LDAP Search Scope'),
162 title=_('LDAP Search Scope'),
163 validator=colander.OneOf(search_scope_choices),
163 validator=colander.OneOf(search_scope_choices),
164 widget='select')
164 widget='select')
165 attr_login = colander.SchemaNode(
165 attr_login = colander.SchemaNode(
166 colander.String(),
166 colander.String(),
167 default='uid',
167 default='uid',
168 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
168 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
169 preparer=strip_whitespace,
169 preparer=strip_whitespace,
170 title=_('Login Attribute'),
170 title=_('Login Attribute'),
171 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
171 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
172 widget='string')
172 widget='string')
173 attr_firstname = colander.SchemaNode(
173 attr_firstname = colander.SchemaNode(
174 colander.String(),
174 colander.String(),
175 default='',
175 default='',
176 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
176 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
177 missing='',
177 missing='',
178 preparer=strip_whitespace,
178 preparer=strip_whitespace,
179 title=_('First Name Attribute'),
179 title=_('First Name Attribute'),
180 widget='string')
180 widget='string')
181 attr_lastname = colander.SchemaNode(
181 attr_lastname = colander.SchemaNode(
182 colander.String(),
182 colander.String(),
183 default='',
183 default='',
184 description=_('LDAP Attribute to map to last name (e.g., sn)'),
184 description=_('LDAP Attribute to map to last name (e.g., sn)'),
185 missing='',
185 missing='',
186 preparer=strip_whitespace,
186 preparer=strip_whitespace,
187 title=_('Last Name Attribute'),
187 title=_('Last Name Attribute'),
188 widget='string')
188 widget='string')
189 attr_email = colander.SchemaNode(
189 attr_email = colander.SchemaNode(
190 colander.String(),
190 colander.String(),
191 default='',
191 default='',
192 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
192 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
193 'Emails are a crucial part of RhodeCode. \n'
193 'Emails are a crucial part of RhodeCode. \n'
194 'If possible add a valid email attribute to ldap users.'),
194 'If possible add a valid email attribute to ldap users.'),
195 missing='',
195 missing='',
196 preparer=strip_whitespace,
196 preparer=strip_whitespace,
197 title=_('Email Attribute'),
197 title=_('Email Attribute'),
198 widget='string')
198 widget='string')
199
199
200
200
201 class AuthLdap(AuthLdapBase):
201 class AuthLdap(AuthLdapBase):
202
202
203 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
203 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
204 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
204 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
205 search_scope='SUBTREE', attr_login='uid',
205 search_scope='SUBTREE', attr_login='uid',
206 ldap_filter='', timeout=None):
206 ldap_filter='', timeout=None):
207 if ldap == Missing:
207 if ldap == Missing:
208 raise LdapImportError("Missing or incompatible ldap library")
208 raise LdapImportError("Missing or incompatible ldap library")
209
209
210 self.debug = False
210 self.debug = False
211 self.timeout = timeout or 60 * 5
211 self.timeout = timeout or 60 * 5
212 self.ldap_version = ldap_version
212 self.ldap_version = ldap_version
213 self.ldap_server_type = 'ldap'
213 self.ldap_server_type = 'ldap'
214
214
215 self.TLS_KIND = tls_kind
215 self.TLS_KIND = tls_kind
216
216
217 if self.TLS_KIND == 'LDAPS':
217 if self.TLS_KIND == 'LDAPS':
218 port = port or 689
218 port = port or 689
219 self.ldap_server_type += 's'
219 self.ldap_server_type += 's'
220
220
221 OPT_X_TLS_DEMAND = 2
221 OPT_X_TLS_DEMAND = 2
222 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
222 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
223 OPT_X_TLS_DEMAND)
223 OPT_X_TLS_DEMAND)
224 self.LDAP_SERVER = server
224 self.LDAP_SERVER = server
225 # split server into list
225 # split server into list
226 self.SERVER_ADDRESSES = self._get_server_list(server)
226 self.SERVER_ADDRESSES = self._get_server_list(server)
227 self.LDAP_SERVER_PORT = port
227 self.LDAP_SERVER_PORT = port
228
228
229 # USE FOR READ ONLY BIND TO LDAP SERVER
229 # USE FOR READ ONLY BIND TO LDAP SERVER
230 self.attr_login = attr_login
230 self.attr_login = attr_login
231
231
232 self.LDAP_BIND_DN = safe_str(bind_dn)
232 self.LDAP_BIND_DN = safe_str(bind_dn)
233 self.LDAP_BIND_PASS = safe_str(bind_pass)
233 self.LDAP_BIND_PASS = safe_str(bind_pass)
234
234
235 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
235 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
236 self.BASE_DN = safe_str(base_dn)
236 self.BASE_DN = safe_str(base_dn)
237 self.LDAP_FILTER = safe_str(ldap_filter)
237 self.LDAP_FILTER = safe_str(ldap_filter)
238
238
239 def _get_ldap_conn(self):
239 def _get_ldap_conn(self):
240
240
241 if self.debug:
241 if self.debug:
242 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
242 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
243
243
244 default_cert_path = os.environ.get('SSL_CERT_FILE')
244 default_cert_path = os.environ.get('SSL_CERT_FILE')
245 default_cert_dir = os.environ.get('SSL_CERT_DIR', '/etc/openldap/cacerts')
245 default_cert_dir = os.environ.get('SSL_CERT_DIR', '/etc/openldap/cacerts')
246 if default_cert_path and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
246 if default_cert_path and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
247 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, default_cert_path)
247 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, default_cert_path)
248
248
249 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
249 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
250 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, default_cert_dir)
250 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, default_cert_dir)
251
251
252 if self.TLS_KIND != 'PLAIN':
252 if self.TLS_KIND != 'PLAIN':
253 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
253 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
254
254
255 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
255 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
256 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
256 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
257
257
258 # init connection now
258 # init connection now
259 ldap_servers = self._build_servers(
259 ldap_servers = self._build_servers(
260 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
260 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
261 log.debug('initializing LDAP connection to:%s', ldap_servers)
261 log.debug('initializing LDAP connection to:%s', ldap_servers)
262 ldap_conn = ldap.initialize(ldap_servers)
262 ldap_conn = ldap.initialize(ldap_servers)
263 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
263 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
264 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
264 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
265 ldap_conn.timeout = self.timeout
265 ldap_conn.timeout = self.timeout
266
266
267 if self.ldap_version == 2:
267 if self.ldap_version == 2:
268 ldap_conn.protocol = ldap.VERSION2
268 ldap_conn.protocol = ldap.VERSION2
269 else:
269 else:
270 ldap_conn.protocol = ldap.VERSION3
270 ldap_conn.protocol = ldap.VERSION3
271
271
272 if self.TLS_KIND == 'START_TLS':
272 if self.TLS_KIND == 'START_TLS':
273 ldap_conn.start_tls_s()
273 ldap_conn.start_tls_s()
274
274
275 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
275 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
276 log.debug('Trying simple_bind with password and given login DN: %s',
276 log.debug('Trying simple_bind with password and given login DN: %s',
277 self.LDAP_BIND_DN)
277 self.LDAP_BIND_DN)
278 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
278 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
279
279
280 return ldap_conn
280 return ldap_conn
281
281
282 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
282 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
283 try:
283 try:
284 log.debug('Trying simple bind with %s', dn)
284 log.debug('Trying simple bind with %s', dn)
285 server.simple_bind_s(dn, safe_str(password))
285 server.simple_bind_s(dn, safe_str(password))
286 user = server.search_ext_s(
286 user = server.search_ext_s(
287 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
287 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
288 _, attrs = user
288 _, attrs = user
289 return attrs
289 return attrs
290
290
291 except ldap.INVALID_CREDENTIALS:
291 except ldap.INVALID_CREDENTIALS:
292 log.debug(
292 log.debug(
293 "LDAP rejected password for user '%s': %s, org_exc:",
293 "LDAP rejected password for user '%s': %s, org_exc:",
294 username, dn, exc_info=True)
294 username, dn, exc_info=True)
295
295
296 def authenticate_ldap(self, username, password):
296 def authenticate_ldap(self, username, password):
297 """
297 """
298 Authenticate a user via LDAP and return his/her LDAP properties.
298 Authenticate a user via LDAP and return his/her LDAP properties.
299
299
300 Raises AuthenticationError if the credentials are rejected, or
300 Raises AuthenticationError if the credentials are rejected, or
301 EnvironmentError if the LDAP server can't be reached.
301 EnvironmentError if the LDAP server can't be reached.
302
302
303 :param username: username
303 :param username: username
304 :param password: password
304 :param password: password
305 """
305 """
306
306
307 uid = self.get_uid(username, self.SERVER_ADDRESSES)
307 uid = self.get_uid(username, self.SERVER_ADDRESSES)
308 user_attrs = {}
308 user_attrs = {}
309 dn = ''
309 dn = ''
310
310
311 if not password:
311 if not password:
312 msg = "Authenticating user %s with blank password not allowed"
312 msg = "Authenticating user %s with blank password not allowed"
313 log.warning(msg, username)
313 log.warning(msg, username)
314 raise LdapPasswordError(msg)
314 raise LdapPasswordError(msg)
315 if "," in username:
315 if "," in username:
316 raise LdapUsernameError(
316 raise LdapUsernameError(
317 "invalid character `,` in username: `{}`".format(username))
317 "invalid character `,` in username: `{}`".format(username))
318 ldap_conn = None
318 ldap_conn = None
319 try:
319 try:
320 ldap_conn = self._get_ldap_conn()
320 ldap_conn = self._get_ldap_conn()
321 filter_ = '(&%s(%s=%s))' % (
321 filter_ = '(&%s(%s=%s))' % (
322 self.LDAP_FILTER, self.attr_login, username)
322 self.LDAP_FILTER, self.attr_login, username)
323 log.debug(
323 log.debug(
324 "Authenticating %r filter %s", self.BASE_DN, filter_)
324 "Authenticating %r filter %s", self.BASE_DN, filter_)
325 lobjects = ldap_conn.search_ext_s(
325 lobjects = ldap_conn.search_ext_s(
326 self.BASE_DN, self.SEARCH_SCOPE, filter_)
326 self.BASE_DN, self.SEARCH_SCOPE, filter_)
327
327
328 if not lobjects:
328 if not lobjects:
329 log.debug("No matching LDAP objects for authentication "
329 log.debug("No matching LDAP objects for authentication "
330 "of UID:'%s' username:(%s)", uid, username)
330 "of UID:'%s' username:(%s)", uid, username)
331 raise ldap.NO_SUCH_OBJECT()
331 raise ldap.NO_SUCH_OBJECT()
332
332
333 log.debug('Found matching ldap object, trying to authenticate')
333 log.debug('Found matching ldap object, trying to authenticate')
334 for (dn, _attrs) in lobjects:
334 for (dn, _attrs) in lobjects:
335 if dn is None:
335 if dn is None:
336 continue
336 continue
337
337
338 user_attrs = self.fetch_attrs_from_simple_bind(
338 user_attrs = self.fetch_attrs_from_simple_bind(
339 ldap_conn, dn, username, password)
339 ldap_conn, dn, username, password)
340 if user_attrs:
340 if user_attrs:
341 break
341 break
342
342
343 else:
343 else:
344 raise LdapPasswordError(
344 raise LdapPasswordError(
345 'Failed to authenticate user `{}`'
345 'Failed to authenticate user `{}`'
346 'with given password'.format(username))
346 'with given password'.format(username))
347
347
348 except ldap.NO_SUCH_OBJECT:
348 except ldap.NO_SUCH_OBJECT:
349 log.debug("LDAP says no such user '%s' (%s), org_exc:",
349 log.debug("LDAP says no such user '%s' (%s), org_exc:",
350 uid, username, exc_info=True)
350 uid, username, exc_info=True)
351 raise LdapUsernameError('Unable to find user')
351 raise LdapUsernameError('Unable to find user')
352 except ldap.SERVER_DOWN:
352 except ldap.SERVER_DOWN:
353 org_exc = traceback.format_exc()
353 org_exc = traceback.format_exc()
354 raise LdapConnectionError(
354 raise LdapConnectionError(
355 "LDAP can't access authentication "
355 "LDAP can't access authentication "
356 "server, org_exc:%s" % org_exc)
356 "server, org_exc:%s" % org_exc)
357 finally:
357 finally:
358 if ldap_conn:
358 if ldap_conn:
359 log.debug('ldap: connection release')
359 log.debug('ldap: connection release')
360 try:
360 try:
361 ldap_conn.unbind_s()
361 ldap_conn.unbind_s()
362 except Exception:
362 except Exception:
363 # for any reason this can raise exception we must catch it
363 # for any reason this can raise exception we must catch it
364 # to not crush the server
364 # to not crush the server
365 pass
365 pass
366
366
367 return dn, user_attrs
367 return dn, user_attrs
368
368
369
369
370 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
370 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
371 # used to define dynamic binding in the
371 # used to define dynamic binding in the
372 DYNAMIC_BIND_VAR = '$login'
372 DYNAMIC_BIND_VAR = '$login'
373 _settings_unsafe_keys = ['dn_pass']
373 _settings_unsafe_keys = ['dn_pass']
374
374
375 def includeme(self, config):
375 def includeme(self, config):
376 config.add_authn_plugin(self)
376 config.add_authn_plugin(self)
377 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
377 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
378 config.add_view(
378 config.add_view(
379 'rhodecode.authentication.views.AuthnPluginViewBase',
379 'rhodecode.authentication.views.AuthnPluginViewBase',
380 attr='settings_get',
380 attr='settings_get',
381 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
381 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
382 request_method='GET',
382 request_method='GET',
383 route_name='auth_home',
383 route_name='auth_home',
384 context=LdapAuthnResource)
384 context=LdapAuthnResource)
385 config.add_view(
385 config.add_view(
386 'rhodecode.authentication.views.AuthnPluginViewBase',
386 'rhodecode.authentication.views.AuthnPluginViewBase',
387 attr='settings_post',
387 attr='settings_post',
388 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
388 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
389 request_method='POST',
389 request_method='POST',
390 route_name='auth_home',
390 route_name='auth_home',
391 context=LdapAuthnResource)
391 context=LdapAuthnResource)
392
392
393 def get_settings_schema(self):
393 def get_settings_schema(self):
394 return LdapSettingsSchema()
394 return LdapSettingsSchema()
395
395
396 def get_display_name(self):
396 def get_display_name(self):
397 return _('LDAP')
397 return _('LDAP')
398
398
399 @classmethod
400 def docs(cls):
401 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
402
399 @hybrid_property
403 @hybrid_property
400 def name(self):
404 def name(self):
401 return "ldap"
405 return "ldap"
402
406
403 def use_fake_password(self):
407 def use_fake_password(self):
404 return True
408 return True
405
409
406 def user_activation_state(self):
410 def user_activation_state(self):
407 def_user_perms = User.get_default_user().AuthUser().permissions['global']
411 def_user_perms = User.get_default_user().AuthUser().permissions['global']
408 return 'hg.extern_activate.auto' in def_user_perms
412 return 'hg.extern_activate.auto' in def_user_perms
409
413
410 def try_dynamic_binding(self, username, password, current_args):
414 def try_dynamic_binding(self, username, password, current_args):
411 """
415 """
412 Detects marker inside our original bind, and uses dynamic auth if
416 Detects marker inside our original bind, and uses dynamic auth if
413 present
417 present
414 """
418 """
415
419
416 org_bind = current_args['bind_dn']
420 org_bind = current_args['bind_dn']
417 passwd = current_args['bind_pass']
421 passwd = current_args['bind_pass']
418
422
419 def has_bind_marker(username):
423 def has_bind_marker(username):
420 if self.DYNAMIC_BIND_VAR in username:
424 if self.DYNAMIC_BIND_VAR in username:
421 return True
425 return True
422
426
423 # we only passed in user with "special" variable
427 # we only passed in user with "special" variable
424 if org_bind and has_bind_marker(org_bind) and not passwd:
428 if org_bind and has_bind_marker(org_bind) and not passwd:
425 log.debug('Using dynamic user/password binding for ldap '
429 log.debug('Using dynamic user/password binding for ldap '
426 'authentication. Replacing `%s` with username',
430 'authentication. Replacing `%s` with username',
427 self.DYNAMIC_BIND_VAR)
431 self.DYNAMIC_BIND_VAR)
428 current_args['bind_dn'] = org_bind.replace(
432 current_args['bind_dn'] = org_bind.replace(
429 self.DYNAMIC_BIND_VAR, username)
433 self.DYNAMIC_BIND_VAR, username)
430 current_args['bind_pass'] = password
434 current_args['bind_pass'] = password
431
435
432 return current_args
436 return current_args
433
437
434 def auth(self, userobj, username, password, settings, **kwargs):
438 def auth(self, userobj, username, password, settings, **kwargs):
435 """
439 """
436 Given a user object (which may be null), username, a plaintext password,
440 Given a user object (which may be null), username, a plaintext password,
437 and a settings object (containing all the keys needed as listed in
441 and a settings object (containing all the keys needed as listed in
438 settings()), authenticate this user's login attempt.
442 settings()), authenticate this user's login attempt.
439
443
440 Return None on failure. On success, return a dictionary of the form:
444 Return None on failure. On success, return a dictionary of the form:
441
445
442 see: RhodeCodeAuthPluginBase.auth_func_attrs
446 see: RhodeCodeAuthPluginBase.auth_func_attrs
443 This is later validated for correctness
447 This is later validated for correctness
444 """
448 """
445
449
446 if not username or not password:
450 if not username or not password:
447 log.debug('Empty username or password skipping...')
451 log.debug('Empty username or password skipping...')
448 return None
452 return None
449
453
450 ldap_args = {
454 ldap_args = {
451 'server': settings.get('host', ''),
455 'server': settings.get('host', ''),
452 'base_dn': settings.get('base_dn', ''),
456 'base_dn': settings.get('base_dn', ''),
453 'port': settings.get('port'),
457 'port': settings.get('port'),
454 'bind_dn': settings.get('dn_user'),
458 'bind_dn': settings.get('dn_user'),
455 'bind_pass': settings.get('dn_pass'),
459 'bind_pass': settings.get('dn_pass'),
456 'tls_kind': settings.get('tls_kind'),
460 'tls_kind': settings.get('tls_kind'),
457 'tls_reqcert': settings.get('tls_reqcert'),
461 'tls_reqcert': settings.get('tls_reqcert'),
458 'search_scope': settings.get('search_scope'),
462 'search_scope': settings.get('search_scope'),
459 'attr_login': settings.get('attr_login'),
463 'attr_login': settings.get('attr_login'),
460 'ldap_version': 3,
464 'ldap_version': 3,
461 'ldap_filter': settings.get('filter'),
465 'ldap_filter': settings.get('filter'),
462 'timeout': settings.get('timeout')
466 'timeout': settings.get('timeout')
463 }
467 }
464
468
465 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
469 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
466
470
467 log.debug('Checking for ldap authentication.')
471 log.debug('Checking for ldap authentication.')
468
472
469 try:
473 try:
470 aldap = AuthLdap(**ldap_args)
474 aldap = AuthLdap(**ldap_args)
471 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
475 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
472 log.debug('Got ldap DN response %s', user_dn)
476 log.debug('Got ldap DN response %s', user_dn)
473
477
474 def get_ldap_attr(k):
478 def get_ldap_attr(k):
475 return ldap_attrs.get(settings.get(k), [''])[0]
479 return ldap_attrs.get(settings.get(k), [''])[0]
476
480
477 # old attrs fetched from RhodeCode database
481 # old attrs fetched from RhodeCode database
478 admin = getattr(userobj, 'admin', False)
482 admin = getattr(userobj, 'admin', False)
479 active = getattr(userobj, 'active', True)
483 active = getattr(userobj, 'active', True)
480 email = getattr(userobj, 'email', '')
484 email = getattr(userobj, 'email', '')
481 username = getattr(userobj, 'username', username)
485 username = getattr(userobj, 'username', username)
482 firstname = getattr(userobj, 'firstname', '')
486 firstname = getattr(userobj, 'firstname', '')
483 lastname = getattr(userobj, 'lastname', '')
487 lastname = getattr(userobj, 'lastname', '')
484 extern_type = getattr(userobj, 'extern_type', '')
488 extern_type = getattr(userobj, 'extern_type', '')
485
489
486 groups = []
490 groups = []
487 user_attrs = {
491 user_attrs = {
488 'username': username,
492 'username': username,
489 'firstname': safe_unicode(
493 'firstname': safe_unicode(
490 get_ldap_attr('attr_firstname') or firstname),
494 get_ldap_attr('attr_firstname') or firstname),
491 'lastname': safe_unicode(
495 'lastname': safe_unicode(
492 get_ldap_attr('attr_lastname') or lastname),
496 get_ldap_attr('attr_lastname') or lastname),
493 'groups': groups,
497 'groups': groups,
494 'user_group_sync': False,
498 'user_group_sync': False,
495 'email': get_ldap_attr('attr_email') or email,
499 'email': get_ldap_attr('attr_email') or email,
496 'admin': admin,
500 'admin': admin,
497 'active': active,
501 'active': active,
498 'active_from_extern': None,
502 'active_from_extern': None,
499 'extern_name': user_dn,
503 'extern_name': user_dn,
500 'extern_type': extern_type,
504 'extern_type': extern_type,
501 }
505 }
502 log.debug('ldap user: %s', user_attrs)
506 log.debug('ldap user: %s', user_attrs)
503 log.info('user `%s` authenticated correctly', user_attrs['username'])
507 log.info('user `%s` authenticated correctly', user_attrs['username'])
504
508
505 return user_attrs
509 return user_attrs
506
510
507 except (LdapUsernameError, LdapPasswordError, LdapImportError):
511 except (LdapUsernameError, LdapPasswordError, LdapImportError):
508 log.exception("LDAP related exception")
512 log.exception("LDAP related exception")
509 return None
513 return None
510 except (Exception,):
514 except (Exception,):
511 log.exception("Other exception")
515 log.exception("Other exception")
512 return None
516 return None
513
517
@@ -1,161 +1,165 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
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 # PAM authentication can be slow. Repository operations involve a lot of
75 # PAM authentication can be slow. Repository operations involve a lot of
76 # auth calls. Little caching helps speedup push/pull operations significantly
76 # auth calls. Little caching helps speedup push/pull operations significantly
77 AUTH_CACHE_TTL = 4
77 AUTH_CACHE_TTL = 4
78
78
79 def includeme(self, config):
79 def includeme(self, config):
80 config.add_authn_plugin(self)
80 config.add_authn_plugin(self)
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
82 config.add_view(
82 config.add_view(
83 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 attr='settings_get',
84 attr='settings_get',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 request_method='GET',
86 request_method='GET',
87 route_name='auth_home',
87 route_name='auth_home',
88 context=PamAuthnResource)
88 context=PamAuthnResource)
89 config.add_view(
89 config.add_view(
90 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 attr='settings_post',
91 attr='settings_post',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 request_method='POST',
93 request_method='POST',
94 route_name='auth_home',
94 route_name='auth_home',
95 context=PamAuthnResource)
95 context=PamAuthnResource)
96
96
97 def get_display_name(self):
97 def get_display_name(self):
98 return _('PAM')
98 return _('PAM')
99
99
100 @classmethod
101 def docs(cls):
102 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
103
100 @hybrid_property
104 @hybrid_property
101 def name(self):
105 def name(self):
102 return "pam"
106 return "pam"
103
107
104 def get_settings_schema(self):
108 def get_settings_schema(self):
105 return PamSettingsSchema()
109 return PamSettingsSchema()
106
110
107 def use_fake_password(self):
111 def use_fake_password(self):
108 return True
112 return True
109
113
110 def auth(self, userobj, username, password, settings, **kwargs):
114 def auth(self, userobj, username, password, settings, **kwargs):
111 if not username or not password:
115 if not username or not password:
112 log.debug('Empty username or password skipping...')
116 log.debug('Empty username or password skipping...')
113 return None
117 return None
114 _pam = pam.pam()
118 _pam = pam.pam()
115 auth_result = _pam.authenticate(username, password, settings["service"])
119 auth_result = _pam.authenticate(username, password, settings["service"])
116
120
117 if not auth_result:
121 if not auth_result:
118 log.error("PAM was unable to authenticate user: %s", username)
122 log.error("PAM was unable to authenticate user: %s", username)
119 return None
123 return None
120
124
121 log.debug('Got PAM response %s', auth_result)
125 log.debug('Got PAM response %s', auth_result)
122
126
123 # old attrs fetched from RhodeCode database
127 # old attrs fetched from RhodeCode database
124 default_email = "%s@%s" % (username, socket.gethostname())
128 default_email = "%s@%s" % (username, socket.gethostname())
125 admin = getattr(userobj, 'admin', False)
129 admin = getattr(userobj, 'admin', False)
126 active = getattr(userobj, 'active', True)
130 active = getattr(userobj, 'active', True)
127 email = getattr(userobj, 'email', '') or default_email
131 email = getattr(userobj, 'email', '') or default_email
128 username = getattr(userobj, 'username', username)
132 username = getattr(userobj, 'username', username)
129 firstname = getattr(userobj, 'firstname', '')
133 firstname = getattr(userobj, 'firstname', '')
130 lastname = getattr(userobj, 'lastname', '')
134 lastname = getattr(userobj, 'lastname', '')
131 extern_type = getattr(userobj, 'extern_type', '')
135 extern_type = getattr(userobj, 'extern_type', '')
132
136
133 user_attrs = {
137 user_attrs = {
134 'username': username,
138 'username': username,
135 'firstname': firstname,
139 'firstname': firstname,
136 'lastname': lastname,
140 'lastname': lastname,
137 'groups': [g.gr_name for g in grp.getgrall()
141 'groups': [g.gr_name for g in grp.getgrall()
138 if username in g.gr_mem],
142 if username in g.gr_mem],
139 'user_group_sync': True,
143 'user_group_sync': True,
140 'email': email,
144 'email': email,
141 'admin': admin,
145 'admin': admin,
142 'active': active,
146 'active': active,
143 'active_from_extern': None,
147 'active_from_extern': None,
144 'extern_name': username,
148 'extern_name': username,
145 'extern_type': extern_type,
149 'extern_type': extern_type,
146 }
150 }
147
151
148 try:
152 try:
149 user_data = pwd.getpwnam(username)
153 user_data = pwd.getpwnam(username)
150 regex = settings["gecos"]
154 regex = settings["gecos"]
151 match = re.search(regex, user_data.pw_gecos)
155 match = re.search(regex, user_data.pw_gecos)
152 if match:
156 if match:
153 user_attrs["firstname"] = match.group('first_name')
157 user_attrs["firstname"] = match.group('first_name')
154 user_attrs["lastname"] = match.group('last_name')
158 user_attrs["lastname"] = match.group('last_name')
155 except Exception:
159 except Exception:
156 log.warning("Cannot extract additional info for PAM user")
160 log.warning("Cannot extract additional info for PAM user")
157 pass
161 pass
158
162
159 log.debug("pamuser: %s", user_attrs)
163 log.debug("pamuser: %s", user_attrs)
160 log.info('user `%s` authenticated correctly', user_attrs['username'])
164 log.info('user `%s` authenticated correctly', user_attrs['username'])
161 return user_attrs
165 return user_attrs
@@ -1,147 +1,151 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2018 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
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.authentication.base import (
28 from rhodecode.authentication.base import (
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.model.db import User, UserApiKeys, Repository
31 from rhodecode.model.db import User, UserApiKeys, Repository
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, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 """
47 """
48 Enables usage of authentication tokens for vcs operations.
48 Enables usage of authentication tokens for vcs operations.
49 """
49 """
50
50
51 def includeme(self, config):
51 def includeme(self, config):
52 config.add_authn_plugin(self)
52 config.add_authn_plugin(self)
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 config.add_view(
54 config.add_view(
55 'rhodecode.authentication.views.AuthnPluginViewBase',
55 'rhodecode.authentication.views.AuthnPluginViewBase',
56 attr='settings_get',
56 attr='settings_get',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
58 request_method='GET',
58 request_method='GET',
59 route_name='auth_home',
59 route_name='auth_home',
60 context=RhodecodeAuthnResource)
60 context=RhodecodeAuthnResource)
61 config.add_view(
61 config.add_view(
62 'rhodecode.authentication.views.AuthnPluginViewBase',
62 'rhodecode.authentication.views.AuthnPluginViewBase',
63 attr='settings_post',
63 attr='settings_post',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
65 request_method='POST',
65 request_method='POST',
66 route_name='auth_home',
66 route_name='auth_home',
67 context=RhodecodeAuthnResource)
67 context=RhodecodeAuthnResource)
68
68
69 def get_display_name(self):
69 def get_display_name(self):
70 return _('Rhodecode Token Auth')
70 return _('Rhodecode Token Auth')
71
71
72 @classmethod
73 def docs(cls):
74 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-token.html"
75
72 @hybrid_property
76 @hybrid_property
73 def name(self):
77 def name(self):
74 return "authtoken"
78 return "authtoken"
75
79
76 def user_activation_state(self):
80 def user_activation_state(self):
77 def_user_perms = User.get_default_user().AuthUser().permissions['global']
81 def_user_perms = User.get_default_user().AuthUser().permissions['global']
78 return 'hg.register.auto_activate' in def_user_perms
82 return 'hg.register.auto_activate' in def_user_perms
79
83
80 def allows_authentication_from(
84 def allows_authentication_from(
81 self, user, allows_non_existing_user=True,
85 self, user, allows_non_existing_user=True,
82 allowed_auth_plugins=None, allowed_auth_sources=None):
86 allowed_auth_plugins=None, allowed_auth_sources=None):
83 """
87 """
84 Custom method for this auth that doesn't accept empty users. And also
88 Custom method for this auth that doesn't accept empty users. And also
85 allows users from all other active plugins to use it and also
89 allows users from all other active plugins to use it and also
86 authenticate against it. But only via vcs mode
90 authenticate against it. But only via vcs mode
87 """
91 """
88 from rhodecode.authentication.base import get_authn_registry
92 from rhodecode.authentication.base import get_authn_registry
89 authn_registry = get_authn_registry()
93 authn_registry = get_authn_registry()
90
94
91 active_plugins = set(
95 active_plugins = set(
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
96 [x.name for x in authn_registry.get_plugins_for_authentication()])
93 active_plugins.discard(self.name)
97 active_plugins.discard(self.name)
94
98
95 allowed_auth_plugins = [self.name] + list(active_plugins)
99 allowed_auth_plugins = [self.name] + list(active_plugins)
96 # only for vcs operations
100 # only for vcs operations
97 allowed_auth_sources = [VCS_TYPE]
101 allowed_auth_sources = [VCS_TYPE]
98
102
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
103 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
100 user, allows_non_existing_user=False,
104 user, allows_non_existing_user=False,
101 allowed_auth_plugins=allowed_auth_plugins,
105 allowed_auth_plugins=allowed_auth_plugins,
102 allowed_auth_sources=allowed_auth_sources)
106 allowed_auth_sources=allowed_auth_sources)
103
107
104 def auth(self, userobj, username, password, settings, **kwargs):
108 def auth(self, userobj, username, password, settings, **kwargs):
105 if not userobj:
109 if not userobj:
106 log.debug('userobj was:%s skipping', userobj)
110 log.debug('userobj was:%s skipping', userobj)
107 return None
111 return None
108
112
109 user_attrs = {
113 user_attrs = {
110 "username": userobj.username,
114 "username": userobj.username,
111 "firstname": userobj.firstname,
115 "firstname": userobj.firstname,
112 "lastname": userobj.lastname,
116 "lastname": userobj.lastname,
113 "groups": [],
117 "groups": [],
114 'user_group_sync': False,
118 'user_group_sync': False,
115 "email": userobj.email,
119 "email": userobj.email,
116 "admin": userobj.admin,
120 "admin": userobj.admin,
117 "active": userobj.active,
121 "active": userobj.active,
118 "active_from_extern": userobj.active,
122 "active_from_extern": userobj.active,
119 "extern_name": userobj.user_id,
123 "extern_name": userobj.user_id,
120 "extern_type": userobj.extern_type,
124 "extern_type": userobj.extern_type,
121 }
125 }
122
126
123 log.debug('Authenticating user with args %s', user_attrs)
127 log.debug('Authenticating user with args %s', user_attrs)
124 if userobj.active:
128 if userobj.active:
125 # calling context repo for token scopes
129 # calling context repo for token scopes
126 scope_repo_id = None
130 scope_repo_id = None
127 if self.acl_repo_name:
131 if self.acl_repo_name:
128 repo = Repository.get_by_repo_name(self.acl_repo_name)
132 repo = Repository.get_by_repo_name(self.acl_repo_name)
129 scope_repo_id = repo.repo_id if repo else None
133 scope_repo_id = repo.repo_id if repo else None
130
134
131 token_match = userobj.authenticate_by_token(
135 token_match = userobj.authenticate_by_token(
132 password, roles=[UserApiKeys.ROLE_VCS],
136 password, roles=[UserApiKeys.ROLE_VCS],
133 scope_repo_id=scope_repo_id)
137 scope_repo_id=scope_repo_id)
134
138
135 if userobj.username == username and token_match:
139 if userobj.username == username and token_match:
136 log.info(
140 log.info(
137 'user `%s` successfully authenticated via %s',
141 'user `%s` successfully authenticated via %s',
138 user_attrs['username'], self.name)
142 user_attrs['username'], self.name)
139 return user_attrs
143 return user_attrs
140 log.warn(
144 log.warn(
141 'user `%s` failed to authenticate via %s, reason: bad or '
145 'user `%s` failed to authenticate via %s, reason: bad or '
142 'inactive token.', username, self.name)
146 'inactive token.', username, self.name)
143 else:
147 else:
144 log.warning(
148 log.warning(
145 'user `%s` failed to authenticate via %s, reason: account not '
149 'user `%s` failed to authenticate via %s, reason: account not '
146 'active.', username, self.name)
150 'active.', username, self.name)
147 return None
151 return None
General Comments 0
You need to be logged in to leave comments. Login now