__init__.py
414 lines
| 16.2 KiB
| text/x-python
|
PythonLexer
Bradley M. Kuhn
|
r4116 | # -*- coding: utf-8 -*- | ||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU General Public License as published by | ||||
# the Free Software Foundation, either version 3 of the License, or | ||||
# (at your option) any later version. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
""" | ||||
Authentication modules | ||||
""" | ||||
import logging | ||||
import traceback | ||||
from rhodecode.lib.compat import importlib | ||||
from rhodecode.lib.utils2 import str2bool | ||||
from rhodecode.lib.compat import formatted_json, hybrid_property | ||||
from rhodecode.lib.auth import PasswordGenerator | ||||
from rhodecode.model.user import UserModel | ||||
from rhodecode.model.db import RhodeCodeSetting, User, UserGroup | ||||
from rhodecode.model.meta import Session | ||||
from rhodecode.model.user_group import UserGroupModel | ||||
log = logging.getLogger(__name__) | ||||
class LazyFormencode(object): | ||||
def __init__(self, formencode_obj, *args, **kwargs): | ||||
self.formencode_obj = formencode_obj | ||||
self.args = args | ||||
self.kwargs = kwargs | ||||
def __call__(self, *args, **kwargs): | ||||
from inspect import isfunction | ||||
formencode_obj = self.formencode_obj | ||||
if isfunction(formencode_obj): | ||||
#case we wrap validators into functions | ||||
formencode_obj = self.formencode_obj(*args, **kwargs) | ||||
return formencode_obj(*self.args, **self.kwargs) | ||||
class RhodeCodeAuthPluginBase(object): | ||||
auth_func_attrs = { | ||||
"username": "unique username", | ||||
"firstname": "first name", | ||||
"lastname": "last name", | ||||
"email": "email address", | ||||
"groups": '["list", "of", "groups"]', | ||||
"extern_name": "name in external source of record", | ||||
"extern_type": "type of external source of record", | ||||
"admin": 'True|False defines if user should be RhodeCode super admin', | ||||
"active": 'True|False defines active state of user internally for RhodeCode', | ||||
"active_from_extern": "True|False\None, active state from the external auth, " | ||||
"None means use definition from RhodeCode extern_type active value" | ||||
} | ||||
@property | ||||
def validators(self): | ||||
""" | ||||
Exposes RhodeCode validators modules | ||||
""" | ||||
# this is a hack to overcome issues with pylons threadlocals and | ||||
# translator object _() not beein registered properly. | ||||
class LazyCaller(object): | ||||
def __init__(self, name): | ||||
self.validator_name = name | ||||
def __call__(self, *args, **kwargs): | ||||
from rhodecode.model import validators as v | ||||
obj = getattr(v, self.validator_name) | ||||
#log.debug('Initializing lazy formencode object: %s' % obj) | ||||
return LazyFormencode(obj, *args, **kwargs) | ||||
class ProxyGet(object): | ||||
def __getattribute__(self, name): | ||||
return LazyCaller(name) | ||||
return ProxyGet() | ||||
@hybrid_property | ||||
def name(self): | ||||
""" | ||||
Returns the name of this authentication plugin. | ||||
:returns: string | ||||
""" | ||||
raise NotImplementedError("Not implemented in base class") | ||||
@hybrid_property | ||||
def is_container_auth(self): | ||||
""" | ||||
Returns bool if this module uses container auth. | ||||
This property will trigger an automatic call to authenticate on | ||||
a visit to the website or during a push/pull. | ||||
:returns: bool | ||||
""" | ||||
return False | ||||
def accepts(self, user, accepts_empty=True): | ||||
""" | ||||
Checks if this authentication module should accept a request for | ||||
the current user. | ||||
:param user: user object fetched using plugin's get_user() method. | ||||
:param accepts_empty: if True accepts don't allow the user to be empty | ||||
:returns: boolean | ||||
""" | ||||
plugin_name = self.name | ||||
if not user and not accepts_empty: | ||||
log.debug('User is empty not allowed to authenticate') | ||||
return False | ||||
if user and user.extern_type and user.extern_type != plugin_name: | ||||
log.debug('User %s should authenticate using %s this is %s, skipping' | ||||
% (user, user.extern_type, plugin_name)) | ||||
return False | ||||
return True | ||||
def get_user(self, username=None, **kwargs): | ||||
""" | ||||
Helper method for user fetching in plugins, by default it's using | ||||
simple fetch by username, but this method can be custimized in plugins | ||||
eg. container auth plugin to fetch user by environ params | ||||
:param username: username if given to fetch from database | ||||
:param kwargs: extra arguments needed for user fetching. | ||||
""" | ||||
user = None | ||||
log.debug('Trying to fetch user `%s` from RhodeCode database' | ||||
% (username)) | ||||
if username: | ||||
user = User.get_by_username(username) | ||||
if not user: | ||||
log.debug('Fallback to fetch user in case insensitive mode') | ||||
user = User.get_by_username(username, case_insensitive=True) | ||||
else: | ||||
log.debug('provided username:`%s` is empty skipping...' % username) | ||||
return user | ||||
def settings(self): | ||||
""" | ||||
Return a list of the form: | ||||
[ | ||||
{ | ||||
"name": "OPTION_NAME", | ||||
"type": "[bool|password|string|int|select]", | ||||
["values": ["opt1", "opt2", ...]] | ||||
"validator": "expr" | ||||
"description": "A short description of the option" [, | ||||
"default": Default Value], | ||||
["formname": "Friendly Name for Forms"] | ||||
} [, ...] | ||||
] | ||||
This is used to interrogate the authentication plugin as to what | ||||
settings it expects to be present and configured. | ||||
'type' is a shorthand notation for what kind of value this option is. | ||||
This is primarily used by the auth web form to control how the option | ||||
is configured. | ||||
bool : checkbox | ||||
password : password input box | ||||
string : input box | ||||
select : single select dropdown | ||||
'validator' is an lazy instantiated form field validator object, ala | ||||
formencode. You need to *call* this object to init the validators. | ||||
All calls to RhodeCode validators should be used through self.validators | ||||
which is a lazy loading proxy of formencode module. | ||||
""" | ||||
raise NotImplementedError("Not implemented in base class") | ||||
def plugin_settings(self): | ||||
""" | ||||
This method is called by the authentication framework, not the .settings() | ||||
method. This method adds a few default settings (e.g., "active"), so that | ||||
plugin authors don't have to maintain a bunch of boilerplate. | ||||
OVERRIDING THIS METHOD WILL CAUSE YOUR PLUGIN TO FAIL. | ||||
""" | ||||
rcsettings = self.settings() | ||||
rcsettings.insert(0, { | ||||
"name": "enabled", | ||||
"validator": self.validators.StringBoolean(if_missing=False), | ||||
"type": "bool", | ||||
"description": "Enable or Disable this Authentication Plugin", | ||||
"formname": "Enabled" | ||||
} | ||||
) | ||||
return rcsettings | ||||
def user_activation_state(self): | ||||
""" | ||||
Defines user activation state when creating new users | ||||
:returns: boolean | ||||
""" | ||||
raise NotImplementedError("Not implemented in base class") | ||||
def auth(self, userobj, username, passwd, settings, **kwargs): | ||||
""" | ||||
Given a user object (which may be null), username, a plaintext password, | ||||
and a settings object (containing all the keys needed as listed in settings()), | ||||
authenticate this user's login attempt. | ||||
Return None on failure. On success, return a dictionary of the form: | ||||
see: RhodeCodeAuthPluginBase.auth_func_attrs | ||||
This is later validated for correctness | ||||
""" | ||||
raise NotImplementedError("not implemented in base class") | ||||
def _authenticate(self, userobj, username, passwd, settings, **kwargs): | ||||
""" | ||||
Wrapper to call self.auth() that validates call on it | ||||
:param userobj: userobj | ||||
:param username: username | ||||
:param passwd: plaintext password | ||||
:param settings: plugin settings | ||||
""" | ||||
auth = self.auth(userobj, username, passwd, settings, **kwargs) | ||||
if auth: | ||||
return self._validate_auth_return(auth) | ||||
return auth | ||||
def _validate_auth_return(self, ret): | ||||
if not isinstance(ret, dict): | ||||
raise Exception('returned value from auth must be a dict') | ||||
for k in self.auth_func_attrs: | ||||
if k not in ret: | ||||
raise Exception('Missing %s attribute from returned data' % k) | ||||
return ret | ||||
class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase): | ||||
def use_fake_password(self): | ||||
""" | ||||
Return a boolean that indicates whether or not we should set the user's | ||||
password to a random value when it is authenticated by this plugin. | ||||
If your plugin provides authentication, then you will generally want this. | ||||
:returns: boolean | ||||
""" | ||||
raise NotImplementedError("Not implemented in base class") | ||||
def _authenticate(self, userobj, username, passwd, settings, **kwargs): | ||||
auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate( | ||||
userobj, username, passwd, settings, **kwargs) | ||||
if auth: | ||||
# maybe plugin will clean the username ? | ||||
# we should use the return value | ||||
username = auth['username'] | ||||
# if user is not active from our extern type we should fail to authe | ||||
# this can prevent from creating users in RhodeCode when using | ||||
# external authentication, but if it's inactive user we shouldn't | ||||
# create that user anyway | ||||
if auth['active_from_extern'] is False: | ||||
log.warning("User %s authenticated against %s, but is inactive" | ||||
% (username, self.__module__)) | ||||
return None | ||||
if self.use_fake_password(): | ||||
# Randomize the PW because we don't need it, but don't want | ||||
# them blank either | ||||
passwd = PasswordGenerator().gen_password(length=8) | ||||
log.debug('Updating or creating user info from %s plugin' | ||||
% self.name) | ||||
user = UserModel().create_or_update( | ||||
username=username, | ||||
password=passwd, | ||||
email=auth["email"], | ||||
firstname=auth["firstname"], | ||||
lastname=auth["lastname"], | ||||
active=auth["active"], | ||||
admin=auth["admin"], | ||||
extern_name=auth["extern_name"], | ||||
extern_type=self.name | ||||
) | ||||
Session().flush() | ||||
# enforce user is just in given groups, all of them has to be ones | ||||
# created from plugins. We store this info in _group_data JSON field | ||||
try: | ||||
groups = auth['groups'] or [] | ||||
UserGroupModel().enforce_groups(user, groups, self.name) | ||||
except Exception: | ||||
# for any reason group syncing fails, we should proceed with login | ||||
log.error(traceback.format_exc()) | ||||
Session().commit() | ||||
return auth | ||||
def importplugin(plugin): | ||||
""" | ||||
Imports and returns the authentication plugin in the module named by plugin | ||||
(e.g., plugin='rhodecode.lib.auth_modules.auth_rhodecode'). Returns the | ||||
RhodeCodeAuthPluginBase subclass on success, raises exceptions on failure. | ||||
raises: | ||||
AttributeError -- no RhodeCodeAuthPlugin class in the module | ||||
TypeError -- if the RhodeCodeAuthPlugin is not a subclass of ours RhodeCodeAuthPluginBase | ||||
ImportError -- if we couldn't import the plugin at all | ||||
""" | ||||
log.debug("Importing %s" % plugin) | ||||
PLUGIN_CLASS_NAME = "RhodeCodeAuthPlugin" | ||||
try: | ||||
module = importlib.import_module(plugin) | ||||
except (ImportError, TypeError): | ||||
log.error(traceback.format_exc()) | ||||
# TODO: make this more error prone, if by some accident we screw up | ||||
# the plugin name, the crash is preatty bad and hard to recover | ||||
raise | ||||
log.debug("Loaded auth plugin from %s (module:%s, file:%s)" | ||||
% (plugin, module.__name__, module.__file__)) | ||||
pluginclass = getattr(module, PLUGIN_CLASS_NAME) | ||||
if not issubclass(pluginclass, RhodeCodeAuthPluginBase): | ||||
raise TypeError("Authentication class %s.RhodeCodeAuthPlugin is not " | ||||
"a subclass of %s" % (plugin, RhodeCodeAuthPluginBase)) | ||||
return pluginclass | ||||
def loadplugin(plugin): | ||||
""" | ||||
Loads and returns an instantiated authentication plugin. | ||||
see: importplugin | ||||
""" | ||||
plugin = importplugin(plugin)() | ||||
if plugin.plugin_settings.im_func != RhodeCodeAuthPluginBase.plugin_settings.im_func: | ||||
raise TypeError("Authentication class %s.RhodeCodeAuthPluginBase " | ||||
"has overriden the plugin_settings method, which is " | ||||
"forbidden." % plugin) | ||||
return plugin | ||||
def authenticate(username, password, environ=None): | ||||
""" | ||||
Authentication function used for access control, | ||||
It tries to authenticate based on enabled authentication modules. | ||||
:param username: username can be empty for container auth | ||||
:param password: password can be empty for container auth | ||||
:param environ: environ headers passed for container auth | ||||
:returns: None if auth failed, plugin_user dict if auth is correct | ||||
""" | ||||
auth_plugins = RhodeCodeSetting.get_auth_plugins() | ||||
log.debug('Authentication against %s plugins' % (auth_plugins,)) | ||||
for module in auth_plugins: | ||||
try: | ||||
plugin = loadplugin(module) | ||||
except (ImportError, AttributeError, TypeError), e: | ||||
raise ImportError('Failed to load authentication module %s : %s' | ||||
% (module, str(e))) | ||||
log.debug('Trying authentication using ** %s **' % (module,)) | ||||
# load plugin settings from RhodeCode database | ||||
plugin_name = plugin.name | ||||
plugin_settings = {} | ||||
for v in plugin.plugin_settings(): | ||||
conf_key = "auth_%s_%s" % (plugin_name, v["name"]) | ||||
setting = RhodeCodeSetting.get_by_name(conf_key) | ||||
plugin_settings[v["name"]] = setting.app_settings_value if setting else None | ||||
log.debug('Plugin settings \n%s' % formatted_json(plugin_settings)) | ||||
if not str2bool(plugin_settings["enabled"]): | ||||
log.info("Authentication plugin %s is disabled, skipping for %s" | ||||
% (module, username)) | ||||
continue | ||||
# use plugin's method of user extraction. | ||||
user = plugin.get_user(username, environ=environ, | ||||
settings=plugin_settings) | ||||
log.debug('Plugin %s extracted user is `%s`' % (module, user)) | ||||
if not plugin.accepts(user): | ||||
log.debug('Plugin %s does not accept user `%s` for authentication' | ||||
% (module, user)) | ||||
continue | ||||
else: | ||||
log.debug('Plugin %s accepted user `%s` for authentication' | ||||
% (module, user)) | ||||
log.info('Authenticating user using %s plugin' % plugin.__module__) | ||||
# _authenticate is a wrapper for .auth() method of plugin. | ||||
# it checks if .auth() sends proper data. for RhodeCodeExternalAuthPlugin | ||||
# it also maps users to Database and maps the attributes returned | ||||
# from .auth() to RhodeCode database. If this function returns data | ||||
# then auth is correct. | ||||
plugin_user = plugin._authenticate(user, username, password, | ||||
plugin_settings, | ||||
environ=environ or {}) | ||||
log.debug('PLUGIN USER DATA: %s' % plugin_user) | ||||
if plugin_user: | ||||
log.debug('Plugin returned proper authentication data') | ||||
return plugin_user | ||||
# we failed to Auth because .auth() method didn't return proper the user | ||||
log.warning("User `%s` failed to authenticate against %s" | ||||
% (username, plugin.__module__)) | ||||
return None | ||||