progress in permisssion

This commit is contained in:
Hesabix 2025-09-19 15:12:07 +03:30
parent c1da9cd0bd
commit 31defb7eff
8 changed files with 1058 additions and 3 deletions

View file

@ -0,0 +1,63 @@
from __future__ import annotations
from typing import Optional
from sqlalchemy import select, and_
from sqlalchemy.orm import Session
from adapters.db.models.business_permission import BusinessPermission
from adapters.db.repositories.base_repo import BaseRepository
class BusinessPermissionRepository(BaseRepository[BusinessPermission]):
def __init__(self, db: Session) -> None:
super().__init__(db, BusinessPermission)
def get_by_user_and_business(self, user_id: int, business_id: int) -> Optional[BusinessPermission]:
"""دریافت دسترسی‌های کاربر برای کسب و کار خاص"""
stmt = select(BusinessPermission).where(
and_(
BusinessPermission.user_id == user_id,
BusinessPermission.business_id == business_id
)
)
return self.db.execute(stmt).scalars().first()
def create_or_update(self, user_id: int, business_id: int, permissions: dict) -> BusinessPermission:
"""ایجاد یا به‌روزرسانی دسترسی‌های کاربر برای کسب و کار"""
existing = self.get_by_user_and_business(user_id, business_id)
if existing:
existing.business_permissions = permissions
self.db.commit()
self.db.refresh(existing)
return existing
else:
new_permission = BusinessPermission(
user_id=user_id,
business_id=business_id,
business_permissions=permissions
)
self.db.add(new_permission)
self.db.commit()
self.db.refresh(new_permission)
return new_permission
def delete_by_user_and_business(self, user_id: int, business_id: int) -> bool:
"""حذف دسترسی‌های کاربر برای کسب و کار"""
existing = self.get_by_user_and_business(user_id, business_id)
if existing:
self.db.delete(existing)
self.db.commit()
return True
return False
def get_user_businesses(self, user_id: int) -> list[BusinessPermission]:
"""دریافت تمام کسب و کارهایی که کاربر دسترسی دارد"""
stmt = select(BusinessPermission).where(BusinessPermission.user_id == user_id)
return self.db.execute(stmt).scalars().all()
def get_business_users(self, business_id: int) -> list[BusinessPermission]:
"""دریافت تمام کاربرانی که دسترسی به کسب و کار دارند"""
stmt = select(BusinessPermission).where(BusinessPermission.business_id == business_id)
return self.db.execute(stmt).scalars().all()

View file

