1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-04 18:04:04 +00:00

Merge branch 'may25' into resolve-test-todos

This commit is contained in:
Samuel Štancl 2025-06-23 12:49:58 +02:00
commit f9fadce538
15 changed files with 246 additions and 86 deletions

View file

@ -132,6 +132,7 @@ $finder = Finder::create()
->in([ ->in([
$project_path . '/src', $project_path . '/src',
]) ])
->exclude('Enums')
->name('*.php') ->name('*.php')
->notName('*.blade.php') ->notName('*.blade.php')
->ignoreDotFiles(true) ->ignoreDotFiles(true)

View file

@ -20,8 +20,6 @@
"ext-json": "*", "ext-json": "*",
"illuminate/support": "^12.0", "illuminate/support": "^12.0",
"laravel/tinker": "^2.0", "laravel/tinker": "^2.0",
"facade/ignition-contracts": "^1.0.2",
"spatie/ignition": "^1.4",
"ramsey/uuid": "^4.7.3", "ramsey/uuid": "^4.7.3",
"stancl/jobpipeline": "2.0.0-rc5", "stancl/jobpipeline": "2.0.0-rc5",
"stancl/virtualcolumn": "^1.5.0", "stancl/virtualcolumn": "^1.5.0",

View file

@ -22,6 +22,23 @@ class CreateUserWithRLSPolicies extends Command
protected $description = "Creates RLS policies for all tables related to the tenant table. Also creates the RLS user if it doesn't exist yet"; protected $description = "Creates RLS policies for all tables related to the tenant table. Also creates the RLS user if it doesn't exist yet";
/**
* Force, rather than just enable, the created RLS policies.
*
* By default, table owners bypass RLS policies. When this is enabled,
* they also need the BYPASSRLS permission. If your setup lets you create
* a user with BYPASSRLS, you may prefer leaving this on for additional
* safety. Otherwise, if you can't use BYPASSRLS, you can set this to false
* and depend on the behavior of table owners bypassing RLS automatically.
*
* This setting generally doesn't affect behavior at all with "default"
* setups, however if you have a more custom setup, with additional users
* involved (e.g. central connection user not being the same user that
* creates tables, or the created "RLS user" creating some tables) you
* should take care with how you configure this.
*/
public static bool $forceRls = true;
public function handle(PermissionControlledPostgreSQLSchemaManager $manager): int public function handle(PermissionControlledPostgreSQLSchemaManager $manager): int
{ {
$username = config('tenancy.rls.user.username'); $username = config('tenancy.rls.user.username');
@ -49,14 +66,9 @@ class CreateUserWithRLSPolicies extends Command
// Enable RLS scoping on the table (without this, queries won't be scoped using RLS) // Enable RLS scoping on the table (without this, queries won't be scoped using RLS)
DB::statement("ALTER TABLE {$table} ENABLE ROW LEVEL SECURITY"); DB::statement("ALTER TABLE {$table} ENABLE ROW LEVEL SECURITY");
/** if (static::$forceRls) {
* Force RLS scoping on the table, so that the table owner users DB::statement("ALTER TABLE {$table} FORCE ROW LEVEL SECURITY");
* don't bypass the scoping table owners bypass RLS by default. }
*
* E.g. when using a custom implementation where you create tables as the RLS user,
* the queries won't be scoped for the RLS user unless we force the RLS scoping using this query.
*/
DB::statement("ALTER TABLE {$table} FORCE ROW LEVEL SECURITY");
} }
/** /**

View file

@ -5,49 +5,11 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Contracts; namespace Stancl\Tenancy\Contracts;
use Exception; use Exception;
use Spatie\ErrorSolutions\Contracts\BaseSolution;
use Spatie\ErrorSolutions\Contracts\ProvidesSolution;
abstract class TenantCouldNotBeIdentifiedException extends Exception implements ProvidesSolution abstract class TenantCouldNotBeIdentifiedException extends Exception
{ {
/** Default solution title. */ protected function tenantCouldNotBeIdentified(string $how): void
protected string $solutionTitle = 'Tenant could not be identified';
/** Default solution description. */
protected string $solutionDescription = 'Are you sure this tenant exists?';
/** Set the message. */
protected function tenantCouldNotBeIdentified(string $how): static
{ {
$this->message = 'Tenant could not be identified ' . $how; $this->message = 'Tenant could not be identified ' . $how;
return $this;
}
/** Set the solution title. */
protected function title(string $solutionTitle): static
{
$this->solutionTitle = $solutionTitle;
return $this;
}
/** Set the solution description. */
protected function description(string $solutionDescription): static
{
$this->solutionDescription = $solutionDescription;
return $this;
}
/** Get the Ignition description. */
public function getSolution(): BaseSolution
{
return BaseSolution::create($this->solutionTitle)
->setSolutionDescription($this->solutionDescription)
->setDocumentationLinks([
'Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants',
'Tenant Identification' => 'https://tenancyforlaravel.com/docs/v3/tenant-identification',
]);
} }
} }

