diff --git a/extend/base/common/service/AuthServiceBase.php b/extend/base/common/service/AuthServiceBase.php index ab96f2f..994234e 100644 --- a/extend/base/common/service/AuthServiceBase.php +++ b/extend/base/common/service/AuthServiceBase.php @@ -181,6 +181,45 @@ class AuthServiceBase return $allowedNodes; } + /** + * 构建当前管理员的权限载荷. + * + * 形状契约(全局按钮显隐等前端消费): + * - 超管:['all' => true, 'nodes' => []] + * - 普通管理员:['all' => false, 'nodes' => [控制器 => [动作, ...]]] + * + * 允许集合完全复用 getAdminAllowedNodes(内部走 checkNodeResolved 判定链), + * 本方法不复制判定逻辑;底层 nodeList/adminNode 已带缓存,不再叠加独立缓存。 + * 无 '/' 或动作段为空的畸形节点(如控制器级节点)跳过。 + * @return array + */ + public function getAuthPayload(): array + { + if ($this->isSuperAdmin()) { + return ['all' => true, 'nodes' => []]; + } + + // 候选仅保留动作节点(含 '/'):控制器级节点无 auth 键,进判定链会触发 + // Undefined array key,且它们本就因无 '/' 不会进入载荷,提前过滤语义等价。 + $candidates = array_filter( + array_keys($this->getNodeList()), + fn ($node) => strpos((string) $node, '/') !== false + ); + + $allowed = $this->getAdminAllowedNodes($candidates); + + $nodes = []; + foreach ($allowed as $node) { + $parts = explode('/', $node, 2); + if (count($parts) !== 2 || $parts[1] === '') { + continue; + } + $nodes[$parts[0]][] = $parts[1]; + } + + return ['all' => false, 'nodes' => $nodes]; + } + /** * 获取当前节点. * @return string diff --git a/tests/Feature/AuthPayloadTest.php b/tests/Feature/AuthPayloadTest.php new file mode 100644 index 0000000..adda225 --- /dev/null +++ b/tests/Feature/AuthPayloadTest.php @@ -0,0 +1,132 @@ +insertGetId([ + 'title' => 'apt' . uniqid(), + 'status' => 2, // system_auth 枚举:1=禁用 2=启用 + 'sort' => 100, + 'create_time' => time(), + 'update_time' => time(), + 'delete_time' => 0, + ]); + foreach (self::ALLOWED_ACTIONS as $action) { + Db::name('system_auth_node')->insert([ + 'auth_id' => $roleId, + 'node' => self::CONTROLLER . '/' . $action, + ]); + } + + return Db::name('system_admin')->insertGetId([ + 'username' => 'apt_' . uniqid(), + 'password' => 'should-not-leak', + 'auth_ids' => (string) $roleId, + 'status' => 1, + 'create_time' => time(), + 'update_time' => time(), + 'delete_time' => 0, + ]); + } + + // ========================================================================= + // a) 超管 + // ========================================================================= + + public function test_super_admin_payload_is_all_true(): void + { + $service = new AuthService(AdminConstant::SUPER_ADMIN_ID); + + $payload = $service->getAuthPayload(); + + $this->assertTrue($payload['all']); + $this->assertSame([], $payload['nodes']); + } + + // ========================================================================= + // b) 普通管理员:按角色授权精确聚合 + // ========================================================================= + + public function test_restricted_admin_payload_contains_only_seeded_nodes(): void + { + $adminId = $this->createRestrictedAdmin(); + $service = new AuthService($adminId); + + $payload = $service->getAuthPayload(); + + $this->assertFalse($payload['all']); + $this->assertArrayHasKey(self::CONTROLLER, $payload['nodes'], 'payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); + $this->assertCount( + count(self::ALLOWED_ACTIONS), + $payload['nodes'][self::CONTROLLER], + 'payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES) + ); + $this->assertEqualsCanonicalizing(self::ALLOWED_ACTIONS, $payload['nodes'][self::CONTROLLER]); + foreach (self::FORBIDDEN_ACTIONS as $action) { + $this->assertNotContains($action, $payload['nodes'][self::CONTROLLER]); + } + } + + // ========================================================================= + // c) 载荷格式契约 + // ========================================================================= + + public function test_payload_nodes_format_contract(): void + { + $adminId = $this->createRestrictedAdmin(); + $service = new AuthService($adminId); + + $payload = $service->getAuthPayload(); + + $this->assertFalse($payload['all']); + $this->assertIsArray($payload['nodes']); + $this->assertNotEmpty($payload['nodes'], '受限管理员 nodes 不应为空'); + foreach ($payload['nodes'] as $controller => $actions) { + $this->assertIsString($controller); + $this->assertStringNotContainsString('/', $controller, '控制器键不得含 /'); + $this->assertIsArray($actions); + $this->assertNotEmpty($actions); + foreach ($actions as $action) { + $this->assertIsString($action); + $this->assertNotSame('', $action, '动作不得为空字符串'); + } + } + } +}