پرش به مطلب اصلی

محدوده فعالیت (Row Access)

قابلیت RowAccess در دراول برای کنترل دسترسی کاربران به رکوردهای دیتابیس در سطح سطر (Row-Level Access) طراحی شده است.
این قابلیت به شما اجازه می‌دهد دسترسی کاربران را بر اساس محدوده‌های مشخصی مثل استان، شهر، شعبه یا هر ساختار سازمانی مشابه، به‌صورت متمرکز و یکپارچه مدیریت کنید.

هدف اصلی RowAccess حذف شرط‌های تکراری در Queryها و اعمال خودکار محدودیت‌ها در سطح مدل است؛ به‌طوری که منطق دسترسی از کدهای پراکنده خارج شده و در یک نقطه مشخص کنترل شود.


پیش‌نیازها

تعریف موجودیت‌ها در فایل کانفیگ

در اولین قدم، باید موجودیت‌هایی که قرار است محدودسازی سطری روی آن‌ها انجام شود را در فایل کانفیگ معرفی کنید.

فایل: config/dornica-access-hub.php

return [
// ...

'row_access' => [
'entities' => [
[
[
'model' => Province::class,
'name' => 'province',
'label' => 'استان',
],
[
'model' => City::class,
'route_name' => 'api.admin.cities.index',
'name' => 'city',
'label' => 'شهر',
]
],
[
'model' => Branch::class,
'name' => 'branch',
'label' => 'شعبه',
],
// سایر موجودیت‌ها
],
],

// ...
];

هر مقدار در آرایه entities نمایانگر یک نوع محدوده دسترسی است که می‌تواند در مدل‌ها مورد استفاده قرار گیرد.

برای تعریف موجودیت‌ها برای فیلدهای select وابسته در فرم محدوده، آن‌ها را در آرایه‌های تو در تو (nested arrays) تعریف کنید. ترتیب موجودیت‌ها در آرایه تو در تو نشان‌دهنده رابطه وابستگی آن‌هاست: موجودیت اول والد است، موجودیت دوم به موجودیت اول وابسته است و به همین ترتیب (مثلاً شهرها به استان‌ها وابسته هستند). برای موجودیت‌های وابسته، می‌توانید route_name را برای مشخص کردن مسیر API که داده‌های آن‌ها را ارائه می‌دهد، تعریف کنید (مانند api.admin.cities.index برای شهرها). موجودیت‌هایی که وابستگی ندارند (مانند branch) باید به عنوان ورودی‌های جداگانه تعریف شوند، نه در داخل یک آرایه تو در تو.

توجه

اگر entities خالی باشد (مانند مثال زیر)، قابلیت RowAccess غیرفعال خواهد شد.

return [
'row_access' => [
'entities' => [],
],
];

همچنین قسمت محدوده فعالیت در مدیریت کاربران نیز نمایش داده نخواهد شد.


تعریف جداول واسط

برای تعریف جداول واسط بین کاربر و entity دستور

php artisan doravel:make-row-access-migrations

اجرا شود.


نحوه استفاده

1. افزودن Trait به مدل

در هر مدلی که نیاز به اعمال محدودیت سطری دارد، Trait زیر را اضافه کنید:

use Dornica\AccessHub\RowAccess\Traits\HasRowAccessPolicy;

class Order extends Model
{
use HasRowAccessPolicy;
}

این Trait مسئول اضافه کردن Scopeهای لازم به Queryهای مدل است و منطق RowAccess را به‌صورت خودکار اعمال می‌کند.


2. تعریف ثابت ROW_ACCESS در مدل

در مدل باید مشخص شود که هر موجودیت به کدام ستون از جدول دیتابیس نگاشت می‌شود. این نگاشت از طریق ثابت ROW_ACCESS انجام می‌شود:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Dornica\AccessHub\RowAccess\Traits\HasRowAccessPolicy;

class Order extends Model
{
use HasRowAccessPolicy;

const ROW_ACCESS = [
'province' => 'province_id',
'city' => 'city_id',
'branch' => 'branch_id',
];
}

در این مثال:

  • موجودیت province بر اساس ستون province_id محدود می‌شود.
  • موجودیت city بر اساس ستون city_id محدود می‌شود.
  • موجودیت branch بر اساس ستون branch_id محدود می‌شود.

