n8n:protect-endpoints
- Repo stars 190,957
- Author updated Live
- Author repo n8n
- Domain
- Writing
- Compatible agents
-
- Claude Code
- Cursor
- Cline
- Codex
- Windsurf
- Gemini CLI
- +20
- Trust score
- 88 / 100 · community maintained
- Author / version / license
- @n8n-io · no license declared
- Token usage
- Lean
- Setup complexity
- Guided setup
- External API key
- Not required
- Operating systems
- Unspecified (assume cross-platform)
- Runtime requirements
- No special requirements
- Permissions
-
- Read-only
- Write / modify
- Shell exec
- Network behavior
- External requests
- Install commands
- 26 variants
Profile is derived at build time from SKILL.md and install vectors. Subject to drift from author intent.
Heads up: 未限定 allowed-tools,默认拥有全部工具权限。
---
name: n8n:protect-endpoints
description: Applies n8n's RBAC scope decorators to REST endpoints. Use when creating a new @RestController…
category: writing
runtime: no special runtime
---
# n8n:protect-endpoints output preview
## PART A: Task fit
- Use case: Applies n8n's RBAC scope decorators to REST endpoints. Use when creating a new @RestController, adding any @Get/@Post/@Put/@Patch/@Delete route to an existing controller, or reviewing endpoint authorization. Every authenticated endpoint must be gated by @ProjectScope or @GlobalScope..
- Inputs: target material, constraints, expected output, and acceptance criteria.
- Evidence boundary: follow “Decision / Apply the decorator / When the scope doesn't exist yet” and do not present inference as author intent.
## PART B: Execution result
- **01** The card summarizes the use case; runtime output centers on “Applies n8n's RBAC scope decorators to REST endpoints. Use when creating a new @RestController, adding any @Get/@Post/@Put/@Patch/@Delete route to an existing controller, or reviewing endpoint authorization. Every authenticated endpoint must be gated by @ProjectScope or @GlobalScope.”.
- **02** When the source has headings, the agent prioritizes “Decision / Apply the decorator / When the scope doesn't exist yet” so the result follows the author’s structure.
- **03** Typical output includes task judgment, concrete steps, required commands or file edits, validation, and follow-up options.
- **04** Risk context follows the fingerprint: read files, write/modify files, run shell commands; may access external network resources; usually needs no extra API key.
## Running Rules
- read files, write/modify files, run shell commands; may access external network resources; usually needs no extra API key.
- Validate with a small sample before expanding scope.
- Return the result, validation criteria, and next iteration options. The source does not require a stable slash command. After installation, invoke the skill by name and describe the task.
Name target files or source material, expected output, forbidden changes, and whether network or shell access is allowed. Permission fingerprint: read files, write/modify files, run shell commands.
Start with a small task and check whether the result follows “Decision / Apply the decorator / When the scope doesn't exist yet”. Inspect diffs, logs, previews, or tests before expanding scope.
Confirm the final output includes a concrete result, evidence, and next action. If it stays generic, tighten inputs, boundaries, and acceptance criteria.
---
name: n8n:protect-endpoints
description: Applies n8n's RBAC scope decorators to REST endpoints. Use when creating a new @RestController…
category: writing
source: n8n-io/n8n
---
# n8n:protect-endpoints
## When to use
- Applies n8n's RBAC scope decorators to REST endpoints. Use when creating a new @RestController, adding any @Get/@Post/…
- Use it when the task has clear inputs, repeatable steps, and validation criteria.
## What to provide
- Target material, scope, expected result, and forbidden changes.
- Whether network, commands, file writes, or external services are allowed.
## Execution rules
- Organize steps around “Decision / Apply the decorator / When the scope doesn't exist yet” and keep inference separate from source facts.
- read files, write/modify files, run shell commands; may access external network resources; usually needs no extra API key.
- Validate with a small sample before expanding the task.
## Output requirements
- Return the deliverable, key evidence, validation method, and next action.
- Mark missing information as unknown; do not invent commands, platforms, or dependencies. The author source anchors workflow facts; repository files anchor sources and commands; Fluxly only adds fit, limitations, and quality judgment.
skill "n8n:protect-endpoints" {
input -> user goal + target files + boundaries + acceptance criteria
context -> Decision / Apply the decorator / When the scope doesn't exist yet
rules -> SKILL.md triggers / order / output contract
runtime -> no special runtime | read files, write/modify files, run shell commands | may access external network resources
guardrails -> usually needs no extra API key + small-sample validation + diff/log review
output -> copyable result + checklist + next iteration
} Protect REST endpoints with RBAC
Rule: every authenticated route on a @RestController MUST carry an access-scope decorator. If you add a route without one, the IDOR/permission bypass is on you.
Decision
URL has :projectId → @ProjectScope('<resource>:<op>')
URL has no project → @GlobalScope('<resource>:<op>')
skipAuth: true → no decorator + comment explaining alternate auth
@ProjectScope succeeds if the user has the scope globally OR in the project named in the URL. @GlobalScope ignores project relations entirely.
Both decorators come from @n8n/decorators. The middleware lives in packages/cli/src/controller.registry.ts (createScopedMiddleware) and resolves access via userHasScopes in packages/cli/src/permissions.ee/check-access.ts.
Apply the decorator
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(...) { ... }
}
Conventions:
- One decorator per route, placed directly under the HTTP-method decorator.
- Use the most specific scope that fits. Reuse
*:updatefor state-changing actions likepublish/unpublish/buildunless the resource needs to gate them separately (seeworkflow:publishfor the precedent). - Routes without
:projectIdand not global-only operations are usually a design smell — flag it.
When the scope doesn't exist yet
Add the resource and ops in packages/@n8n/permissions/:
src/constants.ee.ts— add toRESOURCES(alphabetical):
Thewidget: [...DEFAULT_OPERATIONS, 'execute'] as const,Scopeunion (<resource>:<op>template-literal type) auto-derives.src/scope-information.ts— add a display name + description per scope.src/roles/scopes/project-scopes.ee.ts— add to project roles. Match theworkflowprecedent unless product says otherwise:REGULAR_PROJECT_ADMIN_SCOPES,PERSONAL_PROJECT_OWNER_SCOPES,PROJECT_EDITOR_SCOPES→ all CRUDL+execute scopes.PROJECT_VIEWER_SCOPES→ read/list/execute only.PROJECT_CHAT_USER_SCOPES→ execute only (if applicable).
src/roles/scopes/global-scopes.ee.ts— add toGLOBAL_OWNER_SCOPES(admin inherits viaconcat()). Do not add to member/chat-user globals — they get scopes via project relations.- Personal-space publishing: if you add a
<resource>:publishscope, also append it toPERSONAL_SPACE_PUBLISHING_SETTING.scopesinconstants.ee.tsso personal-owner gating matchesworkflow:publish. - Frontend wiring — three files in the editor; skipping any of them means the new scopes will not appear in the project-role configuration UI:
packages/frontend/editor-ui/src/app/stores/rbac.store.ts— add<resource>: {}toscopesByResourceId(typecheck will fail otherwise).packages/frontend/editor-ui/src/features/project-roles/projectRoleScopes.ts— add the resource toUI_OPERATIONS(operations to render in the permissions matrix, in display order) and toSCOPE_TYPES(the order the resource group appears on the page).packages/frontend/@n8n/i18n/src/locales/en.json— addprojectRoles.<resource>:<op>(column label) andprojectRoles.<resource>:<op>.tooltip(hover description) for every op, plusprojectRoles.type.<resource>(the group header).
- Snapshot — update
packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snapto include the new<resource>:*entries.
No DB migration needed — AuthRolesService.init() syncs scopes/roles on every startup. Custom team roles created in the UI are not auto-updated; mention this in the PR description.
Public / unauthenticated routes
{ skipAuth: true } skips the auth middleware → req.user is undefined → adding @ProjectScope would 401 every call. Public routes (third-party webhooks, signed callbacks) must:
- Omit the scope decorator.
- Authenticate via signature/HMAC verification inside the handler (or another route-specific mechanism).
- Carry a comment explaining why no scope is applied, so the next reviewer doesn't try to "fix" it.
Example:
// 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(...) { ... }
Verify with a route-metadata test
Add a regression test that fails when a future route is added without a scope. Iterate every route on the controller via ControllerRegistryMetadata and assert the gate.
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);
},
);
});
Defense in depth (still required)
Decorator alone is not enough when handlers leak data via downstream calls. Service/repository methods should still filter by projectId (or user-scoped helpers like findByUser). The decorator gates who can call this URL; the service gates what they can read. Both, always.
Reference patterns
- Project-scoped 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. - Mixed global + project:
packages/cli/src/controllers/project.controller.ts.
Decide Fit First
Design Intent
How To Use It
Boundaries And Review