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