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:
@@ -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) {}
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
+10
-4
@@ -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) {}
|
||||
|
||||
+7
-1
@@ -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> {
|
||||
|
||||
+12
-3
@@ -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,
|
||||
|
||||
+13
-2
@@ -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(
|
||||
|
||||
+11
-2
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+34
-9
@@ -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);
|
||||
|
||||
+3
-2
@@ -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();
|
||||
|
||||
|
||||
+2
-1
@@ -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
-1
@@ -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();
|
||||
}
|
||||
|
||||
+4
-3
@@ -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) {}
|
||||
|
||||
|
||||
+3
-2
@@ -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 = {
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+8
@@ -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,
|
||||
|
||||
+8
@@ -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,
|
||||
|
||||
+8
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+10
-4
@@ -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()
|
||||
|
||||
+7
-1
@@ -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) {}
|
||||
|
||||
+3
-2
@@ -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(
|
||||
|
||||
+4
-1
@@ -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()
|
||||
|
||||
+4
-1
@@ -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()
|
||||
|
||||
+3
-2
@@ -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) {}
|
||||
|
||||
+5
-1
@@ -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)
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+39
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+35
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+14
-1
@@ -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)
|
||||
|
||||
+87
@@ -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'>({
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user