feat: Implement secretary permissions enforcement across multiple resources

- 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.
This commit is contained in:
hamed
2026-07-23 16:36:35 +03:30
parent f00ed23f00
commit 5c4976d65f
24 changed files with 790 additions and 87 deletions
+27 -25
View File
@@ -196,41 +196,43 @@ Returns stats for the authenticated secretary and (conditionally) today's appoin
### Response `200`
پاسخ بر اساس `scope` محیطِ فعالِ منشی دو شکل دارد. **کلید `scope` تمایزدهنده است**: در `scope=doctor` فیلد `doctor` هست (نه `clinic`) و در `scope=clinic` فیلد `clinic` (نه `doctor`). کلاینت باید هر دو را مدیریت کند و به‌صورت مستقیم به `data.doctor.name` دسترسی نگیرد.
**منشیِ مطبِ شخصی (`scope=doctor`):**
```json
{
"success": true,
"data": {
"doctor": {
"uuid": "string",
"name": "string",
"degree": "string | null"
},
"permissions": {
"resources": {
"appointments": {
"view": true,
"edit": false
}
}
},
"stats": {
"today_appointments": 8,
"tomorrow_appointments": 5
},
"scope": "doctor",
"doctor": { "uuid": "string", "name": "string", "degree": "string | null" },
"permissions": { "resources": { "appointments": { "view": true, "update_status": false } } },
"stats": { "today_appointments": 8, "tomorrow_appointments": 5 },
"today_appointments": [
{
"uuid": "string",
"patient_name": "string | null",
"patient_mobile": "string",
"slot_start": 1700000000,
"status": "reserved"
}
{ "uuid": "string", "patient_name": "string | null", "patient_mobile": "string", "slot_start": 1700000000, "status": "reserved" }
]
}
}
```
`today_appointments` — only populated when `permissions.resources.appointments.view === true`; up to 10 records when visible.
**منشیِ کلینیک (`scope=clinic`) — بدون کلید `doctor`:**
```json
{
"success": true,
"data": {
"scope": "clinic",
"clinic": { "uuid": "string", "name": "string" },
"permissions": { "resources": { "appointments": { "view": true } } },
"stats": { "today_appointments": 12, "tomorrow_appointments": 7 },
"today_appointments": [
{ "uuid": "string", "patient_name": "string | null", "patient_mobile": "string", "slot_start": 1700000000, "status": "reserved", "doctor_name": "string" }
]
}
}
```
`today_appointments` — only populated when `permissions.resources.appointments.view === true`; up to 10 records (doctor scope) / 20 (clinic scope) when visible. در scope کلینیک هر ردیف `doctor_name` هم دارد.
### Errors
+2
View File
@@ -2,6 +2,8 @@
> **Prefix:** `/api/v1/insurances`, `/api/v1/insurance`, `/api/v1/admin/insurance`
> **دسترسی منشی:** endpointهای غیرادمینِ بیمه (insurance-pricing, billing/tenant-insurances, service-coverage, `/api/v1/insurance/*`) برای `ROLE_SECRETARY` روی منبع `insurances` اعمال می‌شوند (`SecretaryAccessChecker`): GET→`view`, POST→`create`, PATCH/PUT→`update`, DELETE→`delete`؛ نبودِ مجوز → `403`. جزئیات: [secretary.md](secretary.md).
Two resource types:
1. **Insurance** — master list of insurance companies managed by admin
2. **DoctorInsurance** — a doctor's acceptance of a specific insurance (with optional price)
+2
View File
@@ -2,6 +2,8 @@
> **Prefix:** `/api/v1/inventory-*`
> **دسترسی منشی:** برای `ROLE_SECRETARY` روی منبع `inventory` اعمال می‌شود (`SecretaryAccessChecker`): GET→`view`, POST→`create`, PATCH→`update`, DELETE→`delete`؛ نبودِ مجوز → `403`. پیش‌فرضِ منشی برای این منبع همه `false` است. جزئیات: [secretary.md](secretary.md).
Per-tenant (doctor/clinic) consumable-stock management: **items** and **packages**
(bundles of items). Every row is scoped to the caller's resolved entity
(`doctor` / `clinic`), exactly like Tenant Tags — a tenant only ever sees and
+2
View File
@@ -4,6 +4,8 @@
Patient records track patients per entity (doctor or clinic). Each record holds multiple sessions (visits). Access requires an active subscription with the `patient_records` feature.
> **دسترسی منشی:** برای `ROLE_SECRETARY` روی منبع `patients` اعمال می‌شود (`SecretaryAccessChecker`). خواندن‌ها از طریق `scope()` کنترل می‌شوند: منشیِ بدون `patients.view` هیچ پرونده‌ای نمی‌بیند (scope = unknown → 404/403). نوشتن‌ها guard جداگانه دارند: ایجاد بیمار→`patients.create`؛ ویرایش/زیرمنابع (note/call/message/medical-record/attachment/session)→`patients.update`. عملیاتِ مالیِ بیمار (کیف‌پول، پرداختِ جلسه) روی منبع `payments` اعمال می‌شوند. نبودِ مجوز → `403`. جزئیات: [secretary.md](secretary.md).
**Base path:** `/api/v1`
**Auth:** Bearer JWT (doctor, clinic, or secretary)
+2
View File
@@ -4,6 +4,8 @@
Per-clinic payment methods managed from the settings screen (`/admin/my-financial`,
tab "مدیریت پرداخت"). Two resources: **bank accounts** and **POS (card reader) devices**.
> **دسترسی منشی:** روش‌های پرداخت زیرمجموعهٔ منبع `payments` هستند. برای `ROLE_SECRETARY` (`SecretaryAccessChecker`): GET→`payments.view`, POST→`payments.create`, PUT/PATCH→`payments.update`؛ نبودِ مجوز یا رابطهٔ فعال → `403`. جزئیات: [secretary.md](secretary.md).
Records are stored so a patient invoice can later reference which account/device a
service payment was made to.
+2
View File
@@ -3,6 +3,8 @@
> **Prefix:** `/api/v1/payment`, `/api/v1/subscription-payment`
> **Supported Gateways:** `mellat` (Mellat Bank SOAP) | `sep` (SEP REST)
> **دسترسی منشی:** `GET /api/v1/my/payments` برای `ROLE_SECRETARY` به مجوز `payments.view` نیاز دارد (`SecretaryAccessChecker`)؛ نبودِ مجوز → `403`. جزئیات: [secretary.md](secretary.md).
---
## معماری (Flow & مسئولیت‌ها)
+39 -2
View File
@@ -69,7 +69,19 @@ Create a secretary for a doctor.
"update": false,
"delete": false
},
"clinic_info": { "view": true, "update": false }
"clinic_info": { "view": true, "update": false },
"inventory": {
"view": false,
"create": false,
"update": false,
"delete": false
},
"tags": {
"view": false,
"create": false,
"update": false,
"delete": false
}
}
}
}
@@ -107,7 +119,20 @@ Create a secretary for a doctor.
**Permissions Structure:**
مجموعهٔ منابع (resources) بر اساس صفحات موجود پنل ادمین است. `mergePermissions` هر منبع/اکشن ارسال‌شده را deep-merge می‌کند؛ فقط `appointments` در بک‌اند enforce می‌شود (`MyAppointmentsController`, `DashboardController`)، بقیه UI/ذخیره‌ای هستند.
مجموعهٔ منابع (resources) بر اساس صفحات و ماژول‌های در دسترسِ منشی است: `appointments`, `patients`, `payments`, `insurances`, `addresses`, `clinic_info`, `inventory`, `tags`. `mergePermissions` هر منبع/اکشن ارسال‌شده را deep-merge می‌کند. منابع `inventory` و `tags` به‌صورت پیش‌فرض همه `false`‌اند (default-deny)؛ بقیه طبق `DEFAULT_PERMISSIONS`.
**اعمال (enforcement):** همهٔ منابع در بک‌اند enforce می‌شوند، نه فقط `appointments`. منبعِ حقیقت، ستون JSON `permission` روی ردیفِ فعالِ `DoctorSecretary` در محیطِ فعالِ کاربر (`UserActiveContext.db_uuid`) است؛ نقطهٔ مرکزی `App\Secretary\Security\SecretaryAccessChecker` (`can` / `canOrNonSecretary` / `denyUnlessGranted`). نبودِ مجوز → `403 ERR_FORBIDDEN_001`. نقشه:
| Resource | Enforced in | Action → endpoint |
| --- | --- | --- |
| `appointments` | `AppointmentAccessChecker`, `MyAppointmentsController`, `DashboardController` | view/create/cancel/update_status |
| `patients` | `PatientController` (خواندن‌ها via `scope()` → بدون `view` هیچ پرونده‌ای؛ نوشتن‌ها با guard) | view/create/update/delete |
| `payments` | `PaymentController::myPayments`, `PaymentMethodController` (bank/pos), `PatientController` (کیف‌پول + پرداختِ جلسه) | view/create/update/delete |
| `insurances` | `InsuranceController` (insurance-pricing, tenant-insurances, service-coverage, doctor-insurance) | view/create/update/delete |
| `inventory` | `InventoryController` (items + packages) | view/create/update/delete |
| `tags` | `TenantTagController` (لیست با `tags.view` یا `patients.view`؛ نوشتن‌ها با `tags.*`) | view/create/update/delete |
نقش‌های غیرمنشی (`ROLE_CLINIC`/`ROLE_DOCTOR`/`ROLE_ADMIN`) از این چک عبور می‌کنند (`canOrNonSecretary` برایشان `true`). منشیِ بدون رابطهٔ فعال/context هیچ مجوزی ندارد → همه‌چیز `403`.
```json
{
@@ -146,6 +171,18 @@ Create a secretary for a doctor.
"clinic_info": {
"view": true,
"update": false
},
"inventory": {
"view": false, // انبار: مشاهده
"create": false, // ایجاد کالا/بسته
"update": false,
"delete": false
},
"tags": {
"view": false, // تگ‌ها؛ لیست با tags.view یا patients.view
"create": false,
"update": false,
"delete": false
}
}
}