diff --git a/openstack_auth/backend.py b/openstack_auth/backend.py index ef914483..437b09b4 100644 --- a/openstack_auth/backend.py +++ b/openstack_auth/backend.py @@ -33,6 +33,21 @@ KEYSTONE_CLIENT_ATTR = "_keystoneclient" class KeystoneBackend(object): """Django authentication backend for use with ``django.contrib.auth``.""" + def __init__(self): + self._auth_plugins = None + + @property + def auth_plugins(self): + if self._auth_plugins is None: + plugins = getattr( + settings, + 'AUTH_PLUGINS', + ['openstack_auth.plugin.password.PasswordPlugin']) + + self._auth_plugins = [utils.import_string(p)() for p in plugins] + + return self._auth_plugins + def check_auth_expiry(self, auth_ref, margin=None): if not utils.is_token_valid(auth_ref, margin): msg = _("The authentication token issued by the Identity service " @@ -64,25 +79,26 @@ class KeystoneBackend(object): else: return None - def authenticate(self, request=None, username=None, password=None, - user_domain_name=None, auth_url=None): + def authenticate(self, auth_url=None, **kwargs): """Authenticates a user via the Keystone Identity API.""" - LOG.debug('Beginning user authentication for user "%s".' % username) + LOG.debug('Beginning user authentication') - interface = getattr(settings, 'OPENSTACK_ENDPOINT_TYPE', 'public') - - if auth_url is None: + if not auth_url: auth_url = settings.OPENSTACK_KEYSTONE_URL + auth_url = utils.fix_auth_url_version(auth_url) + + for plugin in self.auth_plugins: + unscoped_auth = plugin.get_plugin(auth_url=auth_url, **kwargs) + + if unscoped_auth: + break + else: + return None + session = utils.get_session() keystone_client_class = utils.get_keystone_client().Client - auth_url = utils.fix_auth_url_version(auth_url) - unscoped_auth = utils.get_password_auth_plugin(auth_url, - username, - password, - user_domain_name) - try: unscoped_auth_ref = unscoped_auth.get_access(session) except (keystone_exceptions.Unauthorized, @@ -126,6 +142,8 @@ class KeystoneBackend(object): # the recent project id a user might have set in a cookie recent_project = None + request = kwargs.get('request') + if request: # Check if token is automatically scoped to default_project # grab the project from this token, to use as a default @@ -162,6 +180,8 @@ class KeystoneBackend(object): # Check expiry for our new scoped token. self.check_auth_expiry(scoped_auth_ref) + interface = getattr(settings, 'OPENSTACK_ENDPOINT_TYPE', 'public') + # If we made it here we succeeded. Create our User! user = auth_user.create_user_from_token( request, @@ -177,7 +197,7 @@ class KeystoneBackend(object): # Support client caching to save on auth calls. setattr(request, KEYSTONE_CLIENT_ATTR, scoped_client) - LOG.debug('Authentication completed for user "%s".' % username) + LOG.debug('Authentication completed.') return user def get_group_permissions(self, user, obj=None): diff --git a/openstack_auth/plugin/__init__.py b/openstack_auth/plugin/__init__.py new file mode 100644 index 00000000..26c2b499 --- /dev/null +++ b/openstack_auth/plugin/__init__.py @@ -0,0 +1,18 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack_auth.plugin.base import * # noqa +from openstack_auth.plugin.password import * # noqa + + +__all__ = ['BasePlugin', + 'PasswordPlugin'] diff --git a/openstack_auth/plugin/base.py b/openstack_auth/plugin/base.py new file mode 100644 index 00000000..fed0cc2b --- /dev/null +++ b/openstack_auth/plugin/base.py @@ -0,0 +1,44 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + +import six + +__all__ = ['BasePlugin'] + + +@six.add_metaclass(abc.ABCMeta) +class BasePlugin(object): + """Base plugin to provide ways to log in to dashboard. + + Provides a framework for keystoneclient plugins that can be used with the + information provided to return an unscoped token. + """ + + @abc.abstractmethod + def get_plugin(self, auth_url=None, **kwargs): + """Create a new plugin to attempt to authenticate. + + Given the information provided by the login providers attempt to create + an authentication plugin that can be used to authenticate the user. + + If the provided login information does not contain enough information + for this plugin to proceed then it should return None. + + :param str auth_url: The URL to authenticate against. + + :returns: A plugin that will be used to authenticate or None if the + plugin cannot authenticate with the data provided. + :rtype: keystoneclient.auth.BaseAuthPlugin + """ + return None diff --git a/openstack_auth/plugin/password.py b/openstack_auth/plugin/password.py new file mode 100644 index 00000000..4a1e7c18 --- /dev/null +++ b/openstack_auth/plugin/password.py @@ -0,0 +1,45 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.auth.identity import v2 as v2_auth +from keystoneclient.auth.identity import v3 as v3_auth + +from openstack_auth.plugin import base +from openstack_auth import utils + + +__all__ = ['PasswordPlugin'] + + +class PasswordPlugin(base.BasePlugin): + """Authenticate against keystone given a username and password. + + This is the default login mechanism. Given a username and password inputted + from a login form returns a v2 or v3 keystone Password plugin for + authentication. + """ + + def get_plugin(self, auth_url=None, username=None, password=None, + user_domain_name=None, **kwargs): + if not all((auth_url, username, password)): + return None + + if utils.get_keystone_version() >= 3: + return v3_auth.Password(auth_url=auth_url, + username=username, + password=password, + user_domain_name=user_domain_name) + + else: + return v2_auth.Password(auth_url=auth_url, + username=username, + password=password) diff --git a/openstack_auth/utils.py b/openstack_auth/utils.py index fb00b8ed..034d5c8b 100644 --- a/openstack_auth/utils.py +++ b/openstack_auth/utils.py @@ -14,7 +14,9 @@ import datetime import functools import logging +import sys +import django from django.conf import settings from django.contrib import auth from django.contrib.auth import middleware @@ -27,6 +29,7 @@ from keystoneclient.auth import token_endpoint from keystoneclient import session from keystoneclient.v2_0 import client as client_v2 from keystoneclient.v3 import client as client_v3 +import six from six.moves.urllib import parse as urlparse @@ -211,19 +214,6 @@ def fix_auth_url_version(auth_url): return auth_url -def get_password_auth_plugin(auth_url, username, password, user_domain_name): - if get_keystone_version() >= 3: - return v3_auth.Password(auth_url=auth_url, - username=username, - password=password, - user_domain_name=user_domain_name) - - else: - return v2_auth.Password(auth_url=auth_url, - username=username, - password=password) - - def get_token_auth_plugin(auth_url, token, project_id): if get_keystone_version() >= 3: return v3_auth.Token(auth_url=auth_url, @@ -295,3 +285,65 @@ def set_response_cookie(response, cookie_name, cookie_value): now = timezone.now() expire_date = now + datetime.timedelta(days=365) response.set_cookie(cookie_name, cookie_value, expires=expire_date) + + +if django.VERSION < (1, 7): + try: + from importlib import import_module + except ImportError: + # NOTE(jamielennox): importlib was introduced in python 2.7. This is + # copied from the backported importlib library. See: + # http://svn.python.org/projects/python/trunk/Lib/importlib/__init__.py + + def _resolve_name(name, package, level): + """Return the absolute name of the module to be imported.""" + if not hasattr(package, 'rindex'): + raise ValueError("'package' not set to a string") + dot = len(package) + for x in xrange(level, 1, -1): + try: + dot = package.rindex('.', 0, dot) + except ValueError: + raise ValueError("attempted relative import beyond " + "top-level package") + return "%s.%s" % (package[:dot], name) + + def import_module(name, package=None): + """Import a module. + + The 'package' argument is required when performing a relative + import. It specifies the package to use as the anchor point from + which to resolve the relative import to an absolute import. + """ + if name.startswith('.'): + if not package: + raise TypeError("relative imports require the " + "'package' argument") + level = 0 + for character in name: + if character != '.': + break + level += 1 + name = _resolve_name(name[level:], package, level) + __import__(name) + return sys.modules[name] + + # NOTE(jamielennox): copied verbatim from django 1.7 + def import_string(dotted_path): + try: + module_path, class_name = dotted_path.rsplit('.', 1) + except ValueError: + msg = "%s doesn't look like a module path" % dotted_path + six.reraise(ImportError, ImportError(msg), sys.exc_info()[2]) + + module = import_module(module_path) + + try: + return getattr(module, class_name) + except AttributeError: + msg = 'Module "%s" does not define a "%s" attribute/class' % ( + dotted_path, class_name) + six.reraise(ImportError, ImportError(msg), sys.exc_info()[2]) + +else: + from django.utils.module_loading import import_string # noqa