From a2fc6cf4f4cd718e8ca01d2b692193b69fc724ad Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 25 Feb 2015 02:11:47 -0500 Subject: [PATCH] Add support to create SAML assertion based on a token A user should be able to exchange their token for a SAML assertion that is valid on a service provider (the user should must provide this data). implements bp generate-saml-assertions Change-Id: I5cb635929c7f6823ab1e4b1db5e48045be9e0737 --- .../tests/unit/v3/saml2_fixtures.py | 84 +++++++++++++++++++ .../tests/unit/v3/test_auth_saml2.py | 33 ++++++++ keystoneclient/v3/contrib/federation/core.py | 2 + keystoneclient/v3/contrib/federation/saml.py | 56 +++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 keystoneclient/v3/contrib/federation/saml.py diff --git a/keystoneclient/tests/unit/v3/saml2_fixtures.py b/keystoneclient/tests/unit/v3/saml2_fixtures.py index 2ecae6ad8..513082df6 100644 --- a/keystoneclient/tests/unit/v3/saml2_fixtures.py +++ b/keystoneclient/tests/unit/v3/saml2_fixtures.py @@ -169,3 +169,87 @@ DOMAINS = { "next": 'null' } } + +TOKEN_BASED_SAML = """ + + + + http://keystone.idp/v3/OS-FEDERATION/saml2/idp + + + + + + + http://keystone.idp/v3/OS-FEDERATION/saml2/idp + + + + + + + + + + + + 0KH2CxdkfzU+6eiRhTC+mbObUKI= + + + + + m2jh5gDvX/1k+4uKtbb08CHp2b9UWsLw + + + + ... + + + + + admin + + + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + http://keystone.idp/v3/OS-FEDERATION/saml2/idp + + + + + + admin + + + admin + + + admin + + + + +""" diff --git a/keystoneclient/tests/unit/v3/test_auth_saml2.py b/keystoneclient/tests/unit/v3/test_auth_saml2.py index 33bfdac2d..f77a163f6 100644 --- a/keystoneclient/tests/unit/v3/test_auth_saml2.py +++ b/keystoneclient/tests/unit/v3/test_auth_saml2.py @@ -25,6 +25,7 @@ from keystoneclient import session from keystoneclient.tests.unit.v3 import client_fixtures from keystoneclient.tests.unit.v3 import saml2_fixtures from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib.federation import saml as saml_manager ROOTDIR = os.path.dirname(os.path.abspath(__file__)) XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/') @@ -621,3 +622,35 @@ class AuthenticateviaADFSTests(utils.TestCase): token, token_json = self.adfsplugin._get_unscoped_token(self.session) self.assertEqual(token, client_fixtures.AUTH_SUBJECT_TOKEN) self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_json) + + +class SAMLGenerationTests(utils.TestCase): + + def setUp(self): + super(SAMLGenerationTests, self).setUp() + self.manager = self.client.federation.saml + self.SAML2_FULL_URL = ''.join([self.TEST_URL, + saml_manager.SAML2_ENDPOINT]) + + def test_saml_create(self): + """Test that a token can be exchanged for a SAML assertion.""" + + token_id = uuid.uuid4().hex + service_provider_id = uuid.uuid4().hex + + # Mock the returned text for '/auth/OS-FEDERATION/saml2 + self.requests_mock.post(self.SAML2_FULL_URL, + text=saml2_fixtures.TOKEN_BASED_SAML) + + text = self.manager.create_saml_assertion(service_provider_id, + token_id) + + # Ensure returned text is correct + self.assertEqual(saml2_fixtures.TOKEN_BASED_SAML, text) + + # Ensure request headers and body are correct + req_json = self.requests_mock.last_request.json() + self.assertEqual(token_id, req_json['auth']['identity']['token']['id']) + self.assertEqual(service_provider_id, + req_json['auth']['scope']['service_provider']['id']) + self.assertRequestHeaderEqual('Content-Type', 'application/json') diff --git a/keystoneclient/v3/contrib/federation/core.py b/keystoneclient/v3/contrib/federation/core.py index b8074606b..2e12cf607 100644 --- a/keystoneclient/v3/contrib/federation/core.py +++ b/keystoneclient/v3/contrib/federation/core.py @@ -15,6 +15,7 @@ from keystoneclient.v3.contrib.federation import identity_providers from keystoneclient.v3.contrib.federation import mappings from keystoneclient.v3.contrib.federation import projects from keystoneclient.v3.contrib.federation import protocols +from keystoneclient.v3.contrib.federation import saml from keystoneclient.v3.contrib.federation import service_providers @@ -26,4 +27,5 @@ class FederationManager(object): self.protocols = protocols.ProtocolManager(api) self.projects = projects.ProjectManager(api) self.domains = domains.DomainManager(api) + self.saml = saml.SamlManager(api) self.service_providers = service_providers.ServiceProviderManager(api) diff --git a/keystoneclient/v3/contrib/federation/saml.py b/keystoneclient/v3/contrib/federation/saml.py new file mode 100644 index 000000000..c3cd28663 --- /dev/null +++ b/keystoneclient/v3/contrib/federation/saml.py @@ -0,0 +1,56 @@ +# 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 import base + + +SAML2_ENDPOINT = '/auth/OS-FEDERATION/saml2' + + +class SamlManager(base.Manager): + """Manager class for creating SAML assertions.""" + + def create_saml_assertion(self, service_provider, token_id): + """Create a SAML assertion from a token. + + Equivalent Identity API call: + POST /auth/OS-FEDERATION/saml2 + + :param service_provider: Service Provider resource. + :type service_provider: string + :param token_id: Token to transform to SAML assertion. + :type token_id: string + + :returns: SAML representation of token_id + :rtype: string + """ + + body = { + 'auth': { + 'identity': { + 'methods': ['token'], + 'token': { + 'id': token_id + } + }, + 'scope': { + 'service_provider': { + 'id': base.getid(service_provider) + } + } + } + } + + headers = {'Content-Type': 'application/json'} + resp, body = self.client.post(SAML2_ENDPOINT, json=body, + headers=headers) + return resp.text