Verify that the user can do an action - php

I have two entites Person and Nursery and a ManyToMany association between them.
A user can have the role ROLE_MANAGER and be a manager for several nurseries.
For that in every action on his dashboard I need to verify if he's linked to the nursery if I don't do it he can modify the nursery slug in the url and have access to a nursery that he is not linked with.
Is there a way to check that on every action in the nursery manager dashboard without copy/paste a verification code in every action ?
As I understood Symfony Events (or Voters ?) can do that but I've never used them before ...
EDIT : Maybe it's easier to understand with a little bit of code !
So my nursery dashboard function is :
public function dashboardAction($nursery_slug)
{
//$currentUser = $this->get('security.token_storage')->getToken()->getUser();
$nurseryRepo = $this->getDoctrine()->getRepository('VSCrmBundle:Nursery');
$nursery = $nurseryRepo->findOneBy(array('slug' => $nursery_slug));
// Sometimes may help
if(!$nursery)
{
throw $this->createNotFoundException("The nursery has not been found or you are not allowed to access it.");
}
return $this->render("VSCrmBundle:Manager:dashboard.html.twig", array(
'nursery' => $nursery
));
}
To protect this dashboard I need to verify if the current user is linked to the nursery, somethink like :
$verification = $nurseryRepo->findOneBy(array('person' => $currentUser));
if(!$verification){throw accessDeniedException();}
But at the moment I'm obliged to do this test on every action in the manager dashboard ....

There are two things you need to implement to make this work smoothly.
First off, you need a NurseryVoter: http://symfony.com/doc/current/security/voters.html
Something like:
class NurseryVoter extends Voter
{
const MANAGE = 'manage';
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::MANAGE))) {
return false;
}
if (!$subject instanceof Nursery) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $nursery, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
// Check the role and do your query to verify user can manage specific nursery
Wire everything up per the link. And at this point your controller code is reduces to:
$this->denyAccessUnlessGranted('manage', $nursery);
Get all that working first. After that, use a Kernel:Controller event to move the deny access code from the controller to a listener. Follow the docs: http://symfony.com/doc/current/event_dispatcher.html
Your controller listener gets called after the controller is assigned but before the controller action is actually called. The trick here is how to determine which action actually needs the check to be done. There are a couple of approaches. Some folks like to flag the actual controller class perhaps by adding a NurseryManagerInterface. The listeners check the controller to see if it has the interface. But I don't really care for that.
I like to add this sort of stuff directly to the route. So I might have:
// routes.yml
manage_nursery:
path: /manage/{nursery}
defaults:
_controller: manage_nursery_action
_permission: CAN_MANAGE_NURSERY
Your listener would then check the permission.
Updated with a few more details on the kernel listener. Basically you inject the authorization checker and pull _permission from the request object.
class KernelListener implements EventSubscriberInterface
{
// #security.authorization_checker service
private $authorizationChecker;
public function __construct($authorizationChecker,$nuseryRepository)
{
$this->authorizationChecker = $authorizationChecker;
$this->nurseryRepository = $nuseryRepository;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => [['onController']],
];
}
public function onController(FilterControllerEvent $event)
{
$request = $event->getRequest();
$permission = $request->attributes->get('_permission');
if ($permission !== 'CAN_MANAGE_NURSERY') {
return;
}
$nursery = $this->nurseryRepository->find($request->attributes->get('nursery');
if ($this->authorizationChecker->isGranted('MANAGE',$nursery) {
return;
}
throw new AccessDeniedException('Some message');
}

Related

How to register a default login user?

**Context: ** I have 2 associated entities, being "persona" and "ingreso".
I tried to capture the logged in user and send it as a default variable in the form:
TextField::new('person','Person')
->formatValue(function ($value) {
return $value = $this->getUser();
})
->hideOnForm()
But: This arrives as a Null value in the database.
This is why I try to capture the user and save it from the entity, but I don't know a correct way to do it.
You are using ->hideOnForm, which remove the field in your form, so nothing is sent concerning person.
There is multiple way to do what you want, including similar answer such has having a hidden select with your user, however I do not consider it a good solution.
Have you considered using Event ?
In your case you could either listen using Doctrine events or with EasyAdmin Events.
Symfony events
<?php
namespace App\EventSubscriber;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
//... other imports
class EasyAdminSubscriber implements EventSubscriberInterface
{
private $tokenStorage
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage
}
public static function getSubscribedEvents()
{
return [BeforeEntityUpdatedEvent => ['beforeEntityUpdatedEvent'], ];
}
public function beforeEntityUpdatedEvent(BeforeEntityUpdatedEvent $event)
{
$entity = $event->getEntityInstance();
if ($entity instanceof YourEntityYouWantToListenTo) {
$entity->setPerson($this->tokenStorage->getToken()->getUser());
}
}

FOSUserBundle - perform function upon initial login

I have a Symfony 3 application with user management via FOSUserBundle. I want to implement a Listener class that checks for an initial user login, by checking the user's "last_login" value. While I did read the article on event subscribers, I struggle to find out when exactly the last_login value is updated.
Is there anyone with such knowledge, or does anyone know a better approach regarding a user's initial login?
I just had a look at this and it seems you can use an eventSubscriber to subscribe to the SecurityEvents::INTERACTIVE_LOGIN event to get where you need to be.
class RegistrationSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => [
['lastLogin', 150],
],
];
}
public function lastLogin(InteractiveLoginEvent $event){
$user = $this->tokenStorage->getToken()->getUser();
if($user->getLastLogin() == null){
//Do something
}
}
In my testing on the first login $user->getLastLogin() is null so you can put any logic you need here.
Symfony and FOSUserBundle have events for that. Even FOSUserBundle has an event subscriber for those events
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::SECURITY_IMPLICIT_LOGIN => 'onImplicitLogin',
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
);
}
you could subscribe for those events and make your own logic. There is no such thibg as initial login in symfony, you need to manage by your self.
Subscribe to the events and try something like this:
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event){
$user = $event->getAuthenticationToken()->getUser();
//some logic to check the user
if(!$user->getLastLogin()){
//its my first login!!! do what ever you want here
}else{
//I have already logged before so just pass
}
}
Also you need to change your subscriber priority to -10 or something like that to catch the event before FOSUserBundle or you will be passing always.
Hope it helps

