Dynamically adding roles to a user - symfony

We are using Symfony2's roles feature to restrict users' access to certain parts of our app. Users can purchase yearly subscriptions and each of our User entities has many Subscription entities that have a start date and an end.
Now, is there a way to dynamically add a role to a user based on whether they have an 'active' subscription? In rails i would simply let the model handle whether it has the necessary rights but I know that by design symfony2 entities are not supposed to have access to Doctrine.
I know that you can access an entity's associations from within an entity instance but that would go through all the user's subscription objects and that seems unnecessaryly cumbersome to me.

I think you would do better setting up a custom voter and attribute.
/**
* #Route("/whatever/")
* #Template
* #Secure("SUBSCRIPTION_X")
*/
public function viewAction()
{
// etc...
}
The SUBSCRIPTION_X role (aka attribute) would need to be handled by a custom voter class.
class SubscriptionVoter implements VoterInterface
{
private $em;
public function __construct($em)
{
$this->em = $em;
}
public function supportsAttribute($attribute)
{
return 0 === strpos($attribute, 'SUBSCRIPTION_');
}
public function supportsClass($class)
{
return true;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
// run your query and return either...
// * VoterInterface::ACCESS_GRANTED
// * VoterInterface::ACCESS_ABSTAIN
// * VoterInterface::ACCESS_DENIED
}
}
You would need to configure and tag your voter:
services:
subscription_voter:
class: SubscriptionVoter
public: false
arguments: [ #doctrine.orm.entity_manager ]
tags:
- { name: security.voter }

Assuming that you have the right relation "subscriptions" in your User Entity.
You can maybe try something like :
public function getRoles()
{
$todayDate = new DateTime();
$activesSubscriptions = $this->subscriptions->filter(function($entity) use ($todayDate) {
return (($todayDate >= $entity->dateBegin()) && ($todayDate < $entity->dateEnd()));
});
if (!isEmpty($activesSubscriptions)) {
return array('ROLE_OK');
}
return array('ROLE_KO');
}
Changing role can be done with :
$sc = $this->get('security.context')
$user = $sc->getToken()->getUser();
$user->setRole('ROLE_NEW');
// Assuming that "main" is your firewall name :
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, null, 'main', $user->getRoles());
$sc->setToken($token);
But after a page change, the refreshUser function of the provider is called and sometimes, as this is the case with EntityUserProvider, the role is overwrite by a query.
You need a custom provider to avoid this.

Related

Symfony restrict page access to a group of people

I have pages like 'localhost/articles/show/id', representing the article details with the corresponding id.
I'd like to restrict the page access to a group of people.
In my database, each User belongs to a Family and each Article belongs to a Family as well.
And I want the users to be able to access article informations only if the article has been created by the family that the user is member of.
I could just verify manually by comparing the article's family to the current user family with some request in the Controller before rendering but I would to duplicated this code for every page like '/show/id', '/edit/id', ... Yet I'd like to know if there is a more beautiful way of doing it with symfony, something like 'every page that refers to a specific Article (/edit/id, /show/id and so on so forth) use a specific class to verify if the user is a member of the Family that created the article.
I think the thing you're looking for is Voter.
Security voters are the most granular way of checking permissions. All voters are
called each time you use the isGranted() method on Symfony’s authorization
checker or call denyAccessUnlessGranted() in a controller.
see: https://symfony.com/doc/current/security/voters.html
// src/Security/PostVoter.php
//....
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class ArticleVoter extends Voter
{
// these strings are just invented: you can use anything
const VIEW = 'view';
const EDIT = 'edit';
/**
* return true if the voter support your entity ($subject) type
*/
protected function supports(string $attribute, $subject)
{
// if the attribute isn't one we support, return false
if (!in_array($attribute, [self::VIEW, self::EDIT])) {
return false;
}
// only vote on `Article` objects
if (!$subject instanceof Article) {
return false;
}
return true;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
// you know $subject is a Article object, thanks to `supports()`
/** #var Article $post */
$article = $subject;
switch ($attribute) {
case self::VIEW:
return $this->canView($article, $user);
case self::EDIT:
return $this->canEdit($article, $user);
}
throw new \LogicException('This code should not be reached!');
}
private function canView(Article $article, User $user)
{
//Return true if user can view article, false otherwise
}
private function canEdit(Article $article, User $user)
//Return true if user can edit article, false otherwise
}
}
Voters are used when you call $this->denyAccessUnlessGranted(String $actionName, $entity) from your controllers. this method will throws an exception if a voter that support your $entity type and your $actionName return false.
// src/Controller/ArticleController.php
// ...
class ArticleController extends AbstractController
{
/**
* #Route("/article/{id}", name="article_show")
*/
public function show($id)
{
$article = ...;
// check for "view" access: calls all voters
$this->denyAccessUnlessGranted('view', $article);
// ...do your stuff
}
/**
* #Route("/article/{id}/edit", name="article_edit")
*/
public function edit($id)
{
$article = ...;
// check for "edit" access: calls all voters
$this->denyAccessUnlessGranted('edit', $article);
// ... do your stuff
}
}

