SecurityVoter based on class instead of object - symfony

It's a question about SecurityVoter.
With Symfony 2.6, you can create simpler security voters to check if user have specifics permissions on a object:
protected function isGranted($attribute, $object, $user = null)
{
switch ($attribute) {
case self::VIEW:
return $object->getAuthor() === $user;
case self::EDIT:
return $object->getAuthor() === $user && time() - $object->getAddedAt()->getTimestamp() <= TicketMessage::PERMIT_EDIT_GAP;
default:
return false;
}
}
That works. But what if I want to check an attributes based on the class instead of a object ? For example, a 'create' permissions for creating a new object that not exists for the moment.
Code sample:
is_granted('create', 'AppBundle\Entity\MyEntity')
or:
is_granted('create', 'AppBundle:Entity')
Is there a way to do that ?
Thanks.

Please, read the documentation on Symfony Best Practicles and Symfony Voters for better understanding the concepts of Symfony.
If you really need to check permissions based only on class attributes, then the decision is going to be constant. So, the recommended way to control your application's logic is described in Symfony Best Practicles:
Best Practice: Use constants to define configuration options that
rarely change.
As you can see, there are two recommended ways of doing what you want:
use constants (For example you can define some constants in your
custom YourBundle\SecurityManager)
use parameters.yml for configuration

Related

What is the best way to create a singleton entity in Symfony 4?

I want to create a settings page, which only has a form in it. If the form is submitted it only updates settings entity but never creates another one. Currently, I achieved this like:
/**
* #param SettingsRepository $settingsRepository
* #return Settings
*/
public function getEntity(SettingsRepository $settingsRepository): Settings
{
$settings = $settingsRepository->find(1);
if($settings == null)
{
$settings = new Settings();
}
return $settings;
}
In SettingsController I call getEntity() method which returns new Settings entity (if the setting were not set yet) or already existing Settings entity (if setting were set at least once).
However my solution is quite ugly and it has hardcoded entity id "1", so I'm looking for a better solution.
Settings controller:
public function index(
Request $request,
SettingsRepository $settingsRepository,
FlashBagInterface $flashBag,
TranslatorInterface $translator,
SettingsService $settingsService
): Response
{
// getEntity() method above
$settings = $settingsService->getEntity($settingsRepository);
$settingsForm = $this->createForm(SettingsType::class, $settings);
$settingsForm->handleRequest($request);
if ($settingsForm->isSubmitted() && $settingsForm->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($settings);
$em->flush();
return $this->redirectToRoute('app_admin_settings_index');
}
return $this->render(
'admin/settings/index.html.twig',
[
'settings_form' => $settingsForm->createView(),
]
);
}
You could use Doctrine Embeddables here.
Settings, strictly speaking, should not be mapped to entities, since they are not identifiable, nor meant to be. That is, of course, a matter of debate. Really, a Settings object is more of a value object than an entity. Read here for more info.
So, in cases like these better than having a one to one relationship and all that fuzz, you probably will be fine with a simple Value Object called settings, that will be mapped to the database as a Doctrine Embeddable.
You can make this object a singleton by creating instances of it only in factory methods, making the constructor private, preventing cloning and all that. Usually, it is enough only making it immutable, meaning, no behavior can alter it's state. If you need to mutate it, then the method responsible for that should create a new instance of it.
You can have a a method like this Settings::createFromArray() and antoher called Settings::createDefaults() that you will use when you new up an entity: always default config.
Then, the setSettings method on your entity receieves only a settings object as an argument.
If you don't like inmutablity, you can also make setter methods for the Settings object.

SonataAdminBundle - check changes in `preUpdate` hook

Is it possible to check if field was changed on preUpdate hook? I'm looking for something like preUpdate hasChangedField($fieldName) Doctrine functionality. Any ideas?
This question is a bit similar to this one
Your solution is just to compare the field of the old object with the new one and see where it differs.
So for example:
public function preUpdate($newObject)
{
$em = $this->getModelManager()->getEntityManager($this->getClass());
$originalObject = $em->getUnitOfWork()->getOriginalEntityData($newObject);
if ($newObject->getSomeField() !== $originalObject['fieldName']) {
// Field has been changed
}
}
For me the best approach is this in Sonata Admin:
$newField = $this->getForm()->get('field')->getData();
$oldField = $this->getForm()->get('field')->getConfig()->getData();
You shouldn't use unit of work unless there is no option. Also, if you have a not mapped field, you can't access it by entity object.
In a normal Doctrine lyfe cycle event, the best option is Doctrine preupdate event doc

SonataUser - Restricting selectable groups

