Federated authentication via ECP functional tests

Adds a first test for the federated authentication feature. It handles
first the authentication using the SAML2 ECP profile.

The tests cleanup have some issues, see related bug.

Related-Bug: 1642692
Change-Id: I3b393a695c6d9f846efdaf302c1beea34e6bd54b
diff --git a/keystone_tempest_plugin/tests/api/identity/v3/test_identity_providers.py b/keystone_tempest_plugin/tests/api/identity/v3/test_identity_providers.py
index 619fc65..97f651f 100644
--- a/keystone_tempest_plugin/tests/api/identity/v3/test_identity_providers.py
+++ b/keystone_tempest_plugin/tests/api/identity/v3/test_identity_providers.py
@@ -16,8 +16,8 @@
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 
-from keystone_tempest_plugin.tests.api.identity import base
 from keystone_tempest_plugin.tests.api.identity.v3 import fixtures
+from keystone_tempest_plugin.tests import base
 
 
 class IndentityProvidersTest(base.BaseIdentityTest):
diff --git a/keystone_tempest_plugin/tests/api/identity/v3/test_mapping_rules.py b/keystone_tempest_plugin/tests/api/identity/v3/test_mapping_rules.py
index e7ac47b..1c0743a 100644
--- a/keystone_tempest_plugin/tests/api/identity/v3/test_mapping_rules.py
+++ b/keystone_tempest_plugin/tests/api/identity/v3/test_mapping_rules.py
@@ -17,8 +17,8 @@
 from tempest.lib import exceptions as lib_exc
 from tempest import test
 
-from keystone_tempest_plugin.tests.api.identity import base
 from keystone_tempest_plugin.tests.api.identity.v3 import fixtures
+from keystone_tempest_plugin.tests import base
 
 
 class MappingRulesTest(base.BaseIdentityTest):
diff --git a/keystone_tempest_plugin/tests/api/identity/v3/test_service_providers.py b/keystone_tempest_plugin/tests/api/identity/v3/test_service_providers.py
index 26363d4..d56b35b 100644
--- a/keystone_tempest_plugin/tests/api/identity/v3/test_service_providers.py
+++ b/keystone_tempest_plugin/tests/api/identity/v3/test_service_providers.py
@@ -17,8 +17,8 @@
 from tempest.lib import exceptions as lib_exc
 from tempest import test
 
-from keystone_tempest_plugin.tests.api.identity import base
 from keystone_tempest_plugin.tests.api.identity.v3 import fixtures
+from keystone_tempest_plugin.tests import base
 
 DEFAULT_RELAY_STATE_PREFIX = 'ss:mem:'
 
diff --git a/keystone_tempest_plugin/tests/api/identity/base.py b/keystone_tempest_plugin/tests/base.py
similarity index 89%
rename from keystone_tempest_plugin/tests/api/identity/base.py
rename to keystone_tempest_plugin/tests/base.py
index 8fa3c79..83f5c12 100644
--- a/keystone_tempest_plugin/tests/api/identity/base.py
+++ b/keystone_tempest_plugin/tests/base.py
@@ -33,6 +33,9 @@
         credentials = common_creds.get_configured_admin_credentials(
             cls.credential_type, identity_version=cls.identity_version)
         cls.keystone_manager = clients.Manager(credentials=credentials)
+        cls.auth_client = cls.keystone_manager.auth_client
         cls.idps_client = cls.keystone_manager.identity_providers_client
         cls.mappings_client = cls.keystone_manager.mapping_rules_client
+        cls.saml2_client = cls.keystone_manager.saml2_client
         cls.sps_client = cls.keystone_manager.service_providers_client
