From 31defb7effdb478dd4df20617d35a3a0c1a1c7fd Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Fri, 19 Sep 2025 15:12:07 +0330 Subject: [PATCH] progress in permisssion --- .../repositories/business_permission_repo.py | 63 +++++ hesabixAPI/app/core/auth_dependency.py | 152 +++++++++- hesabixAPI/app/core/permissions.py | 130 +++++++++ hesabixAPI/app/core/permissions/__init__.py | 37 +++ hesabixAPI/docs/PERMISSIONS_SYSTEM.md | 259 ++++++++++++++++++ hesabixAPI/examples/permission_usage.py | 188 +++++++++++++ hesabixAPI/hesabix_api.egg-info/SOURCES.txt | 6 +- hesabixAPI/tests/test_permissions.py | 226 +++++++++++++++ 8 files changed, 1058 insertions(+), 3 deletions(-) create mode 100644 hesabixAPI/adapters/db/repositories/business_permission_repo.py create mode 100644 hesabixAPI/app/core/permissions.py create mode 100644 hesabixAPI/app/core/permissions/__init__.py create mode 100644 hesabixAPI/docs/PERMISSIONS_SYSTEM.md create mode 100644 hesabixAPI/examples/permission_usage.py create mode 100644 hesabixAPI/tests/test_permissions.py diff --git a/hesabixAPI/adapters/db/repositories/business_permission_repo.py b/hesabixAPI/adapters/db/repositories/business_permission_repo.py new file mode 100644 index 0000000..d5138da --- /dev/null +++ b/hesabixAPI/adapters/db/repositories/business_permission_repo.py @@ -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() diff --git a/hesabixAPI/app/core/auth_dependency.py b/hesabixAPI/app/core/auth_dependency.py index 7ee65ab..a360d23 100644 --- a/hesabixAPI/app/core/auth_dependency.py +++ b/hesabixAPI/app/core/auth_dependency.py @@ -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 ) diff --git a/hesabixAPI/app/core/permissions.py b/hesabixAPI/app/core/permissions.py new file mode 100644 index 0000000..a9d749e --- /dev/null +++ b/hesabixAPI/app/core/permissions.py @@ -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") diff --git a/hesabixAPI/app/core/permissions/__init__.py b/hesabixAPI/app/core/permissions/__init__.py new file mode 100644 index 0000000..5a046c5 --- /dev/null +++ b/hesabixAPI/app/core/permissions/__init__.py @@ -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", +] diff --git a/hesabixAPI/docs/PERMISSIONS_SYSTEM.md b/hesabixAPI/docs/PERMISSIONS_SYSTEM.md new file mode 100644 index 0000000..ce73de0 --- /dev/null +++ b/hesabixAPI/docs/PERMISSIONS_SYSTEM.md @@ -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 +} +``` diff --git a/hesabixAPI/examples/permission_usage.py b/hesabixAPI/examples/permission_usage.py new file mode 100644 index 0000000..e93d828 --- /dev/null +++ b/hesabixAPI/examples/permission_usage.py @@ -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 + } diff --git a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt index c2f0bee..31d998b 100644 --- a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt +++ b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt @@ -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 @@ -58,4 +61,5 @@ migrations/versions/20250117_000007_create_business_permissions_table.py migrations/versions/20250915_000001_init_auth_tables.py migrations/versions/20250916_000002_add_referral_fields.py tests/__init__.py -tests/test_health.py \ No newline at end of file +tests/test_health.py +tests/test_permissions.py \ No newline at end of file diff --git a/hesabixAPI/tests/test_permissions.py b/hesabixAPI/tests/test_permissions.py new file mode 100644 index 0000000..3b8d700 --- /dev/null +++ b/hesabixAPI/tests/test_permissions.py @@ -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__])