How to use dump() or dd() inside a Symfony Custom Voter? - symfony

I have followed several example from symfony, api-platform and several stackoverflow examples but to no avail. I don't know if I am doing something wrong or I don't understand the concept of the voter and roles. When I tried to access the endpoint, it throws Only user with permission can view dashboard.
In services.yaml
app.user_permission:
class: App\Security\SecurityVoter
arguments: ['#security.access.decision_manager']
tags:
- { name: security.voter}
I created a custom voter to use. Here I have done several changes, deleted several things to adopt the example I saw on StackOverflow Example
use App\Entity\Product;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class SecurityVoter extends Voter {
private $decisionManager;
const VIEW = 'view';
const EDIT = 'edit';
public function __construct (AccessDecisionManagerInterface $decisionManager) {
$this->decisionManager = $decisionManager;
}
protected function supports($attribute, $subject): bool {
// if the attribute isn't one we support, return false
if (!in_array($attribute, [self::VIEW, self::EDIT])) {
return false;
}
return true;
}
/**
* #param string $attribute
* #param TokenInterface $token
* #return bool
*/
protected function voteOnAttribute($attribute, $object, TokenInterface $token): bool {
$user = $token->getUser();
if (!$user instanceof UserInterface) {
// the user must be logged in; if not, deny access
return false;
}
// check ROLE_USER
if ($this->security->isGranted('ROLE_USER')) {
return true;
}
switch ($attribute) {
case self::VIEW:
if($this->decisionManager->decide($token, ['ROLE_USER'])) {
return true;
}
break;
case self::EDIT:
if($this->decisionManager->decide($token, ['ROLE_USER'])) {
return true;
}
break;
}
throw new \LogicException('This code should not be reached!');
}
}
In my Entity, I defined something like this.
#[ApiResource(
attributes: ["security" => "is_granted('ROLE_USER')"],
collectionOperations: [
"get",
"post" => [
"security_post_denormalize" => "is_granted('ROLE_USER)",
"security_message" => "Only user with permission can create a dashboard.",
],
],
itemOperations: [
"get" => [ "security" => "is_granted('VIEW') " , "security_message" => "Only user with permission can view dashboard."],
"put" => [ "security" => "is_granted('EDIT')", "security_message" => "Only user with permission can edit dashboard."],
],
)]
I am currently on Symfony 5.4.7 and I have tried to use the example code. Nothing seems to be working. I have to use dd() and dump(), nothing was printed on the console or profiler. I have used loggerInterface (maybe I didn't do it correctly), and I didn't get to see anything output to var.

You're closer than you think. You don't need use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface. You can make use of Security class as follows.
use App\Entity\Product;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class SecurityVoter extends Voter {
private $security;
const VIEW = 'view';
const EDIT = 'edit';
public function __construct ( Security $security) {
$this->security = $security;
}
protected function supports($attribute, $subject): bool {
// if the attribute isn't one we support, return false
$supportsAttribute = in_array($attribute, ['VIEW', 'EDIT']);
$supportsSubject = $subject instanceof WorkshopSession;
return $supportsAttribute && $supportsSubject;
}
/**
* #param string $attribute
* #param Product $product
* #param TokenInterface $token
* #return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool {
$user = $token->getUser();
if (!$user instanceof UserInterface) {
// the user must be logged in; if not, deny access
return false;
}
dd($user);
// check ROLE_USER
if ($this->security->isGranted('ROLE_USER')) {
return true;
}
switch ($attribute) {
case self::VIEW:
if($this->security->isGranted('ROLE_USER')) {
return true;
}
break;
case self::EDIT:
if($this->security->isGranted('ROLE_USER')) {
return true;
}
break;
}
throw new \LogicException('This code should not be reached!');
}
}
Meanwhile, you don't need to configure service for this.
To inject the voter into the security layer, you must declare it as a service and tag it with security.voter. But if you're using the default services.yaml configuration, that's done automatically for you!
In your entity
#[ApiResource(
attributes: ["security" => "is_granted('ROLE_USER')"],
collectionOperations: [
"get",
"post" => [
"security_post_denormalize" => "is_granted('ROLE_USER')",
"security_message" => "Only user with permission can create a dashboard.",
],
],
itemOperations: [
"get" => [ "security" => "is_granted('VIEW', object) " ],
"put" => [ "security" => "is_granted('EDIT')", "security_message" => "Only user with permission can edit dashboard."],
],
)]
You can also read this for reference - API Platform
NOTE: you can use dd() - e.g. dd($user);

Related

The App\Security\LoginFormAuthenticator::getUser() method must return a UserInterface. You returned Softonic\GraphQL\Response

I'm trying to customize the login with a graphql query to load the User data and it shows me that error.
I use php bin / console make: auth and it works correctly when I query the $ user variable from MySQL but when I load the $ user variable from GRAPHQL it shows me that error.
This is the code:
public function getUser($credentials, UserProviderInterface $userProvider)
{
if ($credentials["csrf_token"] != "") {
$client = new GH();
$headers = ['Content-Type' => 'application/x-www-form-urlencoded'];
$result = $client->request('POST', 'https://graphql.clientecliente.com/get-token', [
'json' => [
'usuario' => $credentials["email"],
'clave' => $credentials["password"]
]
]);
$data = $result->getBody()->getContents();
$objetodata=(json_decode($data));
$token = $objetodata->token;
}
//Conexion servidor GraphQL
$servidor = $this->params->get('enlace_graphql');
//Codigo debe salir desde la base de datos
$codigoAuth = $token;
$options = [
'headers' => [
'Authorization' => 'Bearer ' . $codigoAuth,
],
];
$client = \Softonic\GraphQL\ClientBuilder::build($servidor, $options);
$gpl1 = <<<'QUERY'
query ($limit: Int!, $offset: Int!, $CODIGO_USUARIO: String) {
obt_usuarios(limit: $limit, offset: $offset, CODIGO_USUARIO: $CODIGO_USUARIO) {
totalCount
OBT_USUARIOS {
CODIGO_USUARIO
CLAVE_USUARIO
CODIGO_EMPRESA
CODIGO_CLIENTE
CODIGO_PASAJERO
ES_ADMINISTRADOR
}
}
}
QUERY;
$variables1 = [
'limit' => 10,
'offset' => 0,
'CODIGO_USUARIO' => $credentials["email"]
];
//$user = $this->entityManager->getRepository(Usuario::class)->findOneBy(['email' => $credentials['email']]);
$user = $client->query($gpl1,$variables1);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('El usuario no existe');
}
return $user;
}
Maybe, Anything idea?
Update.
Now create the custom user provider. I use this command: php bin/console make:user;
<?php
//src/Security/UserProvider.php
namespace App\Security;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
//Consulta API
use GuzzleHttp\Client as GH;
use GuzzleHttp\Pool;
//Consulta GraphQL
use Softonic\GraphQL;
use Softonic\GraphQL\Client;
use Softonic\GraphQL\ClientBuilder;
use function GuzzleHttp\json_decode;
class UserProvider implements UserProviderInterface
{
/**
* Symfony calls this method if you use features like switch_user
* or remember_me.
*
* If you're not using these features, you do not need to implement
* this method.
*
* #return UserInterface
*
* #throws UsernameNotFoundException if the user is not found
*/
public function loadUserByUsername($username)
{
$client = new GH();
$headers = ['Content-Type' => 'application/x-www-form-urlencoded'];
$result = $client->request('POST', 'https://graphql.cliente.com.ec/get-token', [
'json' => [
'usuario' => 'test',
'clave' => 'test1234',
]
]);
$data = $result->getBody()->getContents();
$objetodata=(json_decode($data));
//var_dump($objetodata->token);
//exit;
$token = $objetodata->token;
$servidor = "https://graphql.cliente.com/graphql";
//Codigo debe salir desde la base de datos
//$codigoAuth = $token;
$codigoAuth= "12345678912345678mlvIjoiT0JUX0NjIxOTI1ODN9.8dNiZI6iZsYS0plVU0fuqFlhkTDSOt9OFy5B-WZiRmk";
//var_dump($codigoAuth);
//exit;
//echo $codigoAuth; exit;
$options = [
'headers' => [
'Authorization' => 'Bearer ' . $codigoAuth,
],
];
$client = \Softonic\GraphQL\ClientBuilder::build($servidor, $options);
$gpl1 = <<<'QUERY'
query ($limit: Int!, $offset: Int!, $CODIGO_USUARIO: String) {
obt_usuarios(limit: $limit, offset: $offset, CODIGO_USUARIO: $CODIGO_USUARIO) {
totalCount
OBT_USUARIOS {
CODIGO_USUARIO
CLAVE_USUARIO
CODIGO_EMPRESA
CODIGO_CLIENTE
CODIGO_PASAJERO
ES_ADMINISTRADOR
}
}
}
QUERY;
$variables1 = [
'limit' => 10,
'offset' => 0,
'CODIGO_USUARIO' => 'test'
];
$user=$client->query($gpl1,$variables1);
//var_dump($user);exit;
//$username=$user;
return $user;
//throw new \Exception('TODO: fill in loadUserByUsername() inside '.__FILE__);
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*
* #return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
// Return a User object after making sure its data is "fresh".
// Or throw a UsernameNotFoundException if the user no longer exists.
throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
return $this->loadUserByUsername($user->getUsername());
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass($class)
{
return User::class === $class;
}
}
config/packages/security.yaml:
providers:
app_user_provider:
id: App\Security\UserProvider
firewalls:
secured_area:
anonymous: true
form_login:
login_path: loginanterior
check_path: login_check
default_target_path: index
provider: app_user_provider
remember_me: true
logout:
path: logout
target: index
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
src/Security/User.php
<?php
namespace App\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
private $email;
private $roles = [];
/**
* #var string The hashed password
*/
private $password;
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}
However, after this it does not show me any errors or anything. Just go back to the login again
You have to map your Softonic\GraphQL\Response to your User model. This should be done in your custom UserProvider so Authenticator doesn't know where the user actually comes from. You can also look at existing user providers code for inspiration.
Thanks to the help of Ion Bazan
I understood that I had to make a custom UserProvider.
Final steps encoder correction
config/packages/security.yaml
encoders:
App\Security\User:bcrypt
GraphQL result mapping on USER object by sending user, ROL and password (must be sent in the encryption correctly)
App/src/Security/UserProvider.php
$userData=$client->query($gpl1,$variables1)->getData();
//var_dump($userData);
$user = new User();
$user->setEmail($userData['obt_usuarios']["OBT_USUARIOS"][0]["CODIGO_USUARIO"]);
$user->setRoles(array("ROLE_USER"));
$user->setPassword('$2a$10$JSMRqSFX9Xm1gAbc/YzVcu5gETsPF8HJ3k5Zra/RZlx4IXfadwmW.');
return $user;
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*
* #return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
// Return a User object after making sure its data is "fresh".
// Or throw a UsernameNotFoundException if the user no longer exists.
//throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
return $this->loadUserByUsername($user);
}
With this finally it worked

Dynamic EntityManager find () method returns "table not found" when a custom repository is in use

First I will explain why and how the solution works and then the problems I have encountered. If you think there is a better way to do what I do, I'd love to hear it. I would also like to know why doctrine behaves in this way.
It turns out that my aplication needs to connect to a different database according to the client. I have a table, in a fixed database, containing the connection information that is used in some request.
I have had success with the following code:
class DynamicEntityManager {
protected $em;
private $request;
private $client_id;
public function __construct(RequestStack $request, EntityManagerInterface $em){
$this->em = $em;
$this->request = $request;
}
public function getEntityManager(ClientConn $client = null) {
$request = $this->request->getCurrentRequest();
if($client == NULL){
$domain = $request->attributes->get('domain');
if($domain == "" || $domain == NULL){
throw new \Exception("Error de conexion", 1);
}
$client = $this->em->getRepository(ClientConn::class)->findOneBy(array(
"subdomain" => $domain
));
if($client == NULL){
throw new \Exception("Error de conexion", 1);
}
}
$connectionDB = $client->getConnection();
$dbdriver = 'oci8';
$conexionSplit = explode(':',$connectionDB);
$dbhost = $conexionSplit[0];
$dbport = $conexionSplit[1];
$dbname = $conexionSplit[2];
$dbuser = $client->getUsuarioBd();
$dbpass = $client->getClaveBd();
$service = false;
$this->client_id = $client->getId();
if(strpos($dbname,'SN=') !== false){
$parts = explode('=',$dbname);
$dbname = $parts[1];
$service = true;
}
$request->attributes->set('client_id',$client->getId());
$conn = array(
'driver' => $dbdriver,
'host' => $dbhost,
'port' => $dbport,
'dbname' => $dbname,
'user' => $dbuser,
'password' => $dbpass,
'service' => $service,
'charset' => 'UTF8',
'schema' => null
);
return EntityManager::create($conn, $this->em->getConfiguration());
}
}
As you can see I return EntityManager::create($conn, $this->em->getConfiguration ()) with the new connection. The way I use it is the next:
/**
* #Route("/api/client/{id}/conf/{confID}", name="conf.show")
* #Method({"GET"})
*/
public function show(ClientConn $client, Request $request, DynamicEntityManager $dem ,$confId){
try {
$em = $dem->getEntityManager($client);
$entity = $em->getRepository(Configuration::class)->find($confId);
return new JsonResponse($entity, 200);
}
catch(\Exception $ex) {
return new JsonResponse([
"excepcion" => $ex->getMessage()
], $ex->getCode());
}
}
It works as expected or so I believed until I saw that when the entity has a custom repository it is unable to use the dynamic connection and therefore the previous route will return a table not found exception.
#ORM\Entity() <-- Works like a charm
#ORM\Entity(repositoryClass="App\Repository\ConfigurationRepository")<-- Table not found.
It works in the repository if I create the connection again, although I do not like the solution. So, what do I want? I would like to be able to use the basic methods like find (), findBy () and others without having to rewrite them every time I use a custom repository.
class ConfigurationRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry, DynamicEntityManager $dem)
{
parent::__construct($registry, Configuration::class);
$this->dem= $dem;
}
public function uglyFind($client, $confID)
{
$query = $this->dem->getEntityManager($client)->createQueryBuilder('conf')
->select("conf")
->from(ConfPedidosLentes::class,'conf')
->where('conf.id = :value')->setParameter('value', $confID)
->getQuery();
return $query->getOneOrNullResult();
}
I will really appreciate any contribution and thought in this matter.
Instead of:
class ConfigurationRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry, DynamicEntityManager $dem)
{
parent::__construct($registry, Configuration::class);
$this->dem= $dem;
}
...
try extending EntityRepository (without using a constructor) and use find as you did in your controller:
use Doctrine\ORM\EntityRepository;
class ConfigurationRepository extends EntityRepository
{
}
ServiceEntityRepository is an optional EntityRepository base class with a simplified constructor for autowiring, that explicitly sets the entity manager to the EntityRepository base class. Since you have not configured your doctrine managers to handle these connections properly (it's not even possible actually with so many connections), ServiceEntityRepository will pass a wrong EntityManager instance to the EntityRepository subclass, that's why you should not extend ServiceEntityRepository but EntityRepository.

Redirect to page prior to login form Symfony 3.4 is resolving to liip_imagine route in a custom listener

I'm having problems getting a redirect after login to work in Symfony.
It works for some pages but for others, the last_route session variable is being set to a users profile picture that uses the liip_imagine_filter:
"last_route" => [
"name" => "liip_imagine_filter",
"params" => [
"filter" => "profile_picture"
"path" => "frederick-jacobson/5ba60fc93056b.png"
]
]
LoginFormAuthenticator:
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
/*...*/
protected function getDefaultSuccessRedirectURL()
{
/** #var Session $session */
$session = $this->container->get('session');
$priorPage = $session->get('last_route');
return $this->router->generate($priorPage['name'], $priorPage['params']);
// return $this->router->generate('poll_index');
}
}
This means that it tries to redirect to an image URL.
services.yml:
poll.last_route_event_listener:
class: PollBundle\Services\LastRouteListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 30 }
LastRouteListener:
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernel;
class LastRouteListener
{
public function onKernelRequest(GetResponseEvent $event)
{
// Do not save subrequests
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
$request = $event->getRequest();
$session = $request->getSession();
$routeName = $request->get('_route');
$routeParams = $request->get('_route_params');
if ($routeName[0] == '_') {
return;
}
$routeData = ['name' => $routeName, 'params' => $routeParams];
// Do not save same matched route twice
$thisRoute = $session->get('this_route', []);
if ($thisRoute == $routeData) {
return;
}
$session->set('last_route', $thisRoute);
$session->set('this_route', $routeData);
}
}
Can someone please help me work out what I'm doing wrong and/or tell me the correct way to handle redirecting to the page prior to login?
As pointed out in the comments below my question. I was using a custom listener that was picking up the liip_imagine_filter route and setting it to the last_route session variable.
I could just add a check in to the listener, like this:
if ($routeName[0] == '_' || $routeName == 'liip_imagine_filter') {
return;
}
But a better way to handle it is to use the built in Symfony\Component\Security\Http\Util\TargetPathTrait
It is usually set automatically when a user hits a restricted page, but it can be set manually with $this->saveTargetPath($request->getSession(), $providerKey, $request->getUri());
Then you can use $targetPath to find the route to redirect to in LoginFormAuthenticator:
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
/*...*/
use TargetPathTrait;
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$targetPath = $this->getTargetPath($request->getSession(), $providerKey);
if (!$targetPath || $request->getBaseUrl() && !strpos($targetPath, $request->getBaseUrl())) {
$targetPath = $this->container->get('router')
->generate('poll_index');
}
return new RedirectResponse($targetPath);
}
}

Symfony 2.8 session lost after login_check redirect

I am currently porting my old custom framework to a Symfony-based custom framework using Symfony's components. So far everything is going smoothly, except for the login part. Here are a few details about the project:
I'm using Symfony Security Component v2.8
My sessions are being stored in a database using PDOSessionHandler
I'm using Guard to authenticate my users.
The problem arises when a user tries to login to an admin area using a form. After the form submission, the user is forwarded to the login_check route where all credentials are successfully checked. The user role ROLE_ADMIN is set and finally the user is redirected to a secure page, but then gets redirected automatically back to the login. The order of events is like so:
login -> login_check -> admin -> login
I have done some debugging by setting breakpoints in ContextListener::OnKernelResponse and found out that a token is never saved in the session,because the method returns here:
if (!$event->getRequest()->hasSession()) {
return;
}
Also, I am able to see a session being added to the database table and the session id remains constant throughout the redirect. In the end I am bounced back to the login page and my user is set to .anon Somewhere between /login_check and /admin my token is lost.
I have run out of ideas on how to debug this. I am pasting some code to help get an idea of my setup, but I think these are fine.
My firewall configuration is looking like this
return[
'security'=>[
//Providers
'providers'=>[
'user' => array(
'id' => 'security.user.provider.default',
),
],
//Encoders
'encoders'=>[
'Library\\Security\\Users\\User::class' => array('algorithm' => 'bcrypt', 'cost'=> 15)
],
'firewalls'=>
[
'backend'=>array(
'security' =>true,
'anonymous' => true,
'pattern' => '^/',
'guard' => array(
'authenticators' => array(
'security.guard.form.authenticator',
'security.authenticator.token'
),
'entry_point'=>'security.guard.form.authenticator'
),
),
],
'access_control'=>array(
array('path' => '^/admin', 'roles' => ['ROLE_ADMIN']),
array('path' => '^/api', 'roles' => ['ROLE_API']),
array('path' => '^/pos', 'roles' => ['ROLE_POS']),
array('path' => '^/dashboard', 'roles' => ['ROLE_SUPER_ADMIN']),
array('path' => '^/login', 'roles' => ['IS_AUTHENTICATED_ANONYMOUSLY']),
array('path' => '/', 'roles' => ['IS_AUTHENTICATED_ANONYMOUSLY']),
)
]];
My UserInterface:
class User implements UserInterface, EquatableInterface{
private $username;
private $password;
private $salt;
private $roles;
public function __construct($username, $password, $salt, array $roles)
{
$this->username = $username;
$this->password = $password;
$this->salt = $salt;
$this->roles = $roles;
}
public function getRoles()
{
return $this->roles;
}
public function getPassword()
{
return $this->password;
}
public function getSalt()
{
return $this->salt;
}
public function getUsername()
{
return $this->username;
}
public function eraseCredentials()
{
}
public function isEqualTo(UserInterface $user)
{
if (!$user instanceof DefaultUserProvider) {
return false;
}
if ($this->password !== $user->getPassword()) {
return false;
}
if ($this->salt !== $user->getSalt()) {
return false;
}
if ($this->username !== $user->getUsername()) {
return false;
}
return true;
}}
My UserProvider
namespace Library\Security\UserProviders;
use Library\Nosh\Project\Project;
use Library\Security\Users\User;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use PDO;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
class DefaultUserProvider implements UserProviderInterface{
private $db;
private $project;
public function __construct(\PDO $db, Project $project)
{
$this->db = $db;
$this->project=$project;
}
public function loadUserByUsername($username)
{
$projectId = $this->project->id();
$statement = $this->db->prepare("SELECT * FROM users WHERE :userLogin IN (user_login, user_email) AND project_id=:project_id AND user_active=:user_active");
$statement->bindParam(':userLogin', $username, PDO::PARAM_STR);
$statement->bindValue(':user_active', 1, PDO::PARAM_INT);
$statement->bindValue(':project_id', $projectId, PDO::PARAM_INT);
$statement->execute();
if (!$user = $statement->fetch()) {
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
$roles = explode(',', $user['user_roles']);
return new User($user['user_login'], $user['user_password'],$salt='',$roles);
}
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 === 'Library\\Security\\Users\\User';
}
}
I was able to solve my own problem after several days of debugging. The reason I had no session is because I had failed to implement a SessionListener to store the session into the request. This should not be an issue for anyone using the Symfony Framework or Silex. It was only an issue for me, because I am actually creating something from scratch.
For anyone wondering how to do this, here are the necessary steps:
Create a class which extends Symfony\Component\HttpKernel\EventListener\SessionListener
Implement the method getSession()
Make sure you add the class to the dispatcher with addSubscriber()
See my example below:
SessionListener
use Symfony\Component\HttpKernel\EventListener\SessionListener as AbstractSessionListener;
class SessionListener extends AbstractSessionListener {
private $container;
public function __construct(Container $container)
{
$this->container=$container;
}
protected function getSession()
{
if (!$this->container->has('session')) {
return;
}
return $this->container->get('session');
}
}
SessionServiceProvider
use Core\Container;
use Interfaces\EventListenerProviderInterface;
use Interfaces\ServiceProviderInterface;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class SessionServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface {
protected $options=[
'cookie_lifetime'=>2592000,//1 month
'gc_probability'=>1,
'gc_divisor'=>1000,
'gc_maxlifetime'=>2592000
];
public function register(Container $container){
switch($container->getParameter('session_driver')){
case 'database':
$storage = new NativeSessionStorage($this->options, new PdoSessionHandler($container->get('db')));
break;
case 'file':
$storage = new NativeSessionStorage($this->options, new NativeFileSessionHandler($container->getParameter('session_dir')));
break;
default:
$storage = new NativeSessionStorage($this->options, new NativeFileSessionHandler($container->getParameter('session_dir')));
break;
}
$container->register('session',Session::class)->setArguments([$storage]);
}
public function subscribe(Container $container, EventDispatcherInterface $dispatcher)
{
$dispatcher->addSubscriber(new SessionListener($container));
}
}

