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
- 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.
181 lines
5.6 KiB
Python
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,
|
|
}
|