From a4da017272a057e21c853568373924d1cf70c96c Mon Sep 17 00:00:00 2001 From: Scott Poore Date: Tue, 16 Aug 2022 16:19:04 -0500 Subject: [PATCH] ipatests: add Keycloak Bridge test Add test code for new bridge server (ipa-tuura) and Keycloak plugin. Add uninstall functions for create_keycloak.py so that the tests can be run repeatedly. Fixes: https://pagure.io/freeipa/issue/9227 Signed-off-by: Scott Poore Reviewed-By: Alexander Bokovoy Reviewed-By: Florence Blanc-Renaud Reviewed-By: Anuja More Reviewed-By: Rob Crittenden --- .../pytest_ipa/integration/create_bridge.py | 171 ++++++++++++++++++ .../pytest_ipa/integration/create_keycloak.py | 19 ++ ipatests/test_integration/test_sso.py | 100 ++++++++++ 3 files changed, 290 insertions(+) create mode 100644 ipatests/pytest_ipa/integration/create_bridge.py create mode 100644 ipatests/test_integration/test_sso.py diff --git a/ipatests/pytest_ipa/integration/create_bridge.py b/ipatests/pytest_ipa/integration/create_bridge.py new file mode 100644 index 000000000..538c59c51 --- /dev/null +++ b/ipatests/pytest_ipa/integration/create_bridge.py @@ -0,0 +1,171 @@ +import re +import textwrap + +from ipatests.pytest_ipa.integration import tasks + + +def setup_scim_server(host, version="main"): + dir = "/opt/ipa-tuura" + password = host.config.admin_password + tasks.install_packages(host, ["unzip", "java-11-openjdk-headless", + "openssl", "maven", "wget", "git", + "firefox", "xorg-x11-server-Xvfb", + "python3-pip"]) + + # Download ipa-tuura project + url = "https://github.com/freeipa/ipa-tuura" + host.run_command(["git", "clone", "-b", f"{version}", f"{url}", f"{dir}"]) + + # Prepare SSSD config + host.run_command(["python", "./prepare_sssd.py"], + cwd=f"{dir}/src/install") + + # Install django requirements + django_reqs = f"{dir}/src/install/requirements.txt" + host.run_command(["pip", "install", "-r", f"{django_reqs}"]) + + # Prepare models and database + host.run_command(["python", "manage.py", "makemigrations", "ipatuura"], + cwd=f"{dir}/src/ipa-tuura") + host.run_command(["python", "manage.py", "migrate"], + cwd=f"{dir}/src/ipa-tuura") + + # Add necessary admin vars to bashrc + env_vars = textwrap.dedent(f""" + export DJANGO_SUPERUSER_PASSWORD={password} + export DJANGO_SUPERUSER_USERNAME=scim + export DJANGO_SUPERUSER_EMAIL=scim@{host.domain.name} + """) + + tasks.backup_file(host, '/etc/bashrc') + content = host.get_file_contents('/etc/bashrc', encoding='utf-8') + new_content = content + f"\n{env_vars}" + host.put_file_contents('/etc/bashrc', new_content) + host.run_command(['bash']) + + # Create django admin + host.run_command(["python", "manage.py", "createsuperuser", + "--scim_username", "scim", "--noinput"], + cwd=f"{dir}/src/ipa-tuura") + + # Open allowed hosts to any for testing + regex = r"^(ALLOWED_HOSTS) .*$" + replace = r"\1 = ['*']" + settings_file = f"{dir}/src/ipa-tuura/root/settings.py" + settings = host.get_file_contents(settings_file, encoding='utf-8') + new_settings = re.sub(regex, replace, settings, flags=re.MULTILINE) + host.put_file_contents(settings_file, new_settings) + + # Setup keycloak service and config files + contents = textwrap.dedent(f""" + DJANGO_SUPERUSER_USERNAME=scim + DJANGO_SUPERUSER_PASSWORD={password} + DJANGO_SUPERUSER_EMAIL=scim@{host.domain.name} + """) + host.put_file_contents("/etc/sysconfig/scim", contents) + + manage = f"{dir}/src/ipa-tuura/manage.py" + contents = textwrap.dedent(f""" + [Unit] + Description=SCIMv2 Bridge Server + After=network.target + + [Service] + Type=idle + WorkingDirectory={dir}/src/ipa-tuura/ + EnvironmentFile=/etc/sysconfig/scim + # Fix this later + # User=scim + # Group=scim + ExecStart=/usr/bin/python {manage} runserver 0.0.0.0:8000 + TimeoutStartSec=600 + TimeoutStopSec=600 + + [Install] + WantedBy=multi-user.target + """) + host.put_file_contents("/etc/systemd/system/scim.service", contents) + host.run_command(["systemctl", "daemon-reload"]) + host.run_command(["systemctl", "start", "scim"]) + + +def setup_keycloak_scim_plugin(host, bridge_server): + dir = "/opt/keycloak" + password = host.config.admin_password + + # Install needed packages + tasks.install_packages(host, ["unzip", "java-11-openjdk-headless", + "openssl", "maven"]) + + # Add necessary admin vars to bashrc + env_vars = textwrap.dedent(f""" + export KEYCLOAK_PATH={dir} + """) + + content = host.get_file_contents('/etc/bashrc', encoding='utf-8') + new_content = content + f"\n{env_vars}" + host.put_file_contents('/etc/bashrc', new_content) + host.run_command(['bash']) + + # Download keycloak plugin + zipfile = "scim-keycloak-user-storage-spi/archive/refs/tags/0.1.zip" + url = f"https://github.com/justin-stephenson/{zipfile}" + dest = "/tmp/keycloak-scim-plugin.zip" + host.run_command(["wget", "-O", dest, url]) + + # Unzip keycloak plugin + host.run_command(["unzip", dest, "-d", "/tmp"]) + + # Install plugin + host.run_command(["./redeploy-plugin.sh"], + cwd="/tmp/scim-keycloak-user-storage-spi-0.1") + + # Fix ownership of plugin files + host.run_command(["chown", "-R", "keycloak:keycloak", dir]) + + # Restore SELinux contexts + host.run_command(["restorecon", "-R", f"{dir}"]) + + # Rerun Keycloak build step and restart to pickup plugin + # This relies on the KC_* vars set in /etc/bashrc from create_keycloak.py + host.run_command(['su', '-', 'keycloak', '-c', + '/opt/keycloak/bin/kc.sh build']) + host.run_command(["systemctl", "restart", "keycloak"]) + host.run_command(["/opt/keycloak/bin/kc.sh", "show-config"]) + + # Login to keycloak as admin + kcadmin_sh = "/opt/keycloak/bin/kcadm.sh" + kcadmin = [kcadmin_sh, "config", "credentials", "--server", + f"https://{host.hostname}:8443/auth/", + "--realm", "master", "--user", "admin", + "--password", password] + tasks.run_repeatedly(host, kcadmin, timeout=60) + + # Configure SCIM User Storage to point to Bridge server + provider_type = "org.keycloak.storage.UserStorageProvider" + host.run_command([kcadmin_sh, "create", "components", + "-r", "master", + "-s", "name=scimprov", + "-s", "providerId=scim", + "-s", f"providerType={provider_type}", + "-s", "parentId=master", + "-s", f'config.scimurl=["{bridge_server}:8000"]', + "-s", 'config.loginusername=["scim"]', + "-s", f'config.loginpassword=["{password}"]']) + + +def uninstall_scim_server(host): + host.run_command(["systemctl", "stop", "scim"], raiseonerr=False) + host.run_command(["rm", "-rf", "/opt/ipa-tuura", + "/etc/sysconfig/scim", + "/etc/systemd/system/scim.service", + "/tmp/scim-keycloak-user-storage-spi-main", + "/tmp/keycloak-scim-plugin.zip"]) + host.run_command(["systemctl", "daemon-reload"]) + tasks.restore_files(host) + + +def uninstall_scim_plugin(host): + host.run_command(["rm", "-rf", + "/tmp/scim-keycloak-user-storage-spi-main", + "/tmp/keycloak-scim-plugin.zip"]) diff --git a/ipatests/pytest_ipa/integration/create_keycloak.py b/ipatests/pytest_ipa/integration/create_keycloak.py index 3e8301e3a..1340b9571 100644 --- a/ipatests/pytest_ipa/integration/create_keycloak.py +++ b/ipatests/pytest_ipa/integration/create_keycloak.py @@ -95,6 +95,7 @@ def setup_keycloakserver(host, version='17.0.0'): """).format(hostname=host.hostname, STORE_PASS=password, ADMIN_PASS=password) + tasks.backup_file(host, '/etc/bashrc') content = host.get_file_contents('/etc/bashrc', encoding='utf-8') new_content = content + "\n{}".format(env_vars) @@ -157,3 +158,21 @@ def setup_keycloak_client(host): "-s", "secret={0}".format(password)] ) time.sleep(60) + + +def uninstall_keycloak(host): + key = os.path.join(paths.OPENSSL_PRIVATE_DIR, "keycloak.key") + crt = os.path.join(paths.OPENSSL_PRIVATE_DIR, "keycloak.crt") + keystore = os.path.join(paths.OPENSSL_PRIVATE_DIR, "keycloak.store") + + host.run_command(["systemctl", "stop", "keycloak"], raiseonerr=False) + host.run_command(["getcert", "stop-tracking", "-k", key, "-f", crt], + raiseonerr=False) + host.run_command(["rm", "-rf", "/opt/keycloak", + "/etc/sysconfig/keycloak", + "/etc/systemd/system/keycloak.service", + key, crt, keystore]) + host.run_command(["systemctl", "daemon-reload"]) + host.run_command(["userdel", "keycloak"]) + host.run_command(["groupdel", "keycloak"], raiseonerr=False) + tasks.restore_files(host) diff --git a/ipatests/test_integration/test_sso.py b/ipatests/test_integration/test_sso.py new file mode 100644 index 000000000..5345f987f --- /dev/null +++ b/ipatests/test_integration/test_sso.py @@ -0,0 +1,100 @@ +from __future__ import absolute_import + +import textwrap +from ipatests.test_integration.base import IntegrationTest +from ipatests.pytest_ipa.integration import tasks, create_keycloak +from ipatests.pytest_ipa.integration import create_bridge + +user_code_script = textwrap.dedent(""" +from selenium import webdriver +from datetime import datetime +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +options = Options() +options.headless = True +driver = webdriver.Firefox(executable_path="/opt/geckodriver", options=options) +verification_uri = "https://{hostname}:8443/auth/realms/master/account/#/" +driver.get(verification_uri) + +try: + element = WebDriverWait(driver, 90).until( + EC.element_to_be_clickable((By.ID, "landingSignInButton"))) + driver.find_element(By.ID, "landingSignInButton").click() + element = WebDriverWait(driver, 90).until( + EC.presence_of_element_located((By.ID, "kc-login"))) + driver.find_element(By.ID, "username").send_keys("{username}") + driver.find_element(By.ID, "password").send_keys("{password}") + driver.find_element(By.ID, "kc-login").click() + element = WebDriverWait(driver, 900).until( + EC.text_to_be_present_in_element((By.ID, "landingLoggedInUser"), + "{username_fl}")) + assert driver.find_element(By.ID, "landingLoggedInUser").text \ + == "{username_fl}" +finally: + now = datetime.now().strftime("%M-%S") + driver.get_screenshot_as_file("/var/log/httpd/screenshot-%s.png" % now) + driver.quit() +""") + + +def keycloak_login(host, username, password, username_fl=None): + if username_fl is None: + username_fl = username + contents = user_code_script.format(hostname=host.hostname, + username=username, + password=password, + username_fl=username_fl) + try: + host.put_file_contents("/tmp/keycloak_login.py", contents) + tasks.run_repeatedly(host, ['python3', '/tmp/keycloak_login.py']) + finally: + host.run_command(["rm", "-f", "/tmp/keycloak_login.py"]) + + +class TestSsoBridge(IntegrationTest): + + # Replicas used instead of clients due to memory requirements + # for running Keycloak and Bridge servers + num_replicas = 2 + + @classmethod + def install(cls, mh): + cls.keycloak = cls.replicas[0] + cls.bridge = cls.replicas[1] + tasks.install_master(cls.master, extra_args=['--no-dnssec-validation']) + tasks.install_client(cls.master, cls.replicas[0], + extra_args=["--mkhomedir"]) + tasks.install_client(cls.master, cls.replicas[1], + extra_args=["--mkhomedir"]) + tasks.clear_sssd_cache(cls.master) + tasks.clear_sssd_cache(cls.keycloak) + tasks.clear_sssd_cache(cls.bridge) + tasks.kinit_admin(cls.master) + username = 'ipauser1' + password = cls.keycloak.config.admin_password + tasks.create_active_user(cls.master, username, password) + create_keycloak.setup_keycloakserver(cls.keycloak) + create_keycloak.setup_keycloak_client(cls.keycloak) + create_bridge.setup_scim_server(cls.bridge) + create_bridge.setup_keycloak_scim_plugin(cls.keycloak, + cls.bridge.hostname) + + @classmethod + def uninstall(cls, mh): + tasks.uninstall_client(cls.keycloak) + tasks.uninstall_client(cls.bridge) + tasks.uninstall_master(cls.master) + create_keycloak.uninstall_keycloak(cls.keycloak) + create_bridge.uninstall_scim_server(cls.bridge) + create_bridge.uninstall_scim_plugin(cls.keycloak) + + def test_sso_login_with_ipa_user(self): + """ + Test case to check authenticating to Keycloak as an IPA user + """ + username = 'ipauser1' + username_fl = 'test user' + password = self.keycloak.config.admin_password + keycloak_login(self.keycloak, username, password, username_fl)