241 lines
11 KiB
PHP
241 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
use app\adminapi\controller\doctor\AppointmentController;
|
|
use app\adminapi\controller\tcm\DiagnosisController;
|
|
use app\adminapi\controller\tcm\PrescriptionController;
|
|
use app\adminapi\logic\doctor\AppointmentLogic;
|
|
use app\adminapi\logic\firstvisit\MyPatientLogic;
|
|
use app\adminapi\logic\tcm\DiagnosisAiLogic;
|
|
use app\adminapi\logic\tcm\DiagnosisLogic;
|
|
use app\adminapi\logic\tcm\PrescriptionLogic;
|
|
|
|
function diagnosisWorkspaceAuthExpect(bool $condition, string $message): void
|
|
{
|
|
if (!$condition) {
|
|
throw new RuntimeException($message);
|
|
}
|
|
}
|
|
|
|
function diagnosisWorkspaceMethodSource(ReflectionMethod $method): string
|
|
{
|
|
$file = file($method->getFileName());
|
|
if (!is_array($file)) {
|
|
throw new RuntimeException('authorization method source is readable');
|
|
}
|
|
|
|
return implode('', array_slice(
|
|
$file,
|
|
$method->getStartLine() - 1,
|
|
$method->getEndLine() - $method->getStartLine() + 1
|
|
));
|
|
}
|
|
|
|
// Pure policy helpers are invoked directly so this security regression test never needs a real database.
|
|
$appointmentScope = (new ReflectionClass(AppointmentLogic::class))
|
|
->getMethod('appointmentRowManageableForScope');
|
|
$filterPrescriptions = (new ReflectionClass(PrescriptionLogic::class))
|
|
->getMethod('filterViewablePrescriptions');
|
|
|
|
diagnosisWorkspaceAuthExpect(
|
|
$appointmentScope->invoke(null, 31, 41, 31, [1], null, false) === true,
|
|
'assigned doctor can open the reception row'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
$appointmentScope->invoke(null, 32, 41, 31, [1], null, false) === false,
|
|
'doctor cannot open another doctor appointment row'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
$appointmentScope->invoke(null, 32, 41, 41, [2], null, false) === true,
|
|
'assigned assistant can open the reception row'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
$appointmentScope->invoke(null, 999, 999, 1, [1, 2], [], true) === true,
|
|
'root keeps reception compatibility regardless of role and data scope'
|
|
);
|
|
|
|
$ownPrescription = [
|
|
'id' => 51,
|
|
'creator_id' => 7,
|
|
'assistant_id' => 0,
|
|
'is_shared' => 0,
|
|
'visible_role_ids' => '',
|
|
];
|
|
$otherPrescription = [
|
|
'id' => 52,
|
|
'creator_id' => 8,
|
|
'assistant_id' => 9,
|
|
'is_shared' => 0,
|
|
'visible_role_ids' => '',
|
|
];
|
|
diagnosisWorkspaceAuthExpect(
|
|
$filterPrescriptions->invoke(null, [$ownPrescription], 7, []) === [$ownPrescription],
|
|
'visible prescription keeps the existing response row unchanged'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
$filterPrescriptions->invoke(null, [$otherPrescription], 1, ['root' => 1]) === [$otherPrescription],
|
|
'root keeps prescription compatibility'
|
|
);
|
|
|
|
$diagnosisLogicSource = file_get_contents((new ReflectionClass(DiagnosisLogic::class))->getFileName());
|
|
$diagnosisAiLogicSource = file_get_contents((new ReflectionClass(DiagnosisAiLogic::class))->getFileName());
|
|
$myPatientLogicSource = file_get_contents((new ReflectionClass(MyPatientLogic::class))->getFileName());
|
|
$appointmentLogicSource = file_get_contents((new ReflectionClass(AppointmentLogic::class))->getFileName());
|
|
$prescriptionLogicSource = file_get_contents((new ReflectionClass(PrescriptionLogic::class))->getFileName());
|
|
$diagnosisControllerSource = file_get_contents(
|
|
dirname(__DIR__) . '/app/adminapi/controller/tcm/DiagnosisController.php'
|
|
);
|
|
$appointmentControllerSource = file_get_contents(
|
|
dirname(__DIR__) . '/app/adminapi/controller/doctor/AppointmentController.php'
|
|
);
|
|
$prescriptionControllerSource = file_get_contents(
|
|
dirname(__DIR__) . '/app/adminapi/controller/tcm/PrescriptionController.php'
|
|
);
|
|
$appointmentListsSource = file_get_contents(
|
|
dirname(__DIR__) . '/app/adminapi/lists/doctor/AppointmentLists.php'
|
|
);
|
|
$doctorNoteLogicSource = file_get_contents(
|
|
dirname(__DIR__) . '/app/adminapi/logic/doctor/DoctorNoteLogic.php'
|
|
);
|
|
foreach ([
|
|
$diagnosisLogicSource,
|
|
$diagnosisAiLogicSource,
|
|
$myPatientLogicSource,
|
|
$appointmentLogicSource,
|
|
$prescriptionLogicSource,
|
|
$diagnosisControllerSource,
|
|
$appointmentControllerSource,
|
|
$prescriptionControllerSource,
|
|
$appointmentListsSource,
|
|
$doctorNoteLogicSource,
|
|
] as $source) {
|
|
diagnosisWorkspaceAuthExpect(is_string($source), 'authorization source is readable');
|
|
}
|
|
|
|
$myPatientScopeMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(MyPatientLogic::class))->getMethod('applyScope')
|
|
);
|
|
$diagnosisReadonlyAuthMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(DiagnosisLogic::class))->getMethod('canViewReadonlyDiagnosis')
|
|
);
|
|
$diagnosisAiAuthMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(DiagnosisAiLogic::class))->getMethod('loadAuthorizedDiagnosis')
|
|
);
|
|
$prescriptionListMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(PrescriptionLogic::class))->getMethod('listByDiagnosis')
|
|
);
|
|
$trackingWindowMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(DiagnosisLogic::class))->getMethod('fetchTrackingWindow')
|
|
);
|
|
$trackingWindowControllerMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(DiagnosisController::class))->getMethod('trackingWindow')
|
|
);
|
|
$doctorNotesControllerMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(AppointmentController::class))->getMethod('doctorNotes')
|
|
);
|
|
$addDoctorNoteControllerMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(AppointmentController::class))->getMethod('addDoctorNote')
|
|
);
|
|
$receptionMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(AppointmentLogic::class))->getMethod('reception')
|
|
);
|
|
$prescriptionControllerMethod = diagnosisWorkspaceMethodSource(
|
|
(new ReflectionClass(PrescriptionController::class))->getMethod('listByDiagnosis')
|
|
);
|
|
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($myPatientScopeMethod, 'in_array(self::ASSISTANT_ROLE_ID, $roleIds, true)')
|
|
&& str_contains($myPatientScopeMethod, "'CAST(d.assistant_id AS UNSIGNED) = ' . \$adminId")
|
|
&& str_contains($myPatientScopeMethod, 'in_array(self::DOCTOR_ROLE_ID, $roleIds, true)')
|
|
&& str_contains($myPatientScopeMethod, 'scope_apt.doctor_id = {$adminId}'),
|
|
'diagnosis row policy keeps assistant assignment and doctor appointment ownership contracts'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($myPatientScopeMethod, 'array_intersect($roleIds, self::TEAM_ROLE_IDS)')
|
|
&& str_contains($myPatientScopeMethod, 'DataScopeService::getVisibleAdminIds($adminId, $adminInfo)')
|
|
&& strpos($myPatientScopeMethod, 'array_intersect($roleIds, self::TEAM_ROLE_IDS)')
|
|
< strpos($myPatientScopeMethod, 'in_array(self::DOCTOR_ROLE_ID, $roleIds, true)'),
|
|
'DataScope ALL is reserved for team roles before ordinary doctor and assistant self-relations'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($diagnosisReadonlyAuthMethod, 'MyPatientLogic::canAccessDiagnosis(')
|
|
&& !str_contains($diagnosisReadonlyAuthMethod, 'DataScopeService::getVisibleAdminIds'),
|
|
'readonly diagnosis authorization reuses the canonical patient row policy'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($diagnosisAiAuthMethod, 'MyPatientLogic::canAccessDiagnosis(')
|
|
&& strpos($diagnosisAiAuthMethod, 'MyPatientLogic::canAccessDiagnosis(')
|
|
< strpos($diagnosisAiAuthMethod, 'DiagnosisLogic::detail(')
|
|
&& !str_contains($diagnosisAiAuthMethod, 'DataScopeService::getVisibleAdminIds'),
|
|
'AI diagnosis authorization reuses the canonical row policy before loading case details'
|
|
);
|
|
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($trackingWindowControllerMethod, 'canViewReadonlyDiagnosis((int) $params[\'id\']')
|
|
&& strpos($trackingWindowControllerMethod, 'canViewReadonlyDiagnosis((int) $params[\'id\']')
|
|
< strpos($trackingWindowControllerMethod, 'DiagnosisLogic::fetchTrackingWindow('),
|
|
'trackingWindow authorizes the diagnosis before reading tracking records'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($trackingWindowMethod, "'diagnosis_id' => \$diagnosisId")
|
|
&& strpos($trackingWindowMethod, "'diagnosis_id' => \$diagnosisId")
|
|
< strpos($trackingWindowMethod, "'blood_records'"),
|
|
'trackingWindow returns the authorized diagnosis id at the response top level'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($doctorNotesControllerMethod, 'DiagnosisLogic::canViewReadonlyDiagnosis(')
|
|
&& strpos($doctorNotesControllerMethod, 'DiagnosisLogic::canViewReadonlyDiagnosis(')
|
|
< strpos($doctorNotesControllerMethod, 'DoctorNoteLogic::getByDiagnosis('),
|
|
'doctorNotes authorizes the diagnosis before reading notes'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($addDoctorNoteControllerMethod, 'DiagnosisLogic::canViewReadonlyDiagnosis(')
|
|
&& strpos($addDoctorNoteControllerMethod, 'DiagnosisLogic::canViewReadonlyDiagnosis(')
|
|
< strpos($addDoctorNoteControllerMethod, 'DoctorNoteLogic::addOrAppend('),
|
|
'addDoctorNote authorizes the diagnosis before writing any note data'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($receptionMethod, 'appointmentRowManageableByAdmin(')
|
|
&& strpos($receptionMethod, 'appointmentRowManageableByAdmin(')
|
|
< strpos($receptionMethod, '$appointment = self::detail($params);'),
|
|
'reception authorizes the appointment before loading its detail DTO'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($prescriptionListMethod, 'MyPatientLogic::canAccessDiagnosis(')
|
|
&& strpos($prescriptionListMethod, 'MyPatientLogic::canAccessDiagnosis(')
|
|
< strpos($prescriptionListMethod, "Prescription::where('diagnosis_id', \$diagnosisId)"),
|
|
'listByDiagnosis authorizes its parent diagnosis before the first prescription SQL query'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($prescriptionLogicSource, 'self::canViewPrescription($row, $viewerAdminId, $viewerAdminInfo)')
|
|
&& str_contains(
|
|
$prescriptionControllerMethod,
|
|
'PrescriptionLogic::listByDiagnosis($diagnosisId, (int) $this->adminId, $this->adminInfo)'
|
|
)
|
|
&& str_contains($prescriptionControllerMethod, "PrescriptionLogic::getError() !== ''"),
|
|
'listByDiagnosis keeps child visibility filtering and surfaces parent authorization failure'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($appointmentListsSource, 'u.patient_id AS source_patient_id'),
|
|
'appointment DTO exposes the source patient id separately from the diagnosis id'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
str_contains($doctorNoteLogicSource, 'normalizeNewAttachmentPaths(')
|
|
&& str_contains($doctorNoteLogicSource, "\$domainHost === \$urlHost")
|
|
&& str_contains($doctorNoteLogicSource, "\$domainPort === \$urlPort")
|
|
&& str_contains($doctorNoteLogicSource, "str_starts_with(\$urlPath, \$domainPath . '/')")
|
|
&& str_contains($doctorNoteLogicSource, "str_starts_with(\$path, '//')"),
|
|
'new note attachments require an exact configured storage origin and path boundary'
|
|
);
|
|
diagnosisWorkspaceAuthExpect(
|
|
substr_count($diagnosisControllerSource, '诊单不存在或无权访问') >= 2
|
|
&& str_contains($appointmentControllerSource, '预约记录不存在或无权访问')
|
|
&& str_contains($appointmentControllerSource, '诊单不存在或无权访问'),
|
|
'missing and forbidden child-resource lookups share non-enumerating errors'
|
|
);
|
|
|
|
echo "Diagnosis workspace row authorization: OK\n";
|