View file

@ -4,9 +4,12 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Enums; namespace Stancl\Tenancy\Enums;
enum RouteMode /**
* Note: The backing values are not part of the public API and are subject to change.
*/
enum RouteMode: int
{ {
case TENANT; case CENTRAL = 0b01;
case CENTRAL; case TENANT = 0b10;
case UNIVERSAL; case UNIVERSAL = 0b11;
} }

View file

@ -10,9 +10,6 @@ class TenantColumnNotWhitelistedException extends TenantCouldNotBeIdentifiedExce
{ {
public function __construct(int|string $tenant_id) public function __construct(int|string $tenant_id)
{ {
$this $this->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id (column not whitelisted)");
->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id (column not whitelisted)")
->title('Tenant could not be identified on this route because the used column is not whitelisted.')
->description('Please add the column to the list of allowed columns in the PathTenantResolver config.');
} }
} }

View file

@ -10,9 +10,6 @@ class TenantCouldNotBeIdentifiedByIdException extends TenantCouldNotBeIdentified
{ {
public function __construct(int|string $tenant_id) public function __construct(int|string $tenant_id)
{ {
$this $this->tenantCouldNotBeIdentified("by tenant key: $tenant_id");
->tenantCouldNotBeIdentified("by tenant key: $tenant_id")
->title('Tenant could not be identified with that key')
->description('Are you sure the key is correct and the tenant exists?');
} }
} }

View file

@ -10,9 +10,6 @@ class TenantCouldNotBeIdentifiedByPathException extends TenantCouldNotBeIdentifi
{ {
public function __construct(int|string $tenant_id) public function __construct(int|string $tenant_id)
{ {
$this $this->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id");
->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id")
->title('Tenant could not be identified on this path')
->description('Did you forget to create a tenant for this path?');
} }
} }

View file

@ -10,9 +10,6 @@ class TenantCouldNotBeIdentifiedByRequestDataException extends TenantCouldNotBeI
{ {
public function __construct(mixed $payload) public function __construct(mixed $payload)
{ {
$this $this->tenantCouldNotBeIdentified("by request data with payload: $payload");
->tenantCouldNotBeIdentified("by request data with payload: $payload")
->title('Tenant could not be identified using this request data')
->description('Did you forget to create a tenant with this id?');
} }
} }

View file

@ -10,9 +10,6 @@ class TenantCouldNotBeIdentifiedOnDomainException extends TenantCouldNotBeIdenti
{ {
public function __construct(string $domain) public function __construct(string $domain)
{ {
$this $this->tenantCouldNotBeIdentified("on domain $domain");
->tenantCouldNotBeIdentified("on domain $domain")
->title('Tenant could not be identified on this domain')
->description('Did you forget to create a tenant for this domain?');
} }
} }

