Custom validation in symfony2 - symfony

I am new to symfony. I have decided to move my wheel with Symfony version 2.
In my user form:
I would like to validate uniqueness of email in the database .
I would like to also validate password with confirm password field .
I could find any help in the symfony2 doc.

This stuff took me a while to track down too, so here's what I came up with. To be honest I'm not really sure about the getRoles() method of the User entity, but this is just a test setup for me. Context items like that are provided solely for clarity.
Here are some helpful links for further reading:
A validation constraint that forces uniqueness
A field type for repeated (double-entry-verified) fields
I set this all up to make sure it worked as a UserProvider for security as well since I figured you were probably doing that. I also assumed you were using the email as the username, but you don't have to. You could create a separate username field and use that. See Security for more information.
The Entity (only the important parts; autogenerateable getters/setters are omitted):
namespace Acme\UserBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints as DoctrineAssert;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*
* list any fields here that must be unique
* #DoctrineAssert\UniqueEntity(
* fields = { "email" }
* )
*/
class User implements UserInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length="255", unique="true")
*/
protected $email;
/**
* #ORM\Column(type="string", length="128")
*/
protected $password;
/**
* #ORM\Column(type="string", length="5")
*/
protected $salt;
/**
* Create a new User object
*/
public function __construct() {
$this->initSalt();
}
/**
* Generate a new salt - can't be done as prepersist because we need it before then
*/
public function initSalt() {
$this->salt = substr(str_shuffle(str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',5)),0,5);
}
/**
* Is the provided user the same as "this"?
*
* #return bool
*/
public function equals(UserInterface $user) {
if($user->email !== $this->email) {
return false;
}
return true;
}
/**
* Remove sensitive information from the user object
*/
public function eraseCredentials() {
$this->password = "";
$this->salt = "";
}
/**
* Get the list of roles for the user
*
* #return string array
*/
public function getRoles() {
return array("ROLE_USER");
}
/**
* Get the user's password
*
* #return string
*/
public function getPassword() {
return $this->password;
}
/**
* Get the user's username
*
* We MUST have this to fulfill the requirements of UserInterface
*
* #return string
*/
public function getUsername() {
return $this->email;
}
/**
* Get the user's "email"
*
* #return string
*/
public function getEmail() {
return $this->email;
}
/**
* Get the user's salt
*
* #return string
*/
public function getSalt() {
return $this->salt;
}
/**
* Convert this user to a string representation
*
* #return string
*/
public function __toString() {
return $this->email;
}
}
?>
The Form class:
namespace Acme\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class UserType extends AbstractType {
public function buildForm(FormBuilder $builder, array $options) {
$builder->add('email');
/* this field type lets you show two fields that represent just
one field in the model and they both must match */
$builder->add('password', 'repeated', array (
'type' => 'password',
'first_name' => "Password",
'second_name' => "Re-enter Password",
'invalid_message' => "The passwords don't match!"
));
}
public function getName() {
return 'user';
}
public function getDefaultOptions(array $options) {
return array(
'data_class' => 'Acme\UserBundle\Entity\User',
);
}
}
?>
The Controller:
namespace Acme\UserBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Acme\UserBundle\Entity\User;
use Acme\UserBundle\Form\Type\UserType;
class userController extends Controller
{
public function newAction(Request $request) {
$user = new User();
$form = $this->createForm(new UserType(), $user);
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
// encode the password
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword($user->getPassword(), $user->getSalt());
$user->setPassword($password);
$em = $this->getDoctrine()->getEntityManager();
$em->persist($user);
$em->flush();
return $this->redirect($this->generateUrl('AcmeUserBundle_submitNewSuccess'));
}
}
return $this->render('AcmeUserBundle:User:new.html.twig', array (
'form' => $form->createView()
));
}
public function submitNewSuccessAction() {
return $this->render("AcmeUserBundle:User:submitNewSuccess.html.twig");
}
Relevant section of security.yml:
security:
encoders:
Acme\UserBundle\Entity\User:
algorithm: sha512
iterations: 1
encode_as_base64: true
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
main:
entity: { class: Acme\UserBundle\Entity\User, property: email }
firewalls:
secured_area:
pattern: ^/
form_login:
check_path: /login_check
login_path: /login
logout:
path: /logout
target: /demo/
anonymous: ~

