refactor: tighten team scoping on resource creation and admin nav (#9651)

This commit is contained in:
Andras Bacsai 2026-04-19 12:01:11 +02:00 committed by GitHub
commit 33518b24a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 653 additions and 159 deletions

View file

@ -1766,7 +1766,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('postgres_conf', $postgresConf);
}
$database = create_standalone_postgresql($environment->id, $destination->uuid, $request->only($allowedFields));
$database = create_standalone_postgresql($environment->id, $destination, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -1821,7 +1821,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mariadb_conf', $mariadbConf);
}
$database = create_standalone_mariadb($environment->id, $destination->uuid, $request->only($allowedFields));
$database = create_standalone_mariadb($environment->id, $destination, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -1880,7 +1880,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mysql_conf', $mysqlConf);
}
$database = create_standalone_mysql($environment->id, $destination->uuid, $request->only($allowedFields));
$database = create_standalone_mysql($environment->id, $destination, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -1936,7 +1936,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('redis_conf', $redisConf);
}
$database = create_standalone_redis($environment->id, $destination->uuid, $request->only($allowedFields));
$database = create_standalone_redis($environment->id, $destination, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -1973,7 +1973,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->only($allowedFields));
$database = create_standalone_dragonfly($environment->id, $destination, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -2022,7 +2022,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('keydb_conf', $keydbConf);
}
$database = create_standalone_keydb($environment->id, $destination->uuid, $request->only($allowedFields));
$database = create_standalone_keydb($environment->id, $destination, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -2058,7 +2058,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
], 422);
}
removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->only($allowedFields));
$database = create_standalone_clickhouse($environment->id, $destination, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -2116,7 +2116,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mongo_conf', $mongoConf);
}
$database = create_standalone_mongodb($environment->id, $destination->uuid, $request->only($allowedFields));
$database = create_standalone_mongodb($environment->id, $destination, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}

View file

@ -37,7 +37,7 @@ public function back()
Auth::login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
return redirect()->route('admin.index');
}
}
@ -70,7 +70,7 @@ public function switchUser(int $user_id)
Auth::login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
return redirect()->route('dashboard');
}
private function authorizeAdminAccess(): void

View file

