Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 7]

in polish •  7 years ago 

email-marketing-2362038_640.png

Czego nauczę się w tym poradniku?

  • Jak rozszerzyć mechanizm głosujący o możliwość weryfikacji uprawnień opartych na rolach użytkowników

Wymagania

  • System z rodziny UNIX/Linux
  • Serwer Apache2 z PHP w wersji 7.1
  • Baza danych MySQL
  • Projekt Symfony pobrany stąd
  • Dowolny edytor tekstowy
  • Istniejący projekt w Firebase
  • Narzędzie composer

Poziom trudności

  • Średni

Treść poradnika

Poradnik jest kolejnym z serii Web Development, gdzie opisywane są procesy budowy aplikacji serwerowej w Symfony, zdolnej do wysyłki powiadomień push na urządzenia mobilne. W poprzednim artykule przedstawiony został proces tworzenia mechanizmu głosującego, umożliwiającego weryfikację uprawnień autoryzowanych użytkowników do wyświetlenia danego zasobu RESTful API.

Przykładowy projekt wykorzystywany na potrzeby tego poradnika znajduje się tutaj. Jest to w zasadzie kod, który otrzymamy po ukończeniu poprzedniego kompendium. Wskazanym jest pobranie wspomnianego projektu z repozytorium github.

Jakie aspekty Web Development z Symfony zostaną przedstawione w tym poradniku?

  • Proces rozszerzenia mechanizmu głosującego, poprzez dodanie funkcjonalności umożliwiającej weryfikowanie uprawnień na podstawie ról przypisanych do użytkowników aplikacji.

Przykłady zastosowania funkcjonalności utworzonych w tym poradniku znajdują się na jego końcu.

Jak rozszerzyć mechanizm głosujący o możliwość weryfikacji uprawnień opartych na rolach użytkowników?

W chwili obecnej, istniejący mechanizm głosujący sprawdza czy istnieje powiązanie pomiędzy obiektem użytkownika wykonującego żądanie, a obiektem wybranego urządzenia, celem weryfikacji uprawnień. Takie rozwiązanie nie pozwala na interakcję z obiektem urządzenia użytkownikowi, z którym nie jest ono w korelacji. Administrator aplikacji nie ma więc możliwości zarządzania urządzeniami za pomocą usług RESTful API.

Prosty mechanizm pozwalający na powiązanie ról z obiektami użytkowników zostanie wykorzystany w celu rozszerzenia funkcjonalności usługi głosującej o możliwość weryfikowania dostępu na podstawie ról przydzielonych konkretnemu użytkownikowi.

Informacja: Nowa rola ROLE_ADMIN zostanie zdefiniowana dla kont z uprawnieniami administracyjnymi.

Modyfikacja klasy encji użytkownika

Aby zarządzanie rolami użytkowników było możliwe, do klasy encji użytkownika User należy dodać odpowiednie mapowanie ORM. Pozwoli to na przechowywanie informacji o rolach użytkownika w bazie danych.

Przed przejściem do procesu aktualizacji klasy encji urządzenia, wskazane jest zmodyfikowanie interfejsu UserInterface oraz bazowej klasy User.

Warstwa abstrakcyjna

Ze względu na fakt, że interfejsy pozwalają również na definiowanie stałych, dlatego też, dla wygody, dostępne role zostaną zdefiniowane w pliku interfejsu UserInterface.php.

    // src/Ptrio/MessageBundle/Model/UserInterface.php
    const ROLE_USER = 'ROLE_USER';
    const ROLE_ADMIN = 'ROLE_ADMIN';

Dodatkowo obiekt użytkownika implementujący interfejs UserInterface powinien również implementować metody pozwalające na dodawanie i usuwanie ról.

    // src/Ptrio/MessageBundle/Model/UserInterface.php
    /**
     * @param string $role
     */
    public function addRole(string $role);
    /**
     * @param string $role
     */
    public function removeRole(string $role);

Ponieważ od tej pory role mają być przechowywane w bazie danych, należy utworzyć własność User::$roles w pliku src/Ptrio/MessageBundle/Model/User.php.

    /**
     * @var array
     */
    protected $roles;

