From af49f091f50739b77fc22b271e5b24b845c4c0df Mon Sep 17 00:00:00 2001 From: Boban Blaskovic Date: Fri, 22 May 2026 00:53:10 +0200 Subject: [PATCH] feat(tenant): HasUuid + TenantScope (null-user default-deny) + BelongsToTenant Co-Authored-By: Claude Sonnet 4.6 --- app/Scopes/TenantScope.php | 28 ++++++++++++++++++++++++++++ app/Traits/BelongsToTenant.php | 21 +++++++++++++++++++++ app/Traits/HasUuid.php | 20 ++++++++++++++++++++ tests/Pest.php | 3 +++ tests/Unit/Models/HasUuidTest.php | 16 ++++++++++++++++ 5 files changed, 88 insertions(+) create mode 100644 app/Scopes/TenantScope.php create mode 100644 app/Traits/BelongsToTenant.php create mode 100644 app/Traits/HasUuid.php create mode 100644 tests/Unit/Models/HasUuidTest.php diff --git a/app/Scopes/TenantScope.php b/app/Scopes/TenantScope.php new file mode 100644 index 0000000..4c7e15d --- /dev/null +++ b/app/Scopes/TenantScope.php @@ -0,0 +1,28 @@ +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); + } +} diff --git a/app/Traits/BelongsToTenant.php b/app/Traits/BelongsToTenant.php new file mode 100644 index 0000000..aca57d7 --- /dev/null +++ b/app/Traits/BelongsToTenant.php @@ -0,0 +1,21 @@ +user(); + if ($user && !$user->hasRole('platform-admin')) { + // Force tenant_id — prevents cross-tenant mass-assign attacks + $model->tenant_id = $user->tenant_id; + } + }); + } +} diff --git a/app/Traits/HasUuid.php b/app/Traits/HasUuid.php new file mode 100644 index 0000000..a6a1942 --- /dev/null +++ b/app/Traits/HasUuid.php @@ -0,0 +1,20 @@ +uuid ??= (string) Str::uuid(); + }); + } + + public function getRouteKeyName(): string + { + return 'uuid'; + } +} diff --git a/tests/Pest.php b/tests/Pest.php index b9fb6f7..f6eb1db 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -22,6 +22,9 @@ pest()->extend(TestCase::class) // ->use(RefreshDatabase::class) ->in('Feature'); +pest()->extend(TestCase::class) + ->in('Unit'); + /* |-------------------------------------------------------------------------- | Expectations diff --git a/tests/Unit/Models/HasUuidTest.php b/tests/Unit/Models/HasUuidTest.php new file mode 100644 index 0000000..33b49dc --- /dev/null +++ b/tests/Unit/Models/HasUuidTest.php @@ -0,0 +1,16 @@ +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'); +});