mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(dns): add opt-in Cloudflare proxy mode for new DNS records
This commit is contained in:
@@ -30,6 +30,8 @@ class IntegrationTokenEditor extends Component
|
||||
|
||||
public bool $automaticDns = true;
|
||||
|
||||
public bool $proxiedDns = false;
|
||||
|
||||
public function mount(string $integration_token_uuid): void
|
||||
{
|
||||
$this->integrationToken = IntegrationToken::ownedByCurrentTeam()
|
||||
@@ -43,6 +45,7 @@ class IntegrationTokenEditor extends Component
|
||||
$this->metadata = $this->integrationToken->metadata ?? [];
|
||||
$this->loadZones();
|
||||
$this->automaticDns = $this->integrationToken->automaticDnsEnabled();
|
||||
$this->proxiedDns = $this->integrationToken->proxiedDnsEnabled();
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
@@ -55,6 +58,7 @@ class IntegrationTokenEditor extends Component
|
||||
'capabilities' => ['required', 'array', 'min:1'],
|
||||
'capabilities.*' => ['required', 'in:'.$allowedCapability],
|
||||
'automaticDns' => ['boolean'],
|
||||
'proxiedDns' => ['boolean'],
|
||||
];
|
||||
|
||||
if ($this->integrationToken->provider === 'infisical') {
|
||||
@@ -91,6 +95,11 @@ class IntegrationTokenEditor extends Component
|
||||
} else {
|
||||
$metadata['automatic_dns'] = false;
|
||||
}
|
||||
if ($validated['proxiedDns']) {
|
||||
$metadata['proxied_dns'] = true;
|
||||
} else {
|
||||
unset($metadata['proxied_dns']);
|
||||
}
|
||||
}
|
||||
$storedMetadata = $this->integrationToken->metadata ?? [];
|
||||
$changedConnectionFields = $this->changedConnectionFields($provider, $storedMetadata, $metadata);
|
||||
|
||||
@@ -27,6 +27,8 @@ class IntegrationTokenForm extends Component
|
||||
|
||||
public bool $automaticDns = true;
|
||||
|
||||
public bool $proxiedDns = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('create', IntegrationToken::class);
|
||||
@@ -38,6 +40,7 @@ class IntegrationTokenForm extends Component
|
||||
$this->capabilities = ['dns'];
|
||||
$this->metadata = [];
|
||||
$this->automaticDns = true;
|
||||
$this->proxiedDns = false;
|
||||
} else {
|
||||
$this->capabilities = ['secrets'];
|
||||
$this->metadata = $this->provider === 'infisical'
|
||||
@@ -57,6 +60,7 @@ class IntegrationTokenForm extends Component
|
||||
'capabilities' => ['required', 'array', 'min:1'],
|
||||
'capabilities.*' => ['required', 'in:'.$allowedCapability],
|
||||
'automaticDns' => ['boolean'],
|
||||
'proxiedDns' => ['boolean'],
|
||||
];
|
||||
|
||||
if ($this->provider === 'infisical') {
|
||||
@@ -94,6 +98,9 @@ class IntegrationTokenForm extends Component
|
||||
if ($validated['provider'] === 'cloudflare' && ! $validated['automaticDns']) {
|
||||
$metadata['automatic_dns'] = false;
|
||||
}
|
||||
if ($validated['provider'] === 'cloudflare' && $validated['proxiedDns']) {
|
||||
$metadata['proxied_dns'] = true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (! $validator->validate($validated['provider'], $validated['token'], $validated['capabilities'], $metadata)) {
|
||||
|
||||
@@ -76,6 +76,12 @@ class IntegrationToken extends BaseModel
|
||||
return $this->provider === 'cloudflare' && data_get($this->metadata, 'automatic_dns', true) !== false;
|
||||
}
|
||||
|
||||
/** New Cloudflare records are created proxied (orange cloud) when enabled, so the server IP is not exposed. */
|
||||
public function proxiedDnsEnabled(): bool
|
||||
{
|
||||
return $this->provider === 'cloudflare' && data_get($this->metadata, 'proxied_dns') === true;
|
||||
}
|
||||
|
||||
public function dopplerTokenType(): ?string
|
||||
{
|
||||
if ($this->provider !== 'doppler') {
|
||||
|
||||
@@ -153,7 +153,8 @@ class CloudflareDnsProvider
|
||||
|
||||
$uuid = new_public_id();
|
||||
$response = $this->client($zone->integrationToken)->post("https://api.cloudflare.com/client/v4/zones/{$zone->provider_zone_id}/dns_records", [
|
||||
'type' => $type, 'name' => $hostname, 'content' => $content, 'ttl' => 1, 'proxied' => false,
|
||||
'type' => $type, 'name' => $hostname, 'content' => $content, 'ttl' => 1,
|
||||
'proxied' => $zone->integrationToken->proxiedDnsEnabled(),
|
||||
'comment' => ManagedDnsRecord::ownershipCommentFor($uuid),
|
||||
]);
|
||||
if (! $response->successful() || ! is_string($response->json('result.id'))) {
|
||||
|
||||
@@ -50,6 +50,13 @@
|
||||
Create DNS records automatically when a new domain has one unambiguous matching credential.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-neutral-200 p-1 dark:border-white/[0.08]">
|
||||
<x-forms.checkbox id="edit-proxied-dns" label="Proxy DNS records through Cloudflare" fullWidth
|
||||
wire:model.live="proxiedDns" canGate="update" :canResource="$integrationToken" />
|
||||
<p class="px-2.5 pb-2 text-[11px] text-neutral-500 dark:text-fg-dim">
|
||||
Create new DNS records with the Cloudflare proxy on (orange cloud), so the server IP is not public. Existing records keep their proxy status.
|
||||
</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[11px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
|
||||
<div class="font-medium text-black dark:text-fg">Capability: Secrets (read-only)</div>
|
||||
|
||||
@@ -58,6 +58,13 @@
|
||||
Create DNS records automatically when a new domain has one unambiguous matching credential.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-neutral-200 p-1 dark:border-white/[0.08]">
|
||||
<x-forms.checkbox id="proxied-dns" label="Proxy DNS records through Cloudflare" fullWidth
|
||||
wire:model.live="proxiedDns" canGate="create" :canResource="\App\Models\IntegrationToken::class" />
|
||||
<p class="px-2.5 pb-2 text-[11px] text-neutral-500 dark:text-fg-dim">
|
||||
Create new DNS records with the Cloudflare proxy on (orange cloud), so the server IP is not public. Existing records keep their proxy status.
|
||||
</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[11px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
|
||||
<div class="font-medium text-black dark:text-fg">Capability: Secrets (read-only)</div>
|
||||
|
||||
@@ -120,6 +120,23 @@ test('a cloudflare record is created and tracked as managed by coolify', functio
|
||||
&& $request->data()['comment'] === $record->ownershipComment());
|
||||
});
|
||||
|
||||
test('cloudflare records are created proxied only when the credential enables proxy mode', function (?array $metadata, bool $proxied) {
|
||||
$token = IntegrationToken::factory()->create(['provider' => 'cloudflare', 'token' => 'secret', 'metadata' => $metadata]);
|
||||
$zone = DnsProviderZone::factory()->for($token)->create(['provider_zone_id' => 'zone-1', 'name' => 'example.com']);
|
||||
|
||||
Http::fake([
|
||||
'https://api.cloudflare.com/client/v4/zones/zone-1/dns_records?*' => Http::response(['success' => true, 'result' => []]),
|
||||
'https://api.cloudflare.com/client/v4/zones/zone-1/dns_records' => Http::response(['success' => true, 'result' => ['id' => 'record-1']]),
|
||||
]);
|
||||
|
||||
app(CloudflareDnsProvider::class)->createRecord($zone, 'app.example.com', '203.0.113.10');
|
||||
|
||||
Http::assertSent(fn ($request) => $request->method() === 'POST' && $request->data()['proxied'] === $proxied);
|
||||
})->with([
|
||||
'default' => [null, false],
|
||||
'enabled' => [['proxied_dns' => true], true],
|
||||
]);
|
||||
|
||||
test('queued dns configuration creates the record and broadcasts completion', function () {
|
||||
Event::fake([DnsRecordConfigurationFinished::class]);
|
||||
$token = IntegrationToken::factory()->create(['provider' => 'cloudflare', 'token' => 'secret']);
|
||||
|
||||
@@ -102,6 +102,38 @@ test('automatic dns is enabled by default and can be disabled when saving a clou
|
||||
expect(IntegrationToken::query()->sole()->automaticDnsEnabled())->toBeFalse();
|
||||
});
|
||||
|
||||
test('cloudflare proxy mode is off by default and can be enabled when adding and editing a token', function () {
|
||||
Http::fake([
|
||||
'https://api.cloudflare.com/client/v4/user/tokens/verify' => Http::response(['success' => true, 'result' => ['status' => 'active']]),
|
||||
'https://api.cloudflare.com/client/v4/zones?per_page=1' => Http::response(['success' => true, 'result' => [['id' => 'zone-id']]]),
|
||||
'https://api.cloudflare.com/client/v4/zones/zone-id/dns_records?per_page=1' => Http::response(['success' => true, 'result' => []]),
|
||||
'https://api.cloudflare.com/client/v4/zones?page=1&per_page=50' => Http::response([
|
||||
'success' => true,
|
||||
'result' => [['id' => 'zone-id', 'name' => 'example.com', 'account' => ['id' => 'account-id', 'name' => 'Production']]],
|
||||
'result_info' => ['total_pages' => 1],
|
||||
]),
|
||||
]);
|
||||
|
||||
Livewire::test(IntegrationTokenForm::class)
|
||||
->assertSet('proxiedDns', false)
|
||||
->set('name', 'Proxied DNS')
|
||||
->set('token', 'cloudflare-token')
|
||||
->set('proxiedDns', true)
|
||||
->call('addToken')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$token = IntegrationToken::query()->sole();
|
||||
expect($token->proxiedDnsEnabled())->toBeTrue();
|
||||
|
||||
Livewire::test(IntegrationTokenEditor::class, ['integration_token_uuid' => $token->uuid])
|
||||
->assertSet('proxiedDns', true)
|
||||
->set('proxiedDns', false)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($token->refresh()->proxiedDnsEnabled())->toBeFalse();
|
||||
});
|
||||
|
||||
test('deleting an integration token is audited without storing its value', function () {
|
||||
$token = IntegrationToken::query()->create([
|
||||
'team_id' => $this->team->id,
|
||||
|
||||
Reference in New Issue
Block a user