Files
twenty/tools/eslint-rules/rules/rest-api-methods-should-be-guarded.spec.ts
T
Félix Malfait 9a80164cf3 Add comprehensive permission guard coverage across GraphQL and REST endpoints (#15739)
This PR enhances our security model by ensuring all GraphQL resolvers
and REST API endpoints have appropriate permission guards.

## Changes

### ESLint Rules
- Enhanced `graphql-resolvers-should-be-guarded` to require permission
guards on all resolvers (Query, Mutation, Subscription), not just
mutations
- Enhanced `rest-api-methods-should-be-guarded` to require permission
guards on all REST endpoints (GET, POST, PUT, PATCH, DELETE), not just
mutating methods
- Both rules now enforce consistent security: authentication guards +
permission guards for all endpoints

### Permission Guards Added

**Public Endpoints** - Added `NoPermissionGuard`:
- Auth-related queries (checkUserExists, findWorkspaceFromInviteHash,
validatePasswordResetToken)
- Billing webhooks (Stripe callbacks)
- SSO callbacks (SAML authentication)
- Workflow webhooks
- Cloudflare webhooks
- Route trigger endpoints
- GraphQL subscriptions
- Current workspace queries
- Geo-map address autocomplete
- View-related read operations (view-field, view-filter, view-group,
view-sort, view-filter-group)

**Settings Permission Guards** - Added `SettingsPermissionGuard`:
- API Keys management: `PermissionFlagType.API_KEYS_AND_WEBHOOKS`
- Webhooks management: `PermissionFlagType.API_KEYS_AND_WEBHOOKS`
- Page Layouts (write operations): `PermissionFlagType.LAYOUTS`
- REST Metadata API: `PermissionFlagType.DATA_MODEL`
- Agent operations: `PermissionFlagType.AI`
- Remote servers: `PermissionFlagType.DATA_MODEL`
- Remote tables: `PermissionFlagType.DATA_MODEL`
- Serverless functions: `PermissionFlagType.WORKFLOWS`

**Custom Permission Guards** - Added `CustomPermissionGuard`:
- REST Core API (permissions checked at query execution layer)
- Timeline calendar events (permission checks in service layer)
- Timeline messaging (permission checks in service layer)
- Search operations (permission checks in service layer)
- View operations (permission checks via dedicated view permission
guards)

### View Permission Guards
- Created dedicated `FindManyViewsPermissionGuard` and
`FindOneViewPermissionGuard` for reading views
- Created `CreateViewPermissionGuard` for view creation with
visibility-based permission checks
- All view child entities (view-field, view-filter, view-sort,
view-group, view-filter-group) use `NoPermissionGuard` for reads
- Write operations on view child entities use dedicated permission
guards that check parent view access

### Page Layout Permissions
- Read operations (GET/Query) now use `NoPermissionGuard` - users can
view layouts without LAYOUTS permission
- Write operations (POST/PATCH/DELETE/Mutation) require
`SettingsPermissionGuard(PermissionFlagType.LAYOUTS)`
- Applied consistently across page-layout, page-layout-tab, and
page-layout-widget endpoints

## Security Model
All endpoints now follow a consistent pattern:
1. **Authentication**: `UserAuthGuard`, `WorkspaceAuthGuard`, or
`PublicEndpointGuard`
2. **Authorization**: One of:
- `SettingsPermissionGuard(PermissionFlagType.XXX)` - for settings/admin
operations
- `CustomPermissionGuard` - when permissions are checked in service/data
layer
   - `NoPermissionGuard` - for public or non-sensitive read operations

The ESLint rules automatically enforce this pattern going forward.

## Stats
- 47 files changed
- 603 insertions, 163 deletions
- 3 new guard files created
2025-11-10 12:17:38 +01:00

305 lines
5.7 KiB
TypeScript

import { TSESLint } from '@typescript-eslint/utils';
import { rule, RULE_NAME } from './rest-api-methods-should-be-guarded';
const ruleTester = new TSESLint.RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
});
ruleTester.run(RULE_NAME, rule, {
valid: [
{
code: `
class TestController {
@Get()
@UseGuards(UserAuthGuard, NoPermissionGuard)
testMethod() {}
}
`,
},
{
code: `
class TestController {
@Get()
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
testMethod() {}
}
`,
},
{
code: `
class TestController {
@Get()
@UseGuards(PublicEndpoint, NoPermissionGuard)
testMethod() {}
}
`,
},
{
code: `
class TestController {
@Get()
@UseGuards(CaptchaGuard, PublicEndpoint, NoPermissionGuard)
testMethod() {}
}
`,
},
{
code: `
@UseGuards(UserAuthGuard, NoPermissionGuard)
class TestController {
@Get()
testMethod() {}
}
`,
},
{
code: `
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
class TestController {
@Get()
testMethod() {}
}
`,
},
{
code: `
@UseGuards(PublicEndpoint, NoPermissionGuard)
class TestController {
@Get()
testMethod() {}
}
`,
},
{
code: `
class TestController {
@Post()
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
createMethod() {}
}
`,
},
{
code: `
class TestController {
@Put()
@UseGuards(WorkspaceAuthGuard, UpdatePermissionGuard)
updateMethod() {}
}
`,
},
{
code: `
class TestController {
@Patch()
@UseGuards(WorkspaceAuthGuard, NoPermissionGuard)
patchMethod() {}
}
`,
},
{
code: `
class TestController {
@Delete()
@UseGuards(WorkspaceAuthGuard, DeletePermissionGuard)
deleteMethod() {}
}
`,
},
{
code: `
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
class TestController {
@Post()
createMethod() {}
}
`,
},
{
code: `
@UseGuards(WorkspaceAuthGuard, SettingsPermissionsGuard(PermissionFlagType.WORKSPACE))
class TestController {
@Delete()
deleteMethod() {}
}
`,
},
{
code: `
class TestController {
regularMethod() {}
}
`,
},
],
invalid: [
{
code: `
class TestController {
@Get()
testMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
class TestController {
@Post()
testMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
class TestController {
@Get()
@UseGuards(WorkspaceAuthGuard)
testMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
class TestController {
@Get()
@UseGuards(CaptchaGuard)
testMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
@UseGuards(CaptchaGuard)
class TestController {
@Get()
testMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
@UseGuards(WorkspaceAuthGuard)
class TestController {
@Get()
testMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
class TestController {
@Post()
@UseGuards(WorkspaceAuthGuard)
createMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
class TestController {
@Put()
@UseGuards(WorkspaceAuthGuard)
updateMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
class TestController {
@Patch()
@UseGuards(WorkspaceAuthGuard)
patchMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
class TestController {
@Delete()
@UseGuards(WorkspaceAuthGuard)
deleteMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
@UseGuards(WorkspaceAuthGuard)
class TestController {
@Post()
createMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
@UseGuards(WorkspaceAuthGuard)
class TestController {
@Delete()
deleteMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
],
});