Check out http://github.com/friendsofsymfony there is a UserBundle that have that functionality. You can also check http://blog.bearwoods.com where there is a blog post about adding a custom field, constraint and validator for Recaptcha.
Thoose resources should get you started on the right path if you are still running into trouble people are generally helpful and friendly on irc at #symfony-dev on the Freenode network. On Freenoce there is also a general channel #symfony where you can ask questions about how to use stuff where #symfony-dev is for the development of Symfony2 Core.
Hopefully this will help you move forward with your project.

I think the main thing you need to look out for when creating your custom validator is the constant specified in the getTargets() method.
If you change
self::PROPERTY_CONSTRAINT
to:
self::CLASS_CONSTRAINT
You should be able to access all properties of the entity, not only a single property.
Note: If you are using annotations to define your constraints you will now need to move the annotation which defines your validator up to the top of the class as it is now applicable to the whole Entity and not just the single property.

You should be able to get everything you need from the docs. Specifically the constraints which has information on email checking. There's also documentation on writing custom validators.

I've done all like on http://symfony.com/doc/2.0/book/validation.html
My config:
validator.debit_card:
class: My\Validator\Constraints\DebitCardValidator
tags:
- { name: validator.constraint_validator, alias: debit_card }
tried to use it with
#assert:DebitCard
#assert:debitCard
#assert:debit_card
but it is not triggered?

unique email from database
validation.yml
Dashboard\ArticleBundle\Entity\Article:
constraints:
#- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: senderEmail
- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: { fields: senderEmail, message: This email already exist }
Password with confirm password
$builder->add('password', 'repeated', array(
'first_name' => 'password',
'second_name' => 'confirm',
'type' => 'password',
'required' => false,
));

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

Symfony authentication doesn't work

I am writing regarding the Symfony authentication problem, which occurred last month and I still cannot find a solution, so I am dependent on you :D
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Table(name="app_users")
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, \Serializable
{
//id,username,password
public function getSalt()
{
return null;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt,
));
}
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
) = unserialize($serialized);
}
}
This is my User entity and now below you can see my security.yaml which I think I configured right:
security:
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
db_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|img|js)/
security: false
main:
anonymous: true
http_basic: ~
provider: db_provider
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
Whenever I am trying to access /admin route it shows me http-basic login but whenever I input "admin, admin" nothing happens. IN my database I have one user with username:admin and password admin which is hashed by bcrypt.
Not using authentication then everything works as it should, I get all data from the database as it should be after authentication.
Thanks for your help guys!
Your problem
As Med already pointed out, your User entity has the ROLE_USER role as default:
/* App/Entity/User.php */
public function getRoles()
{
return array('ROLE_USER');
}
Your access_control configuration on the other hand states that the route /admin can only be accessed with a user that has the ROLE_ADMIN role:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
That means, your user "admin" lacks the sufficient role to access /admin.
Solution
You need to be able to assign multiple roles to the user. One possible way is saving the roles as a concatenated string and returning it as an array:
/* App/Entity/User.php */
/**
* #ORM\Column(name="roles", type="string")
* #var string
*/
private $roles;
/**
* Get the user roles as an array of strings
* #return array
*/
public function getRoles()
{
return explode($roles, ',');
}
You can even add some methods to manage your roles via the entity class:
/* App/Entity/User.php */
/**
* Add a new role
* #param string $role name of the role
* #return this
*/
public function addRole($role)
{
$roles = $this->getRoles();
if (array_search($role, $roles) === false) {
$roles[] = $role;
$this->roles = implode(',', $roles);
}
return $this;
}
/**
* Remove a role
* #param string $role name of the role
* #return this
*/
public function removeRole($role)
{
$roles = $this->getRoles();
$searchResult = array_search($role, $roles);
if ($searchResult !== false) {
unset($roles[$searchResult]);
$this->roles = implode(',', $roles);
}
return $this;
}

Symfony 4 Guard Neo4j OGM

