Add JSON integrity hash checks

This commit is contained in:
Marcel Peterkau
2026-06-27 10:35:35 +02:00
parent d1dab793a6
commit 87e972bb43
9 changed files with 302 additions and 11 deletions
+43 -1
View File
@@ -21,7 +21,7 @@ from ccma.domain.contributions import (
)
from ccma.domain.dates import DateValidationError, normalize_date_input, validate_member_dates
from ccma.domain.models import ASSET_STATUS_LABELS, MEMBERSHIP_STATUS_LABELS, Asset, ContributionData, Event, Member
from ccma.storage.atomic import read_json, write_json_atomic
from ccma.storage.atomic import json_content_hash_matches, read_json, write_json_atomic
class RepositoryError(RuntimeError):
@@ -142,6 +142,8 @@ class MemberRepository:
errors: list[str] = []
try:
config = read_json(self.root / "repository.json")
if not json_content_hash_matches(config):
errors.append("repository.json: Hash fehlt oder stimmt nicht; Datei wurde vermutlich extern geändert.")
if int(config.get("schema_version", 0)) != 1:
errors.append("repository.json: nicht unterstützte schema_version")
policy = config.get("member_number_policy") or {}
@@ -155,6 +157,7 @@ class MemberRepository:
for member_dir in self._member_directories():
try:
member, _contributions = self.preflight_member_record(member_dir.name)
errors.extend(f"{member_dir.name}/{warning}" for warning in self.member_hash_warnings(member_dir.name))
validate_member_dates(
birth_date=member.birth_date,
accepted_at=member.accepted_at,
@@ -183,6 +186,7 @@ class MemberRepository:
for asset_dir in self._asset_directories():
try:
asset = self.get_asset(asset_dir.name)
errors.extend(f"{asset_dir.name}/{warning}" for warning in self.asset_hash_warnings(asset_dir.name))
if asset.asset_id != asset_dir.name:
errors.append(f"{asset_dir.name}/asset.json: asset_id stimmt nicht mit Ordner überein")
if asset.schema_version != 1:
@@ -1211,6 +1215,44 @@ class MemberRepository:
raise RepositoryError("repository.json enthält keine gültige Konfiguration.")
return configuration
def member_hash_warnings(self, member_id: str) -> list[str]:
warnings: list[str] = []
try:
member_raw = read_json(self._member_path(member_id) / "member.json")
if not json_content_hash_matches(member_raw):
warnings.append("member.json: Hash fehlt oder stimmt nicht; Datei wurde vermutlich extern geändert.")
except (OSError, ValueError, TypeError, json.JSONDecodeError):
pass
try:
contributions_raw = read_json(self._member_path(member_id) / "contributions.json")
if not json_content_hash_matches(contributions_raw):
warnings.append(
"contributions.json: Hash fehlt oder stimmt nicht; Datei wurde vermutlich extern geändert."
)
except (OSError, ValueError, TypeError, json.JSONDecodeError):
pass
return warnings
def asset_hash_warnings(self, asset_id: str) -> list[str]:
warnings: list[str] = []
try:
asset_raw = read_json(self._asset_path(asset_id) / "asset.json")
if not json_content_hash_matches(asset_raw):
warnings.append("asset.json: Hash fehlt oder stimmt nicht; Datei wurde vermutlich extern geändert.")
except (OSError, ValueError, TypeError, json.JSONDecodeError):
pass
return warnings
def refresh_member_record_hashes(self, member_id: str) -> None:
member = self.get_member(member_id)
contributions = self.get_contributions(member_id)
write_json_atomic(self._member_path(member_id) / "member.json", member.to_dict())
write_json_atomic(self._member_path(member_id) / "contributions.json", contributions.to_dict())
def refresh_asset_record_hashes(self, asset_id: str) -> None:
asset = self.get_asset(asset_id)
write_json_atomic(self._asset_path(asset_id) / "asset.json", asset.to_dict())
def get_member_number_policy(self) -> dict[str, str]:
try:
config = read_json(self.root / "repository.json")