From bcd8ba969ab526132fd6530e13b2d54b55ae65a9 Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Tue, 9 Jun 2026 14:40:20 +0200 Subject: [PATCH] White-Label Phase 2: Login-Scoping je Host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TenantAccessPolicy: Plattform-Host nur Plattform-/Reseller-Admins, Reseller-Host nur dessen Nutzer, Firmen-Host nur Firmen-Nutzer (+ zugehöriger Reseller-Admin); Plattform-Admin überall. - LoginSuccessHandler prüft vor JWT-Ausstellung → 403 bei falschem Host. - Login zeigt die 403-Hinweismeldung. Co-Authored-By: Claude Opus 4.8 --- backend/config/packages/security.yaml | 2 +- backend/src/Security/LoginSuccessHandler.php | 45 ++++++++++++++++ backend/src/Security/TenantAccessPolicy.php | 56 ++++++++++++++++++++ frontend/src/views/LoginView.vue | 7 ++- 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 backend/src/Security/LoginSuccessHandler.php create mode 100644 backend/src/Security/TenantAccessPolicy.php diff --git a/backend/config/packages/security.yaml b/backend/config/packages/security.yaml index 056c8ac..4f5a8a6 100644 --- a/backend/config/packages/security.yaml +++ b/backend/config/packages/security.yaml @@ -21,7 +21,7 @@ security: check_path: /api/login username_path: email password_path: password - success_handler: lexik_jwt_authentication.handler.authentication_success + success_handler: App\Security\LoginSuccessHandler failure_handler: lexik_jwt_authentication.handler.authentication_failure # Geschützte API: JWT im Authorization-Header diff --git a/backend/src/Security/LoginSuccessHandler.php b/backend/src/Security/LoginSuccessHandler.php new file mode 100644 index 0000000..a152e45 --- /dev/null +++ b/backend/src/Security/LoginSuccessHandler.php @@ -0,0 +1,45 @@ +getUser(); + if ($user instanceof Employee) { + $tenant = $this->resolver->resolve($request->getHost()) ?? ResolvedTenant::platform(); + if (!$this->policy->canLogin($user, $tenant)) { + return new JsonResponse( + ['message' => 'Für diese Adresse besteht kein Zugang. Bitte die richtige Login-Adresse verwenden.'], + Response::HTTP_FORBIDDEN, + ); + } + } + + return $this->inner->onAuthenticationSuccess($request, $token); + } +} diff --git a/backend/src/Security/TenantAccessPolicy.php b/backend/src/Security/TenantAccessPolicy.php new file mode 100644 index 0000000..9fa059d --- /dev/null +++ b/backend/src/Security/TenantAccessPolicy.php @@ -0,0 +1,56 @@ +getRoles(); + $isPlatform = \in_array(Employee::ROLE_PLATFORM_ADMIN, $roles, true); + $isReseller = \in_array(Employee::ROLE_RESELLER_ADMIN, $roles, true); + + if ($isPlatform) { + return true; + } + + $userReseller = $user->getReseller(); + $userCompany = $user->getCompany(); + + return match ($tenant->kind) { + ResolvedTenant::KIND_PLATFORM => $isReseller, + ResolvedTenant::KIND_RESELLER => null !== $userReseller + && null !== $tenant->reseller + && $userReseller->getId()->equals($tenant->reseller->getId()), + ResolvedTenant::KIND_COMPANY => $this->canLoginCompany($userCompany, $userReseller, $isReseller, $tenant), + default => false, + }; + } + + private function canLoginCompany( + ?\App\Entity\Company $userCompany, + ?\App\Entity\Reseller $userReseller, + bool $isReseller, + ResolvedTenant $tenant, + ): bool { + if (null !== $userCompany && null !== $tenant->company + && $userCompany->getId()->equals($tenant->company->getId())) { + return true; + } + + // Reseller-Admin des zugehörigen Resellers darf den Firmen-Host ebenfalls nutzen + return $isReseller && null !== $userReseller && null !== $tenant->reseller + && $userReseller->getId()->equals($tenant->reseller->getId()); + } +} diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 0c96432..a53fd7d 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -20,8 +20,11 @@ async function submit() { try { await auth.login(email.value, password.value) router.push((route.query.redirect as string) ?? '/app') - } catch { - error.value = 'Anmeldung fehlgeschlagen. Bitte E-Mail und Passwort prüfen.' + } catch (e: unknown) { + const err = e as { response?: { status?: number; data?: { message?: string } } } + error.value = err.response?.status === 403 && err.response.data?.message + ? err.response.data.message + : 'Anmeldung fehlgeschlagen. Bitte E-Mail und Passwort prüfen.' } finally { loading.value = false }