1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-05-06 14:24:04 +00:00

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.
This commit is contained in:
lukinovec 2026-05-01 11:44:56 +02:00
parent 1a01164b87
commit 665404e7fa
2 changed files with 88 additions and 2 deletions

View file

@ -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.');
}
}
}

View file

@ -1,18 +1,76 @@
<?php
use Illuminate\Support\Facades\Event;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
use Illuminate\Support\Str;
beforeEach(function () {
$cleanup = function () {
DatabaseTenancyBootstrapper::$harden = false;
};
beforeEach(function () use ($cleanup) {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
$cleanup();
});
afterEach($cleanup);
test('harden prevents tenants from using the central database', 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->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]);