Skip to content

Commit

Permalink
Add openstack_project_domain to assertion
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rodrigods committed Apr 29, 2015
1 parent 4817739 commit fa844bc
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 9 deletions.
5 changes: 4 additions & 1 deletion keystone/contrib/federation/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
23 changes: 19 additions & 4 deletions keystone/contrib/federation/idp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -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.
<ns0:AttributeStatement>
Expand All @@ -171,6 +174,10 @@ def _create_attribute_statement(self, user, roles, project):
<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
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions keystone/tests/unit/saml2/signed_saml2_assertion.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
23 changes: 19 additions & 4 deletions keystone/tests/unit/test_v3_federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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."""
Expand Down

0 comments on commit fa844bc

Please sign in to comment.