Custom decision manager authorisation in Symfony 4

I have a specific authorisation system in my application (asked by my managers). It is based on Joomla. Users are attached to usergroups. Every action (i.e page) in my application are resources and for each resources I have set an access level. I have then to compare the resource access level with the usergroups of the current user to grant access or not to this specific resource.
All those informations are stored in database which are in return entities in Symfony :
User <- ManyToMany -> Usergroups
Menu (all resources with path and access level)
I thought about the Voter system. It is kind alike of what I would want, I think. Can I hijack the support function for this ?
protected function supports($user, $resource)
{
//get usergroups of the $user => $usergroups
//get the access level of the resource => $resource_access
// if the attribute isn't one we support, return false
if (!in_array($usergroups, $resource_access)) {
return false;
}
return true;
}
The get the usergroups and the access level of the resource I will have to do some queries in the database. To use this, then I would to use the denyAccessUnlessGranted() function in all my controller (seems redundant by the way) ?
Do you think it would work or there is another system more suited for this case ? I thought of doing the control in a listener to the kernel.request event too.
Hope I am clear enough, I'm new to symfony and still have some issues to understand how everything are related and working.
The voter component should be a good fit for this, as its a passive approach that lets you implement any logic in a way where its fixable through code, without modifying any database specific acl tree not managed by symfony itself.
Voters are called if you use denyAccessUnlessGranted() or isGranted() either through code, annotation or twig.
Lets take a look at how you want to check if the current user has access to view the index page:
class SomeController {
public function index() {
$this->denyAccessUnlessGranted('VIEW', '/index');
// or use some magic method to replace '/index' with wathever you require,
// like injecting $request->getUri(), just make sure your voter can
// parse it quickly.
// ...
}
}
Now build the a very simple voter:
class ViewPageVoter extends Voter
{
/**
* #var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
protected function supports($attribute, $subject)
{
return is_string($subject) && substr($subject, 0, 1) === '/';
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$currentUser = $token->getUser();
if(!$currentUser) {
// no user or authentication, deny
return false;
}
// Do the query to see if the user is allowed to view the resource.
// $this->em->getRepository(...) or
// $this->em->getConnection()
//
// $attribute = VIEW
// $subject = '/index'
// $currentUser = authenticated user
// return TRUE if allowed, return FALSE if not.
}
}
As a nice bonus you can easily see additional details on security voters in the /_profiler of that request, also indicating their respective vote on the subject.

How to get dynamic security privileges with Symfony Security

I'm need to do add custom access law to my access control on Symfony, I try to explain me.
I have a web application with some customer, and I want to able the access of some part of the code when that customer have the right plugin. So this is my customer:
namespace AppBundle\Entity;
class CustomerProfile{
private $id;
private $user;
private $plugins;
}
The Entity for the Plugin
namespace AppBundle\Entity;
class Plugin{
private $id;
private $name;
private $customerProfiles;
}
I use doctrine for the relations so from customer i can get his plugins. For example, we have 2 customer and 2 plugin:
AppBundle\Entity\CustomerProfile:
customer_1:
user: '#user_1'
plugins: ['#plugin_1','#plugin_2']
customer_2:
user: '#user_2'
plugins: ['#plugin_1']
AppBundle\Entity\Plugin:
plugin_1:
name: 'plugin 1'
plugin_2:
name: 'plugin 2'
In my project all the code about customer is under the /customer namespace, Symfony like, and all work.
access_control:
- { path: ^/customer, roles: ROLE_CUSTOMER }
But, for this customer with different plugin i would set an dinamic access control, but i don't know how. I need to control something like this:
access_control:
- { path: ^/code_for_plugin_1, roles: ROLE_CUSTOMER_WHIT_PLUGIN_1}
- { path: ^/code_for_plugin_2, roles: ROLE_CUSTOMER_WHIT_PLUGIN_2}
but I think the good way is set a "sub role" (if exist) to set for each customer that have a plugin a role to access in that namespace.
I hope I was clear enough, thanks for help.
I would suggest to use custom voter rather than the access_control and role approach as
this is in my opinion more flexible for your use case. Your suggested solution requires
a generated role (ROLE_CUSTOMER_WHIT_PLUGIN_{X}) for every plugin, which in case
of adding pluggins dynamically would just not work.
Check the How to Use Voters to Check User Permissions article in Symfony documentation for more detail.
You basically need to implement a user voter which will check if the logged user has an access
to requested resource. In your case it would look similar to this:
/src/YourBundle/Controller/YourController.php
<?php
...
class YourController extends Controller
{
public function getFooAction($id)
{
$this->denyAccessUnlessGranted(YourVoter::VIEW_FOO);
// ...method logic
}
public function getBarAction($id)
{
$this->denyAccessUnlessGranted(YourVoter::VIEW_BAR);
// ...method logic
}
}
/src/YourBundle/Security/YourVoter.php
<?php
...
class YourVoter extends AbstractVoter
{
const VIEW_FOO = 'YOUR_VIEW_FOO';
const VIEW_BAR = 'YOUR_VIEW_BAR';
public function getVoterAttributes()
{
return [self::VIEW_FOO, self::VIEW_BAR,];
}
protected function supports($attribute, $subject)
{
...
}
protected function voteOnAttribute($attribute, $item, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
switch ($attribute) {
case self::VIEW_FOO:
return $this->canViewFoo($user);
case self::VIEW_BAR:
return $this->canViewBar($user);
}
throw new \Exception(sprintf(
'Invalid vote attribute "%s".',
$attribute
));
}
private function canViewFoo(User $user)
{
return $user->getProfile()->hasRoleFooXYZ()
}
private function canViewBar(User $user)
{
return $user->getProfile()->hasRoleBarXYZ()
}
}

Sysmfony REST API hash id of entities

I'm building a multitenancy backend using Symfony 2.7.9 with FOSRestBundle and JMSSerializerBundle.
When returning objects over the API, I'd like to hash all the id's of the returned objects, so instead of returning { id: 5 } it should become something like { id: 6uPQF1bVzPA } so I can work with the hashed id's in the frontend (maybe by using http://hashids.org)
I was thinking about configuring JMSSerializer to set a virtual property (e.g. '_id') on my entities with a custom getter-method that calculates the hash for the id, but I don't have access to the container / to any service.
How could I properly handle this?
You could use a Doctrine postLoad listener to generate a hash and set a hashId property in your class. Then you could call expose the property in the serializer but set the serialized_name as id (or you could just leave it at hash_id).
Due to the hashing taking place int the postLoad you would need to refresh your object if you have just created it using $manager->refresh($entity) for it take effect.
AppBundle\Doctrine\Listener\HashIdListener
class HashIdListsner
{
private $hashIdService;
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$reflectionClass = new \ReflectionClass($entity);
// Only hash the id if the class has a "hashId" property
if (!$reflectionClass->hasProperty('hashId')) {
return;
}
// Hash the id
$hashId = $this->hashIdService->encode($entity->getId());
// Set the property through reflection so no need for a setter
// that could be used incorrectly in future
$property = $reflectionClass->getProperty('hashId');
$property->setAccessible(true);
$property->setValue($entity, $hashId);
}
}
services.yml
services:
app.doctrine_listsner.hash_id:
class: AppBundle\Doctrine\Listener\HashIdListener
arguments:
# assuming your are using cayetanosoriano/hashids-bundle
- "#hashids"
tags:
- { name: doctrine.event_listener, event: postLoad }
AppBundle\Resources\config\serializer\Entity.User.yml
AppBundle\Entity\User:
exclusion_policy: ALL
properties:
# ...
hashId:
expose: true
serialized_name: id
# ...
Thanks a lot for your detailed answer qooplmao.
However, I don't particularly like this approach because I don't intend to store the hashed in the entity. I now ended up subscribing to the serializer's onPostSerialize event in which I can add the hashed id as follows:
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MySubscriber implements EventSubscriberInterface
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public static function getSubscribedEvents()
{
return array(
array('event' => 'serializer.post_serialize', 'method' => 'onPostSerialize'),
);
}
/**
* #param ObjectEvent $event
*/
public function onPostSerialize(ObjectEvent $event)
{
$service = $this->container->get('myservice');
$event->getVisitor()->addData('_id', $service->hash($event->getObject()->getId()));
}
}

