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();
}
$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(

View File

@@ -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(

View File

@@ -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);

View File

@@ -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());

View File

@@ -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);