Refuse OPDS logins when CWA accepts every password

A CWA with anonymous browsing can answer an OPDS request whatever the
password, so each password logged a user in to Bindery. Bindery now
tests CWA with a login that cannot exist, and refuses OPDS logins if
CWA accepts it. A login check no longer shares the session of the
account of Bindery. The CWA account stays optional for a CWA that asks
for no login, and a missing account now gets its own message.
This commit is contained in:
tim
2026-10-02 23:37:33 +02:00
parent 869e56eaf0
commit 9196dd75a8
8 changed files with 96 additions and 8 deletions
+3 -1
View File
@@ -26,6 +26,8 @@ class FakeCwa:
self.requests: list[str] = []
# CWA's "Show Publisher Section" setting. Without it, the publisher feed is 404.
self.publisher_view = True
# CWA's anonymous browsing. With it, the fake answers every request, with or without a login.
self.anonymous = False
fake = self
class Handler(BaseHTTPRequestHandler):
@@ -87,7 +89,7 @@ class FakeCwa:
self._send(h, 200, self._feed([self._book_entry(b) for b in page], next_href), "application/atom+xml")
def handle(self, h) -> None:
if not self._authorized(h):
if not self.anonymous and not self._authorized(h):
self._send(h, 401, b"Unauthorized", "text/plain")
return
url = urlparse(h.path)
+14
View File
@@ -81,6 +81,20 @@ def test_cwa_login(client, cwa):
assert client.get("/api/stories").status_code == 401
def test_cwa_that_accepts_every_login_cannot_check_a_password(client, cwa, caplog):
cwa.anonymous = True
for password in ("pw", "wrong"):
response = login(client, password=password)
assert response.status_code == 401
assert response.json["error"] == "Cannot check the login with CWA. The log has the details."
assert "CWA accepts every OPDS login" in caplog.text
assert client.get("/api/stories").status_code == 401
# These are not wrong passwords, so they lock no one out.
cwa.anonymous = False
orchestrator.cwa()._login_test = None
assert login(client).status_code == 200
def test_a_session_ends_after_one_day(client, cwa, monkeypatch):
response = client.post(
"/api/auth/login", json={"username": "reader", "password": "pw", "remember": True}, headers=H
+1
View File
@@ -53,6 +53,7 @@ def db_login(client, user_db, monkeypatch):
monkeypatch.setattr(config, "CWA_DB_PATH", user_db)
remote = Mock(spec=CwaClient)
remote.login_ok.return_value = True
remote.accepts_any_login.return_value = False
monkeypatch.setattr(orchestrator, "cwa", lambda: remote)
return remote
+32 -1
View File
@@ -1,13 +1,15 @@
"""The OPDS client, the book finder, the login check, the ingest folder and the epub reader."""
import json
import time
from unittest.mock import Mock
import pytest
from fake_cwa import FakeBook, FakeCwa
from test_build import URL, chapters, metahtml
from bindery import fff
from bindery.cwa import BookFinder, CwaClient, CwaError, plain_title, search_text, title_rank
from bindery.cwa import LOGIN_TEST_SECONDS, BookFinder, CwaClient, CwaError, plain_title, search_text, title_rank
from bindery.epub import epub_metadata, epub_urls
from bindery.ingest import MAX_NAME_LENGTH, IngestError, IngestFolder
@@ -63,6 +65,35 @@ def test_wrong_password_and_missing_address(fake):
CwaClient("").search("x")
def test_cwa_that_needs_no_login_works_without_an_account(fake):
fake.anonymous = True
open_client = CwaClient(fake.url)
assert [b.id for b in open_client.search("Another")] == [3]
assert open_client.get_book("uuid-2")["id"] == 2
fake.anonymous = False
with pytest.raises(CwaError, match="CWA asks for a login. Set CWA_USERNAME and CWA_PASSWORD."):
open_client.search("Test Story")
def test_a_login_check_does_not_use_the_session_of_the_account(fake, monkeypatch):
c = client(fake)
monkeypatch.setattr(c.session, "get", Mock(side_effect=AssertionError("The login check used the session")))
assert c.login_ok("bindery", "secret")
assert not c.login_ok("bindery", "wrong")
def test_the_test_of_the_logins_of_cwa_is_kept_for_a_time(fake, monkeypatch):
c = client(fake)
assert not c.accepts_any_login()
fake.anonymous = True
assert not c.accepts_any_login() # the last test still counts
assert len([r for r in fake.requests if r == "/opds"]) == 1
clock = time.monotonic()
monkeypatch.setattr(time, "monotonic", lambda: clock + LOGIN_TEST_SECONDS + 1)
assert c.accepts_any_login()
assert CwaClient(fake.url).accepts_any_login()
def test_get_book(fake):
c = client(fake)
assert c.get_book("uuid-2") == {