Cache issue with spatie/laravel-permission in TenantAware Command
Closed this issue · 5 comments
Hello,
I ran into an issue when messing with TenantAware Commands. I am not sure whether this is an actual bug, or if it is related to this package or related to spatie/laravel-permission
My code was the following :
class GivePermissionsCommand extends Command
{
use TenantAware;
protected $signature = 'give:permissions {role : Slug name of the role to update} {permissions : Comma separated list of permissions} {--revoke : Adding this option will revoke the permission from the role instead} {--tenant=*}';
protected $description = 'Give/Revoke permissions from a given role';
public function handle(): int
{
$tenant = Tenant::current();
$this->line('');
$this->info("Running command for tenant `{$tenant->name}` (id: {$tenant->getKey()})...");
$this->line('---------------------------------------------------------');
$role = Role::findByName($this->argument('role'));
$permissions = explode(',', $this->argument('permissions'));
if ($this->option('revoke')) {
$role->revokePermissionTo($permissions);
} else {
$role->givePermissionTo($permissions);
}
return Command::SUCCESS;
}
}
After running the command, I refresh my website, and I still don't have my permission. However, the permission exists in the database. Leading me to an obvious cache issue.
As a workaround and after digging through the code base, I added the following to my code :
// Force cache for Tenant.
app()->make(PermissionRegistrar::class)->initializeCache();
The final result being :
class GivePermissionsCommand extends Command
{
use TenantAware;
protected $signature = 'give:permissions {role : Slug name of the role to update} {permissions : Comma separated list of permissions} {--revoke : Adding this option will revoke the permission from the role instead} {--tenant=*}';
protected $description = 'Give/Revoke permissions from a given role';
public function handle(): int
{
$tenant = Tenant::current();
$this->line('');
$this->info("Running command for tenant `{$tenant->name}` (id: {$tenant->getKey()})...");
$this->line('---------------------------------------------------------');
// Force cache for Tenant.
app()->make(PermissionRegistrar::class)->initializeCache();
$role = Role::findByName($this->argument('role'));
$permissions = explode(',', $this->argument('permissions'));
if ($this->option('revoke')) {
$role->revokePermissionTo($permissions);
} else {
$role->givePermissionTo($permissions);
}
return Command::SUCCESS;
}
}
Hi @Kryptonien. Please post your migration here.
Thanks
Hey @masterix21
Here's what I have:
// migrations/landlord/2023_06_30_210521_create_landlord_tenants_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateLandlordTenantsTable extends Migration
{
public function up(): void
{
Schema::create('tenants', static function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('domain')->unique();
$table->string('database')->unique();
$table->timestamps();
});
}
}
// migrations/2022_12_07_013735_create_permission_tables.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Spatie\Permission\PermissionRegistrar;
class CreatePermissionTables extends Migration
{
public function up()
{
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$teams = config('permission.teams');
if (empty($tableNames)) {
throw new Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
throw new Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
Schema::create($tableNames['permissions'], function (Blueprint $table) {
$table->bigIncrements('id'); // permission id
$table->string('name', 125); // For MySQL 8.0 use string('name', 125);
$table->string('guard_name', 125); // For MySQL 8.0 use string('guard_name', 125);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('display_name', 125);
$table->string('name', 125); // For MySQL 8.0 use string('name', 125);
$table->string('guard_name', 125); // For MySQL 8.0 use string('guard_name', 125);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) {
$table->unsignedBigInteger(PermissionRegistrar::$pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign(PermissionRegistrar::$pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) {
$table->unsignedBigInteger(PermissionRegistrar::$pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign(PermissionRegistrar::$pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
} else {
$table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) {
$table->unsignedBigInteger(PermissionRegistrar::$pivotPermission);
$table->unsignedBigInteger(PermissionRegistrar::$pivotRole);
$table->foreign(PermissionRegistrar::$pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign(PermissionRegistrar::$pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([PermissionRegistrar::$pivotPermission, PermissionRegistrar::$pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') !== 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
public function down()
{
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
}
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
}
Try with it:
<?php
use Spatie\Multitenancy\Models\Tenant;
use Spatie\Multitenancy\Tasks\SwitchTenantTask;
use Spatie\Permission\PermissionRegistrar;
class SwitchSpatiePermissionsTask implements SwitchTenantTask
{
protected ?string $originalSpatiePermissionCacheKey;
public function makeCurrent(Tenant $tenant): void
{
$this->originalSpatiePermissionCacheKey = PermissionRegistrar::$cacheKey;
$this->setup($tenant);
}
private function setup(Tenant $tenant)
{
PermissionRegistrar::$cacheKey .= '.'.$tenant->id;
}
public function forgetCurrent(): void
{
PermissionRegistrar::$cacheKey = $this->originalSpatiePermissionCacheKey;
}
}
This won't work as I am relying on PrefixCacheTask
provided by this package. The root problem comes from the fact that even after switching cache prefix, it is somehow ignored in console.
Here's a rough idea of the workflow (imo),
- From a browser
- Accessing my website (hello.example.com)
- DomainTenantFinder.php will switch to Tenant hello
- Tasks are executed, new prefix cache is set by
PrefixCacheTask.php
- spatie/laravel-permission is initialised by the Framework (set the cacheManager)
- Render the website, permissions are loaded properly, no issue here
- From a console
- Running artisan command
php artisan give:permission admin 'edit file'
- spatie/laravel-permission is initialised by the Framework (set the cacheManager)
GivePermissionsCommand.php
will switch to Tenant hello usingTenantAware.php
- Tasks are executed, new prefix cache is set by
PrefixCacheTask.php
(but laravel-permission cacheManager is not updated) handle
method is executed by the Framework
- Running artisan command
Conclusion, spatie/laravel-permission is initialised before PrefixCacheTask
is being executed while in console. Changing $cacheKey
won't fix the problem.
However, based on your suggestion, I could override PrefixCacheTask.php
and do the following
protected function setCachePrefix(string $prefix)
{
config()->set('cache.prefix', $prefix);
app('cache')->forgetDriver($this->storeName);
// This is important because the `CacheManager` will have the `$app['config']` array cached
// with old prefixes on the `cache` instance. Simply calling `forgetDriver` only removes
// the `$store` but doesn't update the `$app['config']`.
app()->forgetInstance('cache');
//This is important because the Cache Repository is using an old version of the CacheManager
app()->forgetInstance('cache.store');
// Forget the cache repository in the container
app()->forgetInstance(Repository::class);
// Force cache for Tenant.
app()->make(PermissionRegistrar::class)->initializeCache();
Cache::clearResolvedInstances();
}
This might work.
Thanks for your share.