Initial monorepo structure

This commit is contained in:
2026-06-30 20:37:40 +02:00
commit 2f2b295531
121 changed files with 39171 additions and 0 deletions
@@ -0,0 +1,548 @@
"""Unit-Tests für ContextGuard und AuditLogger-Integration.
Testet die Zugriffskontrolle zwischen Arbeitskontexten:
- Zugriff innerhalb des eigenen Kontexts (erlaubt)
- Zugriff auf fremden Kontext (verweigert)
- Zugriff auf shared-Bereich (erlaubt gemäß Konfiguration)
- Audit-Log-Format und Vollständigkeit
- Edge Cases: shared-Kontext, Tool-Ausführung im shared-Kontext
Requirements: 2.1, 2.3, 2.5, 2.6, 2.7
"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import pytest
import yaml
from monorepo.audit import AuditLogger
from monorepo.models import SecurityEvent
from monorepo.security import ContextGuard
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def monorepo_root(tmp_path: Path) -> Path:
"""Erstellt eine Monorepo-Struktur mit access-config.yaml."""
root = tmp_path / "monorepo"
root.mkdir()
# Kontextordner erstellen
(root / "privat").mkdir()
(root / "dhive").mkdir()
(root / "bahn").mkdir()
(root / "shared" / "config").mkdir(parents=True)
(root / "shared" / "tools").mkdir(parents=True)
(root / "shared" / "powers").mkdir(parents=True)
(root / "shared" / "knowledge-store").mkdir(parents=True)
(root / "shared" / "mcp-servers").mkdir(parents=True)
# access-config.yaml erstellen
config = {
"contexts": {
"privat": {
"env_file": "privat/.env",
"allowed_shared": [
"shared/tools/",
"shared/powers/",
"shared/config/",
],
},
"dhive": {
"env_file": "dhive/.env",
"allowed_shared": [
"shared/tools/",
"shared/powers/",
"shared/config/",
],
},
"bahn": {
"env_file": "bahn/.env",
"allowed_shared": [
"shared/tools/",
"shared/powers/",
"shared/config/",
"shared/knowledge-store/",
],
},
"shared": {
"env_file": "shared/.env",
"allowed_shared": ["*"],
},
}
}
config_path = root / "shared" / "config" / "access-config.yaml"
with open(config_path, "w", encoding="utf-8") as f:
yaml.dump(config, f)
return root
@pytest.fixture
def guard(monorepo_root: Path) -> ContextGuard:
"""Erstellt einen ContextGuard mit der Test-Konfiguration."""
return ContextGuard(root_path=monorepo_root)
@pytest.fixture
def env_files(monorepo_root: Path) -> Path:
"""Erstellt .env-Dateien für alle Kontexte."""
for ctx in ("privat", "dhive", "bahn", "shared"):
env_path = monorepo_root / ctx / ".env"
env_path.write_text(
f"# {ctx} secrets\n"
f"{ctx.upper()}_API_KEY=secret-{ctx}-123\n"
f"{ctx.upper()}_DB_URL=postgresql://localhost/{ctx}\n",
encoding="utf-8",
)
return monorepo_root
# ---------------------------------------------------------------------------
# Tests: Zugriff innerhalb des eigenen Kontexts (erlaubt)
# ---------------------------------------------------------------------------
class TestOwnContextAccess:
"""Teste Zugriff innerhalb des eigenen Kontexts (erlaubt).
Requirement 2.1, 2.3: Jeder Kontext darf auf eigene Dateien zugreifen.
"""
def test_privat_accesses_own_env(self, guard: ContextGuard, monorepo_root: Path) -> None:
"""privat darf auf privat/.env zugreifen."""
target = monorepo_root / "privat" / ".env"
assert guard.check_access("privat", target) is True
def test_dhive_accesses_own_env(self, guard: ContextGuard, monorepo_root: Path) -> None:
"""dhive darf auf dhive/.env zugreifen."""
target = monorepo_root / "dhive" / ".env"
assert guard.check_access("dhive", target) is True
def test_bahn_accesses_own_env(self, guard: ContextGuard, monorepo_root: Path) -> None:
"""bahn darf auf bahn/.env zugreifen."""
target = monorepo_root / "bahn" / ".env"
assert guard.check_access("bahn", target) is True
def test_privat_accesses_own_project_file(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""privat darf auf beliebige Dateien im eigenen Kontext zugreifen."""
target = monorepo_root / "privat" / "project-a" / "src" / "main.py"
assert guard.check_access("privat", target) is True
def test_dhive_accesses_own_nested_file(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""dhive darf auf tief verschachtelte Dateien im eigenen Kontext zugreifen."""
target = monorepo_root / "dhive" / "my-project" / "module" / "secret.key"
assert guard.check_access("dhive", target) is True
def test_shared_accesses_own_files(self, guard: ContextGuard, monorepo_root: Path) -> None:
"""shared-Kontext darf auf eigene Dateien zugreifen."""
target = monorepo_root / "shared" / ".env"
assert guard.check_access("shared", target) is True
# ---------------------------------------------------------------------------
# Tests: Zugriff auf fremden Kontext (verweigert)
# ---------------------------------------------------------------------------
class TestForeignContextAccessDenied:
"""Teste Zugriff auf fremden Kontext (verweigert).
Requirement 2.1, 2.3: Secrets eines Arbeitskontexts sind für andere
Kontexte nicht lesbar.
"""
def test_privat_cannot_access_dhive_env(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""privat darf NICHT auf dhive/.env zugreifen."""
target = monorepo_root / "dhive" / ".env"
assert guard.check_access("privat", target) is False
def test_privat_cannot_access_bahn_env(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""privat darf NICHT auf bahn/.env zugreifen."""
target = monorepo_root / "bahn" / ".env"
assert guard.check_access("privat", target) is False
def test_dhive_cannot_access_privat_env(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""dhive darf NICHT auf privat/.env zugreifen."""
target = monorepo_root / "privat" / ".env"
assert guard.check_access("dhive", target) is False
def test_dhive_cannot_access_bahn_secrets(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""dhive darf NICHT auf bahn/secret.key zugreifen."""
target = monorepo_root / "bahn" / "secret.key"
assert guard.check_access("dhive", target) is False
def test_bahn_cannot_access_privat_project(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""bahn darf NICHT auf Dateien im privat-Kontext zugreifen."""
target = monorepo_root / "privat" / "project-a" / "config.yaml"
assert guard.check_access("bahn", target) is False
def test_bahn_cannot_access_dhive_env(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""bahn darf NICHT auf dhive/.env zugreifen."""
target = monorepo_root / "dhive" / ".env"
assert guard.check_access("bahn", target) is False
# ---------------------------------------------------------------------------
# Tests: Zugriff auf shared-Bereich (erlaubt gemäß Konfiguration)
# ---------------------------------------------------------------------------
class TestSharedAreaAccess:
"""Teste Zugriff auf shared-Bereich (erlaubt gemäß Konfiguration).
Requirement 2.6, 2.7: Zugriff auf shared-Ressourcen nur auf explizit
freigegebene Pfade.
"""
def test_privat_accesses_shared_tools(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""privat darf auf shared/tools/ zugreifen."""
target = monorepo_root / "shared" / "tools" / "monorepo-cli" / "src" / "main.py"
assert guard.check_access("privat", target) is True
def test_privat_accesses_shared_powers(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""privat darf auf shared/powers/ zugreifen."""
target = monorepo_root / "shared" / "powers" / "db-platform" / "power.md"
assert guard.check_access("privat", target) is True
def test_privat_accesses_shared_config(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""privat darf auf shared/config/ zugreifen."""
target = monorepo_root / "shared" / "config" / "access-config.yaml"
assert guard.check_access("privat", target) is True
def test_privat_cannot_access_shared_knowledge_store(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""privat darf NICHT auf shared/knowledge-store/ zugreifen (nicht in allowed_shared)."""
target = monorepo_root / "shared" / "knowledge-store" / "index.yaml"
assert guard.check_access("privat", target) is False
def test_privat_cannot_access_shared_mcp_servers(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""privat darf NICHT auf shared/mcp-servers/ zugreifen (nicht in allowed_shared)."""
target = monorepo_root / "shared" / "mcp-servers" / "config.json"
assert guard.check_access("privat", target) is False
def test_bahn_accesses_shared_knowledge_store(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""bahn darf auf shared/knowledge-store/ zugreifen (in bahn's allowed_shared)."""
target = monorepo_root / "shared" / "knowledge-store" / "_index.yaml"
assert guard.check_access("bahn", target) is True
def test_dhive_cannot_access_shared_knowledge_store(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""dhive darf NICHT auf shared/knowledge-store/ zugreifen."""
target = monorepo_root / "shared" / "knowledge-store" / "data.md"
assert guard.check_access("dhive", target) is False
def test_dhive_accesses_shared_tools(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""dhive darf auf shared/tools/ zugreifen."""
target = monorepo_root / "shared" / "tools" / "script.sh"
assert guard.check_access("dhive", target) is True
# ---------------------------------------------------------------------------
# Tests: Shared-Kontext darf auf alles zugreifen (Wildcard)
# ---------------------------------------------------------------------------
class TestSharedContextWildcard:
"""Edge Case: shared-Kontext mit allowed_shared: ['*'] darf auf alles in shared zugreifen.
Requirement 2.7: Tools im shared-Bereich brauchen Zugriff auf shared-Ressourcen.
"""
def test_shared_accesses_shared_tools(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""shared darf auf shared/tools/ zugreifen."""
target = monorepo_root / "shared" / "tools" / "any-tool" / "main.py"
assert guard.check_access("shared", target) is True
def test_shared_accesses_shared_knowledge_store(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""shared darf auf shared/knowledge-store/ zugreifen."""
target = monorepo_root / "shared" / "knowledge-store" / "deep" / "doc.md"
assert guard.check_access("shared", target) is True
def test_shared_accesses_shared_mcp_servers(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""shared darf auf shared/mcp-servers/ zugreifen (Wildcard)."""
target = monorepo_root / "shared" / "mcp-servers" / "server.json"
assert guard.check_access("shared", target) is True
def test_shared_cannot_access_privat(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""shared darf NICHT auf privat/ zugreifen (Wildcard gilt nur für shared/)."""
target = monorepo_root / "privat" / ".env"
assert guard.check_access("shared", target) is False
def test_shared_cannot_access_dhive(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""shared darf NICHT auf dhive/ zugreifen."""
target = monorepo_root / "dhive" / "project" / "main.py"
assert guard.check_access("shared", target) is False
def test_shared_cannot_access_bahn(
self, guard: ContextGuard, monorepo_root: Path
) -> None:
"""shared darf NICHT auf bahn/ zugreifen."""
target = monorepo_root / "bahn" / ".env"
assert guard.check_access("shared", target) is False
# ---------------------------------------------------------------------------
# Tests: load_env Funktion
# ---------------------------------------------------------------------------
class TestLoadEnv:
"""Teste load_env-Methode: Lädt nur die eigene .env-Datei.
Requirement 2.2, 2.3: Jeder Kontext hat eine eigene .env-Datei.
"""
def test_load_privat_env(
self, guard: ContextGuard, env_files: Path, monorepo_root: Path
) -> None:
"""load_env lädt korrekt die privat/.env."""
env = guard.load_env("privat")
assert env["PRIVAT_API_KEY"] == "secret-privat-123"
assert env["PRIVAT_DB_URL"] == "postgresql://localhost/privat"
def test_load_dhive_env(
self, guard: ContextGuard, env_files: Path, monorepo_root: Path
) -> None:
"""load_env lädt korrekt die dhive/.env."""
env = guard.load_env("dhive")
assert env["DHIVE_API_KEY"] == "secret-dhive-123"
def test_load_unknown_context_raises(self, guard: ContextGuard) -> None:
"""load_env wirft ValueError bei unbekanntem Kontext."""
with pytest.raises(ValueError, match="Unbekannter Kontext"):
guard.load_env("unknown")
def test_load_env_missing_file_raises(self, guard: ContextGuard) -> None:
"""load_env wirft FileNotFoundError wenn .env-Datei nicht existiert."""
with pytest.raises(FileNotFoundError, match=".env-Datei"):
guard.load_env("privat")
# ---------------------------------------------------------------------------
# Tests: Audit-Log-Format und Vollständigkeit
# ---------------------------------------------------------------------------
class TestAuditLogFormatAndCompleteness:
"""Teste Audit-Log-Format und Vollständigkeit.
Requirement 2.5: Protokolleintrag muss enthalten:
- Zeitstempel
- anfragender Kontext
- Zielkontext
- angefragte Ressource
"""
def test_audit_log_contains_all_required_fields(self, tmp_path: Path) -> None:
"""Ein Audit-Eintrag enthält alle Pflichtfelder: Zeitstempel, Kontexte, Ressource."""
log_path = tmp_path / "audit.log"
logger = AuditLogger(log_path=log_path)
event = SecurityEvent(
timestamp=datetime(2025, 7, 1, 9, 15, 30),
requesting_context="dhive",
target_context="privat",
resource="privat/.env",
action="read",
outcome="denied",
)
logger.log_violation(event)
content = log_path.read_text(encoding="utf-8")
# Zeitstempel vorhanden (ISO-Format)
assert "2025-07-01T09:15:30" in content
# Anfragender Kontext vorhanden
assert "dhive" in content
# Zielkontext vorhanden
assert "privat" in content
# Ressource vorhanden
assert "privat/.env" in content
# Aktion vorhanden
assert "read" in content
# Outcome vorhanden
assert "DENIED" in content
def test_audit_log_format_structure(self, tmp_path: Path) -> None:
"""Audit-Log-Einträge folgen dem definierten Format-Schema."""
log_path = tmp_path / "audit.log"
logger = AuditLogger(log_path=log_path)
event = SecurityEvent(
timestamp=datetime(2025, 3, 20, 12, 0, 0),
requesting_context="bahn",
target_context="dhive",
resource="dhive/.env",
action="write",
outcome="denied",
)
logger.log_violation(event)
line = log_path.read_text(encoding="utf-8").strip()
# Format: [ISO-TIMESTAMP] OUTCOME | requesting -> target | action on resource
assert line == "[2025-03-20T12:00:00] DENIED | bahn -> dhive | write on dhive/.env"
def test_audit_log_allowed_event(self, tmp_path: Path) -> None:
"""Auch erlaubte Zugriffe können protokolliert werden."""
log_path = tmp_path / "audit.log"
logger = AuditLogger(log_path=log_path)
event = SecurityEvent(
timestamp=datetime(2025, 5, 10, 8, 30, 0),
requesting_context="privat",
target_context="shared",
resource="shared/tools/script.sh",
action="execute",
outcome="allowed",
)
logger.log_violation(event)
content = log_path.read_text(encoding="utf-8")
assert "ALLOWED" in content
assert "privat -> shared" in content
assert "execute on shared/tools/script.sh" in content
def test_audit_log_completeness_multiple_events(self, tmp_path: Path) -> None:
"""Mehrere Events werden vollständig und geordnet protokolliert."""
log_path = tmp_path / "audit.log"
logger = AuditLogger(log_path=log_path)
events = [
SecurityEvent(
timestamp=datetime(2025, 1, 1, i, 0, 0),
requesting_context=f"ctx{i}",
target_context="privat",
resource=f"privat/file{i}.txt",
action="read",
outcome="denied",
)
for i in range(3)
]
for event in events:
logger.log_violation(event)
lines = log_path.read_text(encoding="utf-8").splitlines()
assert len(lines) == 3
# Jede Zeile enthält die korrekten Daten
for i, line in enumerate(lines):
assert f"ctx{i}" in line
assert f"file{i}.txt" in line
assert "DENIED" in line
# ---------------------------------------------------------------------------
# Tests: ContextGuard Initialisierung und Konfiguration
# ---------------------------------------------------------------------------
class TestContextGuardInit:
"""Teste ContextGuard-Initialisierung und Fehlerbehandlung.
Requirement 2.6: Zentrale Konfigurationsdatei.
"""
def test_init_with_valid_config(self, monorepo_root: Path) -> None:
"""ContextGuard wird mit gültiger Konfiguration korrekt initialisiert."""
guard = ContextGuard(root_path=monorepo_root)
assert guard.root_path == monorepo_root.resolve()
def test_init_missing_config_raises(self, tmp_path: Path) -> None:
"""ContextGuard wirft FileNotFoundError bei fehlender Konfiguration."""
with pytest.raises(FileNotFoundError, match="Access-Konfiguration nicht gefunden"):
ContextGuard(root_path=tmp_path)
def test_init_invalid_yaml_raises(self, tmp_path: Path) -> None:
"""ContextGuard wirft ValueError bei ungültigem YAML-Format."""
config_dir = tmp_path / "shared" / "config"
config_dir.mkdir(parents=True)
config_path = config_dir / "access-config.yaml"
config_path.write_text("invalid: data\n", encoding="utf-8")
with pytest.raises(ValueError, match="Ungültiges access-config.yaml-Format"):
ContextGuard(root_path=tmp_path)
def test_get_context_config(self, guard: ContextGuard) -> None:
"""get_context_config gibt die korrekte Konfiguration zurück."""
config = guard.get_context_config("bahn")
assert config["env_file"] == "bahn/.env"
assert "shared/knowledge-store/" in config["allowed_shared"]
def test_get_context_config_unknown_raises(self, guard: ContextGuard) -> None:
"""get_context_config wirft ValueError bei unbekanntem Kontext."""
with pytest.raises(ValueError, match="Unbekannter Kontext"):
guard.get_context_config("nonexistent")
# ---------------------------------------------------------------------------
# Tests: Edge Cases relative Pfade
# ---------------------------------------------------------------------------
class TestRelativePaths:
"""Teste check_access mit relativen Pfaden."""
def test_relative_path_own_context(self, guard: ContextGuard) -> None:
"""Relativer Pfad innerhalb eigenen Kontexts wird erkannt."""
target = Path("privat/.env")
assert guard.check_access("privat", target) is True
def test_relative_path_foreign_context(self, guard: ContextGuard) -> None:
"""Relativer Pfad zu fremdem Kontext wird verweigert."""
target = Path("dhive/.env")
assert guard.check_access("privat", target) is False
def test_relative_path_shared(self, guard: ContextGuard) -> None:
"""Relativer Pfad zu erlaubtem shared-Bereich wird akzeptiert."""
target = Path("shared/tools/monorepo-cli/main.py")
assert guard.check_access("privat", target) is True