How to define findOneBy($criteria) function? - symfony

I am very new to Symfony2, designing a simple login system. The userclass,
router, controlerclass everything is working fine. I am stuck to
userRepository class.
My controller part is:
public function loginProcessAction(Request $request){
if($request->getMethod() == "POST") {
$username = $request->get('username');
$password = $request->get('password');
$em= $this->getDoctrine()->getEntityManager();
$repository = $em->getRepository("LoginLoginBundle:Student");
$user = $repository->findOneBy(array('username'=>$username,
'password'=>$password));
if($user){
return $this->render('loginSuccess twig page') ;
}
else{
return $this->render('error twig page') ;
}
} else{
return $this->render("login error page");
}
}
How to define findOneBy(username, password) function in reopository class.

This is not the best way to handle authentication when using Symfony2. Take a look at the Security component integrated with Symfony2.
So check How Security Works: Authentication and Authorization part of the security documentation, all you need to implement/configure is Firewalls to handle Authentication and
Access Controls for Authorization.
But ...
Here's an answer to the common question: How to a define findOneBy(parameter1, parameter2) function for a given repository class?
First, map your entity to the appropriate repository as follow,
/*
* #ORM\Entity(repositoryClass="YourNamespace\YourBundle\Entity\yourRepository")
*/
class YourEntity
{
// ...
}
You should then add the mapped repository class and implement a findOneBy(parameter1, parameter2) method.
You can then access this class within your controller as follow,
$em= $this->getDoctrine()->getManager();
$yourEntityInstance = $em->getRepository("yourNamespaceYourBundle:YourEntity")
->findOneBy($parameter1, $parameter2);

Related

Symfony : how to set init data on login

I'm facing a dilemna as well as an optimization problem :
In my Symfony 2.8 application, I have custom settings and other business logic data to load (from database tables, not from SF parameters) that a logged in user can be needed to use at different pages.
At first those data where scarcely needed, so i loaded them only when the page required it. But now as the application grows, i need them more often.
So i was thinking about loading them when the user logs in, and save them as localStorage on client side because cookies are too small.
But i'm not sure how to best do it.
I have a login success handler, that allows to redirect on the correct page when user is successfully logged.
For the moment i have this one :
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Router;
class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface
{
protected $router;
protected $authorizationChecker;
public function __construct(Router $router, AuthorizationChecker $authorizationChecker)
{
$this->router = $router;
$this->authorizationChecker = $authorizationChecker;
}
/**
* What to do when user logs in.
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$response = null;
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
//an admin is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_admin'));
} else if ($this->authorizationChecker->isGranted('ROLE_USER')) {
//a user is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_user'));
}
//redirect to any last visited page if any
$key = '_security.main.target_path';
if ($request->getSession()->has($key)) {
$url = $request->getSession()->get($key);
$request->getSession()->remove($key);
$response = new RedirectResponse($url);
}
return $response;
}
}
So i was thinking about adding a setInitialData() method in which i would get all the settings i need and modifying onAuthenticationSuccess :
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$response = null;
//retrieve array of data to be set in the init
$toBeSaved = $this->setInitialData();
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
//an admin is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_admin', ['initdata'=>$toBeSaved]));
} else if ($this->authorizationChecker->isGranted('ROLE_USER')) {
//a user is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_user', ['initdata'=>$toBeSaved]));
}
//redirect to any last visited page if any
$key = '_security.main.target_path';
if ($request->getSession()->has($key)) {
$url = $request->getSession()->get($key);
$request->getSession()->remove($key);
$response = new RedirectResponse($url, ['initdata'=>$toBeSaved]);
}
return $response;
}
And then on the main template, i would retrieve that data
{% for paramName, paramValue in app.request.query %}
{% if paramName == 'initdata' %}
<div id="initdata" data-init="{{paramValue|json_encode}}"></div>
{% endif %}
{% endfor %}
and add a javascript block with something like :
<script>
if ($('#initdata').length > 0){
localStorage.removeItem('initdata');
localStorage.setItem('initdata', JSON.stringify($('#initdata').data('init')));
}
</script>
But this method doesn't seems right : i'm not sure this is the best way to do it.
And furthermore, since these are sent in a redirect, the data are shown in the query string, which is not ideal :(
This will not fly as by having multiple parameters you create multiple <div> elements with identical ID = initdata. Subsequent jQuery selector will only capture the first one (afaik).
I see that you indeed send params via query string. This takes care of multiple value, but this also exposes your user setting in user URL, doesn't it? If it does, it has security vulnerability all over the wall. Remember, such URLs are persisted in your browser's history.
Instead, I suggest you create a separate controller action /_get_user_settings which you will call via AJAX GET. Server will serve JSON response which you can save to your localStorage with little or no problem at all.
Hope this helps...

PHP/Symfony - Parsing object properties from Request

We're building a REST API in Symfony and in many Controllers we're repeating the same code for parsing and settings properties of objects/entities such as this:
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
I'm aware that Symfony forms provide this functionality, however, we've decided in the company that we want to move away from Symfony forms and want to use something simplier and more customisable instead.
Could anybody please provide any ideas or examples of libraries that might achieve property parsing and settings to an object/entity? Thank you!
It seems like a good use case for ParamConverter. Basically it allows you, by using #ParamConverter annotation to convert params which are coming into your controller into anything you want, so you might just create ParamConverter with code which is repeated in many controllers and have it in one place. Then, when using ParamConverter your controller will receive your entity/object as a parameter.
class ExampleParamConverter implements ParamConverterInterface
{
public function apply(Request $request, ParamConverter $configuration)
{
//put any code you want here
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
//now you are setting object which will be injected into controller action
$request->attributes->set($configuration->getName(), $solution);
return true;
}
public function supports(ParamConverter $configuration)
{
return true;
}
}
And in controller:
/**
* #ParamConverter("exampleParamConverter", converter="your_converter")
*/
public function action(Entity $entity)
{
//you have your object available
}

get users who have a specific role

I need to get the list of all my users having a specific role, is there any way to do it easily? The solution I figured out for now would be to retrive all users then apply a filter on each using is granted function (which is hardcore)
PS: I don't like using the db request that skims over data and if the user role equals the wanted role it returns it, else it doesn't. Which means that we don't take into account users with super roles.
Because of the role hierarchy, I don't see a way to avoid grabbing all the users and then filtering. You could make a user role table and add all possible user roles but that would get out of date if you changed the hierarchy.
However, once you have all the roles for a given user then you can test if a specific one is supported.
There is a role hierarchy object to help.
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
class RoleChecker
{
protected $roleHeirarchy;
public function __construct(RoleHierarchy $roleHierarchy)
{
$this->roleHierarchy = $roleHierarchy; // serviceId = security.role_hierarchy
}
protected function hasRole($roles,$targetRole)
{
$reachableRoles = $this->roleHierarchy->getReachableRoles($roles);
foreach($reachableRoles as $role)
{
if ($role->getRole() == $targetRole) return true;
}
return false;
}
}
# services.yml
# You need to alias the security.role_hierarchy service
cerad_core__role_hierarchy:
alias: security.role_hierarchy
You need to pass an array of role objects to hasRole. This is basically the same code that the security context object uses. I could not find another Symfony service just for this.
The is also a parameter value called '%security.role_hierarchy.roles%' that comes in handy at times as well.
Symfony 5 answer, it's a little bit easier:
namespace App\Controller;
...
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
class UserController extends AbstractController
{
private $roleHierarchy;
/**
* #Route("/users", name="users")
*/
public function usersIndex(RoleHierarchyInterface $roleHierarchy)
{
$this->roleHierarchy = $roleHierarchy;
// your user service or your Doctrine code here
$users = ...
foreach ($users as $user) {
$roles = $roleHierarchy->getReachableRoleNames($user->getRoles());
\dump($roles);
if ($this->isGranted($user, 'ROLE_SUPER_ADMIN')) {
...
}
}
...
}
private function isGranted(User $user, string $role): bool
{
$reachableRoles = $this->roleHierarchy->getReachableRoleNames($user->getRoles());
foreach ($reachableRoles as $reachableRole) {
if ($reachableRole === $role) {
return true;
}
}
return false;
}
}
Note: I put everything in the controller for the sake of simplicity here, but of course I'd recommend to move the Role Management code into a separate service.

Symfony 2: Two step (not two-factor) authentication

I need to implement a two step (not two-factor) authentication in Symfony 2.3. The first step is the usual user+password+csrf form. The second step is "Terms & Conditions", which the user should see when they first log in or when the terms are updated, and they should have to tick a box in order to proceed to the rest of the site.
The second step isn't really an authentication step, but a user shouldn't be able to access the rest of the site unless that second step is acted upon, so it makes sense to conceptually think of it as part of the authentication.
While writing this, the rubber duck tells me that I should think about authorisation instead, and the idea of starting the user on a "didn't accept terms yet" role, and updating the role to "fully authorised user" if the terms have been accepted. This sounds like the most sound solution so far, as I can let the firewall take care of the logic.
Stumbled upon these pieces of information so far:
http://blogsh.de/2011/11/15/change-user-roles-during-a-session-in-symfony/
http://php-and-symfony.matthiasnoback.nl/2012/07/symfony2-security-creating-dynamic-roles-using-roleinterface/
There's one behaviour that I expect to encounter as I dig deeper into this: the firewall will display an error instead of re-directing the user to the Terms page and then let them on their way once they accept them.
Has anybody done this before, so I have to invent as little of the wheel as possible?
I found somebody with a similar problem, and he received a solution I could use:
Symfony 2 : Redirect a user to a page if he has a specific role
The event listener class:
namespace Acme\DemoBundle\Lib;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\HttpKernel;
use Acme\DemoBundle\Entity\User;
class TermsAndConditionsRequestListener
{
private $security;
private $router;
public function __construct($security, $router)
{
$this->security = $security;
$this->router = $router;
}
public function onKernelRequest(GetResponseEvent $event)
{
/* http://symfony.com/doc/2.3/cookbook/service_container/event_listener.html */
if (HttpKernel::MASTER_REQUEST !== $event->getRequestType())
{
// don't do anything if it's not the master request
return;
}
$request = $event->getRequest();
$route = $request->attributes->get('_route');
if ($route === '_wdt' || substr_compare($route, '_profiler', 0, 9) === 0)
{
// ignore development routes
return;
}
if (in_array($route, array('terms_and_conditions_force', 'terms_and_conditions_accept')))
{
// don't redirect into an infinite loop
return;
}
$token = $this->security->getToken();
$user = $token ? $token->getUser() : null;
$user_role = ($user instanceof User) ? $user->getRole() : null;
if ($user_role === 'ROLE_USER' && (is_null($user->getTermsAcceptedDate()) || $terms_are_newer_than_acceptance_date))
{
$url = $this->router->generate('terms_and_conditions_force');
$event->setResponse(new RedirectResponse($url));
}
}
}
The event listener service:
acme.wvml.event_listener.request.terms_and_conditions:
class: Acme\DemoBundle\Lib\TermsAndConditionsRequestListener
arguments: [#security.context, #router]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
You will have to extend the symfony UserAuthenticationProvider. you'll probably want to add the check in the checkAuthentication function and if it fails return the error message regarding the terms and conditions.

Symfony2: creating a new SecurityIdentity

I'm using ACL in Symfony 2.1, and I need to create a new SecurityIdentity, so that my ACL can be set in function of some sort of groups.
Picture the following situation: there are groups with users (with different roles) that each have user information. In group 1, users with the ROLE_ADMIN can't edit other users from the same group's information, but in group 2, users with ROLE_ADMIN can edit others information.
So basically my ACL will vary in function of what group the user is in.
I thought I'd start solving this problem with the creation of a new "GroupSecurityIdentity". However the class itself doesn't suffice, as I get this exception when I use it:
$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.
My question is: how do I "register" my new SecurityIdentity so I can use it as RoleSecurityIdentity and UserSecurityIdentity?
What better ways are there to implement a system similar to this I want to do?
2 years ago I went down that path, it turned out to be a bad decision. Modifying the ACL system is difficult and might cause problems when updating Symfony. There are at least 2 better solutions. I'll list them all so you can decide which best suits your needs.
New security identity
I'm using the GroupInterface from FOSUserBundle, but I guess you could use your own too. The following files need to be added:
AclProvider.php
The method to change is private - the whole file has to be copied, but the only change has to be made to hydrateObjectIdentities
GroupSecurityIdentity.php
MutableAclProvider.php
We have to duplicate the whole file as it must extend AclProvider, but we're using a custom one and can't therefore extend the stock MutableAclProvider. The methods changed are getInsertSecurityIdentitySql and getSelectSecurityIdentityIdSql.
SecurityIdentityRetrievalStrategy.php
Next up: rewire the dependency injection container by providing the following parameters:
<parameter key="security.acl.dbal.provider.class">
Acme\Bundle\DemoBundle\Security\Acl\Dbal\MutableAclProvider
</parameter>
<parameter key="security.acl.security_identity_retrieval_strategy.class">
Acme\Bundle\DemoBundle\Security\Acl\Domain\SecurityIdentityRetrievalStrategy
</parameter>
Time to cross fingers and see whether it works. Since this is old code I might have forgotten something.
Use roles for groups
The idea is to have group names correspond to roles.
A simple way is to have your User entity re-implement UserInterface::getRoles:
public function getRoles()
{
$roles = parent::getRoles();
// This can be cached should there be any performance issues
// which I highly doubt there would be.
foreach ($this->getGroups() as $group) {
// GroupInterface::getRole() would probably have to use its
// canonical name to get something like `ROLE_GROUP_NAME_OF_GROUP`
$roles[] = $group->getRole();
}
return $roles;
}
A possible implementation of GroupInterface::getRole():
public function getRole()
{
$name = $this->getNameCanonical();
return 'ROLE_GROUP_'.mb_convert_case($name, MB_CASE_UPPER, 'UTF-8');
}
It's now just a matter of creating the required ACE-s as written in the cookbook article.
Create a voter
Finally, you could use custom voters that check for the presence of specific groups and whether the user has access to said object. A possible implementation:
<?php
namespace Acme\Bundle\DemoBundle\Authorization\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
class MySecureObjectVoter implements VoterInterface
{
/**
* {#inheritDoc}
*/
public function supportsAttribute($attribute)
{
$supported = array('VIEW');
return in_array($attribute, $supported);
}
/**
* {#inheritDoc}
*/
public function supportsClass($class)
{
return $class instanceof GroupableInterface;
}
/**
* {#inheritDoc}
*/
public function vote(TokenInterface $token, $object, array $attributes)
{
$result = VoterInterface::ACCESS_ABSTAIN;
if (!$object instanceof MySecureObject) {
return VoterInterface::ACCESS_ABSTAIN;
}
foreach ($attributes as $attribute) {
if (!$this->supportsAttribute($attribute)) {
continue;
}
// Access is granted, if the user and object have at least 1
// group in common.
if ('VIEW' === $attribute) {
$objGroups = $object->getGroups();
$userGroups = $token->getUser()->getGroups();
foreach ($userGroups as $userGroup) {
foreach ($objGroups as $objGroup) {
if ($userGroup->equals($objGroup)) {
return VoterInterface::ACCESS_GRANTED;
}
}
}
return voterInterface::ACCESS_DENIED;
}
}
}
}
For more details on voters please refer to the cookbook example.
I would avoid creating a custom security identity. Use the two other methods provided. The second solution works best, if you will be having lots of records and each of them must have different access settings. Voters could be used for setting up simple access granting logic (which most smaller systems seem to fall under) or when flexibility is necessary.
I write my answer here to keep a track of this error message.
I implemented group support with ACL and i had to hack a bit the symfony core "MutableAclProvider.php"
protected function getSelectSecurityIdentityIdSql(SecurityIdentityInterface $sid)
{
if ($sid instanceof UserSecurityIdentity) {
$identifier = $sid->getClass().'-'.$sid->getUsername();
$username = true;
} elseif ($sid instanceof RoleSecurityIdentity) {
$identifier = $sid->getRole();
$username = false;
}else {
//throw new \InvalidArgumentException('$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.');
$identifier = $sid->getClass().'-'.$sid->getGroupname();
$username = true;
}
return sprintf(
'SELECT id FROM %s WHERE identifier = %s AND username = %s',
$this->options['sid_table_name'],
$this->connection->quote($identifier),
$this->connection->getDatabasePlatform()->convertBooleans($username)
);
}
Even if the provided object is not an instance of UserSecurityIdentity or RoleSecurityIdentity it return a value. So now i can use a custom "GroupSecurityIdentity"
It's not easy to put in place but was much adapted to my system.

Resources