Symfony2: isGranted for other entity - php

My application has the following hierarchy of roles:
ROLE_SUPER_ADMIN
ROLE_ADMIN
ROLE_USER
I have my controller that makes this work:
/**
* #Route("/admin/delete/{id}", name="_admin_delete")
* #Secure(roles="ROLE_ADMIN")
*/
public function deleteuserAction($id)
{
$user = $this->container->get('fos_user.user_manager')->findUserBy(array('id' => $id));
if (null === $user) {
throw $this->createNotFoundException('User id not found');
}
if ($user->hasRole('ROLE_ADMIN')) {
// You can not delete this user!
throw new ...
}
// delete user
$this->container->get('fos_user.user_manager')->deleteUser($user);
// ...
}
deleteuserAction is accessible to all those with ROLE_ADMIN.. but I want that they can not delete users who have the role ROLE_ADMIN or HIGHER.
There is a proper way to do this?
With this configuration you can delete ROLE_SUPER_ADMIN... yeah, you can add it to the list to block it, but with a complicated hierarchy can become difficult to manage.

If you don't want to use ACLs, another option would be to create a custom voter that handles your hierarchy. A basic voter is described here: http://symfony.com/doc/current/cookbook/security/voters.html

Related

Sylius - Dynamic change of channels from the client group

I come from Magento and I now use Sylius to modernize things a bit and have access to a less xml oriented platform because I find it really painful in 2022...
Unfortunately I did not find anything in Sylius regarding the management of prices according to the client currently logged.
So I want to use groups and channels: I added a channel relation to a user group to be able to use a channel according to the logged in user.
/**
* #ORM\Entity
* #ORM\Table(name="sylius_customer_group")
*/
class CustomerGroup extends BaseCustomerGroup
{
/**
* #ORM\ManyToOne(targetEntity=Channel::class)
*/
private $channel;
public function getChannel(): ?Channel
{
return $this->channel;
}
public function setChannel(?Channel $channel): self
{
$this->channel = $channel;
return $this;
}
}
Here is what I am trying to do with a service
services:
ChangingContextWithCustomerGroup:
class: App\Context\RequestQueryChannelContext
arguments:
- '#sylius.repository.channel'
- '#request_stack'
- '#sylius.context.customer'
tags:
- { name: sylius.context.channel, priority: 150 }
// src/Context/RequestQueryChannelContext.php
public function getChannel(): ChannelInterface
{
$request = $this->requestStack->getMainRequest();
if (!$request) {
throw new ChannelNotFoundException('Request Not Found!');
}
$customer = $this->customerContext->getCustomer();
if (!$customer instanceof Customer) {
throw new ChannelNotFoundException('Customer Not Found!');
}
$group = $customer->getGroup();
if (!$group instanceof CustomerGroup) {
throw new ChannelNotFoundException('Group Not Found!');
}
$channel = $group->getChannel();
if (!$channel instanceof ChannelInterface) {
throw new ChannelNotFoundException('Channel Not Found!');
}
return $channel;
}
My problem is that I can't get the customer on the mainRequest. It is null, so I cant have the customer => group => channel.
It works very well when I force the channel like this :
public function getChannel(): ChannelInterface
{
// ...
return $this->channelRepository->findOneByCode('fooBar');
}
so my system doesn't work. Is there a better solution?
thanks
The problem here is that the Channel context is called from the Sylius\Bundle\ShopBundle\EventListener\NonChannelLocaleListener which has priority 10 on the kernel.request event. In the channel context you want to use the customer context class, which in turn uses the TokenStorage to retrieve the user information.
The token however is not yet populated in the token storage at that point, because that happens in the firewall listener (Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener in dev environments), which has priority 8.
The solution I found is to lower the priority of the NonChannelLocaleListener to 7. That will make sure that the token is available and that the customer context can be used to retrieve the customer/shop user information.
Lowering the priority of that listener can be done by overriding the service definition in config/services.yaml:
services:
...
sylius.listener.non_channel_request_locale:
class: Sylius\Bundle\ShopBundle\EventListener\NonChannelLocaleListener
arguments:
- '#router'
- '#sylius.locale_provider'
- '#security.firewall.map'
- ['%sylius_shop.firewall_context_name%']
tags:
- { name: kernel.event_listener, event: kernel.request, method: restrictRequestLocale, priority: 7 }
Please note that this is on Sylius 1.10, so it might be possible that on other Sylius versions the priority of these listeners are slightly different. In that case, just use the bin/console debug:event-dispatcher command to figure out what the right priority should be.

How to change user's accesses depending on the authentication mechanism used in Symfony 5

