feat(auth): AuthService 新增 getAuthPayload 权限载荷构建

This commit is contained in:
augushong
2026-10-04 00:47:11 +00:00
parent 1a27454c38
commit d3a44a2e1c
2 changed files with 171 additions and 0 deletions

View File

@@ -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

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace tests\Feature;
use app\common\constants\AdminConstant;
use app\common\service\AuthService;
use app\common\test\TestCase;
use think\facade\Db;
/**
* AuthService::getAuthPayload 权限载荷构建测试.
*
* 覆盖三组契约:
* a) 超管(AdminConstant::SUPER_ADMIN_ID)→ all === true
* b) 仅授权 system.quick/index 与 system.quick/read 的普通管理员
* → all === false,nodes['system.quick'] 恰为两者,不含 add/edit/delete
* c) 载荷格式:nodes 每个键不含 '/',每个值为非空字符串数组
*
* fixture 沿用 McpServiceTest 既定模式(ulthon-testing 技能约定):
* 事务内自造全新管理员/角色/节点授权,避开 SystemAuthNode 60 秒查询缓存与
* adminInfo autoCache 窗口;tearDown 自动回滚,无残留。
*/
class AuthPayloadTest extends TestCase
{
/** 受测控制器(system.quick 的 index/read 动作节点均经 CurdTrait 注解注册,auth 默认 true). */
private const CONTROLLER = 'system.quick';
/** 授权动作(恰为载荷期望值). */
private const ALLOWED_ACTIONS = ['index', 'read'];
/** 不应出现的动作(同控制器其余 auth 受控动作). */
private const FORBIDDEN_ACTIONS = ['add', 'edit', 'delete'];
/**
* fixture:角色(仅授权 index/read)+ 绑定该角色的启用管理员.
*
* @return int 管理员ID
*/
private function createRestrictedAdmin(): int
{
$roleId = Db::name('system_auth')->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, '动作不得为空字符串');
}
}
}
}