diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 35ff2632d..f02c4255d 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -25,26 +25,24 @@ class ApplicationsController extends Controller { private function removeSensitiveData($application) { - $token = auth()->user()->currentAccessToken(); $application->makeHidden([ 'id', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($application); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $application->makeHidden([ + 'custom_labels', + 'dockerfile', + 'docker_compose', + 'docker_compose_raw', + 'manual_webhook_secret_bitbucket', + 'manual_webhook_secret_gitea', + 'manual_webhook_secret_github', + 'manual_webhook_secret_gitlab', + 'private_key_id', + 'value', + 'real_value', + ]); } - $application->makeHidden([ - 'custom_labels', - 'dockerfile', - 'docker_compose', - 'docker_compose_raw', - 'manual_webhook_secret_bitbucket', - 'manual_webhook_secret_gitea', - 'manual_webhook_secret_github', - 'manual_webhook_secret_gitlab', - 'private_key_id', - 'value', - 'real_value', - ]); return serializeApiResponse($application); } diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 98a076c49..917171e5c 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -19,26 +19,23 @@ class DatabasesController extends Controller { private function removeSensitiveData($database) { - $token = auth()->user()->currentAccessToken(); $database->makeHidden([ 'id', 'laravel_through_key', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($database); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $database->makeHidden([ + 'internal_db_url', + 'external_db_url', + 'postgres_password', + 'dragonfly_password', + 'redis_password', + 'mongo_initdb_root_password', + 'keydb_password', + 'clickhouse_admin_password', + ]); } - $database->makeHidden([ - 'internal_db_url', - 'external_db_url', - 'postgres_password', - 'dragonfly_password', - 'redis_password', - 'mongo_initdb_root_password', - 'keydb_password', - 'clickhouse_admin_password', - ]); - return serializeApiResponse($database); } diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 666dc55a5..73b452f86 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -16,15 +16,12 @@ class DeployController extends Controller { private function removeSensitiveData($deployment) { - $token = auth()->user()->currentAccessToken(); - if ($token->can('view:sensitive')) { - return serializeApiResponse($deployment); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $deployment->makeHidden([ + 'logs', + ]); } - $deployment->makeHidden([ - 'logs', - ]); - return serializeApiResponse($deployment); } diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index b7190ab1e..a14b0da20 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -11,13 +11,11 @@ class SecurityController extends Controller { private function removeSensitiveData($team) { - $token = auth()->user()->currentAccessToken(); - if ($token->can('view:sensitive')) { - return serializeApiResponse($team); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $team->makeHidden([ + 'private_key', + ]); } - $team->makeHidden([ - 'private_key', - ]); return serializeApiResponse($team); } diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 8c13b1a01..f37040bdd 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -19,25 +19,22 @@ class ServersController extends Controller { private function removeSensitiveDataFromSettings($settings) { - $token = auth()->user()->currentAccessToken(); - if ($token->can('view:sensitive')) { - return serializeApiResponse($settings); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $settings = $settings->makeHidden([ + 'sentinel_token', + ]); } - $settings = $settings->makeHidden([ - 'sentinel_token', - ]); return serializeApiResponse($settings); } private function removeSensitiveData($server) { - $token = auth()->user()->currentAccessToken(); $server->makeHidden([ 'id', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($server); + if (request()->attributes->get('can_read_sensitive', false) === false) { + // Do nothing } return serializeApiResponse($server); diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index bf90322e2..e6b7e9854 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -18,19 +18,16 @@ class ServicesController extends Controller { private function removeSensitiveData($service) { - $token = auth()->user()->currentAccessToken(); $service->makeHidden([ 'id', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($service); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $service->makeHidden([ + 'docker_compose_raw', + 'docker_compose', + ]); } - $service->makeHidden([ - 'docker_compose_raw', - 'docker_compose', - ]); - return serializeApiResponse($service); } diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php index 3f951c6f7..d4b24d8ab 100644 --- a/app/Http/Controllers/Api/TeamController.php +++ b/app/Http/Controllers/Api/TeamController.php @@ -10,20 +10,18 @@ class TeamController extends Controller { private function removeSensitiveData($team) { - $token = auth()->user()->currentAccessToken(); $team->makeHidden([ 'custom_server_limit', 'pivot', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($team); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $team->makeHidden([ + 'smtp_username', + 'smtp_password', + 'resend_api_key', + 'telegram_token', + ]); } - $team->makeHidden([ - 'smtp_username', - 'smtp_password', - 'resend_api_key', - 'telegram_token', - ]); return serializeApiResponse($team); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 5f1731071..a1ce20295 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -69,5 +69,7 @@ class Kernel extends HttpKernel 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, + 'api.ability' => \App\Http\Middleware\ApiAbility::class, + 'api.sensitive' => \App\Http\Middleware\ApiSensitiveData::class, ]; } diff --git a/app/Http/Middleware/ApiAbility.php b/app/Http/Middleware/ApiAbility.php new file mode 100644 index 000000000..324eeebaa --- /dev/null +++ b/app/Http/Middleware/ApiAbility.php @@ -0,0 +1,27 @@ +user()->tokenCan('root')) { + return $next($request); + } + + return parent::handle($request, $next, ...$abilities); + } catch (\Illuminate\Auth\AuthenticationException $e) { + return response()->json([ + 'message' => 'Unauthenticated.', + ], 401); + } catch (\Exception $e) { + return response()->json([ + 'message' => 'Missing required permissions: '.implode(', ', $abilities), + ], 403); + } + } +} diff --git a/app/Http/Middleware/ApiSensitiveData.php b/app/Http/Middleware/ApiSensitiveData.php new file mode 100644 index 000000000..49584ddb3 --- /dev/null +++ b/app/Http/Middleware/ApiSensitiveData.php @@ -0,0 +1,21 @@ +user()->currentAccessToken(); + + // Allow access to sensitive data if token has root or read:sensitive permission + $request->attributes->add([ + 'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'), + ]); + + return $next($request); + } +} diff --git a/app/Http/Middleware/IgnoreReadOnlyApiToken.php b/app/Http/Middleware/IgnoreReadOnlyApiToken.php deleted file mode 100644 index bd6cd1f8a..000000000 --- a/app/Http/Middleware/IgnoreReadOnlyApiToken.php +++ /dev/null @@ -1,28 +0,0 @@ -user()->currentAccessToken(); - if ($token->can('*')) { - return $next($request); - } - if ($token->can('read-only')) { - return response()->json(['message' => 'You are not allowed to perform this action.'], 403); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/OnlyRootApiToken.php b/app/Http/Middleware/OnlyRootApiToken.php deleted file mode 100644 index 8ff1fa0e5..000000000 --- a/app/Http/Middleware/OnlyRootApiToken.php +++ /dev/null @@ -1,25 +0,0 @@ -user()->currentAccessToken(); - if ($token->can('*')) { - return $next($request); - } - - return response()->json(['message' => 'You are not allowed to perform this action.'], 403); - } -} diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 0710e37ff..74eac7132 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -24,6 +24,14 @@ class Executions extends Component #[Locked] public ?string $serverTimezone = null; + public $currentPage = 1; + + public $logsPerPage = 100; + + public $selectedExecution = null; + + public $isPollingActive = false; + public function getListeners() { $teamId = Auth::user()->currentTeam()->id; @@ -54,16 +62,84 @@ class Executions extends Component public function refreshExecutions(): void { $this->executions = $this->task->executions()->take(20)->get(); + if ($this->selectedKey) { + $this->selectedExecution = $this->task->executions()->find($this->selectedKey); + if ($this->selectedExecution && $this->selectedExecution->status !== 'running') { + $this->isPollingActive = false; + } + } } public function selectTask($key): void { if ($key == $this->selectedKey) { $this->selectedKey = null; + $this->selectedExecution = null; + $this->currentPage = 1; + $this->isPollingActive = false; return; } $this->selectedKey = $key; + $this->selectedExecution = $this->task->executions()->find($key); + $this->currentPage = 1; + + // Start polling if task is running + if ($this->selectedExecution && $this->selectedExecution->status === 'running') { + $this->isPollingActive = true; + } + } + + public function polling() + { + if ($this->selectedExecution && $this->isPollingActive) { + $this->selectedExecution->refresh(); + if ($this->selectedExecution->status !== 'running') { + $this->isPollingActive = false; + } + } + } + + public function loadMoreLogs() + { + $this->currentPage++; + } + + public function getLogLinesProperty() + { + if (! $this->selectedExecution) { + return collect(); + } + + if (! $this->selectedExecution->message) { + return collect(['Waiting for task output...']); + } + + $lines = collect(explode("\n", $this->selectedExecution->message)); + + return $lines->take($this->currentPage * $this->logsPerPage); + } + + public function downloadLogs(int $executionId) + { + $execution = $this->executions->firstWhere('id', $executionId); + if (! $execution) { + return; + } + + return response()->streamDownload(function () use ($execution) { + echo $execution->message; + }, 'task-execution-'.$execution->id.'.log'); + } + + public function hasMoreLogs() + { + if (! $this->selectedExecution || ! $this->selectedExecution->message) { + return false; + } + $lines = collect(explode("\n", $this->selectedExecution->message)); + + return $lines->count() > ($this->currentPage * $this->logsPerPage); } public function formatDateInServerTimezone($date) diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index fe68a8ba5..72684bdc6 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -11,13 +11,7 @@ class ApiTokens extends Component public $tokens = []; - public bool $viewSensitiveData = false; - - public bool $readOnly = true; - - public bool $rootAccess = false; - - public array $permissions = ['read-only']; + public array $permissions = ['read']; public $isApiEnabled; @@ -29,51 +23,28 @@ class ApiTokens extends Component public function mount() { $this->isApiEnabled = InstanceSettings::get()->is_api_enabled; + $this->getTokens(); + } + + private function getTokens() + { $this->tokens = auth()->user()->tokens->sortByDesc('created_at'); } - public function updatedViewSensitiveData() + public function updatedPermissions($permissionToUpdate) { - if ($this->viewSensitiveData) { - $this->permissions[] = 'view:sensitive'; - $this->permissions = array_diff($this->permissions, ['*']); - $this->rootAccess = false; + if ($permissionToUpdate == 'root') { + $this->permissions = ['root']; + } elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions)) { + $this->permissions[] = 'read'; + } elseif ($permissionToUpdate == 'deploy') { + $this->permissions = ['deploy']; } else { - $this->permissions = array_diff($this->permissions, ['view:sensitive']); - } - $this->makeSureOneIsSelected(); - } - - public function updatedReadOnly() - { - if ($this->readOnly) { - $this->permissions[] = 'read-only'; - $this->permissions = array_diff($this->permissions, ['*']); - $this->rootAccess = false; - } else { - $this->permissions = array_diff($this->permissions, ['read-only']); - } - $this->makeSureOneIsSelected(); - } - - public function updatedRootAccess() - { - if ($this->rootAccess) { - $this->permissions = ['*']; - $this->readOnly = false; - $this->viewSensitiveData = false; - } else { - $this->readOnly = true; - $this->permissions = ['read-only']; - } - } - - public function makeSureOneIsSelected() - { - if (count($this->permissions) == 0) { - $this->permissions = ['read-only']; - $this->readOnly = true; + if (count($this->permissions) == 0) { + $this->permissions = ['read']; + } } + sort($this->permissions); } public function addNewToken() @@ -82,8 +53,8 @@ class ApiTokens extends Component $this->validate([ 'description' => 'required|min:3|max:255', ]); - $token = auth()->user()->createToken($this->description, $this->permissions); - $this->tokens = auth()->user()->tokens; + $token = auth()->user()->createToken($this->description, array_values($this->permissions)); + $this->getTokens(); session()->flash('token', $token->plainTextToken); } catch (\Exception $e) { return handleError($e, $this); @@ -92,8 +63,12 @@ class ApiTokens extends Component public function revoke(int $id) { - $token = auth()->user()->tokens()->where('id', $id)->first(); - $token->delete(); - $this->tokens = auth()->user()->tokens; + try { + $token = auth()->user()->tokens()->where('id', $id)->firstOrFail(); + $token->delete(); + $this->getTokens(); + } catch (\Exception $e) { + return handleError($e, $this); + } } } diff --git a/app/Notifications/Channels/SendsSlack.php b/app/Notifications/Channels/SendsSlack.php index 417d4adda..ab2dd6f11 100644 --- a/app/Notifications/Channels/SendsSlack.php +++ b/app/Notifications/Channels/SendsSlack.php @@ -5,4 +5,4 @@ namespace App\Notifications\Channels; interface SendsSlack { public function routeNotificationForSlack(); -} \ No newline at end of file +} diff --git a/app/Notifications/Dto/SlackMessage.php b/app/Notifications/Dto/SlackMessage.php index 86532c65b..879bf6547 100644 --- a/app/Notifications/Dto/SlackMessage.php +++ b/app/Notifications/Dto/SlackMessage.php @@ -8,8 +8,7 @@ class SlackMessage public string $title, public string $description, public string $color = '#0099ff' - ) { - } + ) {} public static function infoColor(): string { @@ -30,4 +29,4 @@ class SlackMessage { return '#ffa500'; } -} \ No newline at end of file +} diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php index 0bdebe7e4..e46598e8e 100644 --- a/app/View/Components/Forms/Checkbox.php +++ b/app/View/Components/Forms/Checkbox.php @@ -15,6 +15,7 @@ class Checkbox extends Component public ?string $id = null, public ?string $name = null, public ?string $value = null, + public ?string $domValue = null, public ?string $label = null, public ?string $helper = null, public string|bool|null $checked = false, diff --git a/config/constants.php b/config/constants.php index 648af735a..4ea4411b6 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.376', + 'version' => '4.0.0-beta.377', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/database/migrations/2024_10_30_074601_rename_token_permissions.php b/database/migrations/2024_10_30_074601_rename_token_permissions.php new file mode 100644 index 000000000..2ca98d090 --- /dev/null +++ b/database/migrations/2024_10_30_074601_rename_token_permissions.php @@ -0,0 +1,60 @@ +abilities)) { + $abilities->push('root'); + } + if (in_array('read-only', $token->abilities)) { + $abilities->push('read'); + } + if (in_array('view:sensitive', $token->abilities)) { + $abilities->push('read', 'read:sensitive'); + } + $token->abilities = $abilities->unique()->values()->all(); + $token->save(); + } + } catch (\Exception $e) { + \Log::error('Error renaming token permissions: '.$e->getMessage()); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + try { + $tokens = PersonalAccessToken::all(); + foreach ($tokens as $token) { + $abilities = collect(); + if (in_array('write', $token->abilities)) { + $abilities->push('*'); + } else { + if (in_array('read', $token->abilities)) { + $abilities->push('read-only'); + } + if (in_array('read:sensitive', $token->abilities)) { + $abilities->push('view:sensitive'); + } + } + $token->abilities = $abilities->unique()->values()->all(); + $token->save(); + } + } catch (\Exception $e) { + \Log::error('Error renaming token permissions: '.$e->getMessage()); + } + } +}; diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index fb244962d..39704a122 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -5,8 +5,8 @@ 'disabled' => false, 'instantSave' => false, 'value' => null, + 'domValue' => null, 'checked' => false, - 'hideLabel' => false, 'fullWidth' => false, ]) @@ -14,26 +14,32 @@ 'flex flex-row items-center gap-4 pr-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100', 'w-full' => $fullWidth, ])> - @if (!$hideLabel) - - @endif + diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index 323191bfd..54a2e953e 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -12,7 +12,7 @@
{{ data_get($execution, 'message') }}+
+@foreach ($this->logLines as $line) +{{ $line }} +@endforeach ++