Sonata admin and Custom Security handler - symfony

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
}
}

Related

How to get user details on FOSUserBundle logout page?

I am using FOSUserBundle in Symfone 2.8 webapp project. Currently the user is simply redirected to the homepage when he logs out. This should be changed to a "personal" logout page that can (optionally) display personal information (e.g. reminders for upcoming tasks or simple "Goodbey USERNAME" instead of just "Goodbey")...
So I need to access/use details of the currently logged out user. But since the user has just been logged out, I cannot access the user object any more?
How to solve this?
This is the configuration I use:
// config
security:
...
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
main:
...
logout:
path: fos_user_security_logout
target: /logoutpage
// route
<route id="user_logout" path="/logoutpage" methods="GET">
<default key="_controller">AppBundle:Default:logout</default>
</route>
// Controller action
public function logoutAction() {
$loggedOutUser = HOW_TO_GET_USER(???);
$template = 'AppBundle:Default:logout.html.twig';
return $this->render($template, array('user' => $loggedOutUser));
}
The clean way would be to save the User's name/data in the session within an EventSubscriber/Listener that listens for a security.interactive_logout event.
The 2 problems arising thereby would be:
there is no logout event dispatched by the default LogoutHandler
symfony clears the session on logout per default configuration
You can change the session-clearing behavior by setting invalidate_session to false
security:
firewalls:
main:
# [..]
logout:
path: 'fos_user_security_logout'
target: '/logoutpage'
invalidate_session: false # <- do not clear the session
handlers:
- 'Namespace\Bridge\Symfony\Security\Handler\DispatchingLogoutHandler'
For the logout event you can create a logout handler like this:
class DispatchingLogoutHandler implements LogoutHandlerInterface
{
/** #var EventDispatcherInterface */
protected $eventDispatcher;
/**
* #param EventDispatcherInterface $event_dispatcher
*/
public function __construct(EventDispatcherInterface $event_dispatcher)
{
$this->eventDispatcher = $event_dispatcher;
}
/**
* {#inheritdoc}
*/
public function logout(Request $request, Response $response, TokenInterface $token)
{
$this->eventDispatcher->dispatch(
SecurityExtraEvents::INTERACTIVE_LOGOUT,
new InteractiveLogoutEvent($request, $response, $token)
);
}
}
Add some service configuration (or use autowiring):
Namespace\Bridge\Symfony\Security\Handler\DispatchingLogoutHandler:
class: 'Namespace\Bridge\Symfony\Security\Handler\DispatchingLogoutHandler'
arguments:
- '#event_dispatcher'
Events class
namespace Namespace\Bridge\Symfony;
final class SecurityExtraEvents
{
/**
* #Event("\Namespace\Bridge\Symfony\Security\Event\Logout\InteractiveLogoutEvent")
*/
const INTERACTIVE_LOGOUT = 'security.interactive_logout';
}
Event itself:
final class InteractiveLogoutEvent extends Event
{
/**
* #var Request
*/
protected $request;
/**
* #var Response
*/
protected $response;
/**
* #var TokenInterface
*/
protected $token;
/**
* #param Request $request
* #param Response $response
* #param TokenInterface $token
*/
public function __construct(Request $request, Response $response, TokenInterface $token)
{
$this->request = $request;
$this->response = $response;
$this->token = $token;
}
/**
* #return TokenInterface
*/
public function getToken()
{
return $this->token;
}
/**
* #return TokenInterface
*/
public function getRequest()
{
return $this->token;
}
/**
* #return Response
*/
public function getResponse()
{
return $this->response;
}
/**
* #return string
*/
public function getName()
{
return SecurityExtraEvents::INTERACTIVE_LOGOUT;
}
}
And the subscriber:
class UserEventSubscriber implements EventSubscriberInterface
{
/** #var LoggerInterface */
protected $logger;
/** #param LoggerInterface $logger */
public function __construct(LoggerInterface $logger)
{
// inject the session here
$this->logger = $logger;
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
SecurityExtraEvents::INTERACTIVE_LOGOUT => 'onInteractiveLogout',
);
}
/**
* {#inheritdoc}
*/
public function onInteractiveLogout(InteractiveLogoutEvent $event)
{
$user = $event->getToken()->getUser();
// save the username in the session here
$this->logger->info(
'A User has logged out.',
array(
'event' => SecurityExtraEvents::INTERACTIVE_LOGOUT,
'user' => array(
'id' => $user->getId(),
'email' => $user->getEmail(),
)
)
);
}
}
Enable the subscriber by tagging it with kernel.event_subscriber
Namespace\EventSubscriber\UserEventSubscriber:
class: 'Namespace\EventSubscriber\UserEventSubscriber'
arguments: ['#monolog.logger.user']
tags:
- { name: 'kernel.event_subscriber' }
Easy huh? A somewhat dirty solution would be creating a request listener that saves the username in the session-flashbag on every request so you can get it from there in the logout-page template.