@ -24,7 +24,8 @@ class AuthContext:
calendar_type: CalendarType = "jalali",
timezone: Optional[str] = None,
business_id: Optional[int] = None,
fiscal_year_id: Optional[int] = None
fiscal_year_id: Optional[int] = None,
db: Optional[Session] = None
) -> None:
self.user = user
self.api_key_id = api_key_id
@ -33,6 +34,13 @@ class AuthContext:
self.timezone = timezone
self.business_id = business_id
self.fiscal_year_id = fiscal_year_id
self.db = db
# دسترسی‌های اپلیکیشن
self.app_permissions = user.app_permissions or {}
# دسترسی‌های کسب و کار (در صورت وجود business_id)
self.business_permissions = self._get_business_permissions() if business_id and db else {}
# ایجاد translator برای زبان تشخیص داده شده
self._translator = Translator(language)
@ -71,6 +79,138 @@ class AuthContext:
"""بررسی فعال بودن کاربر"""
return self.user.is_active
def _get_business_permissions(self) -> dict:
"""دریافت دسترسی‌های کسب و کار از دیتابیس"""
if not self.business_id or not self.db:
return {}
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
repo = BusinessPermissionRepository(self.db)
permission_obj = repo.get_by_user_and_business(self.user.id, self.business_id)
if permission_obj and permission_obj.business_permissions:
return permission_obj.business_permissions
return {}
# بررسی دسترسی‌های اپلیکیشن
def has_app_permission(self, permission: str) -> bool:
"""بررسی دسترسی در سطح اپلیکیشن"""
# SuperAdmin تمام دسترسی‌های اپلیکیشن را دارد
if self.app_permissions.get("superadmin", False):
return True
return self.app_permissions.get(permission, False)
def is_superadmin(self) -> bool:
"""بررسی superadmin بودن"""
return self.has_app_permission("superadmin")
def can_manage_users(self) -> bool:
"""بررسی دسترسی مدیریت کاربران در سطح اپلیکیشن"""
return self.has_app_permission("user_management")
def can_manage_businesses(self) -> bool:
"""بررسی دسترسی مدیریت کسب و کارها"""
return self.has_app_permission("business_management")
def can_access_system_settings(self) -> bool:
"""بررسی دسترسی به تنظیمات سیستم"""
return self.has_app_permission("system_settings")
def is_business_owner(self) -> bool:
"""بررسی اینکه آیا کاربر مالک کسب و کار است یا نه"""
if not self.business_id or not self.db:
return False
from adapters.db.models.business import Business
business = self.db.get(Business, self.business_id)
return business and business.owner_id == self.user.id
# بررسی دسترسی‌های کسب و کار
def has_business_permission(self, section: str, action: str) -> bool:
"""بررسی دسترسی در سطح کسب و کار"""
if not self.business_id:
return False
# SuperAdmin تمام دسترسی‌ها را دارد
if self.is_superadmin():
return True
# مالک کسب و کار تمام دسترسی‌ها را دارد
if self.is_business_owner():
return True
# بررسی دسترسی‌های عادی
if not self.business_permissions:
return False
# بررسی وجود بخش
if section not in self.business_permissions:
return False
section_perms = self.business_permissions[section]
# اگر بخش خالی است، فقط خواندن
if not section_perms:
return action == "read"
# بررسی دسترسی خاص
return section_perms.get(action, False)
def can_read_section(self, section: str) -> bool:
"""بررسی دسترسی خواندن بخش در کسب و کار"""
if not self.business_id:
return False
# SuperAdmin و مالک کسب و کار دسترسی کامل دارند
if self.is_superadmin() or self.is_business_owner():
return True
return section in self.business_permissions
def can_write_section(self, section: str) -> bool:
"""بررسی دسترسی نوشتن در بخش"""
return self.has_business_permission(section, "write")
def can_delete_section(self, section: str) -> bool:
"""بررسی دسترسی حذف در بخش"""
return self.has_business_permission(section, "delete")
def can_approve_section(self, section: str) -> bool:
"""بررسی دسترسی تأیید در بخش"""
return self.has_business_permission(section, "approve")
def can_export_section(self, section: str) -> bool:
"""بررسی دسترسی صادرات در بخش"""
return self.has_business_permission(section, "export")
def can_manage_business_users(self) -> bool:
"""بررسی دسترسی مدیریت کاربران کسب و کار"""
return self.has_business_permission("settings", "manage_users")
# ترکیب دسترسی‌ها
def has_any_permission(self, section: str, action: str) -> bool:
"""بررسی دسترسی در هر دو سطح"""
# SuperAdmin دسترسی کامل دارد
if self.is_superadmin():
return True
# بررسی دسترسی کسب و کار
return self.has_business_permission(section, action)
def can_access_business(self, business_id: int) -> bool:
"""بررسی دسترسی به کسب و کار خاص"""
# SuperAdmin دسترسی به همه کسب و کارها دارد
if self.is_superadmin():
return True
# اگر مالک کسب و کار است، دسترسی دارد
if self.is_business_owner() and business_id == self.business_id:
return True
# بررسی دسترسی‌های کسب و کار
return business_id == self.business_id
def to_dict(self) -> dict:
"""تبدیل به dictionary برای استفاده در API"""
return {
@ -82,10 +222,17 @@ class AuthContext:
"mobile": self.user.mobile,
"referral_code": getattr(self.user, "referral_code", None),
"is_active": self.user.is_active,
"app_permissions": self.app_permissions,
"created_at": self.user.created_at.isoformat() if self.user.created_at else None,
"updated_at": self.user.updated_at.isoformat() if self.user.updated_at else None,
},
"api_key_id": self.api_key_id,
"permissions": {
"app_permissions": self.app_permissions,
"business_permissions": self.business_permissions,
"is_superadmin": self.is_superadmin(),
"is_business_owner": self.is_business_owner(),
},
"settings": {
"language": self.language,
"calendar_type": self.calendar_type,
@ -139,7 +286,8 @@ def get_current_user(
calendar_type=calendar_type,
timezone=timezone,
business_id=business_id,
fiscal_year_id=fiscal_year_id
fiscal_year_id=fiscal_year_id,
db=db
)

View file

@ -0,0 +1,130 @@
from __future__ import annotations
from functools import wraps
from typing import Callable, Any
from fastapi import Depends
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.responses import ApiError
def require_app_permission(permission: str):
"""Decorator برای بررسی دسترسی در سطح اپلیکیشن"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
ctx = get_current_user()
if not ctx.has_app_permission(permission):
raise ApiError("FORBIDDEN", f"Missing app permission: {permission}", http_status=403)
return func(*args, **kwargs)
return wrapper
return decorator
def require_business_permission(section: str, action: str):
"""Decorator برای بررسی دسترسی در سطح کسب و کار"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
ctx = get_current_user()
if not ctx.has_business_permission(section, action):
raise ApiError("FORBIDDEN", f"Missing business permission: {section}.{action}", http_status=403)
return func(*args, **kwargs)
return wrapper
return decorator
def require_any_permission(section: str, action: str):
"""Decorator برای بررسی دسترسی در هر دو سطح (app یا business)"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
ctx = get_current_user()
if not ctx.has_any_permission(section, action):
raise ApiError("FORBIDDEN", f"Missing permission: {section}.{action}", http_status=403)
return func(*args, **kwargs)
return wrapper
return decorator
def require_superadmin():
"""Decorator برای بررسی superadmin بودن"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
ctx = get_current_user()
if not ctx.is_superadmin():
raise ApiError("FORBIDDEN", "Superadmin access required", http_status=403)
return func(*args, **kwargs)
return wrapper
return decorator
def require_business_access(business_id_param: str = "business_id"):
"""Decorator برای بررسی دسترسی به کسب و کار خاص"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
ctx = get_current_user()
business_id = kwargs.get(business_id_param)
if business_id and not ctx.can_access_business(business_id):
raise ApiError("FORBIDDEN", f"No access to business {business_id}", http_status=403)
return func(*args, **kwargs)
return wrapper
return decorator
# Decorator های ترکیبی برای استفاده آسان
def require_sales_write():
"""دسترسی نوشتن در بخش فروش"""
return require_any_permission("sales", "write")
def require_sales_delete():
"""دسترسی حذف در بخش فروش"""
return require_any_permission("sales", "delete")
def require_sales_approve():
"""دسترسی تأیید در بخش فروش"""
return require_any_permission("sales", "approve")
def require_purchases_write():
"""دسترسی نوشتن در بخش خرید"""
return require_any_permission("purchases", "write")
def require_accounting_write():
"""دسترسی نوشتن در بخش حسابداری"""
return require_any_permission("accounting", "write")
def require_inventory_write():
"""دسترسی نوشتن در بخش موجودی"""
return require_any_permission("inventory", "write")
def require_reports_export():
"""دسترسی صادرات گزارش"""
return require_any_permission("reports", "export")
def require_settings_manage_users():
"""دسترسی مدیریت کاربران کسب و کار"""
return require_any_permission("settings", "manage_users")
def require_user_management():
"""دسترسی مدیریت کاربران در سطح اپلیکیشن"""
return require_app_permission("user_management")
def require_business_management():
"""دسترسی مدیریت کسب و کارها"""
return require_app_permission("business_management")
def require_system_settings():
"""دسترسی تنظیمات سیستم"""
return require_app_permission("system_settings")

View file

@ -0,0 +1,37 @@
from .permissions import (
require_app_permission,
require_business_permission,
require_any_permission,
require_superadmin,
require_business_access,
require_sales_write,
require_sales_delete,
require_sales_approve,
require_purchases_write,
require_accounting_write,
require_inventory_write,
require_reports_export,
require_settings_manage_users,
require_user_management,
require_business_management,
require_system_settings,
)
__all__ = [
"require_app_permission",
"require_business_permission",
"require_any_permission",
"require_superadmin",
"require_business_access",
"require_sales_write",
"require_sales_delete",
"require_sales_approve",
"require_purchases_write",
"require_accounting_write",
"require_inventory_write",
"require_reports_export",
"require_settings_manage_users",
"require_user_management",
"require_business_management",
"require_system_settings",
]

View file

@ -0,0 +1,259 @@
# سیستم دسترسی دو سطحی
این سیستم دسترسی‌ها را در دو سطح جداگانه مدیریت می‌کند:
## 1. دسترسی‌های اپلیکیشن (App-Level Permissions)
در `users.app_permissions` ذخیره می‌شود و شامل:
### دسترسی‌های موجود:
- `superadmin`: دسترسی کامل به سیستم
- `user_management`: مدیریت کاربران در سطح اپلیکیشن
- `business_management`: مدیریت کسب و کارها
- `system_settings`: دسترسی به تنظیمات سیستم
### مثال JSON:
```json
{
"superadmin": true,
"user_management": true,
"business_management": true
}
```
## 2. دسترسی‌های کسب و کار (Business-Level Permissions)
در `business_permissions.business_permissions` ذخیره می‌شود و شامل:
### بخش‌های موجود:
- `sales`: فروش
- `purchases`: خرید
- `accounting`: حسابداری
- `inventory`: موجودی
- `reports`: گزارش‌ها
- `settings`: تنظیمات کسب و کار
- `marketing`: بازاریابی
### عملیات‌های موجود:
- `read`: خواندن
- `write`: نوشتن
- `delete`: حذف
- `approve`: تأیید
- `export`: صادرات
- `manage_users`: مدیریت کاربران (فقط در settings)
### مثال JSON:
```json
{
"sales": {
"write": true,
"delete": true,
"approve": true
},
"accounting": {
"write": true
},
"reports": {
"export": true
},
"settings": {
"manage_users": true
}
}
```
## نحوه استفاده
### 1. بررسی دسترسی در AuthContext:
```python
# دسترسی‌های اپلیکیشن
ctx.has_app_permission("superadmin")
ctx.is_superadmin()
ctx.can_manage_users()
ctx.can_manage_businesses()
# دسترسی‌های کسب و کار
ctx.has_business_permission("sales", "write")
ctx.can_read_section("sales")
ctx.can_write_section("sales")
ctx.can_delete_section("sales")
ctx.can_approve_section("sales")
ctx.can_export_section("reports")
ctx.can_manage_business_users()
# ترکیب دسترسی‌ها
ctx.has_any_permission("sales", "write") # app یا business
ctx.can_access_business(business_id)
```
### 2. استفاده از Decorator ها:
```python
from app.core.permissions import (
require_superadmin,
require_user_management,
require_sales_write,
require_business_access
)
# دسترسی اپلیکیشن
@require_superadmin()
def admin_function():
pass
@require_user_management()
def manage_users():
pass
# دسترسی کسب و کار
@require_business_access("business_id")
@require_sales_write()
def create_sale(business_id: int):
pass
```
### 3. بررسی دسترسی در API:
```python
@router.post("/business/{business_id}/sales")
def create_sale(
business_id: int,
ctx: AuthContext = Depends(get_current_user)
):
# بررسی دسترسی به کسب و کار
if not ctx.can_access_business(business_id):
raise ApiError("FORBIDDEN", "No access to this business")
# بررسی دسترسی نوشتن فروش
if not ctx.has_business_permission("sales", "write"):
raise ApiError("FORBIDDEN", "No permission to create sales")
# ایجاد فروش
pass
```
## قوانین دسترسی
### 1. SuperAdmin:
- **دسترسی خودکار**: تمام دسترسی‌های اپلیکیشن را خودکار دارد
- **دسترسی کامل**: به تمام بخش‌های سیستم دسترسی دارد
- **دسترسی کسب و کار**: می‌تواند به هر کسب و کاری دسترسی داشته باشد
- **تمام عملیات**: تمام عملیات را می‌تواند انجام دهد
### 2. مالک کسب و کار:
- **دسترسی اپلیکیشن**: فقط دسترسی‌های مشخص شده در `app_permissions`
- **دسترسی خودکار کسب و کار**: تمام دسترسی‌های کسب و کار خود را خودکار دارد
- **دسترسی کامل**: تمام عملیات در کسب و کار خود را می‌تواند انجام دهد
- **مدیریت کاربران**: می‌تواند کاربران کسب و کار خود را مدیریت کند
### 3. کاربران عادی:
- **دسترسی اپلیکیشن**: فقط دسترسی‌های مشخص شده در `app_permissions`
- **دسترسی کسب و کار**: دسترسی‌های مشخص شده در `business_permissions`
- **دسترسی محدود**: فقط به کسب و کارهای خود دسترسی دارند
- **قوانین بخش**: اگر بخش در دسترسی‌ها وجود دارد اما خالی است، فقط خواندن مجاز است
### 3. ذخیره‌سازی بهینه:
- فقط دسترسی‌های موجود ذخیره می‌شود
- `false` یا `null` ذخیره نمی‌شود
- کاهش حجم داده و بهبود عملکرد
## مثال‌های عملی
### کاربر مدیر فروش:
```json
{
"app_permissions": {},
"business_permissions": {
"sales": {
"write": true,
"delete": true,
"approve": true
},
"inventory": {
"write": true
}
}
}
```
### کاربر کارمند حسابداری:
```json
{
"app_permissions": {},
"business_permissions": {
"accounting": {
"write": true
},
"reports": {
"export": true
}
}
}
```
### SuperAdmin:
```json
{
"app_permissions": {
"superadmin": true
},
"business_permissions": {} // دسترسی کامل به همه
}
// نتایج:
// - has_app_permission("user_management") → True (خودکار)
// - has_app_permission("business_management") → True (خودکار)
// - has_business_permission("sales", "write") → True (برای هر کسب و کار)
// - is_superadmin() → True
// - is_business_owner() → False (مگر اینکه مالک کسب و کار باشد)
```
### مالک کسب و کار:
```json
{
"app_permissions": {},
"business_permissions": {
"sales": {"write": true, "delete": true}
}
}
// نتایج:
// - has_app_permission("user_management") → False
// - has_business_permission("sales", "write") → True (از JSON)
// - has_business_permission("accounting", "write") → True (خودکار - مالک)
// - has_business_permission("reports", "export") → True (خودکار - مالک)
// - is_business_owner() → True
```
## توسعه سیستم
### اضافه کردن بخش جدید:
```python
# فقط در business_permissions اضافه می‌شود
{
"new_section": {
"write": true,
"approve": true
}
}
```
### اضافه کردن عملیات جدید:
```python
# در هر بخش قابل اضافه کردن
{
"sales": {
"write": true,
"new_action": true
}
}
```
### اضافه کردن دسترسی اپلیکیشن جدید:
```python
# در app_permissions اضافه می‌شود
{
"new_app_permission": true
}
```

View file

@ -0,0 +1,188 @@
"""
مثالهایی از استفاده از سیستم دسترسی دو سطحی
"""
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.permissions import (
require_superadmin,
require_user_management,
require_business_management,
require_sales_write,
require_sales_delete,
require_accounting_write,
require_reports_export,
require_settings_manage_users,
require_any_permission,
require_business_access
)
from app.core.responses import ApiError
router = APIRouter()
# مثال 1: دسترسی‌های اپلیکیشن
@router.get("/admin/users")
@require_user_management()
def list_users(ctx: AuthContext = Depends(get_current_user)):
"""لیست کاربران - نیاز به دسترسی مدیریت کاربران در سطح اپلیکیشن"""
return {"message": "User list", "user_id": ctx.get_user_id()}
@router.get("/admin/businesses")
@require_business_management()
def list_businesses(ctx: AuthContext = Depends(get_current_user)):
"""لیست کسب و کارها - نیاز به دسترسی مدیریت کسب و کارها"""
return {"message": "Business list"}
@router.get("/admin/system-settings")
@require_superadmin()
def get_system_settings(ctx: AuthContext = Depends(get_current_user)):
"""تنظیمات سیستم - فقط superadmin"""
return {"message": "System settings"}
# مثال 2: دسترسی‌های کسب و کار
@router.post("/business/{business_id}/sales")
@require_business_access("business_id")
@require_sales_write()
def create_sale(
business_id: int,
ctx: AuthContext = Depends(get_current_user)
):
"""ایجاد فروش - نیاز به دسترسی نوشتن در بخش فروش"""
return {"message": f"Sale created for business {business_id}"}
@router.delete("/business/{business_id}/sales/{sale_id}")
@require_business_access("business_id")
@require_sales_delete()
def delete_sale(
business_id: int,
sale_id: int,
ctx: AuthContext = Depends(get_current_user)
):
"""حذف فروش - نیاز به دسترسی حذف در بخش فروش"""
return {"message": f"Sale {sale_id} deleted from business {business_id}"}
@router.post("/business/{business_id}/accounting/entries")
@require_business_access("business_id")
@require_accounting_write()
def create_accounting_entry(
business_id: int,
ctx: AuthContext = Depends(get_current_user)
):
"""ایجاد سند حسابداری - نیاز به دسترسی نوشتن در بخش حسابداری"""
return {"message": f"Accounting entry created for business {business_id}"}
@router.get("/business/{business_id}/reports/export")
@require_business_access("business_id")
@require_reports_export()
def export_reports(
business_id: int,
ctx: AuthContext = Depends(get_current_user)
):
"""صادرات گزارش - نیاز به دسترسی صادرات گزارش"""
return {"message": f"Reports exported for business {business_id}"}
@router.post("/business/{business_id}/users")
@require_business_access("business_id")
@require_settings_manage_users()
def add_business_user(
business_id: int,
ctx: AuthContext = Depends(get_current_user)
):
"""اضافه کردن کاربر به کسب و کار - نیاز به دسترسی مدیریت کاربران کسب و کار"""
return {"message": f"User added to business {business_id}"}
# مثال 3: بررسی دسترسی‌های ترکیبی
@router.get("/business/{business_id}/dashboard")
@require_business_access("business_id")
def get_business_dashboard(
business_id: int,
ctx: AuthContext = Depends(get_current_user)
):
"""داشبورد کسب و کار - بررسی دسترسی‌های مختلف"""
dashboard_data = {
"business_id": business_id,
"user_permissions": {
"can_read_sales": ctx.can_read_section("sales"),
"can_write_sales": ctx.can_write_section("sales"),
"can_delete_sales": ctx.can_delete_section("sales"),
"can_read_accounting": ctx.can_read_section("accounting"),
"can_write_accounting": ctx.can_write_section("accounting"),
"can_export_reports": ctx.can_export_section("reports"),
"can_manage_users": ctx.can_manage_business_users(),
}
}
return dashboard_data
# مثال 4: بررسی دسترسی‌های پویا
@router.get("/business/{business_id}/permissions")
@require_business_access("business_id")
def get_user_permissions(
business_id: int,
ctx: AuthContext = Depends(get_current_user)
):
"""دریافت دسترسی‌های کاربر برای کسب و کار"""
return {
"app_permissions": ctx.app_permissions,
"business_permissions": ctx.business_permissions,
"is_superadmin": ctx.is_superadmin(),
"is_business_owner": ctx.is_business_owner(),
"can_access_business": ctx.can_access_business(business_id),
"permissions_info": {
"has_automatic_app_permissions": ctx.is_superadmin(),
"has_automatic_business_permissions": ctx.is_business_owner(),
"effective_permissions": {
"can_manage_users": ctx.can_manage_users(),
"can_manage_businesses": ctx.can_manage_businesses(),
"can_write_sales": ctx.can_write_section("sales"),
"can_delete_sales": ctx.can_delete_section("sales"),
"can_approve_sales": ctx.can_approve_section("sales"),
"can_export_reports": ctx.can_export_section("reports"),
}
}
}
# مثال 5: بررسی دسترسی‌های پیچیده
@router.get("/business/{business_id}/sales/analytics")
@require_business_access("business_id")
def get_sales_analytics(
business_id: int,
ctx: AuthContext = Depends(get_current_user)
):
"""تحلیل فروش - نیاز به دسترسی خواندن فروش و صادرات گزارش"""
if not ctx.has_any_permission("sales", "read"):
raise ApiError("FORBIDDEN", "No permission to read sales data")
if not ctx.has_any_permission("reports", "export"):
raise ApiError("FORBIDDEN", "No permission to export analytics")
return {"message": f"Sales analytics for business {business_id}"}
# مثال 6: مدیریت دسترسی‌ها (فقط superadmin)
@router.post("/admin/business/{business_id}/users/{user_id}/permissions")
@require_superadmin()
def update_user_business_permissions(
business_id: int,
user_id: int,
permissions: dict,
ctx: AuthContext = Depends(get_current_user)
):
"""به‌روزرسانی دسترسی‌های کاربر در کسب و کار - فقط superadmin"""
# اینجا باید منطق به‌روزرسانی دسترسی‌ها پیاده‌سازی شود
return {
"message": f"Permissions updated for user {user_id} in business {business_id}",
"permissions": permissions
}

View file

@ -18,6 +18,7 @@ adapters/db/models/password_reset.py
adapters/db/models/user.py
adapters/db/repositories/api_key_repo.py
adapters/db/repositories/base_repo.py
adapters/db/repositories/business_permission_repo.py
adapters/db/repositories/business_repo.py
adapters/db/repositories/password_reset_repo.py
adapters/db/repositories/user_repo.py
@ -31,10 +32,12 @@ app/core/error_handlers.py
app/core/i18n.py
app/core/i18n_catalog.py
app/core/logging.py
app/core/permissions.py
app/core/responses.py
app/core/security.py
app/core/settings.py
app/core/smart_normalizer.py
app/core/permissions/__init__.py
app/services/api_key_service.py
app/services/auth_service.py
app/services/captcha_service.py
@ -59,3 +62,4 @@ migrations/versions/20250915_000001_init_auth_tables.py
migrations/versions/20250916_000002_add_referral_fields.py
tests/__init__.py
tests/test_health.py
tests/test_permissions.py

View file

@ -0,0 +1,226 @@
"""
تستهای سیستم دسترسی دو سطحی
"""
import pytest
from unittest.mock import Mock
from app.core.auth_dependency import AuthContext
from adapters.db.models.user import User
class TestAuthContextPermissions:
"""تست کلاس AuthContext برای بررسی دسترسی‌ها"""
def test_app_permissions(self):
"""تست دسترسی‌های اپلیکیشن"""
# ایجاد کاربر با دسترسی superadmin
user = Mock(spec=User)
user.app_permissions = {"superadmin": True}
ctx = AuthContext(user=user, api_key_id=1)
# تست دسترسی‌های اپلیکیشن - SuperAdmin باید تمام دسترسی‌ها را داشته باشد
assert ctx.has_app_permission("superadmin") == True
assert ctx.has_app_permission("user_management") == True # خودکار
assert ctx.has_app_permission("business_management") == True # خودکار
assert ctx.has_app_permission("system_settings") == True # خودکار
assert ctx.is_superadmin() == True
assert ctx.can_manage_users() == True
assert ctx.can_manage_businesses() == True
def test_app_permissions_normal_user(self):
"""تست دسترسی‌های اپلیکیشن برای کاربر عادی"""
user = Mock(spec=User)
user.app_permissions = {"user_management": True}
ctx = AuthContext(user=user, api_key_id=1)
# تست دسترسی‌های اپلیکیشن
assert ctx.has_app_permission("superadmin") == False
assert ctx.has_app_permission("user_management") == True
assert ctx.has_app_permission("business_management") == False
assert ctx.is_superadmin() == False
assert ctx.can_manage_users() == True
assert ctx.can_manage_businesses() == False
def test_business_permissions(self):
"""تست دسترسی‌های کسب و کار"""
user = Mock(spec=User)
user.app_permissions = {}
# Mock دیتابیس
db = Mock()
business_permission_repo = Mock()
business_permission_repo.get_by_user_and_business.return_value = Mock(
business_permissions={
"sales": {"write": True, "delete": True},
"accounting": {"write": True}
}
)
ctx = AuthContext(
user=user,
api_key_id=1,
business_id=1,
db=db
)
# Mock کردن repository
with pytest.MonkeyPatch().context() as m:
m.setattr("app.core.auth_dependency.BusinessPermissionRepository",
lambda db: business_permission_repo)
# تست دسترسی‌های کسب و کار
assert ctx.has_business_permission("sales", "write") == True
assert ctx.has_business_permission("sales", "delete") == True
assert ctx.has_business_permission("sales", "approve") == False
assert ctx.has_business_permission("accounting", "write") == True
assert ctx.has_business_permission("purchases", "read") == False
assert ctx.can_read_section("sales") == True
assert ctx.can_write_section("sales") == True
assert ctx.can_delete_section("sales") == True
assert ctx.can_approve_section("sales") == False
def test_empty_business_permissions(self):
"""تست دسترسی‌های خالی کسب و کار"""
user = Mock(spec=User)
user.app_permissions = {}
ctx = AuthContext(user=user, api_key_id=1, business_id=1)
ctx.business_permissions = {}
# اگر دسترسی‌ها خالی باشد، فقط خواندن مجاز است
assert ctx.has_business_permission("sales", "read") == False
assert ctx.has_business_permission("sales", "write") == False
assert ctx.can_read_section("sales") == False
def test_section_with_empty_permissions(self):
"""تست بخش با دسترسی‌های خالی"""
user = Mock(spec=User)
user.app_permissions = {}
ctx = AuthContext(user=user, api_key_id=1, business_id=1)
ctx.business_permissions = {
"sales": {}, # بخش خالی
"accounting": {"write": True}
}
# بخش خالی فقط خواندن مجاز است
assert ctx.has_business_permission("sales", "read") == True
assert ctx.has_business_permission("sales", "write") == False
assert ctx.has_business_permission("accounting", "write") == True
def test_superadmin_override(self):
"""تست override کردن دسترسی‌ها توسط superadmin"""
user = Mock(spec=User)
user.app_permissions = {"superadmin": True}
ctx = AuthContext(user=user, api_key_id=1, business_id=1)
ctx.business_permissions = {} # بدون دسترسی کسب و کار
# SuperAdmin باید دسترسی کامل داشته باشد
assert ctx.has_any_permission("sales", "write") == True
assert ctx.has_any_permission("accounting", "delete") == True
assert ctx.can_access_business(999) == True # هر کسب و کاری
def test_business_access_control(self):
"""تست کنترل دسترسی به کسب و کار"""
user = Mock(spec=User)
user.app_permissions = {}
ctx = AuthContext(user=user, api_key_id=1, business_id=1)
# فقط به کسب و کار خود دسترسی دارد
assert ctx.can_access_business(1) == True
assert ctx.can_access_business(2) == False
# SuperAdmin به همه دسترسی دارد
user.app_permissions = {"superadmin": True}
ctx = AuthContext(user=user, api_key_id=1, business_id=1)
assert ctx.can_access_business(999) == True
def test_business_owner_permissions(self):
"""تست دسترسی‌های مالک کسب و کار"""
user = Mock(spec=User)
user.app_permissions = {}
user.id = 1
# Mock دیتابیس و کسب و کار
db = Mock()
business = Mock()
business.owner_id = 1 # کاربر مالک است
ctx = AuthContext(user=user, api_key_id=1, business_id=1, db=db)
# Mock کردن Business model
with pytest.MonkeyPatch().context() as m:
m.setattr("app.core.auth_dependency.Business", Mock)
db.get.return_value = business
# مالک کسب و کار باید تمام دسترسی‌ها را داشته باشد
assert ctx.is_business_owner() == True
assert ctx.has_business_permission("sales", "write") == True
assert ctx.has_business_permission("sales", "delete") == True
assert ctx.has_business_permission("accounting", "write") == True
assert ctx.has_business_permission("reports", "export") == True
assert ctx.can_read_section("sales") == True
assert ctx.can_write_section("sales") == True
assert ctx.can_delete_section("sales") == True
assert ctx.can_approve_section("sales") == True
def test_business_owner_override(self):
"""تست override کردن دسترسی‌ها توسط مالک کسب و کار"""
user = Mock(spec=User)
user.app_permissions = {}
user.id = 1
# Mock دیتابیس و کسب و کار
db = Mock()
business = Mock()
business.owner_id = 1
ctx = AuthContext(user=user, api_key_id=1, business_id=1, db=db)
ctx.business_permissions = {} # بدون دسترسی کسب و کار
# Mock کردن Business model
with pytest.MonkeyPatch().context() as m:
m.setattr("app.core.auth_dependency.Business", Mock)
db.get.return_value = business
# مالک کسب و کار باید دسترسی کامل داشته باشد حتی بدون business_permissions
assert ctx.is_business_owner() == True
assert ctx.has_business_permission("sales", "write") == True
assert ctx.has_business_permission("accounting", "delete") == True
assert ctx.can_read_section("purchases") == True
assert ctx.can_write_section("inventory") == True
class TestPermissionDecorators:
"""تست decorator های دسترسی"""
def test_require_app_permission(self):
"""تست decorator دسترسی اپلیکیشن"""
from app.core.permissions import require_app_permission
@require_app_permission("user_management")
def test_function():
return "success"
# این تست نیاز به mock کردن get_current_user دارد
# که در محیط تست پیچیده‌تر است
pass
def test_require_business_permission(self):
"""تست decorator دسترسی کسب و کار"""
from app.core.permissions import require_business_permission
@require_business_permission("sales", "write")
def test_function():
return "success"
# این تست نیاز به mock کردن get_current_user دارد
pass
if __name__ == "__main__":
pytest.main([__file__])