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
188 lines
7.2 KiB
TypeScript
188 lines
7.2 KiB
TypeScript
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import graphqlTypeJson from 'graphql-type-json';
|
|
import { Repository } from 'typeorm';
|
|
|
|
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
|
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
|
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
|
import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard';
|
|
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
|
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
|
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
|
import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input';
|
|
import { GetServerlessFunctionSourceCodeInput } from 'src/engine/metadata-modules/serverless-function/dtos/get-serverless-function-source-code.input';
|
|
import { PublishServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/publish-serverless-function.input';
|
|
import { ServerlessFunctionExecutionResultDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
|
import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input';
|
|
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
|
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
|
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
|
|
|
@UseGuards(
|
|
WorkspaceAuthGuard,
|
|
FeatureFlagGuard,
|
|
SettingsPermissionGuard(PermissionFlagType.WORKFLOWS),
|
|
)
|
|
@Resolver()
|
|
@UsePipes(ResolverValidationPipe)
|
|
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
|
|
export class ServerlessFunctionResolver {
|
|
constructor(
|
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
|
@InjectRepository(ServerlessFunctionEntity)
|
|
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
|
) {}
|
|
|
|
@Query(() => ServerlessFunctionDTO)
|
|
async findOneServerlessFunction(
|
|
@Args('input') { id }: ServerlessFunctionIdInput,
|
|
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
|
) {
|
|
try {
|
|
return await this.serverlessFunctionRepository.findOneOrFail({
|
|
where: {
|
|
id,
|
|
workspaceId,
|
|
},
|
|
relations: ['cronTriggers', 'databaseEventTriggers', 'routeTriggers'],
|
|
});
|
|
} catch (error) {
|
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
}
|
|
}
|
|
|
|
@Query(() => [ServerlessFunctionDTO])
|
|
async findManyServerlessFunctions(
|
|
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
|
) {
|
|
try {
|
|
return this.serverlessFunctionRepository.find({
|
|
where: { workspaceId },
|
|
relations: ['cronTriggers', 'databaseEventTriggers', 'routeTriggers'],
|
|
});
|
|
} catch (error) {
|
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
}
|
|
}
|
|
|
|
@Query(() => graphqlTypeJson)
|
|
async getAvailablePackages(@Args('input') { id }: ServerlessFunctionIdInput) {
|
|
try {
|
|
return await this.serverlessFunctionService.getAvailablePackages(id);
|
|
} catch (error) {
|
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
}
|
|
}
|
|
|
|
@Query(() => graphqlTypeJson, { nullable: true })
|
|
async getServerlessFunctionSourceCode(
|
|
@Args('input') input: GetServerlessFunctionSourceCodeInput,
|
|
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
|
) {
|
|
try {
|
|
return await this.serverlessFunctionService.getServerlessFunctionSourceCode(
|
|
workspaceId,
|
|
input.id,
|
|
input.version,
|
|
);
|
|
} catch (error) {
|
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
}
|
|
}
|
|
|
|
@Mutation(() => ServerlessFunctionDTO)
|
|
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
|
|
async deleteOneServerlessFunction(
|
|
@Args('input') input: ServerlessFunctionIdInput,
|
|
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
|
) {
|
|
try {
|
|
return await this.serverlessFunctionService.deleteOneServerlessFunction({
|
|
id: input.id,
|
|
workspaceId,
|
|
});
|
|
} catch (error) {
|
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
}
|
|
}
|
|
|
|
@Mutation(() => ServerlessFunctionDTO)
|
|
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
|
|
async updateOneServerlessFunction(
|
|
@Args('input')
|
|
input: UpdateServerlessFunctionInput,
|
|
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
|
) {
|
|
try {
|
|
return await this.serverlessFunctionService.updateOneServerlessFunction(
|
|
input,
|
|
workspaceId,
|
|
);
|
|
} catch (error) {
|
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
}
|
|
}
|
|
|
|
@Mutation(() => ServerlessFunctionDTO)
|
|
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
|
|
async createOneServerlessFunction(
|
|
@Args('input')
|
|
input: CreateServerlessFunctionInput,
|
|
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
|
) {
|
|
try {
|
|
return await this.serverlessFunctionService.createOneServerlessFunction(
|
|
input,
|
|
workspaceId,
|
|
);
|
|
} catch (error) {
|
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
}
|
|
}
|
|
|
|
@Mutation(() => ServerlessFunctionExecutionResultDTO)
|
|
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
|
|
async executeOneServerlessFunction(
|
|
@Args('input') input: ExecuteServerlessFunctionInput,
|
|
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
|
) {
|
|
try {
|
|
const { id, payload, version } = input;
|
|
|
|
return await this.serverlessFunctionService.executeOneServerlessFunction(
|
|
id,
|
|
workspaceId,
|
|
payload,
|
|
version,
|
|
);
|
|
} catch (error) {
|
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
}
|
|
}
|
|
|
|
@Mutation(() => ServerlessFunctionDTO)
|
|
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
|
|
async publishServerlessFunction(
|
|
@Args('input') input: PublishServerlessFunctionInput,
|
|
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
|
) {
|
|
try {
|
|
const { id } = input;
|
|
|
|
return await this.serverlessFunctionService.publishOneServerlessFunctionOrFail(
|
|
id,
|
|
workspaceId,
|
|
);
|
|
} catch (error) {
|
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
}
|
|
}
|
|
}
|