Add openstack_user_domain to assertion

Currently, a keystone IdP does not provide the domain of the user
when generating SAML assertions. Since it is possible to have two
users with the same username but in different domains, this patch
adds an additional attribute called "openstack_user_domain"
in the assertion to identify the domain of the user.

Closes-Bug: 1442787
bp assertion-extra-attributes

Change-Id: I65d5c02c0a21f4d4c1b54f8aa56e27950d20badd
This commit is contained in:
Rodrigo Duarte Sousa 2015-04-10 17:27:12 -03:00
parent 832de9c680
commit ae2d7075ff
4 changed files with 87 additions and 38 deletions

View File

@ -337,21 +337,24 @@ class Auth(auth_controllers.Auth):
token_id = auth['identity']['token']['id']
token_data = self.token_provider_api.validate_token(token_id)
token_ref = token_model.KeystoneToken(token_id, token_data)
subject = token_ref.user_name
roles = token_ref.role_names
if not token_ref.project_scoped:
action = _('Use a project scoped token when attempting to create '
'a SAML assertion')
raise exception.ForbiddenAction(action=action)
subject = token_ref.user_name
roles = token_ref.role_names
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
# between projects and users with the same name in different domains.
project_domain_name = token_ref.project_domain_name
subject_domain_name = token_ref.user_domain_name
generator = keystone_idp.SAMLGenerator()
response = generator.samlize_token(issuer, sp_url, subject, roles,
project, domain)
response = generator.samlize_token(
issuer, sp_url, subject, subject_domain_name,
roles, project, project_domain_name)
return (response, service_provider)
def _build_response_headers(self, service_provider):

View File

@ -43,8 +43,8 @@ class SAMLGenerator(object):
def __init__(self):
self.assertion_id = uuid.uuid4().hex
def samlize_token(self, issuer, recipient, user, roles, project,
project_domain_name, expires_in=None):
def samlize_token(self, issuer, recipient, user, user_domain_name, roles,
project, project_domain_name, expires_in=None):
"""Convert Keystone attributes to a SAML assertion.
:param issuer: URL of the issuing party
@ -53,6 +53,8 @@ class SAMLGenerator(object):
:type recipient: string
:param user: User name
:type user: string
:param user_domain_name: User Domain name
:type user_domain_name: string
:param roles: List of role names
:type roles: list
:param project: Project name
@ -70,7 +72,7 @@ class SAMLGenerator(object):
saml_issuer = self._create_issuer(issuer)
subject = self._create_subject(user, expiration_time, recipient)
attribute_statement = self._create_attribute_statement(
user, roles, project, project_domain_name)
user, user_domain_name, roles, project, project_domain_name)
authn_statement = self._create_authn_statement(issuer, expiration_time)
signature = self._create_signature()
@ -155,8 +157,8 @@ class SAMLGenerator(object):
subject.name_id = name_id
return subject
def _create_attribute_statement(self, user, roles, project,
project_domain_name):
def _create_attribute_statement(self, user, user_domain_name, roles,
project, project_domain_name):
"""Create an object that represents a SAML AttributeStatement.
<ns0:AttributeStatement>
@ -164,6 +166,10 @@ class SAMLGenerator(object):
<ns0:AttributeValue
xsi:type="xs:string">test_user</ns0:AttributeValue>
</ns0:Attribute>
<ns0:Attribute Name="openstack_user_domain">
<ns0:AttributeValue
xsi:type="xs:string">Default</ns0:AttributeValue>
</ns0:Attribute>
<ns0:Attribute Name="openstack_roles">
<ns0:AttributeValue
xsi:type="xs:string">admin</ns0:AttributeValue>
@ -190,6 +196,13 @@ class SAMLGenerator(object):
user_value.set_text(user)
user_attribute.attribute_value = user_value
openstack_user_domain = 'openstack_user_domain'
user_domain_attribute = saml.Attribute()
user_domain_attribute.name = openstack_user_domain
user_domain_value = saml.AttributeValue()
user_domain_value.set_text(user_domain_name)
user_domain_attribute.attribute_value = user_domain_value
openstack_roles = 'openstack_roles'
roles_attribute = saml.Attribute()
roles_attribute.name = openstack_roles
@ -218,6 +231,7 @@ class SAMLGenerator(object):
attribute_statement.attribute.append(roles_attribute)
attribute_statement.attribute.append(project_attribute)
attribute_statement.attribute.append(project_domain_attribute)
attribute_statement.attribute.append(user_domain_attribute)
return attribute_statement
def _create_authn_statement(self, issuer, expiration_time):

View File

