From b1334a1bc6c1770146696b95ce0689b68757822c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 23 Aug 2025 18:51:10 +0200 Subject: [PATCH] feat(auth): implement comprehensive authorization checks across API controllers --- .../Api/ApplicationsController.php | 30 +++++++++++++++++++ .../Controllers/Api/DatabasesController.php | 20 +++++++++++++ app/Http/Controllers/Api/DeployController.php | 23 +++++++++++++- .../Controllers/Api/ResourcesController.php | 4 +++ .../Controllers/Api/ServicesController.php | 27 +++++++++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 728ddfff5..c07ac354d 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -740,6 +740,8 @@ class ApplicationsController extends Controller return invalidTokenResponse(); } + $this->authorize('create', Application::class); + $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; @@ -1521,6 +1523,8 @@ class ApplicationsController extends Controller return response()->json(['message' => 'Application not found.'], 404); } + $this->authorize('view', $application); + return response()->json($this->removeSensitiveData($application)); } @@ -1699,6 +1703,8 @@ class ApplicationsController extends Controller ], 404); } + $this->authorize('delete', $application); + DeleteResourceJob::dispatch( resource: $application, deleteVolumes: $request->query->get('delete_volumes', true), @@ -1856,6 +1862,9 @@ class ApplicationsController extends Controller 'message' => 'Application not found', ], 404); } + + $this->authorize('update', $application); + $server = $application->destination->server; $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network']; @@ -2140,6 +2149,9 @@ class ApplicationsController extends Controller 'message' => 'Application not found', ], 404); } + + $this->authorize('view', $application); + $envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id')); $envs = $envs->map(function ($env) { @@ -2254,6 +2266,9 @@ class ApplicationsController extends Controller 'message' => 'Application not found', ], 404); } + + $this->authorize('manageEnvironment', $application); + $validator = customApiValidator($request->all(), [ 'key' => 'string|required', 'value' => 'string|nullable', @@ -2444,6 +2459,8 @@ class ApplicationsController extends Controller ], 404); } + $this->authorize('manageEnvironment', $application); + $bulk_data = $request->get('data'); if (! $bulk_data) { return response()->json([ @@ -2628,6 +2645,9 @@ class ApplicationsController extends Controller 'message' => 'Application not found', ], 404); } + + $this->authorize('manageEnvironment', $application); + $validator = customApiValidator($request->all(), [ 'key' => 'string|required', 'value' => 'string|nullable', @@ -2778,6 +2798,9 @@ class ApplicationsController extends Controller 'message' => 'Application not found.', ], 404); } + + $this->authorize('manageEnvironment', $application); + $found_env = EnvironmentVariable::where('uuid', $request->env_uuid) ->where('resourceable_type', Application::class) ->where('resourceable_id', $application->id) @@ -2881,6 +2904,8 @@ class ApplicationsController extends Controller return response()->json(['message' => 'Application not found.'], 404); } + $this->authorize('deploy', $application); + $deployment_uuid = new Cuid2; $result = queue_application_deployment( @@ -2973,6 +2998,9 @@ class ApplicationsController extends Controller if (! $application) { return response()->json(['message' => 'Application not found.'], 404); } + + $this->authorize('deploy', $application); + StopApplication::dispatch($application); return response()->json( @@ -3050,6 +3078,8 @@ class ApplicationsController extends Controller return response()->json(['message' => 'Application not found.'], 404); } + $this->authorize('deploy', $application); + $deployment_uuid = new Cuid2; $result = queue_application_deployment( diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 6ac052b3c..389d119bd 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -12,6 +12,7 @@ use App\Http\Controllers\Controller; use App\Jobs\DeleteResourceJob; use App\Models\Project; use App\Models\Server; +use App\Models\StandalonePostgresql; use Illuminate\Http\Request; use OpenApi\Attributes as OA; @@ -143,6 +144,8 @@ class DatabasesController extends Controller return response()->json(['message' => 'Database not found.'], 404); } + $this->authorize('view', $database); + return response()->json($this->removeSensitiveData($database)); } @@ -276,6 +279,9 @@ class DatabasesController extends Controller if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } + + $this->authorize('update', $database); + if ($request->is_public && $request->public_port) { if (isPublicPortAlreadyUsed($database->destination->server, $request->public_port, $database->id)) { return response()->json(['message' => 'Public port already used by another database.'], 400); @@ -1028,6 +1034,9 @@ class DatabasesController extends Controller return invalidTokenResponse(); } + // Use a generic authorization for database creation - using PostgreSQL as representative model + $this->authorize('create', StandalonePostgresql::class); + $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; @@ -1606,6 +1615,8 @@ class DatabasesController extends Controller return response()->json(['message' => 'Database not found.'], 404); } + $this->authorize('delete', $database); + DeleteResourceJob::dispatch( resource: $database, deleteVolumes: $request->query->get('delete_volumes', true), @@ -1684,6 +1695,9 @@ class DatabasesController extends Controller if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } + + $this->authorize('manage', $database); + if (str($database->status)->contains('running')) { return response()->json(['message' => 'Database is already running.'], 400); } @@ -1762,6 +1776,9 @@ class DatabasesController extends Controller if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } + + $this->authorize('manage', $database); + if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) { return response()->json(['message' => 'Database is already stopped.'], 400); } @@ -1840,6 +1857,9 @@ class DatabasesController extends Controller if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } + + $this->authorize('manage', $database); + RestartDatabase::dispatch($database); return response()->json( diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 5c7f20902..b87420f72 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -299,6 +299,12 @@ class DeployController extends Controller } switch ($resource?->getMorphClass()) { case Application::class: + // Check authorization for application deployment + try { + $this->authorize('deploy', $resource); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + return ['message' => 'Unauthorized to deploy this application.', 'deployment_uuid' => null]; + } $deployment_uuid = new Cuid2; $result = queue_application_deployment( application: $resource, @@ -313,11 +319,22 @@ class DeployController extends Controller } break; case Service::class: + // Check authorization for service deployment + try { + $this->authorize('deploy', $resource); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + return ['message' => 'Unauthorized to deploy this service.', 'deployment_uuid' => null]; + } StartService::run($resource); $message = "Service {$resource->name} started. It could take a while, be patient."; break; default: - // Database resource + // Database resource - check authorization + try { + $this->authorize('manage', $resource); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + return ['message' => 'Unauthorized to start this database.', 'deployment_uuid' => null]; + } StartDatabase::dispatch($resource); $resource->started_at ??= now(); @@ -423,6 +440,10 @@ class DeployController extends Controller if (is_null($application)) { return response()->json(['message' => 'Application not found'], 404); } + + // Check authorization to view application deployments + $this->authorize('view', $application); + $deployments = $application->deployments($skip, $take); return response()->json($deployments); diff --git a/app/Http/Controllers/Api/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php index ad12c83ab..d5dc4a046 100644 --- a/app/Http/Controllers/Api/ResourcesController.php +++ b/app/Http/Controllers/Api/ResourcesController.php @@ -43,6 +43,10 @@ class ResourcesController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } + + // General authorization check for viewing resources - using Project as base resource type + $this->authorize('viewAny', Project::class); + $projects = Project::where('team_id', $teamId)->get(); $resources = collect(); $resources->push($projects->pluck('applications')->flatten()); diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 2c831879b..162f637c5 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -246,6 +246,8 @@ class ServicesController extends Controller return invalidTokenResponse(); } + $this->authorize('create', Service::class); + $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; @@ -547,6 +549,8 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } + $this->authorize('view', $service); + $service = $service->load(['applications', 'databases']); return response()->json($this->removeSensitiveData($service)); @@ -612,6 +616,8 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } + $this->authorize('delete', $service); + DeleteResourceJob::dispatch( resource: $service, deleteVolumes: $request->query->get('delete_volumes', true), @@ -718,6 +724,8 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } + $this->authorize('update', $service); + $allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network']; $validator = customApiValidator($request->all(), [ @@ -856,6 +864,8 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } + $this->authorize('manageEnvironment', $service); + $envs = $service->environment_variables->map(function ($env) { $env->makeHidden([ 'application_id', @@ -960,6 +970,8 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } + $this->authorize('manageEnvironment', $service); + $validator = customApiValidator($request->all(), [ 'key' => 'string|required', 'value' => 'string|nullable', @@ -1081,6 +1093,8 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } + $this->authorize('manageEnvironment', $service); + $bulk_data = $request->get('data'); if (! $bulk_data) { return response()->json(['message' => 'Bulk data is required.'], 400); @@ -1197,6 +1211,8 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } + $this->authorize('manageEnvironment', $service); + $validator = customApiValidator($request->all(), [ 'key' => 'string|required', 'value' => 'string|nullable', @@ -1299,6 +1315,8 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } + $this->authorize('manageEnvironment', $service); + $env = EnvironmentVariable::where('uuid', $request->env_uuid) ->where('resourceable_type', Service::class) ->where('resourceable_id', $service->id) @@ -1378,6 +1396,9 @@ class ServicesController extends Controller if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } + + $this->authorize('deploy', $service); + if (str($service->status)->contains('running')) { return response()->json(['message' => 'Service is already running.'], 400); } @@ -1456,6 +1477,9 @@ class ServicesController extends Controller if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } + + $this->authorize('stop', $service); + if (str($service->status)->contains('stopped') || str($service->status)->contains('exited')) { return response()->json(['message' => 'Service is already stopped.'], 400); } @@ -1543,6 +1567,9 @@ class ServicesController extends Controller if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } + + $this->authorize('deploy', $service); + $pullLatest = $request->boolean('latest'); RestartService::dispatch($service, $pullLatest);