astroklient: własna nazwa produktu zamiast odziedziczonej po rdzeniu
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m20s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Successful in 9m25s
Testy / Testy astroklient (pull_request) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 12s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astrodemo (push) Successful in 9m25s
Testy / Testy astroklient (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m20s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Successful in 9m25s
Testy / Testy astroklient (pull_request) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 12s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astrodemo (push) Successful in 9m25s
Testy / Testy astroklient (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Astroklient przedstawiał się na stronie głównej jako „astrololo" — w tytule karty i w nagłówku. To najpoważniejszy z możliwych śladów: mówi klientowi wprost, że trzyma okrojoną wersję czegoś większego, i podaje frazę, po której da się wyszukać, czego mu nie dano. Cała reszta izolacji przestaje mieć znaczenie, gdy produkt sam się przedstawia nazwą rodzica. Wzięło się stąd, że nazwa była wpisana na sztywno w szablony, a szablony są WSPÓLNE — dokładnie ta sama przyczyna, co przy zaszytym w base.html warunku na administratora. Nazwa mieszka teraz w module `produkt.py`, który nakładka produktu nadpisuje; szablony o nią pytają, zamiast ją znać. astrodemo NIE było dotknięte: ma własne kopie szablonów i pokazuje „astrodemo". DRUGIE ŹRÓDŁO WYCIEKU, mniej oczywiste: nazwa siedziała też w identyfikatorach JavaScriptu, które widzi każdy, kto otworzy narzędzia deweloperskie — zdarzenie `astrololo:coords` i klucz `astrololo.form.v1` w localStorage. Przemianowane na neutralne `kosmogram:*`; te identyfikatory nie mają powodu nosić nazwy rodziny produktów. SKUTEK UBOCZNY: zmiana klucza w localStorage kasuje raz zapamiętane wersje robocze formularza. Sprawdzone, że NIE wyciekają: nagłówki X-Astrololo-* jadą wyłącznie między warstwami, a dokumentacja OpenAPI (gdzie zostaje tytuł aplikacji FastAPI) oddaje 404 także zalogowanemu. TEST rozdziela to, co widzi człowiek, od identyfikatorów wewnętrznych. Nazwa rdzenia wolno występować w nazwach loggerów, materiale do wyprowadzania kluczy i nagłówkach międzywarstwowych; nie wolno w szablonach, zasobach ani w treści odpowiedzi. Zakaz na wszystko byłby albo obejściem przez zmianę nazw kluczy kryptograficznych, albo martwą regułą wyłączaną przy pierwszej okazji. Sprawdzony w obie strony: po cofnięciu nakładki test pada. Testy: astroklient 15, presentation 368, astrodemo 28, logic 342, data 42, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit was merged in pull request #85.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
"""Nazwa produktu — nakładka astroklienta.
|
||||
|
||||
Nadpisuje wersję z rdzenia. Test słownikowy pilnuje, żeby po nadpisaniu nazwa
|
||||
tamtego produktu nie została nigdzie w szablonach, zasobach ani w odpowiedziach.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
NAZWA = "astroklient"
|
||||
@@ -7,8 +7,12 @@ inaczej sprawdzałyby garść plików, a nie produkt.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -35,3 +39,56 @@ def zloz(cel: pathlib.Path) -> pathlib.Path:
|
||||
@pytest.fixture(scope="session")
|
||||
def drzewo(tmp_path_factory) -> pathlib.Path:
|
||||
return zloz(tmp_path_factory.mktemp("astroklient"))
|
||||
|
||||
|
||||
SONDA = '''
|
||||
import json, os, hashlib, secrets, re
|
||||
sol = secrets.token_bytes(16)
|
||||
dk = hashlib.scrypt(b"tajne", salt=sol, n=2**14, r=8, p=1, dklen=32)
|
||||
os.environ.update(ASTROKLIENT_USERS="klient:scrypt$%s$%s" % (sol.hex(), dk.hex()),
|
||||
SESSION_SECRET="t"*64, COOKIES_INSECURE="1", RATE_LIMIT_PER_MIN="0")
|
||||
from starlette.testclient import TestClient
|
||||
from app.main import app
|
||||
from app import security, session, features
|
||||
from app.clients import logic_client
|
||||
c = TestClient(app, follow_redirects=False)
|
||||
H = {"Cookie": f"{session.COOKIE}={security.issue_session('klient')}"}
|
||||
strona = c.get("/", headers=H).text
|
||||
zebrane = []
|
||||
logic_client.link_crypto.call_json = lambda *a, **kw: (zebrane.append(kw.get("payload")), {})[1]
|
||||
c.post("/interpret", headers=H, data={"date":"1984-04-30","time":"11:20",
|
||||
"tz_offset":"2","lat":"50","lon":"19","tenant":"ktos-inny"})
|
||||
print("WYNIK:" + json.dumps({
|
||||
"ekrany": [f.label for f in features.SCREENS],
|
||||
"uprawnienia": sorted(security._uprawnienia()),
|
||||
"dziala": {a: c.get(a, headers=H).status_code
|
||||
for a in ("/", "/interpret", "/timeline", "/synastry",
|
||||
"/significators", "/pliki")},
|
||||
"nie_ma": {a: c.get(a, headers=H).status_code
|
||||
for a in ("/compile", "/settings", "/accounts", "/files",
|
||||
"/horoscope/stream", "/compile/pdf")},
|
||||
"nawigacja": re.findall(r"<nav>(.*?)</nav>", strona, re.S)[0].count('<a href="'),
|
||||
"pula": (zebrane[0] or {}).get("tenant"),
|
||||
"nazwa_w_naglowku": (re.search(r"<h1>(.*?)</h1>", strona) or [None, "?"])[1],
|
||||
"nazwa_w_tytule": (re.search(r"<title>(.*?) ·", strona) or [None, "?"])[1],
|
||||
"rdzen_w_tresci": "astrololo" in strona.lower(),
|
||||
"dokumentacja": {a: c.get(a, headers=H).status_code
|
||||
for a in ("/docs", "/openapi.json", "/redoc")},
|
||||
"logowanie_dziala": bool(security.verify("klient", "tajne")),
|
||||
"zle_haslo_odmowa": security.verify("klient", "inne") is None,
|
||||
}))
|
||||
'''
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def wynik(drzewo):
|
||||
"""Aplikacja uruchomiona w OSOBNYM PROCESIE.
|
||||
|
||||
Importy Pythona są zapamiętywane w procesie, więc sprawdzanie „produktu bez
|
||||
modułu" tam, gdzie moduł już się zaimportował, dałoby wynik fałszywie
|
||||
pozytywny."""
|
||||
r = subprocess.run([sys.executable, "-c", SONDA], cwd=drzewo, capture_output=True,
|
||||
text=True, env={"PYTHONPATH": str(drzewo), "PATH": "/usr/bin:/bin"})
|
||||
linia = next((l for l in r.stdout.splitlines() if l.startswith("WYNIK:")), None)
|
||||
assert linia, f"sonda nie wystartowała:\n{r.stdout[-2000:]}\n{r.stderr[-2000:]}"
|
||||
return json.loads(linia[len("WYNIK:"):])
|
||||
|
||||
@@ -86,3 +86,52 @@ def test_the_removal_list_matches_what_is_actually_composed(drzewo):
|
||||
def test_no_optional_module_survived(drzewo):
|
||||
"""Katalogu modułów opcjonalnych nie ma wcale — most odkrywa wtedy zero."""
|
||||
assert not (drzewo / "app" / "moduly").exists()
|
||||
|
||||
|
||||
# ── nazwa produktu, z którego ten produkt powstał ─────────────────────────
|
||||
# Osobno od listy wyżej, bo granica przebiega gdzie indziej: „astrololo" wolno
|
||||
# występować w identyfikatorach WEWNĘTRZNYCH (nazwy loggerów, materiał do
|
||||
# wyprowadzania kluczy, nagłówki między warstwami), a nie wolno w niczym, co
|
||||
# dociera do przeglądarki. Zakaz na wszystko byłby albo obejściem przez zmianę
|
||||
# nazw kluczy kryptograficznych, albo martwą regułą wyłączaną przy pierwszej
|
||||
# okazji.
|
||||
NAZWA_RDZENIA = "astrololo"
|
||||
WIDOCZNE_DLA_KLIENTA = ("templates", "static")
|
||||
|
||||
|
||||
def test_the_parent_product_is_not_named_in_anything_the_browser_receives(drzewo):
|
||||
"""Szablony i zasoby są WSPÓLNE z pełną aplikacją, więc jej nazwa jechała
|
||||
z nimi do tego obrazu — i astroklient przedstawiał się jako astrololo.
|
||||
|
||||
To jest najpoważniejszy z możliwych śladów: mówi klientowi wprost, że trzyma
|
||||
okrojoną wersję czegoś większego, i podaje frazę, po której da się wyszukać,
|
||||
czego mu nie dano."""
|
||||
grzechy = []
|
||||
for katalog in WIDOCZNE_DLA_KLIENTA:
|
||||
for f in sorted((drzewo / "app" / katalog).rglob("*")):
|
||||
if not f.is_file() or "__pycache__" in str(f):
|
||||
continue
|
||||
try:
|
||||
tresc = f.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if NAZWA_RDZENIA in tresc.lower():
|
||||
grzechy.append(str(f.relative_to(drzewo)))
|
||||
assert not grzechy, (
|
||||
f"\nNazwa „{NAZWA_RDZENIA}” pada w plikach, które dostaje "
|
||||
f"przeglądarka:\n " + "\n ".join(grzechy))
|
||||
|
||||
|
||||
def test_the_rendered_pages_carry_this_products_own_name(wynik):
|
||||
"""Sprawdzane na ODPOWIEDZI, nie na szablonie: liczy się to, co dociera do
|
||||
człowieka, a nie to, co jest w pliku przed złożeniem."""
|
||||
assert wynik["nazwa_w_naglowku"] == "astroklient"
|
||||
assert wynik["nazwa_w_tytule"] == "astroklient"
|
||||
assert not wynik["rdzen_w_tresci"], (
|
||||
"nazwa pełnej aplikacji pada w treści strony oddawanej klientowi")
|
||||
|
||||
|
||||
def test_the_api_documentation_is_not_reachable(wynik):
|
||||
"""Tytuł aplikacji FastAPI wciąż niesie nazwę rdzenia, więc dokumentacja
|
||||
OpenAPI byłaby wyciekiem tylnymi drzwiami — nawet gdy szablony są czyste."""
|
||||
assert all(k == 404 for k in wynik["dokumentacja"].values()), wynik["dokumentacja"]
|
||||
|
||||
@@ -5,61 +5,6 @@ czego w nim nie ma, że to, co zostało, działa, i że nie zostało po niczym s
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
SONDA = '''
|
||||
import json, os, hashlib, secrets, re
|
||||
sol = secrets.token_bytes(16)
|
||||
dk = hashlib.scrypt(b"tajne", salt=sol, n=2**14, r=8, p=1, dklen=32)
|
||||
os.environ.update(ASTROKLIENT_USERS="klient:scrypt$%s$%s" % (sol.hex(), dk.hex()),
|
||||
SESSION_SECRET="t"*64, COOKIES_INSECURE="1", RATE_LIMIT_PER_MIN="0")
|
||||
from starlette.testclient import TestClient
|
||||
from app.main import app
|
||||
from app import security, session, features
|
||||
from app.clients import logic_client
|
||||
c = TestClient(app, follow_redirects=False)
|
||||
H = {"Cookie": f"{session.COOKIE}={security.issue_session('klient')}"}
|
||||
strona = c.get("/", headers=H).text
|
||||
zebrane = []
|
||||
logic_client.link_crypto.call_json = lambda *a, **kw: (zebrane.append(kw.get("payload")), {})[1]
|
||||
c.post("/interpret", headers=H, data={"date":"1984-04-30","time":"11:20",
|
||||
"tz_offset":"2","lat":"50","lon":"19","tenant":"ktos-inny"})
|
||||
print("WYNIK:" + json.dumps({
|
||||
"ekrany": [f.label for f in features.SCREENS],
|
||||
"uprawnienia": sorted(security._uprawnienia()),
|
||||
"dziala": {a: c.get(a, headers=H).status_code
|
||||
for a in ("/", "/interpret", "/timeline", "/synastry",
|
||||
"/significators", "/pliki")},
|
||||
"nie_ma": {a: c.get(a, headers=H).status_code
|
||||
for a in ("/compile", "/settings", "/accounts", "/files",
|
||||
"/horoscope/stream", "/compile/pdf")},
|
||||
"nawigacja": re.findall(r"<nav>(.*?)</nav>", strona, re.S)[0].count('<a href="'),
|
||||
"pula": (zebrane[0] or {}).get("tenant"),
|
||||
"logowanie_dziala": bool(security.verify("klient", "tajne")),
|
||||
"zle_haslo_odmowa": security.verify("klient", "inne") is None,
|
||||
}))
|
||||
'''
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def wynik(drzewo):
|
||||
"""Aplikacja uruchomiona w OSOBNYM PROCESIE.
|
||||
|
||||
Importy Pythona są zapamiętywane w procesie, więc sprawdzanie „produktu bez
|
||||
modułu" tam, gdzie moduł już się zaimportował, dałoby wynik fałszywie
|
||||
pozytywny."""
|
||||
r = subprocess.run([sys.executable, "-c", SONDA], cwd=drzewo, capture_output=True,
|
||||
text=True, env={"PYTHONPATH": str(drzewo), "PATH": "/usr/bin:/bin"})
|
||||
linia = next((l for l in r.stdout.splitlines() if l.startswith("WYNIK:")), None)
|
||||
assert linia, f"sonda nie wystartowała:\n{r.stdout[-2000:]}\n{r.stderr[-2000:]}"
|
||||
return json.loads(linia[len("WYNIK:"):])
|
||||
|
||||
|
||||
def test_the_build_starts_at_all(wynik):
|
||||
assert wynik["dziala"]["/"] == 200
|
||||
|
||||
|
||||
Reference in New Issue
Block a user