From 665404e7faa248b00f3751abc8bdaed9eeb30e78 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Fri, 1 May 2026 11:44:56 +0200 Subject: [PATCH] Add `DatabaseTenancyBootstrapper::$harden` Since It's possible to update tenant's db_name to the central DB or the DB of another tenant. Setting $harden to true prevents tenants from connecting to the wrong databases. --- .../DatabaseTenancyBootstrapper.php | 29 +++++++++ .../DatabaseTenancyBootstrapper.php | 61 ++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/Bootstrappers/DatabaseTenancyBootstrapper.php b/src/Bootstrappers/DatabaseTenancyBootstrapper.php index 7f0bce0a..427b79a5 100644 --- a/src/Bootstrappers/DatabaseTenancyBootstrapper.php +++ b/src/Bootstrappers/DatabaseTenancyBootstrapper.php @@ -5,14 +5,23 @@ declare(strict_types=1); namespace Stancl\Tenancy\Bootstrappers; use Exception; +use Illuminate\Support\Facades\Schema; +use RuntimeException; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\DatabaseManager; use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException; +use Illuminate\Database\Eloquent\Model; class DatabaseTenancyBootstrapper implements TenancyBootstrapper { + /** + * When true, throw an exception if a tenant gets connected to + * another tenant's database or to the central database. + */ + public static bool $harden = false; + /** @var DatabaseManager */ protected $database; @@ -41,10 +50,30 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper } $this->database->connectToTenant($tenant); + + if (static::$harden) $this->harden($tenant); } public function revert(): void { $this->database->reconnectToCentral(); } + + protected function harden(Tenant $tenant): void + { + /** @var TenantWithDatabase&Model $tenant */ + $dbName = $tenant->database()->getName(); + + // Check if the current database is unique (i.e. no other tenant uses this database) + if ($tenant::where($tenant->getTenantKeyName(), '!=', $tenant->getTenantKey()) + ->where('data->tenancy_db_name', $dbName) + ->exists()) { + throw new RuntimeException("Tenant cannot use a database of another tenant."); + } + + // Check if the current database doesn't have the tenants table (i.e. it's not the central database) + if (Schema::hasTable($tenant->getTable())) { + throw new RuntimeException('Tenant cannot use the central database.'); + } + } } diff --git a/tests/Bootstrappers/DatabaseTenancyBootstrapper.php b/tests/Bootstrappers/DatabaseTenancyBootstrapper.php index 14109500..ec480135 100644 --- a/tests/Bootstrappers/DatabaseTenancyBootstrapper.php +++ b/tests/Bootstrappers/DatabaseTenancyBootstrapper.php @@ -1,18 +1,76 @@ [DatabaseTenancyBootstrapper::class], + ]); + + DatabaseTenancyBootstrapper::$harden = true; + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $tenant = Tenant::create(); + + $tenant->update([ + 'tenancy_db_name' => 'main', // Central database name + ]); + + // Harden blocks initialization for tenants that use central database + expect(fn () => tenancy()->initialize($tenant))->toThrow(RuntimeException::class); +}); + +test('harden prevents tenants from using a database of another tenant', function () { + config([ + 'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class], + ]); + + DatabaseTenancyBootstrapper::$harden = true; + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $tenant = Tenant::create(); + + Tenant::create([ + 'tenancy_db_name' => $tenantDbName = 'foo' . Str::random(8), + ]); + + $tenant->update([ + 'tenancy_db_name' => $tenantDbName, // Database of another tenant + ]); + + // Harden blocks initialization for tenants that use a database of another tenant + expect(fn () => tenancy()->initialize($tenant))->toThrow(RuntimeException::class); }); test('database tenancy bootstrapper throws an exception if DATABASE_URL is set', function (string|null $databaseUrl) { @@ -32,4 +90,3 @@ test('database tenancy bootstrapper throws an exception if DATABASE_URL is set', expect(true)->toBe(true); })->with(['abc.us-east-1.rds.amazonaws.com', null]); -