@ -2,9 +2,7 @@
namespace App\Livewire\Destination;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
@ -29,16 +27,8 @@ class Show extends Component
public function mount(string $destination_uuid)
{
try {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first() ??
SwarmDocker::whereUuid($destination_uuid)->firstOrFail();
$ownedByTeam = Server::ownedByCurrentTeam()->each(function ($server) use ($destination) {
if ($server->standaloneDockers->contains($destination) || $server->swarmDockers->contains($destination)) {
$this->destination = $destination;
$this->syncData();
}
});
if ($ownedByTeam === false) {
$destination = find_destination_for_current_team($destination_uuid);
if (! $destination) {
return redirect()->route('destination.index');
}
$this->destination = $destination;
@ -80,7 +70,7 @@ public function delete()
try {
$this->authorize('delete', $this->destination);
if ($this->destination->getMorphClass() === \App\Models\StandaloneDocker::class) {
if ($this->destination->getMorphClass() === StandaloneDocker::class) {
if ($this->destination->attachedTo()) {
return $this->dispatch('error', 'You must delete all resources before deleting this destination.');
}

View file

@ -5,8 +5,6 @@
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Service;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
@ -31,7 +29,6 @@ public function mount()
public function submit()
{
$server_id = $this->query['server_id'];
try {
$this->validate([
'dockerComposeRaw' => 'required',
@ -44,20 +41,17 @@ public function submit()
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
$destination_uuid = $this->query['destination'] ?? null;
$destination = find_destination_for_current_team($destination_uuid);
if (! $destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
}
if (! $destination) {
throw new \Exception('Destination not found. What?!');
throw new \Exception('Destination not found.');
}
$destination_class = $destination->getMorphClass();
$service = Service::create([
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
'server_id' => $destination->server_id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
]);

View file

@ -4,8 +4,6 @@
use App\Models\Application;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Services\DockerImageParser;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@ -111,13 +109,10 @@ public function submit()
$parser = new DockerImageParser;
$parser->parse($dockerImage);
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
$destination_uuid = $this->query['destination'] ?? null;
$destination = find_destination_for_current_team($destination_uuid);
if (! $destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
}
if (! $destination) {
throw new \Exception('Destination not found. What?!');
throw new \Exception('Destination not found.');
}
$destination_class = $destination->getMorphClass();

View file

@ -5,8 +5,6 @@
use App\Models\Application;
use App\Models\GithubApp;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
use App\Support\ValidationPatterns;
use Illuminate\Support\Facades\Http;
@ -178,13 +176,10 @@ public function submit()
throw new \RuntimeException('Invalid repository data: '.$validator->errors()->first());
}
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
$destination_uuid = $this->query['destination'] ?? null;
$destination = find_destination_for_current_team($destination_uuid);
if (! $destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
}
if (! $destination) {
throw new \Exception('Destination not found. What?!');
throw new \Exception('Destination not found.');
}
$destination_class = $destination->getMorphClass();

View file

@ -7,8 +7,6 @@
use App\Models\GitlabApp;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
use App\Support\ValidationPatterns;
@ -130,13 +128,10 @@ public function submit()
{
$this->validate();
try {
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
$destination_uuid = $this->query['destination'] ?? null;
$destination = find_destination_for_current_team($destination_uuid);
if (! $destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
}
if (! $destination) {
throw new \Exception('Destination not found. What?!');
throw new \Exception('Destination not found.');
}
$destination_class = $destination->getMorphClass();

View file

@ -7,8 +7,6 @@
use App\Models\GitlabApp;
use App\Models\Project;
use App\Models\Service;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
use App\Support\ValidationPatterns;
@ -34,8 +32,6 @@ class PublicGitRepository extends Component
public bool $isStatic = false;
public bool $checkCoolifyConfig = true;
public ?string $publish_directory = null;
// In case of docker compose
@ -284,16 +280,13 @@ public function submit()
throw new \RuntimeException('Invalid branch: '.$branchValidator->errors()->first('git_branch'));
}
$destination_uuid = $this->query['destination'];
$destination_uuid = $this->query['destination'] ?? null;
$project_uuid = $this->parameters['project_uuid'];
$environment_uuid = $this->parameters['environment_uuid'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
$destination = find_destination_for_current_team($destination_uuid);
if (! $destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
}
if (! $destination) {
throw new \Exception('Destination not found. What?!');
throw new \Exception('Destination not found.');
}
$destination_class = $destination->getMorphClass();
@ -371,12 +364,6 @@ public function submit()
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
$application->fqdn = $fqdn;
$application->save();
if ($this->checkCoolifyConfig) {
// $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id);
// if ($config) {
// $application->setConfig($config);
// }
}
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,

View file

@ -5,8 +5,6 @@
use App\Models\Application;
use App\Models\GithubApp;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@ -35,13 +33,10 @@ public function submit()
$this->validate([
'dockerfile' => 'required',
]);
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
$destination_uuid = $this->query['destination'] ?? null;
$destination = find_destination_for_current_team($destination_uuid);
if (! $destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
}
if (! $destination) {
throw new \Exception('Destination not found. What?!');
throw new \Exception('Destination not found.');
}
$destination_class = $destination->getMorphClass();

View file

@ -4,7 +4,6 @@
use App\Models\EnvironmentVariable;
use App\Models\Service;
use App\Models\StandaloneDocker;
use Livewire\Component;
class Create extends Component
@ -18,7 +17,6 @@ public function mount()
$type = str(request()->query('type'));
$destination_uuid = request()->query('destination');
$server_id = request()->query('server_id');
$database_image = request()->query('database_image');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
@ -30,7 +28,11 @@ public function mount()
if (! $environment) {
return redirect()->route('dashboard');
}
if (isset($type) && isset($destination_uuid) && isset($server_id)) {
if (isset($type) && isset($destination_uuid)) {
$destination = find_destination_for_current_team($destination_uuid);
if (! $destination) {
return redirect()->route('dashboard');
}
$services = get_service_templates();
if (in_array($type, DATABASE_TYPES)) {
@ -44,23 +46,23 @@ public function mount()
}
$database = create_standalone_postgresql(
environmentId: $environment->id,
destinationUuid: $destination_uuid,
destination: $destination,
databaseImage: $database_image
);
} elseif ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
$database = create_standalone_redis($environment->id, $destination);
} elseif ($type->value() === 'mongodb') {
$database = create_standalone_mongodb($environment->id, $destination_uuid);
$database = create_standalone_mongodb($environment->id, $destination);
} elseif ($type->value() === 'mysql') {
$database = create_standalone_mysql($environment->id, $destination_uuid);
$database = create_standalone_mysql($environment->id, $destination);
} elseif ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid);
$database = create_standalone_mariadb($environment->id, $destination);
} elseif ($type->value() === 'keydb') {
$database = create_standalone_keydb($environment->id, $destination_uuid);
$database = create_standalone_keydb($environment->id, $destination);
} elseif ($type->value() === 'dragonfly') {
$database = create_standalone_dragonfly($environment->id, $destination_uuid);
$database = create_standalone_dragonfly($environment->id, $destination);
} elseif ($type->value() === 'clickhouse') {
$database = create_standalone_clickhouse($environment->id, $destination_uuid);
$database = create_standalone_clickhouse($environment->id, $destination);
}
return redirect()->route('project.database.configuration', [
@ -69,7 +71,7 @@ public function mount()
'database_uuid' => $database->uuid,
]);
}
if ($type->startsWith('one-click-service-') && ! is_null((int) $server_id)) {
if ($type->startsWith('one-click-service-')) {
$oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
@ -79,12 +81,11 @@ public function mount()
});
}
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service_payload = [
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'service_type' => $oneClickServiceName,
'server_id' => (int) $server_id,
'server_id' => $destination->server_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
];

View file

@ -58,10 +58,9 @@ public function cloneTo($destination_id)
{
$this->authorize('update', $this->resource);
$teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
$new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
$new_destination = StandaloneDocker::ownedByCurrentTeam()->find($destination_id);
if (! $new_destination) {
$new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
$new_destination = SwarmDocker::ownedByCurrentTeam()->find($destination_id);
}
if (! $new_destination) {
return $this->addError('destination_id', 'Destination not found.');

View file

@ -25,7 +25,9 @@ public function mount(): void
public function disableS3(int $backupId): void
{
$backup = ScheduledDatabaseBackup::findOrFail($backupId);
$backup = ScheduledDatabaseBackup::where('id', $backupId)
->where('s3_storage_id', $this->storage->id)
->firstOrFail();
$backup->update([
'save_s3' => false,
@ -39,7 +41,9 @@ public function disableS3(int $backupId): void
public function moveBackup(int $backupId): void
{
$backup = ScheduledDatabaseBackup::findOrFail($backupId);
$backup = ScheduledDatabaseBackup::where('id', $backupId)
->where('s3_storage_id', $this->storage->id)
->firstOrFail();
$newStorageId = $this->selectedStorages[$backupId] ?? null;
if (! $newStorageId || (int) $newStorageId === $this->storage->id) {

View file

@ -90,6 +90,16 @@ public function server()
return $this->belongsTo(Server::class);
}
public static function ownedByCurrentTeam()
{
return static::whereHas('server', fn ($q) => $q->whereTeamId(currentTeam()->id));
}
public static function ownedByCurrentTeamAPI(int $teamId)
{
return static::whereHas('server', fn ($q) => $q->whereTeamId($teamId));
}
/**
* Get the server attribute using identity map caching.
* This intercepts lazy-loading to use cached Server lookups.

View file

@ -71,6 +71,16 @@ public function server()
return $this->belongsTo(Server::class);
}
public static function ownedByCurrentTeam()
{
return static::whereHas('server', fn ($q) => $q->whereTeamId(currentTeam()->id));
}
public static function ownedByCurrentTeamAPI(int $teamId)
{
return static::whereHas('server', fn ($q) => $q->whereTeamId($teamId));
}
/**
* Get the server attribute using identity map caching.
* This intercepts lazy-loading to use cached Server lookups.

View file

@ -3,6 +3,7 @@
use App\Models\EnvironmentVariable;
use App\Models\S3Storage;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker;
use App\Models\StandaloneDragonfly;
@ -12,18 +13,19 @@
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Models\SwarmDocker;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null, string $databaseImage = 'postgres:16-alpine'): StandalonePostgresql
function create_standalone_postgresql($environmentId, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null, string $databaseImage = 'postgres:16-alpine'): StandalonePostgresql
{
$destination = StandaloneDocker::where('uuid', $destinationUuid)->firstOrFail();
$database = new StandalonePostgresql;
$database->uuid = (new Cuid2);
$database->name = 'postgresql-database-'.$database->uuid;
$database->image = $databaseImage;
$database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->postgres_password = Str::password(length: 64, symbols: false);
$database->environment_id = $environmentId;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@ -35,14 +37,13 @@ function create_standalone_postgresql($environmentId, $destinationUuid, ?array $
return $database;
}
function create_standalone_redis($environment_id, $destination_uuid, ?array $otherData = null): StandaloneRedis
function create_standalone_redis($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneRedis
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
$database = new StandaloneRedis;
$database->uuid = (new Cuid2);
$database->name = 'redis-database-'.$database->uuid;
$redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$redis_password = Str::password(length: 64, symbols: false);
if ($otherData && isset($otherData['redis_password'])) {
$redis_password = $otherData['redis_password'];
unset($otherData['redis_password']);
@ -75,13 +76,12 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
return $database;
}
function create_standalone_mongodb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMongodb
function create_standalone_mongodb($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneMongodb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
$database = new StandaloneMongodb;
$database->uuid = (new Cuid2);
$database->name = 'mongodb-database-'.$database->uuid;
$database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->mongo_initdb_root_password = Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@ -93,14 +93,13 @@ function create_standalone_mongodb($environment_id, $destination_uuid, ?array $o
return $database;
}
function create_standalone_mysql($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMysql
function create_standalone_mysql($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneMysql
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
$database = new StandaloneMysql;
$database->uuid = (new Cuid2);
$database->name = 'mysql-database-'.$database->uuid;
$database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->mysql_root_password = Str::password(length: 64, symbols: false);
$database->mysql_password = Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@ -112,14 +111,13 @@ function create_standalone_mysql($environment_id, $destination_uuid, ?array $oth
return $database;
}
function create_standalone_mariadb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMariadb
function create_standalone_mariadb($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneMariadb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
$database = new StandaloneMariadb;
$database->uuid = (new Cuid2);
$database->name = 'mariadb-database-'.$database->uuid;
$database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->mariadb_root_password = Str::password(length: 64, symbols: false);
$database->mariadb_password = Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@ -131,13 +129,12 @@ function create_standalone_mariadb($environment_id, $destination_uuid, ?array $o
return $database;
}
function create_standalone_keydb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneKeydb
function create_standalone_keydb($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneKeydb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
$database = new StandaloneKeydb;
$database->uuid = (new Cuid2);
$database->name = 'keydb-database-'.$database->uuid;
$database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->keydb_password = Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@ -149,13 +146,12 @@ function create_standalone_keydb($environment_id, $destination_uuid, ?array $oth
return $database;
}
function create_standalone_dragonfly($environment_id, $destination_uuid, ?array $otherData = null): StandaloneDragonfly
function create_standalone_dragonfly($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneDragonfly
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
$database = new StandaloneDragonfly;
$database->uuid = (new Cuid2);
$database->name = 'dragonfly-database-'.$database->uuid;
$database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->dragonfly_password = Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@ -167,13 +163,12 @@ function create_standalone_dragonfly($environment_id, $destination_uuid, ?array
return $database;
}
function create_standalone_clickhouse($environment_id, $destination_uuid, ?array $otherData = null): StandaloneClickhouse
function create_standalone_clickhouse($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneClickhouse
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
$database = new StandaloneClickhouse;
$database->uuid = (new Cuid2);
$database->name = 'clickhouse-database-'.$database->uuid;
$database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->clickhouse_admin_password = Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@ -279,7 +274,7 @@ function removeOldBackups($backup): void
->whereNull('s3_uploaded')
->delete();
} catch (\Exception $e) {
} catch (Exception $e) {
throw $e;
}
}
@ -345,7 +340,7 @@ function deleteOldBackupsLocally($backup): Collection
$processedBackups = collect();
$server = null;
if ($backup->database_type === \App\Models\ServiceDatabase::class) {
if ($backup->database_type === ServiceDatabase::class) {
$server = $backup->database->service->server;
} else {
$server = $backup->database->destination->server;

View file

@ -18,6 +18,7 @@
use App\Models\ServiceDatabase;
use App\Models\SharedEnvironmentVariable;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
@ -25,6 +26,7 @@
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Models\SwarmDocker;
use App\Models\Team;
use App\Models\User;
use Carbon\CarbonImmutable;
@ -259,6 +261,16 @@ function currentTeam()
return Auth::user()?->currentTeam() ?? null;
}
function find_destination_for_current_team(?string $uuid): StandaloneDocker|SwarmDocker|null
{
if (blank($uuid) || ! currentTeam()) {
return null;
}
return StandaloneDocker::ownedByCurrentTeam()->where('uuid', $uuid)->first()
?? SwarmDocker::ownedByCurrentTeam()->where('uuid', $uuid)->first();
}
function showBoarding(): bool
{
if (isDev()) {
@ -3489,34 +3501,6 @@ function getHelperVersion(): string
return config('constants.coolify.helper_version');
}
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
{
$server = Server::find($server_id)->where('team_id', $team_id)->first();
if (! $server) {
return;
}
$uuid = new Cuid2;
$cloneCommand = "git clone --no-checkout -b $branch $repository .";
$workdir = rtrim($base_directory, '/');
$fileList = collect([".$workdir/coolify.json"]);
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir/coolify.json",
'rm -rf /tmp/{$uuid}',
]);
try {
return instant_remote_process($commands, $server);
} catch (Exception) {
// continue
}
}
function loggy($message = null, array $context = [])
{
if (! isDev()) {

View file

@ -1,6 +1,7 @@
<?php
use App\Livewire\Admin\Index as AdminIndex;
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -70,9 +71,9 @@
test('switchUser requires root user id 0', function () {
config()->set('constants.coolify.self_hosted', false);
$rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]);
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
$rootUser = User::factory()->create(['id' => 0]);
$rootTeam->members()->attach($rootUser->id, ['role' => 'admin']);
$rootTeam = Team::find(0);
$targetUser = User::factory()->create();
$targetTeam = Team::factory()->create();
@ -84,7 +85,47 @@
Livewire::test(AdminIndex::class)
->assertOk()
->call('switchUser', $targetUser->id)
->assertRedirect();
->assertRedirect(route('dashboard'));
});
test('back() redirects impersonator to admin index and clears session', function () {
config()->set('constants.coolify.self_hosted', false);
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
$rootUser = User::factory()->create(['id' => 0]);
$rootTeam = Team::find(0);
$this->actingAs($rootUser);
session([
'currentTeam' => ['id' => $rootTeam->id],
'impersonating' => true,
]);
Livewire::test(AdminIndex::class)
->call('back')
->assertRedirect(route('admin.index'));
expect(session('impersonating'))->toBeNull();
});
test('switchUser ignores Referer header and uses dashboard route', function () {
config()->set('constants.coolify.self_hosted', false);
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
$rootUser = User::factory()->create(['id' => 0]);
$rootTeam = Team::find(0);
$targetUser = User::factory()->create();
$targetTeam = Team::factory()->create();
$targetTeam->members()->attach($targetUser->id, ['role' => 'admin']);
$this->actingAs($rootUser);
session(['currentTeam' => ['id' => $rootTeam->id]]);
Livewire::withHeaders(['Referer' => 'https://example.com/elsewhere'])
->test(AdminIndex::class)
->call('switchUser', $targetUser->id)
->assertRedirect(route('dashboard'));
});
test('switchUser rejects non-root user', function () {

View file

@ -0,0 +1,106 @@
<?php
use App\Livewire\Storage\Resources as StorageResources;
use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
Queue::fake();
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
$this->userA = User::factory()->create();
$this->teamA = Team::factory()->create();
$this->userA->teams()->attach($this->teamA, ['role' => 'owner']);
$this->userB = User::factory()->create();
$this->teamB = Team::factory()->create();
$this->userB->teams()->attach($this->teamB, ['role' => 'owner']);
$this->storageA = S3Storage::unguarded(fn () => S3Storage::create([
'uuid' => fake()->uuid(),
'name' => 'storage-a-'.fake()->unique()->word(),
'region' => 'us-east-1',
'key' => 'key-a',
'secret' => 'secret-a',
'bucket' => 'bucket-a',
'endpoint' => 'https://s3.example.com',
'team_id' => $this->teamA->id,
]));
$this->storageB = S3Storage::unguarded(fn () => S3Storage::create([
'uuid' => fake()->uuid(),
'name' => 'storage-b-'.fake()->unique()->word(),
'region' => 'us-east-1',
'key' => 'key-b',
'secret' => 'secret-b',
'bucket' => 'bucket-b',
'endpoint' => 'https://s3.example.com',
'team_id' => $this->teamB->id,
]));
$this->backupA = ScheduledDatabaseBackup::create([
'uuid' => fake()->uuid(),
'team_id' => $this->teamA->id,
'enabled' => true,
'save_s3' => true,
'frequency' => '0 0 * * *',
'database_type' => 'App\\Models\\StandalonePostgresql',
'database_id' => 1,
's3_storage_id' => $this->storageA->id,
]);
$this->backupB = ScheduledDatabaseBackup::create([
'uuid' => fake()->uuid(),
'team_id' => $this->teamB->id,
'enabled' => true,
'save_s3' => true,
'frequency' => '0 0 * * *',
'database_type' => 'App\\Models\\StandalonePostgresql',
'database_id' => 2,
's3_storage_id' => $this->storageB->id,
]);
$this->actingAs($this->userA);
session(['currentTeam' => $this->teamA]);
});
describe('Storage/Resources team-scoped backup access', function () {
test('disableS3 on other team backup throws and leaves row unchanged', function () {
expect(fn () => Livewire::test(StorageResources::class, ['storage' => $this->storageA])
->call('disableS3', $this->backupB->id))
->toThrow(ModelNotFoundException::class);
$this->backupB->refresh();
expect((bool) $this->backupB->save_s3)->toBeTrue();
expect($this->backupB->s3_storage_id)->toBe($this->storageB->id);
});
test('moveBackup on other team backup throws and leaves row unchanged', function () {
expect(fn () => Livewire::test(StorageResources::class, ['storage' => $this->storageA])
->set('selectedStorages', [$this->backupB->id => $this->storageA->id])
->call('moveBackup', $this->backupB->id))
->toThrow(ModelNotFoundException::class);
$this->backupB->refresh();
expect($this->backupB->s3_storage_id)->toBe($this->storageB->id);
});
test('disableS3 on own backup succeeds', function () {
Livewire::test(StorageResources::class, ['storage' => $this->storageA])
->call('disableS3', $this->backupA->id);
$this->backupA->refresh();
expect((bool) $this->backupA->save_s3)->toBeFalse();
expect($this->backupA->s3_storage_id)->toBeNull();
});
});

View file

@ -0,0 +1,297 @@
<?php
use App\Livewire\Destination\Show as DestinationShow;
use App\Livewire\Project\New\DockerCompose;
use App\Livewire\Project\New\DockerImage;
use App\Livewire\Project\New\GithubPrivateRepository;
use App\Livewire\Project\New\GithubPrivateRepositoryDeployKey;
use App\Livewire\Project\New\PublicGitRepository;
use App\Livewire\Project\New\SimpleDockerfile;
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql;
use App\Models\SwarmDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
Queue::fake();
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
$this->userA = User::factory()->create();
$this->teamA = Team::factory()->create();
$this->userA->teams()->attach($this->teamA, ['role' => 'owner']);
$this->serverA = Server::factory()->create(['team_id' => $this->teamA->id]);
$this->projectA = Project::factory()->create(['team_id' => $this->teamA->id]);
$this->environmentA = Environment::factory()->create(['project_id' => $this->projectA->id]);
$this->destinationA = StandaloneDocker::factory()->create([
'server_id' => $this->serverA->id,
'name' => 'dest-a-'.fake()->unique()->word(),
'network' => 'coolify-a-'.fake()->unique()->word(),
]);
$this->userB = User::factory()->create();
$this->teamB = Team::factory()->create();
$this->userB->teams()->attach($this->teamB, ['role' => 'owner']);
$this->serverB = Server::factory()->create(['team_id' => $this->teamB->id]);
$this->projectB = Project::factory()->create(['team_id' => $this->teamB->id]);
$this->environmentB = Environment::factory()->create(['project_id' => $this->projectB->id]);
$this->destinationB = StandaloneDocker::factory()->create([
'server_id' => $this->serverB->id,
'name' => 'dest-b-'.fake()->unique()->word(),
'network' => 'coolify-b-'.fake()->unique()->word(),
]);
$this->swarmDestinationB = SwarmDocker::create([
'uuid' => fake()->uuid(),
'name' => 'swarm-b-'.fake()->unique()->word(),
'network' => 'swarm-b-'.fake()->unique()->word(),
'server_id' => $this->serverB->id,
]);
$this->actingAs($this->userA);
session(['currentTeam' => $this->teamA]);
});
describe('find_destination_for_current_team helper', function () {
test('returns null for other team destination UUID', function () {
expect(find_destination_for_current_team($this->destinationB->uuid))->toBeNull();
});
test('returns null for other team swarm destination UUID', function () {
expect(find_destination_for_current_team($this->swarmDestinationB->uuid))->toBeNull();
});
test('returns own team destination', function () {
$found = find_destination_for_current_team($this->destinationA->uuid);
expect($found)->not->toBeNull();
expect($found->id)->toBe($this->destinationA->id);
});
test('returns null for blank uuid', function () {
expect(find_destination_for_current_team(null))->toBeNull();
expect(find_destination_for_current_team(''))->toBeNull();
});
});
describe('SimpleDockerfile destination team scope', function () {
test('submit with other team destination throws and creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
request()->headers->set('referer', route('project.resource.create', $routeParams).'?destination='.$this->destinationB->uuid);
$before = Application::count();
expect(fn () => Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(SimpleDockerfile::class, $routeParams)
->set('dockerfile', "FROM nginx\nCMD [\"nginx\"]\n")
->call('submit'))
->toThrow(Exception::class, 'Destination not found.');
expect(Application::count())->toBe($before);
});
});
describe('DockerImage destination team scope', function () {
test('submit with other team destination throws and creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Application::count();
expect(fn () => Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(DockerImage::class, $routeParams)
->set('imageName', 'nginx')
->set('imageTag', 'latest')
->call('submit'))
->toThrow(Exception::class, 'Destination not found.');
expect(Application::count())->toBe($before);
});
test('submit with other team swarm destination throws', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
expect(fn () => Livewire::withUrlParams(['destination' => $this->swarmDestinationB->uuid])
->test(DockerImage::class, $routeParams)
->set('imageName', 'nginx')
->set('imageTag', 'latest')
->call('submit'))
->toThrow(Exception::class, 'Destination not found.');
});
});
describe('DockerCompose destination + server_id team scope', function () {
test('submit with other team destination throws and creates no service', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Service::count();
Livewire::withUrlParams([
'destination' => $this->destinationB->uuid,
'server_id' => $this->serverB->id,
])
->test(DockerCompose::class, $routeParams)
->set('dockerComposeRaw', "services:\n app:\n image: nginx\n")
->call('submit');
expect(Service::count())->toBe($before);
});
});
describe('PublicGitRepository destination team scope', function () {
test('submit with other team destination creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Application::count();
try {
Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(PublicGitRepository::class, $routeParams)
->set('repository_url', 'https://github.com/coollabsio/coolify')
->set('git_repository', 'coollabsio/coolify')
->set('git_branch', 'main')
->set('port', 3000)
->set('build_pack', 'nixpacks')
->set('git_source', 'other')
->call('submit');
} catch (Throwable $e) {
// submit wraps errors via handleError; count assertion below is source of truth
}
expect(Application::count())->toBe($before);
});
});
describe('GithubPrivateRepository destination team scope', function () {
test('submit with other team destination throws and creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Application::count();
try {
Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(GithubPrivateRepository::class, $routeParams)
->call('submit');
} catch (Throwable $e) {
// expected
}
expect(Application::count())->toBe($before);
});
});
describe('GithubPrivateRepositoryDeployKey destination team scope', function () {
test('submit with other team destination throws and creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Application::count();
try {
Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(GithubPrivateRepositoryDeployKey::class, $routeParams)
->call('submit');
} catch (Throwable $e) {
// expected
}
expect(Application::count())->toBe($before);
});
});
describe('Resource/Create database destination team scope', function () {
test('mount with other team destination does not create database', function () {
$before = StandalonePostgresql::count();
$url = route('project.resource.create', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
]).'?type=postgresql&destination='.$this->destinationB->uuid.'&server_id='.$this->serverB->id.'&database_image=postgres:16-alpine';
$this->get($url);
expect(StandalonePostgresql::count())->toBe($before);
});
});
describe('StandaloneDocker/SwarmDocker ownedByCurrentTeam scope', function () {
test('StandaloneDocker::ownedByCurrentTeam excludes other team destinations', function () {
expect(StandaloneDocker::ownedByCurrentTeam()->where('uuid', $this->destinationB->uuid)->first())->toBeNull();
});
test('SwarmDocker::ownedByCurrentTeam excludes other team destinations', function () {
expect(SwarmDocker::ownedByCurrentTeam()->where('uuid', $this->swarmDestinationB->uuid)->first())->toBeNull();
});
test('StandaloneDocker::ownedByCurrentTeam returns own destination', function () {
$found = StandaloneDocker::ownedByCurrentTeam()->where('uuid', $this->destinationA->uuid)->first();
expect($found)->not->toBeNull();
expect($found->id)->toBe($this->destinationA->id);
});
test('StandaloneDocker::ownedByCurrentTeamAPI scopes by explicit team id', function () {
expect(StandaloneDocker::ownedByCurrentTeamAPI($this->teamA->id)->where('uuid', $this->destinationB->uuid)->first())->toBeNull();
expect(StandaloneDocker::ownedByCurrentTeamAPI($this->teamB->id)->where('uuid', $this->destinationB->uuid)->first()?->id)->toBe($this->destinationB->id);
});
test('SwarmDocker::ownedByCurrentTeamAPI scopes by explicit team id', function () {
expect(SwarmDocker::ownedByCurrentTeamAPI($this->teamA->id)->where('uuid', $this->swarmDestinationB->uuid)->first())->toBeNull();
expect(SwarmDocker::ownedByCurrentTeamAPI($this->teamB->id)->where('uuid', $this->swarmDestinationB->uuid)->first()?->id)->toBe($this->swarmDestinationB->id);
});
});
describe('Destination/Show team scope', function () {
test('mount with other team destination UUID redirects to index', function () {
$component = Livewire::test(DestinationShow::class, ['destination_uuid' => $this->destinationB->uuid]);
expect($component->get('destination'))->toBeNull();
$component->assertRedirect(route('destination.index'));
});
test('mount with own destination UUID loads it', function () {
$component = Livewire::test(DestinationShow::class, ['destination_uuid' => $this->destinationA->uuid]);
expect($component->get('destination'))->not->toBeNull();
expect($component->get('destination')->id)->toBe($this->destinationA->id);
});
test('mount with other team swarm destination UUID redirects to index', function () {
$component = Livewire::test(DestinationShow::class, ['destination_uuid' => $this->swarmDestinationB->uuid]);
expect($component->get('destination'))->toBeNull();
$component->assertRedirect(route('destination.index'));
});
});

View file

@ -0,0 +1,96 @@
<?php
use App\Models\Application;
use App\Models\Environment;
use App\Models\GithubApp;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
// Team A (current actor)
$this->userA = User::factory()->create();
$this->teamA = Team::factory()->create();
$this->userA->teams()->attach($this->teamA, ['role' => 'owner']);
$this->serverA = Server::factory()->create(['team_id' => $this->teamA->id]);
$this->destinationA = StandaloneDocker::factory()->create(['server_id' => $this->serverA->id, 'network' => 'net-a-'.fake()->uuid()]);
$this->projectA = Project::factory()->create(['team_id' => $this->teamA->id]);
$this->environmentA = Environment::factory()->create(['project_id' => $this->projectA->id]);
// Team B (other team)
$this->userB = User::factory()->create();
$this->teamB = Team::factory()->create();
$this->userB->teams()->attach($this->teamB, ['role' => 'owner']);
$this->serverB = Server::factory()->create(['team_id' => $this->teamB->id]);
$this->destinationB = StandaloneDocker::factory()->create(['server_id' => $this->serverB->id, 'network' => 'net-b-'.fake()->uuid()]);
$this->projectB = Project::factory()->create(['team_id' => $this->teamB->id]);
$this->environmentB = Environment::factory()->create(['project_id' => $this->projectB->id]);
// Authenticate as Team A
$this->actingAs($this->userA);
session(['currentTeam' => $this->teamA]);
});
test('unscoped Project lookup returns another teams project', function () {
$project = Project::where('uuid', $this->projectB->uuid)->first();
expect($project)->not->toBeNull()
->and($project->team_id)->toBe($this->teamB->id)
->and($project->team_id)->not->toBe($this->teamA->id);
});
test('unscoped StandaloneDocker lookup returns another teams destination', function () {
$dest = StandaloneDocker::where('uuid', $this->destinationB->uuid)->first();
expect($dest)->not->toBeNull()
->and($dest->server->team_id)->toBe($this->teamB->id);
});
test('ownedByCurrentTeam scope blocks other-team Project access', function () {
expect(Project::ownedByCurrentTeam()->where('uuid', $this->projectB->uuid)->first())->toBeNull();
});
test('ownedByCurrentTeam scope allows own Project access', function () {
expect(Project::ownedByCurrentTeam()->where('uuid', $this->projectA->uuid)->first())->not->toBeNull();
});
test('Team A can create Application in Team B environment via unscoped lookups', function () {
$destination = StandaloneDocker::where('uuid', $this->destinationB->uuid)->first();
$project = Project::where('uuid', $this->projectB->uuid)->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->environmentB->uuid)->first();
$application = Application::create([
'name' => 'team-scope-test-canary',
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',
'git_branch' => 'main',
'build_pack' => 'dockerfile',
'dockerfile' => "FROM alpine\nCMD echo hello",
'ports_exposes' => 80,
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
'health_check_enabled' => false,
'source_id' => 0,
'source_type' => GithubApp::class,
]);
expect($application->environment_id)->toBe($this->environmentB->id)
->and($application->destination_id)->toBe($this->destinationB->id)
->and($application->environment->project->team->id)->toBe($this->teamB->id)
->and($application->environment->project->team->id)->not->toBe($this->teamA->id);
});
test('resource creation page loads with another teams project UUID', function () {
$response = $this->get(route('project.resource.create', [
'project_uuid' => $this->projectB->uuid,
'environment_uuid' => $this->environmentB->uuid,
]));
expect($response->status())->not->toBe(403);
});