How to check if user roles have changed? - symfony

I want to check if the user's roles have changed.
Look at this example : I'm an administrator and I want to change the roles of an another administrator (ROLE_MEMBER_ADMIN to ROLE_USER). But the member's roles changes only if he disconnect and reconnect.
Does the isEqualTo method of EquatableInterface is the solution? How can I implement it?

I think you should implement it on your own. This sounds a bit like a user log.
You can make a new table where you log all events related to the userid. Then you can log every event.
After that you can write a function which checks wheather there is a change for a user.

In your User Entity:
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface, \Serializable, EquatableInterface {
/* took out getters/setters/ members declaration for clarity */
/**
* #see \Serializable::serialize()
*/
public function serialize() {
return serialize(array(
$this->id,
$this->username,
$this->email,
$this->password,
$this->isActive,
$this->roles
));
}
/**
* #see \Serializable::unserialize()
*/
public function unserialize($serialized) {
list (
$this->id,
$this->username,
$this->email,
$this->password,
$this->isActive,
$this->roles
) = unserialize($serialized);
}
public function isEqualTo(UserInterface $user) {
if (!$user instanceof User) {
return false;
}
if ($this->password !== $user->getPassword()) {
return false;
}
if ($this->username !== $user->getUsername()) {
return false;
}
if ($this->email !== $user->getEmail()) {
return false;
}
if ($this->isActive !== $user->isEnabled()) {
return false;
}
// check roles
// http://www.metod.si/symfony2-reload-user-roles/
if (md5(serialize($this->getRoles())) !== md5(serialize($user->getRoles()))) {
return false;
}
return true;
}
}
It should do it, tested with PHP 5.3.27, PHP 5.4.X has some issues with serialization.
Hope this helps.

Related

Symfony route access check

I have a website made with Symfony 3.4 and within my actions I must check if the current user can edit the target product, something like this:
/**
* #Route("/products/{id}/edit")
*/
public function editAction(Request $request, Product $product)
{
// security
$user = $this->getUser();
if ($user != $product->getUser()) {
throw $this->createAccessDeniedException();
}
// ...
}
How can I avoid making the same check on every action (bonus points if using annotations and expressions)?
I am already using security.yml with access_control to deny access based on roles.
You can use Voters for this exact purpose. No magic involved. After creating and registering the Voter authentication will be done automatically in the security layer.
You just have to create the Voter class and then register it as a service. But if you're using the default services.yaml configuration, registering it as a service is done automatically for you!
Here is an example you can use. You may have to change a few items but this is basically it.
To read more visit: https://symfony.com/doc/current/security/voters.html
<?php
namespace AppBundle\Security;
use AppBundle\Entity\Product;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use AppBundle\Entity\User;
class ProductVoter extends Voter
{
const EDIT = 'EDIT_USER_PRODUCT';
protected function supports($attribute, $subject)
{
if($attribute !== self::EDIT) {
return false;
}
if(!$subject instanceof Product) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** #var Product $product */
$product= $subject;
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
return $this->belongsToUser($product, $user);
}
private function belongsToUser(Product $product, User $user)
{
return $user->getId() === $product->getUser()->getId();
}
}
You could try with a listener:
Check the action name,for example, if it is "edit_product", them continue.
Get the current logged User.
Get the user of the product entity.
Check if current user is different to Product user, if it is true, throw CreateAccessDeniedException.
services.yml
app.user.listener:
class: AppBundle\EventListener\ValidateUserListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
arguments: ["#service_container", "#doctrine.orm.entity_manager"]
Edit Action:
Added name "edit_product" to the action.
/**
*
* #Route("/products/{id}/edit",name="edit_product")
*/
public function editAction()
{
...
src\AppBundle\EventListener\ValidateUserListener.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class ValidateUserListener
{
private $container;
private $entityManager;
public function __construct($container, $entityManager)
{
$this->container = $container;
$this->entityManager = $entityManager;
}
public function onKernelRequest(GetResponseEvent $event)
{
$currentRoute = $event->getRequest()->attributes->get('_route');
if($currentRoute=='edit_product' || $currentRoute=='edit_item' )
{
$array_user = $this->getCurrentUser();
if($array_user['is_auth'])
{
$current_user = $array_user['current_user'];
$product = $this->entityManager->getRepository('AppBundle:User')->findOneByUsername($current_user);
$product_user = $product->getUsername();
if ($current_user !==$product_user)
{
throw $this->createAccessDeniedException();
}
}
}
}
private function getCurrentUser()
{
//Get the current logged User
$user = $this->container->get('security.token_storage')->getToken()->getUser();
if(null!=$user)
{
//If user is authenticated
$isauth = $this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY');
return array('is_auth'=>$isauth, 'current_user'=>$user);
}
return array('is_auth'=>false, 'current_user'=>$user);
}
}
Tested in Symfony 3.3

Symfony 4 Voter Verify User is Fully Authenticated

Is there an easier way (or just better alternative) to while doing a Voter check to verify a user is actually logged in?
Example:
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
switch ($attribute) {
case self::VIEW:
return $this->canView($subject, $token);
case self::EDIT:
return $this->canEdit($subject, $token);
case self::CREATE:
return $this->canCreate($token);
}
}
/**
* #param TokenInterface $token
* #return bool
*/
private function canCreate(TokenInterface $token)
{
if (!$token->getUser() instanceof User)
{
return false;
}
if ($token->getUser()->isEnabled() && !$token->getUser()->isFreeze())
{
return true;
}
return false;
}
The problem I'm having is stemming from $token->getUser() returns a string when the user is anon. and not an actual User entity.
This is fairly easily done within the controller with $this->isGranted('IS_AUTHENTICATED_FULLY') I just feel like I'm missing something similar that can be done within voters.
You can inject the AuthorizationChecker into the voter and then do the isGranted()-check.
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class MyVoter
{
private $authChecker;
public function __construct(AuthorizationCheckerInterface $authChecker)
{
$this->authChecker = $authChecker;
}
private function canCreate(TokenInterface $token)
{
if (!$this->authChecker->isGranted('IS_FULLY_AUTHENTICATED')) {
return false;
}
// ...
}
}

