admin felület fejlesztése garázs, előfizeztési csomagok

This commit is contained in:
Roo
2026-06-25 01:58:04 +00:00
parent 80a5d67f79
commit 52011606ff
334 changed files with 46636 additions and 1606 deletions

View File

@@ -0,0 +1,321 @@
# /opt/docker/dev/service_finder/backend/app/services/rbac_service.py
"""
🎯 Scope-Based Admin RBAC Service (P0)
Implements scope-based access control for admin/staff roles.
Instead of direct organization membership, admins are assigned
a scope (e.g., REGION → "Pest_County") that determines which
organizations they can manage.
Architecture:
- SUPERADMIN: Unrestricted access (bypasses all scope checks)
- ADMIN/MODERATOR: Full access within their assigned scope
- SALES_REP/SERVICE_MGR: Scoped access with action-level filtering
Scope Types:
- REGION: Geographic region (e.g., "Pest_County", "Budapest")
- SEGMENT: Business segment (e.g., "Fleet", "Dealer", "Service")
- GLOBAL: Unrestricted (equivalent to SUPERADMIN for scope purposes)
Usage:
rbac = RBACService()
await rbac.check_admin_access(db, user, "EDIT_DATA", target_org_id=42)
"""
from __future__ import annotations
import logging
from enum import Enum
from typing import Optional, Dict, Set, List, Union
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.identity import User, UserRole
from app.models.marketplace.organization import Organization
from app.core.capabilities import SYSTEM_CAPABILITIES_MATRIX, role_has_capability
logger = logging.getLogger(__name__)
class ScopeType(str, Enum):
"""The type of scope assigned to an admin user."""
GLOBAL = "GLOBAL"
REGION = "REGION"
SEGMENT = "SEGMENT"
class AdminAction(str, Enum):
"""Admin actions that can be checked against scope."""
# ── Data Management ──
EDIT_DATA = "EDIT_DATA"
VIEW_DATA = "VIEW_DATA"
DELETE_DATA = "DELETE_DATA"
EXPORT_DATA = "EXPORT_DATA"
# ── Subscription Management ──
VIEW_SUBSCRIPTION = "VIEW_SUBSCRIPTION"
EDIT_SUBSCRIPTION = "EDIT_SUBSCRIPTION"
CANCEL_SUBSCRIPTION = "CANCEL_SUBSCRIPTION"
# ── User Management ──
MANAGE_USERS = "MANAGE_USERS"
SUSPEND_USERS = "SUSPEND_USERS"
BAN_USERS = "BAN_USERS"
# ── Financial ──
VIEW_FINANCIALS = "VIEW_FINANCIALS"
MANAGE_BILLING = "MANAGE_BILLING"
ISSUE_REFUNDS = "ISSUE_REFUNDS"
# ── Moderation ──
APPROVE_CONTENT = "APPROVE_CONTENT"
MODERATE_REVIEWS = "MODERATE_REVIEWS"
VERIFY_PROVIDERS = "VERIFY_PROVIDERS"
# ──────────────────────────────────────────────
# ACTION-TO-ROLE MAPPING
# Which roles are permitted to perform which actions
# ──────────────────────────────────────────────
# Actions that SUPERADMIN can always do (implicitly all)
# Actions that ADMIN can do within their scope
ADMIN_SCOPE_ACTIONS: Set[str] = {
AdminAction.EDIT_DATA,
AdminAction.VIEW_DATA,
AdminAction.DELETE_DATA,
AdminAction.EXPORT_DATA,
AdminAction.VIEW_SUBSCRIPTION,
AdminAction.EDIT_SUBSCRIPTION,
AdminAction.MANAGE_USERS,
AdminAction.SUSPEND_USERS,
AdminAction.BAN_USERS,
AdminAction.VIEW_FINANCIALS,
AdminAction.MANAGE_BILLING,
AdminAction.APPROVE_CONTENT,
AdminAction.MODERATE_REVIEWS,
AdminAction.VERIFY_PROVIDERS,
}
# MODERATOR can do within their scope
MODERATOR_SCOPE_ACTIONS: Set[str] = {
AdminAction.VIEW_DATA,
AdminAction.VIEW_SUBSCRIPTION,
AdminAction.APPROVE_CONTENT,
AdminAction.MODERATE_REVIEWS,
AdminAction.VERIFY_PROVIDERS,
AdminAction.SUSPEND_USERS,
}
# SALES_REP can do within their scope
SALES_REP_SCOPE_ACTIONS: Set[str] = {
AdminAction.VIEW_DATA,
AdminAction.VIEW_SUBSCRIPTION,
AdminAction.EDIT_SUBSCRIPTION,
AdminAction.VIEW_FINANCIALS,
AdminAction.EXPORT_DATA,
}
# SERVICE_MGR can do within their scope
SERVICE_MGR_SCOPE_ACTIONS: Set[str] = {
AdminAction.VIEW_DATA,
AdminAction.EDIT_DATA,
AdminAction.APPROVE_CONTENT,
AdminAction.VERIFY_PROVIDERS,
AdminAction.MODERATE_REVIEWS,
}
# ──────────────────────────────────────────────
# RBAC SERVICE
# ──────────────────────────────────────────────
class RBACService:
"""
Scope-Based Admin RBAC Service.
Provides methods to check whether a staff user has access
to perform a specific action on a target organization,
based on the user's role and assigned scope.
"""
# Mapping of role → set of permitted actions
ROLE_ACTIONS: Dict[UserRole, Set[str]] = {
UserRole.SUPERADMIN: set(AdminAction), # All actions
UserRole.ADMIN: ADMIN_SCOPE_ACTIONS,
UserRole.MODERATOR: MODERATOR_SCOPE_ACTIONS,
UserRole.SALES_REP: SALES_REP_SCOPE_ACTIONS,
UserRole.SERVICE_MGR: SERVICE_MGR_SCOPE_ACTIONS,
}
async def check_admin_access(
self,
db: AsyncSession,
user: User,
action: Union[str, AdminAction],
target_org_id: Optional[int] = None,
) -> bool:
"""
Check if a user has scope-based access to perform an action.
Args:
db: Database session
user: The staff user to check
action: The action being attempted (AdminAction enum or string)
target_org_id: Optional target organization ID for scope matching
Returns:
True if access is granted
Raises:
PermissionError: If access is denied (with details)
"""
action_str = action.value if isinstance(action, AdminAction) else action
# ── 1. SUPERADMIN bypass ──
if user.role == UserRole.SUPERADMIN:
return True
# ── 2. Check if the role is permitted for this action ──
permitted_actions = self.ROLE_ACTIONS.get(user.role, set())
if action_str not in permitted_actions:
raise PermissionError(
f"Action '{action_str}' is not permitted for role '{user.role.value}'."
)
# ── 3. If no target_org_id, just check action permission ──
if target_org_id is None:
return True
# ── 4. Scope-based organization access check ──
return await self._check_org_scope_access(db, user, target_org_id)
async def _check_org_scope_access(
self,
db: AsyncSession,
user: User,
target_org_id: int,
) -> bool:
"""
Check if the user's scope covers the target organization.
The user's scope is defined by:
- scope_type: "REGION", "SEGMENT", or "GLOBAL"
- scope_value: The specific value (e.g., "Pest_County", "Fleet")
The organization's scope attributes:
- region: Geographic region (e.g., "Pest_County")
- segment: Business segment (e.g., "Fleet")
"""
# Get user scope info
scope_type = getattr(user, 'scope_type', None)
scope_value = getattr(user, 'scope_value', None)
# GLOBAL scope → access to all organizations
if scope_type == ScopeType.GLOBAL.value:
return True
# No scope assigned → deny
if not scope_type or not scope_value:
raise PermissionError(
f"User {user.id} has no scope assigned. "
f"Contact a SUPERADMIN to assign a scope."
)
# Fetch the target organization
stmt = select(Organization).where(Organization.id == target_org_id)
result = await db.execute(stmt)
org = result.scalar_one_or_none()
if not org:
raise PermissionError(f"Organization {target_org_id} not found.")
# ── Scope matching logic ──
if scope_type == ScopeType.REGION.value:
# Check if org's region matches user's scope_value
org_region = getattr(org, 'region', None)
if not org_region:
raise PermissionError(
f"Organization {target_org_id} has no region assigned. "
f"Cannot match against REGION scope '{scope_value}'."
)
if org_region != scope_value:
raise PermissionError(
f"REGION scope mismatch: user scope='{scope_value}', "
f"org region='{org_region}'."
)
return True
elif scope_type == ScopeType.SEGMENT.value:
# Check if org's segment matches user's scope_value
org_segment = getattr(org, 'segment', None)
if not org_segment:
raise PermissionError(
f"Organization {target_org_id} has no segment assigned. "
f"Cannot match against SEGMENT scope '{scope_value}'."
)
if org_segment != scope_value:
raise PermissionError(
f"SEGMENT scope mismatch: user scope='{scope_value}', "
f"org segment='{org_segment}'."
)
return True
else:
raise PermissionError(f"Unknown scope type: '{scope_type}'.")
async def get_accessible_org_ids(
self,
db: AsyncSession,
user: User,
) -> Optional[List[int]]:
"""
Get list of organization IDs accessible by this user based on their scope.
Returns None for SUPERADMIN/GLOBAL scope (unrestricted).
Returns a list of matching org IDs for REGION/SEGMENT scope.
"""
# SUPERADMIN and GLOBAL scope → unrestricted
if user.role == UserRole.SUPERADMIN:
return None
scope_type = getattr(user, 'scope_type', None)
scope_value = getattr(user, 'scope_value', None)
if not scope_type or not scope_value:
return [] # No scope → no orgs
if scope_type == ScopeType.GLOBAL.value:
return None
# Build query based on scope type
if scope_type == ScopeType.REGION.value:
stmt = select(Organization.id).where(
Organization.region == scope_value,
Organization.is_deleted == False
)
elif scope_type == ScopeType.SEGMENT.value:
stmt = select(Organization.id).where(
Organization.segment == scope_value,
Organization.is_deleted == False
)
else:
return []
result = await db.execute(stmt)
return [row[0] for row in result.all()]
def get_permitted_actions(self, role: UserRole) -> Set[str]:
"""Get the set of actions permitted for a given role."""
return self.ROLE_ACTIONS.get(role, set())
def is_action_permitted(self, role: UserRole, action: Union[str, AdminAction]) -> bool:
"""Check if a specific action is permitted for a given role."""
action_str = action.value if isinstance(action, AdminAction) else action
return action_str in self.ROLE_ACTIONS.get(role, set())
# ──────────────────────────────────────────────
# Singleton instance
# ──────────────────────────────────────────────
rbac_service = RBACService()