jogosultsági szintek RBAC beállítva tesztelve
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user