Symfony2 custom Voter: cannot have access to getDoctrine from inside the Voter

I'm trying to implement a custom Voter.
From the controller I call it this way:
$prj = $this->getDoctrine()->getRepository('AppBundle:Project')->findOneById($id);
if (false === $this->get('security.authorization_checker')->isGranted('responsible', $prj)) {
throw new AccessDeniedException('Unauthorised access!');
}
The first line properly retrieves the Project object (I checked with a dump).
The problem occurs inside the voter
<?php
namespace AppBundle\Security\Authorization\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ProjectVoter implements VoterInterface
{
const RESPONSIBLE = 'responsible';
const ACCOUNTABLE = 'accountable';
const SUPPORT = 'support';
const CONSULTED = 'consulted';
const INFORMED = 'informed';
public function supportsAttribute($attribute)
{
return in_array($attribute, array(
self::RESPONSIBLE,
self::ACCOUNTABLE,
self::SUPPORT,
self::CONSULTED,
self::INFORMED,
));
}
public function supportsClass($class)
{
$supportedClass = 'AppBundle\Entity\Project';
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}
/**
* #var \AppBundle\Entity\Project $project
*/
public function vote(TokenInterface $token, $project, array $attributes)
{
// check if class of this object is supported by this voter
if (!$this->supportsClass(get_class($project))) {
return VoterInterface::ACCESS_ABSTAIN;
}
// check if the voter is used correct, only allow one attribute
// this isn't a requirement, it's just one easy way for you to
// design your voter
if (1 !== count($attributes)) {
throw new \InvalidArgumentException(
'Only one attribute is allowed'
); //in origin it was 'for VIEW or EDIT, which were the supported attributes
}
// set the attribute to check against
$attribute = $attributes[0];
// check if the given attribute is covered by this voter
if (!$this->supportsAttribute($attribute)) {
return VoterInterface::ACCESS_ABSTAIN;
}
// get current logged in user
$user = $token->getUser();
// make sure there is a user object (i.e. that the user is logged in)
if (!$user instanceof UserInterface) {
return VoterInterface::ACCESS_DENIED;
}
$em = $this->getDoctrine()->getManager();
$projects = $em->getRepository('AppBundle:Project')->findPrjByUserAndRole($user, $attribute);
foreach ($projects as $key => $prj) {
if ($prj['id'] === $project['id'])
{
$granted = true;
$index = $key; // save the index of the last time a specifif project changed status
}
}
if($projects[$index]['is_active']===true) //if the last status is active
return VoterInterface::ACCESS_GRANTED;
else
return VoterInterface::ACCESS_DENIED;
}
}
I get the following error
Attempted to call method "getDoctrine" on class
"AppBundle\Security\Authorization\Voter\ProjectVoter".
I understand that the controller extends Controller, that is why I can use "getDoctrine" there. How can I have access to my DB from inside the Voter?
I solved it. This is pretty curious: I spend hours or days on a problem, then post a question here, and I solve it myself within an hour :/
I needed to add the following in my voter class:
public function __construct(EntityManager $em)
{
$this->em = $em;
}
I needed to add the following on top:
use Doctrine\ORM\EntityManager;
I also needed to add the arguments in the service.yml
security.access.project_voter:
class: AppBundle\Security\Authorization\Voter\ProjectVoter
arguments: [ #doctrine.orm.entity_manager ]
public: false
tags:
- { name: security.voter }

Resources