Do klasy należy dodać konstruktor, aby umożliwić inicjalizację tablicy zawierającej role użytkownika.

    /**
     * User constructor.
     */
    public function __construct()
    {
        $this->roles = [];
    }

Obecnie klasa bazowa User posiada zdefiniowaną jedną domyślną rolę zwracaną przez metodę User::getRoles() jako element tablicy. Wnętrze wspomnianej metody należy zmienić, tak aby zwracała ona role przechowywane we własności User::$roles.

    /**
     * @return array
     */
    public function getRoles(): array
    {
        return $this->roles; // change this line
    }

Następnie konieczne będzie dodanie logiki odpowiedzialnej za obsługę metod addRole(string $role) oraz removeRole(string $role).

    /**
     * {@inheritdoc}
     */
    public function addRole(string $role)
    {
        if (!in_array(strtoupper($role), $this->getRoles())) {
            $this->roles[] = $role;
        }
    }
    /**
     * {@inheritdoc}
     */
    public function removeRole(string $role)
    {
        if (false !== $key = array_search(strtoupper($role), $this->getRoles())) {
            unset($this->roles[$key]);
        }
    }

Metoda User::addRole(string $role), przed dodaniem roli do tablicy User::$roles, weryfikuje czy dana rola jest już przypisana do użytkownika.

Z kolei metoda User::removeRole(string $role) korzysta z funkcji array_search w celu weryfikacji czy usuwana rola znajduje się w tablicy User::$roles.

Informacja: Funkcja array_search zwraca klucz elementu, w przypadku gdy dany element został odnaleziony w przeszukiwanej tablicy.

Klasa właściwa

Aby kolumna, w której przechowywane będą role powiązane z użytkownikiem, mogła zostać utworzona należy nad własnością User::$roles dodać odpowiednią adnotację.

    /**
     * @ORM\Column(type="array")
     */
    protected $roles;

Typ array pozwoli na przechowywanie w bazie danych tablicy przetworzonej za pomocą funkcji PHP serialize w kolumnie typu TEXT.

Następnie należy wykonać migracje bazy danych, aby zaktualizować strukturę tabel.

$ php bin/console doctrine:migration:diff

Powyższy skrypt wygeneruje polecenia SQL wymagane do przeprowadzenia migracji.

Generated new migration class to "/var/www/html/src/Migrations/Version20180330110824.php" from schema differences.

Należy zmodyfikować skrypt migracyjny, aby uzupełnić puste wartości wartością a:0:{}. W innym przypadku Doctrine nie będzie w stanie odpowiednio przetworzyć wartości znajdującej się w kolumnie user.role, ze względu na fakt, że wspomniana wartość jest poddawana procesowi deserializacji nim będzie obsłużona przez metodę User::getRoles().

$this->addSql('UPDATE user SET roles=\'a:0:{}\' WHERE roles=\'\'');

Następnie można uruchomić skrypt migracyjny.

$ php bin/console doctrine:migration:migrate
Modyfikacja klasy mechanizmu głosującego

Mechanizm głosujący korzystał będzie z obiektu abstrakcyjnego typu AccessDecisionManagerInterface, w celu weryfikacji czy dany użytkownik posiada rolę pozwalającą na interakcję z danym zasobem.

Na początku należy dodać import interfejsu AccessDecisionManagerInterface na górze klasy, pomiędzy operatorami namespace i class.

use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;

Koniecznym będzie utworzenie własności DeviceVoter::$decisionManager, która przechowywać będzie instancję decyzyjnego managera dostępu.

    /**
     * @var AccessDecisionManagerInterface
     */
    private $decisionManager;

Należy również dodać nowy argument konstruktora, aby umożliwić wstrzyknięcie obiektu typu AccessDecisionManagerInterface w procesie inicjalizacji mechanizmu głosującego.

    /**
     * DeviceVoter constructor.
     * @param AccessDecisionManagerInterface $decisionManager
     */
    public function __construct(
        AccessDecisionManagerInterface $decisionManager
    )
    {
        $this->decisionManager = $decisionManager;
    }

