From 0c0bf69ceff55d81054a61123cccabb721b96b09 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 (cherry picked from commit fa844bc88edb417f9513d19c749886a61d7b26ce) --- 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 @@ class Auth(auth_controllers.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 @@ class SAMLGenerator(object): 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 @@ class SAMLGenerator(object): :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 @@ class SAMLGenerator(object): 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 @@ class SAMLGenerator(object): 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 @@ class SAMLGenerator(object): development + + Default + :return: XML object @@ -199,10 +206,18 @@ class SAMLGenerator(object): 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 @@ class SAMLGenerationTests(FederationTests): 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 @@ class SAMLGenerationTests(FederationTests): 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 @@ class SAMLGenerationTests(FederationTests): 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 @@ class SAMLGenerationTests(FederationTests): 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 @@ class SAMLGenerationTests(FederationTests): 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 @@ class SAMLGenerationTests(FederationTests): 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 @@ class SAMLGenerationTests(FederationTests): 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 @@ class SAMLGenerationTests(FederationTests): 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."""