View file

@ -30,6 +30,7 @@ if (! function_exists('tenant')) {
return app(Tenant::class); return app(Tenant::class);
} }
// @phpstan-ignore-next-line nullsafe.neverNull
return app(Tenant::class)?->getAttribute($key); return app(Tenant::class)?->getAttribute($key);
} }
} }

View file

@ -3,15 +3,29 @@
declare(strict_types=1); declare(strict_types=1);
use Illuminate\Contracts\Http\Kernel; use Illuminate\Contracts\Http\Kernel;
use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException; use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException;
use Stancl\Tenancy\Exceptions\TenantColumnNotWhitelistedException; use Stancl\Tenancy\Exceptions\TenantColumnNotWhitelistedException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Resolvers\PathTenantResolver;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Jobs\MigrateDatabase;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\User;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Events\TenancyEnded;
use function Stancl\Tenancy\Tests\pest; use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
@ -246,3 +260,38 @@ test('any extra model column needs to be whitelisted', function () {
config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.allowed_extra_model_columns' => ['slug']]); config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.allowed_extra_model_columns' => ['slug']]);
pest()->get('/acme/foo')->assertSee($tenant->getTenantKey()); pest()->get('/acme/foo')->assertSee($tenant->getTenantKey());
}); });
test('route model binding works with path identification', function() {
config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenantCreated::class, JobPipeline::make([
CreateDatabase::class, MigrateDatabase::class,
])->send(fn (TenantCreated $event) => $event->tenant)->toListener());
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
$tenant = Tenant::create();
$this->withoutExceptionHandling();
// Importantly, the route must have the 'web' middleware group, or SubstituteBindings directly
Route::get('/{tenant}/foo/{user}', fn (User $user) => $user->name)->middleware([InitializeTenancyByPath::class, 'web']);
Route::get('/{tenant}/bar/{user}', fn (User $user) => $user->name)->middleware([InitializeTenancyByPath::class, SubstituteBindings::class]);
$user = $tenant->run(fn () => User::create(['name' => 'John Doe', 'email' => 'john@doe.com', 'password' => 'foobar']));
pest()->get("/{$tenant->id}/foo/{$user->id}")->assertSee("John Doe");
tenancy()->end();
pest()->get("/{$tenant->id}/bar/{$user->id}")->assertSee("John Doe");
tenancy()->end();
// If SubstituteBindings comes BEFORE tenancy middleware and middleware priority is not set, route model binding is NOT expected to work correctly
// Since SubstituteBindings runs first, it tries to query the central database instead of the tenant database (which fails with a QueryException in this case)
Route::get('/{tenant}/baz/{user}', fn (User $user) => $user->name ?: 'No user')->middleware([SubstituteBindings::class, InitializeTenancyByPath::class]);
expect(fn () => pest()->get("/{$tenant->id}/baz/{$user->id}"))->toThrow(QueryException::class);
tenancy()->end();
// If SubstituteBindings is NOT USED AT ALL, we simply get an empty User instance
Route::get('/{tenant}/xyz/{user}', fn (User $user) => $user->name ?: 'No user')->middleware([InitializeTenancyByPath::class]);
pest()->get("/{$tenant->id}/xyz/{$user->id}")->assertSee('No user');
});

View file