How to disable Blameable-behaviour programmatically in Symfony2

I'm trying to run a console command in symfony2 in which some properties of a certain class are being updated. One of the properties has got a corresponding reviewedBy-property which is being set by the blameable-behaviour like so:
/**
* #var bool
* #ORM\Column(name="public_cmt", type="boolean", nullable=true)
*/
private $publicCmt;
/**
* #var User $publicCmtReviewedBy
*
* #Gedmo\Blameable(on="change", field="public_cmt")
* #ORM\ManyToOne(targetEntity="My\Bundle\EntityBundle\Entity\User")
* #ORM\JoinColumn(name="public_cmt_reviewed_by", referencedColumnName="id", nullable=true)
*/
private $publicCmtReviewedBy;
When i run the task there's no user which can be 'blamed' so I get the following exception:
[Doctrine\ORM\ORMInvalidArgumentException]
EntityManager#persist() expects parameter 1 to be an entity object, NULL given.
However I can also not disable blameable because it's not registered as a filter by the time i start the task and programmatically trying to set the user through:
// create the authentication token
$token = new UsernamePasswordToken(
$user,
null,
'main',
$user->getRoles());
// give it to the security context
$this->getService('security.context')->setToken($token);
doesn't work. Anyone got an idea?
If you use the StofDoctrineExtensionsBundle you can simply do :
$this->container->get('stof_doctrine_extensions.listener.blameable')
->setUserValue('task-user');
see : https://github.com/stof/StofDoctrineExtensionsBundle/issues/197
First of all, I'm not sure if 'field' cares if you use the database column or the property, but you might need to change it to field="publicCmt".
What you should do is override the Blameable Listener. I'm going to assume you are using the StofDoctrineExtensionsBundle. First override in your config:
# app/config/config.yml
stof_doctrine_extensions:
class:
blameable: MyBundle\BlameableListener
Now just extend the existing listener. You have a couple options - either you want to allow for NULL values (no blame), or, you want to have a default user. Say for example you want to just skip the persist and allow a null, you would override as such:
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
public function getUserValue($meta, $field)
{
try {
$user = parent::getUserValue($meta, $field);
}
catch (\Exception $e) {
$user = null;
return $user;
}
protected function updateField($object, $ea, $meta, $field)
{
if (!$user) {
return;
}
parent::updateField($object, $ea, $meta, $field);
}
}
So it tries to use the parent getUserValue() function first to grab the user, and if not it returns null. We must put in a try/catch because it throws an Exception if there is no current user. Now in our updateField() function, we simply don't do anything if there is no user.
Disclaimer - there may be parts of that updateField() function that you still need...I haven't tested this.
This is just an example. Another idea would be to have a default database user. You could put that in your config file with a particular username. Then instead of returning null if there is no user from the security token, you could instead grab the default user from the database and use that (naturally you'd have to inject the entity manager in the service as well).
Slight modification of the above answer with identical config.yml-entry: we can check if a user is set and if not: since we have access to the object-manager in the updateField-method, get a default-user, set it and then execute the parent-method.
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
protected function updateField($object, $ea, $meta, $field)
{
// If we don't have a user, we are in a task and set a default-user
if (null === $this->getUserValue($meta, $field)) {
/* #var $ur UserRepository */
$ur = $ea->getObjectManager()->getRepository('MyBundle:User');
$taskUser = $ur->findOneBy(array('name' => 'task-user'));
$this->setUserValue($taskUser);
}
parent::updateField($object, $ea, $meta, $field);
}
}

Resources