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
This commit is contained in:
Félix Malfait
2025-11-10 12:17:38 +01:00
committed by GitHub
parent ae2d399d6e
commit 9a80164cf3
62 changed files with 727 additions and 147 deletions
@@ -18,6 +18,7 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
interface ChatRequest {
@@ -27,7 +28,7 @@ interface ChatRequest {
}
@Controller('chat')
@UseGuards(WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
export class AiController {
constructor(
private readonly aiService: AiService,
@@ -8,18 +8,19 @@ import {
ValidationPipe,
} from '@nestjs/common';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AuthApiKey } from 'src/engine/decorators/auth/auth-api-key.decorator';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
import { JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc';
import { McpService } from 'src/engine/core-modules/ai/services/mcp.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthApiKey } from 'src/engine/decorators/auth/auth-api-key.decorator';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('mcp')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard, NoPermissionGuard)
@UseFilters(RestApiExceptionFilter)
export class McpController {
constructor(private readonly mcpService: McpService) {}
@@ -20,14 +20,20 @@ import { UpdateApiKeyInput } from 'src/engine/core-modules/api-key/dtos/update-a
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.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';
/**
* rest/apiKeys is deprecated, use rest/metadata/apiKeys instead
* rest/apiKeys will be removed in the future
*/
@Controller(['rest/apiKeys', 'rest/metadata/apiKeys'])
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(
JwtAuthGuard,
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS),
)
@UseFilters(RestApiExceptionFilter)
export class ApiKeyController {
constructor(private readonly apiKeyService: ApiKeyService) {}
@@ -125,7 +125,7 @@ export class AuthResolver {
private readonly permissionsService: PermissionsService,
) {}
@UseGuards(CaptchaGuard, PublicEndpointGuard)
@UseGuards(CaptchaGuard, PublicEndpointGuard, NoPermissionGuard)
@Query(() => CheckUserExistOutput)
async checkUserExists(
@Args() checkUserExistsInput: EmailAndCaptchaInput,
@@ -147,7 +147,7 @@ export class AuthResolver {
}
@Query(() => WorkspaceInviteHashValidOutput)
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async checkWorkspaceInviteHashIsValid(
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
): Promise<WorkspaceInviteHashValidOutput> {
@@ -157,7 +157,7 @@ export class AuthResolver {
}
@Query(() => WorkspaceEntity)
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async findWorkspaceFromInviteHash(
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
): Promise<WorkspaceEntity> {
@@ -833,7 +833,7 @@ export class AuthResolver {
}
@Query(() => ValidatePasswordResetTokenOutput)
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async validatePasswordResetToken(
@Args() args: ValidatePasswordResetTokenInput,
): Promise<ValidatePasswordResetTokenOutput> {
@@ -23,12 +23,13 @@ import { GoogleAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/gu
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
@Controller('auth/google-apis')
@UseFilters(AuthRestApiExceptionFilter)
@@ -45,14 +46,22 @@ export class GoogleAPIsAuthController {
) {}
@Get()
@UseGuards(GoogleAPIsOauthRequestCodeGuard, PublicEndpointGuard)
@UseGuards(
GoogleAPIsOauthRequestCodeGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async googleAuth() {
// As this method is protected by Google Auth guard, it will trigger Google SSO flow
return;
}
@Get('get-access-token')
@UseGuards(GoogleAPIsOauthExchangeCodeForTokenGuard, PublicEndpointGuard)
@UseGuards(
GoogleAPIsOauthExchangeCodeForTokenGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async googleAuthGetAccessToken(
@Req() req: GoogleAPIsRequest,
@Res() res: Response,
@@ -16,6 +16,7 @@ import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller('auth/google')
@@ -24,14 +25,24 @@ export class GoogleAuthController {
constructor(private readonly authService: AuthService) {}
@Get()
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard, PublicEndpointGuard)
@UseGuards(
GoogleProviderEnabledGuard,
GoogleOauthGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async googleAuth() {
// As this method is protected by Google Auth guard, it will trigger Google SSO flow
return;
}
@Get('redirect')
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard, PublicEndpointGuard)
@UseGuards(
GoogleProviderEnabledGuard,
GoogleOauthGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
@UseFilters(AuthOAuthExceptionFilter)
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
return res.redirect(
@@ -28,6 +28,7 @@ import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/ser
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller('auth/microsoft-apis')
@@ -45,14 +46,22 @@ export class MicrosoftAPIsAuthController {
) {}
@Get()
@UseGuards(MicrosoftAPIsOauthRequestCodeGuard, PublicEndpointGuard)
@UseGuards(
MicrosoftAPIsOauthRequestCodeGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async MicrosoftAuth() {
// As this method is protected by Microsoft Auth guard, it will trigger Microsoft SSO flow
return;
}
@Get('get-access-token')
@UseGuards(MicrosoftAPIsOauthExchangeCodeForTokenGuard, PublicEndpointGuard)
@UseGuards(
MicrosoftAPIsOauthExchangeCodeForTokenGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async MicrosoftAuthGetAccessToken(
@Req() req: MicrosoftAPIsRequest,
@Res() res: Response,
@@ -15,6 +15,7 @@ import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guar
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller('auth/microsoft')
@@ -27,6 +28,7 @@ export class MicrosoftAuthController {
MicrosoftProviderEnabledGuard,
MicrosoftOAuthGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async microsoftAuth() {
// As this method is protected by Microsoft Auth guard, it will trigger Microsoft SSO flow
@@ -38,6 +40,7 @@ export class MicrosoftAuthController {
MicrosoftProviderEnabledGuard,
MicrosoftOAuthGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async microsoftAuthRedirect(
@Req() req: MicrosoftRequest,
@@ -21,10 +21,6 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import {
WorkspaceSSOIdentityProviderEntity,
IdentityProviderType,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard';
import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard';
@@ -36,9 +32,14 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import {
IdentityProviderType,
WorkspaceSSOIdentityProviderEntity,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller('auth')
@@ -56,7 +57,11 @@ export class SSOAuthController {
) {}
@Get('saml/metadata/:identityProviderId')
@UseGuards(EnterpriseFeaturesEnabledGuard, PublicEndpointGuard)
@UseGuards(
EnterpriseFeaturesEnabledGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async generateMetadata(@Req() req: any): Promise<string | void> {
return generateServiceProviderMetadata({
@@ -73,27 +78,47 @@ export class SSOAuthController {
}
@Get('oidc/login/:identityProviderId')
@UseGuards(EnterpriseFeaturesEnabledGuard, OIDCAuthGuard, PublicEndpointGuard)
@UseGuards(
EnterpriseFeaturesEnabledGuard,
OIDCAuthGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async oidcAuth() {
// As this method is protected by OIDC Auth guard, it will trigger OIDC SSO flow
return;
}
@Get('saml/login/:identityProviderId')
@UseGuards(EnterpriseFeaturesEnabledGuard, SAMLAuthGuard, PublicEndpointGuard)
@UseGuards(
EnterpriseFeaturesEnabledGuard,
SAMLAuthGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async samlAuth() {
// As this method is protected by SAML Auth guard, it will trigger SAML SSO flow
return;
}
@Get('oidc/callback')
@UseGuards(EnterpriseFeaturesEnabledGuard, OIDCAuthGuard, PublicEndpointGuard)
@UseGuards(
EnterpriseFeaturesEnabledGuard,
OIDCAuthGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async oidcAuthCallback(@Req() req: OIDCRequest, @Res() res: Response) {
return await this.authCallback(req, res);
}
@Post('saml/callback/:identityProviderId')
@UseGuards(EnterpriseFeaturesEnabledGuard, SAMLAuthGuard, PublicEndpointGuard)
@UseGuards(
EnterpriseFeaturesEnabledGuard,
SAMLAuthGuard,
PublicEndpointGuard,
NoPermissionGuard,
)
async samlAuthCallback(@Req() req: SAMLRequest, @Res() res: Response) {
try {
return await this.authCallback(req, res);
@@ -21,8 +21,8 @@ import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billin
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-invoice.service';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service';
import { BillingWebhookSubscriptionScheduleService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription-schedule.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service';
import {
BillingException,
BillingExceptionCode,
@@ -31,6 +31,7 @@ import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billi
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller()
@@ -52,7 +53,7 @@ export class BillingWebhookController {
) {}
@Post(['webhooks/stripe'])
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async handleWebhooks(
@Headers('stripe-signature') signature: string,
@Req() req: RawBodyRequest<Request>,
@@ -246,7 +246,7 @@ export class BillingResolver {
}
@Query(() => [BillingPlanOutput])
@UseGuards(WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard, NoPermissionGuard)
async listPlans(): Promise<BillingPlanOutput[]> {
const plans = await this.billingPlanService.listPlans();
@@ -8,6 +8,7 @@ import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/
import { TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@ArgsType()
@@ -49,7 +50,7 @@ class GetTimelineCalendarEventsFromOpportunityIdArgs {
pageSize: number;
}
@UseGuards(WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
@Resolver(() => TimelineCalendarEventsWithTotalDTO)
export class TimelineCalendarEventResolver {
constructor(
@@ -2,6 +2,7 @@ import { Controller, Get, UseGuards } from '@nestjs/common';
import { type ClientConfig } from 'src/engine/core-modules/client-config/client-config.entity';
import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller('/client-config')
@@ -9,7 +10,7 @@ export class ClientConfigController {
constructor(private readonly clientConfigService: ClientConfigService) {}
@Get()
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async getClientConfig(): Promise<ClientConfig> {
return this.clientConfigService.getClientConfig();
}
@@ -5,11 +5,12 @@ import { Controller, Post, Req, UseFilters, UseGuards } from '@nestjs/common';
import { Request } from 'express';
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { DnsManagerExceptionFilter } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter';
import { CloudflareSecretMatchGuard } from 'src/engine/core-modules/cloudflare/guards/cloudflare-secret.guard';
import { DnsCloudflareService } from 'src/engine/core-modules/cloudflare/services/dns-cloudflare.service';
import { DnsManagerExceptionFilter } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller()
@UseFilters(AuthRestApiExceptionFilter, DnsManagerExceptionFilter)
@@ -20,7 +21,7 @@ export class DnsCloudflareController {
) {}
@Post(['cloudflare/custom-hostname-webhooks', 'webhooks/cloudflare'])
@UseGuards(CloudflareSecretMatchGuard, PublicEndpointGuard)
@UseGuards(CloudflareSecretMatchGuard, PublicEndpointGuard, NoPermissionGuard)
async customHostnameWebhooks(@Req() req: Request) {
const hostname = req.body?.data?.data?.hostname;
@@ -23,6 +23,7 @@ import { FileApiExceptionFilter } from 'src/engine/core-modules/file/filters/fil
import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { extractFileInfoFromRequest } from 'src/engine/core-modules/file/utils/extract-file-info-from-request.utils';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller('files')
@@ -32,7 +33,7 @@ export class FileController {
constructor(private readonly fileService: FileService) {}
@Get('*/:filename')
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async getFile(
@Param() _params: string[],
@Res() res: Response,
@@ -4,10 +4,11 @@ import { Args, Query, Resolver } from '@nestjs/graphql';
import { AutocompleteResultDTO } from 'src/engine/core-modules/geo-map/dtos/autocomplete-result.dto';
import { PlaceDetailsResultDTO } from 'src/engine/core-modules/geo-map/dtos/place-details-result.dto';
import { GeoMapService } from 'src/engine/core-modules/geo-map/services/geo-map.service';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver()
@UseGuards(WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard, NoPermissionGuard)
export class GeoMapResolver {
constructor(private readonly geoMapService: GeoMapService) {}
@@ -13,6 +13,7 @@ import { ConnectedAccountHealth } from 'src/engine/core-modules/health/indicator
import { DatabaseHealthIndicator } from 'src/engine/core-modules/health/indicators/database.health';
import { RedisHealthIndicator } from 'src/engine/core-modules/health/indicators/redis.health';
import { WorkerHealthIndicator } from 'src/engine/core-modules/health/indicators/worker.health';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller('healthz')
@@ -27,14 +28,14 @@ export class HealthController {
) {}
@Get()
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
@HealthCheck()
check() {
return this.health.check([]);
}
@Get(':indicatorId')
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
@HealthCheck()
checkService(@Param('indicatorId') indicatorId: HealthIndicatorId) {
const checks = {
@@ -12,6 +12,7 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@@ -54,7 +55,7 @@ class GetTimelineThreadsFromOpportunityIdArgs {
pageSize: number;
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, CustomPermissionGuard)
@Resolver(() => TimelineThreadsWithTotalDTO)
export class TimelineMessagingResolver {
constructor(
@@ -3,6 +3,7 @@ import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { Request, Response } from 'express';
import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller()
@@ -10,7 +11,7 @@ export class OpenApiController {
constructor(private readonly openApiService: OpenApiService) {}
@Get(['open-api/core', 'rest/open-api/core'])
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async generateOpenApiSchemaCore(
@Req() request: Request,
@Res() res: Response,
@@ -21,7 +22,7 @@ export class OpenApiController {
}
@Get(['open-api/metadata', 'rest/open-api/metadata'])
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async generateOpenApiSchemaMetaData(
@Req() request: Request,
@Res() res: Response,
@@ -26,7 +26,10 @@ import { PageLayoutTabRestApiExceptionFilter } from 'src/engine/core-modules/pag
import { PageLayoutTabService } from 'src/engine/core-modules/page-layout/services/page-layout-tab.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.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';
@Controller('rest/metadata/pageLayoutTabs')
@UseGuards(WorkspaceAuthGuard)
@@ -35,6 +38,7 @@ export class PageLayoutTabController {
constructor(private readonly pageLayoutTabService: PageLayoutTabService) {}
@Get()
@UseGuards(NoPermissionGuard)
async findMany(
@AuthWorkspace() workspace: WorkspaceEntity,
@Query('pageLayoutId') pageLayoutId: string,
@@ -55,6 +59,7 @@ export class PageLayoutTabController {
}
@Get(':id')
@UseGuards(NoPermissionGuard)
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -63,6 +68,7 @@ export class PageLayoutTabController {
}
@Post()
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async create(
@Body() input: CreatePageLayoutTabInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -71,6 +77,7 @@ export class PageLayoutTabController {
}
@Patch(':id')
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async update(
@Param('id') id: string,
@Body() input: UpdatePageLayoutTabInput,
@@ -80,6 +87,7 @@ export class PageLayoutTabController {
}
@Delete(':id')
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -26,7 +26,10 @@ import { PageLayoutWidgetRestApiExceptionFilter } from 'src/engine/core-modules/
import { PageLayoutWidgetService } from 'src/engine/core-modules/page-layout/services/page-layout-widget.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.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';
@Controller('rest/metadata/pageLayoutWidgets')
@UseGuards(WorkspaceAuthGuard)
@@ -37,6 +40,7 @@ export class PageLayoutWidgetController {
) {}
@Get()
@UseGuards(NoPermissionGuard)
async findMany(
@AuthWorkspace() workspace: WorkspaceEntity,
@Query('pageLayoutTabId') pageLayoutTabId: string,
@@ -57,6 +61,7 @@ export class PageLayoutWidgetController {
}
@Get(':id')
@UseGuards(NoPermissionGuard)
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -65,6 +70,7 @@ export class PageLayoutWidgetController {
}
@Post()
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async create(
@Body() input: CreatePageLayoutWidgetInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -73,6 +79,7 @@ export class PageLayoutWidgetController {
}
@Patch(':id')
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async update(
@Param('id') id: string,
@Body() input: UpdatePageLayoutWidgetInput,
@@ -82,6 +89,7 @@ export class PageLayoutWidgetController {
}
@Delete(':id')
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -21,7 +21,10 @@ import { PageLayoutRestApiExceptionFilter } from 'src/engine/core-modules/page-l
import { PageLayoutService } from 'src/engine/core-modules/page-layout/services/page-layout.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.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';
@Controller('rest/metadata/pageLayouts')
@UseGuards(WorkspaceAuthGuard)
@@ -30,6 +33,7 @@ export class PageLayoutController {
constructor(private readonly pageLayoutService: PageLayoutService) {}
@Get()
@UseGuards(NoPermissionGuard)
async findMany(
@AuthWorkspace() workspace: WorkspaceEntity,
@Query('objectMetadataId') objectMetadataId?: string,
@@ -45,6 +49,7 @@ export class PageLayoutController {
}
@Get(':id')
@UseGuards(NoPermissionGuard)
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -53,6 +58,7 @@ export class PageLayoutController {
}
@Post()
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async create(
@Body() input: CreatePageLayoutInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -61,6 +67,7 @@ export class PageLayoutController {
}
@Patch(':id')
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async update(
@Param('id') id: string,
@Body() input: UpdatePageLayoutInput,
@@ -76,6 +83,7 @@ export class PageLayoutController {
}
@Delete(':id')
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -11,6 +11,7 @@ import { PageLayoutTabService } from 'src/engine/core-modules/page-layout/servic
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/core-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.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';
@@ -23,6 +24,7 @@ export class PageLayoutTabResolver {
constructor(private readonly pageLayoutTabService: PageLayoutTabService) {}
@Query(() => [PageLayoutTabDTO])
@UseGuards(NoPermissionGuard)
async getPageLayoutTabs(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('pageLayoutId', { type: () => String }) pageLayoutId: string,
@@ -34,6 +36,7 @@ export class PageLayoutTabResolver {
}
@Query(() => PageLayoutTabDTO)
@UseGuards(NoPermissionGuard)
async getPageLayoutTab(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -18,6 +18,7 @@ import { injectWidgetConfigurationDiscriminator } from 'src/engine/core-modules/
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/core-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.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';
@@ -32,6 +33,7 @@ export class PageLayoutWidgetResolver {
) {}
@Query(() => [PageLayoutWidgetDTO])
@UseGuards(NoPermissionGuard)
async getPageLayoutWidgets(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('pageLayoutTabId', { type: () => String }) pageLayoutTabId: string,
@@ -43,6 +45,7 @@ export class PageLayoutWidgetResolver {
}
@Query(() => PageLayoutWidgetDTO)
@UseGuards(NoPermissionGuard)
async getPageLayoutWidget(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -13,16 +13,14 @@ import { PageLayoutService } from 'src/engine/core-modules/page-layout/services/
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/core-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.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';
@Resolver(() => PageLayoutDTO)
@UseFilters(PageLayoutGraphqlApiExceptionFilter)
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.LAYOUTS),
)
@UseGuards(WorkspaceAuthGuard)
@UsePipes(ResolverValidationPipe)
export class PageLayoutResolver {
constructor(
@@ -31,6 +29,7 @@ export class PageLayoutResolver {
) {}
@Query(() => [PageLayoutDTO])
@UseGuards(NoPermissionGuard)
async getPageLayouts(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('objectMetadataId', { type: () => String, nullable: true })
@@ -47,6 +46,7 @@ export class PageLayoutResolver {
}
@Query(() => PageLayoutDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
async getPageLayout(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -55,6 +55,7 @@ export class PageLayoutResolver {
}
@Mutation(() => PageLayoutDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async createPageLayout(
@Args('input') input: CreatePageLayoutInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -63,6 +64,7 @@ export class PageLayoutResolver {
}
@Mutation(() => PageLayoutDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async updatePageLayout(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdatePageLayoutInput,
@@ -72,6 +74,7 @@ export class PageLayoutResolver {
}
@Mutation(() => PageLayoutDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async deletePageLayout(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -85,6 +88,7 @@ export class PageLayoutResolver {
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async destroyPageLayout(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -98,6 +102,7 @@ export class PageLayoutResolver {
}
@Mutation(() => PageLayoutDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async restorePageLayout(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -106,6 +111,7 @@ export class PageLayoutResolver {
}
@Mutation(() => PageLayoutDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async updatePageLayoutWithTabsAndWidgets(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdatePageLayoutWithTabsInput,
@@ -11,12 +11,14 @@ import { SearchApiExceptionFilter } from 'src/engine/core-modules/search/filters
import { SearchService } from 'src/engine/core-modules/search/services/search.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Resolver()
@UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter)
@UsePipes(ResolverValidationPipe)
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
export class SearchResolver {
constructor(
private readonly searchService: SearchService,
@@ -24,7 +26,6 @@ export class SearchResolver {
) {}
@Query(() => SearchResultConnectionDTO)
@UseGuards(WorkspaceAuthGuard)
async search(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args()
@@ -18,14 +18,20 @@ import { WebhookService } from 'src/engine/core-modules/webhook/webhook.service'
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.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';
/**
* rest/webhooks is deprecated, use rest/metadata/webhooks instead
* rest/webhooks will be removed in the future
*/
@Controller(['rest/webhooks', 'rest/metadata/webhooks'])
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(
JwtAuthGuard,
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS),
)
@UseFilters(RestApiExceptionFilter)
export class WebhookController {
constructor(private readonly webhookService: WebhookService) {}
@@ -12,6 +12,7 @@ import { Request } from 'express';
import { isDefined } from 'twenty-shared/utils';
import { WorkflowTriggerRestApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
@@ -40,7 +41,7 @@ export class WorkflowTriggerController {
) {}
@Post('workflows/:workspaceId/:workflowId')
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async runWorkflowByPostRequest(
@Param('workspaceId') workspaceId: string,
@Param('workflowId') workflowId: string,
@@ -54,7 +55,7 @@ export class WorkflowTriggerController {
}
@Get('workflows/:workspaceId/:workflowId')
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async runWorkflowByGetRequest(
@Param('workspaceId') workspaceId: string,
@Param('workflowId') workflowId: string,
@@ -104,7 +104,7 @@ export class WorkspaceResolver {
) {}
@Query(() => WorkspaceEntity)
@UseGuards(WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard, NoPermissionGuard)
async currentWorkspace(@AuthWorkspace() { id }: WorkspaceEntity) {
const workspace = await this.workspaceService.findById(id);
@@ -317,7 +317,7 @@ export class WorkspaceResolver {
}
@Query(() => PublicWorkspaceDataOutput)
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async getPublicWorkspaceDataByDomain(
@OriginHeader() originHeader: string,
@Args('origin', { nullable: true }) origin?: string,