mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-27 18:14:24 -05:00
fix(audit): prevent duplicate API update events
Suppress model audit logging during API application saves while preserving the explicit event, and improve audit log actor display spacing and token tooltips.
This commit is contained in:
@@ -3122,7 +3122,7 @@ class ApplicationsController extends Controller
|
||||
if ($application->settings->is_container_label_readonly_enabled && ($requestHasDomains || $requestHasNoindexDomains || $requestHasHttpBasicAuth) && $server->isProxyShouldRun()) {
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
}
|
||||
$application->save();
|
||||
$application->withoutAuditLogging(fn () => $application->save());
|
||||
|
||||
auditLog('api.application.updated', [
|
||||
'team_id' => $teamId,
|
||||
|
||||
@@ -4,11 +4,14 @@ namespace App\Traits;
|
||||
|
||||
use App\Models\PersonalAccessToken;
|
||||
use App\Models\Team;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait Auditable
|
||||
{
|
||||
private bool $auditLoggingEnabled = true;
|
||||
|
||||
public static function bootAuditable(): void
|
||||
{
|
||||
static::created(fn (Model $model) => $model->recordAuditMutation('created'));
|
||||
@@ -18,7 +21,7 @@ trait Auditable
|
||||
|
||||
private function recordAuditMutation(string $action): void
|
||||
{
|
||||
if (! auth()->check()) {
|
||||
if (! $this->auditLoggingEnabled || ! auth()->check()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,6 +57,17 @@ trait Auditable
|
||||
]);
|
||||
}
|
||||
|
||||
public function withoutAuditLogging(Closure $callback): mixed
|
||||
{
|
||||
$this->auditLoggingEnabled = false;
|
||||
|
||||
try {
|
||||
return $callback();
|
||||
} finally {
|
||||
$this->auditLoggingEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private function auditTeamId(): ?int
|
||||
{
|
||||
if ($this instanceof Team) {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="overflow-x-auto">
|
||||
<div class="data-table min-w-[760px] transition-opacity" wire:loading.class="opacity-50 pointer-events-none"
|
||||
wire:target="search,action,source,setPage,previousPage,nextPage">
|
||||
<div class="grid grid-cols-[10rem_minmax(0,1fr)_12rem_9rem] gap-4 border-b border-neutral-200 px-4 py-2 text-[11px] font-medium uppercase tracking-wide text-neutral-500 dark:border-white/[0.07] dark:text-fg-faint">
|
||||
<div class="grid grid-cols-[14rem_minmax(0,1fr)_12rem_9rem] gap-4 border-b border-neutral-200 px-4 py-2 text-[11px] font-medium uppercase tracking-wide text-neutral-500 dark:border-white/[0.07] dark:text-fg-faint">
|
||||
<span>Actor</span>
|
||||
<span>Activity</span>
|
||||
<span>Source</span>
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
@foreach ($events as $event)
|
||||
<div wire:key="audit-event-{{ $event->id }}"
|
||||
class="grid grid-cols-[10rem_minmax(0,1fr)_12rem_9rem] gap-4 border-b border-neutral-200 px-4 py-3 last:border-b-0 dark:border-white/[0.07]">
|
||||
class="grid grid-cols-[14rem_minmax(0,1fr)_12rem_9rem] gap-4 border-b border-neutral-200 px-4 py-3 last:border-b-0 dark:border-white/[0.07]">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-[12px] font-medium text-black dark:text-fg">
|
||||
{{ $event->actor_name ?: Str::headline($event->actor_type) }}
|
||||
@@ -56,7 +56,8 @@
|
||||
</div>
|
||||
@endif
|
||||
@if ($event->actor_token_name)
|
||||
<div class="truncate text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
<div class="truncate text-[11px] text-neutral-500 dark:text-fg-faint"
|
||||
title="Token: {{ $event->actor_token_name }}">
|
||||
Token: {{ $event->actor_token_name }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -77,7 +78,7 @@
|
||||
</div>
|
||||
<time datetime="{{ $event->created_at->toIso8601String() }}"
|
||||
title="{{ $event->created_at->toDayDateTimeString() }}"
|
||||
class="text-right text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
class="self-center text-right text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
{{ $event->created_at->diffForHumans() }}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
@@ -412,6 +413,33 @@ test('API model mutations produce one audit event', function () {
|
||||
->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('API application updates produce one audit event', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
AuditEvent::query()->delete();
|
||||
|
||||
$token = $this->user->createToken('audit-api', ['root']);
|
||||
$token->accessToken->forceFill(['team_id' => $this->team->id])->save();
|
||||
auth()->logout();
|
||||
auth()->forgetGuards();
|
||||
|
||||
$this->withToken($token->plainTextToken)
|
||||
->patchJson("/api/v1/applications/{$application->uuid}", ['description' => 'Updated through API'])
|
||||
->assertOk();
|
||||
|
||||
expect(AuditEvent::query()
|
||||
->where('event', 'api.application.updated')
|
||||
->where('resource_uuid', $application->uuid)
|
||||
->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('deployment queue records rollback and cancellation operations', function () {
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
@@ -778,11 +806,15 @@ test('audit log table keeps actor details visible in a mobile scroll area', func
|
||||
AuditEvent::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'actor_name' => 'Visible Actor',
|
||||
'actor_token_name' => 'visible-audit-token',
|
||||
]);
|
||||
|
||||
Livewire::test(AuditLog::class)
|
||||
->assertSeeHtml('class="overflow-x-auto"')
|
||||
->assertSeeHtml('min-w-[760px]')
|
||||
->assertSeeHtml('grid-cols-[14rem_minmax(0,1fr)_12rem_9rem]')
|
||||
->assertSeeHtml('class="self-center text-right text-[11px]')
|
||||
->assertSeeHtml('title="Token: visible-audit-token"')
|
||||
->assertSee('Actor')
|
||||
->assertSee('Visible Actor');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user