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