Zwei angeforderte Funktionen, dazu zwei Befunde, die ihnen im Weg lagen. == Zweite Stufe der Anmeldung (#666) == Nach korrektem Passwort verlangt das WebUI einen registrierten FIDO2-Schluessel. Die Pflicht haengt je Konto daran, OB ein Schluessel registriert ist - nicht an einem Schalter. Ein Schalter, den man vor der Registrierung umlegt, sperrt den Betreiber aus seiner eigenen Verwaltung aus. Ohne Fremdbibliothek gebaut (das Projekt hat bewusst kein Composer): src/Core/Webauthn/ mit CBOR-Leser, COSE-nach-DER-Wandlung und Pruefung ueber openssl_verify (ES256, RS256). Auth::login() heisst jetzt Auth::startLogin() und liefert drei Zustaende statt eines bool - aus "wahr heisst drin" ist "wahr heisst vielleicht drin" geworden, und ein uebersehener Aufrufer soll an einem fehlenden Methodennamen scheitern statt an einer truthy Zeichenkette. Die Setup-API der Zielsysteme geht ueber verifyCredentials() daran vorbei und bleibt unberuehrt. Notfalltoken als Ersatzweg: nur als Hash in der Datenbank, Klartext einmalig angezeigt, Einmalgebrauch. Laeuft als gewoehnliches Formular ohne JavaScript - ein Ersatzweg mit denselben Voraussetzungen wie der Hauptweg waere keiner. Fehlversuche auf der zweiten Stufe laufen in dieselbe Konto- und IP-Sperre wie das Passwort; zuvor zaehlte nur die erste Stufe. Benutzerverwaltung kann Konten umbenennen. Beim Loeschen eines Kontos und beim Zuruecksetzen der Testinstanz werden seine Schluessel mitgenommen: credential_id ist geraeteweit eindeutig, eine verwaiste Zeile haette denselben Yubikey dauerhaft unbrauchbar gemacht. == Roadmap-Tafel (#664) == Neuer Unterreiter Roadmap -> Tafel. Der Server liefert die fertige Gliederung (RoadmapBoard, ?action=board), die Seite rendert nur - sonst schneidet das WebUI anders als die API und ein spaeterer Export ein drittes Mal anders. Phasen stehen nicht im Code: gepflegt in der neuen Tabelle roadmap_phases, sonst an den groessten Luecken der Rangfolge geschnitten und sichtbar als geraten gekennzeichnet. Links die gekappte Spalte des Erledigten, rechts "Wird ueberlegt". Nachpruefung ist ein eigener Zustand und wird nicht als erledigt gezeichnet. Klick oeffnet ein Seitenpanel: Volltext, Verlauf, Kommentieren mit action_taken, Aendern von Bereich, Rang, Zielfassung, Status und Ausloeser - ohne Seitenwechsel. Das war der eigentliche Gewinn: Wer etwas bemerkte, musste sich bisher die Nummer merken und im Bugtracker danach suchen. Tiefe Verweise #roadmap/<projekt>/item/<id>. Ziehen zwischen Phasen ueber ?action=rank_between. Fremdtext wird ausschliesslich ueber textContent gesetzt, nie ueber innerHTML. == Befund: roadmap_rank war projektweit und bereichslokal zugleich (#177) == Entschieden: projektweit. move() sucht den Nachbarn ohne Bereichsfilter, reorder() schreibt keinen ganzen Bereich mehr auf 10/20/30 um, sondern vertauscht nur die Raenge, die die Eintraege ohnehin halten. rank_between rechnet die Mitte zwischen zwei Nachbarn und nummeriert nur dann einmal neu - unter Erhalt der Reihenfolge -, wenn dort kein Platz mehr ist; die Antwort sagt es dann. == Befund: CSRF an sitzungsauthentifizierten API-Schreibzugriffen == ApiAuth akzeptiert die WebUI-Sitzung, die Endpunkte prueften dabei kein CSRF-Token. Der Kommentar in Http::beginJson - "ausschliesslich per Token-Header, niemals per Cookie" - traf auf die API nie zu. Schreibende Anfragen im Sitzungsweg verlangen jetzt csrf_token bzw. X-CSRF-Token. Token-Clients sind nicht betroffen und wurden gegengeprueft. == Geprueft == tests/e2e/suite_tafel.py neu (16 Faelle), Suite gesamt 172 von 172 gegen die Testinstanz. Die WebAuthn-Zeremonien zusaetzlich gegen einen Software- Authenticator mit echtem P-256 und echtem CBOR: Registrieren, Anmelden, verfaelschte Signatur, veraltete Herausforderung und Zaehler-Rueckfall als Klonverdacht werden je mit 403 abgewiesen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
4.4 KiB
Python
109 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Testlauf gegen die Testinstanz.
|
|
|
|
python tests/e2e/run.py # alles, mit Zuruecksetzen
|
|
python tests/e2e/run.py --gruppe sperre # nur eine Gruppe
|
|
python tests/e2e/run.py --nur roadmap # Namensfilter
|
|
python tests/e2e/run.py --kein-reset # bestehenden Datenbestand behalten
|
|
python tests/e2e/run.py --bericht bericht.json
|
|
python tests/e2e/run.py --log # bei Fehlern das Serverlog zeigen
|
|
|
|
Rueckgabewert: 0 wenn alles bestanden ist, 1 bei Fehlschlaegen, 2 wenn der
|
|
Lauf gar nicht erst starten konnte. scripts/deploy.py wertet genau das aus und
|
|
uebertraegt nichts auf die Produktion, solange hier etwas rot ist.
|
|
|
|
Testfaelle, die einen gemeldeten und noch offenen Befund festhalten, stehen in
|
|
bekannte_befunde.json. Sie laufen mit, erscheinen als BEK - und blockieren das
|
|
Deployment nicht. Besteht so ein Fall wieder, sagt der Lauf das ausdruecklich.
|
|
|
|
Gruppen:
|
|
sperre Zugangssperre und Webroot (.htaccess)
|
|
kern Zustand, OpenAPI, Migration, allgemeines Verhalten
|
|
token Token, Rechte, Provisionierung
|
|
bugtracker Melden, Arbeit holen, uebernehmen, abschliessen
|
|
roadmap Planung
|
|
fehlerstrom Ausnahmen aus Clients
|
|
lizenz Lizenzen und Entwicklungsschluessel
|
|
watchdog Lebenszeichen, Ereignisse, Auswertung
|
|
update Releases veroeffentlichen und finden
|
|
setup Erstinstallation
|
|
supervisor Befunde
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
import harness # noqa: E402
|
|
|
|
# Die Reihenfolge der Importe ist die Reihenfolge der Testfaelle.
|
|
import suite_sperre # noqa: E402,F401
|
|
import suite_kern # noqa: E402,F401
|
|
import suite_bugtracker # noqa: E402,F401
|
|
import suite_dienste # noqa: E402,F401
|
|
import suite_tafel # noqa: E402,F401
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description="Testlauf gegen die Deploymentcenter-Testinstanz")
|
|
p.add_argument('--config', help="Pfad zu deploy_dev_config.json")
|
|
p.add_argument('--gruppe', action='append', help="nur diese Gruppe (mehrfach moeglich)")
|
|
p.add_argument('--nur', help="nur Testfaelle, deren Name diesen Text enthaelt")
|
|
p.add_argument('--kein-reset', dest='reset', action='store_false',
|
|
help="Datenbank nicht zuruecksetzen (dann master_token in der Konfiguration)")
|
|
p.add_argument('--bericht', help="Ergebnisse zusaetzlich als JSON ablegen")
|
|
p.add_argument('--log', action='store_true',
|
|
help="bei Fehlschlaegen das Serverlog der Testinstanz anzeigen")
|
|
p.add_argument('--stopp', action='store_true', help="beim ersten Fehlschlag abbrechen")
|
|
p.add_argument('--liste', action='store_true', help="nur die Testfaelle auflisten")
|
|
p.add_argument('--ipv4', action='store_true',
|
|
help="IPv4 erzwingen (sonst wird IPv6 einmal geprueft und bei "
|
|
"fehlender Route automatisch umgeschaltet)")
|
|
args = p.parse_args()
|
|
|
|
cfg = harness.lade_config(args.config)
|
|
|
|
if args.liste:
|
|
gruppe = None
|
|
for fall in harness.FAELLE:
|
|
if fall.gruppe != gruppe:
|
|
gruppe = fall.gruppe
|
|
print("")
|
|
print("%s:" % gruppe)
|
|
print(" %s" % fall.name)
|
|
print("")
|
|
print("%d Testfaelle" % len(harness.FAELLE))
|
|
return 0
|
|
|
|
code = harness.run(cfg,
|
|
gruppen=args.gruppe,
|
|
muster=args.nur,
|
|
reset=args.reset,
|
|
bericht_datei=args.bericht,
|
|
stoppen=args.stopp,
|
|
adressfamilie='ipv4' if args.ipv4 else None)
|
|
|
|
if code == 1 and args.log:
|
|
# Das Log liegt hinter der Sperre; ohne diesen Weg endet ein
|
|
# fehlgeschlagener Lauf bei "HTTP 500" und dem Raten, was dahinter
|
|
# stand.
|
|
client = harness.Client(cfg['base_url'],
|
|
cfg['basic_auth']['user'], cfg['basic_auth']['pass'])
|
|
r = client.post('/testctl.php', body={'action': 'log', 'lines': 40},
|
|
token=cfg['keys']['shared_key'])
|
|
print("")
|
|
print("--- Serverlog der Testinstanz " + "-" * 40)
|
|
for datei, zeilen in (r.feld('log', default={}) or {}).items():
|
|
print("[%s]" % datei)
|
|
for zeile in zeilen:
|
|
print(" %s" % zeile)
|
|
|
|
return code
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|