Files
clinicpro/src/Secretary/Security/SecretaryAccessChecker.php
T
hamed 5c4976d65f 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.
2026-07-23 16:36:35 +03:30

80 lines
3.1 KiB
PHP

<?php
namespace App\Secretary\Security;
use App\Auth\Entity\User;
use App\Auth\Repository\UserActiveContextRepository;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Secretary\Entity\DoctorSecretary;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
/**
* Single entry point that answers «آیا این منشی مجاز به resource/action هست؟».
*
* منبع حقیقت، ستون JSON `permission` روی ردیف فعالِ DoctorSecretary در محیطِ
* فعال کاربر (UserActiveContext.db_uuid) است — دقیقاً مثل PatientRecordScopeResolver
* و DashboardController::secretary. کنترلرهایی که چند نقش می‌گیرند فقط وقتی کاربر
* ROLE_SECRETARY دارد این checker را صدا می‌زنند؛ نقش‌های دیگر دست‌نخورده می‌مانند.
*/
class SecretaryAccessChecker
{
public function __construct(
private readonly UserActiveContextRepository $contextRepo,
private readonly ClinicRepository $clinicRepo,
private readonly DoctorRepository $doctorRepo,
private readonly DoctorSecretaryRepository $secretaryRepo,
private readonly SecretaryPermissionChecker $permissions,
) {}
/** ردیف فعالِ منشی در محیط فعال؛ null اگر محیط تنظیم نشده یا رابطه‌ای نیست. */
public function activeRelation(User $user): ?DoctorSecretary
{
$dbUuid = $this->contextRepo->findByUser($user)?->getDbUuid();
if ($dbUuid === null) {
return null;
}
$clinic = $this->clinicRepo->findByUuid($dbUuid);
if ($clinic !== null) {
return $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic);
}
$doctor = $this->doctorRepo->findByUuid($dbUuid);
if ($doctor !== null) {
return $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);
}
return null;
}
public function can(User $user, string $resource, string $action): bool
{
$relation = $this->activeRelation($user);
return $relation !== null && $this->permissions->can($relation, $resource, $action);
}
/**
* برای مسیرهایی که چند نقش دارند: فقط منشی را محدود کن. سایر نقش‌ها true.
*/
public function canOrNonSecretary(User $user, string $resource, string $action): bool
{
if (!$user->hasRole('ROLE_SECRETARY')) {
return true;
}
return $this->can($user, $resource, $action);
}
/** 403 اگر منشی مجاز نباشد؛ نقش‌های دیگر بدون تغییر عبور می‌کنند. */
public function denyUnlessGranted(User $user, string $resource, string $action): void
{
if (!$this->canOrNonSecretary($user, $resource, $action)) {
throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403);
}
}
}