Custom decision manager authorisation in Symfony 4 - symfony

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.

Related

How to use Symfony parameters in User Entity

This is a Symfony 3 project.
In User entity, i need to implement the method getRoles(). I have a private member $roles that is an array and I added it into serialize and unserialize methods.
public function getRoles()
{
if (count($this->roles) == 0) {
$this->roles = { ... read from db ... };
}
return $this->roles;
}
A issue I'm facing is that in ... read from db ... part, I have to use some parameters from parameters.yml. Usually, $this->container->getParameter(...) does the job. Unfortunately, from an entity I have no access to the container.
My question is: How can I access parameters.yml from an Entity?
Can I somehow inject the required parameters?
Another question is: do I need to serialize $roles as well or should they be read on every request?
--- EDIT ---
That logic seems to me correctly placed.
getRoles() function is supposed to get user's role to Security bundle. It accomplishes it by querying private members and ORM relations. The only problem is that I need do identify certain groups, as they don't have similar names in all deployments. Thats why I need the parameters.yml.
Here is a fragment from User entity, which implements AdvancedUserInterface.
public function getRoles() {
$ADMIN_GRP = "ADMIN_GROUP"; // I need this from parameters.yml
$SUPPORT_GRP = "SUPPORT_GROUP"; // I need this from parameters.yml
$roles = ['ROLE_USER'];
foreach ($this->memberships as $m) {
if ($m->getGroupId() == $SUPPORT_GRP)
array_push($roles, "ROLE_SUPPORT");
if ($m->getGroupId()) == $ADMIN_GRP)
array_push($roles, "ROLE_ADMIN");
}
return $roles;
}
as malcolm said, you should not be touching the EntityManager, from inside your entity, that logic is NOT correctly placed.
also, you should not read parameters.yml from inside your entity
(you COULD)
use Symfony\Component\Yaml\Yaml;
$value = Yaml::parse(file_get_contents('/path/to/file.yml'));
but you really SHOULDNT use the above approach
(you could also add constants to the user entity ...)
Why not adding a group label to your Membership entity ? So you can do...
public function getRoles() {
$roles = ['ROLE_USER'];
foreach ($this->memberships as $m) {
if ($m->getGroupRole() == 'ROLE_SUPPORT')
array_push($roles, "ROLE_SUPPORT");
if ($m->getGroupRole()) == 'ROLE_ADMIN')
array_push($roles, "ROLE_ADMIN");
}
return $roles;
}

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;
}
}

How to disable Blameable-behaviour programmatically in Symfony2

