1487 extensibility look into public domains to identify workspace (#14456)
- identify workspace based on public domains - add cron job to validate public domains - add endpoint to validate a public domain
This commit is contained in:
-106
@@ -1,106 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { DnsCloudflareController } from 'src/engine/core-modules/dns-manager/controllers/dns-cloudflare.controller';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
|
||||
describe('DnsCloudflareController - customHostnameWebhooks', () => {
|
||||
let controller: DnsCloudflareController;
|
||||
let dnsManagerService: DnsManagerService;
|
||||
let domainManagerService: DomainManagerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [DnsCloudflareController],
|
||||
providers: [
|
||||
{
|
||||
provide: DomainManagerService,
|
||||
useValue: {
|
||||
handleCustomDomainActivation: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DnsManagerService,
|
||||
useValue: {
|
||||
isHostnameWorking: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: HttpExceptionHandlerService,
|
||||
useValue: {
|
||||
handleError: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ExceptionHandlerService,
|
||||
useValue: {
|
||||
captureExceptions: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<DnsCloudflareController>(DnsCloudflareController);
|
||||
dnsManagerService = module.get<DnsManagerService>(DnsManagerService);
|
||||
domainManagerService =
|
||||
module.get<DomainManagerService>(DomainManagerService);
|
||||
});
|
||||
|
||||
it('should return if hostname is missing', async () => {
|
||||
const req = {
|
||||
headers: { 'cf-webhook-auth': 'correct-secret' },
|
||||
body: { data: { data: {} } },
|
||||
} as unknown as Request;
|
||||
|
||||
await controller.customHostnameWebhooks(req);
|
||||
|
||||
expect(dnsManagerService.isHostnameWorking).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return if wrong alert_type', async () => {
|
||||
const req = {
|
||||
headers: { 'cf-webhook-auth': 'correct-secret' },
|
||||
body: { alert_type: 'wrong_alert_type', data: { data: {} } },
|
||||
} as unknown as Request;
|
||||
|
||||
await controller.customHostnameWebhooks(req);
|
||||
|
||||
expect(dnsManagerService.isHostnameWorking).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update workspace for a valid hostname and save changes', async () => {
|
||||
const req = {
|
||||
headers: { 'cf-webhook-auth': 'correct-secret' },
|
||||
body: {
|
||||
alert_type: 'custom_ssl_certificate_event_type',
|
||||
data: { data: { hostname: 'example.com' } },
|
||||
},
|
||||
} as unknown as Request;
|
||||
|
||||
jest.spyOn(dnsManagerService, 'isHostnameWorking').mockResolvedValue(true);
|
||||
|
||||
await controller.customHostnameWebhooks(req);
|
||||
|
||||
expect(dnsManagerService.isHostnameWorking).toHaveBeenCalled();
|
||||
|
||||
expect(
|
||||
domainManagerService.handleCustomDomainActivation,
|
||||
).toHaveBeenCalledWith({
|
||||
customDomain: 'example.com',
|
||||
isCustomDomainWorking: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
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 { CloudflareSecretMatchGuard } from 'src/engine/core-modules/dns-manager/guards/cloudflare-secret.guard';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
|
||||
import { DnsManagerExceptionFilter } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter';
|
||||
|
||||
@Controller()
|
||||
@UseFilters(AuthRestApiExceptionFilter, DnsManagerExceptionFilter)
|
||||
export class DnsCloudflareController {
|
||||
constructor(
|
||||
protected readonly domainManagerService: DomainManagerService,
|
||||
protected readonly dnsManagerService: DnsManagerService,
|
||||
) {}
|
||||
|
||||
@Post(['cloudflare/custom-hostname-webhooks', 'webhooks/cloudflare'])
|
||||
@UseGuards(CloudflareSecretMatchGuard, PublicEndpointGuard)
|
||||
async customHostnameWebhooks(@Req() req: Request) {
|
||||
const alertType = req.body?.alert_type;
|
||||
|
||||
const hostname = req.body?.data?.data?.hostname;
|
||||
|
||||
if (alertType !== 'custom_ssl_certificate_event_type' || !hostname) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isCustomDomainWorking =
|
||||
await this.dnsManagerService.isHostnameWorking(hostname);
|
||||
|
||||
await this.domainManagerService.handleCustomDomainActivation({
|
||||
customDomain: hostname,
|
||||
isCustomDomainWorking,
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
import { DnsCloudflareController } from 'src/engine/core-modules/dns-manager/controllers/dns-cloudflare.controller';
|
||||
|
||||
@Module({
|
||||
imports: [DomainManagerModule],
|
||||
providers: [DnsManagerService],
|
||||
controllers: [DnsCloudflareController],
|
||||
exports: [DnsManagerService],
|
||||
})
|
||||
export class DnsManagerModule {}
|
||||
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import {
|
||||
type CanActivate,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { timingSafeEqual } from 'crypto';
|
||||
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class CloudflareSecretMatchGuard implements CanActivate {
|
||||
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
try {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
|
||||
const cloudflareWebhookSecret = this.twentyConfigService.get(
|
||||
'CLOUDFLARE_WEBHOOK_SECRET',
|
||||
);
|
||||
|
||||
if (
|
||||
!cloudflareWebhookSecret ||
|
||||
(cloudflareWebhookSecret &&
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
(typeof request.headers['cf-webhook-auth'] === 'string' ||
|
||||
timingSafeEqual(
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
Buffer.from(request.headers['cf-webhook-auth']),
|
||||
Buffer.from(cloudflareWebhookSecret),
|
||||
)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import { type ExecutionContext } from '@nestjs/common';
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { CloudflareSecretMatchGuard } from 'src/engine/core-modules/dns-manager/guards/cloudflare-secret.guard';
|
||||
|
||||
describe('CloudflareSecretMatchGuard.canActivate', () => {
|
||||
let guard: CloudflareSecretMatchGuard;
|
||||
let twentyConfigService: TwentyConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
twentyConfigService = {
|
||||
get: jest.fn(),
|
||||
} as unknown as TwentyConfigService;
|
||||
guard = new CloudflareSecretMatchGuard(twentyConfigService);
|
||||
});
|
||||
|
||||
it('should return true when the webhook secret matches', () => {
|
||||
const mockRequest = { headers: { 'cf-webhook-auth': 'valid-secret' } };
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue('valid-secret');
|
||||
|
||||
const mockContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => mockRequest,
|
||||
}),
|
||||
} as unknown as ExecutionContext;
|
||||
|
||||
jest.spyOn(crypto, 'timingSafeEqual').mockReturnValue(true);
|
||||
|
||||
expect(guard.canActivate(mockContext)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when env is not set', () => {
|
||||
const mockRequest = { headers: { 'cf-webhook-auth': 'valid-secret' } };
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(undefined);
|
||||
|
||||
const mockContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => mockRequest,
|
||||
}),
|
||||
} as unknown as ExecutionContext;
|
||||
|
||||
jest.spyOn(crypto, 'timingSafeEqual').mockReturnValue(true);
|
||||
|
||||
expect(guard.canActivate(mockContext)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if an error occurs', () => {
|
||||
const mockRequest = { headers: {} };
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue('valid-secret');
|
||||
|
||||
const mockContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => mockRequest,
|
||||
}),
|
||||
} as unknown as ExecutionContext;
|
||||
|
||||
expect(guard.canActivate(mockContext)).toBe(false);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {
|
||||
DnsManagerExceptionCode,
|
||||
} from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { dnsManagerValidator } from 'src/engine/core-modules/dns-manager/validator/cloudflare.validate';
|
||||
import { dnsManagerValidator } from 'src/engine/core-modules/dns-manager/validator/dns-manager.validate';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user