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).
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
probe-discourse-signup.py -- Dump Discourse signup validation state.
|
||||
|
||||
Gets an agent to the OIDC-authenticated /signup page, fills a candidate
|
||||
username, clicks Create Account, and dumps every field hint/error plus
|
||||
the page state 8s later. Reveals why "account created" never sticks.
|
||||
|
||||
Usage:
|
||||
docker compose run --rm --entrypoint python3 \
|
||||
-e AGENT=vp-secops provision probe-discourse-signup.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"
|
||||
DISCOURSE_URL = "https://community.turnsys.com"
|
||||
STATE_DIR = Path("/app/state")
|
||||
AGENT = os.environ.get("AGENT", "vp-secops")
|
||||
|
||||
EMAILS = {
|
||||
"vp-secops": "tsgstaff-coo-vpsecops@turnsys.com",
|
||||
"svp-knel": "tsgstaff-coo-svpknel@turnsys.com",
|
||||
}
|
||||
USERNAME = AGENT.replace("-", "")
|
||||
|
||||
|
||||
def field_state(page):
|
||||
return page.evaluate("""() => {
|
||||
const out = [];
|
||||
document.querySelectorAll('input, .tip, .invalid, .good, .bad, .warning, [class*="hint"]').forEach(el => {
|
||||
const id = el.id || '';
|
||||
const val = (el.value || '').substring(0, 40);
|
||||
const text = (el.textContent || '').trim().substring(0, 100);
|
||||
const cls = (el.getAttribute('class') || '').substring(0, 60);
|
||||
const vis = el.offsetParent !== null;
|
||||
if (text || val || id) out.push(`<${el.tagName.toLowerCase()}> id=${id} class=${cls} value="${val}" vis=${vis} text="${text}"`);
|
||||
});
|
||||
return out.join('\\n');
|
||||
}""")
|
||||
|
||||
|
||||
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.get(AGENT, f"{AGENT}@turnsys.com")
|
||||
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
|
||||
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}")
|
||||
|
||||
# Discourse: login modal -> OpenID
|
||||
page.goto(DISCOURSE_URL, wait_until="domcontentloaded", timeout=30000)
|
||||
page.wait_for_timeout(3000)
|
||||
page.locator(".login-button").first.click()
|
||||
page.wait_for_timeout(3000)
|
||||
page.locator('button:has-text("OpenID")').first.click()
|
||||
page.wait_for_timeout(5000)
|
||||
|
||||
# Handle any OIDC interaction
|
||||
for _ in range(4):
|
||||
pw = page.query_selector("#inputPassword")
|
||||
if pw and pw.is_visible():
|
||||
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)
|
||||
continue
|
||||
t = page.query_selector("#inputTotpToken")
|
||||
if t and t.is_visible():
|
||||
t.click(); page.keyboard.type(bw.get_totp(item))
|
||||
page.locator("#totpTokenSubmitButton").click()
|
||||
page.wait_for_timeout(5000)
|
||||
continue
|
||||
if "community.turnsys.com" in page.url:
|
||||
break
|
||||
page.wait_for_timeout(2500)
|
||||
|
||||
print(f"POST-OIDC: {page.url}")
|
||||
if "/signup" not in page.url:
|
||||
print("NOT ON SIGNUP -- dumping body:")
|
||||
print(page.evaluate("() => document.body.innerText.substring(0, 400)"))
|
||||
browser.close()
|
||||
return
|
||||
|
||||
# Fill username
|
||||
page.wait_for_timeout(2000)
|
||||
inp = page.locator("#new-account-username, input[name='username']").first
|
||||
inp.click()
|
||||
page.keyboard.type(USERNAME)
|
||||
page.wait_for_timeout(2000)
|
||||
print(f"\n=== FIELD STATE after typing {USERNAME} ===")
|
||||
print(field_state(page))
|
||||
|
||||
# Click create
|
||||
for btn in ["Create Account", "Sign Up"]:
|
||||
loc = page.locator(f'button:has-text("{btn}")')
|
||||
if loc.count() > 0 and loc.first.is_visible():
|
||||
loc.first.click()
|
||||
print(f"\nClicked: {btn}")
|
||||
break
|
||||
|
||||
page.wait_for_timeout(8000)
|
||||
print(f"\n=== 8s LATER: URL={page.url} ===")
|
||||
print(field_state(page))
|
||||
body = page.evaluate("() => document.body.innerText.substring(0, 400)")
|
||||
print(f"BODY: {body!r}")
|
||||
page.screenshot(path=str(STATE_DIR / f"discourse-signup-{AGENT}-{time.strftime('%H%M%S')}.png"), full_page=True)
|
||||
|
||||
browser.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user