rasuvaeff/yii3-mcp-rbac-bridge

GitHub: rasuvaeff/yii3-mcp-rbac-bridge

为 Yii3 MCP 服务器提供基于 RBAC 的用户级授权,在工具调用和列表过滤中实施细粒度权限控制并绑定会话身份。

Stars: 0 | Forks: 0

# rasuvaeff/yii3-mcp-rbac-bridge [![稳定版本](https://img.shields.io/packagist/v/rasuvaeff/yii3-mcp-rbac-bridge?label=stable&sort_semver=1)](https://packagist.org/packages/rasuvaeff/yii3-mcp-rbac-bridge) [![总下载量](https://img.shields.io/packagist/dt/rasuvaeff/yii3-mcp-rbac-bridge)](https://packagist.org/packages/rasuvaeff/yii3-mcp-rbac-bridge) [![构建](https://img.shields.io/github/actions/workflow/status/rasuvaeff/yii3-mcp-rbac-bridge/build.yml?branch=master)](https://github.com/rasuvaeff/yii3-mcp-rbac-bridge/actions) [![静态分析](https://img.shields.io/github/actions/workflow/status/rasuvaeff/yii3-mcp-rbac-bridge/static-analysis.yml?branch=master&label=static%20analysis)](https://github.com/rasuvaeff/yii3-mcp-rbac-bridge/actions) [![Psalm 等级](https://img.shields.io/badge/psalm-level%201-141F48?logo=psalm&logoColor=white)](https://github.com/rasuvaeff/yii3-mcp-rbac-bridge/blob/master/psalm.xml) [![PHP](https://img.shields.io/packagist/dependency-v/rasuvaeff/yii3-mcp-rbac-bridge/php)](https://packagist.org/packages/rasuvaeff/yii3-mcp-rbac-bridge) [![许可证](https://img.shields.io/packagist/l/rasuvaeff/yii3-mcp-rbac-bridge)](LICENSE.md) [俄语版本](README.ru.md) 基于 Yii3 auth stack 的 [rasuvaeff/yii3-mcp](https://github.com/rasuvaeff/yii3-mcp) 服务器用户级授权 —— 这是面向应用的 OAuth 2.1 替代方案:在每次 `tools/call` 上执行 RBAC 权限校验,支持基于权限的 `tools/list` 过滤,以及针对会话劫持的会话身份绑定。 ## 环境要求 | 需求 | 版本 | |-------------|---------| | PHP | 8.3 – 8.5 | | `rasuvaeff/yii3-mcp` | `^1.1 \|\| ^2.0` | | `yiisoft/access` | `^2.0` (将 `AccessCheckerInterface` 绑定到你的 RBAC 管理器) | | `yiisoft/user` | `^2.0` (当前请求的身份) | ## 安装说明 ``` composer require rasuvaeff/yii3-mcp-rbac-bridge ``` ## 模型:两个授权层 `SharedSecretMiddleware` (属于 yii3-mcp)会被保留 —— 它是**机器授权**:即该 MCP 客户端是否完全有权与该端点进行通信。此 bridge 增加了**用户授权**:即发起调用的已认证用户实际上能做什么。两层同时运行;添加 RBAC 并不意味着要移除共享密钥。 ``` // config/routes.php — secret first (cheap fail-closed), then identity Route::methods(['POST', 'GET', 'DELETE', 'OPTIONS'], '/mcp') ->middleware(SharedSecretMiddleware::class) ->middleware(Authentication::class) // yiisoft/auth: token -> CurrentUser ->action(McpAction::class), ``` ## 用法 ### 1. 在工具上声明权限 ``` use Rasuvaeff\Yii3McpRbacBridge\RequiredPermission; final readonly class OrderTools { #[McpTool(name: 'order.status')] #[RequiredPermission('orders.view')] public function status(string $orderId): string { ... } #[McpTool(name: 'ping')] // no attribute = unrestricted public function ping(): string { ... } } ``` 限制是显式且针对单个工具的:没有声明权限的工具保持开放(在共享密钥保护之后)。在没有 `#[McpTool]` 的方法上使用 `#[RequiredPermission]` 会导致构建失败 —— 永远不会被强制执行的权限是一个 bug,而不是默认行为。通过属性将一个工具名称映射到两个不同的权限也会导致构建失败(因为静默的“最后生效”规则会强制执行任意一个权限);根据设计,显式覆盖优先。 ### 工具名称:map 键必须是什么 `PermissionMap` 的键是工具名称,并且 bridge 会完全按照 yii3-mcp 注册它们的方式来推导每一个键 —— 这样 list 和 call 就永远不会基于不同的名称: | 工具声明 | 注册的名称 = map 的键 | |---|---| | `#[McpTool(name: 'order.status')]` | `order.status` —— 显式名称优先 | | `public function status()` 上的 `#[McpTool]` | `status` —— 方法名 | | `public function __invoke()` 上的 `#[McpTool]` | **类的短名称**(例如 `RefundTool`),**而不是** `__invoke` | `fromToolClasses()` 会为你计算这些键。只有**显式** map (`$overrides` 参数或 `new PermissionMap([...])`)才需要你正确指定键 —— 对于可调用工具(invokable tool),该键是类的短名称: ``` new PermissionMap(['RefundTool' => 'orders.refund']); // invokable RefundTool::__invoke ``` 与任何已注册工具都不匹配的键将不起作用(该工具保持不受限制),因此请保持显式键与上面的工具名称同步。 ### 2. 连接 bridge ``` // config/common/di/mcp-rbac.php use Rasuvaeff\Yii3McpRbacBridge\ { CurrentUserIdentitySource, IdentitySourceInterface, PermissionMap, StaticIdentitySource, }; return [ IdentitySourceInterface::class => CurrentUserIdentitySource::class, PermissionMap::class => static fn () => PermissionMap::fromToolClasses( [OrderTools::class], // same list as the `tools` params // ['order.status' => 'orders.admin'], // optional explicit overrides ), ]; ``` 根据入口点拆分身份连接;stdio 没有 HTTP `CurrentUser`: ``` // config/web/di.php return [IdentitySourceInterface::class => CurrentUserIdentitySource::class]; // config/console/di.php return [ IdentitySourceInterface::class => static fn () => new StaticIdentitySource( getenv('MCP_USER_ID') ?: null, ), ]; ``` ``` // config/params.php 'rasuvaeff/yii3-mcp' => [ 'tools' => [OrderTools::class], 'interceptors' => [ SessionIdentityInterceptor::class, // outermost: binding before anything trusts the session RbacToolCallInterceptor::class, ], 'tool_visibility' => RbacToolVisibility::class, ], ``` `AccessCheckerInterface` 来自你的 RBAC 设置(带有 `rbac-php`/`rbac-db` 存储的 `yiisoft/rbac` 管理器 —— 参见 `suggest`)。 ### 各组件作用 | 类 | 角色 | |---|---| | `RbacToolCallInterceptor` | 拒绝没有映射权限的 `tools/call`(常规 MCP 工具错误,对访客实行 fail-closed) | | `RbacToolVisibility` | 在 `tools/list` 中隐藏相同的工具 —— list 和 call 永远不会产生分歧(同一个 `PermissionMap`) | | `SessionIdentityInterceptor` | 将 MCP 会话绑定到其第一个身份;如果泄露的 `Mcp-Session-Id` 搭配了其他用户的 token 将被拒绝 | | `PermissionMap` | 工具名称 -> 权限:扫描 `#[RequiredPermission]` + 显式覆盖 | | `CurrentUserIdentitySource` | 来自 yiisoft/user `CurrentUser` 的身份 ID(null = 访客);针对 stdio/自定义设置实现 `IdentitySourceInterface` | | `StaticIdentitySource` | 用于控制台/stdio 的固定配置/环境身份;`null` 表示访客 | ## 安全说明 - **两个会话绑定,两个层级。** yii3-mcp 2.0 将每个会话绑定到创建它的 **MCP 客户端**(在 `initialize` 时打上不可变所有者标记);此 bridge 的 `SessionIdentityInterceptor` 在第一次 `tools/call` 时将此会话绑定到**应用程序用户**(yii3-mcp 拦截器链在 `initialize` 时不可见)。这两个层级互为补充:在 `initialize` 和第一次调用之间,会话仍然不带任何用户身份 —— 在该时间窗口内没有任何内容被授权,因此没有任何内容被暴露 —— 但在核心 2.0 上,外部的 MCP 客户端无法再利用泄露的 `Mcp-Session-Id` 潜入该窗口;仅剩下同一客户端内的竞争条件。在核心 1.x 上,客户端所有者层并不存在,首次调用的用户绑定是唯一的会话保护措施。 - **会话所有权拒绝将绕过此 bridge。** yii3-mcp 2.0 会在拦截器链运行之前拒绝外部或无所有者的会话(来自 `McpAction` 的 SDK 形态的 404,来自 `InterceptingReferenceHandler` 的 `SessionOwnershipException`) —— 此类拒绝永远不会到达此 bridge 的 RBAC 拦截器或可见性过滤器。因此,被核心阻止的劫持尝试仅在应用程序/Web 服务器日志中可见,而不在此 bridge 观察到的任何内容中。 - 访客是一等公民:访客会以访客身份绑定会话,并且在每个映射了权限的工具上被拒绝(`AccessCheckerInterface` 接收到 `null`)。内部的访客标记无法通过字面量 `"guest"` 用户 ID 伪造。 - 权限撤销将在下一次调用时应用(fail-closed)。此版本不包含撤销时的实时 `notifications/tools/list_changed` —— SDK 仅在带有事件分发器时才暴露此功能,而 yii3-mcp 的工厂尚未携带该功能。 - 对于 stdio(`mcp:serve`),没有 HTTP 请求:将 `IdentitySourceInterface` 绑定到 `StaticIdentitySource`,或者将共享密钥保留为唯一的(机器)身份。 - 访客加上 `RbacToolVisibility` 在合法情况下可能会产生一个空的 `tools/list`。这是一种授权结果,而不是核心的 `mcp:list` 失败;在将零可见工具视为注册表 bug 之前,请验证控制台的身份绑定。 ## 示例 请参阅 [examples/](examples/) —— 可离线运行。 | 脚本 | 展示内容 | 需要服务器? | |--------|-------|:-------------:| | [`rbac.php`](examples/rbac.php) | 过滤后的列表、允许/拒绝的调用、会话绑定 | 否 | ### 依赖分析器 此叶子包由根应用程序通过 config-plugin 选择,并且在自动加载的源代码目录中可能合法地没有类引用。请保留直接依赖关系:由应用程序(而不是核心包)选择后端或 bridge。将 Composer Dependency Analyser 的异常范围限定在此包: ``` use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; return (new Configuration())->ignoreErrorsOnPackage( 'rasuvaeff/yii3-mcp-rbac-bridge', [ErrorType::UNUSED_DEPENDENCY], ); ``` `composer-require-checker` 检测的是已使用但未声明的符号,而不是未使用的包,因此这种仅配置的依赖关系不需要进行 require-checker 抑制。 ## 开发说明 主机上没有 PHP/Composer —— 通过 `composer:2` 镜像在 Docker 中运行: ``` docker run --rm -v "$PWD":/app -w /app composer:2 composer build ``` 或者使用 Make:`make build`、`make cs-fix`、`make psalm`、`make test`。 ## 许可证 BSD-3-Clause。请参阅 [LICENSE.md](LICENSE.md)。
标签:ffuf, MCP, OpenVAS, PHP, RBAC, Yii3, 中间件, 权限控制