Nasz system umożliwia już tworzenie konta, oraz logowanie do systemu. Teraz pora, aby nadać odpowiednie uprawnienia użytkownikom. Trzeci artykuł odnośnie systemu uwierzytelniania w Symfony, poświęcony będzie całkowicie access control.
W porównaniu do poprzednich części, tym razem jesteśmy w pełni gotowi do działania. Nie będzie potrzebne instalowanie żadnych dodatkowych vendorów. Od razu możemy zacząć pracę nad systemem dostępu.
Założenia początkowe
Musimy ustalić, jaki będzie system ról w naszej aplikacji. Na potrzeby tego artykułu, stworzymy 3 role. Pierwszą z nich, będzie każdy zarejestrowany użytkownik. Nazwa roli to ROLE_USER. Kolejnym poziomem uprawnień w naszej aplikacji będzie ROLE_ADMIN, która będzie miała te same uprawnienia, co ROLE_USER plus swoje dodatkowe. Ostatnim, najwyższym, poziomem będzie ROLE_SUPERADMIN. Jak łatwo się domyślić, będzie to użytkownik z uprawnieniami jeszcze wyższymi, niż te, które posiadają admini.
Rozszerzenie encji User
Pierwszym krokiem będzie dodanie systemu ról do naszej encji użytkownika. Otwieramy plik src/Entity/User.php i dodajemy następujący fragment kodu:
class User implements UserInterface { /**...*/ /** * @ORM\Column(type="array") */ private $roles; public function getRoles() { $roles = $this->roles; $roles[] = 'ROLE_USER'; return array_unique($roles); } public function setRoles(array $roles): void { $this->roles = $roles; } public function addRole(string $role) { if (false === in_array($role, $this->roles)) { $this->roles[] = $role; } } }
Jak widać dodajemy do naszej encji nowe pole o nazwie “roles“, które jest tablicą. Oczywiście bazy danych nie mają pola tego typu – jest to zwykłe pole typu LONGTEXT, które jest w odpowiedni sposób zserializowane i automatycznie podczas pobierania danych z bazy formatowane do tablicy.
Kolejnymi elementami są metody do obsługi ról. W tym przypadku prosty setter, getter, który automatycznie dodaje rolę podstawową (ROLE_USER) oraz metoda addRole, której zadaniem jest dodanie nowego elementu do tablicy. Tak przygotowaną encję aktualizujemy w naszej bazie przy pomocy polecenia:
bin/console doctrine:schema:update --force
Od teraz nasza encja posiada pole roles, w którym przechowywane będą odpowiednie uprawnienia użytkownika.
Dodanie użytkowników z uprawnieniami
Gdy mamy przygotowaną encję użytkownika, przyszedł czas na dodanie userów z odpowiednimi uprawnieniami. Moglibyśmy użyć systemu rejestracji, ale nie mamy tam możliwości dodania odpowiednich ról. Zamiast rozszerzać nasz formularz, dobrym wyjściem będzie dodanie odpowiednich fixtures. Otwieramy nasz plik z src/DataFixtures/UserFixtures.php i zmieniamy metodę load() w następujący sposób:
public function load(ObjectManager $manager): void { $users = [ [ 'username' => 'user', 'email' => 'user@test.com', 'roles' => ['ROLE_USER'] ], [ 'username' => 'admin', 'email' => 'admin@test.com', 'roles' => ['ROLE_ADMIN'] ], [ 'username' => 'superadmin', 'email' => 'superadmin@test.com', 'roles' => ['ROLE_SUPERADMIN'] ] ]; foreach ($users as $userToCreate) { $user = new User($userToCreate['username'], $userToCreate['email']); $password = $this->encoder->encodePassword($user, 'secret'); $user->setPassword($password); $user->setRoles($userToCreate['roles']); $this->entityManager->persist($user); } $this->entityManager->flush(); }
W porównaniu do pierwotnej wersji, widzimy tutaj 2 zmiany. Pierwszą z nich jest tablica użytkowników, którzy mają określoną nazwę, mail oraz role w formie tablicy. Iterując po liście użytkowników, dodajemy wywołanie metody setRoles, która ustawia odpowiednie uprawnienia.
Wywołujemy nasz data fixtures (UWAGA! Wywołanie tego polecenia, spowoduje wyczyszczenie tabeli USER przez co wszelkie dane, które tam wprowadziliście przepadną).
bin/console doctrine:fixtures:load
Teraz sprawdzamy, czy dane zostały wprowadzone poprawnie. Próbujemy logować się na każdego z 3 użytkowników po kolei.
Ustawienie listy dostępów
Gdy mamy już wszystkich potrzebnych użytkowników, czas na zdefiniowanie, które obszary naszego systemu będą miały restrykcję dostępu. Przechodzimy do ustawień security (config/packages/security.yaml) i ustawiamy dwa elementy. Pierwszym z nich, będzie role_hierarchy:
role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPERADMIN: ROLE_ADMIN
Jest to nic innego, jak hierarchia ról. Jak łatwo można się domyślić, ustawienia pokazują, jaka rola, dziedziczy z innej. W tym przypadku widzimy, że ROLE_ADMIN posiada te same uprawnienia co ROLE_USER, a ROLE_SUPERADMIN dziedziczy uprawnienia od ROLE_ADMIN (i tym samym od ROLE_USER). Przy bardziej zaawansowanych systemach, możemy dziedziczyć po więcej niż jednej roli. Zamiast ROLE_USER, moglibyśmy przekazać listę. Np. ROLE_ADMIN: [ROLE_USER, ROLE_PARTNER, ROLE_WRITER]
Drugim elementem, jaki należy skonfigurować, jest lista access_control. To nic innego jak zbiór wszystkich reguł dostępu, jakimi kieruje się nasza aplikacja. Poza systemem ról, możemy także zdefiniować takie parametry jak chociażby IP, czy metoda (GET/POST) jakimi mamy dostęp do poszczególnych adresów. Nas na chwilę obecną interesują dwa najważniejsze parametry path i roles. Pierwsza określa ścieżkę dostępu, a druga role, jakie mają do niej dostęp. Stwórzmy ścieżki dla poszczególnych ról. /superadmin dla ROLE_SUPERADMIN, /admin dla ROLE_ADMIN oraz /profile dla ROLE_USER.
access_control: - { path: ^/superadmin/, roles: ROLE_SUPERADMIN} - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/profile, roles: ROLE_USER }
Tak wyglądałaby cała sekcja.
Kontrolery i akcje
Mamy już wszystkie potrzebne ustawienia. Teraz czas stworzyć miejsca docelowe, czyli kontrolery, które obsłużą nasze żądania.
Pierwszym z nich będzie ProfileController.php w katalogu src/Controller
<?php declare(strict_types=1); namespace App\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; class ProfileController extends Controller { /** * @Route("/profile", name="app.profile") * @return Response */ public function profile(): Response { return $this->render('User/profile.html.twig', []); } }
Do tego kontrolera dodajemy widok w templates/User o nazwie profile.html.twig, który mógłby wygladać w ten sposób:
{% extends 'base.html.twig' %} {% block body %} <div class="row"> <div class="col-lg-12"> <h1 class="mt-5">User profile: {{ app.user.username }}</h1> <div class="col-lg-6"> <ul> <li>{{ app.user.username }}</li> <li>{{ app.user.email }}</li> <li> {% for role in app.user.roles %} {{ role }} {% if not loop.last %}, {% endif %} {% endfor %} </li> </ul> </div> </div> </div> {% endblock %}
Sam widok wyświetla jedynie podstawowe informacje o użytkowniku.
Kolejnym kontrolerem będzie AdminController, który ma odpowiadać za akcje związane z panelem admina.
<?php declare(strict_types=1); namespace App\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; /** * Class AdminController * @package App\Controller * @Route("/admin") */ class AdminController extends Controller { /** * @Route("/", name="admin.dashboard") * @return Response */ public function dashboard(): Response { return $this->render('Admin/dashboard.html.twig', []); } }
Wywołujący widok z katalogu /templates/Admin o nazwie dashboard.html.twig.
{% extends 'base.html.twig' %} {% block body %} <div class="row"> <div class="col-lg-12"> <h1 class="mt-5">Admin panel</h1> <div class="col-lg-6"> Welcome in Admin dashboard </div> </div> </div> {% endblock %}
Ostatnim krokiem będzie dodanie SuperAdminController
<?php declare(strict_types=1); namespace App\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; /** * Class AdminController * @package App\Controller * @Route("/superadminadmin") */ class SuperAdminController extends Controller { /** * @Route("/", name="superadmin.dashboard") * @return Response */ public function dashboard(): Response { return $this->render('SuperAdmin/dashboard.html.twig', []); } }
Oraz jego widoku
{% extends 'base.html.twig' %} {% block body %} <div class="row"> <div class="col-lg-12"> <h1 class="mt-5">Super Admin panel</h1> <div class="col-lg-6"> Welcome in Super Admin dashboard </div> </div> </div> {% endblock %}
Role a TWIG
Wszystko gotowe. Teraz chcielibyśmy mieć możliwość nawigacji do poszczególnych sekcji z poziomu widoku. Przechodzimy do pliku naszego głównego szablonu z /templates/base.html.twig.
W miejscu, w którym generujemy menu dla zalogowanego użytkownika, chcemy dodać linki do admin / superadmin / profile, w zależności od roli, jaką pełni użytkownik.
Do sprawdzania uprawnień użytkownika na poziomie TWIG użyjemy funkcji is_grated, która jako parametr przyjmuje rolę, którą sprawdzamy. Np. jeśli chcemy sprawdzić, czy użytkownik posiada rolę admina użyjemy is_granted(‘ROLE_ADMIN’). Przykładowy kod, który użyjemy do wygenerowania uprawnień:
<ul class="navbar-nav ml-auto"> {% if app.user %} {% if is_granted('ROLE_SUPERADMIN') %} <li class="nav-item"> <a class="nav-link" href="{{ path('superadmin.dashboard') }}">SuperAdmin</a> </li> {% endif %} {% if is_granted('ROLE_ADMIN') %} <li class="nav-item"> <a class="nav-link" href="{{ path('admin.dashboard') }}">Admin</a> </li> {% endif %} {% if is_granted('ROLE_USER') %} <li class="nav-item"> <a class="nav-link" href="{{ path('app.profile') }}">Profile</a> </li> {% endif %} <li class="nav-item"> <a class="nav-link" href="{{ path('logout') }}">Logout</a> </li> {% else %} <li class="nav-item"> <a class="nav-link" href="{{ path('login') }}">Login</a> </li> <li> <a class="nav-link" href="{{ path('registration') }}">Sign Up</a> </li> {% endif %} </ul>
Teraz w zależności od tego, na jakie konto się zalogujemy, zobaczymy inne menu.
Podsumowanie
Jest to trzeci artykuł z serii “system logowania w Symfony bez użycia FOSa“, który mam nadzieję, przekona was do porzucenia ciężkich bibliotek w małych projektach, na rzecz prostych i funkcjonalnych implementacji.
Całość możesz znaleźć na githubie.
Uruchom przykład na Dockerze
Przykład opisany w tym artykule możesz pobrać z linku umieszczonego powyżej. Sam przykład uruchomić można bez konieczności instalowania PHP czy MySQL w systemie. Wystarczy, że uruchomisz go przy pomocy Doker’a. Jeśli nie wiesz jak to zrobić zapraszam po więcej informacji do artykułów Docker – pierwsze kroki [część pierwsza] i [część druga]
Instrukcja uruchomienia znajduje się w pliku README.