jogosultsági szintek RBAC beállítva tesztelve

This commit is contained in:
Roo
2026-06-25 20:41:49 +00:00
parent 52011606ff
commit 36109fc722
242 changed files with 8672 additions and 2237 deletions

View File

@@ -247,10 +247,11 @@ class AuthService:
# Dinamikus szervezet generálás
org_full_name = org_tpl.format(last_name=p.last_name, first_name=p.first_name)
# Naming convention: "{last_name} {first_name} - Privát Garázs (#{user_id})"
org_full_name = f"{p.last_name} {p.first_name} - Privát Garázs (#{user.id})"
new_org = Organization(
full_name=org_full_name,
name=f"{p.last_name} Garázsa",
name=org_full_name,
folder_slug=generate_secure_slug(12),
org_type=OrgType.individual,
owner_id=user.id,

View File

@@ -31,7 +31,7 @@ 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
from app.models.system.rbac import SystemRolePermission, SystemPermission
logger = logging.getLogger(__name__)
@@ -43,88 +43,6 @@ class ScopeType(str, Enum):
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
# ──────────────────────────────────────────────
@@ -136,22 +54,17 @@ class RBACService:
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,
}
P0 Phase 6: All permission checks are now DB-driven via
system.role_permissions. The legacy AdminAction enum has been
removed. Use get_role_permissions() for all role lookups.
"""
async def check_admin_access(
self,
db: AsyncSession,
user: User,
action: Union[str, AdminAction],
action: str,
target_org_id: Optional[int] = None,
) -> bool:
"""
@@ -160,7 +73,7 @@ class RBACService:
Args:
db: Database session
user: The staff user to check
action: The action being attempted (AdminAction enum or string)
action: The action being attempted (string code)
target_org_id: Optional target organization ID for scope matching
Returns:
@@ -169,17 +82,15 @@ class RBACService:
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:
permitted_actions = await self.get_permitted_actions(db, user.role)
if action not in permitted_actions:
raise PermissionError(
f"Action '{action_str}' is not permitted for role '{user.role.value}'."
f"Action '{action}' is not permitted for role '{user.role.value}'."
)
# ── 3. If no target_org_id, just check action permission ──
@@ -304,14 +215,98 @@ class RBACService:
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())
async def get_permitted_actions(
self,
db: AsyncSession,
role: UserRole,
) -> Set[str]:
"""
Get the set of permission codes permitted for a given role.
P0 Phase 6: Fully DB-driven via system.role_permissions.
The role name is used to look up the system.roles.id, then
all granted permission codes are returned.
Args:
db: Database session
role: The UserRole enum value to look up
Returns:
Set of permission code strings (e.g., {"fleet:view", "user:create"})
"""
# Look up the system.roles.id by name
stmt = select(SystemRolePermission).limit(0) # just to get the table
# Use raw SQL for the join to avoid circular import issues
from sqlalchemy import text
result = await db.execute(
text("""
SELECT p.code
FROM system.role_permissions rp
JOIN system.permissions p ON p.id = rp.permission_id
JOIN system.roles r ON r.id = rp.role_id
WHERE r.name = :role_name AND rp.granted = TRUE
"""),
{"role_name": role.value},
)
return {row[0] for row in result.all()}
def is_action_permitted(self, role: UserRole, action: Union[str, AdminAction]) -> bool:
async def is_action_permitted(
self,
db: AsyncSession,
role: UserRole,
action: str,
) -> 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())
permitted = await self.get_permitted_actions(db, role)
return action in permitted
# ──────────────────────────────────────────────
# RBAC Phase 2: DB-Driven Permission Code Lookup
# ──────────────────────────────────────────────
async def get_role_permissions(
self,
db: AsyncSession,
role_id: int,
) -> Set[str]:
"""
Query the system.role_permissions table joined with system.permissions
to retrieve all granted permission codes for a given role_id.
Returns a set of permission code strings (e.g., {"fleet:view", "user:create"}).
Only permissions where granted == True are included.
Args:
db: Database session
role_id: The system.roles.id to look up
Returns:
Set of permission code strings
"""
stmt = (
select(SystemPermission.code)
.join(
SystemRolePermission,
SystemRolePermission.permission_id == SystemPermission.id,
)
.where(
SystemRolePermission.role_id == role_id,
SystemRolePermission.granted == True,
)
)
result = await db.execute(stmt)
return {row[0] for row in result.all()}
def invalidate_role_cache(self, role_id: int) -> None:
"""
Placeholder for cache invalidation when Redis is available.
Currently a no-op; structured so caching can be dropped in easily.
Args:
role_id: The role ID whose cache should be invalidated
"""
logger.debug(f"Cache invalidation requested for role_id={role_id} (no-op, Redis not configured)")
pass
# ──────────────────────────────────────────────