9a80164cf3
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
305 lines
5.7 KiB
TypeScript
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',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|