feat: Implement permission gate for appointment and billing controllers
- Added PermissionGateTrait to manage access control for AppointmentPlanController and BillingController. - Introduced denyUnlessGrantedForPlanning method in AppointmentPlanController to handle specific permission checks for planning appointments. - Updated existing methods in both controllers to utilize the new permission checks. - Refactored ResourcePermissionTrait to use PermissionGateTrait for cleaner permission management. - Added tests to ensure proper permission enforcement across different scenarios, including cross-tenant access restrictions for staff.
This commit is contained in:
@@ -24,6 +24,28 @@
|
||||
|
||||
---
|
||||
|
||||
## مجوزها
|
||||
|
||||
از ۲۰۲۶-۰۸-۰۸:
|
||||
|
||||
- `GET /service-item/{uuid}/segments` → `services.view`
|
||||
- `PUT /service-item/{uuid}/segments` → `services.update`
|
||||
- `POST /appointment-plan/preview` → `services.view` **یا** `appointments.view`
|
||||
|
||||
منبعش `services` است نه `appointments`: بخشبندی یک خاصیتِ `ServiceItem` است و صفحهاش
|
||||
داخل کاتالوگ خدمات مینشیند. همان استدلالِ پروتکل درمان در آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
|
||||
`preview` استثناست و «یا» میگیرد، چون ورودیِ فرمِ ثبت نوبت است نه پیکربندیِ سرویس:
|
||||
منشیای که اجازهٔ ثبت نوبت دارد ولی کاتالوگ خدمات برایش بسته است، وگرنه نمیتوانست
|
||||
همان نوبتی را که مجاز است ثبت کند. قرینهٔ `ResourcePermissionTrait::denyUnlessGrantedForBooking`.
|
||||
|
||||
> **چرا اضافه شد:** این کنترلر دقیقاً همان شکلِ `TreatmentProtocolController` پیش از
|
||||
> رفعِ یافتهٔ ۱ آدیت را داشت — `#[IsGranted('IS_AUTHENTICATED_FULLY')]` سطحکلاس و یک
|
||||
> `requireItem()` که فقط مالکیتِ محیط را میسنجد. مالکیت مجوز نیست: عبور از آن فقط
|
||||
> ثابت میکند سرویس مالِ همین محیط است، نه اینکه این کاربر حق دستزدن به آن را دارد.
|
||||
|
||||
---
|
||||
|
||||
## `GET/PUT /api/v1/service-item/{uuid}/segments`
|
||||
|
||||
`PUT` جایگزینی کامل است. هر بخش:
|
||||
|
||||
+26
-8
@@ -524,7 +524,13 @@ The doctor dashboard filter bar uses the paginated form, defaulting `statuses` t
|
||||
|
||||
Get all appointments for the authenticated user.
|
||||
|
||||
**Permission:** `AUTH`
|
||||
**Permission:** `AUTH` — عمداً بدون مجوزِ رجیستری.
|
||||
|
||||
> این اندپوینت `a.user = خودِ کاربر` را میدهد، یعنی نوبتهای خودِ فرد **بهعنوان
|
||||
> بیمار**، نه دادهٔ محیط. مصرفکنندهاش داشبورد بیمار در `nobat724_front` است.
|
||||
> آدیت ۲۰۲۶-۰۸-۰۷ آن را در فهرست گَپها آورده بود؛ در ۲۰۲۶-۰۸-۰۸ مثبت کاذب تشخیص
|
||||
> داده شد: گِیتِ `appointments.view` یعنی منشیای که جایی بیمار است نوبتهای شخصیاش
|
||||
> را نبیند. در `ApiLeastPrivilegeTest::ALLOWED_200` با همین دلیل ثبت است.
|
||||
|
||||
### Query Parameters
|
||||
| Param | Type | Required | Description |
|
||||
@@ -780,6 +786,9 @@ Events are ordered oldest → newest. `data` is a flat array (single nesting). `
|
||||
|
||||
## POST `/api/v1/my/appointment`
|
||||
|
||||
**Permission:** `appointments.create` — علاوه بر بررسی نقش (`ROLE_DOCTOR`/`ROLE_CLINIC`/`ROLE_SECRETARY`/`ROLE_ADMIN`).
|
||||
تا پیش از ۲۰۲۶-۰۸-۰۸ فقط نقش بررسی میشد، پس منشیِ `appointments.create:false` هم نوبت ثبت میکرد.
|
||||
|
||||
Create a new appointment for a patient. Used by doctor/clinic/secretary to book appointments on behalf of patients. If no user exists with the given mobile, a new user account is created automatically.
|
||||
|
||||
> **Initial status is `pending` («ثبت شده»), not `confirmed`.** Every appointment —
|
||||
@@ -854,9 +863,12 @@ Create a new appointment for a patient. Used by doctor/clinic/secretary to book
|
||||
|
||||
جستجوی بیمار با شماره موبایل **یا** کد ملی، پیش از ثبت نوبت. فرم ثبت نوبت با یکی از این دو معیار جستجو میکند؛ اگر بیمار یافت شد و کد ملی دارد، مستقیم استفاده میشود، وگرنه بقیهٔ مشخصات (نام و موبایل یا کد ملی) از کاربر گرفته میشود.
|
||||
|
||||
**Auth:** `IS_AUTHENTICATED_FULLY` — Roles: `ROLE_DOCTOR`, `ROLE_CLINIC`, `ROLE_SECRETARY`, `ROLE_ADMIN`
|
||||
**Auth:** `IS_AUTHENTICATED_FULLY` — Roles: `ROLE_DOCTOR`, `ROLE_CLINIC`, `ROLE_SECRETARY`, `ROLE_ADMIN` — **Permission:** `appointments.create`
|
||||
|
||||
> برخلاف `GET /api/v1/patient/search-user`، این endpoint به فیچر `patient_records` اشتراک وابسته نیست و `ROLE_ADMIN` را هم میپذیرد، چون ثبت نوبت باید مستقل از اشتراک کار کند.
|
||||
>
|
||||
> مجوزش عمداً `appointments.create` است نه `patients.view`: بخشی از فرمِ ثبت نوبت است،
|
||||
> و با گیتِ پروندهٔ بیمار، منشیای که فقط اجازهٔ نوبتدهی دارد فرمش را از دست میداد.
|
||||
|
||||
### Query Parameters
|
||||
یکی از `mobile` یا `national_code` الزامی است. اگر هر دو ارسال شوند، `national_code` اولویت دارد.
|
||||
@@ -897,7 +909,10 @@ Create a new appointment for a patient. Used by doctor/clinic/secretary to book
|
||||
|
||||
Role-aware paginated list of appointments. Returns only what the authenticated user is authorized to see.
|
||||
|
||||
**Auth:** `IS_AUTHENTICATED_FULLY` (any role)
|
||||
**Auth:** `IS_AUTHENTICATED_FULLY` — **Permission:** `appointments.view`
|
||||
|
||||
> از ۲۰۲۶-۰۸-۰۸ گِیت دارد. پیش از آن منشیِ `appointments:false` با درخواست مستقیم به
|
||||
> API همان فهرستی را میگرفت که توگل، دکمهاش را در پنل پنهان کرده بود.
|
||||
|
||||
**Role behavior:**
|
||||
| Role | Scope |
|
||||
@@ -913,10 +928,13 @@ Role-aware paginated list of appointments. Returns only what the authenticated u
|
||||
Same scoping rules as the list above, aggregated into `{ total, completed, waiting, cancelled }`
|
||||
for one day (`?date=Y-m-d`, defaults to today).
|
||||
|
||||
**Auth:** `IS_AUTHENTICATED_FULLY`. A caller with no resolvable scope (clinic/doctor row
|
||||
missing, secretary without `appointments.view` or with no assigned doctors) gets all-zero
|
||||
counts rather than an unscoped, system-wide count. A plain patient gets counts over their
|
||||
own appointments only.
|
||||
**Auth:** `IS_AUTHENTICATED_FULLY` — **Permission:** `appointments.view`. A caller with no
|
||||
resolvable scope (clinic/doctor row missing, or no assigned doctors) gets all-zero counts
|
||||
rather than an unscoped, system-wide count. A plain patient gets counts over their own
|
||||
appointments only.
|
||||
|
||||
> پیش از ۲۰۲۶-۰۸-۰۸ منشیِ بدون `appointments.view` بهجای ۴۰۳ صفر میگرفت. صفرِ خاموش
|
||||
> با «اجازه نداری» یکی نیست؛ حالا ۴۰۳ میگیرد.
|
||||
|
||||
### Query Parameters
|
||||
| Param | Type | Default | Description |
|
||||
@@ -1399,7 +1417,7 @@ active — the request simply carried no `clinic_uuid`.
|
||||
|
||||
## GET /api/v1/my/clinic-doctors
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY`
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY` + `appointments.view`
|
||||
|
||||
پزشکانِ در دسترسِ کاربرِ پنل، برای ساختِ تبها/تایملاینِ صفحهٔ نوبتها. برخلاف
|
||||
`GET /api/v1/clinic/doctor-list/{clinicUuid}` که روی firewallِ عمومی است و **همهٔ** پزشکانِ
|
||||
|
||||
+27
-1
@@ -52,11 +52,37 @@ ddev exec php bin/console app:billing:backfill-claims # ساخت م
|
||||
|
||||
---
|
||||
|
||||
## مجوزها
|
||||
|
||||
از ۲۰۲۶-۰۸-۰۸ هر روتِ این کنترلر پیش از هر واکشی، مجوزِ `payments` را با
|
||||
`App\Shared\Controller\PermissionGateTrait` میسنجد. پیش از آن هیچ روتی گِیت مجوزی
|
||||
نداشت: منشیِ `payments:false` هم پرداختها را میدید، هم صورتحساب میساخت، هم وضعیت
|
||||
مطالبه را عوض میکرد. جزئیات در `docs/security/AUDIT-2026-08-07.md` یافتهٔ ۸.
|
||||
|
||||
منبعِ مجوز برای صورتحساب و مطالبه یکی است — `payments` — چون هر دو زیر همان توگلِ
|
||||
«مدیریت پرداختها»ی پنل نشستهاند و توگل جداگانهای ندارند.
|
||||
|
||||
- `payments.view` — خواندن: صورتحساب، فهرست پرداختها و خلاصهشان، فهرست مطالبات،
|
||||
مطالبات به تفکیک بیمار، گزارش بدهی بیمه، صورتحسابهای یک بیمار.
|
||||
- `payments.create` — ساخت: صورتحساب تازه، مطالبهٔ تازه.
|
||||
- `payments.update` — تغییر وضعیت: نهاییکردن صورتحساب، و `submit`/`approve`/`reject`/`pay`
|
||||
روی مطالبه.
|
||||
|
||||
گِیت پیش از `findByUuid()` مینشیند. ترتیب عمدی است: اگر بعدش بود، uuidِ ناموجود ۴۰۴
|
||||
میداد و همان تفاوت ۴۰۳/۴۰۴ به کاربرِ بیمجوز میگفت کدام uuid در این محیط وجود دارد.
|
||||
|
||||
نقشهای دیگر اثری نمیگیرند: هر دو checker برای ادمین، مالک کلینیک و پزشک مطب شخصی
|
||||
pass-through هستند و فقط منشی و پزشکِ عضوِ کلینیک را محدود میکنند.
|
||||
|
||||
خطای رد: `403` با `ERR_FORBIDDEN_001`.
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/billing/invoices
|
||||
|
||||
ساخت صورتحساب از یک مراجعه. اگر صورتحساب برای آن مراجعه قبلاً ساخته شده، همان برگردانده میشود (idempotent).
|
||||
|
||||
**Permission:** `AUTH` (مالک مراجعه)
|
||||
**Permission:** `payments.create` + مالکیت محیطِ مراجعه
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
|
||||
@@ -8,7 +8,15 @@
|
||||
|
||||
List all active doctor services.
|
||||
|
||||
**Permission:** `PUBLIC`
|
||||
**Permission:** `AUTH` — بدون مجوزِ رجیستری، و این عمدی است.
|
||||
|
||||
> سند تا ۲۰۲۶-۰۸-۰۸ اینجا `PUBLIC` نوشته بود که با رفتار نمیخواند: مسیر پشت firewall
|
||||
> است و درخواستِ بدون توکن `401` میگیرد.
|
||||
>
|
||||
> کاتالوگ سراسری است — `findActive()` بدون هیچ فیلترِ محیط. همردهٔ `specialties` و
|
||||
> `tags`. آدیت ۲۰۲۶-۰۸-۰۷ آن را گَپِ `services.view` دانسته بود؛ در ۲۰۲۶-۰۸-۰۸ مثبت
|
||||
> کاذب تشخیص داده شد: این فهرست dropdown فرمها را پر میکند، پس گِیتزدنش یک مجوز را
|
||||
> با نبودِ مجوزِ دیگری میشکند. در `ApiLeastPrivilegeTest::ALLOWED_200` ثبت است.
|
||||
|
||||
### Query Parameters
|
||||
| Param | Type | Required | Description |
|
||||
|
||||
@@ -41,7 +41,15 @@ hardcode نمیشود. ویزیت همیشه `outpatient` است.
|
||||
|
||||
List all active insurances.
|
||||
|
||||
**Permission:** `PUBLIC`
|
||||
**Permission:** `AUTH` — بدون مجوزِ رجیستری، و این عمدی است.
|
||||
|
||||
> سند تا ۲۰۲۶-۰۸-۰۸ اینجا `PUBLIC` نوشته بود که با رفتار نمیخواند: مسیر پشت firewall
|
||||
> است و درخواستِ بدون توکن `401` میگیرد.
|
||||
>
|
||||
> کاتالوگ سراسری بیمههاست — `findActive()` بدون فیلترِ محیط، جدا از قرارداد بیمهٔ
|
||||
> tenant (`TenantInsurance`) که مجوز خودش را دارد. گِیتزدنش با `insurances.view` فرمِ
|
||||
> ثبت بیمار را برای منشیِ دارای `patients.create` با کمبوی خالی میشکست. در
|
||||
> `ApiLeastPrivilegeTest::ALLOWED_200` با همین دلیل ثبت است.
|
||||
|
||||
### Query Parameters
|
||||
| Param | Type | Required | Description |
|
||||
|
||||
@@ -25,12 +25,17 @@ authz/headers/cors/inject) + پروبهای دستی با JWT واقعی هر
|
||||
| 5 | `dangerouslySetInnerHTML` روی بدنهٔ بلاگ در `BlogReviewPage` | 🟨 Medium | ✅ رفع شد (sanitize هنگام ذخیره) |
|
||||
| 6 | `APP_SECRET` واقعی در `.env.test` تحت git | 🟦 Low | ✅ رفع شد |
|
||||
| 7 | پسورد sandbox درگاه ملت هاردکد | ⬜ Info | ✅ رفع شد (به env منتقل شد) |
|
||||
| 8 | ۱۰ روت `GET` که مجوزِ رجیستریشان را enforce نمیکنند | 🟨 Medium | ⚠️ باز — با تست baseline مهار شد |
|
||||
| 8 | ۱۰ روت `GET` که مجوزِ رجیستریشان را enforce نمیکنند | 🟨 Medium | ✅ رفع شد — ۲۰۲۶-۰۸-۰۸ |
|
||||
| 9 | `AppointmentPlanController` هیچ گِیت مجوزی نداشت — دوقلوی یافتهٔ ۱ | 🟧 High | ✅ رفع شد — ۲۰۲۶-۰۸-۰۸ |
|
||||
| 10 | ۳۴ روت نوشتنی که گِیتشان **بعد از** واکشی رکورد است | 🟦 Low | ⚠️ باز — فهرست کامل زیر |
|
||||
|
||||
سیاست اولیه «فقط Critical/High رفع شود» بود؛ کاربر بعداً رفعِ همهٔ یافتههای باز را خواست، پس
|
||||
یافتههای ۲، ۳، ۵، ۶ و ۷ هم بسته شدند. یافتهٔ ۴ طبق تصمیم صریح خارج از محدوده ماند و یافتهٔ ۸
|
||||
حین همین کار کشف شد.
|
||||
|
||||
یافتههای ۹ و ۱۰ در جلسهٔ ۲۰۲۶-۰۸-۰۸ کشف شدند، حین بستنِ یافتهٔ ۸. شرحشان در بخش
|
||||
«پیگیری ۲۰۲۶-۰۸-۰۸» انتهای همین سند است.
|
||||
|
||||
```bash
|
||||
npm audit --omit=dev # قبل: high=2 moderate=62 بعد: high=0 moderate=3
|
||||
ddev exec php bin/phpunit # ۱۵۵۵ تست، ۴۸۳۲ assertion، سبز
|
||||
@@ -539,13 +544,156 @@ docs/api/treatment.md مجوز هر سه ا
|
||||
|
||||
## پیشنهادها
|
||||
|
||||
1. **بستنِ ده گَپِ یافتهٔ ۸** — با دیدنِ مصرفِ واقعیِ پنل تصمیم بگیر هر کدام کدام مجوز را بخواهد،
|
||||
بعد ردیفش را از `KNOWN_GAPS` بردار. تست دوم خودش یادآوری میکند.
|
||||
1. ~~**بستنِ ده گَپِ یافتهٔ ۸**~~ — انجام شد ۲۰۲۶-۰۸-۰۸. جزئیات پایین.
|
||||
2. **مهاجرت CKEditor** به پکیج umbrella `ckeditor5` v45+ — تنها راهِ بستنِ یافتهٔ ۴.
|
||||
3. **یک `ClinicStaff` در tenant دوم** — تا آدیت بعدی بتواند IDOR بیننقشیِ پرسنل را واقعاً بزند.
|
||||
4. **گسترشِ `ApiLeastPrivilegeTest` به روتهای نوشتنی** — الان فقط `GET` بدون path parameter را
|
||||
پوشش میدهد. `POST`/`PATCH`/`DELETE` بدنهٔ معتبر میخواهند، ولی همانها خطرناکترند.
|
||||
3. ~~**یک `ClinicStaff` در tenant دوم**~~ — انجام شد، ولی در fixture نه در DB زنده.
|
||||
4. ~~**گسترشِ `ApiLeastPrivilegeTest` به روتهای نوشتنی**~~ — انجام شد ۲۰۲۶-۰۸-۰۸.
|
||||
5. **پاکسازیِ ۱۷ خطای `phpstan`** — بدهیِ قدیمی، بیربط به امنیت، ولی مانعِ «سبز یعنی سبز» است.
|
||||
6. **بستنِ یافتهٔ ۱۰** — گِیت را در ۳۴ روت به بالای واکشی ببر. ~۱۵ تایشان object-scoped
|
||||
هستند و پیشچکِ منبعسطح میخواهند، پس تسک جدا لازم است.
|
||||
|
||||
---
|
||||
|
||||
# پیگیری ۲۰۲۶-۰۸-۰۸
|
||||
|
||||
جلسهٔ بعدی، با هدفِ بستنِ یافتهٔ ۸ و پیشنهادهای ۳ و ۴. دو یافتهٔ تازه حین کار پیدا شد.
|
||||
|
||||
## یافتهٔ ۸ — بسته شد، ولی سه ردیفش مثبت کاذب بود
|
||||
|
||||
فهرست دهتایی از نو با کد سنجیده شد. سه ردیف اصلاً گَپ نبودند:
|
||||
|
||||
| روت | چرا مثبت کاذب بود |
|
||||
|---|---|
|
||||
| `/api/v1/appointments/user` | `findByUser($user)` یعنی `a.user = خودِ کاربر`. نوبتهای خودِ فرد بهعنوان بیمار است، نه دادهٔ محیط. مصرفکنندهاش داشبورد بیمار در `nobat724_front` است؛ گِیتزدنش رگرسیون بود. |
|
||||
| `/api/v1/doctor-services` | `findActive()` بدون هیچ فیلترِ محیط — کاتالوگ سراسری، همردهٔ `specialties` و `tags`. |
|
||||
| `/api/v1/insurances` | همان. گِیتزدنش یک مجوز را با نبودِ مجوزِ دیگری میشکست: منشیِ دارای `patients.create` فرمِ ثبت بیمار را با کمبوی خالیِ بیمه میگرفت. |
|
||||
|
||||
درسِ روشی: خروجیِ پویشِ رفتاری lead است نه یافته — حتی وقتی پویش را خودِ آدیت نوشته باشد. سه
|
||||
ردیف بالا با «۲۰۰ داد پس نشت است» ثبت شده بودند، بیآنکه کوئریِ پشتشان خوانده شود.
|
||||
|
||||
هفت ردیف باقیمانده گَپ واقعی بودند و گِیت گرفتند. ولی چون هر سه کنترلر بیگِیت بودند و پویشِ
|
||||
قبلی فقط `GET` بدون path parameter را میدید، **کلِ روتهای نوشتنیشان هم باز بود**:
|
||||
|
||||
```
|
||||
sec (بدون هیچ مجوزی) | POST /api/v1/billing/invoices | صورتحساب میساخت
|
||||
sec (بدون هیچ مجوزی) | POST /api/v1/billing/claims/{uuid}/pay | مطالبه را «پرداختشده» میکرد
|
||||
sec (بدون هیچ مجوزی) | POST /api/v1/my/appointment | نوبت ثبت میکرد
|
||||
sec (بدون هیچ مجوزی) | GET /api/v1/my/appointment/patient-lookup | نام و کد ملی هر بیمار را میگرفت
|
||||
```
|
||||
|
||||
پس محدوده به همهٔ روتهای آن سه کنترلر باز شد: ۱۲ روت `BillingController` و ۵ روت
|
||||
`MyAppointmentsController`.
|
||||
|
||||
**نگاشت مجوزها:**
|
||||
|
||||
- Billing خواندنی → `payments.view` · ساخت → `payments.create` · `finalize` و `transitionClaim` → `payments.update`
|
||||
- `my/appointments`, `today-stats`, `my/clinic-doctors` → `appointments.view`
|
||||
- `POST /my/appointment` و `patient-lookup` → `appointments.create`
|
||||
|
||||
`patient-lookup` عمداً `appointments.create` گرفت نه `patients.view`: کامنت خودِ متد میگوید
|
||||
عمداً از گیتِ پروندهٔ بیمار جدا شده تا ثبت نوبت مستقل از اشتراک کار کند. با `patients.view`
|
||||
همان قابلیت میشکست.
|
||||
|
||||
**مکانیزم:** `App\Shared\Controller\PermissionGateTrait` از دلِ `ResourcePermissionTrait` بیرون
|
||||
کشیده شد؛ trait قبلی حالا رویش سوار است و فقط منبعِ پیشفرض و استثنای نوبتدهیاش را نگه داشته.
|
||||
هیچ voter یا checker تازهای ساخته نشد.
|
||||
|
||||
**یک تغییر رفتار که باید بدانی:** منشیِ **بدون رابطهٔ فعال** روی `/my/appointments` دیگر لیست
|
||||
خالی نمیگیرد، `403` میگیرد. `SecretaryAccessChecker::can()` برای او `false` میدهد — همان
|
||||
fail-closed مستندِ `SecretaryPermissionChecker`. «خالیِ خاموش» با «اجازه نداری» یکی نیست.
|
||||
`SecretaryAppointmentScopeTest::testSecretaryWithNoAssignmentIsDenied` همین را تثبیت میکند.
|
||||
|
||||
## یافتهٔ ۹ 🟧 HIGH — `AppointmentPlanController` بدون گِیت
|
||||
|
||||
**۱. فایل:** `src/Appointment/Plan/Controller/AppointmentPlanController.php`
|
||||
|
||||
**۲. شرح:** دوقلوی یافتهٔ ۱. همان `#[IsGranted('IS_AUTHENTICATED_FULLY')]` سطحکلاس، و همان
|
||||
`requireItem()` — کپیِ کلمهبهکلمهٔ تابعی که آدیت دیروز در `TreatmentProtocolController`
|
||||
ناکافی خواند. آدیت قلِ اول را بست و دوقلویش را ندید، چون `GET .../segments` پارامتر دارد و
|
||||
پویشِ `GET` روتهای پارامتردار را رد میکرد.
|
||||
|
||||
**۳. ریسک:** منشیِ `services:false` بخشبندیِ هر سرویسِ محیط را میخواند و با `PUT` کاملاً
|
||||
بازنویسی میکرد. بخشبندی طولِ نوبت و منابعِ لازم را تعیین میکند، پس بازنویسیاش یعنی
|
||||
بههمریختنِ برنامهٔ همهٔ نوبتهای آن سرویس.
|
||||
|
||||
**۴. رفع:** `show` → `services.view`، `replace` → `services.update`، `preview` →
|
||||
`services.view` **یا** `appointments.view`. «یا» عمدی است: `preview` ورودیِ فرمِ ثبت نوبت است،
|
||||
پس قرینهٔ `denyUnlessGrantedForBooking`.
|
||||
|
||||
**۵. مستندات:** `docs/api/appointment-plan.md` بخش «مجوزها».
|
||||
|
||||
## یافتهٔ ۱۰ 🟦 LOW — گِیت بعد از واکشی در ۳۴ روت نوشتنی
|
||||
|
||||
**۱. کشف چطور شد:** حین نوشتنِ پویشِ روتهای نوشتنی. قاعدهٔ اولیه «منشیِ بیمجوز فقط `403`»
|
||||
بود؛ ۷۵ روت قرمز شد. پس از خواندنِ کد معلوم شد بیشترشان گِیت **دارند**، ولی بعد از
|
||||
`findByUuid()` مینشیند، پس uuidِ ناموجود اول `404` میگیرد.
|
||||
|
||||
**۲. ریسک:** enumeration oracle. کاربرِ بیمجوز از تفاوت `403`/`404` میفهمد کدام uuid در این
|
||||
محیط وجود دارد. همان چیزی که یافتهٔ ۱ عمداً با «گیت پیش از `requireItem`» بست، ولی هیچ چیزی
|
||||
در بقیهٔ کد اجبارش نمیکرد.
|
||||
|
||||
**۳. چرا رفع نشد:** حدود ۱۵ تای این چکها **object-scoped** هستند، نه resource-scoped —
|
||||
مثلاً `ClinicController::update` چکش `permChecker->can($user, $clinic, …)` است و بدون `$clinic`
|
||||
اصلاً قابل صدا زدن نیست. بالا بردنشان یعنی افزودنِ یک پیشچکِ منبعسطحِ تازه به هر کدام، نه
|
||||
جابهجا کردن دو خط. این طراحی است، نه reorder، و در محدودهٔ این جلسه نبود.
|
||||
|
||||
**۴. فهرست کامل** — گروهبندی بر اساس کنترلر:
|
||||
|
||||
- `AppointmentController` — `updateStatus`, `confirm`, `update`, `serviceReschedule`
|
||||
- `AppointmentSettingsController` — `createSchedule`, `updateSchedule`, `deleteSchedule`,
|
||||
`createOverride`, `updateOverride`, `deleteOverride`, `createHoliday`, `updateHoliday`,
|
||||
`deleteHoliday`
|
||||
- `ClinicController` — `update`, `detachDoctor`, `createAddress`, `updateAddress`, `deleteAddress`
|
||||
- `ClinicDoctorPermissionController` — `updatePermissions`
|
||||
- `ClinicInvitationController` — `inviteDoctor`, `resendInvitation`, `changeInvitationStatus`,
|
||||
`deleteInvitation`
|
||||
- `DoctorController` — `update`, `delete`, `updateAddress`, `deleteAddress`
|
||||
- `ResourceBlockController` — `create`, `delete`
|
||||
- `RepresentationController` — `update`
|
||||
- `SecretaryController` — `create`, `update`, `deactivate`, `syncClinicDoctors`
|
||||
|
||||
سه روتِ `AppointmentSettingsController` و `SecretaryController::create` پارامتر مسیری ندارند و
|
||||
هدفشان از بدنه میآید؛ رفتارشان همان است.
|
||||
|
||||
**۵. مهار:** `ApiLeastPrivilegeTest::testNoApiWriteRouteSkipsItsPermissionGate` قاعدهاش دو
|
||||
سطحی است — روتِ **بدون** path parameter باید `403` بدهد، روتِ پارامتردار `403` یا `404`. پس
|
||||
روتهای این فهرست عبور میکنند ولی هر روتِ نوشتنیِ تازهای که `2xx` یا `422` بدهد قرمز میشود.
|
||||
|
||||
## پویش روتهای نوشتنی — نتیجه
|
||||
|
||||
۷۵ روتِ `POST`/`PUT`/`PATCH`/`DELETE` با منشیِ بدونِ هیچ مجوزی زده شد. پس از triage:
|
||||
|
||||
- **۳۴** عمداً باز — لاگین و ثبتنام و OTP، کالبک درگاه، امتیاز و کامنت بیمار، پروفایل و
|
||||
کیفپول خودِ کاربر، و کلِ جریان رزرو عمومی. همه با دلیل در `ALLOWED_WRITE`.
|
||||
- **۲** عمداً `409` — حذف سرویس و بخش سرویس اصلاً ممکن نیست، چون نوبت و فاکتور به سرویس ارجاع
|
||||
دارند.
|
||||
- **۳۴** یافتهٔ ۱۰ بالا.
|
||||
- **۵** گَپِ واقعی که همین جلسه بسته شد — روتهای نوشتنیِ Billing و MyAppointments و
|
||||
`service_segments_replace`.
|
||||
|
||||
## پرسنل tenant دوم — پوشش بسته شد
|
||||
|
||||
`tests/Staff/StaffCrossTenantTest.php` هر دو محیط را در fixture میسازد. چهار تست: شاهد مثبت
|
||||
(پرسنل روی جلسهٔ محیط خودش `200`)، جلسهٔ محیط دیگر روی `GET`/`start`/`finish`، ناحیهٔ جلسهٔ
|
||||
محیط دیگر روی `start`/`complete`/`skip`/`reopen`، و تستِ مرزی که uuidِ ناموجود و uuidِ محیطِ
|
||||
دیگر باید **دقیقاً یک پاسخ** بدهند. همه `404 / ERR_NOT_FOUND_001` — بدون نشت و بدون oracle.
|
||||
|
||||
برخلاف پیشنهاد ۳ گزارش، پرسنل در DB زنده ساخته **نشد**: دادهٔ دستی با اولین re-seed میرود و
|
||||
هیچوقت خودکار اجرا نمیشود.
|
||||
|
||||
## تغییرات این جلسه
|
||||
|
||||
```
|
||||
src/Shared/Controller/PermissionGateTrait.php گِیت مشترک (جدید)
|
||||
src/Resource/Controller/ResourcePermissionTrait.php روی گِیت مشترک سوار شد
|
||||
src/Billing/Controller/BillingController.php ۱۲ روت → payments.view/create/update
|
||||
src/Appointment/Controller/MyAppointmentsController.php ۵ روت → appointments.view/create
|
||||
src/Appointment/Plan/Controller/AppointmentPlanController.php ۳ روت → services.* (یافتهٔ ۹)
|
||||
tests/Shared/ApiLeastPrivilegeTest.php پویش نوشتنی + ALLOWED_WRITE؛ KNOWN_GAPS خالی شد
|
||||
tests/Staff/StaffCrossTenantTest.php IDOR بینمحیطیِ پرسنل (جدید)
|
||||
tests/Secretary/SecretaryAppointmentScopeTest.php منشیِ بیرابطه: خالی → ۴۰۳
|
||||
docs/api/billing.md · appointment.md · appointment-plan.md مجوز هر روت
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user