n8n:protect-endpoints について
n8nのRESTコントローラー開発向けスキルです。認証済みルートにRBACのアクセススコープを適用し、新しいスコープに必要な設定とテストを説明します。
主な機能
- 01プロジェクトまたはグローバルなスコープ設定
- 02権限スコープの追加手順
- 03未認証ルートの扱い
- 04ルートメタデータの回帰テスト
- 05サービス層でのデータ絞り込み
活用シーン
- 01RESTルートへのアクセス制御追加
- 02n8nへの権限スコープ導入
- 03公開Webhookの認証確認
- 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
@ProjectScope は、ユーザーがスコープをグローバルに、または URL で指定されたプロジェクト内で持っていれば成功します。@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(...) { ... }
}
規約:
- ルートごとにデコレーターを 1 つ使い、HTTP メソッドデコレーターの直下に配置します。
- 条件に合う最も具体的なスコープを使います。リソースごとに個別の制御が必要でない限り、
publish/unpublish/buildなどの状態変更操作には*:updateを再利用します(先例としてworkflow:publishを参照してください)。 :projectIdがなく、グローバル限定の操作でもないルートは、通常、設計上の問題を示しています。見つけたら指摘してください。
スコープがまだ存在しない場合
packages/@n8n/permissions/ にリソースと操作を追加します。
src/constants.ee.ts—RESOURCESに追加します(アルファベット順)。widget: [...DEFAULT_OPERATIONS, 'execute'] as const,Scope共用体(<resource>:<op>のテンプレートリテラル型)は自動的に導出されます。src/scope-information.ts— 各スコープの表示名と説明を追加します。src/roles/scopes/project-scopes.ee.ts— プロジェクトロールに追加します。製品側から別途指示がない限り、workflowの先例に合わせます。REGULAR_PROJECT_ADMIN_SCOPES、PERSONAL_PROJECT_OWNER_SCOPES、PROJECT_EDITOR_SCOPES→ CRUDL と実行の全スコープ。PROJECT_VIEWER_SCOPES→ 読み取り/一覧表示/実行のみ。PROJECT_CHAT_USER_SCOPES→ 実行のみ(該当する場合)。
src/roles/scopes/global-scopes.ee.ts—GLOBAL_OWNER_SCOPESに追加します(admin はconcat()経由で継承します)。メンバー/チャットユーザーのグローバルスコープには追加しないでください。これらのロールはプロジェクトとの関係を通じてスコープを取得します。- パーソナルスペースでの公開:
<resource>:publishスコープを追加する場合は、パーソナルオーナーの制御をworkflow:publishと一致させるため、constants.ee.tsのPERSONAL_SPACE_PUBLISHING_SETTING.scopesにも追加します。 src/roles/custom-role-scopes.ee.ts— 権限マトリックスに表示する操作とともに、リソースをPROJECT_CUSTOM_ROLE_OPERATIONSに表示順で追加します。エディターのSCOPES/SCOPE_TYPESと保存時の許可リストPROJECT_CUSTOM_ROLE_SCOPESは、いずれもここから導出されます。ここにリソースがないと UI に表示されず、スコープがないと保存時に拒否されます。- フロントエンドの設定 — 次の 3 つのファイルが必要です。いずれかを省略すると、新しいスコープはプロジェクトロール設定 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>(グループ見出し)を追加します。
- スナップショット —
packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snapを更新し、新しい<resource>:*エントリーを追加します。
DB のマイグレーションは不要です。AuthRolesService.init() が起動のたびにスコープ/ロールを同期します。UI で作成済みのカスタムチームロールは自動更新されません。PR の説明にその旨を記載してください。
公開/認証不要のルート
{ skipAuth: true } は認証ミドルウェアをスキップするため、req.user は undefined になります。そこに @ProjectScope を追加すると、すべての呼び出しが 401 になります。サードパーティの Webhook や署名付きコールバックなどの公開ルートでは、次の対応が必要です。
- スコープデコレーターを付けないでください。
- ハンドラー内で署名/HMAC 検証(またはルート固有の別の仕組み)を使って認証してください。
- スコープを適用しない理由をコメントに記載し、後続のレビュー担当者が「修正」しようとしないようにしてください。
例:
// 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。