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
(cherry picked from commit ae2d7075ff
)
This commit is contained in:
parent
ec30329e0e
commit
e9aa267392
|
@ -321,21 +321,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>
|
||||
|
|
|
@ -2986,12 +2986,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"
|
||||
|
@ -3011,7 +3017,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},
|
||||
|
@ -3029,8 +3035,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)
|
||||
|
@ -3042,17 +3050,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):
|
||||
|
@ -3066,8 +3079,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)
|
||||
|
||||
|
@ -3083,8 +3098,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)
|
||||
|
@ -3097,15 +3114,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):
|
||||
|
@ -3121,8 +3141,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
|
||||
|
@ -3145,8 +3167,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)
|
||||
|
@ -3249,13 +3272,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):
|
||||
|
@ -3361,13 +3387,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