From c80b28cf006b481b414d9ce44bd23677770c9002 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 20 May 2025 13:58:33 +0200 Subject: [PATCH] Add option to provide constraint information in column comment --- src/RLS/PolicyManagers/TableRLSManager.php | 43 ++++++- tests/RLS/TableManagerTest.php | 132 +++++++++++++++++++++ 2 files changed, 173 insertions(+), 2 deletions(-) diff --git a/src/RLS/PolicyManagers/TableRLSManager.php b/src/RLS/PolicyManagers/TableRLSManager.php index 8e941b31..15701d45 100644 --- a/src/RLS/PolicyManagers/TableRLSManager.php +++ b/src/RLS/PolicyManagers/TableRLSManager.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\RLS\PolicyManagers; use Illuminate\Database\DatabaseManager; use Stancl\Tenancy\Database\Exceptions\RecursiveRelationshipException; +use Illuminate\Support\Str; // todo@samuel logical + structural refactor. the tree generation could use some dynamic programming optimizations class TableRLSManager implements RLSPolicyManager @@ -80,7 +81,7 @@ class TableRLSManager implements RLSPolicyManager $table = str($table)->afterLast('.')->toString(); // For each table, we get a list of all foreign key columns - $foreignKeys = collect($builder->getForeignKeys($table))->map(function ($foreign) use ($table) { + $foreignKeys = collect($builder->getForeignKeys($table))->merge($this->getFakeForeignKeys($table))->map(function ($foreign) use ($table) { return $this->formatForeignKey($foreign, $table); }); @@ -124,12 +125,50 @@ class TableRLSManager implements RLSPolicyManager $paths[] = $currentPath; } else { // If not, recursively generate paths for the foreign table - foreach ($this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable']) as $nextConstraint) { + foreach (array_merge($this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable'], $this->getFakeForeignKeys($table))) as $nextConstraint) { $this->generatePaths($table, $this->formatForeignKey($nextConstraint, $foreign['foreignTable']), $paths, $currentPath); } } } + protected function getFakeForeignKeys(string $tableName): array + { + $columns = $this->database->getSchemaBuilder()->getColumns($tableName); + + $fakeForeignKeys = array_filter($columns, function ($column) { + // Constraint comment should be "rls ." + if ($column['comment']) { + return Str::contains($column['comment'], 'rls '); + } + + return false; + }); + + if ($tableName === 'non_constrained_posts') { + dump(array_map(function ($fakeForeignKey) { + // Constraint comment is "rls ." + $constraint = explode('.', Str::after($fakeForeignKey['comment'], 'rls ')); + + return [ + 'foreign_table' => $constraint[0], + 'foreign_columns' => [$constraint[1]], + 'columns' => [$fakeForeignKey['name']], + ]; + }, $fakeForeignKeys)); + } + + return array_map(function ($fakeForeignKey) { + // Constraint comment is "rls ." + $constraint = explode('.', Str::after($fakeForeignKey['comment'], 'rls ')); + + return [ + 'foreign_table' => $constraint[0], + 'foreign_columns' => [$constraint[1]], + 'columns' => [$fakeForeignKey['name']], + ]; + }, $fakeForeignKeys); + } + /** Get tree's non-nullable paths. */ protected function filterNonNullablePaths(array $tree): array { diff --git a/tests/RLS/TableManagerTest.php b/tests/RLS/TableManagerTest.php index fd9c6f44..0fd31083 100644 --- a/tests/RLS/TableManagerTest.php +++ b/tests/RLS/TableManagerTest.php @@ -663,6 +663,138 @@ test('table manager ignores recursive relationship if the foreign key responsibl expect(fn () => app(TableRLSManager::class)->generateTrees())->not()->toThrow(RecursiveRelationshipException::class); }); +test('table manager can generate paths leading through non-constrained foreign keys', function() { + Schema::create('non_constrained_users', function (Blueprint $table) { + $table->id(); + $table->string('tenant_id')->comment('rls tenants.id'); // "fake" constraint + }); + + Schema::create('non_constrained_posts', function (Blueprint $table) { + $table->id(); + $table->foreignId('author_id')->comment('rls non_constrained_users.id'); // another "fake" constraint + }); + + /** @var TableRLSManager $manager */ + $manager = app(TableRLSManager::class); + + $expectedTrees = [ + 'authors' => [ + // Directly related to tenants + 'tenant_id' => [ + [ + [ + 'foreignKey' => 'tenant_id', + 'foreignTable' => 'tenants', + 'foreignId' => 'id', + 'nullable' => false, + ] + ], + ], + ], + 'comments' => [ + // Tree starting from the post_id foreign key + 'post_id' => [ + [ + [ + 'foreignKey' => 'post_id', + 'foreignTable' => 'posts', + 'foreignId' => 'id', + 'nullable' => false, + ], + [ + 'foreignKey' => 'author_id', + 'foreignTable' => 'authors', + 'foreignId' => 'id', + 'nullable' => false, + ], + [ + 'foreignKey' => 'tenant_id', + 'foreignTable' => 'tenants', + 'foreignId' => 'id', + 'nullable' => false, + ], + ], + [ + [ + 'foreignKey' => 'post_id', + 'foreignTable' => 'posts', + 'foreignId' => 'id', + 'nullable' => false, + ], + [ + 'foreignKey' => 'tenant_id', + 'foreignTable' => 'tenants', + 'foreignId' => 'id', + 'nullable' => true, + ], + ], + ], + ], + 'non_constrained_posts' => [ + 'author_id' => [ + [ + [ + 'foreignKey' => 'author_id', + 'foreignTable' => 'non_constrained_users', + 'foreignId' => 'id', + 'nullable' => false, + ], + [ + 'foreignKey' => 'tenant_id', + 'foreignTable' => 'tenants', + 'foreignId' => 'id', + 'nullable' => false, + ] + ], + ], + ], + 'non_constrained_users' => [ + // Category tree gets excluded because the category table is related to the tenant table + // only through a column with the 'no-rls' comment + 'tenant_id' => [ + [ + [ + 'foreignKey' => 'tenant_id', + 'foreignTable' => 'tenants', + 'foreignId' => 'id', + 'nullable' => false, + ] + ] + ], + ], + 'posts' => [ + 'author_id' => [ + [ + [ + 'foreignKey' => 'author_id', + 'foreignTable' => 'authors', + 'foreignId' => 'id', + 'nullable' => false, + ], + [ + 'foreignKey' => 'tenant_id', + 'foreignTable' => 'tenants', + 'foreignId' => 'id', + 'nullable' => false, + ] + ], + ], + 'tenant_id' => [ + [ + [ + 'foreignKey' => 'tenant_id', + 'foreignTable' => 'tenants', + 'foreignId' => 'id', + 'nullable' => true, + ] + ] + ], + ], + ]; + + expect($manager->generateTrees())->toEqual($expectedTrees); +}); + class Post extends Model { protected $guarded = [];