From 34d19e94e208029834dad490905df091f44f82c1 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 10 Jun 2026 13:17:17 +0200 Subject: [PATCH] Make hardening work with all db/schema managers Previously, hardening only worked with databases, not with schemas. Also test that hardening works with all relevant db managers. --- .../DatabaseTenancyBootstrapper.php | 17 ++++--- .../Contracts/TenantDatabaseManager.php | 10 ++++ .../PostgreSQLSchemaManager.php | 7 +++ .../SQLiteDatabaseManager.php | 6 +++ .../TenantDatabaseManager.php | 5 ++ .../DatabaseTenancyBootstrapperTest.php | 51 ++++++++++++++----- 6 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/Bootstrappers/DatabaseTenancyBootstrapper.php b/src/Bootstrappers/DatabaseTenancyBootstrapper.php index 906b335a..a5eb6f81 100644 --- a/src/Bootstrappers/DatabaseTenancyBootstrapper.php +++ b/src/Bootstrappers/DatabaseTenancyBootstrapper.php @@ -90,7 +90,9 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper protected function verifyTenantCanUseDatabase(Tenant $tenant): void { /** @var \Stancl\Tenancy\Database\Models\Tenant&TenantWithDatabase $tenant */ - $tenantDbName = $tenant->database()->getName(); + + $tenantDbConfig = $tenant->database(); + $tenantDbName = $tenantDbConfig->getName(); // Check that no other tenant uses this tenant's database if ($tenant::where($tenant->getTenantKeyName(), '!=', $tenant->getTenantKey()) @@ -99,13 +101,14 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper throw new RuntimeException('Tenant cannot use a database of another tenant.'); } - $centralDbName = DB::connection( - config('tenancy.database.central_connection', 'central') - )->getDatabaseName(); + $manager = $tenantDbConfig->manager(); - if (DB::getDatabaseName() === $centralDbName) { - // Throw if the current database is central. - // DB::getDatabaseName() is the current DB name, which should not be central at this point. + $centralConnection = DB::connection(config('tenancy.database.central_connection', 'central')); + $currentConnection = DB::connection(); + + // Throw if the current database/schema is central. + // At this point the connection should be the tenant's, so it should not match central. + if ($manager->getCurrentDatabaseName($currentConnection) === $manager->getCurrentDatabaseName($centralConnection)) { throw new RuntimeException('Tenant cannot use the central database.'); } } diff --git a/src/Database/Contracts/TenantDatabaseManager.php b/src/Database/Contracts/TenantDatabaseManager.php index 8b5007b9..f1ac6d5f 100644 --- a/src/Database/Contracts/TenantDatabaseManager.php +++ b/src/Database/Contracts/TenantDatabaseManager.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Contracts; +use Illuminate\Database\Connection; + interface TenantDatabaseManager { /** Create a database. */ @@ -17,4 +19,12 @@ interface TenantDatabaseManager /** Construct a DB connection config array. */ public function makeConnectionConfig(array $baseConfig, string $databaseName): array; + + /** + * Get the schema/database name that the given connection points to. + * + * In database managers, this should return the *database* name of the passed connection, + * while in schema managers, this should return the *schema* name of the passed connection. + */ + public function getCurrentDatabaseName(Connection $connection): string; } diff --git a/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php index 354eb768..fdb294a2 100644 --- a/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php +++ b/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\TenantDatabaseManagers; +use Illuminate\Database\Connection; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; class PostgreSQLSchemaManager extends TenantDatabaseManager @@ -37,4 +38,10 @@ class PostgreSQLSchemaManager extends TenantDatabaseManager return $baseConfig; } + + public function getCurrentDatabaseName(Connection $connection): string + { + // Get the *schema* name (not the database name) + return $connection->selectOne('SELECT current_schema() AS schema')->schema; + } } diff --git a/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php b/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php index ce3582c0..9d749044 100644 --- a/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\TenantDatabaseManagers; use Closure; +use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Model; use InvalidArgumentException; use PDO; @@ -149,6 +150,11 @@ class SQLiteDatabaseManager implements TenantDatabaseManager return $baseConfig; } + public function getCurrentDatabaseName(Connection $connection): string + { + return $connection->getDatabaseName(); + } + public function getPath(string $name): string { $this->validateDatabaseName($name); diff --git a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php index a0822615..0dc4e642 100644 --- a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php @@ -37,4 +37,9 @@ abstract class TenantDatabaseManager implements StatefulTenantDatabaseManager return $baseConfig; } + + public function getCurrentDatabaseName(Connection $connection): string + { + return $connection->getDatabaseName(); + } } diff --git a/tests/Bootstrappers/DatabaseTenancyBootstrapperTest.php b/tests/Bootstrappers/DatabaseTenancyBootstrapperTest.php index ce78e4aa..8fd123b7 100644 --- a/tests/Bootstrappers/DatabaseTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/DatabaseTenancyBootstrapperTest.php @@ -13,6 +13,10 @@ use Stancl\Tenancy\Tests\Etc\Tenant; use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; use Illuminate\Database\QueryException; +use Stancl\Tenancy\Database\TenantDatabaseManagers\MySQLDatabaseManager; +use Stancl\Tenancy\Database\TenantDatabaseManagers\SQLiteDatabaseManager; +use Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLDatabaseManager; +use Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLSchemaManager; use function Stancl\Tenancy\Tests\pest; @@ -29,9 +33,21 @@ beforeEach(function () use ($cleanup) { afterEach($cleanup); -test('harden prevents tenants from using the central database', function ($harden) { +test('harden prevents tenants from using the central database', function (bool $harden, string $connection, string $manager) { config([ 'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class], + "tenancy.database.managers.{$connection}" => $manager, + ]); + + // Set up and migrate the central database + $centralConnection = config('tenancy.database.central_connection'); + DB::purge($centralConnection); + config(["database.connections.{$centralConnection}" => config("database.connections.{$connection}")]); + + pest()->artisan('migrate:fresh', [ + '--force' => true, + '--path' => __DIR__ . '/../../assets/migrations', + '--realpath' => true, ]); DatabaseTenancyBootstrapper::$harden = $harden; @@ -40,20 +56,20 @@ test('harden prevents tenants from using the central database', function ($harde return $event->tenant; })->toListener()); - $tenant = Tenant::create(); - + // Create the tenant with its own database, then repoint it at the central database/schema. + $tenant = Tenant::create(['tenancy_db_connection' => $connection]); $tenant->update([ - 'tenancy_db_name' => config('database.connections.central.database'), // Central database name + 'tenancy_db_name' => $tenant->database()->manager()->getCurrentDatabaseName(DB::connection($centralConnection)), ]); if ($harden) { - // Harden blocks initialization for tenants that use central database + // Harden blocks initialization for tenants that use the central database expect(fn () => tenancy()->initialize($tenant))->toThrow(RuntimeException::class); // Connection should be reverted back to central - expect(DB::connection()->getName())->toBe('central'); + expect(DB::connection()->getName())->toBe($centralConnection); } else { - expect(fn() => tenancy()->initialize($tenant))->not()->toThrow(Throwable::class); + expect(fn () => tenancy()->initialize($tenant))->not()->toThrow(Throwable::class); // Connection not reverted to central expect(DB::connection()->getName())->toBe('tenant'); @@ -61,11 +77,12 @@ test('harden prevents tenants from using the central database', function ($harde })->with([ 'hardening enabled' => true, 'hardening disabled' => false, -]); +])->with('db_managers'); -test('harden prevents tenants from using a database of another tenant', function (bool $harden, string $connection) { +test('harden prevents tenants from using a database of another tenant', function (bool $harden, string $connection, string $manager) { config([ 'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class], + "tenancy.database.managers.{$connection}" => $manager, ]); DatabaseTenancyBootstrapper::$harden = $harden; @@ -97,10 +114,7 @@ test('harden prevents tenants from using a database of another tenant', function })->with([ 'hardening enabled' => true, 'hardening disabled' => false, -])->with([ - 'mysql' => 'mysql', - 'named sqlite' => 'sqlite', -]); +])->with('db_managers'); test('database tenancy bootstrapper throws an exception if DATABASE_URL is set', function (string|null $databaseUrl) { config(['database.connections.central.url' => $databaseUrl]); @@ -123,3 +137,14 @@ test('database tenancy bootstrapper throws an exception if DATABASE_URL is set', })->not()->toThrow(Throwable::class); } })->with(['abc.us-east-1.rds.amazonaws.com', null]); + +// Database managers to test with hardening. +// Permission controlled managers omitted as they inherit the non-perm controlled managers (= they share the same code paths), +// each important code path is covered by testing the non-permission controlled manager, so adding permission controlled managers +// would add unnecessary complexity to the tests. +dataset('db_managers', [ + 'mysql' => ['mysql', MySQLDatabaseManager::class], + 'pgsql (database)' => ['pgsql', PostgreSQLDatabaseManager::class], + 'pgsql (schema)' => ['pgsql', PostgreSQLSchemaManager::class], + 'sqlite' => ['sqlite', SQLiteDatabaseManager::class], +]);