refactor: scope destination and resource lookups by current team
Use find_destination_for_current_team helper across resource creation flows and the destination controller. Pass full destination objects to database creation helpers instead of UUIDs so team relationships are resolved consistently before the resource is created or linked. Add feature tests covering destination, backup storage, and resource proof lookups across teams.
This commit is contained in:
parent
57ea0764b8
commit
a478ac66eb
18 changed files with 607 additions and 154 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
106
tests/Feature/TeamScopedBackupStorageTest.php
Normal file
106
tests/Feature/TeamScopedBackupStorageTest.php
Normal 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();
|
||||
});
|
||||
});
|
||||
297
tests/Feature/TeamScopedDestinationTest.php
Normal file
297
tests/Feature/TeamScopedDestinationTest.php
Normal 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'));
|
||||
});
|
||||
});
|
||||
96
tests/Feature/TeamScopedResourceProofsTest.php
Normal file
96
tests/Feature/TeamScopedResourceProofsTest.php
Normal 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);
|
||||
});
|
||||
Loading…
Reference in a new issue