Im having some trouble getting the Neo4j OGM/Symfony bundle to work with the Symfony Guard. I have added users to the database successfully. unfortunately it doesn't want to sign in and I get the following error:
Symfony\Component\Security\Core\Exception\AuthenticationServiceException: Class "App\Entity\Generic\User" is not a valid entity or mapped super class. in /home/vagrant/Code/support4neo/vendor/symfony/security/Core/Authentication/Provider/DaoAuthenticationProvider.php:85 Stack trace:
#0 /home/vagrant/Code/support4neo/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php(142): session_start()
#1 /home/vagrant/Code/support4neo/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php(299): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->start()
#2 /home/vagrant/Code/support4neo/vendor/symfony/http-foundation/Session/Session.php(249): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->getBag('attributes')
#3 /home/vagrant/Code/support4neo/vendor/symfony/http-foundation/Session/Session.php(271): Symfony\Component\HttpFoundation\Session\Session->getBag('attributes')
#4 /home/vagrant/Code/support4neo/vendor/symfony/http-foundation/Session/Session.php(73): Symfony\Component\HttpFoundation\Session\Session->getAttributeBag()
#5 /home/vagrant/Code/support4neo/vendor/symfony/security/Http/Firewall/ContextListener.php(88): Symfony\Component\HttpFoundation\Session\Session->get('_security_main')
#6 /home/vagrant/Code/support4neo/vendor/symfony/security-bundle/Debug/WrappedListener.php(46): Symfony\Component\Security\Http\Firewall\ContextListener->handle(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#7 /home/vagrant/Code/support4neo/vendor/symfony/security-bundle/Debug/TraceableFirewallListener.php(35): Symfony\Bundle\SecurityBundle\Debug\WrappedListener->handle(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#8 /home/vagrant/Code/support4neo/vendor/symfony/security/Http/Firewall.php(56): Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener->handleRequest(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), Object(Symfony\Component\DependencyInjection\Argument\RewindableGenerator))
#9 /home/vagrant/Code/support4neo/vendor/symfony/security-bundle/EventListener/FirewallListener.php(48): Symfony\Component\Security\Http\Firewall->onKernelRequest(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#10 [internal function]: Symfony\Bundle\SecurityBundle\EventListener\FirewallListener->onKernelRequest(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), 'kernel.request', Object(Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher))
#11 /home/vagrant/Code/support4neo/vendor/symfony/event-dispatcher/Debug/WrappedListener.php(104): call_user_func(Array, Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), 'kernel.request', Object(Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher))
#12 /home/vagrant/Code/support4neo/vendor/symfony/event-dispatcher/EventDispatcher.php(212): Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), 'kernel.request', Object(Symfony\Component\EventDispatcher\EventDispatcher))
#13 /home/vagrant/Code/support4neo/vendor/symfony/event-dispatcher/EventDispatcher.php(44): Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(Array, 'kernel.request', Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#14 /home/vagrant/Code/support4neo/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php(139): Symfony\Component\EventDispatcher\EventDispatcher->dispatch('kernel.request', Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#15 /home/vagrant/Code/support4neo/vendor/symfony/http-kernel/HttpKernel.php(125): Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch('kernel.request', Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#16 /home/vagrant/Code/support4neo/vendor/symfony/http-kernel/HttpKernel.php(66): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1)
#17 /home/vagrant/Code/support4neo/vendor/symfony/http-kernel/Kernel.php(190): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#18 /home/vagrant/Code/support4neo/public/index.php(37): Symfony\Component\HttpKernel\Kernel->handle(Object(Symfony\Component\HttpFoundation\Request))
#19 {main}
What could i be doing wrong?
Thanks in advance!
User Class:
<?php
/**
* Created by PhpStorm.
* User: kevin.oosterhout
* Date: 24/04/2018
* Time: 20:58
*/
namespace App\Entity\Generic;
use GraphAware\Neo4j\Client\Client;
use GraphAware\Neo4j\OGM\Annotations as OGM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
*
* #OGM\Node(label="User")
*/
class User implements UserInterface
{
/**
* #OGM\GraphId()
*/
protected $id;
/**
* #OGM\Property(type="string")
*/
protected $firstname;
/**
* #OGM\Property(type="string")
*/
protected $lastname;
/**
* #OGM\Property(type="string")
*/
protected $email;
/**
* #OGM\Property(type="string")
*/
protected $password;
/**
* #return mixed
*/
public function getFirstname()
{
return $this->firstname;
}
/**
* #return mixed
*/
public function getLastname()
{
return $this->lastname;
}
/**
* #return mixed
*/
public function getEmail()
{
return $this->email;
}
/**
* #return mixed
*/
public function getPassword()
{
return $this->password;
}
/**
* #param mixed $firstname
*/
public function setFirstname($firstname): void
{
$this->firstname = $firstname;
}
/**
* #param mixed $lastname
*/
public function setLastname($lastname): void
{
$this->lastname = $lastname;
}
/**
* #param mixed $email
*/
public function setEmail($email): void
{
$this->email = $email;
}
/**
* #param mixed $password
*/
public function setPassword($password): void
{
$this->password = $password;
}
/**
* Returns the roles granted to the user.
*
* <code>
* public function getRoles()
* {
* return array('ROLE_USER');
* }
* </code>
*
* Alternatively, the roles might be stored on a ``roles`` property,
* and populated in any number of different ways when the user object
* is created.
*
* #return (Role|string)[] The user roles
*/
public function getRoles()
{
return array('ROLE_USER');
}
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* #return string|null The salt
*/
public function getSalt()
{
return null;
}
/**
* Returns the username used to authenticate the user.
*
* #return string The username
*/
public function getUsername()
{
return $this->email;
}
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
// TODO: Implement eraseCredentials() method.
}
}
Authenticator class:
<?php
/**
* Created by PhpStorm.
* User: kevin.oosterhout
* Date: 03/05/2018
* Time: 17:37
*/
namespace App\Security;
use App\Entity\Generic\User;
use App\Forms\LoginForm;
use GraphAware\Neo4j\OGM\EntityManager;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function getCredentials(Request $request)
{
return array(
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
if($credentials['username'] === null){
return null;
}
$user = $userProvider->loadUserByUsername($credentials['username']);
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
protected function getLoginUrl()
{
// TODO: Implement getLoginUrl() method.
}
public function supports(Request $request)
{
return false;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// TODO: Implement onAuthenticationSuccess() method.
}
}
Security.Yaml
security:
encoders:
App\Entity\Generic\User:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
UserProvider:
id: 'App\Security\UserProvider'
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
form_login:
login_path: login
check_path: login
guard:
authenticators:
- 'App\Security\LoginFormAuthenticator'
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
User Provider:
<?php
/**
* Created by PhpStorm.
* User: kevin.oosterhout
* Date: 04/05/2018
* Time: 07:38
*/
namespace App\Security;
use App\Entity\Generic\User;
use GraphAware\Neo4j\OGM\EntityManager;
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;
class UserProvider implements UserProviderInterface
{
protected $userRepository;
public function __construct(EntityManager $entityManager)
{
$this->userRepository = $entityManager->getRepository(User::class);
}
/**
* #param string $username
* #return null|User
*/
public function loadUserByUsername($username)
{
return $this->userRepository->findOneBy(['email' => $username]);
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(
sprintf('Instance of %s is not support', get_class($user))
);
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return User::class === $class;
}
}
Edit:
Currently it doesn't sign me in, and it doesn't give me an error. I have followed the steps explained by #dbrumann and #Christophe Willemsen
I think the problem might be with your security.yaml:
providers:
UserProvider:
entity:
class: 'App\Entity\Generic\User'
property: email
This provider tries to use Doctrine ORM to load users from. Since your User-entity is not a Doctrine-Entity, i.e. is missing the doctrine annotations, it fails. Even though the bundle registers entity managers, they don't seem to be used by the user provider.
You could create a custom user provider that is inspired by the EntityUserProvider.
I am not sure if the ogm entity managers adhere to doctrine's interfaces, but if they do, you might be able to configure the default doctrine user provider to use the neo4j entity manager by adjusting the service configuration, but it's a real pain because you have to overwrite the UserProvider-service and then inject a new ManagerRegistry with your entity manager in it. So, writing a custom UserProvider really seems to be the preferred way.
The answer from #dbrumman is correct, you will need a custom UserProvider.
I have a demo example on Github, check here : https://github.com/ikwattro/neo4j-ogm-symfony-security
There is also a PullRequest that shows how to add roles based on the Neo4j content :
https://github.com/ikwattro/neo4j-ogm-symfony-security/pull/1/files

Sonata admin and Custom Security handler

I wanna write a custom Security handler and this will be a simple ACL which restrict data by user id. I don't want use a standart ACL, no need to use all functional and create aditional database with permissions.
So I create my new handler and now I recieve $object as Admin class. With Admin class I can restrict access to services but can't restrict any rows in service.
The question is how I can recieve Entities and check permission on Entities like this:
public function isGranted(AdminInterface $admin, $attributes, $object = null)
{
if ($object->getUserId()==5){
return true
}
}
Overwrite the security handler in sonata config:
sonata_admin:
title: "Admin"
security:
handler: custom.sonata.security.handler.role
Create your service:
custom.sonata.security.handler.role:
class: MyApp\MyBundle\Security\Handler\CustomRoleSecurityHandler
arguments:
- #security.context
- [ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_USER]
- %security.role_hierarchy.roles%
Last step, but not less important is to create your class, retrieve your user and based by his credentials allow/deny access:
/**
* Class CustomRoleSecurityHandler
*/
class CustomRoleSecurityHandler extends RoleSecurityHandler
{
protected $securityContext;
protected $superAdminRoles;
protected $roles;
/**
* #param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
* #param array $superAdminRoles
* #param $roles
*/
public function __construct(SecurityContextInterface $securityContext, array $superAdminRoles, $roles)
{
$this->securityContext = $securityContext;
$this->superAdminRoles = $superAdminRoles;
$this->roles = $roles;
}
/**
* {#inheritDoc}
*/
public function isGranted(AdminInterface $admin, $attributes, $object = null)
{
/** #var $user User */
$user = $this->securityContext->getToken()->getUser();
if ($user->hasRole('ROLE_ADMIN')){
return true;
}
// do your stuff
}
}

Validation of fields not present in form but in Entity

I have a form for user registration, and only username field is present in the form. And in my form, I wish to allow user input the username only. Nicename would be same as username on registration.
This form is bind to a User entity, i.e., in my form type class:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Some\Bundle\Entity\User',
));
}
entity User, which has a NotBlank constraint set for both username and nicename.
namespace Some\Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Constraints;
//...
class User
{
//...
/**
* #var string $username
*
* #ORM\Column(name="user_login", type="string", length=60, unique=true)
* #Constraints\NotBlank()
*/
private $username;
/**
* #var string $nicename
*
* #ORM\Column(name="user_nicename", type="string", length=64)
* #Constraints\NotBlank()
*/
private $nicename;
//...
However, if I build a form with only username but not nicename, on validation i.e. $form->isValid() it fails to validate.
To bypass this, I come up with the following:
namespace Some\Bundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Some\Bundle\Form\Type\RegisterType;
//...
class UserController extends Controller
{
//...
public function registerAction()
{
//...
$request = $this->getRequest();
$form = $this->createForm(new RegisterType());
if ($request->getMethod() == 'POST') {
// force set nicename to username.
$registerFields = $request->request->get('register');
$registerFields['nicename'] = $registerFields['username'];
$request->request->set('register', $registerFields);
$form->bind($request);
if ($form->isValid()) {
$user = $form->getData();
//persist $user, etc...
And in form type I add this to my buildForm method:
$builder->add('nicename', 'hidden');
But I find this very inelegant, leave some burden to the controller (extract from the request object, put in data, and put it back into the request object, ouch!), and user can see the hidden field if he were to inspect the source code of generated HTML.
Is there anyway that can at least any controller using the form type does not need do things like above, while retaining the entity constraints?
I cannot change the table schema which backs up the User entity, and I would like to keep the NotBlank constraint.
EDIT: After long hassle, I decided to use Validation groups and it worked.
class User
{
//...
/**
* #var string $username
*
* #ORM\Column(name="user_login", type="string", length=60, unique=true)
* #Constraints\NotBlank(groups={"register", "edit"})
*/
private $username;
/**
* #var string $nicename
*
* #ORM\Column(name="user_nicename", type="string", length=64)
* #Constraints\NotBlank(groups={"edit"})
*/
private $nicename;
Form Type:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Some\Bundle\Entity\User',
'validation_groups' => array('register', 'Default')
));
}
That 'Default' is needed or it ignores all other constraints I added in the form type buildForm method... Mind you, its case sensitive: 'default' does not work.
Though, I find that it is not enough (and sorry I didn't put it in my original question), because when I persist, I need to do this in my controller:
$user->setNicename($user->getUsername());
As a bonus, I move this from controller to Form Type level by adding a Form Event Subscriber
In form type buildForm method:
$builder->addEventSubscriber(new RegisterPostBindListener($builder->getFormFactory()));
And the RegisterPostBindListener class
<?php
namespace Some\Bundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
class RegisterPostBindListener implements EventSubscriberInterface
{
public function __construct(FormFactoryInterface $factory)
{
}
public static function getSubscribedEvents()
{
return array(FormEvents::POST_BIND => 'setNames');
}
public function setNames(DataEvent $event)
{
$data = $event->getData();
$data->setNicename($data->getUsername());
}
}
I think you should use validation groups.
In your User entity you can tell which field can be nullable:
/**
*#ORM\Column(type="string", length=100, nullable=TRUE)
*/
protected $someVar;
This way your view controllers don't need to do anything.
Forgot to mention. You can also define a PrePersist condition that initialises your nicename variable:
// you need to first tell your User entity class it has LifeCycleCallBacks:
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class User
{
...
/**
*#ORM\PrePersist
*/
public function cloneName()
{
$this->nicename = $this->username;
}
}
In this case, you should use a Callback assertion to create a custom validation rule.

Resources