Merge "Add keystone oidc tests"
diff --git a/keystone_tempest_plugin/config.py b/keystone_tempest_plugin/config.py
index 2d4d189..ae93471 100644
--- a/keystone_tempest_plugin/config.py
+++ b/keystone_tempest_plugin/config.py
@@ -47,6 +47,14 @@
                help='Password used to login in the Identity Provider'),
     cfg.StrOpt('idp_ecp_url',
                help='Identity Provider SAML2/ECP URL'),
+    cfg.StrOpt('idp_oidc_url',
+               help='Identity Provider OIDC URL'),
+
+    # client id (oidc)
+    cfg.StrOpt('idp_client_id',
+               help='Identity Provider Client ID'),
+    cfg.StrOpt('idp_client_secret',
+               help='Identity Provider Client Secret'),
 
     # Mapping rules
     cfg.StrOpt('mapping_remote_type',
@@ -72,5 +80,6 @@
     # Protocol
     cfg.StrOpt('protocol_id',
                default='mapped',
-               help='The Protocol ID')
+               help='The Protocol ID'),
+
 ]
diff --git a/keystone_tempest_plugin/tests/scenario/keycloak.py b/keystone_tempest_plugin/tests/scenario/keycloak.py
new file mode 100644
index 0000000..50c3495
--- /dev/null
+++ b/keystone_tempest_plugin/tests/scenario/keycloak.py
@@ -0,0 +1,90 @@
+# 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.
+
+import requests
+
+
+class KeycloakClient(object):
+    def __init__(self, keycloak_url, keycloak_username, keycloak_password,
+                 realm='master', ca_certs_file=False):
+        self.keycloak_url = keycloak_url
+        self.keycloak_username = keycloak_username
+        self.keycloak_password = keycloak_password
+        self.session = requests.session()
+        self.realm = realm
+        self.ca_certs_file = ca_certs_file
+        self._admin_auth()
+
+    @property
+    def url_base(self):
+        return self.keycloak_url + f'/admin/realms'
+
+    @property
+    def token_endpoint(self):
+        return self.keycloak_url + \
+            f'/realms/{self.realm}/protocol/openid-connect/token'
+
+    @property
+    def discovery_endpoint(self):
+        return self.keycloak_url + \
+            f'/realms/{self.realm}/.well-known/openid-configuration'
+
+    def _construct_url(self, path):
+        return self.url_base + f'/{self.realm}/{path}'
+
+    def _admin_auth(self):
+        params = {
+            'grant_type': 'password',
+            'client_id': 'admin-cli',
+            'username': self.keycloak_username,
+            'password': self.keycloak_password,
+            'scope': 'openid',
+        }
+        r = requests.post(
+            self.token_endpoint,
+            data=params,
+            verify=self.ca_certs_file).json()
+
+        headers = {
+            'Authorization': ("Bearer %s" % r['access_token']),
+            'Content-Type': 'application/json'
+        }
+        self.session.headers.update(headers)
+        return r
+
+    def create_user(self, email, first_name, last_name):
+        self._admin_auth()
+        data = {
+            'username': email,
+            'email': email,
+            'firstName': first_name,
+            'lastName': last_name,
+            'enabled': True,
+            'emailVerified': True,
+            'credentials': [{
+                'value': 'secret',
+                'type': 'password',
+            }],
+            'requiredActions': []
+        }
+        return self.session.post(
+            self._construct_url('users'),
+            json=data, verify=self.ca_certs_file)
+
+    def delete_user(self, username):
+        self._admin_auth()
+        data = {
+            'id': username,
+        }
+        return self.session.delete(
+            self._construct_url('users'),
+            json=data, verify=self.ca_certs_file)
diff --git a/keystone_tempest_plugin/tests/scenario/test_federated_authentication.py b/keystone_tempest_plugin/tests/scenario/test_federated_authentication.py
index 09e95d9..68ee19d 100644
--- a/keystone_tempest_plugin/tests/scenario/test_federated_authentication.py
+++ b/keystone_tempest_plugin/tests/scenario/test_federated_authentication.py
@@ -206,6 +206,8 @@
                           "Federated Identity feature not enabled")
     @testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
                           "External identity provider is not available")
+    @testtools.skipUnless(CONF.fed_scenario.protocol_id == 'mapped',
+                          "Protocol not mapped")
     def test_request_unscoped_token(self):
         self._test_request_unscoped_token()
 
@@ -213,6 +215,8 @@
                           "Federated Identity feature not enabled")
     @testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
                           "External identity provider is not available")
+    @testtools.skipUnless(CONF.fed_scenario.protocol_id == 'mapped',
+                          "Protocol not mapped")
     def test_request_scoped_token(self):
         self._test_request_scoped_token()
 
@@ -328,10 +332,14 @@
 
     @testtools.skipUnless(CONF.identity_feature_enabled.federation,
                           "Federated Identity feature not enabled")
+    @testtools.skipUnless(CONF.fed_scenario.protocol_id == 'mapped',
+                          "Protocol not mapped")
     def test_request_unscoped_token(self):
         self._test_request_unscoped_token()
 
     @testtools.skipUnless(CONF.identity_feature_enabled.federation,
                           "Federated Identity feature not enabled")
