mcpField
N
n8n:protect-endpoints

n8n:protect-endpoints

github.com/n8n-io安全209.0k244 次安装今天更新
代码仓库

关于 n8n:protect-endpoints

面向 n8n REST 控制器开发的技能,为已认证路由应用 RBAC 访问范围规则,并说明新增权限范围所需的配置和测试。

主要功能

  1. 01为路由分配项目或全局范围
  2. 02说明如何添加权限范围
  3. 03涵盖未认证路由处理
  4. 04要求路由元数据回归测试
  5. 05强调服务层数据过滤

适用场景

  1. 01为 REST 路由添加访问检查
  2. 02在 n8n 中引入权限范围
  3. 03审查公开 Webhook 的认证
  4. 04防止跨项目数据泄露

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/n8n-io/n8n --skill n8n:protect-endpoints

安装后在 Claude Code 中输入/n8n:protect-endpoints确认已加载。

详细介绍

使用 RBAC 保护 REST 端点

规则: @RestController 上的每个已认证路由都必须带有访问范围装饰器。如果添加了不带装饰器的路由,IDOR/权限绕过的责任由你承担。

决策

URL has :projectId  → @ProjectScope('<resource>:<op>')
URL has no project  → @GlobalScope('<resource>:<op>')
skipAuth: true      → no decorator + comment explaining alternate auth

如果用户在全局范围内 或在 URL 中指定的项目内 拥有相应范围,@ProjectScope 就会通过。@GlobalScope 则完全忽略项目关系。

这两个装饰器都来自 @n8n/decorators。中间件位于 packages/cli/src/controller.registry.ts(createScopedMiddleware),通过 packages/cli/src/permissions.ee/check-access.ts 中的 userHasScopes 解析访问权限。

应用装饰器

import { Get, Post, ProjectScope, RestController } from '@n8n/decorators';

@RestController('/projects/:projectId/widgets')
export class WidgetsController {
  @Post('/')
  @ProjectScope('widget:create')          // create
  async create(...) { ... }

  @Get('/:widgetId')
  @ProjectScope('widget:read')            // read one
  async get(...) { ... }

  @Get('/')
  @ProjectScope('widget:list')            // list
  async list(...) { ... }

  @Patch('/:widgetId')
  @ProjectScope('widget:update')          // update
  async update(...) { ... }

  @Delete('/:widgetId')
  @ProjectScope('widget:delete')          // delete
  async delete(...) { ... }
}

约定:

  • 每个路由使用一个装饰器,紧跟在 HTTP 方法装饰器下方。
  • 使用适用范围内最具体的范围。对于 publish/unpublish/build 等会更改状态的操作,复用 *:update,除非该资源需要单独控制这些操作(参见先例 workflow:publish)。
  • 不带 :projectId 且并非仅限全局操作的路由通常表明设计存在问题——请标记出来。

尚无对应范围时

在 packages/@n8n/permissions/ 中添加资源和操作:

  1. src/constants.ee.ts — 将资源添加到 RESOURCES(按字母顺序):
    widget: [...DEFAULT_OPERATIONS, 'execute'] as const,
    
    Scope 联合类型(<resource>:<op> 模板字面量类型)会自动派生。
  2. src/scope-information.ts — 为每个范围添加显示名称和说明。
  3. src/roles/scopes/project-scopes.ee.ts — 添加到项目角色中。除非产品另有要求,否则遵循 workflow 先例:
    • REGULAR_PROJECT_ADMIN_SCOPES、PERSONAL_PROJECT_OWNER_SCOPES、PROJECT_EDITOR_SCOPES → 所有 CRUDL+execute 范围。
    • PROJECT_VIEWER_SCOPES → 仅 read/list/execute。
    • PROJECT_CHAT_USER_SCOPES → 仅 execute(如适用)。
  4. src/roles/scopes/global-scopes.ee.ts — 添加到 GLOBAL_OWNER_SCOPES(管理员通过 concat() 继承)。不要添加到成员/聊天用户的全局范围中——他们通过项目关系获取范围。
  5. 个人空间发布:如果添加了 <resource>:publish 范围,还要将其追加到 constants.ee.ts 中的 PERSONAL_SPACE_PUBLISHING_SETTING.scopes,使个人所有者的权限控制与 workflow:publish 保持一致。
  6. src/roles/custom-role-scopes.ee.ts — 将资源和要在权限矩阵中显示的操作添加到 PROJECT_CUSTOM_ROLE_OPERATIONS,并按显示顺序排列。编辑器的 SCOPES/SCOPE_TYPES 和保存时的允许列表 PROJECT_CUSTOM_ROLE_SCOPES 都由此派生:此处缺少资源,就无法在 UI 中显示;此处缺少范围,则保存时会被拒绝。
  7. 前端接线 — 共涉及三个文件;漏掉任何一个,新范围都不会出现在项目角色配置 UI 中:
    • packages/frontend/@n8n/stores/src/rbac.store.ts — 在 scopesByResourceId 中添加 <resource>: {}(否则类型检查会失败)。
    • packages/frontend/editor-ui/src/features/roles/project/projectRoleScopes.ts — 将资源添加到 SCOPE_TYPES(决定资源组在页面上的显示顺序)。
    • packages/frontend/@n8n/i18n/src/locales/en.json — 为每个操作添加 projectRoles.<resource>:<op>(列标签)和 projectRoles.<resource>:<op>.tooltip(悬停说明),并添加 projectRoles.type.<resource>(组标题)。
  8. 快照 — 更新 packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snap,纳入新的 <resource>:* 条目。