Suppose I have two authentication mechanisms. How do I allow/deny a user's access to content depending on which authentication mechanism they used to authenticate themselves.
For example, let's say I have:
two type of users: admin and user such that all admins are users but users are not admins
two authentication mechanisms: admin_login and user_login
I would like to make it so that an admin has to authenticate through admin_login in order to have admin accesses. This means he would be considered a regular user if he was authenticated through user_login.
I first thought about using firewall context, but quickly realized it wasn't gonna work as I would need a firewall to support multiple contexts :
# config/packages/security.yaml
security:
# ...
firewalls:
admin:
# ...
context: // not currently supported
- admin
- user
user:
# ...
context: user
The other idea I came up with was creating a property called is_allow_admin in the User class and using it to change the way the roles are retrieved by setting it in the admin_login authenticator :
// src/Entity/User.php
// ...
class User implements UserInterface
{
// ...
private $is_allow_admin = false;
// ...
public function setIsAllowAdmin(bool $is_allow_admin)
{
$this->is_allow_admin = $is_allow_admin ;
}
public function getRoles(): array
{
if (!$this->is_allow_admin) {
return ['ROLE_USER'];
}
//...
return $roles;
}
// ...
}
// src/Security/AdminAuthenticator.php
class AdminAuthenticator extends AbstractFormLoginAuthenticator
{
//...
public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
{
$user = $userProvider->loadUserByUsername($credentials['username']);
if (!$user) {
throw new CustomUserMessageAuthenticationException('username could not be found.');
}
$user->setIsAllowAdmin(true); // <-
return $user;
}
//...
}
This unfortunately doesn't work. Everything goes smoothly with the authenticator, onAuthenticationSuccess is triggered. But somehow in the end, no user is authenticated.
I know it has to do with $this->setIsAllowAdmin(true); since it works correctly when I remove the line.
Is there another way to tackle this problem?
Thank you in advance.

Sonata Admin ACL hide element in list

After a lot of effort I was finally able to configure Sonata Admin with ACL following this guide:
https://sonata-project.org/bundles/admin/master/doc/reference/security.html
I wanted to users to be able to view and edit only items with the same country property as the user.
This is my config.yml:
parameters:
locale: en
sonata.user.admin.user.class: AppBundle\Admin\UserAdmin
sonata.admin.security.mask.builder.class: Sonata\AdminBundle\Security\Acl\Permission\MaskBuilder
# SonataAdminBundle Configuration
sonata_admin:
security:
handler: sonata.admin.security.handler.acl
role_admin: ROLE_ADMIN
role_super_admin: ROLE_SUPER_ADMIN
# acl security information
information:
GUEST: [VIEW, LIST]
STAFF: [EDIT, LIST, CREATE]
EDITOR: [OPERATOR, EXPORT]
ADMIN: [MASTER]
# permissions not related to an object instance and also to be available when objects do not exist
# the DELETE admin permission means the user is allowed to batch delete objects
admin_permissions: [CREATE, LIST, DELETE, UNDELETE, EXPORT, OPERATOR, MASTER]
# permission related to the objects
object_permissions: [VIEW, EDIT, DELETE, UNDELETE, OPERATOR, MASTER, OWNER]
I created an AclVoter in order to show/hides elements:
services:
security.acl.voter.country_owned_permissions:
class: AppBundle\Security\Authorization\Voter\CountryOwnedAclVoter
arguments:
- "#security.acl.provider"
- "#security.acl.object_identity_retrieval_strategy"
- "#security.acl.security_identity_retrieval_strategy"
- "#security.acl.permission.map"
- "#logger"
tags:
- { name: monolog.logger, channel: security }
- { name: security.voter, priority: 255 }
public: false
This is the actual class:
<?php
namespace AppBundle\Security\Authorization\Voter;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Acl\Voter\AclVoter;
class CountryOwnedAclVoter extends AclVoter
{
public function supportsClass($class)
{
// support the Class-Scope ACL for votes with the custom permission map
// return $class === 'Sonata\UserBundle\Admin\Entity\UserAdmin' || is_subclass_of($class, 'FOS\UserBundle\Model\UserInterface');
// if you use php >=5.3.7 you can check the inheritance with is_a($class, 'Sonata\UserBundle\Admin\Entity\UserAdmin');
// support the Object-Scope ACL
return is_subclass_of($class, 'AppBundle\Model\CountryOwnedInterface');
}
public function supportsAttribute($attribute)
{
return in_array($attribute, array(
'LIST',
'VIEW',
'EDIT',
'DELETE',
'EXPORT',
));
}
public function vote(TokenInterface $token, $object, array $attributes)
{
if (!$this->supportsClass(get_class($object))) {
return self::ACCESS_ABSTAIN;
}
foreach ($attributes as $attribute) {
if ($this->supportsAttribute($attribute)) {
if ($object->getCountry() != $token->getUser()->getCountry()) {
//if ($object->isSuperAdmin() && !$token->getUser()->isSuperAdmin()) {
// deny a non super admin user to edit a super admin user
return self::ACCESS_DENIED;
}
}
}
// use the parent vote with the custom permission map:
// return parent::vote($token, $object, $attributes);
// otherwise leave the permission voting to the AclVoter that is using the default permission map
return self::ACCESS_ABSTAIN;
}
}
It seems to work fine since a user can only edit items which have the same country as the users. The problem is that he can still view the items in the list.
What am I doing wrong?
As specified in the official documentation I just needed to install a specific bundle:
5.4.6. LIST FILTERING
List filtering using ACL is available as a third party bundle: CoopTilleulsAclSonataAdminExtensionBundle. When enabled,
the logged in user will only see the objects for which it has the VIEW
right (or superior).
This will suffice:
composer require tilleuls/acl-sonata-admin-extension-bundle
In AppKernel.php:
// ACL list filter
new CoopTilleuls\Bundle\AclSonataAdminExtensionBundle\CoopTilleulsAclSonataAdminExtensionBundle(),

