1, 'show' => 0, 'msg' => '', 'data' => $data]; } } } namespace app\common\model\dept { final class Dept { public static array $rows = []; public static int $reads = 0; public static function order(array $order): self { self::$reads++; return new self(); } public function select(): self { return $this; } public function toArray(): array { return self::$rows; } } } namespace app\common\service\DataScope { final class DataScopeService { public static ?array $allowed = [12 => true]; public static array $actors = []; public static function isEnabled(): bool { return true; } public static function getAllowedDeptIdSet(int $adminId, array $adminInfo): ?array { self::$actors[] = [$adminId, $adminInfo]; return self::$allowed; } } } namespace app\adminapi\http\middleware { function request(): object { return $GLOBALS['diagnosis_department_request']; } function env(...$arguments): never { throw new \RuntimeException('This test must not read environment configuration'); } } namespace { require dirname(__DIR__) . '/vendor/autoload.php'; use app\adminapi\controller\dept\DeptController; use app\adminapi\http\middleware\AuthMiddleware; use app\common\cache\AdminAuthCache; use app\common\model\dept\Dept; use app\common\service\DataScope\DataScopeService; use app\common\service\JsonService; final class DiagnosisDepartmentRequest { public array $adminInfo; public object $controllerObject; public string $controllerName = 'dept.Dept'; public string $actionName = 'all'; public function __construct(public array $query = [], public string $method = 'GET', public array $body = [], bool $root = false) { $this->adminInfo = ['admin_id' => 91, 'root' => $root ? 1 : 0, 'role_id' => [7], 'login_ip' => '192.0.2.91', 'work_wechat_userid' => 'synthetic-doctor-91']; } public function get(?string $name = null, $default = null) { return $name === null ? $this->query : ($this->query[$name] ?? $default); } public function param(string $name, $default = null) { return $this->body[$name] ?? $this->query[$name] ?? $default; } public function isGet(): bool { return $this->method === 'GET'; } public function controller(): string { return $this->controllerName; } public function action(): string { return $this->actionName; } public function ip(): string { return '192.0.2.91'; } } final class DiagnosisDepartmentController extends DeptController { public function __construct(DiagnosisDepartmentRequest $request) { $this->request = $request; $this->adminId = $request->adminInfo['admin_id']; $this->adminInfo = $request->adminInfo; } protected function data($data) { return JsonService::data($data); } } $assertions = 0; function departmentPermissionExpect(bool $ok, string $label): void { global $assertions; if (!$ok) { throw new RuntimeException('FAIL: ' . $label); } $assertions++; } function departmentPermissionInvoke(DiagnosisDepartmentRequest $request, array $permissions, ?array $registered = null): array { AdminAuthCache::$permissions = $permissions; AdminAuthCache::$allUris = $registered ?? [ 'dept.dept/all', 'dept.dept/lists', 'dept.dept/leaderDept', 'dept.dept/detail', 'dept.dept/add', 'dept.dept/edit', 'dept.dept/delete', 'dept.jobs/all', ]; AdminAuthCache::$actors = []; AdminAuthCache::$allUriReads = 0; DataScopeService::$actors = []; Dept::$reads = 0; $GLOBALS['diagnosis_department_request'] = $request; $controller = new DiagnosisDepartmentController($request); $request->controllerObject = $controller; return (new AuthMiddleware())->handle($request, static function () use ($request, $controller): array { if (strtolower($request->controller() . '/' . $request->action()) === 'dept.dept/all') { return $controller->all(); } // A sentinel verifies function-level grants without performing CRUD writes. return ['code' => 1, 'continued' => true]; }); } Dept::$rows = [ ['id' => 10, 'pid' => 0, 'name' => '共享祖先'], ['id' => 11, 'pid' => 10, 'name' => '未授权同级'], ['id' => 12, 'pid' => 10, 'name' => '授权部门'], ['id' => 20, 'pid' => 0, 'name' => '其他部门'], ]; $scopedTree = [[ 'id' => 10, 'pid' => 0, 'name' => '共享祖先', 'level' => 0, 'children' => [['id' => 12, 'pid' => 10, 'name' => '授权部门', 'level' => 1, 'children' => []]], ]]; foreach ([1, '1'] as $scope) { foreach (['tcm.diagnosis/lists', 'TCM.DIAGNOSIS/LISTS'] as $permission) { $request = new DiagnosisDepartmentRequest(['apply_data_scope' => $scope]); $request->controllerName = 'DePt.DePt'; $request->actionName = 'AlL'; $result = departmentPermissionInvoke($request, [$permission]); departmentPermissionExpect($result === ['code' => 1, 'show' => 0, 'msg' => '', 'data' => $scopedTree], 'exact patient-list grant returns the real scoped tree and original envelope'); departmentPermissionExpect(DataScopeService::$actors === [[91, $request->adminInfo]], 'controller passes the authenticated identity to data scope'); departmentPermissionExpect(Dept::$reads === 1 && AdminAuthCache::$allUriReads === 0, 'read-only alias does not need a department management grant'); } } $invalidQueries = [[], ['apply_data_scope' => null]]; foreach ([0, '0', '', '01', '1x', '1.0', ' 1', '1 ', true, false, 1.0, 2, -1, [], ['1'], new stdClass()] as $invalid) { $invalidQueries[] = ['apply_data_scope' => $invalid]; } foreach ($invalidQueries as $query) { $result = departmentPermissionInvoke(new DiagnosisDepartmentRequest($query), ['tcm.diagnosis/lists']); departmentPermissionExpect($result['code'] === 0, 'missing or malformed scope cannot acquire the alias ' . json_encode($query)); departmentPermissionExpect(Dept::$reads === 0 && DataScopeService::$actors === [], 'invalid scope is rejected before any department read'); } foreach ([[], ['tcm.diagnosis/detail'], ['tcm.diagnosis/edit'], ['tcm.diagnosis/listsOther'], ['dept.dept/lists']] as $permissions) { $result = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => '1']), $permissions); departmentPermissionExpect($result['code'] === 0 && Dept::$reads === 0, 'unrelated permissions do not acquire the patient-list alias ' . json_encode($permissions)); } foreach (['POST', 'PUT', 'DELETE', 'HEAD'] as $method) { $result = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => '1'], $method), ['tcm.diagnosis/lists']); departmentPermissionExpect($result['code'] === 0 && Dept::$reads === 0, 'alias denies HTTP method ' . $method); } foreach (['GET', 'POST'] as $method) { $result = departmentPermissionInvoke(new DiagnosisDepartmentRequest([], $method, ['apply_data_scope' => '1']), ['tcm.diagnosis/lists']); departmentPermissionExpect($result['code'] === 0 && Dept::$reads === 0, 'request body cannot enable GET-only scope on ' . $method); } foreach (['lists', 'leaderDept', 'detail', 'add', 'edit', 'delete'] as $action) { foreach (['GET', 'POST'] as $method) { $request = new DiagnosisDepartmentRequest(['apply_data_scope' => '1'], $method); $request->actionName = $action; $result = departmentPermissionInvoke($request, ['tcm.diagnosis/lists']); departmentPermissionExpect($result['code'] === 0 && Dept::$reads === 0, 'patient-list grant never authorizes department action ' . $method . ' ' . $action); } $request = new DiagnosisDepartmentRequest([], 'POST'); $request->actionName = $action; $result = departmentPermissionInvoke($request, ['dept.dept/' . $action]); departmentPermissionExpect(($result['continued'] ?? false) === true, 'original exact department permission still authorizes ' . $action); } $otherController = new DiagnosisDepartmentRequest(['apply_data_scope' => '1']); $otherController->controllerName = 'dept.Jobs'; $result = departmentPermissionInvoke($otherController, ['tcm.diagnosis/lists']); departmentPermissionExpect($result['code'] === 0, 'alias is limited to the exact department controller'); $originalGrant = departmentPermissionInvoke(new DiagnosisDepartmentRequest(), ['dept.dept/all']); departmentPermissionExpect($originalGrant['code'] === 1 && count($originalGrant['data']) === 2 && count($originalGrant['data'][0]['children']) === 2 && DataScopeService::$actors === [], 'original all-departments permission retains its unscoped response'); $originalScoped = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => 1]), ['dept.dept/all']); departmentPermissionExpect($originalScoped['data'] === $scopedTree && count(DataScopeService::$actors) === 1, 'original department grant can still request a scoped tree'); $root = departmentPermissionInvoke(new DiagnosisDepartmentRequest([], 'GET', [], true), []); departmentPermissionExpect($root['code'] === 1 && count($root['data']) === 2 && AdminAuthCache::$actors === [], 'root retains the existing middleware bypass and full tree'); DataScopeService::$allowed = null; $rootScoped = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => 1], 'GET', [], true), []); departmentPermissionExpect($rootScoped['data'] === $root['data'] && count(DataScopeService::$actors) === 1, 'root scoped requests still execute the controller data-scope contract'); DataScopeService::$allowed = []; $empty = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => '1']), ['tcm.diagnosis/lists']); departmentPermissionExpect($empty === ['code' => 1, 'show' => 0, 'msg' => '', 'data' => []], 'authorized empty scope remains a successful empty tree, never a full-data fallback'); $legacy = departmentPermissionInvoke(new DiagnosisDepartmentRequest(), [], []); departmentPermissionExpect($legacy['code'] === 1 && count($legacy['data']) === 2 && AdminAuthCache::$allUriReads === 1, 'unmatched requests continue through the existing unregistered-route policy unchanged'); echo "Diagnosis department filter permission: {$assertions} assertions passed (no DB or network)\n"; }