UserChecker - Use entity manager

My website is running Symfony 3.4 and I made my own user member system.
My User entity contains a Datetime field 'lastLogin' and I can't find a solution to update it every time a user logged in.
I created a custom UserChecker then I tried to update the field in it :
<?php
namespace CoreBundle\Security;
use CoreBundle\Entity\User as AppUser;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() )
{
throw new AuthenticationException();
}
else
{
// BELOW IS WHAT I TRY, BUT FAIL.
$entityManager = $this->get('doctrine')->getManager();
$user->setLastLogin(new \DateTime());
$entityManager->persist($user);
$entityManager->flush();
}
}
public function checkPostAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
}
}
But it doesn't work. Maybe I can't use the doctrine entity manager in this file ?
If I use $this->get('doctrine')->getManager(); I get :
Fatal Error: Call to undefined method
CoreBundle\Security\UserChecker::get()
Dunno why #doncallisto removed his post. It was (IMHO) the right thing.
Take a look at http://symfony.com/doc/current/components/security/authentication.html#authentication-success-and-failure-events
So you have several options.
SecurityEvents::INTERACTIVE_LOGIN - triggers every time the user
full out the login form and submit credentials. Will work, but you
won't get last_login updates if you have remember_me cookie or similar
AuthenticationEvents::AUTHENTICATION_SUCCESS - triggers each time
(every request) when authentication was successful. It means your last_login will be updated each time on every request unless user logged out
so you'll need a EventSubscriber. Take a look at this article. https://thisdata.com/blog/subscribing-to-symfonys-security-events/
MAybe you'll need a simplified version.
public static function getSubscribedEvents()
{
return array(
// AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure', // no need for this at that moment
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin', // this ist what you want
);
}
and then the onSecurityInteractiveLogin method itself.
public function onSecurityInteractiveLogin( InteractiveLoginEvent $event )
{
$user = $this->tokenStorage->getToken()->getUser();
if( $user instanceof User )
{
$user->setLastLogin( new \DateTime() );
$this->entityManager->flush();
}
}
P.S.
FosUserBundle uses interactive_login and a custom event to set last_login on entity
look at: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/EventListener/LastLoginListener.php#L63
Friend you can use this to inject the entityManager by a constructor
use Doctrine\ORM\EntityManagerInterface;
public function __construct(EntityManagerInterface $userManager){
$this->userManager = $userManager;
}
And in the checkPreAuth you call it
public function checkPreAuth(UserInterface $user){
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() ){
throw new AuthenticationException();
}else{
// BELOW IS WHAT I TRY, BUT FAIL.
$user->setLastLogin(new \DateTime());
$this->userManager->persist($user);
$this->userManager->flush();
}
}

Symfony Custom Authentication Provider - User Not Fully Logged In (Logged In, Not Authenticated)

