- Added SecretaryAccessChecker to manage resource access for secretaries. - Integrated permission checks for payments, inventory, and tags in relevant controllers. - Updated PaymentController and PaymentMethodController to enforce secretary permissions. - Enhanced TenantTagController to check permissions for tag management actions. - Introduced tests for secretary resource enforcement, ensuring proper access control. - Updated DoctorSecretary entity to include inventory and tags permissions. - Created a comprehensive audit document for secretary permissions coverage and enforcement. - Fixed potential crashes in SecretaryDashboard when rendering without doctor data.
18 KiB
حسابرسی و رفع سیستم مجوز منشی: پوشش کامل + اعمال در پنل + خطای ورود/کرش
پروژه
clinicpro (backend Symfony + پنل ادمین React). تکپروژه، cross-repo نیست.
پیش از هر گرِپ/خواندن، طبق قانون پروژه اول
graphify query "..."بزن.
زمینه
سیستم منشی دو لایه دارد که فعلاً ناهماهنگاند:
-
تعریف مجوز: هر منشی یک ردیف
DoctorSecretaryدارد با ستون JSONpermission. ساختار پیشفرض درsrc/Secretary/Entity/DoctorSecretary.php::DEFAULT_PERMISSIONS:'resources' => [ 'appointments' => ['view','create','cancel','update_status'], 'patients' => ['view','create','update','delete'], 'payments' => ['view','create','update','delete'], 'insurances' => ['view','create','update','delete'], 'addresses' => ['view','create','update','delete'], 'clinic_info' => ['view','update'], ]UI افزودن/ویرایش منشی (
assets/admin/pages/MySecretariesPage.tsx→PERMISSION_SECTIONS) دقیقاً همین ۶ منبع را نمایش میدهد. -
اعمال مجوز: بررسی واقعی فقط با
src/Secretary/Security/SecretaryPermissionChecker.php::can()انجام میشود و این checker تنها درsrc/Appointment/Security/AppointmentAccessChecker.phpصدا زده میشود.
نتیجه: فقط منبع appointments واقعاً enforce میشود؛ بقیهٔ toggleها (patients/payments/insurances/addresses/clinic_info) ذخیره میشوند ولی هیچجا چک نمیشوند. علاوهبر این چند صفحه/قابلیت که منشی به آنها دسترسی دارد اصلاً toggle ندارند.
مشکل / هدف
سه خواسته، به ترتیب اولویت:
- پوشش کامل مجوزها (Coverage): هر صفحه/ماژول/قابلیتی که منشی میتواند به آن دسترسی داشته باشد، باید یک toggle مجوز در بخش «مجوزهای دسترسی» فرم افزودن/ویرایش منشی داشته باشد. هیچ قابلیتی نباید خارج از این ماتریس باقی بماند.
- اعمال واقعی (Enforcement): هر toggle باید هم در API (۴۰۳ اگر مجوز نبود) و هم در پنل (پنهانکردن صفحه/دکمه اگر مجوز نبود) اثر کند. هیچ دسترسیای خارج از سیستم Permission نباشد.
- رفع خطای ورود/کرش منشی: رفع ۴۰۱ روی
/api/v1/dashboard/secretaryو/api/v1/subscription/myو کرش فرانتاندCannot read properties of undefined (reading 'name').
فایلهای مرتبط
| فایل | نقش |
|---|---|
src/Secretary/Entity/DoctorSecretary.php |
تعریف DEFAULT_PERMISSIONS + merge/مدل مجوز |
src/Secretary/Security/SecretaryPermissionChecker.php |
تنها checker مجوز منشی (can, canAll) |
src/Secretary/Service/SecretaryService.php |
ساخت/تخصیص منشی (resolveSecretaryUser) — رمز عبور نمیسازد |
src/Secretary/Controller/SecretaryController.php |
endpointهای CRUD منشی (owner میسازد/ویرایش میکند) |
src/Appointment/Security/AppointmentAccessChecker.php |
تنها جای فعلی که SecretaryPermissionChecker صدا زده میشود |
src/Patient/Security/PatientRecordScopeResolver.php |
scope بیماران منشی — فقط بر اساس پزشکِ تخصیصیافته، بدون چک patients permission |
src/Dashboard/Controller/DashboardController.php |
secretary() (L531)، secretaryDoctorDashboard() (L560)، secretaryClinicDashboard() (L615) |
src/Subscription/Controller/SubscriptionController.php |
my() (L55) با IsGranted('IS_AUTHENTICATED_FULLY') |
src/Inventory/Controller/InventoryController.php |
ROLE_SECRETARY دارد ولی toggle ندارد |
src/Tag/Controller/TenantTagController.php |
ROLE_SECRETARY دارد ولی toggle ندارد |
assets/admin/pages/MySecretariesPage.tsx |
فرم افزودن/ویرایش + PERMISSION_SECTIONS + EMPTY_PERMISSIONS |
assets/admin/pages/DashboardPage.tsx |
SecretaryDashboard() (L878) — محل کرش .name |
assets/admin/App.tsx |
جدول route؛ گِیتکردن صفحات منشی |
assets/admin/stores/authStore.ts |
context.permissions، primaryRole، availableContexts |
src/Auth/Controller/AuthController.php |
userinfo (L~588)، buildAvailableContexts (L697) — permissions منشی داخل context |
config/packages/security.yaml |
firewall api (jwt) + access_control |
وضعیت فعلی (واقعیتهای تأییدشده)
الف) ماتریس فعلی و شکاف پوشش
ROLE_SECRETARY در این کنترلرها ظاهر میشود:
MyAppointmentsController, PaymentMethodController, SecretaryController, AdminApiController, DashboardController, SubscriptionController, Inventory/InventoryController, Tag/TenantTagController, و scope در Patient/PatientRecordScopeResolver.
اما toggle فقط برای ۶ منبع appointments/patients/payments/insurances/addresses/clinic_info وجود دارد.
→ شکاف پوشش: inventory (انبار) و tags (تگها) — و هر ماژول دیگری که در ممیزی پیدا شد (services/sms/staff/settlement اگر منشی دسترسی دارد) — toggle ندارند.
ب) شکاف اعمال (Enforcement gap)
SecretaryPermissionChecker::can() فقط از AppointmentAccessChecker صدا زده میشود:
// src/Appointment/Security/AppointmentAccessChecker.php (تنها مصرفکننده)
return $relation !== null && $this->secretaryPermissions->can($relation, self::RESOURCE, $action);
در PatientRecordScopeResolver::forSecretary() هیچ چکی روی permissions['resources']['patients'] نیست — منشی با patients.view=false هم بیماران را میبیند:
// src/Patient/Security/PatientRecordScopeResolver.php:87
private function forSecretary(User $user): PatientRecordScope
{
// ... فقط scope بر اساس پزشکانِ تخصیصیافته؛ toggle مجوز اصلاً خوانده نمیشود
return PatientRecordScope::forClinicRestrictedToDoctors($clinic->getId(), $doctorIds);
}
منابع payments/insurances/addresses/clinic_info هم هیچ نقطهٔ enforcement مبتنیبر DoctorSecretary.permissions ندارند.
توجه: یک سیستم مجوز دوم و جدا برای پزشکِ عضو کلینیک وجود دارد:
src/Clinic/Security/ClinicDoctorPermissionChecker.php(can($user,$clinic,$resource,$action)) که درPatientRecordScopeResolver,InsuranceController,ClinicControllerاستفاده میشود. این برای پزشکان است نه منشی. هنگام طراحی enforcement منشی، الگوی این checker را دنبال کن ولی منبع حقیقت راDoctorSecretary.permissionsبگذار.
ج) خطای ۴۰۱ + کرش .name
واقعیت دیتابیس (تأییدشده): شماره ۰۹۱۵۰۰۰۰۰۰۱ = مالک کلینیک نمونه (users.id=2, roles ["ROLE_USER","ROLE_CLINIC"]) است، نه منشی؛ هیچ ردیف doctor_secretaries ندارد. منشیهای واقعی: 09150000021, 09150000041, 09150000042, 09150000043. پس فرض «منشی ۰۹۱۵۰۰۰۰۰۰۱» غلط است و باید با یک منشیِ واقعی (یا منشیِ تازهساخته از UI) بازتولید شود.
کرش .name (تأییدشده در کد): SecretaryDashboard فقط شکل «مطب پزشک» را میخواند:
// assets/admin/pages/DashboardPage.tsx
const d = useMemo(() => (q.data?.data as any)?.data ?? q.data?.data, [q.data]);
// L892 — اگر d موجود ولی stats نباشد:
value: formatNumber(d?.stats.today_appointments ?? 0),
// L904 و L913 — کرش وقتی منشیِ scope=clinic است و doctor وجود ندارد:
منشی {displayDoctorName(d?.doctor.name)}
<AvatarEl initials={(d?.doctor.name ?? 'D').slice(0, 1)} ... />
ولی برای منشیِ scope کلینیک، بکاند شکل متفاوت برمیگرداند (بدون کلید doctor):
// src/Dashboard/Controller/DashboardController.php:666 (secretaryClinicDashboard)
return $this->success([
'scope' => 'clinic',
'clinic' => ['uuid' => ..., 'name' => ...], // ← doctor وجود ندارد
'permissions' => $permissions,
'stats' => [...],
'today_appointments' => $todayAppts,
]);
→ d.doctor تعریفنشده است و d?.doctor.name (نه d?.doctor?.name) کرش میکند. همینطور d?.stats.today_appointments.
۴۰۱ (نیازمند تشخیص runtime): هر دو endpoint گارد نقش دارند (/dashboard/secretary→ROLE_SECRETARY، /subscription/my→IS_AUTHENTICATED_FULLY). چون /subscription/my فقط احراز هویت میخواهد، ۴۰۱ روی آن یعنی توکن پذیرفته نشده = مشکل Authentication نه Authorization. دو فرضیهٔ محتمل که باید runtime رد/تأیید شوند:
- منشیِ ساختهشده از UI رمز عبور ندارد (
SecretaryService::resolveSecretaryUserفقط اگرpasswordپاس داده شود ست میکند و فرم UI فیلد رمز ندارد) → با endpoint ورودِ رمزی (که تنها راه ورود staff است) اصلاً نمیتواند لاگین کند. - یا توکن صادر میشود ولی context/نقش سرِ درخواستها درست منتقل نمیشود.
وظایف
بعد از هر تغییر کد:
graphify update .(بعد از commit). هر تغییر endpoint → بهروزرسانیdocs/api/*در همان session. هر تغییر Entity →doctrine:migrations:diff+migrate. هیچ تسک بدون تست (موفق+خطا+مرزی) تمامشده نیست.
۰. بازتولید و تشخیص دقیق (اول این)
- یک منشیِ تازه از UI به «کلینیک نمونه» اضافه کن (با owner
09150000001لاگین شو؛ رمزش را ازclinicpro-QA accounts/create_test_users.phpبردار). دقت کن آیا فرم رمز عبور میگیرد یا نه. - با همان منشی تلاش به لاگین کن و مشخص کن ۴۰۱ در کدام مرحله است: خودِ
oauth/token(ورود)، یاoauth/userinfo، یا/dashboard/secretary. باcurl/driver مقدار HTTP و بدنه را ثبت کن. - نتیجه را صریح بنویس: ۴۰۱ بهخاطر «نبود رمز/عدماحراز» است یا «نبود مجوز». مسیر رفع را بر همین اساس انتخاب کن.
۱. پوشش کامل مجوزها
- ممیزی کن: هر مسیر/کنترلری که
ROLE_SECRETARYمیپذیرد یا منشی از طریق context به آن میرسد را فهرست کن (شروع ازgrep -rln ROLE_SECRETARY src/و بررسی صفحات پنل که منشی میبیند). - برای هر قابلیتی که toggle ندارد (حداقل
inventory,tags؛ و هرچه در ممیزی پیدا شد) یک منبع جدید به هر سه جای همزمان اضافه کن تا از هم نشکنند:DoctorSecretary::DEFAULT_PERMISSIONS['resources']EMPTY_PERMISSIONSوPERMISSION_SECTIONSدرMySecretariesPage.tsx- نوع
SecretaryPermissionsدرassets/admin/types/index.ts
- اگر قابلیتی نباید هرگز در دسترس منشی باشد، بهجای toggle،
ROLE_SECRETARYرا از آن کنترلر بردار و در پرامپت مستند کن چرا.
// نمونه افزودن منبع به DEFAULT_PERMISSIONS
'inventory' => ['view' => true, 'create' => false, 'update' => false, 'delete' => false],
'tags' => ['view' => true, 'create' => false, 'update' => false, 'delete' => false],
mergePermissionsعمیق merge میکند، پس منشیهای موجود با نبودِ کلید جدید نمیشکنند؛ اما یک migration دادهٔ اختیاری برای backfill کلیدهای جدید روی ردیفهای قدیمی در نظر بگیر (یا در زمان خواندن باDEFAULT_PERMISSIONSادغام کن — همان کاری کهgetPermissions()تا حدی میکند).
۲. اعمال واقعی مجوز (API + پنل)
- API: برای هر منبع، در نقطهٔ درست enforce کن با
SecretaryPermissionChecker::can($rel, $resource, $action). الگو را ازAppointmentAccessCheckerبگیر. حداقل:patients: درPatientRecordScopeResolver::forSecretary()اگرpatients.view=false→PatientRecordScope::unknown()(یا معادل «هیچ»)؛ و برای create/update/delete درPatientControllerگارد بگذار.payments,insurances,addresses,clinic_info: در کنترلرهای متناظر (بهازای هر اکشن) گارد بیفزا. اگر یک نقطهٔ مشترک (voter/checker سرویس) تمیزتر است، یکSecretaryAccessCheckerبساز تا SOLID رعایت شود و منطق تکرار نشود.- نبودِ مجوز → پاسخ ۴۰۳ استاندارد (
$this->error(ErrorCodes::ERR_FORBIDDEN_001, ...)یاAppException).
- پنل: صفحه/دکمهای که مجوزش نیست نباید رندر شود.
context.permissionsاز قبل درauthStoreهست (buildAvailableContextsآن را داخل context منشی میگذارد). یک helper مثلuseSecretaryCan(resource, action)بساز و درApp.tsx(گِیت route) و در صفحات (پنهانکردن اکشن) استفاده کن. ازFeatureGateموجود اگر مناسب بود بهره ببر. - مطمئن شو منشیِ بدون مجوز یک منبع، نه صفحه را میبیند نه میتواند API را صدا بزند (تست هر دو لایه).
۳. رفع کرش داشبورد منشی
SecretaryDashboard را طوری بازنویسی کن که هر دو scope را بپذیرد و هرگز روی undefined کرش نکند:
interface SecretaryDashboardData {
scope: 'doctor' | 'clinic';
doctor?: { uuid: string; name: string; degree: string | null };
clinic?: { uuid: string; name: string };
permissions: Record<string, unknown>;
stats: { today_appointments: number; tomorrow_appointments: number };
today_appointments: ApptRow[];
}
// optional chaining کامل روی همهجا:
const scopeName =
d?.scope === 'clinic' ? d?.clinic?.name : displayDoctorName(d?.doctor?.name);
value: formatNumber(d?.stats?.today_appointments ?? 0),
initials={(scopeName ?? 'D').slice(0, 1)}
اگر q.isError بود، بهجای رندر داده، یک پیام خطای مناسب فارسی نشان بده (نه صفحهٔ سفید).
۴. رفع ۴۰۱ ورود منشی
بر اساس تشخیص وظیفهٔ ۰:
- اگر علت نبود رمز عبور است: در جریان افزودن منشی (
SecretaryService/SecretaryController/فرمMySecretariesPage) یک راه ورود فراهم کن — یا فیلد رمز در فرم، یا اجازهٔ ورود منشی با OTP (user/otp-login) مثل کاربر عادی، یا لینک set-password در پیامک خوشآمد. تصمیم را مستند کن. - اگر علت عدم انتقال نقش/توکن است: firewall
api(jwt) وaccess_controlرا بررسی کن و نقطهٔ رد شدن توکن را رفع کن. - در فرانتاند: اگر یک endpoint برای منشی مجاز نیست، اصلاً صدا زده نشود (بر اساس
primaryRole/permissions شرطی کن — مثلuseSubscriptionکه نباید برای منشیِ بدون دسترسی اشتراک، ۴۰۱ بگیرد و کرش کند).
۵. تستها
- بکاند (
ddev exec php bin/phpunit): برای هر منبع، تست منشیِ مجاز (۲۰۰) و غیرمجاز (۴۰۳)؛ تست scope بیمار باpatients.view=false. - فرانتاند (
yarn test):SecretaryDashboardبا payload کلینیک (بدونdoctor)، با payload پزشک، و با حالت خطا — بدون کرش. - دستی: با منشیِ واقعی لاگین، تأیید نبودِ ۴۰۱ و نبودِ کرش، و اعمالشدن toggleها.
نکات مهم
- منبع حقیقت مجوز منشی =
DoctorSecretary.permissionsJSON. enforcement جدید باید همین را بخواند، نه سیستمClinicDoctorPermission(که مال پزشک است). - هر تغییر در سهگانهٔ (Entity default / UI sections / TS type) باید همزمان باشد وگرنه merge/نمایش میشکند.
mergePermissionsفقط کلیدهای ارسالی را بهروز میکند؛ حذف toggle از UI داده را پاک نمیکند.- تاریخها Unix timestamp؛ پاسخها با
$this->success()/$this->error()؛ لیستها array-hydration. - رشتههای UI فارسی، RTL، تاریخ شمسی.
- بعد از هر تغییر API، فایل مربوط در
docs/api/(secretary.md,dashboard.md,patient.md,subscription.md, ...) بهروز شود. - SOLID: اگر enforcement در چند کنترلر تکرار شد، یک سرویس/voter مشترک بساز.
- کد/کامیت/مستندات انگلیسی؛ گفتوگو فارسی. اول spec انگلیسی و تأیید فارسی برداشت، بعد پیادهسازی.