273 lines
9.9 KiB
Python
273 lines
9.9 KiB
Python
|
|
"""Keycloak initial setup scenario.
|
||
|
|
|
||
|
|
Automates the first-time setup for a fresh Keycloak installation.
|
||
|
|
This scenario:
|
||
|
|
1. Navigates to the Keycloak admin console
|
||
|
|
2. Logs in with the admin credentials (set via env vars)
|
||
|
|
3. Creates a "letsbe" realm
|
||
|
|
4. Configures basic realm settings
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from playwright.async_api import Page
|
||
|
|
|
||
|
|
from app.playwright_scenarios import register_scenario
|
||
|
|
from app.playwright_scenarios.base import BaseScenario, ScenarioOptions, ScenarioResult
|
||
|
|
|
||
|
|
|
||
|
|
@register_scenario
|
||
|
|
class KeycloakInitialSetup(BaseScenario):
|
||
|
|
"""Automate Keycloak initial realm setup.
|
||
|
|
|
||
|
|
This scenario handles the initial configuration after Keycloak is deployed.
|
||
|
|
It logs into the admin console and creates the "letsbe" realm with
|
||
|
|
appropriate settings.
|
||
|
|
|
||
|
|
Keycloak admin credentials are set via environment variables during
|
||
|
|
deployment (KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD), so this scenario
|
||
|
|
only needs to create the realm.
|
||
|
|
|
||
|
|
Required inputs:
|
||
|
|
base_url: The Keycloak instance URL (e.g., https://auth.example.com)
|
||
|
|
admin_user: Admin username (set during deployment)
|
||
|
|
admin_password: Admin password (set during deployment)
|
||
|
|
|
||
|
|
Optional inputs:
|
||
|
|
realm_name: Name of the realm to create (default: "letsbe")
|
||
|
|
|
||
|
|
Result data:
|
||
|
|
login_successful: Whether admin login succeeded
|
||
|
|
realm_created: Whether the realm was created
|
||
|
|
realm_name: Name of the created realm
|
||
|
|
already_configured: True if realm already exists
|
||
|
|
"""
|
||
|
|
|
||
|
|
@property
|
||
|
|
def name(self) -> str:
|
||
|
|
return "keycloak_initial_setup"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def required_inputs(self) -> list[str]:
|
||
|
|
return ["base_url", "admin_user", "admin_password"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def optional_inputs(self) -> list[str]:
|
||
|
|
return ["realm_name"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def description(self) -> str:
|
||
|
|
return "Automate Keycloak admin login and realm creation"
|
||
|
|
|
||
|
|
async def execute(
|
||
|
|
self,
|
||
|
|
page: Page,
|
||
|
|
inputs: dict[str, Any],
|
||
|
|
options: ScenarioOptions,
|
||
|
|
) -> ScenarioResult:
|
||
|
|
"""Execute the Keycloak initial setup.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
page: Playwright Page object
|
||
|
|
inputs: Scenario inputs (base_url, admin_user, admin_password)
|
||
|
|
options: Scenario options
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ScenarioResult with setup status
|
||
|
|
"""
|
||
|
|
base_url = inputs["base_url"].rstrip("/")
|
||
|
|
admin_user = inputs["admin_user"]
|
||
|
|
admin_password = inputs["admin_password"]
|
||
|
|
realm_name = inputs.get("realm_name", "letsbe")
|
||
|
|
|
||
|
|
screenshots = []
|
||
|
|
result_data = {
|
||
|
|
"login_successful": False,
|
||
|
|
"realm_created": False,
|
||
|
|
"realm_name": realm_name,
|
||
|
|
"already_configured": False,
|
||
|
|
}
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Navigate to Keycloak admin console
|
||
|
|
admin_url = f"{base_url}/admin/master/console/"
|
||
|
|
await page.goto(admin_url, wait_until="networkidle")
|
||
|
|
|
||
|
|
# Keycloak redirects to login page
|
||
|
|
# Wait for the login form
|
||
|
|
username_input = page.locator('input#username, input[name="username"]')
|
||
|
|
await username_input.wait_for(state="visible", timeout=15000)
|
||
|
|
|
||
|
|
# Fill login form
|
||
|
|
await username_input.fill(admin_user)
|
||
|
|
|
||
|
|
password_input = page.locator('input#password, input[name="password"]')
|
||
|
|
await password_input.fill(admin_password)
|
||
|
|
|
||
|
|
# Click login button
|
||
|
|
login_button = page.locator(
|
||
|
|
'button#kc-login, '
|
||
|
|
'input#kc-login, '
|
||
|
|
'button[type="submit"], '
|
||
|
|
'input[type="submit"]'
|
||
|
|
)
|
||
|
|
await login_button.click()
|
||
|
|
|
||
|
|
# Wait for admin console to load
|
||
|
|
try:
|
||
|
|
await page.wait_for_url(
|
||
|
|
lambda url: "/admin" in url and "login" not in url.lower(),
|
||
|
|
timeout=30000,
|
||
|
|
)
|
||
|
|
result_data["login_successful"] = True
|
||
|
|
except Exception:
|
||
|
|
# Check for error message
|
||
|
|
error_el = page.locator('.alert-error, .kc-feedback-text, #input-error')
|
||
|
|
if await error_el.count() > 0:
|
||
|
|
error_text = await error_el.first.text_content()
|
||
|
|
return ScenarioResult(
|
||
|
|
success=False,
|
||
|
|
data=result_data,
|
||
|
|
screenshots=screenshots,
|
||
|
|
error=f"Login failed: {error_text}",
|
||
|
|
)
|
||
|
|
return ScenarioResult(
|
||
|
|
success=False,
|
||
|
|
data=result_data,
|
||
|
|
screenshots=screenshots,
|
||
|
|
error="Login failed - could not reach admin console",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check if realm already exists by navigating to realm selector
|
||
|
|
# Look for the realm dropdown or realm list
|
||
|
|
realm_selector = page.locator(
|
||
|
|
'[data-testid="realmSelector"], '
|
||
|
|
'.pf-c-dropdown__toggle, '
|
||
|
|
'#realm-select'
|
||
|
|
)
|
||
|
|
|
||
|
|
if await realm_selector.count() > 0:
|
||
|
|
await realm_selector.first.click()
|
||
|
|
await page.wait_for_timeout(1000)
|
||
|
|
|
||
|
|
# Check if our realm already exists in the dropdown
|
||
|
|
existing_realm = page.locator(
|
||
|
|
f'a:has-text("{realm_name}"), '
|
||
|
|
f'button:has-text("{realm_name}"), '
|
||
|
|
f'[data-testid="realmSelector"] >> text="{realm_name}"'
|
||
|
|
)
|
||
|
|
if await existing_realm.count() > 0:
|
||
|
|
result_data["already_configured"] = True
|
||
|
|
result_data["realm_created"] = True
|
||
|
|
|
||
|
|
# Click away to close dropdown
|
||
|
|
await page.keyboard.press("Escape")
|
||
|
|
|
||
|
|
return ScenarioResult(
|
||
|
|
success=True,
|
||
|
|
data=result_data,
|
||
|
|
screenshots=screenshots,
|
||
|
|
error=None,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Close dropdown
|
||
|
|
await page.keyboard.press("Escape")
|
||
|
|
|
||
|
|
# Create new realm
|
||
|
|
# Navigate to realm creation page
|
||
|
|
create_realm_button = page.locator(
|
||
|
|
'a:has-text("Create Realm"), '
|
||
|
|
'button:has-text("Create Realm"), '
|
||
|
|
'a:has-text("Create realm"), '
|
||
|
|
'button:has-text("Create realm"), '
|
||
|
|
'[data-testid="add-realm"]'
|
||
|
|
)
|
||
|
|
|
||
|
|
if await create_realm_button.count() > 0:
|
||
|
|
await create_realm_button.first.click()
|
||
|
|
else:
|
||
|
|
# Try navigating directly
|
||
|
|
await page.goto(
|
||
|
|
f"{base_url}/admin/master/console/#/create/realm",
|
||
|
|
wait_until="networkidle",
|
||
|
|
)
|
||
|
|
|
||
|
|
await page.wait_for_timeout(2000)
|
||
|
|
|
||
|
|
# Fill in realm name
|
||
|
|
realm_name_input = page.locator(
|
||
|
|
'input#kc-realm, '
|
||
|
|
'input[name="realm"], '
|
||
|
|
'input[data-testid="realmName"], '
|
||
|
|
'input#name'
|
||
|
|
)
|
||
|
|
await realm_name_input.wait_for(state="visible", timeout=10000)
|
||
|
|
await realm_name_input.fill(realm_name)
|
||
|
|
|
||
|
|
# Ensure realm is enabled
|
||
|
|
enabled_toggle = page.locator(
|
||
|
|
'input[name="enabled"], '
|
||
|
|
'[data-testid="realmEnabled"]'
|
||
|
|
)
|
||
|
|
if await enabled_toggle.count() > 0:
|
||
|
|
is_checked = await enabled_toggle.first.is_checked()
|
||
|
|
if not is_checked:
|
||
|
|
await enabled_toggle.first.click()
|
||
|
|
|
||
|
|
# Take screenshot before creating
|
||
|
|
if options.screenshot_on_success and options.artifacts_dir:
|
||
|
|
pre_create_path = options.artifacts_dir / "keycloak_pre_create.png"
|
||
|
|
await page.screenshot(path=str(pre_create_path))
|
||
|
|
screenshots.append(str(pre_create_path))
|
||
|
|
|
||
|
|
# Click Create button
|
||
|
|
create_button = page.locator(
|
||
|
|
'button:has-text("Create"), '
|
||
|
|
'button[type="submit"]'
|
||
|
|
).first
|
||
|
|
await create_button.click()
|
||
|
|
|
||
|
|
# Wait for realm to be created (redirects to realm settings)
|
||
|
|
await page.wait_for_timeout(3000)
|
||
|
|
|
||
|
|
# Verify realm was created by checking URL or page content
|
||
|
|
current_url = page.url
|
||
|
|
if realm_name in current_url or "realm-settings" in current_url:
|
||
|
|
result_data["realm_created"] = True
|
||
|
|
else:
|
||
|
|
# Check for success notification
|
||
|
|
success_el = page.locator(
|
||
|
|
'.pf-c-alert.pf-m-success, '
|
||
|
|
'[class*="success"], '
|
||
|
|
':has-text("Realm created")'
|
||
|
|
)
|
||
|
|
if await success_el.count() > 0:
|
||
|
|
result_data["realm_created"] = True
|
||
|
|
|
||
|
|
# Take final screenshot
|
||
|
|
if options.screenshot_on_success and options.artifacts_dir:
|
||
|
|
final_path = options.artifacts_dir / "keycloak_setup_complete.png"
|
||
|
|
await page.screenshot(path=str(final_path))
|
||
|
|
screenshots.append(str(final_path))
|
||
|
|
|
||
|
|
return ScenarioResult(
|
||
|
|
success=result_data["realm_created"],
|
||
|
|
data=result_data,
|
||
|
|
screenshots=screenshots,
|
||
|
|
error=None if result_data["realm_created"] else "Realm creation may not have completed",
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
# Take error screenshot
|
||
|
|
if options.screenshot_on_failure and options.artifacts_dir:
|
||
|
|
error_path = options.artifacts_dir / "keycloak_setup_error.png"
|
||
|
|
await page.screenshot(path=str(error_path))
|
||
|
|
screenshots.append(str(error_path))
|
||
|
|
|
||
|
|
return ScenarioResult(
|
||
|
|
success=False,
|
||
|
|
data=result_data,
|
||
|
|
screenshots=screenshots,
|
||
|
|
error=f"Keycloak setup failed: {str(e)}",
|
||
|
|
)
|