sonata-admin: creating a voter - symfony

I am using Symfony 4.4 and sonata-admin 3.107
I created a voter for one of my admin pages (SampleAdmin).
class SampleVoter extends Voter
{
protected function supports($attribute, $subject): bool
{
return in_array($attribute, ['LIST', 'VIEW', 'CREATE', 'EDIT'])
&& $subject instanceof 'App\Entity\Sample';
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
if ($attribute === 'EDIT') {
return false;
}
return true;
}
}
And I registered it in my services:
App\Voter\SampleVoter:
tags: [ security.voter ]
But it is not loaded when loading the sonata page in the browser.
Should I do something more?

How do you know not loaded? Raise exception in supports() method:
class SampleVoter extends Voter
{
protected function supports($attribute, $subject): bool
{
return in_array($attribute, ['LIST', 'VIEW', 'CREATE', 'EDIT']);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
if ($attribute === 'EDIT') {
return false;
}
return true;
}
}
btw, No need to register in services.yaml, because of service auto wiring.

Related

How to use dump() or dd() inside a Symfony Custom Voter?

I have followed several example from symfony, api-platform and several stackoverflow examples but to no avail. I don't know if I am doing something wrong or I don't understand the concept of the voter and roles. When I tried to access the endpoint, it throws Only user with permission can view dashboard.
In services.yaml
app.user_permission:
class: App\Security\SecurityVoter
arguments: ['#security.access.decision_manager']
tags:
- { name: security.voter}
I created a custom voter to use. Here I have done several changes, deleted several things to adopt the example I saw on StackOverflow Example
use App\Entity\Product;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class SecurityVoter extends Voter {
private $decisionManager;
const VIEW = 'view';
const EDIT = 'edit';
public function __construct (AccessDecisionManagerInterface $decisionManager) {
$this->decisionManager = $decisionManager;
}
protected function supports($attribute, $subject): bool {
// if the attribute isn't one we support, return false
if (!in_array($attribute, [self::VIEW, self::EDIT])) {
return false;
}
return true;
}
/**
* #param string $attribute
* #param TokenInterface $token
* #return bool
*/
protected function voteOnAttribute($attribute, $object, TokenInterface $token): bool {
$user = $token->getUser();
if (!$user instanceof UserInterface) {
// the user must be logged in; if not, deny access
return false;
}
// check ROLE_USER
if ($this->security->isGranted('ROLE_USER')) {
return true;
}
switch ($attribute) {
case self::VIEW:
if($this->decisionManager->decide($token, ['ROLE_USER'])) {
return true;
}
break;
case self::EDIT:
if($this->decisionManager->decide($token, ['ROLE_USER'])) {
return true;
}
break;
}
throw new \LogicException('This code should not be reached!');
}
}
In my Entity, I defined something like this.
#[ApiResource(
attributes: ["security" => "is_granted('ROLE_USER')"],
collectionOperations: [
"get",
"post" => [
"security_post_denormalize" => "is_granted('ROLE_USER)",
"security_message" => "Only user with permission can create a dashboard.",
],
],
itemOperations: [
"get" => [ "security" => "is_granted('VIEW') " , "security_message" => "Only user with permission can view dashboard."],
"put" => [ "security" => "is_granted('EDIT')", "security_message" => "Only user with permission can edit dashboard."],
],
)]
I am currently on Symfony 5.4.7 and I have tried to use the example code. Nothing seems to be working. I have to use dd() and dump(), nothing was printed on the console or profiler. I have used loggerInterface (maybe I didn't do it correctly), and I didn't get to see anything output to var.
You're closer than you think. You don't need use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface. You can make use of Security class as follows.
use App\Entity\Product;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class SecurityVoter extends Voter {
private $security;
const VIEW = 'view';
const EDIT = 'edit';
public function __construct ( Security $security) {
$this->security = $security;
}
protected function supports($attribute, $subject): bool {
// if the attribute isn't one we support, return false
$supportsAttribute = in_array($attribute, ['VIEW', 'EDIT']);
$supportsSubject = $subject instanceof WorkshopSession;
return $supportsAttribute && $supportsSubject;
}
/**
* #param string $attribute
* #param Product $product
* #param TokenInterface $token
* #return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool {
$user = $token->getUser();
if (!$user instanceof UserInterface) {
// the user must be logged in; if not, deny access
return false;
}
dd($user);
// check ROLE_USER
if ($this->security->isGranted('ROLE_USER')) {
return true;
}
switch ($attribute) {
case self::VIEW:
if($this->security->isGranted('ROLE_USER')) {
return true;
}
break;
case self::EDIT:
if($this->security->isGranted('ROLE_USER')) {
return true;
}
break;
}
throw new \LogicException('This code should not be reached!');
}
}
Meanwhile, you don't need to configure service for this.
To inject the voter into the security layer, you must declare it as a service and tag it with security.voter. But if you're using the default services.yaml configuration, that's done automatically for you!
In your entity
#[ApiResource(
attributes: ["security" => "is_granted('ROLE_USER')"],
collectionOperations: [
"get",
"post" => [
"security_post_denormalize" => "is_granted('ROLE_USER')",
"security_message" => "Only user with permission can create a dashboard.",
],
],
itemOperations: [
"get" => [ "security" => "is_granted('VIEW', object) " ],
"put" => [ "security" => "is_granted('EDIT')", "security_message" => "Only user with permission can edit dashboard."],
],
)]
You can also read this for reference - API Platform
NOTE: you can use dd() - e.g. dd($user);

Voter classes in Symfony 4

I'm taking over someone's code and I don't understand something about the voting.
Here is the PhotosController class:
class PhotosController extends Controller
{
/**
* #Route("/dashboard/photos/{id}/view", name="dashboard_photos_view")
* #Security("is_granted('view.photo', photo)")
* #param Photo $photo
* #param PhotoRepository $photoRepository
*/
public function index(Photo $photo, PhotoRepository $photoRepository)
{
$obj = $photoRepository->getFileObjectFromS3($photo);
header("Content-Type: {$obj['ContentType']}");
echo $obj['Body'];
exit;
}
Here is the voter class:
class PhotoVoter extends Voter
{
const VIEW = 'view.photo';
protected function supports($attribute, $subject)
{
if (!$subject instanceof Photo) {
return false;
}
if (!in_array($attribute, array(self::VIEW))) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
return $subject->getUser()->getId() === $token->getUser()->getId();
}
}
I don't understand what the
, photo
is for in the PhotosController class. And in PhpStorm I get "cannot find declaration" when I try to go to the "is_granted" declaration.

symfony 3.4 redirect a user on a page depending on his status

Goal : redirect a user depending on a status, on the whole website when the user is logged.
I need to force the user to be on one page until he has changed his profile
So i try to make a redirection with the event kernel but i've got an infinite loop. however I tried to avoid doing this redirection once the page wanted
So please find what i try to do
class TokenSubscriber implements EventSubscriberInterface {
private $user;
private $tokenStorage;
private $router;
protected $redirect;
public function __construct(TokenStorageInterface $tokenStorage, RouterInterface $router
) {
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}
public function onKernelController(FilterControllerEvent $event) {
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
$this->user = $this->tokenStorage->getToken()->getUser();
if ($this->user->getValider() == 3 && $controller[1] == 'indexUserAction' && $controller[0] instanceof DefaultUserController) {
$this->redirect = null;
} else {
$this->redirect = 'user_index';
}
}
public function onKernelResponse(FilterResponseEvent $event) {
if (null !== $this->redirect) {
$url = $this->router->generate($this->redirect);
$event->setResponse(new RedirectResponse($url));
}
}
public static function getSubscribedEvents() {
return array(
KernelEvents::CONTROLLER => 'onKernelController',
KernelEvents::RESPONSE => 'onKernelResponse',
);
}
}
Now the redirection works but when the page is loaded all my css et javascript are not loader , because the redirection i think.
I work just in the kernel response.
public function onKernelResponse(FilterResponseEvent $event){
if ($event->getRequest()->get('_route') != null && $event->getRequest()->get('_route') != 'user_index') {
$url = $this->router->generate('user_index');
$event->setResponse(new RedirectResponse($url));
}
}
You could try this, but using ->get('_route') should normally only be used for debugging. You should dump the Event and the Response to find out what else you can use.
public function onKernelRequest(GetResponseEvent $event)
if($event->getRequest()->get('_route') != 'user_index'){
$event->setResponse(new Response('[NOT_ALLOWED]'));
}
}
This should solve your issue
public function onKernelController(FilterControllerEvent $event) {
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
$this->user = $this->tokenStorage->getToken()->getUser();
if ($this->user->getValider() == 3 && $controller[1] == 'indexUserAction' && $controller[0] instanceof DefaultUserController) {
$this->redirect = null;
} else {
$this->redirect = 'user_index';
}
// Add this to Empty redirect if on already same page
if($event->getRequest()->get('_route') == 'user_index'){
$this->redirect = null;
}
}

Symfony route access check

I have a website made with Symfony 3.4 and within my actions I must check if the current user can edit the target product, something like this:
/**
* #Route("/products/{id}/edit")
*/
public function editAction(Request $request, Product $product)
{
// security
$user = $this->getUser();
if ($user != $product->getUser()) {
throw $this->createAccessDeniedException();
}
// ...
}
How can I avoid making the same check on every action (bonus points if using annotations and expressions)?
I am already using security.yml with access_control to deny access based on roles.
You can use Voters for this exact purpose. No magic involved. After creating and registering the Voter authentication will be done automatically in the security layer.
You just have to create the Voter class and then register it as a service. But if you're using the default services.yaml configuration, registering it as a service is done automatically for you!
Here is an example you can use. You may have to change a few items but this is basically it.
To read more visit: https://symfony.com/doc/current/security/voters.html
<?php
namespace AppBundle\Security;
use AppBundle\Entity\Product;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use AppBundle\Entity\User;
class ProductVoter extends Voter
{
const EDIT = 'EDIT_USER_PRODUCT';
protected function supports($attribute, $subject)
{
if($attribute !== self::EDIT) {
return false;
}
if(!$subject instanceof Product) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** #var Product $product */
$product= $subject;
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
return $this->belongsToUser($product, $user);
}
private function belongsToUser(Product $product, User $user)
{
return $user->getId() === $product->getUser()->getId();
}
}
You could try with a listener:
Check the action name,for example, if it is "edit_product", them continue.
Get the current logged User.
Get the user of the product entity.
Check if current user is different to Product user, if it is true, throw CreateAccessDeniedException.
services.yml
app.user.listener:
class: AppBundle\EventListener\ValidateUserListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
arguments: ["#service_container", "#doctrine.orm.entity_manager"]
Edit Action:
Added name "edit_product" to the action.
/**
*
* #Route("/products/{id}/edit",name="edit_product")
*/
public function editAction()
{
...
src\AppBundle\EventListener\ValidateUserListener.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class ValidateUserListener
{
private $container;
private $entityManager;
public function __construct($container, $entityManager)
{
$this->container = $container;
$this->entityManager = $entityManager;
}
public function onKernelRequest(GetResponseEvent $event)
{
$currentRoute = $event->getRequest()->attributes->get('_route');
if($currentRoute=='edit_product' || $currentRoute=='edit_item' )
{
$array_user = $this->getCurrentUser();
if($array_user['is_auth'])
{
$current_user = $array_user['current_user'];
$product = $this->entityManager->getRepository('AppBundle:User')->findOneByUsername($current_user);
$product_user = $product->getUsername();
if ($current_user !==$product_user)
{
throw $this->createAccessDeniedException();
}
}
}
}
private function getCurrentUser()
{
//Get the current logged User
$user = $this->container->get('security.token_storage')->getToken()->getUser();
if(null!=$user)
{
//If user is authenticated
$isauth = $this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY');
return array('is_auth'=>$isauth, 'current_user'=>$user);
}
return array('is_auth'=>false, 'current_user'=>$user);
}
}
Tested in Symfony 3.3

Symfony 2.8 session lost after login_check redirect

I am currently porting my old custom framework to a Symfony-based custom framework using Symfony's components. So far everything is going smoothly, except for the login part. Here are a few details about the project:
I'm using Symfony Security Component v2.8
My sessions are being stored in a database using PDOSessionHandler
I'm using Guard to authenticate my users.
The problem arises when a user tries to login to an admin area using a form. After the form submission, the user is forwarded to the login_check route where all credentials are successfully checked. The user role ROLE_ADMIN is set and finally the user is redirected to a secure page, but then gets redirected automatically back to the login. The order of events is like so:
login -> login_check -> admin -> login
I have done some debugging by setting breakpoints in ContextListener::OnKernelResponse and found out that a token is never saved in the session,because the method returns here:
if (!$event->getRequest()->hasSession()) {
return;
}
Also, I am able to see a session being added to the database table and the session id remains constant throughout the redirect. In the end I am bounced back to the login page and my user is set to .anon Somewhere between /login_check and /admin my token is lost.
I have run out of ideas on how to debug this. I am pasting some code to help get an idea of my setup, but I think these are fine.
My firewall configuration is looking like this
return[
'security'=>[
//Providers
'providers'=>[
'user' => array(
'id' => 'security.user.provider.default',
),
],
//Encoders
'encoders'=>[
'Library\\Security\\Users\\User::class' => array('algorithm' => 'bcrypt', 'cost'=> 15)
],
'firewalls'=>
[
'backend'=>array(
'security' =>true,
'anonymous' => true,
'pattern' => '^/',
'guard' => array(
'authenticators' => array(
'security.guard.form.authenticator',
'security.authenticator.token'
),
'entry_point'=>'security.guard.form.authenticator'
),
),
],
'access_control'=>array(
array('path' => '^/admin', 'roles' => ['ROLE_ADMIN']),
array('path' => '^/api', 'roles' => ['ROLE_API']),
array('path' => '^/pos', 'roles' => ['ROLE_POS']),
array('path' => '^/dashboard', 'roles' => ['ROLE_SUPER_ADMIN']),
array('path' => '^/login', 'roles' => ['IS_AUTHENTICATED_ANONYMOUSLY']),
array('path' => '/', 'roles' => ['IS_AUTHENTICATED_ANONYMOUSLY']),
)
]];
My UserInterface:
class User implements UserInterface, EquatableInterface{
private $username;
private $password;
private $salt;
private $roles;
public function __construct($username, $password, $salt, array $roles)
{
$this->username = $username;
$this->password = $password;
$this->salt = $salt;
$this->roles = $roles;
}
public function getRoles()
{
return $this->roles;
}
public function getPassword()
{
return $this->password;
}
public function getSalt()
{
return $this->salt;
}
public function getUsername()
{
return $this->username;
}
public function eraseCredentials()
{
}
public function isEqualTo(UserInterface $user)
{
if (!$user instanceof DefaultUserProvider) {
return false;
}
if ($this->password !== $user->getPassword()) {
return false;
}
if ($this->salt !== $user->getSalt()) {
return false;
}
if ($this->username !== $user->getUsername()) {
return false;
}
return true;
}}
My UserProvider
namespace Library\Security\UserProviders;
use Library\Nosh\Project\Project;
use Library\Security\Users\User;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use PDO;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
class DefaultUserProvider implements UserProviderInterface{
private $db;
private $project;
public function __construct(\PDO $db, Project $project)
{
$this->db = $db;
$this->project=$project;
}
public function loadUserByUsername($username)
{
$projectId = $this->project->id();
$statement = $this->db->prepare("SELECT * FROM users WHERE :userLogin IN (user_login, user_email) AND project_id=:project_id AND user_active=:user_active");
$statement->bindParam(':userLogin', $username, PDO::PARAM_STR);
$statement->bindValue(':user_active', 1, PDO::PARAM_INT);
$statement->bindValue(':project_id', $projectId, PDO::PARAM_INT);
$statement->execute();
if (!$user = $statement->fetch()) {
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
$roles = explode(',', $user['user_roles']);
return new User($user['user_login'], $user['user_password'],$salt='',$roles);
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'Library\\Security\\Users\\User';
}
}
I was able to solve my own problem after several days of debugging. The reason I had no session is because I had failed to implement a SessionListener to store the session into the request. This should not be an issue for anyone using the Symfony Framework or Silex. It was only an issue for me, because I am actually creating something from scratch.
For anyone wondering how to do this, here are the necessary steps:
Create a class which extends Symfony\Component\HttpKernel\EventListener\SessionListener
Implement the method getSession()
Make sure you add the class to the dispatcher with addSubscriber()
See my example below:
SessionListener
use Symfony\Component\HttpKernel\EventListener\SessionListener as AbstractSessionListener;
class SessionListener extends AbstractSessionListener {
private $container;
public function __construct(Container $container)
{
$this->container=$container;
}
protected function getSession()
{
if (!$this->container->has('session')) {
return;
}
return $this->container->get('session');
}
}
SessionServiceProvider
use Core\Container;
use Interfaces\EventListenerProviderInterface;
use Interfaces\ServiceProviderInterface;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class SessionServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface {
protected $options=[
'cookie_lifetime'=>2592000,//1 month
'gc_probability'=>1,
'gc_divisor'=>1000,
'gc_maxlifetime'=>2592000
];
public function register(Container $container){
switch($container->getParameter('session_driver')){
case 'database':
$storage = new NativeSessionStorage($this->options, new PdoSessionHandler($container->get('db')));
break;
case 'file':
$storage = new NativeSessionStorage($this->options, new NativeFileSessionHandler($container->getParameter('session_dir')));
break;
default:
$storage = new NativeSessionStorage($this->options, new NativeFileSessionHandler($container->getParameter('session_dir')));
break;
}
$container->register('session',Session::class)->setArguments([$storage]);
}
public function subscribe(Container $container, EventDispatcherInterface $dispatcher)
{
$dispatcher->addSubscriber(new SessionListener($container));
}
}

Resources