Say for example I grant a new role to the currently authenticated user in a controller, like so:
$em = $this->getDoctrine()->getManager();
$loggedInUser = $this->get('security.context')->getToken()->getUser();
$loggedInUser->addRole('ROLE_XYZ');
$em->persist($loggedInUser);
$em->flush();
On the next page load, when I grab the authenticated user again:
$loggedInUser = $this->get('security.context')->getToken()->getUser();
They are not granted the role. I am guessing this is because the user is stored in the session and needs to be refreshed.
How do I do this?
I am using FOSUserBundle if that makes a difference.
EDIT: This question was originally asked in the context of Symfony version 2.3 but there are answers for more recent versions below as well.
Try this:
$em = $this->getDoctrine()->getManager();
$loggedInUser = $this->get('security.context')->getToken()->getUser();
$loggedInUser->addRole('ROLE_XYZ');
$em->persist($loggedInUser);
$em->flush();
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken(
$loggedInUser,
null,
'main',
$loggedInUser->getRoles()
);
$this->container->get('security.context')->setToken($token);
There's no need for the token reset in the previous answer. Just, in your security config file (security.yml, etc...), add this:
security:
always_authenticate_before_granting: true
While an answer is accepted, Symfony actually has a native way to refresh the User object. Credit goes out to Joeri Timmermans for this article.
Steps for refreshing the User object:
Make your User entity implement the interface
Symfony\Component\Security\Core\User\EquatableInterface
Implement the abstract function isEqualTo:
public function isEqualTo(UserInterface $user)
{
if ($user instanceof User) {
// Check that the roles are the same, in any order
$isEqual = count($this->getRoles()) == count($user->getRoles());
if ($isEqual) {
foreach($this->getRoles() as $role) {
$isEqual = $isEqual && in_array($role, $user->getRoles());
}
}
return $isEqual;
}
return false;
}
The code above refreshes the User object if any new roles are added. The same principle also holds true for other fields you compare.
$user = $this->getUser();
$userManager = $this->get('fos_user.user_manager');
$user->addRole('ROLE_TEACHER');
$userManager->updateUser($user);
$newtoken = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user,null,'main', $user->getRoles());
$token = $this->get('security.token_storage')->setToken($newtoken);
In Symfony 4
public function somename(ObjectManager $om, TokenStorageInterface $ts)
{
$user = $this->getUser();
if ($user) {
$user->setRoles(['ROLE_VIP']); //change/update role
// persist if need
$om->flush();
$ts->setToken(
new PostAuthenticationGuardToken($user, 'main', $user->getRoles())
);
//...
} else {
//...
}
}
Related
I have a Symfony 3 app that uses Doctrine ORM for Entity management. Currently, I am working on enabling CRUD support. I've already found out that I can use security voters to restrict access to entities or controllers. For example, I configured it the way that only admins can create, update or delete entities of type A.
For instances of my entity type B I also want to give the respective owner the power to update (not create or delete), which I managed to do easily. However, an owner shouldn't be allowed to modify all of the entity's properties - just some of them. How can I realize this with Symfony? Also, I am using the Form Bundle to create and validate forms.
EDIT: I added some related code. The controller invokes denyAccessUnlessGranted, which triggers the voter. Just to clarify, that code works fine already. My question is related to code I don't yet have.
Controller:
public function editAction(Request $request, int $id) {
$em = $this->getDoctrine()->getManager();
$project = $em->getRepository(Project::class)->findOneBy(['id'=>$id]);
$this->denyAccessUnlessGranted(ProjectVoter::EDIT, $project);
$users = $em->getRepository(EntityUser::class)->findAll();
$groups = $em->getRepository(Group::class)->findAll();
$tags = $em->getRepository(Tag::class)->findAll();
$form = $this->createForm(ProjectType::class, $project, [
'possibleAdmins' => $users,
'possibleRequiredGroups' => $groups,
'possibleTags' => $tags,
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$project = $form->getData();
$em->flush();
return $this->redirectToRoute('projects_show', ['id'=>$project->getId()]);
}
return $this->render('project/editor.html.twig',
['project'=>$project, 'form'=>$form->createView()]);
}
Voter:
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
/** #var UserInterface $user */
$user = $token->getUser();
if (!$user instanceof UserInterface) {
// the user must be logged in; if not, deny access
return false;
}
else if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) {
return true; // system-wide admins shall always have access
}
switch($attribute) {
case self::SHOW:
return ($subject->isVisible() || $subject->getAdmins()->contains($user);
case self::EDIT:
return $subject->getAdmins()->contains($user);
case self::REMOVE:
return false;
}
return false;
}
As far as I know there is no access functionality specifically related to individual properties. Of course as soon as I post this, someone else will come by with exactly that.
What you might consider doing is to define two edit roles, EDIT_BY_ADMIN and EDIT_BY_OWNER. You could then test the condition and select which form type to use.
$projectTypeClass = null;
if ($this->isGranted(ProjectVoter::EDIT_BY_ADMIN,$project)) {
$projectTypeClass = ProjectAdminType::class);
}
elseif ($this->isGranted(ProjectVoter::EDIT_BY_OWNER,$project)) {
$projectTypeClass = ProjectOwnerType::class);
}
if (!$projectTypeClass) {
// throw access denied exception
}
$form = $this->createForm($projectTypeClass, $project, [
And that should do the trick. There are of course many variations. You could stick with one project type and do the access testing within the type class though that would require a form listener.
If you need more granularity then you could instead add some EDIT_PROP1, EDIT_PROP2 type roles.
And of course if you were really into it then you could move some of the access code into a database of some sort. Or maybe take a look at some of the Access Control List components out there.
I came up with this solution in the end:
Instead of having multiple FormTypes I stuck with only a single one and ended up enabling or disabling the property's form field based on the result of the voter. For that I defined a new switch case as Cerad suggested (named ProjectVoter::MODIFY_PROTECTED_PROPERTY in this answer for demonstration purposes) and added the business logic per my liking.
I just enabled or disabled the form field because I actually want the user to see that he/she can't edit that property. But it would likely easily be possible to not add the field in the first place as well, so it's not visible.
Form Type:
Info: $this->tokenStorage and $this->accessDecisionManager are injected services ("security.token_storage" and "security.access.decision_manager" respectively).
public function buildForm(FormBuilderInterface $builder, array $options) {
$token = $options['token'] ?? $this->tokenStorage->getToken();
$project = $builder->getData();
$builder
->add('name')
// ...
->add('protectedProperty', null, [
'disabled' => !$this->accessDecisionManager->decide($token, [ProjectVoter::MODIFY_PROTECTED_PROPERTY], $project),
])
;
}
I also added an option to the form type called token in its configureOptions function which defaults to null, so that the form can be built for an arbitrary user instead of the one currently logged-in, if required.
I have a page where i can modify user details (username,first name,avatar...).
I have an element in my navbar with informations about the currently logged in user. The thing is I can't figure out how to refresh the session immediately after data is modified.
in UsersController:
public function edit($id = null)
{
if (!$id) {
throw new NotFoundException(__('Invalid user'));
}
$user = $this->Users->get($id);
if ($this->request->is(['post', 'put'])) {
$this->Users->patchEntity($user, $this->request->data);
if ($this->Users->save($user)) {
//REFRESH SESSION ????\\
$this->request->session()->write('Auth.User', $user);
//\\
$this->Flash->success(__('User has been updated.'));
return $this->redirect(['action' => 'edit/' . $id]);
}
$this->Flash->error(__('Unable to update User details.'));
}
$this->set(compact('user'));
}
Just update it the same way as you're setting it when loggin in, ie using AuthComponent::setUser().
Also you may want to do that only in case the user that has been edited, is actually the user that is currently logged in. And you want to set the data in the same format as the Auth component does, that is (for now), as an array, and, for security purposes, without the password.
A simple example that assumes a single column primary key named id, and a password column named password
if ($this->Auth->user('id') === $user->id) {
$data = $user->toArray();
unset($data['password']);
$this->Auth->setUser($data);
}
See also
Cookbook > Controllers > Components > Authentication > Identifying Users and Logging Them In
Cookbook > Controllers > Components > Authentication > Manually Logging Users In
I need to implement two-way authentication process in one of my Symfony 2 projects according to this algorithm:
User enters his username and password in the authentication form and submits it.
System first check his username and password the default way (all users are stored with Doctrine ORM).
If previous step failed, calling an external API and passing to it username and md5(password).
If previous step succeeded, creating a new User entity and using it as authenticated user.
If step #3 failed, then authentication is considered failed.
I already have a service that can be called to authenticate a user by he's username and password using external API, I'm just looking for a way to use it in authentication process.
What is the simplest way to implement this behavior? I just need to be pointed in the right direction.
Update
Is implementing a "custom authenticator" is a good solution to this problem? Or is there a better approach?
Looking at the documentation, I will have to implement both steps of authentication in my custom authenticator. Is it possible to implement only additional step?
Yes, that's the way to go. If your web service can give you user just by username then you could do it in UserProvider only, since in it's scope you have only username. If you must query by un/pw then you must do it in authenticator since in that scope you have password. So, with simple form it will look something like this
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
try {
$user = $userProvider->loadUserByUsername($token->getUsername());
} catch (UsernameNotFoundException $e) {
throw new AuthenticationException('Invalid username or password');
}
$encoder = $this->encoderFactory->getEncoder($user);
$passwordValid = $encoder->isPasswordValid(
$user->getPassword(),
$token->getCredentials(),
$user->getSalt()
);
if ($passwordValid) {
return new UsernamePasswordToken(
$user,
$user->getPassword(),
$providerKey,
$user->getRoles()
);
}
// remote users fallback
$webUser = $this->externalUserService->getByUsernamePassword(
$token->getUsername(),
$token->getCredentials()
);
if ($webUser) {
return new UsernamePasswordToken(
$webUser,
$token->getCredentials(),
$providerKey,
$webUser->getRoles()
);
}
throw new AuthenticationException('Invalid username or password');
}
Ofc there are too many ifs in this class and it's responsible for more then one thing, so to be tidy you could apply composite pattern and have 3 authenticators, one generic composite, second local db authenticator and third external service authenticator, and build it from the services config like this.
# services.yml
my_app.authenticator.main:
class: MyApp/Security/Core/Authentication/CompisiteAuthenticator
calls:
- [ add, [#my_app.authenticator.locale]]
- [ add, [#my_app.authenticator.remote]]
my_app.authenticator.locale:
class: MyApp/Security/Core/Authentication/LocalAuthenticator
arguments: [#security.encoder_factory]
my_app.authenticator.remote:
class: MyApp/Security/Core/Authentication/RemoteAuthenticator
arguments: [#my_app.remote_user_service]
Composite
<?php
namespace MyApp/Security/Core/;
class CompositeAuthenticator implements SimpleFormAuthenticatorInterface
{
/** #var SimpleFormAuthenticatorInterface[] */
protected $children = array();
public function add(SimpleFormAuthenticatorInterface $authenticator)
{
$this->children[] = $authenticator;
}
public function createToken(Request $request, $username, $password, $providerKey)
{
return new UsernamePasswordToken($username, $password, $providerKey);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UsernamePasswordToken
&& $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$result = null;
foreach ($this->children as $authenticator)
{
$result = $authenticator->authenticateToken($token, $userProvider, $providerKey);
if ($result) {
return $result;
}
}
throw new AuthenticationException('Invalid username or password');
}
}
And local and remote authenticators are I guess trivial now
I'm trying to create a user with the Zizaco/confide library, the user data comes from Facebook and not from a form.
I try this :
$user_profile = Facebook::api('/me','GET');
$new_user = new User;
$new_user->username = $user_profile['name'];
$new_user->password = Hash::make('secret');
$new_user->email = $user_profile['email'];
$new_user->confirmation_code = '456456';
$new_user->confirmed = true;
$new_user->save();
but it doesn't save the user. Any help ?
I found the problem, the confide library sets some default rules to create the user, you need to pass this rules to save a user:
public static $rules = array(
'username' => 'required|alpha_dash|unique:users',
'email' => 'required|email|unique:users',
'password' => 'required|between:4,11|confirmed',
'password_confirmation' => 'between:4,11',
);
with this example it works:
$user = new User();
$user->username = 'asdasd';
$user->email = 'angorusadf#gmail.com';
$user->password = '12312312';
$user->password_confirmation = '12312312';
$user->save();
Mabe the method should give you some information when you don't pass the rules.
Maybe it's a pretty late answer, but I'm posting this for future reference because I was myself looking for an answer to the same question, How to create a user programatically in zizaco/confide? or more generally, how to bypass zizaco/confide's requirement for setting a password when saving a user for the first time?
well, the answer is pretty simple, thanks to the new architecture which Zizaco implemented in the 4.* branch, it's now possible to register a new user validator class see more at the package's readme in short all you need in this very case though, is just to extend the current User validator and override validatePassword() to make it accept empty passwords for new users.
Below is an example implementation:
In routes.php
// we need to register our validator so that it gets used instead of the default one
// Register custom Validator for ConfideUsers
App::bind('confide.user_validator', 'MyUserValidator');
In app/models/MyUserValidator.php (that's basically a copy of the function in the class, simply just added a check whether this is a old user or not (if the user has an ID then this is an update operation) if this is a new user, the method always returns true!
/**
* Class MyUserValidator
* Custom Validation for Confide User
*/
class MyUserValidator extends \Zizaco\Confide\UserValidator //implements \Zizaco\Confide\UserValidatorInterface
{
/**
* Validates the password and password_confirmation of the given
* user
* #param ConfideUserInterface $user
* #return boolean True if password is valid
*/
public function validatePassword(\Zizaco\Confide\ConfideUserInterface $user)
{
$hash = App::make('hash');
if($user->getOriginal('password') != $user->password && $user->getOriginal('id')) {
if ($user->password == $user->password_confirmation) {
// Hashes password and unset password_confirmation field
$user->password = $hash->make($user->password);
unset($user->password_confirmation);
return true;
} else {
$this->attachErrorMsg(
$user,
'validation.confirmed::confide.alerts.wrong_confirmation',
'password_confirmation'
);
return false;
}
}
unset($user->password_confirmation);
return true;
}
}
I am using Sonata admin bundle for my application all works well,In my application i have users and admin,admin can add/edit/delete the users when i am trying to update a user there is a problem the password data is overrided from user table. i have overrided the preUpdate method of admin controller ,I got $object which has an instance of user entity manager so if user leaves to update password and saves data the password is lost.
public function preUpdate($object)
{
$Password = $object->getUserPassword();
if (!empty($Password)) { /* i check here if user has enter password then update it goes well*/
$salt = md5(time());
$encoderservice = $this->getConfigurationPool()->getContainer()->get('security.encoder_factory');
$User = new User();
$encoder = $encoderservice->getEncoder($User);
$encoded_pass = $encoder->encodePassword($Password, $salt);
$object->setUserSalt($salt)->setUserPassword($encoded_pass);
} else { /* here i try to set the old password if user not enters the new password but fails */
$object->setUserPassword($object->getUserPassword());
}
}
When i try to set $object->setUserPassword($object->getUserPassword()); it gets null and updates the password as null its not getting the edit data i have tried to get the repository (below) again to get the password but no luck its getting the same
$DM = $this->getConfigurationPool()->getContainer()->get('Doctrine')->getManager()->getRepository("...")->find(id here);
Is there a way i can access the original data of current entity in entity manager
You can access the original data by getting doctrine's Unit of Work.As from docs
You can get direct access to the Unit of Work by calling
EntityManager#getUnitOfWork(). This will return the UnitOfWork
instance the EntityManager is currently using.An array containing the
original data of entity
Grab the password from Unit of Work and use in your setter method
public function preUpdate($object)
{
$DM = $this->getConfigurationPool()->getContainer()->get('Doctrine')->getManager();
$uow = $DM->getUnitOfWork();
$OriginalEntityData = $uow->getOriginalEntityData( $object );
$Password = $object->getUserPassword();
if (!empty($Password)) { /* i check here if user has enter password then update it goes well*/
$salt = md5(time());
$encoderservice = $this->getConfigurationPool()->getContainer()->get('security.encoder_factory');
$User = new User();
$encoder = $encoderservice->getEncoder($User);
$encoded_pass = $encoder->encodePassword($Password, $salt);
$object->setUserSalt($salt)->setUserPassword($encoded_pass);
} else { /* here i try to set the old password if user not enters the new password but fails */
$object->setUserPassword($OriginalEntityData['Password']);/* your property name for password field */
}
}
Hope it works fine
Direct access to a Unit of Work
Reset entity in entity manage, example for onFlush event
/**
* #param OnFlushEventArgs $args
*
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*/
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $keyEntity => $entity) {
if ($entity instanceof Bill) {
$em->refresh($entity);
$this->createPdfs($entity);
}
}
}
$this->getConfigurationPool()
->getContainer()
->get('Doctrine')
->getRepository("...")
->find(id here);
So leave out the getManager() part;