silex symfony doctrine ORM many to many: getRoles at login returns empty List

Hello Silex (and Symfony) experts,
I need to implement a database authentification User/Role model via Doctrine /ORM.
This is my silex composer setup:
"require": {
"silex/web-profiler": "^2.0",
"monolog/monolog": "1.13.*",
"symfony/twig-bridge": "^3.2",
"symfony/monolog-bridge": "^3.2",
"symfony/console": "^3.2",
"symfony/yaml": "^3.2",
"symfony/security-bundle": "^3.2",
"doctrine/orm": "^2.5",
"dflydev/doctrine-orm-service-provider": "^2.0",
"symfony/form": "^3.2",
"symfony/validator": "^3.2",
"symfony/config": "^3.2",
"symfony/doctrine-bridge": "^3.2",
"doctrine/migrations": "^1.5"
},
Users can register. Registered users can login and logout. Non registered visitors have anonymous role.
The symfony profiler is working, so I can see the security status (authentification/authoriszation). I also track the apache logfile for php errors.
I started from here https://github.com/fredjuvaux/silex-orm-user-provider (User from db, roles as array) and tried to expand it to get user roles from database via doctrine many-to-many relation.
There are:
class MyUserController (different user actions like user,edit, register,... )
class MyUserManager implements UserProviderInterface (loadUserByUsername, ...)
class MyUserServiceProvider implements ServiceProviderInterface, ControllerProviderInterface, BootableProviderInterface (controller routing and template setting)
The ORM entities are:
User:
/**
* MyUser
*
* #Entity
* #Table(name="myuser")
*/
class MyUser implements UserInterface, \Serializable
{
....
/**
* #ManyToMany(targetEntity="MyRole", inversedBy="users")
*
*/
private $roles;
...
* Constructor.
*
* #param string $email
*/
public function __construct($email)
{
$this->email = $email;
$this->created = time();
$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
$this->roles = new ArrayCollection();
}
...
/**
*
* #return ArrayCollection list of the user's roles.
*/
public function getRoles()
{
$result = $this->roles->toArray(); // throws error for login:
// $result = $this->roles; // test // thhrows error : null object
dump($this->roles);
// $result = array("ROLE_USER", "ROLE_OTHER"); // static setting and
works for login
return $result;
}
...
}
Roles (implements Roleinterface)
/**
* MyRole
*
* #Entity
* #Table(name="myrole")
*/
class MyRole implements RoleInterface
{
/**
* #var string
* #Column(name="role", type="string", length=20, unique=true)
*/
private $role;
/**
* #ManyToMany(targetEntity="MyUser", mappedBy="roles")
*/
private $users;
...
/*
* methods for RoleInterface
* #return string|null A string representation of the role, or null
*/
public function getRole()
{
$result = $this->role;
return $result;
}
}
When a user registers, he gets for that session the ROLE_USER role,
authentification and authorisation are ok and a user is created in the
database.
Then I can assign new roles ("role_test1", "role_test2") in the controller for the new user, the many-to-many table myuser_myrole is filled (myuser_id myrole_id).
When I change the roles, they are correctly updated by the entity manager.
When I access the user Entity from the userController to work on it, I can access the assigned roles:
// MyUserController.php
$user = $em->getRepository('MyEntities\MyUser')->find($id);
$roles= $user->getRoles()
$role_length = count($roles);
$role_list = array();
for ($i=0; $i <$role_length ; $i++)
{
array_push($role_list,$roles[$i]->getRole()); // MyRole::getRole() prints out something to screen.
}
printf("<br> role-list:"); dump($role_list);
Calling this controller prints out the assigned roles via MyRole::getRole(), so ORM access works here.
Now comes the strange:
I want to login the new user with the login form.
When I use
// MyUser::getRoles()
return $this->roles;
It throws:
Argument 4 passed to Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken::__construct() must be of the type array, object given,
Ok, makes maybe sense because the $roles is an Doctrine ArrayCollection.
When I use
// MyUser::getRoles()
return $this->roles->toArray();
I can login with user password,but am not authenticated (yellow status). Dumping out the roles, I receive an empty array ArrayCollection.
roles:
ArrayCollection {#388 ▼
-elements: []
}
The UsernamePasswordToken has an empty role-array.
When I use
// MyUser::getRoles()
return array("ROLE_HELLO1", "ROLE_HELLO2"); // static role array with strings
I can login and am authenticated with these roles:
Roles
array:2 [▼
0 => "ROLE_HELLO1"
1 => "ROLE_HELLO2"
]
There are old docs about this (Managing Roles in the Database) for symfony 2 http://symfony.com/doc/2.0/cookbook/security/entity_provider.html, but it doesnt work in symfony3.
Here they use
//class User
public function getRoles()
{
return $this->groups->toArray();
}
//class Group extends Role (not RoleInterface, old?)
public function getRole()
{
return $this->role;
}
The actual symfony docs for user management do not show how to use roles stored in database.
In summary:
Login and user/role do not work as expected:
MyUser::getRoles()
does not receive the Roles from database via doctrine ORM.
has to return a string array of roles for login.
delivers the correct role association in another controller.
Questions:
(1) Is this a Silex specific issue?
(2) How to use it correctly or where is a good link/doc for a workaround?
(3) Does the method LoadUserByUsername() interfere with all this?
(4) Do I need a class MyUserRepository extends EntityRepository {} to do the query and get the Role List?
(5) Do I need to use the Role Hierarchy service?
(6) Are there special naming conventions(tablename or class name) for "user" and "role"?
I found many posts asking the same/similar but they do not help here.
Thank you for help, I am really stuck on that!
dirk
Try this:
public function getRoles()
{
return $this->roles->map(function (MyRole $role) {
return $role->getRole();
})->toArray();
}
You should also check if the relationship is correctly saved in database.
If there is ManyToMany relationship between MyUser and MyRole, you have to ensure that relationship is saved in both entities.
//class MyUser
public function addRole(MyRole $role)
{
$this-roles->add($role);
$role->users->add($user);
}
I had a break on this, but now it seems to work. Thank you miikes for the addRole() suggestion!
finally I have: MyUser.php:
//Myuser.php
/**
* MyUser
*
* #Entity
* #Table(name="myuser")
*/
class MyUser implements UserInterface, \Serializable //, ObjectManagerAware
{
...
/**
* #ManyToMany(targetEntity="MyRole", inversedBy="users")
*/
private $roles;
public function __construct($email)
{
(...)
$this->roles = new ArrayCollection();
/**
*
* #return ArrayCollection list of the user's roles.
*/
public function getRoles()
{
$result = $this->roles->toArray();
return $result;
}
public function assignToRole($role)
{
$this->roles[] = $role;
}
public function setRole($myrole)
{
$this->roles= $myrole;
}
public function hasRole($role)
{
return in_array(strtoupper($role), $this->getRoles(), true);
}
public function addRole(MyRole $role)
{
$this->roles->add($role);
//$role->users->addRole($this); // could not access roles->user->...
// because private variable in MyRole but it works
}
/**
* Remove the given role from the user.
*
* #param string $role
*/
public function removeRole($role)
{
dump($role);
$this->roles->removeElement($role);
}
(...) // other setters getters
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
$this->salt,
));
}
/**
* #see \Serializable::unserialize()
*/
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
$this->salt,
) = unserialize($serialized);
}
}
and MyRole.php:
// MyRole.php
/**
* MyRole
*
* #Entity
* #Table(name="myrole")
*/
class MyRole implements RoleInterface
{
(...)
/**
* #ManyToMany(targetEntity="MyUser", mappedBy="roles")
*/
private $users;
/**
* #var string
* #Column(name="role", type="string", length=20, unique=true)
*/
private $role;
/*
* methods for RoleInterface
* #return string|null A string representation of the role, or null
*/
public function getRole()
{
$result = $this->role;
return ($result);
}
public function setRole($role)
{
$this->role= $role;
return $this;
}
(...)
/**
* Constructor
*/
public function __construct()
{
$this->users = new ArrayCollection();
}
/**
* Add user
* #param \MyEntities\MyUser $user
* #return MyRole
*/
public function addUser($user)
{
$this->users[] = $user;
return $this;
}
public function setUser($user)
{
$this->users[] = $user;
return $this;
}
/**
* Remove user
*
* #param \MyEntities\MyUser $user
*/
public function removeUser($user)
{
$this->users->removeElement($user);
}
/**
* Get users
*
* #return ArrayCollection $users
*/
public function getUsers()
{
return $this->users;
}
/**
* __toString()
*
* #return string
*/
public function __toString()
{
return $this->bezeichnung;
}
}
With the help of the doctrine orm commands
vendor/bin/doctrine orm:validate-schema
vendor/bin/doctrine orm:schema-tool:update --dump-sql
the correct manyToMany table myuser_myrole was generated and the role setting works at login of a user.
I think, the most important was the correct use of the function addRole() (with this->roles->add($role), and not something like this->roles->addRole($role) ) to let doctrine do the magic stuff in the background.
Thanks for any help and comments!
dirk

