From b6e47527cc1320aa83e7749cfc7e22485fd7cafd Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Wed, 18 Jun 2025 22:01:14 +0530 Subject: [PATCH] Add keycloak OIDC based federation tests This patch adds federation tests for horizon and cover the scenario to make sure authentication via an external IDP like keycloak works. Note: The federation based tests are skipped for now as we don't have a mechanism upstream to run these in a job and will be skipped until such job is developed Change-Id: I5afd1c05a74d905b6c732df2659c9e5d155aa633 --- .../test/integration_tests/config.py | 16 +++++ .../test/integration_tests/horizon.conf | 7 ++ openstack_dashboard/test/selenium/conftest.py | 28 ++++++++ .../integration/test_federation_login.py | 64 +++++++++++++++++++ tox.ini | 2 +- 5 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 openstack_dashboard/test/selenium/integration/test_federation_login.py diff --git a/openstack_dashboard/test/integration_tests/config.py b/openstack_dashboard/test/integration_tests/config.py index d2be001a45..962d29b3bd 100644 --- a/openstack_dashboard/test/integration_tests/config.py +++ b/openstack_dashboard/test/integration_tests/config.py @@ -242,6 +242,20 @@ ThemeGroup = [ help='Default xpath for second dropdown of browse panel'), ] +OIDCGroup = [ + cfg.StrOpt('keycloak_test_user1_username', + default='kctestuser1', + help='Username to use for keycloak test user 1'), + cfg.StrOpt('keycloak_test_user1_password', + default='nomoresecrets1', + help='Password to use for keycloak test user 1.', + secret=True), + cfg.StrOpt('keycloak_test_user_home_project', + default='SSOproject', + help='Project to keep all objects belonging to a ' + 'regular keycloak user.'), +] + def _get_config_files(): conf_dir = os.path.join( @@ -270,6 +284,7 @@ def get_config(): cfg.CONF.register_opts(PluginGroup, group="plugin") cfg.CONF.register_opts(VolumeGroup, group="volume") cfg.CONF.register_opts(ThemeGroup, group="theme") + cfg.CONF.register_opts(OIDCGroup, group="OIDC") return cfg.CONF @@ -288,4 +303,5 @@ def list_opts(): ("plugin", PluginGroup), ("volume", VolumeGroup), ("theme", ThemeGroup), + ("OIDC", OIDCGroup), ] diff --git a/openstack_dashboard/test/integration_tests/horizon.conf b/openstack_dashboard/test/integration_tests/horizon.conf index a80cd715a0..4f4a18c1bb 100644 --- a/openstack_dashboard/test/integration_tests/horizon.conf +++ b/openstack_dashboard/test/integration_tests/horizon.conf @@ -133,3 +133,10 @@ browse_left_panel_sec=None,compute,compute,compute,compute,compute,volumes,volum b_l_p_sec_line_xpath=.//*[@id="sidebar-accordion-{main_panel}"] b_l_p_sec_line_req_btn=.//a[@data-target="#sidebar-accordion-{main_panel}-{sec_panel}"] b_l_p_sidebar_xpath=.//*[@id="sidebar-accordion-{main_panel}-{sec_panel}"] + +#Parameters for keycloak test user +[OIDC] +#OIDC parameters for keycloak login +keycloak_test_user1_username=kctestuser1 +keycloak_test_user1_password=nomoresecrets1 +keycloak_test_user_home_project=SSOproject diff --git a/openstack_dashboard/test/selenium/conftest.py b/openstack_dashboard/test/selenium/conftest.py index 43c82dfbaf..d7579cd120 100644 --- a/openstack_dashboard/test/selenium/conftest.py +++ b/openstack_dashboard/test/selenium/conftest.py @@ -18,6 +18,7 @@ from threading import Thread import time import pytest +from selenium.webdriver.support.ui import Select import xvfbwrapper from horizon.test import webdriver @@ -45,6 +46,13 @@ class Session: config.identity.admin_home_project, ), } + self.oidc_credentials = { + 'user': ( + config.OIDC.keycloak_test_user1_username, + config.OIDC.keycloak_test_user1_password, + config.OIDC.keycloak_test_user_home_project, + ), + } self.project_name_xpath = config.theme.project_name_xpath self.logout_url = '/'.join(( config.dashboard.dashboard_url, @@ -78,6 +86,26 @@ class Session: self.current_project = self.driver.find_element_by_xpath( self.project_name_xpath).text + def login_oidc(self, user, project=None): + # Keycloak/OIDC login + username, password, home_project = self.oidc_credentials[user] + if project is None: + project = home_project + self.driver.get(self.logout_url) + select_auth = self.driver.find_element_by_id('id_auth_type') + select_auth.click() + select_opt = Select(select_auth) + select_opt.select_by_visible_text('OpenID Connect') + button = self.driver.find_element_by_css_selector( + '.btn-primary') + button.click() + keycloak_user_field = self.driver.find_element_by_id('username') + keycloak_user_field.send_keys(username) + keycloak_pass_field = self.driver.find_element_by_id('password') + keycloak_pass_field.send_keys(password) + kc_login_button = self.driver.find_element_by_id('kc-login') + kc_login_button.click() + @pytest.fixture(scope='session') def login(driver, config): diff --git a/openstack_dashboard/test/selenium/integration/test_federation_login.py b/openstack_dashboard/test/selenium/integration/test_federation_login.py new file mode 100644 index 0000000000..d4ca41191f --- /dev/null +++ b/openstack_dashboard/test/selenium/integration/test_federation_login.py @@ -0,0 +1,64 @@ +# 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 pytest +from selenium.common.exceptions import NoSuchElementException + +from openstack_dashboard.test.selenium.conftest import Session + + +@pytest.fixture(scope='session') +def login_oidc(driver, config): + session = Session(driver, config) + return session.login_oidc + + +def test_federation_keystone_user_login(login, driver, config): + login('user') + try: + driver.find_element_by_xpath( + config.theme.user_name_xpath) + assert True + except NoSuchElementException: + assert False + + +def test_federation_keystone_admin_login(login, driver, config): + login('admin') + try: + driver.find_element_by_xpath( + config.theme.user_name_xpath) + assert True + except NoSuchElementException: + assert False + + +def test_federation_keycloak_test_user_login(login_oidc, driver, config): + login_oidc('user') + project = config.OIDC.keycloak_test_user_home_project + username = config.OIDC.keycloak_test_user1_username + # Check that expected project exists + try: + project_element = driver.find_element_by_xpath( + config.theme.project_name_xpath) + project_element.find_element_by_xpath( + f'.//*[normalize-space()="{project}"]') + except NoSuchElementException: + assert False, f"Project name '{project}' isn't found" + # Check that expected username exists + try: + username_element = driver.find_element_by_xpath( + config.theme.user_name_xpath) + username_element.find_element_by_xpath( + f'.//*[normalize-space()="{username}"]') + except NoSuchElementException: + assert False, f"Username '{username}' isn't found" diff --git a/tox.ini b/tox.ini index 63ddd377e6..272dbece56 100644 --- a/tox.ini +++ b/tox.ini @@ -98,7 +98,7 @@ setenv = SELENIUM_HEADLESS=1 commands = oslo-config-generator --namespace openstack_dashboard_integration_tests - pytest -v --junitxml="{toxinidir}/test_reports/integration_pytest_results.xml" --html="{toxinidir}/test_reports/integration_pytest_results.html" --self-contained-html {posargs:{toxinidir}/openstack_dashboard/test/selenium/integration} + pytest -v -k "not federation" --junitxml="{toxinidir}/test_reports/integration_pytest_results.xml" --html="{toxinidir}/test_reports/integration_pytest_results.html" --self-contained-html {posargs:{toxinidir}/openstack_dashboard/test/selenium/integration} [testenv:ui-pytest] # Run pytest ui tests only