How I can add a "roles" to a "path" dynamically in Symfony2?

In SF2 I have the following scenario, according to the site itself documentation:
app\config\security.yml
security:
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_ADMIN }
I need to manually add each "path" and "roles" that can access this "path".
How can I do this dynamically?
Like RBAC on Yii2:
Is there any ready Bundle or something in SF2 own documentation that allows this? As the hypothetical example:
app\config\security.yml
security:
access_control:
type: dynamically
If you want to make adding the roles easier you can use annotations.
Your question asks for dynamic security, which is complicated. The routes, and all roles, are compiled during the cache warmup phase. So, for this to work you'll first need to store your dynamic values. The database would be a good option for this. Here I am only going to show how to check the roles, the actual role manipulation I'll leave to you.
The easiest method is to inject the authorization checker into your controller.
services:
acme_controller:
class: "AcmeDemoBundle\Controller"
arguments: ["#security.authorization_checker"]
Then check the roles in the action(s):
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
public function listAction()
{
$role = /* load your role here */;
if (false === $this->authorizationChecker->isGranted($role)) {
throw new AccessDeniedException();
}
// ...
}
The above will cause duplicated code if you want it in many controllers, so you could also create a voter:
services:
acme.route.voter:
class: AcmeDemoBundle\RouteVoter
arguments:
- #security.role_hierarchy
public: false
tags:
- { name: security.voter, priority: 300 }
Example:
public function __construct ( RoleHierarchyInterface $roleHierarchy )
{
$this->roleVoter = new RoleHierarchyVoter( $roleHierarchy );
}
public function vote ( TokenInterface $token, $object, array $attributes )
{
if ( !$object instanceof Request ) {
return VoterInterface::ACCESS_ABSTAIN;
}
$requestUri = $object->getPathInfo();
if ( isset($this->votes[ $requestUri ]) ) {
return $this->votes[ $requestUri ];
}
$roles = /* load your roles */;
return $this->votes[ $requestUri ] = $this->roleVoter->vote( $token, $object, $roles );
}
Another method would be to replace the router service with your own implementation. This is the approach taken my the CMF Bundle.
You can manage role/route relation dynamically like this :
You create a listener on the kernel
<service id="toto.security.controller_listener" class="Administration\SecurityBundle\EventListener\SecurityListener">
<tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
<argument type="service" id="service_container" />
</service>
and after in the listener you implement this method
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
$request = $event->getRequest();
$baseUrl = $request->getBaseUrl();
$requestUri = $request->getRequestUri();
$route = str_replace($baseUrl, "", $requestUri);
//you put your check logic
//you can implement a relation beetween routes and roles/ users in database etc. you got the entire control on what you do
if(!$this->accessMananager->isGrantAccess(User $user, $route)){
throw new AccessDeniedException("blah blah blah")
}
}
since this listener will always be called before any of your controller, consider creating a cache system

How can I display a users profile to another user with the FOUserbundle in Symfony2?

I would like to give the user a possibility to display other users, so that they can check out e.g. the level of other users.
Is there a way to give each user a specific route? As an example: /username/profile ?
Should I create a new view and a new controller for this?
Thanks in advance for your help!
Create a route and controller such as /users which lists all Users. Only give access to /users to a particular role. Create a further route and controller such as users/{id} and have your controller extract the necessary information for that user that you wish to display. The trick here is that you restrict it to super admin users, for example
So your security.yml may look like this:
role_hierarchy:
...
ROLE_SUPER_ADMIN: ROLE_SUPER_ADMIN
access_control:
...
- { path: ^/users, role: ROLE_SUPER_ADMIN }
Here is how I did it with the help of #Alex:
The user controller which handles the displaying of all list with all FOSUserBundle users + the displaying of each user individually:
/**
* User controller.
*/
class UsersController extends Controller
{
/**
* Lists all CategoryShop entities.
*
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('DbeUserBundle:User')->findAll();
return $this->render('DbeUserBundle:Users:userlist.html.twig', array(
'entities' => $entities,
));
}
/**
* Finds and displays a CategoryShop entity.
*
*/
public function showAction($id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('DbeUserBundle:User')->find($id);
return $this->render('DbeUserBundle:Users:userprofile.html.twig', array(
'entity' => $entity,
));
}
Here is the routes for the two actions:
<route id="fos_user_profile_show_users" pattern="/users">
<default key="_controller">FOSUserBundle:Users:index</default>
</route>
<route id="fos_user_profile_show_user" pattern="/user/{id}">
<default key="_controller">FOSUserBundle:Users:show</default>
</route>
I finnaly allowed even non-authenticated users to list all users. So there was no need for a setting in the firewall.

Categories