Disabled user by default on FOSUserbundle

I'm using FOSUserBundle and I want when each user registers to be disabled by default. The administrator will contact every user by phone and he will make user active if it's appropriate. I have read about Overriding Default FOSUserBundle Controllers but I can't figure out how to make it working. I have created RegistrationController.php in src/AppBundle/Controller/RegistrationController.php with this method inside:
<?php
/*
* This file is part of the FOSUserBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\UserBundle\Controller;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use FOS\UserBundle\Model\UserInterface;
/**
* Controller managing the registration
*
* #author Thibault Duplessis <thibault.duplessis#gmail.com>
* #author Christophe Coevoet <stof#notk.org>
*/
class RegistrationController extends ContainerAware
{
/**
* Receive the confirmation token from user email provider, login the user
*/
public function confirmAction($token)
{
$user = $this->container->get('fos_user.user_manager')->findUserByConfirmationToken($token);
if (null === $user) {
throw new NotFoundHttpException(sprintf('The user with confirmation token "%s" does not exist', $token));
}
$user->setConfirmationToken(null);
$user->setEnabled(false);
$user->setLastLogin(new \DateTime());
$this->container->get('fos_user.user_manager')->updateUser($user);
$response = new RedirectResponse($this->container->get('router')->generate('fos_user_registration_confirmed'));
$this->authenticateUser($user, $response);
return $response;
}
}
, but nothing works, maybe I need someone to show me the way to do it, nothing more.
For those still struggling with this question, set an observer listening the event: fos_user.registration.initialize like this (adapt you code path) :
app.listener.disable_registered_user:
class: AppBundle\Observer\DisableRegisteredUserListener
arguments:
- "#templating"
tags:
# split to multiple line for readability
# can be made into a single line like - { name: ..., event: ... , method: ... }
-
name: "kernel.event_listener"
event: "fos_user.registration.initialize"
method: "disableUser"
Then this is the content of your event listener class :
namespace AppBundle\Observer;
use FOS\UserBundle\Event\GetResponseUserEvent;
/**
* Class DisableRegisteredUserListener
* #package AppBundle\Observer
*/
class DisableRegisteredUserListener
{
/**
* #param \FOS\UserBundle\Event\GetResponseUserEvent $event
*/
public function disableUser(GetResponseUserEvent $event)
{
$user = $event->getUser();
/** #var \AppBundle\Entity\User $user */
$user->setEnabled(false);
}
}
You could just listen to the FOSUserEvents::REGISTRATION_CONFIRM and disable the registered user again before it gets persisted to the database.
As the FOSUserBundle automatically forwards the new user to the confirmedAction that requires a user to be logged in, you would need to provide your own response to override this.
Your listener...
class DisableRegisteredUserListener
{
/**
* #var EngineInterface
*/
private $templating;
/**
* #var EngineInterface $templating
*/
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
/**
* #var GetResponseUserEvent $event
* #return null
*/
public function disableUser(GetResponseUserEvent $event)
{
$user = $event->getUser();
$user->setEnabled(false);
$response = $this->templating->renderResponse(
'AppBundle:Registration:registration_complete.html.twig',
array(
'user' => $user,
)
);
}
}
Your services file (YAML)...
services:
app.listener.disable_registered_user:
class: AppBundle\EventListener\DisableRegisteredUserListener
arguments:
- "#templating"
tags:
# split to multiple line for readability
# can be made into a single line like - { name: ..., event: ... , method: ... }
-
name: "kernel.event_listener"
event: "fos_user.registration.confirm"
method: "disableUser"
Your AppBundle:Registration:registration_complete.html.twig could then be used to tell the new users that their account had been created but disabled and they would then be contacted by a member of your team to complete the process.

