summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2015-04-01 19:13:43 +0000
committerGerrit Code Review <review@openstack.org>2015-04-01 19:13:43 +0000
commitf5b2827a4de3375ff0c39dbe2884feb5cac0c740 (patch)
tree52faa488578c591f88a4025f2efc9368ea1b2dad
parent3e3080205990bf7d967acd72e266ecf89c3159ed (diff)
parent302f422568a32b513ffbb3089ba799a4416df108 (diff)
downloaddjango_openstack_auth-f5b2827a4de3375ff0c39dbe2884feb5cac0c740.tar.gz
Merge "Add authentication using openID and SAML"
-rw-r--r--openstack_auth/backend.py9
-rw-r--r--openstack_auth/forms.py18
-rw-r--r--openstack_auth/plugin/base.py5
-rw-r--r--openstack_auth/tests/data_v3.py68
-rw-r--r--openstack_auth/tests/tests.py83
-rw-r--r--openstack_auth/tests/urls.py1
-rw-r--r--openstack_auth/urls.py6
-rw-r--r--openstack_auth/user.py33
-rw-r--r--openstack_auth/utils.py12
-rw-r--r--openstack_auth/views.py50
10 files changed, 273 insertions, 12 deletions
diff --git a/openstack_auth/backend.py b/openstack_auth/backend.py
index f2d08fa..362855c 100644
--- a/openstack_auth/backend.py
+++ b/openstack_auth/backend.py
@@ -95,10 +95,12 @@ class KeystoneBackend(object):
if unscoped_auth:
break
else:
+ msg = _('No authentication backend could be determined to '
+ 'handle the provided credentials.')
LOG.warn('No authentication backend could be determined to '
'handle the provided credentials. This is likely a '
'configuration error that should be addressed.')
- return None
+ raise exceptions.KeystoneAuthException(msg)
session = utils.get_session()
keystone_client_class = utils.get_keystone_client().Client
@@ -174,13 +176,14 @@ class KeystoneBackend(object):
interface = getattr(settings, 'OPENSTACK_ENDPOINT_TYPE', 'public')
# If we made it here we succeeded. Create our User!
+ unscoped_token = unscoped_auth_ref.auth_token
user = auth_user.create_user_from_token(
request,
- auth_user.Token(scoped_auth_ref),
+ auth_user.Token(scoped_auth_ref, unscoped_token=unscoped_token),
scoped_auth_ref.service_catalog.url_for(endpoint_type=interface))
if request is not None:
- request.session['unscoped_token'] = unscoped_auth_ref.auth_token
+ request.session['unscoped_token'] = unscoped_token
request.user = user
scoped_client = keystone_client_class(session=session,
auth=scoped_auth)
diff --git a/openstack_auth/forms.py b/openstack_auth/forms.py
index f5afaba..be3fdc7 100644
--- a/openstack_auth/forms.py
+++ b/openstack_auth/forms.py
@@ -21,6 +21,7 @@ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_variables # noqa
from openstack_auth import exceptions
+from openstack_auth import utils
LOG = logging.getLogger(__name__)
@@ -72,6 +73,23 @@ class Login(django_auth_forms.AuthenticationForm):
self.fields['region'].initial = self.request.COOKIES.get(
'login_region')
+ # if websso is enabled and keystone version supported
+ # prepend the websso_choices select input to the form
+ if utils.is_websso_enabled():
+ initial = getattr(settings, 'WEBSSO_INITIAL_CHOICE', 'credentials')
+ choicefield = forms.ChoiceField(
+ label=_("Authenticate using"),
+ choices=getattr(settings, 'WEBSSO_CHOICES', ()),
+ required=False,
+ initial=initial)
+ self.fields.insert(0, 'auth_type', choicefield)
+
+ # websso is enabled, but keystone version is not supported
+ elif getattr(settings, 'WEBSSO_ENABLED', False):
+ msg = ("Websso is enabled but horizon is not configured to work " +
+ "with keystone version 3 or above.")
+ LOG.warning(msg)
+
@staticmethod
def get_region_choices():
default_region = (settings.OPENSTACK_KEYSTONE_URL, "Default Region")
diff --git a/openstack_auth/plugin/base.py b/openstack_auth/plugin/base.py
index bd0b200..a2fc755 100644
--- a/openstack_auth/plugin/base.py
+++ b/openstack_auth/plugin/base.py
@@ -81,7 +81,10 @@ class BasePlugin(object):
try:
if self.keystone_version >= 3:
client = v3_client.Client(session=session, auth=auth_plugin)
- return client.projects.list(user=auth_ref.user_id)
+ if auth_ref.is_federated:
+ return client.federation.projects.list()
+ else:
+ return client.projects.list(user=auth_ref.user_id)
else:
client = v2_client.Client(session=session, auth=auth_plugin)
diff --git a/openstack_auth/tests/data_v3.py b/openstack_auth/tests/data_v3.py
index be95b15..9018561 100644
--- a/openstack_auth/tests/data_v3.py
+++ b/openstack_auth/tests/data_v3.py
@@ -244,4 +244,72 @@ def generate_test_data():
'catalog': [keystone_service, nova_service],
}, token=auth_token)
+ # federated user
+ federated_scoped_token_dict = {
+ 'token': {
+ 'methods': ['password'],
+ 'expires_at': expiration,
+ 'project': {
+ 'id': project_dict_1['id'],
+ 'name': project_dict_1['name'],
+ 'domain': {
+ 'id': domain_dict['id'],
+ 'name': domain_dict['name']
+ }
+ },
+ 'user': {
+ 'id': user_dict['id'],
+ 'name': user_dict['name'],
+ 'domain': {
+ 'id': domain_dict['id'],
+ 'name': domain_dict['name']
+ },
+ 'OS-FEDERATION': {
+ 'identity_provider': 'ACME',
+ 'protocol': 'OIDC',
+ 'groups': [
+ {'id': uuid.uuid4().hex},
+ {'id': uuid.uuid4().hex}
+ ]
+ }
+ },
+ 'roles': [role_dict],
+ 'catalog': [keystone_service, nova_service]
+ }
+ }
+
+ test_data.federated_scoped_access_info = access.AccessInfo.factory(
+ resp=auth_response,
+ body=federated_scoped_token_dict
+ )
+
+ federated_unscoped_token_dict = {
+ 'token': {
+ 'methods': ['password'],
+ 'expires_at': expiration,
+ 'user': {
+ 'id': user_dict['id'],
+ 'name': user_dict['name'],
+ 'domain': {
+ 'id': domain_dict['id'],
+ 'name': domain_dict['name']
+ },
+ 'OS-FEDERATION': {
+ 'identity_provider': 'ACME',
+ 'protocol': 'OIDC',
+ 'groups': [
+ {'id': uuid.uuid4().hex},
+ {'id': uuid.uuid4().hex}
+ ]
+ }
+ },
+ 'catalog': [keystone_service]
+ }
+ }
+
+ test_data.federated_unscoped_access_info = access.AccessInfo.factory(
+ resp=auth_response,
+ body=federated_unscoped_token_dict
+ )
+
return test_data
diff --git a/openstack_auth/tests/tests.py b/openstack_auth/tests/tests.py
index dbfa345..76e6519 100644
--- a/openstack_auth/tests/tests.py
+++ b/openstack_auth/tests/tests.py
@@ -791,6 +791,89 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin, test.TestCase):
self.assertIsNone(utils._PROJECT_CACHE.get(unscoped.auth_token))
+class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin, test.TestCase):
+
+ def _create_token_auth(self, project_id=None, token=None, url=None):
+ if not token:
+ token = self.data.federated_unscoped_access_info.auth_token
+
+ if not url:
+ url = settings.OPENSTACK_KEYSTONE_URL
+
+ return auth_v3.Token(auth_url=url,
+ token=token,
+ project_id=project_id,
+ reauthenticate=False)
+
+ def _mock_unscoped_client(self, unscoped):
+ plugin = self._create_token_auth(
+ None,
+ token=unscoped.auth_token,
+ url=settings.OPENSTACK_KEYSTONE_URL)
+ plugin.get_access(mox.IsA(session.Session)).AndReturn(unscoped)
+
+ return self.ks_client_module.Client(session=mox.IsA(session.Session),
+ auth=plugin)
+
+ def _mock_unscoped_federated_list_projects(self, client, projects):
+ client.federation = self.mox.CreateMockAnything()
+ client.federation.projects = self.mox.CreateMockAnything()
+ client.federation.projects.list().AndReturn(projects)
+
+ def _mock_unscoped_client_list_projects(self, unscoped, projects):
+ client = self._mock_unscoped_client(unscoped)
+ self._mock_unscoped_federated_list_projects(client, projects)
+
+ def setUp(self):
+ super(OpenStackAuthTestsWebSSO, self).setUp()
+
+ self.mox = mox.Mox()
+ self.addCleanup(self.mox.VerifyAll)
+ self.addCleanup(self.mox.UnsetStubs)
+
+ self.data = data_v3.generate_test_data()
+ self.ks_client_module = client_v3
+
+ settings.OPENSTACK_API_VERSIONS['identity'] = 3
+ settings.OPENSTACK_KEYSTONE_URL = 'http://localhost:5000/v3'
+ settings.WEBSSO_ENABLED = True
+ settings.WEBSSO_CHOICES = (
+ ('credentials', 'Keystone Credentials'),
+ ('oidc', 'OpenID Connect'),
+ ('saml2', 'Security Assertion Markup Language')
+ )
+
+ self.mox.StubOutClassWithMocks(token_endpoint, 'Token')
+ self.mox.StubOutClassWithMocks(auth_v3, 'Token')
+ self.mox.StubOutClassWithMocks(auth_v3, 'Password')
+ self.mox.StubOutClassWithMocks(client_v3, 'Client')
+
+ def test_login_form(self):
+ url = reverse('login')
+
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'credentials')
+ self.assertContains(response, 'oidc')
+ self.assertContains(response, 'saml2')
+
+ def test_web_sso_login(self):
+ projects = [self.data.project_one, self.data.project_two]
+ unscoped = self.data.federated_unscoped_access_info
+ token = unscoped.auth_token
+
+ form_data = {'token': token}
+ self._mock_unscoped_client_list_projects(unscoped, projects)
+ self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id)
+
+ self.mox.ReplayAll()
+
+ url = reverse('websso')
+
+ # POST to the page to log in.
+ response = self.client.post(url, form_data)
+ self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
+
load_tests = load_tests_apply_scenarios
diff --git a/openstack_auth/tests/urls.py b/openstack_auth/tests/urls.py
index be27296..4c9045d 100644
--- a/openstack_auth/tests/urls.py
+++ b/openstack_auth/tests/urls.py
@@ -25,5 +25,6 @@ utils.patch_middleware_get_user()
urlpatterns = patterns(
'',
url(r"", include('openstack_auth.urls')),
+ url(r"^websso/$", "openstack_auth.views.websso", name='websso'),
url(r"^$", generic.TemplateView.as_view(template_name="auth/blank.html"))
)
diff --git a/openstack_auth/urls.py b/openstack_auth/urls.py
index d661805..db11f47 100644
--- a/openstack_auth/urls.py
+++ b/openstack_auth/urls.py
@@ -27,3 +27,9 @@ urlpatterns = patterns(
url(r'^switch_services_region/(?P<region_name>[^/]+)/$', 'switch_region',
name='switch_services_region')
)
+
+if utils.is_websso_enabled():
+ urlpatterns += patterns(
+ 'openstack_auth.views',
+ url(r"^websso/$", "websso", name='websso')
+ )
diff --git a/openstack_auth/user.py b/openstack_auth/user.py
index 947f725..57c6d67 100644
--- a/openstack_auth/user.py
+++ b/openstack_auth/user.py
@@ -53,7 +53,9 @@ def create_user_from_token(request, token, endpoint, services_region=None):
service_catalog=token.serviceCatalog,
roles=token.roles,
endpoint=endpoint,
- services_region=svc_region)
+ services_region=svc_region,
+ is_federated=token.is_federated,
+ unscoped_token=token.unscoped_token)
class Token(object):
@@ -65,7 +67,7 @@ class Token(object):
Added for maintaining backward compatibility with horizon that expects
Token object in the user object.
"""
- def __init__(self, auth_ref):
+ def __init__(self, auth_ref, unscoped_token=None):
# User-related attributes
user = {}
user['id'] = auth_ref.user_id
@@ -76,12 +78,16 @@ class Token(object):
# Token-related attributes
self.id = auth_ref.auth_token
+ self.unscoped_token = unscoped_token
if len(self.id) > 64:
algorithm = getattr(settings, 'OPENSTACK_TOKEN_HASH_ALGORITHM',
'md5')
hasher = hashlib.new(algorithm)
hasher.update(self.id)
self.id = hasher.hexdigest()
+ # If the scoped_token is long, then unscoped_token must be too.
+ hasher.update(self.unscoped_token)
+ self.unscoped_token = hasher.hexdigest()
self.expires = auth_ref.expires
# Project-related attributes
@@ -97,6 +103,9 @@ class Token(object):
domain['name'] = auth_ref.domain_name
self.domain = domain
+ # Federation-related attributes
+ self.is_federated = auth_ref.is_federated
+
if auth_ref.version == 'v2.0':
self.roles = auth_ref['user'].get('roles', [])
else:
@@ -167,13 +176,22 @@ class User(models.AnonymousUser):
The id of the Keystone domain scoped for the current user/token.
+ .. attribute:: is_federated
+
+ Whether user is federated Keystone user. (Boolean)
+
+ .. attribute:: unscoped_token
+
+ Unscoped Keystone token.
+
"""
def __init__(self, id=None, token=None, user=None, tenant_id=None,
service_catalog=None, tenant_name=None, roles=None,
authorized_tenants=None, endpoint=None, enabled=False,
services_region=None, user_domain_id=None,
user_domain_name=None, domain_id=None, domain_name=None,
- project_id=None, project_name=None):
+ project_id=None, project_name=None,
+ is_federated=False, unscoped_token=None):
self.id = id
self.pk = id
self.token = token
@@ -193,6 +211,11 @@ class User(models.AnonymousUser):
self.endpoint = endpoint
self.enabled = enabled
self._authorized_tenants = authorized_tenants
+ self.is_federated = is_federated
+
+ # Unscoped token is used for listing user's project that works
+ # for both federated and keystone user.
+ self.unscoped_token = unscoped_token
# List of variables to be deprecated.
self.tenant_id = self.project_id
@@ -277,12 +300,12 @@ class User(models.AnonymousUser):
"""Returns a memoized list of tenants this user may access."""
if self.is_authenticated() and self._authorized_tenants is None:
endpoint = self.endpoint
- token = self.token
try:
self._authorized_tenants = utils.get_project_list(
user_id=self.id,
auth_url=endpoint,
- token=token.id)
+ token=self.unscoped_token,
+ is_federated=self.is_federated)
except (keystone_exceptions.ClientException,
keystone_exceptions.AuthorizationFailure):
LOG.exception('Unable to retrieve project list.')
diff --git a/openstack_auth/utils.py b/openstack_auth/utils.py
index 034d5c8..198c8a0 100644
--- a/openstack_auth/utils.py
+++ b/openstack_auth/utils.py
@@ -176,6 +176,13 @@ def get_keystone_client():
return client_v3
+def is_websso_enabled():
+ """Websso is supported in Keystone version 3."""
+ websso_enabled = getattr(settings, 'WEBSSO_ENABLED', False)
+ keystonev3_plus = (get_keystone_version() >= 3)
+ return websso_enabled and keystonev3_plus
+
+
def has_in_url_path(url, sub):
"""Test if the `sub` string is in the `url` path."""
scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
@@ -214,7 +221,7 @@ def fix_auth_url_version(auth_url):
return auth_url
-def get_token_auth_plugin(auth_url, token, project_id):
+def get_token_auth_plugin(auth_url, token, project_id=None):
if get_keystone_version() >= 3:
return v3_auth.Token(auth_url=auth_url,
token=token,
@@ -230,6 +237,7 @@ def get_token_auth_plugin(auth_url, token, project_id):
@memoize_by_keyword_arg(_PROJECT_CACHE, ('token', ))
def get_project_list(*args, **kwargs):
+ is_federated = kwargs.get('is_federated', False)
sess = kwargs.get('session') or get_session()
auth_url = fix_auth_url_version(kwargs['auth_url'])
auth = token_endpoint.Token(auth_url, kwargs['token'])
@@ -237,6 +245,8 @@ def get_project_list(*args, **kwargs):
if get_keystone_version() < 3:
projects = client.tenants.list()
+ elif is_federated:
+ projects = client.federation.projects.list()
else:
projects = client.projects.list(user=kwargs.get('user_id'))
diff --git a/openstack_auth/views.py b/openstack_auth/views.py
index 314afd7..8d5a0f5 100644
--- a/openstack_auth/views.py
+++ b/openstack_auth/views.py
@@ -11,6 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
+import re
import time
import django
@@ -18,15 +19,18 @@ from django.conf import settings
from django.contrib import auth
from django.contrib.auth.decorators import login_required # noqa
from django.contrib.auth import views as django_auth_views
+from django import http as django_http
from django import shortcuts
from django.utils import functional
from django.utils import http
from django.views.decorators.cache import never_cache # noqa
+from django.views.decorators.csrf import csrf_exempt # noqa
from django.views.decorators.csrf import csrf_protect # noqa
from django.views.decorators.debug import sensitive_post_parameters # noqa
from keystoneclient.auth import token_endpoint
from keystoneclient import exceptions as keystone_exceptions
+from openstack_auth import exceptions
from openstack_auth import forms
# This is historic and is added back in to not break older versions of
# Horizon, fix to Horizon to remove this requirement was committed in
@@ -49,6 +53,18 @@ LOG = logging.getLogger(__name__)
@never_cache
def login(request, template_name=None, extra_context=None, **kwargs):
"""Logs a user in using the :class:`~openstack_auth.forms.Login` form."""
+
+ # If the user enabled websso and selects default protocol
+ # from the dropdown, We need to redirect user to the websso url
+ if request.method == 'POST':
+ protocol = request.POST.get('auth_type', 'credentials')
+ if utils.is_websso_enabled() and protocol != 'credentials':
+ region = request.POST.get('region')
+ origin = request.build_absolute_uri('/auth/websso/')
+ url = ('%s/auth/OS-FEDERATION/websso/%s?origin=%s' %
+ (region, protocol, origin))
+ return shortcuts.redirect(url)
+
if not request.is_ajax():
# If the user is already authenticated, redirect them to the
# dashboard straight away, unless the 'next' parameter is set as it
@@ -112,6 +128,30 @@ def login(request, template_name=None, extra_context=None, **kwargs):
return res
+@sensitive_post_parameters()
+@csrf_exempt
+@never_cache
+def websso(request):
+ """Logs a user in using a token from Keystone's POST."""
+ referer = request.META.get('HTTP_REFERER', settings.OPENSTACK_KEYSTONE_URL)
+ auth_url = re.sub(r'/auth.*', '', referer)
+ token = request.POST.get('token')
+ try:
+ request.user = auth.authenticate(request=request, auth_url=auth_url,
+ token=token)
+ except exceptions.KeystoneAuthException as exc:
+ msg = 'Login failed: %s' % unicode(exc)
+ res = django_http.HttpResponseRedirect(settings.LOGIN_URL)
+ res.set_cookie('logout_reason', msg, max_age=10)
+ return res
+
+ auth_user.set_session_from_user(request, request.user)
+ auth.login(request, request.user)
+ if request.session.test_cookie_worked():
+ request.session.delete_test_cookie()
+ return django_http.HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
+
+
def logout(request, login_url=None, **kwargs):
"""Logs out the user if he is logged in. Then redirects to the log-in page.
@@ -165,8 +205,12 @@ def switch(request, tenant_id, redirect_field_name=auth.REDIRECT_FIELD_NAME):
endpoint = utils.fix_auth_url_version(request.user.endpoint)
session = utils.get_session()
+ # Keystone can be configured to prevent exchanging a scoped token for
+ # another token. Always use the unscoped token for requesting a
+ # scoped token.
+ unscoped_token = request.user.unscoped_token
auth = utils.get_token_auth_plugin(auth_url=endpoint,
- token=request.user.token.id,
+ token=unscoped_token,
project_id=tenant_id)
try:
@@ -193,7 +237,9 @@ def switch(request, tenant_id, redirect_field_name=auth.REDIRECT_FIELD_NAME):
if old_token and old_endpoint and old_token.id != auth_ref.auth_token:
delete_token(endpoint=old_endpoint, token_id=old_token.id)
user = auth_user.create_user_from_token(
- request, auth_user.Token(auth_ref), endpoint)
+ request,
+ auth_user.Token(auth_ref, unscoped_token=unscoped_token),
+ endpoint)
auth_user.set_session_from_user(request, user)
response = shortcuts.redirect(redirect_to)
utils.set_response_cookie(response, 'recent_project',