Symfony Event Listener: kernel.controller

I have a controller like this:
class ArticleController extends Controller implements AuthControllerInterface
{
public function listAllArticleAction (Request $request)
{
// ... ignore
}
public function addArticleAction (Request $request)
{
// ... ignore
}
// ... another article control method
}
Users who want to add article must log in, however, user can visit listAllArticleAction without logging in.
I try to use Event Listener to solve the problem:
class AuthListener
{
private $m_router;
public function __construct(Router $router)
{
$this->m_router = $router;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if ($controller[0] instanceof AuthControllerInterface) {
$session = $event->getRequest()->getSession()->get('userId');
if (!isset($session)) {
$redirectUrl = $this->m_router->generate('page.login');
$event->setController(function () use ($redirectUrl){
return new RedirectResponse($redirectUrl);
});
}
}
}
}
If user doesn't login, user will be redirected to login page, however, this approach also takes effect on "listAllArticleAction" method.
I think that checking session at the start of the function is not a good idea, because I have another article control methods such as "deleteArticle", "getArticle" and so on, some of them need to log in first, and the others not.
How do I implement this function with event listener? Or, is there any better way to do this?
You're trying to do manually something that is already implemented in Symfony.
You have a two ways to do that. Take a look on documentation of Security Component
Use access_control section in security configuration (security.yml or via annotations)
See also How Does the Security access_control Work?
In YAML configuration it would be something like:
security:
access_control:
- { path: ^/path/to/add_article, roles: IS_AUTHENTICATED_REMEMBERED }
Check if used is logged at the begining of action
if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
throw $this->createAccessDeniedException();
}
The right way to do that is :
class ArticleController extends Controller
{
public function listAllArticleAction (Request $request)
{
// ... ignore
}
public function addArticleAction (Request $request)
{
if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
throw $this->createAccessDeniedException();
}
// ... ignore
}
// ... another article control method
}
If your user is not logged in, they will be redirected to the login page

Laravel resource policy always false

