Symfony Voters on collection of objects - symfony

I am using Voters to manage permissions on my application and everything works fine for single objects.
What I don't seem to be able to do is apply a Voter on a collection of objects. For example, I have an end-point /persons that will return the complete list of people, but it should be filtered according to the rights of each user (a department manager should only see the people in their own department). Is there any way of doing this with Voters?

Well this can be done using an authorization checker:
// your controller
private $authorizationChecker;
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
// in your action
return array_filter($userCollection, function (User $user) {
return $this->authorizationChecker->isGranted('VOTER_NAME', $user);
});
or simply if you're controllers extend the Symfony's Controller class:
return array_filter($userCollection, function (User $user) {
return $this->isGranted('VOTER_NAME', $user);
});

Related

Getter method for services in Symfony controller

Is it a good practice to have a service getter for frequently used services in a controller? For example I mean:
class SomeController Extends Contorller {
private function getSomethingManager()
{
return $this->get('myvendorname.something.manager');
}
}
Your example is a bit confusing because you can use the Doctrine service directly with your controller. You can inject it in your Action if you use the Autowire function.
public function test(EntityManagerInterface $em) {
}
Then you have the entity manager injected or you can load it over the controller with:
$this->getDoctrine()->getManager()
So this is not a real good example. When you use autowire all classes are registered as service and you can use it.
For database queries you have to use entities and repositories.
https://symfony.com/doc/current/doctrine.html
If you are above Symfony 3.3 you can use a Service Locater. You list all common services in Service Locator class. When you need to fetch a specific service from anywhere (from example, Controller, Command, Service so on), all you have to do is, inject ServiceLocator class and fetch required service via ServiceLocator:locate.
It is pretty simple and useful. It helps you to reduce dependency injection as well. Have a look at the full example in the link above.
class ServiceLocator implements ServiceLocatorInterface, ServiceSubscriberInterface
{
private $locator;
public function __construct(ContainerInterface $locator)
{
$this->locator = $locator;
}
public static function getSubscribedServices()
{
return [
ModelFactoryInterface::class,
CalculatorUtilInterface::class,
EntityManagerInterface::class,
AnotherClass::class,
AndAnother::class,
];
}
public function get(string $id)
{
if (!$this->locator->has($id)) {
throw new ServiceLocatorException(sprintf(
'The entry for the given "%s" identifier was not found.',
$id
));
}
try {
return $this->locator->get($id);
} catch (ContainerExceptionInterface $e) {
throw new ServiceLocatorException(sprintf(
'Failed to fetch the entry for the given "%s" identifier.',
$id
));
}
}
}
And this is how you use it: ServiceLocator->locate(AnotherClass::class);

Zend Framework 3 Redirect from factory

