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