I'm trying to run a console command in symfony2 in which some properties of a certain class are being updated. One of the properties has got a corresponding reviewedBy-property which is being set by the blameable-behaviour like so:
/**
* #var bool
* #ORM\Column(name="public_cmt", type="boolean", nullable=true)
*/
private $publicCmt;
/**
* #var User $publicCmtReviewedBy
*
* #Gedmo\Blameable(on="change", field="public_cmt")
* #ORM\ManyToOne(targetEntity="My\Bundle\EntityBundle\Entity\User")
* #ORM\JoinColumn(name="public_cmt_reviewed_by", referencedColumnName="id", nullable=true)
*/
private $publicCmtReviewedBy;
When i run the task there's no user which can be 'blamed' so I get the following exception:
[Doctrine\ORM\ORMInvalidArgumentException]
EntityManager#persist() expects parameter 1 to be an entity object, NULL given.
However I can also not disable blameable because it's not registered as a filter by the time i start the task and programmatically trying to set the user through:
// create the authentication token
$token = new UsernamePasswordToken(
$user,
null,
'main',
$user->getRoles());
// give it to the security context
$this->getService('security.context')->setToken($token);
doesn't work. Anyone got an idea?
If you use the StofDoctrineExtensionsBundle you can simply do :
$this->container->get('stof_doctrine_extensions.listener.blameable')
->setUserValue('task-user');
see : https://github.com/stof/StofDoctrineExtensionsBundle/issues/197
First of all, I'm not sure if 'field' cares if you use the database column or the property, but you might need to change it to field="publicCmt".
What you should do is override the Blameable Listener. I'm going to assume you are using the StofDoctrineExtensionsBundle. First override in your config:
# app/config/config.yml
stof_doctrine_extensions:
class:
blameable: MyBundle\BlameableListener
Now just extend the existing listener. You have a couple options - either you want to allow for NULL values (no blame), or, you want to have a default user. Say for example you want to just skip the persist and allow a null, you would override as such:
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
public function getUserValue($meta, $field)
{
try {
$user = parent::getUserValue($meta, $field);
}
catch (\Exception $e) {
$user = null;
return $user;
}
protected function updateField($object, $ea, $meta, $field)
{
if (!$user) {
return;
}
parent::updateField($object, $ea, $meta, $field);
}
}
So it tries to use the parent getUserValue() function first to grab the user, and if not it returns null. We must put in a try/catch because it throws an Exception if there is no current user. Now in our updateField() function, we simply don't do anything if there is no user.
Disclaimer - there may be parts of that updateField() function that you still need...I haven't tested this.
This is just an example. Another idea would be to have a default database user. You could put that in your config file with a particular username. Then instead of returning null if there is no user from the security token, you could instead grab the default user from the database and use that (naturally you'd have to inject the entity manager in the service as well).
Slight modification of the above answer with identical config.yml-entry: we can check if a user is set and if not: since we have access to the object-manager in the updateField-method, get a default-user, set it and then execute the parent-method.
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
protected function updateField($object, $ea, $meta, $field)
{
// If we don't have a user, we are in a task and set a default-user
if (null === $this->getUserValue($meta, $field)) {
/* #var $ur UserRepository */
$ur = $ea->getObjectManager()->getRepository('MyBundle:User');
$taskUser = $ur->findOneBy(array('name' => 'task-user'));
$this->setUserValue($taskUser);
}
parent::updateField($object, $ea, $meta, $field);
}
}

Symfony2 ACL access to multiple objects for multiple users

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.

Dynamically adding roles to a user

We are using Symfony2's roles feature to restrict users' access to certain parts of our app. Users can purchase yearly subscriptions and each of our User entities has many Subscription entities that have a start date and an end.
Now, is there a way to dynamically add a role to a user based on whether they have an 'active' subscription? In rails i would simply let the model handle whether it has the necessary rights but I know that by design symfony2 entities are not supposed to have access to Doctrine.
I know that you can access an entity's associations from within an entity instance but that would go through all the user's subscription objects and that seems unnecessaryly cumbersome to me.
I think you would do better setting up a custom voter and attribute.
/**
* #Route("/whatever/")
* #Template
* #Secure("SUBSCRIPTION_X")
*/
public function viewAction()
{
// etc...
}
The SUBSCRIPTION_X role (aka attribute) would need to be handled by a custom voter class.
class SubscriptionVoter implements VoterInterface
{
private $em;
public function __construct($em)
{
$this->em = $em;
}
public function supportsAttribute($attribute)
{
return 0 === strpos($attribute, 'SUBSCRIPTION_');
}
public function supportsClass($class)
{
return true;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
// run your query and return either...
// * VoterInterface::ACCESS_GRANTED
// * VoterInterface::ACCESS_ABSTAIN
// * VoterInterface::ACCESS_DENIED
}
}
You would need to configure and tag your voter:
services:
subscription_voter:
class: SubscriptionVoter
public: false
arguments: [ #doctrine.orm.entity_manager ]
tags:
- { name: security.voter }
Assuming that you have the right relation "subscriptions" in your User Entity.
You can maybe try something like :
public function getRoles()
{
$todayDate = new DateTime();
$activesSubscriptions = $this->subscriptions->filter(function($entity) use ($todayDate) {
return (($todayDate >= $entity->dateBegin()) && ($todayDate < $entity->dateEnd()));
});
if (!isEmpty($activesSubscriptions)) {
return array('ROLE_OK');
}
return array('ROLE_KO');
}
Changing role can be done with :
$sc = $this->get('security.context')
$user = $sc->getToken()->getUser();
$user->setRole('ROLE_NEW');
// Assuming that "main" is your firewall name :
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, null, 'main', $user->getRoles());
$sc->setToken($token);
But after a page change, the refreshUser function of the provider is called and sometimes, as this is the case with EntityUserProvider, the role is overwrite by a query.
You need a custom provider to avoid this.

Resources