I'm working on a creating a custom authentication provider. I've written my own Authentication Provider, Listener, Token and everything. It's based off a form login, and I've stepped through the code and everything seems to be configured properly. Everything is called in the right order, and my authentication provider is invoked perfectly. The authentication provider successfully authenticates the user, and returns the authenticated token. I extend AbstractAuthenticationListener which, in the handle method, will set the security context.
The user seems to be logged in, but in the debug toolbar, the token is not set and I see "You are not authenticated" and "No token".
Is there any configuration settings that I'm missing? Why would the user would be logging in, authentication provider returning successfully, with an authenticated token, being set in the security context but still be not authenticated? Any tips on how to debug this?
(I will post code as needed.)
EDIT: Token Definition:
This is very simple, just extending from AbstractToken:
class UserToken extends AbstractToken
{
private $username;
private $password;
private $domain;
private $providerKey;
public function __construct($username, $password, $domain, $provider_key, array $roles = array('ROLE_USER'))
{
parent::__construct($roles);
$this->username = $username;
$this->password = $password;
$this->domain = $domain;
$this->providerKey = $provider_key;
}
public function getCredentials()
{
return '';
}
function getUsername() {
return $this->username;
}
function getDomain() {
return $this->domain;
}
function getPassword() {
return $this->password;
}
function getProviderKey(){
return $this->providerKey;
}
}
Authentication Listener:
class Listener extends AbstractAuthenticationListener
{
protected $authenticationManager;
public function __construct(
SecurityContextInterface $securityContext,
AuthenticationManagerInterface $authenticationManager,
SessionAuthenticationStrategyInterface $sessionStrategy,
HttpUtils $httpUtils,
$providerKey,
AuthenticationSuccessHandlerInterface $successHandler,
AuthenticationFailureHandlerInterface $failureHandler,
array $options = array(),
LoggerInterface $logger = null,
EventDispatcherInterface $dispatcher = null
//CsrfProviderInterface $csrfProvider = null
) {
parent::__construct(
$securityContext,
$authenticationManager,
$sessionStrategy,
$httpUtils,
$providerKey,
$successHandler,
$failureHandler,
array_merge(
array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'domain_parameter' => '_domain',
'csrf_parameter' => '_csrf_token',
'intention' => 'authenticate',
'post_only' => true,
),
$options
),
$logger,
$dispatcher
);
}
/**
* Performs authentication.
*
* #param Request $request A Request instance
*
* #return TokenInterface|Response|null The authenticated token, null if full authentication is not possible, or a Response
*
* #throws AuthenticationException if the authentication fails
*/
protected function attemptAuthentication(Request $request)
{
// Create initial unauthenticated token and pass data to the authentication manager.
// TODO validate request data.
$username = trim($request->request->get($this->options['username_parameter'], null, true));
$password = $request->request->get($this->options['password_parameter'], null, true);
$domain = $request->request->get($this->options['domain_parameter'], null, true);
$token = $this->authenticationManager->authenticate(new UserToken($username, $password, $domain, $this->providerKey));
return $token;
}
}
The above code will invoke the the auth function on the provider via the AuthenticationManager:
//This is from the AuthenticationProvider
public function authenticate(TokenInterface $token) {
$loginHandler = new LoginAuthenticationHandler($token->getUsername(), $token->getPassword(), $token->getDomain());
//This code just calls our web service and authenticates. I removed some business logic here, but this shows the gist of it.
if(!$boAuthenticationToken = $loginHandler->authenticate())
{
throw new AuthenticationException('Bad credentials');
}
else{
$user = $this->userProvider->loadUserByUsername($token->getUsername());
//$user = $this->userProvider->getUser($token, $boAuthenticationToken);
// Set the user which will be invoked in the controllers.
$token->setUser($user);
$token->setAuthenticated(true);
return $token;
}
}
Bundle Services.yml
parameters:
services:
ws.security.authentication.provider:
#http://blog.vandenbrand.org/2012/06/19/symfony2-authentication-provider-authenticate-against-webservice/
class: Aurora\OurCustomBundle\Security\Authentication\Provider\Provider
arguments: ["bo_remove_this_with_bo_auth_service", "", "#security.user_checker", "", "#security.encoder_factory"]
ws.security.authentication.listener:
class: Aurora\OurCustomBundle\Security\Firewall\Listener
parent: security.authentication.listener.abstract
abstract: true
#arguments: []
arguments: ["#security.context", "#security.authentication.manager", "#security.authentication.session_strategy", "#security.http_utils", "ws.user_provider", "#security.authentication.customized_success_handler", "#main.cas.rest.user.authentication.failure.service"]
ws.user_provider:
class: Aurora\OurCustomBundle\Security\User\UserProvider
And lastly, the UserProvider
class UserProvider implements UserProviderInterface
{
public function loadUserByUsername($username)
{
//Just return a simple user for now.
return new User($username, array('ROLE_USER'));
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'Aurora\OurCustomBundle\Security\User\User';
}
}
After many hours of hair pulling, I figured out the problem!
The token implementation was incorrect. Since I was implementing my own Token, which extends from AbstractToken, I needed to also implement the serialize() and unserialize() functions.
Once I did that, the code worked. The updated Token class is below for future reference:
class UserToken extends AbstractToken
{
private $username;
private $password;
private $domain;
private $providerKey;
public function __construct($username, $password, $domain, $provider_key, array $roles = array('ROLE_USER'))
{
parent::__construct($roles);
$this->username = $username;
$this->password = $password;
$this->domain = $domain;
$this->providerKey = $provider_key;
}
public function getCredentials()
{
return '';
}
function getUsername() {
return $this->username;
}
function getDomain() {
return $this->domain;
}
function getPassword() {
return $this->password;
}
function getProviderKey(){
return $this->providerKey;
}
function serialize(){
return serialize(array($this->username, $this->password, $this->domain, parent::serialize()));
}
function unserialize($serialized){
list($this->username, $this->password, $this->domain, $parentSerialization) = unserialize($serialized);
parent::unserialize($parentSerialization);
}
}

