I am using Voters to restrict access to entities in a REST API.
Step 1
Consider this voter that restricts users access to blog posts:
class BlogPostVoter extends Voter
{
public function __construct(AccessDecisionManagerInterface $decisionManager)
{
$this->decisionManager = $decisionManager;
}
/**
* Determines if the attribute and subject are supported by this voter.
*
* #param string $attribute An attribute
* #param int $subject The subject to secure, e.g. an object the user wants to access or any other PHP type
*
* #return bool True if the attribute and subject are supported, false otherwise
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, $this->allowedAttributes)) {
return false;
}
if (!$subject instanceof BlogPost) {
return false;
}
return true;
}
/**
* Perform a single access check operation on a given attribute, subject and token.
*
* #param string $attribute
* #param mixed $subject
* #param TokenInterface $token
* #return bool
* #throws \Exception
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
return $this->canUserAccess($attribute, $subject, $token);
}
public function canUserAccess($attribute, $subject, TokenInterface $token) {
if ($this->decisionManager->decide($token, array('ROLE_SUPPORT', 'ROLE_ADMIN'))) {
return true;
}
//other logic here omitted ...
return false;
}
}
You can see there is a public function canUserAccess to determine if the user is allowed to see the BlogPost. This all works just fine.
Step 2
Now I have another voter that checks something else, but also needs to check this same exact logic for BlogPosts. My thought was to:
add a new voter
perform some other checks
but then also perform this BlogPost check
So I thought I would inject the BlogPostVoter into my other voter like this:
class SomeOtherVoter extends Voter
{
public function __construct(BlogPostVoter $blogPostVoter)
{
$this->decisionManager = $decisionManager;
}
...
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
//other logic
if ($this->blogPostVoter->canUserAccess($attribute, $subject, $token)) {
return true;
}
return false;
}
}
Problem
When I do this I get the following error, using both setter and constructor injection:
Circular reference detected for service "security.access.decision_manager", path: "security.access.decision_manager"
I don't see where the security.access.decision_manager should depend on the Voter implementations. So I'm not seeing where the circular reference is.
Is there another way I can call VoterA from VoterB?
To reference VoterOne from VoterTwo you can inject the AuthorizationCheckerInterface into VoterTwo and then call ->isGranted('ONE'). Where ONE is the supported attribute of VoterOne.
Like:
class VoterTwo extends Voter
{
private $authorizationChecker;
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
protected function supports($attribute, $subject)
{
return in_array($attribute, ['TWO']);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
return $this->authorizationChecker->isGranted('ONE', $subject);
}
}
In this example VoterTwo does just redirect the request to VoterOne (or the voter that supports the attribute ONE). This can then be extended through additional conditions.
Related
I'm using ApiPlatform and Symfony5
I placed a filter on the User entity to sort them by a boolean value of the class named $expose
Use case:
For the /users?expose=true route ROLE_USER can get list of every user with filter $expose set to true
For the /users/ route ROLE_ADMIN can get list of every user no matter what
Here is my User class:
/**
* #ApiResource(
* attributes={
* "normalization_context"={"groups"={"user:read", "user:list"}},
* "order"={"somefield.value": "ASC"}
* },
* collectionOperations={
* "get"={
* "mehtod"="GET",
* "security"="is_granted('LIST', object)",
* "normalization_context"={"groups"={"user:list"}},
* }
* }
* )
* #ApiFilter(ExistsFilter::class, properties={"expose"})
* #ApiFilter(SearchFilter::class, properties={
* "somefield.name": "exact"
* })
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
I implement my authorization rules through UserVoter:
protected function supports($attribute, $subject): bool
{
return parent::supports($attribute, $subject) &&
($subject instanceof User ||
$this->arrayOf($subject, User::class) ||
(is_a($subject, Paginator::class) &&
$this->arrayOf($subject->getQuery()->getResult(), User::class))
);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
/** #var User $user */
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
if ($this->accessDecisionManager->decide($token, [GenericRoles::ROLE_ADMIN])) {
return true;
}
switch ($attribute) {
case Actions::LIST:
break;
}
return false;
}
To recover the list of User I recover the paginator object passed through the LIST attribute and make sure the object inside the request result are of type User.
This part have been tested and work properly.
Now my issue come from the fact that both those route are essentialy the same to my voter, so my authorization rules implemented through it apply to them both.
What I would like to do would be to tell my voter that both request are different (which I thought I could do as I recover a Paginator object but doesn't seem possible) so I can treat them separately in the same switch case.
So far I havn't found a way to implement it
Is there a way to implement this kind of rules ?
Or is there another way to implement this kind of authorization ?
Thank you!
If you can live with ordinary users and admin users using the same request /users/ but getting different results,
this docs page describes a way to make the result of GET collection operations depend on the user that is logged in. I adapted it for your question:
<?php
// api/src/Doctrine/CurrentUserExtension.php
namespace App\Doctrine;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Offer;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
final class CurrentUserExtension implements QueryCollectionExtensionInterface
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
{
if (User::class !== $resourceClass || $this->security->isGranted('ROLE_ADMIN')) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("$rootAlias.expose = true");
}
}
BTW, any users that do not have ROLE_ADMIN will get the filtered result, ROLE_USER is not required.
If you choose to stick with your use case that requires users with ROLE_USER to use /users?expose=true you can make a custom CollectionDataProvider that throws a FilterValidationException:
<?php
namespace App\DataProvider;
use Symfony\Component\Security\Core\Security;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use ApiPlatform\Core\Exception\FilterValidationException;
use App\Entity\User;
class UserCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
/** #var CollectionDataProviderInterface */
private $dataProvider;
private $security;
/**
* #param CollectionDataProviderInterface $dataProvider The built-in orm CollectionDataProvider of API Platform
*/
public function __construct(CollectionDataProviderInterface $dataProvider, Security $security)
{
$this->dataProvider = $dataProvider;
$this->security = $security;
}
/**
* {#inheritdoc}
*/
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return User::class === $resourceClass;
}
/** throws FilterValidationException */
private function validateFilters($context)
{
if ($this->security->isGranted('ROLE_ADMIN')) {
// Allow any filters, including no filters
return;
}
if (!$this->security->isGranted('ROLE_USER')) {
throw new \LogicException('No use case has been defined for this situation');
}
$errorList = [];
if (!isset($context["filters"]["expose"]) ||
$context["filters"]["expose"] !== "true" && $context["filters"]["expose"] !== '1'
) {
$errorList[] = 'expose=true filter is required.'
throw new FilterValidationException($errorList);
}
}
/**
* {#inheritdoc}
* #throws FilterValidationException;
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): array
{
$this->validateFilters($context);
return $this->dataProvider->getCollection($resourceClass, $operationName, $context);
}
You do need to add the following to api/config/services.yaml:
'App\DataProvider\UserCollectionDataProvider':
arguments:
$dataProvider: '#api_platform.doctrine.orm.default.collection_data_provider'
BTW, to filter by a boolean one usually uses a BooleanFilter:
* #ApiFilter(BooleanFilter::class, properties={"expose"})
This is relevant because users with ROLE_ADMIN may try to filter by expose=false. BTW, If $expose is nullable you need to test what happens with Users that have $expose set to null
WARNING: Be aware that your security will fail silently, allowing all users access to all User entities, if the property $expose is no longer mapped or if the name of the property $expose is changed but in the UserCollectionDataProvider it is not or the Filter spec it is not!
When the user logs in to the system, I need to fill a class variable (Login-> testInfo) with information, but in the controller the variable always returns null.
Here is a generic example.
The Login class
class Login extends UserInterface
{
private $testInfo = null;
public function setTestInfo(string $testInfo)
{
$this->testInfo = $testInfo;
}
public function getTestInfo() : ?string
{
return $this->testInfo;
}
}
The Authenticator:
class FormAuthenticator extends AbstractFormLoginAuthenticator
{
...
public function getUser($credentials, UserProviderInterface $userProvider)
{
$user = $this->entityManager->getRepository(Login::class)->findByUsername(credentials['username']);
if (!$user)
{
throw new CustomUserMessageAuthenticationException('Username could not be found.');
}
//this prints NULL
dd($user->getTestInfo());
$user->setTestInfo('testing the string');
//this prints 'testing the string'
dd($user->getTestInfo());
return $user;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
//this prints 'testing the string'
dd($token->getUser()->getTestInfo());
}
...
}
The Controller Class:
class MyController extends AbstractController
{
private $login = null;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->login = $tokenStorage->getToken() ? $tokenStorage->getToken()->getUser() : null;
}
public function home()
{
//this prints null
dd($this->login->getTestInfo());
}
}
If $user goes to the tokenStorage with the new value ('testing the string'), why, when I try to use it on the controller, does the variable always return null? what am I doing wrong?
Is testInfo a transient variable? Because you gotta know that there is UserProvider that tries to refresh user from token (maybe it could be "changed" somehow between requests). I'm pretty sure you're losing those infos right in this process.
Are you sure your controller constructor isn't being executed too soon, prior to the authentication success event writing the token to the token storage service? I'd dd() the token in the constructor to verify if the token and Login instance are present at that point.
You may need to use setContainer() instead of __construct() in your controller to retrieve the authenticated token, which would look something like this:
private $tokenStorage = null;
private $login = null;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #param ContainerInterface $container Symfony service container interface
* #return ContainerInterface|null
*/
public function setContainer(\Psr\Container\ContainerInterface $container): ?\Psr\Container\ContainerInterface
{
if ($this->tokenStorage instanceof TokenStorageInterface && $this->tokenStorage->getToken() instanceof TokenInterface && $this->tokenStorage->getToken()->getUser() instanceof Login) {
$this->login = $this->tokenStorage->getToken()->getUser();
}
return $container;
}
Is there an easier way (or just better alternative) to while doing a Voter check to verify a user is actually logged in?
Example:
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
switch ($attribute) {
case self::VIEW:
return $this->canView($subject, $token);
case self::EDIT:
return $this->canEdit($subject, $token);
case self::CREATE:
return $this->canCreate($token);
}
}
/**
* #param TokenInterface $token
* #return bool
*/
private function canCreate(TokenInterface $token)
{
if (!$token->getUser() instanceof User)
{
return false;
}
if ($token->getUser()->isEnabled() && !$token->getUser()->isFreeze())
{
return true;
}
return false;
}
The problem I'm having is stemming from $token->getUser() returns a string when the user is anon. and not an actual User entity.
This is fairly easily done within the controller with $this->isGranted('IS_AUTHENTICATED_FULLY') I just feel like I'm missing something similar that can be done within voters.
You can inject the AuthorizationChecker into the voter and then do the isGranted()-check.
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class MyVoter
{
private $authChecker;
public function __construct(AuthorizationCheckerInterface $authChecker)
{
$this->authChecker = $authChecker;
}
private function canCreate(TokenInterface $token)
{
if (!$this->authChecker->isGranted('IS_FULLY_AUTHENTICATED')) {
return false;
}
// ...
}
}
First of I all, I created the whole example below specifically for this question because the actual example is very big so if it looks stupid then assume that it is not for now!
I'm trying to come up with a solution so that I can call a correct private method (bankA() or bankB()) in controller if the validation successfully passes. As you can see in the custom validation constraint, I only check the $bank->code property however the condition is not actually that simple (there is repository checks so on) - (as I said above, it is trimmed down version). So, could please someone tell me, how will I know that which private method I should call in controller after successful validation? I'm happy to create dedicated validators if necessary so open for suggestions and examples.
Note: I looked into symfony group validation documentation but didn't really get the picture how I could apply to my scenario.
EXAMPLE REQUEST
{ "id": 66, "code": "A" }
{ "id": 34, "code": "B" }
CONTROLLER
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* #Route("bank", service="application_frontend.controller.bank")
*/
class BankController extends Controller
{
private $validator;
public function __construct(
ValidatorInterface $validator
) {
$this->validator = $validator;
}
/**
* #param Request $request
*
* #Route("")
* #Method({"POST"})
*
* #throws Exception
*/
public function indexAction(Request $request)
{
$content = $request->getContent();
$content = json_decode($content, true);
$bank = new Bank();
$bank->id = $content['id'];
$bank->code = $content['code'];
$errors = $this->validator->validate($bank);
if (count($errors)) {
throw new Exception($errors[0]->getMessage());
}
// OK, validation has passed so which one do I call now ?!?!
$this->bankA($bank);
$this->bankB($bank);
}
private function bankA(Bank $bank)
{
// Do something nice with Bank
}
private function bankB(Bank $bank)
{
// Do something bad with Bank
}
}
BANK MODEL
use Application\FrontendBundle\Validator\Constraint as BankAssert;
/**
* #BankAssert\Bank
*/
class Bank
{
/**
* #var int
*/
public $id;
/**
* #var string
*/
public $code;
}
CUSTOM VALIDATOR
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class Bank extends Constraint
{
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return get_class($this).'Validator';
}
}
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class BankValidator extends ConstraintValidator
{
public function validate($bank, Constraint $constraint)
{
if ($bank->code == 'A') {
return;
}
if ($bank->code == 'B') {
return;
}
$this->context->buildViolation('Bank error')->addViolation();
}
}
Depending on how many codes there are you could either do...
if ('A' === $bank->getCode()) {
$this->bankA($bank);
} else {
$this->bankB($bank);
}
Or..
$method = 'bank'.$bank->getCode();
if (!method_exists($this, $method)) {
throw new \Exception('Method "'.$method.'" does not exist');
}
$this->$method();
All of that being said, it would be advisable to move all of this work into a dedicated service rather than in your controller. Then in your controller use something like...
$this->container->get('do_something_to_bank.service')->processAction($bank);
I use the sonata-admin bundle.
I have the relationship with the user (FOSUserBundle) in the PageEntity.
I want to save the current user which create or change a page.
My guess is get the user object in postUpdate and postPersist methods of the admin class and this object transmit in setUser method.
But how to realize this?
On the google's group I saw
public function setSecurityContext($securityContext) {
$this->securityContext = $securityContext;
}
public function getSecurityContext() {
return $this->securityContext;
}
public function prePersist($article) {
$user = $this->getSecurityContext()->getToken()->getUser();
$appunto->setOperatore($user->getUsername());
}
but this doesn't work
In the admin class you can get the current logged in user like this:
$this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser()
EDIT based on feedback
And you are doing it this? Because this should work.
/**
* {#inheritdoc}
*/
public function prePersist($object)
{
$user = $this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser();
$object->setUser($user);
}
/**
* {#inheritdoc}
*/
public function preUpdate($object)
{
$user = $this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser();
$object->setUser($user);
}
Starting with symfony 2.8, you should use security.token_storage instead of security.context to retrieve the user. Use constructor injection to get it in your admin:
public function __construct(
$code,
$class,
$baseControllerName,
TokenStorageInterface $tokenStorage
) {
parent::__construct($code, $class, $baseControllerName);
$this->tokenStorage = $tokenStorage;
}
admin.yml :
arguments:
- ~
- Your\Entity
- ~
- '#security.token_storage'
then use $this->tokenStorage->getToken()->getUser() to get the current user.
I was dealing with this issue on the version 5.3.10 of symfony and 4.2 of sonata. The answer from greg0ire was really helpful, also this info from symfony docs, here is my approach:
In my case I was trying to set a custom query based on a property from User.
// ...
use Symfony\Component\Security\Core\Security;
final class YourClassAdmin extends from AbstractAdmin {
// ...
private $security;
public function __construct($code, $class, $baseControllerName, Security $security)
{
parent::__construct($code, $class, $baseControllerName);
// Avoid calling getUser() in the constructor: auth may not
// be complete yet. Instead, store the entire Security object.
$this->security = $security;
}
// customize the query used to generate the list
protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface
{
$query = parent::configureQuery($query);
$rootAlias = current($query->getRootAliases());
// ..
$user = $this->security->getUser();
// ...
return $query;
}
}