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:
parent
07f1649457
commit
e6c25ad380
@ -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):
|
||||
|
18
openstack_auth/plugin/__init__.py
Normal file
18
openstack_auth/plugin/__init__.py
Normal 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']
|
44
openstack_auth/plugin/base.py
Normal file
44
openstack_auth/plugin/base.py
Normal 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
|
45
openstack_auth/plugin/password.py
Normal file
45
openstack_auth/plugin/password.py
Normal 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)
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user