fix: treat enabled-but-passwordless portal as inactive (no dead form / self-lockout)
This commit is contained in:
@@ -124,7 +124,10 @@ def portal_open(project_id: str, request: Request, db: Session = Depends(get_db)
|
||||
"portal/access_required.html", {"request": request, "reason": "required"},
|
||||
status_code=404)
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project:
|
||||
if not project or not project.portal_password_hash:
|
||||
# unknown token, disabled portal, or enabled-but-no-password-set — all look
|
||||
# identical to a client (no existence/config leak, no self-lockout on a
|
||||
# passwordless project).
|
||||
return templates.TemplateResponse(
|
||||
"portal/access_required.html", {"request": request, "reason": "invalid"},
|
||||
status_code=404)
|
||||
@@ -155,7 +158,10 @@ def portal_password_prompt(link_token: str, request: Request, db: Session = Depe
|
||||
"""Secure per-project link: resolve the project from the token, prompt for the
|
||||
shared password. Generic page if the token is unknown/disabled (no leak)."""
|
||||
project = resolve_project_by_link_token(link_token, db)
|
||||
if not project:
|
||||
if not project or not project.portal_password_hash:
|
||||
# unknown token, disabled portal, or enabled-but-no-password-set — all look
|
||||
# identical to a client (no existence/config leak, no self-lockout on a
|
||||
# passwordless project).
|
||||
return templates.TemplateResponse(
|
||||
"portal/access_required.html", {"request": request, "reason": "invalid"},
|
||||
status_code=404)
|
||||
@@ -169,7 +175,10 @@ def portal_password_submit(link_token: str, request: Request,
|
||||
password: str = Form(...), db: Session = Depends(get_db)):
|
||||
"""Verify the shared password; on success mint a project-scoped session cookie."""
|
||||
project = resolve_project_by_link_token(link_token, db)
|
||||
if not project:
|
||||
if not project or not project.portal_password_hash:
|
||||
# unknown token, disabled portal, or enabled-but-no-password-set — all look
|
||||
# identical to a client (no existence/config leak, no self-lockout on a
|
||||
# passwordless project).
|
||||
return templates.TemplateResponse(
|
||||
"portal/access_required.html", {"request": request, "reason": "invalid"},
|
||||
status_code=404)
|
||||
@@ -180,7 +189,7 @@ def portal_password_submit(link_token: str, request: Request,
|
||||
"request": request, "link_token": link_token, "project_name": project.name,
|
||||
"error": "Too many attempts. Try again in 15 minutes."}, status_code=200)
|
||||
|
||||
if not project.portal_password_hash or not verify_password(password, project.portal_password_hash):
|
||||
if not verify_password(password, project.portal_password_hash):
|
||||
register_failure(lock_key)
|
||||
return templates.TemplateResponse("portal/password.html", {
|
||||
"request": request, "link_token": link_token, "project_name": project.name,
|
||||
|
||||
@@ -45,3 +45,16 @@ def test_lockout_after_five_wrong(client, db_session):
|
||||
assert r.status_code == 200
|
||||
assert "portal_session=" not in r.headers.get("set-cookie", "")
|
||||
assert "too many" in r.text.lower()
|
||||
|
||||
|
||||
def test_enabled_without_password_is_not_accessible(client, db_session):
|
||||
# enabled portal but no password set yet (operator enabled before generating one)
|
||||
# must NOT show a usable form — looks like an invalid link, no self-lockout.
|
||||
make_project(db_session, portal_enabled=True, portal_link_token="tok-nopw")
|
||||
r = client.get("/portal/p/tok-nopw")
|
||||
assert r.status_code == 404
|
||||
assert "isn't valid" in r.text.lower()
|
||||
# and a POST can't succeed or set a cookie either
|
||||
r2 = client.post("/portal/p/tok-nopw", data={"password": "anything"}, follow_redirects=False)
|
||||
assert r2.status_code == 404
|
||||
assert "portal_session=" not in r2.headers.get("set-cookie", "")
|
||||
|
||||
Reference in New Issue
Block a user