From fa844bc88edb417f9513d19c749886a61d7b26ce Mon Sep 17 00:00:00 2001 From: Rodrigo Duarte Sousa Date: Fri, 10 Apr 2015 14:59:34 -0300 Subject: [PATCH] Add openstack_project_domain to assertion Currently, a keystone IdP does not provide the domain of the project when generating SAML assertions. Since it is possible to have two projects with the same name but in different domains, this patch adds an additional attribute called "openstack_project_domain" in the assertion to identify the domain of the project. Closes-Bug: 1442343 bp assertion-extra-attributes Change-Id: I62ed73d87f268c73294738845421deb87088326b --- keystone/contrib/federation/controllers.py | 5 +++- keystone/contrib/federation/idp.py | 23 +++++++++++++++---- .../unit/saml2/signed_saml2_assertion.xml | 3 +++ keystone/tests/unit/test_v3_federation.py | 23 +++++++++++++++---- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/keystone/contrib/federation/controllers.py b/keystone/contrib/federation/controllers.py index cdbba4162d..505103cc99 100644 --- a/keystone/contrib/federation/controllers.py +++ b/keystone/contrib/federation/controllers.py @@ -330,9 +330,12 @@ def _create_base_saml_assertion(self, context, auth): raise exception.ForbiddenAction(action=action) project = token_ref.project_name + # NOTE(rodrigods): the domain name is necessary in order to distinguish + # between projects with the same name in different domains. + domain = token_ref.project_domain_name generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(issuer, sp_url, subject, roles, - project) + project, domain) return (response, service_provider) def _build_response_headers(self, service_provider): diff --git a/keystone/contrib/federation/idp.py b/keystone/contrib/federation/idp.py index 292abea3c8..47a29a397e 100644 --- a/keystone/contrib/federation/idp.py +++ b/keystone/contrib/federation/idp.py @@ -44,7 +44,7 @@ def __init__(self): self.assertion_id = uuid.uuid4().hex def samlize_token(self, issuer, recipient, user, roles, project, - expires_in=None): + project_domain_name, expires_in=None): """Convert Keystone attributes to a SAML assertion. :param issuer: URL of the issuing party @@ -57,6 +57,8 @@ def samlize_token(self, issuer, recipient, user, roles, project, :type roles: list :param project: Project name :type project: string + :param project_domain_name: Project Domain name + :type project_domain_name: string :param expires_in: Sets how long the assertion is valid for, in seconds :type expires_in: int @@ -67,8 +69,8 @@ def samlize_token(self, issuer, recipient, user, roles, project, status = self._create_status() saml_issuer = self._create_issuer(issuer) subject = self._create_subject(user, expiration_time, recipient) - attribute_statement = self._create_attribute_statement(user, roles, - project) + attribute_statement = self._create_attribute_statement( + user, roles, project, project_domain_name) authn_statement = self._create_authn_statement(issuer, expiration_time) signature = self._create_signature() @@ -153,7 +155,8 @@ def _create_subject(self, user, expiration_time, recipient): subject.name_id = name_id return subject - def _create_attribute_statement(self, user, roles, project): + def _create_attribute_statement(self, user, roles, project, + project_domain_name): """Create an object that represents a SAML AttributeStatement. @@ -171,6 +174,10 @@ def _create_attribute_statement(self, user, roles, project): development + + Default + :return: XML object @@ -199,10 +206,18 @@ def _create_attribute_statement(self, user, roles, project): project_value.set_text(project) project_attribute.attribute_value = project_value + openstack_project_domain = 'openstack_project_domain' + project_domain_attribute = saml.Attribute() + project_domain_attribute.name = openstack_project_domain + project_domain_value = saml.AttributeValue() + project_domain_value.set_text(project_domain_name) + project_domain_attribute.attribute_value = project_domain_value + attribute_statement = saml.AttributeStatement() attribute_statement.attribute.append(user_attribute) attribute_statement.attribute.append(roles_attribute) attribute_statement.attribute.append(project_attribute) + attribute_statement.attribute.append(project_domain_attribute) return attribute_statement def _create_authn_statement(self, issuer, expiration_time): diff --git a/keystone/tests/unit/saml2/signed_saml2_assertion.xml b/keystone/tests/unit/saml2/signed_saml2_assertion.xml index f570642eef..965c163bc9 100644 --- a/keystone/tests/unit/saml2/signed_saml2_assertion.xml +++ b/keystone/tests/unit/saml2/signed_saml2_assertion.xml @@ -59,5 +59,8 @@ UHeBXxQq/GmfBv3l+V5ObQ+EHKnyDodLHCk= development + + Default + diff --git a/keystone/tests/unit/test_v3_federation.py b/keystone/tests/unit/test_v3_federation.py index c1a4a677a0..589286dfdc 100644 --- a/keystone/tests/unit/test_v3_federation.py +++ b/keystone/tests/unit/test_v3_federation.py @@ -2991,6 +2991,7 @@ class SAMLGenerationTests(FederationTests): SUBJECT = 'test_user' ROLES = ['admin', 'member'] PROJECT = 'development' + DOMAIN = 'Default' SAML_GENERATION_ROUTE = '/auth/OS-FEDERATION/saml2' ECP_GENERATION_ROUTE = '/auth/OS-FEDERATION/saml2/ecp' ASSERTION_VERSION = "2.0" @@ -3029,7 +3030,7 @@ def test_samlize_token_values(self): generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, self.SUBJECT, self.ROLES, - self.PROJECT) + self.PROJECT, self.DOMAIN) assertion = response.assertion self.assertIsNotNone(assertion) @@ -3049,6 +3050,11 @@ def test_samlize_token_values(self): self.assertEqual(self.PROJECT, project_attribute.attribute_value[0].text) + project_domain_attribute = ( + assertion.attribute_statement[0].attribute[3]) + self.assertEqual(self.DOMAIN, + project_domain_attribute.attribute_value[0].text) + def test_verify_assertion_object(self): """Test that the Assertion object is built properly. @@ -3061,7 +3067,7 @@ def test_verify_assertion_object(self): generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, self.SUBJECT, self.ROLES, - self.PROJECT) + self.PROJECT, self.DOMAIN) assertion = response.assertion self.assertEqual(self.ASSERTION_VERSION, assertion.version) @@ -3078,7 +3084,7 @@ def test_valid_saml_xml(self): generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, self.SUBJECT, self.ROLES, - self.PROJECT) + self.PROJECT, self.DOMAIN) saml_str = response.to_string() response = etree.fromstring(saml_str) @@ -3098,6 +3104,9 @@ def test_valid_saml_xml(self): project_attribute = assertion[4][2] self.assertEqual(self.PROJECT, project_attribute[0].text) + project_domain_attribute = assertion[4][3] + self.assertEqual(self.DOMAIN, project_domain_attribute[0].text) + def test_assertion_using_explicit_namespace_prefixes(self): def mocked_subprocess_check_output(*popenargs, **kwargs): # the last option is the assertion file to be signed @@ -3113,7 +3122,7 @@ def mocked_subprocess_check_output(*popenargs, **kwargs): generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, self.SUBJECT, self.ROLES, - self.PROJECT) + self.PROJECT, self.DOMAIN) assertion_xml = response.assertion.to_string() # make sure we have the proper tag and prefix for the assertion # namespace @@ -3246,6 +3255,9 @@ def test_generate_saml_route(self): project_attribute = assertion[4][2] self.assertIsInstance(project_attribute[0].text, str) + project_domain_attribute = assertion[4][3] + self.assertIsInstance(project_domain_attribute[0].text, str) + def test_invalid_scope_body(self): """Test that missing the scope in request body raises an exception. @@ -3355,6 +3367,9 @@ def test_generate_ecp_route(self): project_attribute = assertion[4][2] self.assertIsInstance(project_attribute[0].text, str) + project_domain_attribute = assertion[4][3] + self.assertIsInstance(project_domain_attribute[0].text, str) + class IdPMetadataGenerationTests(FederationTests): """A class for testing Identity Provider Metadata generation."""