Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 7]

Source: pixabay.com. Licensed under CC0 Creative Commons
What will I learn?
- How to extend a voter mechanism to check role-based user permissions
Requirements
- UNIX/Linux based OS
- Apache2 server with PHP7 installed
- MySQL database
- Text editor of choice
- Existing Firebase project
- Composer
- Base project found here
Difficulty
- Intermediate
Tutorial contents
This tutorial is a seventh one in the Web Development series where we jump into details on how to develop applications with Symfony using a sample project capable of sending push notifications to mobile devices. In the previous article the process of creating a voting mechanism, to check user permissions while interacting with RESTful API resources, was briefly described.
A sample project used with this tutorial can be found here. It is basically the code you will end up with after completing the previous tutorial. It is recommended to clone the aforementioned project from the given github repository.
What aspects of Symfony web development will be covered in this tutorial?
- The process of extending a voter mechanism by adding an ability to verify user permissions based on a current role assigned to a user object.
An example usage for the created functionalities will be shown at the end of this tutorial.
How to extend a voter mechanism to check role-based user permissions?
At the moment, the existing voting mechanism, created to verify user permissions to interact with a resource, checks whether there is a relation between a user making a request and a specific device object. This approach however, does not allow for an interaction with a device object which is not correlated with a currently authorised user. Thence, an application administrator would not be able to manage devices via web services.
A simple mechanism, allowing to assign roles to user objects, will be used to extend the existing voting system, making it capable of validating user permissions based on roles possessed by a user.
Note: A
ROLE_ADMIN
role will be defined for users with administrative privileges.
Modifying a user entity class
Before the role management will be possible, a User
entity class should be extended with an ORM relational mapping , to allow for storing user roles in a database.
Note: Prior to updating a user entity class, extending a
UserInterface
andUser
abstract declarations is recommended.
An abstraction layer
Due to the fact, that interfaces allow for defining constants within them, available user roles will be added to a UserInterface.php
file.
// src/Ptrio/MessageBundle/Model/UserInterface.php
const ROLE_USER = 'ROLE_USER';
const ROLE_ADMIN = 'ROLE_ADMIN';
Additionally, every User
type object should also implement methods capable of adding and removing roles.
// src/Ptrio/MessageBundle/Model/UserInterface.php
/**
* @param string $role
*/
public function addRole(string $role);
/**
* @param string $role
*/
public function removeRole(string $role);
Since, from now on, user roles are supposed to be stored in a database, adding a User::$roles
property to a User
base class is also required.
/**
* @var array
*/
protected $roles;
A constructor function should be added to a User
class, so an array containing user roles could be initialised.
/**
* User constructor.
*/
public function __construct()
{
$this->roles = [];
}
Currently, a User
base class contains only one, hard-coded role which is returned in an array by a User::getRoles()
method. The aforementioned method’s body should be modified to return values contained in a User::$roles
property.
/**
* @return array
*/
public function getRoles(): array
{
return $this->roles; // change this line
}
Next, logic responsible for handling addRole(string $role)
and removeRole(string $role)
methods should be added.
/**
* {@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]);
}
}
A User::addRole(string $role)
, prior to adding a role to a User::$roles
array property, verifies whether a given role is already assigned to a user object.
On the other hand, a User::removeRole(string $role)
uses an array_search
function to verify whether a given role exists within a User::$roles
array property.
Note: An
array_search
function returns an element’s key in case it was found within a given array.
A concrete class
In order for a column responsible for storing roles assigned to a user to be created, an appropriate annotation should be placed above a User::$roles
property declaration.
/**
* @ORM\Column(type="array")
*/
protected $roles;
An array
type allows for storing a serialised array in a TEXT
type database column.
Next, a Doctrine migrations script has to be executed to update the database schema.
$ php bin/console doctrine:migration:diff
The above script will create SQL commands which will be used to migrate the database schema.
Generated new migration class to "/var/www/html/src/Migrations/Version20180330110824.php" from schema differences.
A SQL command that will update user
.roles
columns with a a:0:{}
value (an empty serialised array) should be added to a generated migration script. This is necessary because a value stored in the aforementioned columns will be deserialised by Doctrine prior to calling a User::getRoles()
// src/Migrations/Version20180330110824.php
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE user ADD roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\'');
$this->addSql('UPDATE user SET roles=\'a:0:{}\' WHERE roles=\'\''); // add this line
}
Next, a migration script should be executed.
$ php bin/console doctrine:migration:migrate
Modifying a voting mechanism class
A voting mechanism will use a AccessDecisionManagerInterface
abstract type instance to verify whether an authorised user owns a role which allows for an interaction with a given device resource.
Start by adding an AccessDecisionManagerInterface
type import at the top of a DeviceVoter
class, between namespace
and class
operators.
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
Creating a DeviceVoter::$decisionManager
property, holding a decision manager instance, is necessary to proceed.
/**
* @var AccessDecisionManagerInterface
*/
private $decisionManager;
A class constructor, with a AccessDecisionManagerInterface
instance as an argument, should be added to allow for dependency injection.
/**
* DeviceVoter constructor.
* @param AccessDecisionManagerInterface $decisionManager
*/
public function __construct(
AccessDecisionManagerInterface $decisionManager
)
{
$this->decisionManager = $decisionManager;
}
A user having a ROLE_ADMIN
role will be capable of interacting with a non-related device object. Logic responsible for verifying whether a user making a request possesses the aforementioned role, should be placed within a DeviceVoter::voteOnAttribute($attribute, $subject, TokenInterface $token)
method body.
/**
* {@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
}
A AccessDecisionManager::decide(TokenInterface $token, array $attributes, $object = null)
method returns a true
value in case a user owns a ROLE_ADMIN
role.
Service configuration
A security.access.decision_manager
decision manager service should be added as an argument to a ptrio_message.device_voter
service definition.
# 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 }
Since the autowire
functionality is not being used intentionally for the automatic wiring of services, the autoconfigure
option found in a config/services.yaml
file, should also be set to false
. This will help you avoid a situation where a ptrio_message.device_voter
service arguments are not being recognised by higher level services.
The autoconfigure
option is set to false
by default, so commenting out an appropriate line in a config/services.yaml
file will be just enough.
# config/services.yaml
services:
_defaults:
# autowire: true
# autoconfigure: true # comment out this line
Modifying a AddUserCommand class
To assign a default USER_ROLE
role to each newly created user object, modifying a AddUserCommand
command class is necessary.
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);
}
A default role $user::ROLE_USER
is assigned to a user object with a User::addRole(string $role)
method.
AddRoleCommand class
A command class responsible for adding roles to user objects will be created at this stage. It will utilise a user manager service to look up a user object by username. Next, a User::setRole(string $role)
method will be used to assign a role to a given user object.
A concrete class
<?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.');
}
}
}
As shown above, role
and username
are both required arguments.
Note: Command classes were well documented in the previous Web Development with Symfony series articles.
Service configuration
A ptrio_message.add_role_command
service definition should to be added to a services.yaml
file and tagged as a 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 }
RemoveRoleCommand class
A RemoveRoleCommand
class will be created in the next step, to add a functionality responsible for removing roles from user objects. Similarly to a AddRoleCommand
class, a user manager service will be used to make this happen, but this time a User::removeRole(string $role)
method will be called.
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.');
}
}
}
Service configuration
Again, a services.yaml
file should be updated, this time with a ptrio_message.remove_role_command
service definition.
# 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 }
Examples
An example usage for the created functionalities is presented below.
Adding roles
$ php bin/console ptrio:message:add-role ROLE_ADMIN piotr42
An output similar to the one below will be displayed if the process of adding a role to a user object went well.
ROLE_ADMIN role has been added to user: piotr42
Removing roles
$ php bin/console ptrio:message:remove-role ROLE_ADMIN piotr42
Once a role is successfully removed from a user, an output similar to the one shown below should be produced.
ROLE_ADMIN role has been removed from user: piotr42
Voting mechanism tests
Let’s try to display the details for a device which is not related to a user making a HTTP request.
curl -H 'Accept: application/json' -H 'X-AUTH-TOKEN: MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM' http://localhost/api/v1/devices/redmi-ewa
An information, indicating that an access could not be granted to a user, should be displayed.
{"code":403,"message":"Access Denied."}
Now, let’s assign an administrative role to our user.
$ php bin/console ptrio:message:add-role ROLE_ADMIN piotr42
This time, when a request is made to display a redmi-ewa
device details, an appropriate output should be returned.
{"id":2,"name":"redmi-ewa","token":"d1KeQHgkIoo:APA91bGBG7vg9rfX0PFb...","user":null}
Curriculum
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 6]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 5]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 4]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 3]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 2]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 1]
Posted on Utopian.io - Rewarding Open Source Contributors
Thank you for the contribution. It has been approved.
You can contact us on Discord.
[utopian-moderator]
Hey @zonguin, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!
This tutorial its a with new Symfony becouse i see the new version and how moore changes ?
Hey @piotr42 I am @utopian-io. I have just upvoted you!
Achievements
Community-Driven Witness!
I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!
Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x