Sonata Admin: How to skip or remove dashboard based on a role?

I have a couple of roles that only manage one type of entity. Landing the users on the dashboard and having them click into the entity section seems redundant.
Is there a way that the dashboard can be removed and an alternate default landing page can be set based on the role?
You can create a new service and override isGranted method from Sonata:
.yml
custom.sonata.security.handler.role:
class: AdminBundle\Security\Handler\CustomRoleSecurityHandler
arguments:
- #security.token_storage
- #security.authorization_checker
- [ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_USER]
- %security.role_hierarchy.roles%
.php
class CustomRoleSecurityHandler extends RoleSecurityHandler
{
protected $securityContext;
protected $superAdminRoles;
protected $roles;
/**
* #var TokenStorageInterface
*/
private $tokenStorageInterface;
/**
* #param TokenStorageInterface $tokenStorageInterface
* #param AuthorizationCheckerInterface $securityContext
* #param array $superAdminRoles
* #param $roles
*/
public function __construct(TokenStorageInterface $tokenStorageInterface, AuthorizationCheckerInterface $securityContext, array $superAdminRoles, $roles)
{
$this->securityContext = $securityContext;
$this->superAdminRoles = $superAdminRoles;
$this->roles = $roles;
$this->tokenStorageInterface = $tokenStorageInterface;
}
/**
* {#inheritDoc}
*/
public function isGranted(AdminInterface $admin, $attributes, $object = null)
{
if (!is_array($attributes)) {
$attributes = array($attributes);
}
foreach ($attributes as $pos => $attribute) {
$attributes[$pos] = sprintf($this->getBaseRole($admin), $attribute);
}
$user = $this->tokenStorageInterface->getToken()->getUser();
// ... check user role and do your stuff
}
}

