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 fa844bc88e
)
This commit is contained in:
parent
dafe5f9fdc
commit
0c0bf69cef
|
@ -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