diff --git a/keystoneclient/contrib/auth/v3/saml2.py b/keystoneclient/contrib/auth/v3/saml2.py index 6434bd435..947c1e781 100644 --- a/keystoneclient/contrib/auth/v3/saml2.py +++ b/keystoneclient/contrib/auth/v3/saml2.py @@ -141,7 +141,7 @@ class Saml2UnscopedToken(v3.AuthConstructor): def _first(self, _list): if len(_list) != 1: - raise IndexError("Only single element list can be flatten") + raise IndexError("Only single element is acceptable") return _list[0] def _prepare_idp_saml2_request(self, saml2_authn_request): @@ -409,3 +409,27 @@ class Saml2UnscopedToken(v3.AuthConstructor): token, token_json = self._get_unscoped_token(session, **kwargs) return access.AccessInfoV3(token, **token_json) + + +class Saml2ScopedTokenMethod(v3.TokenMethod): + _method_name = 'saml2' + + def get_auth_data(self, session, auth, headers, **kwargs): + """Build and return request body for token scoping step.""" + + t = super(Saml2ScopedTokenMethod, self).get_auth_data( + session, auth, headers, **kwargs) + _token_method, token = t + return self._method_name, token + + +class Saml2ScopedToken(v3.Token): + """Class for scoping unscoped saml2 token.""" + + _auth_method_class = Saml2ScopedTokenMethod + + def __init__(self, auth_url, token, **kwargs): + super(Saml2ScopedToken, self).__init__(auth_url, token, **kwargs) + if not (self.project_id or self.domain_id): + raise exceptions.ValidationError( + 'Neither project nor domain specified') diff --git a/keystoneclient/tests/v3/saml2_fixtures.py b/keystoneclient/tests/v3/saml2_fixtures.py index 3327120e6..afe3536ee 100644 --- a/keystoneclient/tests/v3/saml2_fixtures.py +++ b/keystoneclient/tests/v3/saml2_fixtures.py @@ -119,3 +119,50 @@ UNSCOPED_TOKEN = { } } } + +PROJECTS = { + "projects": [ + { + "domain_id": "37ef61", + "enabled": 'true', + "id": "12d706", + "links": { + "self": "http://identity:35357/v3/projects/12d706" + }, + "name": "a project name" + }, + { + "domain_id": "37ef61", + "enabled": 'true', + "id": "9ca0eb", + "links": { + "self": "http://identity:35357/v3/projects/9ca0eb" + }, + "name": "another project" + } + ], + "links": { + "self": "http://identity:35357/v3/OS-FEDERATION/projects", + "previous": 'null', + "next": 'null' + } +} + +DOMAINS = { + "domains": [ + { + "description": "desc of domain", + "enabled": 'true', + "id": "37ef61", + "links": { + "self": "http://identity:35357/v3/domains/37ef61" + }, + "name": "my domain" + } + ], + "links": { + "self": "http://identity:35357/v3/OS-FEDERATION/domains", + "previous": 'null', + "next": 'null' + } +} diff --git a/keystoneclient/tests/v3/test_auth_saml2.py b/keystoneclient/tests/v3/test_auth_saml2.py index d77fe13f3..4e6d2008e 100644 --- a/keystoneclient/tests/v3/test_auth_saml2.py +++ b/keystoneclient/tests/v3/test_auth_saml2.py @@ -23,6 +23,7 @@ from keystoneclient.openstack.common.fixture import config from keystoneclient.openstack.common import jsonutils from keystoneclient import session from keystoneclient.tests.auth import utils as auth_utils +from keystoneclient.tests.v3 import client_fixtures from keystoneclient.tests.v3 import saml2_fixtures from keystoneclient.tests.v3 import utils @@ -309,3 +310,79 @@ class AuthenticateviaSAML2Tests(auth_utils.TestCase, utils.TestCase): response = self.saml2plugin.get_auth_ref(self.session) self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, response.auth_token) + + +class ScopeFederationTokenTests(AuthenticateviaSAML2Tests): + def setUp(self): + super(ScopeFederationTokenTests, self).setUp() + + self.PROJECT_SCOPED_TOKEN_JSON = client_fixtures.project_scoped_token() + self.PROJECT_SCOPED_TOKEN_JSON['methods'] = ['saml2'] + + # for better readibility + self.TEST_TENANT_ID = self.PROJECT_SCOPED_TOKEN_JSON.project_id + self.TEST_TENANT_NAME = self.PROJECT_SCOPED_TOKEN_JSON.project_name + + self.DOMAIN_SCOPED_TOKEN_JSON = client_fixtures.domain_scoped_token() + self.DOMAIN_SCOPED_TOKEN_JSON['methods'] = ['saml2'] + + #for better readibility + self.TEST_DOMAIN_ID = self.DOMAIN_SCOPED_TOKEN_JSON.domain_id + self.TEST_DOMAIN_NAME = self.DOMAIN_SCOPED_TOKEN_JSON.domain_name + + self.saml2_scope_plugin = saml2.Saml2ScopedToken( + self.TEST_URL, saml2_fixtures.UNSCOPED_TOKEN_HEADER, + project_id=self.TEST_TENANT_ID) + + @httpretty.activate + def test_scope_saml2_token_to_project(self): + self.simple_http('POST', self.TEST_URL + '/auth/tokens', + body=jsonutils.dumps(self.PROJECT_SCOPED_TOKEN_JSON), + content_type='application/json', + headers=client_fixtures.AUTH_RESPONSE_HEADERS) + + token = self.saml2_scope_plugin.get_auth_ref(self.session) + self.assertTrue(token.project_scoped, "Received token is not scoped") + self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token) + self.assertEqual(self.TEST_TENANT_ID, token.project_id) + self.assertEqual(self.TEST_TENANT_NAME, token.project_name) + + @httpretty.activate + def test_scope_saml2_token_to_invalid_project(self): + self.simple_http('POST', self.TEST_URL + '/auth/tokens', status=401) + self.saml2_scope_plugin.project_id = uuid.uuid4().hex + self.saml2_scope_plugin.project_name = None + self.assertRaises(exceptions.Unauthorized, + self.saml2_scope_plugin.get_auth_ref, + self.session) + + @httpretty.activate + def test_scope_saml2_token_to_invalid_domain(self): + self.simple_http('POST', self.TEST_URL + '/auth/tokens', status=401) + self.saml2_scope_plugin.project_id = None + self.saml2_scope_plugin.project_name = None + self.saml2_scope_plugin.domain_id = uuid.uuid4().hex + self.saml2_scope_plugin.domain_name = None + self.assertRaises(exceptions.Unauthorized, + self.saml2_scope_plugin.get_auth_ref, + self.session) + + @httpretty.activate + def test_scope_saml2_token_to_domain(self): + self.simple_http('POST', self.TEST_URL + '/auth/tokens', + body=jsonutils.dumps(self.DOMAIN_SCOPED_TOKEN_JSON), + content_type='application/json', + headers=client_fixtures.AUTH_RESPONSE_HEADERS) + + token = self.saml2_scope_plugin.get_auth_ref(self.session) + self.assertTrue(token.domain_scoped, "Received token is not scoped") + self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token) + self.assertEqual(self.TEST_DOMAIN_ID, token.domain_id) + self.assertEqual(self.TEST_DOMAIN_NAME, token.domain_name) + + def test_dont_set_project_nor_domain(self): + self.saml2_scope_plugin.project_id = None + self.saml2_scope_plugin.domain_id = None + self.assertRaises(exceptions.ValidationError, + saml2.Saml2ScopedToken, + self.TEST_URL, client_fixtures.AUTH_SUBJECT_TOKEN)