diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 8964044..b4e1ef2 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,39 +1,37 @@ security: - # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords - password_hashers: - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + # No password_hashers: there are no passwords here. Authentication is OAuth2 against myep + # (plan step 6); until that lands there is no way to log in outside dev. - # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: - users_in_memory: { memory: null } + app_user_provider: + entity: + class: App\Entity\User + property: email firewalls: dev: - # Ensure dev tools and static assets are always allowed - pattern: ^/(_profiler|_wdt|assets|build)/ + pattern: ^/(_profiler|_wdt|assets)/ security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider + logout: + path: app_logout - # Activate different ways to authenticate: - # https://symfony.com/doc/current/security.html#the-firewall - - # https://symfony.com/doc/current/security/impersonating_user.html - # switch_user: true - - # Note: Only the *first* matching rule is applied access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + # ROLE_EMPLOYEE is the gate, not ROLE_USER: myep authenticates more people than are staff, + # and only staff may book a desk here. + - { path: ^/dev/, roles: PUBLIC_ACCESS } + - { path: ^/, roles: ROLE_EMPLOYEE } -when@test: +when@dev: security: - password_hashers: - # Password hashers are resource-intensive by design to ensure security. - # In tests, it's safe to reduce their cost to improve performance. - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: - algorithm: auto - cost: 4 # Lowest possible value for bcrypt - time_cost: 3 # Lowest possible value for argon - memory_cost: 10 # Lowest possible value for argon + firewalls: + main: + # Real login is blocked on OAuth2 client registration on myep. Until then + # /dev/login lists the seeded users and logs one in without credentials. + # The entry point, the route and the controller are all dev-only, so this + # cannot leak into another environment by accident. + entry_point: App\Security\DevLoginEntryPoint + custom_authenticators: + - App\Security\DevLoginAuthenticator diff --git a/config/routes/dev_login.yaml b/config/routes/dev_login.yaml new file mode 100644 index 0000000..5609d2c --- /dev/null +++ b/config/routes/dev_login.yaml @@ -0,0 +1,7 @@ +# Dev-only stand-in for the OAuth2 login. The controller carries no #[Route] attribute, so +# outside dev this route simply does not exist. +when@dev: + app_dev_login: + path: /dev/login + controller: App\Controller\DevLoginController::login + methods: [GET, POST] diff --git a/src/Command/SeedCommand.php b/src/Command/SeedCommand.php new file mode 100644 index 0000000..d75ad9b --- /dev/null +++ b/src/Command/SeedCommand.php @@ -0,0 +1,96 @@ +ensureResource(ResourceType::DESK, \sprintf('Schreibtisch %02d', $i), \sprintf('desk-%02d', $i)); + } + + for ($i = 1; $i <= 6; ++$i) { + $created += $this->ensureResource(ResourceType::PARKING, \sprintf('Parkplatz %02d', $i), \sprintf('parking-%02d', $i)); + } + + foreach ([ + ['anna.berger@example.test', 'Anna Berger'], + ['ben.kuhn@example.test', 'Ben Kuhn'], + ['clara.dietz@example.test', 'Clara Dietz'], + ['david.roth@example.test', 'David Roth'], + ['elif.yilmaz@example.test', 'Elif Yılmaz'], + ] as [$email, $name]) { + $created += $this->ensureUser($email, $name); + } + + $this->em->flush(); + + $io->success(\sprintf('%d neue Datensätze angelegt.', $created)); + + return Command::SUCCESS; + } + + private function ensureResource(ResourceType $type, string $label, string $svgElementId): int + { + if (null !== $this->resources->findOneBy(['type' => $type, 'label' => $label])) { + return 0; + } + + $this->em->persist(new Resource($type, $label, $svgElementId)); + + return 1; + } + + private function ensureUser(string $email, string $name): int + { + if (null !== $this->users->findOneBy(['email' => $email])) { + return 0; + } + + $user = new User($email, $name); + // Mirrors what the OAuth2 authenticator will do: the remote roles claim is filtered + // through a local whitelist, and ROLE_EMPLOYEE is what the access_control gate wants. + $user->setRoles(['ROLE_EMPLOYEE']); + $this->em->persist($user); + + return 1; + } +} diff --git a/src/Controller/DevLoginController.php b/src/Controller/DevLoginController.php new file mode 100644 index 0000000..4151562 --- /dev/null +++ b/src/Controller/DevLoginController.php @@ -0,0 +1,38 @@ +getUser()) { + return $this->redirectToRoute('app_dashboard'); + } + + // POST never reaches here -- DevLoginAuthenticator intercepts it on the firewall. + return $this->render('dev/login.html.twig', [ + 'users' => $this->users->findBy([], ['name' => 'ASC']), + ]); + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..ae54aab --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,23 @@ +attributes->get('_route') && $request->isMethod('POST'); + } + + public function authenticate(Request $request): Passport + { + $id = $request->request->getInt('user'); + + return new SelfValidatingPassport(new UserBadge((string) $id, function (string $identifier) { + $user = $this->users->find((int) $identifier); + + if (null === $user) { + throw new CustomUserMessageAuthenticationException('Unbekannter Benutzer.'); + } + + return $user; + })); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return new RedirectResponse($this->urlGenerator->generate('app_dashboard')); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $request->getSession()->getFlashBag()->add('error', $exception->getMessageKey()); + + return new RedirectResponse($this->urlGenerator->generate('app_dev_login')); + } +} diff --git a/src/Security/DevLoginEntryPoint.php b/src/Security/DevLoginEntryPoint.php new file mode 100644 index 0000000..f4d1169 --- /dev/null +++ b/src/Security/DevLoginEntryPoint.php @@ -0,0 +1,33 @@ +urlGenerator->generate('app_dev_login')); + } +} diff --git a/templates/dev/login.html.twig b/templates/dev/login.html.twig new file mode 100644 index 0000000..39a05d0 --- /dev/null +++ b/templates/dev/login.html.twig @@ -0,0 +1,36 @@ +{% extends 'base.html.twig' %} + +{% block title %}Anmelden · ephub{% endblock %} + +{% block body %} +
+
+ ephub + +
+ Nur für die Entwicklung. + Die echte Anmeldung läuft über myep (OAuth2) und wartet auf die Client-Registrierung dort. +
+ + {% if users is empty %} +

+ Keine Benutzer vorhanden. bin/console app:seed ausführen. +

+ {% else %} +
+ {% for user in users %} + + {% endfor %} +
+ {% endif %} + +
+ {% include '_partials/_flashes.html.twig' %} +
+
+
+{% endblock %}