feat(tenant): HasUuid + TenantScope (null-user default-deny) + BelongsToTenant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
4e428cc58e
commit
af49f091f5
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Scopes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Scope;
|
||||
|
||||
class TenantScope implements Scope
|
||||
{
|
||||
public function apply(Builder $builder, Model $model): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user === null) {
|
||||
// Default-deny: null-user must NEVER silently see all records
|
||||
// (prevents leaks in queue jobs, Tinker, etc.)
|
||||
$builder->whereRaw('0 = 1');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->hasRole('platform-admin')) {
|
||||
return; // platform-admin sees all tenants
|
||||
}
|
||||
|
||||
$builder->where($model->getTable() . '.tenant_id', $user->tenant_id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Scopes\TenantScope;
|
||||
|
||||
trait BelongsToTenant
|
||||
{
|
||||
protected static function bootBelongsToTenant(): void
|
||||
{
|
||||
static::addGlobalScope(new TenantScope());
|
||||
|
||||
static::creating(function ($model) {
|
||||
$user = auth()->user();
|
||||
if ($user && !$user->hasRole('platform-admin')) {
|
||||
// Force tenant_id — prevents cross-tenant mass-assign attacks
|
||||
$model->tenant_id = $user->tenant_id;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait HasUuid
|
||||
{
|
||||
protected static function bootHasUuid(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
$model->uuid ??= (string) Str::uuid();
|
||||
});
|
||||
}
|
||||
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'uuid';
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@ pest()->extend(TestCase::class)
|
|||
// ->use(RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
|
||||
pest()->extend(TestCase::class)
|
||||
->in('Unit');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('generiert UUID beim Erstellen eines Users', function () {
|
||||
$user = User::factory()->create();
|
||||
expect($user->uuid)->toMatch('/^[0-9a-f-]{36}$/');
|
||||
});
|
||||
|
||||
it('nutzt UUID als Route-Key', function () {
|
||||
$user = User::factory()->create();
|
||||
expect($user->getRouteKeyName())->toBe('uuid');
|
||||
});
|
||||
Loading…
Reference in New Issue