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:
@@ -13,6 +13,7 @@ use App\Patient\Repository\PatientRecordRepository;
|
||||
use App\Patient\Repository\PatientSessionRepository;
|
||||
use App\Patient\Security\PatientRecordScope;
|
||||
use App\Patient\Security\PatientRecordScopeResolver;
|
||||
use App\Secretary\Security\SecretaryAccessChecker;
|
||||
use App\Patient\Service\PatientService;
|
||||
use App\UserProfile\Entity\UserProfile;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
@@ -57,6 +58,7 @@ class PatientController extends BaseController
|
||||
private readonly \App\Discount\Repository\DiscountRuleRepository $discountRuleRepo,
|
||||
private readonly \App\Patient\Repository\SessionPaymentRepository $sessionPaymentRepo,
|
||||
private readonly \App\Patient\Repository\SessionAuditLogRepository $sessionAuditRepo,
|
||||
private readonly SecretaryAccessChecker $secretaryAccess,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
@@ -141,6 +143,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/wallet/charge', methods: ['POST'])]
|
||||
public function chargeWallet(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'create');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -176,6 +179,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/wallet/withdraw', methods: ['POST'])]
|
||||
public function withdrawWallet(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'create');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -235,6 +239,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/call', methods: ['POST'])]
|
||||
public function createCall(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -271,6 +276,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/call/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteCall(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$call = $this->callRepo->findByUuid($uuid);
|
||||
if ($call === null || !$this->ownsRecord($call->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -302,6 +308,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/message', methods: ['POST'])]
|
||||
public function createMessage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -327,6 +334,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/message/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteMessage(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$message = $this->messageRepo->findByUuid($uuid);
|
||||
if ($message === null || !$this->ownsRecord($message->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -361,6 +369,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/note', methods: ['POST'])]
|
||||
public function createNote(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -383,6 +392,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/note/{uuid}', methods: ['PATCH'])]
|
||||
public function updateNote(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$note = $this->noteRepo->findByUuid($uuid);
|
||||
if ($note === null || !$this->ownsRecord($note->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -408,6 +418,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/note/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteNote(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$note = $this->noteRepo->findByUuid($uuid);
|
||||
if ($note === null || !$this->ownsRecord($note->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -439,6 +450,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/medical-record', methods: ['POST'])]
|
||||
public function createMedicalRecord(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -463,6 +475,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/medical-record/{uuid}', methods: ['PATCH'])]
|
||||
public function updateMedicalRecord(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$medical = $this->medicalRepo->findByUuid($uuid);
|
||||
if ($medical === null || !$this->ownsRecord($medical->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -493,6 +506,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/medical-record/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteMedicalRecord(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$medical = $this->medicalRepo->findByUuid($uuid);
|
||||
if ($medical === null || !$this->ownsRecord($medical->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -524,6 +538,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/attachment', methods: ['POST'])]
|
||||
public function uploadAttachment(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$record = $this->recordRepo->findByUuid($uuid);
|
||||
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
|
||||
@@ -546,6 +561,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/attachment/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteAttachment(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$attachment = $this->attachmentRepo->findByUuid($uuid);
|
||||
if ($attachment === null || !$this->ownsRecord($attachment->getRecord(), $entityType, $entityId, $user)) {
|
||||
@@ -683,6 +699,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient', methods: ['POST'])]
|
||||
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'create');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -776,6 +793,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}', methods: ['PATCH'])]
|
||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -985,6 +1003,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/patient/{uuid}/session', methods: ['POST'])]
|
||||
public function createSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1028,6 +1047,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/session/{uuid}', methods: ['PATCH'])]
|
||||
public function updateSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1103,6 +1123,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/session/{uuid}/payments', methods: ['POST'])]
|
||||
public function addSessionPayment(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'create');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1128,6 +1149,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/session/{uuid}/payments/{paymentUuid}', methods: ['PATCH'])]
|
||||
public function updateSessionPayment(string $uuid, string $paymentUuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1149,6 +1171,7 @@ class PatientController extends BaseController
|
||||
#[Route('/api/v1/session/{uuid}/payments/{paymentUuid}', methods: ['DELETE'])]
|
||||
public function deleteSessionPayment(string $uuid, string $paymentUuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'payments', 'delete');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
@@ -1186,7 +1209,17 @@ class PatientController extends BaseController
|
||||
|
||||
private function scope(User $user): PatientRecordScope
|
||||
{
|
||||
return $this->scopeCache[$user->getId()] ??= $this->scopeResolver->resolve($user);
|
||||
return $this->scopeCache[$user->getId()] ??= $this->resolveScope($user);
|
||||
}
|
||||
|
||||
/** منشیِ بدون مجوز patients.view هیچ پروندهای نمیبیند؛ سایر نقشها از رزولور. */
|
||||
private function resolveScope(User $user): PatientRecordScope
|
||||
{
|
||||
if ($user->hasRole('ROLE_SECRETARY') && !$this->secretaryAccess->can($user, 'patients', 'view')) {
|
||||
return PatientRecordScope::unknown();
|
||||
}
|
||||
|
||||
return $this->scopeResolver->resolve($user);
|
||||
}
|
||||
|
||||
/** @return array{0: string, 1: int|null} */
|
||||
|
||||
Reference in New Issue
Block a user