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 |
|
||||
|
||||
Reference in New Issue
Block a user