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
@@ -2,14 +2,15 @@ import { Controller, Post, Req, UseGuards } from '@nestjs/common';
import { Request } from 'express';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { MCPMetadataService } from 'src/engine/api/mcp/services/mcp-metadata.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 { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('mcp/metadata')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard, NoPermissionGuard)
export class McpMetadataController {
constructor(private readonly mCPMetadataService: MCPMetadataService) {}
@@ -17,11 +17,12 @@ import { Response } from 'express';
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard, CustomPermissionGuard)
@UseFilters(RestApiExceptionFilter)
export class RestApiCoreController {
private readonly logger = new Logger(RestApiCoreController.name);
@@ -1,12 +1,12 @@
import {
Controller,
Get,
Delete,
Get,
Patch,
Post,
Put,
Req,
Res,
Patch,
Put,
UseGuards,
} from '@nestjs/common';
@@ -15,10 +15,16 @@ import { Request, Response } from 'express';
import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils';
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';
@Controller('rest/metadata/*')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(
JwtAuthGuard,
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.DATA_MODEL),
)
export class RestApiMetadataController {
constructor(
private readonly restApiMetadataService: RestApiMetadataService,
@@ -8,6 +8,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
import { RestApiCoreModule } from 'src/engine/api/rest/core/rest-api-core.module';
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api-metadata.controller';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@Module({
imports: [
@@ -16,6 +17,7 @@ import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api
AuthModule,
HttpModule,
RestApiCoreModule,
PermissionsModule,
],
controllers: [RestApiMetadataController],
providers: [RestApiService, RestApiMetadataService],
@@ -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,
@@ -15,8 +15,10 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
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 { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/agent/types/recordIdsByObjectMetadataNameSingular.type';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { AgentChatService } from './agent-chat.service';
import { AgentStreamingService } from './agent-streaming.service';
@@ -31,6 +33,7 @@ export class AgentChatController {
) {}
@Post('stream')
@UseGuards(SettingsPermissionGuard(PermissionFlagType.AI))
async streamAgentChat(
@Body()
body: {
@@ -23,7 +23,11 @@ import { AgentDTO } from './dtos/agent.dto';
import { CreateAgentInput } from './dtos/create-agent.input';
import { UpdateAgentInput } from './dtos/update-agent.input';
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
@UseGuards(
WorkspaceAuthGuard,
FeatureFlagGuard,
SettingsPermissionGuard(PermissionFlagType.AI),
)
@Resolver()
export class AgentResolver {
constructor(
@@ -17,7 +17,10 @@ import { type RemoteServerType } from 'src/engine/metadata-modules/remote-server
import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service';
import { remoteServerGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/remote-server/utils/remote-server-graphql-api-exception-handler.util';
@UseGuards(WorkspaceAuthGuard)
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.DATA_MODEL),
)
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
@Resolver()
@@ -14,7 +14,10 @@ import { RemoteTableDTO } from 'src/engine/metadata-modules/remote-server/remote
import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service';
import { remoteTableGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/remote-server/remote-table/utils/remote-table-graphql-api-exception-handler.util';
@UseGuards(WorkspaceAuthGuard)
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.DATA_MODEL),
)
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
@Resolver()
@@ -12,13 +12,14 @@ import {
import { Request } from 'express';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { RouteTriggerRestApiExceptionFilter } from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger-rest-api-exception-filter';
import { HTTPMethod } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { RouteTriggerService } from 'src/engine/metadata-modules/route-trigger/route-trigger.service';
import { RouteTriggerRestApiExceptionFilter } from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger-rest-api-exception-filter';
@Controller('s')
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
@UseFilters(RouteTriggerRestApiExceptionFilter)
export class RouteTriggerController {
constructor(private readonly routeTriggerService: RouteTriggerService) {}
@@ -25,7 +25,11 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless
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)
@UseGuards(
WorkspaceAuthGuard,
FeatureFlagGuard,
SettingsPermissionGuard(PermissionFlagType.WORKFLOWS),
)
@Resolver()
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
@@ -17,6 +17,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 { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input';
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
@@ -47,6 +48,7 @@ export class ViewFieldController {
) {}
@Get()
@UseGuards(NoPermissionGuard)
async findMany(
@AuthWorkspace() workspace: WorkspaceEntity,
@Query('viewId') viewId?: string,
@@ -59,6 +61,7 @@ export class ViewFieldController {
}
@Get(':id')
@UseGuards(NoPermissionGuard)
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -5,6 +5,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 { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input';
import { DeleteViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/delete-view-field.input';
@@ -35,6 +36,7 @@ export class ViewFieldResolver {
) {}
@Query(() => [ViewFieldDTO])
@UseGuards(NoPermissionGuard)
async getCoreViewFields(
@Args('viewId', { type: () => String }) viewId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -43,6 +45,7 @@ export class ViewFieldResolver {
}
@Query(() => ViewFieldDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
async getCoreViewField(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -15,6 +15,7 @@ import { isDefined } from 'twenty-shared/utils';
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 { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewFilterGroupInput } from 'src/engine/metadata-modules/view-filter-group/dtos/inputs/create-view-filter-group.input';
import { UpdateViewFilterGroupInput } from 'src/engine/metadata-modules/view-filter-group/dtos/inputs/update-view-filter-group.input';
@@ -41,6 +42,7 @@ export class ViewFilterGroupController {
) {}
@Get()
@UseGuards(NoPermissionGuard)
async findMany(
@AuthWorkspace() workspace: WorkspaceEntity,
@Query('viewId') viewId?: string,
@@ -53,6 +55,7 @@ export class ViewFilterGroupController {
}
@Get(':id')
@UseGuards(NoPermissionGuard)
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
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 { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewFilterGroupInput } from 'src/engine/metadata-modules/view-filter-group/dtos/inputs/create-view-filter-group.input';
import { UpdateViewFilterGroupInput } from 'src/engine/metadata-modules/view-filter-group/dtos/inputs/update-view-filter-group.input';
@@ -25,6 +26,7 @@ export class ViewFilterGroupResolver {
) {}
@Query(() => [ViewFilterGroupDTO])
@UseGuards(NoPermissionGuard)
async getCoreViewFilterGroups(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('viewId', { type: () => String, nullable: true })
@@ -38,6 +40,7 @@ export class ViewFilterGroupResolver {
}
@Query(() => ViewFilterGroupDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
async getCoreViewFilterGroup(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -17,6 +17,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 { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/create-view-filter.input';
import { UpdateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/update-view-filter.input';
@@ -46,6 +47,7 @@ export class ViewFilterController {
) {}
@Get()
@UseGuards(NoPermissionGuard)
async findMany(
@AuthWorkspace() workspace: WorkspaceEntity,
@Query('viewId') viewId?: string,
@@ -58,6 +60,7 @@ export class ViewFilterController {
}
@Get(':id')
@UseGuards(NoPermissionGuard)
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -5,6 +5,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 { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/create-view-filter.input';
import { DeleteViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/delete-view-filter.input';
@@ -30,6 +31,7 @@ export class ViewFilterResolver {
) {}
@Query(() => [ViewFilterDTO])
@UseGuards(NoPermissionGuard)
async getCoreViewFilters(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('viewId', { type: () => String, nullable: true })
@@ -43,6 +45,7 @@ export class ViewFilterResolver {
}
@Query(() => ViewFilterDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
async getCoreViewFilter(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -17,6 +17,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 { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
import { UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input';
@@ -46,6 +47,7 @@ export class ViewGroupController {
) {}
@Get()
@UseGuards(NoPermissionGuard)
async findMany(
@AuthWorkspace() workspace: WorkspaceEntity,
@Query('viewId') viewId?: string,
@@ -58,6 +60,7 @@ export class ViewGroupController {
}
@Get(':id')
@UseGuards(NoPermissionGuard)
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -5,6 +5,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 { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
import { DeleteViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/delete-view-group.input';
@@ -34,6 +35,7 @@ export class ViewGroupResolver {
) {}
@Query(() => [ViewGroupDTO])
@UseGuards(NoPermissionGuard)
async getCoreViewGroups(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('viewId', { type: () => String, nullable: true })
@@ -47,6 +49,7 @@ export class ViewGroupResolver {
}
@Query(() => ViewGroupDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
async getCoreViewGroup(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -0,0 +1,39 @@
import {
Injectable,
type CanActivate,
type ExecutionContext,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { ViewAccessService } from 'src/engine/metadata-modules/view-permissions/services/view-access.service';
import { ViewVisibility } from 'src/engine/metadata-modules/view/enums/view-visibility.enum';
@Injectable()
export class CreateViewPermissionGuard implements CanActivate {
constructor(private readonly viewAccessService: ViewAccessService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
let visibility: ViewVisibility = ViewVisibility.WORKSPACE;
// For GraphQL: extract from args.input
const args = gqlContext.getArgs();
if (args?.input?.visibility) {
visibility = args.input.visibility as ViewVisibility;
}
// For REST: extract from request body
if (!args?.input && request.body?.visibility) {
visibility = request.body.visibility as ViewVisibility;
}
return this.viewAccessService.canUserCreateView(
visibility,
request.userWorkspaceId,
request.workspace.id,
);
}
}
@@ -71,6 +71,41 @@ export class ViewAccessService {
return this.checkViewAccess(view, userWorkspaceId, workspaceId);
}
async canUserCreateView(
visibility: ViewVisibility,
userWorkspaceId: string | undefined,
workspaceId: string,
): Promise<boolean> {
// For WORKSPACE visibility views, user must have VIEWS permission
if (visibility === ViewVisibility.WORKSPACE && isDefined(userWorkspaceId)) {
const permissions =
await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,
});
const hasViewsPermission =
permissions.permissionFlags[PermissionFlagType.VIEWS] ?? false;
if (!hasViewsPermission) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
),
ViewExceptionCode.VIEW_CREATE_PERMISSION_DENIED,
{
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
),
},
);
}
}
// For UNLISTED views or users without userWorkspaceId, allow creation
return true;
}
private async checkViewAccess(
view: ViewEntity,
userWorkspaceId: string | undefined,
@@ -11,6 +11,7 @@ import { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view
import { CreateViewFilterGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-filter-group-permission.guard';
import { CreateViewFilterPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-filter-permission.guard';
import { CreateViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-group-permission.guard';
import { CreateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-permission.guard';
import { CreateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-sort-permission.guard';
import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard';
import { DeleteViewFilterGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-filter-group-permission.guard';
@@ -55,6 +56,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
ViewService,
ViewEntityLookupService,
ViewAccessService,
CreateViewPermissionGuard,
UpdateViewPermissionGuard,
DeleteViewPermissionGuard,
DestroyViewPermissionGuard,
@@ -83,6 +85,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
ViewService,
ViewEntityLookupService,
ViewAccessService,
CreateViewPermissionGuard,
UpdateViewPermissionGuard,
DeleteViewPermissionGuard,
DestroyViewPermissionGuard,
@@ -15,6 +15,7 @@ import { isDefined } from 'twenty-shared/utils';
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 { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-sort-permission.guard';
import { DeleteViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-sort-permission.guard';
@@ -39,6 +40,7 @@ export class ViewSortController {
constructor(private readonly viewSortService: ViewSortService) {}
@Get()
@UseGuards(NoPermissionGuard)
async findMany(
@AuthWorkspace() workspace: WorkspaceEntity,
@Query('viewId') viewId?: string,
@@ -51,6 +53,7 @@ export class ViewSortController {
}
@Get(':id')
@UseGuards(NoPermissionGuard)
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
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 { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-sort-permission.guard';
import { DeleteViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-sort-permission.guard';
@@ -23,6 +24,7 @@ export class ViewSortResolver {
constructor(private readonly viewSortService: ViewSortService) {}
@Query(() => [ViewSortDTO])
@UseGuards(NoPermissionGuard)
async getCoreViewSorts(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('viewId', { type: () => String, nullable: true })
@@ -36,6 +38,7 @@ export class ViewSortResolver {
}
@Query(() => ViewSortDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
async getCoreViewSort(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -21,8 +21,11 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { RequestLocale } from 'src/engine/decorators/locale/request-locale.decorator';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
import { CreateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-permission.guard';
import { DeleteViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-permission.guard';
import { UpdateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-permission.guard';
import { CreateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/create-view.input';
@@ -53,6 +56,7 @@ export class ViewController {
) {}
@Get()
@UseGuards(CustomPermissionGuard)
async findMany(
@RequestLocale() locale: keyof typeof APP_LOCALES | undefined,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -71,6 +75,7 @@ export class ViewController {
}
@Get(':id')
@UseGuards(NoPermissionGuard)
async findOne(
@Param('id') id: string,
@RequestLocale() locale: keyof typeof APP_LOCALES | undefined,
@@ -103,6 +108,7 @@ export class ViewController {
}
@Post()
@UseGuards(CreateViewPermissionGuard)
async create(
@Body() input: CreateViewInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -144,6 +150,7 @@ export class ViewController {
@Body() input: UpdateViewInput,
@RequestLocale() locale: keyof typeof APP_LOCALES | undefined,
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
): Promise<ViewDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
@@ -160,9 +167,15 @@ export class ViewController {
id,
},
workspaceId: workspace.id,
userWorkspaceId,
});
} else {
updatedView = await this.viewService.update(id, workspace.id, input);
updatedView = await this.viewService.update(
id,
workspace.id,
input,
userWorkspaceId,
);
}
const processedViews = await this.processViewsWithTemplates(
@@ -21,10 +21,9 @@ import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
@@ -34,6 +33,7 @@ import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view
import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
import { ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto';
import { ViewGroupService } from 'src/engine/metadata-modules/view-group/services/view-group.service';
import { CreateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-permission.guard';
import { DeleteViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-permission.guard';
import { DestroyViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/destroy-view-permission.guard';
import { UpdateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-permission.guard';
@@ -44,13 +44,6 @@ import { UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/up
import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewVisibility } from 'src/engine/metadata-modules/view/enums/view-visibility.enum';
import {
ViewException,
ViewExceptionCode,
ViewExceptionMessageKey,
generateViewExceptionMessage,
generateViewUserFriendlyExceptionMessage,
} from 'src/engine/metadata-modules/view/exceptions/view.exception';
import { ViewV2Service } from 'src/engine/metadata-modules/view/services/view-v2.service';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter';
@@ -70,7 +63,6 @@ export class ViewResolver {
private readonly featureFlagService: FeatureFlagService,
private readonly viewV2Service: ViewV2Service,
private readonly userRoleService: UserRoleService,
private readonly permissionsService: PermissionsService,
) {}
@ResolveField(() => String)
@@ -119,6 +111,7 @@ export class ViewResolver {
}
@Query(() => [ViewDTO])
@UseGuards(CustomPermissionGuard)
async getCoreViews(
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
@@ -137,6 +130,7 @@ export class ViewResolver {
}
@Query(() => ViewDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
async getCoreView(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -152,7 +146,7 @@ export class ViewResolver {
}
@Mutation(() => ViewDTO)
@UseGuards(CustomPermissionGuard)
@UseGuards(CreateViewPermissionGuard)
async createCoreView(
@Args('input') input: CreateViewInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -160,28 +154,6 @@ export class ViewResolver {
): Promise<ViewDTO> {
const visibility = input.visibility ?? ViewVisibility.WORKSPACE;
if (visibility === ViewVisibility.WORKSPACE && isDefined(userWorkspaceId)) {
const permissions =
await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId,
workspaceId: workspace.id,
});
if (!permissions.permissionFlags[PermissionFlagType.VIEWS]) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
),
ViewExceptionCode.VIEW_CREATE_PERMISSION_DENIED,
{
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
),
},
);
}
}
input.visibility = visibility;
const isWorkspaceMigrationV2Enabled =
@@ -213,6 +185,7 @@ export class ViewResolver {
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdateViewInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
): Promise<ViewDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
@@ -224,10 +197,11 @@ export class ViewResolver {
return await this.viewV2Service.updateOne({
updateViewInput: { ...input, id },
workspaceId: workspace.id,
userWorkspaceId,
});
}
return this.viewService.update(id, workspace.id, input);
return this.viewService.update(id, workspace.id, input, userWorkspaceId);
}
@Mutation(() => Boolean)
@@ -382,6 +382,93 @@ describe('ViewService', () => {
),
);
});
it('should re-allocate view to current user when changing from WORKSPACE to UNLISTED visibility', async () => {
const id = 'view-id';
const workspaceId = 'workspace-id';
const userWorkspaceId = 'current-user-workspace-id';
const workspaceView = {
...mockView,
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
} as ViewEntity;
const updateData = { visibility: ViewVisibility.UNLISTED };
const expectedSaveData = {
id,
visibility: ViewVisibility.UNLISTED,
createdByUserWorkspaceId: userWorkspaceId,
};
const updatedView = {
...workspaceView,
...expectedSaveData,
};
jest.spyOn(viewService, 'findById').mockResolvedValue(workspaceView);
jest.spyOn(viewRepository, 'save').mockResolvedValue(updatedView);
const result = await viewService.update(
id,
workspaceId,
updateData,
userWorkspaceId,
);
expect(viewService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewRepository.save).toHaveBeenCalledWith(expectedSaveData);
expect(result.createdByUserWorkspaceId).toBe(userWorkspaceId);
});
it('should not change createdByUserWorkspaceId when visibility is not changing to UNLISTED', async () => {
const id = 'view-id';
const workspaceId = 'workspace-id';
const userWorkspaceId = 'current-user-workspace-id';
const updateData = { name: 'Updated Name' };
const updatedView = { ...mockView, ...updateData };
jest.spyOn(viewService, 'findById').mockResolvedValue(mockView);
jest.spyOn(viewRepository, 'save').mockResolvedValue(updatedView);
await viewService.update(id, workspaceId, updateData, userWorkspaceId);
expect(viewRepository.save).toHaveBeenCalledWith({
id,
...updateData,
});
expect(viewRepository.save).not.toHaveBeenCalledWith(
expect.objectContaining({
createdByUserWorkspaceId: userWorkspaceId,
}),
);
});
it('should not change createdByUserWorkspaceId when view is already UNLISTED', async () => {
const id = 'view-id';
const workspaceId = 'workspace-id';
const userWorkspaceId = 'current-user-workspace-id';
const originalOwner = 'original-owner-workspace-id';
const unlistedView = {
...mockView,
visibility: ViewVisibility.UNLISTED,
createdByUserWorkspaceId: originalOwner,
} as ViewEntity;
const updateData = { visibility: ViewVisibility.UNLISTED };
const updatedView = { ...unlistedView, ...updateData };
jest.spyOn(viewService, 'findById').mockResolvedValue(unlistedView);
jest.spyOn(viewRepository, 'save').mockResolvedValue(updatedView);
await viewService.update(id, workspaceId, updateData, userWorkspaceId);
expect(viewRepository.save).toHaveBeenCalledWith({
id,
...updateData,
});
expect(viewRepository.save).not.toHaveBeenCalledWith(
expect.objectContaining({
createdByUserWorkspaceId: userWorkspaceId,
}),
);
});
});
describe('delete', () => {
@@ -100,9 +100,11 @@ export class ViewV2Service {
async updateOne({
updateViewInput,
workspaceId,
userWorkspaceId,
}: {
updateViewInput: UpdateViewInput;
workspaceId: string;
userWorkspaceId?: string;
}): Promise<ViewDTO> {
const {
flatViewMaps: existingFlatViewMaps,
@@ -121,6 +123,21 @@ export class ViewV2Service {
flatViewMaps: existingFlatViewMaps,
});
const existingFlatView = existingFlatViewMaps.byId[updateViewInput.id];
// If changing visibility from WORKSPACE to UNLISTED, ensure createdByUserWorkspaceId is set
// This prevents the view from disappearing for the user making the change
if (
isDefined(existingFlatView) &&
isDefined(updateViewInput.visibility) &&
updateViewInput.visibility === 'UNLISTED' &&
existingFlatView.visibility === 'WORKSPACE' &&
isDefined(userWorkspaceId)
) {
// Re-allocate the view to the current user
flatViewFromUpdateInput.createdByUserWorkspaceId = userWorkspaceId;
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
@@ -193,6 +193,7 @@ export class ViewService {
id: string,
workspaceId: string,
updateData: Partial<ViewEntity>,
userWorkspaceId?: string,
): Promise<ViewEntity> {
const existingView = await this.findById(id, workspaceId);
@@ -206,9 +207,23 @@ export class ViewService {
);
}
// If changing visibility from WORKSPACE to UNLISTED, ensure createdByUserWorkspaceId is set
// This prevents the view from disappearing for the user making the change
const dataToUpdate = { ...updateData };
if (
isDefined(updateData.visibility) &&
updateData.visibility === ViewVisibility.UNLISTED &&
existingView.visibility === ViewVisibility.WORKSPACE &&
isDefined(userWorkspaceId)
) {
// Re-allocate the view to the current user if it has no owner or a different owner
dataToUpdate.createdByUserWorkspaceId = userWorkspaceId;
}
const updatedView = await this.viewRepository.save({
id,
...updateData,
...dataToUpdate,
});
await this.flushGraphQLCache(workspaceId);
@@ -6,13 +6,14 @@ import { isDefined } from 'twenty-shared/utils';
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 { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { OnDbEventDTO } from 'src/engine/subscriptions/dtos/on-db-event.dto';
import { OnDbEventInput } from 'src/engine/subscriptions/dtos/on-db-event.input';
@Resolver()
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, NoPermissionGuard)
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
export class SubscriptionsResolver {
@@ -12,7 +12,7 @@ ruleTester.run(RULE_NAME, rule, {
code: `
class TestResolver {
@Query()
@UseGuards(UserAuthGuard)
@UseGuards(UserAuthGuard, NoPermissionGuard)
testQuery() {}
}
`,
@@ -21,7 +21,7 @@ ruleTester.run(RULE_NAME, rule, {
code: `
class TestResolver {
@Query()
@UseGuards(WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
testQuery() {}
}
`,
@@ -30,23 +30,14 @@ ruleTester.run(RULE_NAME, rule, {
code: `
class TestResolver {
@Query()
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
testQuery() {}
}
`,
},
{
code: `
class TestResolver {
@Query()
@UseGuards(CaptchaGuard, PublicEndpointGuard)
testQuery() {}
}
`,
},
{
code: `
@UseGuards(UserAuthGuard)
@UseGuards(UserAuthGuard, NoPermissionGuard)
class TestResolver {
@Query()
testQuery() {}
@@ -55,7 +46,7 @@ ruleTester.run(RULE_NAME, rule, {
},
{
code: `
@UseGuards(WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
class TestResolver {
@Query()
testQuery() {}
@@ -64,13 +55,31 @@ ruleTester.run(RULE_NAME, rule, {
},
{
code: `
@UseGuards(PublicEndpointGuard)
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
class TestResolver {
@Query()
testQuery() {}
}
`,
},
{
code: `
class TestResolver {
@Subscription()
@UseGuards(WorkspaceAuthGuard, NoPermissionGuard)
testSubscription() {}
}
`,
},
{
code: `
@UseGuards(WorkspaceAuthGuard, NoPermissionGuard)
class TestResolver {
@Subscription()
testSubscription() {}
}
`,
},
{
code: `
class TestResolver {
@@ -163,6 +172,20 @@ ruleTester.run(RULE_NAME, rule, {
},
],
},
{
code: `
class TestResolver {
@Query()
@UseGuards(UserAuthGuard)
testQuery() {}
}
`,
errors: [
{
messageId: 'graphqlResolversShouldBeGuarded',
},
],
},
{
code: `
class TestResolver {
@@ -191,6 +214,34 @@ ruleTester.run(RULE_NAME, rule, {
},
],
},
{
code: `
class TestResolver {
@Subscription()
@UseGuards(WorkspaceAuthGuard)
testSubscription() {}
}
`,
errors: [
{
messageId: 'graphqlResolversShouldBeGuarded',
},
],
},
{
code: `
@UseGuards(WorkspaceAuthGuard)
class TestResolver {
@Subscription()
testSubscription() {}
}
`,
errors: [
{
messageId: 'graphqlResolversShouldBeGuarded',
},
],
},
{
code: `
class TestResolver {
@@ -14,10 +14,6 @@ export const graphqlResolversShouldBeGuarded = (
['Query', 'Mutation', 'Subscription'],
);
const isMutation = typedTokenHelpers.nodeHasDecoratorsNamed(node, [
'Mutation',
]);
const hasAuthGuards = typedTokenHelpers.nodeHasAuthGuards(node);
const hasPermissionsGuard = typedTokenHelpers.nodeHasPermissionsGuard(node);
@@ -43,13 +39,12 @@ export const graphqlResolversShouldBeGuarded = (
? typedTokenHelpers.nodeHasPermissionsGuard(classNode)
: false;
// Basic requirement: all resolvers need auth guards
// All resolvers need both auth guards and permission guards
const missingAuthGuard =
hasGraphQLResolverDecorator && !hasAuthGuards && !hasAuthGuardsOnResolver;
// Additional requirement: mutations need permission guards
const missingPermissionGuard =
isMutation && !hasPermissionsGuard && !hasPermissionsGuardOnResolver;
hasGraphQLResolverDecorator && !hasPermissionsGuard && !hasPermissionsGuardOnResolver;
return missingAuthGuard || missingPermissionGuard;
};
@@ -59,11 +54,11 @@ export const rule = createRule<[], 'graphqlResolversShouldBeGuarded'>({
meta: {
docs: {
description:
'GraphQL root resolvers (Query, Mutation, Subscription) should have authentication guards (UserAuthGuard or WorkspaceAuthGuard) or be explicitly marked as public (PublicEndpointGuard) to maintain our security model. Mutations also require permission guards (SettingsPermissionsGuard or CustomPermissionGuard).',
'GraphQL root resolvers (Query, Mutation, Subscription) should have authentication guards (UserAuthGuard or WorkspaceAuthGuard) or be explicitly marked as public (PublicEndpointGuard) and permission guards (SettingsPermissionsGuard or CustomPermissionGuard) to maintain our security model.',
},
messages: {
graphqlResolversShouldBeGuarded:
'All GraphQL resolvers must have authentication guards (@UseGuards(UserAuthGuard/WorkspaceAuthGuard)). Mutations also require permission guards (@UseGuards(..., SettingsPermissionsGuard(PermissionFlagType.XXX)), CustomPermissionGuard for custom logic, or NoPermissionGuard for special cases like onboarding).',
'All GraphQL resolvers must have authentication guards (@UseGuards(UserAuthGuard/WorkspaceAuthGuard)) and permission guards (@UseGuards(..., SettingsPermissionsGuard(PermissionFlagType.XXX)), CustomPermissionGuard for custom logic, or NoPermissionGuard for special cases like onboarding).',
},
schema: [],
hasSuggestions: false,
@@ -12,7 +12,7 @@ ruleTester.run(RULE_NAME, rule, {
code: `
class TestController {
@Get()
@UseGuards(UserAuthGuard)
@UseGuards(UserAuthGuard, NoPermissionGuard)
testMethod() {}
}
`,
@@ -21,7 +21,7 @@ ruleTester.run(RULE_NAME, rule, {
code: `
class TestController {
@Get()
@UseGuards(WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
testMethod() {}
}
`,
@@ -30,7 +30,7 @@ ruleTester.run(RULE_NAME, rule, {
code: `
class TestController {
@Get()
@UseGuards(PublicEndpoint)
@UseGuards(PublicEndpoint, NoPermissionGuard)
testMethod() {}
}
`,
@@ -39,14 +39,14 @@ ruleTester.run(RULE_NAME, rule, {
code: `
class TestController {
@Get()
@UseGuards(CaptchaGuard, PublicEndpoint)
@UseGuards(CaptchaGuard, PublicEndpoint, NoPermissionGuard)
testMethod() {}
}
`,
},
{
code: `
@UseGuards(UserAuthGuard)
@UseGuards(UserAuthGuard, NoPermissionGuard)
class TestController {
@Get()
testMethod() {}
@@ -55,7 +55,7 @@ ruleTester.run(RULE_NAME, rule, {
},
{
code: `
@UseGuards(WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
class TestController {
@Get()
testMethod() {}
@@ -64,13 +64,67 @@ ruleTester.run(RULE_NAME, rule, {
},
{
code: `
@UseGuards(PublicEndpoint)
@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 {
@@ -106,6 +160,20 @@ ruleTester.run(RULE_NAME, rule, {
},
],
},
{
code: `
class TestController {
@Get()
@UseGuards(WorkspaceAuthGuard)
testMethod() {}
}
`,
errors: [
{
messageId: 'restApiMethodsShouldBeGuarded',
},
],
},
{
code: `
class TestController {
@@ -134,5 +202,103 @@ ruleTester.run(RULE_NAME, rule, {
},
],
},
{
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',
},
],
},
],
});
});
@@ -13,6 +13,7 @@ export const restApiMethodsShouldBeGuarded = (node: TSESTree.MethodDefinition) =
);
const hasAuthGuards = typedTokenHelpers.nodeHasAuthGuards(node);
const hasPermissionsGuard = typedTokenHelpers.nodeHasPermissionsGuard(node);
function findClassDeclaration(
node: TSESTree.Node
@@ -32,11 +33,18 @@ export const restApiMethodsShouldBeGuarded = (node: TSESTree.MethodDefinition) =
? typedTokenHelpers.nodeHasAuthGuards(classNode)
: false;
return (
hasRestApiMethodDecorator &&
!hasAuthGuards &&
!hasAuthGuardsOnController
);
const hasPermissionsGuardOnController = classNode
? typedTokenHelpers.nodeHasPermissionsGuard(classNode)
: false;
// All endpoints need both auth guards and permission guards
const missingAuthGuard =
hasRestApiMethodDecorator && !hasAuthGuards && !hasAuthGuardsOnController;
const missingPermissionGuard =
hasRestApiMethodDecorator && !hasPermissionsGuard && !hasPermissionsGuardOnController;
return missingAuthGuard || missingPermissionGuard;
};
export const rule = createRule<[], 'restApiMethodsShouldBeGuarded'>({
@@ -44,11 +52,11 @@ export const rule = createRule<[], 'restApiMethodsShouldBeGuarded'>({
meta: {
docs: {
description:
'REST API endpoints should have authentication guards (UserAuthGuard or WorkspaceAuthGuard) or be explicitly marked as public (PublicEndpointGuard) to maintain our security model.',
'REST API endpoints should have authentication guards (UserAuthGuard or WorkspaceAuthGuard) or be explicitly marked as public (PublicEndpointGuard) and permission guards (SettingsPermissionsGuard or CustomPermissionGuard) to maintain our security model.',
},
messages: {
restApiMethodsShouldBeGuarded:
'All REST API controller endpoints should have @UseGuards(UserAuthGuard), @UseGuards(WorkspaceAuthGuard), or @UseGuards(PublicEndpointGuard) decorators, or one decorating the root of the Controller.',
'All REST API controller endpoints must have authentication guards (@UseGuards(UserAuthGuard/WorkspaceAuthGuard/PublicEndpointGuard)) and permission guards (@UseGuards(..., SettingsPermissionsGuard(PermissionFlagType.XXX)), CustomPermissionGuard for custom logic, or NoPermissionGuard for special cases).',
},
schema: [],
hasSuggestions: false,
@@ -67,4 +75,4 @@ export const rule = createRule<[], 'restApiMethodsShouldBeGuarded'>({
},
};
},
});
});