Disable user instantly in symfony security - symfony

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

Related

Symfony how to use annotation #Security

I cant find any clear posts on how to use the annotation #Security of symfony.
What are the parameters i can use? And most important, how can i secure a controller from guests and only accessible for users?
Currently i have
/**
*
* #Route("/reseller/create/", name="app_reseller_create", methods={"POST", "GET"})
* #IsGranted("ROLE_ADMIN")
*/
public function create(Request $request): Response
{
}
#IsGranted
If you only want to check if a user is logged in, you can use a special attribute instead of a role. For the full controller you must set it over the class definition.
/**
* #IsGranted("IS_AUTHENTICATED_FULLY")
*/
class MyClass {
There are some special attributes that can use everywhere you can use ROLE_.
IS_AUTHENTICATED_FULLY is logged in.
IS_AUTHENTICATED_REMEMBERED is logged in or have an remembered cookie.
IS_AUTHENTICATED_ANONYMOUSLY any user have this
IS_ANONYMOUS only guests
IS_REMEMBERED only users with the rmembered cookie
IS_IMPERSONATOR only users that impersonating another user in the session.
The IS_ANONYMOUS, IS_REMEMBERED and IS_IMPERSONATOR attributes were introduced in Symfony 5.1.
#Security
The security annotation is more flexible as the IsGranted annotation and can use expressions.
Say you want a page, that only can be access if the user is an admin and have a specific token in his request.
with #IsGranted
/**
* #IsGranted("ROLE_ADMIN", statusCode=404)
*/
public function show(Request $request, Post $post)
{
if (!$request->request->has('privatetoken') || 'mytoken' !== $request->request->get('privatetoken')) {
return $this->createNotFoundException('not found');
}
// Show Post
}
with #Security
/**
* #Security("is_granted('ROLE_ADMIN') and request.get('privatetoken') == 'mytoken'", statusCode=404)
*/
public function show(Post $post)
{
// Show Post
}
With this, you must have an admin role and have an privatetoken parameter in your url like mydomain.com/post/show/?privatetoken=mytoken and you don't need the Request instance.
The expression has access to the following variables:
token: The current security token;
user: The current user object;
request: The request instance;
roles: The user roles;
and all request attributes.
There are too many possibilities to post this all. but i think it shows the difference to #IsGranted.
Symfony Docs

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.

How to set access_control to disallow users having a 'ROLE_USER' to access path: ^/login after successful login?

In security.yaml file we define the access control for various routes and the ROLES who can access that same route.
But how can we set the user, who is logged-in but can't revisit the /login page unless and untill it logs out and "ROLE_USER" changes to "anon".
I am new to Symfony 4.2.
Controller:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
//use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class SecurityController extends AbstractController
{
/**
* #Route("/login", name="login")
*/
public function login(Request $request, AuthenticationUtils $utils, AuthorizationCheckerInterface $authChecker)
{
// to check whether user is looged-in
if ($authChecker->isGranted('IS_AUTHENTICATED_FULLY')) {
die('Logged in user cannot access this page');
}
// get the login error if there is one
$error = $utils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $utils->getLastUsername();
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error
]);
}
public function logout()
{
# code...
}
You cannot deny access for logged in user to the login page by editing security.yml. It is all users of a Symfony app, whether logged in or not, will have the base access privilege: IS_AUTHENTICATED_ANONYMOUSLY and Symfony does not have a exclusive role to not logged in user.
However, you can achieve the same thing by checking whether the user has logged in or not in your controller and perform a redirect or throw an AccessDeniedException:
public function login($name, AuthorizationCheckerInterface $authChecker)
{
if ($authChecker->isGranted('IS_AUTHENTICATED_FULLY')) {
throw new AccessDeniedException('Logged in user cannot access this page');
}
// ...
}
As I mentioned in the comments, in my opinion throwing an AccessDeniedException to an already logged in user isn't a good approach. What would your users think? If I have already logged in, why can't I access a page, that I can normally access even if I'm not logged in.
Therefore I'd strongly suggest to redirect logged in users, when accessing the /login path, to the start page of your application.
Just adapt the if-condition block in the method login of your SecurityController:
if ($authChecker->isGranted('IS_AUTHENTICATED_FULLY)) {
$this->redirectToRoute('name of the route - replace with an appropriate value');
}
You should take care that the route you're redirecting to, doesn't cause another redirect and thus puts you in an endless loop.

Unsubscribe users in FOSUserBundle

I'm redesigning a website in symfony2, where users must be able to unsubscribe.
When they do, for database integrity reasons, we have to de-activate their account, and not completely delete it. We must also keep track of their personnal information, like email adress, for a certain time (legal obligation).
I'm using FOSUserBundle, and I initially thought a simple way to deactivate accounts of people who unsubscribed, would be to set the User property "enabled" to false.
But when a user is not enabled, if he tries to register again with the same email adress, he sees : "The email is already used". And I would like he could register again and create a new account (or reactivate the old one).
Is there a way to do this ?
Is there a best practice to handle unsubscriptions with FOSUserBundle ?
Thanks for your help.
You need to override the main registration process with, the easyextendbundle, you could have a look at the documentation at this url:
https://sonata-project.org/bundles/user/2-2/doc/reference/installation.html
Then in your extended controller, you have to create a new action to activate or deactivate your user, this action must be public in the security rules.
In this method, you could use the services to activate or deactivate a user:
fos:user:deactivate Deactivate a user
fos:user:activate Activate a user
You could inspire yourself from this earlier post : Symfony2: List Users with Option to Deactivate Each User
Another possibility is to update your User class with the property $subscribed as below:
/**
* #ORM\Column(name="subscribed", type="boolean")
*/
protected $subscribed;
public function setSubscribed($subscribed)
{
$this->subscribed = $subscribed;
}
public function isSubscribed($)
{
return $this->subscribed;
}
public function changeSubscribed()
{
$this->subscribed = !$this->subscribed;
}
This avoids e-mail address conflicts without adding another third-party bundle.
Edit (note also set method above)
in YourBundle\EventListener
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class RegistrationListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess',
);
}
/**
* Persist organization on staff registration success.
*
* #param \FOS\UserBundle\Event\FormEvent $event
*/
public function onRegistrationSuccess(FormEvent $event)
{
/** #var $user \FOS\UserBundle\Model\UserInterface */
$user = $event->getForm()->getData();
$user->setSubscribed(true);
}
}
also, add to app/config/services.yml:
your_bundle.regisration.listener:
class: YourBundle\EventListener\RegistrationListener
tags:
- { name: kernel.event_subscriber }

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.

Resources