diff --git a/src/Actions/CreateRLSPoliciesForTables.php b/src/Actions/CreateRLSPoliciesForTables.php new file mode 100644 index 00000000..4c9479fb --- /dev/null +++ b/src/Actions/CreateRLSPoliciesForTables.php @@ -0,0 +1,71 @@ +getTable(); + + DB::statement("DROP POLICY IF EXISTS {$table}_rls_policy ON {$table}"); + + if (! Schema::hasColumn($table, $tenantKey)) { + // Table is not directly related to tenant + if (in_array(BelongsToPrimaryModel::class, class_uses_recursive($model::class))) { + $parentName = $model->getRelationshipToPrimaryModel(); + $parentKey = $model->$parentName()->getForeignKeyName(); + $parentModel = $model->$parentName()->make(); + $parentTable = str($parentModel->getTable())->toString(); + + DB::statement("CREATE POLICY {$table}_rls_policy ON {$table} USING ( + {$parentKey} IN ( + SELECT id + FROM {$parentTable} + WHERE ({$tenantKey}::TEXT = ( + SELECT {$tenantKey} + FROM {$parentTable} + WHERE id = {$parentKey} + )) + ) + )"); + + DB::statement("ALTER TABLE {$table} FORCE ROW LEVEL SECURITY"); + + return Command::SUCCESS; + } else { + $modelName = $model::class; + // $this->components->info("Table '$table' is not related to tenant. Make sure $modelName uses the BelongsToPrimaryModel trait."); + + return Command::FAILURE; + } + } + + DB::statement("CREATE POLICY {$table}_rls_policy ON {$table} USING ({$tenantKey}::TEXT = current_user);"); + + DB::statement("ALTER TABLE {$table} FORCE ROW LEVEL SECURITY"); + + // $this->components->info("Created RLS policy for table '$table'"); + } + + return Command::SUCCESS; + } + + public static function getTenantModels(): array + { + return array_map(fn (string $modelName) => (new $modelName), config('tenancy.models.rls')); + } +} diff --git a/tests/PostgresTest.php b/tests/PostgresTest.php index 9b186088..1a36357c 100644 --- a/tests/PostgresTest.php +++ b/tests/PostgresTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Jobs\DeleteTenantsPostgresUser; use Stancl\Tenancy\Jobs\CreatePostgresUserForTenant; +use Stancl\Tenancy\Actions\CreateRLSPoliciesForTables; beforeEach(function () { DB::setDefaultConnection('pgsql'); @@ -45,7 +46,7 @@ test('postgres user can get deleted using the job', function() { expect($tenantHasPostgresUser())->toBeFalse(); }); -test('correct rls policies get created using the command', function() { +test('correct rls policies get created using the action or the command', function(bool $action) { config([ 'tenancy.models.rls' => [ Post::class, // Primary model (directly belongs to tenant) @@ -57,7 +58,13 @@ test('correct rls policies get created using the command', function() { $getRlsTables = fn() => $getModelTables()->map(fn ($table) => DB::select('select relname, relrowsecurity, relforcerowsecurity from pg_class WHERE oid = ' . "'$table'::regclass"))->collapse(); expect($getRlsPolicies())->toHaveCount(0); - pest()->artisan('tenants:create-rls-policies'); + + if ($action) { + CreateRLSPoliciesForTables::handle(); + } else { + pest()->artisan('tenants:create-rls-policies'); + } + expect($getRlsPolicies())->toHaveCount(count(config('tenancy.models.rls'))); // 1 expect($getRlsTables())->toHaveCount(count(config('tenancy.models.rls'))); // 1 // Check if tables with policies are RLS protected @@ -72,7 +79,12 @@ test('correct rls policies get created using the command', function() { ], config('tenancy.models.rls')), ]); - pest()->artisan('tenants:create-rls-policies'); + if ($action) { + CreateRLSPoliciesForTables::handle(); + } else { + pest()->artisan('tenants:create-rls-policies'); + } + // Check if tables with policies are RLS protected (even the ones not directly related to the tenant) // Models related to tenant through some model must use the BelongsToPrimaryModel trait to work properly expect($getRlsPolicies())->toHaveCount(count(config('tenancy.models.rls'))); // 2 @@ -82,4 +94,4 @@ test('correct rls policies get created using the command', function() { expect($getModelTables())->toContain($table->relname); expect($table->relforcerowsecurity)->toBeTrue(); } -}); +})->with([true, false]);