Hello, I'm starting a project in api-platform V2 and I need to restrict access to some actions (edit, delete) only for resources creators.
I create a Voter to manage this security behavior, but I don't know how to use the Voter: in custom actions(How its works with the ADR pattern), Event Listeners??
namespace AppBundle\Security;
use AppBundle\Entity\LocalBusiness;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class LocalBusinessVoter extends Voter
{
const DELETE = 'edit';
const EDIT = 'delete';
private $decisionManager;
public function __construct(AccessDecisionManagerInterface $decisionManager)
{
$this->decisionManager = $decisionManager;
}
protected function supports($attribute, $subject)
{
// if the attribute isn't one we support, return false
if (!in_array($attribute, array(self::DELETE, self::EDIT))) {
return false;
}
// only vote on LocalBusiness objects inside this voter
if (!$subject instanceof LocalBusiness) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
// ROLE_SUPER_ADMIN can do anything! The power!
if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) {
return true;
}
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
// you know $subject is a LocalBusiness object, thanks to supports
/** @var LocalBusiness $localBusiness */
$localBusiness = $subject;
switch($attribute) {
case self::DELETE:
return $this->canDelete($localBusiness, $user);
case self::EDIT:
return $this->canEdit($localBusiness, $user);
}
throw new \LogicException('This code should not be reached!');
}
private function canEdit(LocalBusiness $localBusiness, User $user)
{
// if they can delete, they can edit
if ($this->canDelete($localBusiness, $user)) {
return true;
}
return false;
}
private function canDelete(LocalBusiness $localBusiness, User $user)
{
return $user->getUsername() === $localBusiness->getUpdatedBy();
}
}
Hi, you should listen to kernel.view event through a listener to execute your voter (adapt following code to your needs):
<service id="user_access_listener" class="UserBundle\EventListener\UserAccessListener">
<argument type="service" id="security.authorization_checker" />
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="12" />
</service>
namespace UserBundle\EventListener;
use UserBundle\Entity\User;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class UserAccessListener
{
/**
* @var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* @param AuthorizationCheckerInterface $authorizationChecker
*/
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
/**
* @param GetResponseForControllerResultEvent $event
*/
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$user = $event->getControllerResult();
if (!$user instanceof User) {
return;
}
if (!$this->authorizationChecker->isGranted(null, $user)) {
throw new AccessDeniedException();
}
}
}
It would be nice to add this example to the doc.
@rafix Is it ok for you ?
So I cannot find the above in the documentation. I would have imagined it being in the Security section of the documentation. That section links off to other areas, but doesn't mention anything about Voters and Listeners being used together. I think this needs to be more prominent. Most people are going to need to add permissions to their Api calls, and won't want to write their own actions as that's part of what Api-Platform is supposed to be able to handle.
Did I miss it somewhere? Here's where I think this should be prominently displayed... https://api-platform.com/docs/core/security I would even recommend having it linked as a sub section like you have others on the docs page...
If this needs a PR to implement this into the documentation, I would be happy to supply one. Just let me know.
@BallisticPain please take a look at #938 sadly it's not documented yet, any help is really appreciated! Hope this helps.
We talked about Voters in this repository issues, I can dig back the tickets if needed.
@vincentchalamon
This solution has terrible security issue: kernel.view event is executed AFTER the controller executed.
I.e. apiplatform executes post, put or even delete, and only AFTER this listener works with event and throws AccessDeniedException. You will get _"_access denied_"_ error, but restricted operation is done!
You should use kernel.controller event.
See https://symfony.com/doc/4.1/reference/events.html
I'd recommend using kernel.request (POST_READ and POST_DESERIALIZE) to deny access as early as possible. But in some cases you might need to also use kernel.view, if the authorization decision cannot be made that early in the process.
See the rationale in https://github.com/api-platform/core/issues/583#issuecomment-228304144
Things have obviously changed since 2016, but the problem remains the same. You could not have a complete view (pun intended) of the before and after states if you only use kernel.view.
Most helpful comment
It would be nice to add this example to the doc.