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()
}
}
Related
sry if something is not so accurate, but im less experienced with Symfony
I have the following orm mapping:
src/app/ExampleBundle/Resources/config/doctrine/Base.orm.yml
app\ExampleBundle\Entity\Base:
type: mappedSuperclass
fields:
createdAt:
type: datetime
nullable: true
options:
default: null
updatedAt:
type: datetime
nullable: true
options:
default: null
This creates a entity Base which i modified to be abstract
src/app/ExampleBundle/Entity/Base.php
abstract class Base {
...
}
I have some other entities they extend this abstract class e.g.
src/app/ExampleBundle/Entity/Category.php
class Category extends Base
{
...
}
Now i tried to add a listener that sets the createdAt/updatedAt datetime on every persist for every entity that extends the Base Entity
src/app/ExampleBundle/EventListener/BaseListener.php
namespace app\ExampleBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\User\UserInterface;
use app\ExampleBundle\Entity\Base;
class BaseListener
{
protected $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function prePersist(Base $base, LifecycleEventArgs $event)
{
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof UserInterface) {
$user = null;
}
if ($base->getCreatedAt() === null) {
$base->setCreated($user, new \DateTime());
} else {
$base->setUpdated($user, new \DateTime());
}
}
}
And added it to the bundles services.yml
src/app/ExampleBundle/Resources/config
services:
app\ExampleBundle\EventListener\BaseListener:
arguments: ['#security.token_storage']
tags:
- { name: doctrine.orm.entity_listener, entity: app\ExampleBundle\Entity\Base, event: prePersist }
Symfony throws no Exception, but the defined event seems also not triggered.
I tried to change the entity param in services to the "real" entity Category, but still no error, nor the event triggered.
I think, i did everything as it is decribed in the documentation. But it still not working.
The command
debug:event-dispatcher
does also not show the event
So, the question is: What did i wrong?
Here the documentation I follow https://symfony.com/doc/3.4/doctrine/event_listeners_subscribers.html
The prePersist method is called for all the entities so you must exclude non instance of app\ExampleBundle\Entity\Base. The first argument is LifecycleEventArgs.
public function prePersist(LifecycleEventArgs $event)
{
$base = $event->getObject();
if (!$base instanceof Base) {
return;
}
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof UserInterface) {
$user = null;
}
if ($base->getCreatedAt() === null) {
$base->setCreated($user, new \DateTime());
} else {
$base->setUpdated($user, new \DateTime());
}
}
I can recommend you StofDoctrineExtensionsBundle (Timestampable) that does exactly what you want. It based on DoctrineExtensions.
There is even a trait that works like a charm.
After some research, many more tests, diving into the EntityManager and the UnitOfWork. Nothing seems to work fine. I get it so far to work on doctrine:fixtures:load, but for any reason they still not working if i use the entity manager in the Controllers. So, i decided to try another way with a subscriber.
tags:
- { name: doctrine.event_subscriber }
class ... implements EventSubscriber
So i still dont know why the Listener did not work as expected, but with the subscribers i found a solution that does.
Thanks to all of you for support :)
For Symfony 5 and anybody who will struggle with issue when Events::loadClassMetadata is not fired in subscriber.
Class should implement "EventSubscriberInterface"
class DiscriminatorSubscriber implements EventSubscriberInterface
{
public function getSubscribedEvents()
{
return [Events::loadClassMetadata];
}
public function loadClassMetadata(LoadClassMetadataEventArgs $event): void
{
}
Than you dont have to do anythng more if you have autoconfigure and autowire on TRUE in your services.yml
Doctrine will handle your subscriber registration (check DoctrineExtension.php)
$container->registerForAutoconfiguration(EventSubscriberInterface::class)
->addTag('doctrine.event_subscriber');
What I want to do is show logged user only content that he has access to.
First thing I was doing access_control in security.yml & redirect but i must do at least 30 deferent accounts;/
Next i create twig extension that will connect to DB and get current logged user specific settings -
access to panels. Is this good way?
The problem is
$user = $this->getUser()->getId();
$currentUser = $this->em->getRepository('AppBundle:User')->find($user);
It will not work, Blank page appears in dev env
But when i put 1
$currentUser = $this->em->getRepository('AppBundle:User')->find(1);
& 1 is user id everything is ok.
services.yml
app.twig.users_extension:
class: AppBundle\Twig\Extension\AccesExtension
arguments: ["#doctrine.orm.entity_manager","#security.token_storage"]
tags:
- { name: twig.extension }
Twig Extension
class AccesExtension extends \Twig_Extension
{
protected $em;
protected $tokenStorage;
public function __construct(EntityManager $em, TokenStorage $tokenStorage)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
}
public function getUser()
{
return $this->tokenStorage->getToken()->getUser();
}
public function getGlobals()
{
$user = $this->getUser()->getId();
$currentUser = $this->em->getRepository('AppBundle:User')->find($user);
return array (
"acces" => $currentUser,
);
}
public function getName()
{
return "AppBundle:AccesExtension ";
}
}
As people in the comments already told you, you can use security voters.
Here is a simple tutorial with video/text that explains the use of voters and also how to use them in twig, this should help you with the problem you have.
I can't figure out how to impersonate a user by user's id instead of user's username in Symfony?
The following trick which works with username can't work with id, as symfony is looking for username:
?_switch_user={id}
This is impossible to do without implementing your own firewall listener, as behind the scenes it loads the user from the userprovider (which only has a loadUserByUsername() method in its interface).
You could however implement your own firewall listener and get inspired by having a look at the code in Symfony\Component\Security\Http\Firewall\SwitchUserListener. For detailed information on implementing your own authentication provider, check the cookbook article.
EDIT:
One possible solution might be registering an extra request listener:
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LookupSwitchUserListener implements EventSubscriberInterface
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['lookup', 12] // before the firewall
];
}
public function lookup(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->has('_switch_user') {
return; // do nothing if already a _switch_user param present
}
if (!$id = $request->query->has('_switch_user_by_id')) {
return; // do nothing if no _switch_user_by_id param
}
// lookup $username by $id using the repository here
$request->attributes->set('_switch_user', $username);
}
}
Now register this listener in the service container:
services:
my_listener:
class: LookupSwitchUserListener
tags:
- { name: kernel.event_subscriber }
Calling a url with the ?_switch_user_by_id=xxx parameter should now correctly look up the username and set it so the SwitchUserListener can switch to the specified user.
I'm using fos user bundle and pugx multi user bundle.
I've read all the documentation and I'm new to Symfony.
In the pugx multi user bundle there's a sample on every point but one: sucessful registration.
Samples of overriding controllers for generating forms => ok
Samples of overriding templates for generating forms => ok
Samples of overriding successful registration sample => nothing.
Here's my code:
class RegistrationController extends BaseController
{
public function registerAction(Request $request)
{
$response = parent::registerAction($request);
return $response;
}
public function registerTeacherAction()
{
return $this->container
->get('pugx_multi_user.registration_manager')
->register('MyBundle\Entity\PersonTeacher');
}
public function registerStudentAction()
{
return $this->container
->get('pugx_multi_user.registration_manager')
->register('MyBundle\Entity\PersonStudent');
}
}
The problem is with ->get('pugx_multi_user.registration_manager') which returns a manager. In the fos user overring controllers help, they get either a form or a form.handler. I'm having hard times to "link" those with the pugx_multi_user manager.
What code should I put in the registerTeacherAction() to set roles for teacher, and in registerStudentAction() to set roles for student on a successful registration?
Solution 1 (Doctrine Listener/Subscriber)
You can easily add a doctrine prePersist listener/subscriber that adds the roles/groups to your entities depending on their type before persisting.
The listener
namespace Acme\YourBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\YourBundle\Entity\Student;
class RoleListener
{
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
// check for students, teachers, whatever ...
if ($entity instanceof Student) {
$entity->addRole('ROLE_WHATEVER');
// or
$entity->addGroup('students');
// ...
}
// ...
}
}
The service configuration
# app/config/config.yml or load inside a bundle extension
services:
your.role_listener:
class: Acme\YourBundle\EventListener\RoleListener
tags:
- { name: doctrine.event_listener, event: prePersist }
Solution 2 (Doctrine LifeCycle Callbacks):
Using lifecycle callbacks you can integrate the role-/group-operations directly into your entity.
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Student
{
/**
* #ORM\PrePersist
*/
public function setCreatedAtValue()
{
$this->addRole('ROLE_WHATEVER');
$this->addGroup('students');
}
Solution 3 (Event Dispatcher):
Register an event listener/subscriber for the "fos_user.registration.success" event.
How to create an event listener / The EventDispatcher component.
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.