From abee6171df5325fb942029bdce1d93ee31ba2fa2 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 5 May 2026 22:09:25 +0200 Subject: [PATCH] feat: enhance access layer governance and visibility checks - 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. --- .../ACCESS_LAYER_AND_GOVERNANCE_PLAN.md | 2 + .../working/ACCESS_LAYER_ENDPOINT_AUDIT.md | 13 +++- .claude/rules/ARCHITECTURE.md | 15 ++++ .claude/rules/CODING_RULES.md | 29 +++++-- .cursor/rules/access-layer.mdc | 36 +++++++++ CLAUDE.md | 3 + backend/club_tenancy.py | 14 ++++ .../routers/exercise_progression_graphs.py | 4 +- backend/scripts/check_access_layer_hints.py | 76 +++++++++++++++++++ backend/version.py | 15 +++- frontend/src/version.js | 2 +- 11 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 .cursor/rules/access-layer.mdc create mode 100644 backend/scripts/check_access_layer_hints.py diff --git a/.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md b/.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md index f26f060..f055312 100644 --- a/.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md +++ b/.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md @@ -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). | diff --git a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md index aa7dbfb..bfc97ec 100644 --- a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md +++ b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md @@ -13,12 +13,17 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C. | 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. --- diff --git a/.claude/rules/ARCHITECTURE.md b/.claude/rules/ARCHITECTURE.md index d5e4e3b..066dbd3 100644 --- a/.claude/rules/ARCHITECTURE.md +++ b/.claude/rules/ARCHITECTURE.md @@ -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 diff --git a/.claude/rules/CODING_RULES.md b/.claude/rules/CODING_RULES.md index bc219fe..3bdc1d2 100644 --- a/.claude/rules/CODING_RULES.md +++ b/.claude/rules/CODING_RULES.md @@ -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') ❌ ``` diff --git a/.cursor/rules/access-layer.mdc b/.cursor/rules/access-layer.mdc new file mode 100644 index 0000000..d563d04 --- /dev/null +++ b/.cursor/rules/access-layer.mdc @@ -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)): + ... +``` diff --git a/CLAUDE.md b/CLAUDE.md index fa53f8f..85026c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` | diff --git a/backend/club_tenancy.py b/backend/club_tenancy.py index 6756c24..f96d3f8 100644 --- a/backend/club_tenancy.py +++ b/backend/club_tenancy.py @@ -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, diff --git a/backend/routers/exercise_progression_graphs.py b/backend/routers/exercise_progression_graphs.py index 683e63c..124b681 100644 --- a/backend/routers/exercise_progression_graphs.py +++ b/backend/routers/exercise_progression_graphs.py @@ -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") diff --git a/backend/scripts/check_access_layer_hints.py b/backend/scripts/check_access_layer_hints.py new file mode 100644 index 0000000..858b664 --- /dev/null +++ b/backend/scripts/check_access_layer_hints.py @@ -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()) diff --git a/backend/version.py b/backend/version.py index 5dad9f4..7e37fa6 100644 --- a/backend/version.py +++ b/backend/version.py @@ -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", diff --git a/frontend/src/version.js b/frontend/src/version.js index 56180ae..607eb36 100644 --- a/frontend/src/version.js +++ b/frontend/src/version.js @@ -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 = {