feat: update access layer governance and visibility checks
- Enhanced ACCESS_LAYER_AND_GOVERNANCE_PLAN.md with additional details on heuristic checks and testing procedures for cross-tenant scenarios. - Updated club_tenancy.py to recommend using `library_content_visible_to_profile` for exercise visibility checks. - Refactored multiple routers to utilize `library_content_visible_to_profile`, improving consistency in access control across exercises and training planning. - Bumped application version to 0.8.28 and updated changelog to reflect these changes.
This commit is contained in:
parent
abee6171df
commit
e0ecfe927f
|
|
@ -97,7 +97,7 @@ 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`. |
|
||||
| **Heuristik-Check** | `python backend/scripts/check_access_layer_hints.py`; CI optional mit `ACCESS_LAYER_STRICT=1`. Optional danach: `cd backend && pytest tests/` (nach `pip install -r requirements-dev.txt`). |
|
||||
| **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). |
|
||||
|
|
@ -109,7 +109,7 @@ Ausgangslage im Code: `private` \| `club` \| `official` (siehe `club_tenancy`).
|
|||
|
||||
1. **TenantContext-Spezifikation** (ein Abschnitt in diesem Dokument oder Kurz-ADR): Request-Lebenszyklus, Fehlerbilder, Superadmin.
|
||||
2. **Endpoint-Audit-Tabelle** (Working-Dokument, bei jedem Merge pflegen bis Stufe C abgeschlossen).
|
||||
3. **Testplan „Cross-Tenant“** (manuell oder pytest): Minimalsetup zweier Vereine.
|
||||
3. **Testplan „Cross-Tenant“** (manuell oder pytest): Minimalsetup zweier Vereine — erste rein-funktionale Tests unter `backend/tests/test_access_layer.py` (ohne DB); Integration folgt.
|
||||
|
||||
**Audit-Tabelle (fortlaufend):** `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md`
|
||||
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ def exercise_visible_to_profile(
|
|||
created_by: Optional[int],
|
||||
global_role: Optional[str],
|
||||
) -> bool:
|
||||
"""Leserechte einer Übung. Für neue Codepfade lieber `library_content_visible_to_profile` verwenden."""
|
||||
if is_platform_admin(global_role):
|
||||
return True
|
||||
if visibility == "official":
|
||||
|
|
|
|||
3
backend/pytest.ini
Normal file
3
backend/pytest.ini
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[pytest]
|
||||
testpaths = tests
|
||||
pythonpath = .
|
||||
3
backend/requirements-dev.txt
Normal file
3
backend/requirements-dev.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Entwicklung / CI — zusätzlich zu backend/requirements.txt installieren:
|
||||
# pip install -r backend/requirements.txt -r backend/requirements-dev.txt
|
||||
pytest>=8.0,<9
|
||||
|
|
@ -15,7 +15,11 @@ from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File,
|
|||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
from db import get_db, get_cursor, r2d
|
||||
from club_tenancy import assert_valid_governance_visibility, exercise_visible_to_profile, is_platform_admin
|
||||
from club_tenancy import (
|
||||
assert_valid_governance_visibility,
|
||||
is_platform_admin,
|
||||
library_content_visible_to_profile,
|
||||
)
|
||||
from tenant_context import TenantContext, get_tenant_context, library_content_visibility_sql
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -765,7 +769,7 @@ def get_exercise(
|
|||
if not exercise:
|
||||
raise HTTPException(status_code=404, detail="Übung nicht gefunden")
|
||||
|
||||
if not exercise_visible_to_profile(
|
||||
if not library_content_visible_to_profile(
|
||||
cur,
|
||||
profile_id,
|
||||
exercise["visibility"],
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||
|
||||
from club_tenancy import (
|
||||
assert_valid_governance_visibility,
|
||||
exercise_visible_to_profile,
|
||||
is_platform_admin,
|
||||
library_content_visible_to_profile,
|
||||
)
|
||||
from db import get_db, get_cursor, r2d
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ def _framework_assert_readable(
|
|||
) -> None:
|
||||
if is_platform_admin(role):
|
||||
return
|
||||
if not exercise_visible_to_profile(
|
||||
if not library_content_visible_to_profile(
|
||||
cur,
|
||||
profile_id,
|
||||
row.get("visibility") or "private",
|
||||
|
|
|
|||
|
|
@ -12,8 +12,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,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["training_planning"])
|
||||
|
|
@ -531,7 +531,7 @@ def _fetch_training_plan_template_row(cur, tid: int) -> Dict[str, Any]:
|
|||
def _template_assert_readable(cur, row: Dict[str, Any], profile_id: int, role: Optional[str]) -> None:
|
||||
if is_platform_admin(role):
|
||||
return
|
||||
if not exercise_visible_to_profile(
|
||||
if not library_content_visible_to_profile(
|
||||
cur,
|
||||
profile_id,
|
||||
row.get("visibility") or "club",
|
||||
|
|
|
|||
76
backend/tests/test_access_layer.py
Normal file
76
backend/tests/test_access_layer.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"""Unit tests ohne Datenbank für die Zugriffsschicht (Visibility-SQL, Header-Parsing)."""
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from tenant_context import library_content_visibility_sql, parse_active_club_header
|
||||
|
||||
|
||||
def test_library_visibility_sql_platform_admin_no_filter():
|
||||
sql, params = library_content_visibility_sql(
|
||||
alias="e",
|
||||
profile_id=1,
|
||||
role="admin",
|
||||
effective_club_id=None,
|
||||
)
|
||||
assert sql == "TRUE"
|
||||
assert params == []
|
||||
|
||||
|
||||
def test_library_visibility_sql_superadmin():
|
||||
sql, params = library_content_visibility_sql(
|
||||
alias="fp",
|
||||
profile_id=2,
|
||||
role="superadmin",
|
||||
effective_club_id=100,
|
||||
)
|
||||
assert sql == "TRUE"
|
||||
assert params == []
|
||||
|
||||
|
||||
def test_library_visibility_sql_trainer_without_active_club_no_shared_club_branch():
|
||||
sql, params = library_content_visibility_sql(
|
||||
alias="g",
|
||||
profile_id=42,
|
||||
role="trainer",
|
||||
effective_club_id=None,
|
||||
)
|
||||
assert "official" in sql
|
||||
assert "private" in sql
|
||||
assert "visibility = 'club'" not in sql
|
||||
assert params == [42]
|
||||
|
||||
|
||||
def test_library_visibility_sql_user_with_active_club_includes_club_branch():
|
||||
sql, params = library_content_visibility_sql(
|
||||
alias="t",
|
||||
profile_id=7,
|
||||
role="user",
|
||||
effective_club_id=99,
|
||||
)
|
||||
assert "visibility = 'club'" in sql
|
||||
assert "club_members" in sql
|
||||
assert params[0] == 7 # private branch created_by
|
||||
assert 99 in params
|
||||
assert params.count(7) >= 2 # private + EXISTS membership
|
||||
|
||||
|
||||
def test_parse_active_club_header_none_and_empty():
|
||||
assert parse_active_club_header(None) is None
|
||||
assert parse_active_club_header("") is None
|
||||
assert parse_active_club_header(" ") is None
|
||||
|
||||
|
||||
def test_parse_active_club_header_valid():
|
||||
assert parse_active_club_header("12") == 12
|
||||
|
||||
|
||||
def test_parse_active_club_header_invalid():
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
parse_active_club_header("not-int")
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
def test_parse_active_club_header_non_positive():
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
parse_active_club_header("0")
|
||||
assert exc.value.status_code == 400
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.27"
|
||||
APP_VERSION = "0.8.28"
|
||||
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.2", # Dokumentation: ACCESS_LAYER Pflicht + Script check_access_layer_hints.py
|
||||
"tenant_context": "1.0.3", # pytest: backend/tests/test_access_layer.py (Visibility-SQL, Header)
|
||||
"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,10 +15,10 @@ MODULE_VERSIONS = {
|
|||
"groups": "0.1.0",
|
||||
"skills": "0.1.0",
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.6.3", # Progressionsgraphen: library_content_visible_to_profile aus club_tenancy
|
||||
"exercises": "2.6.4", # Detail-Lesen über library_content_visible_to_profile
|
||||
"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
|
||||
"planning": "0.8.1", # Vorlagen Leseprüfung library_content_visible_to_profile
|
||||
"import_wiki": "1.0.0",
|
||||
"admin": "1.0.0",
|
||||
"membership": "1.0.0",
|
||||
|
|
@ -27,6 +27,14 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.28",
|
||||
"date": "2026-05-05",
|
||||
"changes": [
|
||||
"ACCESS_LAYER: Übungen-, Trainingsplanung-, Rahmenprogramme-Detail nutzen library_content_visible_to_profile (einheitliche Leseprüfung)",
|
||||
"pytest: backend/requirements-dev.txt, pytest.ini, backend/tests/test_access_layer.py — ohne DB (library_content_visibility_sql, parse_active_club_header)",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.27",
|
||||
"date": "2026-05-05",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Shinkan Jinkendo Frontend Version
|
||||
|
||||
export const APP_VERSION = "0.8.27"
|
||||
export const APP_VERSION = "0.8.28"
|
||||
export const BUILD_DATE = "2026-05-05"
|
||||
|
||||
export const PAGE_VERSIONS = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user