FOSUser Bundle, Symfony Landing Page

I am using the FOSUser Bundle for Symfony... My question is;
I have two different group of users.... For example; Teachers and Students, which it is set when they register to the system. (using the user table of FOSUser Bundle)
After a successful login, I want to user to go to the correct landing page.. So
If the user is a teacher, I want the user to go to /teacher and for student to /student.
What is the best way to approach this?
Thanks
You need an event listener to listen for an login event. Then you can route the client to different pages based on their roles.
services.yml:
services:
login_listener:
class: Acme\UserBundle\Listener\LoginListener
arguments: [#security.context, #doctrine]
tags:
- { name: kernel.event_listener, event: security.interactive_login }
LoginListener:
<?php
namespace Acme\UserBundle\Listener;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Security\Core\SecurityContext;
use Doctrine\Bundle\DoctrineBundle\Registry as Doctrine; // for Symfony 2.1.x
// use Symfony\Bundle\DoctrineBundle\Registry as Doctrine; // for Symfony 2.0.x
/**
* Custom login listener.
*/
class LoginListener
{
/** #var \Symfony\Component\Security\Core\SecurityContext */
private $securityContext;
/** #var \Doctrine\ORM\EntityManager */
private $em;
/**
* Constructor
*
* #param SecurityContext $securityContext
* #param Doctrine $doctrine
*/
public function __construct(SecurityContext $securityContext, Doctrine $doctrine)
{
$this->securityContext = $securityContext;
$this->em = $doctrine->getEntityManager();
}
/**
* Do the magic.
*
* #param Event $event
*/
public function onSecurityInteractiveLogin(Event $event)
{
if ($this->securityContext->isGranted('ROLE_1')) {
// redirect 1
}
if ($this->securityContext->isGranted('ROLE_2')) {
// redirect 2
}
// do some other magic here
$user = $this->securityContext->getToken()->getUser();
// ...
}
}
From: http://www.metod.si/login-event-listener-in-symfony2/

Resources