diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 008bd3905..eef9961f3 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -145,6 +145,9 @@ class General extends Component #[Validate(['string', 'nullable'])] public ?string $httpBasicAuthPassword = null; + #[Validate(['string', 'nullable'])] + public ?string $testIpallowlist = null; + #[Validate(['nullable'])] public ?string $watchPaths = null; @@ -411,6 +414,7 @@ class General extends Component $this->application->is_http_basic_auth_enabled = $this->isHttpBasicAuthEnabled; $this->application->http_basic_auth_username = $this->httpBasicAuthUsername; $this->application->http_basic_auth_password = $this->httpBasicAuthPassword; + $this->application->test_ipallowlist = $this->testIpallowlist; $this->application->watch_paths = $this->watchPaths; $this->application->redirect = $this->redirect; @@ -461,6 +465,7 @@ class General extends Component $this->isHttpBasicAuthEnabled = $this->application->is_http_basic_auth_enabled; $this->httpBasicAuthUsername = $this->application->http_basic_auth_username; $this->httpBasicAuthPassword = $this->application->http_basic_auth_password; + $this->testIpallowlist = $this->application->test_ipallowlist; $this->watchPaths = $this->application->watch_paths; $this->redirect = $this->application->redirect; diff --git a/app/Models/Application.php b/app/Models/Application.php index a4f51780e..d0b6c1c70 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -109,6 +109,7 @@ use Visus\Cuid2\Cuid2; 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], 'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'], + 'test_ipallowlist' => ['type' => 'string', 'nullable' => true, 'description' => 'List of allowed ips'], ] )] diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 7b74392cf..78049414f 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -5,6 +5,7 @@ use App\Models\Application; use App\Models\ApplicationPreview; use App\Models\Server; use App\Models\ServiceApplication; +use Aws\Middleware; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\Url\Url; @@ -406,7 +407,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, return $labels->sort(); } -function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null) +function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?string $test_ipallowlist = null) { $labels = collect([]); $labels->push('traefik.enable=true'); @@ -417,6 +418,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $is_http_basic_auth_enabled = $is_http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null; $http_basic_auth_label = "http-basic-auth-{$uuid}"; + $list_string = ''; if ($is_http_basic_auth_enabled) { $hashedPassword = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]); } @@ -425,6 +427,22 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push("traefik.http.middlewares.{$http_basic_auth_label}.basicauth.users={$http_basic_auth_username}:{$hashedPassword}"); } + if ($test_ipallowlist) { + $ip_list = explode(',', $test_ipallowlist); + $sane_ip_list = []; + foreach ($ip_list as $_ip) { + $_ip = trim($_ip); + if (preg_match('/^(\d{1,3}\.){3}\d{1,3}(\/([0-9]|[12][0-9]|3[0-2]))?$/', $_ip)) { + $sane_ip_list[] = $_ip; + } + } + + if (count($sane_ip_list) > 0) { + $list_string = implode(',', $sane_ip_list); + $labels->push("traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange={$list_string}"); + } + } + $middlewares_from_labels = collect([]); if ($serviceLabels) { @@ -523,6 +541,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_http_basic_auth_enabled) { $middlewares->push($http_basic_auth_label); } + if (isListStringFilled($test_ipallowlist)) { + $middlewares->push('test-ipallowlist'); + } $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { $middlewares->push($middleware_name); }); @@ -568,7 +589,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push("traefik.http.routers.{$http_label}.service={$http_label}"); } if ($is_force_https_enabled) { - $labels->push("traefik.http.routers.{$http_label}.middlewares=redirect-to-https"); + $middleware_append_string = 'redirect-to-https'; + if (isListStringFilled($test_ipallowlist)) { + $middleware_append_string .= ',test-ipallowlist'; + } + $labels->push("traefik.http.routers.{$http_label}.middlewares={$middleware_append_string}"); } } else { // Set labels for http @@ -601,6 +626,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_http_basic_auth_enabled) { $middlewares->push($http_basic_auth_label); } + if (isListStringFilled($test_ipallowlist)) { + $middlewares->push('test-ipallowlist'); + } $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { $middlewares->push($middleware_name); }); @@ -613,6 +641,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_gzip_enabled) { $middlewares->push('gzip'); } + if (isListStringFilled($test_ipallowlist)) { + $middlewares->push('test-ipallowlist'); + } if (str($image)->contains('ghost')) { $middlewares->push("redir-ghost-{$uuid}"); } @@ -674,6 +705,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, + test_ipallowlist: $application->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -704,6 +736,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, + test_ipallowlist: $application->test_ipallowlist, )); $labels = $labels->merge(fqdnLabelsForCaddy( network: $application->destination->network, @@ -740,6 +773,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, + test_ipallowlist: $application->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -768,6 +802,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, + test_ipallowlist: $application->test_ipallowlist, )); $labels = $labels->merge(fqdnLabelsForCaddy( network: $application->destination->network, @@ -982,6 +1017,21 @@ function isDatabaseImageWithContext(string $imageName, array $serviceConfig): bo return true; } +function isListStringFilled(string $list) +{ + $items = explode(',', $list); + if (! empty($items)) { + $sane_list = []; + foreach ($items as $item) { + if (strlen($item) > 0) { + return true; + } + } + } + + return false; +} + function convertDockerRunToCompose(?string $custom_docker_run_options = null) { $options = []; diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 99ce9185a..0a4095d53 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -1324,7 +1324,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), service_name: $serviceName, - image: $image + image: $image, + test_ipallowlist: $originalResource->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -1351,7 +1352,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), service_name: $serviceName, - image: $image + image: $image, + test_ipallowlist: $originalResource->test_ipallowlist, )); $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( network: $network, @@ -2573,7 +2575,8 @@ function serviceParser(Service $resource): Collection is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), service_name: $serviceName, - image: $image + image: $image, + test_ipallowlist: $originalResource->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -2600,7 +2603,8 @@ function serviceParser(Service $resource): Collection is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), service_name: $serviceName, - image: $image + image: $image, + test_ipallowlist: $originalResource->test_ipallowlist, )); $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( network: $network, diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 3e993dbf3..a212d1f3c 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2299,7 +2299,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_gzip_enabled: $savedService->isGzipEnabled(), is_stripprefix_enabled: $savedService->isStripprefixEnabled(), service_name: $serviceName, - image: data_get($service, 'image') + image: data_get($service, 'image'), + test_ipallowlist: $savedService->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -2325,7 +2326,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_gzip_enabled: $savedService->isGzipEnabled(), is_stripprefix_enabled: $savedService->isStripprefixEnabled(), service_name: $serviceName, - image: data_get($service, 'image') + image: data_get($service, 'image'), + test_ipallowlist: $savedService->test_ipallowlist, )); $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( network: $resource->destination->network, @@ -3075,6 +3077,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_force_https_enabled: $resource->isForceHttpsEnabled(), is_gzip_enabled: $resource->isGzipEnabled(), is_stripprefix_enabled: $resource->isStripprefixEnabled(), + test_ipallowlist: $resource->test_ipallowlist, ) ); break; @@ -3104,6 +3107,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_force_https_enabled: $resource->isForceHttpsEnabled(), is_gzip_enabled: $resource->isGzipEnabled(), is_stripprefix_enabled: $resource->isStripprefixEnabled(), + test_ipallowlist: $resource->test_ipallowlist, ) ); $serviceLabels = $serviceLabels->merge( diff --git a/database/migrations/2025_12_18_095331_add_test_ipallowlist_to_applications_table.php b/database/migrations/2025_12_18_095331_add_test_ipallowlist_to_applications_table.php new file mode 100644 index 000000000..42fd47652 --- /dev/null +++ b/database/migrations/2025_12_18_095331_add_test_ipallowlist_to_applications_table.php @@ -0,0 +1,28 @@ +text('test_ipallowlist')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('test_ipallowlist'); + }); + } +}; diff --git a/openapi.json b/openapi.json index 69f5ef53d..4e7c0c261 100644 --- a/openapi.json +++ b/openapi.json @@ -11256,6 +11256,11 @@ "type": "string", "nullable": true, "description": "Password for HTTP Basic Authentication" + }, + "test_ipallowlist": { + "type": "string", + "nullable": true, + "description": "List of allowed ips" } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index fab3df54e..5a0c7ed20 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -7143,6 +7143,10 @@ components: type: string nullable: true description: 'Password for HTTP Basic Authentication' + test_ipallowlist: + type: string + nullable: true + description: 'List of allowed ips' type: object ApplicationDeploymentQueue: description: 'Project model' diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index aada339cc..918165020 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -510,6 +510,13 @@ wire:model="customNetworkAliases" x-bind:disabled="!canUpdate" /> @endif + @if(!$application->destination->server->isSwarm()) +
+ +
+ @endif

HTTP Basic Authentication