Files
vptechops a4a54f553e feat: full app provisioning for all COO agents + race-hardened BW helper
App credentials now flow for every agent: shared handle_oidc_interaction
helper fills the per-app OIDC login form/TOTP/consent (the panel session
is not shared across app clients -- each may demand fresh credentials),
and the Discourse signup clears the prefilled username field before
typing (prefill+typed concatenation exceeded the 20-char cap and failed
validation silently).

BW helper hardened against the sync races observed across concurrent
containers: get_item_id re-syncs stale caches, create/edit retry with
backoff and post-write sync. This class of failure was mine -- the
login-path fix from session 3 left read paths on stale caches.

Final validated matrix (validate-all-logins.py, fresh-context logins
plus live API checks): 9/10 fully green; vp-compliance blocked on a
corrupt stored password (Cloudron admin reset needed). Redmine access
still Cloudron-denied for vp-secops, svp-knel, vp-techcompliance,
vp-facilities ("You do not have access" at the OIDC interaction).
2026-08-14 13:53:01 -05:00

84 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""
probe-redmine-access.py -- What does the Redmine OIDC page actually say?
Logs in via panel, clicks Redmine SSO, dumps the interaction page text
verbatim. Distinguishes: login form vs consent vs "You do not have access".
Usage:
docker compose run --rm --entrypoint python3 \
-e AGENT=svp-knel provision probe-redmine-access.py
"""
import os, sys, time
from pathlib import Path
from playwright.sync_api import sync_playwright
sys.path.insert(0, os.path.dirname(__file__))
from bw_helper import BitwardenHelper
CLOUDRON_BASE = "https://my.knownelement.com"
REDMINE_URL = "https://projects.knownelement.com"
STATE_DIR = Path("/app/state")
AGENT = os.environ.get("AGENT", "svp-knel")
EMAILS = {
"svp-knel": "tsgstaff-coo-svpknel@turnsys.com",
"vp-secops": "tsgstaff-coo-vpsecops@turnsys.com",
"coo": "tsgstaff-coo@turnsys.com",
}
def main():
bw = BitwardenHelper(
client_id=os.environ["BW_CLIENTID"],
client_secret=os.environ["BW_CLIENTSECRET"],
password=os.environ["BW_PASSWORD"],
server_url=os.environ.get("BW_SERVER", ""),
)
bw.login()
item = f"{AGENT} Cloudron"
email = EMAILS[AGENT]
password = bw.get_item_password(item)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(viewport={"width": 1280, "height": 1024})
page = context.new_page()
# Panel login (networkidle like the working loop code)
page.goto(f"{CLOUDRON_BASE}/login.html", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(2000)
page.wait_for_selector("#inputPassword", timeout=15000)
page.click("#inputUsername")
page.keyboard.type(email)
page.click("#inputPassword")
page.keyboard.type(password)
page.locator('[role="button"]:has-text("Log in")').first.click()
page.wait_for_timeout(4000)
totp = page.query_selector("#inputTotpToken")
if totp and totp.is_visible():
totp.click()
page.keyboard.type(bw.get_totp(item))
page.locator("#totpTokenSubmitButton").click()
page.wait_for_timeout(5000)
print(f"PANEL: {page.url}")
# Redmine SSO
page.goto(f"{REDMINE_URL}/login", wait_until="domcontentloaded", timeout=30000)
page.wait_for_timeout(2000)
page.locator("#login-oauth-submit-1").first.click()
page.wait_for_timeout(6000)
print(f"\nOIDC URL: {page.url}")
body = page.evaluate("() => document.body.innerText")
print("OIDC BODY (verbatim):")
print(body)
page.screenshot(path=str(STATE_DIR / f"redmine-access-{AGENT}-{time.strftime('%H%M%S')}.png"))
browser.close()
if __name__ == "__main__":
main()