无需数据库迁移——AuthRolesService.init() 会在每次启动时同步范围/角色。通过 UI 创建的自定义团队角色不会自动更新;请在 PR 描述中说明这一点。

公开 / 未经身份验证的路由

{ skipAuth: true } 会跳过身份验证中间件 → req.user 未定义 → 添加 @ProjectScope 会导致每次调用都返回 401。公开路由(第三方 Webhook、已签名的回调)必须:

  1. 省略作用域装饰器。
  2. 在处理程序中通过签名 / HMAC 验证进行身份验证(或使用其他特定于路由的机制)。
  3. 添加注释,说明为何不应用作用域,以免下一位审查者试图“修复”它。

示例:

// Third-party webhook callback: do not add @ProjectScope. Auth happens
// via per-platform signature verification inside webhookHandler, and
// :projectId is unused in the (agentId, platform) lookup.
@Post('/:agentId/webhooks/:platform', { skipAuth: true, allowBots: true })
async handleWebhook(...) { ... }

使用路由元数据测试进行验证

添加一个回归测试,确保未来新增路由但未设置作用域时测试会失败。通过 ControllerRegistryMetadata 遍历控制器上的每条路由,并断言其访问控制。

import { ControllerRegistryMetadata } from '@n8n/decorators';
import { Container } from '@n8n/di';
import { WidgetsController } from '../widgets.controller';

const UNAUTHENTICATED_HANDLERS = new Set<string>(); // add public handler names here

const metadata = Container.get(ControllerRegistryMetadata).getControllerMetadata(
  WidgetsController as never,
);
const routeCases = Array.from(metadata.routes.entries()).map(([handlerName, route]) => ({
  handlerName, route,
}));

describe('WidgetsController route access scopes', () => {
  it.each(routeCases)(
    '$handlerName is gated by a project-scoped widget:* check',
    ({ handlerName, route }) => {
      if (UNAUTHENTICATED_HANDLERS.has(handlerName)) {
        expect(route.accessScope).toBeUndefined();
        expect(route.skipAuth).toBe(true);
        return;
      }
      expect(route.accessScope).toBeDefined();
      expect(route.accessScope?.globalOnly).toBe(false);
      expect(route.accessScope?.scope.startsWith('widget:')).toBe(true);
    },
  );
});

纵深防御(仍然必需)

仅靠装饰器还不够,处理程序仍可能通过下游调用泄露数据。服务 / 仓库方法仍应按 projectId 进行过滤(或使用 findByUser 这类按用户作用域限定的辅助方法)。装饰器限制谁可以调用此 URL;服务限制他们可以读取什么。两者始终都要做到。

参考模式

  • 项目作用域 CRUD:packages/cli/src/workflows/workflows.controller.ts、packages/cli/src/credentials/credentials.controller.ts、packages/cli/src/modules/data-table/data-table.controller.ts。
  • 混合全局 + 项目作用域:packages/cli/src/controllers/project.controller.ts。

评论

支持 Markdown