docs(api): document the permission registry and correct two enforcement claims
clinic.md's default envelope is regenerated from the running app, so it now shows all 17 resources instead of 13, including services with its full create/delete actions. Both role docs point at permission.md for the shared registry and spell out the merge rule that makes new resources work on existing rows: deleting a key means "take the default", not "deny" — denying requires an explicit false. A systematic sweep over every gated route with all permissions off found two places where the docs claimed enforcement that does not exist: - GET /api/v1/subscription/my returns 200 with every permission off. Only trial is gated. - ServiceCatalogController has no gate at all. Both are pre-existing and both are left as-is rather than half-fixed: their endpoints are also consumed by the booking and subscription flows, where a hard gate would break secretaries who legitimately need them. The docs now say so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+19
-11
@@ -386,32 +386,40 @@ Detach a doctor from a clinic. This removes the clinic↔doctor link (the `clini
|
||||
|
||||
Each doctor attached to a clinic has a permission envelope scoped to **that clinic only** — the doctor's own practice is never affected. Rows live in `clinic_doctor_permissions` (one per clinic+doctor) and are created lazily with defaults for doctors who joined before this feature existed.
|
||||
|
||||
مجموعهٔ منابع را `App\Shared\Security\PermissionCatalog` تعیین میکند و از `GET /api/v1/permission-catalog` هم خوانده میشود — [permission.md](permission.md). این کلاس با منشی مشترک است، پس هر دو نقش دقیقاً یک فهرست از منابع و اکشنها دارند؛ فقط **پیشفرضها** فرق میکنند. تا پیش از این `services` برای پزشکِ عضو فقط `view/update` داشت و `create`/`delete` اصلاً قابل ذخیره نبود.
|
||||
|
||||
منبعی که بعد از ساختِ یک ردیف به رجیستری اضافه شود، هنگام خواندن **پیشفرضِ نقش** را میگیرد نه `false`، پس migration داده لازم نیست. توجه: حذفِ یک کلید از JSON یعنی «پیشفرض را بگیر»، نه «ممنوع» — برای ممنوعکردن باید `false` صریح ذخیره شود.
|
||||
|
||||
The envelope is always returned in full (`{version, resources}`); it is never flattened.
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"resources": {
|
||||
"appointments": { "view": true, "create": true, "cancel": true, "update_status": true },
|
||||
"appointment_settings": { "view": true, "update": true },
|
||||
"patients": { "view": true, "create": true, "update": true, "delete": false },
|
||||
"payments": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"services": { "view": true, "update": false },
|
||||
"clinic_info": { "view": true, "update": false },
|
||||
"insurances": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"addresses": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"appointments": { "view": true, "create": true, "cancel": true, "update_status": true },
|
||||
"patients": { "view": true, "create": true, "update": true, "delete": false },
|
||||
"treatment": { "view": true, "update": true },
|
||||
"payments": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"insurances": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"addresses": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"clinic_info": { "view": true, "update": false },
|
||||
"services": { "view": true, "create": false, "update": false, "delete": false },
|
||||
"inventory": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"tags": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"staff": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"tags": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"discounts": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"sms": { "view": false, "create": false, "update": false, "delete": false }
|
||||
"sms": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"appointment_settings": { "view": true, "update": true },
|
||||
"resources": { "view": true, "create": true, "update": true, "delete": true },
|
||||
"clinic_doctors": { "view": false, "create": false, "update": false, "delete": false },
|
||||
"subscription": { "view": false, "create": false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`active: false` revokes everything at once regardless of the individual flags. The clinic owner and `ROLE_ADMIN` bypass all checks and can never be locked out.
|
||||
|
||||
Unknown resources and unknown actions in a PATCH body are silently ignored, so a client cannot invent permission keys. `subscription` و `clinic_doctors` عمداً منبع نیستند — عملیاتِ مالکِ کلینیکاند، نه پزشکِ عضو.
|
||||
Unknown resources and unknown actions in a PATCH body are silently ignored (اعتبارسنجی از `PermissionCatalog::filterPatch`)، so a client cannot invent permission keys. `subscription` و `clinic_doctors` حالا در رجیستری هستند ولی پیشفرضشان برای پزشکِ عضو خاموش است — عملیاتِ مالکِ کلینیکاند.
|
||||
|
||||
**اعمال (enforcement):** همهٔ منابع در بکاند enforce میشوند. نقطهٔ واحد `App\Clinic\Security\ClinicDoctorAccessChecker` (`denyUnlessGranted` / `memberClinicId`) که **فقط پزشکِ عضوِ کلینیک در محیطِ فعالِ کلینیک** را محدود میکند؛ مالک/ادمین/منشی/پزشکِ مطبِ شخصی دستنخورده عبور میکنند. کنترلرهایی که tenant را نقشمحور حل میکنند (Inventory/Tag/Staff/Discount/Sms) با `memberClinicId` پزشکِ عضو را به دادهٔ کلینیک میبرند (نه مطبِ شخصی). نبودِ مجوز → `403`. در پنل، سایدبار/Route/دکمههای CRUD با `usePermissions().can` برای محیطِ `scope=clinic` گِیت میشوند.
|
||||
|
||||
|
||||
@@ -150,13 +150,13 @@ Create a secretary for a doctor.
|
||||
| `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 |
|
||||
| `services` | `ClinicServiceController` (sections + items). owner از محیطِ فعال با `SecretaryAccessChecker::resolveOwnerEntity` حل میشود چون `EntityContextResolver` منشی را نمیشناسد. گیتِ `services.*` پیش از گیتِ اشتراک اجرا میشود | view/create/update/delete |
|
||||
| `services` | `ClinicServiceController` (sections + items). ⚠ `ServiceCatalogController` (دستهبندی سرویسها، گروهها، روابط) گِیت **ندارد** — اندپوینتهایش در جریانِ ثبت نوبت هم مصرف میشوند و بستنِ یکجا نوبتدهی منشی را میشکند. owner از محیطِ فعال با `SecretaryAccessChecker::resolveOwnerEntity` حل میشود چون `EntityContextResolver` منشی را نمیشناسد. گیتِ `services.*` پیش از گیتِ اشتراک اجرا میشود | view/create/update/delete |
|
||||
| `staff` | `StaffController` (resolveEntity منشیآگاه) | view/create/update/delete |
|
||||
| `discounts` | `DiscountController` (CRUD؛ `suggestions` جزو flowِ جلسه است و با discounts گِیت نمیشود) | view/create/update/delete |
|
||||
| `sms` | `SmsWalletController` (balance/charge/logs/settings). endpointهای admin (قالب/ارسال) همچنان `ROLE_ADMIN` | view/create/update |
|
||||
| `appointment_settings` | `AppointmentSettingsController::denyDoctorAccess` → `SecretaryAccessChecker::canForDoctor` (اسکوپِ پزشکِ تخصیصیافته + توگل). `clinic_uuid` برای محیطِ کلینیک لازم است | view/update |
|
||||
| `clinic_doctors` (فقط کلینیک) | `ClinicController::detachDoctor` (delete)، `ClinicDoctorPermissionController` (view/update)، `ClinicInvitationController` (create/view/update/delete) via `SecretaryAccessChecker::canForClinic` | view/create/update/delete |
|
||||
| `subscription` | `SubscriptionController::my` (view) و `trial` (create)، `PaymentController::initiateSubscription` (create). resolveEntity از قبل منشیآگاه است | view/create |
|
||||
| `subscription` | `SubscriptionController::trial` (create)، `PaymentController::initiateSubscription` (create). ⚠ خواندنِ `GET /api/v1/subscription/my` گِیت **ندارد** — با همهٔ مجوزها خاموش هم ۲۰۰ میدهد؛ چون FeatureGate و useSubscription همهجا صداش میزنند، بستنش نیاز به بررسی جداگانه دارد | create |
|
||||
|
||||
نقشهای غیرمنشی (`ROLE_CLINIC`/`ROLE_DOCTOR`/`ROLE_ADMIN`) از این چک عبور میکنند (`canOrNonSecretary` برایشان `true`). منشیِ بدون رابطهٔ فعال/context هیچ مجوزی ندارد → همهچیز `403`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user