رفتار پیش‌فرض در نبود دسترسی سطری

در صورتی که برای یک موجودیت مشخص، هیچ رکوردی از دسترسی سطری برای کاربر ثبت نشده باشد، سیستم فرض می‌کند که کاربر محدودیتی در آن موجودیت ندارد.

توجه

به بیان ساده‌تر، نبود داده‌ی دسترسی به معنی دسترسی کامل است، نه عدم دسترسی.

مثال عملی

فرض کنید موجودیتی به نام branch در سیستم تعریف شده است.

برای این موجودیت، جدولی مانند user_branches وجود دارد که دسترسی کاربران به شعب مختلف را نگهداری می‌کند.

رفتار سیستم به شکل زیر خواهد بود:

  • اگر کاربر هیچ رکوردی در جدول user_branches نداشته باشد → کاربر به تمام شعب دسترسی دارد.

  • اگر کاربر یک یا چند رکورد در جدول user_branches داشته باشد → کاربر فقط به شعب مشخص‌شده در همان جدول دسترسی خواهد داشت.

نکته

این رفتار پیش‌فرض برای کاهش پیچیدگی مدیریت دسترسی‌ها طراحی شده است. رکوردهای دسترسی فقط زمانی ایجاد می‌شوند که هدف، محدود کردن کاربر باشد؛ نه زمانی که دسترسی کامل مدنظر است.


نحوه عملکرد کلی

  1. کاربر وارد سیستم می‌شود.
  2. نقش و محدوده‌های مجاز او (مانند استان‌ها یا شعب مجاز) تعیین می‌شود.
  3. هنگام اجرای Query روی مدل، Trait مربوطه به‌صورت خودکار شرط‌های لازم را اعمال می‌کند.
  4. فقط رکوردهایی بازگردانده می‌شوند که با محدوده‌های مجاز کاربر هم‌خوانی دارند.

نکات مهم

1. تطابق نام موجودیت‌ها

کلیدهای تعریف‌شده در ثابت ROW_ACCESS باید دقیقاً با نام موجودیت‌های تعریف‌شده در فایل کانفیگ یکسان باشند.

اشتباه:

'provinces' => 'province_id'

صحیح:

'province' => 'province_id'

2. تخصیص دسترسی به کاربر

تعریف ROW_ACCESS به‌تنهایی کافی نیست. کاربر باید در سیستم احراز هویت، به مقادیر مورد نظر هر موجودیت دسترسی داشته باشد.

مثال:

  • اگر کاربر فقط به استان‌های [1, 3] دسترسی دارد
  • فقط رکوردهایی با شرط province_id IN (1, 3) برای او قابل مشاهده خواهند بود

3. استفاده در Queryهای سفارشی

RowAccess فقط روی Queryهایی اعمال می‌شود که از Eloquent Model استفاده می‌کنند. در Queryهای خام (DB::table) این محدودیت به‌صورت خودکار اعمال نخواهد شد.


4. استفاده در Queryهای دارای Relation

توجه

اگر مدل شما از طریق یک رابطه (Relation) به موجودیت دارای RowAccess متصل است، باید حتماً از whereHas استفاده کنید تا محدودیت‌های سطری روی مدل مرتبط نیز اعمال شود.

مثال نادرست (RowAccess روی Adviser اعمال نمی‌شود):

AdviserReferralBan::query()
->leftJoin('advisers', 'adviser_referral_bans.adviser_id', '=', 'advisers.id')
->get();

مثال صحیح (RowAccess روی Adviser اعمال می‌شود):

AdviserReferralBan::query()
->whereHas('adviser')
->leftJoin('advisers', 'adviser_referral_bans.adviser_id', '=', 'advisers.id')
->get();

با افزودن whereHas('adviser')، سیستم تضمین می‌کند که فقط رکوردهایی بازگردانده می‌شوند که مدل مرتبط (Adviser) نیز در محدوده دسترسی کاربر قرار دارد.

متدهای اصلی

  • RowAccess::ids(string $entity)
  • RowAccess::getScopeActivity(string $entity, int $roleUserId, int $userId)
  • RowAccess::updateScopeActivity(string $entity, int $roleUserId, int $userId, ?array $entityIds = [])