@ -20,6 +20,7 @@ use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
use function Stancl\Tenancy\Tests\pest; use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
CreateUserWithRLSPolicies::$forceRls = true;
TraitRLSManager::$excludedModels = [Article::class]; TraitRLSManager::$excludedModels = [Article::class];
TraitRLSManager::$modelDirectories = [__DIR__ . '/Etc']; TraitRLSManager::$modelDirectories = [__DIR__ . '/Etc'];
@ -79,6 +80,10 @@ beforeEach(function () {
}); });
}); });
afterEach(function () {
CreateUserWithRLSPolicies::$forceRls = true;
});
// Regression test for https://github.com/archtechx/tenancy/pull/1280 // Regression test for https://github.com/archtechx/tenancy/pull/1280
test('rls command doesnt fail when a view is in the database', function (string $manager) { test('rls command doesnt fail when a view is in the database', function (string $manager) {
DB::statement(" DB::statement("
@ -184,7 +189,9 @@ test('rls command recreates policies if the force option is passed', function (s
TraitRLSManager::class, TraitRLSManager::class,
]); ]);
test('queries will stop working when the tenant session variable is not set', function(string $manager) { test('queries will stop working when the tenant session variable is not set', function(string $manager, bool $forceRls) {
CreateUserWithRLSPolicies::$forceRls = $forceRls;
config(['tenancy.rls.manager' => $manager]); config(['tenancy.rls.manager' => $manager]);
$sessionVariableName = config('tenancy.rls.session_variable_name'); $sessionVariableName = config('tenancy.rls.session_variable_name');
@ -216,7 +223,4 @@ test('queries will stop working when the tenant session variable is not set', fu
INSERT INTO posts (text, tenant_id, author_id) INSERT INTO posts (text, tenant_id, author_id)
VALUES ('post2', ?, ?) VALUES ('post2', ?, ?)
SQL, [$tenant->id, $authorId]))->toThrow(QueryException::class); SQL, [$tenant->id, $authorId]))->toThrow(QueryException::class);
})->with([ })->with([TableRLSManager::class, TraitRLSManager::class])->with([true, false]);
TableRLSManager::class,
TraitRLSManager::class,
]);

View file