@ -52,6 +52,9 @@ UHeBXxQq/GmfBv3l+V5ObQ+EHKnyDodLHCk=</ns1:X509Certificate>
<ns0:Attribute Name="openstack_user" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xsi:type="xs:string">test_user</ns0:AttributeValue>
</ns0:Attribute>
<ns0:Attribute Name="openstack_user_domain" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xsi:type="xs:string">user_domain</ns0:AttributeValue>
</ns0:Attribute>
<ns0:Attribute Name="openstack_roles" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<ns0:AttributeValue xsi:type="xs:string">admin</ns0:AttributeValue>
<ns0:AttributeValue xsi:type="xs:string">member</ns0:AttributeValue>
@ -60,7 +63,7 @@ UHeBXxQq/GmfBv3l+V5ObQ+EHKnyDodLHCk=</ns1:X509Certificate>
<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:AttributeValue xsi:type="xs:string">project_domain</ns0:AttributeValue>
</ns0:Attribute>
</ns0:AttributeStatement>
</ns0:Assertion>

View File

@ -3013,12 +3013,18 @@ class SAMLGenerationTests(FederationTests):
SP_AUTH_URL = ('http://beta.com:5000/v3/OS-FEDERATION/identity_providers'
'/BETA/protocols/saml2/auth')
ASSERTION_FILE = 'signed_saml2_assertion.xml'
# The values of the following variables match the attributes values found
# in ASSERTION_FILE
ISSUER = 'https://acme.com/FIM/sps/openstack/saml20'
RECIPIENT = 'http://beta.com/Shibboleth.sso/SAML2/POST'
SUBJECT = 'test_user'
SUBJECT_DOMAIN = 'user_domain'
ROLES = ['admin', 'member']
PROJECT = 'development'
DOMAIN = 'Default'
PROJECT_DOMAIN = 'project_domain'
SAML_GENERATION_ROUTE = '/auth/OS-FEDERATION/saml2'
ECP_GENERATION_ROUTE = '/auth/OS-FEDERATION/saml2/ecp'
ASSERTION_VERSION = "2.0"
@ -3038,7 +3044,7 @@ class SAMLGenerationTests(FederationTests):
def setUp(self):
super(SAMLGenerationTests, self).setUp()
self.signed_assertion = saml2.create_class_from_xml_string(
saml.Assertion, _load_xml('signed_saml2_assertion.xml'))
saml.Assertion, _load_xml(self.ASSERTION_FILE))
self.sp = self.sp_ref()
url = '/OS-FEDERATION/service_providers/' + self.SERVICE_PROVDIER_ID
self.put(url, body={'service_provider': self.sp},
@ -3056,8 +3062,10 @@ class SAMLGenerationTests(FederationTests):
return_value=self.signed_assertion):
generator = keystone_idp.SAMLGenerator()
response = generator.samlize_token(self.ISSUER, self.RECIPIENT,
self.SUBJECT, self.ROLES,
self.PROJECT, self.DOMAIN)
self.SUBJECT,
self.SUBJECT_DOMAIN,
self.ROLES, self.PROJECT,
self.PROJECT_DOMAIN)
assertion = response.assertion
self.assertIsNotNone(assertion)
@ -3069,17 +3077,22 @@ class SAMLGenerationTests(FederationTests):
user_attribute = assertion.attribute_statement[0].attribute[0]
self.assertEqual(self.SUBJECT, user_attribute.attribute_value[0].text)
role_attribute = assertion.attribute_statement[0].attribute[1]
user_domain_attribute = (
assertion.attribute_statement[0].attribute[1])
self.assertEqual(self.SUBJECT_DOMAIN,
user_domain_attribute.attribute_value[0].text)
role_attribute = assertion.attribute_statement[0].attribute[2]
for attribute_value in role_attribute.attribute_value:
self.assertIn(attribute_value.text, self.ROLES)
project_attribute = assertion.attribute_statement[0].attribute[2]
project_attribute = assertion.attribute_statement[0].attribute[3]
self.assertEqual(self.PROJECT,
project_attribute.attribute_value[0].text)
project_domain_attribute = (
assertion.attribute_statement[0].attribute[3])
self.assertEqual(self.DOMAIN,
assertion.attribute_statement[0].attribute[4])
self.assertEqual(self.PROJECT_DOMAIN,
project_domain_attribute.attribute_value[0].text)
def test_verify_assertion_object(self):
@ -3093,8 +3106,10 @@ class SAMLGenerationTests(FederationTests):
side_effect=lambda x: x):
generator = keystone_idp.SAMLGenerator()
response = generator.samlize_token(self.ISSUER, self.RECIPIENT,
self.SUBJECT, self.ROLES,
self.PROJECT, self.DOMAIN)
self.SUBJECT,
self.SUBJECT_DOMAIN,
self.ROLES, self.PROJECT,
self.PROJECT_DOMAIN)
assertion = response.assertion
self.assertEqual(self.ASSERTION_VERSION, assertion.version)
@ -3110,8 +3125,10 @@ class SAMLGenerationTests(FederationTests):
return_value=self.signed_assertion):
generator = keystone_idp.SAMLGenerator()
response = generator.samlize_token(self.ISSUER, self.RECIPIENT,
self.SUBJECT, self.ROLES,
self.PROJECT, self.DOMAIN)
self.SUBJECT,
self.SUBJECT_DOMAIN,
self.ROLES, self.PROJECT,
self.PROJECT_DOMAIN)
saml_str = response.to_string()
response = etree.fromstring(saml_str)
@ -3124,15 +3141,18 @@ class SAMLGenerationTests(FederationTests):
user_attribute = assertion[4][0]
self.assertEqual(self.SUBJECT, user_attribute[0].text)
role_attribute = assertion[4][1]
user_domain_attribute = assertion[4][1]
self.assertEqual(self.SUBJECT_DOMAIN, user_domain_attribute[0].text)
role_attribute = assertion[4][2]
for attribute_value in role_attribute:
self.assertIn(attribute_value.text, self.ROLES)
project_attribute = assertion[4][2]
project_attribute = assertion[4][3]
self.assertEqual(self.PROJECT, project_attribute[0].text)
project_domain_attribute = assertion[4][3]
self.assertEqual(self.DOMAIN, project_domain_attribute[0].text)
project_domain_attribute = assertion[4][4]
self.assertEqual(self.PROJECT_DOMAIN, project_domain_attribute[0].text)
def test_assertion_using_explicit_namespace_prefixes(self):
def mocked_subprocess_check_output(*popenargs, **kwargs):
@ -3148,8 +3168,10 @@ class SAMLGenerationTests(FederationTests):
side_effect=mocked_subprocess_check_output):
generator = keystone_idp.SAMLGenerator()
response = generator.samlize_token(self.ISSUER, self.RECIPIENT,
self.SUBJECT, self.ROLES,
self.PROJECT, self.DOMAIN)
self.SUBJECT,
self.SUBJECT_DOMAIN,
self.ROLES, self.PROJECT,
self.PROJECT_DOMAIN)
assertion_xml = response.assertion.to_string()
# make sure we have the proper tag and prefix for the assertion
# namespace
@ -3172,8 +3194,9 @@ class SAMLGenerationTests(FederationTests):
generator = keystone_idp.SAMLGenerator()
response = generator.samlize_token(self.ISSUER, self.RECIPIENT,
self.SUBJECT, self.ROLES,
self.PROJECT, self.DOMAIN)
self.SUBJECT, self.SUBJECT_DOMAIN,
self.ROLES, self.PROJECT,
self.PROJECT_DOMAIN)
signature = response.assertion.signature
self.assertIsNotNone(signature)
@ -3276,13 +3299,16 @@ class SAMLGenerationTests(FederationTests):
user_attribute = assertion[4][0]
self.assertIsInstance(user_attribute[0].text, str)
role_attribute = assertion[4][1]
user_domain_attribute = assertion[4][1]
self.assertIsInstance(user_domain_attribute[0].text, str)
role_attribute = assertion[4][2]
self.assertIsInstance(role_attribute[0].text, str)
project_attribute = assertion[4][2]
project_attribute = assertion[4][3]
self.assertIsInstance(project_attribute[0].text, str)
project_domain_attribute = assertion[4][3]
project_domain_attribute = assertion[4][4]
self.assertIsInstance(project_domain_attribute[0].text, str)
def test_invalid_scope_body(self):
@ -3388,13 +3414,16 @@ class SAMLGenerationTests(FederationTests):
user_attribute = assertion[4][0]
self.assertIsInstance(user_attribute[0].text, str)
role_attribute = assertion[4][1]
user_domain_attribute = assertion[4][1]
self.assertIsInstance(user_domain_attribute[0].text, str)
role_attribute = assertion[4][2]
self.assertIsInstance(role_attribute[0].text, str)
project_attribute = assertion[4][2]
project_attribute = assertion[4][3]
self.assertIsInstance(project_attribute[0].text, str)
project_domain_attribute = assertion[4][3]
project_domain_attribute = assertion[4][4]
self.assertIsInstance(project_domain_attribute[0].text, str)