محدوده فعالیت (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داشته باشد → کاربر فقط به شعب مشخصشده در همان جدول دسترسی خواهد داشت.
این رفتار پیشفرض برای کاهش پیچیدگی مدیریت دسترسیها طراحی شده است. رکوردهای دسترسی فقط زمانی ایجاد میشوند که هدف، محدود کردن کاربر باشد؛ نه زمانی که دسترسی کامل مدنظر است.
نحوه عملکرد کلی
- کاربر وارد سیستم میشود.
- نقش و محدودههای مجاز او (مانند استانها یا شعب مجاز) تعیین میشود.
- هنگام اجرای Query روی مدل، Trait مربوطه بهصورت خودکار شرطهای لازم را اعمال میکند.
- فقط رکوردهایی بازگردانده میشوند که با محدودههای مجاز کاربر هم خوانی دارند.
نکات مهم
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 |
| تنظیم نقش کاربر Scope | roleUser |
| تنظیم نقش برای Reverse Lookup کاربران | role |
| انتخاب موجودیت و شناسهها برای Reverse Lookup | provinces / cities / ... |
| دریافت کاربران بر اساس نقش و موجودیت انتخابشده | getUsers |
| دریافت شناسه کشورهای قابل دسترس | getCountries |
| دریافت شناسه استانهای قابل دسترس | getProvinces |
| دریافت شناسه شهرهای قابل دسترس | getCities |
| دریافت شناسه بخشهای قابل دسترس | getDistricts |
| دریافت شناسه دهستانها ی قابل دسترس | getRuralDistricts |
| دریافت شناسه روستاهای قابل دسترس | getVillages |
| دریافت شناسه محلههای قابل دسترس | getNeighborhoods |
Builder Scope (بدون پاس دادن پارامتر به Getter)
برای اِعمال Scope باید از Builder استفاده کنید:
RowAccess::user($user)یاRowAccess::user()RowAccess::roleUser($roleUser)
قواعد
-
اگر
user()وroleUser()را نزنید: متدهایی مثلRowAccess::getCities()همه شناسهها را برمیگردانند. -
اگر Scope بگذارید: باید هر دو متد
user(...)وroleUser(...)تنظیم شده باشند؛ در غیر این صورت خطا (RowAccessException) برمیگردد. -
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();
قواعد
role(...)اجباری است.- یکی از selectorهای موجودیت اجباری است.
- اگر آرایه شناسهها خالی باشد، خروجی
[]است. - این 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، میتوانید بدون افزودن پیچیدگی به کدها، امنیت دادهها را در سطح رکورد تضمین کنید.