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:
parent
832de9c680
commit
ae2d7075ff
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue