##// END OF EJS Templates
authn: Use new is_headers_auth function.
johbo -
r108:15a894ab default
parent child Browse files
Show More
@@ -1,608 +1,609 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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
24
25 import logging
25 import logging
26 import time
26 import time
27 import traceback
27 import traceback
28 import warnings
28 import warnings
29
29
30 from pyramid.threadlocal import get_current_registry
30 from pyramid.threadlocal import get_current_registry
31 from sqlalchemy.ext.hybrid import hybrid_property
31 from sqlalchemy.ext.hybrid import hybrid_property
32
32
33 from rhodecode.authentication.interface import IAuthnPluginRegistry
33 from rhodecode.authentication.interface import IAuthnPluginRegistry
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.lib import caches
35 from rhodecode.lib import caches
36 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
36 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
37 from rhodecode.lib.utils2 import md5_safe, safe_int
37 from rhodecode.lib.utils2 import md5_safe, safe_int
38 from rhodecode.lib.utils2 import safe_str
38 from rhodecode.lib.utils2 import safe_str
39 from rhodecode.model.db import User
39 from rhodecode.model.db import User
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.settings import SettingsModel
41 from rhodecode.model.settings import SettingsModel
42 from rhodecode.model.user import UserModel
42 from rhodecode.model.user import UserModel
43 from rhodecode.model.user_group import UserGroupModel
43 from rhodecode.model.user_group import UserGroupModel
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48 # auth types that authenticate() function can receive
48 # auth types that authenticate() function can receive
49 VCS_TYPE = 'vcs'
49 VCS_TYPE = 'vcs'
50 HTTP_TYPE = 'http'
50 HTTP_TYPE = 'http'
51
51
52
52
53 class LazyFormencode(object):
53 class LazyFormencode(object):
54 def __init__(self, formencode_obj, *args, **kwargs):
54 def __init__(self, formencode_obj, *args, **kwargs):
55 self.formencode_obj = formencode_obj
55 self.formencode_obj = formencode_obj
56 self.args = args
56 self.args = args
57 self.kwargs = kwargs
57 self.kwargs = kwargs
58
58
59 def __call__(self, *args, **kwargs):
59 def __call__(self, *args, **kwargs):
60 from inspect import isfunction
60 from inspect import isfunction
61 formencode_obj = self.formencode_obj
61 formencode_obj = self.formencode_obj
62 if isfunction(formencode_obj):
62 if isfunction(formencode_obj):
63 # case we wrap validators into functions
63 # case we wrap validators into functions
64 formencode_obj = self.formencode_obj(*args, **kwargs)
64 formencode_obj = self.formencode_obj(*args, **kwargs)
65 return formencode_obj(*self.args, **self.kwargs)
65 return formencode_obj(*self.args, **self.kwargs)
66
66
67
67
68 class RhodeCodeAuthPluginBase(object):
68 class RhodeCodeAuthPluginBase(object):
69 # cache the authentication request for N amount of seconds. Some kind
69 # cache the authentication request for N amount of seconds. Some kind
70 # of authentication methods are very heavy and it's very efficient to cache
70 # of authentication methods are very heavy and it's very efficient to cache
71 # the result of a call. If it's set to None (default) cache is off
71 # the result of a call. If it's set to None (default) cache is off
72 AUTH_CACHE_TTL = None
72 AUTH_CACHE_TTL = None
73 AUTH_CACHE = {}
73 AUTH_CACHE = {}
74
74
75 auth_func_attrs = {
75 auth_func_attrs = {
76 "username": "unique username",
76 "username": "unique username",
77 "firstname": "first name",
77 "firstname": "first name",
78 "lastname": "last name",
78 "lastname": "last name",
79 "email": "email address",
79 "email": "email address",
80 "groups": '["list", "of", "groups"]',
80 "groups": '["list", "of", "groups"]',
81 "extern_name": "name in external source of record",
81 "extern_name": "name in external source of record",
82 "extern_type": "type of external source of record",
82 "extern_type": "type of external source of record",
83 "admin": 'True|False defines if user should be RhodeCode super admin',
83 "admin": 'True|False defines if user should be RhodeCode super admin',
84 "active":
84 "active":
85 'True|False defines active state of user internally for RhodeCode',
85 'True|False defines active state of user internally for RhodeCode',
86 "active_from_extern":
86 "active_from_extern":
87 "True|False\None, active state from the external auth, "
87 "True|False\None, active state from the external auth, "
88 "None means use definition from RhodeCode extern_type active value"
88 "None means use definition from RhodeCode extern_type active value"
89 }
89 }
90 # set on authenticate() method and via set_auth_type func.
90 # set on authenticate() method and via set_auth_type func.
91 auth_type = None
91 auth_type = None
92
92
93 # List of setting names to store encrypted. Plugins may override this list
93 # List of setting names to store encrypted. Plugins may override this list
94 # to store settings encrypted.
94 # to store settings encrypted.
95 _settings_encrypted = []
95 _settings_encrypted = []
96
96
97 # Mapping of python to DB settings model types. Plugins may override or
97 # Mapping of python to DB settings model types. Plugins may override or
98 # extend this mapping.
98 # extend this mapping.
99 _settings_type_map = {
99 _settings_type_map = {
100 str: 'str',
100 str: 'str',
101 int: 'int',
101 int: 'int',
102 unicode: 'unicode',
102 unicode: 'unicode',
103 bool: 'bool',
103 bool: 'bool',
104 list: 'list',
104 list: 'list',
105 }
105 }
106
106
107 def __init__(self, plugin_id):
107 def __init__(self, plugin_id):
108 self._plugin_id = plugin_id
108 self._plugin_id = plugin_id
109
109
110 def _get_setting_full_name(self, name):
110 def _get_setting_full_name(self, name):
111 """
111 """
112 Return the full setting name used for storing values in the database.
112 Return the full setting name used for storing values in the database.
113 """
113 """
114 # TODO: johbo: Using the name here is problematic. It would be good to
114 # TODO: johbo: Using the name here is problematic. It would be good to
115 # introduce either new models in the database to hold Plugin and
115 # introduce either new models in the database to hold Plugin and
116 # PluginSetting or to use the plugin id here.
116 # PluginSetting or to use the plugin id here.
117 return 'auth_{}_{}'.format(self.name, name)
117 return 'auth_{}_{}'.format(self.name, name)
118
118
119 def _get_setting_type(self, name, value):
119 def _get_setting_type(self, name, value):
120 """
120 """
121 Get the type as used by the SettingsModel accordingly to type of passed
121 Get the type as used by the SettingsModel accordingly to type of passed
122 value. Optionally the suffix `.encrypted` is appended to instruct
122 value. Optionally the suffix `.encrypted` is appended to instruct
123 SettingsModel to store it encrypted.
123 SettingsModel to store it encrypted.
124 """
124 """
125 type_ = self._settings_type_map.get(type(value), 'unicode')
125 type_ = self._settings_type_map.get(type(value), 'unicode')
126 if name in self._settings_encrypted:
126 if name in self._settings_encrypted:
127 type_ = '{}.encrypted'.format(type_)
127 type_ = '{}.encrypted'.format(type_)
128 return type_
128 return type_
129
129
130 def is_enabled(self):
130 def is_enabled(self):
131 """
131 """
132 Returns true if this plugin is enabled. An enabled plugin can be
132 Returns true if this plugin is enabled. An enabled plugin can be
133 configured in the admin interface but it is not consulted during
133 configured in the admin interface but it is not consulted during
134 authentication.
134 authentication.
135 """
135 """
136 auth_plugins = SettingsModel().get_auth_plugins()
136 auth_plugins = SettingsModel().get_auth_plugins()
137 return self.get_id() in auth_plugins
137 return self.get_id() in auth_plugins
138
138
139 def is_active(self):
139 def is_active(self):
140 """
140 """
141 Returns true if the plugin is activated. An activated plugin is
141 Returns true if the plugin is activated. An activated plugin is
142 consulted during authentication, assumed it is also enabled.
142 consulted during authentication, assumed it is also enabled.
143 """
143 """
144 return self.get_setting_by_name('enabled')
144 return self.get_setting_by_name('enabled')
145
145
146 def get_id(self):
146 def get_id(self):
147 """
147 """
148 Returns the plugin id.
148 Returns the plugin id.
149 """
149 """
150 return self._plugin_id
150 return self._plugin_id
151
151
152 def get_display_name(self):
152 def get_display_name(self):
153 """
153 """
154 Returns a translation string for displaying purposes.
154 Returns a translation string for displaying purposes.
155 """
155 """
156 raise NotImplementedError('Not implemented in base class')
156 raise NotImplementedError('Not implemented in base class')
157
157
158 def get_settings_schema(self):
158 def get_settings_schema(self):
159 """
159 """
160 Returns a colander schema, representing the plugin settings.
160 Returns a colander schema, representing the plugin settings.
161 """
161 """
162 return AuthnPluginSettingsSchemaBase()
162 return AuthnPluginSettingsSchemaBase()
163
163
164 def get_setting_by_name(self, name):
164 def get_setting_by_name(self, name):
165 """
165 """
166 Returns a plugin setting by name.
166 Returns a plugin setting by name.
167 """
167 """
168 full_name = self._get_setting_full_name(name)
168 full_name = self._get_setting_full_name(name)
169 db_setting = SettingsModel().get_setting_by_name(full_name)
169 db_setting = SettingsModel().get_setting_by_name(full_name)
170 return db_setting.app_settings_value if db_setting else None
170 return db_setting.app_settings_value if db_setting else None
171
171
172 def create_or_update_setting(self, name, value):
172 def create_or_update_setting(self, name, value):
173 """
173 """
174 Create or update a setting for this plugin in the persistent storage.
174 Create or update a setting for this plugin in the persistent storage.
175 """
175 """
176 full_name = self._get_setting_full_name(name)
176 full_name = self._get_setting_full_name(name)
177 type_ = self._get_setting_type(name, value)
177 type_ = self._get_setting_type(name, value)
178 db_setting = SettingsModel().create_or_update_setting(
178 db_setting = SettingsModel().create_or_update_setting(
179 full_name, value, type_)
179 full_name, value, type_)
180 return db_setting.app_settings_value
180 return db_setting.app_settings_value
181
181
182 def get_settings(self):
182 def get_settings(self):
183 """
183 """
184 Returns the plugin settings as dictionary.
184 Returns the plugin settings as dictionary.
185 """
185 """
186 settings = {}
186 settings = {}
187 for node in self.get_settings_schema():
187 for node in self.get_settings_schema():
188 settings[node.name] = self.get_setting_by_name(node.name)
188 settings[node.name] = self.get_setting_by_name(node.name)
189 return settings
189 return settings
190
190
191 @property
191 @property
192 def validators(self):
192 def validators(self):
193 """
193 """
194 Exposes RhodeCode validators modules
194 Exposes RhodeCode validators modules
195 """
195 """
196 # this is a hack to overcome issues with pylons threadlocals and
196 # this is a hack to overcome issues with pylons threadlocals and
197 # translator object _() not beein registered properly.
197 # translator object _() not beein registered properly.
198 class LazyCaller(object):
198 class LazyCaller(object):
199 def __init__(self, name):
199 def __init__(self, name):
200 self.validator_name = name
200 self.validator_name = name
201
201
202 def __call__(self, *args, **kwargs):
202 def __call__(self, *args, **kwargs):
203 from rhodecode.model import validators as v
203 from rhodecode.model import validators as v
204 obj = getattr(v, self.validator_name)
204 obj = getattr(v, self.validator_name)
205 # log.debug('Initializing lazy formencode object: %s', obj)
205 # log.debug('Initializing lazy formencode object: %s', obj)
206 return LazyFormencode(obj, *args, **kwargs)
206 return LazyFormencode(obj, *args, **kwargs)
207
207
208 class ProxyGet(object):
208 class ProxyGet(object):
209 def __getattribute__(self, name):
209 def __getattribute__(self, name):
210 return LazyCaller(name)
210 return LazyCaller(name)
211
211
212 return ProxyGet()
212 return ProxyGet()
213
213
214 @hybrid_property
214 @hybrid_property
215 def name(self):
215 def name(self):
216 """
216 """
217 Returns the name of this authentication plugin.
217 Returns the name of this authentication plugin.
218
218
219 :returns: string
219 :returns: string
220 """
220 """
221 raise NotImplementedError("Not implemented in base class")
221 raise NotImplementedError("Not implemented in base class")
222
222
223 @property
223 def is_headers_auth(self):
224 def is_headers_auth(self):
224 """
225 """
225 Returns True if this authentication plugin uses HTTP headers as
226 Returns True if this authentication plugin uses HTTP headers as
226 authentication method.
227 authentication method.
227 """
228 """
228 return False
229 return False
229
230
230 @hybrid_property
231 @hybrid_property
231 def is_container_auth(self):
232 def is_container_auth(self):
232 """
233 """
233 Deprecated method that indicates if this authentication plugin uses
234 Deprecated method that indicates if this authentication plugin uses
234 HTTP headers as authentication method.
235 HTTP headers as authentication method.
235 """
236 """
236 warnings.warn(
237 warnings.warn(
237 'Use is_headers_auth instead.', category=DeprecationWarning)
238 'Use is_headers_auth instead.', category=DeprecationWarning)
238 return self.is_headers_auth
239 return self.is_headers_auth
239
240
240 @hybrid_property
241 @hybrid_property
241 def allows_creating_users(self):
242 def allows_creating_users(self):
242 """
243 """
243 Defines if Plugin allows users to be created on-the-fly when
244 Defines if Plugin allows users to be created on-the-fly when
244 authentication is called. Controls how external plugins should behave
245 authentication is called. Controls how external plugins should behave
245 in terms if they are allowed to create new users, or not. Base plugins
246 in terms if they are allowed to create new users, or not. Base plugins
246 should not be allowed to, but External ones should be !
247 should not be allowed to, but External ones should be !
247
248
248 :return: bool
249 :return: bool
249 """
250 """
250 return False
251 return False
251
252
252 def set_auth_type(self, auth_type):
253 def set_auth_type(self, auth_type):
253 self.auth_type = auth_type
254 self.auth_type = auth_type
254
255
255 def allows_authentication_from(
256 def allows_authentication_from(
256 self, user, allows_non_existing_user=True,
257 self, user, allows_non_existing_user=True,
257 allowed_auth_plugins=None, allowed_auth_sources=None):
258 allowed_auth_plugins=None, allowed_auth_sources=None):
258 """
259 """
259 Checks if this authentication module should accept a request for
260 Checks if this authentication module should accept a request for
260 the current user.
261 the current user.
261
262
262 :param user: user object fetched using plugin's get_user() method.
263 :param user: user object fetched using plugin's get_user() method.
263 :param allows_non_existing_user: if True, don't allow the
264 :param allows_non_existing_user: if True, don't allow the
264 user to be empty, meaning not existing in our database
265 user to be empty, meaning not existing in our database
265 :param allowed_auth_plugins: if provided, users extern_type will be
266 :param allowed_auth_plugins: if provided, users extern_type will be
266 checked against a list of provided extern types, which are plugin
267 checked against a list of provided extern types, which are plugin
267 auth_names in the end
268 auth_names in the end
268 :param allowed_auth_sources: authentication type allowed,
269 :param allowed_auth_sources: authentication type allowed,
269 `http` or `vcs` default is both.
270 `http` or `vcs` default is both.
270 defines if plugin will accept only http authentication vcs
271 defines if plugin will accept only http authentication vcs
271 authentication(git/hg) or both
272 authentication(git/hg) or both
272 :returns: boolean
273 :returns: boolean
273 """
274 """
274 if not user and not allows_non_existing_user:
275 if not user and not allows_non_existing_user:
275 log.debug('User is empty but plugin does not allow empty users,'
276 log.debug('User is empty but plugin does not allow empty users,'
276 'not allowed to authenticate')
277 'not allowed to authenticate')
277 return False
278 return False
278
279
279 expected_auth_plugins = allowed_auth_plugins or [self.name]
280 expected_auth_plugins = allowed_auth_plugins or [self.name]
280 if user and (user.extern_type and
281 if user and (user.extern_type and
281 user.extern_type not in expected_auth_plugins):
282 user.extern_type not in expected_auth_plugins):
282 log.debug(
283 log.debug(
283 'User `%s` is bound to `%s` auth type. Plugin allows only '
284 'User `%s` is bound to `%s` auth type. Plugin allows only '
284 '%s, skipping', user, user.extern_type, expected_auth_plugins)
285 '%s, skipping', user, user.extern_type, expected_auth_plugins)
285
286
286 return False
287 return False
287
288
288 # by default accept both
289 # by default accept both
289 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
290 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
290 if self.auth_type not in expected_auth_from:
291 if self.auth_type not in expected_auth_from:
291 log.debug('Current auth source is %s but plugin only allows %s',
292 log.debug('Current auth source is %s but plugin only allows %s',
292 self.auth_type, expected_auth_from)
293 self.auth_type, expected_auth_from)
293 return False
294 return False
294
295
295 return True
296 return True
296
297
297 def get_user(self, username=None, **kwargs):
298 def get_user(self, username=None, **kwargs):
298 """
299 """
299 Helper method for user fetching in plugins, by default it's using
300 Helper method for user fetching in plugins, by default it's using
300 simple fetch by username, but this method can be custimized in plugins
301 simple fetch by username, but this method can be custimized in plugins
301 eg. headers auth plugin to fetch user by environ params
302 eg. headers auth plugin to fetch user by environ params
302
303
303 :param username: username if given to fetch from database
304 :param username: username if given to fetch from database
304 :param kwargs: extra arguments needed for user fetching.
305 :param kwargs: extra arguments needed for user fetching.
305 """
306 """
306 user = None
307 user = None
307 log.debug(
308 log.debug(
308 'Trying to fetch user `%s` from RhodeCode database', username)
309 'Trying to fetch user `%s` from RhodeCode database', username)
309 if username:
310 if username:
310 user = User.get_by_username(username)
311 user = User.get_by_username(username)
311 if not user:
312 if not user:
312 log.debug('User not found, fallback to fetch user in '
313 log.debug('User not found, fallback to fetch user in '
313 'case insensitive mode')
314 'case insensitive mode')
314 user = User.get_by_username(username, case_insensitive=True)
315 user = User.get_by_username(username, case_insensitive=True)
315 else:
316 else:
316 log.debug('provided username:`%s` is empty skipping...', username)
317 log.debug('provided username:`%s` is empty skipping...', username)
317 if not user:
318 if not user:
318 log.debug('User `%s` not found in database', username)
319 log.debug('User `%s` not found in database', username)
319 return user
320 return user
320
321
321 def user_activation_state(self):
322 def user_activation_state(self):
322 """
323 """
323 Defines user activation state when creating new users
324 Defines user activation state when creating new users
324
325
325 :returns: boolean
326 :returns: boolean
326 """
327 """
327 raise NotImplementedError("Not implemented in base class")
328 raise NotImplementedError("Not implemented in base class")
328
329
329 def auth(self, userobj, username, passwd, settings, **kwargs):
330 def auth(self, userobj, username, passwd, settings, **kwargs):
330 """
331 """
331 Given a user object (which may be null), username, a plaintext
332 Given a user object (which may be null), username, a plaintext
332 password, and a settings object (containing all the keys needed as
333 password, and a settings object (containing all the keys needed as
333 listed in settings()), authenticate this user's login attempt.
334 listed in settings()), authenticate this user's login attempt.
334
335
335 Return None on failure. On success, return a dictionary of the form:
336 Return None on failure. On success, return a dictionary of the form:
336
337
337 see: RhodeCodeAuthPluginBase.auth_func_attrs
338 see: RhodeCodeAuthPluginBase.auth_func_attrs
338 This is later validated for correctness
339 This is later validated for correctness
339 """
340 """
340 raise NotImplementedError("not implemented in base class")
341 raise NotImplementedError("not implemented in base class")
341
342
342 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
343 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
343 """
344 """
344 Wrapper to call self.auth() that validates call on it
345 Wrapper to call self.auth() that validates call on it
345
346
346 :param userobj: userobj
347 :param userobj: userobj
347 :param username: username
348 :param username: username
348 :param passwd: plaintext password
349 :param passwd: plaintext password
349 :param settings: plugin settings
350 :param settings: plugin settings
350 """
351 """
351 auth = self.auth(userobj, username, passwd, settings, **kwargs)
352 auth = self.auth(userobj, username, passwd, settings, **kwargs)
352 if auth:
353 if auth:
353 # check if hash should be migrated ?
354 # check if hash should be migrated ?
354 new_hash = auth.get('_hash_migrate')
355 new_hash = auth.get('_hash_migrate')
355 if new_hash:
356 if new_hash:
356 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
357 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
357 return self._validate_auth_return(auth)
358 return self._validate_auth_return(auth)
358 return auth
359 return auth
359
360
360 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
361 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
361 new_hash_cypher = _RhodeCodeCryptoBCrypt()
362 new_hash_cypher = _RhodeCodeCryptoBCrypt()
362 # extra checks, so make sure new hash is correct.
363 # extra checks, so make sure new hash is correct.
363 password_encoded = safe_str(password)
364 password_encoded = safe_str(password)
364 if new_hash and new_hash_cypher.hash_check(
365 if new_hash and new_hash_cypher.hash_check(
365 password_encoded, new_hash):
366 password_encoded, new_hash):
366 cur_user = User.get_by_username(username)
367 cur_user = User.get_by_username(username)
367 cur_user.password = new_hash
368 cur_user.password = new_hash
368 Session().add(cur_user)
369 Session().add(cur_user)
369 Session().flush()
370 Session().flush()
370 log.info('Migrated user %s hash to bcrypt', cur_user)
371 log.info('Migrated user %s hash to bcrypt', cur_user)
371
372
372 def _validate_auth_return(self, ret):
373 def _validate_auth_return(self, ret):
373 if not isinstance(ret, dict):
374 if not isinstance(ret, dict):
374 raise Exception('returned value from auth must be a dict')
375 raise Exception('returned value from auth must be a dict')
375 for k in self.auth_func_attrs:
376 for k in self.auth_func_attrs:
376 if k not in ret:
377 if k not in ret:
377 raise Exception('Missing %s attribute from returned data' % k)
378 raise Exception('Missing %s attribute from returned data' % k)
378 return ret
379 return ret
379
380
380
381
381 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
382 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
382
383
383 @hybrid_property
384 @hybrid_property
384 def allows_creating_users(self):
385 def allows_creating_users(self):
385 return True
386 return True
386
387
387 def use_fake_password(self):
388 def use_fake_password(self):
388 """
389 """
389 Return a boolean that indicates whether or not we should set the user's
390 Return a boolean that indicates whether or not we should set the user's
390 password to a random value when it is authenticated by this plugin.
391 password to a random value when it is authenticated by this plugin.
391 If your plugin provides authentication, then you will generally
392 If your plugin provides authentication, then you will generally
392 want this.
393 want this.
393
394
394 :returns: boolean
395 :returns: boolean
395 """
396 """
396 raise NotImplementedError("Not implemented in base class")
397 raise NotImplementedError("Not implemented in base class")
397
398
398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
399 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
399 # at this point _authenticate calls plugin's `auth()` function
400 # at this point _authenticate calls plugin's `auth()` function
400 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
401 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
401 userobj, username, passwd, settings, **kwargs)
402 userobj, username, passwd, settings, **kwargs)
402 if auth:
403 if auth:
403 # maybe plugin will clean the username ?
404 # maybe plugin will clean the username ?
404 # we should use the return value
405 # we should use the return value
405 username = auth['username']
406 username = auth['username']
406
407
407 # if external source tells us that user is not active, we should
408 # if external source tells us that user is not active, we should
408 # skip rest of the process. This can prevent from creating users in
409 # skip rest of the process. This can prevent from creating users in
409 # RhodeCode when using external authentication, but if it's
410 # RhodeCode when using external authentication, but if it's
410 # inactive user we shouldn't create that user anyway
411 # inactive user we shouldn't create that user anyway
411 if auth['active_from_extern'] is False:
412 if auth['active_from_extern'] is False:
412 log.warning(
413 log.warning(
413 "User %s authenticated against %s, but is inactive",
414 "User %s authenticated against %s, but is inactive",
414 username, self.__module__)
415 username, self.__module__)
415 return None
416 return None
416
417
417 cur_user = User.get_by_username(username, case_insensitive=True)
418 cur_user = User.get_by_username(username, case_insensitive=True)
418 is_user_existing = cur_user is not None
419 is_user_existing = cur_user is not None
419
420
420 if is_user_existing:
421 if is_user_existing:
421 log.debug('Syncing user `%s` from '
422 log.debug('Syncing user `%s` from '
422 '`%s` plugin', username, self.name)
423 '`%s` plugin', username, self.name)
423 else:
424 else:
424 log.debug('Creating non existing user `%s` from '
425 log.debug('Creating non existing user `%s` from '
425 '`%s` plugin', username, self.name)
426 '`%s` plugin', username, self.name)
426
427
427 if self.allows_creating_users:
428 if self.allows_creating_users:
428 log.debug('Plugin `%s` allows to '
429 log.debug('Plugin `%s` allows to '
429 'create new users', self.name)
430 'create new users', self.name)
430 else:
431 else:
431 log.debug('Plugin `%s` does not allow to '
432 log.debug('Plugin `%s` does not allow to '
432 'create new users', self.name)
433 'create new users', self.name)
433
434
434 user_parameters = {
435 user_parameters = {
435 'username': username,
436 'username': username,
436 'email': auth["email"],
437 'email': auth["email"],
437 'firstname': auth["firstname"],
438 'firstname': auth["firstname"],
438 'lastname': auth["lastname"],
439 'lastname': auth["lastname"],
439 'active': auth["active"],
440 'active': auth["active"],
440 'admin': auth["admin"],
441 'admin': auth["admin"],
441 'extern_name': auth["extern_name"],
442 'extern_name': auth["extern_name"],
442 'extern_type': self.name,
443 'extern_type': self.name,
443 'plugin': self,
444 'plugin': self,
444 'allow_to_create_user': self.allows_creating_users,
445 'allow_to_create_user': self.allows_creating_users,
445 }
446 }
446
447
447 if not is_user_existing:
448 if not is_user_existing:
448 if self.use_fake_password():
449 if self.use_fake_password():
449 # Randomize the PW because we don't need it, but don't want
450 # Randomize the PW because we don't need it, but don't want
450 # them blank either
451 # them blank either
451 passwd = PasswordGenerator().gen_password(length=16)
452 passwd = PasswordGenerator().gen_password(length=16)
452 user_parameters['password'] = passwd
453 user_parameters['password'] = passwd
453 else:
454 else:
454 # Since the password is required by create_or_update method of
455 # Since the password is required by create_or_update method of
455 # UserModel, we need to set it explicitly.
456 # UserModel, we need to set it explicitly.
456 # The create_or_update method is smart and recognises the
457 # The create_or_update method is smart and recognises the
457 # password hashes as well.
458 # password hashes as well.
458 user_parameters['password'] = cur_user.password
459 user_parameters['password'] = cur_user.password
459
460
460 # we either create or update users, we also pass the flag
461 # we either create or update users, we also pass the flag
461 # that controls if this method can actually do that.
462 # that controls if this method can actually do that.
462 # raises NotAllowedToCreateUserError if it cannot, and we try to.
463 # raises NotAllowedToCreateUserError if it cannot, and we try to.
463 user = UserModel().create_or_update(**user_parameters)
464 user = UserModel().create_or_update(**user_parameters)
464 Session().flush()
465 Session().flush()
465 # enforce user is just in given groups, all of them has to be ones
466 # enforce user is just in given groups, all of them has to be ones
466 # created from plugins. We store this info in _group_data JSON
467 # created from plugins. We store this info in _group_data JSON
467 # field
468 # field
468 try:
469 try:
469 groups = auth['groups'] or []
470 groups = auth['groups'] or []
470 UserGroupModel().enforce_groups(user, groups, self.name)
471 UserGroupModel().enforce_groups(user, groups, self.name)
471 except Exception:
472 except Exception:
472 # for any reason group syncing fails, we should
473 # for any reason group syncing fails, we should
473 # proceed with login
474 # proceed with login
474 log.error(traceback.format_exc())
475 log.error(traceback.format_exc())
475 Session().commit()
476 Session().commit()
476 return auth
477 return auth
477
478
478
479
479 def loadplugin(plugin_id):
480 def loadplugin(plugin_id):
480 """
481 """
481 Loads and returns an instantiated authentication plugin.
482 Loads and returns an instantiated authentication plugin.
482 Returns the RhodeCodeAuthPluginBase subclass on success,
483 Returns the RhodeCodeAuthPluginBase subclass on success,
483 or None on failure.
484 or None on failure.
484 """
485 """
485 # TODO: Disusing pyramids thread locals to retrieve the registry.
486 # TODO: Disusing pyramids thread locals to retrieve the registry.
486 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
487 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
487 plugin = authn_registry.get_plugin(plugin_id)
488 plugin = authn_registry.get_plugin(plugin_id)
488 if plugin is None:
489 if plugin is None:
489 log.error('Authentication plugin not found: "%s"', plugin_id)
490 log.error('Authentication plugin not found: "%s"', plugin_id)
490 return plugin
491 return plugin
491
492
492
493
493 def get_auth_cache_manager(custom_ttl=None):
494 def get_auth_cache_manager(custom_ttl=None):
494 return caches.get_cache_manager(
495 return caches.get_cache_manager(
495 'auth_plugins', 'rhodecode.authentication', custom_ttl)
496 'auth_plugins', 'rhodecode.authentication', custom_ttl)
496
497
497
498
498 def authenticate(username, password, environ=None, auth_type=None,
499 def authenticate(username, password, environ=None, auth_type=None,
499 skip_missing=False):
500 skip_missing=False):
500 """
501 """
501 Authentication function used for access control,
502 Authentication function used for access control,
502 It tries to authenticate based on enabled authentication modules.
503 It tries to authenticate based on enabled authentication modules.
503
504
504 :param username: username can be empty for headers auth
505 :param username: username can be empty for headers auth
505 :param password: password can be empty for headers auth
506 :param password: password can be empty for headers auth
506 :param environ: environ headers passed for headers auth
507 :param environ: environ headers passed for headers auth
507 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
508 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
508 :param skip_missing: ignores plugins that are in db but not in environment
509 :param skip_missing: ignores plugins that are in db but not in environment
509 :returns: None if auth failed, plugin_user dict if auth is correct
510 :returns: None if auth failed, plugin_user dict if auth is correct
510 """
511 """
511 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
512 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
512 raise ValueError('auth type must be on of http, vcs got "%s" instead'
513 raise ValueError('auth type must be on of http, vcs got "%s" instead'
513 % auth_type)
514 % auth_type)
514 headers_only = environ and not (username and password)
515 headers_only = environ and not (username and password)
515
516
516 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
517 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
517 for plugin in authn_registry.get_plugins_for_authentication():
518 for plugin in authn_registry.get_plugins_for_authentication():
518 plugin.set_auth_type(auth_type)
519 plugin.set_auth_type(auth_type)
519 user = plugin.get_user(username)
520 user = plugin.get_user(username)
520 display_user = user.username if user else username
521 display_user = user.username if user else username
521
522
522 if headers_only and not plugin.is_headers_auth:
523 if headers_only and not plugin.is_headers_auth:
523 log.debug('Auth type is for headers only and plugin `%s` is not '
524 log.debug('Auth type is for headers only and plugin `%s` is not '
524 'headers plugin, skipping...', plugin.get_id())
525 'headers plugin, skipping...', plugin.get_id())
525 continue
526 continue
526
527
527 # load plugin settings from RhodeCode database
528 # load plugin settings from RhodeCode database
528 plugin_settings = plugin.get_settings()
529 plugin_settings = plugin.get_settings()
529 log.debug('Plugin settings:%s', plugin_settings)
530 log.debug('Plugin settings:%s', plugin_settings)
530
531
531 log.debug('Trying authentication using ** %s **', plugin.get_id())
532 log.debug('Trying authentication using ** %s **', plugin.get_id())
532 # use plugin's method of user extraction.
533 # use plugin's method of user extraction.
533 user = plugin.get_user(username, environ=environ,
534 user = plugin.get_user(username, environ=environ,
534 settings=plugin_settings)
535 settings=plugin_settings)
535 display_user = user.username if user else username
536 display_user = user.username if user else username
536 log.debug(
537 log.debug(
537 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
538 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
538
539
539 if not plugin.allows_authentication_from(user):
540 if not plugin.allows_authentication_from(user):
540 log.debug('Plugin %s does not accept user `%s` for authentication',
541 log.debug('Plugin %s does not accept user `%s` for authentication',
541 plugin.get_id(), display_user)
542 plugin.get_id(), display_user)
542 continue
543 continue
543 else:
544 else:
544 log.debug('Plugin %s accepted user `%s` for authentication',
545 log.debug('Plugin %s accepted user `%s` for authentication',
545 plugin.get_id(), display_user)
546 plugin.get_id(), display_user)
546
547
547 log.info('Authenticating user `%s` using %s plugin',
548 log.info('Authenticating user `%s` using %s plugin',
548 display_user, plugin.get_id())
549 display_user, plugin.get_id())
549
550
550 _cache_ttl = 0
551 _cache_ttl = 0
551
552
552 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
553 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
553 # plugin cache set inside is more important than the settings value
554 # plugin cache set inside is more important than the settings value
554 _cache_ttl = plugin.AUTH_CACHE_TTL
555 _cache_ttl = plugin.AUTH_CACHE_TTL
555 elif plugin_settings.get('auth_cache_ttl'):
556 elif plugin_settings.get('auth_cache_ttl'):
556 _cache_ttl = safe_int(plugin_settings.get('auth_cache_ttl'), 0)
557 _cache_ttl = safe_int(plugin_settings.get('auth_cache_ttl'), 0)
557
558
558 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
559 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
559
560
560 # get instance of cache manager configured for a namespace
561 # get instance of cache manager configured for a namespace
561 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
562 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
562
563
563 log.debug('Cache for plugin `%s` active: %s', plugin.get_id(),
564 log.debug('Cache for plugin `%s` active: %s', plugin.get_id(),
564 plugin_cache_active)
565 plugin_cache_active)
565
566
566 # for environ based password can be empty, but then the validation is
567 # for environ based password can be empty, but then the validation is
567 # on the server that fills in the env data needed for authentication
568 # on the server that fills in the env data needed for authentication
568 _password_hash = md5_safe(plugin.name + username + (password or ''))
569 _password_hash = md5_safe(plugin.name + username + (password or ''))
569
570
570 # _authenticate is a wrapper for .auth() method of plugin.
571 # _authenticate is a wrapper for .auth() method of plugin.
571 # it checks if .auth() sends proper data.
572 # it checks if .auth() sends proper data.
572 # For RhodeCodeExternalAuthPlugin it also maps users to
573 # For RhodeCodeExternalAuthPlugin it also maps users to
573 # Database and maps the attributes returned from .auth()
574 # Database and maps the attributes returned from .auth()
574 # to RhodeCode database. If this function returns data
575 # to RhodeCode database. If this function returns data
575 # then auth is correct.
576 # then auth is correct.
576 start = time.time()
577 start = time.time()
577 log.debug('Running plugin `%s` _authenticate method',
578 log.debug('Running plugin `%s` _authenticate method',
578 plugin.get_id())
579 plugin.get_id())
579
580
580 def auth_func():
581 def auth_func():
581 """
582 """
582 This function is used internally in Cache of Beaker to calculate
583 This function is used internally in Cache of Beaker to calculate
583 Results
584 Results
584 """
585 """
585 return plugin._authenticate(
586 return plugin._authenticate(
586 user, username, password, plugin_settings,
587 user, username, password, plugin_settings,
587 environ=environ or {})
588 environ=environ or {})
588
589
589 if plugin_cache_active:
590 if plugin_cache_active:
590 plugin_user = cache_manager.get(
591 plugin_user = cache_manager.get(
591 _password_hash, createfunc=auth_func)
592 _password_hash, createfunc=auth_func)
592 else:
593 else:
593 plugin_user = auth_func()
594 plugin_user = auth_func()
594
595
595 auth_time = time.time() - start
596 auth_time = time.time() - start
596 log.debug('Authentication for plugin `%s` completed in %.3fs, '
597 log.debug('Authentication for plugin `%s` completed in %.3fs, '
597 'expiration time of fetched cache %.1fs.',
598 'expiration time of fetched cache %.1fs.',
598 plugin.get_id(), auth_time, _cache_ttl)
599 plugin.get_id(), auth_time, _cache_ttl)
599
600
600 log.debug('PLUGIN USER DATA: %s', plugin_user)
601 log.debug('PLUGIN USER DATA: %s', plugin_user)
601
602
602 if plugin_user:
603 if plugin_user:
603 log.debug('Plugin returned proper authentication data')
604 log.debug('Plugin returned proper authentication data')
604 return plugin_user
605 return plugin_user
605 # we failed to Auth because .auth() method didn't return proper user
606 # we failed to Auth because .auth() method didn't return proper user
606 log.debug("User `%s` failed to authenticate against %s",
607 log.debug("User `%s` failed to authenticate against %s",
607 display_user, plugin.get_id())
608 display_user, plugin.get_id())
608 return None
609 return None
@@ -1,225 +1,225 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 sqlalchemy.ext.hybrid import hybrid_property
24 from sqlalchemy.ext.hybrid import hybrid_property
25
25
26 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
26 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
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 from rhodecode.translation import _
32 from rhodecode.translation import _
33
33
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 def plugin_factory(plugin_id, *args, **kwds):
38 def plugin_factory(plugin_id, *args, **kwds):
39 """
39 """
40 Factory function that is called during plugin discovery.
40 Factory function that is called during plugin discovery.
41 It returns the plugin instance.
41 It returns the plugin instance.
42 """
42 """
43 plugin = RhodeCodeAuthPlugin(plugin_id)
43 plugin = RhodeCodeAuthPlugin(plugin_id)
44 return plugin
44 return plugin
45
45
46
46
47 class HeadersAuthnResource(AuthnPluginResourceBase):
47 class HeadersAuthnResource(AuthnPluginResourceBase):
48 pass
48 pass
49
49
50
50
51 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
52 header = colander.SchemaNode(
52 header = colander.SchemaNode(
53 colander.String(),
53 colander.String(),
54 default='REMOTE_USER',
54 default='REMOTE_USER',
55 description=_('Header to extract the user from'),
55 description=_('Header to extract the user from'),
56 preparer=strip_whitespace,
56 preparer=strip_whitespace,
57 title=_('Header'),
57 title=_('Header'),
58 widget='string')
58 widget='string')
59 fallback_header = colander.SchemaNode(
59 fallback_header = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 default='HTTP_X_FORWARDED_USER',
61 default='HTTP_X_FORWARDED_USER',
62 description=_('Header to extract the user from when main one fails'),
62 description=_('Header to extract the user from when main one fails'),
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 title=_('Fallback header'),
64 title=_('Fallback header'),
65 widget='string')
65 widget='string')
66 clean_username = colander.SchemaNode(
66 clean_username = colander.SchemaNode(
67 colander.Boolean(),
67 colander.Boolean(),
68 default=True,
68 default=True,
69 description=_('Perform cleaning of user, if passed user has @ in '
69 description=_('Perform cleaning of user, if passed user has @ in '
70 'username then first part before @ is taken. '
70 'username then first part before @ is taken. '
71 'If there\'s \\ in the username only the part after '
71 'If there\'s \\ in the username only the part after '
72 ' \\ is taken'),
72 ' \\ is taken'),
73 missing=False,
73 missing=False,
74 title=_('Clean username'),
74 title=_('Clean username'),
75 widget='bool')
75 widget='bool')
76
76
77
77
78 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
79
79
80 def includeme(self, config):
80 def includeme(self, config):
81 config.add_authn_plugin(self)
81 config.add_authn_plugin(self)
82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
83 config.add_view(
83 config.add_view(
84 'rhodecode.authentication.views.AuthnPluginViewBase',
84 'rhodecode.authentication.views.AuthnPluginViewBase',
85 attr='settings_get',
85 attr='settings_get',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
87 request_method='GET',
87 request_method='GET',
88 route_name='auth_home',
88 route_name='auth_home',
89 context=HeadersAuthnResource)
89 context=HeadersAuthnResource)
90 config.add_view(
90 config.add_view(
91 'rhodecode.authentication.views.AuthnPluginViewBase',
91 'rhodecode.authentication.views.AuthnPluginViewBase',
92 attr='settings_post',
92 attr='settings_post',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
94 request_method='POST',
94 request_method='POST',
95 route_name='auth_home',
95 route_name='auth_home',
96 context=HeadersAuthnResource)
96 context=HeadersAuthnResource)
97
97
98 def get_display_name(self):
98 def get_display_name(self):
99 return _('Headers')
99 return _('Headers')
100
100
101 def get_settings_schema(self):
101 def get_settings_schema(self):
102 return HeadersSettingsSchema()
102 return HeadersSettingsSchema()
103
103
104 @hybrid_property
104 @hybrid_property
105 def name(self):
105 def name(self):
106 return 'headers'
106 return 'headers'
107
107
108 @hybrid_property
108 @property
109 def is_container_auth(self):
109 def is_headers_auth(self):
110 return True
110 return True
111
111
112 def use_fake_password(self):
112 def use_fake_password(self):
113 return True
113 return True
114
114
115 def user_activation_state(self):
115 def user_activation_state(self):
116 def_user_perms = User.get_default_user().AuthUser.permissions['global']
116 def_user_perms = User.get_default_user().AuthUser.permissions['global']
117 return 'hg.extern_activate.auto' in def_user_perms
117 return 'hg.extern_activate.auto' in def_user_perms
118
118
119 def _clean_username(self, username):
119 def _clean_username(self, username):
120 # Removing realm and domain from username
120 # Removing realm and domain from username
121 username = username.split('@')[0]
121 username = username.split('@')[0]
122 username = username.rsplit('\\')[-1]
122 username = username.rsplit('\\')[-1]
123 return username
123 return username
124
124
125 def _get_username(self, environ, settings):
125 def _get_username(self, environ, settings):
126 username = None
126 username = None
127 environ = environ or {}
127 environ = environ or {}
128 if not environ:
128 if not environ:
129 log.debug('got empty environ: %s' % environ)
129 log.debug('got empty environ: %s' % environ)
130
130
131 settings = settings or {}
131 settings = settings or {}
132 if settings.get('header'):
132 if settings.get('header'):
133 header = settings.get('header')
133 header = settings.get('header')
134 username = environ.get(header)
134 username = environ.get(header)
135 log.debug('extracted %s:%s' % (header, username))
135 log.debug('extracted %s:%s' % (header, username))
136
136
137 # fallback mode
137 # fallback mode
138 if not username and settings.get('fallback_header'):
138 if not username and settings.get('fallback_header'):
139 header = settings.get('fallback_header')
139 header = settings.get('fallback_header')
140 username = environ.get(header)
140 username = environ.get(header)
141 log.debug('extracted %s:%s' % (header, username))
141 log.debug('extracted %s:%s' % (header, username))
142
142
143 if username and str2bool(settings.get('clean_username')):
143 if username and str2bool(settings.get('clean_username')):
144 log.debug('Received username `%s` from headers' % username)
144 log.debug('Received username `%s` from headers' % username)
145 username = self._clean_username(username)
145 username = self._clean_username(username)
146 log.debug('New cleanup user is:%s' % username)
146 log.debug('New cleanup user is:%s' % username)
147 return username
147 return username
148
148
149 def get_user(self, username=None, **kwargs):
149 def get_user(self, username=None, **kwargs):
150 """
150 """
151 Helper method for user fetching in plugins, by default it's using
151 Helper method for user fetching in plugins, by default it's using
152 simple fetch by username, but this method can be custimized in plugins
152 simple fetch by username, but this method can be custimized in plugins
153 eg. headers auth plugin to fetch user by environ params
153 eg. headers auth plugin to fetch user by environ params
154 :param username: username if given to fetch
154 :param username: username if given to fetch
155 :param kwargs: extra arguments needed for user fetching.
155 :param kwargs: extra arguments needed for user fetching.
156 """
156 """
157 environ = kwargs.get('environ') or {}
157 environ = kwargs.get('environ') or {}
158 settings = kwargs.get('settings') or {}
158 settings = kwargs.get('settings') or {}
159 username = self._get_username(environ, settings)
159 username = self._get_username(environ, settings)
160 # we got the username, so use default method now
160 # we got the username, so use default method now
161 return super(RhodeCodeAuthPlugin, self).get_user(username)
161 return super(RhodeCodeAuthPlugin, self).get_user(username)
162
162
163 def auth(self, userobj, username, password, settings, **kwargs):
163 def auth(self, userobj, username, password, settings, **kwargs):
164 """
164 """
165 Get's the headers_auth username (or email). It tries to get username
165 Get's the headers_auth username (or email). It tries to get username
166 from REMOTE_USER if this plugin is enabled, if that fails
166 from REMOTE_USER if this plugin is enabled, if that fails
167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
168 is set. clean_username extracts the username from this data if it's
168 is set. clean_username extracts the username from this data if it's
169 having @ in it.
169 having @ in it.
170 Return None on failure. On success, return a dictionary of the form:
170 Return None on failure. On success, return a dictionary of the form:
171
171
172 see: RhodeCodeAuthPluginBase.auth_func_attrs
172 see: RhodeCodeAuthPluginBase.auth_func_attrs
173
173
174 :param userobj:
174 :param userobj:
175 :param username:
175 :param username:
176 :param password:
176 :param password:
177 :param settings:
177 :param settings:
178 :param kwargs:
178 :param kwargs:
179 """
179 """
180 environ = kwargs.get('environ')
180 environ = kwargs.get('environ')
181 if not environ:
181 if not environ:
182 log.debug('Empty environ data skipping...')
182 log.debug('Empty environ data skipping...')
183 return None
183 return None
184
184
185 if not userobj:
185 if not userobj:
186 userobj = self.get_user('', environ=environ, settings=settings)
186 userobj = self.get_user('', environ=environ, settings=settings)
187
187
188 # we don't care passed username/password for headers auth plugins.
188 # we don't care passed username/password for headers auth plugins.
189 # only way to log in is using environ
189 # only way to log in is using environ
190 username = None
190 username = None
191 if userobj:
191 if userobj:
192 username = getattr(userobj, 'username')
192 username = getattr(userobj, 'username')
193
193
194 if not username:
194 if not username:
195 # we don't have any objects in DB user doesn't exist extract
195 # we don't have any objects in DB user doesn't exist extract
196 # username from environ based on the settings
196 # username from environ based on the settings
197 username = self._get_username(environ, settings)
197 username = self._get_username(environ, settings)
198
198
199 # if cannot fetch username, it's a no-go for this plugin to proceed
199 # if cannot fetch username, it's a no-go for this plugin to proceed
200 if not username:
200 if not username:
201 return None
201 return None
202
202
203 # old attrs fetched from RhodeCode database
203 # old attrs fetched from RhodeCode database
204 admin = getattr(userobj, 'admin', False)
204 admin = getattr(userobj, 'admin', False)
205 active = getattr(userobj, 'active', True)
205 active = getattr(userobj, 'active', True)
206 email = getattr(userobj, 'email', '')
206 email = getattr(userobj, 'email', '')
207 firstname = getattr(userobj, 'firstname', '')
207 firstname = getattr(userobj, 'firstname', '')
208 lastname = getattr(userobj, 'lastname', '')
208 lastname = getattr(userobj, 'lastname', '')
209 extern_type = getattr(userobj, 'extern_type', '')
209 extern_type = getattr(userobj, 'extern_type', '')
210
210
211 user_attrs = {
211 user_attrs = {
212 'username': username,
212 'username': username,
213 'firstname': safe_unicode(firstname or username),
213 'firstname': safe_unicode(firstname or username),
214 'lastname': safe_unicode(lastname or ''),
214 'lastname': safe_unicode(lastname or ''),
215 'groups': [],
215 'groups': [],
216 '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
@@ -1,167 +1,167 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 pylons.i18n.translation import lazy_ugettext as _
33 from pylons.i18n.translation import lazy_ugettext as _
34 from sqlalchemy.ext.hybrid import hybrid_property
34 from sqlalchemy.ext.hybrid import hybrid_property
35
35
36 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
36 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.authentication.routes import AuthnPluginResourceBase
39 from rhodecode.lib.colander_utils import strip_whitespace
39 from rhodecode.lib.colander_utils import strip_whitespace
40 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.lib.utils2 import safe_unicode
41 from rhodecode.model.db import User
41 from rhodecode.model.db import User
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 def plugin_factory(plugin_id, *args, **kwds):
46 def plugin_factory(plugin_id, *args, **kwds):
47 """
47 """
48 Factory function that is called during plugin discovery.
48 Factory function that is called during plugin discovery.
49 It returns the plugin instance.
49 It returns the plugin instance.
50 """
50 """
51 plugin = RhodeCodeAuthPlugin(plugin_id)
51 plugin = RhodeCodeAuthPlugin(plugin_id)
52 return plugin
52 return plugin
53
53
54
54
55 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 class JasigCasAuthnResource(AuthnPluginResourceBase):
56 pass
56 pass
57
57
58
58
59 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
60 service_url = colander.SchemaNode(
60 service_url = colander.SchemaNode(
61 colander.String(),
61 colander.String(),
62 default='https://domain.com/cas/v1/tickets',
62 default='https://domain.com/cas/v1/tickets',
63 description=_('The url of the Jasig CAS REST service'),
63 description=_('The url of the Jasig CAS REST service'),
64 preparer=strip_whitespace,
64 preparer=strip_whitespace,
65 title=_('URL'),
65 title=_('URL'),
66 widget='string')
66 widget='string')
67
67
68
68
69 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
70
70
71 def includeme(self, config):
71 def includeme(self, config):
72 config.add_authn_plugin(self)
72 config.add_authn_plugin(self)
73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
74 config.add_view(
74 config.add_view(
75 'rhodecode.authentication.views.AuthnPluginViewBase',
75 'rhodecode.authentication.views.AuthnPluginViewBase',
76 attr='settings_get',
76 attr='settings_get',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
78 request_method='GET',
78 request_method='GET',
79 route_name='auth_home',
79 route_name='auth_home',
80 context=JasigCasAuthnResource)
80 context=JasigCasAuthnResource)
81 config.add_view(
81 config.add_view(
82 'rhodecode.authentication.views.AuthnPluginViewBase',
82 'rhodecode.authentication.views.AuthnPluginViewBase',
83 attr='settings_post',
83 attr='settings_post',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
85 request_method='POST',
85 request_method='POST',
86 route_name='auth_home',
86 route_name='auth_home',
87 context=JasigCasAuthnResource)
87 context=JasigCasAuthnResource)
88
88
89 def get_settings_schema(self):
89 def get_settings_schema(self):
90 return JasigCasSettingsSchema()
90 return JasigCasSettingsSchema()
91
91
92 def get_display_name(self):
92 def get_display_name(self):
93 return _('Jasig-CAS')
93 return _('Jasig-CAS')
94
94
95 @hybrid_property
95 @hybrid_property
96 def name(self):
96 def name(self):
97 return "jasig-cas"
97 return "jasig-cas"
98
98
99 @hybrid_property
99 @property
100 def is_container_auth(self):
100 def is_headers_auth(self):
101 return True
101 return True
102
102
103 def use_fake_password(self):
103 def use_fake_password(self):
104 return True
104 return True
105
105
106 def user_activation_state(self):
106 def user_activation_state(self):
107 def_user_perms = User.get_default_user().AuthUser.permissions['global']
107 def_user_perms = User.get_default_user().AuthUser.permissions['global']
108 return 'hg.extern_activate.auto' in def_user_perms
108 return 'hg.extern_activate.auto' in def_user_perms
109
109
110 def auth(self, userobj, username, password, settings, **kwargs):
110 def auth(self, userobj, username, password, settings, **kwargs):
111 """
111 """
112 Given a user object (which may be null), username, a plaintext password,
112 Given a user object (which may be null), username, a plaintext password,
113 and a settings object (containing all the keys needed as listed in settings()),
113 and a settings object (containing all the keys needed as listed in settings()),
114 authenticate this user's login attempt.
114 authenticate this user's login attempt.
115
115
116 Return None on failure. On success, return a dictionary of the form:
116 Return None on failure. On success, return a dictionary of the form:
117
117
118 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 see: RhodeCodeAuthPluginBase.auth_func_attrs
119 This is later validated for correctness
119 This is later validated for correctness
120 """
120 """
121 if not username or not password:
121 if not username or not password:
122 log.debug('Empty username or password skipping...')
122 log.debug('Empty username or password skipping...')
123 return None
123 return None
124
124
125 log.debug("Jasig CAS settings: %s", settings)
125 log.debug("Jasig CAS settings: %s", settings)
126 params = urllib.urlencode({'username': username, 'password': password})
126 params = urllib.urlencode({'username': username, 'password': password})
127 headers = {"Content-type": "application/x-www-form-urlencoded",
127 headers = {"Content-type": "application/x-www-form-urlencoded",
128 "Accept": "text/plain",
128 "Accept": "text/plain",
129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
130 url = settings["service_url"]
130 url = settings["service_url"]
131
131
132 log.debug("Sent Jasig CAS: \n%s",
132 log.debug("Sent Jasig CAS: \n%s",
133 {"url": url, "body": params, "headers": headers})
133 {"url": url, "body": params, "headers": headers})
134 request = urllib2.Request(url, params, headers)
134 request = urllib2.Request(url, params, headers)
135 try:
135 try:
136 response = urllib2.urlopen(request)
136 response = urllib2.urlopen(request)
137 except urllib2.HTTPError as e:
137 except urllib2.HTTPError as e:
138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
139 return None
139 return None
140 except urllib2.URLError as e:
140 except urllib2.URLError as e:
141 log.debug("URLError when requesting Jasig CAS url: %s " % url)
141 log.debug("URLError when requesting Jasig CAS url: %s " % url)
142 return None
142 return None
143
143
144 # old attrs fetched from RhodeCode database
144 # old attrs fetched from RhodeCode database
145 admin = getattr(userobj, 'admin', False)
145 admin = getattr(userobj, 'admin', False)
146 active = getattr(userobj, 'active', True)
146 active = getattr(userobj, 'active', True)
147 email = getattr(userobj, 'email', '')
147 email = getattr(userobj, 'email', '')
148 username = getattr(userobj, 'username', username)
148 username = getattr(userobj, 'username', username)
149 firstname = getattr(userobj, 'firstname', '')
149 firstname = getattr(userobj, 'firstname', '')
150 lastname = getattr(userobj, 'lastname', '')
150 lastname = getattr(userobj, 'lastname', '')
151 extern_type = getattr(userobj, 'extern_type', '')
151 extern_type = getattr(userobj, 'extern_type', '')
152
152
153 user_attrs = {
153 user_attrs = {
154 'username': username,
154 'username': username,
155 'firstname': safe_unicode(firstname or username),
155 'firstname': safe_unicode(firstname or username),
156 'lastname': safe_unicode(lastname or ''),
156 'lastname': safe_unicode(lastname or ''),
157 'groups': [],
157 'groups': [],
158 'email': email or '',
158 'email': email or '',
159 'admin': admin or False,
159 'admin': admin or False,
160 'active': active,
160 'active': active,
161 'active_from_extern': True,
161 'active_from_extern': True,
162 'extern_name': username,
162 'extern_name': username,
163 'extern_type': extern_type,
163 'extern_type': extern_type,
164 }
164 }
165
165
166 log.info('user %s authenticated correctly' % user_attrs['username'])
166 log.info('user %s authenticated correctly' % user_attrs['username'])
167 return user_attrs
167 return user_attrs
General Comments 0
You need to be logged in to leave comments. Login now