summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamie Lennox <jamielennox@redhat.com>2015-02-05 09:21:00 +0000
committerJamie Lennox <jamielennox@redhat.com>2015-03-25 05:32:25 +1100
commite6c25ad380dc1feb0ed121d088151d9fde8cacef (patch)
tree6d721147fbf9f1d83be6507064f0e59499eadd19
parent07f1649457aa4e6eeb3c8cdc9e5faeb89e6173dc (diff)
downloaddjango_openstack_auth-e6c25ad380dc1feb0ed121d088151d9fde8cacef.tar.gz
Create plugin model for DOA authentication
With federated and kerberos logins coming we need an extensible way to specify additional ways to fetch an unscoped token from keystone. Create a plugin model that when authenticate is called a series of plugins can be queried for a token depending on the information provided. Closes-Bug: #1433389 Change-Id: Ifbd7077173844a8eb3400799fd512b62a5dc7dcc
-rw-r--r--openstack_auth/backend.py46
-rw-r--r--openstack_auth/plugin/__init__.py18
-rw-r--r--openstack_auth/plugin/base.py44
-rw-r--r--openstack_auth/plugin/password.py45
-rw-r--r--openstack_auth/utils.py78
5 files changed, 205 insertions, 26 deletions
diff --git a/openstack_auth/backend.py b/openstack_auth/backend.py
index ef91448..437b09b 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 0000000..26c2b49
--- /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 0000000..fed0cc2
--- /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 0000000..4a1e7c1
--- /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 fb00b8e..034d5c8 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