feat: enhance access layer governance and visibility checks
Some checks failed
Deploy Development / deploy (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 34s

- Added new documentation references for access layer governance in CLAUDE.md, including multi-tenancy and endpoint audit guidelines.
- Updated ACCESS_LAYER_AND_GOVERNANCE_PLAN.md to include cursor and heuristic checks for access layer compliance.
- Enhanced ACCESS_LAYER_ENDPOINT_AUDIT.md to clarify endpoint visibility and governance requirements, including exemptions for certain routers.
- Introduced library_content_visible_to_profile function in club_tenancy.py to streamline visibility checks for library content.
- Updated exercise progression graphs router to utilize the new visibility function, improving access control.
- Bumped application version to 0.8.27 and updated changelog to reflect these changes.
This commit is contained in:
Lars 2026-05-05 22:09:25 +02:00
parent 5aee9c52fc
commit abee6171df
11 changed files with 192 additions and 17 deletions

View File

@ -96,6 +96,8 @@ Ausgangslage im Code: `private` \| `club` \| `official` (siehe `club_tenancy`).
| Mechanismus | Inhalt |
|-------------|--------|
| **Cursor / IDE** | Projektregel `.cursor/rules/access-layer.mdc` (Router); Agents sollen nicht auf „nur require_auth“ ausweichen. |
| **Heuristik-Check** | `python backend/scripts/check_access_layer_hints.py`; CI optional mit `ACCESS_LAYER_STRICT=1`. |
| **PR-Checkliste** | Neuer/changed Endpoint: TenantContext verwendet? Governance für Listen + Detail? Tests für zweiten Verein? |
| **Single Source of Truth** | Sichtbarkeitsregeln nur in Zugriffsmodul(en), nicht in Routers dupliziert. |
| **Änderungen am Enum** | Nur zusammen mit Migration + Kurzbeschreibung in diesem Dokument (Datum/Changelog-Zeile). |

View File

@ -13,12 +13,17 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe AC.
| exercise_progression_graphs | `/api/exercise-progression-graphs*` | ja | `get_tenant_context` | Liste wie Bibliothek; Schreiben Ersteller/Plattform-Admin | Kanten: Lesen wenn Graph lesbar |
| training_planning | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Vorlagen-Liste wie Übungen; POST Vorlage Default club_id |
| training_framework_programs | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Liste + POST Default club_id |
| admin_users | `GET /api/admin/users` | Plattform | optional | Admin-Rolle | |
| Sonstige | skills, methods, catalogs | zu klären | — | oft global | Zeilen ergänzen |
| admin_users | `GET /api/admin/users` | Plattform | `require_auth` | Admin-Rolle | EXEMPT `check_access_layer_hints.py` |
| auth | `/api/auth/*` | nein | — | Login/Session | EXEMPT |
| catalogs | Katalog-CRUD | nein (global) | `require_auth` | Admin/Trainer je Endpoint | EXEMPT; bei späterem `club_id` nachziehen |
| skills | `/api/skills*` | nein (global) | `require_auth` | je Endpoint | EXEMPT |
| maturity_models | Admin-Matrix | nein (global) | `require_auth` | Admin | EXEMPT |
| matrix_stack_bundle | Export/Import Bundles | Plattform/Test | `require_auth` | Admin | EXEMPT |
| import_wiki / import_wiki_admin | Wiki-Import | Werkzeug | `require_auth`/Admin | Admin | EXEMPT |
**Legende:** „zu klären“ = keine Vereinsdaten oder globales Lesen; bei neuem Bezug zu `club_id`/`visibility` nachziehen.
**Legende:** Router auf der EXEMPT-Liste des Scripts sind globale oder Auth-only-Pfade; sobald ein Router Vereinsdaten oder Bibliotheks-Sichtbarkeit erhält, EXEMPT entfernen und `get_tenant_context` einführen.
Letzte Änderung: 2026-05-05 — Vereins-/Mitgliedschafts-/Antrags-Router und Profil-PUT auf `get_tenant_context`; Progressionsgraphen Sichtbarkeit wie Bibliothek.
Letzte Änderung: 2026-05-05 — Cursor-Regel + Architektur-/Coding-Pflicht + Script `backend/scripts/check_access_layer_hints.py`; Katalog-Router im Audit als global dokumentiert.
---

View File

@ -55,6 +55,21 @@ return {"error": "not found"}
return {"message": "Fehler", "success": False}
```
### 1.4 Mandanten & Zugriffsschicht (Shinkan / ACCESS_LAYER)
**Verbindlicher Rahmen:** `.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`
**Fortlaufendes Inventar:** `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md`
**Definition of Done für neue oder geänderte geschützte APIs**, sobald Daten **Verein**, **Sichtbarkeit** oder **mandantenbezogene Listen** betreffen:
1. Request-Kontext: `Depends(get_tenant_context)` (oder dokumentierte Ausnahme mit Kommentar `# ACCESS_LAYER exempt:` + Audit).
2. Lesen: gleiche Sichtbarkeitslogik wie vergleichbare Bibliotheks-/Planungsartefakte (`library_content_visibility_sql`, `exercise_visible_to_profile` / zentrale Helfer — nicht ad-hoc „alles aus der Tabelle“).
3. Schreiben: `assert_valid_governance_visibility` wo `visibility` / `club_id` gesetzt oder geändert wird.
4. Audit-Tabelle und bei Bedarf die EXEMPT-Liste im Script `backend/scripts/check_access_layer_hints.py` aktualisieren.
5. Optional vor Commit: `python backend/scripts/check_access_layer_hints.py` (mit `ACCESS_LAYER_STRICT=1` schlägt bei neuen Verstößen fehl).
Router ohne Vereinsbezug (z.B. globale Kataloge, Auth-Login) bleiben bewusst ohne `get_tenant_context`; sie stehen im Script auf der **EXEMPT**-Liste.
---
## 2. Versionskontrollsystem

View File

@ -4,17 +4,32 @@ Diese Regeln IMMER befolgen. Sie basieren auf Erfahrungen aus der Entwicklung.
## Backend
### 1. Auth auf jeden Endpoint
### 1. Auth und Mandantenkontext (Shinkan)
**Jeder geschützte Endpoint braucht Auth.** Sofern der Endpoint **Vereinsdaten**, **visibility/club_id** oder **mandanten-gefilterte Listen** betrifft, zusätzlich **`TenantContext`** — nicht nur `require_auth` allein.
```python
# Jeder neue Endpoint braucht Auth:
@router.get("/neuer-endpoint")
def neuer_endpoint(session: dict = Depends(require_auth)):
pid = session['profile_id']
from tenant_context import TenantContext, get_tenant_context
@router.get("/beispiel")
def beispiel(tenant: TenantContext = Depends(get_tenant_context)):
pid = tenant.profile_id
role = tenant.global_role
club_ctx = tenant.effective_club_id # kann None sein (z. B. Plattform-Admin)
```
### 2. Profile-ID aus Session nie aus Header
- **Bibliotheks-/Planungslisten:** Filter wie bestehende Module (`library_content_visibility_sql` oder gleiche Leseprüfung); keine vollständige Tabelle für normale Nutzer.
- **Schreiben:** `assert_valid_governance_visibility` aus `club_tenancy`, wenn `visibility` / `club_id` gesetzt werden.
- **Dokumentation:** Änderungen in `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md` festhalten.
- **Ausnahmen** (z.B. reiner Login, globale Kataloge): Kommentar `# ACCESS_LAYER exempt: …` und ggf. Eintrag in `backend/scripts/check_access_layer_hints.py`.
Reine Plattform-Admin-Router (ohne Vereinskontext) können bei Bedarf weiter `Depends(require_auth)` nutzen — dann im Audit als „Plattform“ kennzeichnen.
### 2. Profile-ID aus TenantContext oder Session — nie aus freiem Header
```python
pid = session['profile_id'] # ✅
pid = tenant.profile_id # ✅ bei Depends(get_tenant_context)
# oder session['profile_id'] nur wenn Endpoint ausdrücklich ohne TenantContext (Ausnahme dokumentieren)
# Nicht: request.headers.get('X-Profile-Id') ❌
```

View File

@ -0,0 +1,36 @@
---
description: Mandanten & Governance — TenantContext, Sichtbarkeit, keine Schnellpfade
globs: backend/routers/*.py
alwaysApply: false
---
# Zugriffsschicht (Shinkan)
Vor neuen oder geänderten Endpoints in `backend/routers/` kurz prüfen:
1. **Pflichtlektüre** (bei inhaltsbezogenen APIs): `.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` und `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md`.
2. **Auth**: kein geschützter Endpoint ohne `Depends(require_auth)` bzw. eingebettet in `Depends(get_tenant_context)` (der holt die Session bereits).
3. **Verein / Sichtbarkeit** (`visibility`, `club_id`, Mitglieder-Inhalte):
`tenant: TenantContext = Depends(get_tenant_context)` verwenden; Profile-ID aus `tenant.profile_id`, Rolle aus `tenant.global_role`.
4. **Bibliothekslisten** (Übungen, Vorlagen, Rahmenprogramme, Progressionsgraphen, gleiche Semantik): Filter über `library_content_visibility_sql(...)` bzw. gleiche Leseregel wie bestehende Module — nicht „alle Zeilen aus SELECT“.
5. **Schreiben mit Governance**: bei `visibility`/`club_id`-Änderungen `assert_valid_governance_visibility`; bei `club` ohne `club_id` im Body → `tenant.effective_club_id` oder klare 400-Hinweise (wie bei Übungen).
6. **Ausnahmen** nur mit kurzem Kommentar im Code: `# ACCESS_LAYER exempt: …` und Eintrag im Audit oder in `backend/scripts/check_access_layer_hints.py` (EXEMPT-Liste).
7. **Nach Merge**: eine Zeile `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md` anpassen, wenn sich Tenant oder Governance ändert.
```python
# ❌ Schnellpfad: nur Session, obwohl Vereinsdaten betroffen
@router.get("/foo")
def foo(session=Depends(require_auth)):
...
# ✅ Konsistent zu clubs/exercises/planning
@router.get("/foo")
def foo(tenant: TenantContext = Depends(get_tenant_context)):
...
```

View File

@ -5,6 +5,9 @@
> VOR jeder Implementierung lesen:
> | Architektur-Regeln | `.claude/rules/ARCHITECTURE.md` |
> | Coding-Regeln | `.claude/rules/CODING_RULES.md` |
> | Zugriffsschicht (Multi-Tenancy, Governance) | `.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` |
> | Endpoint-Audit (Tenant/Governance) | `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md` |
> | Cursor-Regel Zugriffsschicht | `.cursor/rules/access-layer.mdc` |
> | Lessons Learned | `.claude/rules/LESSONS_LEARNED.md` |
> | Setup-Dokument | `.claude/docs/working/SHINKAN_PROJECT_SETUP.md` |
> | Anforderungen | `.claude/docs/functional/SHINKAN_REQUIREMENTS.md` |

View File

@ -127,6 +127,20 @@ def assert_valid_governance_visibility(
assert_club_member(cur, profile_id, club_id)
def library_content_visible_to_profile(
cur,
profile_id: int,
visibility: str,
content_club_id: Optional[int],
created_by: Optional[int],
global_role: Optional[str],
) -> bool:
"""Leserechte wie Übungen für alle Objekte mit visibility/club_id/created_by (Bibliothek & Co.)."""
return exercise_visible_to_profile(
cur, profile_id, visibility, content_club_id, created_by, global_role
)
def exercise_visible_to_profile(
cur,
profile_id: int,

View File

@ -13,8 +13,8 @@ from db import get_db, get_cursor, r2d
from tenant_context import TenantContext, get_tenant_context, library_content_visibility_sql
from club_tenancy import (
assert_valid_governance_visibility,
exercise_visible_to_profile,
is_platform_admin,
library_content_visible_to_profile,
)
from routers.training_planning import _has_planning_role
@ -106,7 +106,7 @@ def _assert_graph_readable(cur, row: dict, profile_id: int, role: str) -> None:
cr = row.get("created_by")
if cr is not None:
cr = int(cr)
if not exercise_visible_to_profile(cur, profile_id, vis, cid, cr, role):
if not library_content_visible_to_profile(cur, profile_id, vis, cid, cr, role):
raise HTTPException(status_code=403, detail="Keine Berechtigung für diesen Progressionsgraph")

View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""
Heuristik-Check: Router mit FastAPI-Routen und Auth sollten bei mandantenrelevanten APIs
get_tenant_context nutzen siehe ACCESS_LAYER_AND_GOVERNANCE_PLAN.md.
Lauf aus Repo-Root:
python backend/scripts/check_access_layer_hints.py
Mit Fehler-Exit bei Verstößen (z. B. CI):
set ACCESS_LAYER_STRICT=1 # Windows: set ACCESS_LAYER_STRICT=1
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
# Router, die bewusst keinen TenantContext verwenden (global / Auth-only / Admin-Tools).
# Neuen Eintrag nur mit kurzer Begründung im Commit/Audit ergänzen.
EXEMPT_ROUTERS: frozenset[str] = frozenset(
{
"auth.py",
"admin_users.py",
"catalogs.py",
"skills.py",
"maturity_models.py",
"matrix_stack_bundle.py",
"import_wiki.py",
"import_wiki_admin.py",
}
)
def main() -> int:
strict = os.environ.get("ACCESS_LAYER_STRICT", "").strip().lower() in ("1", "true", "yes")
root = Path(__file__).resolve().parents[1]
routers = root / "routers"
if not routers.is_dir():
print("check_access_layer_hints: routers/ nicht gefunden", file=sys.stderr)
return 1
issues: list[str] = []
for path in sorted(routers.glob("*.py")):
name = path.name
text = path.read_text(encoding="utf-8")
if "@router." not in text:
continue
if name in EXEMPT_ROUTERS:
continue
if "get_tenant_context" in text:
continue
if "require_auth" not in text:
continue
issues.append(
f" {path.relative_to(root.parent)}: Routen + require_auth, "
f"aber kein get_tenant_context — TenantContext ergänzen oder in EXEMPT_ROUTERS eintragen "
f"(mit Begründung / Audit)."
)
if not issues:
print("check_access_layer_hints: OK (keine auffälligen Router außerhalb EXEMPT).")
return 0
print("check_access_layer_hints: mögliche ACCESS_LAYER-Abweichungen:\n", file=sys.stderr)
for line in issues:
print(line, file=sys.stderr)
print(
"\nHinweis: Heuristik — false positives möglich. "
"Bei echter Ausnahme Datei zu EXEMPT_ROUTERS hinzufügen.",
file=sys.stderr,
)
return 1 if strict else 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -1,13 +1,13 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.26"
APP_VERSION = "0.8.27"
BUILD_DATE = "2026-05-05"
DB_SCHEMA_VERSION = "20260505041"
MODULE_VERSIONS = {
"auth": "1.2.1", # Login-Rate-Limit 30/minute pro IP (vorher 5/minute)
"profiles": "1.4.1", # PUT /profiles*, Legacy /profile: Depends(get_tenant_context); profile_document für internes Laden
"tenant_context": "1.0.1", # Vereine/Mitglieder/Vereinsanträge/Profil-PUT nutzen get_tenant_context (Header-Membership)
"tenant_context": "1.0.2", # Dokumentation: ACCESS_LAYER Pflicht + Script check_access_layer_hints.py
"clubs": "0.4.1", # Alle geschützten Endpoints Depends(get_tenant_context); profile_id/role aus TenantContext
"club_memberships": "1.0.1", # Depends(get_tenant_context)
"club_join_requests": "1.0.1", # Depends(get_tenant_context)
@ -15,7 +15,7 @@ MODULE_VERSIONS = {
"groups": "0.1.0",
"skills": "0.1.0",
"methods": "0.1.0",
"exercises": "2.6.2", # Progressionsgraphen: library_content_visibility_sql + Lesen wie Bibliothek; Schreiben Ersteller/Admin; Governance club_id wie Übungen
"exercises": "2.6.3", # Progressionsgraphen: library_content_visible_to_profile aus club_tenancy
"training_units": "0.1.0",
"training_programs": "0.1.0",
"planning": "0.8.0", # TenantContext auf allen Planungs-Endpunkten; Vorlagen-Liste wie Übungen nach aktivem Verein
@ -27,6 +27,15 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
{
"version": "0.8.27",
"date": "2026-05-05",
"changes": [
"ACCESS_LAYER Governance-Disziplin: .cursor/rules/access-layer.mdc; ARCHITECTURE 1.4 + CODING_RULES Tenant-Pfad; CLAUDE Pflichtlektüre Zugriffsschicht",
"backend/scripts/check_access_layer_hints.py — Router ohne get_tenant_context außerhalb EXEMPT melden (optional ACCESS_LAYER_STRICT=1)",
"club_tenancy.library_content_visible_to_profile; Audit-Tabelle um globale Router (EXEMPT) ergänzt",
],
},
{
"version": "0.8.26",
"date": "2026-05-05",

View File

@ -1,6 +1,6 @@
// Shinkan Jinkendo Frontend Version
export const APP_VERSION = "0.8.26"
export const APP_VERSION = "0.8.27"
export const BUILD_DATE = "2026-05-05"
export const PAGE_VERSIONS = {