I'm trying to implement a custom Voter.
From the controller I call it this way:
$prj = $this->getDoctrine()->getRepository('AppBundle:Project')->findOneById($id);
if (false === $this->get('security.authorization_checker')->isGranted('responsible', $prj)) {
throw new AccessDeniedException('Unauthorised access!');
}
The first line properly retrieves the Project object (I checked with a dump).
The problem occurs inside the voter
<?php
namespace AppBundle\Security\Authorization\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ProjectVoter implements VoterInterface
{
const RESPONSIBLE = 'responsible';
const ACCOUNTABLE = 'accountable';
const SUPPORT = 'support';
const CONSULTED = 'consulted';
const INFORMED = 'informed';
public function supportsAttribute($attribute)
{
return in_array($attribute, array(
self::RESPONSIBLE,
self::ACCOUNTABLE,
self::SUPPORT,
self::CONSULTED,
self::INFORMED,
));
}
public function supportsClass($class)
{
$supportedClass = 'AppBundle\Entity\Project';
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}
/**
* #var \AppBundle\Entity\Project $project
*/
public function vote(TokenInterface $token, $project, array $attributes)
{
// check if class of this object is supported by this voter
if (!$this->supportsClass(get_class($project))) {
return VoterInterface::ACCESS_ABSTAIN;
}
// check if the voter is used correct, only allow one attribute
// this isn't a requirement, it's just one easy way for you to
// design your voter
if (1 !== count($attributes)) {
throw new \InvalidArgumentException(
'Only one attribute is allowed'
); //in origin it was 'for VIEW or EDIT, which were the supported attributes
}
// set the attribute to check against
$attribute = $attributes[0];
// check if the given attribute is covered by this voter
if (!$this->supportsAttribute($attribute)) {
return VoterInterface::ACCESS_ABSTAIN;
}
// get current logged in user
$user = $token->getUser();
// make sure there is a user object (i.e. that the user is logged in)
if (!$user instanceof UserInterface) {
return VoterInterface::ACCESS_DENIED;
}
$em = $this->getDoctrine()->getManager();
$projects = $em->getRepository('AppBundle:Project')->findPrjByUserAndRole($user, $attribute);
foreach ($projects as $key => $prj) {
if ($prj['id'] === $project['id'])
{
$granted = true;
$index = $key; // save the index of the last time a specifif project changed status
}
}
if($projects[$index]['is_active']===true) //if the last status is active
return VoterInterface::ACCESS_GRANTED;
else
return VoterInterface::ACCESS_DENIED;
}
}
I get the following error
Attempted to call method "getDoctrine" on class
"AppBundle\Security\Authorization\Voter\ProjectVoter".
I understand that the controller extends Controller, that is why I can use "getDoctrine" there. How can I have access to my DB from inside the Voter?
I solved it. This is pretty curious: I spend hours or days on a problem, then post a question here, and I solve it myself within an hour :/
I needed to add the following in my voter class:
public function __construct(EntityManager $em)
{
$this->em = $em;
}
I needed to add the following on top:
use Doctrine\ORM\EntityManager;
I also needed to add the arguments in the service.yml
security.access.project_voter:
class: AppBundle\Security\Authorization\Voter\ProjectVoter
arguments: [ #doctrine.orm.entity_manager ]
public: false
tags:
- { name: security.voter }
Related
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);
I override (custom operation and service) the DELETE operation of my app to avoid deleting data from DB. What I do is I update a field value: isDeleted === true.
Here is my controller :
class ConferenceDeleteAction extends BaseAction
{
public function __invoke(EntityService $entityService, Conference $data)
{
$entityService->markAsDeleted($data, Conference::class);
}
...
My service :
class EntityService extends BaseService
{
public function markAsDeleted(ApiBaseEntity $data, string $className)
{
/**
* #var ApiBaseEntity $entity
*/
$entity = $this->em->getRepository($className)
->findOneBy(["id" => $data->getId()]);
if ($entity === null || $entity->getDeleted()) {
throw new NotFoundHttpException('Unable to find this resource.');
}
$entity->setDeleted(true);
if ($this->dataPersister->supports($entity)) {
$this->dataPersister->persist($entity);
} else {
throw new BadRequestHttpException('An error occurs. Please do try later.');
}
}
}
How can I hide the "deleted" items from collection on GET verb (filter them from the result so that they aren't visible) ?
Here is my operation for GET verb, I don't know how to handle this :
class ConferenceListAction extends BaseAction
{
public function __invoke(Request $request, $data)
{
return $data;
}
}
I did something; I'm not sure it's a best pratice.
Since when we do :
return $data;
in our controller, API Platform has already fetch data and fill $data with.
So I decided to add my logic before the return; like :
public function __invoke(Request $request, $data)
{
$cleanDatas = [];
/**
* #var Conference $conf
*/
foreach ($data as $conf) {
if (!$conf->getDeleted()) {
$cleanDatas[] = $conf;
}
}
return $cleanDatas;
}
So now I only have undeleted items. Feel free to let me know if there is something better.
Thanks.
Custom controllers are discouraged in the docs. You are using Doctrine ORM so you can use a Custom Doctrine ORM Extension:
// api/src/Doctrine/ConferenceCollectionExtension.php
namespace App\Doctrine;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Conference;
use Doctrine\ORM\QueryBuilder;
final class CarCollectionExtension implements QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
{
if ($resourceClass != Conference::class) return;
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("$rootAlias.isDeleted = false OR $rootAlias.isDeleted IS NULL);
}
}
This will automatically be combined with any filters, sorting and pagination of collection operations with method GET.
You can make this Extension specific to an operation by adding to the if statement something like:
|| $operationName == 'conference_list'
If you're not using the autoconfiguration, you have to register the custom extension:
# api/config/services.yaml
services:
# ...
'App\Doctrine\ConferenceCollectionExtension':
tags:
- { name: api_platform.doctrine.orm.query_extension.collection }
If you also want to add a criterium for item operations, see the docs on Extensions
I'm having problems getting a redirect after login to work in Symfony.
It works for some pages but for others, the last_route session variable is being set to a users profile picture that uses the liip_imagine_filter:
"last_route" => [
"name" => "liip_imagine_filter",
"params" => [
"filter" => "profile_picture"
"path" => "frederick-jacobson/5ba60fc93056b.png"
]
]
LoginFormAuthenticator:
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
/*...*/
protected function getDefaultSuccessRedirectURL()
{
/** #var Session $session */
$session = $this->container->get('session');
$priorPage = $session->get('last_route');
return $this->router->generate($priorPage['name'], $priorPage['params']);
// return $this->router->generate('poll_index');
}
}
This means that it tries to redirect to an image URL.
services.yml:
poll.last_route_event_listener:
class: PollBundle\Services\LastRouteListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 30 }
LastRouteListener:
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernel;
class LastRouteListener
{
public function onKernelRequest(GetResponseEvent $event)
{
// Do not save subrequests
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
$request = $event->getRequest();
$session = $request->getSession();
$routeName = $request->get('_route');
$routeParams = $request->get('_route_params');
if ($routeName[0] == '_') {
return;
}
$routeData = ['name' => $routeName, 'params' => $routeParams];
// Do not save same matched route twice
$thisRoute = $session->get('this_route', []);
if ($thisRoute == $routeData) {
return;
}
$session->set('last_route', $thisRoute);
$session->set('this_route', $routeData);
}
}
Can someone please help me work out what I'm doing wrong and/or tell me the correct way to handle redirecting to the page prior to login?
As pointed out in the comments below my question. I was using a custom listener that was picking up the liip_imagine_filter route and setting it to the last_route session variable.
I could just add a check in to the listener, like this:
if ($routeName[0] == '_' || $routeName == 'liip_imagine_filter') {
return;
}
But a better way to handle it is to use the built in Symfony\Component\Security\Http\Util\TargetPathTrait
It is usually set automatically when a user hits a restricted page, but it can be set manually with $this->saveTargetPath($request->getSession(), $providerKey, $request->getUri());
Then you can use $targetPath to find the route to redirect to in LoginFormAuthenticator:
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
/*...*/
use TargetPathTrait;
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$targetPath = $this->getTargetPath($request->getSession(), $providerKey);
if (!$targetPath || $request->getBaseUrl() && !strpos($targetPath, $request->getBaseUrl())) {
$targetPath = $this->container->get('router')
->generate('poll_index');
}
return new RedirectResponse($targetPath);
}
}
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
My website is running Symfony 3.4 and I made my own user member system.
My User entity contains a Datetime field 'lastLogin' and I can't find a solution to update it every time a user logged in.
I created a custom UserChecker then I tried to update the field in it :
<?php
namespace CoreBundle\Security;
use CoreBundle\Entity\User as AppUser;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() )
{
throw new AuthenticationException();
}
else
{
// BELOW IS WHAT I TRY, BUT FAIL.
$entityManager = $this->get('doctrine')->getManager();
$user->setLastLogin(new \DateTime());
$entityManager->persist($user);
$entityManager->flush();
}
}
public function checkPostAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
}
}
But it doesn't work. Maybe I can't use the doctrine entity manager in this file ?
If I use $this->get('doctrine')->getManager(); I get :
Fatal Error: Call to undefined method
CoreBundle\Security\UserChecker::get()
Dunno why #doncallisto removed his post. It was (IMHO) the right thing.
Take a look at http://symfony.com/doc/current/components/security/authentication.html#authentication-success-and-failure-events
So you have several options.
SecurityEvents::INTERACTIVE_LOGIN - triggers every time the user
full out the login form and submit credentials. Will work, but you
won't get last_login updates if you have remember_me cookie or similar
AuthenticationEvents::AUTHENTICATION_SUCCESS - triggers each time
(every request) when authentication was successful. It means your last_login will be updated each time on every request unless user logged out
so you'll need a EventSubscriber. Take a look at this article. https://thisdata.com/blog/subscribing-to-symfonys-security-events/
MAybe you'll need a simplified version.
public static function getSubscribedEvents()
{
return array(
// AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure', // no need for this at that moment
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin', // this ist what you want
);
}
and then the onSecurityInteractiveLogin method itself.
public function onSecurityInteractiveLogin( InteractiveLoginEvent $event )
{
$user = $this->tokenStorage->getToken()->getUser();
if( $user instanceof User )
{
$user->setLastLogin( new \DateTime() );
$this->entityManager->flush();
}
}
P.S.
FosUserBundle uses interactive_login and a custom event to set last_login on entity
look at: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/EventListener/LastLoginListener.php#L63
Friend you can use this to inject the entityManager by a constructor
use Doctrine\ORM\EntityManagerInterface;
public function __construct(EntityManagerInterface $userManager){
$this->userManager = $userManager;
}
And in the checkPreAuth you call it
public function checkPreAuth(UserInterface $user){
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() ){
throw new AuthenticationException();
}else{
// BELOW IS WHAT I TRY, BUT FAIL.
$user->setLastLogin(new \DateTime());
$this->userManager->persist($user);
$this->userManager->flush();
}
}