+    @testtools.skipUnless(CONF.fed_scenario.protocol_id == 'mapped',
+                          "Protocol not mapped")
     def test_request_scoped_token(self):
         self._test_request_scoped_token()
diff --git a/keystone_tempest_plugin/tests/scenario/test_oidc_federated_authentication.py b/keystone_tempest_plugin/tests/scenario/test_oidc_federated_authentication.py
new file mode 100644
index 0000000..d6d064f
--- /dev/null
+++ b/keystone_tempest_plugin/tests/scenario/test_oidc_federated_authentication.py
@@ -0,0 +1,151 @@
+# Copyright 2022 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.
+
+import uuid
+
+from keystoneauth1 import identity
+from keystoneauth1 import session as ks_session
+from tempest import config
+from tempest.lib.common.utils import data_utils
+import testtools
+
+from .keycloak import KeycloakClient
+from keystone_tempest_plugin.tests import base
+
+CONF = config.CONF
+
+
+class TestOidcFederatedAuthentication(base.BaseIdentityTest):
+
+    def _setup_settings(self):
+        # Keycloak Settings
+        self.idp_id = CONF.fed_scenario.idp_id
+        self.idp_remote_ids = CONF.fed_scenario.idp_remote_ids
+        self.idp_url = CONF.fed_scenario.idp_oidc_url
+        self.idp_client_id = CONF.fed_scenario.idp_client_id
+        self.idp_client_secret = CONF.fed_scenario.idp_client_secret
+        self.idp_password = CONF.fed_scenario.idp_password
+        self.idp_username = CONF.fed_scenario.idp_username
+
+        self.protocol_id = CONF.fed_scenario.protocol_id
+        self.keystone_v3_endpoint = CONF.identity.uri_v3
+
+        # mapping settings
+        self.mapping_remote_type = CONF.fed_scenario.mapping_remote_type
+        self.mapping_user_name = CONF.fed_scenario.mapping_user_name
+        self.mapping_group_name = CONF.fed_scenario.mapping_group_name
+        self.mapping_group_domain_name = \
+            CONF.fed_scenario.mapping_group_domain_name
+
+        # custom CA certificate settings
+        self.ca_certificates_file = CONF.identity.ca_certificates_file
+
+    def _setup_mapping(self):
+        self.mapping_id = data_utils.rand_uuid_hex()
+        rules = [{
+            'local': [
+                {
+                    'user': {'name': self.mapping_user_name}
+                },
+                {
+                    'group': {
+                        'domain': {'name': self.mapping_group_domain_name},
+                        'name': self.mapping_group_name
+                    }
+                }
+            ],
+            'remote': [
+                {
+                    'type': self.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(TestOidcFederatedAuthentication, self).setUp()
+        self._setup_settings()
+
+        # Setup mapping and protocol
+        self._setup_mapping()
+        self._setup_protocol()
+        self.keycloak = KeycloakClient(
+            keycloak_url=self.idp_url,
+            keycloak_username=self.idp_username,
+            keycloak_password=self.idp_password,
+            ca_certs_file=self.ca_certificates_file,
+        )
+
+    def _setup_user(self, email=None):
+        email = email if email else f'test-{uuid.uuid4().hex}@example.com'
+        self.keycloak.create_user(email, 'Test', 'User')
+        return email
+
+    def _request_unscoped_token(self, user):
+        auth = identity.v3.OidcPassword(
+            auth_url=self.keystone_v3_endpoint,
+            identity_provider=self.idp_id,
+            protocol=self.protocol_id,
+            client_id=self.idp_client_id,
+            client_secret=self.idp_client_secret,
+            access_token_endpoint=self.keycloak.token_endpoint,
+            discovery_endpoint=self.keycloak.discovery_endpoint,
+            username=user,
+            password='secret'
+        )
+        s = ks_session.Session(auth, verify=self.ca_certificates_file)
+        return s.get_auth_headers()
+
+    @testtools.skipUnless(CONF.identity_feature_enabled.federation,
+                          "Federated Identity feature not enabled")
+    @testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
+                          "External identity provider is not available")
+    @testtools.skipUnless(CONF.fed_scenario.protocol_id == 'openid',
+                          "Protocol not openid")
+    def test_request_unscoped_token(self):
+        user = self._setup_user()
+        token = self._request_unscoped_token(user)
+        self.assertNotEmpty(token)
+        self.keycloak.delete_user(user)
+
+    @testtools.skipUnless(CONF.identity_feature_enabled.federation,
+                          "Federated Identity feature not enabled")
+    @testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
+                          "External identity provider is not available")
+    @testtools.skipUnless(CONF.fed_scenario.protocol_id == 'openid',
+                          "Protocol not openid")
+    def test_request_scoped_token(self):
+        user = self._setup_user()
+        token = self._request_unscoped_token(user)
+        token_id = token['X-Auth-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)
+        self.keycloak.delete_user(user)
diff --git a/requirements.txt b/requirements.txt
index 790e605..67f303d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,6 +5,7 @@
 pbr!=2.1.0,>=2.0.0 # Apache-2.0
 
 # xml parsing
+keystoneauth1>=5.1.1 # Apache-2.0
 lxml!=3.7.0,>=3.4.1 # BSD
 tempest>=17.1.0 # Apache-2.0
 oslo.config>=5.2.0 # Apache-2.0