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:
parent
481773994f
commit
fa844bc88e
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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."""
|
||||
|
|
Loading…
Reference in New Issue