@ -22,6 +22,7 @@ use Stancl\Tenancy\Database\Exceptions\RecursiveRelationshipException;
use function Stancl\Tenancy\Tests\pest; use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
CreateUserWithRLSPolicies::$forceRls = true;
TableRLSManager::$scopeByDefault = true; TableRLSManager::$scopeByDefault = true;
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
@ -107,6 +108,10 @@ beforeEach(function () {
}); });
}); });
afterEach(function () {
CreateUserWithRLSPolicies::$forceRls = true;
});
test('correct rls policies get created with the correct hash using table manager', function() { test('correct rls policies get created with the correct hash using table manager', function() {
$manager = app(config('tenancy.rls.manager')); $manager = app(config('tenancy.rls.manager'));
@ -159,7 +164,9 @@ test('correct rls policies get created with the correct hash using table manager
} }
}); });
test('queries are correctly scoped using RLS', function() { test('queries are correctly scoped using RLS', function (bool $forceRls) {
CreateUserWithRLSPolicies::$forceRls = $forceRls;
// 3-levels deep relationship // 3-levels deep relationship
Schema::create('notes', function (Blueprint $table) { Schema::create('notes', function (Blueprint $table) {
$table->id(); $table->id();
@ -320,7 +327,7 @@ test('queries are correctly scoped using RLS', function() {
expect(fn () => DB::statement("INSERT INTO notes (text, comment_id) VALUES ('baz', {$post1Comment->id})")) expect(fn () => DB::statement("INSERT INTO notes (text, comment_id) VALUES ('baz', {$post1Comment->id})"))
->toThrow(QueryException::class); ->toThrow(QueryException::class);
}); })->with([true, false]);
test('table rls manager generates relationship trees with tables related to the tenants table', function (bool $scopeByDefault) { test('table rls manager generates relationship trees with tables related to the tenants table', function (bool $scopeByDefault) {
TableRLSManager::$scopeByDefault = $scopeByDefault; TableRLSManager::$scopeByDefault = $scopeByDefault;
@ -535,6 +542,109 @@ test('table rls manager generates relationship trees with tables related to the
]); ]);
})->with([true, false]); })->with([true, false]);
// https://github.com/archtechx/tenancy/pull/1293
test('forceRls prevents even the table owner from querying his own tables if he doesnt have a BYPASSRLS permission', function (bool $forceRls) {
CreateUserWithRLSPolicies::$forceRls = $forceRls;
// Drop all tables created in beforeEach
DB::statement("DROP TABLE authors, categories, posts, comments, reactions, articles;");
// Create a new user so we have full control over the permissions.
// We explicitly set bypassRls to false.
[$username, $password] = createPostgresUser('administrator', bypassRls: false);
config(['database.connections.central' => array_merge(config('database.connections.pgsql'), [
'username' => $username,
'password' => $password,
])]);
DB::reconnect();
// This table is owned by the newly created 'administrator' user
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('tenant_id')->comment('rls');
$table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
$table->timestamps();
});
$tenant1 = Tenant::create();
// Create RLS policy for the orders table
pest()->artisan('tenants:rls');
$tenant1->run(fn () => Order::create(['name' => 'order1', 'tenant_id' => $tenant1->getTenantKey()]));
// We are still using the 'administrator' user - owner of the orders table
if ($forceRls) {
// RLS is forced, so by default, not even the table owner should be able to query the table protected by the RLS policy.
// The RLS policy is not being bypassed, 'unrecognized configuration parameter' means
// that the my.current_tenant session variable isn't set -- the RLS policy is *still* being enforced.
expect(fn () => Order::first())->toThrow(QueryException::class, 'unrecognized configuration parameter "my.current_tenant"');
} else {
// RLS is not forced, so the table owner should be able to query the table, bypassing the RLS policy
expect(Order::first())->not()->toBeNull();
}
})->with([true, false]);
test('users with BYPASSRLS privilege can bypass RLS regardless of forceRls setting', function (bool $forceRls, bool $bypassRls) {
CreateUserWithRLSPolicies::$forceRls = $forceRls;
// Drop all tables created in beforeEach
DB::statement("DROP TABLE authors, categories, posts, comments, reactions, articles;");
// Create a new user so we have control over his BYPASSRLS permission
// and use that as the new central connection user
[$username, $password] = createPostgresUser('administrator', 'password', $bypassRls);
config(['database.connections.central' => array_merge(config('database.connections.pgsql'), [
'username' => $username,
'password' => $password,
])]);
DB::reconnect();
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('tenant_id')->comment('rls');
$table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
$table->timestamps();
});
$tenant1 = Tenant::create();
// Create RLS policy for the orders table
pest()->artisan('tenants:rls');
$tenant1->run(fn () => Order::create(['name' => 'order1', 'tenant_id' => $tenant1->getTenantKey()]));
// We are still using the 'administrator' user
if ($bypassRls) {
// Users with BYPASSRLS can always query tables regardless of forceRls setting
expect(Order::count())->toBe(1);
expect(Order::first()->name)->toBe('order1');
} else {
// Users without BYPASSRLS are subject to RLS policies even if they're table owners when forceRls is true
// OR they can bypass as table owners (when forceRls=false)
if ($forceRls) {
// Even table owners need session variable -- this means RLS was NOT bypassed
expect(fn () => Order::first())->toThrow(QueryException::class, 'unrecognized configuration parameter "my.current_tenant"');
} else {
// Table owners can bypass RLS automatically when forceRls is false
expect(Order::count())->toBe(1);
expect(Order::first()->name)->toBe('order1');
}
}
})->with([true, false])->with([true, false]);
test('table rls manager generates queries correctly', function() { test('table rls manager generates queries correctly', function() {
expect(array_values(app(TableRLSManager::class)->generateQueries()))->toEqualCanonicalizing([ expect(array_values(app(TableRLSManager::class)->generateQueries()))->toEqualCanonicalizing([
<<<SQL <<<SQL
@ -663,6 +773,33 @@ test('table manager ignores recursive relationship if the foreign key responsibl
expect(fn () => app(TableRLSManager::class)->generateTrees())->not()->toThrow(RecursiveRelationshipException::class); expect(fn () => app(TableRLSManager::class)->generateTrees())->not()->toThrow(RecursiveRelationshipException::class);
}); });
function createPostgresUser(string $username, string $password = 'password', bool $bypassRls = false): array
{
try {
DB::statement("DROP OWNED BY {$username};");
} catch (\Throwable) {}
DB::statement("DROP USER IF EXISTS {$username};");
DB::statement("CREATE USER {$username} WITH ENCRYPTED PASSWORD '{$password}'");
DB::statement("ALTER USER {$username} CREATEDB");
DB::statement("ALTER USER {$username} CREATEROLE");
// Grant BYPASSRLS privilege if requested
if ($bypassRls) {
DB::statement("ALTER USER {$username} BYPASSRLS");
}
// Grant privileges to the new central user
DB::statement("GRANT ALL PRIVILEGES ON DATABASE main to {$username}");
DB::statement("GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO {$username}");
DB::statement("GRANT ALL ON SCHEMA public TO {$username}");
DB::statement("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO {$username}");
DB::statement("GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO {$username}");
return [$username, $password];
}
class Post extends Model class Post extends Model
{ {
protected $guarded = []; protected $guarded = [];
@ -715,3 +852,8 @@ class Author extends Model
{ {
protected $guarded = []; protected $guarded = [];
} }
class Order extends Model
{
protected $guarded = [];
}

View file

@ -28,6 +28,7 @@ use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel;
use function Stancl\Tenancy\Tests\pest; use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
CreateUserWithRLSPolicies::$forceRls = true;
TraitRLSManager::$implicitRLS = true; TraitRLSManager::$implicitRLS = true;
TraitRLSManager::$modelDirectories = [__DIR__ . '/Etc']; TraitRLSManager::$modelDirectories = [__DIR__ . '/Etc'];
TraitRLSManager::$excludedModels = [Article::class]; TraitRLSManager::$excludedModels = [Article::class];
@ -78,6 +79,10 @@ beforeEach(function () {
}); });
}); });
afterEach(function () {
CreateUserWithRLSPolicies::$forceRls = true;
});
test('correct rls policies get created with the correct hash using trait manager', function () { test('correct rls policies get created with the correct hash using trait manager', function () {
$manager = app(TraitRLSManager::class); $manager = app(TraitRLSManager::class);
@ -149,7 +154,8 @@ test('global scope is not applied when using rls with single db traits', functio
expect(NonRLSComment::make()->hasGlobalScope(ParentModelScope::class))->toBeFalse(); expect(NonRLSComment::make()->hasGlobalScope(ParentModelScope::class))->toBeFalse();
}); });
test('queries are correctly scoped using RLS with trait rls manager', function (bool $implicitRLS) { test('queries are correctly scoped using RLS with trait rls manager', function (bool $implicitRLS, bool $forceRls) {
CreateUserWithRLSPolicies::$forceRls = $forceRls;
TraitRLSManager::$implicitRLS = $implicitRLS; TraitRLSManager::$implicitRLS = $implicitRLS;
$postModel = $implicitRLS ? NonRLSPost::class : Post::class; $postModel = $implicitRLS ? NonRLSPost::class : Post::class;
@ -263,10 +269,7 @@ test('queries are correctly scoped using RLS with trait rls manager', function (
expect(fn () => DB::statement("INSERT INTO comments (text, post_id) VALUES ('third comment', {$post1->id})")) expect(fn () => DB::statement("INSERT INTO comments (text, post_id) VALUES ('third comment', {$post1->id})"))
->toThrow(QueryException::class); ->toThrow(QueryException::class);
})->with([ })->with([true, false])->with([true, false]);
true,
false
]);
test('trait rls manager generates queries correctly', function() { test('trait rls manager generates queries correctly', function() {
/** @var TraitRLSManager $manager */ /** @var TraitRLSManager $manager */