My Controller has a Factory that gives it a Form
$formManager = $container->get('FormElementManager');
return new MyController(
$formManager->get(MyForm::class)
);
My Form has also a Factory that gives it an AuthenticationService
return new MyForm(
$container->get(AuthenticationService::class)
);
That way I can check in the form if the user has identity.
But how can i redirect him from the form?
Just like in a Controller?
if(!$authService->hasIdentity()) {
return $this->redirect()->toRoute('myRoute);
}
Or how can i redirect from a (Controller and/or Form) Factory?
A possible solution for your issue could be the possibilty of using the build method with the factory call.
You haven 't shown your factories, so I will use some standard examples, which explain the solution.
The first approach is not injecting the whole form to the controller. Instead just inject the form element manager. So you can use the build method of the factory inside your controller.
The controller factory
namespace Application\Controller\Factory;
use Application\Controller\YourController;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class YourControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$formElementManager = $container->get('FormElementManager');
return new YourController($formElementManager);
}
}
This differs from your original factory. Only the form element manager is injected to the controller. This holds a few advantages for you. One of this is the build method of the manager.
The Controller
namespace Application\Controller;
class YourController extends AbstractActionController
{
protected $formElementManager;
public function __construct($formElementManager)
{
$this->formElementManager = $formElementManager;
}
public function indexAction()
{
$user = $this->currentUser();
if ($user === null) {
$this->redirect('to/somewhere/the/user/belongs');
}
// here 's the magic!
$form = $this->formElementManager->build(YourForm::class, [
'userID' => $user->getUserId(),
]);
// some form stuff follows here
}
}
As the form was not injected directly to your controller but the form element manager, you can use the form element manager instead inside the controller. This offers you the opportunity to use the build function. With this function you can add some options to your form factory. In this case I 'm using the user id for the form factory.
If there 's no valid user, no form will be created because an exception is thrown before.
The Form Factory
The form factory creates a new instance of your form. All needed dependencies should be created in the factory. How the build function works here, I 'll explain later in the answer.
namespace Application\Form\Factory;
use Application\Form\YourForm;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class YourFormFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$selectOptions = [];
if ($options !== null) {
if (isset($options['userID])) {
$tablegateway = $container->get(YourTableGateway::class);
$selectOptions = $tablegateway->findOptionsByUserId($options['userID]);
}
}
$form = $container->get(YourForm::class);
if (count($selectOptions))
$form->get('YourSelectElement')->setValueOptions($selectOptions);
return $form;
}
}
This factory does all you need. Via the build method you hand over the user id. If a user id is present a table gateway is created from wich you retrieve select options by the given user id. These options will be set to the form field. This logic is kept in the factory to keep the form class itself clean and simple.
With this solution you don 't need the auth service in your form. Your form is only generated when a valid user id is given. Your form instance will not crash, if there 's no user id given. The only conceivable case could be a form with default or no select options for the specific field.
Hope this helps a bit.

Testing arguments of the method tested

is it possible or good idea to test arguments of the class that has the method mocked ?
Example :
class CarType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->addEventSubscriber(new User('argument example')
}
}
This is my test:
public function testFormBuilder()
{
$builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder')
->disableOriginalConstructor()
->getMock();
// Test
$type = new CarType();
$this->builder->expects($this->once())
->method('addEventSubscriber')
->with(
$this->isInstanceOf(User::class)
);
$type->buildForm($this->builder, []);
}
This test work fine, But...
I want to know if the first argument of the User class is a string, but can I do in this same test?
You are not able to check directly because the User is being created directly in your code that you are testing.
However depending if the string can be retrieved via a public method of the User class, then you could do something like this:
$this->builder->expects($this->once())
->method('addEventSubscriber')
->with(
$this->callback(function($user) {
if (! $user instanceof User) {
return False;
}
if ($user->getName() != <Expected String>) {
return False;
}
return True;
})
);
You can use a callback check further details of User instance. You are not verifying that the string was passed to the constructor but you can check that the object is properly created.
Though this isn't the best way to go about this. Your code is not extensible because you are not able to change the User class. It would be better to pass in a User object to the method that gets passed on. This would allow you have different types of User objects in the future.

Authenticated user on constructor

After research and watch over the docs and other links, i understand perfectly why this can't be done the "old way", but i really need a workaround/solution for this:
I have a base class, it's not called directly on the request (not declared on route):
class AdminPanelController extends Controller
{
protected $_authUser;
public function __construct() {
$this->middleware(function ($request, $next) {
$this->_authUser = Auth::guard('admin')->user();
dd($this->_authUser); // ok
return $next($request);
});
}
...
protected function yoo() {
dd($this->_authUser); // auth user is null, but i need this here
}
}
I need for the authenticated user to be available on the yoo() method. The controller called directly:
Route::get('users', 'UsersController#hey');
UsersController:
class UsersController extends AdminPanelController
{
protected params;
public function __construct(Request $request) {
parent::__construct();
$this->params = $this->yoo(); // auth user is null, but i need this here
}
...
}
Note that if i call the method $this->yoo() on another place instead of the constructor the user would be available
NOTE: i also tried $request->user() (Authentication user provider [] is not defined.) but since i have a multi auth system i have to provide a guard and tried $request->guard('admin')->user(), with the result being Method guard does not exist.

Override service for specific user role

I have 3 services which should override the default services only if the user has a specific role.
Or even better. Inject the current user/security in the new services.
The service then performs the check for the user role and calls the original service.
I tried to inject security.context into it. But then $security->getToken() returns null.
In the controllers it works fine. How can i get the current user in my service? This is what i want to do:
class AlwaysVisibleNavigationQueryBuilder extends NavigationQueryBuilder
{
public function __construct(\Sulu\Component\Content\Compat\StructureManagerInterface $structureManager, $languageNamespace, SecurityContext $security)
{
if (in_array('ROLE_SULU_ADMINISTRATOR', $security->getToken()->getRoles())) {
// Show unpublished content, too
$this->published = false;
}
parent::__construct($structureManager, $languageNamespace);
}
}
At the moment of creation of the service, the securityContext was not aware of the current user. The Security is filles when the application runs and not on dependency-resolution.
The following Code works.
class AlwaysVisibleNavigationQueryBuilder extends NavigationQueryBuilder
{
protected $security;
public function __construct(\Sulu\Component\Content\Compat\StructureManagerInterface $structureManager, $languageNamespace, SecurityContext $security)
{
$this->security = $security;
parent::__construct($structureManager, $languageNamespace);
}
public function build($webspaceKey, $locales)
{
$roles = $this->security->getToken()->getRoles();
if (in_array('ROLE_SULU_ADMINISTRATOR', $roles)) {
// Show unpublished content, too
$this->published = false;
}
return parent::build($webspaceKey, $locales);
}
}
Thanks to Matteo!

Resources