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

in #utopian-io6 years ago (edited)

email-marketing-2362038_640.png

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 and User 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



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

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

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

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

Coin Marketplace

STEEM 0.29
TRX 0.11
JST 0.031
BTC 67685.77
ETH 3856.07
USDT 1.00
SBD 3.69