Implement Club Feature Enforcement Logic and Update Versioning
Some checks failed
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Failing after 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
Some checks failed
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Failing after 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced a new environment variable `CLUB_FEATURE_ENFORCE` to control club feature access, allowing values of 1, true, or yes for activation. - Updated the backend logic to check for club feature enforcement, raising HTTP exceptions when access is denied without an active club context. - Enhanced the admin rights router with a new endpoint to check the enforcement status of club features. - Incremented application version to 0.8.202 to reflect these changes.
This commit is contained in:
parent
4130a63dfe
commit
a9a6153ed5
|
|
@ -34,6 +34,10 @@ DB_PASSWORD=CHANGE_ME_SECURE_PASSWORD
|
|||
|
||||
OPENROUTER_API_KEY=your_api_key_here
|
||||
OPENROUTER_MODEL=anthropic/claude-sonnet-4
|
||||
|
||||
# Vereins-Kontingente hart blockieren (KI-Kosten!). Nur 1, true oder yes aktivieren.
|
||||
# Nach Änderung: docker compose -f docker-compose.dev-env.yml up -d backend
|
||||
CLUB_FEATURE_ENFORCE=1
|
||||
# Standard-OpenRouter-Modell (alle Aufrufe). Optional pro Prompt in ai_prompts.openrouter_model
|
||||
# ueberschreibbar (Migration 070, Superadmin unter „KI Prompts“).
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
def capability_enforcement_enabled() -> bool:
|
||||
return os.getenv("CAPABILITY_ENFORCE", "0").strip() == "1"
|
||||
v = os.getenv("CAPABILITY_ENFORCE", "0").strip().lower()
|
||||
return v in ("1", "true", "yes")
|
||||
|
||||
|
||||
def club_roles_in_club(tenant: "TenantContext", club_id: Optional[int]) -> List[str]:
|
||||
|
|
|
|||
|
|
@ -321,8 +321,9 @@ def _check_club_impl(club_id: int, feature_id: str, conn) -> Dict[str, Any]:
|
|||
|
||||
|
||||
def club_feature_enforcement_enabled() -> bool:
|
||||
"""Phase 4: Hard-Block aktiv (Env CLUB_FEATURE_ENFORCE=1)."""
|
||||
return os.getenv("CLUB_FEATURE_ENFORCE", "0").strip() == "1"
|
||||
"""Phase 4: Hard-Block aktiv (Env CLUB_FEATURE_ENFORCE=1|true|yes)."""
|
||||
v = os.getenv("CLUB_FEATURE_ENFORCE", "0").strip().lower()
|
||||
return v in ("1", "true", "yes")
|
||||
|
||||
|
||||
def probe_club_feature_access(
|
||||
|
|
@ -333,6 +334,7 @@ def probe_club_feature_access(
|
|||
profile_id: Optional[int] = None,
|
||||
portal_role: Optional[str] = None,
|
||||
endpoint: Optional[str] = None,
|
||||
tenant: Optional["TenantContext"] = None,
|
||||
conn=None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
|
|
@ -344,7 +346,7 @@ def probe_club_feature_access(
|
|||
|
||||
if club_id is None:
|
||||
access = {
|
||||
"allowed": True,
|
||||
"allowed": not club_feature_enforcement_enabled(),
|
||||
"limit": None,
|
||||
"used": 0,
|
||||
"remaining": None,
|
||||
|
|
@ -358,7 +360,15 @@ def probe_club_feature_access(
|
|||
action=action,
|
||||
access=access,
|
||||
endpoint=endpoint,
|
||||
phase="probe",
|
||||
phase="enforce" if club_feature_enforcement_enabled() else "probe",
|
||||
)
|
||||
if club_feature_enforcement_enabled() and not access.get("allowed"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=(
|
||||
f"Kein Vereinskontext für {feature_id} — "
|
||||
"aktiven Verein wählen (X-Active-Club-Id)."
|
||||
),
|
||||
)
|
||||
return access
|
||||
|
||||
|
|
@ -371,6 +381,7 @@ def probe_club_feature_access(
|
|||
profile_id=profile_id,
|
||||
portal_role=portal_role,
|
||||
feature_id=feature_id,
|
||||
tenant=tenant,
|
||||
):
|
||||
plan_id = get_effective_club_plan(cur, int(club_id))
|
||||
return quota_bypass_access(
|
||||
|
|
|
|||
|
|
@ -66,6 +66,14 @@ if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() not in ("1", "true", "yes"):
|
|||
print(f"[FAIL] Rights registry sync: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
from club_features import club_feature_enforcement_enabled
|
||||
|
||||
_cfe = os.getenv("CLUB_FEATURE_ENFORCE", "0")
|
||||
print(
|
||||
f"[OK] CLUB_FEATURE_ENFORCE raw={_cfe!r} "
|
||||
f"active={club_feature_enforcement_enabled()}"
|
||||
)
|
||||
|
||||
from routers.auth import limiter as auth_rate_limiter
|
||||
|
||||
# OpenAPI: in Produktion standardmäßig aus (Schema nicht öffentlich). Notfall: PUBLIC_OPENAPI=1
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Superadmin: Rollen & Rechte — Capability-Grants, Kontingent-Bypass, Vereins-Ko
|
|||
|
||||
Ein Router für das Rechtesystem (M6). Kein paralleles Exemption-Schema.
|
||||
"""
|
||||
import os
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
|
@ -15,10 +16,12 @@ from club_quota_bypass import (
|
|||
list_quota_bypass_grants,
|
||||
quota_bypass_capability_id_for_feature,
|
||||
)
|
||||
from capabilities import capability_enforcement_enabled
|
||||
from capability_enforcement_audit import (
|
||||
enforcement_status_for_capability,
|
||||
feature_consume_status,
|
||||
)
|
||||
from club_features import club_feature_enforcement_enabled
|
||||
from club_tenancy import is_superadmin
|
||||
from db import get_db, get_cursor, r2d
|
||||
|
||||
|
|
@ -83,6 +86,26 @@ class QuotaBypassProfileBody(BaseModel):
|
|||
reason: Optional[str] = Field(None, max_length=500)
|
||||
|
||||
|
||||
# ── Enforcement-Diagnose (Superadmin) ────────────────────────────────────────
|
||||
|
||||
@router.get("/enforcement-status")
|
||||
def get_enforcement_status(session: dict = Depends(require_auth)):
|
||||
"""Prüft ob Hard-Block-Env im laufenden Container/Prozess ankommt."""
|
||||
_require_superadmin(session)
|
||||
raw = os.getenv("CLUB_FEATURE_ENFORCE", "0")
|
||||
return {
|
||||
"club_feature_enforce_active": club_feature_enforcement_enabled(),
|
||||
"club_feature_enforce_raw": raw,
|
||||
"capability_enforce_active": capability_enforcement_enabled(),
|
||||
"capability_enforce_raw": os.getenv("CAPABILITY_ENFORCE", "0"),
|
||||
"hints": [
|
||||
"Nach .env-Änderung: docker compose up -d backend (Container neu erstellen).",
|
||||
"Superadmin hat Quota-Bypass — KI-Limit-Test als Trainer, nicht als Superadmin.",
|
||||
"Ohne aktiven Verein (X-Active-Club-Id) blockiert Enforce ebenfalls.",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ── Capability-Matrix (Rollen → Fähigkeiten) ─────────────────────────────────
|
||||
|
||||
@router.get("/capability-matrix")
|
||||
|
|
|
|||
|
|
@ -2341,6 +2341,7 @@ def exercise_ai_suggest_endpoint(
|
|||
profile_id=tenant.profile_id,
|
||||
portal_role=tenant.global_role,
|
||||
endpoint="POST /exercises/ai/suggest",
|
||||
tenant=tenant,
|
||||
)
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -2388,6 +2389,7 @@ def exercise_ai_regenerate_endpoint(
|
|||
profile_id=tenant.profile_id,
|
||||
portal_role=tenant.global_role,
|
||||
endpoint="POST /exercises/{id}/ai/regenerate",
|
||||
tenant=tenant,
|
||||
)
|
||||
want_summary = "summary" in body.regenerate
|
||||
want_skills = "skills" in body.regenerate
|
||||
|
|
@ -2500,6 +2502,7 @@ def create_exercise(
|
|||
profile_id=profile_id,
|
||||
portal_role=tenant.global_role,
|
||||
endpoint="POST /exercises",
|
||||
tenant=tenant,
|
||||
)
|
||||
|
||||
# §11 Inline-Medien: Kurzsyntax → kanonisches Markup; Verweise erst nach Medien-Anlage möglich
|
||||
|
|
@ -3319,6 +3322,7 @@ async def upload_exercise_media(
|
|||
profile_id=profile_id,
|
||||
portal_role=tenant.global_role,
|
||||
endpoint="POST /exercises/{id}/media",
|
||||
tenant=tenant,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ def post_planning_exercise_suggest(
|
|||
profile_id=tenant.profile_id,
|
||||
portal_role=tenant.global_role,
|
||||
endpoint="POST /planning/exercise-suggest",
|
||||
tenant=tenant,
|
||||
)
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -90,6 +91,7 @@ def post_progression_path_suggest(
|
|||
profile_id=tenant.profile_id,
|
||||
portal_role=tenant.global_role,
|
||||
endpoint="POST /planning/progression-path-suggest",
|
||||
tenant=tenant,
|
||||
)
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
|
|||
|
|
@ -204,6 +204,22 @@ def test_consume_with_usage_returns_snapshot(monkeypatch):
|
|||
assert usage["ai_calls"]["used"] == 4
|
||||
|
||||
|
||||
def test_probe_blocks_no_club_context_when_enforce(monkeypatch):
|
||||
monkeypatch.setenv("CLUB_FEATURE_ENFORCE", "1")
|
||||
monkeypatch.setattr("club_feature_logger.log_club_feature_usage", lambda **kwargs: None)
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
probe_club_feature_access(
|
||||
feature_id="ai_calls",
|
||||
action="suggest",
|
||||
club_id=None,
|
||||
profile_id=3,
|
||||
endpoint="POST /exercises/ai/suggest",
|
||||
)
|
||||
assert exc.value.status_code == 403
|
||||
assert "Vereinskontext" in str(exc.value.detail)
|
||||
|
||||
|
||||
def test_club_feature_enforcement_env_default_off(monkeypatch):
|
||||
monkeypatch.delenv("CLUB_FEATURE_ENFORCE", raising=False)
|
||||
assert club_feature_enforcement_enabled() is False
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.201"
|
||||
APP_VERSION = "0.8.202"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260606084"
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ services:
|
|||
ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-https://dev.shinkan.jinkendo.de,http://192.168.2.49:3098}"
|
||||
ENVIRONMENT: "${ENVIRONMENT:-development}"
|
||||
# M5: Hard-Block Vereins-Kontingente (Default aus — in .env auf 1 setzen zum Testen)
|
||||
CLUB_FEATURE_ENFORCE: "${CLUB_FEATURE_ENFORCE:-0}"
|
||||
CLUB_FEATURE_ENFORCE: "${CLUB_FEATURE_ENFORCE:-1}"
|
||||
MEDIAWIKI_API_URL: "${MEDIAWIKI_API_URL:-https://karatetrainer.net/api.php}"
|
||||
MEDIAWIKI_USER: "${MEDIAWIKI_USER:-Jinkendo}"
|
||||
MEDIAWIKI_PASSWORD: "${MEDIAWIKI_PASSWORD:-CHANGE_ME}"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ services:
|
|||
APP_URL: "${APP_URL:-https://shinkan.jinkendo.de}"
|
||||
ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-https://shinkan.jinkendo.de}"
|
||||
ENVIRONMENT: "${ENVIRONMENT:-production}"
|
||||
CLUB_FEATURE_ENFORCE: "${CLUB_FEATURE_ENFORCE:-1}"
|
||||
# MediaWiki/SMW Import — in dev-env.yml bereits gesetzt; Prod brauchte diese Zeilen ebenfalls,
|
||||
# sonst: leere MEDIAWIKI_API_URL im Container → Import bricht ab (auf Test/Dev war es immer gesetzt).
|
||||
MEDIAWIKI_API_URL: "${MEDIAWIKI_API_URL:-https://karatetrainer.net/api.php}"
|
||||
|
|
|
|||
|
|
@ -262,6 +262,10 @@ export async function rejectClubCreationRequest(requestId) {
|
|||
}
|
||||
|
||||
/** M6: Rollen & Rechte — Capabilities, Kontingent-Bypass, Vereins-Kontingente (Superadmin). */
|
||||
export async function getAdminRightsEnforcementStatus() {
|
||||
return request('/api/admin/rights/enforcement-status')
|
||||
}
|
||||
|
||||
export async function getAdminRightsCapabilityMatrix() {
|
||||
return request('/api/admin/rights/capability-matrix')
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user