Forked from jeffdeville/django_openstack_auth changes.diff
Last active
August 29, 2015 14:17
-
-
Save jgarber/6a49b0400d7ba81fe1cb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This was just a branch off of master. We're not using the Federated path, | |
but we are using SSO (it'd be nice if we could make this a separate setting) | |
"""Logs a user in using a token from Keystone's POST.""" | |
referer = request.META.get('HTTP_REFERER') | |
auth_url = re.sub(r'/auth.*', '', referer) | |
- request.federated_login = True | |
+ request.federated_login = False | |
request.user = auth.authenticate(request=request, auth_url=auth_url) | |
auth_user.set_session_from_user(request, request.user) | |
auth.login(request, request.user) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This works, but it's a bit crufty. We'll clean it up once we've got the api work | |
sorted out. But if you look at it conceptually, you can see that it's extremely | |
similar to what's been done in Keystone, but rather than put it in the federated | |
controller, we've just added the changes to the auth controller | |
From c999fb584385de906dcd4744701f29711f51cf0e Mon Sep 17 00:00:00 2001 | |
From: Jeff Deville <jeffdeville@gmail.com> | |
Date: Thu, 19 Mar 2015 16:40:23 -0400 | |
Subject: [PATCH] * add a new auth mechanism called InferredDomain that will | |
set the REMOTE_DOMAIN info based on the REMOTE_USER value * sso working in | |
keystone using the remote provider * Update the url for the return call to | |
live under the mod_openidc plugin's sphere of influence | |
--- | |
etc/sso_callback_template.html | 22 +++++++++ | |
keystone/auth/controllers.py | 47 +++++++++++++++++-- | |
keystone/auth/plugins/external.py | 6 +++ | |
keystone/auth/routers.py | 6 +++ | |
keystone/common/config.py | 13 +++++- | |
keystone/tests/default_fixtures.py | 2 +- | |
keystone/tests/test_auth_plugin.py | 91 +++++++++++++++++++++++++++++++++++- | |
keystone/tests/test_v3_federation.py | 2 +- | |
9 files changed, 188 insertions(+), 9 deletions(-) | |
create mode 100644 etc/sso_callback_template.html | |
diff --git a/etc/sso_callback_template.html b/etc/sso_callback_template.html | |
new file mode 100644 | |
index 0000000..3364d69 | |
--- /dev/null | |
+++ b/etc/sso_callback_template.html | |
@@ -0,0 +1,22 @@ | |
+<!DOCTYPE html> | |
+<html xmlns="http://www.w3.org/1999/xhtml"> | |
+ <head> | |
+ <title>Keystone WebSSO redirect</title> | |
+ </head> | |
+ <body> | |
+ <form id="sso" name="sso" action="$host" method="post"> | |
+ Please wait... | |
+ <br/> | |
+ <input type="hidden" name="token" id="token" value="$token"/> | |
+ <noscript> | |
+ <input type="submit" name="submit_no_javascript" id="submit_no_javascript" | |
+ value="If your JavaScript is disabled, please click to continue"/> | |
+ </noscript> | |
+ </form> | |
+ <script type="text/javascript"> | |
+ window.onload = function() { | |
+ document.forms['sso'].submit(); | |
+ } | |
+ </script> | |
+ </body> | |
+</html> | |
diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py | |
index 21e4c9b..7df1106 100644 | |
--- a/keystone/auth/controllers.py | |
+++ b/keystone/auth/controllers.py | |
@@ -13,10 +13,12 @@ | |
# under the License. | |
import sys | |
- | |
+import string | |
from keystoneclient.common import cms | |
from oslo.utils import timeutils | |
import six | |
+from six.moves import urllib | |
+import webob | |
from keystone.assignment import controllers as assignment_controllers | |
from keystone.common import authorization | |
@@ -361,7 +363,41 @@ class Auth(controller.V3Controller): | |
super(Auth, self).__init__(*args, **kw) | |
config.setup_authentication() | |
- def authenticate_for_token(self, context, auth=None): | |
+ def sso_auth(self, context, auth=None): | |
+ if 'origin' in context['query_string']: | |
+ origin = context['query_string'].get('origin') | |
+ host = urllib.parse.unquote_plus(origin) | |
+ else: | |
+ msg = 'Request must have an origin query parameter' | |
+ LOG.error(msg) | |
+ raise exception.ValidationError(msg) | |
+ | |
+ if host in CONF.federation.trusted_dashboard: | |
+ # I don't really need token_data, but I'm not sure what's in it either | |
+ auth = { 'identity': {'methods': [] } } | |
+ token_id, token_data = self.authenticate_for_token(context, auth=auth, renderToken=False) | |
+ return self.render_html_response(host, token_id) | |
+ else: | |
+ msg = '%(host)s is not a trusted dashboard host' | |
+ msg = msg % {'host': host} | |
+ LOG.error(msg) | |
+ raise exception.Unauthorized(msg) | |
+ | |
+ def render_html_response(self, host, token_id): | |
+ """Forms an HTML Form from a template with autosubmit.""" | |
+ headers = [('Content-Type', 'text/html')] | |
+ with open(CONF.federation.sso_callback_template) as template: | |
+ src = string.Template(template.read()) | |
+ subs = {'host': host, 'token': token_id} | |
+ body = src.substitute(subs) | |
+ return webob.Response(body=body, status='200', | |
+ headerlist=headers) | |
+ | |
+ def authenticate_for_token(self, context, auth=None, renderToken=True): | |
"""Authenticate user and issue a token.""" | |
include_catalog = 'nocatalog' not in context['query_string'] | |
@@ -397,8 +433,11 @@ class Auth(controller.V3Controller): | |
if trust: | |
self.trust_api.consume_use(trust['id']) | |
- return render_token_data_response(token_id, token_data, | |
- created=True) | |
+ if renderToken: | |
+ return render_token_data_response(token_id, token_data, | |
+ created=True) | |
+ else: | |
+ return (token_id, token_data) | |
except exception.TrustNotFound as e: | |
raise exception.Unauthorized(e) | |
diff --git a/keystone/auth/plugins/external.py b/keystone/auth/plugins/external.py | |
index 3cf51eb..29349bc 100644 | |
--- a/keystone/auth/plugins/external.py | |
+++ b/keystone/auth/plugins/external.py | |
@@ -95,6 +95,12 @@ class Domain(Base): | |
user_ref = self.identity_api.get_user_by_name(username, domain_id) | |
return user_ref | |
+@dependency.requires('assignment_api', 'identity_api') | |
+class InferredDomain(Domain): | |
+ def _authenticate(self, remote_user, context): | |
+ context['environment']['REMOTE_DOMAIN'] = remote_user.split("@")[1] | |
+ return super(InferredDomain, self)._authenticate(remote_user, context) | |
@dependency.requires('assignment_api', 'identity_api') | |
class KerberosDomain(Domain): | |
diff --git a/keystone/auth/routers.py b/keystone/auth/routers.py | |
index 63b4730..56e25f5 100644 | |
--- a/keystone/auth/routers.py | |
+++ b/keystone/auth/routers.py | |
@@ -59,3 +59,9 @@ class Routers(wsgi.RoutersBase): | |
path='/auth/domains', | |
get_action='get_auth_domains', | |
rel=json_home.build_v3_resource_relation('auth_domains')) | |
+ | |
+ self._add_resource( | |
+ mapper, auth_controller, | |
+ path='/auth/OS-FEDERATION/websso/oidc', | |
+ get_post_action='sso_auth', | |
+ rel=json_home.build_v3_resource_relation('auth_domains')) | |
diff --git a/keystone/common/config.py b/keystone/common/config.py | |
index d7f9dd8..cc59f9d 100644 | |
--- a/keystone/common/config.py | |
+++ b/keystone/common/config.py | |
@@ -19,7 +19,7 @@ from oslo import messaging | |
_DEFAULT_AUTH_METHODS = ['external', 'password', 'token'] | |
_CERTFILE = '/etc/keystone/ssl/certs/signing_cert.pem' | |
_KEYFILE = '/etc/keystone/ssl/private/signing_key.pem' | |
- | |
+_SSO_CALLBACK = '/etc/keystone/sso_callback_template.html' | |
FILE_OPTIONS = { | |
None: [ | |
@@ -464,6 +464,17 @@ FILE_OPTIONS = { | |
cfg.StrOpt('assertion_prefix', default='', | |
help='Value to be used when filtering assertion parameters ' | |
'from the environment.'), | |
+ cfg.MultiStrOpt('trusted_dashboard', default=[], | |
+ help='A list of trusted dashboard hosts. Before ' | |
+ 'accepting a Single Sign-On request to return a ' | |
+ 'token, the origin host must be a member of the ' | |
+ 'trusted_dashboard list. This configuration ' | |
+ 'option may be repeated for multiple values. ' | |
+ 'For example: trusted_dashboard=http://acme.com ' | |
+ 'trusted_dashboard=http://beta.com'), | |
+ cfg.StrOpt('sso_callback_template', default=_SSO_CALLBACK, | |
+ help='Location of Single Sign-On callback handler, will ' | |
+ 'return a token to a trusted dashboard host.'), | |
], | |
'policy': [ | |
cfg.StrOpt('driver', | |
diff --git a/keystone/tests/default_fixtures.py b/keystone/tests/default_fixtures.py | |
index fb8ea04..976b476 100644 | |
--- a/keystone/tests/default_fixtures.py | |
+++ b/keystone/tests/default_fixtures.py | |
@@ -49,7 +49,7 @@ TENANTS = [ | |
USERS = [ | |
{ | |
'id': 'foo', | |
- 'name': 'FOO', | |
+ 'name': 'FOO@Default@ssosite.com', | |
'domain_id': DEFAULT_DOMAIN_ID, | |
'password': 'foo2', | |
'tenants': ['bar'], | |
diff --git a/keystone/tests/test_auth_plugin.py b/keystone/tests/test_auth_plugin.py | |
index 90a3b9e..22fe688 100644 | |
--- a/keystone/tests/test_auth_plugin.py | |
+++ b/keystone/tests/test_auth_plugin.py | |
@@ -13,13 +13,16 @@ | |
# under the License. | |
import uuid | |
- | |
import mock | |
+import os | |
+from six.moves import urllib | |
from keystone import auth | |
from keystone import exception | |
from keystone import tests | |
- | |
+from keystone.tests import core | |
+from keystone.tests.ksfixtures import database | |
+from keystone.tests import default_fixtures | |
# for testing purposes only | |
METHOD_NAME = 'simple_challenge_response' | |
@@ -160,6 +163,90 @@ class TestInvalidAuthMethodRegistration(tests.TestCase): | |
self.assertRaises(ValueError, auth.controllers.load_auth_methods) | |
+class TestInferredDomain(tests.TestCase): | |
+ SSO_TEMPLATE_NAME = 'sso_callback_template.html' | |
+ SSO_TEMPLATE_PATH = os.path.join(core.dirs.etc(), SSO_TEMPLATE_NAME) | |
+ TRUSTED_DASHBOARD = 'http://horizon.com' | |
+ ORIGIN = urllib.parse.quote_plus(TRUSTED_DASHBOARD) | |
+ METHOD_NAME = 'keystone.auth.plugins.external.InferredDomain' | |
+ | |
+ def setUp(self): | |
+ self.useFixture(database.Database()) | |
+ super(TestInferredDomain, self).setUp() | |
+ | |
+ self.load_backends() | |
+ self.load_fixtures(default_fixtures) | |
+ self.api = auth.controllers.Auth() | |
+ | |
+ def config_files(self): | |
+ config_files = super(TestInferredDomain, self).config_files() | |
+ config_files.append(tests.dirs.tests_conf('test_auth_plugin.conf')) | |
+ return config_files | |
+ | |
+ def config_overrides(self): | |
+ super(TestInferredDomain, self).config_overrides() | |
+ method_opts = dict( | |
+ [ | |
+ ('external', 'keystone.auth.plugins.external.InferredDomain'), | |
+ ('password', 'keystone.auth.plugins.password.Password'), | |
+ ('token', 'keystone.auth.plugins.token.Token'), | |
+ ]) | |
+ self.auth_plugin_config_override( | |
+ methods=['external', 'password', 'token'], | |
+ **method_opts) | |
+ | |
+ def test_render_callback_template(self): | |
+ self.config_fixture.config( | |
+ group='federation', | |
+ trusted_dashboard=[self.TRUSTED_DASHBOARD], | |
+ sso_callback_template=self.SSO_TEMPLATE_PATH) | |
+ token_id = uuid.uuid4().hex | |
+ auth_controller = auth.controllers.Auth() | |
+ resp = auth_controller.render_html_response(self.TRUSTED_DASHBOARD, | |
+ token_id) | |
+ self.assertIn(token_id, resp.body) | |
+ self.assertIn(self.TRUSTED_DASHBOARD, resp.body) | |
+ | |
+ def test_federated_sso_missing_query(self): | |
+ environment = {} | |
+ context = {'environment': environment, 'query_string': []} | |
+ auth_controller = auth.controllers.Auth() | |
+ self.assertRaises(exception.ValidationError, | |
+ auth_controller.sso_auth, | |
+ context) | |
+ | |
+ def test_federated_sso_untrusted_dashboard(self): | |
+ environment = {} | |
+ context = { | |
+ 'environment': environment, | |
+ 'query_string': {'origin': uuid.uuid4().hex}, | |
+ } | |
+ auth_controller = auth.controllers.Auth() | |
+ self.assertRaises(exception.Unauthorized, | |
+ auth_controller.sso_auth, | |
+ context) | |
+ | |
+ def test_redirect_from_SSO_login(self): | |
+ self.config_fixture.config( | |
+ group='federation', | |
+ trusted_dashboard=[self.TRUSTED_DASHBOARD], | |
+ sso_callback_template=self.SSO_TEMPLATE_PATH) | |
+ context = { | |
+ 'environment': { | |
+ 'REMOTE_USER': "FOO@Default@ssosite.com", | |
+ 'REMOTE_DOMAIN': "default", | |
+ }, | |
+ 'query_string': {'origin': self.ORIGIN} | |
+ } | |
+ # Ok, I think that if | |
+ auth_controller = auth.controllers.Auth() | |
+ resp = auth_controller.sso_auth(context) | |
+ self.assertIn(self.TRUSTED_DASHBOARD, resp.body) | |
+ | |
+ | |
class TestMapped(tests.TestCase): | |
def setUp(self): | |
super(TestMapped, self).setUp() | |
diff --git a/keystone/tests/test_v3_federation.py b/keystone/tests/test_v3_federation.py | |
index 202e61c..7447a96 100644 | |
--- a/keystone/tests/test_v3_federation.py | |
+++ b/keystone/tests/test_v3_federation.py | |
@@ -37,7 +37,7 @@ from keystone.openstack.common import log | |
from keystone.tests import federation_fixtures | |
from keystone.tests import mapping_fixtures | |
from keystone.tests import test_v3 | |
- | |
+from oslo_config import fixture as config_fixture | |
CONF = config.CONF | |
LOG = log.getLogger(__name__) | |
-- | |
2.2.1 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment