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 }
Related
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
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.
I have user entity with custom repository for it. I have created an event listener to check if given user is authorized to access given game account.
To this I have created custom function hasAccountOfId($id) in my user repository. However I have problem with accessing this function from the event listener.
User entity:
/**
* #ORM\Entity(repositoryClass="AppBundle\Entity\UserRepository")
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/.../
/**
* #ORM\OneToMany(targetEntity="Account", mappedBy="user")
*/
protected $accounts;
User entity custom repository:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\EntityRepository;
/**
* UserRepository
*/
class UserRepository extends EntityRepository
{
public function hasAccountOfId($accountId)
{
foreach ($this->accounts as $account) {
if ($account.id == $accountId) {
return true;
}
}
return false;
}
}
And the listener where I am struggling with accessing hasAccountOfId function.
// Account is set, verify if user is authorized
$id = $this->session->get("accountId");
$user = $this->token_storage->getToken()->getUser();
$userEm = $this->em->getRepository('AppBundle:User');
if(!$user->hasAccountOfId($id))
{
// Not authorized
die("Not authorized");
}
returns:
Attempted to call an undefined method named "hasAccountOfId" of class
"AppBundle\Entity\User".
while using !$userEm->hasAccountOfId($id) results in:
Notice: Undefined property: AppBundle\Entity\UserRepository::$accounts
How may I access my user repository for $user = $this->token_storage->getToken()->getUser() ?
Firstly for this simple OneToMany association You need to provide the Owning side of relation (in Account class). By creating new Account for User You will need to call $account->setUser($user); to make the Relation.
Secondly You don't need a custom method in Your RepositoryClass as You will get Accounts from Doctrine's Relation: $user->getAccounts().
The iteration through the Accounts to look for ID should be done NOT in RepositoryClass or Entity itself, but using a Manager Service or something else, because other way will violate Singe Responsibility Principle.
And finally, there is no . for class properties or methods, it must be ->. Note if statement in hasAccountOfId method.
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);
}
}
I am using a custom UserProvider for the authentication in my Symfony2.1 application. I would like to use FOSCommentBundle to implement comments. But when it comes to sign the comment by the comment's author, I am stuck.
Basically, I have two databases. One from which I can retrieve users' credentials (username, salt, password,...) but which I can't make any modification, the other one I can use to store the users information (like her/his comment(s)) in an User entity.
When I am mapping the Comment entity with this User entity, there is a problem since FOSCommentBundle retrieves the entity which implements the UserInterface (in my security bundle) and not this User entity.
Basically, is there a way to tell FOSCommentBundle to retrieve another User entity than the one used for Authentication?
Thanks
Did you try FOSUserBundle integration with FOSCommentsBundle?
You need to implement SignedCommentInterface like this.
<?php
// src/MyProject/MyBundle/Entity/Comment.php
namespace MyProject\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\CommentBundle\Entity\Comment as BaseComment;
use FOS\CommentBundle\Model\SignedCommentInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity
*/
class Comment extends BaseComment implements SignedCommentInterface
{
// .. fields
/**
* Author of the comment
*
* #ORM\ManyToOne(targetEntity="MyProject\MyBundle\Entity\User")
* #var User
*/
protected $author;
public function setAuthor(UserInterface $author)
{
$this->author = $author;
}
public function getAuthor()
{
return $this->author;
}
public function getAuthorName()
{
if (null === $this->getAuthor()) {
return 'Anonymous';
}
return $this->getAuthor()->getUsername();
}
}