+        cls.tokens_client = cls.keystone_manager.token_v3_client
diff --git a/keystone_tempest_plugin/tests/scenario/test_federated_authentication.py b/keystone_tempest_plugin/tests/scenario/test_federated_authentication.py
new file mode 100644
index 0000000..2ac6958
--- /dev/null
+++ b/keystone_tempest_plugin/tests/scenario/test_federated_authentication.py
@@ -0,0 +1,177 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from six.moves import http_client
+
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from testtools import skipUnless
+
+from keystone_tempest_plugin.tests import base
+
+
+CONF = config.CONF
+
+
+class TestSaml2EcpFederatedAuthentication(base.BaseIdentityTest):
+
+    ECP_SAML2_NAMESPACES = {
+        'ecp': 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp',
+        'S': 'http://schemas.xmlsoap.org/soap/envelope/',
+        'paos': 'urn:liberty:paos:2003-08'
+    }
+
+    ECP_SERVICE_PROVIDER_CONSUMER_URL = ('/S:Envelope/S:Header/paos:Request/'
+                                         '@responseConsumerURL')
+
+    ECP_IDP_CONSUMER_URL = ('/S:Envelope/S:Header/ecp:Response/'
+                            '@AssertionConsumerServiceURL')
+
+    ECP_RELAY_STATE = '//ecp:RelayState'
+
+    def _setup_settings(self):
+        self.idp_id = CONF.fed_scenario.idp_id
+        self.idp_url = CONF.fed_scenario.idp_ecp_url
+        self.keystone_v3_endpoint = CONF.identity.uri_v3
+        self.password = CONF.fed_scenario.idp_password
+        self.protocol_id = CONF.fed_scenario.protocol_id
+        self.username = CONF.fed_scenario.idp_username
+
+    def _setup_idp(self):
+        remote_ids = CONF.fed_scenario.idp_remote_ids
+        self.idps_client.create_identity_provider(
+            self.idp_id, remote_ids=remote_ids, enabled=True)
+        self.addCleanup(
+            self.idps_client.delete_identity_provider, self.idp_id)
+
+    def _setup_mapping(self):
+        self.mapping_id = data_utils.rand_uuid_hex()
+        mapping_remote_type = CONF.fed_scenario.mapping_remote_type
+        mapping_user_name = CONF.fed_scenario.mapping_user_name
+        mapping_group_name = CONF.fed_scenario.mapping_group_name
+        mapping_group_domain_name = CONF.fed_scenario.mapping_group_domain_name
+
+        rules = [{
+            'local': [
+                {
+                    'user': {'name': mapping_user_name}
+                },
+                {
+                    'group': {
+                        'domain': {'name': mapping_group_domain_name},
+                        'name': mapping_group_name
+                    }
+                }
+            ],
+            'remote': [
+                {
+                    'type': mapping_remote_type
+                }
+            ]
+        }]
+        mapping_ref = {'rules': rules}
+        self.mappings_client.create_mapping_rule(self.mapping_id, mapping_ref)
+        self.addCleanup(
+            self.mappings_client.delete_mapping_rule, self.mapping_id)
+
+    def _setup_protocol(self):
+        self.idps_client.add_protocol_and_mapping(
+            self.idp_id, self.protocol_id, self.mapping_id)
+        self.addCleanup(
+            self.idps_client.delete_protocol_and_mapping,
+            self.idp_id,
+            self.protocol_id)
+
+    def setUp(self):
+        super(TestSaml2EcpFederatedAuthentication, self).setUp()
+        self._setup_settings()
+
+        # Reset client's session to avoid getting garbage from another runs
+        self.saml2_client.reset_session()
+
+        # Setup identity provider, mapping and protocol
+        self._setup_idp()
+        self._setup_mapping()
+        self._setup_protocol()
+
+    def _str_from_xml(self, xml, path):
+        l = xml.xpath(path, namespaces=self.ECP_SAML2_NAMESPACES)
+        self.assertEqual(1, len(l))
+        return l[0]
+
+    def _request_unscoped_token(self):
+        resp = self.saml2_client.send_service_provider_request(
+            self.keystone_v3_endpoint, self.idp_id, self.protocol_id)
+        self.assertEqual(http_client.OK, resp.status_code)
+        saml2_authn_request = etree.XML(resp.content)
+
+        relay_state = self._str_from_xml(
+            saml2_authn_request, self.ECP_RELAY_STATE)
+        sp_consumer_url = self._str_from_xml(
+            saml2_authn_request, self.ECP_SERVICE_PROVIDER_CONSUMER_URL)
+
+        # Perform the authn request to the identity provider
+        resp = self.saml2_client.send_identity_provider_authn_request(
+            saml2_authn_request, self.idp_url, self.username, self.password)
+        self.assertEqual(http_client.OK, resp.status_code)
+        saml2_idp_authn_response = etree.XML(resp.content)
+
+        idp_consumer_url = self._str_from_xml(
+            saml2_idp_authn_response, self.ECP_IDP_CONSUMER_URL)
+
+        # Assert that both saml2_authn_request and saml2_idp_authn_response
+        # have the same consumer URL.
+        self.assertEqual(sp_consumer_url, idp_consumer_url)
+
+        # Present the identity provider authn response to the service provider.
+        resp = self.saml2_client.send_service_provider_saml2_authn_response(
+            saml2_idp_authn_response, relay_state, idp_consumer_url)
+        # Must receive a redirect from service provider to the URL where the
+        # unscoped token can be retrieved.
+        self.assertIn(resp.status_code,
+                      [http_client.FOUND, http_client.SEE_OTHER])
+
+        # We can receive multiple types of errors here, the response depends on
+        # the mapping and the username used to authenticate in the Identity
+        # Provider and also in the Identity Provider remote ID validation.
+        # If everything works well, we receive an unscoped token.
+        sp_url = resp.headers['location']
+        resp = (
+            self.saml2_client.send_service_provider_unscoped_token_request(
+                sp_url))
+        self.assertEqual(http_client.CREATED, resp.status_code)
+        self.assertIn('X-Subject-Token', resp.headers)
+        self.assertNotEmpty(resp.json())
+
+        return resp
+
+    @skipUnless(CONF.identity_feature_enabled.federation,
+                "Federated Identity feature not enabled")
+    def test_request_unscoped_token(self):
+        self._request_unscoped_token()
+
+    @skipUnless(CONF.identity_feature_enabled.federation,
+                "Federated Identity feature not enabled")
+    def test_request_scoped_token(self):
+        resp = self._request_unscoped_token()
+        token_id = resp.headers['X-Subject-Token']
+
+        projects = self.auth_client.get_available_projects_scopes(
+            self.keystone_v3_endpoint, token_id)['projects']
+        self.assertNotEmpty(projects)
+
+        # Get a scoped token to one of the listed projects
+        self.tokens_client.auth(
+            project_id=projects[0]['id'], token=token_id)