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
This commit is contained in:
Rodrigo Duarte Sousa 2015-04-10 14:59:34 -03:00
parent 481773994f
commit fa844bc88e
4 changed files with 45 additions and 9 deletions

View File

@ -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):

View File

@ -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.
<ns0:AttributeStatement>
@ -171,6 +174,10 @@ class SAMLGenerator(object):
<ns0:AttributeValue
xsi:type="xs:string">development</ns0:AttributeValue>
</ns0:Attribute>
<ns0:Attribute Name="openstack_project_domain">
<ns0:AttributeValue
xsi:type="xs:string">Default</ns0:AttributeValue>
</ns0:Attribute>
</ns0:AttributeStatement>
:return: XML <AttributeStatement> 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):

View File

@ -59,5 +59,8 @@ UHeBXxQq/GmfBv3l+V5ObQ+EHKnyDodLHCk=</ns1:X509Certificate>
<ns0:Attribute Name="openstack_project" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xsi:type="xs:string">development</ns0:AttributeValue>
</ns0:Attribute>
<ns0:Attribute Name="openstack_project_domain" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xsi:type="xs:string">Default</ns0:AttributeValue>
</ns0:Attribute>
</ns0:AttributeStatement>
</ns0:Assertion>

View File

@ -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."""