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 klasyUser
.
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
- Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 6]
- Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 5]
- Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 4]
- Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 3]
- Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 2]
- Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 1]
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.
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit