#!/usr/bin/env python3 # Covers frontend role UI behavior for the multi-tenant/RBAC plan: admin/user # login, admin-only navigation affordances, user inability to access admin # resource management routes, and absence of global-shared controls for users. import os import sys try: from playwright.sync_api import expect, sync_playwright except ImportError: print("SKIP: Playwright is not installed; run after installing frontend test dependencies") sys.exit(77) FRONTEND_URL = os.environ.get("FRONTEND_URL", "http://localhost:18080") ADMIN_USER = os.environ.get("ADMIN_USER", os.environ.get("BOOTSTRAP_ADMIN_USER", "admin")) ADMIN_PASS = os.environ.get("ADMIN_PASS", os.environ.get("BOOTSTRAP_ADMIN_PASS", "")) USER_A = os.environ.get("USER_A", "") USER_A_PASS = os.environ.get("USER_A_PASS", "") def require_env(name: str, value: str) -> None: if not value: print(f"SKIP: {name} is required for role UI contract checks") sys.exit(77) def login(page, username: str, password: str) -> None: page.goto(FRONTEND_URL, wait_until="networkidle") assert "Register" not in page.locator("body").inner_text(timeout=10000), "login page must not expose public registration" if page.locator("input[type='password']").count() == 0: page.evaluate("localStorage.clear()") page.goto(FRONTEND_URL, wait_until="networkidle") text_inputs = page.locator("input:not([type='password'])") expect(text_inputs.first).to_be_visible(timeout=10000) text_inputs.first.fill(username) page.locator("input[type='password']").first.fill(password) page.get_by_role("button").filter(has_text="Login").last.click() page.wait_for_url("**/home", timeout=15000) page.wait_for_load_state("networkidle") expect(page.locator("body")).not_to_contain_text("Login failed") def visible_text(page) -> str: return page.locator("body").inner_text(timeout=10000) def assert_user_restrictions(page) -> None: body = visible_text(page).lower() forbidden_labels = [ "global shared", "global_shared", "make shared", "all workspaces", "admin console", "user management", "workspace management", ] found = [label for label in forbidden_labels if label in body] assert not found, f"user UI exposes admin/global controls: {found}" admin_paths = [ "/admin", "/admin/users", "/admin/workspaces", "/configuration/users", "/configuration/workspaces", ] for path in admin_paths: page.goto(FRONTEND_URL.rstrip("/") + path, wait_until="networkidle") page.wait_for_timeout(500) text = visible_text(page).lower() assert "forbidden" in text or "unauthorized" in text or page.url.rstrip("/") != FRONTEND_URL.rstrip("/") + path, ( f"user can access admin route {path}" ) def assert_admin_affordances(page) -> None: page.goto(FRONTEND_URL.rstrip("/") + "/configuration/clusters", wait_until="networkidle") page.wait_for_load_state("networkidle") text = visible_text(page).lower() expected_any = ["cluster", "registry", "workspace", "user", "admin"] assert any(item in text for item in expected_any), "admin UI did not render management affordances" page.goto(FRONTEND_URL.rstrip("/") + "/configuration/users", wait_until="networkidle") page.wait_for_load_state("networkidle") text = visible_text(page).lower() assert "create user" in text and "accounts" in text, "admin user management UI did not render" if USER_A: row = page.locator("tr").filter(has_text=USER_A).first expect(row).to_be_visible(timeout=10000) delete_button = row.get_by_role("button", name="Delete") expect(delete_button).to_be_visible(timeout=5000) box = delete_button.bounding_box() viewport = page.viewport_size or {"width": 1440, "height": 950} assert box and 0 <= box["x"] <= viewport["width"] - box["width"], "Delete User button is outside the visible viewport" require_env("ADMIN_PASS or BOOTSTRAP_ADMIN_PASS", ADMIN_PASS) require_env("USER_A", USER_A) require_env("USER_A_PASS", USER_A_PASS) with sync_playwright() as p: browser = p.chromium.launch(headless=True) admin_page = browser.new_page(viewport={"width": 1440, "height": 950}) login(admin_page, ADMIN_USER, ADMIN_PASS) assert_admin_affordances(admin_page) user_page = browser.new_page(viewport={"width": 1440, "height": 950}) login(user_page, USER_A, USER_A_PASS) assert_user_restrictions(user_page) browser.close() print("PASS: multi-tenant/RBAC UI contract")