I'm trying to allow user to view their own profile in Laravel 5.4.
UserPolicy.php
public function view(User $authUser, $user)
{
return true;
}
registered policy in AuthServiceProvider.php
protected $policies = [
App\Task::class => App\Policies\TaskPolicy::class,
App\User::class => App\Policies\UserPolicy::class
];
Routes
Route::group(['middleware' => 'auth'], function() {
Route::resource('user', 'UserController');
} );
Blade template
#can ( 'view', $user )
// yes
#else
// no
#endcan
UserController.php
public function profile()
{
return $this->show(Auth::user()->id);
}
public function show($id)
{
$user = User::find($id);
return view('user.show', array( 'user'=>$user,'data'=>$this->data ) );
}
The return is always 'false'. Same for calling policy form the controller. Where do I go wrong?
Answering my own question feels weird, but I hate it when I come across questions without followups.
So after double checking It turned out that if I remove authorizeResource from the constructor:
public function __construct()
{
$this->authorizeResource(User::class);
}
and check for authorization in the controller function:
$this->authorize('view',$user);
everything works.
I must've missed this part when I added $user as a parameter in the policy function. So the user to be viewed is never passed in the authorizeResource method.
Thanks everyone for taking your time to help me.
When you add
public function __construct()
{
$this->authorizeResource(User::class);
}
to your Controller, you have to edit all your function signatures to match it to the class e.g. your show signature has to change from public function show($id)
to public function show(User $user)
After that it should work
Just a different approach here to users viewing their own profile.
First, I will create a route for that
Route::group(['middleware' => 'auth'], function() {
Route::get('profile', 'UserController#profile');
});
Then in the profile function I do
public function profile()
{
$user = Auth::user();
return view('profile', compact('user'));
}
This way, user automatically only views their own profile.
Now, if you want to allow some users to view others' profiles, then you can use Policy. Why? Because I think user should ALWAYS be able to view their own profile. But not all users should view other users profiles.
Solution:
Change the second parameter from #can( 'view', $user ) to #can( 'view', $subject ) and it will work find.
Why:
Because you're doing it the wrong way.
public function view(User $user, $subject){
return true;
}
Just look carefully the policy view method, first parameter is authenticated user or current user and second parameter is $subject, Since policies organize authorization logic around models.
Policies are classes that organize authorization logic around a
particular model or resource. For example, if your application is a
blog, you may have a Post model and a corresponding PostPolicy to
authorize user actions such as creating or updating posts.
if you want to go further deep inside it.
https://github.com/laravel/framework/blob/5.3/src/Illuminate/Auth/Access/Gate.php#L353
/**
* Resolve the callback for a policy check.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $ability
* #param array $arguments
* #return callable
*/
protected function resolvePolicyCallback($user, $ability, array $arguments)
{
return function () use ($user, $ability, $arguments) {
$instance = $this->getPolicyFor($arguments[0]);
// If we receive a non-null result from the before method, we will return it
// as the final result. This will allow developers to override the checks
// in the policy to return a result for all rules defined in the class.
if (method_exists($instance, 'before')) {
if (! is_null($result = $instance->before($user, $ability, ...$arguments))) {
return $result;
}
}
if (strpos($ability, '-') !== false) {
$ability = Str::camel($ability);
}
// If the first argument is a string, that means they are passing a class name
// to the policy. We will remove the first argument from this argument list
// because the policy already knows what type of models it can authorize.
if (isset($arguments[0]) && is_string($arguments[0])) {
array_shift($arguments);
}
if (! is_callable([$instance, $ability])) {
return false;
}
return $instance->{$ability}($user, ...$arguments);
};
}
See the last line where it is calling the method with $user and $argument( in our case Model ) is passed.
Laravel Docs for Authorization/Policies
It's possible to escape one or more policies methods using options parameter at authorizeResource with except:
public function __construct()
{
$this->authorizeResource(User::class, 'user', ['except' => ['view']]);
}
This should be on Laravel's documentation, but it isn't. I discovered this just guessing. I think this way it is a better approach thus, by removing authorizeResource method in the construct, it would be necessary to implement the authorization method for each resource action in order to protect the controller.

Redirect if authenticated logic in Laravel's built-in auth?

