fix(permissions): make the addresses resource real instead of decorative

A full role-by-role sweep (9 roles x 18 endpoints against the running app) showed
the addresses toggles in the owner's permission form controlled nothing. Grep
confirms it: no gate anywhere referenced 'addresses'. The panel's address list was
gated on appointment_settings.view instead — the same borrowed-permission pattern
already fixed for resources and treatment.

GET /api/v1/addresses now gates on addresses.view.

The resource drops to view-only. Creating, updating and deleting an address in
ClinicController is explicitly owner-or-admin
($clinic->getUser()->getId() !== $user->getId()), so those three actions could
never be delegated to a secretary or an invited doctor no matter what the form
said. Both role defaults narrow to ['view' => true] to match, and stored JSON
keeps its old keys harmlessly since merge only reads registry keys.

This widens secretary access: addresses.view defaults to true while
appointment_settings.view defaults to false, so secretaries who could not list
addresses now can. That is deliberate and costs no confidentiality — the same
addresses are already served anonymously from
GET /api/v1/clinic/{uuid}/addresses, which is whitelisted in security.yaml.

Verified live in three states: default 200, addresses.view off 403, and
addresses off with appointment_settings on still 403, proving the borrow is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 19:16:02 +03:30
co-authored by Claude Opus 5
parent be5cdf8af6
commit 294ca19a46
9 changed files with 75 additions and 12 deletions
@@ -376,4 +376,43 @@ class SecretaryResourceEnforcementTest extends ApiTestCase
return [$secretary, $rel, $clinic, $doctor];
}
// ── آدرس‌ها ──────────────────────────────────────────────────────────────
/** پیش‌فرضِ منشی `addresses.view = true` است. */
public function testAddressListAllowedByDefault(): void
{
[$secretary] = $this->makeClinicSecretary();
$this->em->flush();
$this->authJson('GET', '/api/v1/addresses', $secretary);
$this->assertSame(200, $this->responseCode());
}
public function testAddressListDeniedWhenAddressesViewOff(): void
{
[$secretary, $rel] = $this->makeClinicSecretary();
$rel->mergePermissions(['resources' => ['addresses' => ['view' => false]]]);
$this->em->flush();
$this->authJson('GET', '/api/v1/addresses', $secretary);
$this->assertSame(403, $this->responseCode());
}
/**
* تا پیش از این، فهرست آدرس روی `appointment_settings.view` سوار بود و توگلِ
* «آدرس‌ها» هیچ چیزی را کنترل نمی‌کرد. حالا تنظیمات نوبت‌دهی درش را باز نمی‌کند.
*/
public function testAppointmentSettingsNoLongerOpensTheAddressList(): void
{
[$secretary, $rel] = $this->makeClinicSecretary();
$rel->mergePermissions(['resources' => [
'addresses' => ['view' => false],
'appointment_settings' => ['view' => true, 'update' => true],
]]);
$this->em->flush();
$this->authJson('GET', '/api/v1/addresses', $secretary);
$this->assertSame(403, $this->responseCode());
}
}
+12
View File
@@ -119,4 +119,16 @@ class PermissionCatalogTest extends TestCase
$api[array_search('clinic_doctors', array_column($api, 'key'), true)]['clinic_only'],
);
}
/**
* ساخت/ویرایش/حذفِ آدرس در ClinicController صریحاً owner-or-admin است و قابل
* واگذاری نیست؛ پس رجیستری نباید توگلی نشان دهد که هیچ‌وقت اثر ندارد.
*/
public function testAddressesExposesOnlyView(): void
{
$this->assertSame(
['view'],
array_keys(PermissionCatalog::RESOURCES['addresses']['actions']),
);
}
}