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
This commit is contained in:
Jamie Lennox 2015-02-05 09:21:00 +00:00
parent 07f1649457
commit e6c25ad380
5 changed files with 205 additions and 26 deletions

View File

@ -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):

View File

@ -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']

View File

@ -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

View File

@ -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)

View File

@ -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