امکانات ارائه‌شده توسط RowAccess

شرحمتد
دریافت شناسه‌های مجاز بر اساس کاربر و نقش جاریids
دریافت شناسه‌های Scope برای یک کاربر/نقش مشخصgetScopeActivity
به‌روزرسانی Scope برای یک کاربر/نقش مشخصupdateScopeActivity
تنظیم کاربر Scope (کاربر لاگین‌شده یا کاربر ارسالی)user
تنظیم نقش کاربر ScoperoleUser
تنظیم نقش برای Reverse Lookup کاربرانrole
انتخاب موجودیت و شناسه‌ها برای Reverse Lookupprovinces / cities / ...
دریافت کاربران بر اساس نقش و موجودیت انتخاب‌شدهgetUsers
دریافت شناسه کشورهای قابل دسترسgetCountries
دریافت شناسه استان‌های قابل دسترسgetProvinces
دریافت شناسه شهرهای قابل دسترسgetCities
دریافت شناسه بخش‌های قابل دسترسgetDistricts
دریافت شناسه دهستان‌های قابل دسترسgetRuralDistricts
دریافت شناسه روستاهای قابل دسترسgetVillages
دریافت شناسه محله‌های قابل دسترسgetNeighborhoods

Builder Scope (بدون پاس دادن پارامتر به Getter)

برای اِعمال Scope باید از Builder استفاده کنید:

  • RowAccess::user($user) یا RowAccess::user()
  • RowAccess::roleUser($roleUser)

قواعد

  1. اگر user() و roleUser() را نزنید: متدهایی مثل RowAccess::getCities() همه شناسه‌ها را برمی‌گردانند.

  2. اگر Scope بگذارید: باید هر دو متد user(...) و roleUser(...) تنظیم شده باشند؛ در غیر این صورت خطا (RowAccessException) برمی‌گردد.

  3. Scope یک‌بارمصرف است: بعد از اجرای Getter، Scope داخلی پاک می‌شود.


Builder Reverse Lookup (کاربران بر اساس ناحیه)

برای یافتن کاربران از روی یک نقش و مجموعه‌ای از ناحیه‌ها، از Builder جدید استفاده کنید:

  • RowAccess::role($role)
  • یکی از selectorها:
  • countries([...])
  • provinces([...])
  • cities([...])
  • districts([...])
  • ruralDistricts([...])
  • villages([...])
  • neighborhoods([...])
  • در نهایت: getUsers()

مثال

use Dornica\AccessHub\RowAccess\Facades\RowAccess;

$userIds = RowAccess::role($roleId)
->provinces($provinceIds)
->getUsers();

$cityUserIds = RowAccess::role($roleId)
->cities($cityIds)
->getUsers();

قواعد

  1. role(...) اجباری است.
  2. یکی از selectorهای موجودیت اجباری است.
  3. اگر آرایه شناسه‌ها خالی باشد، خروجی [] است.
  4. این Scope هم یک‌بارمصرف است و بعد از getUsers() پاک می‌شود.

مثال‌ها

use Dornica\AccessHub\RowAccess\Facades\RowAccess;

// همه شهرها (بدون Scope)
$allCities = RowAccess::getCities();

// شهرهای مجاز کاربر لاگین‌شده با roleUser مشخص
$scopedCities = RowAccess::user()->roleUser($roleUser)->getCities();

// شهرهای مجاز یک کاربر مشخص
$targetUserCities = RowAccess::user($targetUser)->roleUser($targetRoleUser)->getCities();

// سایر موجودیت‌ها
$provinces = RowAccess::user($targetUser)->roleUser($targetRoleUser)->getProvinces();
$villages = RowAccess::user($targetUser)->roleUser($targetRoleUser)->getVillages();
$neighborhoods = RowAccess::user($targetUser)->roleUser($targetRoleUser)->getNeighborhoods();

جمع‌بندی

قابلیت RowAccess یک راه‌حل استاندارد، متمرکز و قابل توسعه برای کنترل دسترسی سطری در دراول است. با تعریف صحیح موجودیت‌ها، استفاده از Trait و نگاشت دقیق ROW_ACCESS، می‌توانید بدون افزودن پیچیدگی به کدها، امنیت داده‌ها را در سطح رکورد تضمین کنید.