feat(auth): implement comprehensive authorization checks across API controllers

This commit is contained in:
Andras Bacsai
2025-08-23 18:51:10 +02:00
parent b5fe5dd909
commit b1334a1bc6
5 changed files with 103 additions and 1 deletions

View File

@@ -740,6 +740,8 @@ class ApplicationsController extends Controller
return invalidTokenResponse(); return invalidTokenResponse();
} }
$this->authorize('create', Application::class);
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return; return $return;
@@ -1521,6 +1523,8 @@ class ApplicationsController extends Controller
return response()->json(['message' => 'Application not found.'], 404); return response()->json(['message' => 'Application not found.'], 404);
} }
$this->authorize('view', $application);
return response()->json($this->removeSensitiveData($application)); return response()->json($this->removeSensitiveData($application));
} }
@@ -1699,6 +1703,8 @@ class ApplicationsController extends Controller
], 404); ], 404);
} }
$this->authorize('delete', $application);
DeleteResourceJob::dispatch( DeleteResourceJob::dispatch(
resource: $application, resource: $application,
deleteVolumes: $request->query->get('delete_volumes', true), deleteVolumes: $request->query->get('delete_volumes', true),
@@ -1856,6 +1862,9 @@ class ApplicationsController extends Controller
'message' => 'Application not found', 'message' => 'Application not found',
], 404); ], 404);
} }
$this->authorize('update', $application);
$server = $application->destination->server; $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']; $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', 'message' => 'Application not found',
], 404); ], 404);
} }
$this->authorize('view', $application);
$envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id')); $envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id'));
$envs = $envs->map(function ($env) { $envs = $envs->map(function ($env) {
@@ -2254,6 +2266,9 @@ class ApplicationsController extends Controller
'message' => 'Application not found', 'message' => 'Application not found',
], 404); ], 404);
} }
$this->authorize('manageEnvironment', $application);
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'key' => 'string|required', 'key' => 'string|required',
'value' => 'string|nullable', 'value' => 'string|nullable',
@@ -2444,6 +2459,8 @@ class ApplicationsController extends Controller
], 404); ], 404);
} }
$this->authorize('manageEnvironment', $application);
$bulk_data = $request->get('data'); $bulk_data = $request->get('data');
if (! $bulk_data) { if (! $bulk_data) {
return response()->json([ return response()->json([
@@ -2628,6 +2645,9 @@ class ApplicationsController extends Controller
'message' => 'Application not found', 'message' => 'Application not found',
], 404); ], 404);
} }
$this->authorize('manageEnvironment', $application);
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'key' => 'string|required', 'key' => 'string|required',
'value' => 'string|nullable', 'value' => 'string|nullable',
@@ -2778,6 +2798,9 @@ class ApplicationsController extends Controller
'message' => 'Application not found.', 'message' => 'Application not found.',
], 404); ], 404);
} }
$this->authorize('manageEnvironment', $application);
$found_env = EnvironmentVariable::where('uuid', $request->env_uuid) $found_env = EnvironmentVariable::where('uuid', $request->env_uuid)
->where('resourceable_type', Application::class) ->where('resourceable_type', Application::class)
->where('resourceable_id', $application->id) ->where('resourceable_id', $application->id)
@@ -2881,6 +2904,8 @@ class ApplicationsController extends Controller
return response()->json(['message' => 'Application not found.'], 404); return response()->json(['message' => 'Application not found.'], 404);
} }
$this->authorize('deploy', $application);
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
$result = queue_application_deployment( $result = queue_application_deployment(
@@ -2973,6 +2998,9 @@ class ApplicationsController extends Controller
if (! $application) { if (! $application) {
return response()->json(['message' => 'Application not found.'], 404); return response()->json(['message' => 'Application not found.'], 404);
} }
$this->authorize('deploy', $application);
StopApplication::dispatch($application); StopApplication::dispatch($application);
return response()->json( return response()->json(
@@ -3050,6 +3078,8 @@ class ApplicationsController extends Controller
return response()->json(['message' => 'Application not found.'], 404); return response()->json(['message' => 'Application not found.'], 404);
} }
$this->authorize('deploy', $application);
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
$result = queue_application_deployment( $result = queue_application_deployment(

View File

@@ -12,6 +12,7 @@ use App\Http\Controllers\Controller;
use App\Jobs\DeleteResourceJob; use App\Jobs\DeleteResourceJob;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -143,6 +144,8 @@ class DatabasesController extends Controller
return response()->json(['message' => 'Database not found.'], 404); return response()->json(['message' => 'Database not found.'], 404);
} }
$this->authorize('view', $database);
return response()->json($this->removeSensitiveData($database)); return response()->json($this->removeSensitiveData($database));
} }
@@ -276,6 +279,9 @@ class DatabasesController extends Controller
if (! $database) { if (! $database) {
return response()->json(['message' => 'Database not found.'], 404); return response()->json(['message' => 'Database not found.'], 404);
} }
$this->authorize('update', $database);
if ($request->is_public && $request->public_port) { if ($request->is_public && $request->public_port) {
if (isPublicPortAlreadyUsed($database->destination->server, $request->public_port, $database->id)) { if (isPublicPortAlreadyUsed($database->destination->server, $request->public_port, $database->id)) {
return response()->json(['message' => 'Public port already used by another database.'], 400); return response()->json(['message' => 'Public port already used by another database.'], 400);
@@ -1028,6 +1034,9 @@ class DatabasesController extends Controller
return invalidTokenResponse(); return invalidTokenResponse();
} }
// Use a generic authorization for database creation - using PostgreSQL as representative model
$this->authorize('create', StandalonePostgresql::class);
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return; return $return;
@@ -1606,6 +1615,8 @@ class DatabasesController extends Controller
return response()->json(['message' => 'Database not found.'], 404); return response()->json(['message' => 'Database not found.'], 404);
} }
$this->authorize('delete', $database);
DeleteResourceJob::dispatch( DeleteResourceJob::dispatch(
resource: $database, resource: $database,
deleteVolumes: $request->query->get('delete_volumes', true), deleteVolumes: $request->query->get('delete_volumes', true),
@@ -1684,6 +1695,9 @@ class DatabasesController extends Controller
if (! $database) { if (! $database) {
return response()->json(['message' => 'Database not found.'], 404); return response()->json(['message' => 'Database not found.'], 404);
} }
$this->authorize('manage', $database);
if (str($database->status)->contains('running')) { if (str($database->status)->contains('running')) {
return response()->json(['message' => 'Database is already running.'], 400); return response()->json(['message' => 'Database is already running.'], 400);
} }
@@ -1762,6 +1776,9 @@ class DatabasesController extends Controller
if (! $database) { if (! $database) {
return response()->json(['message' => 'Database not found.'], 404); return response()->json(['message' => 'Database not found.'], 404);
} }
$this->authorize('manage', $database);
if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) { if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) {
return response()->json(['message' => 'Database is already stopped.'], 400); return response()->json(['message' => 'Database is already stopped.'], 400);
} }
@@ -1840,6 +1857,9 @@ class DatabasesController extends Controller
if (! $database) { if (! $database) {
return response()->json(['message' => 'Database not found.'], 404); return response()->json(['message' => 'Database not found.'], 404);
} }
$this->authorize('manage', $database);
RestartDatabase::dispatch($database); RestartDatabase::dispatch($database);
return response()->json( return response()->json(

View File

@@ -299,6 +299,12 @@ class DeployController extends Controller
} }
switch ($resource?->getMorphClass()) { switch ($resource?->getMorphClass()) {
case Application::class: 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; $deployment_uuid = new Cuid2;
$result = queue_application_deployment( $result = queue_application_deployment(
application: $resource, application: $resource,
@@ -313,11 +319,22 @@ class DeployController extends Controller
} }
break; break;
case Service::class: 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); StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient."; $message = "Service {$resource->name} started. It could take a while, be patient.";
break; break;
default: 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); StartDatabase::dispatch($resource);
$resource->started_at ??= now(); $resource->started_at ??= now();
@@ -423,6 +440,10 @@ class DeployController extends Controller
if (is_null($application)) { if (is_null($application)) {
return response()->json(['message' => 'Application not found'], 404); return response()->json(['message' => 'Application not found'], 404);
} }
// Check authorization to view application deployments
$this->authorize('view', $application);
$deployments = $application->deployments($skip, $take); $deployments = $application->deployments($skip, $take);
return response()->json($deployments); return response()->json($deployments);

