How to get User class instead of Userinterface when i do :
$this->security->getUser()
(Security is Symfony\Component\Security\Core\Security;)
By Example (and its just an example :), i have this custom function :
public function getUser(User $user){
}
and when i do that :
public function __construct(
Security $security,
) {
$this->security = $security;
}
getUser($this->security->getUser());
I have a warning :
getUser expects App\Entity\User, Symfony\Component\Security\Core\User\UserInterface|null given.
When a code analysis tool like phpstan or psalm warns you about a type mismatch there are multiple ways to deal with it.
Most likely you want to change your method signature and then handle the cases, the message complains about, e.g. like this:
public function getUser(UserInterface $user = null)
{
if (null === $user || ! $user instanceof User) {
// do something about the wrong types, that you might get from getSecurity()->getUser(), e.g. return or throw an exception
throw Exception(sprintf('Expected App\\Entity\\User, got %s', $user === null ? 'null' : get_class($user)));
}
... your logic
}
Now your method accepts both the interface and null that might get in there. You could also do the error handling before calling your getUser method and leave it as is, so instead of just getUser($this->security->getUser());:
$temporaryUser = $this->security->getUser();
if (!$temporaryUser instanceof User) {
throw Exception(sprintf('Expected App\\Entity\\User, got %s', $user === null ? 'null' : get_class($user)));
}
getUser($temporaryUser);
If you are sure that the code will not run into problems, you can also ignore certain error messages by creating a phpstan.neon in your project root. See: https://github.com/phpstan/phpstan#ignore-error-messages-with-regular-expressions
If $this->security->getUser() calls Symfony's core security class to return a user, it will always return an object that implements UserInterface - that's what the return type of that class defines (either implicitly through a proper return type, or through PHPDoc). This can not be altered by your own application.
To overcome your problem, you own method getUser should use this interface as the parameter type to it's argument. In that method, you can check for a more specific class (like: if($argument instanceof User)), but not in the method's function signature.
Related
I have an issue returning correct behaviour when mocking specific object while writing tests in Symfony 5.
My method:
public function myTestFunction(TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
.....
}
I was trying in my tests like:
$this->token = $this->createMock(TokenInterface::class);
$this->token
->method('getUser')
->willReturn(UserInterface::class);
Result does not replicate the behaviour I want to accomplished based on the code I posted.
As I am defining $token as a stub, I am forced to describe every interaction with it, otherwise PHPUnit, will return null for every method call.
In your ->willReturn call you should pass the real object instead of string of the class name. This is what I see wrong in your current code.
So you should create real User class which is an instance of the UserInterface and pass this object into ->willReturn($realUserObject).
I think it should help you.
I have a common structure for Symfony controller (using FOSRestBundle)
/**
* #Route\Get("users/{id}", requirements={"userId" = "(\d+)"})
*/
public function getUserAction(User $user)
{
}
Now if I request http://localhost/users/1 everything is fine. But if I request http://localhost/users/11111111111111111 I get 500 error and Exception
ERROR: value \"11111111111111111\" is out of range for type integer"
Is there a way to check id before it is transferred to database?
As a solution I can specify length of id
/**
* #Route\Get("users/{id}", requirements={"userId" = "(\d{,10})"})
*/
but then Symfony will say that there is no such route, instead of showing that the id is incorrect.
By telling Symfony that the getUserAction() argument is a User instance, it will take for granted that the {id} url parameter must be matched to the as primary key, handing it over to the Doctrine ParamConverter to fetch the corresponding User.
There are at least two workarounds.
1. Use the ParamConverter repository_method config
In the controller function's comment, we can add the #ParamConverter annotation and tell it to use the repository_method option.
This way Symfony will hand the url parameter to a function in our entity repository, from which we'll be able to check the integrity of the url parameter.
In UserRepository, let's create a function getting an entity by primary key, checking first the integrity of the argument. That is, $id must not be larger than the largest integer that PHP can handle (the PHP_INT_MAX constant).
Please note: $id is a string, so it's safe to compare it to PHP_INT_MAX, because PHP will automatically typecast PHP_INT_MAX to a string and compare it to $id. If it were an integer, the test would always fail (by design, all integers are less than or equal to PHP_INT_MAX).
// ...
use Symfony\Component\Form\Exception\OutOfBoundsException;
class UserRepository extends ...
{
// ...
public function findSafeById($id) {
if ($id > PHP_INT_MAX) {
throw new OutOfBoundsException($id . " is too large to fit in an integer");
}
return $this->find($id);
}
}
This is only an example: we can do anything we like before throwing the exception (for example logging the failed attempt).
Then, in our controller, let's include the ParamConverter annotation:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
and modify the function comment adding the annotation:
#ParamConverter("id", class="App:User", options={"repository_method" = "findSafeById"})
Our controller function should look like:
/**
* #Get("users/{id}")
* #ParamConverter("id", class="App:User", options={"repository_method" = "findSafeById"})
*/
public function getUserAction(User $user) {
// Return a "OK" response with the content you like
}
This technique allows customizing the exception, but does not give you control over the response - you'll still get a 500 error in production.
Documentation: see here.
2. Parse the route "the old way"
This way was the only viable one up to Symfony 3, and gives you a more fine-grained control over the generated response.
Let's change the action prototype like this:
/**
* #Route\Get("users/{id}", requirements={"id" = "(\d+)"})
*/
public function getUserAction($id)
{
}
Now, in the action we'll receive the requested $id and we'll be able to check whether it's ok. If not, we throw an exception and/or return some error response (we can choose the HTTP status code, the format and anything else).
Below you find a sample implementation of this procedure.
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\Form\Exception\OutOfBoundsException;
use Symfony\Component\HttpFoundation\JsonResponse;
class MyRestController extends FOSRestController {
/**
* #Get("users/{id}", requirements={"id" = "(\d+)"})
*/
public function getUserAction($id) {
try {
if ($id > PHP_INT_MAX) {
throw new OutOfBoundsException($id . " is too large to fit in an integer");
}
// Replace App\Entity\User with your actual Entity alias
$user = $this->getDoctrine()->getRepository('App\Entity\User')->find($id);
if (!$user) {
throw new \Doctrine\ORM\NoResultException("User not found");
}
// Return a "OK" response with the content you like
return new JsonResponse(['key' => 123]);
} catch (Exception $e) {
return new JsonResponse(['message' => $e->getMessage()], 400);
}
}
I would like to access my database that contains all my user inside my provider with doctrine. I followed a tutorial (http://symfony.com/doc/current/security/custom_provider.html) to build my provider for my user, so I have an loadUserByUsername function :
public function loadUserByUsername($username)
{
// make a call to your webservice here
$player = new Player();
$player = $this->getDoctrine()
->getRepository('AppBundle:Player')
->findOneByPseudo($username);
// pretend it returns an array on success, false if there is no user
if ($player) {
return $player;
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
But of course my getDoctrine() function is undefined. So there is something I don't understand with the provider, I am trying to use it to be authenticated when I login so I need a provider, but why I can't search inside my database? How should I write this function? Thank for your help
EDIT :
When I add doctrine by service.yml (and after writting my constructor inside my provider), I have this error :
FatalThrowableError in PlayerProvider.php line 13:
Type error: Argument 1 passed to AppBundle\Security\PlayerProvider::__construct() must be an instance of Doctrine\Bundle\DoctrineBundle\Registry, instance of Doctrine\ORM\EntityManager given, called in /home/jean/PW6/SkA/SkeletonsOnlineV2/skeleton-online/var/cache/dev/appDevDebugProjectContainer.php on line 327
EDIT 2 : When I just put arguments: ['#doctrine'] inside my service.yml, I get an error that says that doctrine is undefined
EDIT 3 : It works now, I just made a dumb mistake
If you read further, it says the following (emphasis mine):
The real implementation of the user provider will probably have some dependencies or configuration options or other services. Add these as arguments in the service definition.
So in your case it would be something like
# app/config/services.yml
services:
app.webservice_user_provider:
class: AppBundle\Security\User\WebserviceUserProvider
arguments: ['#doctrine']
And your class needs a constructor
class WebserviceUserProvider implements UserProviderInterface
{
protected $doctrine;
public function __construct (\Doctrine\Bundle\DoctrineBundle\Registry $doctrine)
{
$this->doctrine = $doctrine;
}
// ...
}
Then in your method replace $this->getDoctrine() with just $this->doctine
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.
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.