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)
|
// ->use(RefreshDatabase::class)
|
||||||
->in('Feature');
|
->in('Feature');
|
||||||
|
|
||||||
|
pest()->extend(TestCase::class)
|
||||||
|
->in('Unit');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Expectations
|
| 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