Skip to content

Commit 06572e5

Browse files
committed
Add a Test sign in button that checks both accounts on an external site
A scan signs in silently, and the second account is signed in deep inside the curated rule stage, so an administrator had no way to tell whether an account actually works until a scan quietly produced no authenticated coverage. This adds a Test sign in button on the site page that signs in now with each saved account and reports the result of each, the compare account included. A form login is tried live against the site. A header or cookie is reported as saved, since there is nothing to sign into from here. The live logins run off the event loop so a slow site cannot stall other requests. The second account is also shown on the site's About card now.
1 parent 5087f52 commit 06572e5

3 files changed

Lines changed: 72 additions & 0 deletions

File tree

‎app/core/external_auth.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,39 @@ def session_for(data: dict, allow_private: bool = False) -> dict:
346346
return out
347347

348348

349+
def test_signin(slots: dict, allow_private: bool = False) -> list:
350+
"""Sign in now with each configured account, so an administrator can
351+
confirm it works before a scan leans on it. slots is {"a": data, "b":
352+
data} from load(). -> a list of {slot, label, kind, kind_name,
353+
configured, live, ok, detail}, in order.
354+
355+
A form login is signed in for real against the site. A header or cookie
356+
is reported as saved, since there is nothing here to sign into; it is sent
357+
to the engines as given. The second account's result is here too, which a
358+
scan runs silently and never shows.
359+
"""
360+
labels = {"a": "Account A", "b": "Account B (compare account)"}
361+
out = []
362+
for slot in ("a", "b"):
363+
data = slots.get(slot) or {}
364+
kind = data.get("kind", "")
365+
row = {"slot": slot, "label": labels[slot], "kind": kind,
366+
"kind_name": KINDS.get(kind, kind), "configured": bool(kind),
367+
"live": kind == "form", "ok": False, "detail": ""}
368+
if not kind:
369+
out.append(row)
370+
continue
371+
if kind == "form":
372+
sess = session_for(data, allow_private)
373+
row["ok"], row["detail"] = sess["ok"], sess["detail"]
374+
else:
375+
row["ok"] = True
376+
row["detail"] = ("saved and sent to the engines as given; there is "
377+
"nothing to sign into from here, so it is not tested live")
378+
out.append(row)
379+
return out
380+
381+
349382
def engine_args(tool: str, session: dict) -> list:
350383
"""The command line options that sign an engine in. [] when it cannot."""
351384
if not session or not session.get("kind"):

‎app/web/routes_external.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,36 @@ async def refresh_one(asset_id: int, request: Request, user: User = Depends(curr
870870
"refreshes until it is done."))
871871

872872

873+
@router.post("/external/{asset_id}/test-signin")
874+
async def signin_test(asset_id: int, request: Request, user: User = Depends(current_user)):
875+
"""Sign in now with the saved account(s) so an admin can confirm they work
876+
before a scan relies on them. The second account is tested here too, which
877+
a scan signs in with silently and never surfaces. The live logins run off
878+
the event loop so a slow site cannot stall other requests."""
879+
from starlette.concurrency import run_in_threadpool
880+
881+
await check_csrf(request)
882+
with session_scope() as db:
883+
_guard(db, user)
884+
asset = _asset(db, asset_id)
885+
allow_private = external.options(db)["allow_private"]
886+
slots = {"a": external_auth_mod.load(asset, "a"),
887+
"b": external_auth_mod.load(asset, "b")}
888+
if not slots["a"].get("kind") and not slots["b"].get("kind"):
889+
return redirect(_back(f"/external/{asset_id}",
890+
"No sign in is set for this site, so there is nothing to test.",
891+
"warn"))
892+
results = await run_in_threadpool(external_auth_mod.test_signin, slots, allow_private)
893+
parts, failed = [], False
894+
for r in results:
895+
if not r["configured"]:
896+
continue
897+
parts.append(f"{r['label']} ({r['kind_name'].lower()}): {r['detail']}")
898+
failed = failed or not r["ok"]
899+
return redirect(_back(f"/external/{asset_id}", "Sign in test. " + " ".join(parts),
900+
"warn" if failed else "ok"))
901+
902+
873903
@router.post("/external/{asset_id}/repos")
874904
async def link_repo(asset_id: int, request: Request, user: User = Depends(current_user)):
875905
"""Link a repository by name, or confirm, hide or forget a found one."""

‎app/web/templates/external_site.html‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,17 @@
9999
{%- if auth.username %}, {{ auth.username }} at <span class="mono">{{ auth.login_url }}</span>{% endif %}
100100
<div class="faint small">The secret is saved encrypted and never shown. Set {{ auth.set_at[:10] if auth.set_at else '' }}{% if auth.set_by %} by {{ auth.set_by }}{% endif %}.</div>
101101
{% else %}<span class="faint">none, scans see only the public site</span>{% endif %}</td></tr>
102+
{% if auth_b and auth_b.kind %}<tr><th>Second account</th><td>{{ auth_b.kind_name }}
103+
{%- if auth_b.username %}, {{ auth_b.username }} at <span class="mono">{{ auth_b.login_url }}</span>{% endif %}
104+
<div class="faint small">The compare account that unlocks the two account access checks. Saved encrypted. Set {{ auth_b.set_at[:10] if auth_b.set_at else '' }}{% if auth_b.set_by %} by {{ auth_b.set_by }}{% endif %}.</div></td></tr>{% endif %}
102105
{% if asset.notes %}<tr><th>Comments</th><td class="small" style="white-space:pre-wrap">{{ asset.notes }}</td></tr>{% endif %}
103106
</tbody></table>
107+
{% if (auth and auth.kind) or (auth_b and auth_b.kind) %}
108+
<form method="post" action="{{ base }}/external/{{ asset.id }}/test-signin" style="margin-top:.6rem">
109+
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
110+
<button class="btn sm" type="submit">Test sign in</button>
111+
<span class="faint small">Signs in now and reports whether each account works. A form login is tried live against the site.</span></form>
112+
{% endif %}
104113
<form method="post" action="{{ base }}/external/{{ asset.id }}/delete" data-confirm="Remove {{ asset.host }} and every scan recorded against it?" style="margin-top:.6rem">
105114
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
106115
<button class="btn sm danger" type="submit">Remove this site</button></form>

0 commit comments

Comments
 (0)