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):
|
class KeystoneBackend(object):
|
||||||
"""Django authentication backend for use with ``django.contrib.auth``."""
|
"""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):
|
def check_auth_expiry(self, auth_ref, margin=None):
|
||||||
if not utils.is_token_valid(auth_ref, margin):
|
if not utils.is_token_valid(auth_ref, margin):
|
||||||
msg = _("The authentication token issued by the Identity service "
|
msg = _("The authentication token issued by the Identity service "
|
||||||
@ -64,25 +79,26 @@ class KeystoneBackend(object):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def authenticate(self, request=None, username=None, password=None,
|
def authenticate(self, auth_url=None, **kwargs):
|
||||||
user_domain_name=None, auth_url=None):
|
|
||||||
"""Authenticates a user via the Keystone Identity API."""
|
"""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 not auth_url:
|
||||||
|
|
||||||
if auth_url is None:
|
|
||||||
auth_url = settings.OPENSTACK_KEYSTONE_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()
|
session = utils.get_session()
|
||||||
keystone_client_class = utils.get_keystone_client().Client
|
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:
|
try:
|
||||||
unscoped_auth_ref = unscoped_auth.get_access(session)
|
unscoped_auth_ref = unscoped_auth.get_access(session)
|
||||||
except (keystone_exceptions.Unauthorized,
|
except (keystone_exceptions.Unauthorized,
|
||||||
@ -126,6 +142,8 @@ class KeystoneBackend(object):
|
|||||||
|
|
||||||
# the recent project id a user might have set in a cookie
|
# the recent project id a user might have set in a cookie
|
||||||
recent_project = None
|
recent_project = None
|
||||||
|
request = kwargs.get('request')
|
||||||
|
|
||||||
if request:
|
if request:
|
||||||
# Check if token is automatically scoped to default_project
|
# Check if token is automatically scoped to default_project
|
||||||
# grab the project from this token, to use as a default
|
# 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.
|
# Check expiry for our new scoped token.
|
||||||
self.check_auth_expiry(scoped_auth_ref)
|
self.check_auth_expiry(scoped_auth_ref)
|
||||||
|
|
||||||
|
interface = getattr(settings, 'OPENSTACK_ENDPOINT_TYPE', 'public')
|
||||||
|
|
||||||
# If we made it here we succeeded. Create our User!
|
# If we made it here we succeeded. Create our User!
|
||||||
user = auth_user.create_user_from_token(
|
user = auth_user.create_user_from_token(
|
||||||
request,
|
request,
|
||||||
@ -177,7 +197,7 @@ class KeystoneBackend(object):
|
|||||||
# Support client caching to save on auth calls.
|
# Support client caching to save on auth calls.
|
||||||
setattr(request, KEYSTONE_CLIENT_ATTR, scoped_client)
|
setattr(request, KEYSTONE_CLIENT_ATTR, scoped_client)
|
||||||
|
|
||||||
LOG.debug('Authentication completed for user "%s".' % username)
|
LOG.debug('Authentication completed.')
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def get_group_permissions(self, user, obj=None):
|
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 datetime
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.contrib.auth import middleware
|
from django.contrib.auth import middleware
|
||||||
@ -27,6 +29,7 @@ from keystoneclient.auth import token_endpoint
|
|||||||
from keystoneclient import session
|
from keystoneclient import session
|
||||||
from keystoneclient.v2_0 import client as client_v2
|
from keystoneclient.v2_0 import client as client_v2
|
||||||
from keystoneclient.v3 import client as client_v3
|
from keystoneclient.v3 import client as client_v3
|
||||||
|
import six
|
||||||
from six.moves.urllib import parse as urlparse
|
from six.moves.urllib import parse as urlparse
|
||||||
|
|
||||||
|
|
||||||
@ -211,19 +214,6 @@ def fix_auth_url_version(auth_url):
|
|||||||
return 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):
|
def get_token_auth_plugin(auth_url, token, project_id):
|
||||||
if get_keystone_version() >= 3:
|
if get_keystone_version() >= 3:
|
||||||
return v3_auth.Token(auth_url=auth_url,
|
return v3_auth.Token(auth_url=auth_url,
|
||||||
@ -295,3 +285,65 @@ def set_response_cookie(response, cookie_name, cookie_value):
|
|||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
expire_date = now + datetime.timedelta(days=365)
|
expire_date = now + datetime.timedelta(days=365)
|
||||||
response.set_cookie(cookie_name, cookie_value, expires=expire_date)
|
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