View File

@@ -43,6 +43,10 @@ class ResourcesController extends Controller
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); 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(); $projects = Project::where('team_id', $teamId)->get();
$resources = collect(); $resources = collect();
$resources->push($projects->pluck('applications')->flatten()); $resources->push($projects->pluck('applications')->flatten());

View File

@@ -246,6 +246,8 @@ class ServicesController extends Controller
return invalidTokenResponse(); return invalidTokenResponse();
} }
$this->authorize('create', Service::class);
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return; return $return;
@@ -547,6 +549,8 @@ class ServicesController extends Controller
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('view', $service);
$service = $service->load(['applications', 'databases']); $service = $service->load(['applications', 'databases']);
return response()->json($this->removeSensitiveData($service)); return response()->json($this->removeSensitiveData($service));
@@ -612,6 +616,8 @@ class ServicesController extends Controller
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('delete', $service);
DeleteResourceJob::dispatch( DeleteResourceJob::dispatch(
resource: $service, resource: $service,
deleteVolumes: $request->query->get('delete_volumes', true), deleteVolumes: $request->query->get('delete_volumes', true),
@@ -718,6 +724,8 @@ class ServicesController extends Controller
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('update', $service);
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network']; $allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
@@ -856,6 +864,8 @@ class ServicesController extends Controller
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('manageEnvironment', $service);
$envs = $service->environment_variables->map(function ($env) { $envs = $service->environment_variables->map(function ($env) {
$env->makeHidden([ $env->makeHidden([
'application_id', 'application_id',
@@ -960,6 +970,8 @@ class ServicesController extends Controller
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('manageEnvironment', $service);
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'key' => 'string|required', 'key' => 'string|required',
'value' => 'string|nullable', 'value' => 'string|nullable',
@@ -1081,6 +1093,8 @@ class ServicesController extends Controller
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('manageEnvironment', $service);
$bulk_data = $request->get('data'); $bulk_data = $request->get('data');
if (! $bulk_data) { if (! $bulk_data) {
return response()->json(['message' => 'Bulk data is required.'], 400); 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); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('manageEnvironment', $service);
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'key' => 'string|required', 'key' => 'string|required',
'value' => 'string|nullable', 'value' => 'string|nullable',
@@ -1299,6 +1315,8 @@ class ServicesController extends Controller
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('manageEnvironment', $service);
$env = EnvironmentVariable::where('uuid', $request->env_uuid) $env = EnvironmentVariable::where('uuid', $request->env_uuid)
->where('resourceable_type', Service::class) ->where('resourceable_type', Service::class)
->where('resourceable_id', $service->id) ->where('resourceable_id', $service->id)
@@ -1378,6 +1396,9 @@ class ServicesController extends Controller
if (! $service) { if (! $service) {
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('deploy', $service);
if (str($service->status)->contains('running')) { if (str($service->status)->contains('running')) {
return response()->json(['message' => 'Service is already running.'], 400); return response()->json(['message' => 'Service is already running.'], 400);
} }
@@ -1456,6 +1477,9 @@ class ServicesController extends Controller
if (! $service) { if (! $service) {
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('stop', $service);
if (str($service->status)->contains('stopped') || str($service->status)->contains('exited')) { if (str($service->status)->contains('stopped') || str($service->status)->contains('exited')) {
return response()->json(['message' => 'Service is already stopped.'], 400); return response()->json(['message' => 'Service is already stopped.'], 400);
} }
@@ -1543,6 +1567,9 @@ class ServicesController extends Controller
if (! $service) { if (! $service) {
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$this->authorize('deploy', $service);
$pullLatest = $request->boolean('latest'); $pullLatest = $request->boolean('latest');
RestartService::dispatch($service, $pullLatest); RestartService::dispatch($service, $pullLatest);