Extract SecureHttpClientService into its own module (#17828)
## Summary - **Extract `SecureHttpClientService`** from `tool` module into a dedicated `core-modules/secure-http-client/` module with proper NestJS module encapsulation - **Fix module hygiene**: 12 modules that incorrectly listed `SecureHttpClientService` as a direct provider now properly import `SecureHttpClientModule` - **Add structured logging** for outbound HTTP requests with workspace/user context (for GuardDuty alert correlation) - **Rename type files** to follow one-export-per-file convention (`get-secure-axios-adapter.types.ts` -> `secure-adapter-dependencies.type.ts`, new `outbound-request-context.type.ts` / `outbound-request-source.type.ts`) ### Why `SecureHttpClientService` is a cross-cutting concern (used by auth, captcha, file upload, geo-map, telemetry, admin-panel, REST API, contact creation, webhooks, and workflow tools) but was bundled inside the `tool` module. Most consumers worked around this by listing it as a direct provider instead of importing a module, which is fragile and not idiomatic NestJS. ## Test plan - [x] All 60 unit tests pass (`secure-http-client.service.spec.ts`, `get-secure-axios-adapter.util.spec.ts`, `is-private-ip.util.spec.ts`) - [x] Related module tests pass (admin-panel, contact-creation, tool) - [x] `npx nx typecheck twenty-server` passes - [x] `npx nx lint:diff-with-main twenty-server` passes - [x] Server compiles and bootstraps successfully Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,12 +21,12 @@ import { restToCommonArgsHandlers } from 'src/engine/api/rest/core/rest-to-commo
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
@@ -66,12 +66,12 @@ const restApiCoreResolvers = [
|
||||
CoreCommonApiModule,
|
||||
WorkspaceDomainsModule,
|
||||
WorkspaceCacheModule,
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
controllers: [RestApiCoreController],
|
||||
providers: [
|
||||
RestApiService,
|
||||
RestApiCoreService,
|
||||
SecureHttpClientService,
|
||||
...restApiCoreResolvers,
|
||||
...restToCommonArgsHandlers,
|
||||
],
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Module } from '@nestjs/common';
|
||||
import { MetadataQueryBuilderModule } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.module';
|
||||
import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { RestApiCoreModule } from 'src/engine/api/rest/core/rest-api-core.module';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
@@ -17,9 +17,10 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
AuthModule,
|
||||
RestApiCoreModule,
|
||||
PermissionsModule,
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
controllers: [RestApiMetadataController],
|
||||
providers: [RestApiService, RestApiMetadataService, SecureHttpClientService],
|
||||
providers: [RestApiService, RestApiMetadataService],
|
||||
exports: [RestApiMetadataService, RestApiService],
|
||||
})
|
||||
export class RestApiModule {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { type AxiosResponse } from 'axios';
|
||||
import { type Query } from 'src/engine/api/rest/core/types/query.type';
|
||||
import { RestApiException } from 'src/engine/api/rest/errors/RestApiException';
|
||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
|
||||
export enum GraphqlApiType {
|
||||
CORE = 'core',
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { AuditService } from 'src/engine/core-modules/audit/services/audit.servi
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admi
|
||||
import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-panel.resolver';
|
||||
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
@@ -20,6 +19,7 @@ import { WorkerHealthIndicator } from 'src/engine/core-modules/admin-panel/indic
|
||||
import { ImpersonationModule } from 'src/engine/core-modules/impersonation/impersonation.module';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -39,13 +39,13 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
TelemetryModule,
|
||||
ImpersonationModule,
|
||||
PermissionsModule,
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
providers: [
|
||||
AdminPanelResolver,
|
||||
AdminPanelService,
|
||||
AdminPanelHealthService,
|
||||
AdminPanelQueueService,
|
||||
SecureHttpClientService,
|
||||
DatabaseHealthIndicator,
|
||||
RedisHealthIndicator,
|
||||
WorkerHealthIndicator,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspac
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { type FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||
import { CONFIG_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata';
|
||||
import { type ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||
|
||||
@@ -44,8 +44,8 @@ import { KeyValuePairEntity } from 'src/engine/core-modules/key-value-pair/key-v
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { TwoFactorAuthenticationMethodEntity } from 'src/engine/core-modules/two-factor-authentication/entities/two-factor-authentication-method.entity';
|
||||
import { TwoFactorAuthenticationModule } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
@@ -116,6 +116,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
DomainServerConfigModule,
|
||||
ApplicationModule,
|
||||
WorkspaceCacheModule,
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
controllers: [
|
||||
GoogleAuthController,
|
||||
@@ -150,7 +151,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
UpdateConnectedAccountOnReconnectService,
|
||||
TransientTokenService,
|
||||
AuthSsoService,
|
||||
SecureHttpClientService,
|
||||
],
|
||||
exports: [
|
||||
AccessTokenService,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { includesExpectedScopes } from 'src/engine/core-modules/auth/services/google-apis-scopes.service.util';
|
||||
import { getGoogleApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
|
||||
interface TokenInfoResponse {
|
||||
scope: string;
|
||||
|
||||
@@ -30,7 +30,7 @@ import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomai
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
CaptchaDriverType,
|
||||
type CaptchaModuleAsyncOptions,
|
||||
} from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
|
||||
@Global()
|
||||
export class CaptchaModule {
|
||||
@@ -51,7 +52,8 @@ export class CaptchaModule {
|
||||
|
||||
return {
|
||||
module: CaptchaModule,
|
||||
providers: [CaptchaService, SecureHttpClientService, provider],
|
||||
imports: [SecureHttpClientModule],
|
||||
providers: [CaptchaService, provider],
|
||||
exports: [CaptchaService],
|
||||
};
|
||||
}
|
||||
|
||||
+3
-3
@@ -3,12 +3,12 @@ import { Module } from '@nestjs/common';
|
||||
import { FileUploadResolver } from 'src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver';
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [FileModule, PermissionsModule],
|
||||
providers: [FileUploadService, FileUploadResolver, SecureHttpClientService],
|
||||
imports: [FileModule, PermissionsModule, SecureHttpClientModule],
|
||||
providers: [FileUploadService, FileUploadResolver],
|
||||
exports: [FileUploadService, FileUploadResolver],
|
||||
})
|
||||
export class FileUploadModule {}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { FileStorageService } from 'src/engine/core-modules/file-storage/file-st
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { buildFileInfo } from 'src/engine/core-modules/file/utils/build-file-info.utils';
|
||||
import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { getCropSize, getImageBufferFromUrl } from 'src/utils/image';
|
||||
|
||||
export type SignedFile = { path: string; token: string };
|
||||
|
||||
@@ -9,7 +9,7 @@ import { FileWorkspaceFolderDeletionJob } from 'src/engine/core-modules/file/job
|
||||
import { FileAttachmentListener } from 'src/engine/core-modules/file/listeners/file-attachment.listener';
|
||||
import { FileWorkspaceMemberListener } from 'src/engine/core-modules/file/listeners/file-workspace-member.listener';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@@ -28,6 +28,7 @@ import { FileService } from './services/file.service';
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
FilesFieldModule,
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
providers: [
|
||||
FileService,
|
||||
@@ -39,7 +40,6 @@ import { FileService } from './services/file.service';
|
||||
FileWorkspaceFolderDeletionJob,
|
||||
FileDeletionJob,
|
||||
FileUploadService,
|
||||
SecureHttpClientService,
|
||||
],
|
||||
exports: [FileService, FileMetadataService],
|
||||
controllers: [FileController],
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Module } from '@nestjs/common';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { GeoMapResolver } from 'src/engine/core-modules/geo-map/resolver/geo-map.resolver';
|
||||
import { GeoMapService } from 'src/engine/core-modules/geo-map/services/geo-map.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceCacheStorageModule, TokenModule],
|
||||
providers: [GeoMapService, GeoMapResolver, SecureHttpClientService],
|
||||
imports: [WorkspaceCacheStorageModule, TokenModule, SecureHttpClientModule],
|
||||
providers: [GeoMapService, GeoMapResolver],
|
||||
exports: [],
|
||||
})
|
||||
export class GeoMapModule {}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type AddressFields,
|
||||
sanitizePlaceDetailsResults,
|
||||
} from 'src/engine/core-modules/geo-map/utils/sanitize-place-details-results.util';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
const createMockConfigService = (
|
||||
overrides: Record<string, unknown> = {},
|
||||
): TwentyConfigService => {
|
||||
const defaults: Record<string, unknown> = {
|
||||
OUTBOUND_HTTP_SAFE_MODE_ENABLED: false,
|
||||
};
|
||||
const config = { ...defaults, ...overrides };
|
||||
|
||||
return {
|
||||
get: jest.fn((key: string) => config[key]),
|
||||
} as unknown as TwentyConfigService;
|
||||
};
|
||||
|
||||
describe('SecureHttpClientService', () => {
|
||||
describe('getHttpClient', () => {
|
||||
it('should return a plain axios instance when safe mode is off', () => {
|
||||
const service = new SecureHttpClientService(createMockConfigService());
|
||||
const client = service.getHttpClient();
|
||||
|
||||
expect(client).toBeDefined();
|
||||
expect(client.defaults.httpAgent).toBeUndefined();
|
||||
expect(client.defaults.httpsAgent).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return an axios instance with secure adapter when safe mode is on', () => {
|
||||
const service = new SecureHttpClientService(
|
||||
createMockConfigService({ OUTBOUND_HTTP_SAFE_MODE_ENABLED: true }),
|
||||
);
|
||||
const client = service.getHttpClient();
|
||||
|
||||
expect(client).toBeDefined();
|
||||
expect(typeof client.defaults.adapter).toBe('function');
|
||||
});
|
||||
|
||||
it('should pass through axios config like baseURL', () => {
|
||||
const service = new SecureHttpClientService(createMockConfigService());
|
||||
const client = service.getHttpClient({
|
||||
baseURL: 'https://example.com/api',
|
||||
});
|
||||
|
||||
expect(client.defaults.baseURL).toBe('https://example.com/api');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInternalHttpClient', () => {
|
||||
it('should return a plain axios instance regardless of safe mode', () => {
|
||||
const service = new SecureHttpClientService(
|
||||
createMockConfigService({ OUTBOUND_HTTP_SAFE_MODE_ENABLED: true }),
|
||||
);
|
||||
const client = service.getInternalHttpClient();
|
||||
|
||||
expect(client).toBeDefined();
|
||||
});
|
||||
|
||||
it('should pass through axios config like baseURL', () => {
|
||||
const service = new SecureHttpClientService(createMockConfigService());
|
||||
const client = service.getInternalHttpClient({
|
||||
baseURL: 'http://localhost:3000',
|
||||
});
|
||||
|
||||
expect(client.defaults.baseURL).toBe('http://localhost:3000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logging interceptor', () => {
|
||||
it('should add a request interceptor when context is provided', () => {
|
||||
const service = new SecureHttpClientService(createMockConfigService());
|
||||
const client = service.getHttpClient(undefined, {
|
||||
workspaceId: 'ws-123',
|
||||
source: 'webhook',
|
||||
});
|
||||
|
||||
const interceptorHandlers = (
|
||||
client.interceptors.request as unknown as {
|
||||
handlers: Array<{ fulfilled: Function }>;
|
||||
}
|
||||
).handlers;
|
||||
|
||||
expect(interceptorHandlers.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not add a request interceptor when context is not provided', () => {
|
||||
const service = new SecureHttpClientService(createMockConfigService());
|
||||
const client = service.getHttpClient();
|
||||
|
||||
const interceptorHandlers = (
|
||||
client.interceptors.request as unknown as {
|
||||
handlers: Array<{ fulfilled: Function }>;
|
||||
}
|
||||
).handlers;
|
||||
|
||||
expect(interceptorHandlers.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should pass config through the interceptor and return it', () => {
|
||||
const service = new SecureHttpClientService(createMockConfigService());
|
||||
const client = service.getHttpClient(undefined, {
|
||||
workspaceId: 'ws-456',
|
||||
source: 'workflow-http',
|
||||
userId: 'user-789',
|
||||
});
|
||||
|
||||
const interceptorHandlers = (
|
||||
client.interceptors.request as unknown as {
|
||||
handlers: Array<{ fulfilled: Function }>;
|
||||
}
|
||||
).handlers;
|
||||
|
||||
const interceptorFn = interceptorHandlers[0].fulfilled;
|
||||
const mockConfig = { method: 'GET', url: 'https://example.com/api' };
|
||||
|
||||
const result = interceptorFn(mockConfig);
|
||||
|
||||
expect(result).toBe(mockConfig);
|
||||
});
|
||||
});
|
||||
});
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type OutboundRequestSource } from './outbound-request-source.type';
|
||||
|
||||
export type OutboundRequestContext = {
|
||||
workspaceId: string;
|
||||
source: OutboundRequestSource;
|
||||
userId?: string;
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type OutboundRequestSource =
|
||||
| 'webhook'
|
||||
| 'workflow-http'
|
||||
| 'logic-function';
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { SecureHttpClientService } from './secure-http-client.service';
|
||||
|
||||
@Module({
|
||||
providers: [SecureHttpClientService],
|
||||
exports: [SecureHttpClientService],
|
||||
})
|
||||
export class SecureHttpClientModule {}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import axios, { type AxiosInstance, type CreateAxiosDefaults } from 'axios';
|
||||
|
||||
import { getSecureAxiosAdapter } from 'src/engine/core-modules/secure-http-client/utils/get-secure-axios-adapter.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
import { type OutboundRequestContext } from './outbound-request-context.type';
|
||||
|
||||
@Injectable()
|
||||
export class SecureHttpClientService {
|
||||
private readonly logger = new Logger(SecureHttpClientService.name);
|
||||
|
||||
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||
|
||||
// Returns an SSRF-protected HTTP client for external requests.
|
||||
// When context is provided, outbound requests are logged with
|
||||
// workspace/user info for GuardDuty correlation.
|
||||
getHttpClient(
|
||||
config?: CreateAxiosDefaults,
|
||||
context?: OutboundRequestContext,
|
||||
): AxiosInstance {
|
||||
const isSafeModeEnabled = this.twentyConfigService.get(
|
||||
'OUTBOUND_HTTP_SAFE_MODE_ENABLED',
|
||||
);
|
||||
|
||||
const client = isSafeModeEnabled
|
||||
? axios.create({ ...config, adapter: getSecureAxiosAdapter() })
|
||||
: axios.create(config);
|
||||
|
||||
if (context) {
|
||||
client.interceptors.request.use((requestConfig) => {
|
||||
this.logger.log(
|
||||
`Outbound HTTP request: ${requestConfig.method?.toUpperCase()} ${requestConfig.url} ` +
|
||||
`[workspace=${context.workspaceId}, source=${context.source}` +
|
||||
`${context.userId ? `, user=${context.userId}` : ''}]`,
|
||||
);
|
||||
|
||||
return requestConfig;
|
||||
});
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
// Returns a plain HTTP client for requests to trusted internal URLs
|
||||
// (e.g., the server's own API endpoints). Not SSRF-protected.
|
||||
getInternalHttpClient(config?: CreateAxiosDefaults): AxiosInstance {
|
||||
return axios.create(config);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -3,8 +3,8 @@ import * as https from 'https';
|
||||
|
||||
import { AxiosHeaders, type InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
import { type SecureAdapterDependencies } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.types';
|
||||
import { getSecureAxiosAdapter } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.util';
|
||||
import { type SecureAdapterDependencies } from 'src/engine/core-modules/secure-http-client/utils/secure-adapter-dependencies.type';
|
||||
import { getSecureAxiosAdapter } from 'src/engine/core-modules/secure-http-client/utils/get-secure-axios-adapter.util';
|
||||
|
||||
describe('getSecureAxiosAdapter', () => {
|
||||
let mockDnsLookup: jest.Mock;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { isPrivateIp } from 'src/engine/core-modules/tool/utils/is-private-ip.util';
|
||||
import { isPrivateIp } from 'src/engine/core-modules/secure-http-client/utils/is-private-ip.util';
|
||||
|
||||
describe('isPrivateIp', () => {
|
||||
describe('loopback addresses', () => {
|
||||
+4
-4
@@ -8,9 +8,9 @@ import axios, {
|
||||
type InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
|
||||
import { isPrivateIp } from 'src/engine/core-modules/tool/utils/is-private-ip.util';
|
||||
import { isPrivateIp } from 'src/engine/core-modules/secure-http-client/utils/is-private-ip.util';
|
||||
|
||||
import { type SecureAdapterDependencies } from './get-secure-axios-adapter.types';
|
||||
import { type SecureAdapterDependencies } from './secure-adapter-dependencies.type';
|
||||
|
||||
const defaultDependencies: SecureAdapterDependencies = {
|
||||
dnsLookup: dns.lookup,
|
||||
@@ -62,14 +62,14 @@ export const getSecureAxiosAdapter = (
|
||||
optionsOrCallback,
|
||||
maybeCallback,
|
||||
) => {
|
||||
const options =
|
||||
const lookupOptions =
|
||||
typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
|
||||
const callback =
|
||||
typeof optionsOrCallback === 'function'
|
||||
? optionsOrCallback
|
||||
: maybeCallback;
|
||||
|
||||
if (options.all) {
|
||||
if (lookupOptions.all) {
|
||||
(callback as Function)(null, [
|
||||
{ address: resolvedIp, family: ipFamily },
|
||||
]);
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
|
||||
import { TelemetryService } from './telemetry.service';
|
||||
|
||||
@Module({
|
||||
providers: [TelemetryService, SecureHttpClientService],
|
||||
imports: [SecureHttpClientModule],
|
||||
providers: [TelemetryService],
|
||||
exports: [TelemetryService],
|
||||
})
|
||||
export class TelemetryModule {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
type CreateEventInput = {
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import axios, { type AxiosInstance, type CreateAxiosDefaults } from 'axios';
|
||||
|
||||
import { getSecureAxiosAdapter } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class SecureHttpClientService {
|
||||
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||
|
||||
// Returns an SSRF-protected HTTP client for external requests
|
||||
getHttpClient(config?: CreateAxiosDefaults): AxiosInstance {
|
||||
const isSafeModeEnabled = this.twentyConfigService.get(
|
||||
'OUTBOUND_HTTP_SAFE_MODE_ENABLED',
|
||||
);
|
||||
|
||||
return isSafeModeEnabled
|
||||
? axios.create({ ...config, adapter: getSecureAxiosAdapter() })
|
||||
: axios.create(config);
|
||||
}
|
||||
|
||||
// Returns a plain HTTP client for requests to trusted internal URLs
|
||||
// (e.g., the server's own API endpoints). Not SSRF-protected.
|
||||
getInternalHttpClient(config?: CreateAxiosDefaults): AxiosInstance {
|
||||
return axios.create(config);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { CodeInterpreterTool } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
@@ -17,20 +17,14 @@ import { MessagingImportManagerModule } from 'src/modules/messaging/message-impo
|
||||
TypeOrmModule.forFeature([FileEntity]),
|
||||
FileModule,
|
||||
JwtModule,
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
providers: [
|
||||
HttpTool,
|
||||
SendEmailTool,
|
||||
SearchHelpCenterTool,
|
||||
CodeInterpreterTool,
|
||||
SecureHttpClientService,
|
||||
],
|
||||
exports: [
|
||||
HttpTool,
|
||||
SendEmailTool,
|
||||
SearchHelpCenterTool,
|
||||
CodeInterpreterTool,
|
||||
SecureHttpClientService,
|
||||
],
|
||||
exports: [HttpTool, SendEmailTool, SearchHelpCenterTool, CodeInterpreterTool],
|
||||
})
|
||||
export class ToolModule {}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { CodeInterpreterInputZodSchema } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema';
|
||||
import { TWENTY_MCP_HELPER } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const';
|
||||
import { type CodeInterpreterInput } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { type AxiosRequestConfig, isAxiosError } from 'axios';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { parseDataFromContentType } from 'twenty-shared/workflow';
|
||||
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { HttpRequestInputZodSchema } from 'src/engine/core-modules/tool/tools/http-tool/http-tool.schema';
|
||||
import { type HttpRequestInput } from 'src/engine/core-modules/tool/tools/http-tool/types/http-request-input.type';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
@@ -26,7 +26,7 @@ export class HttpTool implements Tool {
|
||||
|
||||
async execute(
|
||||
parameters: ToolInput,
|
||||
_context: ToolExecutionContext,
|
||||
context: ToolExecutionContext,
|
||||
): Promise<ToolOutput> {
|
||||
const { url, method, headers, body } = parameters as HttpRequestInput;
|
||||
const headersCopy = { ...headers };
|
||||
@@ -48,7 +48,14 @@ export class HttpTool implements Tool {
|
||||
}
|
||||
}
|
||||
|
||||
const axiosClient = this.secureHttpClientService.getHttpClient();
|
||||
const axiosClient = this.secureHttpClientService.getHttpClient(
|
||||
undefined,
|
||||
{
|
||||
workspaceId: context.workspaceId,
|
||||
userId: context.userId,
|
||||
source: 'workflow-http',
|
||||
},
|
||||
);
|
||||
const response = await axiosClient(axiosConfig);
|
||||
|
||||
return {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { SearchHelpCenterInputZodSchema } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
|
||||
export type CallWebhookJobData = {
|
||||
targetUrl: string;
|
||||
@@ -83,7 +83,14 @@ export class CallWebhookJob {
|
||||
.toString('hex');
|
||||
}
|
||||
|
||||
const axiosClient = this.secureHttpClientService.getHttpClient();
|
||||
const axiosClient = this.secureHttpClientService.getHttpClient(
|
||||
undefined,
|
||||
{
|
||||
workspaceId: data.workspaceId,
|
||||
userId: data.userId,
|
||||
source: 'webhook',
|
||||
},
|
||||
);
|
||||
|
||||
const response = await axiosClient.post(
|
||||
getAbsoluteUrl(data.targetUrl),
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { ToolModule } from 'src/engine/core-modules/tool/tool.module';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { CallWebhookJobsJob } from 'src/engine/metadata-modules/webhook/jobs/call-webhook-jobs.job';
|
||||
import { CallWebhookJob } from 'src/engine/metadata-modules/webhook/jobs/call-webhook.job';
|
||||
@@ -13,7 +13,7 @@ import { CallWebhookJob } from 'src/engine/metadata-modules/webhook/jobs/call-we
|
||||
TypeOrmModule.forFeature([WebhookEntity]),
|
||||
AuditModule,
|
||||
MetricsModule,
|
||||
ToolModule,
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
providers: [CallWebhookJobsJob, CallWebhookJob],
|
||||
})
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
@@ -15,12 +15,12 @@ import { CreatePersonService } from 'src/modules/contact-creation-manager/servic
|
||||
WorkspaceDataSourceModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity]),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity, FieldMetadataEntity]),
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
providers: [
|
||||
CreateCompanyService,
|
||||
CreatePersonService,
|
||||
CreateCompanyAndPersonService,
|
||||
SecureHttpClientService,
|
||||
],
|
||||
exports: [CreateCompanyAndPersonService],
|
||||
})
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import {
|
||||
type CompanyToCreate,
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {
|
||||
} from 'twenty-shared/utils';
|
||||
import { type DeepPartial, ILike } from 'typeorm';
|
||||
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
|
||||
Reference in New Issue
Block a user