Symfony2 ACL access to multiple objects for multiple users - symfony

I'm busy with a Symfony2 application that needs some ACL permissions.
I'm a newbie with Symfony2, so not sure if i'm looking at this the right way.
I have multiple clients, each with multiple accounts.
I have a super admin (ROLE_SUPER_ADMIN) that have access to all clients and all accounts.
Then I have an admin role (ROLE_ADMIN), which will only be allowed access to a specific client and all accounts for that clients.
Then there is agents (ROLE_AGENT), which should only have permission to certain accounts for clients.
I saw on the symfony docs that to give a user access to a specific object, I can use the following code:
// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($account);
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$securityContext = $this->get('security.context');
$user = $securityContext->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
So when creating a new account, I can give the current logged-in user access to the newly created account.
But how do I grant access to all the other users of the client access to the account?
I don't want to loop through all users and run the above code for every user.
So for example when viewing all clients, I need to know which clients the user has access to, or when viewing the accounts, I need to know which accounts the user has access to.
Also when adding a new user to a client, the user automatically need to have access to all the accounts for that client.
As a side note, I only need to know if the user has access to the account/client. If a user has access, then they are automatically allowed to view/edit/delete etc.

For this case I used a custom security service that verifies ManyToMany relations between entities. It`s not the ideal decision but keep in mind.
First we need to make listener that will be triggered at every controller action.
class SecurityListener
{
protected $appSecurity;
function __construct(AppSecurity $appSecurity)
{
$this->appSecurity = $appSecurity;
}
public function onKernelController(FilterControllerEvent $event)
{
$c = $event->getController();
/*
* $controller passed can be either a class or a Closure. This is not usual in Symfony2 but it may happen.
* If it is a class, it comes in array format
*/
if (!is_array($c)) {
return;
}
$hasAccess = $this->appSecurity->hasAccessToContoller($c[0], $c[1], $event->getRequest());
if(!$hasAccess) {
throw new AccessDeniedHttpException('Access denied.');
}
}
}
In service we have access to request, controller instance and called action. So we can make a decision have user access or not.
class AppSecurity
{
protected $em;
protected $security;
/** #var $user User */
protected $user;
public function __construct(EntityManager $em, SecurityContext $security)
{
$this->em = $em;
$this->security = $security;
if($security->getToken() !== null && !$security->getToken() instanceof AnonymousToken) {
$this->user = $security->getToken()->getUser();
}
}
/**
* #param $controller
* #param string $action
*/
public function hasAccessToContoller($controller, $action, Request $request)
{
$attrs = $request->attributes->all();
$client = $attrs['client'];
/* db query to check link between logged user and request client */
}
}
If you are using very nasty annotations like ParamConverter you can easily extract ready to use entites from request.

Related

How to get the currently logged in User in EasyAdmin

How can I fetch the currently logged in User from anywhere within the Backend code? For example I have an EventSubscriber class and want to fetch it from there.
How can I do that w/o the help of i.e. AbstractController?
Symfony AbstractController is the core of most Controllers. Including EasyAdmin crud controller (XXXCrudController) extends AbstractController so you can access the same methods.
One of those is getUser() which return the current logged in user.
* Get a user from the Security Token Storage.
*
* #return UserInterface|null
*
* #throws \LogicException If SecurityBundle is not available
*
* #see TokenInterface::getUser()
*/
protected function getUser()
{
if (!$this->container->has('security.token_storage')) {
throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".');
}
if (null === $token = $this->container->get('security.token_storage')->getToken()) {
return null;
}
// #deprecated since 5.4, $user will always be a UserInterface instance
if (!\is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return null;
}
return $user;
}
So when trying to get the logged used in a controller, just use this method.
If you want to get the same thing, but for example in a service, you can basically do the same as what the method actually does by using the service injection with TokenStorageInterface to access the TokenStorage service which can get the current user.
So in your event subscriber, add TokenStorageInterface in your constructor to use it to first get the token and then your user. You may have to add another check to see if there is an user logged in (by checking if there is a token for example)
//YourService.php
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
private $tokenStorage
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function yourMethod()
{
//get token then user
$user = $tokenStorage->getToken()->getUser();
}

Custom decision manager authorisation in Symfony 4

I have a specific authorisation system in my application (asked by my managers). It is based on Joomla. Users are attached to usergroups. Every action (i.e page) in my application are resources and for each resources I have set an access level. I have then to compare the resource access level with the usergroups of the current user to grant access or not to this specific resource.
All those informations are stored in database which are in return entities in Symfony :
User <- ManyToMany -> Usergroups
Menu (all resources with path and access level)
I thought about the Voter system. It is kind alike of what I would want, I think. Can I hijack the support function for this ?
protected function supports($user, $resource)
{
//get usergroups of the $user => $usergroups
//get the access level of the resource => $resource_access
// if the attribute isn't one we support, return false
if (!in_array($usergroups, $resource_access)) {
return false;
}
return true;
}
The get the usergroups and the access level of the resource I will have to do some queries in the database. To use this, then I would to use the denyAccessUnlessGranted() function in all my controller (seems redundant by the way) ?
Do you think it would work or there is another system more suited for this case ? I thought of doing the control in a listener to the kernel.request event too.
Hope I am clear enough, I'm new to symfony and still have some issues to understand how everything are related and working.
The voter component should be a good fit for this, as its a passive approach that lets you implement any logic in a way where its fixable through code, without modifying any database specific acl tree not managed by symfony itself.
Voters are called if you use denyAccessUnlessGranted() or isGranted() either through code, annotation or twig.
Lets take a look at how you want to check if the current user has access to view the index page:
class SomeController {
public function index() {
$this->denyAccessUnlessGranted('VIEW', '/index');
// or use some magic method to replace '/index' with wathever you require,
// like injecting $request->getUri(), just make sure your voter can
// parse it quickly.
// ...
}
}
Now build the a very simple voter:
class ViewPageVoter extends Voter
{
/**
* #var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
protected function supports($attribute, $subject)
{
return is_string($subject) && substr($subject, 0, 1) === '/';
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$currentUser = $token->getUser();
if(!$currentUser) {
// no user or authentication, deny
return false;
}
// Do the query to see if the user is allowed to view the resource.
// $this->em->getRepository(...) or
// $this->em->getConnection()
//
// $attribute = VIEW
// $subject = '/index'
// $currentUser = authenticated user
// return TRUE if allowed, return FALSE if not.
}
}
As a nice bonus you can easily see additional details on security voters in the /_profiler of that request, also indicating their respective vote on the subject.

Retrieve current user in Symfony app while respecting LoD

I'm having some issues understanding how the Law of Demeter should be applied in some cases with Symfony's DI system.
I have some factory that requires to access current logged in user in the application. To do that I need to require #security.token_storage to inject it as a constructor argument.
But in my factory, to access the user I will need to do : $tokenStorage->getToken()->getUser(), and worst, if I want to access some property of my user, I will need to dive one level deeper.
How would you fix this issue according to the law of demeter ?
Here is a sample of my code :
class SomeFactory
{
/**
* #var User
*/
private $currentUser;
/**
* #param TokenStorageInterface $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->currentUser = $this->setCurrentUser($tokenStorage);
}
/**
* #param TokenStorageInterface $tokenStorage
*/
protected function setCurrentUser(TokenStorageInterface $tokenStorage)
{
if ($tokenStorage->getToken()
&& $tokenStorage->getToken()->getUser()
&& in_array('ADMIN_ROLE', $tokenStorage->getToken()->getUser()->getRoles())
) {
$this->currentUser = $tokenStorage->getToken()->getUser();
}
}
}
I hope i am being clear.
Thank you very much :)
It seems that in DI factories the session has not been initialized, which makes the current user token unavailable, at this point.
What I did to make it work:
Register a new event listener for kernel.event:
services:
before_request_listener:
class: App\EventListener\MyRequestListener
tags:
-
name: kernel.event_listener
event: kernel.request
method: onKernelRequest
In MyRequestListener I've added my service as dependency (which invokes the factory) and provides the service, with incomplete data. Also I require Security:
public function __construct(\Symfony\Component\Security\Core\Security $security, MyService $service)
{
$this->security = $security;
$this->service = $service;
}
Now, in MyRequestListener::onKernelRequest I can access the user's data and add/change the incomplete properties of my service.
public function onKernelRequest(): void
{
if ($user = $this->security->getUser()) {
$this->service->whatever = $user->whatever;
}
}
Because Symfony uses the same instance of MyService, those modification will be available in all further services.
But keep in mind, that your service, also needs to deal with the incomplete data, when no active user session is existing (e.g. because no user is logged in, or on CLI).

Disable user instantly in symfony security

AdvancedUserInterface implement has isEnabled method for the User entity. But user properties storing in session. Disabling a user wont work until re-login.
So i need the clear specific user session by user id.
Or, i need the check database for refresh serialized user data.
What is the correct way and how can i do?
I had the same problem with FOSUserBundle, where I would disable a user but if they were logged in they could continue under the existing session. The disable only took effect when they tried to log in again.
To get around it I found a different way of doing this using a Security Voter. I created a custom voter that runs each time you call the "->isGranted" checker. I check for isGranted on every page for various ROLE levels. This voter then checks if the user isEnabled, if they aren't the voter votes to fail and disallows the user, returning them to the login screen.
My Custom Voter:
namespace AppBundle\Security;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/**
* Purpose: A Voter that checks if the user is still enabled. Default FOSUserBundle behavior allows disabled users to
* continue under their existing session. This voter is designed to prevent that
* Created by PhpStorm.
* User: Matt Emerson
* Date: 2/17/2018
* Time: 1:24 PM
*/
class userVoter extends Voter
{
protected function supports($attribute, $subject)
{
//this Voter is always available
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
} elseif ($user->isEnabled() === false) {
// the user is not enabled; deny access
return false;
}
//otherwise return true
return true;
}
}

symfony : best way to set a user with a relationship

I'm very new in development and symfony. I wonder me what's the best way to code this below.
I've 2 entities (user and account). There is a relation between them (create an account requiers a user).
I wonder me what is the best way to set the user in account entity (prepersist, controller, __construct) when I'm adding an new account ?
PREPERSIST
First, I didn't find anything to set the user with prepersit method. Is there a way ?
Something like that :
/**
* #ORM\PrePersist
*/
public function prePersist()
{
$this->user = $this->get('security.context')->getToken()->getUser();
$this->updatedAt = new \Datetime("now");
$this->isActive = false;
}
CONTROLLER
...
$user = new User();
$account = new Account();
$account->setUser($user);
...
CONSTRUCTOR
/* Entity account */
...
public function __construct($user)
{
$this->user = $user;
}
...
/* Controller account */
...
$account = new Account($this->get('security.context')->getToken()->getUser())
...
Hope you can help me.
Based on your code above, you don't need to hook into a doctrine event to accomplish what you want. You can create the association in the controller before persisting the Account object.
If you are using the Symfony security component, obtaining the user in the controller is as simple as $this->getUser(). You can inject User via the Account constructor method __construct($user) or a setter method setUser($user). Although the constructor method is more efficient, either way is correct.
And to persist the Account object to your database from within the controller:
$em = $this->getDoctrine()->getManager();
$em->persist($account);
$em->flush();
I would recommend creating Doctrine2 Listener / Subscriber and register it as a service, than listen to prePersist event of Account entity. You can inject any other needed services in listeners / subscribers.
All information you need can be found on: http://symfony.com/doc/current/cookbook/doctrine/event_listeners_subscribers.html

Resources