diff --git a/keystone/contrib/federation/controllers.py b/keystone/contrib/federation/controllers.py index 505103cc99..54127da212 100644 --- a/keystone/contrib/federation/controllers.py +++ b/keystone/contrib/federation/controllers.py @@ -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): diff --git a/keystone/contrib/federation/idp.py b/keystone/contrib/federation/idp.py index 47a29a397e..fca968f77a 100644 --- a/keystone/contrib/federation/idp.py +++ b/keystone/contrib/federation/idp.py @@ -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. @@ -164,6 +166,10 @@ class SAMLGenerator(object): test_user + + Default + admin @@ -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): diff --git a/keystone/tests/unit/saml2/signed_saml2_assertion.xml b/keystone/tests/unit/saml2/signed_saml2_assertion.xml index 965c163bc9..414ff9cf77 100644 --- a/keystone/tests/unit/saml2/signed_saml2_assertion.xml +++ b/keystone/tests/unit/saml2/signed_saml2_assertion.xml @@ -52,6 +52,9 @@ UHeBXxQq/GmfBv3l+V5ObQ+EHKnyDodLHCk= test_user + + user_domain + admin member @@ -60,7 +63,7 @@ UHeBXxQq/GmfBv3l+V5ObQ+EHKnyDodLHCk= development - Default + project_domain diff --git a/keystone/tests/unit/test_v3_federation.py b/keystone/tests/unit/test_v3_federation.py index 981bd2e614..c9ba2754ce 100644 --- a/keystone/tests/unit/test_v3_federation.py +++ b/keystone/tests/unit/test_v3_federation.py @@ -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)