feat(mcp): 密钥授权页,仅创建者拥有的节点可授权(防越权与漂移)

This commit is contained in:
augushong
2026-08-16 21:18:40 +08:00
parent 25c98b6ed6
commit 744a0819f2
7 changed files with 295 additions and 7 deletions

View File

@@ -3,9 +3,12 @@
namespace base\admin\controller\system;
use app\admin\model\SystemAdmin;
use app\admin\model\SystemMcpKeyNode;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnotation;
use app\admin\service\NodeService;
use app\common\controller\AdminController;
use app\common\service\AuthService;
use think\App;
/**
@@ -139,4 +142,120 @@ class McpKeyBase extends AdminController
return $this->fetch();
}
/**
* 授权页:勾选创建者当前权限的子集给密钥.
* @NodeAnotation(title="授权")
*/
public function authorize($id)
{
$row = $this->model->find($id);
empty($row) && $this->error('数据不存在');
// 全量动作节点来自 getNodeTree 展开全部 type=2 节点
$nodeService = new NodeService();
$module_list = $nodeService->getNodeTree();
// 创建者当前节点集合:与 checkNode 完全一致的判定链(含超管直通/auth_on/动态黑名单)
$authService = new AuthService($row->bind_admin_id);
$allActionNodes = [];
foreach ($module_list as $module) {
foreach ($module['children'] as $controller) {
foreach ($controller['children'] as $action) {
$allActionNodes[] = $action['node'];
}
}
}
$creatorAllowedNodes = $authService->getAdminAllowedNodes($allActionNodes, $row->bind_admin_id);
// 已选节点 = 库中记录 ∩ 创建者当前集合(防漂移:创建者权限被回收后旧勾选自动失效)
$checkedNodeList = (new SystemMcpKeyNode())
->where('key_id', $id)
->column('node');
$checkedNodeList = array_values(array_intersect($checkedNodeList, $creatorAllowedNodes));
// 树过滤:动作节点不在创建者集合内整级剔除;无动作节点的控制器、无控制器的模块不显示
foreach ($module_list as $module_key => $module) {
foreach ($module['children'] as $controllerKey => $controller) {
foreach ($controller['children'] as $actionKey => $action) {
if (!in_array($action['node'], $creatorAllowedNodes)) {
unset($module_list[$module_key]['children'][$controllerKey]['children'][$actionKey]);
continue;
}
$checked = in_array($action['node'], $checkedNodeList);
$checked_string = $checked ? 'checked' : '';
$disabled_string = '';
if (!$action['auth']) {
// 免鉴权节点创建者恒有权限,照 auth/authorize 惯例固定勾选并禁用
$checked_string = 'checked';
$disabled_string = 'disabled';
}
$module_list[$module_key]['children'][$controllerKey]['children'][$actionKey]['checked'] = $checked_string;
$module_list[$module_key]['children'][$controllerKey]['children'][$actionKey]['disabled'] = $disabled_string;
}
if (empty($module_list[$module_key]['children'][$controllerKey]['children'])) {
unset($module_list[$module_key]['children'][$controllerKey]);
}
}
if (empty($module_list[$module_key]['children'])) {
unset($module_list[$module_key]);
}
}
$this->assign('row', $row);
$this->assign('checked_node_list', $checkedNodeList);
$this->assign('module_list', array_values($module_list));
return $this->fetch();
}
/**
* 授权保存:先删后插,服务端强校验防越权.
* @NodeAnotation(title="授权保存")
*/
public function saveAuthorize()
{
$this->checkPostRequest();
$id = $this->request->post('id');
$node = (array) $this->request->post('node', []);
$row = $this->model->find($id);
empty($row) && $this->error('数据不存在');
// 服务端强校验:每个提交节点必须 ∈ 创建者当前节点集合(重新计算,防权限已变)
$nodeService = new NodeService();
$allActionNodes = [];
foreach ($nodeService->getNodeTree() as $module) {
foreach ($module['children'] as $controller) {
foreach ($controller['children'] as $action) {
$allActionNodes[] = $action['node'];
}
}
}
$authService = new AuthService($row->bind_admin_id);
$creatorAllowedNodes = $authService->getAdminAllowedNodes($allActionNodes, $row->bind_admin_id);
foreach ($node as $vo) {
if (!in_array($authService->parseNodeStr($vo), $creatorAllowedNodes)) {
$this->error('包含无权限节点');
}
}
try {
$keyNode = new SystemMcpKeyNode();
$keyNode->where('key_id', $id)->delete();
if (!empty($node)) {
$saveAll = [];
foreach ($node as $vo) {
$saveAll[] = [
'key_id' => $id,
'node' => $authService->parseNodeStr($vo),
];
}
$keyNode->saveAll($saveAll);
}
} catch (\Exception $e) {
$this->error('保存失败');
}
$this->success('保存成功');
}
}