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
@@ -11,6 +11,7 @@ use App\Inventory\Entity\InventoryPackage;
use App\Inventory\Repository\InventoryItemRepository;
use App\Inventory\Repository\InventoryPackageRepository;
use App\Inventory\Service\InventoryService;
use App\Secretary\Security\SecretaryAccessChecker;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Exception\AppException;
@@ -37,6 +38,7 @@ class InventoryController extends BaseController
private readonly DoctorRepository $doctorRepo,
private readonly ClinicRepository $clinicRepo,
private readonly UserActiveContextRepository $contextRepo,
private readonly SecretaryAccessChecker $secretaryAccess,
) {}
// ── Items ────────────────────────────────────────────────────────────────
@@ -44,6 +46,7 @@ class InventoryController extends BaseController
#[Route('/api/v1/inventory-items', methods: ['GET'])]
public function listItems(#[CurrentUser] User $user): JsonResponse
{
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'view');
[$type, $id] = $this->resolveEntity($user);
if ($id === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -60,6 +63,7 @@ class InventoryController extends BaseController
#[Route('/api/v1/inventory-categories', methods: ['GET'])]
public function listCategories(#[CurrentUser] User $user): JsonResponse
{
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'view');
[$type, $id] = $this->resolveEntity($user);
if ($id === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -80,6 +84,7 @@ class InventoryController extends BaseController
#[Route('/api/v1/inventory-item', methods: ['POST'])]
public function createItem(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'create');
[$type, $id] = $this->resolveEntity($user);
if ($id === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -101,6 +106,7 @@ class InventoryController extends BaseController
#[Route('/api/v1/inventory-item/{uuid}', methods: ['PATCH'])]
public function updateItem(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'update');
$item = $this->ownedItem($uuid, $user);
if ($item === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کالا یافت نشد', 404);
@@ -123,6 +129,7 @@ class InventoryController extends BaseController
#[Route('/api/v1/inventory-item/{uuid}', methods: ['DELETE'])]
public function deleteItem(string $uuid, #[CurrentUser] User $user): JsonResponse
{
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'delete');
$item = $this->ownedItem($uuid, $user);
if ($item === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کالا یافت نشد', 404);
@@ -138,6 +145,7 @@ class InventoryController extends BaseController
#[Route('/api/v1/inventory-packages', methods: ['GET'])]
public function listPackages(#[CurrentUser] User $user): JsonResponse
{
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'view');
[$type, $id] = $this->resolveEntity($user);
if ($id === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -152,6 +160,7 @@ class InventoryController extends BaseController
#[Route('/api/v1/inventory-package', methods: ['POST'])]
public function createPackage(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'create');
[$type, $id] = $this->resolveEntity($user);
if ($id === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -173,6 +182,7 @@ class InventoryController extends BaseController
#[Route('/api/v1/inventory-package/{uuid}', methods: ['PATCH'])]
public function updatePackage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'update');
$package = $this->ownedPackage($uuid, $user);
if ($package === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پکیج یافت نشد', 404);
@@ -199,6 +209,7 @@ class InventoryController extends BaseController
#[Route('/api/v1/inventory-package/{uuid}', methods: ['DELETE'])]
public function deletePackage(string $uuid, #[CurrentUser] User $user): JsonResponse
{
$this->secretaryAccess->denyUnlessGranted($user, 'inventory', 'delete');
$package = $this->ownedPackage($uuid, $user);
if ($package === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پکیج یافت نشد', 404);