Property is not flushed

I call a service in a controller:
$this->getContainer()->get('pro_convocation')->sendDelegationEmails();
Here it is the method executed with the service:
function sendDelegationEmails()
{
foreach ($this->getRepository()->findWithPendingDelegationsEmail(1) as $convocation) {
if (! $convocation->delegationsEmailCanBeSent()) continue;
$this->sendDelegationsEmails($convocation);
$convocation->_setDelegationsEmailIsSent(); //<--- it is not saved
$this->save($convocation);
}
}
and the other used methods in the same class:
/**
* #return \Pro\ConvocationBundle\Entity\ConvocationRepository
*/
private function getRepository()
{
return $this->get('doctrine')->getRepository('ProConvocationBundle:Convocation');
}
function save(ConvocationEntity $convocation)
{
$this->getEntityManager()->persist($convocation);
$this->getEntityManager()->flush();
}
function sendDefinitiveMinute(ConvocationEntity $convocation)
{
foreach ($convocation->getCommunity()->getMembers() as $user) {
$this->get('pro.notifications')->send(Notification::create()
...
->setBody($this->get('templating')->render(
'ProConvocationBundle:Convocation:definitive-minute.html.twig',
array(
'convocation' => $convocation,
'convocationIsOrdinary' => $this->get('pro_convocation.ordinariness')->isOrdinary($convocation),
'user' => $user
)
))
);
}
}
EDIT: The send method:
function send(Notification $notification)
{
if (! $notification->getRecipient()) throw new \Exception("Recipient not set");
if (! $notification->getRecipient()->getEmail()) {
$this->logger->info(sprintf(
"Notification \"%s\" ignored as recipient %s has no email",
$notification->getSubject(), $notification->getRecipient()));
return;
}
// Ignore notifications to the own author
if ($notification->getAuthor() === $notification->getRecipient()) {
$this->logger->info(sprintf(
"Notification ignored as recipient is the author (%s)",
$notification->getRecipient()));
return;
}
$em = $this->doctrine->getManager();
$em->persist($notification);
$em->flush();
if ($this->notificationHasToBeMailed($notification)) {
$this->mail($notification);
$this->logger->info("Notification mailed to {$notification->getRecipient()}");
}
}
END EDIT
The _setDelegationsEmailIsSent method and delegationsEmailIsSent property in the Convocation entity:
/**
* #ORM\Column(type="boolean", name="delegations_email_is_sent")
* #Constraints\NotNull
*/
private $delegationsEmailIsSent = false;
/**
* #internal
* #see \Pro\ConvocationBundle\Convocation::sendDelegationsEmails()
*/
function _setDelegationsEmailIsSent()
{
$this->delegationsEmailIsSent = true;
}
The problem is that delegations_email_is_sent in the database is not changing to true when executing the sendDelegationEmails method in a controller. I've tried several changes without success. It seems to be related to flush method, but I don't find the issue.

Resources