Mercurial > kallithea
comparison rhodecode/lib/auth_modules/__init__.py @ 4116:ffd45b185016 rhodecode-2.2.5-gpl
Imported some of the GPLv3'd changes from RhodeCode v2.2.5.
This imports changes between changesets 21af6c4eab3d and 6177597791c2 in
RhodeCode's original repository, including only changes to Python files and HTML.
RhodeCode clearly licensed its changes to these files under GPLv3
in their /LICENSE file, which states the following:
The Python code and integrated HTML are licensed under the GPLv3 license.
(See:
https://code.rhodecode.com/rhodecode/files/v2.2.5/LICENSE
or
http://web.archive.org/web/20140512193334/https://code.rhodecode.com/rhodecode/files/f3b123159901f15426d18e3dc395e8369f70ebe0/LICENSE
for an online copy of that LICENSE file)
Conservancy reviewed these changes and confirmed that they can be licensed as
a whole to the Kallithea project under GPLv3-only.
While some of the contents committed herein are clearly licensed
GPLv3-or-later, on the whole we must assume the are GPLv3-only, since the
statement above from RhodeCode indicates that they intend GPLv3-only as their
license, per GPLv3ยง14 and other relevant sections of GPLv3.
author | Bradley M. Kuhn <bkuhn@sfconservancy.org> |
---|---|
date | Wed, 02 Jul 2014 19:03:13 -0400 |
parents | |
children | 7e5f8c12a3fc |
comparison
equal
deleted
inserted
replaced
4115:8b7294a804a0 | 4116:ffd45b185016 |
---|---|
1 # -*- coding: utf-8 -*- | |
2 # This program is free software: you can redistribute it and/or modify | |
3 # it under the terms of the GNU General Public License as published by | |
4 # the Free Software Foundation, either version 3 of the License, or | |
5 # (at your option) any later version. | |
6 # | |
7 # This program is distributed in the hope that it will be useful, | |
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
10 # GNU General Public License for more details. | |
11 # | |
12 # You should have received a copy of the GNU General Public License | |
13 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 """ | |
15 Authentication modules | |
16 """ | |
17 | |
18 import logging | |
19 import traceback | |
20 | |
21 from rhodecode.lib.compat import importlib | |
22 from rhodecode.lib.utils2 import str2bool | |
23 from rhodecode.lib.compat import formatted_json, hybrid_property | |
24 from rhodecode.lib.auth import PasswordGenerator | |
25 from rhodecode.model.user import UserModel | |
26 from rhodecode.model.db import RhodeCodeSetting, User, UserGroup | |
27 from rhodecode.model.meta import Session | |
28 from rhodecode.model.user_group import UserGroupModel | |
29 | |
30 log = logging.getLogger(__name__) | |
31 | |
32 | |
33 class LazyFormencode(object): | |
34 def __init__(self, formencode_obj, *args, **kwargs): | |
35 self.formencode_obj = formencode_obj | |
36 self.args = args | |
37 self.kwargs = kwargs | |
38 | |
39 def __call__(self, *args, **kwargs): | |
40 from inspect import isfunction | |
41 formencode_obj = self.formencode_obj | |
42 if isfunction(formencode_obj): | |
43 #case we wrap validators into functions | |
44 formencode_obj = self.formencode_obj(*args, **kwargs) | |
45 return formencode_obj(*self.args, **self.kwargs) | |
46 | |
47 | |
48 class RhodeCodeAuthPluginBase(object): | |
49 auth_func_attrs = { | |
50 "username": "unique username", | |
51 "firstname": "first name", | |
52 "lastname": "last name", | |
53 "email": "email address", | |
54 "groups": '["list", "of", "groups"]', | |
55 "extern_name": "name in external source of record", | |
56 "extern_type": "type of external source of record", | |
57 "admin": 'True|False defines if user should be RhodeCode super admin', | |
58 "active": 'True|False defines active state of user internally for RhodeCode', | |
59 "active_from_extern": "True|False\None, active state from the external auth, " | |
60 "None means use definition from RhodeCode extern_type active value" | |
61 } | |
62 | |
63 @property | |
64 def validators(self): | |
65 """ | |
66 Exposes RhodeCode validators modules | |
67 """ | |
68 # this is a hack to overcome issues with pylons threadlocals and | |
69 # translator object _() not beein registered properly. | |
70 class LazyCaller(object): | |
71 def __init__(self, name): | |
72 self.validator_name = name | |
73 | |
74 def __call__(self, *args, **kwargs): | |
75 from rhodecode.model import validators as v | |
76 obj = getattr(v, self.validator_name) | |
77 #log.debug('Initializing lazy formencode object: %s' % obj) | |
78 return LazyFormencode(obj, *args, **kwargs) | |
79 | |
80 | |
81 class ProxyGet(object): | |
82 def __getattribute__(self, name): | |
83 return LazyCaller(name) | |
84 | |
85 return ProxyGet() | |
86 | |
87 @hybrid_property | |
88 def name(self): | |
89 """ | |
90 Returns the name of this authentication plugin. | |
91 | |
92 :returns: string | |
93 """ | |
94 raise NotImplementedError("Not implemented in base class") | |
95 | |
96 @hybrid_property | |
97 def is_container_auth(self): | |
98 """ | |
99 Returns bool if this module uses container auth. | |
100 | |
101 This property will trigger an automatic call to authenticate on | |
102 a visit to the website or during a push/pull. | |
103 | |
104 :returns: bool | |
105 """ | |
106 return False | |
107 | |
108 def accepts(self, user, accepts_empty=True): | |
109 """ | |
110 Checks if this authentication module should accept a request for | |
111 the current user. | |
112 | |
113 :param user: user object fetched using plugin's get_user() method. | |
114 :param accepts_empty: if True accepts don't allow the user to be empty | |
115 :returns: boolean | |
116 """ | |
117 plugin_name = self.name | |
118 if not user and not accepts_empty: | |
119 log.debug('User is empty not allowed to authenticate') | |
120 return False | |
121 | |
122 if user and user.extern_type and user.extern_type != plugin_name: | |
123 log.debug('User %s should authenticate using %s this is %s, skipping' | |
124 % (user, user.extern_type, plugin_name)) | |
125 | |
126 return False | |
127 return True | |
128 | |
129 def get_user(self, username=None, **kwargs): | |
130 """ | |
131 Helper method for user fetching in plugins, by default it's using | |
132 simple fetch by username, but this method can be custimized in plugins | |
133 eg. container auth plugin to fetch user by environ params | |
134 | |
135 :param username: username if given to fetch from database | |
136 :param kwargs: extra arguments needed for user fetching. | |
137 """ | |
138 user = None | |
139 log.debug('Trying to fetch user `%s` from RhodeCode database' | |
140 % (username)) | |
141 if username: | |
142 user = User.get_by_username(username) | |
143 if not user: | |
144 log.debug('Fallback to fetch user in case insensitive mode') | |
145 user = User.get_by_username(username, case_insensitive=True) | |
146 else: | |
147 log.debug('provided username:`%s` is empty skipping...' % username) | |
148 return user | |
149 | |
150 def settings(self): | |
151 """ | |
152 Return a list of the form: | |
153 [ | |
154 { | |
155 "name": "OPTION_NAME", | |
156 "type": "[bool|password|string|int|select]", | |
157 ["values": ["opt1", "opt2", ...]] | |
158 "validator": "expr" | |
159 "description": "A short description of the option" [, | |
160 "default": Default Value], | |
161 ["formname": "Friendly Name for Forms"] | |
162 } [, ...] | |
163 ] | |
164 | |
165 This is used to interrogate the authentication plugin as to what | |
166 settings it expects to be present and configured. | |
167 | |
168 'type' is a shorthand notation for what kind of value this option is. | |
169 This is primarily used by the auth web form to control how the option | |
170 is configured. | |
171 bool : checkbox | |
172 password : password input box | |
173 string : input box | |
174 select : single select dropdown | |
175 | |
176 'validator' is an lazy instantiated form field validator object, ala | |
177 formencode. You need to *call* this object to init the validators. | |
178 All calls to RhodeCode validators should be used through self.validators | |
179 which is a lazy loading proxy of formencode module. | |
180 """ | |
181 raise NotImplementedError("Not implemented in base class") | |
182 | |
183 def plugin_settings(self): | |
184 """ | |
185 This method is called by the authentication framework, not the .settings() | |
186 method. This method adds a few default settings (e.g., "active"), so that | |
187 plugin authors don't have to maintain a bunch of boilerplate. | |
188 | |
189 OVERRIDING THIS METHOD WILL CAUSE YOUR PLUGIN TO FAIL. | |
190 """ | |
191 | |
192 rcsettings = self.settings() | |
193 rcsettings.insert(0, { | |
194 "name": "enabled", | |
195 "validator": self.validators.StringBoolean(if_missing=False), | |
196 "type": "bool", | |
197 "description": "Enable or Disable this Authentication Plugin", | |
198 "formname": "Enabled" | |
199 } | |
200 ) | |
201 return rcsettings | |
202 | |
203 def user_activation_state(self): | |
204 """ | |
205 Defines user activation state when creating new users | |
206 | |
207 :returns: boolean | |
208 """ | |
209 raise NotImplementedError("Not implemented in base class") | |
210 | |
211 def auth(self, userobj, username, passwd, settings, **kwargs): | |
212 """ | |
213 Given a user object (which may be null), username, a plaintext password, | |
214 and a settings object (containing all the keys needed as listed in settings()), | |
215 authenticate this user's login attempt. | |
216 | |
217 Return None on failure. On success, return a dictionary of the form: | |
218 | |
219 see: RhodeCodeAuthPluginBase.auth_func_attrs | |
220 This is later validated for correctness | |
221 """ | |
222 raise NotImplementedError("not implemented in base class") | |
223 | |
224 def _authenticate(self, userobj, username, passwd, settings, **kwargs): | |
225 """ | |
226 Wrapper to call self.auth() that validates call on it | |
227 | |
228 :param userobj: userobj | |
229 :param username: username | |
230 :param passwd: plaintext password | |
231 :param settings: plugin settings | |
232 """ | |
233 auth = self.auth(userobj, username, passwd, settings, **kwargs) | |
234 if auth: | |
235 return self._validate_auth_return(auth) | |
236 return auth | |
237 | |
238 def _validate_auth_return(self, ret): | |
239 if not isinstance(ret, dict): | |
240 raise Exception('returned value from auth must be a dict') | |
241 for k in self.auth_func_attrs: | |
242 if k not in ret: | |
243 raise Exception('Missing %s attribute from returned data' % k) | |
244 return ret | |
245 | |
246 | |
247 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase): | |
248 def use_fake_password(self): | |
249 """ | |
250 Return a boolean that indicates whether or not we should set the user's | |
251 password to a random value when it is authenticated by this plugin. | |
252 If your plugin provides authentication, then you will generally want this. | |
253 | |
254 :returns: boolean | |
255 """ | |
256 raise NotImplementedError("Not implemented in base class") | |
257 | |
258 def _authenticate(self, userobj, username, passwd, settings, **kwargs): | |
259 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate( | |
260 userobj, username, passwd, settings, **kwargs) | |
261 if auth: | |
262 # maybe plugin will clean the username ? | |
263 # we should use the return value | |
264 username = auth['username'] | |
265 # if user is not active from our extern type we should fail to authe | |
266 # this can prevent from creating users in RhodeCode when using | |
267 # external authentication, but if it's inactive user we shouldn't | |
268 # create that user anyway | |
269 if auth['active_from_extern'] is False: | |
270 log.warning("User %s authenticated against %s, but is inactive" | |
271 % (username, self.__module__)) | |
272 return None | |
273 | |
274 if self.use_fake_password(): | |
275 # Randomize the PW because we don't need it, but don't want | |
276 # them blank either | |
277 passwd = PasswordGenerator().gen_password(length=8) | |
278 | |
279 log.debug('Updating or creating user info from %s plugin' | |
280 % self.name) | |
281 user = UserModel().create_or_update( | |
282 username=username, | |
283 password=passwd, | |
284 email=auth["email"], | |
285 firstname=auth["firstname"], | |
286 lastname=auth["lastname"], | |
287 active=auth["active"], | |
288 admin=auth["admin"], | |
289 extern_name=auth["extern_name"], | |
290 extern_type=self.name | |
291 ) | |
292 Session().flush() | |
293 # enforce user is just in given groups, all of them has to be ones | |
294 # created from plugins. We store this info in _group_data JSON field | |
295 try: | |
296 groups = auth['groups'] or [] | |
297 UserGroupModel().enforce_groups(user, groups, self.name) | |
298 except Exception: | |
299 # for any reason group syncing fails, we should proceed with login | |
300 log.error(traceback.format_exc()) | |
301 Session().commit() | |
302 return auth | |
303 | |
304 | |
305 def importplugin(plugin): | |
306 """ | |
307 Imports and returns the authentication plugin in the module named by plugin | |
308 (e.g., plugin='rhodecode.lib.auth_modules.auth_rhodecode'). Returns the | |
309 RhodeCodeAuthPluginBase subclass on success, raises exceptions on failure. | |
310 | |
311 raises: | |
312 AttributeError -- no RhodeCodeAuthPlugin class in the module | |
313 TypeError -- if the RhodeCodeAuthPlugin is not a subclass of ours RhodeCodeAuthPluginBase | |
314 ImportError -- if we couldn't import the plugin at all | |
315 """ | |
316 log.debug("Importing %s" % plugin) | |
317 PLUGIN_CLASS_NAME = "RhodeCodeAuthPlugin" | |
318 try: | |
319 module = importlib.import_module(plugin) | |
320 except (ImportError, TypeError): | |
321 log.error(traceback.format_exc()) | |
322 # TODO: make this more error prone, if by some accident we screw up | |
323 # the plugin name, the crash is preatty bad and hard to recover | |
324 raise | |
325 | |
326 log.debug("Loaded auth plugin from %s (module:%s, file:%s)" | |
327 % (plugin, module.__name__, module.__file__)) | |
328 | |
329 pluginclass = getattr(module, PLUGIN_CLASS_NAME) | |
330 if not issubclass(pluginclass, RhodeCodeAuthPluginBase): | |
331 raise TypeError("Authentication class %s.RhodeCodeAuthPlugin is not " | |
332 "a subclass of %s" % (plugin, RhodeCodeAuthPluginBase)) | |
333 return pluginclass | |
334 | |
335 | |
336 def loadplugin(plugin): | |
337 """ | |
338 Loads and returns an instantiated authentication plugin. | |
339 | |
340 see: importplugin | |
341 """ | |
342 plugin = importplugin(plugin)() | |
343 if plugin.plugin_settings.im_func != RhodeCodeAuthPluginBase.plugin_settings.im_func: | |
344 raise TypeError("Authentication class %s.RhodeCodeAuthPluginBase " | |
345 "has overriden the plugin_settings method, which is " | |
346 "forbidden." % plugin) | |
347 return plugin | |
348 | |
349 | |
350 def authenticate(username, password, environ=None): | |
351 """ | |
352 Authentication function used for access control, | |
353 It tries to authenticate based on enabled authentication modules. | |
354 | |
355 :param username: username can be empty for container auth | |
356 :param password: password can be empty for container auth | |
357 :param environ: environ headers passed for container auth | |
358 :returns: None if auth failed, plugin_user dict if auth is correct | |
359 """ | |
360 | |
361 auth_plugins = RhodeCodeSetting.get_auth_plugins() | |
362 log.debug('Authentication against %s plugins' % (auth_plugins,)) | |
363 for module in auth_plugins: | |
364 try: | |
365 plugin = loadplugin(module) | |
366 except (ImportError, AttributeError, TypeError), e: | |
367 raise ImportError('Failed to load authentication module %s : %s' | |
368 % (module, str(e))) | |
369 log.debug('Trying authentication using ** %s **' % (module,)) | |
370 # load plugin settings from RhodeCode database | |
371 plugin_name = plugin.name | |
372 plugin_settings = {} | |
373 for v in plugin.plugin_settings(): | |
374 conf_key = "auth_%s_%s" % (plugin_name, v["name"]) | |
375 setting = RhodeCodeSetting.get_by_name(conf_key) | |
376 plugin_settings[v["name"]] = setting.app_settings_value if setting else None | |
377 log.debug('Plugin settings \n%s' % formatted_json(plugin_settings)) | |
378 | |
379 if not str2bool(plugin_settings["enabled"]): | |
380 log.info("Authentication plugin %s is disabled, skipping for %s" | |
381 % (module, username)) | |
382 continue | |
383 | |
384 # use plugin's method of user extraction. | |
385 user = plugin.get_user(username, environ=environ, | |
386 settings=plugin_settings) | |
387 log.debug('Plugin %s extracted user is `%s`' % (module, user)) | |
388 if not plugin.accepts(user): | |
389 log.debug('Plugin %s does not accept user `%s` for authentication' | |
390 % (module, user)) | |
391 continue | |
392 else: | |
393 log.debug('Plugin %s accepted user `%s` for authentication' | |
394 % (module, user)) | |
395 | |
396 log.info('Authenticating user using %s plugin' % plugin.__module__) | |
397 # _authenticate is a wrapper for .auth() method of plugin. | |
398 # it checks if .auth() sends proper data. for RhodeCodeExternalAuthPlugin | |
399 # it also maps users to Database and maps the attributes returned | |
400 # from .auth() to RhodeCode database. If this function returns data | |
401 # then auth is correct. | |
402 plugin_user = plugin._authenticate(user, username, password, | |
403 plugin_settings, | |
404 environ=environ or {}) | |
405 log.debug('PLUGIN USER DATA: %s' % plugin_user) | |
406 | |
407 if plugin_user: | |
408 log.debug('Plugin returned proper authentication data') | |
409 return plugin_user | |
410 | |
411 # we failed to Auth because .auth() method didn't return proper the user | |
412 log.warning("User `%s` failed to authenticate against %s" | |
413 % (username, plugin.__module__)) | |
414 return None |