This question has been asked before, and I believe my code to be correct, but I am getting strange behaviour.
I need to redirect the user to different routes after login depending on some database values. I thought that in order to do this I simply had to place my logic in the handle method of app/Http/Middleware/RedirectIfAuthenticated.php. My method currently looks like so:
public function handle($request, Closure $next)
{
if ($this->auth->check()) {
if($this->auth->user()->sign_up_complete == 1){
return redirect('/');
} else {
if($this->auth->user()->step_one_complete == 0){
return redirect('/register/step-1');
} elseif($this->auth->user()->step_two_complete == 0){
return redirect('/register/step-2');
} else {
return redirect('/');
}
}
}
return $next($request);
}
This does not work, and upon login the user is redirected to /home. I have tried placing dd($this->auth->user()) inside the $this->auth->check() condition, but it never gets run. If I place it outside of that check then it's run on every request. It looks like $this->auth->check() is never run.
My question: If not here, where should this logic go?
I have removed protected $redirectTo = '/account'; from the AuthController.php controller too.
High level answer: the purpose of RedirectIfAuthenticated is to keep an already authenticated user from reaching the login or registration routes/views since they're already logged in.
Test: bookmark the login view. Then login. Close the browser or window. Open the login bookmark. You'll go straight to user's home or where ever specified in RedirectIfAuthenticated.
For purposes of the LoginController, create a redirecTo() method, which is what the redirectPath() method looks for to see if you have customized the redirect.
// example
public function redirectTo()
{
switch (auth()->user()->role) {
case 'foo':
return route('foo.home');
case 'bar':
return route('bar.home');
default:
auth()->logout();
return route('web.welcome');
}
}
You are not using the middleware correctly. This piece of code will fire everytime you send a request when you are logged in.
To change the redirect location after login you can override the redirectPath() method in your AuthController. (You can find the original method in vendor/laravel/framework/src/Illuminate/Foundation/Auth/RedirectsUsers.php)
This would look something like this:
...
public class AuthController extends Controller {
...
public function redirectPath()
{
if(Auth::user()->sign_up_complete == 1) {
return '/';
} else {
if(Auth::user()->step_one_complete == 0) {
return '/register/step-1';
} elseif(Auth::user()->step_two_complete == 0) {
return '/register/step-2';
} else {
return '/';
}
}
}
// The rest of the class implementation.
}
Note: I've replaced the $this->auth() method with the Facade alternative (Auth::). Just because I am not sure if the AuthController has an auth() method.
To understand why your routing logic is never reached, you should look in app/Http/Kernel.php where the RedirectIfAuthenticated middleware is registered:
protected $routeMiddleware = [
...
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
...
];
This means if a user navigates to a route that is not protected by the guest route middleware, the request never passes through the RedirectIfAuthenticated class and so misses your logic completely.
You can add guest middleware to your registration routes in your routes file to force the routing to pass through your code like this:
Route::get('/register/step-1', '<YOUR CONTROLLER METHOD>')->middleware('guest');
But, since you say the user is already logged in (not a guest) you should instead move your code as suggested by the other answers.
Only adding this as an answer, because it couldn't be clarified in the space allowed by a comment.
Solution is in Mark Walet's answer, but need little correction. Return should be a string:
public class AuthController extends Controller {
...
public function redirectPath()
{
if(Auth::user()->sign_up_complete == 1) {
return '/';
} else {
if(Auth::user()->step_one_complete == 0) {
return '/register/step-1';
} elseif(Auth::user()->step_two_complete == 0) {
return '/register/step-2';
} else {
return '/';
}
}
}
// The rest of the class implementation.
}
I think it's so easy as setting up a custom Middleware class to validate your requests based on the database values, I do this for excluding users without the correct role.
The role is defined in my user table and only users with the administrator role are allowed to access the system.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\MessageBag;
class RolesMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
// If a user is authenticated
if(\Auth::user() != null)
{
// If the user doesn't have the correct role
if(\Auth::user()->role != 'administrator')
{
// logout the user
\Auth::logout();
// create a new MessageBag
$messageBag = new MessageBag;
// add a message
$messageBag->add('not_allowed', 'You are not allowed to login, because you do not have the right role!');
// redirect back to the previous page with errors
return \Redirect::to('login')->withErrors($messageBag);
}
}
return $next($request);
}
}
u cant change the core files of laravel all u need to do is adding this code to
Auth\AuthController
protected $redirectPath = '/dashboard';

Categories