253 lines
12 KiB
PHP
253 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
// Execute the real middleware, department controller and tree filtering logic.
|
|
// Only authentication storage, department rows and scope lookup are in memory.
|
|
namespace app\adminapi\logic {
|
|
final class LoginLogic
|
|
{
|
|
public static function adminMustBindWorkWechat(array $adminInfo): bool { return false; }
|
|
}
|
|
}
|
|
|
|
namespace app\common\cache {
|
|
final class AdminAuthCache
|
|
{
|
|
public static array $permissions = [];
|
|
public static array $allUris = [];
|
|
public static array $actors = [];
|
|
public static int $allUriReads = 0;
|
|
public function __construct(int $adminId) { self::$actors[] = $adminId; }
|
|
public function getAdminUri(): array { return self::$permissions; }
|
|
public function getAllUri(): array { self::$allUriReads++; return self::$allUris; }
|
|
}
|
|
}
|
|
|
|
namespace app\common\service {
|
|
final class JsonService
|
|
{
|
|
public static function fail(string $msg = 'fail', array $data = [], int $code = 0, int $show = 1): array
|
|
{
|
|
return compact('code', 'show', 'msg', 'data');
|
|
}
|
|
public static function data(array $data): array
|
|
{
|
|
return ['code' => 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";
|
|
}
|