549 lines
21 KiB
Python
549 lines
21 KiB
Python
"""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
|