Użytkownik posiadający rolę ROLE_ADMIN będzie miał możliwość interakcji z obiektem urządzenia, które do niego nie należy. Wewnątrz metody DeviceVoter::voteOnAttribute($attribute, $subject, TokenInterface $token) powinna zostać dodana logika sprawdzająca czy aktualnie autoryzowany użytkownik posiada wspomnianą rolę.

    /**
     * {@inheritdoc}
     */
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();
        if (!$user instanceof UserInterface) {
            return false;
        }
        // add code below
        if ($this->decisionManager->decide($token, [$user::ROLE_ADMIN])) {
            return true;
        }
        // remaining logic
    }

Metoda AccessDecisionManager::decide(TokenInterface $token, array $attributes, $object = null) zwraca wartość true w przypadku gdy użytkownik posiada rolę ROLE_ADMIN.

Konfiguracja usług

Należy zaktualizować definicję usługi ptrio_message.device_voter, dodając do niej usługę decyzyjnego managera dostępu security.access.decision_manager jako argument.

    # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.device_voter:
        class: 'App\Ptrio\MessageBundle\Security\Voter\DeviceVoter'
        arguments:
            - '@security.access.decision_manager' # add this line
        tags:
            - { name: security.voter }

Ponieważ intencjonalnie nie korzystamy z funkcjonalności autowire do automatycznego wiązania usług, nie chcemy aby były one również automatycznie konfigurowane. Należy upewnić się, że wartość autoconfigure w pliku config/services.yaml jest ustawiona na false (wartość domyślna), aby uniknąć problemu z niewidocznymi argumentami usługi ptrio_message.device_voter.

# config/services.yaml
services:
    _defaults:
#        autowire: true 
#        autoconfigure: true # comment out this line
Modyfikacja klasy komendy odpowiedzialnej za tworzenie użytkowników

Konieczne będzie zaktualizowanie klasy komendy AddUserCommand, aby podczas tworzenia nowego obiektu użytkownika, przypisywana była do niego domyślna rola ROLE_USER.

        if ($user = $this->userManager->findUserByUsername($username)) {
            $output->writeln('User with a given username already exists.');
        } else {
            $user = $this->userManager->createUser();
            $user->setUsername($username);
            $apiKey = $this->tokenGenerator->generateToken();
            $user->setApiKey($apiKey);
            $user->addRole($user::ROLE_USER); // add this line
            $this->userManager->updateUser($user);
            $output->writeln('User created successfully with the following api key: ' . $apiKey);
        }
Klasa komendy odpowiedzialnej za dodawanie ról do użytkownika

Na tym etapie zostanie utworzona usługa komendy umożliwiająca przypisanie użytkownikowi konkretnej roli. Wykorzystywać będzie ona usługę managera użytkowników aby odnaleźć użytkownika po nazwie. Następnie metoda User::setRole(string $role) zostanie użyta aby do użytkownika dodać pożądaną rolę.

Klasa właściwa
<?php
// src/Ptrio/MessageBundle/Command/AddRoleCommand.php
namespace App\Ptrio\MessageBundle\Command;
use App\Ptrio\MessageBundle\Model\UserManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AddRoleCommand extends Command 
{
    /**
     * @var string
     */
    public static $defaultName = 'ptrio:message:add-role';
    /**
     * @var UserManagerInterface
     */
    private $userManager;
    /**
     * AddRoleCommand constructor.
     * @param UserManagerInterface $userManager
     */
    public function __construct(
        UserManagerInterface $userManager
    )
    {
        $this->userManager = $userManager;       
        parent::__construct();
    }
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setDefinition([
            new InputArgument('role', InputArgument::REQUIRED),
            new InputArgument('username', InputArgument::REQUIRED),
        ]);
    }
    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $role = $input->getArgument('role');
        $username = $input->getArgument('username');        
        if ($user = $this->userManager->findUserByUsername($username)) {
            $user->addRole($role);
            $this->userManager->updateUser($user);            
            $output->writeln($role.' role has been added to user: '.$user->getUsername());
        } else {
            $output->writeln('The user with the given username cannot be found.');
        }
    }
}

Argumenty role oraz username są wymagane.

Konfiguracja usług

Usługa komendy ptrio_message.add_role_command powinna zostać zdefiniowana w pliku services.yaml oraz oznaczona jako console.command.

    # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.add_role_command:
        class: 'App\Ptrio\MessageBundle\Command\AddRoleCommand'
        arguments:
            - '@ptrio_message.user_manager'
        tags:
            - { name: console.command }
