Files
coolify/app/Providers/AppServiceProvider.php
Andras Bacsai 6f1905b7b0 fix(access): enforce member grants on more actions
- Require write access on the target environment to clone or move a resource
- Check read access on shared variables before a reference is added or resolved
- Limit team credentials, git sources, and imports of other backups to team-wide writers
- Require the delete level for backup, volume, storage, and task deletes
- Require create access for previews, new projects, and git sources
- Use the token team for the access scope, and check team-wide access on Livewire updates
- Require instance admin to toggle the API and MCP server
- Hide server pickers and broadcast details from members without access
2026-10-30 16:11:10 +01:00

198 lines
6.8 KiB
PHP

<?php
namespace App\Providers;
use App\Auth\Oidc\OidcDiscoveryService;
use App\Auth\Oidc\OidcTokenValidator;
use App\Auth\Oidc\Socialite\OidcProvider;
use App\Http\Middleware\EnsureTeamWideAccess;
use App\Models\PersonalAccessToken;
use App\Models\Server;
use App\Rules\SafeExternalUrl;
use App\Services\Access\AccessResolver;
use App\Support\AtomicFilesystem;
use App\Support\RetryHttpConnectionFailures;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Console\ConfigCacheCommand;
use Illuminate\Foundation\Console\RouteCacheCommand;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\TrimStrings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Once;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
use Laravel\Sanctum\Sanctum;
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
use Livewire\Livewire;
use Stripe\StripeClient;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->scoped(AccessResolver::class);
$this->app->bind(StripeClient::class, fn () => new StripeClient(config('subscription.stripe_api_key')));
// Requests must never read a partly written cache file. See AtomicFilesystem.
$this->app->extend(ConfigCacheCommand::class, fn () => new ConfigCacheCommand(new AtomicFilesystem));
$this->app->extend(RouteCacheCommand::class, fn () => new RouteCacheCommand(new AtomicFilesystem));
}
public function boot(): void
{
$this->configureCommands();
$this->configureModels();
$this->configurePasswords();
$this->configureSanctumModel();
$this->configureHttpClient();
$this->configureGitHubHttp();
$this->configureGitLabHttp();
$this->configureOidcSocialite();
$this->configureQueue();
$this->configureServerTransferImport();
$this->configureLivewire();
}
/**
* Livewire updates skip route middleware. Run the team-wide access check again on each update,
* so a page that is already open stops working when an admin limits the member to some projects.
*/
private function configureLivewire(): void
{
Livewire::addPersistentMiddleware([EnsureTeamWideAccess::class]);
}
/**
* A server transfer bundle carries stored values (files, variables, keys, compose files) that must
* arrive unchanged, so the import request skips the global trimming and empty-string conversion.
*/
private function configureServerTransferImport(): void
{
$isImport = fn (Request $request): bool => $request->is('api/v1/servers/import');
TrimStrings::skipWhen($isImport);
ConvertEmptyStringsToNull::skipWhen($isImport);
}
/**
* Queue workers are long-running processes, so once() values (e.g. instanceSettings())
* and the Server identity map would stay stale across jobs. Flush them before each job,
* like a fresh web request.
*/
private function configureQueue(): void
{
Queue::before(function (): void {
Once::flush();
Server::flushIdentityMap();
});
}
private function configureCommands(): void
{
if (App::isProduction()) {
DB::prohibitDestructiveCommands();
}
}
private function configureModels(): void
{
// Disabled because it's causing issues with the application
// Model::shouldBeStrict();
}
private function configurePasswords(): void
{
Password::defaults(function () {
return App::isProduction()
? Password::min(8)
->mixedCase()
->letters()
->numbers()
->symbols()
->uncompromised()
: Password::min(8)->letters();
});
}
private function configureSanctumModel(): void
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
Sanctum::authenticateAccessTokensUsing(
fn (PersonalAccessToken $token, bool $isValid): bool => $isValid && ! $token->isPaused()
);
}
private function configureOidcSocialite(): void
{
if (! $this->app->bound(SocialiteFactory::class)) {
return;
}
$this->app->make(SocialiteFactory::class)->extend('oidc', function ($app) {
return new OidcProvider(
$app['request'],
$app->make(OidcDiscoveryService::class),
$app->make(OidcTokenValidator::class),
'',
'',
'',
);
});
}
private function configureHttpClient(): void
{
Http::globalOptions(['connect_timeout' => 10, 'timeout' => 30]);
Http::globalMiddleware(new RetryHttpConnectionFailures);
}
private function configureGitHubHttp(): void
{
Http::macro('GitSource', function (string $url) {
return Http::withOptions([
...SafeExternalUrl::httpClientOptions(
$url,
allowPrivateNetworks: SafeExternalUrl::gitSourcesMayUsePrivateNetworks(),
),
'allow_redirects' => SafeExternalUrl::sameOriginRedirectOptions(),
]);
});
Http::macro('GitHub', function (string $api_url, ?string $github_access_token = null) {
if ($github_access_token) {
return Http::GitSource($api_url)->withHeaders([
'X-GitHub-Api-Version' => '2022-11-28',
'Accept' => 'application/vnd.github.v3+json',
'Authorization' => "Bearer $github_access_token",
])->baseUrl($api_url);
} else {
return Http::GitSource($api_url)->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
])->baseUrl($api_url);
}
});
}
private function configureGitLabHttp(): void
{
Http::macro('GitLab', function (string $api_url, ?string $access_token = null) {
$client = Http::GitSource($api_url)->withHeaders([
'Accept' => 'application/json',
])->baseUrl($api_url);
return $access_token ? $client->withToken($access_token) : $client;
});
Http::macro('Gitea', function (string $api_url, ?string $access_token = null) {
$client = Http::GitSource($api_url)->withHeaders([
'Accept' => 'application/json',
])->baseUrl($api_url);
return $access_token ? $client->withToken($access_token) : $client;
});
}
}