It seems like a basic need but can't find any resource on the subject.
In the User Admin form, I need to filter the selectable groups depending on current user's roles.
Let's say we should only be able to select groups for which we are granted to all attached roles.
I think ACLs are not required here.
Could I use a voter ?
As it seems to me like a common need, could someone point me to any resource ?
As I can see the maddening rush to respond to my question, I'm going to give my humble contribution.
I have not been able to use ACL nor voters for that.
So I did a custom type extending choice
This type retrieves the groups from a service with security.context and doctrine injected.
This service have a function for gathering groups that have this little algorithm:
public function allExistingGroups()
{
$finalGroupsList = array();
/* Get all existing groups */
$groups = $this->doctrine->getRepository('SonataUserBundle:Group')
->findAll();
foreach ($groups as $group) {
$isGranted = true;
foreach ($group->getRoles() as $role) {
if (!$this->securityContext->isGranted($role)) {
$isGranted = false;
}
}
/* User is granted, so he can see the group */
if ($isGranted) {
$finalGroupsList[$group->getId()] = $group->getName();
}
}
return $finalGroupsList;
}
The user will now see only groups to which he is granted to all the roles.
For sure there should be a prettier solution, but this did the trick for me.

Symfony2 Application Architecture - how to make a function available in all controllers?

I'm building an application where users are tied to accounts (as in, multiple users all share an account), then other entities - lets call them products are tied to the accounts. The products are associated with the accounts and only users that are tied to that account can edit the products.
The difference being in my case, there are many different entities being shared in the same model.
If it was just the one (product) entity, it wouldn't be a problem to have a method in my ProductRepository like:
public function checkOwnership($id, $account)
{
$count = $this->createQueryBuilder('s')
->select('count(s.id)')
->where('s.account = :acc')
->andWhere('s.id = :id')
->setParameters(array('id' => $id, 'acc' => $account))
->getQuery()
->getSingleScalarResult();
if($count == 0)
throw $this->createNotFoundException('ye..');
return;
}
To make sure the id of the product is tied to the user's account.
The account argument is passed to this function by using:
$this->getUser();
In the controller.
What is the best way for me to make this function available to the other entities to prevent code duplication? There are (currently) only 3 other entities, so it wouldn't be the end of the world to just have it in each repository, but I'm just wondering if there were a way to create a 'common' or global repository that follows good standards? I'd sure like to know about it.
Edit:
Or have I just completely over-thought this? Can I just create a 'Common' directory in my 'MainBundle', define a namespace and add a use statement at the start of my controllers that need access to the function?
I hope I fully understand your question.
Solution one, duplication, easy: let #ParamConverter do the job (automatic response to 404 if doesn't exist)
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
/**
* #Route("/pets/{id}")
*/
public function showAction(Pet $pet)
{
// Current logged user
$currentUser = $this->get('security.context')->getToken()->getUser();
// Owner
$ownerUser = $pet->getUser();
if(!$currentUser->equals($ownerUser)) {
// 401, 403 o 404 depends on you
}
}
Solution two, DRY, a bit harder: use JMSAOPBundle and define an interceptor that intercepts all request to you controller and actions (show, delete, etc.). The pointcut (connected to the interceptor) will get the current user from the context and the owner from the entity. Then you do the check...

In Drupal, how to change the values passed to Pathauto?

I have Pathauto configured to generate an alias based on the title of a node, for a specific content type. The problem is that I want to make small changes in this title before Pathauto uses it to generate the alias.
The first comment in this post suggests the use of hook_token_values, but I couldn't really understand how to use it, even after reading the docs. In my tests, when I implement this hook, the alias generated is always "array", which means I'm missing something.
Any help? Thanks.
It might be that you missed to implement hook_token_list as well. Providing a new token is a two step process:
Implement hook_token_list to declare the tokens you are going to provide. This will just be the name of the tokens, along with a short explanation, and the information to what type of objects the tokens will apply (e.g. node, user, taxonomy, ...)
Implement hook_token_value to actually generate the content of the tokens. This will be called when the tokens are to be replaced with the content they should stand for.
As you just want to provide an alternative version of the title token already provided by the token module, it is probably best to just copy the relevant portions from token_node.inc, stripped down to the relevant cases and adjusted to be used in another module:
/**
* Implementation of hook_token_list().
*/
function yourModule_token_list($type = 'all') {
if ($type == 'node' || $type == 'all') {
$tokens['node']['yourModule-title'] = t('Node title (customized version by yourModule)');
return $tokens;
}
}
This simply says that yourModule provides a token for node objects, named yourModule-title, along with a short description. The main work gets done in the other hook:
/**
* Implementation of hook_token_values().
*/
function yourModule_token_values($type, $object = NULL, $options = array()) {
$values = array();
switch ($type) {
case 'node':
$node = $object;
// TODO: Replace the check_plain() call with your own token value creation logic!
$values['yourModule-title'] = check_plain($node->title);
break;
}
return $values;
}
This will be called whenever the tokens for node objects are needed, with the node in question being passed as the $object parameter (for a user token, the $type would be 'user', and $object would be the user object, and so on for other types). What it does is creating an array of values, keyed by the token name, with the replacement for that token as the value. The original code from token_node.inc just runs the title through check_plain(), so this would be the place to insert your own logic.
In Drupal 7, the token functionality has been moved to core. Tokens are implemented by the hook_tokens and hook_token_info methods. For usage examples, follow the links provided, and look for links to functions that implement hook_tokens and hook_token_info… I found the statistics_tokens and statistics_token_info functions helpful in understanding how this hook works.
It's probably also worth noting that this hook needs to be implemented by a module… my first attempt I dropped my test functions into the theme's template.php, only to have nothing happen at all :-p

Resources