Klasa komendy odpowiedzialnej za usuwanie ról

Aby umożliwić usuwanie ról należy utworzyć klasę komendy RemoveRoleCommand. W tym przypadku usługa managera użytkowników zostanie również wykorzystana, a do usunięcia roli z obiektu użytkownika zostanie użyta metoda User::removeRole(string $role).

Klasa właściwa
<?php
// src/Ptrio/MessageBundle/Command/RemoveRoleCommand.php
namespace App\Ptrio\MessageBundle\Command;
use App\Ptrio\MessageBundle\Model\UserManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RemoveRoleCommand extends Command
{
    /**
     * @var string
     */
    public static $defaultName = 'ptrio:message:remove-role';
    /**
     * @var UserManagerInterface
     */
    private $userManager;
    /**
     * AddRoleCommand constructor.
     * @param UserManagerInterface $userManager
     */
    public function __construct(
        UserManagerInterface $userManager
    )
    {
        $this->userManager = $userManager;
        parent::__construct();
    }
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setDefinition([
            new InputArgument('role', InputArgument::REQUIRED),
            new InputArgument('username', InputArgument::REQUIRED),
        ]);
    }
    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $role = $input->getArgument('role');
        $username = $input->getArgument('username');
        if ($user = $this->userManager->findUserByUsername($username)) {
            $user->removeRole($role);
            $this->userManager->updateUser($user);
            $output->writeln($role.' role has been removed from user: '.$user->getUsername());
        } else {
            $output->writeln('The user with the given username cannot be found.');
        }
    }
}
Konfiguracja usług

Pozostaje jeszcze konfiguracja usługi ptrio_message.remove_role_command.

    # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.remove_role_command:
        class: 'App\Ptrio\MessageBundle\Command\AddRoleCommand'
        arguments:
            - '@ptrio_message.user_manager'
        tags:
            - { name: console.command }

Przykłady

Poniżej znajdują się przykłady związane z funkcjonalnościami utworzonymi w tym poradniku.

Dodawanie ról

$ php bin/console ptrio:message:add-role ROLE_ADMIN piotr42

Jeżeli proces dodawania roli przebiegł pomyślnie, powinien zostać wyświetlony poniższy komunikat.

ROLE_ADMIN role has been added to user: piotr42

Usuwanie ról

$ php bin/console ptrio:message:remove-role ROLE_ADMIN piotr42

W przypadku gdy proces usuwania roli przebiegł pomyślnie, zostanie zwrócony komunikat znajdujący się poniżej.

ROLE_ADMIN role has been removed from user: piotr42

Testy mechanizmu głosującego

Spróbujmy wyświetlić szczegóły urządzenia, które nie jest powiązane z kontem użytkownika, z którego będziemy wykonywać żądanie.

curl -H 'Accept: application/json' -H 'X-AUTH-TOKEN: MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM' http://localhost/api/v1/devices/redmi-ewa

Informacja o braku dostępu powinna zostać wyświetlona.

{"code":403,"message":"Access Denied."}

Przydzielmy więc rolę administratora naszemu użytkownikowi.

$ php bin/console ptrio:message:add-role ROLE_ADMIN piotr42

Ponownie spróbujmy wyświetlić szczegóły urządzenia redmi-ewa nie powiązanego z kontem piotr42.

{"id":2,"name":"redmi-ewa","token":"d1KeQHgkIoo:APA91bGBG7vg9rfX0PFb...","user":null}

Curriculum

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Congratulations! This post has been upvoted from the communal account, @minnowsupport, by piotr42 from the Minnow Support Project. It's a witness project run by aggroed, ausbitbank, teamsteem, theprophet0, someguy123, neoxian, followbtcnews, and netuoso. The goal is to help Steemit grow by supporting Minnows. Please find us at the Peace, Abundance, and Liberty Network (PALnet) Discord Channel. It's a completely public and open space to all members of the Steemit community who voluntarily choose to be there.

If you would like to delegate to the Minnow Support Project you can do so by clicking on the following links: 50SP, 100SP, 250SP, 500SP, 1000SP, 5000SP.
Be sure to leave at least 50SP undelegated on your account.