shinkan-jinkendo/backend/club_quota_bypass.py
Lars 8404a42b6c
Some checks failed
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 2s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 42s
Test Suite / playwright-tests (push) Successful in 1m19s
Implement Club Feature Quota Bypass and Update Versioning
- Added support for club feature quota bypass based on portal roles and profile grants in the capabilities check.
- Introduced new functions to handle quota bypass logic in club feature access and consumption.
- Updated the FeatureUsageBadge component to reflect platform exemptions for features.
- Incremented application version to 0.8.195 and database schema version to 20260606083 to reflect these changes.
- Enhanced backend routers to include new logic for consuming club features during AI-related actions.
2026-06-07 07:43:35 +02:00

181 lines
5.6 KiB
Python

"""
Vereins-Kontingent-Bypass über das Capability-System (kein Parallel-Rechtemodell).
Capabilities:
- platform.club_quota.bypass — alle Vereins-Features (Portal-Admin, Grant via portal_role)
- platform.club_quota.bypass.{feature_id} — ein Feature (domain quota_bypass, auch für Nicht-Admins per Grant)
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from tenant_context import TenantContext
QUOTA_BYPASS_ALL = "platform.club_quota.bypass"
QUOTA_BYPASS_FEATURE_PREFIX = "platform.club_quota.bypass."
def quota_bypass_capability_id_for_feature(feature_id: str) -> str:
return f"{QUOTA_BYPASS_FEATURE_PREFIX}{feature_id}"
def ensure_quota_bypass_capability(cur, feature_id: str) -> str:
"""Legt feature-spezifische Bypass-Capability an falls nötig."""
cap_id = quota_bypass_capability_id_for_feature(feature_id)
cur.execute(
"""
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id)
VALUES (%s, %s, 'quota_bypass', 'active_member', %s)
ON CONFLICT (id) DO NOTHING
""",
(cap_id, f"Vereins-Kontingent umgehen: {feature_id}", feature_id),
)
return cap_id
def _bypass_capability_ids(cur, feature_id: str) -> List[str]:
ids: List[str] = [QUOTA_BYPASS_ALL, quota_bypass_capability_id_for_feature(feature_id)]
cur.execute(
"""
SELECT id FROM capabilities
WHERE active = true
AND domain = 'quota_bypass'
AND linked_feature_id = %s
AND id <> %s
""",
(feature_id, quota_bypass_capability_id_for_feature(feature_id)),
)
for row in cur.fetchall():
cid = row.get("id")
if cid and cid not in ids:
ids.append(str(cid))
return ids
def _portal_role_has_grant(cur, portal_role: str, capability_id: str) -> bool:
role = (portal_role or "").strip().lower()
if not role:
return False
cur.execute(
"""
SELECT 1 FROM portal_role_capability_grants
WHERE portal_role = %s AND capability_id = %s
LIMIT 1
""",
(role, capability_id),
)
return cur.fetchone() is not None
def _profile_has_grant(cur, profile_id: int, capability_id: str) -> bool:
cur.execute(
"""
SELECT 1 FROM profile_capability_grants
WHERE profile_id = %s AND capability_id = %s
LIMIT 1
""",
(int(profile_id), capability_id),
)
return cur.fetchone() is not None
def is_club_feature_quota_bypassed(
cur,
*,
profile_id: Optional[int],
portal_role: Optional[str],
feature_id: str,
tenant: Optional["TenantContext"] = None,
) -> bool:
"""
True wenn ein konfigurierter Capability-Grant das Vereins-Kontingent für feature_id umgeht.
"""
if tenant is not None:
from capabilities import check_capability
for cap_id in _bypass_capability_ids(cur, feature_id):
if check_capability(cur, tenant, cap_id).get("allowed"):
return True
return False
for cap_id in _bypass_capability_ids(cur, feature_id):
if _portal_role_has_grant(cur, portal_role or "", cap_id):
return True
if profile_id is not None and _profile_has_grant(cur, int(profile_id), cap_id):
return True
return False
def quota_bypass_access(
*,
feature_id: str,
club_id: Optional[int] = None,
plan_id: Optional[str] = None,
capability_id: Optional[str] = None,
) -> Dict[str, Any]:
return {
"allowed": True,
"limit": None,
"used": 0,
"remaining": None,
"reason": "capability_quota_bypass",
"platform_exempt": True,
"quota_bypass_capability": capability_id,
"plan_id": plan_id,
"club_id": club_id,
"feature_id": feature_id,
}
def list_quota_bypass_grants(cur) -> Dict[str, Any]:
"""Admin: alle Grants zu Kontingent-Bypass-Capabilities."""
cur.execute(
"""
SELECT g.portal_role, g.capability_id, c.name AS capability_name,
c.linked_feature_id, c.domain
FROM portal_role_capability_grants g
INNER JOIN capabilities c ON c.id = g.capability_id
WHERE g.capability_id = %s
OR g.capability_id LIKE %s
OR c.domain = 'quota_bypass'
ORDER BY g.portal_role, g.capability_id
""",
(QUOTA_BYPASS_ALL, f"{QUOTA_BYPASS_FEATURE_PREFIX}%"),
)
portal_grants = [dict(r) for r in cur.fetchall()]
cur.execute(
"""
SELECT g.profile_id, p.email, p.name AS profile_name,
g.capability_id, c.name AS capability_name, c.linked_feature_id,
g.reason, g.granted_by_profile_id, g.created_at
FROM profile_capability_grants g
INNER JOIN profiles p ON p.id = g.profile_id
INNER JOIN capabilities c ON c.id = g.capability_id
WHERE g.capability_id = %s
OR g.capability_id LIKE %s
OR c.domain = 'quota_bypass'
ORDER BY g.profile_id, g.capability_id
""",
(QUOTA_BYPASS_ALL, f"{QUOTA_BYPASS_FEATURE_PREFIX}%"),
)
profile_grants = [dict(r) for r in cur.fetchall()]
cur.execute(
"""
SELECT id, name, domain, linked_feature_id
FROM capabilities
WHERE id = %s OR id LIKE %s OR domain = 'quota_bypass'
ORDER BY id
""",
(QUOTA_BYPASS_ALL, f"{QUOTA_BYPASS_FEATURE_PREFIX}%"),
)
capabilities = [dict(r) for r in cur.fetchall()]
return {
"capabilities": capabilities,
"portal_role_grants": portal_grants,
"profile_grants": profile_grants,
}